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:
2026-05-20 15:56:11 +02:00
parent 8c669e2918
commit 449b9497ab
59 changed files with 1831 additions and 631 deletions

View File

@@ -1,77 +0,0 @@
'use client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets';
/**
* Per-user toggle list for dashboard widgets. The dashboard reads the
* same `useDashboardWidgets` hook, so flipping a switch here causes the
* dashboard to reflow on the next visit (or instantly if the user has
* both pages open in different tabs — TanStack Query's optimistic
* update + invalidate handles the cache sync).
*
* Mounted from UserSettings under the id `dashboard` so the dashboard
* "Customize" button can deep-link via `/settings#dashboard`.
*/
export function DashboardWidgetsCard() {
const { allWidgets, visibility, setVisible, setAll, isSaving } = useDashboardWidgets();
const visibleCount = Object.values(visibility).filter(Boolean).length;
const allVisible = visibleCount === allWidgets.length;
const allHidden = visibleCount === 0;
return (
<Card id="dashboard">
<CardHeader>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<CardTitle>Dashboard widgets</CardTitle>
<CardDescription>
Pick which cards show up on your dashboard. Hidden cards leave no empty space the
layout reflows to fill the available width.
</CardDescription>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setAll(true)}
disabled={allVisible || isSaving}
>
Show all
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setAll(false)}
disabled={allHidden || isSaving}
>
Hide all
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-1">
{allWidgets.map((w) => (
<div
key={w.id}
className="flex items-start justify-between gap-4 rounded-md px-3 py-2 hover:bg-accent/40"
>
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">{w.label}</div>
<p className="text-xs text-muted-foreground">{w.description}</p>
</div>
<Switch
aria-label={`Show ${w.label}`}
checked={visibility[w.id] ?? false}
disabled={isSaving}
onCheckedChange={(checked) => setVisible(w.id, checked)}
/>
</div>
))}
</CardContent>
</Card>
);
}

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useMemo, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { Save, KeyRound, Globe, Upload } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
@@ -16,7 +17,6 @@ import { TimezoneCombobox } from '@/components/shared/timezone-combobox';
import { ImageCropperDialog } from '@/components/shared/image-cropper-dialog';
import { NotificationPreferencesForm } from '@/components/notifications/notification-preferences-form';
import { ReminderDigestForm } from '@/components/notifications/reminder-digest-form';
import { DashboardWidgetsCard } from '@/components/settings/dashboard-widgets-card';
import { apiFetch } from '@/lib/api/client';
import { primaryTimezoneFor } from '@/lib/i18n/timezones';
import type { CountryCode } from '@/lib/i18n/countries';
@@ -34,6 +34,7 @@ interface MeResponse {
}
export function UserSettings() {
const router = useRouter();
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [displayName, setDisplayName] = useState('');
@@ -155,6 +156,10 @@ export function UserSettings() {
},
});
setMessage('Profile saved');
// Topbar + sidebar `user` props come from the dashboard server
// layout reading userProfiles.displayName — refresh so the new
// name flows back through without a hard reload.
router.refresh();
} catch (err: unknown) {
setMessage(err instanceof Error ? err.message : 'Failed to save');
} finally {
@@ -191,6 +196,7 @@ export function UserSettings() {
await apiFetch('/api/v1/me/email', { method: 'PATCH', body: { email } });
setOriginalEmail(email);
setEmailMsg('Email updated. Use the new address next time you sign in.');
router.refresh();
} catch (err: unknown) {
setEmailMsg(err instanceof Error ? err.message : 'Failed to update email');
} finally {
@@ -369,8 +375,6 @@ export function UserSettings() {
</CardContent>
</Card>
<DashboardWidgetsCard />
<Card>
<CardHeader>
<CardTitle>Account</CardTitle>