Files
pn-new-crm/src/components/documents/create-document-wizard.tsx

480 lines
17 KiB
TypeScript
Raw Normal View History

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, Plus, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { PageHeader } from '@/components/shared/page-header';
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
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';
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
// 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 = [
{ key: 'interest', label: 'Interest', field: 'interestId' as const },
{ key: 'reservation', label: 'Reservation', field: 'reservationId' as const },
{ key: 'client', label: 'Client', field: 'clientId' as const },
{ key: 'company', label: 'Company', field: 'companyId' as const },
{ key: 'yacht', label: 'Yacht', field: 'yachtId' as const },
] as const;
interface SignerRow {
signerName: string;
signerEmail: string;
signerRole: (typeof SIGNER_ROLES)[number];
signingOrder: number;
}
interface CreateDocumentWizardProps {
portSlug: string;
}
export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
const router = useRouter();
const [source, setSource] = useState<'template' | 'upload'>('template');
const [pathway, setPathway] = useState<'documenso-template' | 'inapp' | 'upload'>(
'documenso-template',
);
const [templateId, setTemplateId] = useState('');
const [uploadedFileId, setUploadedFileId] = useState('');
const [documentType, setDocumentType] = useState<(typeof DOCUMENT_TYPES)[number]>('eoi');
const [title, setTitle] = useState('');
const [notes, setNotes] = useState('');
const [subjectType, setSubjectType] = useState<(typeof SUBJECT_TYPES)[number]['key']>('interest');
const [subjectId, setSubjectId] = useState('');
const [signers, setSigners] = useState<SignerRow[]>([
{ signerName: '', signerEmail: '', signerRole: 'client', signingOrder: 1 },
]);
const [signingMode, setSigningMode] = useState<'sequential' | 'parallel'>('sequential');
const [reminderMode, setReminderMode] = useState<'default' | 'override' | 'disabled'>('default');
const [reminderDays, setReminderDays] = useState(7);
const [submitting, setSubmitting] = useState(false);
const subjectField = SUBJECT_TYPES.find((s) => s.key === subjectType)!.field;
const setSourceAndPathway = (next: 'template' | 'upload'): void => {
setSource(next);
if (next === 'upload') {
setPathway('upload');
} else if (pathway === 'upload') {
setPathway('documenso-template');
}
};
const updateSigner = (idx: number, patch: Partial<SignerRow>): void => {
setSigners((current) => current.map((s, i) => (i === idx ? { ...s, ...patch } : s)));
};
const addSigner = (): void => {
setSigners((current) => [
...current,
{
signerName: '',
signerEmail: '',
signerRole: 'other',
signingOrder: current.length + 1,
},
]);
};
const removeSigner = (idx: number): void => {
setSigners((current) =>
current.filter((_, i) => i !== idx).map((s, i) => ({ ...s, signingOrder: i + 1 })),
);
};
const handleSubmit = async (): Promise<void> => {
if (!title.trim()) {
toast.error('Title is required');
return;
}
if (!subjectId.trim()) {
toast.error(`Provide a ${subjectType} id`);
return;
}
if (source === 'template' && !templateId.trim()) {
toast.error('Pick a template');
return;
}
if (source === 'upload' && !uploadedFileId.trim()) {
toast.error('Provide an uploaded file id');
return;
}
const cleanSigners = signers.filter((s) => s.signerEmail.trim() && s.signerName.trim());
if (source === 'upload' && cleanSigners.length === 0) {
toast.error('Upload path requires at least one signer');
return;
}
setSubmitting(true);
try {
const body: Record<string, unknown> = {
source,
pathway,
documentType,
title: title.trim(),
notes: notes.trim() || undefined,
[subjectField]: subjectId.trim(),
signingMode,
watchers: [],
autoPlaceFields: true,
sendImmediately: false,
remindersDisabled: reminderMode === 'disabled',
};
if (source === 'template') body.templateId = templateId.trim();
if (source === 'upload') {
body.uploadedFileId = uploadedFileId.trim();
body.signers = cleanSigners;
} else if (cleanSigners.length > 0) {
body.signers = cleanSigners;
}
if (reminderMode === 'override') {
body.reminderCadenceOverride = reminderDays;
}
const res = await apiFetch<{ data: { id: string } }>('/api/v1/documents/wizard', {
method: 'POST',
body,
});
toast.success('Document created');
router.push(`/${portSlug}/documents/${res.data.id}`);
} catch (err) {
toastError(err);
setSubmitting(false);
}
};
return (
<div className="flex flex-col gap-4">
<PageHeader
title="New document"
description="Generate, attach, and send a document for signing."
actions={
<Button asChild variant="outline">
<Link href={`/${portSlug}/documents`}>
<ArrowLeft className="mr-1.5 h-4 w-4" /> Back
</Link>
</Button>
}
/>
<div className="grid gap-4 lg:grid-cols-2">
<section className="rounded-md border bg-white p-4">
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Source
</h2>
<div className="flex flex-col gap-3 text-sm">
<label className="flex items-center gap-2">
<input
type="radio"
checked={source === 'template'}
onChange={() => setSourceAndPathway('template')}
/>
<span>Generate from a template</span>
</label>
<label className="flex items-center gap-2">
<input
type="radio"
checked={source === 'upload'}
onChange={() => setSourceAndPathway('upload')}
/>
<span>Upload a finished PDF</span>
</label>
{source === 'template' ? (
<>
<div className="mt-2 flex flex-col gap-2">
<Label className="text-xs">Pathway</Label>
<Select value={pathway} onValueChange={(v) => setPathway(v as typeof pathway)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
<SelectItem value="documenso-template">Generated EOI rendered + signed externally</SelectItem>
<SelectItem value="inapp">Manual EOI rendered in CRM, sent for e-signature</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
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
<Label className="text-xs">Template</Label>
<DocumentTemplatePicker
value={templateId || null}
onChange={(id) => setTemplateId(id ?? '')}
/>
</div>
</>
) : (
<div className="flex flex-col gap-2">
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
<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">
Drop a PDF or click to browse. The file is stored, then the wizard wires it as the
source for signing.
</p>
</div>
)}
</div>
</section>
<section className="rounded-md border bg-white p-4">
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Document
</h2>
<div className="flex flex-col gap-3 text-sm">
<div className="flex flex-col gap-2">
<Label className="text-xs">Type</Label>
<Select
value={documentType}
onValueChange={(v) => setDocumentType(v as typeof documentType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{DOCUMENT_TYPES.map((t) => (
<SelectItem key={t} value={t}>
{t.replace(/_/g, ' ')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label className="text-xs">Title</Label>
<Input value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
<div className="flex flex-col gap-2">
<Label className="text-xs">Internal notes</Label>
<Input value={notes} onChange={(e) => setNotes(e.target.value)} />
</div>
<div className="grid grid-cols-[max-content_1fr] gap-2">
<Select
value={subjectType}
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
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('');
}}
>
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
<SelectTrigger className="h-9 w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SUBJECT_TYPES.map((s) => (
<SelectItem key={s.key} value={s.key}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
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
{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>
<section className="rounded-md border bg-white p-4 lg:col-span-2">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Signers
</h2>
<Button size="sm" variant="outline" onClick={addSigner}>
<Plus className="mr-1.5 h-3.5 w-3.5" /> Add signer
</Button>
</div>
<ul className="space-y-2">
{signers.map((s, idx) => (
<li
key={idx}
className="grid grid-cols-[2.5rem_1fr_1fr_8rem_2rem] items-center gap-2 text-sm"
>
<span className="text-xs tabular-nums text-muted-foreground">
#{s.signingOrder}
</span>
<Input
value={s.signerName}
onChange={(e) => updateSigner(idx, { signerName: e.target.value })}
placeholder="Name"
/>
<Input
value={s.signerEmail}
onChange={(e) => updateSigner(idx, { signerEmail: e.target.value })}
placeholder="Email"
/>
<Select
value={s.signerRole}
onValueChange={(v) =>
updateSigner(idx, { signerRole: v as SignerRow['signerRole'] })
}
>
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
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SIGNER_ROLES.map((r) => (
<SelectItem key={r} value={r}>
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
{formatSignerRole(r)}
</SelectItem>
))}
</SelectContent>
</Select>
<button
type="button"
aria-label="Remove signer"
onClick={() => removeSigner(idx)}
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</li>
))}
</ul>
<div className="mt-3 flex items-center gap-3 text-xs text-muted-foreground">
<Label className="text-xs">Signing mode</Label>
<Select
value={signingMode}
onValueChange={(v) => setSigningMode(v as typeof signingMode)}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sequential">Sequential</SelectItem>
<SelectItem value="parallel">Parallel</SelectItem>
</SelectContent>
</Select>
</div>
</section>
<section className="rounded-md border bg-white p-4 lg:col-span-2">
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Reminders
</h2>
<div className="flex flex-col gap-2 text-sm">
<label className="flex items-center gap-2">
<input
type="radio"
checked={reminderMode === 'default'}
onChange={() => setReminderMode('default')}
/>
Use template default
</label>
<label className="flex items-center gap-2">
<input
type="radio"
checked={reminderMode === 'override'}
onChange={() => setReminderMode('override')}
/>
Override:
<Input
type="number"
className="ml-1 w-20"
min={1}
max={365}
value={reminderDays}
onChange={(e) => setReminderDays(Number(e.target.value))}
onFocus={() => setReminderMode('override')}
/>
days
</label>
<label className="flex items-center gap-2">
<input
type="radio"
checked={reminderMode === 'disabled'}
onChange={() => setReminderMode('disabled')}
/>
Disable reminders for this document
</label>
</div>
</section>
</div>
<div className="flex items-center justify-end gap-2">
<Button variant="outline" asChild>
<Link href={`/${portSlug}/documents`}>Cancel</Link>
</Button>
<Button onClick={handleSubmit} disabled={submitting}>
{submitting ? 'Creating…' : 'Create document'}
</Button>
</div>
</div>
);
}