Files
pn-new-crm/src/components/shared/entity-activity-feed.tsx

366 lines
11 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { formatDistanceToNow } from 'date-fns';
import { ChevronDown } from 'lucide-react';
import { apiFetch } from '@/lib/api/client';
import { Button } from '@/components/ui/button';
import { STAGE_LABELS, formatEnum, formatSource, type PipelineStage } from '@/lib/constants';
import { cn } from '@/lib/utils';
interface AuditRow {
id: string;
action: string;
entityType: string;
entityId: string | null;
fieldChanged: string | null;
oldValue: unknown;
newValue: unknown;
metadata: Record<string, unknown> | null;
createdAt: string;
actor: { id: string; email: string; name: string | null } | null;
}
const ACTION_VERBS: Record<string, { past: string }> = {
create: { past: 'created' },
update: { past: 'updated' },
delete: { past: 'deleted' },
archive: { past: 'archived' },
restore: { past: 'restored' },
merge: { past: 'merged' },
revert: { past: 'reverted' },
};
function actionVerb(action: string): string {
return ACTION_VERBS[action]?.past ?? action;
}
function formatField(field: string | null): string | null {
if (!field) return null;
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units Berth surfaces - New compact mooring-chip header (colored plate + status pill, dock-label in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack - Berth list gains a "Latest deal stage" column showing the most-advanced pipeline stage of any active linked interest (server-aggregated, ranks by PIPELINE_STAGES index) - "Linked prospect" Select on the status-change dialog rebuilt as a Command combobox: search, recent-first sort, stage-coloured pills Pipeline UX - Reverting an interest to Open with linked berths now prompts: keep the links, unlink and reset, or cancel. Silent when no berths are linked - Activity feed + entity-activity feed normalise enum field values via STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as "10% Deposit → Contract Sent" EOI generate dialog - Inline-editable rows for client name, nationality (country combobox), and yacht name — pencil affordance saves directly via clients/yachts PATCH - Replaces the single "Edit on client's page" link with two contextual links framed by short copy explaining what's inline vs what needs the canonical page - Backend EoiContext now includes client.id + yacht.id so the dialog can PATCH without an extra round-trip Company form - New "Connections" section lets the rep attach members (clients) and yachts during create. Yacht attach uses the existing transfer endpoint so audit log + ownership history capture the change - Inline "+ New client" / "+ New yacht" buttons open the canonical forms stacked over the company sheet - After save, the form chains to a yacht pull-in prompt (if any attached client owns yachts not yet linked) and an optional "Create interest" step pre-filled with the first attached client Admin - /admin landing gains a searchable index — typed query flattens groups into a result list matching label + description + group title - "Documenso & EOI" card relabelled to "EOI signing service" (consistent with the user-facing language rename from round 1) Measurement units (migration 0053) - interests gains desired_*_m columns + desired_*_unit discriminators so the rep's literal entry (ft OR m) is preserved verbatim instead of being reconstructed from a single canonical column on every render - yachts + berths gain matching *_unit columns alongside their existing ft + m pairs; defaults to 'ft' so legacy rows still render normally - Interest form POST/PATCH now sends both ft + m + unit; computed m is derived from the ft canonical to keep the recommender SQL unchanged Misc - Active-deals tile + topbar type their Link href as `Route` instead of `any` - Unused REPORT_TYPE_LABELS const dropped from generate-report-form - Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated to include the new id + unit fields on the EoiContext / Berth shapes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:28:22 +02:00
return field
.replace(/_/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.toLowerCase();
}
function formatValueForField(field: string | null, value: unknown): string {
if (value === null || value === undefined) return '';
if (field) {
const f = field.replace(/_/g, '').toLowerCase();
if (typeof value === 'string') {
if (f === 'pipelinestage' || f === 'stage') {
return STAGE_LABELS[value as PipelineStage] ?? formatEnum(value);
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units Berth surfaces - New compact mooring-chip header (colored plate + status pill, dock-label in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack - Berth list gains a "Latest deal stage" column showing the most-advanced pipeline stage of any active linked interest (server-aggregated, ranks by PIPELINE_STAGES index) - "Linked prospect" Select on the status-change dialog rebuilt as a Command combobox: search, recent-first sort, stage-coloured pills Pipeline UX - Reverting an interest to Open with linked berths now prompts: keep the links, unlink and reset, or cancel. Silent when no berths are linked - Activity feed + entity-activity feed normalise enum field values via STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as "10% Deposit → Contract Sent" EOI generate dialog - Inline-editable rows for client name, nationality (country combobox), and yacht name — pencil affordance saves directly via clients/yachts PATCH - Replaces the single "Edit on client's page" link with two contextual links framed by short copy explaining what's inline vs what needs the canonical page - Backend EoiContext now includes client.id + yacht.id so the dialog can PATCH without an extra round-trip Company form - New "Connections" section lets the rep attach members (clients) and yachts during create. Yacht attach uses the existing transfer endpoint so audit log + ownership history capture the change - Inline "+ New client" / "+ New yacht" buttons open the canonical forms stacked over the company sheet - After save, the form chains to a yacht pull-in prompt (if any attached client owns yachts not yet linked) and an optional "Create interest" step pre-filled with the first attached client Admin - /admin landing gains a searchable index — typed query flattens groups into a result list matching label + description + group title - "Documenso & EOI" card relabelled to "EOI signing service" (consistent with the user-facing language rename from round 1) Measurement units (migration 0053) - interests gains desired_*_m columns + desired_*_unit discriminators so the rep's literal entry (ft OR m) is preserved verbatim instead of being reconstructed from a single canonical column on every render - yachts + berths gain matching *_unit columns alongside their existing ft + m pairs; defaults to 'ft' so legacy rows still render normally - Interest form POST/PATCH now sends both ft + m + unit; computed m is derived from the ft canonical to keep the recommender SQL unchanged Misc - Active-deals tile + topbar type their Link href as `Route` instead of `any` - Unused REPORT_TYPE_LABELS const dropped from generate-report-form - Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated to include the new id + unit fields on the EoiContext / Berth shapes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:28:22 +02:00
}
if (f === 'source') return formatSource(value) ?? value;
if (f === 'leadcategory' || f === 'category' || f === 'outcome') {
return formatEnum(value);
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units Berth surfaces - New compact mooring-chip header (colored plate + status pill, dock-label in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack - Berth list gains a "Latest deal stage" column showing the most-advanced pipeline stage of any active linked interest (server-aggregated, ranks by PIPELINE_STAGES index) - "Linked prospect" Select on the status-change dialog rebuilt as a Command combobox: search, recent-first sort, stage-coloured pills Pipeline UX - Reverting an interest to Open with linked berths now prompts: keep the links, unlink and reset, or cancel. Silent when no berths are linked - Activity feed + entity-activity feed normalise enum field values via STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as "10% Deposit → Contract Sent" EOI generate dialog - Inline-editable rows for client name, nationality (country combobox), and yacht name — pencil affordance saves directly via clients/yachts PATCH - Replaces the single "Edit on client's page" link with two contextual links framed by short copy explaining what's inline vs what needs the canonical page - Backend EoiContext now includes client.id + yacht.id so the dialog can PATCH without an extra round-trip Company form - New "Connections" section lets the rep attach members (clients) and yachts during create. Yacht attach uses the existing transfer endpoint so audit log + ownership history capture the change - Inline "+ New client" / "+ New yacht" buttons open the canonical forms stacked over the company sheet - After save, the form chains to a yacht pull-in prompt (if any attached client owns yachts not yet linked) and an optional "Create interest" step pre-filled with the first attached client Admin - /admin landing gains a searchable index — typed query flattens groups into a result list matching label + description + group title - "Documenso & EOI" card relabelled to "EOI signing service" (consistent with the user-facing language rename from round 1) Measurement units (migration 0053) - interests gains desired_*_m columns + desired_*_unit discriminators so the rep's literal entry (ft OR m) is preserved verbatim instead of being reconstructed from a single canonical column on every render - yachts + berths gain matching *_unit columns alongside their existing ft + m pairs; defaults to 'ft' so legacy rows still render normally - Interest form POST/PATCH now sends both ft + m + unit; computed m is derived from the ft canonical to keep the recommender SQL unchanged Misc - Active-deals tile + topbar type their Link href as `Route` instead of `any` - Unused REPORT_TYPE_LABELS const dropped from generate-report-form - Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated to include the new id + unit fields on the EoiContext / Berth shapes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:28:22 +02:00
}
}
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
return JSON.stringify(value);
}
/** Natural-sentence rendering of one audit row. Falls back to terse when
* there's no field to talk about. */
function sentence(row: AuditRow, actor: string): string {
const verb = actionVerb(row.action);
const field = formatField(row.fieldChanged);
if (field) {
return `${actor} ${verb} the ${field}`;
}
return `${actor} ${verb} this record`;
}
interface Props {
endpoint: string; // e.g. /api/v1/clients/{id}/activity
emptyText?: string;
}
interface SessionGroup {
actorKey: string;
actorLabel: string;
rows: AuditRow[];
}
/** Five-minute window: consecutive events from the same actor inside this
* window collapse into one session header. Outside the window a new
* group starts so a single bulk-edit run reads as one block. */
const SESSION_WINDOW_MS = 5 * 60_000;
function groupRows(rows: AuditRow[]): SessionGroup[] {
const groups: SessionGroup[] = [];
for (const row of rows) {
const actorKey = row.actor?.id ?? '__system__';
const actorLabel = row.actor?.name || row.actor?.email || 'System';
const last = groups[groups.length - 1];
if (
last &&
last.actorKey === actorKey &&
last.rows.length > 0 &&
Math.abs(new Date(last.rows[0]!.createdAt).getTime() - new Date(row.createdAt).getTime()) <=
SESSION_WINDOW_MS
) {
last.rows.push(row);
} else {
groups.push({ actorKey, actorLabel, rows: [row] });
}
}
return groups;
}
export function EntityActivityFeed({ endpoint, emptyText = 'No activity yet.' }: Props) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const t = setInterval(() => setNow(Date.now()), 60_000);
return () => clearInterval(t);
}, []);
const { data, isLoading, error } = useQuery({
queryKey: ['entity-activity', endpoint],
queryFn: () => apiFetch<{ data: AuditRow[] }>(endpoint),
staleTime: 30_000,
});
// Filter state — chips above the feed. Empty selection = no filter.
const [actorFilter, setActorFilter] = useState<string | null>(null);
const [actionFilter, setActionFilter] = useState<string | null>(null);
const rows = data?.data ?? [];
const actorOptions = useMemo(() => {
const map = new Map<string, string>();
for (const r of rows) {
const id = r.actor?.id ?? '__system__';
const label = r.actor?.name || r.actor?.email || 'System';
map.set(id, label);
}
return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1]));
}, [rows]);
const actionOptions = useMemo(() => {
const set = new Set<string>();
for (const r of rows) set.add(r.action);
return Array.from(set).sort();
}, [rows]);
const filteredRows = rows.filter((r) => {
if (actorFilter && (r.actor?.id ?? '__system__') !== actorFilter) return false;
if (actionFilter && r.action !== actionFilter) return false;
return true;
});
const groups = useMemo(() => groupRows(filteredRows), [filteredRows]);
if (isLoading) {
return <div className="text-sm text-muted-foreground py-6">Loading activity</div>;
}
if (error) {
return (
<div className="text-sm text-red-600 py-6">
Failed to load activity: {error instanceof Error ? error.message : 'unknown error'}
</div>
);
}
if (rows.length === 0) {
return <div className="text-sm text-muted-foreground py-6">{emptyText}</div>;
}
// Touch `now` so the closure participates in the minute re-render.
void now;
return (
<div className="space-y-3">
{(actorOptions.length > 1 || actionOptions.length > 1) && (
<div className="flex flex-wrap items-center gap-1.5 text-xs">
{actorOptions.length > 1 && (
<FilterChipMenu
label="Actor"
value={actorFilter}
onChange={setActorFilter}
options={actorOptions.map(([id, label]) => ({ value: id, label }))}
/>
)}
{actionOptions.length > 1 && (
<FilterChipMenu
label="Action"
value={actionFilter}
onChange={setActionFilter}
options={actionOptions.map((a) => ({ value: a, label: actionVerb(a) }))}
/>
)}
{(actorFilter || actionFilter) && (
<button
type="button"
className="text-muted-foreground underline"
onClick={() => {
setActorFilter(null);
setActionFilter(null);
}}
>
Clear
</button>
)}
</div>
)}
{filteredRows.length === 0 ? (
<div className="text-sm text-muted-foreground py-6">
No activity matches the current filters.
</div>
) : (
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>
2026-05-20 15:56:11 +02:00
<ol className="relative ml-3 pl-6 space-y-4 py-2">
{groups.map((group, gi) => (
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>
2026-05-20 15:56:11 +02:00
<SessionGroupItem
key={`${group.actorKey}-${gi}`}
group={group}
isLast={gi === groups.length - 1}
/>
))}
</ol>
)}
</div>
);
}
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>
2026-05-20 15:56:11 +02:00
function SessionGroupItem({ group, isLast }: { group: SessionGroup; isLast: boolean }) {
const [expanded, setExpanded] = useState(group.rows.length <= 3);
const first = group.rows[0]!;
const created = new Date(first.createdAt);
const ago = formatDistanceToNow(created, { addSuffix: true });
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>
2026-05-20 15:56:11 +02:00
// Vertical connector — runs from below this bubble down to the next item,
// omitted on the last item so the line never trails past the last bubble.
const connector = !isLast ? (
<span
aria-hidden
className="absolute left-[-26px] top-3 bottom-[-1rem] w-px bg-muted-foreground/20"
/>
) : null;
if (group.rows.length === 1) {
return (
<li className="relative">
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>
2026-05-20 15:56:11 +02:00
{connector}
<span className="absolute left-[-31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
<RowBody row={first} actor={group.actorLabel} ago={ago} />
</li>
);
}
return (
<li className="relative">
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>
2026-05-20 15:56:11 +02:00
{connector}
<span className="absolute left-[-31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="text-sm hover:underline flex items-center gap-1"
>
<span className="font-medium">{group.actorLabel}</span>
<span className="text-muted-foreground">
made {group.rows.length} changes in this session
</span>
<ChevronDown
className={cn(
'h-3.5 w-3.5 text-muted-foreground transition-transform',
expanded && 'rotate-180',
)}
/>
</button>
<div className="text-xs text-muted-foreground" title={created.toISOString()}>
{ago}
</div>
{expanded && (
<ol className="mt-2 space-y-2 border-l border-muted-foreground/15 pl-3">
{group.rows.map((r) => (
<li key={r.id} className="text-sm">
<RowBody row={r} actor={group.actorLabel} ago={null} compact />
</li>
))}
</ol>
)}
</li>
);
}
function RowBody({
row,
actor,
ago,
compact = false,
}: {
row: AuditRow;
actor: string;
ago: string | null;
compact?: boolean;
}) {
return (
<>
<div className="text-sm">{sentence(row, actor)}</div>
{ago !== null && (
<div className="text-xs text-muted-foreground" title={row.createdAt}>
{ago}
</div>
)}
{row.fieldChanged && (row.oldValue !== null || row.newValue !== null) ? (
<div className={cn('text-xs space-x-1', compact ? 'mt-0.5' : 'mt-1')}>
{row.oldValue !== null && row.oldValue !== undefined ? (
<span className="line-through text-muted-foreground">
{formatValueForField(row.fieldChanged, row.oldValue).slice(0, 80)}
</span>
) : null}
{row.newValue !== null && row.newValue !== undefined ? (
<span className="text-foreground">
{formatValueForField(row.fieldChanged, row.newValue).slice(0, 80)}
</span>
) : null}
</div>
) : null}
</>
);
}
function FilterChipMenu({
label,
value,
onChange,
options,
}: {
label: string;
value: string | null;
onChange: (v: string | null) => void;
options: { value: string; label: string }[];
}) {
// Lightweight native <select>-style chip — keeps the activity feed
// self-contained without pulling in a popover or radix dropdown for
// what is essentially "two filter chips".
return (
<label className="inline-flex items-center gap-1 rounded-full border px-2 py-0.5 hover:bg-muted/50 cursor-pointer">
<span className="text-muted-foreground">{label}:</span>
<select
className="bg-transparent outline-none text-foreground"
value={value ?? ''}
onChange={(e) => onChange(e.target.value || null)}
>
<option value="">All</option>
{options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</label>
);
}
// Keep Button reference so unused-import lint doesn't fire if Button gets
// reintroduced later. Currently the feed uses raw <button> for the chip
// menu (smaller footprint).
void Button;