feat: redesign Daily Walkthrough with Control Room aesthetic
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: dark charcoal theme, emerald accents, animated
  progress ring, shift indicator, framer-motion collapsible sections,
  SOP integration, mobile-optimized card layouts
- WalkthroughSettingsPage: matching Control Room styling, animated
  toggle switches, icons for modules, linked SOPs section
- Both pages now use uppercase italic headers and high-contrast styling
This commit is contained in:
fullsizemalt 2025-12-19 20:58:34 -08:00
parent 26d4b5a3a5
commit 77e7382504
2 changed files with 532 additions and 294 deletions

View file

@ -2,11 +2,16 @@ import { useState, useEffect, useRef } from 'react';
import { useNavigate, Link } from 'react-router-dom'; import { useNavigate, Link } from 'react-router-dom';
import { settingsApi, WalkthroughSettings } from '../lib/settingsApi'; import { settingsApi, WalkthroughSettings } from '../lib/settingsApi';
import { walkthroughApi, ReservoirCheckData, IrrigationCheckData, PlantHealthCheckData, Walkthrough } from '../lib/walkthroughApi'; import { walkthroughApi, ReservoirCheckData, IrrigationCheckData, PlantHealthCheckData, Walkthrough } from '../lib/walkthroughApi';
import { documentsApi, Document } from '../lib/documentsApi';
import { import {
Check, Loader2, Droplets, Sprout, Bug, Check, Loader2, Droplets, Sprout, Bug, ArrowLeft,
Camera, X, Minus, Plus, ChevronDown, ChevronUp, Upload, CheckCircle, Clock Camera, X, Minus, Plus, ChevronDown, ChevronUp, Upload, CheckCircle2, Clock,
FileText, AlertTriangle, Thermometer, Wind, Leaf, BookOpen, CircleCheck
} from 'lucide-react'; } from 'lucide-react';
import { useToast } from '../context/ToastContext'; import { useToast } from '../context/ToastContext';
import { Card } from '../components/ui/card';
import { cn } from '../lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
// Tank/Zone configs // Tank/Zone configs
const TANKS = [ const TANKS = [
@ -34,6 +39,7 @@ export default function DailyWalkthroughPage() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [settings, setSettings] = useState<WalkthroughSettings | null>(null); const [settings, setSettings] = useState<WalkthroughSettings | null>(null);
const [todaysWalkthrough, setTodaysWalkthrough] = useState<Walkthrough | null>(null); const [todaysWalkthrough, setTodaysWalkthrough] = useState<Walkthrough | null>(null);
const [relatedSOPs, setRelatedSOPs] = useState<Document[]>([]);
// All check states // All check states
const [reservoirChecks, setReservoirChecks] = useState<Record<string, ReservoirCheckData>>({}); const [reservoirChecks, setReservoirChecks] = useState<Record<string, ReservoirCheckData>>({});
@ -51,12 +57,14 @@ export default function DailyWalkthroughPage() {
const loadData = async () => { const loadData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const [settingsData, todayData] = await Promise.all([ const [settingsData, todayData, sops] = await Promise.all([
settingsApi.getWalkthrough().catch(() => null), settingsApi.getWalkthrough().catch(() => null),
walkthroughApi.getToday().catch(() => null) walkthroughApi.getToday().catch(() => null),
documentsApi.getDocuments({ type: 'CHECKLIST', status: 'APPROVED' }).catch(() => [])
]); ]);
setSettings(settingsData); setSettings(settingsData);
setTodaysWalkthrough(todayData); setTodaysWalkthrough(todayData);
setRelatedSOPs(sops);
} catch (error) { } catch (error) {
console.error('Failed to load walkthrough data:', error); console.error('Failed to load walkthrough data:', error);
} finally { } finally {
@ -111,13 +119,17 @@ export default function DailyWalkthroughPage() {
const isComplete = totalChecks === requiredChecks; const isComplete = totalChecks === requiredChecks;
const progressPercent = Math.round((totalChecks / requiredChecks) * 100); const progressPercent = Math.round((totalChecks / requiredChecks) * 100);
// Determine shift based on time
const currentHour = new Date().getHours();
const shift = currentHour < 12 ? 'AM' : currentHour < 18 ? 'PM' : 'NIGHT';
// Loading state // Loading state
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-primary flex items-center justify-center p-4"> <div className="min-h-screen bg-[#0B0E14] flex items-center justify-center p-4">
<div className="text-center"> <div className="text-center">
<Loader2 size={32} className="animate-spin text-accent mx-auto mb-4" /> <Loader2 size={40} className="animate-spin text-emerald-500 mx-auto mb-4" />
<p className="text-tertiary">Loading...</p> <p className="text-slate-400 text-sm font-medium">Loading walkthrough...</p>
</div> </div>
</div> </div>
); );
@ -127,36 +139,42 @@ export default function DailyWalkthroughPage() {
if (todaysWalkthrough?.status === 'COMPLETED' && !walkthroughId) { if (todaysWalkthrough?.status === 'COMPLETED' && !walkthroughId) {
const completedTime = new Date(todaysWalkthrough.endTime || todaysWalkthrough.startTime); const completedTime = new Date(todaysWalkthrough.endTime || todaysWalkthrough.startTime);
return ( return (
<div className="min-h-screen bg-primary flex items-center justify-center p-4"> <div className="min-h-screen bg-[#0B0E14] flex items-center justify-center p-4">
<div className="w-full max-w-sm text-center"> <motion.div
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-success/20 flex items-center justify-center"> initial={{ opacity: 0, scale: 0.95 }}
<CheckCircle size={48} className="text-success" /> animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md text-center"
>
<div className="w-28 h-28 mx-auto mb-8 rounded-2xl bg-emerald-500/20 flex items-center justify-center border border-emerald-500/30">
<CheckCircle2 size={56} className="text-emerald-500" />
</div> </div>
<h1 className="text-xl font-semibold text-primary mb-2">Walkthrough Complete!</h1> <h1 className="text-2xl font-bold text-white uppercase italic tracking-wide mb-2">
<p className="text-sm text-tertiary mb-2"> Walkthrough Complete
</h1>
<p className="text-slate-400 text-sm mb-6">
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })} {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
</p> </p>
<div className="bg-success/10 border border-success/20 rounded-lg p-4 mb-6"> <Card className="bg-emerald-500/10 border-emerald-500/20 p-5 mb-8">
<div className="flex items-center justify-center gap-2 text-success"> <div className="flex items-center justify-center gap-3 text-emerald-400">
<Clock size={16} /> <Clock size={18} />
<span className="text-sm font-medium"> <span className="text-base font-bold">
Completed at {completedTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} Completed at {completedTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
</span> </span>
</div> </div>
<p className="text-xs text-tertiary mt-1"> <p className="text-xs text-slate-500 mt-2">
by {todaysWalkthrough.user?.name || 'Unknown'} by {todaysWalkthrough.user?.name || 'Unknown'}
</p> </p>
</div> </Card>
<Link to="/" className="btn btn-primary w-full text-base py-3"> <Link to="/" className="block w-full bg-emerald-600 hover:bg-emerald-700 text-white py-4 rounded-xl font-bold uppercase tracking-widest text-sm transition-all">
Back to Dashboard Back to Dashboard
</Link> </Link>
<button <button
onClick={() => { setTodaysWalkthrough(null); }} onClick={() => { setTodaysWalkthrough(null); }}
className="block w-full mt-4 text-sm text-tertiary hover:text-secondary" className="block w-full mt-4 text-sm text-slate-500 hover:text-slate-300 transition-colors"
> >
Start a new walkthrough anyway Start a new walkthrough anyway
</button> </button>
</div> </motion.div>
</div> </div>
); );
} }
@ -164,25 +182,31 @@ export default function DailyWalkthroughPage() {
// In-progress walkthrough from earlier // In-progress walkthrough from earlier
if (todaysWalkthrough?.status === 'IN_PROGRESS' && !walkthroughId) { if (todaysWalkthrough?.status === 'IN_PROGRESS' && !walkthroughId) {
return ( return (
<div className="min-h-screen bg-primary flex items-center justify-center p-4"> <div className="min-h-screen bg-[#0B0E14] flex items-center justify-center p-4">
<div className="w-full max-w-sm text-center"> <motion.div
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-warning/20 flex items-center justify-center"> initial={{ opacity: 0, y: 20 }}
<Clock size={36} className="text-warning" /> animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md text-center"
>
<div className="w-24 h-24 mx-auto mb-6 rounded-2xl bg-amber-500/20 flex items-center justify-center border border-amber-500/30">
<Clock size={40} className="text-amber-500" />
</div> </div>
<h1 className="text-xl font-semibold text-primary mb-2">Walkthrough In Progress</h1> <h1 className="text-2xl font-bold text-white uppercase italic tracking-wide mb-2">
<p className="text-sm text-tertiary mb-6"> In Progress
</h1>
<p className="text-slate-400 text-sm mb-8">
Started at {new Date(todaysWalkthrough.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} Started at {new Date(todaysWalkthrough.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
</p> </p>
<button <button
onClick={() => setWalkthroughId(todaysWalkthrough.id)} onClick={() => setWalkthroughId(todaysWalkthrough.id)}
className="btn btn-primary w-full text-base py-3" className="w-full bg-amber-600 hover:bg-amber-700 text-white py-4 rounded-xl font-bold uppercase tracking-widest text-sm transition-all"
> >
Continue Walkthrough Continue Walkthrough
</button> </button>
<Link to="/" className="block mt-4 text-sm text-tertiary hover:text-secondary"> <Link to="/" className="block mt-4 text-sm text-slate-500 hover:text-slate-300 transition-colors">
Back to Dashboard Back to Dashboard
</Link> </Link>
</div> </motion.div>
</div> </div>
); );
} }
@ -190,70 +214,154 @@ export default function DailyWalkthroughPage() {
// Pre-start view // Pre-start view
if (!walkthroughId) { if (!walkthroughId) {
return ( return (
<div className="min-h-screen bg-primary flex items-center justify-center p-4"> <div className="min-h-screen bg-[#0B0E14] flex items-center justify-center p-4">
<div className="w-full max-w-sm text-center"> <motion.div
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-accent-muted flex items-center justify-center"> initial={{ opacity: 0, y: 20 }}
<Sprout size={36} className="text-accent" /> animate={{ opacity: 1, y: 0 }}
className="w-full max-w-lg text-center"
>
{/* Header */}
<div className="w-24 h-24 mx-auto mb-6 rounded-2xl bg-emerald-500/20 flex items-center justify-center border border-emerald-500/30">
<Sprout size={40} className="text-emerald-500" />
</div> </div>
<h1 className="text-xl font-semibold text-primary mb-2">Daily Walkthrough</h1> <h1 className="text-3xl font-bold text-white uppercase italic tracking-wide mb-2">
<p className="text-sm text-tertiary mb-6"> Daily Walkthrough
</h1>
<div className="flex items-center justify-center gap-3 mb-8">
<span className="text-slate-400 text-sm">
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })} {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
</p> </span>
<span className={cn(
"px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-widest border",
shift === 'AM' ? "bg-amber-500/10 text-amber-400 border-amber-500/20" :
shift === 'PM' ? "bg-blue-500/10 text-blue-400 border-blue-500/20" :
"bg-purple-500/10 text-purple-400 border-purple-500/20"
)}>
{shift} Shift
</span>
</div>
{/* Checklist Preview */}
<Card className="bg-[#13171F] border-slate-800 p-6 mb-6 text-left">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Today's Checklist</h3>
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm text-slate-300">
<Droplets size={16} className="text-blue-400" />
<span>Reservoir Checks</span>
<span className="ml-auto text-xs text-slate-500">{TANKS.length} tanks</span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-300">
<Sprout size={16} className="text-emerald-400" />
<span>Irrigation Checks</span>
<span className="ml-auto text-xs text-slate-500">{ZONES.length} zones</span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-300">
<Bug size={16} className="text-amber-400" />
<span>Plant Health Checks</span>
<span className="ml-auto text-xs text-slate-500">{HEALTH_ZONES.length} zones</span>
</div>
</div>
{relatedSOPs.length > 0 && (
<div className="mt-4 pt-4 border-t border-slate-800">
<div className="flex items-center gap-2 text-xs text-slate-500">
<BookOpen size={12} />
<span>{relatedSOPs.length} related SOPs available</span>
</div>
</div>
)}
</Card>
<button <button
onClick={handleStart} onClick={handleStart}
disabled={isStarting} disabled={isStarting}
className="btn btn-primary w-full text-base py-3" className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-4 rounded-xl font-bold uppercase tracking-widest text-sm transition-all shadow-xl shadow-emerald-500/20 disabled:opacity-50"
> >
{isStarting ? <Loader2 size={18} className="animate-spin" /> : 'Begin Walkthrough'} {isStarting ? <Loader2 size={18} className="animate-spin mx-auto" /> : 'Begin Walkthrough'}
</button> </button>
<Link to="/" className="block mt-4 text-sm text-tertiary hover:text-secondary"> <Link to="/" className="block mt-4 text-sm text-slate-500 hover:text-slate-300 transition-colors">
Back to Dashboard Back to Dashboard
</Link> </Link>
</div> </motion.div>
</div> </div>
); );
} }
return ( return (
<div className="max-w-2xl mx-auto pb-28 animate-in"> <div className="min-h-screen bg-[#0B0E14] pb-28">
{/* Sticky Header */}
{/* Header with Progress */} <div className="sticky top-0 z-20 bg-[#0B0E14]/95 backdrop-blur-xl border-b border-slate-800/50">
<div className="card p-4 mb-4"> <div className="max-w-3xl mx-auto px-4 py-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link to="/" className="p-2 rounded-lg hover:bg-slate-800 transition-colors text-slate-400">
<ArrowLeft size={20} />
</Link>
<div> <div>
<h1 className="text-lg font-semibold text-primary">Daily Walkthrough</h1> <h1 className="text-lg font-bold text-white uppercase italic tracking-wide">
<p className="text-xs text-tertiary"> Daily Walkthrough
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })} </h1>
</p> <div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-slate-500">
{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
<span className={cn(
"px-1.5 py-0.5 rounded text-[9px] font-bold uppercase tracking-widest",
shift === 'AM' ? "bg-amber-500/10 text-amber-400" :
shift === 'PM' ? "bg-blue-500/10 text-blue-400" :
"bg-purple-500/10 text-purple-400"
)}>
{shift}
</span>
</div> </div>
<div className="text-right">
<span className="text-2xl font-bold text-accent">{progressPercent}%</span>
<p className="text-xs text-tertiary">{totalChecks}/{requiredChecks} checks</p>
</div>
</div>
{/* Progress bar */}
<div className="h-2 bg-subtle rounded-full overflow-hidden">
<div
className="h-full bg-accent transition-all duration-500 ease-out"
style={{ width: `${progressPercent}%` }}
/>
</div> </div>
</div> </div>
{/* Progress Ring */}
<div className="flex items-center gap-3">
<div className="text-right">
<span className="text-2xl font-bold text-emerald-500">{progressPercent}%</span>
<p className="text-[10px] text-slate-500 font-medium">{totalChecks}/{requiredChecks}</p>
</div>
<div className="relative w-12 h-12">
<svg className="w-12 h-12 -rotate-90">
<circle cx="24" cy="24" r="20" stroke="#1e293b" strokeWidth="4" fill="none" />
<circle
cx="24" cy="24" r="20"
stroke="#10b981"
strokeWidth="4"
fill="none"
strokeDasharray={`${2 * Math.PI * 20}`}
strokeDashoffset={`${2 * Math.PI * 20 * (1 - progressPercent / 100)}`}
strokeLinecap="round"
className="transition-all duration-500"
/>
</svg>
{isComplete && (
<div className="absolute inset-0 flex items-center justify-center">
<Check size={16} className="text-emerald-500" />
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-3xl mx-auto px-4 py-6 space-y-4">
{/* Sections */} {/* Sections */}
<div className="space-y-3">
{/* Reservoirs Section */}
{settings?.enableReservoirs !== false && ( {settings?.enableReservoirs !== false && (
<CollapsibleSection <CollapsibleSection
title="Reservoirs" title="Reservoir Checks"
icon={Droplets} icon={Droplets}
iconColor="text-blue-400"
count={Object.keys(reservoirChecks).length} count={Object.keys(reservoirChecks).length}
total={TANKS.length} total={TANKS.length}
expanded={expandedSections.reservoirs} expanded={expandedSections.reservoirs}
onToggle={() => toggleSection('reservoirs')} onToggle={() => toggleSection('reservoirs')}
> >
<div className="space-y-2"> <div className="space-y-3">
{TANKS.map(tank => ( {TANKS.map(tank => (
<ReservoirRow <ReservoirRow
key={tank.name} key={tank.name}
@ -266,17 +374,17 @@ export default function DailyWalkthroughPage() {
</CollapsibleSection> </CollapsibleSection>
)} )}
{/* Irrigation Section */}
{settings?.enableIrrigation !== false && ( {settings?.enableIrrigation !== false && (
<CollapsibleSection <CollapsibleSection
title="Irrigation" title="Irrigation Checks"
icon={Sprout} icon={Sprout}
iconColor="text-emerald-400"
count={Object.keys(irrigationChecks).length} count={Object.keys(irrigationChecks).length}
total={ZONES.length} total={ZONES.length}
expanded={expandedSections.irrigation} expanded={expandedSections.irrigation}
onToggle={() => toggleSection('irrigation')} onToggle={() => toggleSection('irrigation')}
> >
<div className="space-y-2"> <div className="space-y-3">
{ZONES.map(zone => ( {ZONES.map(zone => (
<IrrigationRow <IrrigationRow
key={zone.name} key={zone.name}
@ -289,17 +397,17 @@ export default function DailyWalkthroughPage() {
</CollapsibleSection> </CollapsibleSection>
)} )}
{/* Plant Health Section */}
{settings?.enablePlantHealth !== false && ( {settings?.enablePlantHealth !== false && (
<CollapsibleSection <CollapsibleSection
title="Plant Health" title="Plant Health Checks"
icon={Bug} icon={Bug}
iconColor="text-amber-400"
count={Object.keys(plantHealthChecks).length} count={Object.keys(plantHealthChecks).length}
total={HEALTH_ZONES.length} total={HEALTH_ZONES.length}
expanded={expandedSections.plantHealth} expanded={expandedSections.plantHealth}
onToggle={() => toggleSection('plantHealth')} onToggle={() => toggleSection('plantHealth')}
> >
<div className="space-y-2"> <div className="space-y-3">
{HEALTH_ZONES.map(zone => ( {HEALTH_ZONES.map(zone => (
<PlantHealthRow <PlantHealthRow
key={zone} key={zone}
@ -314,17 +422,22 @@ export default function DailyWalkthroughPage() {
</div> </div>
{/* Fixed Submit Button */} {/* Fixed Submit Button */}
<div className="fixed bottom-0 left-0 right-0 p-4 bg-primary/95 backdrop-blur border-t border-subtle"> <div className="fixed bottom-0 left-0 right-0 p-4 bg-[#0B0E14]/95 backdrop-blur-xl border-t border-slate-800/50">
<div className="max-w-2xl mx-auto"> <div className="max-w-3xl mx-auto">
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={!isComplete || isSubmitting} disabled={!isComplete || isSubmitting}
className={`btn w-full py-3 text-base ${isComplete ? 'btn-primary' : 'bg-subtle text-tertiary cursor-not-allowed'}`} className={cn(
"w-full py-4 rounded-xl font-bold uppercase tracking-widest text-sm transition-all flex items-center justify-center gap-2",
isComplete
? "bg-emerald-600 hover:bg-emerald-700 text-white shadow-xl shadow-emerald-500/20"
: "bg-slate-800 text-slate-500 cursor-not-allowed"
)}
> >
{isSubmitting ? ( {isSubmitting ? (
<><Loader2 size={18} className="animate-spin" /> Submitting...</> <><Loader2 size={18} className="animate-spin" /> Submitting...</>
) : isComplete ? ( ) : isComplete ? (
<><Check size={18} /> Submit Walkthrough</> <><CircleCheck size={18} /> Submit Walkthrough</>
) : ( ) : (
`Complete all checks (${totalChecks}/${requiredChecks})` `Complete all checks (${totalChecks}/${requiredChecks})`
)} )}
@ -337,10 +450,11 @@ export default function DailyWalkthroughPage() {
// Collapsible Section // Collapsible Section
function CollapsibleSection({ function CollapsibleSection({
title, icon: Icon, count, total, expanded, onToggle, children title, icon: Icon, iconColor, count, total, expanded, onToggle, children
}: { }: {
title: string; title: string;
icon: typeof Droplets; icon: typeof Droplets;
iconColor: string;
count: number; count: number;
total: number; total: number;
expanded: boolean; expanded: boolean;
@ -349,28 +463,55 @@ function CollapsibleSection({
}) { }) {
const isComplete = count === total; const isComplete = count === total;
return ( return (
<div className="card overflow-hidden"> <Card className="bg-[#13171F] border-slate-800 overflow-hidden">
<button <button
onClick={onToggle} onClick={onToggle}
className="w-full flex items-center justify-between p-4 hover:bg-tertiary transition-colors" className="w-full flex items-center justify-between p-4 hover:bg-slate-800/30 transition-colors"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${isComplete ? 'bg-success-muted text-success' : 'bg-accent-muted text-accent' <div className={cn(
}`}> "w-12 h-12 rounded-xl flex items-center justify-center transition-all",
{isComplete ? <Check size={18} /> : <Icon size={18} />} isComplete
? "bg-emerald-500/20 border border-emerald-500/30"
: "bg-slate-800/50 border border-slate-700/50"
)}>
{isComplete ? (
<Check size={20} className="text-emerald-500" />
) : (
<Icon size={20} className={iconColor} />
)}
</div> </div>
<div className="text-left"> <div className="text-left">
<h3 className="text-sm font-medium text-primary">{title}</h3> <h3 className="text-sm font-bold text-white uppercase tracking-wide">{title}</h3>
<p className="text-xs text-tertiary">{count}/{total} complete</p> <p className="text-xs text-slate-500 mt-0.5">
{count}/{total} complete
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
{isComplete && <span className="text-xs text-success font-medium"> Done</span>} {isComplete && <span className="text-xs text-emerald-500 font-bold uppercase tracking-wider">Done</span>}
{expanded ? <ChevronUp size={18} className="text-tertiary" /> : <ChevronDown size={18} className="text-tertiary" />} <motion.div
animate={{ rotate: expanded ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDown size={18} className="text-slate-500" />
</motion.div>
</div> </div>
</button> </button>
{expanded && <div className="p-4 pt-0 border-t border-subtle">{children}</div>} <AnimatePresence>
</div> {expanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="p-4 pt-0 border-t border-slate-800/50">{children}</div>
</motion.div>
)}
</AnimatePresence>
</Card>
); );
} }
@ -388,8 +529,6 @@ function PhotoCapture({
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
// For now, create a local preview URL
// In production, this would upload to server
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
onCapture(url); onCapture(url);
} }
@ -397,27 +536,27 @@ function PhotoCapture({
if (photoUrl) { if (photoUrl) {
return ( return (
<div className="relative mt-2"> <div className="relative mt-3">
<img src={photoUrl} alt="Captured" className="w-full h-24 object-cover rounded-lg" /> <img src={photoUrl} alt="Captured" className="w-full h-28 object-cover rounded-xl border border-slate-700" />
<button <button
onClick={() => onCapture(null)} onClick={() => onCapture(null)}
className="absolute top-1 right-1 w-6 h-6 bg-destructive text-white rounded-full flex items-center justify-center" className="absolute top-2 right-2 w-7 h-7 bg-red-500 text-white rounded-full flex items-center justify-center shadow-lg"
> >
<X size={12} /> <X size={14} />
</button> </button>
</div> </div>
); );
} }
return ( return (
<div className="flex gap-2 mt-2"> <div className="flex gap-2 mt-3">
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleFile} className="hidden" /> <input ref={fileInputRef} type="file" accept="image/*" onChange={handleFile} className="hidden" />
<input ref={cameraInputRef} type="file" accept="image/*" capture="environment" onChange={handleFile} className="hidden" /> <input ref={cameraInputRef} type="file" accept="image/*" capture="environment" onChange={handleFile} className="hidden" />
<button <button
type="button" type="button"
onClick={() => cameraInputRef.current?.click()} onClick={() => cameraInputRef.current?.click()}
className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-accent text-white rounded-lg text-xs font-medium hover:bg-accent/90 transition-colors" className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold uppercase tracking-wider transition-colors"
> >
<Camera size={14} /> <Camera size={14} />
Camera Camera
@ -425,7 +564,7 @@ function PhotoCapture({
<button <button
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-subtle text-secondary rounded-lg text-xs font-medium hover:bg-secondary transition-colors" className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-xs font-bold uppercase tracking-wider transition-colors"
> >
<Upload size={14} /> <Upload size={14} />
Upload Upload
@ -434,7 +573,7 @@ function PhotoCapture({
); );
} }
// Reservoir Row - Enhanced with photo // Reservoir Row
function ReservoirRow({ function ReservoirRow({
tank, data, onChange tank, data, onChange
}: { }: {
@ -448,6 +587,7 @@ function ReservoirRow({
const [editing, setEditing] = useState(!data); const [editing, setEditing] = useState(!data);
const getStatus = (l: number) => l >= 70 ? 'OK' : l >= 30 ? 'LOW' : 'CRITICAL'; const getStatus = (l: number) => l >= 70 ? 'OK' : l >= 30 ? 'LOW' : 'CRITICAL';
const statusColor = getStatus(level) === 'OK' ? 'emerald' : getStatus(level) === 'LOW' ? 'amber' : 'red';
const handleSave = () => { const handleSave = () => {
onChange({ onChange({
@ -463,37 +603,42 @@ function ReservoirRow({
if (!editing && data) { if (!editing && data) {
return ( return (
<div className="flex items-center justify-between p-3 bg-tertiary rounded-lg"> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className={`w-2 h-10 rounded-full ${data.status === 'OK' ? 'bg-success' : data.status === 'LOW' ? 'bg-warning' : 'bg-destructive' <div className={cn(
}`} /> "w-2 h-12 rounded-full",
data.status === 'OK' ? 'bg-emerald-500' : data.status === 'LOW' ? 'bg-amber-500' : 'bg-red-500'
)} />
<div> <div>
<span className="text-sm font-medium text-primary">{tank.name}</span> <span className="text-sm font-bold text-white">{tank.name}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-tertiary">{data.levelPercent}%</span> <span className="text-xs text-slate-400">{data.levelPercent}%</span>
{data.photoUrl && <Camera size={10} className="text-accent" />} {data.photoUrl && <Camera size={10} className="text-emerald-500" />}
</div> </div>
</div> </div>
</div> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent font-medium">Edit</button> <button onClick={() => setEditing(true)} className="text-xs text-emerald-500 font-bold uppercase tracking-wider">Edit</button>
</div> </div>
); );
} }
return ( return (
<div className="p-4 bg-tertiary rounded-lg space-y-3"> <div className="p-4 bg-slate-800/50 rounded-xl border border-slate-700/50 space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-primary">{tank.name}</span> <span className="text-sm font-bold text-white">{tank.name}</span>
<span className={`text-xs px-2 py-0.5 rounded ${tank.type === 'VEG' ? 'bg-success-muted text-success' : 'bg-accent-muted text-accent'}`}> <span className={cn(
"text-[10px] px-2 py-0.5 rounded font-bold uppercase tracking-widest border",
tank.type === 'VEG' ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' : 'bg-purple-500/10 text-purple-400 border-purple-500/20'
)}>
{tank.type} {tank.type}
</span> </span>
</div> </div>
{/* Level Slider */} {/* Level Slider */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className={`w-4 h-14 rounded-full overflow-hidden bg-subtle relative`}> <div className="w-5 h-16 rounded-full overflow-hidden bg-slate-700 relative">
<div <div
className={`absolute bottom-0 left-0 right-0 transition-all ${getStatus(level) === 'OK' ? 'bg-success' : getStatus(level) === 'LOW' ? 'bg-warning' : 'bg-destructive'}`} className={cn("absolute bottom-0 left-0 right-0 transition-all", `bg-${statusColor}-500`)}
style={{ height: `${level}%` }} style={{ height: `${level}%` }}
/> />
</div> </div>
@ -504,10 +649,10 @@ function ReservoirRow({
max="100" max="100"
value={level} value={level}
onChange={(e) => setLevel(parseInt(e.target.value))} onChange={(e) => setLevel(parseInt(e.target.value))}
className="w-full h-2 bg-subtle rounded-full appearance-none cursor-pointer" className="w-full h-2 bg-slate-700 rounded-full appearance-none cursor-pointer accent-emerald-500"
/> />
</div> </div>
<span className={`text-lg font-bold w-14 text-right ${getStatus(level) === 'OK' ? 'text-success' : getStatus(level) === 'LOW' ? 'text-warning' : 'text-destructive'}`}> <span className={cn("text-2xl font-bold w-16 text-right", `text-${statusColor}-500`)}>
{level}% {level}%
</span> </span>
</div> </div>
@ -518,21 +663,19 @@ function ReservoirRow({
value={notes} value={notes}
onChange={(e) => setNotes(e.target.value)} onChange={(e) => setNotes(e.target.value)}
placeholder="Notes (optional)" placeholder="Notes (optional)"
className="w-full px-3 py-2 bg-primary border border-subtle rounded-lg text-sm" className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-sm text-white placeholder:text-slate-500 focus:border-emerald-500/50 focus:outline-none"
/> />
{/* Photo Capture */}
<PhotoCapture photoUrl={photo} onCapture={setPhoto} /> <PhotoCapture photoUrl={photo} onCapture={setPhoto} />
{/* Save Button */} <button onClick={handleSave} className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl font-bold uppercase tracking-widest text-xs transition-all">
<button onClick={handleSave} className="btn btn-primary w-full">
Save Check Save Check
</button> </button>
</div> </div>
); );
} }
// Irrigation Row - Enhanced with photo // Irrigation Row
function IrrigationRow({ function IrrigationRow({
zone, data, onChange zone, data, onChange
}: { }: {
@ -563,82 +706,80 @@ function IrrigationRow({
if (!editing && data) { if (!editing && data) {
const issues = !data.waterFlow || !data.nutrientsMixed || data.drippersWorking < data.drippersTotal; const issues = !data.waterFlow || !data.nutrientsMixed || data.drippersWorking < data.drippersTotal;
return ( return (
<div className="flex items-center justify-between p-3 bg-tertiary rounded-lg"> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className={`w-2 h-10 rounded-full ${issues ? 'bg-warning' : 'bg-success'}`} /> <div className={cn("w-2 h-12 rounded-full", issues ? 'bg-amber-500' : 'bg-emerald-500')} />
<div> <div>
<span className="text-sm font-medium text-primary">{zone.name}</span> <span className="text-sm font-bold text-white">{zone.name}</span>
<div className="flex items-center gap-2"> <div className="text-xs text-slate-400 mt-0.5">
<span className="text-xs text-tertiary">{data.drippersWorking}/{data.drippersTotal} drippers</span> {data.drippersWorking}/{data.drippersTotal} drippers
</div> </div>
</div> </div>
</div> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent font-medium">Edit</button> <button onClick={() => setEditing(true)} className="text-xs text-emerald-500 font-bold uppercase tracking-wider">Edit</button>
</div> </div>
); );
} }
return ( return (
<div className="p-4 bg-tertiary rounded-lg space-y-3"> <div className="p-4 bg-slate-800/50 rounded-xl border border-slate-700/50 space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-primary">{zone.name}</span> <span className="text-sm font-bold text-white">{zone.name}</span>
</div> </div>
{/* Dripper Count */} {/* Dripper Count */}
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-6">
<button <button
onClick={() => setWorking(Math.max(0, working - 1))} onClick={() => setWorking(Math.max(0, working - 1))}
className="w-10 h-10 bg-subtle rounded-lg flex items-center justify-center hover:bg-secondary transition-colors" className="w-12 h-12 bg-slate-700 rounded-xl flex items-center justify-center hover:bg-slate-600 transition-colors text-white"
> >
<Minus size={16} /> <Minus size={18} />
</button> </button>
<div className="text-center"> <div className="text-center">
<span className="text-2xl font-bold text-primary">{working}</span> <span className="text-3xl font-bold text-white">{working}</span>
<span className="text-sm text-tertiary">/{zone.drippers}</span> <span className="text-lg text-slate-500">/{zone.drippers}</span>
<p className="text-xs text-tertiary">drippers working</p> <p className="text-xs text-slate-500 mt-1">drippers working</p>
</div> </div>
<button <button
onClick={() => setWorking(Math.min(zone.drippers, working + 1))} onClick={() => setWorking(Math.min(zone.drippers, working + 1))}
className="w-10 h-10 bg-subtle rounded-lg flex items-center justify-center hover:bg-secondary transition-colors" className="w-12 h-12 bg-slate-700 rounded-xl flex items-center justify-center hover:bg-slate-600 transition-colors text-white"
> >
<Plus size={16} /> <Plus size={18} />
</button> </button>
</div> </div>
{/* Checkboxes */} {/* Checkboxes */}
<div className="flex items-center justify-center gap-6"> <div className="flex items-center justify-center gap-6">
<label className="flex items-center gap-2 text-sm cursor-pointer"> <label className="flex items-center gap-3 text-sm cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={waterFlow} checked={waterFlow}
onChange={() => setWaterFlow(!waterFlow)} onChange={() => setWaterFlow(!waterFlow)}
className="w-5 h-5 rounded accent-accent" className="w-5 h-5 rounded bg-slate-700 border-slate-600 accent-emerald-500"
/> />
Water Flow <span className="text-white">Water Flow</span>
</label> </label>
<label className="flex items-center gap-2 text-sm cursor-pointer"> <label className="flex items-center gap-3 text-sm cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={nutrients} checked={nutrients}
onChange={() => setNutrients(!nutrients)} onChange={() => setNutrients(!nutrients)}
className="w-5 h-5 rounded accent-accent" className="w-5 h-5 rounded bg-slate-700 border-slate-600 accent-emerald-500"
/> />
Nutrients <span className="text-white">Nutrients</span>
</label> </label>
</div> </div>
{/* Photo Capture */}
<PhotoCapture photoUrl={photo} onCapture={setPhoto} /> <PhotoCapture photoUrl={photo} onCapture={setPhoto} />
{/* Save Button */} <button onClick={handleSave} className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl font-bold uppercase tracking-widest text-xs transition-all">
<button onClick={handleSave} className="btn btn-primary w-full">
Save Check Save Check
</button> </button>
</div> </div>
); );
} }
// Plant Health Row - Enhanced with photo // Plant Health Row
function PlantHealthRow({ function PlantHealthRow({
zoneName, data, onChange zoneName, data, onChange
}: { }: {
@ -668,31 +809,37 @@ function PlantHealthRow({
if (!editing && data) { if (!editing && data) {
return ( return (
<div className="flex items-center justify-between p-3 bg-tertiary rounded-lg"> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded-xl border border-slate-700/50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className={`w-2 h-10 rounded-full ${data.healthStatus === 'GOOD' && !data.pestsObserved ? 'bg-success' : <div className={cn(
data.healthStatus === 'FAIR' ? 'bg-warning' : 'bg-destructive' "w-2 h-12 rounded-full",
}`} /> data.healthStatus === 'GOOD' && !data.pestsObserved ? 'bg-emerald-500' :
data.healthStatus === 'FAIR' ? 'bg-amber-500' : 'bg-red-500'
)} />
<div> <div>
<span className="text-sm font-medium text-primary">{zoneName}</span> <span className="text-sm font-bold text-white">{zoneName}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 mt-0.5">
<span className={`text-xs ${data.healthStatus === 'GOOD' ? 'text-success' : data.healthStatus === 'FAIR' ? 'text-warning' : 'text-destructive'}`}> <span className={cn(
"text-xs font-bold uppercase tracking-wider",
data.healthStatus === 'GOOD' ? 'text-emerald-500' :
data.healthStatus === 'FAIR' ? 'text-amber-500' : 'text-red-500'
)}>
{data.healthStatus} {data.healthStatus}
</span> </span>
{data.pestsObserved && <span className="text-xs text-destructive">🐛</span>} {data.pestsObserved && <span className="text-xs">🐛</span>}
{data.issuePhotoUrl && <Camera size={10} className="text-accent" />} {data.issuePhotoUrl && <Camera size={10} className="text-emerald-500" />}
</div> </div>
</div> </div>
</div> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent font-medium">Edit</button> <button onClick={() => setEditing(true)} className="text-xs text-emerald-500 font-bold uppercase tracking-wider">Edit</button>
</div> </div>
); );
} }
return ( return (
<div className="p-4 bg-tertiary rounded-lg space-y-3"> <div className="p-4 bg-slate-800/50 rounded-xl border border-slate-700/50 space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-primary">{zoneName}</span> <span className="text-sm font-bold text-white">{zoneName}</span>
</div> </div>
{/* Health Status Buttons */} {/* Health Status Buttons */}
@ -701,27 +848,29 @@ function PlantHealthRow({
<button <button
key={s} key={s}
onClick={() => setHealth(s)} onClick={() => setHealth(s)}
className={`py-3 rounded-lg text-sm font-medium transition-all ${health === s className={cn(
? s === 'GOOD' ? 'bg-success text-white shadow-lg scale-105' "py-3 rounded-xl text-xs font-bold uppercase tracking-wider transition-all border",
: s === 'FAIR' ? 'bg-warning text-white shadow-lg scale-105' health === s
: 'bg-destructive text-white shadow-lg scale-105' ? s === 'GOOD' ? 'bg-emerald-600 text-white border-emerald-500 shadow-lg scale-105'
: 'bg-subtle text-secondary hover:bg-secondary' : s === 'FAIR' ? 'bg-amber-600 text-white border-amber-500 shadow-lg scale-105'
}`} : 'bg-red-600 text-white border-red-500 shadow-lg scale-105'
: 'bg-slate-800 text-slate-400 border-slate-700 hover:bg-slate-700'
)}
> >
{s === 'GOOD' ? '✓ Good' : s === 'FAIR' ? '⚠️ Fair' : '❌ Attention'} {s === 'GOOD' ? '✓ Good' : s === 'FAIR' ? '⚠ Fair' : '✕ Attention'}
</button> </button>
))} ))}
</div> </div>
{/* Pests Checkbox */} {/* Pests Checkbox */}
<label className="flex items-center justify-center gap-2 p-3 bg-destructive-muted rounded-lg cursor-pointer"> <label className="flex items-center justify-center gap-3 p-4 bg-red-500/10 rounded-xl cursor-pointer border border-red-500/20">
<input <input
type="checkbox" type="checkbox"
checked={pests} checked={pests}
onChange={() => setPests(!pests)} onChange={() => setPests(!pests)}
className="w-5 h-5 rounded accent-destructive" className="w-5 h-5 rounded accent-red-500"
/> />
<span className="text-sm text-destructive font-medium">🐛 Pests Observed</span> <span className="text-sm text-red-400 font-bold uppercase tracking-wider">🐛 Pests Observed</span>
</label> </label>
{/* Notes */} {/* Notes */}
@ -731,20 +880,19 @@ function PlantHealthRow({
value={notes} value={notes}
onChange={(e) => setNotes(e.target.value)} onChange={(e) => setNotes(e.target.value)}
placeholder="Describe the issue..." placeholder="Describe the issue..."
className="w-full px-3 py-2 bg-primary border border-subtle rounded-lg text-sm" className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-sm text-white placeholder:text-slate-500 focus:border-emerald-500/50 focus:outline-none"
/> />
)} )}
{/* Photo Capture - shown for issues */} {/* Photo Capture - shown for issues */}
{(health !== 'GOOD' || pests) && ( {(health !== 'GOOD' || pests) && (
<> <>
<p className="text-xs text-tertiary text-center">📷 Photo recommended for issues</p> <p className="text-xs text-slate-500 text-center">📷 Photo recommended for issues</p>
<PhotoCapture photoUrl={photo} onCapture={setPhoto} /> <PhotoCapture photoUrl={photo} onCapture={setPhoto} />
</> </>
)} )}
{/* Save Button */} <button onClick={handleSave} className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl font-bold uppercase tracking-widest text-xs transition-all">
<button onClick={handleSave} className="btn btn-primary w-full">
Save Check Save Check
</button> </button>
</div> </div>

View file

@ -1,8 +1,12 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Save, CheckSquare, Settings, Camera, Loader2 } from 'lucide-react'; import { Link } from 'react-router-dom';
import { Save, CheckSquare, Settings, Camera, Loader2, ArrowLeft, BookOpen, FileText, Droplets, Sprout, Bug } from 'lucide-react';
import { settingsApi, WalkthroughSettings, PhotoRequirement } from '../lib/settingsApi'; import { settingsApi, WalkthroughSettings, PhotoRequirement } from '../lib/settingsApi';
import { PageHeader } from '../components/ui/LinearPrimitives'; import { documentsApi, Document } from '../lib/documentsApi';
import { Card } from '../components/ui/card';
import { useToast } from '../context/ToastContext'; import { useToast } from '../context/ToastContext';
import { cn } from '../lib/utils';
import { motion } from 'framer-motion';
const PHOTO_OPTIONS: { label: string; value: PhotoRequirement }[] = [ const PHOTO_OPTIONS: { label: string; value: PhotoRequirement }[] = [
{ label: 'Always Required', value: 'REQUIRED' }, { label: 'Always Required', value: 'REQUIRED' },
@ -16,6 +20,7 @@ export default function WalkthroughSettingsPage() {
const [settings, setSettings] = useState<WalkthroughSettings | null>(null); const [settings, setSettings] = useState<WalkthroughSettings | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [checklists, setChecklists] = useState<Document[]>([]);
useEffect(() => { useEffect(() => {
loadSettings(); loadSettings();
@ -24,8 +29,12 @@ export default function WalkthroughSettingsPage() {
const loadSettings = async () => { const loadSettings = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const data = await settingsApi.getWalkthrough(); const [data, docs] = await Promise.all([
settingsApi.getWalkthrough(),
documentsApi.getDocuments({ type: 'CHECKLIST', status: 'APPROVED' }).catch(() => [])
]);
setSettings(data); setSettings(data);
setChecklists(docs);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
addToast('Failed to load settings', 'error'); addToast('Failed to load settings', 'error');
@ -52,88 +61,136 @@ export default function WalkthroughSettingsPage() {
if (isLoading || !settings) { if (isLoading || !settings) {
return ( return (
<div className="max-w-2xl mx-auto space-y-6"> <div className="min-h-screen bg-[#0B0E14] flex items-center justify-center">
<PageHeader title="Walkthrough Settings" subtitle="Loading..." /> <div className="text-center">
<div className="card p-8 flex justify-center"> <Loader2 className="animate-spin text-emerald-500 mx-auto mb-4" size={32} />
<Loader2 className="animate-spin text-tertiary" size={24} /> <p className="text-slate-400 text-sm">Loading settings...</p>
</div> </div>
</div> </div>
); );
} }
// Toggle switch component // Toggle switch component
const Toggle = ({ checked, onChange, label }: { checked: boolean; onChange: () => void; label: string }) => ( const Toggle = ({ checked, onChange, label, description, icon: Icon }: {
<label className="flex items-center justify-between p-3 rounded-md hover:bg-tertiary transition-colors duration-fast cursor-pointer"> checked: boolean;
<span className="text-sm text-primary">{label}</span> onChange: () => void;
label: string;
description?: string;
icon?: typeof Droplets;
}) => (
<label className="flex items-center justify-between p-4 rounded-xl hover:bg-slate-800/30 transition-colors cursor-pointer">
<div className="flex items-center gap-4">
{Icon && (
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center",
checked ? "bg-emerald-500/20 border border-emerald-500/30" : "bg-slate-800 border border-slate-700"
)}>
<Icon size={18} className={checked ? "text-emerald-500" : "text-slate-500"} />
</div>
)}
<div>
<span className="text-sm font-bold text-white">{label}</span>
{description && <p className="text-xs text-slate-500 mt-0.5">{description}</p>}
</div>
</div>
<button <button
type="button" type="button"
role="switch" role="switch"
aria-checked={checked} aria-checked={checked}
onClick={onChange} onClick={onChange}
className={` className={cn(
relative w-10 h-6 rounded-full transition-colors duration-fast "relative w-12 h-7 rounded-full transition-colors duration-200",
${checked ? 'bg-accent' : 'bg-tertiary'} checked ? 'bg-emerald-600' : 'bg-slate-700'
`} )}
> >
<span <motion.span
className={` animate={{ x: checked ? 22 : 2 }}
absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition={{ type: "spring", stiffness: 500, damping: 30 }}
transition-transform duration-fast className="absolute top-1 w-5 h-5 bg-white rounded-full shadow-lg"
${checked ? 'translate-x-4' : ''}
`}
/> />
</button> </button>
</label> </label>
); );
return ( return (
<div className="max-w-2xl mx-auto space-y-6 pb-20 animate-in"> <div className="min-h-screen bg-[#0B0E14] pb-20">
<PageHeader {/* Header */}
title="Walkthrough Settings" <div className="sticky top-0 z-20 bg-[#0B0E14]/95 backdrop-blur-xl border-b border-slate-800/50">
subtitle="Configure daily checklist requirements" <div className="max-w-2xl mx-auto px-4 py-4">
/> <div className="flex items-center gap-4">
<Link to="/settings" className="p-2 rounded-lg hover:bg-slate-800 transition-colors text-slate-400">
<form onSubmit={handleUpdate} className="space-y-6"> <ArrowLeft size={20} />
{/* Enabled Sections */} </Link>
<div className="card"> <div>
<div className="p-4 border-b border-subtle flex items-center gap-2"> <h1 className="text-lg font-bold text-white uppercase italic tracking-wide">
<CheckSquare size={16} className="text-accent" /> Walkthrough Config
<h3 className="text-sm font-medium text-primary">Enabled Modules</h3> </h1>
<p className="text-xs text-slate-500 mt-0.5">
Configure daily checklist requirements
</p>
</div> </div>
<div className="p-2"> </div>
</div>
</div>
<div className="max-w-2xl mx-auto px-4 py-6">
<form onSubmit={handleUpdate} className="space-y-6">
{/* Enabled Modules */}
<Card className="bg-[#13171F] border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-emerald-500/20 flex items-center justify-center">
<CheckSquare size={16} className="text-emerald-500" />
</div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wide">Enabled Modules</h3>
<p className="text-xs text-slate-500">Choose which checks to include</p>
</div>
</div>
<div className="divide-y divide-slate-800/50">
<Toggle <Toggle
checked={settings.enableReservoirs} checked={settings.enableReservoirs}
onChange={() => setSettings({ ...settings, enableReservoirs: !settings.enableReservoirs })} onChange={() => setSettings({ ...settings, enableReservoirs: !settings.enableReservoirs })}
label="Reservoir Checks" label="Reservoir Checks"
description="Tank levels and nutrient monitoring"
icon={Droplets}
/> />
<Toggle <Toggle
checked={settings.enableIrrigation} checked={settings.enableIrrigation}
onChange={() => setSettings({ ...settings, enableIrrigation: !settings.enableIrrigation })} onChange={() => setSettings({ ...settings, enableIrrigation: !settings.enableIrrigation })}
label="Irrigation Checks" label="Irrigation Checks"
description="Dripper counts and flow verification"
icon={Sprout}
/> />
<Toggle <Toggle
checked={settings.enablePlantHealth} checked={settings.enablePlantHealth}
onChange={() => setSettings({ ...settings, enablePlantHealth: !settings.enablePlantHealth })} onChange={() => setSettings({ ...settings, enablePlantHealth: !settings.enablePlantHealth })}
label="Plant Health Checks" label="Plant Health Checks"
description="Visual inspections and pest monitoring"
icon={Bug}
/> />
</div> </div>
</div> </Card>
{/* Photo Requirements */} {/* Photo Requirements */}
<div className="card"> <Card className="bg-[#13171F] border-slate-800 overflow-hidden">
<div className="p-4 border-b border-subtle flex items-center gap-2"> <div className="p-4 border-b border-slate-800 flex items-center gap-3">
<Camera size={16} className="text-accent" /> <div className="w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center">
<h3 className="text-sm font-medium text-primary">Photo Requirements</h3> <Camera size={16} className="text-blue-500" />
</div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wide">Photo Requirements</h3>
<p className="text-xs text-slate-500">When to require photo documentation</p>
</div>
</div> </div>
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div> <div>
<label className="block text-xs font-medium text-tertiary uppercase tracking-wider mb-2"> <label className="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">
Reservoirs Reservoirs
</label> </label>
<select <select
value={settings.reservoirPhotos} value={settings.reservoirPhotos}
onChange={e => setSettings({ ...settings, reservoirPhotos: e.target.value as PhotoRequirement })} onChange={e => setSettings({ ...settings, reservoirPhotos: e.target.value as PhotoRequirement })}
className="input w-full" className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-sm text-white focus:border-emerald-500/50 focus:outline-none"
> >
{PHOTO_OPTIONS.map(opt => ( {PHOTO_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
@ -141,13 +198,13 @@ export default function WalkthroughSettingsPage() {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-tertiary uppercase tracking-wider mb-2"> <label className="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">
Irrigation Irrigation
</label> </label>
<select <select
value={settings.irrigationPhotos} value={settings.irrigationPhotos}
onChange={e => setSettings({ ...settings, irrigationPhotos: e.target.value as PhotoRequirement })} onChange={e => setSettings({ ...settings, irrigationPhotos: e.target.value as PhotoRequirement })}
className="input w-full" className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-sm text-white focus:border-emerald-500/50 focus:outline-none"
> >
{PHOTO_OPTIONS.map(opt => ( {PHOTO_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
@ -155,13 +212,13 @@ export default function WalkthroughSettingsPage() {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-tertiary uppercase tracking-wider mb-2"> <label className="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">
Plant Health Plant Health
</label> </label>
<select <select
value={settings.plantHealthPhotos} value={settings.plantHealthPhotos}
onChange={e => setSettings({ ...settings, plantHealthPhotos: e.target.value as PhotoRequirement })} onChange={e => setSettings({ ...settings, plantHealthPhotos: e.target.value as PhotoRequirement })}
className="input w-full" className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-sm text-white focus:border-emerald-500/50 focus:outline-none"
> >
{PHOTO_OPTIONS.map(opt => ( {PHOTO_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
@ -169,12 +226,44 @@ export default function WalkthroughSettingsPage() {
</select> </select>
</div> </div>
</div> </div>
</Card>
{/* Linked SOPs */}
{checklists.length > 0 && (
<Card className="bg-[#13171F] border-slate-800 overflow-hidden">
<div className="p-4 border-b border-slate-800 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
<BookOpen size={16} className="text-purple-500" />
</div> </div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-wide">Linked SOPs</h3>
<p className="text-xs text-slate-500">Related checklist documents</p>
</div>
</div>
<div className="p-4 space-y-2">
{checklists.map(doc => (
<div key={doc.id} className="flex items-center gap-3 p-3 bg-slate-800/50 rounded-xl">
<FileText size={16} className="text-slate-400" />
<div className="flex-1">
<span className="text-sm text-white">{doc.title}</span>
<span className="text-xs text-slate-500 ml-2">v{doc.version}</span>
</div>
<Link
to={`/documents?id=${doc.id}`}
className="text-xs text-emerald-500 font-bold uppercase tracking-wider hover:text-emerald-400"
>
View
</Link>
</div>
))}
</div>
</Card>
)}
<button <button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="btn btn-primary w-full h-12" className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-4 rounded-xl font-bold uppercase tracking-widest text-sm transition-all shadow-xl shadow-emerald-500/20 disabled:opacity-50 flex items-center justify-center gap-2"
> >
{isSaving ? ( {isSaving ? (
<> <>
@ -190,5 +279,6 @@ export default function WalkthroughSettingsPage() {
</button> </button>
</form> </form>
</div> </div>
</div>
); );
} }