fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc
UAT findings landed across the last few Playwright + React Grab passes; single grouped commit so the index doesn't fragment into 30 one-liners. User & auth: - `user-settings`: name now updates the avatar + topbar menu after save (was reading stale session). - `me/password-reset`: 3 bugs (token validation, error response shape, redirect chain). - Admin user permission-overrides route honours the same envelope as the rest of the admin surface. Dashboard: - Removed obsolete `revenue-breakdown-chart` + `dashboard-widgets-card` (replaced by the customisable widget grid). - Strip `revenue_breakdown` from analytics route + use-analytics + service + integration test so nothing renders an empty card. - Activity log timeline overshoot fix (`interest-timeline` + `entity-activity-feed`). - Tightened tiles: active-deals, berth-heat-widget, pipeline-value, kpi-tile. - `dev-mode-banner`: derive dismissed state synchronously instead of via an effect (set-state-in-effect lint rule). Forms & lists (assorted polish): - client / company / yacht / interest / reminder forms — validation + empty-state copy + tab transitions. - companies/yachts list tweaks; berth recommender panel; qualification checklist; supplemental info request button. Infra & misc: - Queue workers (ai / email / notifications) — log shape + per-job timeout consistency. - Auth / brochures / users schema small adjustments; seeds reflect permissions matrix changes. - Scan shell + scanner manifest + AI admin page small fixes. - `next.config.transpilePackages` adds `echarts`/`zrender`/`echarts-for-react` (recommended config from echarts-for-react inside Next). Docs: - `docs/superpowers/audits/alpha-uat-master.md` — single rolling cross-cutting UAT findings doc (per CLAUDE.md convention). - `docs/BACKLOG.md`: dashboard stats cards (§I) + activity-log normalization (§J). - 2026-05-18 audit log updated with this batch. - `CLAUDE.md` — small manual UAT scaffold notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,62 @@ function toLocalDatetimeLocal(d: Date): string {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Date relative to "now" for the quick-pick chips. Day-based
|
||||
* presets land on the user's preferred time-of-day (`digestTimeOfDay`
|
||||
* from user_profiles.preferences) — same source the default dueAt uses.
|
||||
* Hour-based presets use the current time + N hours.
|
||||
*/
|
||||
function buildPresetDate(
|
||||
preset:
|
||||
| { kind: 'hours'; hours: number }
|
||||
| { kind: 'tomorrow' }
|
||||
| { kind: 'in_days'; days: number }
|
||||
| { kind: 'next_monday' },
|
||||
timeOfDay: string | null,
|
||||
): Date {
|
||||
const now = new Date();
|
||||
let h = 9;
|
||||
let m = 0;
|
||||
if (timeOfDay && /^\d{2}:\d{2}$/.test(timeOfDay)) {
|
||||
const [hh = '09', mm = '00'] = timeOfDay.split(':');
|
||||
const parsedH = Number.parseInt(hh, 10);
|
||||
const parsedM = Number.parseInt(mm, 10);
|
||||
if (Number.isFinite(parsedH) && parsedH >= 0 && parsedH <= 23) h = parsedH;
|
||||
if (Number.isFinite(parsedM) && parsedM >= 0 && parsedM <= 59) m = parsedM;
|
||||
}
|
||||
if (preset.kind === 'hours') {
|
||||
return new Date(now.getTime() + preset.hours * 60 * 60 * 1000);
|
||||
}
|
||||
if (preset.kind === 'tomorrow') {
|
||||
const t = new Date(now);
|
||||
t.setDate(t.getDate() + 1);
|
||||
t.setHours(h, m, 0, 0);
|
||||
return t;
|
||||
}
|
||||
if (preset.kind === 'in_days') {
|
||||
const t = new Date(now);
|
||||
t.setDate(t.getDate() + preset.days);
|
||||
t.setHours(h, m, 0, 0);
|
||||
return t;
|
||||
}
|
||||
// next_monday
|
||||
const t = new Date(now);
|
||||
const daysUntilMonday = (8 - t.getDay()) % 7 || 7;
|
||||
t.setDate(t.getDate() + daysUntilMonday);
|
||||
t.setHours(h, m, 0, 0);
|
||||
return t;
|
||||
}
|
||||
|
||||
const DUE_PRESETS = [
|
||||
{ label: 'In 1 hour', spec: { kind: 'hours', hours: 1 } as const },
|
||||
{ label: 'In 4 hours', spec: { kind: 'hours', hours: 4 } as const },
|
||||
{ label: 'Tomorrow', spec: { kind: 'tomorrow' } as const },
|
||||
{ label: 'In 3 days', spec: { kind: 'in_days', days: 3 } as const },
|
||||
{ label: 'Next week', spec: { kind: 'next_monday' } as const },
|
||||
{ label: 'In 2 weeks', spec: { kind: 'in_days', days: 14 } as const },
|
||||
] as const;
|
||||
|
||||
interface UserOption {
|
||||
id: string;
|
||||
displayName: string;
|
||||
@@ -230,6 +286,24 @@ function ReminderFormBody({
|
||||
<div className="grid grid-cols-[2fr_1fr] gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reminder-due">Due Date & Time</Label>
|
||||
{/* Quick-pick chips. Same idiom as the snooze dialog so reps
|
||||
don't have to think about month-day-time for the 80%
|
||||
common cases. Each chip writes the formatted datetime
|
||||
into the input, leaving the rep free to tweak. */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{DUE_PRESETS.map((p) => (
|
||||
<button
|
||||
key={p.label}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setDueAt(toLocalDatetimeLocal(buildPresetDate(p.spec, userTodPref)))
|
||||
}
|
||||
className="rounded-full border bg-background px-2.5 py-0.5 text-xs font-medium text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Input
|
||||
id="reminder-due"
|
||||
type="datetime-local"
|
||||
|
||||
Reference in New Issue
Block a user