fix: intentional walkthrough UI redesign
- 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)
This commit is contained in:
parent
efb298e119
commit
a2120170b6
5 changed files with 429 additions and 699 deletions
|
|
@ -122,16 +122,8 @@ export default function Layout() {
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="mt-auto">
|
<div className="mt-auto p-3 border-t border-default">
|
||||||
{/* Theme Toggle */}
|
<UserMenu />
|
||||||
<div className="px-4 py-3 border-t border-default">
|
|
||||||
<ThemeToggle />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User Menu */}
|
|
||||||
<div className="p-3 border-t border-default">
|
|
||||||
<UserMenu />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ArrowLeft, ChevronRight, Camera, X, Check, AlertTriangle, Minus, Plus } from 'lucide-react';
|
import { ArrowLeft, ChevronRight, Camera, X, Check, Minus, Plus } from 'lucide-react';
|
||||||
|
|
||||||
interface Zone {
|
interface Zone {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -83,206 +83,120 @@ export default function IrrigationChecklist({ onComplete, onBack, isPhotoRequire
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="w-full max-w-sm">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 md:mb-10">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<button onClick={onBack} className="p-2 -ml-2 hover:bg-tertiary rounded-md transition-colors">
|
||||||
<button
|
<ArrowLeft size={16} className="text-secondary" />
|
||||||
onClick={onBack}
|
</button>
|
||||||
className="btn btn-secondary p-2"
|
<div className="flex-1">
|
||||||
aria-label="Back"
|
<p className="text-xs text-tertiary">Zone {currentZoneIndex + 1}/{zones.length}</p>
|
||||||
>
|
<h1 className="text-lg font-semibold text-primary">{currentZone.name}</h1>
|
||||||
<ArrowLeft size={18} />
|
|
||||||
</button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Irrigation System</h1>
|
|
||||||
<p className="text-secondary text-sm mt-0.5">
|
|
||||||
Zone {currentZoneIndex + 1} of {zones.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress */}
|
||||||
<div className="h-1 bg-tertiary rounded-full overflow-hidden">
|
<div className="flex gap-1 mb-6">
|
||||||
|
{zones.map((_, i) => (
|
||||||
<div
|
<div
|
||||||
className="h-full bg-accent transition-all duration-normal"
|
key={i}
|
||||||
style={{ width: `${((currentZoneIndex + 1) / zones.length) * 100}%` }}
|
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>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Main Content - Two Column on Desktop */}
|
{/* Photo */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
{(isPhotoRequired || !allGood) && (
|
||||||
{/* Left Column - Drippers & Status */}
|
<div className="mb-4">
|
||||||
<div className="space-y-6">
|
<input type="file" id="photo-upload" className="hidden" accept="image/*" capture="environment"
|
||||||
{/* Zone Info Card */}
|
onChange={(e) => { if (e.target.files?.[0]) setPhoto(URL.createObjectURL(e.target.files[0])); }} />
|
||||||
<div className="card p-6 md:p-8">
|
{photo ? (
|
||||||
<div className="text-center mb-6">
|
<div className="relative">
|
||||||
<div className="text-4xl mb-3">🚿</div>
|
<img src={photo} alt="Preview" className="w-full h-24 object-cover rounded-md" />
|
||||||
<h2 className="text-xl font-semibold text-primary">{currentZone.name}</h2>
|
<button onClick={() => setPhoto(null)} className="absolute top-1 right-1 p-1 bg-black/50 rounded-full">
|
||||||
<p className="text-sm text-tertiary">{currentZone.defaultDrippers} drippers total</p>
|
<X size={12} className="text-white" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
{/* Status Badge */}
|
<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'
|
||||||
<div className={`flex items-center justify-center gap-2 py-3 rounded-md font-medium ${allGood
|
|
||||||
? 'bg-success-muted text-success'
|
|
||||||
: 'bg-warning-muted text-warning'
|
|
||||||
}`}>
|
}`}>
|
||||||
{allGood ? <Check size={16} /> : <AlertTriangle size={16} />}
|
<Camera size={14} />
|
||||||
{allGood ? 'All Systems Operational' : 'Issues Detected'}
|
{isPhotoRequired ? 'Photo required' : 'Add photo'}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Drippers Counter */}
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-4">
|
|
||||||
Drippers Working
|
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setDrippersWorking(Math.max(0, drippersWorking - 1))}
|
|
||||||
className="btn btn-secondary w-12 h-12 p-0"
|
|
||||||
>
|
|
||||||
<Minus size={18} />
|
|
||||||
</button>
|
|
||||||
<div className="flex-1 text-center">
|
|
||||||
<div className="text-4xl font-semibold text-primary">{drippersWorking}</div>
|
|
||||||
{drippersFailed > 0 && (
|
|
||||||
<div className="text-sm text-destructive mt-1">{drippersFailed} failed</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setDrippersWorking(Math.min(currentZone.defaultDrippers, drippersWorking + 1))}
|
|
||||||
className="btn btn-secondary w-12 h-12 p-0"
|
|
||||||
>
|
|
||||||
<Plus size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - System Checks */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* System Status Toggles */}
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-4">System Status</label>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<StatusToggle
|
|
||||||
label="Water Flow"
|
|
||||||
value={waterFlow}
|
|
||||||
onChange={setWaterFlow}
|
|
||||||
/>
|
|
||||||
<StatusToggle
|
|
||||||
label="Nutrients Mixed"
|
|
||||||
value={nutrientsMixed}
|
|
||||||
onChange={setNutrientsMixed}
|
|
||||||
/>
|
|
||||||
<StatusToggle
|
|
||||||
label="Schedule Active"
|
|
||||||
value={scheduleActive}
|
|
||||||
onChange={setScheduleActive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Issues / Notes */}
|
|
||||||
{!allGood && (
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-2">Issues / Notes</label>
|
|
||||||
<textarea
|
|
||||||
value={issues}
|
|
||||||
onChange={(e) => setIssues(e.target.value)}
|
|
||||||
placeholder="Describe any issues found..."
|
|
||||||
className="input w-full resize-none"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Photo Upload */}
|
|
||||||
{(isPhotoRequired || !allGood) && (
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-2">
|
|
||||||
{isPhotoRequired ? 'Photo (Required)' : 'Photo (Recommended)'}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<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 max-h-48 object-cover rounded-md" />
|
|
||||||
<button
|
|
||||||
onClick={() => setPhoto(null)}
|
|
||||||
className="absolute top-2 right-2 p-1.5 bg-destructive text-white rounded-full shadow-lg"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<label
|
|
||||||
htmlFor="photo-upload"
|
|
||||||
className={`flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-md cursor-pointer transition-colors ${isPhotoRequired
|
|
||||||
? 'border-destructive/50 bg-destructive-muted hover:border-destructive'
|
|
||||||
: 'border-subtle hover:border-default hover:bg-tertiary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Camera size={24} className="text-tertiary mb-2" />
|
|
||||||
<span className="text-sm font-medium text-primary">Tap to capture</span>
|
|
||||||
<span className="text-xs text-tertiary">System status photo</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button onClick={onBack} className="btn btn-secondary flex-1 h-12 md:h-14">
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button onClick={handleNext} className="btn btn-primary flex-1 h-12 md:h-14">
|
|
||||||
{isLastZone ? 'Complete' : 'Next Zone'}
|
|
||||||
{!isLastZone && <ChevronRight size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
|
<button onClick={handleNext} className="btn btn-primary w-full">
|
||||||
|
{isLastZone ? 'Complete' : 'Next'}
|
||||||
|
{!isLastZone && <ChevronRight size={14} />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status Toggle Component
|
function StatusRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
|
||||||
function StatusToggle({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: boolean;
|
|
||||||
onChange: (val: boolean) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange(!value)}
|
onClick={() => onChange(!value)}
|
||||||
className={`w-full flex items-center justify-between p-4 rounded-md border transition-colors ${value
|
className="w-full flex items-center justify-between px-4 py-3 hover:bg-tertiary transition-colors"
|
||||||
? 'bg-success-muted border-success/30 text-success'
|
|
||||||
: 'bg-destructive-muted border-destructive/30 text-destructive'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className="font-medium text-primary">{label}</span>
|
<span className="text-sm text-primary">{label}</span>
|
||||||
<span className="text-lg">{value ? '✓' : '✗'}</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>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ArrowLeft, ChevronRight, Camera, X, Check, Bug, Droplets, Utensils } from 'lucide-react';
|
import { ArrowLeft, ChevronRight, Camera, X, Check, Bug } from 'lucide-react';
|
||||||
|
|
||||||
interface PlantHealthCheckData {
|
interface PlantHealthCheckData {
|
||||||
zoneName: string;
|
zoneName: string;
|
||||||
|
|
@ -73,232 +73,149 @@ export default function PlantHealthChecklist({ onComplete, onBack, isPhotoRequir
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const healthOptions = [
|
|
||||||
{ value: 'GOOD' as const, label: 'Good', emoji: '😊', color: 'bg-success' },
|
|
||||||
{ value: 'FAIR' as const, label: 'Fair', emoji: '😐', color: 'bg-warning' },
|
|
||||||
{ value: 'NEEDS_ATTENTION' as const, label: 'Needs Attention', emoji: '😟', color: 'bg-destructive' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="w-full max-w-sm">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 md:mb-10">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<button onClick={onBack} className="p-2 -ml-2 hover:bg-tertiary rounded-md transition-colors">
|
||||||
<button onClick={onBack} className="btn btn-secondary p-2" aria-label="Back">
|
<ArrowLeft size={16} className="text-secondary" />
|
||||||
<ArrowLeft size={18} />
|
</button>
|
||||||
</button>
|
<div className="flex-1">
|
||||||
<div className="flex-1">
|
<p className="text-xs text-tertiary">Zone {currentZoneIndex + 1}/{zones.length}</p>
|
||||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Plant Health Check</h1>
|
<h1 className="text-lg font-semibold text-primary">{currentZone}</h1>
|
||||||
<p className="text-secondary text-sm mt-0.5">
|
|
||||||
Zone {currentZoneIndex + 1} of {zones.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress */}
|
||||||
<div className="h-1 bg-tertiary rounded-full overflow-hidden">
|
<div className="flex gap-1 mb-6">
|
||||||
|
{zones.map((_, i) => (
|
||||||
<div
|
<div
|
||||||
className="h-full bg-accent transition-all duration-normal"
|
key={i}
|
||||||
style={{ width: `${((currentZoneIndex + 1) / zones.length) * 100}%` }}
|
className={`h-1 flex-1 rounded-full ${i <= currentZoneIndex ? 'bg-accent' : 'bg-tertiary'}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Health Status - compact segmented control */}
|
||||||
|
<div className="card p-4 mb-4">
|
||||||
|
<p className="text-xs text-tertiary mb-2">Overall Health</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(['GOOD', 'FAIR', 'NEEDS_ATTENTION'] as const).map(status => (
|
||||||
|
<button
|
||||||
|
key={status}
|
||||||
|
onClick={() => setHealthStatus(status)}
|
||||||
|
className={`flex-1 py-2 rounded-md text-xs font-medium transition-colors ${healthStatus === status
|
||||||
|
? status === 'GOOD' ? 'bg-success text-white'
|
||||||
|
: status === 'FAIR' ? 'bg-warning text-white'
|
||||||
|
: 'bg-destructive text-white'
|
||||||
|
: 'bg-tertiary text-secondary hover:bg-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status === 'NEEDS_ATTENTION' ? 'Attention' : status.charAt(0) + status.slice(1).toLowerCase()}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pest Check */}
|
||||||
|
<div className="card divide-y divide-subtle mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setPestsObserved(!pestsObserved)}
|
||||||
|
className="w-full flex items-center justify-between px-4 py-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bug size={14} className={pestsObserved ? 'text-destructive' : 'text-tertiary'} />
|
||||||
|
<span className="text-sm text-primary">Pests Observed</span>
|
||||||
|
</div>
|
||||||
|
<div className={`w-5 h-5 rounded flex items-center justify-center ${pestsObserved ? 'bg-destructive text-white' : 'bg-success text-white'
|
||||||
|
}`}>
|
||||||
|
{pestsObserved ? '!' : <Check size={12} />}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{pestsObserved && (
|
||||||
|
<div className="px-4 py-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={pestType}
|
||||||
|
onChange={(e) => setPestType(e.target.value)}
|
||||||
|
placeholder="Type of pest..."
|
||||||
|
className="input w-full text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Access Status */}
|
||||||
|
<div className="card divide-y divide-subtle mb-4">
|
||||||
|
<StatusRow
|
||||||
|
label="Water Access"
|
||||||
|
value={waterAccess === 'OK'}
|
||||||
|
onChange={(v) => setWaterAccess(v ? 'OK' : 'ISSUES')}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Nutrient Access"
|
||||||
|
value={foodAccess === 'OK'}
|
||||||
|
onChange={(v) => setFoodAccess(v ? 'OK' : 'ISSUES')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes - only if issues */}
|
||||||
|
{hasIssues && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={notes}
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
placeholder="Notes..."
|
||||||
|
className="input w-full text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Main Content - Two Column on Desktop */}
|
{/* Photo */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
{(isPhotoRequired || hasIssues) && (
|
||||||
{/* Left Column - Health Status */}
|
<div className="mb-4">
|
||||||
<div className="space-y-6">
|
<input type="file" id="photo-upload" className="hidden" accept="image/*" capture="environment"
|
||||||
{/* Zone Info */}
|
onChange={(e) => { if (e.target.files?.[0]) setReferencePhoto(URL.createObjectURL(e.target.files[0])); }} />
|
||||||
<div className="card p-6 md:p-8 text-center">
|
{referencePhoto ? (
|
||||||
<div className="text-4xl mb-3">🌱</div>
|
<div className="relative">
|
||||||
<h2 className="text-xl font-semibold text-primary">{currentZone}</h2>
|
<img src={referencePhoto} alt="Preview" className="w-full h-24 object-cover rounded-md" />
|
||||||
<p className="text-sm text-tertiary">Quick visual inspection</p>
|
<button onClick={() => setReferencePhoto(null)} className="absolute top-1 right-1 p-1 bg-black/50 rounded-full">
|
||||||
</div>
|
<X size={12} className="text-white" />
|
||||||
|
|
||||||
{/* Overall Health */}
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-4">Overall Health</label>
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
{healthOptions.map((option) => (
|
|
||||||
<button
|
|
||||||
key={option.value}
|
|
||||||
onClick={() => setHealthStatus(option.value)}
|
|
||||||
className={`p-4 rounded-md border transition-all ${healthStatus === option.value
|
|
||||||
? `${option.color} border-transparent text-white`
|
|
||||||
: 'bg-tertiary border-subtle text-secondary hover:bg-secondary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="text-2xl mb-1">{option.emoji}</div>
|
|
||||||
<div className="text-xs font-medium">{option.label}</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pest Check */}
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-4">Pests Observed?</label>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setPestsObserved(false)}
|
|
||||||
className={`p-4 rounded-md border transition-all ${!pestsObserved
|
|
||||||
? 'bg-success border-transparent text-white'
|
|
||||||
: 'bg-tertiary border-subtle text-secondary hover:bg-secondary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Check size={20} className="mx-auto mb-1" />
|
|
||||||
<div className="text-sm font-medium">No Pests</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPestsObserved(true)}
|
|
||||||
className={`p-4 rounded-md border transition-all ${pestsObserved
|
|
||||||
? 'bg-destructive border-transparent text-white'
|
|
||||||
: 'bg-tertiary border-subtle text-secondary hover:bg-secondary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Bug size={20} className="mx-auto mb-1" />
|
|
||||||
<div className="text-sm font-medium">Pests Found</div>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
{pestsObserved && (
|
<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'
|
||||||
<input
|
}`}>
|
||||||
type="text"
|
<Camera size={14} />
|
||||||
value={pestType}
|
{isPhotoRequired ? 'Photo required' : 'Add photo'}
|
||||||
onChange={(e) => setPestType(e.target.value)}
|
|
||||||
placeholder="Type of pest (e.g., spider mites)"
|
|
||||||
className="input w-full mt-3"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - Access & Notes */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Access Status */}
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-4">Access Status</label>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<AccessToggle
|
|
||||||
icon={Droplets}
|
|
||||||
label="Water Access"
|
|
||||||
value={waterAccess === 'OK'}
|
|
||||||
onChange={(val) => setWaterAccess(val ? 'OK' : 'ISSUES')}
|
|
||||||
/>
|
|
||||||
<AccessToggle
|
|
||||||
icon={Utensils}
|
|
||||||
label="Food/Nutrient Access"
|
|
||||||
value={foodAccess === 'OK'}
|
|
||||||
onChange={(val) => setFoodAccess(val ? 'OK' : 'ISSUES')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notes */}
|
|
||||||
{hasIssues && (
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-2">
|
|
||||||
Notes / Observations
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={notes}
|
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
|
||||||
placeholder="Describe any issues..."
|
|
||||||
className="input w-full resize-none"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Photo Upload */}
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-2">
|
|
||||||
{isPhotoRequired ? 'Photo (Required)' : 'Reference Photo'}
|
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="ref-photo-upload"
|
|
||||||
className="hidden"
|
|
||||||
accept="image/*"
|
|
||||||
capture="environment"
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.files?.[0]) {
|
|
||||||
setReferencePhoto(URL.createObjectURL(e.target.files[0]));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{referencePhoto ? (
|
|
||||||
<div className="relative">
|
|
||||||
<img src={referencePhoto} alt="Preview" className="w-full max-h-48 object-cover rounded-md" />
|
|
||||||
<button
|
|
||||||
onClick={() => setReferencePhoto(null)}
|
|
||||||
className="absolute top-2 right-2 p-1.5 bg-destructive text-white rounded-full shadow-lg"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<label
|
|
||||||
htmlFor="ref-photo-upload"
|
|
||||||
className={`flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-md cursor-pointer transition-colors ${isPhotoRequired
|
|
||||||
? 'border-destructive/50 bg-destructive-muted hover:border-destructive'
|
|
||||||
: 'border-subtle hover:border-default hover:bg-tertiary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Camera size={24} className="text-tertiary mb-2" />
|
|
||||||
<span className="text-sm font-medium text-primary">Tap to capture</span>
|
|
||||||
<span className="text-xs text-tertiary">Reference photo</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button onClick={onBack} className="btn btn-secondary flex-1 h-12 md:h-14">
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button onClick={handleNext} className="btn btn-primary flex-1 h-12 md:h-14">
|
|
||||||
{isLastZone ? 'Complete' : 'Next Zone'}
|
|
||||||
{!isLastZone && <ChevronRight size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
|
<button onClick={handleNext} className="btn btn-primary w-full">
|
||||||
|
{isLastZone ? 'Complete' : 'Next'}
|
||||||
|
{!isLastZone && <ChevronRight size={14} />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access Toggle Component
|
function StatusRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
|
||||||
function AccessToggle({
|
|
||||||
icon: Icon,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
}: {
|
|
||||||
icon: typeof Droplets;
|
|
||||||
label: string;
|
|
||||||
value: boolean;
|
|
||||||
onChange: (val: boolean) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => onChange(!value)}
|
onClick={() => onChange(!value)}
|
||||||
className={`w-full flex items-center justify-between p-4 rounded-md border transition-colors ${value
|
className="w-full flex items-center justify-between px-4 py-3 hover:bg-tertiary transition-colors"
|
||||||
? 'bg-success-muted border-success/30'
|
|
||||||
: 'bg-destructive-muted border-destructive/30'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<span className="text-sm text-primary">{label}</span>
|
||||||
<Icon size={18} className={value ? 'text-success' : 'text-destructive'} />
|
<div className={`w-5 h-5 rounded flex items-center justify-center ${value ? 'bg-success text-white' : 'bg-destructive text-white'
|
||||||
<span className="font-medium text-primary">{label}</span>
|
}`}>
|
||||||
|
{value ? <Check size={12} /> : <X size={12} />}
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-lg ${value ? 'text-success' : 'text-destructive'}`}>
|
|
||||||
{value ? '✓' : '✗'}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ArrowLeft, ChevronRight, Camera, X, AlertCircle } from 'lucide-react';
|
import { ArrowLeft, ChevronRight, Camera, X } from 'lucide-react';
|
||||||
|
|
||||||
interface Tank {
|
interface Tank {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -75,164 +75,117 @@ export default function ReservoirChecklist({ onComplete, onBack, isPhotoRequired
|
||||||
|
|
||||||
const status = getStatus(levelPercent);
|
const status = getStatus(levelPercent);
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
OK: { color: 'bg-success', text: 'Good', badge: 'badge-success' },
|
OK: { color: 'bg-success', text: 'Good' },
|
||||||
LOW: { color: 'bg-warning', text: 'Low - Needs Refill', badge: 'badge-warning' },
|
LOW: { color: 'bg-warning', text: 'Low' },
|
||||||
CRITICAL: { color: 'bg-destructive', text: 'Critical - Refill Now!', badge: 'badge-destructive' },
|
CRITICAL: { color: 'bg-destructive', text: 'Critical' },
|
||||||
}[status];
|
}[status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="w-full max-w-sm">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 md:mb-10">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<button onClick={onBack} className="p-2 -ml-2 hover:bg-tertiary rounded-md transition-colors">
|
||||||
<button
|
<ArrowLeft size={16} className="text-secondary" />
|
||||||
onClick={onBack}
|
</button>
|
||||||
className="btn btn-secondary p-2"
|
<div className="flex-1">
|
||||||
aria-label="Back"
|
<p className="text-xs text-tertiary">Reservoir {currentTankIndex + 1}/{tanks.length}</p>
|
||||||
>
|
<h1 className="text-lg font-semibold text-primary">{currentTank.name}</h1>
|
||||||
<ArrowLeft size={18} />
|
|
||||||
</button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Reservoir Checks</h1>
|
|
||||||
<p className="text-secondary text-sm mt-0.5">
|
|
||||||
Tank {currentTankIndex + 1} of {tanks.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-xs text-tertiary">{currentTank.type}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress */}
|
||||||
<div className="h-1 bg-tertiary rounded-full overflow-hidden">
|
<div className="flex gap-1 mb-6">
|
||||||
|
{tanks.map((_, i) => (
|
||||||
<div
|
<div
|
||||||
className="h-full bg-accent transition-all duration-normal"
|
key={i}
|
||||||
style={{ width: `${((currentTankIndex + 1) / tanks.length) * 100}%` }}
|
className={`h-1 flex-1 rounded-full ${i <= currentTankIndex ? 'bg-accent' : 'bg-tertiary'}`}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Level Indicator */}
|
||||||
|
<div className="card p-4 mb-4">
|
||||||
|
<div className="relative h-32 bg-tertiary rounded-md overflow-hidden mb-3">
|
||||||
|
<div
|
||||||
|
className={`absolute bottom-0 left-0 right-0 ${statusConfig.color} transition-all duration-200`}
|
||||||
|
style={{ height: `${levelPercent}%` }}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-3xl font-semibold text-white drop-shadow-sm">{levelPercent}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={levelPercent}
|
||||||
|
onChange={(e) => setLevelPercent(parseInt(e.target.value))}
|
||||||
|
className="w-full h-1.5 bg-tertiary rounded-full appearance-none cursor-pointer accent-accent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={`mt-3 text-center text-sm font-medium ${status === 'OK' ? 'text-success' : status === 'LOW' ? 'text-warning' : 'text-destructive'
|
||||||
|
}`}>
|
||||||
|
{statusConfig.text}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content - Two Column on Desktop */}
|
{/* Notes - compact */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
<div className="mb-4">
|
||||||
{/* Left Column - Level Control */}
|
<input
|
||||||
<div className="card p-6 md:p-8">
|
type="text"
|
||||||
<div className="text-center mb-6">
|
value={notes}
|
||||||
<div className="text-4xl mb-3">💧</div>
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
<h2 className="text-xl font-semibold text-primary">{currentTank.name}</h2>
|
placeholder="Notes (optional)"
|
||||||
<p className="text-sm text-tertiary">
|
className="input w-full text-sm"
|
||||||
{currentTank.type === 'VEG' ? 'Vegetative' : 'Flowering'} Tank
|
/>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Visual Level Indicator */}
|
{/* Photo - only show when needed */}
|
||||||
<div className="relative h-40 md:h-48 bg-tertiary rounded-lg overflow-hidden mb-4">
|
{(isPhotoRequired || status !== 'OK') && (
|
||||||
<div
|
<div className="mb-4">
|
||||||
className={`absolute bottom-0 left-0 right-0 ${statusConfig.color} transition-all duration-normal`}
|
|
||||||
style={{ height: `${levelPercent}%` }}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent" />
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="text-4xl md:text-5xl font-semibold text-white drop-shadow-md">
|
|
||||||
{levelPercent}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Slider */}
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="file"
|
||||||
min="0"
|
id="photo-upload"
|
||||||
max="100"
|
className="hidden"
|
||||||
value={levelPercent}
|
accept="image/*"
|
||||||
onChange={(e) => setLevelPercent(parseInt(e.target.value))}
|
capture="environment"
|
||||||
className="w-full h-2 bg-tertiary rounded-full appearance-none cursor-pointer accent-accent mb-4"
|
onChange={(e) => {
|
||||||
|
if (e.target.files?.[0]) {
|
||||||
|
setPhoto(URL.createObjectURL(e.target.files[0]));
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{photo ? (
|
||||||
{/* Status Badge */}
|
<div className="relative">
|
||||||
<div className={`${statusConfig.color} text-white text-center py-3 rounded-md font-medium`}>
|
<img src={photo} alt="Preview" className="w-full h-24 object-cover rounded-md" />
|
||||||
{statusConfig.text}
|
<button
|
||||||
</div>
|
onClick={() => setPhoto(null)}
|
||||||
</div>
|
className="absolute top-1 right-1 p-1 bg-black/50 rounded-full"
|
||||||
|
>
|
||||||
{/* Right Column - Notes & Photo */}
|
<X size={12} className="text-white" />
|
||||||
<div className="space-y-6">
|
</button>
|
||||||
{/* Notes */}
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-2">
|
|
||||||
Notes (Optional)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={notes}
|
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
|
||||||
placeholder="Any issues or observations..."
|
|
||||||
className="input w-full resize-none"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Photo Upload */}
|
|
||||||
{(isPhotoRequired || status !== 'OK') && (
|
|
||||||
<div className="card p-6">
|
|
||||||
<label className="block text-sm font-medium text-secondary mb-2">
|
|
||||||
{isPhotoRequired ? 'Photo (Required)' : 'Photo (Recommended)'}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<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 max-h-48 object-cover rounded-md" />
|
|
||||||
<button
|
|
||||||
onClick={() => setPhoto(null)}
|
|
||||||
className="absolute top-2 right-2 p-1.5 bg-destructive text-white rounded-full shadow-lg"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<label
|
|
||||||
htmlFor="photo-upload"
|
|
||||||
className={`flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-md cursor-pointer transition-colors ${isPhotoRequired
|
|
||||||
? 'border-destructive/50 bg-destructive-muted hover:border-destructive'
|
|
||||||
: 'border-subtle hover:border-default hover:bg-tertiary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Camera size={24} className="text-tertiary mb-2" />
|
|
||||||
<span className="text-sm font-medium text-primary">Tap to capture</span>
|
|
||||||
<span className="text-xs text-tertiary">Tank level photo</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={onBack}
|
|
||||||
className="btn btn-secondary flex-1 h-12 md:h-14"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleNext}
|
|
||||||
className="btn btn-primary flex-1 h-12 md:h-14"
|
|
||||||
>
|
|
||||||
{isLastTank ? 'Complete' : 'Next Tank'}
|
|
||||||
{!isLastTank && <ChevronRight size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<button onClick={handleNext} className="btn btn-primary w-full">
|
||||||
|
{isLastTank ? 'Complete' : 'Next'}
|
||||||
|
{!isLastTank && <ChevronRight size={14} />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
IrrigationCheckData,
|
IrrigationCheckData,
|
||||||
PlantHealthCheckData
|
PlantHealthCheckData
|
||||||
} from '../lib/walkthroughApi';
|
} from '../lib/walkthroughApi';
|
||||||
import { ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug } from 'lucide-react';
|
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';
|
type Step = 'start' | 'reservoir' | 'irrigation' | 'plant-health' | 'summary';
|
||||||
|
|
@ -172,129 +172,80 @@ export default function DailyWalkthroughPage() {
|
||||||
|
|
||||||
// Summary Step
|
// Summary Step
|
||||||
if (currentStep === 'summary') {
|
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;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="w-full max-w-3xl">
|
||||||
<div className="mb-6 md:mb-10">
|
{/* Compact header */}
|
||||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary">Review & Submit</h1>
|
<div className="text-center mb-8">
|
||||||
<p className="text-secondary mt-1">Review your walkthrough before submitting</p>
|
<h1 className="text-xl font-semibold text-primary">Review & Submit</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Grid - 1 col on mobile, 3 cols on desktop */}
|
{/* Summary stats */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6 mb-6 md:mb-8">
|
<div className="flex justify-center gap-6 mb-6">
|
||||||
{/* Reservoir Summary */}
|
<div className="text-center">
|
||||||
<div className="card p-5 md:p-6">
|
<div className="text-3xl font-semibold text-primary">{totalChecks}</div>
|
||||||
<h3 className="font-medium text-primary mb-4 flex items-center gap-2">
|
<div className="text-xs text-tertiary uppercase tracking-wider">Checks</div>
|
||||||
<Droplets size={18} className="text-accent" />
|
|
||||||
Reservoirs ({reservoirChecks.length})
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{reservoirChecks.map((check, i) => (
|
|
||||||
<div key={i} className="flex justify-between items-center p-3 bg-tertiary rounded-md">
|
|
||||||
<span className="text-sm text-primary">{check.tankName}</span>
|
|
||||||
<span className={`
|
|
||||||
badge text-[10px]
|
|
||||||
${check.status === 'OK' ? 'badge-success' : check.status === 'LOW' ? 'badge-warning' : 'badge-destructive'}
|
|
||||||
`}>
|
|
||||||
{check.levelPercent}% — {check.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{reservoirChecks.length === 0 && (
|
|
||||||
<p className="text-sm text-tertiary text-center py-4">No checks recorded</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|
||||||
{/* Irrigation Summary */}
|
{/* Compact summary cards */}
|
||||||
<div className="card p-5 md:p-6">
|
<div className="grid grid-cols-3 gap-3 mb-6">
|
||||||
<h3 className="font-medium text-primary mb-4 flex items-center gap-2">
|
<SummaryCard
|
||||||
<Sprout size={18} className="text-accent" />
|
icon={Droplets}
|
||||||
Irrigation ({irrigationChecks.length})
|
label="Reservoirs"
|
||||||
</h3>
|
count={reservoirChecks.length}
|
||||||
<div className="space-y-2">
|
status={reservoirChecks.every(c => c.status === 'OK') ? 'good' : 'warning'}
|
||||||
{irrigationChecks.map((check, i) => (
|
/>
|
||||||
<div key={i} className="p-3 bg-tertiary rounded-md">
|
<SummaryCard
|
||||||
<div className="flex justify-between items-center mb-2">
|
icon={Sprout}
|
||||||
<span className="text-sm font-medium text-primary">{check.zoneName}</span>
|
label="Irrigation"
|
||||||
<span className="text-xs text-secondary">
|
count={irrigationChecks.length}
|
||||||
{check.drippersWorking}/{check.drippersTotal}
|
status={irrigationChecks.every(c => c.waterFlow && c.nutrientsMixed) ? 'good' : 'warning'}
|
||||||
</span>
|
/>
|
||||||
</div>
|
<SummaryCard
|
||||||
<div className="flex gap-3 text-[10px]">
|
icon={Bug}
|
||||||
<span className={check.waterFlow ? 'text-success' : 'text-destructive'}>
|
label="Plant Health"
|
||||||
{check.waterFlow ? '✓' : '✗'} Water
|
count={plantHealthChecks.length}
|
||||||
</span>
|
status={plantHealthChecks.every(c => c.healthStatus === 'GOOD' && !c.pestsObserved) ? 'good' : 'warning'}
|
||||||
<span className={check.nutrientsMixed ? 'text-success' : 'text-destructive'}>
|
/>
|
||||||
{check.nutrientsMixed ? '✓' : '✗'} Nutrients
|
|
||||||
</span>
|
|
||||||
<span className={check.scheduleActive ? 'text-success' : 'text-destructive'}>
|
|
||||||
{check.scheduleActive ? '✓' : '✗'} Schedule
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{irrigationChecks.length === 0 && (
|
|
||||||
<p className="text-sm text-tertiary text-center py-4">No checks recorded</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Plant Health Summary */}
|
|
||||||
<div className="card p-5 md:p-6">
|
|
||||||
<h3 className="font-medium text-primary mb-4 flex items-center gap-2">
|
|
||||||
<Bug size={18} className="text-accent" />
|
|
||||||
Plant Health ({plantHealthChecks.length})
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{plantHealthChecks.map((check, i) => (
|
|
||||||
<div key={i} className="p-3 bg-tertiary rounded-md">
|
|
||||||
<div className="flex justify-between items-center mb-1">
|
|
||||||
<span className="text-sm font-medium text-primary">{check.zoneName}</span>
|
|
||||||
<span className={`
|
|
||||||
badge text-[10px]
|
|
||||||
${check.healthStatus === 'GOOD' ? 'badge-success' : check.healthStatus === 'FAIR' ? 'badge-warning' : 'badge-destructive'}
|
|
||||||
`}>
|
|
||||||
{check.healthStatus}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{check.pestsObserved && (
|
|
||||||
<div className="text-xs text-destructive mt-1">
|
|
||||||
🐛 Pests: {check.pestType || 'Observed'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{plantHealthChecks.length === 0 && (
|
|
||||||
<p className="text-sm text-tertiary text-center py-4">No checks recorded</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-3 bg-destructive-muted text-destructive rounded-md text-sm flex items-center gap-2 max-w-2xl mx-auto">
|
<div className="mb-4 p-3 bg-destructive-muted text-destructive rounded-md text-sm flex items-center gap-2">
|
||||||
<AlertCircle size={16} />
|
<AlertCircle size={14} />
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-3 max-w-2xl mx-auto">
|
{/* Actions */}
|
||||||
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentStep('plant-health')}
|
onClick={() => setCurrentStep('plant-health')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="btn btn-secondary flex-1 h-12 md:h-14"
|
className="btn btn-secondary px-6"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={14} />
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmitWalkthrough}
|
onClick={handleSubmitWalkthrough}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="btn btn-primary flex-1 h-12 md:h-14"
|
className="btn btn-primary flex-1"
|
||||||
>
|
>
|
||||||
{isLoading ? <Loader2 size={16} className="animate-spin" /> : <Check size={16} />}
|
{isLoading ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />}
|
||||||
{isLoading ? 'Submitting...' : 'Submit Walkthrough'}
|
{isLoading ? 'Submitting...' : 'Complete Walkthrough'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -302,82 +253,85 @@ export default function DailyWalkthroughPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start Screen
|
// Start Screen - Refined
|
||||||
const steps = [
|
const today = new Date();
|
||||||
{ id: 'reservoir', title: 'Reservoir Checks', description: 'Check all veg and flower tank levels', icon: Droplets },
|
const greeting = today.getHours() < 12 ? 'Good morning' : today.getHours() < 17 ? 'Good afternoon' : 'Good evening';
|
||||||
{ id: 'irrigation', title: 'Irrigation System', description: 'Verify drippers and water flow', icon: Sprout },
|
|
||||||
{ id: 'plant-health', title: 'Plant Health', description: 'Spot check for pests and health', icon: Bug },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-primary p-4 md:p-8 lg:p-12 animate-in">
|
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="w-full max-w-md">
|
||||||
{/* Header */}
|
{/* Minimal header */}
|
||||||
<div className="mb-8 md:mb-12 text-center">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-2xl md:text-3xl font-semibold text-primary mb-2">Daily Walkthrough</h1>
|
<p className="text-sm text-tertiary mb-1">{today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}</p>
|
||||||
<p className="text-secondary">
|
<h1 className="text-xl font-semibold text-primary">{greeting}</h1>
|
||||||
{new Date().toLocaleDateString('en-US', {
|
|
||||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content - Centered Card */}
|
{/* Task list - tight and intentional */}
|
||||||
<div className="card p-6 md:p-10 lg:p-12 max-w-2xl mx-auto">
|
<div className="card overflow-hidden">
|
||||||
<div className="text-center mb-8 md:mb-10">
|
<div className="p-4 border-b border-subtle">
|
||||||
<div className="text-5xl md:text-6xl mb-4">☀️</div>
|
<h2 className="text-sm font-medium text-primary">Daily Walkthrough</h2>
|
||||||
<h2 className="text-xl md:text-2xl font-semibold text-primary mb-2">Good Morning!</h2>
|
<p className="text-xs text-tertiary flex items-center gap-1 mt-0.5">
|
||||||
<p className="text-secondary">Ready to start your daily facility walkthrough?</p>
|
<Clock size={10} />
|
||||||
|
~15 min
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Steps Preview */}
|
<div className="divide-y divide-subtle">
|
||||||
<div className="space-y-3 mb-8">
|
<StepRow icon={Droplets} label="Reservoir Checks" />
|
||||||
{steps.map((step, index) => {
|
<StepRow icon={Sprout} label="Irrigation System" />
|
||||||
const Icon = step.icon;
|
<StepRow icon={Bug} label="Plant Health" />
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={step.id}
|
|
||||||
className="flex items-center gap-4 p-4 md:p-5 bg-tertiary rounded-lg hover:bg-secondary transition-colors duration-fast"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 md:w-12 md:h-12 rounded-lg bg-accent-muted flex items-center justify-center">
|
|
||||||
<Icon size={20} className="text-accent md:w-6 md:h-6" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="font-medium text-primary">
|
|
||||||
{index + 1}. {step.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs md:text-sm text-tertiary">{step.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-5 h-5 rounded-full border-2 border-subtle" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-3 bg-destructive-muted text-destructive rounded-md text-sm flex items-center gap-2">
|
<div className="p-3 bg-destructive-muted text-destructive text-xs flex items-center gap-2">
|
||||||
<AlertCircle size={16} />
|
<AlertCircle size={12} />
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<div className="p-4">
|
||||||
onClick={handleStartWalkthrough}
|
<button
|
||||||
disabled={isLoading}
|
onClick={handleStartWalkthrough}
|
||||||
className="btn btn-primary w-full h-12 md:h-14 text-base"
|
disabled={isLoading}
|
||||||
>
|
className="btn btn-primary w-full"
|
||||||
{isLoading ? <Loader2 size={18} className="animate-spin" /> : null}
|
>
|
||||||
{isLoading ? 'Starting...' : 'Start Walkthrough'}
|
{isLoading ? <Loader2 size={14} className="animate-spin" /> : 'Begin'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="mt-6 p-4 bg-accent-muted rounded-lg border border-accent/20">
|
|
||||||
<p className="text-sm text-accent">
|
|
||||||
<strong>💡 Tip:</strong> This walkthrough typically takes 15–20 minutes.
|
|
||||||
Have your device ready for photos.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 }: {
|
||||||
|
icon: typeof Droplets;
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
status: 'good' | 'warning';
|
||||||
|
}) {
|
||||||
|
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'
|
||||||
|
}`}>
|
||||||
|
<Icon size={14} />
|
||||||
|
</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