feat(uat-p4): inheritance polish - yacht dims, occupancy chip, map-flip flag
Phase 4 of the active UAT sweep wraps the inheritance/polish bucket. - BerthOccupancyChip: new shared component that surfaces the competing active interest on a non-available berth as a colour-coded chip with a stage badge. Adopted in LinkedBerthRowItem, BerthRecommenderPanel recommendation card, and InterestBerthStatusBanner; the banner aligns query keys with the chip so React Query dedupes the network call. - OverviewTab inheritance: getInterestById now ships a yachtDimensions block when the interest is linked to a yacht with dimensions. The Berth Requirements rows render a "↩ <value> from yacht" pill when the desired field is blank; clicking the pill copies the value into the interest. After a manual edit, a toast offers to write the new value back to the yacht record so the canonical truth stays in sync. - Map-flip inheritance: ExternalEoiUploadDialog and UploadForSigningDialog now expose a single "Mark berth(s) as Under Offer on the public map" checkbox that defaults ON when any in-bundle berth already has is_specific_interest=true. On submit, PATCHes the in-bundle berths that don't already match; sister surface to the EOI generate dialog's per-berth picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -218,10 +218,28 @@ interface TierInputs {
|
||||
activeInterestCount: number;
|
||||
lostCount: number;
|
||||
maxActiveStage: number;
|
||||
/** Berth's status column. Reconciles against the interest_berths
|
||||
* aggregates: a berth flagged "Under Offer" or "Sold" via the
|
||||
* status column alone (admin-set, NocoDB import, or a stale row
|
||||
* with no live interest_berths entry) shouldn't fall into Tier A.
|
||||
* Optional for backcompat — pure aggregate-based callers still
|
||||
* classify correctly when this is undefined. */
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export function classifyTier(t: TierInputs): Tier {
|
||||
// Berth status overrides the aggregate path. A sold berth is
|
||||
// effectively closed — treat it as late stage. An Under Offer
|
||||
// berth has at least one party engaged even if interest_berths
|
||||
// doesn't echo them (e.g. admin manually flipped status). Both
|
||||
// collapse the "Open · Under Offer" contradiction surfaced in UAT
|
||||
// 2026-05-26. Sold > UnderOffer > active interest aggregates.
|
||||
const normStatus = (t.status ?? '').toLowerCase();
|
||||
if (normStatus === 'sold') return 'D';
|
||||
if (t.activeInterestCount > 0 && t.maxActiveStage >= LATE_STAGE_THRESHOLD) return 'D';
|
||||
if (normStatus === 'under offer' || normStatus === 'under_offer') {
|
||||
return t.activeInterestCount > 0 ? 'C' : 'C';
|
||||
}
|
||||
if (t.activeInterestCount > 0) return 'C';
|
||||
if (t.lostCount > 0) return 'B';
|
||||
return 'A';
|
||||
|
||||
@@ -665,6 +665,45 @@ export async function getInterestById(id: string, portId: string) {
|
||||
: (berthResoldRaw.rows ?? []);
|
||||
const dateBerthSoldToOther = berthResoldRows[0]?.at ?? null;
|
||||
|
||||
// Yacht dimensions for inheritance display in OverviewTab. When the
|
||||
// interest has a linked yacht we ship the yacht's length/width/draft
|
||||
// alongside the interest record so the Berth Requirements section can
|
||||
// render a "from yacht" pill in place of an empty value. This is a
|
||||
// display-only inheritance - the actual recommender source switch is
|
||||
// still governed by `interests.useYachtDimensions`.
|
||||
let yachtDimensions: {
|
||||
lengthFt: string | null;
|
||||
widthFt: string | null;
|
||||
draftFt: string | null;
|
||||
lengthM: string | null;
|
||||
widthM: string | null;
|
||||
draftM: string | null;
|
||||
} | null = null;
|
||||
if (interest.yachtId) {
|
||||
const [yachtRow] = await db
|
||||
.select({
|
||||
lengthFt: yachts.lengthFt,
|
||||
widthFt: yachts.widthFt,
|
||||
draftFt: yachts.draftFt,
|
||||
lengthM: yachts.lengthM,
|
||||
widthM: yachts.widthM,
|
||||
draftM: yachts.draftM,
|
||||
})
|
||||
.from(yachts)
|
||||
.where(eq(yachts.id, interest.yachtId))
|
||||
.limit(1);
|
||||
if (yachtRow) {
|
||||
const anyDim =
|
||||
yachtRow.lengthFt ||
|
||||
yachtRow.widthFt ||
|
||||
yachtRow.draftFt ||
|
||||
yachtRow.lengthM ||
|
||||
yachtRow.widthM ||
|
||||
yachtRow.draftM;
|
||||
if (anyDim) yachtDimensions = yachtRow;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the assignee's display name for the header chip - falling back
|
||||
// to the raw ID is fine if the user record is missing (deleted/disabled).
|
||||
let assignedToName: string | null = null;
|
||||
@@ -706,6 +745,7 @@ export async function getInterestById(id: string, portId: string) {
|
||||
dateDocumentDeclined,
|
||||
dateReservationCancelled,
|
||||
dateBerthSoldToOther,
|
||||
yachtDimensions,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user