feat: global breadcrumbs + walkthrough photo upload
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

Breadcrumbs:
- Added Breadcrumbs to main Layout (appears on ALL pages)
- Dynamic route support (/batches/:id, /rooms/:id)
- Proper navigation hierarchy

Daily Walkthrough:
- Enhanced layout with progress bar
- Photo capture from camera or file upload
- Notes fields for each check
- Improved touch targets and mobile UX

Removed inline breadcrumbs from individual pages since
they now come from the global Layout.
This commit is contained in:
fullsizemalt 2025-12-12 21:22:01 -08:00
parent 9897e73de1
commit 15b50a74c6
3 changed files with 385 additions and 165 deletions

View file

@ -12,6 +12,7 @@ import { SessionTimeoutWarning } from './ui/SessionTimeoutWarning';
import { PageTitleUpdater } from '../hooks/usePageTitle'; import { PageTitleUpdater } from '../hooks/usePageTitle';
import AnnouncementBanner from './AnnouncementBanner'; import AnnouncementBanner from './AnnouncementBanner';
import { DevTools } from './dev/DevTools'; import { DevTools } from './dev/DevTools';
import { Breadcrumbs } from './ui/Breadcrumbs';
export default function Layout() { export default function Layout() {
const { user } = useAuth(); const { user } = useAuth();
@ -136,6 +137,8 @@ export default function Layout() {
<PageTitleUpdater /> <PageTitleUpdater />
<AnnouncementBanner /> <AnnouncementBanner />
<div className="p-4 md:p-6 lg:p-8"> <div className="p-4 md:p-6 lg:p-8">
{/* Global Breadcrumbs - appears on all pages */}
<Breadcrumbs />
<Outlet /> <Outlet />
</div> </div>
</main> </main>

View file

@ -33,14 +33,34 @@ const ROUTE_CONFIG: Record<string, { label: string; parent?: string }> = {
'/settings/walkthrough': { label: 'Walkthrough Settings', parent: '/settings' }, '/settings/walkthrough': { label: 'Walkthrough Settings', parent: '/settings' },
}; };
// Dynamic route patterns
const DYNAMIC_ROUTES: { pattern: RegExp; getLabel: (match: RegExpMatchArray) => string; parent: string }[] = [
{ pattern: /^\/batches\/(.+)$/, getLabel: () => 'Batch Details', parent: '/batches' },
{ pattern: /^\/rooms\/(.+)$/, getLabel: () => 'Room Details', parent: '/rooms' },
{ pattern: /^\/tasks\/(.+)$/, getLabel: () => 'Task Details', parent: '/tasks' },
];
function getBreadcrumbs(pathname: string): BreadcrumbItem[] { function getBreadcrumbs(pathname: string): BreadcrumbItem[] {
const crumbs: BreadcrumbItem[] = []; const crumbs: BreadcrumbItem[] = [];
let currentPath = pathname; let currentPath = pathname;
// Build breadcrumb chain // Check for dynamic routes first
for (const route of DYNAMIC_ROUTES) {
const match = pathname.match(route.pattern);
if (match) {
crumbs.unshift({ label: route.getLabel(match), path: pathname });
currentPath = route.parent;
break;
}
}
// Build breadcrumb chain from static routes
while (currentPath && ROUTE_CONFIG[currentPath]) { while (currentPath && ROUTE_CONFIG[currentPath]) {
const config = ROUTE_CONFIG[currentPath]; const config = ROUTE_CONFIG[currentPath];
// Don't add if already in crumbs (from dynamic route)
if (!crumbs.find(c => c.path === currentPath)) {
crumbs.unshift({ label: config.label, path: currentPath }); crumbs.unshift({ label: config.label, path: currentPath });
}
currentPath = config.parent || ''; currentPath = config.parent || '';
} }
@ -58,11 +78,11 @@ export function Breadcrumbs() {
return ( return (
<nav aria-label="Breadcrumb" className="mb-4"> <nav aria-label="Breadcrumb" className="mb-4">
<ol className="flex items-center gap-1 text-sm"> <ol className="flex items-center gap-1 text-sm flex-wrap">
<li> <li>
<Link <Link
to="/" to="/"
className="p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors" className="p-1.5 rounded-md hover:bg-tertiary text-tertiary hover:text-primary transition-colors flex items-center"
aria-label="Home" aria-label="Home"
> >
<Home size={16} /> <Home size={16} />
@ -77,15 +97,15 @@ export function Breadcrumbs() {
return ( return (
<li key={crumb.path} className="flex items-center gap-1"> <li key={crumb.path} className="flex items-center gap-1">
<ChevronRight size={14} className="text-slate-300 dark:text-slate-600" /> <ChevronRight size={14} className="text-tertiary flex-shrink-0" />
{isLast ? ( {isLast ? (
<span className="px-2 py-1 font-medium text-slate-900 dark:text-white"> <span className="px-2 py-1 font-medium text-primary">
{crumb.label} {crumb.label}
</span> </span>
) : ( ) : (
<Link <Link
to={crumb.path} to={crumb.path}
className="px-2 py-1 rounded text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" className="px-2 py-1 rounded-md text-tertiary hover:text-primary hover:bg-tertiary transition-colors"
> >
{crumb.label} {crumb.label}
</Link> </Link>
@ -100,9 +120,18 @@ export function Breadcrumbs() {
// Page title helper // Page title helper
export function getPageTitle(pathname: string): string { export function getPageTitle(pathname: string): string {
// Check dynamic routes first
for (const route of DYNAMIC_ROUTES) {
const match = pathname.match(route.pattern);
if (match) {
return `${route.getLabel(match)} | 777 Wolfpack`;
}
}
const config = ROUTE_CONFIG[pathname]; const config = ROUTE_CONFIG[pathname];
if (config) { if (config) {
return `${config.label} | 777 Wolfpack`; return `${config.label} | 777 Wolfpack`;
} }
return '777 Wolfpack - Grow Ops Manager'; return '777 Wolfpack - Grow Ops Manager';
} }

View file

@ -1,10 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useNavigate } 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 } from '../lib/walkthroughApi'; import { walkthroughApi, ReservoirCheckData, IrrigationCheckData, PlantHealthCheckData } from '../lib/walkthroughApi';
import { import {
ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug, Check, Loader2, Droplets, Sprout, Bug,
Camera, X, Minus, Plus, ChevronDown, ChevronUp Camera, X, Minus, Plus, ChevronDown, ChevronUp, Upload
} from 'lucide-react'; } from 'lucide-react';
import { useToast } from '../context/ToastContext'; import { useToast } from '../context/ToastContext';
@ -92,12 +92,16 @@ export default function DailyWalkthroughPage() {
const totalChecks = Object.keys(reservoirChecks).length + Object.keys(irrigationChecks).length + Object.keys(plantHealthChecks).length; const totalChecks = Object.keys(reservoirChecks).length + Object.keys(irrigationChecks).length + Object.keys(plantHealthChecks).length;
const requiredChecks = TANKS.length + ZONES.length + HEALTH_ZONES.length; const requiredChecks = TANKS.length + ZONES.length + HEALTH_ZONES.length;
const isComplete = totalChecks === requiredChecks; const isComplete = totalChecks === requiredChecks;
const progressPercent = Math.round((totalChecks / requiredChecks) * 100);
// 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-primary flex items-center justify-center p-4">
<div className="w-full max-w-sm text-center"> <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>
<h1 className="text-xl font-semibold text-primary mb-2">Daily Walkthrough</h1> <h1 className="text-xl font-semibold text-primary mb-2">Daily Walkthrough</h1>
<p className="text-sm text-tertiary mb-6"> <p className="text-sm text-tertiary mb-6">
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })} {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
@ -105,28 +109,46 @@ export default function DailyWalkthroughPage() {
<button <button
onClick={handleStart} onClick={handleStart}
disabled={isStarting} disabled={isStarting}
className="btn btn-primary w-full" className="btn btn-primary w-full text-base py-3"
> >
{isStarting ? <Loader2 size={14} className="animate-spin" /> : 'Begin Walkthrough'} {isStarting ? <Loader2 size={18} className="animate-spin" /> : 'Begin Walkthrough'}
</button> </button>
<Link to="/" className="block mt-4 text-sm text-tertiary hover:text-secondary">
Back to Dashboard
</Link>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="max-w-2xl mx-auto pb-24 space-y-4 animate-in"> <div className="max-w-2xl mx-auto pb-28 animate-in">
{/* Header */}
<div className="flex items-center gap-3 mb-6"> {/* Header with Progress */}
<button onClick={() => navigate('/')} className="p-2 -ml-2 hover:bg-tertiary rounded-md"> <div className="card p-4 mb-4">
<ArrowLeft size={16} className="text-secondary" /> <div className="flex items-center justify-between mb-3">
</button>
<div> <div>
<h1 className="text-lg font-semibold text-primary">Daily Walkthrough</h1> <h1 className="text-lg font-semibold text-primary">Daily Walkthrough</h1>
<p className="text-xs text-tertiary">{totalChecks}/{requiredChecks} checks completed</p> <p className="text-xs text-tertiary">
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
</p>
</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>
{/* Sections */}
<div className="space-y-3">
{/* Reservoirs Section */} {/* Reservoirs Section */}
{settings?.enableReservoirs !== false && ( {settings?.enableReservoirs !== false && (
<CollapsibleSection <CollapsibleSection
@ -195,19 +217,20 @@ export default function DailyWalkthroughPage() {
</div> </div>
</CollapsibleSection> </CollapsibleSection>
)} )}
</div>
{/* Fixed Submit Button */} {/* Fixed Submit Button */}
<div className="fixed bottom-0 left-0 right-0 p-4 bg-primary border-t border-default"> <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="max-w-2xl mx-auto">
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={!isComplete || isSubmitting} disabled={!isComplete || isSubmitting}
className="btn btn-primary w-full" className={`btn w-full py-3 text-base ${isComplete ? 'btn-primary' : 'bg-subtle text-tertiary cursor-not-allowed'}`}
> >
{isSubmitting ? ( {isSubmitting ? (
<><Loader2 size={14} className="animate-spin" /> Submitting...</> <><Loader2 size={18} className="animate-spin" /> Submitting...</>
) : isComplete ? ( ) : isComplete ? (
<><Check size={14} /> Submit Walkthrough</> <><Check size={18} /> Submit Walkthrough</>
) : ( ) : (
`Complete all checks (${totalChecks}/${requiredChecks})` `Complete all checks (${totalChecks}/${requiredChecks})`
)} )}
@ -238,23 +261,86 @@ function CollapsibleSection({
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-tertiary transition-colors"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-md flex items-center justify-center ${isComplete ? 'bg-success-muted text-success' : 'bg-accent-muted text-accent' <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={14} /> : <Icon size={14} />} {isComplete ? <Check size={18} /> : <Icon size={18} />}
</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-medium text-primary">{title}</h3>
<p className="text-xs text-tertiary">{count}/{total} complete</p> <p className="text-xs text-tertiary">{count}/{total} complete</p>
</div> </div>
</div> </div>
{expanded ? <ChevronUp size={16} className="text-tertiary" /> : <ChevronDown size={16} className="text-tertiary" />} <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>
</button> </button>
{expanded && <div className="p-4 pt-0 border-t border-subtle">{children}</div>} {expanded && <div className="p-4 pt-0 border-t border-subtle">{children}</div>}
</div> </div>
); );
} }
// Reservoir Row - Compact inline // Inline Photo Capture Component
function PhotoCapture({
onCapture,
photoUrl
}: {
onCapture: (url: string | null) => void;
photoUrl?: string | null;
}) {
const fileInputRef = useRef<HTMLInputElement>(null);
const cameraInputRef = useRef<HTMLInputElement>(null);
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);
}
};
if (photoUrl) {
return (
<div className="relative mt-2">
<img src={photoUrl} alt="Captured" className="w-full h-24 object-cover rounded-lg" />
<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"
>
<X size={12} />
</button>
</div>
);
}
return (
<div className="flex gap-2 mt-2">
<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"
>
<Camera size={14} />
Camera
</button>
<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"
>
<Upload size={14} />
Upload
</button>
</div>
);
}
// Reservoir Row - Enhanced with photo
function ReservoirRow({ function ReservoirRow({
tank, data, onChange tank, data, onChange
}: { }: {
@ -263,6 +349,8 @@ function ReservoirRow({
onChange: (data: ReservoirCheckData) => void; onChange: (data: ReservoirCheckData) => void;
}) { }) {
const [level, setLevel] = useState(data?.levelPercent ?? 100); const [level, setLevel] = useState(data?.levelPercent ?? 100);
const [photo, setPhoto] = useState<string | null>(data?.photoUrl || null);
const [notes, setNotes] = useState(data?.notes || '');
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';
@ -273,56 +361,84 @@ function ReservoirRow({
tankType: tank.type, tankType: tank.type,
levelPercent: level, levelPercent: level,
status: getStatus(level), status: getStatus(level),
photoUrl: photo || undefined,
notes: notes || undefined,
}); });
setEditing(false); setEditing(false);
}; };
if (!editing && data) { if (!editing && data) {
return ( return (
<div className="flex items-center justify-between p-3 bg-tertiary rounded-md"> <div className="flex items-center justify-between p-3 bg-tertiary rounded-lg">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-2 h-8 rounded-full ${data.status === 'OK' ? 'bg-success' : data.status === 'LOW' ? 'bg-warning' : 'bg-destructive' <div className={`w-2 h-10 rounded-full ${data.status === 'OK' ? 'bg-success' : data.status === 'LOW' ? 'bg-warning' : 'bg-destructive'
}`} /> }`} />
<div> <div>
<span className="text-sm text-primary">{tank.name}</span> <span className="text-sm font-medium text-primary">{tank.name}</span>
<span className="text-xs text-tertiary ml-2">{data.levelPercent}%</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" />}
</div> </div>
</div> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent">Edit</button> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent font-medium">Edit</button>
</div> </div>
); );
} }
return ( return (
<div className="p-3 bg-tertiary rounded-md"> <div className="p-4 bg-tertiary rounded-lg space-y-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-primary">{tank.name}</span> <span className="text-sm font-medium text-primary">{tank.name}</span>
<span className="text-xs text-tertiary">{tank.type}</span> <span className={`text-xs px-2 py-0.5 rounded ${tank.type === 'VEG' ? 'bg-success-muted text-success' : 'bg-accent-muted text-accent'}`}>
{tank.type}
</span>
</div> </div>
{/* Level Slider */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-3 h-12 rounded-full overflow-hidden bg-subtle relative`}> <div className={`w-4 h-14 rounded-full overflow-hidden bg-subtle 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={`absolute bottom-0 left-0 right-0 transition-all ${getStatus(level) === 'OK' ? 'bg-success' : getStatus(level) === 'LOW' ? 'bg-warning' : 'bg-destructive'}`}
}`}
style={{ height: `${level}%` }} style={{ height: `${level}%` }}
/> />
</div> </div>
<div className="flex-1">
<input <input
type="range" type="range"
min="0" min="0"
max="100" max="100"
value={level} value={level}
onChange={(e) => setLevel(parseInt(e.target.value))} onChange={(e) => setLevel(parseInt(e.target.value))}
className="flex-1 h-1.5 bg-subtle rounded-full appearance-none cursor-pointer accent-accent" className="w-full h-2 bg-subtle rounded-full appearance-none cursor-pointer"
/> />
<span className="text-sm font-medium text-primary w-12 text-right">{level}%</span>
<button onClick={handleSave} className="btn btn-primary h-8 px-3 text-xs">Save</button>
</div> </div>
<span className={`text-lg font-bold w-14 text-right ${getStatus(level) === 'OK' ? 'text-success' : getStatus(level) === 'LOW' ? 'text-warning' : 'text-destructive'}`}>
{level}%
</span>
</div>
{/* Notes */}
<input
type="text"
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"
/>
{/* Photo Capture */}
<PhotoCapture photoUrl={photo} onCapture={setPhoto} />
{/* Save Button */}
<button onClick={handleSave} className="btn btn-primary w-full">
Save Check
</button>
</div> </div>
); );
} }
// Irrigation Row - Compact inline // Irrigation Row - Enhanced with photo
function IrrigationRow({ function IrrigationRow({
zone, data, onChange zone, data, onChange
}: { }: {
@ -333,6 +449,7 @@ function IrrigationRow({
const [working, setWorking] = useState(data?.drippersWorking ?? zone.drippers); const [working, setWorking] = useState(data?.drippersWorking ?? zone.drippers);
const [waterFlow, setWaterFlow] = useState(data?.waterFlow ?? true); const [waterFlow, setWaterFlow] = useState(data?.waterFlow ?? true);
const [nutrients, setNutrients] = useState(data?.nutrientsMixed ?? true); const [nutrients, setNutrients] = useState(data?.nutrientsMixed ?? true);
const [photo, setPhoto] = useState<string | null>(null);
const [editing, setEditing] = useState(!data); const [editing, setEditing] = useState(!data);
const handleSave = () => { const handleSave = () => {
@ -344,6 +461,7 @@ function IrrigationRow({
waterFlow, waterFlow,
nutrientsMixed: nutrients, nutrientsMixed: nutrients,
scheduleActive: true, scheduleActive: true,
photoUrl: photo || undefined,
}); });
setEditing(false); setEditing(false);
}; };
@ -351,50 +469,82 @@ 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-md"> <div className="flex items-center justify-between p-3 bg-tertiary rounded-lg">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-2 h-8 rounded-full ${issues ? 'bg-warning' : 'bg-success'}`} /> <div className={`w-2 h-10 rounded-full ${issues ? 'bg-warning' : 'bg-success'}`} />
<div> <div>
<span className="text-sm text-primary">{zone.name}</span> <span className="text-sm font-medium text-primary">{zone.name}</span>
<span className="text-xs text-tertiary ml-2">{data.drippersWorking}/{data.drippersTotal}</span> <div className="flex items-center gap-2">
<span className="text-xs text-tertiary">{data.drippersWorking}/{data.drippersTotal} drippers</span>
</div> </div>
</div> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent">Edit</button> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent font-medium">Edit</button>
</div> </div>
); );
} }
return ( return (
<div className="p-3 bg-tertiary rounded-md space-y-3"> <div className="p-4 bg-tertiary rounded-lg space-y-3">
<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-medium text-primary">{zone.name}</span>
<div className="flex items-center gap-2"> </div>
<button onClick={() => setWorking(Math.max(0, working - 1))} className="w-6 h-6 bg-subtle rounded flex items-center justify-center">
<Minus size={12} /> {/* Dripper Count */}
<div className="flex items-center justify-center gap-4">
<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"
>
<Minus size={16} />
</button> </button>
<span className="text-sm w-16 text-center">{working}/{zone.drippers}</span> <div className="text-center">
<button onClick={() => setWorking(Math.min(zone.drippers, working + 1))} className="w-6 h-6 bg-subtle rounded flex items-center justify-center"> <span className="text-2xl font-bold text-primary">{working}</span>
<Plus size={12} /> <span className="text-sm text-tertiary">/{zone.drippers}</span>
<p className="text-xs text-tertiary">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"
>
<Plus size={16} />
</button> </button>
</div> </div>
</div>
<div className="flex items-center gap-4"> {/* Checkboxes */}
<label className="flex items-center gap-2 text-xs"> <div className="flex items-center justify-center gap-6">
<input type="checkbox" checked={waterFlow} onChange={() => setWaterFlow(!waterFlow)} className="w-4 h-4 rounded" /> <label className="flex items-center gap-2 text-sm cursor-pointer">
Water <input
type="checkbox"
checked={waterFlow}
onChange={() => setWaterFlow(!waterFlow)}
className="w-5 h-5 rounded accent-accent"
/>
Water Flow
</label> </label>
<label className="flex items-center gap-2 text-xs"> <label className="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" checked={nutrients} onChange={() => setNutrients(!nutrients)} className="w-4 h-4 rounded" /> <input
type="checkbox"
checked={nutrients}
onChange={() => setNutrients(!nutrients)}
className="w-5 h-5 rounded accent-accent"
/>
Nutrients Nutrients
</label> </label>
<div className="flex-1" />
<button onClick={handleSave} className="btn btn-primary h-8 px-3 text-xs">Save</button>
</div> </div>
{/* Photo Capture */}
<PhotoCapture photoUrl={photo} onCapture={setPhoto} />
{/* Save Button */}
<button onClick={handleSave} className="btn btn-primary w-full">
Save Check
</button>
</div> </div>
); );
} }
// Plant Health Row - Compact inline // Plant Health Row - Enhanced with photo
function PlantHealthRow({ function PlantHealthRow({
zoneName, data, onChange zoneName, data, onChange
}: { }: {
@ -404,6 +554,8 @@ function PlantHealthRow({
}) { }) {
const [health, setHealth] = useState<'GOOD' | 'FAIR' | 'NEEDS_ATTENTION'>(data?.healthStatus ?? 'GOOD'); const [health, setHealth] = useState<'GOOD' | 'FAIR' | 'NEEDS_ATTENTION'>(data?.healthStatus ?? 'GOOD');
const [pests, setPests] = useState(data?.pestsObserved ?? false); const [pests, setPests] = useState(data?.pestsObserved ?? false);
const [photo, setPhoto] = useState<string | null>(null);
const [notes, setNotes] = useState('');
const [editing, setEditing] = useState(!data); const [editing, setEditing] = useState(!data);
const handleSave = () => { const handleSave = () => {
@ -414,57 +566,93 @@ function PlantHealthRow({
waterAccess: 'OK', waterAccess: 'OK',
foodAccess: 'OK', foodAccess: 'OK',
flaggedForAttention: health !== 'GOOD' || pests, flaggedForAttention: health !== 'GOOD' || pests,
issuePhotoUrl: photo || undefined,
notes: notes || undefined,
}); });
setEditing(false); setEditing(false);
}; };
if (!editing && data) { if (!editing && data) {
const issues = data.healthStatus !== 'GOOD' || data.pestsObserved;
return ( return (
<div className="flex items-center justify-between p-3 bg-tertiary rounded-md"> <div className="flex items-center justify-between p-3 bg-tertiary rounded-lg">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-2 h-8 rounded-full ${data.healthStatus === 'GOOD' && !data.pestsObserved ? 'bg-success' : <div className={`w-2 h-10 rounded-full ${data.healthStatus === 'GOOD' && !data.pestsObserved ? 'bg-success' :
data.healthStatus === 'FAIR' ? 'bg-warning' : 'bg-destructive' data.healthStatus === 'FAIR' ? 'bg-warning' : 'bg-destructive'
}`} /> }`} />
<div> <div>
<span className="text-sm text-primary">{zoneName}</span> <span className="text-sm font-medium text-primary">{zoneName}</span>
<span className="text-xs text-tertiary ml-2">{data.healthStatus}</span> <div className="flex items-center gap-2">
{data.pestsObserved && <span className="text-xs text-destructive ml-2">🐛 Pests</span>} <span className={`text-xs ${data.healthStatus === 'GOOD' ? 'text-success' : data.healthStatus === 'FAIR' ? 'text-warning' : 'text-destructive'}`}>
{data.healthStatus}
</span>
{data.pestsObserved && <span className="text-xs text-destructive">🐛</span>}
{data.issuePhotoUrl && <Camera size={10} className="text-accent" />}
</div> </div>
</div> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent">Edit</button> </div>
<button onClick={() => setEditing(true)} className="text-xs text-accent font-medium">Edit</button>
</div> </div>
); );
} }
return ( return (
<div className="p-3 bg-tertiary rounded-md space-y-3"> <div className="p-4 bg-tertiary rounded-lg space-y-3">
<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-medium text-primary">{zoneName}</span>
</div> </div>
<div className="flex items-center gap-2">
{/* Health Status Buttons */}
<div className="grid grid-cols-3 gap-2">
{(['GOOD', 'FAIR', 'NEEDS_ATTENTION'] as const).map(s => ( {(['GOOD', 'FAIR', 'NEEDS_ATTENTION'] as const).map(s => (
<button <button
key={s} key={s}
onClick={() => setHealth(s)} onClick={() => setHealth(s)}
className={`flex-1 py-1.5 rounded text-xs font-medium transition-colors ${health === s className={`py-3 rounded-lg text-sm font-medium transition-all ${health === s
? s === 'GOOD' ? 'bg-success text-white' ? s === 'GOOD' ? 'bg-success text-white shadow-lg scale-105'
: s === 'FAIR' ? 'bg-warning text-white' : s === 'FAIR' ? 'bg-warning text-white shadow-lg scale-105'
: 'bg-destructive text-white' : 'bg-destructive text-white shadow-lg scale-105'
: 'bg-subtle text-secondary hover:bg-secondary' : 'bg-subtle text-secondary hover:bg-secondary'
}`} }`}
> >
{s === 'NEEDS_ATTENTION' ? 'Attention' : s} {s === 'GOOD' ? '✓ Good' : s === 'FAIR' ? '⚠️ Fair' : '❌ Attention'}
</button> </button>
))} ))}
</div> </div>
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 text-xs"> {/* Pests Checkbox */}
<input type="checkbox" checked={pests} onChange={() => setPests(!pests)} className="w-4 h-4 rounded" /> <label className="flex items-center justify-center gap-2 p-3 bg-destructive-muted rounded-lg cursor-pointer">
Pests observed <input
type="checkbox"
checked={pests}
onChange={() => setPests(!pests)}
className="w-5 h-5 rounded accent-destructive"
/>
<span className="text-sm text-destructive font-medium">🐛 Pests Observed</span>
</label> </label>
<button onClick={handleSave} className="btn btn-primary h-8 px-3 text-xs">Save</button>
</div> {/* Notes */}
{(health !== 'GOOD' || pests) && (
<input
type="text"
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"
/>
)}
{/* Photo Capture - shown for issues */}
{(health !== 'GOOD' || pests) && (
<>
<p className="text-xs text-tertiary text-center">📷 Photo recommended for issues</p>
<PhotoCapture photoUrl={photo} onCapture={setPhoto} />
</>
)}
{/* Save Button */}
<button onClick={handleSave} className="btn btn-primary w-full">
Save Check
</button>
</div> </div>
); );
} }