fix(05-04): simplify recipients to single pre-filled textarea, remove client dropdown

This commit is contained in:
Chandler Copeland
2026-03-20 00:50:41 -06:00
parent d669b11a50
commit 7f97bbc5e5
2 changed files with 35 additions and 139 deletions

View File

@@ -3,102 +3,51 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { TextFillForm } from './TextFillForm'; import { TextFillForm } from './TextFillForm';
interface Client { id: string; name: string; email: string; }
interface PreparePanelProps { interface PreparePanelProps {
docId: string; docId: string;
clients: Client[]; defaultEmail: string;
/** The client already explicitly assigned to this document (null if not yet assigned). */ clientName: string;
assignedClientId: string | null;
/** The client the document belongs to — used as the default selection when no explicit assignedClientId. */
defaultClientId: string;
currentStatus: string; currentStatus: string;
} }
/**
* Parse a comma/newline-separated string of email addresses into a trimmed, deduplicated array.
* Empty tokens are filtered out.
*/
function parseEmails(raw: string): string[] { function parseEmails(raw: string): string[] {
return raw return raw.split(/[\n,]+/).map((e) => e.trim()).filter(Boolean);
.split(/[\n,]+/)
.map((e) => e.trim())
.filter(Boolean);
} }
/** Very light email format check — catches obvious mistakes without a full RFC 5322 parse. */
function isValidEmail(email: string): boolean { function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
} }
export function PreparePanel({ export function PreparePanel({ docId, defaultEmail, clientName, currentStatus }: PreparePanelProps) {
docId,
clients,
assignedClientId,
defaultClientId,
currentStatus,
}: PreparePanelProps) {
const router = useRouter(); const router = useRouter();
const [recipients, setRecipients] = useState(defaultEmail);
// selectedClientId: id from the dropdown (empty string = "pick manually by email")
const [selectedClientId, setSelectedClientId] = useState<string>(
assignedClientId ?? defaultClientId,
);
// primaryEmail: editable email for the assigned client (pre-filled, can be overridden)
const assignedClient = clients.find(c => c.id === assignedClientId);
const [primaryEmail, setPrimaryEmail] = useState(assignedClient?.email ?? '');
// emailInput: raw text for manual email entry (used when selectedClientId is '' or
// as additional CC addresses)
const [emailInput, setEmailInput] = useState('');
const [textFillData, setTextFillData] = useState<Record<string, string>>({}); const [textFillData, setTextFillData] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null); const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
// Don't show the panel if already sent/signed if (currentStatus !== 'Draft') {
const canPrepare = currentStatus === 'Draft'; return (
<div className="rounded-lg border border-gray-200 p-4 bg-gray-50 text-sm text-gray-500">
// The email addresses that will be sent with the prepare request. Document status is <strong>{currentStatus}</strong> preparation is only available for Draft documents.
// When an assigned client exists, use the (editable) primaryEmail. </div>
// Otherwise use the dropdown-selected client's email. );
// Any manually-entered addresses are appended.
function buildEmailAddresses(): string[] {
const addresses: string[] = [];
if (assignedClientId && primaryEmail.trim()) {
addresses.push(primaryEmail.trim());
} else if (selectedClientId) {
const client = clients.find((c) => c.id === selectedClientId);
if (client?.email) addresses.push(client.email);
}
const extras = parseEmails(emailInput);
for (const e of extras) {
if (!addresses.includes(e)) addresses.push(e);
}
return addresses;
} }
async function handlePrepare() { async function handlePrepare() {
setLoading(true); setLoading(true);
setResult(null); setResult(null);
const emailAddresses = buildEmailAddresses(); const emailAddresses = parseEmails(recipients);
// Require at least one email address
if (emailAddresses.length === 0) { if (emailAddresses.length === 0) {
setResult({ ok: false, message: 'Please select a client or enter at least one email address.' }); setResult({ ok: false, message: 'Enter at least one recipient email.' });
setLoading(false); setLoading(false);
return; return;
} }
// Validate all addresses
const invalid = emailAddresses.filter((e) => !isValidEmail(e)); const invalid = emailAddresses.filter((e) => !isValidEmail(e));
if (invalid.length > 0) { if (invalid.length > 0) {
setResult({ ok: false, message: `Invalid email address(es): ${invalid.join(', ')}` }); setResult({ ok: false, message: `Invalid email(s): ${invalid.join(', ')}` });
setLoading(false); setLoading(false);
return; return;
} }
@@ -107,18 +56,14 @@ export function PreparePanel({
const res = await fetch(`/api/documents/${docId}/prepare`, { const res = await fetch(`/api/documents/${docId}/prepare`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({ textFillData, emailAddresses }),
textFillData,
assignedClientId: selectedClientId || null,
emailAddresses,
}),
}); });
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Unknown error' })); const err = await res.json().catch(() => ({ error: 'Unknown error' }));
setResult({ ok: false, message: err.error ?? 'Prepare failed' }); setResult({ ok: false, message: err.error ?? 'Prepare failed' });
} else { } else {
setResult({ ok: true, message: 'Document prepared successfully. Status updated to Sent.' }); setResult({ ok: true, message: 'Document prepared. Status updated to Sent.' });
router.refresh(); // Update the page to reflect new status router.refresh();
} }
} catch (e) { } catch (e) {
setResult({ ok: false, message: String(e) }); setResult({ ok: false, message: String(e) });
@@ -127,71 +72,26 @@ export function PreparePanel({
} }
} }
if (!canPrepare) {
return (
<div className="rounded-lg border border-gray-200 p-4 bg-gray-50 text-sm text-gray-500">
Document status is <strong>{currentStatus}</strong> preparation is only available for Draft documents.
</div>
);
}
return ( return (
<div className="rounded-lg border border-gray-200 p-4 space-y-4"> <div className="rounded-lg border border-gray-200 p-4 space-y-4">
<h2 className="font-semibold text-gray-900">Prepare Document</h2> <h2 className="font-semibold text-gray-900">Prepare Document</h2>
{/* Primary recipient */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Recipient email Recipients
</label>
{assignedClientId ? (
<>
<input
type="email"
value={primaryEmail}
onChange={(e) => setPrimaryEmail(e.target.value)}
className="w-full border rounded px-2 py-1.5 text-sm"
placeholder="recipient@example.com"
/>
<p className="text-xs text-gray-400 mt-0.5">
Assigned client: {clients.find(c => c.id === assignedClientId)?.name ?? assignedClientId}
</p>
</>
) : (
<select
value={selectedClientId}
onChange={(e) => setSelectedClientId(e.target.value)}
className="w-full border rounded px-2 py-1.5 text-sm"
>
<option value=""> Enter email manually below </option>
{clients.map((c) => (
<option key={c.id} value={c.id}>{c.name} ({c.email})</option>
))}
</select>
)}
</div>
{/* Additional / manual email addresses */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{assignedClientId || selectedClientId
? 'Additional email addresses (optional, comma-separated)'
: 'Email address(es) — required if no client selected above'}
</label> </label>
<textarea <textarea
value={emailInput} value={recipients}
onChange={(e) => setEmailInput(e.target.value)} onChange={(e) => setRecipients(e.target.value)}
placeholder={
assignedClientId || selectedClientId
? 'e.g. cc@example.com, another@example.com'
: 'Enter recipient email, e.g. client@example.com'
}
rows={2} rows={2}
className="w-full border rounded px-2 py-1.5 text-sm resize-none" className="w-full border rounded px-2 py-1.5 text-sm resize-none"
placeholder="email@example.com"
/> />
{clientName && (
<p className="text-xs text-gray-400 mt-0.5"> <p className="text-xs text-gray-400 mt-0.5">
Separate multiple addresses with commas or new lines. {clientName} · add more addresses separated by commas
</p> </p>
)}
</div> </div>
<div> <div>
@@ -201,7 +101,7 @@ export function PreparePanel({
<button <button
onClick={handlePrepare} onClick={handlePrepare}
disabled={loading || (assignedClientId ? !primaryEmail.trim() : (!selectedClientId && parseEmails(emailInput).length === 0))} disabled={loading || parseEmails(recipients).length === 0}
className="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium" className="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
type="button" type="button"
> >

View File

@@ -1,8 +1,8 @@
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { documents, clients } from '@/lib/db/schema'; import { documents } from '@/lib/db/schema';
import { eq, asc } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import Link from 'next/link'; import Link from 'next/link';
import { PdfViewerWrapper } from './_components/PdfViewerWrapper'; import { PdfViewerWrapper } from './_components/PdfViewerWrapper';
import { PreparePanel } from './_components/PreparePanel'; import { PreparePanel } from './_components/PreparePanel';
@@ -17,13 +17,10 @@ export default async function DocumentPage({
const { docId } = await params; const { docId } = await params;
const [doc, allClients] = await Promise.all([ const doc = await db.query.documents.findFirst({
db.query.documents.findFirst({
where: eq(documents.id, docId), where: eq(documents.id, docId),
with: { client: true }, with: { client: true },
}), });
db.select().from(clients).orderBy(asc(clients.name)),
]);
if (!doc) redirect('/portal/dashboard'); if (!doc) redirect('/portal/dashboard');
@@ -51,9 +48,8 @@ export default async function DocumentPage({
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<PreparePanel <PreparePanel
docId={docId} docId={docId}
clients={allClients} defaultEmail={doc.client?.email ?? ''}
assignedClientId={doc.assignedClientId ?? null} clientName={doc.client?.name ?? ''}
defaultClientId={doc.clientId}
currentStatus={doc.status} currentStatus={doc.status}
/> />
</div> </div>