feat: redesign Daily Walkthrough with Control Room aesthetic
- 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:
parent
26d4b5a3a5
commit
77e7382504
2 changed files with 532 additions and 294 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue