feat(11.1-01): AgentInitialsPanel component, profile page section, FieldPlacer token

- Create AgentInitialsPanel.tsx (clone of AgentSignaturePanel with 80px canvas, /api/agent/initials endpoint)
- Update profile/page.tsx to fetch agentInitialsData and render AgentInitialsPanel below signature section
- Add orange 'Agent Initials' token (7th entry) to FieldPlacer PALETTE_TOKENS
- Add 'agent-initials' to FieldPlacer validTypes Set
This commit is contained in:
Chandler Copeland
2026-03-21 14:59:29 -06:00
parent 33f499c61b
commit d9f618f69a
3 changed files with 125 additions and 2 deletions

View File

@@ -74,6 +74,7 @@ const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: stri
{ id: 'date', label: 'Date', color: '#d97706' }, // amber { id: 'date', label: 'Date', color: '#d97706' }, // amber
{ id: 'text', label: 'Text', color: '#64748b' }, // slate { id: 'text', label: 'Text', color: '#64748b' }, // slate
{ id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' }, // red { id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' }, // red
{ id: 'agent-initials', label: 'Agent Initials', color: '#ea580c' }, // orange
]; ];
// Draggable token in the palette // Draggable token in the palette
@@ -256,7 +257,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
const rawY = ghostRect.top - refRect.top; const rawY = ghostRect.top - refRect.top;
// Determine the field type from the dnd-kit active.id (token id IS the SignatureFieldType) // Determine the field type from the dnd-kit active.id (token id IS the SignatureFieldType)
const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature']); const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature', 'agent-initials']);
const droppedType: SignatureFieldType = validTypes.has(active.id as string) const droppedType: SignatureFieldType = validTypes.has(active.id as string)
? (active.id as SignatureFieldType) ? (active.id as SignatureFieldType)
: 'client-signature'; : 'client-signature';

View File

@@ -4,6 +4,7 @@ import { db } from '@/lib/db';
import { users } from '@/lib/db/schema'; import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { AgentSignaturePanel } from '../../_components/AgentSignaturePanel'; import { AgentSignaturePanel } from '../../_components/AgentSignaturePanel';
import { AgentInitialsPanel } from '../../_components/AgentInitialsPanel';
export default async function ProfilePage() { export default async function ProfilePage() {
const session = await auth(); const session = await auth();
@@ -11,7 +12,7 @@ export default async function ProfilePage() {
const user = await db.query.users.findFirst({ const user = await db.query.users.findFirst({
where: eq(users.id, session.user.id), where: eq(users.id, session.user.id),
columns: { agentSignatureData: true }, columns: { agentSignatureData: true, agentInitialsData: true },
}); });
return ( return (
@@ -26,6 +27,15 @@ export default async function ProfilePage() {
</div> </div>
<AgentSignaturePanel initialData={user?.agentSignatureData ?? null} /> <AgentSignaturePanel initialData={user?.agentSignatureData ?? null} />
</section> </section>
<section className="bg-white border border-gray-200 rounded-lg p-6 space-y-4">
<div>
<h2 className="text-lg font-medium text-gray-900">Agent Initials</h2>
<p className="text-sm text-gray-500 mt-1">
Draw your initials once. They will be embedded in any &quot;Agent Initials&quot; fields when you prepare a document.
</p>
</div>
<AgentInitialsPanel initialData={user?.agentInitialsData ?? null} />
</section>
</div> </div>
); );
} }

View File

@@ -0,0 +1,112 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import SignaturePad from 'signature_pad';
interface AgentInitialsPanelProps {
initialData: string | null;
}
export function AgentInitialsPanel({ initialData }: AgentInitialsPanelProps) {
const [savedData, setSavedData] = useState<string | null>(initialData);
const [isDrawing, setIsDrawing] = useState(!initialData);
const canvasRef = useRef<HTMLCanvasElement>(null);
const sigPadRef = useRef<SignaturePad | null>(null);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!isDrawing || !canvasRef.current) return;
const canvas = canvasRef.current;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext('2d')?.scale(ratio, ratio);
sigPadRef.current = new SignaturePad(canvas, {
backgroundColor: 'rgba(0,0,0,0)',
penColor: '#1B2B4B',
});
return () => sigPadRef.current?.off();
}, [isDrawing]);
async function handleSave() {
if (!sigPadRef.current || sigPadRef.current.isEmpty()) {
setError('Please draw your initials first');
return;
}
const dataURL = sigPadRef.current.toDataURL('image/png');
setSaving(true);
setError(null);
try {
const res = await fetch('/api/agent/initials', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dataURL }),
});
if (res.ok) {
setSavedData(dataURL);
setIsDrawing(false);
} else {
const data = await res.json().catch(() => ({ error: 'Save failed' }));
setError(data.error ?? 'Save failed');
}
} catch {
setError('Network error — please try again');
} finally {
setSaving(false);
}
}
if (!isDrawing && savedData) {
return (
<div className="space-y-4">
<p className="text-sm text-gray-600">Your saved initials:</p>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={savedData}
alt="Saved agent initials"
className="max-h-16 border border-gray-200 rounded bg-white p-2"
/>
<button
onClick={() => { setIsDrawing(true); setError(null); }}
className="px-4 py-2 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
>
Update Initials
</button>
</div>
);
}
return (
<div className="space-y-3">
<canvas
ref={canvasRef}
className="w-full border border-gray-300 rounded bg-white"
style={{ height: '80px', touchAction: 'none', display: 'block' }}
/>
<div className="flex gap-2">
<button
onClick={() => sigPadRef.current?.clear()}
className="px-3 py-2 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
>
Clear
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{saving ? 'Saving...' : (savedData ? 'Save Updated Initials' : 'Save Initials')}
</button>
{savedData && (
<button
onClick={() => { setIsDrawing(false); setError(null); }}
className="px-3 py-2 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50"
>
Cancel
</button>
)}
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);
}