fix(b4-bugs): external-EOI cache collision + stage-gate regression test + search popover opacity

Three B4 bug fixes shipped together:

- **#4 Upload-signed-copy blank body** — ExternalEoiUploadDialog used
  queryKey=['interests', interestId] but didn't unwrap the {data} envelope
  while the parent InterestDetail (same key) does, so opening the dialog
  clobbered the cache with a wrapped shape and blanked the detail page
  ("Unknown Client" + empty tab body). Dialog now unwraps to match.

- **#2 Legacy-stage canonicalization regression test** — new integration
  test locks in the external-EOI advance gate: canonical pre-EOI stages
  (enquiry/qualified/nurturing) advance to 'eoi' on upload; at-or-past-EOI
  stages stay put while metadata still writes. 7/7 passing. Backfill
  script intentionally not shipped — dev DB is test data, prod cutover
  is manual.

- **#3 Global-search dropdown translucent rows** — defensive opaque
  background on the popover wrapper (bg-white dark:bg-popover) guards
  against the subtle transparency UAT captured on the Berths page.
  Live-browser repro still needed to identify the exact bleeding row;
  this defense makes the surface unambiguously solid in light mode
  regardless of which class wins tailwind-merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 12:59:25 +02:00
parent 13834afa46
commit 65b92cace1
3 changed files with 171 additions and 7 deletions

View File

@@ -81,14 +81,21 @@ export function ExternalEoiUploadDialog({
// unscannable in any list when a port has multiple deals closing on
// the same day. Also drives auto-fill on signatory rows tagged
// role=client.
// Cache shape MUST match the parent <InterestDetail> consumer of the same
// ['interests', interestId] key, which unwraps the envelope and caches the
// inner row. Caching the wrapped { data: ... } here would clobber the
// parent's cache and blank the detail page (the rep would see "Unknown
// Client" + an empty tab body the moment this dialog opened — UAT
// 2026-05-24).
const { data: interestData } = useQuery<{
data: { clientName: string | null; clientPrimaryEmail: string | null };
clientName: string | null;
clientPrimaryEmail: string | null;
}>({
queryKey: ['interests', interestId],
queryFn: () =>
apiFetch<{ data: { clientName: string | null; clientPrimaryEmail: string | null } }>(
`/api/v1/interests/${interestId}`,
),
).then((r) => r.data),
enabled: open,
staleTime: 60_000,
});
@@ -99,11 +106,11 @@ export function ExternalEoiUploadDialog({
const signatories: SignatoryRow[] = useMemo(() => {
if (signatoriesOverride !== null) return signatoriesOverride;
if (prefillSignatories && prefillSignatories.length > 0) return prefillSignatories;
if (!interestData?.data) return [];
if (!interestData) return [];
return [
{
name: interestData.data.clientName ?? '',
email: interestData.data.clientPrimaryEmail ?? '',
name: interestData.clientName ?? '',
email: interestData.clientPrimaryEmail ?? '',
role: 'client' as const,
},
];
@@ -124,7 +131,7 @@ export function ExternalEoiUploadDialog({
.map((b) => b.mooringNumber)
.filter((m): m is string => !!m);
const berthLabel = moorings.length > 0 ? formatBerthRange(moorings) : null;
const clientName = interestData?.data?.clientName ?? null;
const clientName = interestData?.clientName ?? null;
const parts = ['External EOI'];
if (clientName) parts.push(clientName);
if (berthLabel) parts.push(berthLabel);