|
|
|
|
@@ -8,13 +8,51 @@ interface Client { id: string; name: string; email: string; }
|
|
|
|
|
interface PreparePanelProps {
|
|
|
|
|
docId: string;
|
|
|
|
|
clients: Client[];
|
|
|
|
|
currentClientId: string;
|
|
|
|
|
/** The client already explicitly assigned to this document (null if not yet assigned). */
|
|
|
|
|
assignedClientId: string | null;
|
|
|
|
|
/** The client the document belongs to — used as the default selection when no explicit assignedClientId. */
|
|
|
|
|
defaultClientId: string;
|
|
|
|
|
currentStatus: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PreparePanel({ docId, clients, currentClientId, currentStatus }: PreparePanelProps) {
|
|
|
|
|
/**
|
|
|
|
|
* Parse a comma/newline-separated string of email addresses into a trimmed, deduplicated array.
|
|
|
|
|
* Empty tokens are filtered out.
|
|
|
|
|
*/
|
|
|
|
|
function parseEmails(raw: string): string[] {
|
|
|
|
|
return raw
|
|
|
|
|
.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 {
|
|
|
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PreparePanel({
|
|
|
|
|
docId,
|
|
|
|
|
clients,
|
|
|
|
|
assignedClientId,
|
|
|
|
|
defaultClientId,
|
|
|
|
|
currentStatus,
|
|
|
|
|
}: PreparePanelProps) {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const [assignedClientId, setAssignedClientId] = useState(currentClientId);
|
|
|
|
|
|
|
|
|
|
// If the document already has an explicit assigned client, lock to that client.
|
|
|
|
|
// Otherwise default to the document owner (defaultClientId) but allow changing.
|
|
|
|
|
const isLocked = assignedClientId !== null;
|
|
|
|
|
|
|
|
|
|
// selectedClientId: id from the dropdown (empty string = "pick manually by email")
|
|
|
|
|
const [selectedClientId, setSelectedClientId] = useState<string>(
|
|
|
|
|
assignedClientId ?? defaultClientId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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 [loading, setLoading] = useState(false);
|
|
|
|
|
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
|
|
|
|
|
@@ -22,14 +60,55 @@ export function PreparePanel({ docId, clients, currentClientId, currentStatus }:
|
|
|
|
|
// Don't show the panel if already sent/signed
|
|
|
|
|
const canPrepare = currentStatus === 'Draft';
|
|
|
|
|
|
|
|
|
|
// The email addresses that will be sent with the prepare request.
|
|
|
|
|
// If a known client is selected, start with that client's email.
|
|
|
|
|
// Any manually-entered addresses are appended.
|
|
|
|
|
function buildEmailAddresses(): string[] {
|
|
|
|
|
const addresses: string[] = [];
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setResult(null);
|
|
|
|
|
|
|
|
|
|
const emailAddresses = buildEmailAddresses();
|
|
|
|
|
|
|
|
|
|
// Require at least one email address
|
|
|
|
|
if (emailAddresses.length === 0) {
|
|
|
|
|
setResult({ ok: false, message: 'Please select a client or enter at least one email address.' });
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate all addresses
|
|
|
|
|
const invalid = emailAddresses.filter((e) => !isValidEmail(e));
|
|
|
|
|
if (invalid.length > 0) {
|
|
|
|
|
setResult({ ok: false, message: `Invalid email address(es): ${invalid.join(', ')}` });
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/documents/${docId}/prepare`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ textFillData, assignedClientId }),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
textFillData,
|
|
|
|
|
assignedClientId: selectedClientId || null,
|
|
|
|
|
emailAddresses,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
|
|
|
@@ -57,18 +136,58 @@ export function PreparePanel({ docId, clients, currentClientId, currentStatus }:
|
|
|
|
|
<div className="rounded-lg border border-gray-200 p-4 space-y-4">
|
|
|
|
|
<h2 className="font-semibold text-gray-900">Prepare Document</h2>
|
|
|
|
|
|
|
|
|
|
{/* Client / recipient selection */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Assign to client</label>
|
|
|
|
|
<select
|
|
|
|
|
value={assignedClientId}
|
|
|
|
|
onChange={e => setAssignedClientId(e.target.value)}
|
|
|
|
|
className="w-full border rounded px-2 py-1.5 text-sm"
|
|
|
|
|
>
|
|
|
|
|
<option value="">— Select client —</option>
|
|
|
|
|
{clients.map(c => (
|
|
|
|
|
<option key={c.id} value={c.id}>{c.name} ({c.email})</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
|
|
{isLocked ? 'Assigned client (locked)' : 'Assign to client'}
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
{isLocked ? (
|
|
|
|
|
// Document already has an assigned client — show read-only info
|
|
|
|
|
<div className="w-full border rounded px-2 py-1.5 text-sm bg-gray-50 text-gray-600">
|
|
|
|
|
{(() => {
|
|
|
|
|
const c = clients.find((c) => c.id === assignedClientId);
|
|
|
|
|
return c ? `${c.name} (${c.email})` : assignedClientId;
|
|
|
|
|
})()}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
// No explicit assignment yet — allow choosing from list or entering email manually
|
|
|
|
|
<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">
|
|
|
|
|
{isLocked
|
|
|
|
|
? 'Additional email addresses (optional, comma-separated)'
|
|
|
|
|
: selectedClientId
|
|
|
|
|
? 'Additional email addresses (optional, comma-separated)'
|
|
|
|
|
: 'Email address(es) — required if no client selected above'}
|
|
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={emailInput}
|
|
|
|
|
onChange={(e) => setEmailInput(e.target.value)}
|
|
|
|
|
placeholder={
|
|
|
|
|
isLocked || selectedClientId
|
|
|
|
|
? 'e.g. cc@example.com, another@example.com'
|
|
|
|
|
: 'Enter recipient email, e.g. client@example.com'
|
|
|
|
|
}
|
|
|
|
|
rows={2}
|
|
|
|
|
className="w-full border rounded px-2 py-1.5 text-sm resize-none"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-gray-400 mt-0.5">
|
|
|
|
|
Separate multiple addresses with commas or new lines.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
@@ -78,7 +197,7 @@ export function PreparePanel({ docId, clients, currentClientId, currentStatus }:
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={handlePrepare}
|
|
|
|
|
disabled={loading || !assignedClientId}
|
|
|
|
|
disabled={loading || (!selectedClientId && parseEmails(emailInput).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"
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
|
|