From d768fc6aaeae58dac8f0c10628d0442c879e0ab4 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Fri, 3 Apr 2026 16:25:39 -0600 Subject: [PATCH] feat(16-03): active signer selector, per-signer field coloring, unassigned field red highlight --- .../[docId]/_components/FieldPlacer.tsx | 93 +++++++++++++++++-- 1 file changed, 85 insertions(+), 8 deletions(-) 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 89d4172..06349b2 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 @@ -171,6 +171,7 @@ interface FieldPlacerProps { export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange, aiPlacementKey = 0, signers = [], unassignedFieldIds = new Set() }: FieldPlacerProps) { const [fields, setFields] = useState([]); const [isDraggingToken, setIsDraggingToken] = useState(null); + const [activeSignerEmail, setActiveSignerEmail] = useState(null); const containerRef = useRef(null); // Track rendered canvas dimensions in state so renderFields re-runs when they change const [containerSize, setContainerSize] = useState<{ w: number; h: number } | null>(null); @@ -194,6 +195,18 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = useEffect(() => { containerSizeRef.current = containerSize; }, [containerSize]); useEffect(() => { canvasOffsetRef.current = canvasOffset; }, [canvasOffset]); + // Default active signer to first signer on load; keep current if still valid (D-08) + useEffect(() => { + if (signers && signers.length > 0) { + setActiveSignerEmail(prev => { + if (prev && signers.some(s => s.email === prev)) return prev; + return signers[0].email; + }); + } else { + setActiveSignerEmail(null); + } + }, [signers]); + // Configure sensors: require a minimum drag distance so clicks on delete buttons // are not intercepted as drag starts const sensors = useSensors( @@ -301,6 +314,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = width: fieldW, height: fieldH, type: droppedType, + ...(activeSignerEmail ? { signerEmail: activeSignerEmail } : {}), }; const next = [...fields, newField]; @@ -308,7 +322,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = persistFields(docId, next); onFieldsChanged?.(); }, - [fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged], + [fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged, activeSignerEmail], ); // --- Move / Resize pointer handlers (event delegation on DroppableZone) --- @@ -583,7 +597,18 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = // Per-type color and label const fieldType = getFieldType(field); const tokenMeta = PALETTE_TOKENS.find((t) => t.id === fieldType); - const fieldColor = tokenMeta?.color ?? '#2563eb'; + + // Color override: signer color when signerEmail is set (D-06), else type color (D-07) + let fieldColor = tokenMeta?.color ?? '#2563eb'; + if (field.signerEmail && signers) { + const matchedSigner = signers.find(s => s.email === field.signerEmail); + if (matchedSigner) { + fieldColor = matchedSigner.color; + } + } + + // Validation overlay: unassigned field highlight (D-09/D-10) + const isUnassigned = unassignedFieldIds?.has(field.id) ?? false; const fieldLabel = tokenMeta?.label ?? 'Signature'; // Resize handle style factory @@ -632,8 +657,8 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = top: top - heightPx + canvasOffset.y, // top is y of bottom-left corner; shift up by height width: widthPx, height: heightPx, - border: `2px solid ${fieldColor}`, - background: readOnly ? `${fieldColor}0d` : `${fieldColor}1a`, + border: isUnassigned ? '2px solid #ef4444' : `2px solid ${fieldColor}`, + background: isUnassigned ? '#ef444414' : (readOnly ? `${fieldColor}0d` : `${fieldColor}1a`), borderRadius: '2px', display: 'flex', alignItems: 'center', @@ -754,6 +779,53 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = onDragStart={(event) => setIsDraggingToken(event.active.id as string)} onDragEnd={handleDragEnd} > + {/* Active signer selector — only when signers configured and not read-only (D-05) */} + {!readOnly && signers && signers.length > 0 && ( +
+ + Active signer: + + + {/* Color indicator dot next to the dropdown */} + {(() => { + const activeSigner = signers.find(s => s.email === activeSignerEmail); + return activeSigner ? ( + + ) : null; + })()} +
+ )} + {/* Palette — hidden in read-only mode */} {!readOnly && (
{ const tokenMeta = PALETTE_TOKENS.find((t) => t.id === isDraggingToken); const label = tokenMeta?.label ?? 'Field'; - const color = tokenMeta?.color ?? '#2563eb'; + // Ghost uses active signer color if one is selected, else type color (D-06) + let ghostColor = tokenMeta?.color ?? '#2563eb'; + if (activeSignerEmail && signers) { + const activeSigner = signers.find(s => s.email === activeSignerEmail); + if (activeSigner) ghostColor = activeSigner.color; + } const isCheckbox = isDraggingToken === 'checkbox'; return (