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
This commit is contained in:
fullsizemalt 2026-01-12 23:54:32 -08:00
parent d2abe033f2
commit eca58ecbc2
3 changed files with 226 additions and 2 deletions

View file

@ -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<any[]>([]);
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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-[var(--color-bg-elevated)] rounded-2xl shadow-xl w-full max-w-md mx-4 overflow-hidden border border-[var(--color-border-subtle)]">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[var(--color-border-subtle)]">
<h2 className="text-lg font-bold text-[var(--color-text-primary)]">Create New Batch</h2>
<button
onClick={onClose}
className="p-1 rounded-lg hover:bg-[var(--color-bg-tertiary)] transition-colors"
>
<X size={20} className="text-[var(--color-text-tertiary)]" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-4 space-y-4">
<div>
<label className="block text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-1.5">
Batch Name *
</label>
<input
type="text"
required
value={formData.name}
onChange={e => 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)]"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-1.5">
Strain *
</label>
<input
type="text"
required
value={formData.strain}
onChange={e => 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)]"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-1.5">
Plant Count *
</label>
<input
type="number"
required
min={1}
value={formData.plantCount}
onChange={e => 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)]"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-1.5">
Source *
</label>
<select
value={formData.source}
onChange={e => setFormData({ ...formData, source: e.target.value as 'CLONE' | 'SEED' })}
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)]"
>
<option value="CLONE">Clone</option>
<option value="SEED">Seed</option>
</select>
</div>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-1.5">
Starting Stage
</label>
<select
value={formData.stage}
onChange={e => setFormData({ ...formData, stage: e.target.value })}
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)]"
>
<option value="CLONE_IN">Clone In</option>
<option value="VEGETATIVE">Vegetative</option>
<option value="FLOWERING">Flowering</option>
</select>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-1.5">
Assign to Room
</label>
<select
value={formData.roomId}
onChange={e => setFormData({ ...formData, roomId: e.target.value })}
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)]"
>
<option value="">Unassigned</option>
{rooms.map(room => (
<option key={room.id} value={room.id}>{room.name}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-1.5">
METRC Tags (comma-separated)
</label>
<input
type="text"
value={formData.metrcTags}
onChange={e => 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)]"
/>
</div>
{/* Actions */}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2.5 rounded-xl border border-[var(--color-border-subtle)] text-[var(--color-text-secondary)] font-medium hover:bg-[var(--color-bg-tertiary)] transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2.5 rounded-xl bg-[var(--color-primary)] text-[var(--color-text-inverse)] font-bold hover:bg-[var(--color-primary-hover)] transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
{loading && <Loader2 size={16} className="animate-spin" />}
Create Batch
</button>
</div>
</form>
</div>
</div>
);
}

View file

@ -6,6 +6,7 @@ import { useToast } from '../context/ToastContext';
import BatchTransitionModal from '../components/BatchTransitionModal'; import BatchTransitionModal from '../components/BatchTransitionModal';
import WeightLogModal from '../components/WeightLogModal'; import WeightLogModal from '../components/WeightLogModal';
import CreateTaskModal from '../components/tasks/CreateTaskModal'; import CreateTaskModal from '../components/tasks/CreateTaskModal';
import CreateBatchModal from '../components/CreateBatchModal';
import IPMScheduleModal from '../components/IPMScheduleModal'; import IPMScheduleModal from '../components/IPMScheduleModal';
import ScoutingModal from '../components/ipm/ScoutingModal'; import ScoutingModal from '../components/ipm/ScoutingModal';
import { DataTable, Column } from '../components/ui/DataTable'; import { DataTable, Column } from '../components/ui/DataTable';
@ -63,6 +64,7 @@ export default function BatchesPage() {
const [createTaskBatch, setCreateTaskBatch] = useState<Batch | null>(null); const [createTaskBatch, setCreateTaskBatch] = useState<Batch | null>(null);
const [ipmBatch, setIpmBatch] = useState<Batch | null>(null); const [ipmBatch, setIpmBatch] = useState<Batch | null>(null);
const [scoutingBatch, setScoutingBatch] = useState<Batch | null>(null); const [scoutingBatch, setScoutingBatch] = useState<Batch | null>(null);
const [isCreateOpen, setIsCreateOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetchBatches(); fetchBatches();
@ -205,7 +207,10 @@ export default function BatchesPage() {
</div> </div>
</div> </div>
<button className="flex items-center gap-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-[var(--color-text-inverse)] px-4 py-2.5 rounded-xl font-bold text-sm shadow-lg transition-all"> <button
onClick={() => setIsCreateOpen(true)}
className="flex items-center gap-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-[var(--color-text-inverse)] px-4 py-2.5 rounded-xl font-bold text-sm shadow-lg transition-all"
>
<Plus size={18} /> <Plus size={18} />
New Batch New Batch
</button> </button>
@ -304,6 +309,14 @@ export default function BatchesPage() {
}} }}
/> />
)} )}
<CreateBatchModal
isOpen={isCreateOpen}
onClose={() => setIsCreateOpen(false)}
onSuccess={() => {
fetchBatches();
setIsCreateOpen(false);
}}
/>
</div> </div>
); );
} }

View file

@ -69,7 +69,10 @@ export default function RoomsPage() {
Generate Zone Generate Zone
</button> </button>
{isManager && ( {isManager && (
<button className="flex items-center gap-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-[var(--color-text-inverse)] px-4 py-2.5 rounded-xl font-bold text-sm shadow-lg transition-all"> <button
onClick={() => setIsWizardOpen(true)}
className="flex items-center gap-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-[var(--color-text-inverse)] px-4 py-2.5 rounded-xl font-bold text-sm shadow-lg transition-all"
>
<Plus size={18} /> <Plus size={18} />
Add Zone Add Zone
</button> </button>