feat: logbook filter by crew/vessel and save-on-leave dialog

Extend dashboard search with ship name and crew name parts from local data.
When leaving a dirty travel day, offer save, discard, or stay instead of only leave/cancel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 22:30:41 +02:00
parent c5a9b39057
commit 3d2918e0fe
12 changed files with 423 additions and 79 deletions
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import { logbookMatchesFilter, nameMatchesQuery } from './logbookFilter.js'
describe('nameMatchesQuery', () => {
it('matches full name', () => {
expect(nameMatchesQuery('Anna Müller', 'müller')).toBe(true)
})
it('matches first name part only', () => {
expect(nameMatchesQuery('Anna Müller', 'anna')).toBe(true)
})
it('matches last name part only', () => {
expect(nameMatchesQuery('Anna Müller', 'mül')).toBe(true)
})
it('returns false for unrelated query', () => {
expect(nameMatchesQuery('Anna Müller', 'peter')).toBe(false)
})
})
describe('logbookMatchesFilter', () => {
const lb = { title: 'Sommer 2024', updatedAt: '2024-06-15T12:00:00.000Z' }
it('matches logbook title', () => {
expect(logbookMatchesFilter(lb, 'sommer', 'de')).toBe(true)
})
it('matches vessel name from search fields', () => {
expect(
logbookMatchesFilter(lb, 'wind', 'de', { vesselName: 'Windrose', crewNames: [] })
).toBe(true)
})
it('matches crew first name from search fields', () => {
expect(
logbookMatchesFilter(lb, 'klaus', 'de', {
vesselName: '',
crewNames: ['Klaus Hansen']
})
).toBe(true)
})
it('matches crew last name from search fields', () => {
expect(
logbookMatchesFilter(lb, 'hansen', 'de', {
vesselName: '',
crewNames: ['Klaus Hansen']
})
).toBe(true)
})
})
+45
View File
@@ -0,0 +1,45 @@
export interface LogbookSearchFields {
vesselName: string
crewNames: string[]
}
/** Match full name or any whitespace-separated part (e.g. first or last name). */
export function nameMatchesQuery(name: string, query: string): boolean {
const q = query.trim().toLowerCase()
if (!q) return true
const normalized = name.trim().toLowerCase()
if (!normalized) return false
if (normalized.includes(q)) return true
return normalized.split(/\s+/).some((part) => part.includes(q))
}
export function logbookMatchesFilter(
lb: { title: string; updatedAt: string },
query: string,
locale: string,
fields?: LogbookSearchFields
): boolean {
const q = query.trim().toLowerCase()
if (!q) return true
if (lb.title.toLowerCase().includes(q)) return true
const updated = new Date(lb.updatedAt)
const year = updated.getFullYear().toString()
if (year.includes(q)) return true
const dateLabel = updated.toLocaleDateString(locale, {
year: 'numeric',
month: 'short',
day: 'numeric'
}).toLowerCase()
if (dateLabel.includes(q)) return true
if (fields?.vesselName && nameMatchesQuery(fields.vesselName, q)) return true
if (fields?.crewNames?.some((name) => nameMatchesQuery(name, q))) return true
return false
}