feat: global breadcrumbs + walkthrough photo upload
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:
parent
9897e73de1
commit
15b50a74c6
3 changed files with 385 additions and 165 deletions
|
|
@ -12,6 +12,7 @@ import { SessionTimeoutWarning } from './ui/SessionTimeoutWarning';
|
|||
import { PageTitleUpdater } from '../hooks/usePageTitle';
|
||||
import AnnouncementBanner from './AnnouncementBanner';
|
||||
import { DevTools } from './dev/DevTools';
|
||||
import { Breadcrumbs } from './ui/Breadcrumbs';
|
||||
|
||||
export default function Layout() {
|
||||
const { user } = useAuth();
|
||||
|
|
@ -136,6 +137,8 @@ export default function Layout() {
|
|||
<PageTitleUpdater />
|
||||
<AnnouncementBanner />
|
||||
<div className="p-4 md:p-6 lg:p-8">
|
||||
{/* Global Breadcrumbs - appears on all pages */}
|
||||
<Breadcrumbs />
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -33,14 +33,34 @@ const ROUTE_CONFIG: Record<string, { label: string; parent?: string }> = {
|
|||
'/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[] {
|
||||
const crumbs: BreadcrumbItem[] = [];
|
||||
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]) {
|
||||
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 });
|
||||
}
|
||||
currentPath = config.parent || '';
|
||||
}
|
||||
|
||||
|
|
@ -58,11 +78,11 @@ export function Breadcrumbs() {
|
|||
|
||||
return (
|
||||
<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>
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
<Home size={16} />
|
||||
|
|
@ -77,15 +97,15 @@ export function Breadcrumbs() {
|
|||
|
||||
return (
|
||||
<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 ? (
|
||||
<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}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
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}
|
||||
</Link>
|
||||
|
|
@ -100,9 +120,18 @@ export function Breadcrumbs() {
|
|||
|
||||
// Page title helper
|
||||
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];
|
||||
if (config) {
|
||||
return `${config.label} | 777 Wolfpack`;
|
||||
}
|
||||
return '777 Wolfpack - Grow Ops Manager';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { settingsApi, WalkthroughSettings } from '../lib/settingsApi';
|
||||
import { walkthroughApi, ReservoirCheckData, IrrigationCheckData, PlantHealthCheckData } from '../lib/walkthroughApi';
|
||||
import {
|
||||
ArrowLeft, Check, Loader2, AlertCircle, Droplets, Sprout, Bug,
|
||||
Camera, X, Minus, Plus, ChevronDown, ChevronUp
|
||||
Check, Loader2, Droplets, Sprout, Bug,
|
||||
Camera, X, Minus, Plus, ChevronDown, ChevronUp, Upload
|
||||
} from 'lucide-react';
|
||||
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 requiredChecks = TANKS.length + ZONES.length + HEALTH_ZONES.length;
|
||||
const isComplete = totalChecks === requiredChecks;
|
||||
const progressPercent = Math.round((totalChecks / requiredChecks) * 100);
|
||||
|
||||
// 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>
|
||||
<h1 className="text-xl font-semibold text-primary mb-2">Daily Walkthrough</h1>
|
||||
<p className="text-sm text-tertiary mb-6">
|
||||
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
|
||||
|
|
@ -105,28 +109,46 @@ export default function DailyWalkthroughPage() {
|
|||
<button
|
||||
onClick={handleStart}
|
||||
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>
|
||||
<Link to="/" className="block mt-4 text-sm text-tertiary hover:text-secondary">
|
||||
← Back to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto pb-24 space-y-4 animate-in">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<button onClick={() => navigate('/')} className="p-2 -ml-2 hover:bg-tertiary rounded-md">
|
||||
<ArrowLeft size={16} className="text-secondary" />
|
||||
</button>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* Sections */}
|
||||
<div className="space-y-3">
|
||||
{/* Reservoirs Section */}
|
||||
{settings?.enableReservoirs !== false && (
|
||||
<CollapsibleSection
|
||||
|
|
@ -195,19 +217,20 @@ export default function DailyWalkthroughPage() {
|
|||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
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 ? (
|
||||
<><Loader2 size={14} className="animate-spin" /> Submitting...</>
|
||||
<><Loader2 size={18} className="animate-spin" /> Submitting...</>
|
||||
) : isComplete ? (
|
||||
<><Check size={14} /> Submit Walkthrough</>
|
||||
<><Check size={18} /> Submit Walkthrough</>
|
||||
) : (
|
||||
`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"
|
||||
>
|
||||
<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 className="text-left">
|
||||
<h3 className="text-sm font-medium text-primary">{title}</h3>
|
||||
<p className="text-xs text-tertiary">{count}/{total} complete</p>
|
||||
</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>
|
||||
{expanded && <div className="p-4 pt-0 border-t border-subtle">{children}</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({
|
||||
tank, data, onChange
|
||||
}: {
|
||||
|
|
@ -263,6 +349,8 @@ function ReservoirRow({
|
|||
onChange: (data: ReservoirCheckData) => void;
|
||||
}) {
|
||||
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 getStatus = (l: number) => l >= 70 ? 'OK' : l >= 30 ? 'LOW' : 'CRITICAL';
|
||||
|
|
@ -273,56 +361,84 @@ function ReservoirRow({
|
|||
tankType: tank.type,
|
||||
levelPercent: level,
|
||||
status: getStatus(level),
|
||||
photoUrl: photo || undefined,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
if (!editing && data) {
|
||||
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={`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>
|
||||
<span className="text-sm text-primary">{tank.name}</span>
|
||||
<span className="text-xs text-tertiary ml-2">{data.levelPercent}%</span>
|
||||
<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" />}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-tertiary rounded-md">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-4 bg-tertiary rounded-lg space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
|
||||
{/* Level Slider */}
|
||||
<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
|
||||
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}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={level}
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// Irrigation Row - Compact inline
|
||||
// Irrigation Row - Enhanced with photo
|
||||
function IrrigationRow({
|
||||
zone, data, onChange
|
||||
}: {
|
||||
|
|
@ -333,6 +449,7 @@ function IrrigationRow({
|
|||
const [working, setWorking] = useState(data?.drippersWorking ?? zone.drippers);
|
||||
const [waterFlow, setWaterFlow] = useState(data?.waterFlow ?? true);
|
||||
const [nutrients, setNutrients] = useState(data?.nutrientsMixed ?? true);
|
||||
const [photo, setPhoto] = useState<string | null>(null);
|
||||
const [editing, setEditing] = useState(!data);
|
||||
|
||||
const handleSave = () => {
|
||||
|
|
@ -344,6 +461,7 @@ function IrrigationRow({
|
|||
waterFlow,
|
||||
nutrientsMixed: nutrients,
|
||||
scheduleActive: true,
|
||||
photoUrl: photo || undefined,
|
||||
});
|
||||
setEditing(false);
|
||||
};
|
||||
|
|
@ -351,50 +469,82 @@ 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-md">
|
||||
<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-8 rounded-full ${issues ? 'bg-warning' : 'bg-success'}`} />
|
||||
<div className={`w-2 h-10 rounded-full ${issues ? 'bg-warning' : 'bg-success'}`} />
|
||||
<div>
|
||||
<span className="text-sm text-primary">{zone.name}</span>
|
||||
<span className="text-xs text-tertiary ml-2">{data.drippersWorking}/{data.drippersTotal}</span>
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<span className="text-sm font-medium text-primary">{zone.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => setWorking(Math.max(0, working - 1))} className="w-6 h-6 bg-subtle rounded flex items-center justify-center">
|
||||
<Minus size={12} />
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<span className="text-sm w-16 text-center">{working}/{zone.drippers}</span>
|
||||
<button onClick={() => setWorking(Math.min(zone.drippers, working + 1))} className="w-6 h-6 bg-subtle rounded flex items-center justify-center">
|
||||
<Plus size={12} />
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<input type="checkbox" checked={waterFlow} onChange={() => setWaterFlow(!waterFlow)} className="w-4 h-4 rounded" />
|
||||
Water
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={waterFlow}
|
||||
onChange={() => setWaterFlow(!waterFlow)}
|
||||
className="w-5 h-5 rounded accent-accent"
|
||||
/>
|
||||
Water Flow
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<input type="checkbox" checked={nutrients} onChange={() => setNutrients(!nutrients)} className="w-4 h-4 rounded" />
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nutrients}
|
||||
onChange={() => setNutrients(!nutrients)}
|
||||
className="w-5 h-5 rounded accent-accent"
|
||||
/>
|
||||
Nutrients
|
||||
</label>
|
||||
<div className="flex-1" />
|
||||
<button onClick={handleSave} className="btn btn-primary h-8 px-3 text-xs">Save</button>
|
||||
</div>
|
||||
|
||||
{/* Photo Capture */}
|
||||
<PhotoCapture photoUrl={photo} onCapture={setPhoto} />
|
||||
|
||||
{/* Save Button */}
|
||||
<button onClick={handleSave} className="btn btn-primary w-full">
|
||||
Save Check
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Plant Health Row - Compact inline
|
||||
// Plant Health Row - Enhanced with photo
|
||||
function PlantHealthRow({
|
||||
zoneName, data, onChange
|
||||
}: {
|
||||
|
|
@ -404,6 +554,8 @@ function PlantHealthRow({
|
|||
}) {
|
||||
const [health, setHealth] = useState<'GOOD' | 'FAIR' | 'NEEDS_ATTENTION'>(data?.healthStatus ?? 'GOOD');
|
||||
const [pests, setPests] = useState(data?.pestsObserved ?? false);
|
||||
const [photo, setPhoto] = useState<string | null>(null);
|
||||
const [notes, setNotes] = useState('');
|
||||
const [editing, setEditing] = useState(!data);
|
||||
|
||||
const handleSave = () => {
|
||||
|
|
@ -414,57 +566,93 @@ function PlantHealthRow({
|
|||
waterAccess: 'OK',
|
||||
foodAccess: 'OK',
|
||||
flaggedForAttention: health !== 'GOOD' || pests,
|
||||
issuePhotoUrl: photo || undefined,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
if (!editing && data) {
|
||||
const issues = data.healthStatus !== 'GOOD' || data.pestsObserved;
|
||||
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={`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'
|
||||
}`} />
|
||||
<div>
|
||||
<span className="text-sm text-primary">{zoneName}</span>
|
||||
<span className="text-xs text-tertiary ml-2">{data.healthStatus}</span>
|
||||
{data.pestsObserved && <span className="text-xs text-destructive ml-2">🐛 Pests</span>}
|
||||
<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'}`}>
|
||||
{data.healthStatus}
|
||||
</span>
|
||||
{data.pestsObserved && <span className="text-xs text-destructive">🐛</span>}
|
||||
{data.issuePhotoUrl && <Camera size={10} className="text-accent" />}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<span className="text-sm font-medium text-primary">{zoneName}</span>
|
||||
</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 => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setHealth(s)}
|
||||
className={`flex-1 py-1.5 rounded text-xs font-medium transition-colors ${health === s
|
||||
? s === 'GOOD' ? 'bg-success text-white'
|
||||
: s === 'FAIR' ? 'bg-warning text-white'
|
||||
: 'bg-destructive text-white'
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{s === 'NEEDS_ATTENTION' ? 'Attention' : s}
|
||||
{s === 'GOOD' ? '✓ Good' : s === 'FAIR' ? '⚠️ Fair' : '❌ Attention'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<input type="checkbox" checked={pests} onChange={() => setPests(!pests)} className="w-4 h-4 rounded" />
|
||||
Pests observed
|
||||
|
||||
{/* Pests Checkbox */}
|
||||
<label className="flex items-center justify-center gap-2 p-3 bg-destructive-muted rounded-lg cursor-pointer">
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue