chore(autonomous-session): consolidate uncommitted work from prior session

Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
This commit is contained in:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -31,7 +31,7 @@ interface Props {
*/
export function ActiveInterestsPopover({ berthId, portSlug, count }: Props) {
// Lazy-load: only fetch when the popover opens. Pattern from the
// detail-label fallback queries elsewhere in the codebase the
// detail-label fallback queries elsewhere in the codebase - the
// `enabled` flag flips on first open.
const { data, isLoading, isError } = useQuery<{ data: ActiveInterestRow[] }>({
queryKey: ['berth', berthId, 'active-interests'],

View File

@@ -44,7 +44,7 @@ export function BerthCard({ berth }: BerthCardProps) {
// already conveyed by the pill below, so the stripe is dock-keyed.
const accentClass = mooringLetterDot(berth.mooringNumber) ?? 'bg-slate-300';
// Dimensions string Length × Width × Draft (each segment is optional).
// Dimensions string - Length × Width × Draft (each segment is optional).
// The avatar already conveys the mooring number, so this becomes the
// primary "what is this berth" line.
const dimParts: string[] = [];
@@ -53,7 +53,7 @@ export function BerthCard({ berth }: BerthCardProps) {
if (berth.draftM) dimParts.push(`${berth.draftM}m draft`);
const dimText = dimParts.length > 0 ? dimParts.join(' × ') : null;
// Recommended boat size the most rep-actionable signal in a glance
// Recommended boat size - the most rep-actionable signal in a glance
// ("can my client's yacht park here?"). Tenure was previously here but
// dropped: tenure is set per EOI/contract, not per berth, so showing
// it as a berth property was misleading.
@@ -64,7 +64,7 @@ export function BerthCard({ berth }: BerthCardProps) {
boatCapacityText = `Fits up to ${berth.nominalBoatSize}ft`;
}
// Water depth operational; matters for deep-keel yachts.
// Water depth - operational; matters for deep-keel yachts.
let waterDepthText: string | null = null;
if (berth.waterDepthM) {
const prefix = berth.waterDepthIsMinimum ? '≥ ' : '';
@@ -134,7 +134,7 @@ export function BerthCard({ berth }: BerthCardProps) {
}
>
<div className="flex items-center gap-3">
{/* The mooring number IS the avatar recognisable at a glance
{/* The mooring number IS the avatar - recognisable at a glance
(A1, B12, …) and eliminates the duplicate berth-number heading
that previously sat to the right of an anchor icon. */}
<ListCardAvatar

View File

@@ -82,7 +82,7 @@ export type BerthRow = {
/**
* Toggleable columns for the berth list ColumnPicker. Heavy NocoDB
* fields default to hidden; reps can switch them on per-table-view.
* `mooringNumber` is intentionally omitted from this list it's the
* `mooringNumber` is intentionally omitted from this list - it's the
* primary identifier and always visible.
*/
export const BERTH_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [
@@ -108,7 +108,7 @@ export const BERTH_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [
{ id: 'tags', label: 'Tags' },
];
/** Hidden by default power-users turn them on via the picker. */
/** Hidden by default - power-users turn them on via the picker. */
export const BERTH_DEFAULT_HIDDEN: string[] = [
'tenure',
'sidePontoon',
@@ -148,14 +148,14 @@ function StatusBadge({ status }: { status: string }) {
/**
* #67 Phase 2: small amber chip beside the status pill flagging rows
* whose status was set manually and has no backing interest. These are
* the candidates for the catch-up wizard the rep flipped a berth to
* the candidates for the catch-up wizard - the rep flipped a berth to
* "Under Offer" or "Sold" without ever creating the matching deal.
*/
function ManualBadge() {
return (
<span
className="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-800"
title="Status set manually with no backing interest needs catch-up"
title="Status set manually with no backing interest - needs catch-up"
>
Manual
</span>
@@ -470,7 +470,7 @@ export const berthColumns: ColumnDef<BerthRow, unknown>[] = [
* cell renderer reading a context.
*
* Imperial columns assume the canonical `*Ft` columns are populated
* (true by default the import pipeline + bulk-add wizard write both,
* (true by default - the import pipeline + bulk-add wizard write both,
* and the inline editor in yacht-tabs.tsx auto-fills the counterpart).
* Rows with only the metric counterpart fall through to `?` for that
* dimension; the cell still renders so the rep sees what's set.

View File

@@ -105,7 +105,7 @@ interface InterestOption {
id: string;
clientName: string;
pipelineStage: string;
/** Used to sort the picker most recently interacted with floats to the top. */
/** Used to sort the picker - most recently interacted with floats to the top. */
updatedAt?: string;
}
@@ -138,7 +138,7 @@ function StatusChangeDialog({
const interestId = watch('interestId');
const showInterestPicker = status === 'under_offer' || status === 'sold';
// Active interests for this port used to populate the prospect
// Active interests for this port - used to populate the prospect
// selector when status moves to under_offer / sold. Only fetched when
// the picker is actually visible to avoid an unnecessary round-trip
// for available-status changes.
@@ -317,7 +317,7 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
/**
* Searchable combobox for picking a linked prospect when changing berth
* status. Replaces the bare Select which had no filter, no stage colours,
* and no recency sort for ports with 200+ active interests that became
* and no recency sort - for ports with 200+ active interests that became
* a scroll-fest. Stage labels render with the same coloured pill the rest
* of the CRM uses for stage badges so the rep can scan the list visually.
*/
@@ -332,7 +332,7 @@ function InterestLinkPicker({
}) {
const [open, setOpen] = useState(false);
// Sort with the most recently updated interest first so reps see the
// active deals at the top of the list older / dormant ones drop
// active deals at the top of the list - older / dormant ones drop
// beneath. `updatedAt` is set on every patch + every stage advance.
const sorted = [...options].sort((a, b) => {
if (!a.updatedAt && !b.updatedAt) return 0;

View File

@@ -51,7 +51,7 @@ export function BerthDetail({ berthId }: BerthDetailProps) {
useEffect(() => {
if (searchParams.get('edit') === 'true') {
// setState in effect is the right shape here the URL is an
// setState in effect is the right shape here - the URL is an
// external store and the trigger is a query-param change, not a
// prop in the React tree.
// eslint-disable-next-line react-hooks/set-state-in-effect

View File

@@ -1,9 +1,9 @@
/**
* Documents tab on the berth detail page (Phase 6b see plan §5.6).
* Documents tab on the berth detail page (Phase 6b - see plan §5.6).
*
* Sections:
* - Current PDF panel (download link, "Replace PDF" button, parse-engine chip).
* - Version history list newest first, with rollback affordance on every
* - Version history list - newest first, with rollback affordance on every
* non-current row.
* - Reconcile-diff dialog (PdfReconcileDialog), opened after a successful
* upload + parse. Shows auto-applied vs conflicted fields and lets the

View File

@@ -47,7 +47,7 @@ export function BerthInterestPulse({ berthId }: { berthId: string }) {
// Stay in sync with the linked-berths list + add-to-interest dialog.
// Each of those flows emits a realtime socket event but does NOT
// invalidate this exact query key (it's berth-scoped, theirs are
// interest-scoped) bridge via the invalidation hook.
// interest-scoped) - bridge via the invalidation hook.
useRealtimeInvalidation({
'interest:berthLinked': [queryKey],
'interest:berthUnlinked': [queryKey],

View File

@@ -90,7 +90,7 @@ export function BerthList() {
'berth:statusChanged': [['berths']],
});
// Persisted column visibility + row density + dimension unit same
// Persisted column visibility + row density + dimension unit - same
// pattern as ClientList / InterestList; density falls back to
// 'comfortable' and dimensionUnit to 'ft' for users who haven't picked.
const { hidden, setHidden, density, setDensity, dimensionUnit, setDimensionUnit } =
@@ -98,7 +98,7 @@ export function BerthList() {
const columnVisibility = Object.fromEntries(hidden.map((id) => [id, false]));
const berthColumns = getBerthColumns(dimensionUnit);
// Bulk-action state one dialog per action (status / tenure type /
// Bulk-action state - one dialog per action (status / tenure type /
// tag add+remove). Mirrors the InterestList pattern so reps already
// know the idiom from there.
const qc = useQueryClient();
@@ -143,16 +143,16 @@ export function BerthList() {
// No "New" button - berths are import-only
/>
{/* Toolbar two halves separated by `justify-between` so the
{/* Toolbar - two halves separated by `justify-between` so the
Columns + Saved-views actions stay pinned to the right edge of
the row at every width. The previous `ml-auto` trick didn't
survive flex-wrap on intermediate widths the actions ended
survive flex-wrap on intermediate widths - the actions ended
up centered. */}
<div className="flex items-center gap-2 flex-wrap justify-between">
<div className="flex items-center gap-2 flex-wrap min-w-0 flex-1">
<FilterBar
// Search is hoisted out of the popover into the inline input
// below keeps the daily "find by mooring/area" lookup one
// below - keeps the daily "find by mooring/area" lookup one
// tap away instead of buried behind the Filters dropdown.
filters={berthFilterDefinitions.filter((d) => d.key !== 'search')}
values={filters}
@@ -302,7 +302,7 @@ export function BerthList() {
: undefined
}
cardRender={(row) => <BerthCard berth={row.original} />}
// Group adjacent cards by dock letter (area) on mobile adds a
// Group adjacent cards by dock letter (area) on mobile - adds a
// dim divider + uppercased label above the first card of each
// group. Data is already sorted by mooringNumber (A1, A2, …, B1,
// B2, …) so consecutive rows naturally share dock letters.
@@ -436,7 +436,7 @@ export function BerthList() {
toast.error('Pick at least one tag.');
return;
}
// Per-tag bulk call the endpoint takes one tagId at a
// Per-tag bulk call - the endpoint takes one tagId at a
// time. For the typical 1-2 tag case the round-trips are
// cheap; multi-tag UX can come later.
const action = tagDialog?.mode === 'add' ? 'add_tag' : 'remove_tag';

View File

@@ -81,7 +81,7 @@ function SpecRow({ label, value }: { label: string; value: React.ReactNode }) {
/**
* Tags Card for the berth overview. Wraps the InlineTagEditor in a Card so
* the section header uses CardTitle styling; mirrors the visibility rule
* the editor itself uses hides entirely when the port has no tags
* the editor itself uses - hides entirely when the port has no tags
* defined AND this berth has none applied.
*/
function BerthTagsCard({ berth }: { berth: BerthData }) {
@@ -215,7 +215,7 @@ function OverviewTab({ berth }: { berth: BerthData }) {
const patch = useBerthPatch(berth.id);
// User-selected display unit for dimensions. Persisted in localStorage
// so reps' preferred unit sticks across navigations + sessions.
// Lazy initializer reads localStorage on first render avoids the
// Lazy initializer reads localStorage on first render - avoids the
// mount-effect-setState shape the compiler flags.
const [units, setUnits] = useState<'ft' | 'm'>(() => {
if (typeof window === 'undefined') return 'ft';

View File

@@ -61,7 +61,7 @@ const STATUS_TO_STAGES: Record<string, readonly string[]> = {
* under_offer → enquiry...reservation, available → any)
*
* Doc upload + payment recording (Phases 4.4 / 4.5 of the spec) are
* out of scope for the initial cut once the interest exists, the rep
* out of scope for the initial cut - once the interest exists, the rep
* has the standard interest detail page to upload contracts and record
* payments. The wizard's job is to get them from "manual berth, no
* interest" to "interest exists, override cleared" in one round-trip.
@@ -94,7 +94,7 @@ export function CatchUpWizard({ berthId, open, onOpenChange }: CatchUpWizardProp
});
const allowedStages = berth ? (STATUS_TO_STAGES[berth.data.status] ?? PIPELINE_STAGES) : [];
// Default the stage picker to the "right" default for each status
// Default the stage picker to the "right" default for each status -
// sold defaults to contract (and we auto-set outcome=won server-side),
// under_offer defaults to eoi since that's the most common pre-deal
// status that reps mark manually.
@@ -124,7 +124,7 @@ export function CatchUpWizard({ berthId, open, onOpenChange }: CatchUpWizardProp
);
},
onSuccess: (res) => {
toast.success('Berth reconciled new interest created');
toast.success('Berth reconciled - new interest created');
queryClient.invalidateQueries({ queryKey: ['berths'] });
queryClient.invalidateQueries({ queryKey: ['berths', 'reconcile-queue'] });
queryClient.invalidateQueries({ queryKey: ['interests'] });

View File

@@ -1,14 +1,14 @@
/**
* Maps a berth's mooring-letter prefix (A, B, C…) to a subtle visual
* accent. Pontoons cluster physically A row is one dock, B another
* so the berth grid reads at a glance when each pontoon's rows
* accent. Pontoons cluster physically - A row is one dock, B another
* - so the berth grid reads at a glance when each pontoon's rows
* share a colour cue. Earlier iteration tinted the entire row
* background; that proved visually noisy. This version keeps rows
* white and surfaces the colour as a coloured left border, plus a
* matching dot the column factory uses inside the Mooring # cell.
*
* Cycle wraps at the 8th letter; ports with more pontoons get
* repeats (fine in practice they don't sit adjacent on the page).
* repeats (fine in practice - they don't sit adjacent on the page).
*/
const BORDER_CYCLE = [
'border-l-4 border-l-rose-400',

View File

@@ -1,13 +1,13 @@
/**
* Reconcile-diff dialog (Phase 6b see plan §4.7b, §14.6).
* Reconcile-diff dialog (Phase 6b - see plan §4.7b, §14.6).
*
* Shown after a successful per-berth PDF upload + parse. Surfaces three
* sections:
* - Warnings (mooring-number mismatch, imperial-vs-metric drift, etc.)
* so the rep can abort before applying.
* - Auto-applied fields fields the parser found that the CRM had as null;
* - Auto-applied fields - fields the parser found that the CRM had as null;
* these are pre-checked and applied on confirm.
* - Conflicts fields where CRM and PDF disagree on a non-null value.
* - Conflicts - fields where CRM and PDF disagree on a non-null value.
* The rep picks "Keep CRM" or "Use PDF" per row before confirming.
*
* On confirm, the dialog POSTs to /pdf-versions/parse-results/apply with the