refactor: consolidate Daily Walkthrough to single page
- All sections (reservoirs, irrigation, plant health) on one scrollable page - Collapsible sections with progress indicators - Compact inline check rows (no oversized tank visualizations) - Thin level indicator bars instead of large tank graphics - Fixed submit button at bottom - Edit mode per row for quick adjustments
This commit is contained in:
parent
20e8f994a1
commit
817abb732d
1 changed files with 400 additions and 267 deletions
|
|
@ -1,337 +1,470 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ReservoirChecklist from '../components/walkthrough/ReservoirChecklist';
|
import { settingsApi, WalkthroughSettings } from '../lib/settingsApi';
|
||||||
import IrrigationChecklist from '../components/walkthrough/IrrigationChecklist';
|
import { walkthroughApi, ReservoirCheckData, IrrigationCheckData, PlantHealthCheckData } from '../lib/walkthroughApi';
|
||||||
import PlantHealthChecklist from '../components/walkthrough/PlantHealthChecklist';
|
|
||||||
import { settingsApi, WalkthroughSettings, PhotoRequirement } from '../lib/settingsApi';
|
|
||||||
import {
|
import {
|
||||||
walkthroughApi,
|
ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug,
|
||||||
ReservoirCheckData,
|
Camera, X, Minus, Plus, ChevronDown, ChevronUp
|
||||||
IrrigationCheckData,
|
} from 'lucide-react';
|
||||||
PlantHealthCheckData
|
|
||||||
} from '../lib/walkthroughApi';
|
|
||||||
import { ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug, ChevronRight, Clock } from 'lucide-react';
|
|
||||||
import { useToast } from '../context/ToastContext';
|
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() {
|
export default function DailyWalkthroughPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const [currentStep, setCurrentStep] = useState<Step>('start');
|
|
||||||
const [walkthroughId, setWalkthroughId] = useState<string | null>(null);
|
const [walkthroughId, setWalkthroughId] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isStarting, setIsStarting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const [reservoirChecks, setReservoirChecks] = useState<ReservoirCheckData[]>([]);
|
|
||||||
const [irrigationChecks, setIrrigationChecks] = useState<IrrigationCheckData[]>([]);
|
|
||||||
const [plantHealthChecks, setPlantHealthChecks] = useState<PlantHealthCheckData[]>([]);
|
|
||||||
const [settings, setSettings] = useState<WalkthroughSettings | null>(null);
|
const [settings, setSettings] = useState<WalkthroughSettings | null>(null);
|
||||||
|
|
||||||
|
// All check states
|
||||||
|
const [reservoirChecks, setReservoirChecks] = useState<Record<string, ReservoirCheckData>>({});
|
||||||
|
const [irrigationChecks, setIrrigationChecks] = useState<Record<string, IrrigationCheckData>>({});
|
||||||
|
const [plantHealthChecks, setPlantHealthChecks] = useState<Record<string, PlantHealthCheckData>>({});
|
||||||
|
|
||||||
|
// Section expansion
|
||||||
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
||||||
|
reservoirs: true,
|
||||||
|
irrigation: true,
|
||||||
|
plantHealth: true,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
settingsApi.getWalkthrough().then(setSettings).catch(console.error);
|
settingsApi.getWalkthrough().then(setSettings).catch(console.error);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isPhotoRequired = (type: 'reservoir' | 'irrigation' | 'plantHealth') => {
|
const toggleSection = (section: string) => {
|
||||||
if (!settings) return false;
|
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
|
||||||
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 getNextStep = (current: Step): Step => {
|
const handleStart = async () => {
|
||||||
if (!settings) return 'summary';
|
setIsStarting(true);
|
||||||
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);
|
|
||||||
try {
|
try {
|
||||||
const walkthrough = await walkthroughApi.create();
|
const walkthrough = await walkthroughApi.create();
|
||||||
setWalkthroughId(walkthrough.id);
|
setWalkthroughId(walkthrough.id);
|
||||||
setCurrentStep(getNextStep('start'));
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || 'Failed to start walkthrough');
|
addToast('Failed to start walkthrough', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsStarting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReservoirComplete = async (checks: ReservoirCheckData[]) => {
|
const handleSubmit = async () => {
|
||||||
if (!walkthroughId) return;
|
if (!walkthroughId) return;
|
||||||
setIsLoading(true);
|
setIsSubmitting(true);
|
||||||
setError(null);
|
|
||||||
try {
|
try {
|
||||||
for (const check of checks) {
|
// Submit all checks
|
||||||
|
for (const check of Object.values(reservoirChecks)) {
|
||||||
await walkthroughApi.addReservoirCheck(walkthroughId, check);
|
await walkthroughApi.addReservoirCheck(walkthroughId, check);
|
||||||
}
|
}
|
||||||
setReservoirChecks(checks);
|
for (const check of Object.values(irrigationChecks)) {
|
||||||
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) {
|
|
||||||
await walkthroughApi.addIrrigationCheck(walkthroughId, check);
|
await walkthroughApi.addIrrigationCheck(walkthroughId, check);
|
||||||
}
|
}
|
||||||
setIrrigationChecks(checks);
|
for (const check of Object.values(plantHealthChecks)) {
|
||||||
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) {
|
|
||||||
await walkthroughApi.addPlantHealthCheck(walkthroughId, check);
|
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);
|
await walkthroughApi.complete(walkthroughId);
|
||||||
addToast('Walkthrough completed!', 'success');
|
addToast('Walkthrough completed!', 'success');
|
||||||
navigate('/', { state: { message: 'Daily walkthrough completed!' } });
|
navigate('/');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || 'Failed to complete walkthrough');
|
addToast('Failed to submit walkthrough', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render current step
|
const totalChecks = Object.keys(reservoirChecks).length + Object.keys(irrigationChecks).length + Object.keys(plantHealthChecks).length;
|
||||||
if (currentStep === 'reservoir') {
|
const requiredChecks = TANKS.length + ZONES.length + HEALTH_ZONES.length;
|
||||||
return (
|
const isComplete = totalChecks === requiredChecks;
|
||||||
<ReservoirChecklist
|
|
||||||
onComplete={handleReservoirComplete}
|
|
||||||
onBack={() => setCurrentStep('start')}
|
|
||||||
isPhotoRequired={isPhotoRequired('reservoir')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStep === 'irrigation') {
|
|
||||||
return (
|
|
||||||
<IrrigationChecklist
|
|
||||||
onComplete={handleIrrigationComplete}
|
|
||||||
onBack={() => setCurrentStep('reservoir')}
|
|
||||||
isPhotoRequired={isPhotoRequired('irrigation')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStep === 'plant-health') {
|
|
||||||
return (
|
|
||||||
<PlantHealthChecklist
|
|
||||||
onComplete={handlePlantHealthComplete}
|
|
||||||
onBack={() => 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;
|
|
||||||
|
|
||||||
|
// Pre-start view
|
||||||
|
if (!walkthroughId) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||||
<div className="w-full max-w-3xl">
|
<div className="w-full max-w-sm text-center">
|
||||||
{/* Compact header */}
|
<h1 className="text-xl font-semibold text-primary mb-2">Daily Walkthrough</h1>
|
||||||
<div className="text-center mb-8">
|
<p className="text-sm text-tertiary mb-6">
|
||||||
<h1 className="text-xl font-semibold text-primary">Review & Submit</h1>
|
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary stats */}
|
|
||||||
<div className="flex justify-center gap-6 mb-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-3xl font-semibold text-primary">{totalChecks}</div>
|
|
||||||
<div className="text-xs text-tertiary uppercase tracking-wider">Checks</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-px bg-subtle" />
|
|
||||||
<div className="text-center">
|
|
||||||
<div className={`text-3xl font-semibold ${issues > 0 ? 'text-warning' : 'text-success'}`}>{issues}</div>
|
|
||||||
<div className="text-xs text-tertiary uppercase tracking-wider">Issues</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compact summary cards */}
|
|
||||||
<div className="grid grid-cols-3 gap-3 mb-6">
|
|
||||||
<SummaryCard
|
|
||||||
icon={Droplets}
|
|
||||||
label="Reservoirs"
|
|
||||||
count={reservoirChecks.length}
|
|
||||||
status={reservoirChecks.every(c => c.status === 'OK') ? 'good' : 'warning'}
|
|
||||||
/>
|
|
||||||
<SummaryCard
|
|
||||||
icon={Sprout}
|
|
||||||
label="Irrigation"
|
|
||||||
count={irrigationChecks.length}
|
|
||||||
status={irrigationChecks.every(c => c.waterFlow && c.nutrientsMixed) ? 'good' : 'warning'}
|
|
||||||
/>
|
|
||||||
<SummaryCard
|
|
||||||
icon={Bug}
|
|
||||||
label="Plant Health"
|
|
||||||
count={plantHealthChecks.length}
|
|
||||||
status={plantHealthChecks.every(c => c.healthStatus === 'GOOD' && !c.pestsObserved) ? 'good' : 'warning'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mb-4 p-3 bg-destructive-muted text-destructive rounded-md text-sm flex items-center gap-2">
|
|
||||||
<AlertCircle size={14} />
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentStep('plant-health')}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="btn btn-secondary px-6"
|
|
||||||
>
|
|
||||||
<ArrowLeft size={14} />
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmitWalkthrough}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="btn btn-primary flex-1"
|
|
||||||
>
|
|
||||||
{isLoading ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />}
|
|
||||||
{isLoading ? 'Submitting...' : 'Complete Walkthrough'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start Screen - Refined
|
|
||||||
const today = new Date();
|
|
||||||
const greeting = today.getHours() < 12 ? 'Good morning' : today.getHours() < 17 ? 'Good afternoon' : 'Good evening';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
{/* Minimal header */}
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<p className="text-sm text-tertiary mb-1">{today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}</p>
|
|
||||||
<h1 className="text-xl font-semibold text-primary">{greeting}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Task list - tight and intentional */}
|
|
||||||
<div className="card overflow-hidden">
|
|
||||||
<div className="p-4 border-b border-subtle">
|
|
||||||
<h2 className="text-sm font-medium text-primary">Daily Walkthrough</h2>
|
|
||||||
<p className="text-xs text-tertiary flex items-center gap-1 mt-0.5">
|
|
||||||
<Clock size={10} />
|
|
||||||
~15 min
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-subtle">
|
|
||||||
<StepRow icon={Droplets} label="Reservoir Checks" />
|
|
||||||
<StepRow icon={Sprout} label="Irrigation System" />
|
|
||||||
<StepRow icon={Bug} label="Plant Health" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-destructive-muted text-destructive text-xs flex items-center gap-2">
|
|
||||||
<AlertCircle size={12} />
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="p-4">
|
|
||||||
<button
|
<button
|
||||||
onClick={handleStartWalkthrough}
|
onClick={handleStart}
|
||||||
disabled={isLoading}
|
disabled={isStarting}
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
>
|
>
|
||||||
{isLoading ? <Loader2 size={14} className="animate-spin" /> : 'Begin'}
|
{isStarting ? <Loader2 size={14} className="animate-spin" /> : 'Begin Walkthrough'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto pb-24 space-y-4 animate-in">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<button onClick={() => navigate('/')} className="p-2 -ml-2 hover:bg-tertiary rounded-md">
|
||||||
|
<ArrowLeft size={16} className="text-secondary" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold text-primary">Daily Walkthrough</h1>
|
||||||
|
<p className="text-xs text-tertiary">{totalChecks}/{requiredChecks} checks completed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reservoirs Section */}
|
||||||
|
{settings?.enableReservoirs !== false && (
|
||||||
|
<CollapsibleSection
|
||||||
|
title="Reservoirs"
|
||||||
|
icon={Droplets}
|
||||||
|
count={Object.keys(reservoirChecks).length}
|
||||||
|
total={TANKS.length}
|
||||||
|
expanded={expandedSections.reservoirs}
|
||||||
|
onToggle={() => toggleSection('reservoirs')}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{TANKS.map(tank => (
|
||||||
|
<ReservoirRow
|
||||||
|
key={tank.name}
|
||||||
|
tank={tank}
|
||||||
|
data={reservoirChecks[tank.name]}
|
||||||
|
onChange={(data) => setReservoirChecks(prev => ({ ...prev, [tank.name]: data }))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Irrigation Section */}
|
||||||
|
{settings?.enableIrrigation !== false && (
|
||||||
|
<CollapsibleSection
|
||||||
|
title="Irrigation"
|
||||||
|
icon={Sprout}
|
||||||
|
count={Object.keys(irrigationChecks).length}
|
||||||
|
total={ZONES.length}
|
||||||
|
expanded={expandedSections.irrigation}
|
||||||
|
onToggle={() => toggleSection('irrigation')}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{ZONES.map(zone => (
|
||||||
|
<IrrigationRow
|
||||||
|
key={zone.name}
|
||||||
|
zone={zone}
|
||||||
|
data={irrigationChecks[zone.name]}
|
||||||
|
onChange={(data) => setIrrigationChecks(prev => ({ ...prev, [zone.name]: data }))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Plant Health Section */}
|
||||||
|
{settings?.enablePlantHealth !== false && (
|
||||||
|
<CollapsibleSection
|
||||||
|
title="Plant Health"
|
||||||
|
icon={Bug}
|
||||||
|
count={Object.keys(plantHealthChecks).length}
|
||||||
|
total={HEALTH_ZONES.length}
|
||||||
|
expanded={expandedSections.plantHealth}
|
||||||
|
onToggle={() => toggleSection('plantHealth')}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{HEALTH_ZONES.map(zone => (
|
||||||
|
<PlantHealthRow
|
||||||
|
key={zone}
|
||||||
|
zoneName={zone}
|
||||||
|
data={plantHealthChecks[zone]}
|
||||||
|
onChange={(data) => setPlantHealthChecks(prev => ({ ...prev, [zone]: data }))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fixed Submit Button */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 p-4 bg-primary border-t border-default">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!isComplete || isSubmitting}
|
||||||
|
className="btn btn-primary w-full"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<><Loader2 size={14} className="animate-spin" /> Submitting...</>
|
||||||
|
) : isComplete ? (
|
||||||
|
<><Check size={14} /> Submit Walkthrough</>
|
||||||
|
) : (
|
||||||
|
`Complete all checks (${totalChecks}/${requiredChecks})`
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact step row
|
// Collapsible Section
|
||||||
function StepRow({ icon: Icon, label }: { icon: typeof Droplets; label: string }) {
|
function CollapsibleSection({
|
||||||
return (
|
title, icon: Icon, count, total, expanded, onToggle, children
|
||||||
<div className="flex items-center gap-3 px-4 py-3">
|
}: {
|
||||||
<div className="w-7 h-7 rounded-md bg-tertiary flex items-center justify-center">
|
title: string;
|
||||||
<Icon size={14} className="text-secondary" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-primary flex-1">{label}</span>
|
|
||||||
<ChevronRight size={14} className="text-tertiary" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary card
|
|
||||||
function SummaryCard({ icon: Icon, label, count, status }: {
|
|
||||||
icon: typeof Droplets;
|
icon: typeof Droplets;
|
||||||
label: string;
|
|
||||||
count: number;
|
count: number;
|
||||||
status: 'good' | 'warning';
|
total: number;
|
||||||
|
expanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const isComplete = count === total;
|
||||||
return (
|
return (
|
||||||
<div className="card p-4 text-center">
|
<div className="card overflow-hidden">
|
||||||
<div className={`w-8 h-8 rounded-md mx-auto mb-2 flex items-center justify-center ${status === 'good' ? 'bg-success-muted text-success' : 'bg-warning-muted text-warning'
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="w-full flex items-center justify-between p-4 hover:bg-tertiary transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-8 h-8 rounded-md flex items-center justify-center ${isComplete ? 'bg-success-muted text-success' : 'bg-accent-muted text-accent'
|
||||||
}`}>
|
}`}>
|
||||||
<Icon size={14} />
|
{isComplete ? <Check size={14} /> : <Icon size={14} />}
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<h3 className="text-sm font-medium text-primary">{title}</h3>
|
||||||
|
<p className="text-xs text-tertiary">{count}/{total} complete</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{expanded ? <ChevronUp size={16} className="text-tertiary" /> : <ChevronDown size={16} className="text-tertiary" />}
|
||||||
|
</button>
|
||||||
|
{expanded && <div className="p-4 pt-0 border-t border-subtle">{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="flex items-center justify-between p-3 bg-tertiary rounded-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-2 h-8 rounded-full ${data.status === 'OK' ? 'bg-success' : data.status === 'LOW' ? 'bg-warning' : 'bg-destructive'
|
||||||
|
}`} />
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-primary">{tank.name}</span>
|
||||||
|
<span className="text-xs text-tertiary ml-2">{data.levelPercent}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setEditing(true)} className="text-xs text-accent">Edit</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 bg-tertiary rounded-md">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium text-primary">{tank.name}</span>
|
||||||
|
<span className="text-xs text-tertiary">{tank.type}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-3 h-12 rounded-full overflow-hidden bg-subtle relative`}>
|
||||||
|
<div
|
||||||
|
className={`absolute bottom-0 left-0 right-0 transition-all ${getStatus(level) === 'OK' ? 'bg-success' : getStatus(level) === 'LOW' ? 'bg-warning' : 'bg-destructive'
|
||||||
|
}`}
|
||||||
|
style={{ height: `${level}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={level}
|
||||||
|
onChange={(e) => setLevel(parseInt(e.target.value))}
|
||||||
|
className="flex-1 h-1.5 bg-subtle rounded-full appearance-none cursor-pointer accent-accent"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-primary w-12 text-right">{level}%</span>
|
||||||
|
<button onClick={handleSave} className="btn btn-primary h-8 px-3 text-xs">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="flex items-center justify-between p-3 bg-tertiary rounded-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-2 h-8 rounded-full ${issues ? 'bg-warning' : 'bg-success'}`} />
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-primary">{zone.name}</span>
|
||||||
|
<span className="text-xs text-tertiary ml-2">{data.drippersWorking}/{data.drippersTotal}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setEditing(true)} className="text-xs text-accent">Edit</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 bg-tertiary rounded-md space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-primary">{zone.name}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button onClick={() => setWorking(Math.max(0, working - 1))} className="w-6 h-6 bg-subtle rounded flex items-center justify-center">
|
||||||
|
<Minus size={12} />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm w-16 text-center">{working}/{zone.drippers}</span>
|
||||||
|
<button onClick={() => setWorking(Math.min(zone.drippers, working + 1))} className="w-6 h-6 bg-subtle rounded flex items-center justify-center">
|
||||||
|
<Plus size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<label className="flex items-center gap-2 text-xs">
|
||||||
|
<input type="checkbox" checked={waterFlow} onChange={() => setWaterFlow(!waterFlow)} className="w-4 h-4 rounded" />
|
||||||
|
Water
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-xs">
|
||||||
|
<input type="checkbox" checked={nutrients} onChange={() => setNutrients(!nutrients)} className="w-4 h-4 rounded" />
|
||||||
|
Nutrients
|
||||||
|
</label>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<button onClick={handleSave} className="btn btn-primary h-8 px-3 text-xs">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="flex items-center justify-between p-3 bg-tertiary rounded-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-2 h-8 rounded-full ${data.healthStatus === 'GOOD' && !data.pestsObserved ? 'bg-success' :
|
||||||
|
data.healthStatus === 'FAIR' ? 'bg-warning' : 'bg-destructive'
|
||||||
|
}`} />
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-primary">{zoneName}</span>
|
||||||
|
<span className="text-xs text-tertiary ml-2">{data.healthStatus}</span>
|
||||||
|
{data.pestsObserved && <span className="text-xs text-destructive ml-2">🐛 Pests</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setEditing(true)} className="text-xs text-accent">Edit</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 bg-tertiary rounded-md space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-primary">{zoneName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{(['GOOD', 'FAIR', 'NEEDS_ATTENTION'] as const).map(s => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
onClick={() => setHealth(s)}
|
||||||
|
className={`flex-1 py-1.5 rounded text-xs font-medium transition-colors ${health === s
|
||||||
|
? s === 'GOOD' ? 'bg-success text-white'
|
||||||
|
: s === 'FAIR' ? 'bg-warning text-white'
|
||||||
|
: 'bg-destructive text-white'
|
||||||
|
: 'bg-subtle text-secondary hover:bg-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{s === 'NEEDS_ATTENTION' ? 'Attention' : s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="flex items-center gap-2 text-xs">
|
||||||
|
<input type="checkbox" checked={pests} onChange={() => setPests(!pests)} className="w-4 h-4 rounded" />
|
||||||
|
Pests observed
|
||||||
|
</label>
|
||||||
|
<button onClick={handleSave} className="btn btn-primary h-8 px-3 text-xs">Save</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-semibold text-primary">{count}</div>
|
|
||||||
<div className="text-[10px] text-tertiary uppercase tracking-wider">{label}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue