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:
@@ -74,6 +74,7 @@ const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: stri
|
||||
{ id: 'date', label: 'Date', color: '#d97706' }, // amber
|
||||
{ id: 'text', label: 'Text', color: '#64748b' }, // slate
|
||||
{ id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' }, // red
|
||||
{ id: 'agent-initials', label: 'Agent Initials', color: '#ea580c' }, // orange
|
||||
];
|
||||
|
||||
// Draggable token in the palette
|
||||
@@ -256,7 +257,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
const rawY = ghostRect.top - refRect.top;
|
||||
|
||||
// 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)
|
||||
? (active.id as SignatureFieldType)
|
||||
: 'client-signature';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { db } from '@/lib/db';
|
||||
import { users } from '@/lib/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { AgentSignaturePanel } from '../../_components/AgentSignaturePanel';
|
||||
import { AgentInitialsPanel } from '../../_components/AgentInitialsPanel';
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const session = await auth();
|
||||
@@ -11,7 +12,7 @@ export default async function ProfilePage() {
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, session.user.id),
|
||||
columns: { agentSignatureData: true },
|
||||
columns: { agentSignatureData: true, agentInitialsData: true },
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -26,6 +27,15 @@ export default async function ProfilePage() {
|
||||
</div>
|
||||
<AgentSignaturePanel initialData={user?.agentSignatureData ?? null} />
|
||||
</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 "Agent Initials" fields when you prepare a document.
|
||||
</p>
|
||||
</div>
|
||||
<AgentInitialsPanel initialData={user?.agentInitialsData ?? null} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user