feat(attendance): richer /attendance (here-now, search, day filter, grouping)

Per request. /attendance is now a client view: a 'Here now' section
with host badges + inline self check-in / check-in-as-host / check-out
with a note; and a history with name search, a day picker, grouped-by-
day headers, per-visit duration, status, notes, and a live
visits/members summary for the current filter. New pure tested helpers
dayKey (tz-stable UTC bucket) + visitDurationMinutes (+5 tests, suite
459).
This commit is contained in:
Moheeb Zara
2026-05-16 22:42:07 -07:00
parent 1e1b93e8c4
commit d5a71c9448
4 changed files with 288 additions and 84 deletions
+26
View File
@@ -3,6 +3,8 @@ import {
presenceStatus,
hostEligibility,
summarizePresence,
dayKey,
visitDurationMinutes,
PRESENCE_MAX_OPEN_HOURS,
} from '@/lib/presence-logic'
@@ -63,3 +65,27 @@ describe('summarizePresence', () => {
expect(summarizePresence([], NOW)).toEqual({ present: 0, hosts: 0 })
})
})
describe('dayKey', () => {
it('returns the UTC date portion, stable regardless of runtime tz', () => {
expect(dayKey('2026-05-17T23:30:00Z')).toBe('2026-05-17')
expect(dayKey('2026-01-02T00:00:00+00:00')).toBe('2026-01-02')
})
it('returns empty for unparseable input', () => {
expect(dayKey('not-a-date')).toBe('')
expect(dayKey('')).toBe('')
})
})
describe('visitDurationMinutes', () => {
it('uses checkout when present', () => {
expect(visitDurationMinutes('2026-05-17T10:00:00Z', '2026-05-17T11:30:00Z')).toBe(90)
})
it('uses now when still open', () => {
expect(visitDurationMinutes(hoursAgo(2), null, NOW)).toBe(120)
})
it('never negative; unparseable -> 0', () => {
expect(visitDurationMinutes('2026-05-17T12:00:00Z', '2026-05-17T11:00:00Z')).toBe(0)
expect(visitDurationMinutes('nope', null, NOW)).toBe(0)
})
})