- DailyWalkthroughPage: Centered, compact start screen with minimal chrome - Summary: Statistical overview with compact cards - ReservoirChecklist: Single column centered, tighter spacing - IrrigationChecklist: Compact status rows, inline toggles - PlantHealthChecklist: Segmented health control, minimal layout - Layout: Remove theme toggle from desktop sidebar (cleaner)
202 lines
9 KiB
TypeScript
202 lines
9 KiB
TypeScript
import { useState } from 'react';
|
|
import { ArrowLeft, ChevronRight, Camera, X, Check, Minus, Plus } from 'lucide-react';
|
|
|
|
interface Zone {
|
|
name: string;
|
|
defaultDrippers: number;
|
|
}
|
|
|
|
interface IrrigationCheckData {
|
|
zoneName: string;
|
|
drippersTotal: number;
|
|
drippersWorking: number;
|
|
drippersFailed: string[];
|
|
waterFlow: boolean;
|
|
nutrientsMixed: boolean;
|
|
scheduleActive: boolean;
|
|
photoUrl?: string;
|
|
issues?: string;
|
|
}
|
|
|
|
interface IrrigationChecklistProps {
|
|
onComplete: (checks: IrrigationCheckData[]) => void;
|
|
onBack: () => void;
|
|
isPhotoRequired: boolean;
|
|
}
|
|
|
|
export default function IrrigationChecklist({ onComplete, onBack, isPhotoRequired }: IrrigationChecklistProps) {
|
|
const zones: Zone[] = [
|
|
{ name: 'Veg Upstairs', defaultDrippers: 48 },
|
|
{ name: 'Veg Downstairs', defaultDrippers: 48 },
|
|
{ name: 'Flower Upstairs', defaultDrippers: 64 },
|
|
{ name: 'Flower Downstairs', defaultDrippers: 64 },
|
|
];
|
|
|
|
const [checks, setChecks] = useState<Map<string, IrrigationCheckData>>(new Map());
|
|
const [currentZoneIndex, setCurrentZoneIndex] = useState(0);
|
|
const [drippersWorking, setDrippersWorking] = useState(zones[0].defaultDrippers);
|
|
const [waterFlow, setWaterFlow] = useState(true);
|
|
const [nutrientsMixed, setNutrientsMixed] = useState(true);
|
|
const [scheduleActive, setScheduleActive] = useState(true);
|
|
const [issues, setIssues] = useState('');
|
|
const [photo, setPhoto] = useState<string | null>(null);
|
|
|
|
const currentZone = zones[currentZoneIndex];
|
|
const isLastZone = currentZoneIndex === zones.length - 1;
|
|
const drippersFailed = currentZone.defaultDrippers - drippersWorking;
|
|
const allGood = waterFlow && nutrientsMixed && scheduleActive && drippersFailed === 0;
|
|
|
|
const handleNext = () => {
|
|
if (isPhotoRequired && !photo) {
|
|
alert('Photo is required for this step.');
|
|
return;
|
|
}
|
|
|
|
const checkData: IrrigationCheckData = {
|
|
zoneName: currentZone.name,
|
|
drippersTotal: currentZone.defaultDrippers,
|
|
drippersWorking,
|
|
drippersFailed: drippersFailed > 0 ? [`${drippersFailed} drippers`] : [],
|
|
waterFlow,
|
|
nutrientsMixed,
|
|
scheduleActive,
|
|
issues: issues || undefined,
|
|
photoUrl: photo || undefined
|
|
};
|
|
|
|
const newChecks = new Map(checks);
|
|
newChecks.set(currentZone.name, checkData);
|
|
setChecks(newChecks);
|
|
|
|
if (isLastZone) {
|
|
onComplete(Array.from(newChecks.values()));
|
|
} else {
|
|
const nextZone = zones[currentZoneIndex + 1];
|
|
setCurrentZoneIndex(currentZoneIndex + 1);
|
|
setDrippersWorking(nextZone.defaultDrippers);
|
|
setWaterFlow(true);
|
|
setNutrientsMixed(true);
|
|
setScheduleActive(true);
|
|
setIssues('');
|
|
setPhoto(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
|
<div className="w-full max-w-sm">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<button onClick={onBack} className="p-2 -ml-2 hover:bg-tertiary rounded-md transition-colors">
|
|
<ArrowLeft size={16} className="text-secondary" />
|
|
</button>
|
|
<div className="flex-1">
|
|
<p className="text-xs text-tertiary">Zone {currentZoneIndex + 1}/{zones.length}</p>
|
|
<h1 className="text-lg font-semibold text-primary">{currentZone.name}</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress */}
|
|
<div className="flex gap-1 mb-6">
|
|
{zones.map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className={`h-1 flex-1 rounded-full ${i <= currentZoneIndex ? 'bg-accent' : 'bg-tertiary'}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Drippers Counter */}
|
|
<div className="card p-4 mb-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-sm text-secondary">Drippers</span>
|
|
{drippersFailed > 0 && (
|
|
<span className="text-xs text-destructive">{drippersFailed} failed</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center justify-center gap-4">
|
|
<button
|
|
onClick={() => setDrippersWorking(Math.max(0, drippersWorking - 1))}
|
|
className="w-10 h-10 rounded-md bg-tertiary flex items-center justify-center text-secondary hover:bg-secondary transition-colors"
|
|
>
|
|
<Minus size={16} />
|
|
</button>
|
|
<div className="text-center min-w-[80px]">
|
|
<div className="text-2xl font-semibold text-primary">{drippersWorking}</div>
|
|
<div className="text-xs text-tertiary">of {currentZone.defaultDrippers}</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setDrippersWorking(Math.min(currentZone.defaultDrippers, drippersWorking + 1))}
|
|
className="w-10 h-10 rounded-md bg-tertiary flex items-center justify-center text-secondary hover:bg-secondary transition-colors"
|
|
>
|
|
<Plus size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Toggles - compact */}
|
|
<div className="card divide-y divide-subtle mb-4">
|
|
<StatusRow label="Water Flow" value={waterFlow} onChange={setWaterFlow} />
|
|
<StatusRow label="Nutrients Mixed" value={nutrientsMixed} onChange={setNutrientsMixed} />
|
|
<StatusRow label="Schedule Active" value={scheduleActive} onChange={setScheduleActive} />
|
|
</div>
|
|
|
|
{/* Issues input - only when problems */}
|
|
{!allGood && (
|
|
<div className="mb-4">
|
|
<input
|
|
type="text"
|
|
value={issues}
|
|
onChange={(e) => setIssues(e.target.value)}
|
|
placeholder="Describe issues..."
|
|
className="input w-full text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Photo */}
|
|
{(isPhotoRequired || !allGood) && (
|
|
<div className="mb-4">
|
|
<input type="file" id="photo-upload" className="hidden" accept="image/*" capture="environment"
|
|
onChange={(e) => { if (e.target.files?.[0]) setPhoto(URL.createObjectURL(e.target.files[0])); }} />
|
|
{photo ? (
|
|
<div className="relative">
|
|
<img src={photo} alt="Preview" className="w-full h-24 object-cover rounded-md" />
|
|
<button onClick={() => setPhoto(null)} className="absolute top-1 right-1 p-1 bg-black/50 rounded-full">
|
|
<X size={12} className="text-white" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<label htmlFor="photo-upload" className={`flex items-center justify-center gap-2 p-3 border border-dashed rounded-md cursor-pointer text-sm ${isPhotoRequired ? 'border-destructive/50 text-destructive' : 'border-subtle text-tertiary'
|
|
}`}>
|
|
<Camera size={14} />
|
|
{isPhotoRequired ? 'Photo required' : 'Add photo'}
|
|
</label>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Action */}
|
|
<button onClick={handleNext} className="btn btn-primary w-full">
|
|
{isLastZone ? 'Complete' : 'Next'}
|
|
{!isLastZone && <ChevronRight size={14} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatusRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
|
|
return (
|
|
<button
|
|
onClick={() => onChange(!value)}
|
|
className="w-full flex items-center justify-between px-4 py-3 hover:bg-tertiary transition-colors"
|
|
>
|
|
<span className="text-sm text-primary">{label}</span>
|
|
<div className={`w-5 h-5 rounded flex items-center justify-center ${value ? 'bg-success text-white' : 'bg-destructive text-white'
|
|
}`}>
|
|
{value ? <Check size={12} /> : <X size={12} />}
|
|
</div>
|
|
</button>
|
|
);
|
|
}
|