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:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user