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:
@@ -1,5 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { TrendingUp, TrendingDown, Minus, Info } from 'lucide-react';
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface KPITileProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
@@ -7,10 +9,19 @@ interface KPITileProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
value: React.ReactNode;
|
||||
/** Signed delta vs. prior period; positive = green, negative = red, undefined = no chip. */
|
||||
delta?: number;
|
||||
/** Optional suffix appended to the delta value (e.g. '%' for percentage deltas). */
|
||||
deltaSuffix?: string;
|
||||
/** Pre-rendered sparkline (recharts) - caller decides shape. */
|
||||
sparkline?: React.ReactNode;
|
||||
/** Optional accent stripe colour token; defaults to brand. */
|
||||
accent?: 'brand' | 'success' | 'warning' | 'mint' | 'teal' | 'purple';
|
||||
/** Optional explainer rendered in a Popover when the user clicks the (i)
|
||||
* icon next to the title. Keep it short — one sentence ideally. */
|
||||
tooltip?: React.ReactNode;
|
||||
/** For metrics where lower is better (bounce rate, error count). The
|
||||
* delta number is still shown verbatim, but the colour flips: a
|
||||
* negative delta renders green (improvement) instead of red. */
|
||||
lowerIsBetter?: boolean;
|
||||
}
|
||||
|
||||
const ACCENT_STRIPES: Record<NonNullable<KPITileProps['accent']>, string> = {
|
||||
@@ -26,19 +37,27 @@ export function KPITile({
|
||||
title,
|
||||
value,
|
||||
delta,
|
||||
deltaSuffix,
|
||||
sparkline,
|
||||
accent = 'brand',
|
||||
tooltip,
|
||||
lowerIsBetter = false,
|
||||
className,
|
||||
...props
|
||||
}: KPITileProps) {
|
||||
// Colour logic is independent of the displayed number. For normal
|
||||
// metrics, positive=good (green); for `lowerIsBetter` metrics, flip so
|
||||
// a drop renders green.
|
||||
const isImprovement = typeof delta === 'number' ? (lowerIsBetter ? delta < 0 : delta > 0) : false;
|
||||
const isRegression = typeof delta === 'number' ? (lowerIsBetter ? delta > 0 : delta < 0) : false;
|
||||
const deltaClass =
|
||||
typeof delta === 'number'
|
||||
? delta > 0
|
||||
typeof delta !== 'number'
|
||||
? ''
|
||||
: isImprovement
|
||||
? 'text-success'
|
||||
: delta < 0
|
||||
: isRegression
|
||||
? 'text-error'
|
||||
: 'text-muted-foreground'
|
||||
: '';
|
||||
: 'text-muted-foreground';
|
||||
const deltaPrefix = typeof delta === 'number' ? (delta > 0 ? '+' : '') : '';
|
||||
|
||||
return (
|
||||
@@ -53,18 +72,50 @@ export function KPITile({
|
||||
<div className={cn('absolute inset-x-0 top-0 h-1', ACCENT_STRIPES[accent])} aria-hidden />
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground sm:text-xs">
|
||||
{title}
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground sm:text-xs">
|
||||
<span>{title}</span>
|
||||
{tooltip ? (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
type="button"
|
||||
aria-label={`What is ${title}?`}
|
||||
className="inline-flex size-3.5 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400"
|
||||
>
|
||||
<Info className="size-3" aria-hidden />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-64 text-xs leading-relaxed normal-case tracking-normal text-muted-foreground"
|
||||
>
|
||||
{tooltip}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1 truncate text-lg font-semibold tabular-nums text-foreground sm:mt-2 sm:text-2xl">
|
||||
{value}
|
||||
<div className="mt-1 flex items-baseline gap-2 sm:mt-2">
|
||||
<span className="truncate text-lg font-semibold tabular-nums text-foreground sm:text-2xl">
|
||||
{value}
|
||||
</span>
|
||||
{typeof delta === 'number' ? (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex shrink-0 items-center gap-0.5 text-xs font-medium',
|
||||
deltaClass,
|
||||
)}
|
||||
>
|
||||
{delta > 0 ? (
|
||||
<TrendingUp className="size-3" aria-hidden />
|
||||
) : delta < 0 ? (
|
||||
<TrendingDown className="size-3" aria-hidden />
|
||||
) : (
|
||||
<Minus className="size-3" aria-hidden />
|
||||
)}
|
||||
{deltaPrefix}
|
||||
{delta}
|
||||
{deltaSuffix ?? ''}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{typeof delta === 'number' ? (
|
||||
<div className={cn('mt-1 text-xs font-medium', deltaClass)}>
|
||||
{deltaPrefix}
|
||||
{delta}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{sparkline ? <div className="h-12 w-24 shrink-0 opacity-80">{sparkline}</div> : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user