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 { settingsApi, WalkthroughSettings } from '../lib/settingsApi';
|
||||
import { walkthroughApi, ReservoirCheckData, IrrigationCheckData, PlantHealthCheckData, Walkthrough } from '../lib/walkthroughApi';
|
||||
import { documentsApi, Document } from '../lib/documentsApi';
|
||||
import {
|
||||
Check, Loader2, Droplets, Sprout, Bug,
|
||||
Camera, X, Minus, Plus, ChevronDown, ChevronUp, Upload, CheckCircle, Clock
|
||||
Check, Loader2, Droplets, Sprout, Bug, ArrowLeft,
|
||||
Camera, X, Minus, Plus, ChevronDown, ChevronUp, Upload, CheckCircle2, Clock,
|
||||
FileText, AlertTriangle, Thermometer, Wind, Leaf, BookOpen, CircleCheck
|
||||
} from 'lucide-react';
|
||||
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
|
||||
const TANKS = [
|
||||
|
|
@ -34,6 +39,7 @@ export default function DailyWalkthroughPage() {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [settings, setSettings] = useState<WalkthroughSettings | null>(null);
|
||||
const [todaysWalkthrough, setTodaysWalkthrough] = useState<Walkthrough | null>(null);
|
||||
const [relatedSOPs, setRelatedSOPs] = useState<Document[]>([]);
|
||||
|
||||
// All check states
|
||||
const [reservoirChecks, setReservoirChecks] = useState<Record<string, ReservoirCheckData>>({});
|
||||
|
|
@ -51,12 +57,14 @@ export default function DailyWalkthroughPage() {
|
|||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [settingsData, todayData] = await Promise.all([
|
||||
const [settingsData, todayData, sops] = await Promise.all([
|
||||
settingsApi.getWalkthrough().catch(() => null),
|
||||
walkthroughApi.getToday().catch(() => null)
|
||||
walkthroughApi.getToday().catch(() => null),
|
||||
documentsApi.getDocuments({ type: 'CHECKLIST', status: 'APPROVED' }).catch(() => [])
|
||||
]);
|
||||
setSettings(settingsData);
|
||||
setTodaysWalkthrough(todayData);
|
||||
setRelatedSOPs(sops);
|
||||
} catch (error) {
|
||||
console.error('Failed to load walkthrough data:', error);
|
||||
} finally {
|
||||
|
|
@ -111,13 +119,17 @@ export default function DailyWalkthroughPage() {
|
|||
const isComplete = totalChecks === requiredChecks;
|
||||
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
|
||||
if (isLoading) {
|
||||
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">
|
||||
<Loader2 size={32} className="animate-spin text-accent mx-auto mb-4" />
|
||||
<p className="text-tertiary">Loading...</p>
|
||||
<Loader2 size={40} className="animate-spin text-emerald-500 mx-auto mb-4" />
|
||||
<p className="text-slate-400 text-sm font-medium">Loading walkthrough...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -127,36 +139,42 @@ export default function DailyWalkthroughPage() {
|
|||
if (todaysWalkthrough?.status === 'COMPLETED' && !walkthroughId) {
|
||||
const completedTime = new Date(todaysWalkthrough.endTime || todaysWalkthrough.startTime);
|
||||
return (
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm text-center">
|
||||
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-success/20 flex items-center justify-center">
|
||||
<CheckCircle size={48} className="text-success" />
|
||||
<div className="min-h-screen bg-[#0B0E14] flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
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>
|
||||
<h1 className="text-xl font-semibold text-primary mb-2">Walkthrough Complete!</h1>
|
||||
<p className="text-sm text-tertiary mb-2">
|
||||
<h1 className="text-2xl font-bold text-white uppercase italic tracking-wide 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' })}
|
||||
</p>
|
||||
<div className="bg-success/10 border border-success/20 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-center justify-center gap-2 text-success">
|
||||
<Clock size={16} />
|
||||
<span className="text-sm font-medium">
|
||||
<Card className="bg-emerald-500/10 border-emerald-500/20 p-5 mb-8">
|
||||
<div className="flex items-center justify-center gap-3 text-emerald-400">
|
||||
<Clock size={18} />
|
||||
<span className="text-base font-bold">
|
||||
Completed at {completedTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-tertiary mt-1">
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
by {todaysWalkthrough.user?.name || 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
<Link to="/" className="btn btn-primary w-full text-base py-3">
|
||||
</Card>
|
||||
<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
|
||||
</Link>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -164,25 +182,31 @@ export default function DailyWalkthroughPage() {
|
|||
// In-progress walkthrough from earlier
|
||||
if (todaysWalkthrough?.status === 'IN_PROGRESS' && !walkthroughId) {
|
||||
return (
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm text-center">
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-warning/20 flex items-center justify-center">
|
||||
<Clock size={36} className="text-warning" />
|
||||
<div className="min-h-screen bg-[#0B0E14] flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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>
|
||||
<h1 className="text-xl font-semibold text-primary mb-2">Walkthrough In Progress</h1>
|
||||
<p className="text-sm text-tertiary mb-6">
|
||||
<h1 className="text-2xl font-bold text-white uppercase italic tracking-wide mb-2">
|
||||
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' })}
|
||||
</p>
|
||||
<button
|
||||
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
|
||||
</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
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -190,70 +214,154 @@ export default function DailyWalkthroughPage() {
|
|||
// Pre-start view
|
||||
if (!walkthroughId) {
|
||||
return (
|
||||
<div className="min-h-screen bg-primary flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm text-center">
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-accent-muted flex items-center justify-center">
|
||||
<Sprout size={36} className="text-accent" />
|
||||
<div className="min-h-screen bg-[#0B0E14] flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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>
|
||||
<h1 className="text-xl font-semibold text-primary mb-2">Daily Walkthrough</h1>
|
||||
<p className="text-sm text-tertiary mb-6">
|
||||
<h1 className="text-3xl font-bold text-white uppercase italic tracking-wide mb-2">
|
||||
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' })}
|
||||
</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
|
||||
onClick={handleStart}
|
||||
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>
|
||||
<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
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto pb-28 animate-in">
|
||||
|
||||
{/* Header with Progress */}
|
||||
<div className="card p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="min-h-screen bg-[#0B0E14] pb-28">
|
||||
{/* Sticky Header */}
|
||||
<div className="sticky top-0 z-20 bg-[#0B0E14]/95 backdrop-blur-xl border-b border-slate-800/50">
|
||||
<div className="max-w-3xl mx-auto px-4 py-4">
|
||||
<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>
|
||||
<h1 className="text-lg font-semibold text-primary">Daily Walkthrough</h1>
|
||||
<p className="text-xs text-tertiary">
|
||||
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
|
||||
</p>
|
||||
<h1 className="text-lg font-bold text-white uppercase italic tracking-wide">
|
||||
Daily Walkthrough
|
||||
</h1>
|
||||
<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 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>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="space-y-3">
|
||||
{/* Reservoirs Section */}
|
||||
{settings?.enableReservoirs !== false && (
|
||||
<CollapsibleSection
|
||||
title="Reservoirs"
|
||||
title="Reservoir Checks"
|
||||
icon={Droplets}
|
||||
iconColor="text-blue-400"
|
||||
count={Object.keys(reservoirChecks).length}
|
||||
total={TANKS.length}
|
||||
expanded={expandedSections.reservoirs}
|
||||
onToggle={() => toggleSection('reservoirs')}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{TANKS.map(tank => (
|
||||
<ReservoirRow
|
||||
key={tank.name}
|
||||
|
|
@ -266,17 +374,17 @@ export default function DailyWalkthroughPage() {
|
|||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Irrigation Section */}
|
||||
{settings?.enableIrrigation !== false && (
|
||||
<CollapsibleSection
|
||||
title="Irrigation"
|
||||
title="Irrigation Checks"
|
||||
icon={Sprout}
|
||||
iconColor="text-emerald-400"
|
||||
count={Object.keys(irrigationChecks).length}
|
||||
total={ZONES.length}
|
||||
expanded={expandedSections.irrigation}
|
||||
onToggle={() => toggleSection('irrigation')}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{ZONES.map(zone => (
|
||||
<IrrigationRow
|
||||
key={zone.name}
|
||||
|
|
@ -289,17 +397,17 @@ export default function DailyWalkthroughPage() {
|
|||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Plant Health Section */}
|
||||
{settings?.enablePlantHealth !== false && (
|
||||
<CollapsibleSection
|
||||
title="Plant Health"
|
||||
title="Plant Health Checks"
|
||||
icon={Bug}
|
||||
iconColor="text-amber-400"
|
||||
count={Object.keys(plantHealthChecks).length}
|
||||
total={HEALTH_ZONES.length}
|
||||
expanded={expandedSections.plantHealth}
|
||||
onToggle={() => toggleSection('plantHealth')}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{HEALTH_ZONES.map(zone => (
|
||||
<PlantHealthRow
|
||||
key={zone}
|
||||
|
|
@ -314,17 +422,22 @@ export default function DailyWalkthroughPage() {
|
|||
</div>
|
||||
|
||||
{/* 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="max-w-2xl mx-auto">
|
||||
<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-3xl mx-auto">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
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 ? (
|
||||
<><Loader2 size={18} className="animate-spin" /> Submitting...</>
|
||||
) : isComplete ? (
|
||||
<><Check size={18} /> Submit Walkthrough</>
|
||||
<><CircleCheck size={18} /> Submit Walkthrough</>
|
||||
) : (
|
||||
`Complete all checks (${totalChecks}/${requiredChecks})`
|
||||
)}
|
||||
|
|
@ -337,10 +450,11 @@ export default function DailyWalkthroughPage() {
|
|||
|
||||
// Collapsible Section
|
||||
function CollapsibleSection({
|
||||
title, icon: Icon, count, total, expanded, onToggle, children
|
||||
title, icon: Icon, iconColor, count, total, expanded, onToggle, children
|
||||
}: {
|
||||
title: string;
|
||||
icon: typeof Droplets;
|
||||
iconColor: string;
|
||||
count: number;
|
||||
total: number;
|
||||
expanded: boolean;
|
||||
|
|
@ -349,28 +463,55 @@ function CollapsibleSection({
|
|||
}) {
|
||||
const isComplete = count === total;
|
||||
return (
|
||||
<div className="card overflow-hidden">
|
||||
<Card className="bg-[#13171F] border-slate-800 overflow-hidden">
|
||||
<button
|
||||
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={`w-10 h-10 rounded-lg flex items-center justify-center ${isComplete ? 'bg-success-muted text-success' : 'bg-accent-muted text-accent'
|
||||
}`}>
|
||||
{isComplete ? <Check size={18} /> : <Icon size={18} />}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={cn(
|
||||
"w-12 h-12 rounded-xl flex items-center justify-center transition-all",
|
||||
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 className="text-left">
|
||||
<h3 className="text-sm font-medium text-primary">{title}</h3>
|
||||
<p className="text-xs text-tertiary">{count}/{total} complete</p>
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-wide">{title}</h3>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
{count}/{total} complete
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isComplete && <span className="text-xs text-success font-medium">✓ Done</span>}
|
||||
{expanded ? <ChevronUp size={18} className="text-tertiary" /> : <ChevronDown size={18} className="text-tertiary" />}
|
||||
<div className="flex items-center gap-3">
|
||||
{isComplete && <span className="text-xs text-emerald-500 font-bold uppercase tracking-wider">Done</span>}
|
||||
<motion.div
|
||||
animate={{ rotate: expanded ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronDown size={18} className="text-slate-500" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</button>
|
||||
{expanded && <div className="p-4 pt-0 border-t border-subtle">{children}</div>}
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{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 file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// For now, create a local preview URL
|
||||
// In production, this would upload to server
|
||||
const url = URL.createObjectURL(file);
|
||||
onCapture(url);
|
||||
}
|
||||
|
|
@ -397,27 +536,27 @@ function PhotoCapture({
|
|||
|
||||
if (photoUrl) {
|
||||
return (
|
||||
<div className="relative mt-2">
|
||||
<img src={photoUrl} alt="Captured" className="w-full h-24 object-cover rounded-lg" />
|
||||
<div className="relative mt-3">
|
||||
<img src={photoUrl} alt="Captured" className="w-full h-28 object-cover rounded-xl border border-slate-700" />
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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={cameraInputRef} type="file" accept="image/*" capture="environment" onChange={handleFile} className="hidden" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
|
|
@ -425,7 +564,7 @@ function PhotoCapture({
|
|||
<button
|
||||
type="button"
|
||||
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
|
||||
|
|
@ -434,7 +573,7 @@ function PhotoCapture({
|
|||
);
|
||||
}
|
||||
|
||||
// Reservoir Row - Enhanced with photo
|
||||
// Reservoir Row
|
||||
function ReservoirRow({
|
||||
tank, data, onChange
|
||||
}: {
|
||||
|
|
@ -448,6 +587,7 @@ function ReservoirRow({
|
|||
const [editing, setEditing] = useState(!data);
|
||||
|
||||
const getStatus = (l: number) => l >= 70 ? 'OK' : l >= 30 ? 'LOW' : 'CRITICAL';
|
||||
const statusColor = getStatus(level) === 'OK' ? 'emerald' : getStatus(level) === 'LOW' ? 'amber' : 'red';
|
||||
|
||||
const handleSave = () => {
|
||||
onChange({
|
||||
|
|
@ -463,37 +603,42 @@ function ReservoirRow({
|
|||
|
||||
if (!editing && data) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 bg-tertiary rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-2 h-10 rounded-full ${data.status === 'OK' ? 'bg-success' : data.status === 'LOW' ? 'bg-warning' : 'bg-destructive'
|
||||
}`} />
|
||||
<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-4">
|
||||
<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>
|
||||
<span className="text-sm font-medium text-primary">{tank.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-tertiary">{data.levelPercent}%</span>
|
||||
{data.photoUrl && <Camera size={10} className="text-accent" />}
|
||||
<span className="text-sm font-bold text-white">{tank.name}</span>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-xs text-slate-400">{data.levelPercent}%</span>
|
||||
{data.photoUrl && <Camera size={10} className="text-emerald-500" />}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<span className="text-sm font-medium text-primary">{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="text-sm font-bold text-white">{tank.name}</span>
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Level Slider */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-4 h-14 rounded-full overflow-hidden bg-subtle relative`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-5 h-16 rounded-full overflow-hidden bg-slate-700 relative">
|
||||
<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}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -504,10 +649,10 @@ function ReservoirRow({
|
|||
max="100"
|
||||
value={level}
|
||||
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>
|
||||
<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}%
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -518,21 +663,19 @@ function ReservoirRow({
|
|||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
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} />
|
||||
|
||||
{/* Save Button */}
|
||||
<button onClick={handleSave} className="btn btn-primary w-full">
|
||||
<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">
|
||||
Save Check
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Irrigation Row - Enhanced with photo
|
||||
// Irrigation Row
|
||||
function IrrigationRow({
|
||||
zone, data, onChange
|
||||
}: {
|
||||
|
|
@ -563,82 +706,80 @@ function IrrigationRow({
|
|||
if (!editing && data) {
|
||||
const issues = !data.waterFlow || !data.nutrientsMixed || data.drippersWorking < data.drippersTotal;
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 bg-tertiary rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-2 h-10 rounded-full ${issues ? 'bg-warning' : 'bg-success'}`} />
|
||||
<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-4">
|
||||
<div className={cn("w-2 h-12 rounded-full", issues ? 'bg-amber-500' : 'bg-emerald-500')} />
|
||||
<div>
|
||||
<span className="text-sm font-medium text-primary">{zone.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-tertiary">{data.drippersWorking}/{data.drippersTotal} drippers</span>
|
||||
<span className="text-sm font-bold text-white">{zone.name}</span>
|
||||
<div className="text-xs text-slate-400 mt-0.5">
|
||||
{data.drippersWorking}/{data.drippersTotal} drippers
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<span className="text-sm font-medium text-primary">{zone.name}</span>
|
||||
<span className="text-sm font-bold text-white">{zone.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Dripper Count */}
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<button
|
||||
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>
|
||||
<div className="text-center">
|
||||
<span className="text-2xl font-bold text-primary">{working}</span>
|
||||
<span className="text-sm text-tertiary">/{zone.drippers}</span>
|
||||
<p className="text-xs text-tertiary">drippers working</p>
|
||||
<span className="text-3xl font-bold text-white">{working}</span>
|
||||
<span className="text-lg text-slate-500">/{zone.drippers}</span>
|
||||
<p className="text-xs text-slate-500 mt-1">drippers working</p>
|
||||
</div>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<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
|
||||
type="checkbox"
|
||||
checked={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 className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<label className="flex items-center gap-3 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={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>
|
||||
</div>
|
||||
|
||||
{/* Photo Capture */}
|
||||
<PhotoCapture photoUrl={photo} onCapture={setPhoto} />
|
||||
|
||||
{/* Save Button */}
|
||||
<button onClick={handleSave} className="btn btn-primary w-full">
|
||||
<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">
|
||||
Save Check
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Plant Health Row - Enhanced with photo
|
||||
// Plant Health Row
|
||||
function PlantHealthRow({
|
||||
zoneName, data, onChange
|
||||
}: {
|
||||
|
|
@ -668,31 +809,37 @@ function PlantHealthRow({
|
|||
|
||||
if (!editing && data) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 bg-tertiary rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-2 h-10 rounded-full ${data.healthStatus === 'GOOD' && !data.pestsObserved ? 'bg-success' :
|
||||
data.healthStatus === 'FAIR' ? 'bg-warning' : 'bg-destructive'
|
||||
}`} />
|
||||
<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-4">
|
||||
<div className={cn(
|
||||
"w-2 h-12 rounded-full",
|
||||
data.healthStatus === 'GOOD' && !data.pestsObserved ? 'bg-emerald-500' :
|
||||
data.healthStatus === 'FAIR' ? 'bg-amber-500' : 'bg-red-500'
|
||||
)} />
|
||||
<div>
|
||||
<span className="text-sm font-medium text-primary">{zoneName}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs ${data.healthStatus === 'GOOD' ? 'text-success' : data.healthStatus === 'FAIR' ? 'text-warning' : 'text-destructive'}`}>
|
||||
<span className="text-sm font-bold text-white">{zoneName}</span>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<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}
|
||||
</span>
|
||||
{data.pestsObserved && <span className="text-xs text-destructive">🐛</span>}
|
||||
{data.issuePhotoUrl && <Camera size={10} className="text-accent" />}
|
||||
{data.pestsObserved && <span className="text-xs">🐛</span>}
|
||||
{data.issuePhotoUrl && <Camera size={10} className="text-emerald-500" />}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<span className="text-sm font-medium text-primary">{zoneName}</span>
|
||||
<span className="text-sm font-bold text-white">{zoneName}</span>
|
||||
</div>
|
||||
|
||||
{/* Health Status Buttons */}
|
||||
|
|
@ -701,27 +848,29 @@ function PlantHealthRow({
|
|||
<button
|
||||
key={s}
|
||||
onClick={() => setHealth(s)}
|
||||
className={`py-3 rounded-lg text-sm font-medium transition-all ${health === s
|
||||
? s === 'GOOD' ? 'bg-success text-white shadow-lg scale-105'
|
||||
: s === 'FAIR' ? 'bg-warning text-white shadow-lg scale-105'
|
||||
: 'bg-destructive text-white shadow-lg scale-105'
|
||||
: 'bg-subtle text-secondary hover:bg-secondary'
|
||||
}`}
|
||||
className={cn(
|
||||
"py-3 rounded-xl text-xs font-bold uppercase tracking-wider transition-all border",
|
||||
health === s
|
||||
? s === 'GOOD' ? 'bg-emerald-600 text-white border-emerald-500 shadow-lg scale-105'
|
||||
: 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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
type="checkbox"
|
||||
checked={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>
|
||||
|
||||
{/* Notes */}
|
||||
|
|
@ -731,20 +880,19 @@ function PlantHealthRow({
|
|||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
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 */}
|
||||
{(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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
<button onClick={handleSave} className="btn btn-primary w-full">
|
||||
<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">
|
||||
Save Check
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
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 { PageHeader } from '../components/ui/LinearPrimitives';
|
||||
import { documentsApi, Document } from '../lib/documentsApi';
|
||||
import { Card } from '../components/ui/card';
|
||||
import { useToast } from '../context/ToastContext';
|
||||
import { cn } from '../lib/utils';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const PHOTO_OPTIONS: { label: string; value: PhotoRequirement }[] = [
|
||||
{ label: 'Always Required', value: 'REQUIRED' },
|
||||
|
|
@ -16,6 +20,7 @@ export default function WalkthroughSettingsPage() {
|
|||
const [settings, setSettings] = useState<WalkthroughSettings | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [checklists, setChecklists] = useState<Document[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
|
|
@ -24,8 +29,12 @@ export default function WalkthroughSettingsPage() {
|
|||
const loadSettings = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await settingsApi.getWalkthrough();
|
||||
const [data, docs] = await Promise.all([
|
||||
settingsApi.getWalkthrough(),
|
||||
documentsApi.getDocuments({ type: 'CHECKLIST', status: 'APPROVED' }).catch(() => [])
|
||||
]);
|
||||
setSettings(data);
|
||||
setChecklists(docs);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
addToast('Failed to load settings', 'error');
|
||||
|
|
@ -52,88 +61,136 @@ export default function WalkthroughSettingsPage() {
|
|||
|
||||
if (isLoading || !settings) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<PageHeader title="Walkthrough Settings" subtitle="Loading..." />
|
||||
<div className="card p-8 flex justify-center">
|
||||
<Loader2 className="animate-spin text-tertiary" size={24} />
|
||||
<div className="min-h-screen bg-[#0B0E14] flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="animate-spin text-emerald-500 mx-auto mb-4" size={32} />
|
||||
<p className="text-slate-400 text-sm">Loading settings...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Toggle switch component
|
||||
const Toggle = ({ checked, onChange, label }: { checked: boolean; onChange: () => void; label: string }) => (
|
||||
<label className="flex items-center justify-between p-3 rounded-md hover:bg-tertiary transition-colors duration-fast cursor-pointer">
|
||||
<span className="text-sm text-primary">{label}</span>
|
||||
const Toggle = ({ checked, onChange, label, description, icon: Icon }: {
|
||||
checked: boolean;
|
||||
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
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={onChange}
|
||||
className={`
|
||||
relative w-10 h-6 rounded-full transition-colors duration-fast
|
||||
${checked ? 'bg-accent' : 'bg-tertiary'}
|
||||
`}
|
||||
className={cn(
|
||||
"relative w-12 h-7 rounded-full transition-colors duration-200",
|
||||
checked ? 'bg-emerald-600' : 'bg-slate-700'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
absolute top-1 left-1 w-4 h-4 bg-white rounded-full
|
||||
transition-transform duration-fast
|
||||
${checked ? 'translate-x-4' : ''}
|
||||
`}
|
||||
<motion.span
|
||||
animate={{ x: checked ? 22 : 2 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
className="absolute top-1 w-5 h-5 bg-white rounded-full shadow-lg"
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6 pb-20 animate-in">
|
||||
<PageHeader
|
||||
title="Walkthrough Settings"
|
||||
subtitle="Configure daily checklist requirements"
|
||||
/>
|
||||
|
||||
<form onSubmit={handleUpdate} className="space-y-6">
|
||||
{/* Enabled Sections */}
|
||||
<div className="card">
|
||||
<div className="p-4 border-b border-subtle flex items-center gap-2">
|
||||
<CheckSquare size={16} className="text-accent" />
|
||||
<h3 className="text-sm font-medium text-primary">Enabled Modules</h3>
|
||||
<div className="min-h-screen bg-[#0B0E14] pb-20">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-20 bg-[#0B0E14]/95 backdrop-blur-xl border-b border-slate-800/50">
|
||||
<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">
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white uppercase italic tracking-wide">
|
||||
Walkthrough Config
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Configure daily checklist requirements
|
||||
</p>
|
||||
</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
|
||||
checked={settings.enableReservoirs}
|
||||
onChange={() => setSettings({ ...settings, enableReservoirs: !settings.enableReservoirs })}
|
||||
label="Reservoir Checks"
|
||||
description="Tank levels and nutrient monitoring"
|
||||
icon={Droplets}
|
||||
/>
|
||||
<Toggle
|
||||
checked={settings.enableIrrigation}
|
||||
onChange={() => setSettings({ ...settings, enableIrrigation: !settings.enableIrrigation })}
|
||||
label="Irrigation Checks"
|
||||
description="Dripper counts and flow verification"
|
||||
icon={Sprout}
|
||||
/>
|
||||
<Toggle
|
||||
checked={settings.enablePlantHealth}
|
||||
onChange={() => setSettings({ ...settings, enablePlantHealth: !settings.enablePlantHealth })}
|
||||
label="Plant Health Checks"
|
||||
description="Visual inspections and pest monitoring"
|
||||
icon={Bug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Photo Requirements */}
|
||||
<div className="card">
|
||||
<div className="p-4 border-b border-subtle flex items-center gap-2">
|
||||
<Camera size={16} className="text-accent" />
|
||||
<h3 className="text-sm font-medium text-primary">Photo Requirements</h3>
|
||||
<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-blue-500/20 flex items-center justify-center">
|
||||
<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 className="p-4 space-y-4">
|
||||
<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
|
||||
</label>
|
||||
<select
|
||||
value={settings.reservoirPhotos}
|
||||
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 => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
|
|
@ -141,13 +198,13 @@ export default function WalkthroughSettingsPage() {
|
|||
</select>
|
||||
</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
|
||||
</label>
|
||||
<select
|
||||
value={settings.irrigationPhotos}
|
||||
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 => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
|
|
@ -155,13 +212,13 @@ export default function WalkthroughSettingsPage() {
|
|||
</select>
|
||||
</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
|
||||
</label>
|
||||
<select
|
||||
value={settings.plantHealthPhotos}
|
||||
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 => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
|
|
@ -169,12 +226,44 @@ export default function WalkthroughSettingsPage() {
|
|||
</select>
|
||||
</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>
|
||||
<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
|
||||
type="submit"
|
||||
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 ? (
|
||||
<>
|
||||
|
|
@ -190,5 +279,6 @@ export default function WalkthroughSettingsPage() {
|
|||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue