diff --git a/apps/azure/src/components/ProcessHubReviewPanel.tsx b/apps/azure/src/components/ProcessHubReviewPanel.tsx index e876b79c5..ff728744b 100644 --- a/apps/azure/src/components/ProcessHubReviewPanel.tsx +++ b/apps/azure/src/components/ProcessHubReviewPanel.tsx @@ -10,6 +10,7 @@ import type { ProcessHubInvestigation, ProcessHubRollup, ProcessStateItem, + ProcessStateNote, ResponsePathAction, } from '@variscout/core'; import { ProcessHubCurrentStatePanel } from '@variscout/ui'; @@ -26,6 +27,11 @@ interface ProcessHubReviewPanelProps { onLogReview: (recordId: string) => void; onRecordHandoff: (investigationId: string) => void; onResponsePathAction: (item: ProcessStateItem, action: ResponsePathAction, hubId: string) => void; + /** Notes wiring */ + onRequestAddNote: (item: ProcessStateItem, hubId: string) => void; + onRequestEditNote: (item: ProcessStateItem, note: ProcessStateNote, hubId: string) => void; + onDeleteNote: (item: ProcessStateItem, noteId: string, hubId: string) => void; + currentUserId: string; } const SnapshotCard: React.FC<{ @@ -55,6 +61,10 @@ const ProcessHubReviewPanel: React.FC = ({ onLogReview, onRecordHandoff, onResponsePathAction, + onRequestAddNote, + onRequestEditNote, + onDeleteNote, + currentUserId, }) => { const cadence = buildProcessHubCadence(rollup); const currentState = buildCurrentProcessState(rollup, cadence); @@ -126,6 +136,29 @@ const ProcessHubReviewPanel: React.FC = ({ [rollup.investigations, investigationIdResolver] ); + // Aggregate stateNotes from all linked investigations for an item. + // Per-investigation items use item.investigationIds; aggregate items pull + // from all hub investigations. + const notesFor = React.useCallback( + (item: ProcessStateItem): readonly ProcessStateNote[] => { + const investigationIds = + item.investigationIds && item.investigationIds.length > 0 + ? item.investigationIds + : rollup.investigations.map(inv => inv.id); + const all: ProcessStateNote[] = []; + for (const invId of investigationIds) { + const inv = rollup.investigations.find(i => i.id === invId); + const notes = inv?.metadata?.stateNotes ?? []; + for (const note of notes) { + if (note.itemId === item.id) all.push(note); + } + } + // Sort by createdAt asc so older notes appear first + return all.sort((a, b) => a.createdAt.localeCompare(b.createdAt)); + }, + [rollup.investigations] + ); + const handleChipClick = React.useCallback( (item: ProcessStateItem, findings: readonly Finding[]) => { safeTrackEvent('process_hub.evidence_chip_click', { @@ -183,6 +216,13 @@ const ProcessHubReviewPanel: React.FC = ({ onInvoke: (item, action) => onResponsePathAction(item, action, rollup.hub.id), }} evidence={{ findingsFor, onChipClick: handleChipClick }} + notes={{ + notesFor, + onRequestAddNote: item => onRequestAddNote(item, rollup.hub.id), + onRequestEditNote: (item, note) => onRequestEditNote(item, note, rollup.hub.id), + onDeleteNote: (item, noteId) => onDeleteNote(item, noteId, rollup.hub.id), + currentUserId, + }} />
diff --git a/apps/azure/src/components/StateItemNotesDrawer.tsx b/apps/azure/src/components/StateItemNotesDrawer.tsx new file mode 100644 index 000000000..16c1ae926 --- /dev/null +++ b/apps/azure/src/components/StateItemNotesDrawer.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { PROCESS_STATE_NOTE_KINDS, type ProcessStateNoteKind } from '@variscout/core'; + +const KIND_LABELS: Record = { + question: 'Question', + gemba: 'Gemba', + 'data-gap': 'Data Gap', + decision: 'Decision', +}; + +export interface StateItemNotesDrawerProps { + open: boolean; + initialKind: ProcessStateNoteKind; + initialText: string; + onSave: (kind: ProcessStateNoteKind, text: string) => void; + onCancel: () => void; + /** When true, save is disabled (e.g. during in-flight persistence). */ + disabled?: boolean; +} + +const StateItemNotesDrawer: React.FC = ({ + open, + initialKind, + initialText, + onSave, + onCancel, + disabled = false, +}) => { + const [kind, setKind] = React.useState(initialKind); + const [text, setText] = React.useState(initialText); + + // Reset internal state when drawer is reopened with new initial values + React.useEffect(() => { + if (open) { + setKind(initialKind); + setText(initialText); + } + }, [open, initialKind, initialText]); + + if (!open) return null; + + const trimmed = text.trim(); + const canSave = trimmed.length > 0; + + return ( +
+
+ {PROCESS_STATE_NOTE_KINDS.map(k => ( + + ))} +
+