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:
2026-05-26 21:48:19 +02:00
parent fe5f98db23
commit 2592e28578
10 changed files with 614 additions and 83 deletions

View File

@@ -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';