diff --git a/src/components/interests/interest-form.tsx b/src/components/interests/interest-form.tsx index 61612135..93b84557 100644 --- a/src/components/interests/interest-form.tsx +++ b/src/components/interests/interest-form.tsx @@ -860,6 +860,9 @@ interface DimensionInputProps { unit: 'ft' | 'm'; ftValue: string | number | undefined; onChangeFt: (next: string | undefined) => void; + /** B3 #1: greyed out when the interest is set to read dimensions + * off the linked yacht instead of these fields. */ + disabled?: boolean; } /** @@ -878,6 +881,7 @@ function DimensionInput({ unit, ftValue, onChangeFt, + disabled, }: DimensionInputProps) { const focusedRef = useRef(false); const [display, setDisplay] = useState(() => computeDisplay(ftValue, unit)); @@ -904,6 +908,7 @@ function DimensionInput({ min={0} placeholder={placeholder} value={display} + disabled={disabled} onFocus={() => { focusedRef.current = true; }} diff --git a/src/lib/db/migrations/0087_interest_use_yacht_dims.sql b/src/lib/db/migrations/0087_interest_use_yacht_dims.sql new file mode 100644 index 00000000..a2a6f73d --- /dev/null +++ b/src/lib/db/migrations/0087_interest_use_yacht_dims.sql @@ -0,0 +1,11 @@ +-- 0087_interest_use_yacht_dims.sql +-- Adds a per-interest toggle so the berth recommender can read the +-- linked yacht's dimensions instead of the rep-entered `desired_*` +-- values. Defaults to false so existing data continues to use the +-- desired-dims path the recommender has always used. +-- +-- Per docs/superpowers/audits/alpha-uat-master.md Bucket 3 #1: +-- "Interest dimensions: dual-source model". + +ALTER TABLE interests + ADD COLUMN IF NOT EXISTS use_yacht_dimensions boolean NOT NULL DEFAULT false; diff --git a/src/lib/db/schema/interests.ts b/src/lib/db/schema/interests.ts index 3134097a..80401fda 100644 --- a/src/lib/db/schema/interests.ts +++ b/src/lib/db/schema/interests.ts @@ -102,6 +102,12 @@ export const interests = pgTable( desiredLengthUnit: text('desired_length_unit').notNull().default('ft'), desiredWidthUnit: text('desired_width_unit').notNull().default('ft'), desiredDraftUnit: text('desired_draft_unit').notNull().default('ft'), + /** Per-interest source-of-truth toggle for the berth recommender. + * When true AND a yacht is linked, the recommender uses the yacht's + * length/width/draft instead of the rep-entered desired_* columns + * on this row. Defaults false so existing data continues to use the + * desired-dims path. Migration 0087. */ + useYachtDimensions: boolean('use_yacht_dimensions').notNull().default(false), archivedAt: timestamp('archived_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), diff --git a/src/lib/services/berth-recommender.service.ts b/src/lib/services/berth-recommender.service.ts index bc5d2b84..5b685d56 100644 --- a/src/lib/services/berth-recommender.service.ts +++ b/src/lib/services/berth-recommender.service.ts @@ -34,6 +34,7 @@ import { and, eq, inArray, sql } from 'drizzle-orm'; import { db } from '@/lib/db'; import { systemSettings } from '@/lib/db/schema/system'; import { interests } from '@/lib/db/schema/interests'; +import { yachts } from '@/lib/db/schema/yachts'; import { CodedError } from '@/lib/errors'; // ─── Settings ────────────────────────────────────────────────────────────── @@ -380,6 +381,11 @@ interface InterestInput { desiredWidthFt: number | null; desiredDraftFt: number | null; portId: string; + /** Audit/debug aid — which dimension set the predicates were built + * from. Set by `loadInterestInput` based on the dual-source toggle + * + yacht link. Not consumed by SQL; surfaced in tests and trace + * logs only. */ + dimensionsSource: 'interest' | 'yacht'; } async function loadInterestInput(interestId: string): Promise { @@ -389,6 +395,8 @@ async function loadInterestInput(interestId: string): Promise