fix: intentional walkthrough UI redesign
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- 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:
fullsizemalt 2025-12-12 16:49:41 -08:00
parent efb298e119
commit a2120170b6
5 changed files with 429 additions and 699 deletions

View file

@ -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>

View file

@ -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>
); );
} }

View file

@ -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>
); );
} }

View file

@ -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>
); );

View file

@ -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 1520 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>
);
}