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:
2026-05-21 22:22:30 +02:00
parent a0a4a5d487
commit 991e2223c7
6 changed files with 575 additions and 36 deletions

View File

@@ -41,12 +41,14 @@ export function useTablePreferences(entityType: string, defaultHidden: string[]
const remoteEntry = meQuery.data?.data.preferences?.tablePreferences?.[entityType];
const remoteHidden = remoteEntry?.hiddenColumns;
const remoteDensity = remoteEntry?.density;
const remoteDimensionUnit = remoteEntry?.dimensionUnit;
// Local edits win over the server-loaded prefs. The render-phase
// derivation below (`localHidden ?? remoteHidden ?? defaultHidden`)
// replaces the prior useEffect(setLocalHidden, [remoteHidden]) sync
// that the Compiler flagged as set-state-in-effect.
const [localHidden, setLocalHidden] = useState<string[] | null>(null);
const [localDensity, setLocalDensity] = useState<'comfortable' | 'compact' | null>(null);
const [localDimensionUnit, setLocalDimensionUnit] = useState<'ft' | 'm' | null>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -109,6 +111,14 @@ export function useTablePreferences(entityType: string, defaultHidden: string[]
[flush],
);
const setDimensionUnit = useCallback(
(next: 'ft' | 'm') => {
setLocalDimensionUnit(next);
flush({ dimensionUnit: next });
},
[flush],
);
// Cleanup pending timer on unmount so React doesn't warn about
// setting state after the component is gone.
useEffect(
@@ -125,12 +135,17 @@ export function useTablePreferences(entityType: string, defaultHidden: string[]
// the never-saved case.
const resolved = localHidden ?? remoteHidden ?? defaultHidden;
const density: 'comfortable' | 'compact' = localDensity ?? remoteDensity ?? 'comfortable';
// Dimension unit: local optimistic → remote saved → 'ft' fallback (matches
// the existing canonical-format-in-feet shape of the underlying columns).
const dimensionUnit: 'ft' | 'm' = localDimensionUnit ?? remoteDimensionUnit ?? 'ft';
return {
hidden: resolved,
setHidden,
density,
setDensity,
dimensionUnit,
setDimensionUnit,
isLoaded: !meQuery.isLoading,
};
}