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
+23
View File
@@ -47,6 +47,29 @@ export function hostEligibility(input: {
return { ok: true }
}
// Stable day bucket for grouping/filtering attendance. Uses the UTC date
// portion of the ISO timestamp so it is deterministic (not affected by the
// runtime's timezone) and testable. Returns '' for an unparseable value.
export function dayKey(iso: string): string {
return /^\d{4}-\d{2}-\d{2}/.test(iso) ? iso.slice(0, 10) : ''
}
// Visit length in whole minutes (end = checkout, or `now` if still open).
// Never negative; unparseable input -> 0.
export function visitDurationMinutes(
checkedInAt: string,
checkedOutAt: string | null | undefined,
now?: Date | string,
): number {
const start = new Date(checkedInAt).getTime()
if (Number.isNaN(start)) return 0
const endMs = checkedOutAt
? new Date(checkedOutAt).getTime()
: now == null ? Date.now() : new Date(now).getTime()
if (Number.isNaN(endMs)) return 0
return Math.max(0, Math.floor((endMs - start) / 60000))
}
// Count how many visits are currently present, and how many of those are
// hosting. Stale/checked-out rows are excluded.
export function summarizePresence(