diff --git a/frontend/src/pages/DailyWalkthroughPage.tsx b/frontend/src/pages/DailyWalkthroughPage.tsx index 5874976..0260412 100644 --- a/frontend/src/pages/DailyWalkthroughPage.tsx +++ b/frontend/src/pages/DailyWalkthroughPage.tsx @@ -1,337 +1,470 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import ReservoirChecklist from '../components/walkthrough/ReservoirChecklist'; -import IrrigationChecklist from '../components/walkthrough/IrrigationChecklist'; -import PlantHealthChecklist from '../components/walkthrough/PlantHealthChecklist'; -import { settingsApi, WalkthroughSettings, PhotoRequirement } from '../lib/settingsApi'; +import { settingsApi, WalkthroughSettings } from '../lib/settingsApi'; +import { walkthroughApi, ReservoirCheckData, IrrigationCheckData, PlantHealthCheckData } from '../lib/walkthroughApi'; import { - walkthroughApi, - ReservoirCheckData, - IrrigationCheckData, - PlantHealthCheckData -} from '../lib/walkthroughApi'; -import { ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug, ChevronRight, Clock } from 'lucide-react'; + ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug, + Camera, X, Minus, Plus, ChevronDown, ChevronUp +} from 'lucide-react'; import { useToast } from '../context/ToastContext'; -type Step = 'start' | 'reservoir' | 'irrigation' | 'plant-health' | 'summary'; +// Tank/Zone configs +const TANKS = [ + { name: 'Veg Tank 1', type: 'VEG' as const }, + { name: 'Veg Tank 2', type: 'VEG' as const }, + { name: 'Flower Tank 1', type: 'FLOWER' as const }, + { name: 'Flower Tank 2', type: 'FLOWER' as const }, +]; + +const ZONES = [ + { name: 'Veg Upstairs', drippers: 48 }, + { name: 'Veg Downstairs', drippers: 48 }, + { name: 'Flower Upstairs', drippers: 64 }, + { name: 'Flower Downstairs', drippers: 64 }, +]; + +const HEALTH_ZONES = ['Veg Upstairs', 'Veg Downstairs', 'Flower Upstairs', 'Flower Downstairs']; export default function DailyWalkthroughPage() { const navigate = useNavigate(); const { addToast } = useToast(); - const [currentStep, setCurrentStep] = useState('start'); const [walkthroughId, setWalkthroughId] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const [reservoirChecks, setReservoirChecks] = useState([]); - const [irrigationChecks, setIrrigationChecks] = useState([]); - const [plantHealthChecks, setPlantHealthChecks] = useState([]); + const [isStarting, setIsStarting] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const [settings, setSettings] = useState(null); + // All check states + const [reservoirChecks, setReservoirChecks] = useState>({}); + const [irrigationChecks, setIrrigationChecks] = useState>({}); + const [plantHealthChecks, setPlantHealthChecks] = useState>({}); + + // Section expansion + const [expandedSections, setExpandedSections] = useState>({ + reservoirs: true, + irrigation: true, + plantHealth: true, + }); + useEffect(() => { settingsApi.getWalkthrough().then(setSettings).catch(console.error); }, []); - const isPhotoRequired = (type: 'reservoir' | 'irrigation' | 'plantHealth') => { - if (!settings) return false; - const req = settings[`${type}Photos` as keyof WalkthroughSettings] as PhotoRequirement; - if (req === 'REQUIRED') return true; - if (req === 'WEEKLY') return new Date().getDay() === 1; - return false; + const toggleSection = (section: string) => { + setExpandedSections(prev => ({ ...prev, [section]: !prev[section] })); }; - const getNextStep = (current: Step): Step => { - if (!settings) return 'summary'; - const sequence: Step[] = ['reservoir', 'irrigation', 'plant-health', 'summary']; - const isEnabled = (s: Step) => { - if (s === 'reservoir') return settings.enableReservoirs; - if (s === 'irrigation') return settings.enableIrrigation; - if (s === 'plant-health') return settings.enablePlantHealth; - return true; - }; - - if (current === 'start') return sequence.find(s => isEnabled(s)) || 'summary'; - const idx = sequence.indexOf(current); - if (idx === -1) return 'summary'; - for (let i = idx + 1; i < sequence.length; i++) { - if (isEnabled(sequence[i])) return sequence[i]; - } - return 'summary'; - }; - - const handleStartWalkthrough = async () => { - setIsLoading(true); - setError(null); + const handleStart = async () => { + setIsStarting(true); try { const walkthrough = await walkthroughApi.create(); setWalkthroughId(walkthrough.id); - setCurrentStep(getNextStep('start')); } catch (err: any) { - setError(err.response?.data?.message || 'Failed to start walkthrough'); + addToast('Failed to start walkthrough', 'error'); } finally { - setIsLoading(false); + setIsStarting(false); } }; - const handleReservoirComplete = async (checks: ReservoirCheckData[]) => { + const handleSubmit = async () => { if (!walkthroughId) return; - setIsLoading(true); - setError(null); + setIsSubmitting(true); try { - for (const check of checks) { + // Submit all checks + for (const check of Object.values(reservoirChecks)) { await walkthroughApi.addReservoirCheck(walkthroughId, check); } - setReservoirChecks(checks); - setCurrentStep(getNextStep('reservoir')); - } catch (err: any) { - setError(err.response?.data?.message || 'Failed to save reservoir checks'); - } finally { - setIsLoading(false); - } - }; - - const handleIrrigationComplete = async (checks: IrrigationCheckData[]) => { - if (!walkthroughId) return; - setIsLoading(true); - setError(null); - try { - for (const check of checks) { + for (const check of Object.values(irrigationChecks)) { await walkthroughApi.addIrrigationCheck(walkthroughId, check); } - setIrrigationChecks(checks); - setCurrentStep(getNextStep('irrigation')); - } catch (err: any) { - setError(err.response?.data?.message || 'Failed to save irrigation checks'); - } finally { - setIsLoading(false); - } - }; - - const handlePlantHealthComplete = async (checks: PlantHealthCheckData[]) => { - if (!walkthroughId) return; - setIsLoading(true); - setError(null); - try { - for (const check of checks) { + for (const check of Object.values(plantHealthChecks)) { await walkthroughApi.addPlantHealthCheck(walkthroughId, check); } - setPlantHealthChecks(checks); - setCurrentStep(getNextStep('plant-health')); - } catch (err: any) { - setError(err.response?.data?.message || 'Failed to save plant health checks'); - } finally { - setIsLoading(false); - } - }; - - const handleSubmitWalkthrough = async () => { - if (!walkthroughId) return; - setIsLoading(true); - setError(null); - try { await walkthroughApi.complete(walkthroughId); addToast('Walkthrough completed!', 'success'); - navigate('/', { state: { message: 'Daily walkthrough completed!' } }); + navigate('/'); } catch (err: any) { - setError(err.response?.data?.message || 'Failed to complete walkthrough'); + addToast('Failed to submit walkthrough', 'error'); } finally { - setIsLoading(false); + setIsSubmitting(false); } }; - // Render current step - if (currentStep === 'reservoir') { - return ( - setCurrentStep('start')} - isPhotoRequired={isPhotoRequired('reservoir')} - /> - ); - } - - if (currentStep === 'irrigation') { - return ( - setCurrentStep('reservoir')} - isPhotoRequired={isPhotoRequired('irrigation')} - /> - ); - } - - if (currentStep === 'plant-health') { - return ( - setCurrentStep('irrigation')} - isPhotoRequired={isPhotoRequired('plantHealth')} - /> - ); - } - - // Summary Step - if (currentStep === 'summary') { - const totalChecks = reservoirChecks.length + irrigationChecks.length + plantHealthChecks.length; - const issues = [ - ...reservoirChecks.filter(c => c.status !== 'OK'), - ...irrigationChecks.filter(c => !c.waterFlow || !c.nutrientsMixed || c.drippersWorking < c.drippersTotal), - ...plantHealthChecks.filter(c => c.healthStatus !== 'GOOD' || c.pestsObserved) - ].length; + const totalChecks = Object.keys(reservoirChecks).length + Object.keys(irrigationChecks).length + Object.keys(plantHealthChecks).length; + const requiredChecks = TANKS.length + ZONES.length + HEALTH_ZONES.length; + const isComplete = totalChecks === requiredChecks; + // Pre-start view + if (!walkthroughId) { return (
-
- {/* Compact header */} -
-

Review & Submit

-
- - {/* Summary stats */} -
-
-
{totalChecks}
-
Checks
-
-
-
-
0 ? 'text-warning' : 'text-success'}`}>{issues}
-
Issues
-
-
- - {/* Compact summary cards */} -
- c.status === 'OK') ? 'good' : 'warning'} - /> - c.waterFlow && c.nutrientsMixed) ? 'good' : 'warning'} - /> - c.healthStatus === 'GOOD' && !c.pestsObserved) ? 'good' : 'warning'} - /> -
- - {error && ( -
- - {error} -
- )} - - {/* Actions */} -
- - -
+
+

Daily Walkthrough

+

+ {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })} +

+
); } - // Start Screen - Refined - const today = new Date(); - const greeting = today.getHours() < 12 ? 'Good morning' : today.getHours() < 17 ? 'Good afternoon' : 'Good evening'; - return ( -
-
- {/* Minimal header */} -
-

{today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}

-

{greeting}

+
+ {/* Header */} +
+ +
+

Daily Walkthrough

+

{totalChecks}/{requiredChecks} checks completed

+
- {/* Task list - tight and intentional */} -
-
-

Daily Walkthrough

-

- - ~15 min -

+ {/* Reservoirs Section */} + {settings?.enableReservoirs !== false && ( + toggleSection('reservoirs')} + > +
+ {TANKS.map(tank => ( + setReservoirChecks(prev => ({ ...prev, [tank.name]: data }))} + /> + ))}
+
+ )} -
- - - + {/* Irrigation Section */} + {settings?.enableIrrigation !== false && ( + toggleSection('irrigation')} + > +
+ {ZONES.map(zone => ( + setIrrigationChecks(prev => ({ ...prev, [zone.name]: data }))} + /> + ))}
+
+ )} - {error && ( -
- - {error} -
- )} - -
- + {/* Plant Health Section */} + {settings?.enablePlantHealth !== false && ( + toggleSection('plantHealth')} + > +
+ {HEALTH_ZONES.map(zone => ( + setPlantHealthChecks(prev => ({ ...prev, [zone]: data }))} + /> + ))}
+
+ )} + + {/* Fixed Submit Button */} +
+
+
); } -// Compact step row -function StepRow({ icon: Icon, label }: { icon: typeof Droplets; label: string }) { - return ( -
-
- -
- {label} - -
- ); -} - -// Summary card -function SummaryCard({ icon: Icon, label, count, status }: { +// Collapsible Section +function CollapsibleSection({ + title, icon: Icon, count, total, expanded, onToggle, children +}: { + title: string; icon: typeof Droplets; - label: string; count: number; - status: 'good' | 'warning'; + total: number; + expanded: boolean; + onToggle: () => void; + children: React.ReactNode; }) { + const isComplete = count === total; return ( -
-
- -
-
{count}
-
{label}
+
+ + {expanded &&
{children}
} +
+ ); +} + +// Reservoir Row - Compact inline +function ReservoirRow({ + tank, data, onChange +}: { + tank: { name: string; type: 'VEG' | 'FLOWER' }; + data?: ReservoirCheckData; + onChange: (data: ReservoirCheckData) => void; +}) { + const [level, setLevel] = useState(data?.levelPercent ?? 100); + const [editing, setEditing] = useState(!data); + + const getStatus = (l: number) => l >= 70 ? 'OK' : l >= 30 ? 'LOW' : 'CRITICAL'; + + const handleSave = () => { + onChange({ + tankName: tank.name, + tankType: tank.type, + levelPercent: level, + status: getStatus(level), + }); + setEditing(false); + }; + + if (!editing && data) { + return ( +
+
+
+
+ {tank.name} + {data.levelPercent}% +
+
+ +
+ ); + } + + return ( +
+
+ {tank.name} + {tank.type} +
+
+
+
+
+ setLevel(parseInt(e.target.value))} + className="flex-1 h-1.5 bg-subtle rounded-full appearance-none cursor-pointer accent-accent" + /> + {level}% + +
+
+ ); +} + +// Irrigation Row - Compact inline +function IrrigationRow({ + zone, data, onChange +}: { + zone: { name: string; drippers: number }; + data?: IrrigationCheckData; + onChange: (data: IrrigationCheckData) => void; +}) { + const [working, setWorking] = useState(data?.drippersWorking ?? zone.drippers); + const [waterFlow, setWaterFlow] = useState(data?.waterFlow ?? true); + const [nutrients, setNutrients] = useState(data?.nutrientsMixed ?? true); + const [editing, setEditing] = useState(!data); + + const handleSave = () => { + onChange({ + zoneName: zone.name, + drippersTotal: zone.drippers, + drippersWorking: working, + drippersFailed: [], + waterFlow, + nutrientsMixed: nutrients, + scheduleActive: true, + }); + setEditing(false); + }; + + if (!editing && data) { + const issues = !data.waterFlow || !data.nutrientsMixed || data.drippersWorking < data.drippersTotal; + return ( +
+
+
+
+ {zone.name} + {data.drippersWorking}/{data.drippersTotal} +
+
+ +
+ ); + } + + return ( +
+
+ {zone.name} +
+ + {working}/{zone.drippers} + +
+
+
+ + +
+ +
+
+ ); +} + +// Plant Health Row - Compact inline +function PlantHealthRow({ + zoneName, data, onChange +}: { + zoneName: string; + data?: PlantHealthCheckData; + onChange: (data: PlantHealthCheckData) => void; +}) { + const [health, setHealth] = useState<'GOOD' | 'FAIR' | 'NEEDS_ATTENTION'>(data?.healthStatus ?? 'GOOD'); + const [pests, setPests] = useState(data?.pestsObserved ?? false); + const [editing, setEditing] = useState(!data); + + const handleSave = () => { + onChange({ + zoneName, + healthStatus: health, + pestsObserved: pests, + waterAccess: 'OK', + foodAccess: 'OK', + flaggedForAttention: health !== 'GOOD' || pests, + }); + setEditing(false); + }; + + if (!editing && data) { + const issues = data.healthStatus !== 'GOOD' || data.pestsObserved; + return ( +
+
+
+
+ {zoneName} + {data.healthStatus} + {data.pestsObserved && 🐛 Pests} +
+
+ +
+ ); + } + + return ( +
+
+ {zoneName} +
+
+ {(['GOOD', 'FAIR', 'NEEDS_ATTENTION'] as const).map(s => ( + + ))} +
+
+ + +
); }