feat(documents): edit-metadata UI for externally-uploaded EOIs
External-EOI uploads previously had no edit path. Once the rep clicked
Upload, the recorded title / signed-date / signatories / notes were
stuck. Fixing a misspelled signer name or a wrong signing date meant
re-uploading the whole document.
- New service helper `updateExternalEoiMetadata` patches:
documents.title, documents.notes
interests.dateEoiSigned (when signedAt changes)
document_signers (full-replacement by id-presence: rows with an id
are UPDATEd, rows without are INSERTed, existing rows whose id
isn't in the array are DELETEd)
Mirrors the upload-time invariants. CC rows are stored but excluded
from the X/Y signed count; non-CC rows pre-stamp `status='signed'`
with the effective signedAt. Refuses to touch Documenso-managed docs
(vendor owns their signer rows) or non-EOI types (form shape isn't
widened yet) with ConflictError.
- `PATCH /api/v1/documents/[id]/metadata` route uses strict zod schema
+ documents.edit permission. 204 on success; service throws surface
as the normal errorResponse mapping.
- `<ExternalEoiEditDialog>` mirrors the upload-dialog's signatory
affordance (name + email + role + add/remove) plus title / signed
date / notes. Title is required; remove rows via the trash icon.
- Document detail page gains an "Edit metadata" button (Pencil icon)
that renders only when `isManualUpload && documentType === 'eoi'`.
Initial signing date derives from the earliest stamped signer's
signedAt to match what the upload service writes.
- Trails the edit in document_events as `metadata_updated` so the
activity timeline distinguishes upload-time vs edit-time changes.
Dialog state is initialised once per mount; the parent only renders the
dialog while open so each open is a fresh mount (avoids
setState-in-effect re-hydration banned by lint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
75
src/app/api/v1/documents/[id]/metadata/route.ts
Normal file
75
src/app/api/v1/documents/[id]/metadata/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { updateExternalEoiMetadata } from '@/lib/services/external-eoi.service';
|
||||
|
||||
const signatorySchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
name: z.string().min(1).max(200),
|
||||
email: z.string().email(),
|
||||
role: z.enum(['client', 'developer', 'rep', 'witness', 'cc']),
|
||||
});
|
||||
|
||||
const patchSchema = z
|
||||
.object({
|
||||
title: z.string().min(1).max(500).optional(),
|
||||
signedAt: z
|
||||
.string()
|
||||
.datetime({ offset: true })
|
||||
.or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/))
|
||||
.nullable()
|
||||
.optional(),
|
||||
notes: z.string().max(4000).optional(),
|
||||
signatories: z.array(signatorySchema).max(20).optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(v) =>
|
||||
v.title !== undefined ||
|
||||
v.signedAt !== undefined ||
|
||||
v.notes !== undefined ||
|
||||
v.signatories !== undefined,
|
||||
{ message: 'At least one field must be provided' },
|
||||
);
|
||||
|
||||
/**
|
||||
* PATCH /api/v1/documents/[id]/metadata
|
||||
*
|
||||
* Edits title / signed-date / notes / signatories on a previously-uploaded
|
||||
* external (manually-uploaded) document. Service throws ConflictError for
|
||||
* Documenso-managed docs (vendor owns their signers) and for non-EOI
|
||||
* document types (form shape isn't widened yet).
|
||||
*/
|
||||
export const PATCH = withAuth(
|
||||
withPermission('documents', 'edit', async (req, ctx, params) => {
|
||||
try {
|
||||
const body = await parseBody(req, patchSchema);
|
||||
const signedAtValue =
|
||||
body.signedAt === undefined
|
||||
? undefined
|
||||
: body.signedAt === null
|
||||
? null
|
||||
: new Date(body.signedAt);
|
||||
await updateExternalEoiMetadata({
|
||||
documentId: params.id!,
|
||||
portId: ctx.portId,
|
||||
title: body.title,
|
||||
signedAt: signedAtValue,
|
||||
notes: body.notes,
|
||||
signatories: body.signatories,
|
||||
meta: {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
});
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user