feat(documents): create-document wizard MVP + service dispatch

Implements createFromWizard and createFromUpload service paths covering
the documenso-template, in-app, and upload pathways. Persists subject
FK, signers, watchers, and the per-document reminder controls
(remindersDisabled / reminderCadenceOverride) introduced in PR1. New
POST /api/v1/documents/wizard route and a functional /documents/new UI
with type/source/template/signers/reminders sections. Drag-handle
reorder, watcher autocomplete picker, and PDF preview defer to the
PR10 polish sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 02:43:00 +02:00
parent 2dc53842c0
commit d8f0cdd7d2
5 changed files with 656 additions and 36 deletions

View File

@@ -17,6 +17,58 @@ export const updateDocumentSchema = z.object({
status: z.enum(DOCUMENT_STATUSES).optional(),
});
const wizardSignerSchema = z.object({
signerName: z.string().min(1),
signerEmail: z.string().email(),
signerRole: z.enum(['client', 'sales', 'approver', 'developer', 'other']),
signingOrder: z.number().int().min(1),
});
export const createDocumentWizardSchema = z
.object({
source: z.enum(['template', 'upload']).default('template'),
templateId: z.string().optional(),
uploadedFileId: z.string().optional(),
documentType: z.enum(DOCUMENT_TYPES),
title: z.string().min(1).max(200),
notes: z.string().optional(),
interestId: z.string().optional(),
reservationId: z.string().optional(),
clientId: z.string().optional(),
companyId: z.string().optional(),
yachtId: z.string().optional(),
signers: z.array(wizardSignerSchema).optional(),
signingMode: z.enum(['sequential', 'parallel']).default('sequential'),
pathway: z.enum(['documenso-template', 'inapp', 'upload']).default('documenso-template'),
watchers: z.array(z.string()).default([]),
reminderCadenceOverride: z.number().int().min(1).max(365).nullable().optional(),
remindersDisabled: z.boolean().default(false),
autoPlaceFields: z.boolean().default(true),
sendImmediately: z.boolean().default(true),
})
.refine(
(d) =>
[d.interestId, d.reservationId, d.clientId, d.companyId, d.yachtId].filter(Boolean).length ===
1,
{ message: 'Exactly one subject (interest/reservation/client/company/yacht) is required' },
)
.refine((d) => d.source !== 'template' || Boolean(d.templateId), {
path: ['templateId'],
message: 'templateId is required when source=template',
})
.refine((d) => d.source !== 'upload' || Boolean(d.uploadedFileId), {
path: ['uploadedFileId'],
message: 'uploadedFileId is required when source=upload',
});
export type CreateDocumentWizardInput = z.infer<typeof createDocumentWizardSchema>;
export const documentsHubTabs = [
'all',
'awaiting_them',