From 4f25a8c1248a65ddd65c9c6b0519a6b830f104fb Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Fri, 3 Apr 2026 17:29:27 -0600 Subject: [PATCH] fix(signers): show invalid email error, persist signers immediately via PATCH, add PATCH /api/documents/[id] --- .../src/app/api/documents/[id]/route.ts | 36 +++++++++++++++++++ .../[docId]/_components/PreparePanel.tsx | 19 ++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 teressa-copeland-homes/src/app/api/documents/[id]/route.ts diff --git a/teressa-copeland-homes/src/app/api/documents/[id]/route.ts b/teressa-copeland-homes/src/app/api/documents/[id]/route.ts new file mode 100644 index 0000000..3e5288b --- /dev/null +++ b/teressa-copeland-homes/src/app/api/documents/[id]/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { db } from '@/lib/db'; +import { documents } from '@/lib/db/schema'; +import type { DocumentSigner } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; + +// PATCH /api/documents/[id] — partial update (signers, name, etc.) +export async function PATCH( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session) return new Response('Unauthorized', { status: 401 }); + + const { id } = await params; + const body = await req.json() as { signers?: DocumentSigner[] }; + + const doc = await db.query.documents.findFirst({ where: eq(documents.id, id) }); + if (!doc) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + + const updates: Partial = {}; + if (body.signers !== undefined) updates.signers = body.signers; + + if (Object.keys(updates).length === 0) { + return NextResponse.json({ ok: true }); + } + + const [updated] = await db + .update(documents) + .set(updates) + .where(eq(documents.id, id)) + .returning(); + + return NextResponse.json(updated); +} diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx index b7f70ae..fa8c9d5 100644 --- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx +++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx @@ -67,6 +67,16 @@ export function PreparePanel({ const [signerInput, setSignerInput] = useState(''); const [signerInputError, setSignerInputError] = useState(null); + async function saveSigners(updated: DocumentSigner[]) { + onSignersChange?.(updated); + // Persist immediately so signers survive page refresh + await fetch(`/api/documents/${docId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ signers: updated }), + }); + } + function handleAddSigner() { const email = signerInput.trim().toLowerCase(); if (!email || !isValidEmail(email)) { @@ -78,14 +88,14 @@ export function PreparePanel({ return; } const color = SIGNER_COLORS[signers.length % SIGNER_COLORS.length]; - onSignersChange?.([...signers, { email, color }]); + saveSigners([...signers, { email, color }]); setSignerInput(''); setSignerInputError(null); } function handleRemoveSigner(email: string) { - onSignersChange?.(signers.filter(s => s.email !== email)); - // Clear validation state since signer count changed + const updated = signers.filter(s => s.email !== email); + saveSigners(updated); onUnassignedFieldIdsChange?.(new Set()); } @@ -329,6 +339,9 @@ export function PreparePanel({

Each signer receives their own signing link.

+ {signerInputError === 'invalid' && ( +

Please enter a valid email address.

+ )} {signerInputError === 'duplicate' && (

That email is already in the signer list.

)}