fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab

Surgical fixes for the 7 UAT blockers that prevent productive forward
testing. Each item has a corresponding entry in alpha-uat-master.md.

- supplemental-info route relocated out of (portal) so it bypasses the
  isPortalDisabledGlobally() kill-switch. URL unchanged.
- file upload service derives client_id/company_id/yacht_id from
  (entityType, entityId) when not explicitly passed, so interest-tab
  uploads no longer land with client_id=NULL and stay visible in the
  Attachments list.
- triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils
  attach the anchor to the DOM before click so Chromium honours the
  download attribute; 7 sites refactored, file-named downloads stop
  arriving as bare UUIDs.
- search-nav-catalog dedupes by href at the result-collection layer so
  the same href can no longer surface twice in the command-K dropdown
  (kills the React duplicate-key warning); /admin/templates entries
  merged into a single richer-keyword variant.
- NotesList gains a parentInvalidateKey prop, wired through all five
  callers (interest, client, yacht, company, residential client/
  interest) so the Overview "Latest note" teaser refreshes when a note
  is added in the Notes tab.
- expense-form-dialog: setValue('receiptFileIds') / setValue(
  'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level
  refine sees the field and Create stops silently no-op'ing on submit.
- bulk-add-berths-wizard: side-pontoon dropdown now reads through
  useVocabulary('berth_side_pontoon_options') instead of a wrong local
  enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches
  the rest of the platform + honours admin-editable per-port overrides.

tsc clean. 1419/1419 vitest. lint clean on touched files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 16:50:58 +02:00
parent 449b9497ab
commit 2d574172ec
20 changed files with 147 additions and 66 deletions

View File

@@ -15,6 +15,7 @@ import { usePaginatedQuery } from '@/hooks/use-paginated-query';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import { useConfirmation } from '@/hooks/use-confirmation';
import { apiFetch } from '@/lib/api/client';
import { triggerUrlDownload } from '@/lib/utils/download';
interface InterestDocumentsTabProps {
interestId: string;
@@ -69,10 +70,7 @@ export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps)
const res = await apiFetch<{ data: { url: string; filename: string } }>(
`/api/v1/files/${file.id}/download`,
);
const a = document.createElement('a');
a.href = res.data.url;
a.download = res.data.filename;
a.click();
triggerUrlDownload(res.data.url, res.data.filename);
} catch {
// silent
}
@@ -141,6 +139,7 @@ export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps)
<FileUploadZone
entityType="client"
entityId={interest.clientId}
clientId={interest.clientId}
onUploadComplete={() => {
queryClient.invalidateQueries({ queryKey: filesQueryKey });
}}

View File

@@ -36,6 +36,7 @@ import {
type DocumentStatus,
} from '@/lib/labels/document-status';
import { cn } from '@/lib/utils';
import { triggerUrlDownload } from '@/lib/utils/download';
import { useUIStore } from '@/stores/ui-store';
interface InterestEoiTabProps {
@@ -594,10 +595,7 @@ function SignedPdfActions({ fileId }: { fileId: string }) {
if (mode === 'view') {
window.open(res.data.url, '_blank', 'noopener,noreferrer');
} else {
const a = document.createElement('a');
a.href = res.data.url;
a.download = res.data.filename;
a.click();
triggerUrlDownload(res.data.url, res.data.filename);
}
} catch (err) {
toastError(err, 'Failed to fetch signed PDF');

View File

@@ -1208,7 +1208,12 @@ export function getInterestTabs({
id: 'notes',
label: 'Notes',
content: (
<NotesList entityType="interests" entityId={interestId} currentUserId={currentUserId} />
<NotesList
entityType="interests"
entityId={interestId}
currentUserId={currentUserId}
parentInvalidateKey={['interests', interestId]}
/>
),
},
];