feat(dashboard): local-time greeting + timezone-drift banner

Greeting
- The "Good morning / afternoon / evening, Matt" line now derives from the
  browser's local time, computed inside a useEffect so the rendered HTML
  can't lock to the server's clock during hydration. Until the effect
  fires, the header reads "Welcome" — a neutral phrase that's correct at
  every hour and never produces a hydration warning. The phrase re-evaluates
  hourly so a rep leaving the dashboard open across a boundary (5am, noon,
  6pm) doesn't keep stale text on screen.

Timezone-drift banner
- New <TimezoneDriftBanner> on the dashboard surfaces when the browser's
  resolved timezone (Intl.DateTimeFormat().resolvedOptions().timeZone, which
  follows the OS — and the OS usually follows physical location) doesn't
  match the user's stored CRM preference. The rep gets a one-tap "Update to
  Tokyo" button and a dismiss × that's sticky per browser via localStorage.
- Why a banner rather than auto-update: the stored timezone drives reminder
  firing time, daily-digest delivery, and due-date rendering. Silently
  pinning it to a transient travel location would shift their reminder
  schedule underfoot. The banner gives them control.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 15:48:51 +02:00
parent 04a594963f
commit 0ab7055cf1
9 changed files with 395 additions and 217 deletions

View File

@@ -95,7 +95,12 @@ export function AdminSectionsBrowser({ portSlug, groups }: AdminSectionsBrowserP
</p>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{filteredMatches.map((s) => (
<SectionCard key={`${s.href}-${s.groupTitle}`} portSlug={portSlug} section={s} groupTitle={s.groupTitle} />
<SectionCard
key={`${s.href}-${s.groupTitle}`}
portSlug={portSlug}
section={s}
groupTitle={s.groupTitle}
/>
))}
</div>
</div>
@@ -138,7 +143,11 @@ function SectionCard({
href={`/${portSlug}/admin/${section.href}` as any}
className="block group"
>
<Card className={cn('h-full transition-colors group-hover:border-primary/50 group-hover:bg-muted/30')}>
<Card
className={cn(
'h-full transition-colors group-hover:border-primary/50 group-hover:bg-muted/30',
)}
>
<CardHeader className="flex flex-row items-start gap-3 space-y-0 pb-2">
<Icon className="h-5 w-5 mt-0.5 text-muted-foreground group-hover:text-primary" />
<div className="flex-1">