From d9f618f69ad4c723c12c3c74d26c2127a6ef255b Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 14:59:29 -0600 Subject: [PATCH] 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 --- .../[docId]/_components/FieldPlacer.tsx | 3 +- .../app/portal/(protected)/profile/page.tsx | 12 +- .../portal/_components/AgentInitialsPanel.tsx | 112 ++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx index 574e4e7..0999448 100644 --- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx +++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx @@ -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(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature']); + const validTypes = new Set(['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'; diff --git a/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx b/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx index ae319ea..d455d68 100644 --- a/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx +++ b/teressa-copeland-homes/src/app/portal/(protected)/profile/page.tsx @@ -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() { +
+
+

Agent Initials

+

+ Draw your initials once. They will be embedded in any "Agent Initials" fields when you prepare a document. +

+
+ +
); } diff --git a/teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx b/teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx new file mode 100644 index 0000000..84f64dd --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/_components/AgentInitialsPanel.tsx @@ -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(initialData); + const [isDrawing, setIsDrawing] = useState(!initialData); + const canvasRef = useRef(null); + const sigPadRef = useRef(null); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(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 ( +
+

Your saved initials:

+ {/* eslint-disable-next-line @next/next/no-img-element */} + Saved agent initials + +
+ ); + } + + return ( +
+ +
+ + + {savedData && ( + + )} +
+ {error &&

{error}

} +
+ ); +}