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>
This commit is contained in:
2026-05-11 15:17:02 +02:00
parent 63f96254e5
commit 880c5cbafc
5 changed files with 337 additions and 23 deletions

View File

@@ -17,10 +17,30 @@ import {
SelectValue,
} from '@/components/ui/select';
import { PageHeader } from '@/components/shared/page-header';
import { ClientPicker } from '@/components/shared/client-picker';
import { CompanyPicker } from '@/components/companies/company-picker';
import { YachtPicker } from '@/components/yachts/yacht-picker';
import { InterestPicker } from '@/components/interests/interest-picker';
import { DocumentTemplatePicker } from '@/components/documents/document-template-picker';
import { FileUploadZone } from '@/components/files/file-upload-zone';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
import { DOCUMENT_TYPES } from '@/lib/constants';
// Display labels for SIGNER_ROLES — internal values stay lowercase, UI shows
// capitalized. Falls back to capitalize-first-letter for any value not in the
// explicit map.
const SIGNER_ROLE_LABELS: Record<string, string> = {
client: 'Client',
sales: 'Sales',
approver: 'Approver',
developer: 'Developer',
other: 'Other',
};
function formatSignerRole(r: string): string {
return SIGNER_ROLE_LABELS[r] ?? r.charAt(0).toUpperCase() + r.slice(1);
}
const SIGNER_ROLES = ['client', 'sales', 'approver', 'developer', 'other'] as const;
const SUBJECT_TYPES = [
@@ -216,24 +236,37 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
</Select>
</div>
<div className="flex flex-col gap-2">
<Label className="text-xs">Template id</Label>
<Input
value={templateId}
onChange={(e) => setTemplateId(e.target.value)}
placeholder="Template UUID"
<Label className="text-xs">Template</Label>
<DocumentTemplatePicker
value={templateId || null}
onChange={(id) => setTemplateId(id ?? '')}
/>
</div>
</>
) : (
<div className="flex flex-col gap-2">
<Label className="text-xs">Uploaded file id</Label>
<Input
value={uploadedFileId}
onChange={(e) => setUploadedFileId(e.target.value)}
placeholder="File UUID from /api/v1/files upload"
/>
<Label className="text-xs">Upload PDF</Label>
{uploadedFileId ? (
<div className="flex items-center justify-between rounded-md border bg-muted/30 px-3 py-2 text-xs">
<span className="truncate">File ready (id: {uploadedFileId.slice(0, 8)})</span>
<button
type="button"
onClick={() => setUploadedFileId('')}
className="text-muted-foreground hover:text-destructive"
>
Clear
</button>
</div>
) : (
<FileUploadZone
onUploadComplete={(file) => {
if (file?.id) setUploadedFileId(file.id);
}}
/>
)}
<p className="text-xs text-muted-foreground">
Upload via the existing file uploader, then paste the returned id here.
Drop a PDF or click to browse. The file is stored, then the wizard wires it as
the source for signing.
</p>
</div>
)}
@@ -274,9 +307,14 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
<div className="grid grid-cols-[max-content_1fr] gap-2">
<Select
value={subjectType}
onValueChange={(v) => setSubjectType(v as typeof subjectType)}
onValueChange={(v) => {
setSubjectType(v as typeof subjectType);
// Reset subject id when the type changes — pickers are
// type-specific and old ids belong to the wrong table.
setSubjectId('');
}}
>
<SelectTrigger className="w-32">
<SelectTrigger className="h-9 w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -287,11 +325,27 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
))}
</SelectContent>
</Select>
<Input
value={subjectId}
onChange={(e) => setSubjectId(e.target.value)}
placeholder={`${subjectType} id`}
/>
{subjectType === 'client' ? (
<ClientPicker value={subjectId || null} onChange={(id) => setSubjectId(id ?? '')} />
) : subjectType === 'company' ? (
<CompanyPicker
value={subjectId || null}
onChange={(id) => setSubjectId(id ?? '')}
/>
) : subjectType === 'yacht' ? (
<YachtPicker value={subjectId || null} onChange={(id) => setSubjectId(id ?? '')} />
) : subjectType === 'interest' ? (
<InterestPicker
value={subjectId || null}
onChange={(id) => setSubjectId(id ?? '')}
/>
) : (
<Input
value={subjectId}
onChange={(e) => setSubjectId(e.target.value)}
placeholder="Reservation id"
/>
)}
</div>
</div>
</section>
@@ -330,13 +384,13 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
updateSigner(idx, { signerRole: v as SignerRow['signerRole'] })
}
>
<SelectTrigger>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SIGNER_ROLES.map((r) => (
<SelectItem key={r} value={r}>
{r}
{formatSignerRole(r)}
</SelectItem>
))}
</SelectContent>