From eca58ecbc25951321c8a506a8aa2bf38ad6b8a5d Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:54:32 -0800 Subject: [PATCH] feat: Wire action buttons and add CreateBatchModal - Wire 'Add Zone' button on RoomsPage to open wizard - Create CreateBatchModal for batch creation form - Wire 'New Batch' button on BatchesPage to open modal - Modal includes strain, plant count, source, stage, room, METRC tags --- frontend/src/components/CreateBatchModal.tsx | 208 +++++++++++++++++++ frontend/src/pages/BatchesPage.tsx | 15 +- frontend/src/pages/RoomsPage.tsx | 5 +- 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/CreateBatchModal.tsx diff --git a/frontend/src/components/CreateBatchModal.tsx b/frontend/src/components/CreateBatchModal.tsx new file mode 100644 index 0000000..108d37a --- /dev/null +++ b/frontend/src/components/CreateBatchModal.tsx @@ -0,0 +1,208 @@ +import { useState, useEffect } from 'react'; +import { X, Loader2 } from 'lucide-react'; +import { batchesApi } from '../lib/batchesApi'; +import api from '../lib/api'; +import { useToast } from '../context/ToastContext'; + +interface CreateBatchModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +} + +export default function CreateBatchModal({ isOpen, onClose, onSuccess }: CreateBatchModalProps) { + const { addToast } = useToast(); + const [loading, setLoading] = useState(false); + const [rooms, setRooms] = useState([]); + + const [formData, setFormData] = useState({ + name: '', + strain: '', + plantCount: 1, + source: 'CLONE' as 'CLONE' | 'SEED', + stage: 'CLONE_IN' as string, + roomId: '', + metrcTags: '' + }); + + useEffect(() => { + if (isOpen) { + loadRooms(); + } + }, [isOpen]); + + const loadRooms = async () => { + try { + const { data } = await api.get('/rooms'); + setRooms(data); + } catch (err) { + console.error('Failed to load rooms:', err); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + const payload = { + ...formData, + plantCount: parseInt(String(formData.plantCount)), + roomId: formData.roomId || undefined, + metrcTags: formData.metrcTags ? formData.metrcTags.split(',').map(t => t.trim()) : undefined + }; + + await batchesApi.create(payload); + addToast('Batch created successfully', 'success'); + onSuccess(); + onClose(); + } catch (err: any) { + addToast(err.response?.data?.message || 'Failed to create batch', 'error'); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

Create New Batch

+ +
+ + {/* Form */} +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="e.g., B001-OG Kush" + className="w-full px-3 py-2 rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]" + /> +
+ +
+ + setFormData({ ...formData, strain: e.target.value })} + placeholder="e.g., OG Kush" + className="w-full px-3 py-2 rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]" + /> +
+ +
+
+ + setFormData({ ...formData, plantCount: parseInt(e.target.value) || 1 })} + className="w-full px-3 py-2 rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]" + /> +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + setFormData({ ...formData, metrcTags: e.target.value })} + placeholder="e.g., 1A400100001234, 1A400100001235" + className="w-full px-3 py-2 rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]" + /> +
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/BatchesPage.tsx b/frontend/src/pages/BatchesPage.tsx index 5e4e693..391d644 100644 --- a/frontend/src/pages/BatchesPage.tsx +++ b/frontend/src/pages/BatchesPage.tsx @@ -6,6 +6,7 @@ import { useToast } from '../context/ToastContext'; import BatchTransitionModal from '../components/BatchTransitionModal'; import WeightLogModal from '../components/WeightLogModal'; import CreateTaskModal from '../components/tasks/CreateTaskModal'; +import CreateBatchModal from '../components/CreateBatchModal'; import IPMScheduleModal from '../components/IPMScheduleModal'; import ScoutingModal from '../components/ipm/ScoutingModal'; import { DataTable, Column } from '../components/ui/DataTable'; @@ -63,6 +64,7 @@ export default function BatchesPage() { const [createTaskBatch, setCreateTaskBatch] = useState(null); const [ipmBatch, setIpmBatch] = useState(null); const [scoutingBatch, setScoutingBatch] = useState(null); + const [isCreateOpen, setIsCreateOpen] = useState(false); useEffect(() => { fetchBatches(); @@ -205,7 +207,10 @@ export default function BatchesPage() { - @@ -304,6 +309,14 @@ export default function BatchesPage() { }} /> )} + setIsCreateOpen(false)} + onSuccess={() => { + fetchBatches(); + setIsCreateOpen(false); + }} + /> ); } diff --git a/frontend/src/pages/RoomsPage.tsx b/frontend/src/pages/RoomsPage.tsx index 9647c08..fff88b7 100644 --- a/frontend/src/pages/RoomsPage.tsx +++ b/frontend/src/pages/RoomsPage.tsx @@ -69,7 +69,10 @@ export default function RoomsPage() { Generate Zone {isManager && ( -