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:
@@ -145,8 +145,7 @@ export function EoiGenerateDialog({
|
||||
value: ctx.client.fullName,
|
||||
present: !!ctx.client.fullName,
|
||||
edit: {
|
||||
onSave: async (next: string | null) =>
|
||||
await patchClient({ fullName: next ?? '' }),
|
||||
onSave: async (next: string | null) => await patchClient({ fullName: next ?? '' }),
|
||||
placeholder: 'Full legal name',
|
||||
},
|
||||
},
|
||||
@@ -194,8 +193,7 @@ export function EoiGenerateDialog({
|
||||
value: ctx.yacht?.name ?? null,
|
||||
edit: ctx.yacht
|
||||
? {
|
||||
onSave: async (next: string | null) =>
|
||||
await patchYacht({ name: next ?? '' }),
|
||||
onSave: async (next: string | null) => await patchYacht({ name: next ?? '' }),
|
||||
placeholder: 'Yacht name',
|
||||
}
|
||||
: undefined,
|
||||
@@ -323,12 +321,7 @@ export function EoiGenerateDialog({
|
||||
</p>
|
||||
<dl className="space-y-1.5">
|
||||
{optional.map((row) => (
|
||||
<PreviewRow
|
||||
key={row.key}
|
||||
label={row.label}
|
||||
value={row.value}
|
||||
edit={row.edit}
|
||||
/>
|
||||
<PreviewRow key={row.key} label={row.label} value={row.value} edit={row.edit} />
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
@@ -477,9 +470,7 @@ function PreviewRow({
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1">
|
||||
{value ?? (missing ? 'Missing — required' : 'Not set')}
|
||||
</span>
|
||||
<span className="flex-1">{value ?? (missing ? 'Missing — required' : 'Not set')}</span>
|
||||
{edit ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user