/** * CareCheckInModal — the CARE CHECK-IN modal (Bekky, 2026-08-20). * * Tapping a schedule care task (water/fertilize) opens this instead of * completing in place. It serves BOTH user types: * 1. Fast lane (experts): the bottom "Already did it" button completes the * task in one tap — line-through + undo banner, no questions. * 2. Learning lane (beginners): the per-plant guidance link (readiness.guidance) * teaches HOW to tell if this specific plant is ready, and the postpone * chips (readiness.chips for the plant's location bucket) let them say * "not yet" — which closes the task, adjusts the cadence, and reschedules * the reminder. A custom write-in covers any other reason. * * Every choice feeds back so the AI can regenerate care (never a blind counter). * The chips are STATIC + cached on the plant (not regenerated per tap). */ import React, { 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, haptic, pressWithHaptic } from '../constants/theme'; import type { CareTaskItem } from '../types/appTypes'; import { CareSlider } from './CareSlider'; import { canCompleteCareTaskEarly } from './CareComponents'; /** EdgeModal-style wrapper so statusBarTranslucent/navigationBarTranslucent type-check. */ const EdgeModal = Modal as any; /** A single postpone chip (a "why I didn't need to do it yet" reason). */ export type CareCheckInChip = { /** The short reason text the user sees (e.g. "Soil still moist below the surface"). */ label: string; /** How many days to push the next reminder out by (cadence adjustment). */ postponeDays: number; }; /** Resolved readiness kit for a specific task, passed down from App. */ export type CareCheckInData = { /** Per-plant guidance (the ONLY bespoke part). */ guidanceWater?: string; guidanceFertilize?: string; /** The postpone chips for the plant's location bucket (water). */ waterChips: CareCheckInChip[]; /** The postpone chips for the plant's location bucket (fertilize). */ fertilizeChips: CareCheckInChip[]; /** Fallback cadence (days) to nudge when a chip has no explicit postponeDays. */ defaultWaterDays: number; defaultFertilizeDays: number; /** The plant's stored fertilizer (setup canonical value) — shown in the fertilize * complete capture so the modal can ask "did you use this?" (Bekky, 2026-09-02). */ fertilizeFertilizer?: string; }; interface CareCheckInModalProps { visible: boolean; task: CareTaskItem | null; data: CareCheckInData | null; onCancel: () => void; /** Complete the task (the "I watered it now" / "Fertilize today" path). */ onComplete: (stateAtComplete: string) => void; /** Fertilizer recorded at completion (Bekky, 2026-09-02): when the fertilize * capture captures/confirms what was used, it's passed up so App can store it * on the plant's setup (canonical). Optional — water tasks don't pass it. */ onFertilizerRecorded?: (fertilizer: string | undefined) => void; /** Postpone the task by N days (intelligent radius). */ onPostpone: (stateAtComplete: string, postponeDays: number) => void; /** Skip the task — ALWAYS available, even with no slider state (Bekky, * 2026-08-22): "not home, plant's fine, don't want to think about it." * Skipping never needs the slider. */ onSkip?: () => void; } export function CareCheckInModal({ visible, task, data, onCancel, onComplete, onFertilizerRecorded, onPostpone, onSkip, }: CareCheckInModalProps) { const insets = useSafeAreaInsets(); const [sliderIndex, setSliderIndex] = useState(-1); // -1 = nothing selected yet; buttons appear once the user taps a level // Fertilizer capture (Bekky, 2026-09-02): for fertilize tasks, record what was // actually used. 'same' = used the stored value; 'other' = used something else // (a free-text input appears). Only shown when a stored fertilizer exists; if // none, a plain optional free-text input is offered. const [fertChoice, setFertChoice] = useState<'same' | 'other' | null>(null); const [fertOther, setFertOther] = useState(''); // Gentle closing loop (Bekky, 2026-08-20): after choosing, show a brief // confirmation so the user can register what happened before the modal closes. const [confirmation, setConfirmation] = useState<{ message: string; stateAtComplete: string; postponeDays: number | null } | null>(null); if (!visible || !task) return null; const isWater = task.type === 'Water'; const plantName = task.plantName; const guidance = isWater ? data?.guidanceWater : data?.guidanceFertilize; // The slider levels + the intelligent postpone radius (rule-based, no AI call). 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; // BLOCKED gradient (Bekky, 2026-08-21): each level is a distinct shade, not a // smooth blend — makes each point on the scale visually clear. Water = blue, // fertilize = green, light → dark left → right. 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] : ''; // Intelligent postpone radius (Bekky, 2026-08-21): rule-based, no AI call. // Water: the wetter it is, the further out we push. Fertilize: the more // dormant, the further out. const postponeDaysFor = (idx: number): number => { if (isWater) { // Very dry(0) → 0 (should complete, not postpone); Slightly dry(1) → 1; // Barely damp(2) → 2; Damp(3) → 3; Moist(4) → 4; Really wet(5) → 5. return Math.max(0, idx - 1); } // Fertilize: Dormant(0) → 7; Slow(1) → 4; Active(2) → 0 (fertilize today); // Pushing(3) → 0 (fertilize today). return idx <= 1 ? (idx === 0 ? 7 : 4) : 0; }; const submitComplete = () => { if (!touched) return; setConfirmation({ message: isWater ? `Watered ${plantName} — I've noted it was ${stateLabel.toLowerCase()}.` : `Fertilized ${plantName} — I've noted it's ${stateLabel.toLowerCase()}.`, stateAtComplete: stateLabel, postponeDays: null, }); }; const submitPostpone = () => { if (!touched) return; const days = postponeDaysFor(sliderIndex); if (days <= 0) return; // shouldn't happen — postpone only offered when days > 0 setConfirmation({ message: `Got it — I'll check back in ${days} day${days === 1 ? '' : 's'}.`, stateAtComplete: stateLabel, postponeDays: days, }); }; const confirmAndClose = () => { if (!confirmation) return; // Record what fertilizer was actually used (fertilize tasks only), so App // can persist it as the plant's canonical value (Bekky, 2026-09-02). if (!isWater) { const used = fertChoice === 'other' ? (fertOther.trim() || undefined) : (data?.fertilizeFertilizer?.trim() || undefined); onFertilizerRecorded?.(used); } if (confirmation.postponeDays === null) { onComplete(confirmation.stateAtComplete); } else { onPostpone(confirmation.stateAtComplete, confirmation.postponeDays); } }; // Whether the Postpone button is offered: water → damp or wetter (idx >= 2); // fertilize → dormant or slow (idx <= 1). const offerPostpone = touched && (isWater ? sliderIndex >= 2 : sliderIndex <= 1); // Whether Complete is offered — GATED by the state (Bekky, 2026-08-21): you // can only complete watering when the plant is actually dry (Very dry / // Slightly dry), and only fertilize when it's actively growing. Completing a // soaked plant makes no sense, so Complete is hidden when it's damp/wet. // ALSO gated by the early-complete window (Bekky, 2026-09-01): a task more // than 3 days out (water) / 7 days out (fertilize) must NOT be completable // from the check-in modal — it isn't due yet. canCompleteCareTaskEarly // already encodes the water=3 / fertilize=7 window. const offerComplete = touched && canCompleteCareTaskEarly(task) && (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 ? '💧 Water' : '🌱 Feed'} {plantName} {/* Standard guidance — always visible (Bekky, 2026-08-21): it's a blanket message now, so no need to hide it behind a tap. */} {guidance} {/* Fertilizer capture (Bekky, 2026-09-02) — fertilize only. 1. Stored fertilizer exists → "You said — [X]. Did you use this?" 2. No stored value → a plain optional "what did you use?" input. A "save for next time" is implied (recorded back to setup). */} {!isWater ? ( data?.fertilizeFertilizer?.trim() ? ( What did you feed with? setFertChoice(fertChoice === 'same' ? null : 'same'))} > {data!.fertilizeFertilizer} setFertChoice(fertChoice === 'other' ? null : 'other'))} > Something else {fertChoice === 'other' ? ( ) : null} {fertChoice === 'same' ? ( Noted — I'll keep {data!.fertilizeFertilizer} on file. ) : null} ) : ( What did you feed with? (optional) ) ) : null} {/* The state slider — the user's observable signal. */} {isWater ? 'How damp is the soil?' : "How's it growing?"} {/* Fine-tuning note — only once the user has picked a level. */} {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 the user taps a level; gated by state. */} {touched ? ( {offerPostpone ? ( Postpone ) : null} {offerComplete ? ( {isWater ? 'Complete watering' : 'Fertilize today'} ) : null} ) : ( Select how it's doing. )} Cancel {onSkip ? ( Skip ) : 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, }, tuningNote: { backgroundColor: '#F3F7E6', borderRadius: 12, padding: 12, marginTop: 12, marginBottom: 4, }, tuningNoteText: { fontSize: 13, lineHeight: 19, color: '#2D4A2D', fontWeight: '700', }, fertSection: { marginTop: 2, marginBottom: 2 }, fertChoiceRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 8 }, fertChoicePill: { paddingVertical: 10, paddingHorizontal: 14, borderRadius: 999, borderWidth: 1, borderColor: line, backgroundColor: '#F3F7E6', flexShrink: 1 }, fertChoicePillActive: { borderColor: green, backgroundColor: '#E1F0CE' }, fertChoiceText: { color: '#2D4A2D', fontSize: 13, fontWeight: '700' }, fertChoiceTextActive: { color: green, fontWeight: '900' }, fertInput: { borderWidth: 1, borderColor: line, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, color: dark, backgroundColor: '#FFFDF4', marginBottom: 8 }, fertNote: { fontSize: 12, lineHeight: 18, color: '#2D4A2D', fontWeight: '700', marginBottom: 8 }, sliderHint: { fontSize: 13, color: '#8A8571', textAlign: 'center', marginTop: 4, marginBottom: 8, }, actionRow: { flexDirection: 'row', gap: 10, marginTop: 4, }, postponeButton: { flex: 1, backgroundColor: '#F3F7E6', borderRadius: 999, paddingVertical: 14, alignItems: 'center', borderWidth: 1, borderColor: green, marginTop: 4, }, postponeButtonText: { color: green, fontSize: 15, fontWeight: '900', }, completeButton: { flex: 1, backgroundColor: green, borderRadius: 999, paddingVertical: 14, alignItems: 'center', borderWidth: 1, borderColor: green, marginTop: 4, }, completeButtonText: { color: '#fff', fontSize: 15, fontWeight: '900', }, cancelButton: { backgroundColor: '#F3F7E6', borderRadius: 999, paddingVertical: 12, alignItems: 'center', marginTop: 8, }, cancelButtonText: { color: green, fontSize: 14, fontWeight: '800', }, skipButton: { backgroundColor: '#FCE9E6', borderRadius: 999, paddingVertical: 12, alignItems: 'center', marginTop: 6, borderWidth: 1, borderColor: '#B85C4A', }, skipButtonText: { color: '#8E2F1E', fontSize: 14, fontWeight: '800', }, confirmWrap: { alignItems: 'center', paddingVertical: 12, }, confirmCheck: { fontSize: 44, color: green, marginBottom: 6, }, confirmTitle: { fontSize: 20, fontWeight: '900', color: dark, marginBottom: 8, }, confirmMessage: { fontSize: 15, fontWeight: '700', color: '#2D4A2D', textAlign: 'center', marginBottom: 8, }, confirmHint: { fontSize: 13, lineHeight: 19, color: '#8A8571', textAlign: 'center', marginBottom: 18, }, confirmButton: { alignSelf: 'stretch', backgroundColor: green, borderRadius: 999, paddingVertical: 14, alignItems: 'center', marginTop: 4, }, confirmButtonText: { color: '#fff', fontSize: 15, fontWeight: '900', }, });