/** * CohortSoilCheckModal — the SOIL-CHECK modal (Bekky, 2026-09-01, Chunk 2). * * When the weather keeps a cohort's soil from drying (extreme rain / * overwatering risk / frost / cold-damp), the cohort's quick buttons are * replaced by this modal. It's a MULTI-SELECT check-in: * - The cohort's plant list is at the top. Tap a name → a ✓ appears * (selected). Tap more → more get ✓. Tap again → deselect. * - One slider ("how wet is it") + Complete all / Postpone applies to ALL * selected plants at once. * - Select just one → it's effectively a single-plant check-in, same modal. * * The user's honest "really wet" answer feeds the existing feedback loop so * the app learns not to overwater. This is the "check the soil before * continuing" surface — the app can't feel the soil, so it asks the user to. */ import React, { useEffect, useState } from 'react'; import { Modal, StyleSheet, Text, TouchableOpacity, View, ScrollView, Image } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { green, dark, line, haptic, pressWithHaptic } from '../constants/theme'; import { CareSlider } from './CareSlider'; /** EdgeModal-style wrapper so statusBarTranslucent/navigationBarTranslucent type-check. */ const EdgeModal = Modal as any; export type CohortSoilCheckPlant = { id: string; name: string; /** Optional photo URI (Bekky, 2026-09-02): a tiny tappable thumbnail on the * plant's pill so you can confirm exactly which plant you're checking. Tap it * to enlarge — removes all doubt of which plant it is. */ photo?: string; }; interface CohortSoilCheckModalProps { visible: boolean; action: 'water' | 'fertilize'; plants: CohortSoilCheckPlant[]; onCancel: () => void; /** Complete the SELECTED plants with the slider state. */ onComplete: (plantIds: string[], stateAtComplete: string) => void; /** Postpone the SELECTED plants by N days. */ onPostpone: (plantIds: string[], stateAtComplete: string, postponeDays: number) => void; } export function CohortSoilCheckModal({ visible, action, plants, onCancel, onComplete, onPostpone, }: CohortSoilCheckModalProps) { const insets = useSafeAreaInsets(); const [selected, setSelected] = useState>(new Set()); const [done, setDone] = useState>(new Set()); const [sliderIndex, setSliderIndex] = useState(-1); // -1 = nothing selected yet const [confirmation, setConfirmation] = useState<{ message: string; stateAtComplete: string; postponeDays: number | null } | null>(null); // Enlarged photo viewer (Bekky, 2026-09-02): tap a plant's thumb to enlarge it // and clear all doubt of which plant it is. null = viewer closed. const [enlargedPhoto, setEnlargedPhoto] = useState(null); // Reset the batch state each time the modal opens (Bekky, 2026-09-01): a // fresh open starts a fresh batch — no stale selections or strikethroughs. useEffect(() => { if (visible) { setSelected(new Set()); setDone(new Set()); setSliderIndex(-1); setConfirmation(null); setEnlargedPhoto(null); } }, [visible]); if (!visible) return null; const isWater = action === 'water'; const waterLevels = ['Very dry', 'Slightly dry', 'Barely damp', 'Damp', 'Moist', 'Really wet']; const fertLevels = ['Dormant', 'Slow growth', 'Actively growing', 'Explosive growth']; const levels = isWater ? waterLevels : fertLevels; const waterColors = ['#D6E8F5', '#B7D6EC', '#93C0E0', '#6FA8D3', '#4A8FC4', '#2E7DB8']; const fertColors = ['#E8F5D3', '#CBE3B0', '#A8D18A', '#7FBE63']; const colors = isWater ? waterColors : fertColors; const touched = sliderIndex >= 0; const stateLabel = touched ? levels[sliderIndex] : ''; const selectedIds = [...selected]; const selectedNames = selectedIds .map(id => plants.find(p => p.id === id)?.name) .filter(Boolean) .join(', '); const togglePlant = (id: string) => { setSelected(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; // Intelligent postpone radius (same as CareCheckInModal). const postponeDaysFor = (idx: number): number => { if (isWater) return Math.max(0, idx - 1); return idx <= 1 ? (idx === 0 ? 7 : 4) : 0; }; const submitComplete = () => { if (!touched || selectedIds.length === 0) return; setConfirmation({ message: isWater ? `Watered ${selectedNames} — I've noted it was ${stateLabel.toLowerCase()}.` : `Fertilized ${selectedNames} — I've noted it's ${stateLabel.toLowerCase()}.`, stateAtComplete: stateLabel, postponeDays: null, }); }; const submitPostpone = () => { if (!touched || selectedIds.length === 0) return; const days = postponeDaysFor(sliderIndex); if (days <= 0) return; setConfirmation({ message: `Got it — I'll check back in ${days} day${days === 1 ? '' : 's'}.`, stateAtComplete: stateLabel, postponeDays: days, }); }; const confirmAndClose = () => { if (!confirmation) return; if (confirmation.postponeDays === null) { onComplete(selectedIds, confirmation.stateAtComplete); } else { onPostpone(selectedIds, confirmation.stateAtComplete, confirmation.postponeDays); } // BATCH COMPLETION (Bekky, 2026-09-01): keep the modal OPEN so the user can // finish the whole cohort in one session. Mark the completed plants as done // (strikethrough), then reset the selection + slider for the next batch. setDone(prev => { const next = new Set(prev); for (const id of selectedIds) next.add(id); return next; }); setSelected(new Set()); setSliderIndex(-1); setConfirmation(null); }; const offerPostpone = touched && (isWater ? sliderIndex >= 2 : sliderIndex <= 1); const offerComplete = touched && (isWater ? sliderIndex <= 1 : sliderIndex >= 2); return ( <> {confirmation ? ( /* ── Confirmation state — the gentle closing loop ── */ Noted {confirmation.message} {isWater ? "I've recorded this and will fine-tune your watering rhythm." : "I'm fine-tuning your care guidance — check back soon for the updated plan."} OK ) : ( <> {isWater ? '💧 Check the soil' : '🌱 Check the growth'} {/* The soil-can't-dry note (Bekky, 2026-09-01). */} The soil may be too wet to water — please check the soil before continuing. {/* Multi-select plant list. */} Which plants are you checking? {plants.map(p => { const isSel = selected.has(p.id); const isDone = done.has(p.id); return ( togglePlant(p.id))} accessibilityRole="checkbox" accessibilityState={{ checked: isSel, disabled: isDone }} accessibilityLabel={`${p.name}${isSel ? ' — selected' : ''}${isDone ? ' — done' : ''}`} > {/* Tiny tappable plant photo (Bekky, 2026-09-02): confirms which plant you're checking; tap to enlarge. */} {p.photo ? ( setEnlargedPhoto(p.photo!), haptic.light)} accessibilityRole="imagebutton" accessibilityLabel={`Enlarge ${p.name} photo`} > ) : null} {p.name} {isDone ? '✓' : isSel ? '✓' : ''} ); })} {/* The state slider — the user's observable signal. */} {isWater ? 'How damp is the soil?' : "How's it growing?"} {touched ? ( {isWater ? `Noted: ${stateLabel.toLowerCase()}. I'll adjust your watering rhythm to reflect this.` : `Noted: ${stateLabel.toLowerCase()}. I'll fine-tune your feeding guidance to reflect this.`} ) : null} {/* Action buttons — appear once a plant is selected + a level is tapped. */} {touched && selectedIds.length > 0 ? ( {offerPostpone ? ( Postpone ) : null} {offerComplete ? ( {isWater ? 'Complete watering' : 'Fertilize today'} ({selectedIds.length}) ) : null} ) : ( {selectedIds.length === 0 ? 'Tap a plant to select it, then check the soil.' : "Select how it's doing."} )} Cancel {done.size > 0 ? ( Done ({done.size}) ) : null} )} {/* Enlarged plant photo viewer (Bekky, 2026-09-02): tap a tiny thumb on the pill to see the plant up close and remove all doubt of which one it is. */} setEnlargedPhoto(null)} statusBarTranslucent navigationBarTranslucent hardwareAccelerated > setEnlargedPhoto(null), haptic.light)} /> {enlargedPhoto ? : null} setEnlargedPhoto(null), haptic.light)} accessibilityRole="button" accessibilityLabel="Close enlarged photo"> Close ); } 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, }, plantList: { marginBottom: 14, }, plantRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, paddingHorizontal: 12, borderRadius: 10, borderWidth: 1, borderColor: line, marginBottom: 6, backgroundColor: '#FFFDF4', }, plantRowSelected: { borderColor: green, backgroundColor: '#F3F7E6', }, plantRowDone: { opacity: 0.55, backgroundColor: '#F0F0E8', }, plantName: { fontSize: 16, color: dark, flex: 1, }, plantNameSelected: { fontWeight: '700', }, plantNameDone: { textDecorationLine: 'line-through', color: '#8A8571', }, plantCheck: { fontSize: 18, color: 'transparent', width: 22, textAlign: 'center', }, plantThumbTouch: { marginRight: 10, borderRadius: 8, overflow: 'hidden', }, plantThumb: { width: 40, height: 40, borderRadius: 8, }, plantCheckOn: { color: green, }, plantCheckDone: { color: '#8A8571', }, tuningNote: { backgroundColor: '#F3F7E6', borderRadius: 12, padding: 12, marginTop: 12, marginBottom: 4, }, tuningNoteText: { fontSize: 13, lineHeight: 18, color: '#2D4A2D', }, actionRow: { flexDirection: 'row', gap: 10, marginTop: 14, }, postponeButton: { flex: 1, backgroundColor: '#FFFDF4', borderWidth: 1, borderColor: line, borderRadius: 12, paddingVertical: 14, alignItems: 'center', }, postponeButtonText: { color: dark, fontSize: 15, fontWeight: '700', }, completeButton: { flex: 1, backgroundColor: green, borderRadius: 12, paddingVertical: 14, alignItems: 'center', }, completeButtonText: { color: '#FFF', fontSize: 15, fontWeight: '700', }, sliderHint: { fontSize: 13, color: '#8A8571', textAlign: 'center', marginTop: 14, }, 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', }, confirmWrap: { alignItems: 'center', paddingVertical: 8, }, confirmCheck: { fontSize: 40, color: green, marginBottom: 8, }, confirmTitle: { fontSize: 20, fontWeight: '900', color: dark, marginBottom: 8, }, confirmMessage: { fontSize: 15, color: '#2D4A2D', textAlign: 'center', marginBottom: 8, }, confirmHint: { fontSize: 13, color: '#8A8571', textAlign: 'center', marginBottom: 16, }, confirmButton: { alignSelf: 'stretch', backgroundColor: green, borderRadius: 12, paddingVertical: 14, alignItems: 'center', }, confirmButtonText: { color: '#FFF', fontSize: 15, fontWeight: '700', }, enlargeOverlay: { flex: 1, backgroundColor: 'rgba(10, 20, 10, 0.9)', justifyContent: 'center', alignItems: 'center', padding: 24, }, enlargeScrim: { ...StyleSheet.absoluteFillObject, }, enlargeImg: { width: '100%', height: '75%', borderRadius: 16, }, enlargeClose: { marginTop: 20, minHeight: 44, paddingHorizontal: 28, borderRadius: 999, backgroundColor: 'rgba(255,255,255,0.16)', alignItems: 'center', justifyContent: 'center', }, enlargeCloseText: { color: '#FFF', fontSize: 15, fontWeight: '700', }, });