import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; // Types export interface Position { x: number; y: number; } export interface Size { width: number; height: number; } export interface LayoutPosition { id: string; sectionId: string; row: number; column: number; slot: number; status: 'EMPTY' | 'PLANTED' | 'HARVESTED' | 'ISSUE' | 'RESERVED'; plantId?: string; plant?: { id: string; tagNumber: string; batchId?: string; address: string; status: string; batch?: { id: string; name: string; strain?: string; stage?: string; color?: string; }; healthStatus?: 'HEALTHY' | 'STRESSED' | 'SICK' | 'DEAD'; }; } export interface LayoutSection { id: string; roomId: string; name: string; code: string; type: 'TABLE' | 'RACK' | 'TRAY' | 'HANGER' | 'FLOOR'; position: Position; // Relative to room size: Size; rows: number; columns: number; tiers?: number; spacing: number; // inches between positions positions?: LayoutPosition[]; } export interface LayoutRoom { id: string; name: string; code: string; type: 'VEG' | 'FLOWER' | 'CLONE' | 'DRY' | 'CURE' | 'TRIM' | 'VAULT' | 'OTHER'; floorId: string; position: Position; size: Size; rotation: number; color?: string; // sections removed (normalized) } export interface LayoutFloor { id: string; buildingId?: string; name: string; number: number; width?: number; height?: number; size?: Size; rooms?: LayoutRoom[]; } export interface LayoutBuilding { id: string; propertyId?: string; name: string; code: string; floors: LayoutFloor[]; } export interface LayoutProperty { id: string; name: string; buildings: LayoutBuilding[]; } export interface LayoutSnapshot { rooms: LayoutRoom[]; sections: LayoutSection[]; } // Room type colors export const ROOM_COLORS: Record = { VEG: '#10b981', // Emerald FLOWER: '#a855f7', // Purple CLONE: '#3b82f6', // Blue DRY: '#f59e0b', // Amber CURE: '#f97316', // Orange TRIM: '#64748b', // Slate VAULT: '#71717a', // Zinc OTHER: '#6b7280', // Gray }; interface LayoutState { // Current context propertyId: string | null; buildingId: string | null; floorId: string | null; // Data property: (LayoutProperty & { address?: string }) | null; rooms: LayoutRoom[]; sections: LayoutSection[]; // Canvas state zoom: number; pan: Position; showGrid: boolean; snapToGrid: boolean; gridSize: number; // feet // Selection selectedIds: string[]; hoveredId: string | null; selectedPosition: (LayoutPosition & { sectionName?: string; roomName?: string }) | null; // Tool state activeTool: 'select' | 'pan' | 'room' | 'section'; pendingRoomType: LayoutRoom['type'] | null; // History past: LayoutSnapshot[]; future: LayoutSnapshot[]; // Actions setProperty: (property: LayoutProperty & { address?: string }) => void; setBuilding: (buildingId: string) => void; setFloor: (floorId: string) => void; loadRooms: (rooms: LayoutRoom[]) => void; loadSections: (sections: LayoutSection[]) => void; clearRooms: () => void; // Canvas actions setZoom: (zoom: number) => void; setPan: (pan: Position) => void; zoomIn: () => void; zoomOut: () => void; resetView: () => void; toggleGrid: () => void; toggleSnap: () => void; // Selection select: (ids: string[]) => void; addToSelection: (id: string) => void; clearSelection: () => void; setHovered: (id: string | null) => void; setSelectedPosition: (position: (LayoutPosition & { sectionName?: string; roomName?: string }) | null) => void; // Room CRUD addRoom: (room: Omit) => string; updateRoom: (id: string, updates: Partial) => void; deleteRoom: (id: string) => void; moveRoom: (id: string, position: Position) => void; resizeRoom: (id: string, size: Size) => void; // Section CRUD addSection: (section: Omit) => string; updateSection: (id: string, updates: Partial) => void; deleteSection: (id: string) => void; // Tool setActiveTool: (tool: LayoutState['activeTool']) => void; setPendingRoomType: (type: LayoutRoom['type'] | null) => void; // History saveSnapshot: () => void; undo: () => void; redo: () => void; canUndo: () => boolean; canRedo: () => boolean; } const generateId = () => crypto.randomUUID(); export const useLayoutStore = create()( immer((set, get) => ({ // Initial state propertyId: null, buildingId: null, floorId: null, property: null, rooms: [], sections: [], zoom: 1, pan: { x: 0, y: 0 }, showGrid: true, snapToGrid: true, gridSize: 1, // 1 foot selectedIds: [], hoveredId: null, selectedPosition: null, activeTool: 'select', pendingRoomType: null, past: [], future: [], // Property/Floor setProperty: (property) => set({ property, propertyId: property.id }), setBuilding: (buildingId) => set({ buildingId }), setFloor: (floorId) => set({ floorId }), loadRooms: (rooms) => set(state => { state.rooms = rooms.map(r => ({ ...r, sections: undefined })); // Ensure sections are removed from room object // Extract sections if they exist in the room (during initial full load) const sections: LayoutSection[] = []; rooms.forEach((r: any) => { if (r.sections) { sections.push(...r.sections); } }); // We replace sections on full room load? Or append? // Typically loadRooms implies "Load Floor", so we replace sections for this floor. // But extracting floorId from rooms is tricky if rooms is empty. // For simplicity, we just add them for now. // Actually loadRooms replaces state.rooms. // We should replace state.sections for these rooms. const roomIds = new Set(rooms.map(r => r.id)); state.sections = state.sections.filter(s => !roomIds.has(s.roomId)); state.sections.push(...sections); state.past = []; state.future = []; }), loadSections: (newSections) => set(state => { newSections.forEach(ns => { const index = state.sections.findIndex(s => s.id === ns.id); if (index !== -1) { state.sections[index] = ns; } else { state.sections.push(ns); } }); }), clearRooms: () => set({ rooms: [], sections: [], past: [], future: [] }), // Canvas setZoom: (zoom) => set({ zoom: Math.max(0.1, Math.min(5, zoom)) }), setPan: (pan) => set({ pan }), zoomIn: () => set((state) => ({ zoom: Math.min(5, state.zoom * 1.2) })), zoomOut: () => set((state) => ({ zoom: Math.max(0.1, state.zoom / 1.2) })), resetView: () => set({ zoom: 1, pan: { x: 0, y: 0 } }), toggleGrid: () => set((state) => ({ showGrid: !state.showGrid })), toggleSnap: () => set((state) => ({ snapToGrid: !state.snapToGrid })), // Selection select: (ids) => set({ selectedIds: ids }), addToSelection: (id) => set((state) => ({ selectedIds: [...state.selectedIds, id] })), clearSelection: () => set({ selectedIds: [], selectedPosition: null }), setHovered: (id) => set({ hoveredId: id }), setSelectedPosition: (position) => set({ selectedPosition: position }), // Room CRUD addRoom: (room) => { const id = generateId(); get().saveSnapshot(); set((state) => { state.rooms.push({ ...room, id }); }); return id; }, updateRoom: (id, updates) => { get().saveSnapshot(); set((state) => { const room = state.rooms.find(r => r.id === id); if (room) Object.assign(room, updates); }); }, deleteRoom: (id) => { get().saveSnapshot(); set((state) => { state.rooms = state.rooms.filter(r => r.id !== id); state.sections = state.sections.filter(s => s.roomId !== id); state.selectedIds = state.selectedIds.filter(sid => sid !== id); }); }, moveRoom: (id, position) => { set((state) => { const room = state.rooms.find(r => r.id === id); if (room) { if (state.snapToGrid) { const gridPx = state.gridSize * 20; // 20px per foot room.position = { x: Math.round(position.x / gridPx) * gridPx, y: Math.round(position.y / gridPx) * gridPx, }; } else { room.position = position; } } }); }, resizeRoom: (id, size) => { set((state) => { const room = state.rooms.find(r => r.id === id); if (room) { if (state.snapToGrid) { const gridPx = state.gridSize * 20; room.size = { width: Math.round(size.width / gridPx) * gridPx, height: Math.round(size.height / gridPx) * gridPx, }; } else { room.size = size; } } }); }, // Section CRUD addSection: (section) => { const id = generateId(); get().saveSnapshot(); set((state) => { state.sections.push({ ...section, id }); }); return id; }, updateSection: (id, updates) => { get().saveSnapshot(); set((state) => { const section = state.sections.find(s => s.id === id); if (section) Object.assign(section, updates); }); }, deleteSection: (id) => { get().saveSnapshot(); set((state) => { state.sections = state.sections.filter(s => s.id !== id); }); }, // Tool setActiveTool: (tool) => set({ activeTool: tool }), setPendingRoomType: (type) => set({ pendingRoomType: type }), // History saveSnapshot: () => { const { rooms, sections, past } = get(); set({ past: [...past.slice(-49), { rooms: JSON.parse(JSON.stringify(rooms)), sections: JSON.parse(JSON.stringify(sections)) }], future: [], }); }, undo: () => { const { past, rooms, sections } = get(); if (past.length === 0) return; const previous = past[past.length - 1]; set({ past: past.slice(0, -1), future: [{ rooms: JSON.parse(JSON.stringify(rooms)), sections: JSON.parse(JSON.stringify(sections)) }, ...get().future], rooms: previous.rooms, sections: previous.sections, }); }, redo: () => { const { future, rooms, sections } = get(); if (future.length === 0) return; const next = future[0]; set({ future: future.slice(1), past: [...get().past, { rooms: JSON.parse(JSON.stringify(rooms)), sections: JSON.parse(JSON.stringify(sections)) }], rooms: next.rooms, sections: next.sections, }); }, canUndo: () => get().past.length > 0, canRedo: () => get().future.length > 0, })) );