fix(audit): A1/A2/A4/A6/A8/A9/A16/A17/A19/A20 from 2026-05-15 sweep
Knocks out 10 of the 13 known issues from yesterday's Playwright audit. A4 — Client form silently rejected submit when a contact row had an empty value. The F19 filter ran in mutationFn after zod's handleSubmit had already short-circuited on min(1). Now wraps the onSubmit to prune empty rows BEFORE handleSubmit/zod sees them. A16 — File upload to documents hub root 400'd because FormData.get returns null for absent fields and zod's .optional() rejects null. Route handler now coerces null/empty → undefined before parse. A17 — Added /api/v1/me/ports endpoint that any authenticated user can hit; client.ts now uses it as the bootstrap port-slug→port-id resolver. Eliminates the wasteful 400s sales-reps and viewers were firing on every page load against the super-admin-gated /admin/ports. A1 — Filter permission_denied actions from the dashboard activity feed. Still in the audit log; just not noise on the dashboard. A2 — New LEGACY_STAGE_REMAP table + canonicalizeStage / stageLabelFor helpers in lib/constants. Activity-feed maps legacy 9-stage enum values (deposit_10pct, contract_sent, etc.) to their 7-stage labels on the way out, so historical audit rows read as "Deposit Paid" not "Deposit 10Pct". A19 — Same-stage write now returns 204 No Content. Service returns a STAGE_NOOP sentinel; the route handler translates it. A9 — Catch-up wizard now derives stage from berth status (under_offer → EOI, sold → contract) with a stageOverride state for explicit user picks. Avoids the set-state-in-effect rule violation. A20 — OwnerPicker shows a "Client / Company" hint chip on the trigger when no value is set, so users know the trigger opens a two-tab picker instead of just a client list. A8 — Migration 0066 normalizes legacy `statusOverrideMode = 'auto'` to NULL so the column lives at strictly 3 states. A6 — file-preview-dialog gets a screen-reader DialogDescription so the Radix "Missing aria-describedby" warning stops firing on every preview. A18 closed as not-a-bug: /api/v1/users genuinely doesn't exist (Next returns 404); /api/v1/admin/audit exists and 403s. A5 (Socket.IO dev noise) + A3 (react-grab CSP) left for a separate pass — both are dev-only cosmetic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,54 @@ export const STAGE_LABELS: Record<PipelineStage, string> = {
|
||||
contract: 'Contract',
|
||||
};
|
||||
|
||||
/**
|
||||
* Map legacy 9-stage enum values to their 7-stage equivalents. Audit logs
|
||||
* and any pre-migration data still carry the legacy values; this lets the
|
||||
* activity feed, audit diffs, and reporting render the modern label
|
||||
* without having to back-fill the underlying rows.
|
||||
*
|
||||
* Mirrors the migration applied in `seed-synthetic-data.ts` (and
|
||||
* documented in the 9→7 pipeline refactor):
|
||||
* details_sent → enquiry
|
||||
* in_communication → qualified
|
||||
* eoi_sent, eoi_signed → eoi (doc-status carries sent/signed sub-state)
|
||||
* deposit_10pct → deposit_paid
|
||||
* contract_sent, contract_signed → contract
|
||||
* completed → contract (with outcome=won)
|
||||
* open → enquiry (legacy alias for the initial stage)
|
||||
*/
|
||||
export const LEGACY_STAGE_REMAP: Record<string, PipelineStage> = {
|
||||
open: 'enquiry',
|
||||
details_sent: 'enquiry',
|
||||
in_communication: 'qualified',
|
||||
eoi_sent: 'eoi',
|
||||
eoi_signed: 'eoi',
|
||||
deposit_10pct: 'deposit_paid',
|
||||
contract_sent: 'contract',
|
||||
contract_signed: 'contract',
|
||||
completed: 'contract',
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve any stage-like string to a canonical 7-stage value. Returns
|
||||
* the modern stage as-is, maps legacy values via LEGACY_STAGE_REMAP,
|
||||
* and falls back to 'enquiry' for genuinely unknown values.
|
||||
*/
|
||||
export function canonicalizeStage(value: string | null | undefined): PipelineStage {
|
||||
if (!value) return 'enquiry';
|
||||
if (PIPELINE_STAGES.includes(value as PipelineStage)) return value as PipelineStage;
|
||||
return LEGACY_STAGE_REMAP[value] ?? 'enquiry';
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-friendly label for any stage-like string — modern or legacy. Use
|
||||
* this in any read surface (activity feed, audit diff, notification copy,
|
||||
* reports) that might be handed pre-migration data.
|
||||
*/
|
||||
export function stageLabelFor(value: string | null | undefined): string {
|
||||
return STAGE_LABELS[canonicalizeStage(value)];
|
||||
}
|
||||
|
||||
// Compact labels for cramped contexts (mobile chart axes, dense tables).
|
||||
export const STAGE_SHORT_LABELS: Record<PipelineStage, string> = {
|
||||
enquiry: 'Enquiry',
|
||||
|
||||
Reference in New Issue
Block a user