/** * CheckInModal — follow-up progress check-in for an open investigation case. * Shared by BOTH buttons (Add Progress Update + Get a Second Look). * * Shows the reference intake photo strip (to match angle/scope), a 3-way * Better/Same/Worse selector, an optional short chip set (Second-Look path only), * a note field, and a photo capture. On submit it calls evaluateProgress(). */ import React, { useState } from 'react'; import { ActivityIndicator, Image, Modal, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, useWindowDimensions, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import { green, dark, line, haptic, pressWithHaptic } from '../constants/theme'; import type { TreatmentPlan, ProgressUpdate } from '../services/investigation'; import { evaluateProgress, type ProgressAssessment } from '../services/problemScanService'; type CheckInMode = 'progress' | 'secondLook'; interface CheckInModalProps { visible: boolean; mode: CheckInMode; plantName: string; treatmentPlan?: TreatmentPlan; referenceIntakeUris: string[]; onCancel: () => void; onSubmitted: (update: Omit) => void; } const SECOND_LOOK_CHIPS = ['New spotting', 'Spreading', 'Drooping', 'Wilting', 'Other']; export function CheckInModal({ visible, mode, plantName, treatmentPlan, referenceIntakeUris, onCancel, onSubmitted, }: CheckInModalProps) { const [betterOrWorse, setBetterOrWorse] = useState<'better' | 'same' | 'worse'>('same'); const [photoUri, setPhotoUri] = useState(null); const [photoBase64, setPhotoBase64] = useState(null); const [note, setNote] = useState(''); const [chips, setChips] = useState([]); const [busy, setBusy] = useState(false); const [showPicker, setShowPicker] = useState(false); const isSecondLook = mode === 'secondLook'; // Pixel-based card height (Bekky rule #2 — no percentage maxHeight on modal cards). const insets = useSafeAreaInsets(); const { height: windowHeight } = useWindowDimensions(); const cardMaxHeight = Math.max(320, windowHeight - (insets.top + 24) - (insets.bottom + 24)); const reset = () => { setBetterOrWorse('same'); setPhotoUri(null); setPhotoBase64(null); setNote(''); setChips([]); setShowPicker(false); setBusy(false); }; const close = () => { reset(); onCancel(); }; const toggleChip = (chip: string) => { setChips(cur => cur.includes(chip) ? cur.filter(c => c !== chip) : [...cur, chip]); }; const pickFromLibrary = async () => { try { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], base64: true, quality: 0.5, }); if (!result.canceled && result.assets?.[0]) { setPhotoUri(result.assets[0].uri); setPhotoBase64(result.assets[0].base64 || null); } setShowPicker(false); } catch { setShowPicker(false); } }; const takePhoto = async () => { try { const perm = await ImagePicker.requestCameraPermissionsAsync(); if (!perm.granted) { setShowPicker(false); return; } const result = await ImagePicker.launchCameraAsync({ base64: true, quality: 0.5, }); if (!result.canceled && result.assets?.[0]) { setPhotoUri(result.assets[0].uri); setPhotoBase64(result.assets[0].base64 || null); } setShowPicker(false); } catch { setShowPicker(false); } }; const submit = async () => { if (busy || !photoBase64) return; setBusy(true); try { const treatmentContext = treatmentPlan && treatmentPlan.steps.length > 0 ? treatmentPlan.steps.map(s => `${s.action}${s.dueOffsetDays > 0 ? ` (in ${s.dueOffsetDays}d)` : ''}`).join('; ') : undefined; const assessment: ProgressAssessment = await evaluateProgress({ imageBase64: photoBase64, betterOrWorse, entryType: isSecondLook ? 'early' : 'scheduled', note: note.trim() || undefined, optionalChips: isSecondLook && chips.length > 0 ? chips : undefined, plantName, treatmentContext, }); onSubmitted({ entryType: isSecondLook ? 'early' : 'scheduled', betterOrWorse, note: note.trim() || undefined, photoUri: photoUri || '', referenceIntakeUris: referenceIntakeUris.length > 0 ? referenceIntakeUris : undefined, optionalChips: isSecondLook && chips.length > 0 ? chips : undefined, aiAssessment: { summary: assessment.summary, working: assessment.working, nextStep: assessment.nextStep, planAdjusted: assessment.planAdjusted, recheckDays: assessment.recheckDays, }, }); reset(); } catch (error) { haptic.warning(); setBusy(false); } }; return ( {isSecondLook ? 'Get a Second Look' : 'Add Progress Update'} {isSecondLook ? `Something feel off with ${plantName}? Pixie can take another look and adjust the plan.` : `Share how ${plantName} is looking — a quick photo is enough.`} {/* Reference strip — match the intake angle */} {referenceIntakeUris.length > 0 && ( 📸 Try to match the original angle Each reference is optional — skip any you can't recreate. {referenceIntakeUris.map((uri, idx) => ( ))} )} {/* 3-way change selector */} How is it going? {(['better', 'same', 'worse'] as const).map(v => ( setBetterOrWorse(v))} > {v === 'better' ? '🟢 Better' : v === 'same' ? '🟡 Same' : '🔴 Worse'} ))} {/* Second-Look optional chips */} {isSecondLook && ( Anything new or spreading? (optional) {SECOND_LOOK_CHIPS.map(chip => ( toggleChip(chip))} > {chip} ))} )} {/* Photo capture */} Progress photo {photoUri ? ( setShowPicker(true))}> 📷 Retake ) : ( setShowPicker(true))}> 📷 Add a photo )} {/* Note */} {/* Submit */} {busy ? ( ) : ( {isSecondLook ? 'Check In' : 'Share Progress'} )} {/* Photo source picker */} setShowPicker(false)}> setShowPicker(false)}> Add Progress Photo 📷 Take Photo 🖼️ Choose Photo setShowPicker(false))}> Cancel ); } const s = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(16, 42, 16, 0.6)', justifyContent: 'center', alignItems: 'center', padding: 20, }, card: { width: '100%', maxWidth: 360, maxHeight: '85%', backgroundColor: '#FFFCF5', borderRadius: 24, padding: 20, shadowColor: '#000', shadowOpacity: 0.25, shadowRadius: 20, elevation: 12, }, headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, title: { color: dark, fontSize: 19, fontWeight: '900', flex: 1 }, closePill: { minWidth: 34, height: 34, borderRadius: 17, backgroundColor: '#FCE9E6', borderWidth: 1.5, borderColor: '#B85C4A', alignItems: 'center', justifyContent: 'center', marginLeft: 8, }, closePillText: { color: '#8E2F1E', fontSize: 14, fontWeight: '900' }, subtitle: { color: '#5D5A4E', fontSize: 13, lineHeight: 18, marginTop: 6, marginBottom: 12 }, scroll: { flexGrow: 0 }, scrollContent: { gap: 12, paddingBottom: 4 }, refSection: { backgroundColor: '#F3F7E6', borderRadius: 14, padding: 10 }, refTitle: { color: dark, fontSize: 13, fontWeight: '800', marginBottom: 2 }, refHint: { color: '#5B7553', fontSize: 11, marginBottom: 8 }, refThumb: { width: 64, height: 64, borderRadius: 10, marginRight: 8, backgroundColor: '#E8F5D3' }, fieldLabel: { color: dark, fontSize: 13, fontWeight: '800', marginBottom: 6 }, threeRow: { flexDirection: 'row', gap: 8 }, threePill: { flex: 1, minHeight: 44, borderRadius: 999, borderWidth: 1.5, borderColor: line, backgroundColor: '#FFFDF7', alignItems: 'center', justifyContent: 'center', }, threePillActive: { backgroundColor: green, borderColor: green }, threeText: { color: dark, fontSize: 13, fontWeight: '700' }, threeTextActive: { color: '#fff' }, chipSection: { marginTop: 2 }, chipRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, chip: { minHeight: 34, borderRadius: 999, borderWidth: 1.5, borderColor: line, backgroundColor: '#FFFDF7', paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' }, chipActive: { backgroundColor: '#E8F5D3', borderColor: green }, chipText: { color: dark, fontSize: 12, fontWeight: '700' }, chipTextActive: { color: green }, photo: { width: '100%', height: 140, borderRadius: 14, backgroundColor: '#E8F5D3' }, retakePill: { alignSelf: 'flex-start', minHeight: 34, borderRadius: 999, borderWidth: 1.5, borderColor: line, backgroundColor: '#FFF9EE', paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', marginTop: 8 }, retakeText: { color: dark, fontSize: 12, fontWeight: '700' }, addPhotoTile: { minHeight: 80, borderRadius: 14, borderWidth: 1.5, borderColor: '#BFD9B4', backgroundColor: '#FFF9EE', alignItems: 'center', justifyContent: 'center', borderStyle: 'dashed' }, addPhotoText: { color: green, fontSize: 14, fontWeight: '700' }, noteInput: { minHeight: 60, borderRadius: 14, borderWidth: 1, borderColor: '#BFD9B4', backgroundColor: '#FFFDF7', padding: 12, color: dark, fontSize: 13, textAlignVertical: 'top' }, submitButton: { minHeight: 48, borderRadius: 999, backgroundColor: green, alignItems: 'center', justifyContent: 'center', marginTop: 14 }, submitDisabled: { opacity: 0.5 }, submitText: { color: '#fff', fontSize: 15, fontWeight: '900' }, pickerOverlay: { flex: 1, backgroundColor: 'rgba(16, 42, 16, 0.6)', justifyContent: 'center', alignItems: 'center', padding: 24 }, pickerCard: { width: '100%', maxWidth: 340, backgroundColor: '#FFFCF5', borderRadius: 24, padding: 24 }, pickerTitle: { color: dark, fontSize: 18, fontWeight: '800', textAlign: 'center', marginBottom: 16 }, pickerOption: { minHeight: 48, borderRadius: 999, backgroundColor: green, alignItems: 'center', justifyContent: 'center', marginBottom: 10 }, pickerOptionText: { color: '#fff', fontSize: 15, fontWeight: '900' }, pickerCancel: { minHeight: 44, borderRadius: 999, borderWidth: 1.5, borderColor: '#B85C4A', backgroundColor: '#FCE9E6', alignItems: 'center', justifyContent: 'center' }, pickerCancelText: { color: '#8E2F1E', fontSize: 14, fontWeight: '900' }, });