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 { 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<Step>('start');
|
||||
const [walkthroughId, setWalkthroughId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [reservoirChecks, setReservoirChecks] = useState<ReservoirCheckData[]>([]);
|
||||
const [irrigationChecks, setIrrigationChecks] = useState<IrrigationCheckData[]>([]);
|
||||
const [plantHealthChecks, setPlantHealthChecks] = useState<PlantHealthCheckData[]>([]);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
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(() => {
|
||||
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 (
|
||||
<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;
|
||||
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 (
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-3xl">
|
||||
{/* Compact header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-xl font-semibold text-primary">Review & Submit</h1>
|
||||
</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
|
||||
<div className="w-full max-w-sm text-center">
|
||||
<h1 className="text-xl font-semibold text-primary mb-2">Daily Walkthrough</h1>
|
||||
<p className="text-sm text-tertiary mb-6">
|
||||
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
|
||||
</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
|
||||
onClick={handleStartWalkthrough}
|
||||
disabled={isLoading}
|
||||
onClick={handleStart}
|
||||
disabled={isStarting}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Compact step row
|
||||
function StepRow({ icon: Icon, label }: { icon: typeof Droplets; label: string }) {
|
||||
return (
|
||||
<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">
|
||||
<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 }: {
|
||||
// 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 (
|
||||
<div className="card p-4 text-center">
|
||||
<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'
|
||||
<div className="card overflow-hidden">
|
||||
<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 className="text-lg font-semibold text-primary">{count}</div>
|
||||
<div className="text-[10px] text-tertiary uppercase tracking-wider">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue