/** * CaseTreatmentModal — the CASE-TREATMENT multi-select modal (Bekky, 2026-09-02). * * When a case has treatment steps due, the Care tab groups them into ONE row per * case (no chevron — just a tappable headline). Tapping it opens this modal for * that case listing its DUE steps. The user multi-selects which steps they did, * taps chips or types what they actually used per step, can SKIP a step, and * (on the 2nd+ completed treatment) marks whether it's helping. * * This records *what actually happened* + *what was used* → the magic-bullet * capture. Every complete/skip emits a StepExecution appended onto the case so * the AI can learn what the gardener actually did (and what they chose not to). * * Modeled on CohortSoilCheckModal: multi-select checklist, stays open, * strikethrough completed, "Done (N)" button. */ import React, { useEffect, useState } from 'react'; import { Modal, StyleSheet, Text, TextInput, TouchableOpacity, View, ScrollView, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { green, dark, line, brown, haptic, pressWithHaptic } from '../constants/theme'; import type { Investigation, StepExecution, TreatmentStep } from '../services/investigation'; /** EdgeModal-style wrapper so statusBarTranslucent/navigationBarTranslucent type-check. */ const EdgeModal = Modal as any; interface CaseTreatmentModalProps { visible: boolean; /** The case (must have a treatmentPlan). null = closed/rendering nothing. */ caseItem: Investigation | null; /** stepIds of steps due today — the modal only lists these. */ dueStepIds: string[]; onCancel: () => void; /** Called per step when the user taps its "Complete" / "Complete selected". */ onComplete: (exec: StepExecution) => void; /** Called per step when the user taps Skip. */ onSkip: (exec: StepExecution) => void; } type Outcome = 'better' | 'same' | 'worse'; export function CaseTreatmentModal({ visible, caseItem, dueStepIds, onCancel, onComplete, onSkip, }: CaseTreatmentModalProps) { const insets = useSafeAreaInsets(); // Multi-select: which due steps are currently SELECTED (will be completed). const [selected, setSelected] = useState>(new Set()); // Steps already COMPLETED this modal session (struck through, no longer tappable). const [done, setDone] = useState>(new Set()); // Steps already SKIPPED this modal session (struck through, no longer tappable). const [skipped, setSkipped] = useState>(new Set()); // Per-step "what I used" — set by a chip tap OR free text. const [usedProducts, setUsedProducts] = useState>({}); // Per-step outcome (Better/Same/Worse) — only for 2nd+ completed treatment. const [outcomes, setOutcomes] = useState>({}); // Reset all per-session state each time the modal opens (fresh open = fresh // batch — no stale selections or strikethroughs, mirrors CohortSoilCheckModal). useEffect(() => { if (visible) { setSelected(new Set()); setDone(new Set()); setSkipped(new Set()); setUsedProducts({}); setOutcomes({}); } }, [visible]); if (!visible || !caseItem || !caseItem.treatmentPlan) return null; const steps = caseItem.treatmentPlan.steps; // Only the DUE, not-yet-recorded treatment steps are listed. Skips too are // recorded, so a skipped step must not reappear as selectable. const dueSteps = steps.filter(st => dueStepIds.includes(st.stepId)); // Number of treatment steps ALREADY completed on the case (before this modal // session) — powers the "is it helping?" (2nd+ treatment) gate. const baseDoneCount = (caseItem.stepExecutions || []) .filter(e => e.status === 'done').length; const setUsed = (stepId: string, value: string) => setUsedProducts(prev => ({ ...prev, [stepId]: value })); const toggleSelect = (stepId: string) => setSelected(prev => { const next = new Set(prev); if (next.has(stepId)) next.delete(stepId); else next.add(stepId); return next; }); const nowIso = () => new Date().toISOString(); // Completes ALL currently-selected steps (one onComplete call each), then // strikes them through + becomes non-tappable (like CohortSoilCheckModal). const completeSelected = () => { const ids = [...selected]; if (!ids.length) return; for (const stepId of ids) { onComplete({ stepId, status: 'done', recordedAt: nowIso(), usedProduct: usedProducts[stepId]?.trim() ? usedProducts[stepId].trim() : undefined, outcome: outcomes[stepId], }); } setDone(prev => { const next = new Set(prev); for (const id of ids) next.add(id); return next; }); setSelected(new Set()); // Clear the just-completed steps' capture state so a future batch is clean. setUsedProducts(prev => { const next = { ...prev }; for (const id of ids) delete next[id]; return next; }); setOutcomes(prev => { const next = { ...prev }; for (const id of ids) delete next[id]; return next; }); }; // Skips a SINGLE step — reasons are the user's own (optional free-text note, // never required). Strikes it through with a distinct "skipped" visual. const skipStep = (step: TreatmentStep) => { const stepId = step.stepId; if (done.has(stepId) || skipped.has(stepId)) return; onSkip({ stepId, status: 'skipped', recordedAt: nowIso(), note: usedProducts[stepId]?.trim() ? usedProducts[stepId].trim() : undefined, }); setSkipped(prev => new Set(prev).add(stepId)); setSelected(prev => { const next = new Set(prev); next.delete(stepId); return next; }); }; // "Is it helping?" shows ONLY for the 2nd+ completed treatment: when there // was already a completed treatment on the case OR one completed earlier in // THIS modal session. Never on skips, never on the very first treatment. const showOutcomeFor = (stepId: string): boolean => done.has(stepId) || selected.has(stepId) ? (baseDoneCount + done.size) >= 1 : false; return ( 🩺 Treat {caseItem.title} Which treatment steps did you do? Select them, tell me what you used, and I'll log it on the case so we learn what works for {caseItem.title}. Due steps — tap to select {dueSteps.map(step => { const stepId = step.stepId; const isSelected = selected.has(stepId); const isDone = done.has(stepId); const isSkipped = skipped.has(stepId); const finished = isDone || isSkipped; return ( toggleSelect(stepId))} accessibilityRole="checkbox" accessibilityState={{ checked: isSelected, disabled: finished }} accessibilityLabel={`${step.action}${isSelected ? ' — selected' : ''}${isDone ? ' — done' : isSkipped ? ' — skipped' : ''}`} > {step.action} {isDone ? '✓' : isSelected ? '✓' : ''} {isSkipped ? ( skipped ) : null} ); })} {dueSteps.length === 0 ? ( No due treatment steps for this case right now. ) : null} {/* ── Per-step SKIP affordance (reasons are your own — optional) ── */} {dueSteps .filter(step => !done.has(step.stepId) && !skipped.has(step.stepId)) .map(step => ( skipStep(step))} accessibilityRole="button" accessibilityLabel={`Skip ${step.action}`} > Skip “{step.action}” ))} {/* ── Capture blocks for SELECTED steps ── */} {dueSteps .filter(step => selected.has(step.stepId)) .map(step => { const stepId = step.stepId; const chips = step.chips || []; const current = usedProducts[stepId] || ''; const showOutcome = showOutcomeFor(stepId); return ( What did you use for “{step.action}”? {chips.length > 0 ? ( {chips.map(chip => { const active = current === chip; return ( setUsed(stepId, active ? '' : chip))} accessibilityRole="button" accessibilityState={{ selected: active }} accessibilityLabel={`Use ${chip}`} > {chip} ); })} ) : null} setUsed(stepId, text)} multiline /> {showOutcome ? ( Is it helping? {(['better', 'same', 'worse'] as Outcome[]).map(opt => { const active = outcomes[stepId] === opt; return ( setOutcomes(prev => ({ ...prev, [stepId]: opt })))} accessibilityRole="button" accessibilityState={{ selected: active }} accessibilityLabel={`Is it helping? ${opt}`} > {opt === 'better' ? 'Better' : opt === 'same' ? 'Same' : 'Worse'} ); })} ) : null} ); })} {/* ── Action buttons ── */} {selected.size > 0 ? ( Complete selected ({selected.size}) ) : ( Tap a step to select it, then describe what you used. )} Cancel {done.size > 0 || skipped.size > 0 ? ( Done ({done.size + skipped.size}) ) : null} ); } const s = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(16, 42, 16, 0.55)', justifyContent: 'center', alignItems: 'center', paddingHorizontal: 20, }, card: { width: '100%', maxHeight: '82%', backgroundColor: '#FFFDF4', borderRadius: 20, borderWidth: 1, borderColor: line, padding: 20, shadowColor: '#000', shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 8, }, title: { fontSize: 19, fontWeight: '900', color: dark, textAlign: 'center', marginBottom: 14, }, guidanceBody: { backgroundColor: '#F3F7E6', borderRadius: 12, padding: 12, marginBottom: 14, }, guidanceText: { fontSize: 14, lineHeight: 20, color: '#2D4A2D', }, sectionLabel: { fontSize: 13, fontWeight: '700', color: '#8A8571', marginBottom: 8, }, stepList: { marginBottom: 14, }, stepRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, paddingHorizontal: 12, borderRadius: 10, borderWidth: 1, borderColor: line, marginBottom: 6, backgroundColor: '#FFFDF4', gap: 8, }, stepRowSelected: { borderColor: green, backgroundColor: '#F3F7E6', }, stepRowDone: { opacity: 0.55, backgroundColor: '#F0F0E8', }, stepName: { fontSize: 14, lineHeight: 19, color: dark, flex: 1, }, stepNameSelected: { fontWeight: '700', }, stepNameDone: { textDecorationLine: 'line-through', color: '#8A8571', }, stepCheck: { fontSize: 18, color: 'transparent', width: 22, textAlign: 'center', }, stepCheckOn: { color: green, }, skippedChip: { backgroundColor: '#E4E4DC', borderRadius: 8, paddingHorizontal: 8, paddingVertical: 3, }, skippedChipText: { color: '#8A8571', fontSize: 11, fontWeight: '800', }, skipButton: { marginTop: 2, marginBottom: 8, paddingVertical: 9, paddingHorizontal: 12, borderWidth: 1, borderColor: line, borderRadius: 10, backgroundColor: '#FFFDF4', alignItems: 'center', }, skipButtonText: { color: '#8A8571', fontSize: 13, fontWeight: '700', }, emptyText: { fontSize: 14, color: '#8A8571', textAlign: 'center', marginVertical: 10, }, captureBlock: { backgroundColor: '#FFF9EE', borderRadius: 12, borderWidth: 1, borderColor: line, padding: 12, marginBottom: 12, }, captureTitle: { fontSize: 13, fontWeight: '700', color: dark, marginBottom: 8, }, chipWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 8, }, chip: { borderRadius: 999, borderWidth: 1, borderColor: line, paddingHorizontal: 12, paddingVertical: 7, backgroundColor: '#FFFDF4', }, chipActive: { backgroundColor: green, borderColor: green, }, chipText: { fontSize: 13, fontWeight: '700', color: dark, }, chipTextActive: { color: '#FFF', }, textInput: { backgroundColor: '#FFFDF4', borderRadius: 10, borderWidth: 1, borderColor: line, paddingHorizontal: 10, paddingVertical: 8, fontSize: 14, color: dark, minHeight: 40, }, outcomeWrap: { marginTop: 10, }, outcomeLabel: { fontSize: 13, fontWeight: '700', color: brown, marginBottom: 6, }, outcomeRow: { flexDirection: 'row', gap: 8, }, outcomePill: { flex: 1, borderRadius: 999, borderWidth: 1, borderColor: line, paddingVertical: 8, alignItems: 'center', backgroundColor: '#FFFDF4', }, outcomePillActive: { backgroundColor: green, borderColor: green, }, outcomePillText: { fontSize: 13, fontWeight: '700', color: dark, }, outcomePillTextActive: { color: '#FFF', }, hint: { fontSize: 13, color: '#8A8571', textAlign: 'center', marginBottom: 6, }, completeButton: { backgroundColor: green, borderRadius: 12, paddingVertical: 14, alignItems: 'center', marginTop: 10, }, completeButtonText: { color: '#FFF', fontSize: 15, fontWeight: '700', }, cancelButton: { marginTop: 10, paddingVertical: 12, alignItems: 'center', }, cancelButtonText: { color: '#8A8571', fontSize: 14, fontWeight: '600', }, doneButton: { marginTop: 4, paddingVertical: 12, alignItems: 'center', backgroundColor: green, borderRadius: 12, }, doneButtonText: { color: '#FFF', fontSize: 15, fontWeight: '700', }, });