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:
@@ -400,8 +400,9 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) {
|
||||
onChange={setAttachedYachtIds}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Adding a yacht transfers its ownership to this company (logged in the yacht's
|
||||
audit trail). Skip if you only want to associate without changing ownership.
|
||||
Adding a yacht transfers its ownership to this company (logged in the
|
||||
yacht's audit trail). Skip if you only want to associate without changing
|
||||
ownership.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -662,13 +663,19 @@ function EntityMultiPicker({
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn('w-full justify-between font-normal', !selectedIds.length && 'text-muted-foreground')}
|
||||
className={cn(
|
||||
'w-full justify-between font-normal',
|
||||
!selectedIds.length && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{placeholder}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popper-anchor-width)] min-w-[280px] p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popper-anchor-width)] min-w-[280px] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput placeholder="Search…" onValueChange={setSearch} />
|
||||
<CommandList>
|
||||
@@ -683,10 +690,7 @@ function EntityMultiPicker({
|
||||
onSelect={() => toggle(opt.value)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
isSelected ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
className={cn('mr-2 h-4 w-4', isSelected ? 'opacity-100' : 'opacity-0')}
|
||||
/>
|
||||
{opt.label}
|
||||
</CommandItem>
|
||||
|
||||
Reference in New Issue
Block a user