Files
pn-new-crm/src/components/files/file-upload-zone.tsx

207 lines
6.7 KiB
TypeScript
Raw Normal View History

'use client';
import { useCallback, useRef, useState } from 'react';
import { Upload, X } from 'lucide-react';
import { cn } from '@/lib/utils';
interface UploadingFile {
id: string;
name: string;
progress: number;
error?: string;
}
interface FileUploadZoneProps {
entityType?: string;
entityId?: string;
clientId?: string;
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
yachtId?: string;
companyId?: string;
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
/**
* Optional folder to deposit the file into. Hub uploads pass the
* currently-selected folderId so files land where the user expects.
*/
folderId?: string | null;
/**
* Fires per successful upload with the file metadata. The wizard /
* inline-upload flows use the returned id to wire follow-up actions
* (e.g. set as the source PDF for a Documenso signing flow).
*/
onUploadComplete?: (file?: { id: string; filename?: string }) => void;
}
export function FileUploadZone({
entityType,
entityId,
clientId,
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
yachtId,
companyId,
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
folderId,
onUploadComplete,
}: FileUploadZoneProps) {
const [isDragOver, setIsDragOver] = useState(false);
const [uploading, setUploading] = useState<UploadingFile[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const uploadFiles = useCallback(
async (fileList: FileList) => {
const newUploads: UploadingFile[] = Array.from(fileList).map((f) => ({
id: crypto.randomUUID(),
name: f.name,
progress: 0,
}));
setUploading((prev) => [...prev, ...newUploads]);
await Promise.all(
Array.from(fileList).map(async (file, i) => {
const uploadId = newUploads[i]!.id;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name);
if (clientId) formData.append('clientId', clientId);
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
if (yachtId) formData.append('yachtId', yachtId);
if (companyId) formData.append('companyId', companyId);
if (entityType) formData.append('entityType', entityType);
if (entityId) formData.append('entityId', entityId);
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
if (folderId) formData.append('folderId', folderId);
setUploading((prev) =>
prev.map((u) => (u.id === uploadId ? { ...u, progress: 50 } : u)),
);
// Use fetch directly for FormData (apiFetch JSON-encodes body)
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
const portId = (await import('@/stores/ui-store')).useUIStore.getState().currentPortId;
const headers = new Headers();
if (portId) headers.set('X-Port-Id', portId);
const uploadRes = await fetch('/api/v1/files/upload', {
method: 'POST',
headers,
credentials: 'include',
body: formData,
});
if (!uploadRes.ok) {
throw new Error('Upload failed');
}
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
const uploadJson = (await uploadRes
.json()
.catch(() => null)) as { data?: { id?: string; filename?: string } } | null;
if (uploadJson?.data?.id) {
onUploadComplete?.({
id: uploadJson.data.id,
filename: uploadJson.data.filename,
});
}
setUploading((prev) =>
prev.map((u) => (u.id === uploadId ? { ...u, progress: 100 } : u)),
);
} catch {
setUploading((prev) =>
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
prev.map((u) => (u.id === uploadId ? { ...u, error: 'Upload failed' } : u)),
);
}
}),
);
// Clear completed uploads after a moment
setTimeout(() => {
setUploading((prev) => prev.filter((u) => u.error));
onUploadComplete?.();
}, 1500);
},
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
[clientId, yachtId, companyId, entityType, entityId, folderId, onUploadComplete],
);
const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragOver(false);
if (e.dataTransfer.files.length > 0) {
void uploadFiles(e.dataTransfer.files);
}
},
[uploadFiles],
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
void uploadFiles(e.target.files);
e.target.value = '';
}
},
[uploadFiles],
);
return (
<div className="space-y-3">
<div
role="button"
tabIndex={0}
className={cn(
'flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors cursor-pointer',
isDragOver
? 'border-primary bg-primary/5'
: 'border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/30',
)}
onDragOver={(e) => {
e.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDrop}
onClick={() => inputRef.current?.click()}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') inputRef.current?.click();
}}
>
<Upload className="h-8 w-8 text-muted-foreground mb-2" />
<p className="text-sm font-medium">Drop files here or click to upload</p>
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
<p className="text-xs text-muted-foreground mt-1">PDF, Word, Excel, images up to 50MB</p>
<input
ref={inputRef}
type="file"
multiple
className="hidden"
onChange={handleChange}
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.doc,.docx,.xls,.xlsx,.txt,.csv"
/>
</div>
{uploading.length > 0 && (
<div className="space-y-2">
{uploading.map((u) => (
<div key={u.id} className="flex items-center gap-3 text-sm">
<span className="flex-1 truncate">{u.name}</span>
{u.error ? (
<span className="text-destructive text-xs">{u.error}</span>
) : (
<div className="flex items-center gap-2">
<div className="h-1.5 w-24 rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${u.progress}%` }}
/>
</div>
<span className="text-xs text-muted-foreground">{u.progress}%</span>
</div>
)}
{u.error && (
<button
type="button"
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
onClick={() => setUploading((prev) => prev.filter((x) => x.id !== u.id))}
>
<X className="h-3.5 w-3.5 text-muted-foreground" />
</button>
)}
</div>
))}
</div>
)}
</div>
);
}