feat(uat-batch): Group C Berth list features (3 new ships + 1 verified)
C20–C23 from the 2026-05-21 plan.
Shipped now:
C21 Dimensions ft/m column toggle persisted to user prefs.
`TablePreferences.dimensionUnit` ('ft' | 'm') added to the user-
profiles JSONB. `useTablePreferences` returns `dimensionUnit` +
`setDimensionUnit` alongside hidden/density. New
`getBerthColumns(unit)` factory rewrites the dimensions /
nominalBoatSize / waterDepth cells when ft is requested
(waterDepth converts on-the-fly from the canonical meters
column at 3.2808 ft/m). Berth-list toolbar gains a small
ft/m toggle button next to the density toggle.
C22 ft/m switching on Berth Requirements rows.
`interest-tabs.tsx` Berth-requirements section now honours
`interest.desiredLengthUnit`. Labels flip to "(m)" when set;
value reads from `desired*M` columns; on save, both the chosen-
unit and the canonical counterpart columns are PATCHed (3.28084
ratio) so downstream surfaces (recommender, EOI merge fields)
stay in lockstep. `InterestPatchField` widened with `desired*M`
variants.
C23 Berth list bulk-edit affordance.
New `POST /api/v1/berths/bulk` (mirror of /interests/bulk):
discriminated union of `change_status` / `change_tenure_type` /
`add_tag` / `remove_tag` / `archive`, 500-id cap, per-row
failure reporting, single `berths.edit` permission gate
(no separate `archive` perm exists on berths today). Status
mutations route through `updateBerthStatus` so under-offer /
sold transitions still trigger the primary interest_berths
auto-link + the rules-engine evaluation.
BerthList toolbar wires `bulkActions` on the DataTable —
Change status (Select dialog), Change tenure (permanent /
fixed-term), Add tag, Remove tag, Archive (destructive +
confirmation). Each dialog uses the same `bulkMutation` so
toast + cache-invalidation behaviour is consistent across
actions.
Already shipped (verified):
C20 Berth list rates / pricing valid columns hidden by default —
already in `BERTH_DEFAULT_HIDDEN`.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -242,6 +242,11 @@ function formatMoney(amount: string | null, currency: string): string | null {
|
||||
return formatCurrency(amount, currency, { maxFractionDigits: 0 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Static column list rendered in metric units (the historical default).
|
||||
* Most callers should use `getBerthColumns(unit)` instead, which lets the
|
||||
* berth-list toolbar toggle render imperial when the rep prefers feet.
|
||||
*/
|
||||
export const berthColumns: ColumnDef<BerthRow, unknown>[] = [
|
||||
{
|
||||
accessorKey: 'mooringNumber',
|
||||
@@ -457,3 +462,59 @@ export const berthColumns: ColumnDef<BerthRow, unknown>[] = [
|
||||
cell: ({ row }) => <ActionsCell row={row} />,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns a copy of `berthColumns` with the dimension-bearing cells
|
||||
* rewritten to render in the requested unit. Used by `BerthList` so the
|
||||
* column-header toggle can flip the rendering globally without each
|
||||
* 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,
|
||||
* 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.
|
||||
*/
|
||||
export function getBerthColumns(unit: 'ft' | 'm'): ColumnDef<BerthRow, unknown>[] {
|
||||
if (unit === 'm') return berthColumns;
|
||||
return berthColumns.map((col) => {
|
||||
if (col.id === 'dimensions') {
|
||||
return {
|
||||
...col,
|
||||
cell: ({ row }) => {
|
||||
const { lengthFt, widthFt, draftFt, widthIsMinimum } = row.original;
|
||||
if (!lengthFt && !widthFt) return '-';
|
||||
const widthLabel = widthFt ? `${widthIsMinimum ? '≥' : ''}${widthFt}ft` : '?';
|
||||
const base = `${lengthFt ?? '?'}ft × ${widthLabel}`;
|
||||
return draftFt ? `${base} (draft ${draftFt}ft)` : base;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (col.id === 'nominalBoatSize') {
|
||||
return {
|
||||
...col,
|
||||
cell: ({ row }) => {
|
||||
const ft = row.original.nominalBoatSize;
|
||||
const m = row.original.nominalBoatSizeM;
|
||||
if (!ft && !m) return '-';
|
||||
return ft ? `${ft}ft` : `${m}m`;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (col.id === 'waterDepth') {
|
||||
// Water depth lacks a stored `*Ft` column today; convert from meters
|
||||
// on the fly when the rep prefers ft. 1m = 3.2808ft (canonical
|
||||
// ratio used in yacht-dimensions.ts).
|
||||
return {
|
||||
...col,
|
||||
cell: ({ row }) => {
|
||||
const { waterDepthM, waterDepthIsMinimum } = row.original;
|
||||
if (!waterDepthM) return '-';
|
||||
const ft = Number(waterDepthM) * 3.2808;
|
||||
return `${waterDepthIsMinimum ? '≥' : ''}${ft.toFixed(1)}ft`;
|
||||
},
|
||||
};
|
||||
}
|
||||
return col;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user