- Complete UI refactor with charcoal/bone color palette - Add Space Grotesk font for headlines, Inter for body - Update all 24+ pages with new design system - Add LinearPrimitives reusable components - Improve dark mode support throughout - Add subtle micro-animations and transitions
213 lines
9.8 KiB
TypeScript
213 lines
9.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { batchesApi, Batch } from '../lib/batchesApi';
|
|
import { touchPointsApi } from '../lib/touchPointsApi';
|
|
import { Droplets, Utensils, Scissors, Dumbbell, Search, ShieldCheck, Shovel, Sprout, Fingerprint, ChevronLeft, Loader2 } from 'lucide-react';
|
|
import { PageHeader, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives';
|
|
import { useToast } from '../context/ToastContext';
|
|
|
|
export default function TouchPointPage() {
|
|
const { addToast } = useToast();
|
|
const [batches, setBatches] = useState<Batch[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [selectedBatch, setSelectedBatch] = useState<Batch | null>(null);
|
|
const [actionType, setActionType] = useState<string | null>(null);
|
|
const [notes, setNotes] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
batchesApi.getAll().then(data => {
|
|
setBatches(data.filter(b => b.status === 'ACTIVE'));
|
|
setIsLoading(false);
|
|
}).catch(err => {
|
|
console.error(err);
|
|
setIsLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
const actions = [
|
|
{ id: 'WATER', label: 'Water', icon: Droplets, accent: 'accent' as const },
|
|
{ id: 'FEED', label: 'Feed', icon: Utensils, accent: 'success' as const },
|
|
{ id: 'PRUNE', label: 'Prune', icon: Scissors, accent: 'warning' as const },
|
|
{ id: 'TRAIN', label: 'Train', icon: Dumbbell, accent: 'accent' as const },
|
|
{ id: 'INSPECT', label: 'Inspect', icon: Search, accent: 'default' as const },
|
|
{ id: 'IPM', label: 'IPM', icon: ShieldCheck, accent: 'destructive' as const },
|
|
{ id: 'TRANSPLANT', label: 'Transplant', icon: Shovel, accent: 'warning' as const },
|
|
{ id: 'HARVEST', label: 'Harvest', icon: Sprout, accent: 'success' as const },
|
|
];
|
|
|
|
const getAccentClasses = (accent: string) => {
|
|
switch (accent) {
|
|
case 'success': return 'bg-success-muted text-success border-success/20 hover:border-success/40';
|
|
case 'warning': return 'bg-warning-muted text-warning border-warning/20 hover:border-warning/40';
|
|
case 'destructive': return 'bg-destructive-muted text-destructive border-destructive/20 hover:border-destructive/40';
|
|
case 'accent': return 'bg-accent-muted text-accent border-accent/20 hover:border-accent/40';
|
|
default: return 'bg-tertiary text-secondary border-default hover:border-strong';
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!selectedBatch || !actionType) return;
|
|
setSubmitting(true);
|
|
try {
|
|
await touchPointsApi.create({
|
|
batchId: selectedBatch.id,
|
|
type: actionType as any,
|
|
notes,
|
|
});
|
|
addToast('Touch point recorded!', 'success');
|
|
setActionType(null);
|
|
setNotes('');
|
|
} catch (err) {
|
|
addToast('Failed to record', 'error');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-6 animate-in">
|
|
<PageHeader title="Quick Actions" subtitle="Record plant interactions" />
|
|
<div className="grid grid-cols-1 gap-3">
|
|
{Array.from({ length: 4 }).map((_, i) => <CardSkeleton key={i} />)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 animate-in">
|
|
<PageHeader
|
|
title="Quick Actions"
|
|
subtitle="Record plant interactions efficiently"
|
|
/>
|
|
|
|
{/* Step 1: Select Batch */}
|
|
{!selectedBatch ? (
|
|
<div className="space-y-4">
|
|
<h3 className="text-xs font-medium text-tertiary uppercase tracking-wider px-1">
|
|
Select Batch
|
|
</h3>
|
|
|
|
{batches.length === 0 ? (
|
|
<EmptyState
|
|
icon={Fingerprint}
|
|
title="No active batches"
|
|
description="Create a batch to start recording touch points."
|
|
/>
|
|
) : (
|
|
<div className="grid gap-2">
|
|
{batches.map(batch => (
|
|
<button
|
|
key={batch.id}
|
|
onClick={() => setSelectedBatch(batch)}
|
|
className="card card-interactive p-4 text-left group"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="font-medium text-primary text-sm group-hover:text-accent transition-colors duration-fast">
|
|
{batch.name?.replace('[DEMO] ', '')}
|
|
</div>
|
|
<div className="text-xs text-tertiary">{batch.strain}</div>
|
|
</div>
|
|
<ChevronLeft size={16} className="text-tertiary rotate-180 group-hover:translate-x-1 transition-transform duration-fast" />
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : !actionType ? (
|
|
/* Step 2: Select Action */
|
|
<div className="space-y-4">
|
|
<button
|
|
onClick={() => setSelectedBatch(null)}
|
|
className="flex items-center gap-1 text-xs text-tertiary hover:text-primary transition-colors duration-fast"
|
|
>
|
|
<ChevronLeft size={14} />
|
|
Back to Batches
|
|
</button>
|
|
|
|
<div className="card p-4 flex items-center justify-between">
|
|
<div>
|
|
<div className="font-medium text-primary text-sm">
|
|
{selectedBatch.name?.replace('[DEMO] ', '')}
|
|
</div>
|
|
<div className="text-xs text-tertiary">Select an action</div>
|
|
</div>
|
|
<span className="badge">{selectedBatch.strain}</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
{actions.map(action => (
|
|
<button
|
|
key={action.id}
|
|
onClick={() => setActionType(action.id)}
|
|
className={`
|
|
p-5 rounded-lg border flex flex-col items-center gap-3
|
|
transition-all duration-fast active:scale-95
|
|
${getAccentClasses(action.accent)}
|
|
`}
|
|
>
|
|
<action.icon size={24} />
|
|
<span className="text-sm font-medium">{action.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* Step 3: Confirm */
|
|
<div className="space-y-4">
|
|
<button
|
|
onClick={() => setActionType(null)}
|
|
className="flex items-center gap-1 text-xs text-tertiary hover:text-primary transition-colors duration-fast"
|
|
>
|
|
<ChevronLeft size={14} />
|
|
Back to Actions
|
|
</button>
|
|
|
|
<div className="flex items-center gap-2 text-xl font-semibold text-primary">
|
|
{(() => {
|
|
const action = actions.find(a => a.id === actionType);
|
|
const Icon = action?.icon || Fingerprint;
|
|
return <Icon size={24} className="text-accent" />;
|
|
})()}
|
|
{actions.find(a => a.id === actionType)?.label}
|
|
<span className="text-tertiary font-normal text-base">
|
|
for {selectedBatch.name?.replace('[DEMO] ', '')}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="card p-4 space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-tertiary uppercase tracking-wider mb-2">
|
|
Notes (Optional)
|
|
</label>
|
|
<textarea
|
|
value={notes}
|
|
onChange={e => setNotes(e.target.value)}
|
|
className="input w-full h-32 resize-none py-3"
|
|
placeholder="Add details about this action..."
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={submitting}
|
|
className="btn btn-primary w-full h-12"
|
|
>
|
|
{submitting ? (
|
|
<>
|
|
<Loader2 size={16} className="animate-spin" />
|
|
Saving...
|
|
</>
|
|
) : (
|
|
'Save Record'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|