ca-grow-ops-manager/frontend/src/pages/BatchesPage.tsx
fullsizemalt 32fd739ccf
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
Test / backend-test (push) Failing after 0s
Test / frontend-test (push) Failing after 0s
feat: Complete Phases 8-13 implementation
Phase 8: Visitor Management
- Visitor/VisitorLog/AccessZone models
- Check-in/out with badge generation
- Zone occupancy tracking
- Kiosk and management pages

Phase 9: Messaging & Communication
- Announcements with priority levels
- Acknowledgement tracking
- Shift notes for team handoffs
- AnnouncementBanner component

Phase 10: Compliance & Audit Trail
- Immutable AuditLog model
- Document versioning and approval workflow
- Acknowledgement tracking for SOPs
- CSV export for audit logs

Phase 11: Accessibility & i18n
- WCAG 2.1 AA compliance utilities
- react-i18next with EN/ES translations
- User preferences context (theme, font size, etc)
- High contrast and reduced motion support

Phase 12: Hardware Integration
- QR code generation for batches/plants/visitors
- Printable label system
- Visitor badge printing

Phase 13: Advanced Features
- Environmental monitoring (sensors, readings, alerts)
- Financial tracking (transactions, P&L reports)
- AI/ML insights (yield predictions, anomaly detection)
2025-12-11 00:26:25 -08:00

249 lines
14 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Plus, MoveRight, Sprout, Leaf, Flower, Archive, Scale, ClipboardList, Bug, Search, RefreshCw } from 'lucide-react';
import { Batch, batchesApi } from '../lib/batchesApi';
import { useToast } from '../context/ToastContext';
import BatchTransitionModal from '../components/BatchTransitionModal';
import WeightLogModal from '../components/WeightLogModal';
import CreateTaskModal from '../components/tasks/CreateTaskModal';
import IPMScheduleModal from '../components/IPMScheduleModal';
import ScoutingModal from '../components/ipm/ScoutingModal';
import { SkeletonCard } from '../components/ui/Skeleton';
import { PullToRefresh } from '../components/ui/PullToRefresh';
import { QuickLogBar } from '../components/touchpoints/QuickLogButtons';
const STAGE_GROUPS = [
{ id: 'CLONE_IN', label: 'Clones', icon: Sprout, color: 'text-blue-500 bg-blue-50 border-blue-100' },
{ id: 'VEGETATIVE', label: 'Veg', icon: Leaf, color: 'text-emerald-500 bg-emerald-50 border-emerald-100' },
{ id: 'FLOWERING', label: 'Flower', icon: Flower, color: 'text-purple-500 bg-purple-50 border-purple-100' },
{ id: 'DRYING', label: 'Drying', icon: Archive, color: 'text-amber-500 bg-amber-50 border-amber-100' },
{ id: 'CURING', label: 'Curing', icon: Archive, color: 'text-orange-500 bg-orange-50 border-orange-100' },
{ id: 'FINISHED', label: 'Finished', icon: Archive, color: 'text-slate-500 bg-slate-50 border-slate-100' },
];
export default function BatchesPage() {
const { addToast } = useToast();
const [batches, setBatches] = useState<Batch[]>([]);
const [loading, setLoading] = useState(true);
const [selectedBatch, setSelectedBatch] = useState<Batch | null>(null);
const [weightLogBatch, setWeightLogBatch] = useState<Batch | null>(null);
const [createTaskBatch, setCreateTaskBatch] = useState<Batch | null>(null);
const [ipmBatch, setIpmBatch] = useState<Batch | null>(null);
const [scoutingBatch, setScoutingBatch] = useState<Batch | null>(null);
useEffect(() => {
fetchBatches();
}, []);
const fetchBatches = async (showToast = false) => {
setLoading(true);
try {
const data = await batchesApi.getAll();
setBatches(data);
if (showToast) addToast('Batches refreshed', 'info');
} catch (e) {
console.error(e);
addToast('Failed to load batches', 'error');
} finally {
setLoading(false);
}
};
// Group batches by stage
const groupedBatches = STAGE_GROUPS.map(group => ({
...group,
items: batches.filter(b => b.stage === group.id)
})).filter(group => group.items.length > 0);
const handlePullRefresh = async () => {
await fetchBatches(true);
};
return (
<PullToRefresh onRefresh={handlePullRefresh} disabled={loading}>
<div className="space-y-6 pb-20">
<header className="flex justify-between items-center sticky top-0 bg-slate-50 dark:bg-slate-900 z-10 py-2">
<div>
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Batches</h2>
<p className="text-sm text-slate-500">{loading ? 'Loading...' : `${batches.length} active batches`}</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors shadow-lg">
<Plus size={20} />
<span className="hidden md:inline">Start Batch</span>
</button>
</header>
<div className="space-y-8">
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => <SkeletonCard key={i} />)}
</div>
) : groupedBatches.length === 0 ? (
<div className="text-center py-20 bg-white dark:bg-slate-800 rounded-xl border border-dashed border-slate-300 dark:border-slate-700">
<div className="w-16 h-16 mx-auto bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mb-4">
<Sprout size={32} className="text-slate-400" />
</div>
<h3 className="text-lg font-medium text-slate-900 dark:text-white">No active batches</h3>
<p className="text-slate-500 dark:text-slate-400 mt-2">Get started by creating your first batch.</p>
</div>
) : (
groupedBatches.map(group => (
<div key={group.id} className="space-y-3">
<div className="flex items-center gap-2 px-1">
<div className={`p-2 rounded-lg ${group.color.split(' ')[1]}`}>
<group.icon size={18} className={group.color.split(' ')[0]} />
</div>
<h3 className="font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wide text-sm">
{group.label}
</h3>
<span className="text-xs font-medium px-2 py-0.5 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300">
{group.items.length}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{group.items.map(batch => (
<div
key={batch.id}
className="bg-white dark:bg-slate-800 p-4 rounded-xl border border-slate-100 dark:border-slate-700 shadow-sm hover:shadow-md transition-shadow relative group"
>
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-bold text-lg text-slate-900 dark:text-white leading-tight mb-1">
{batch.name}
</h4>
<div className="text-xs font-medium px-2 py-1 rounded bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 inline-block">
{batch.strain}
</div>
</div>
<div className="flex gap-1">
<button
onClick={() => setCreateTaskBatch(batch)}
className="p-2 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-lg transition-colors"
title="Add Task"
>
<ClipboardList size={20} />
</button>
<button
onClick={() => setIpmBatch(batch)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
title="IPM Schedule"
>
<Bug size={20} />
</button>
<button
onClick={() => setScoutingBatch(batch)}
className="p-2 text-slate-400 hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors"
title="Scout Batch"
>
<Search size={20} />
</button>
{/* Show Scale button for Harvest+ stages */}
{['HARVEST', 'DRYING', 'CURING', 'FINISHED'].includes(batch.stage) && (
<button
onClick={() => setWeightLogBatch(batch)}
className="p-2 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors"
title="Log Weight"
>
<Scale size={20} />
</button>
)}
<button
onClick={() => setSelectedBatch(batch)}
className="p-2 text-slate-400 hover:text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg transition-colors"
title="Transition Stage"
>
<MoveRight size={20} />
</button>
</div>
</div>
<div className="flex items-center justify-between text-sm text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-50 dark:border-slate-700">
<div className="flex flex-col">
<span className="text-xs uppercase tracking-wider text-slate-400">Room</span>
<span className="font-medium text-slate-700 dark:text-slate-300">
{batch.room?.name || 'Unassigned'}
</span>
</div>
<div className="flex flex-col items-end">
<span className="text-xs uppercase tracking-wider text-slate-400">Plants</span>
<span className="font-medium text-slate-700 dark:text-slate-300">
{batch.plantCount}
</span>
</div>
</div>
{/* Quick Log Bar */}
<div className="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700">
<QuickLogBar
batchId={batch.id}
batchName={batch.name}
onSuccess={() => fetchBatches()}
compact
/>
</div>
</div>
))}
</div>
</div>
))
)}
</div>
{selectedBatch && (
<BatchTransitionModal
batch={selectedBatch}
onClose={() => setSelectedBatch(null)}
onSuccess={() => {
fetchBatches();
setSelectedBatch(null);
}}
/>
)}
{weightLogBatch && (
<WeightLogModal
batch={weightLogBatch}
onClose={() => setWeightLogBatch(null)}
onSuccess={() => {
fetchBatches();
setWeightLogBatch(null);
}}
/>
)}
{createTaskBatch && (
<CreateTaskModal
isOpen={true}
initialBatchId={createTaskBatch.id}
initialRoomId={createTaskBatch.roomId || undefined}
onClose={() => setCreateTaskBatch(null)}
onSuccess={() => {
// Optional: show toast
setCreateTaskBatch(null);
}}
/>
)}
{ipmBatch && (
<IPMScheduleModal
batch={ipmBatch}
onClose={() => setIpmBatch(null)}
/>
)}
{scoutingBatch && (
<ScoutingModal
batch={scoutingBatch}
onClose={() => setScoutingBatch(null)}
onSuccess={() => {
// Optional: show toast
}}
/>
)}
</div>
</PullToRefresh>
);
}