import React, { useState, useEffect, useRef } from 'react'; import AppLayout from '@/Layouts/AppLayout'; import { Stage, Layer, Rect, Image as KonvaImage, Text, Transformer } from 'react-konva'; import useImage from 'use-image'; import axios from 'axios'; import {StockRoom, LayoutItem, LayoutLine, StockRack, StockLine, StockPosition} from '@/types'; import RackModalDetails from "@/Components/RackModalDetails"; import EditStockSections from '@/Components/modals/EditStockSections'; const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); export default function FloorPlan() { // at the top of FloorPlan() const [showEditSectionsModal, setShowEditSectionsModal] = useState(false); const [selectedPositionForEditing, setSelectedPositionForEditing] = useState(null); // fire this when RackModalDetails tells us “user clicked a position” const handleOpenSections = async (posId: number) => { try { console.log(posId); // 1️⃣ pull down the fresh position (with capacity & sections) const { data: freshPos } = await axios.get( `/api/stockPositions/${posId}`, { withCredentials: true } ); // 2️⃣ stash it & open the modal setSelectedPositionForEditing(freshPos); setShowEditSectionsModal(true); } catch (err) { console.error(err); alert('Could not load position details. Please try again.'); } }; // pass this into EditStockSections so that onClose we go back to RackModalDetails const handleCloseSections = () => { setShowEditSectionsModal(false); }; // Rooms & selected room const [rooms, setRooms] = useState([]); const [selectedRoom, setSelectedRoom] = useState(null); // Layout data const [layoutItems, setLayoutItems] = useState([]); const [layoutLines, setLayoutLines] = useState([]); const [bgFile, setBgFile] = useState(null); // Track which existing DB items were deleted client-side const [deletedLineDbIds, setDeletedLineDbIds] = useState([]); const [deletedRackDbIds, setDeletedRackDbIds] = useState([]); // New-layout modal state const [showCreateModal, setShowCreateModal] = useState(false); const [newBgFile, setNewBgFile] = useState(null); // Edit-item modal state const [editModal, setEditModal] = useState({ type: 'rack' as 'rack' | 'line' | null, item: null as LayoutItem | LayoutLine | null, item_obj: null as StockLine | StockRack | null, visible: false, name: '', symbol: '' }); // Konva refs & selection const layerRef = useRef(null); const transformerRef = useRef(null); const [selectedId, setSelectedId] = useState(null); const [selectedType, setSelectedType] = useState<'rack' | 'line' | null>(null); // Background image for canvas const [bgImage] = useImage(bgFile || ''); // Fetch room list on mount useEffect(() => { axios.get('/api/rooms') .then(({ data }) => { setRooms(data.rooms); if (data.rooms.length) setSelectedRoom(data.rooms[0]); }) .catch(console.error); }, []); useEffect(() => { if (!showEditSectionsModal && selectedRoom) { loadLayout(selectedRoom.room_id); } }, [showEditSectionsModal, selectedRoom]); useEffect(() => { if (!showEditSectionsModal && editModal.visible && editModal.type === 'rack' && editModal.item_obj) { // grab the just-saved rack by its ID: axios.get(`/api/stock-racks/${(editModal.item_obj as StockRack).rack_id}`, { withCredentials: true }) .then(({ data: freshRack }) => { setEditModal(m => ({ ...m, item_obj: freshRack })); }) .catch(err => { console.error('Could not refresh rack details:', err); }); } }, [showEditSectionsModal, editModal.visible]); // Load a room's layout (or show create modal) const loadLayout = async (roomId: number) => { try { const { data } = await axios.get(`/api/floor-layouts/${roomId}`); if (!data.layoutItems) { setShowCreateModal(true); } else { setLayoutItems(data.layoutItems.racks || []); setLayoutLines(data.layoutItems.lines || []); setBgFile(data.bgFile || null); setShowCreateModal(false); // reset deletion trackers setDeletedLineDbIds([]); setDeletedRackDbIds([]); } } catch { setShowCreateModal(true); } }; // Reload layout whenever selectedRoom changes useEffect(() => { if (selectedRoom) loadLayout(selectedRoom.room_id); }, [selectedRoom]); // Hook up the Konva Transformer to the selected shape useEffect(() => { const tr = transformerRef.current; if (!tr) return; if (selectedId) { const node = layerRef.current.findOne(`#${selectedId}`); node ? tr.nodes([node]) : tr.nodes([]); } else { tr.nodes([]); } tr.getLayer().batchDraw(); }, [selectedId]); // AABB intersection for rack-on-line test const intersects = (r: LayoutItem, l: LayoutLine) => r.x + r.width >= l.x && r.x <= l.x + l.width && r.y + r.height >= l.y && r.y <= l.y + l.height; // Selection handler const onSelect = (e: any, id: string, type: 'rack' | 'line') => { e.cancelBubble = true; setSelectedId(id); setSelectedType(type); }; // Drag end handler const onDragEnd = (e: any, id: string, type: 'rack' | 'line') => { const { x, y } = e.target.position(); if (type === 'line') { setLayoutLines(ls => ls.map(l => l.id === id ? { ...l, x, y } : l)); } else { setLayoutItems(is => is.map(i => { if (i.id === id) { const upd = { ...i, x, y }; const hit = layoutLines.find(l => intersects(upd, l)); upd.lineId = hit?.id ?? null; return upd; } return i; })); } }; // Transform end handler const onTransformEnd = (e: any, id: string) => { const node = e.target; const scaleX = node.scaleX(), scaleY = node.scaleY(); const w = Math.max(5, node.width() * scaleX); const h = Math.max(5, node.height() * scaleY); node.scaleX(1); node.scaleY(1); if (selectedType === 'line') { setLayoutLines(ls => ls.map(l => l.id === id ? { ...l, x: node.x(), y: node.y(), width: w, height: h } : l)); } else { setLayoutItems(is => is.map(i => i.id === id ? { ...i, x: node.x(), y: node.y(), width: w, height: h } : i)); } }; // Helpers for new symbols/numbers const nextLineLetter = () => { const used = new Set(); selectedRoom?.lines.forEach(l => used.add(l.line_symbol)); layoutLines.forEach(l => used.add(l.symbol)); for (let c of ALPHABET) if (!used.has(c)) return c; return ALPHABET[0]; }; const nextRackNumber = (lineId: string) => { const nums = layoutItems .filter(i => i.lineId === lineId) .map(i => parseInt(i.symbol) || 0); return (nums.length ? Math.max(...nums) : 0) + 1; }; // Add new line const addLine = () => { const letter = nextLineLetter(); const id = `line-${Date.now()}`; setLayoutLines(ls => [...ls, { id, dbId: null, name: `Line ${letter}`, symbol: letter, x: 100, y: 100, width: 200, height: 20, fill: 'lightblue' }]); setSelectedId(id); setSelectedType('line'); }; // Add new rack const addRack = () => { if (!selectedId || selectedType !== 'line') { return alert('Select a line first'); } const line = layoutLines.find(l => l.id === selectedId)!; const num = nextRackNumber(line.id); const id = `rack-${Date.now()}`; setLayoutItems(is => [...is, { id, dbId: null, name: `Rack ${num}`, symbol: String(num), x: line.x + 10, y: line.y + line.height + 10, width: 60, height: 60, fill: 'yellow', lineId: line.id }]); setSelectedId(id); setSelectedType('rack'); }; // Open edit modal const openEditModal = (item: any, type: 'rack' | 'line') => { const item_obj = type === 'line' ? selectedRoom?.lines?.find(l => l.line_id === item.dbId) ?? null : selectedRoom ?.lines ?.flatMap(l => l.racks || []) .find(r => r.rack_id === item.dbId) ?? null; setEditModal({ type, item, item_obj, visible: true, name: item.name, symbol: item.symbol }); }; // Save name/symbol edits const saveEdit = async () => { if (!editModal.item) return; const { type, item, name, symbol } = editModal; try { if (type === 'line') { let res; if (item.dbId) { res = await axios.put(`/api/stock-lines/${item.dbId}`, { line_name: name, line_symbol: symbol }); } else { res = await axios.post('/api/stock-lines', { room_id: selectedRoom?.room_id, line_name: name, line_symbol: symbol }); } const dbId = res.data.line_id; setLayoutLines(ls => ls.map(l => l.id === item.id ? { ...l, name, symbol, dbId } : l )); } else { await axios.put(`/api/stock-racks/${item.dbId}`, { rack_name: name, rack_symbol: symbol }); setLayoutItems(is => is.map(i => i.id === item.id ? { ...i, name, symbol } : i )); } setEditModal(m => ({ ...m, visible: false })); } catch (e) { console.error(e); alert('Error saving'); } }; // Delete rack or line (mark for later DB delete + remove visually) const deleteItem = () => { if (!editModal.item) return; const { type, item } = editModal; if (type === 'line') { // if it existed in DB, queue it if (item.dbId) { setDeletedLineDbIds(ids => [...ids, item.dbId!]); // also queue any racks under this line layoutItems .filter(r => r.lineId === item.id && r.dbId) .forEach(r => setDeletedRackDbIds(ids => [...ids, r.dbId!])); } // remove the line and its racks visually setLayoutLines(ls => ls.filter(l => l.id !== item.id)); setLayoutItems(is => is.filter(i => i.lineId !== item.id)); } else { if (item.dbId) { setDeletedRackDbIds(ids => [...ids, item.dbId!]); } setLayoutItems(is => is.filter(i => i.id !== item.id)); } setEditModal(m => ({ ...m, visible: false })); }; // Create new layout const createLayout = async () => { if (!selectedRoom) return; try { const defaultName = `${selectedRoom.room_name} layout`; const form = new FormData(); form.append('room_id', String(selectedRoom.room_id)); form.append('name', defaultName); if (newBgFile) form.append('bgFile', newBgFile); await axios.post('/api/floor-layouts/create', form, { headers: { 'Content-Type': 'multipart/form-data' } }); setShowCreateModal(false); setNewBgFile(null); await loadLayout(selectedRoom.room_id); } catch (e) { console.error(e); alert('Error creating layout'); } }; // --- ENHANCED saveLayout: handle creates, deletes, then update layout record --- const saveLayout = async () => { if (layoutItems.some(r => !r.lineId)) { return alert('Place all racks on lines before saving.'); } try { // 1️⃣ Create any new lines await Promise.all(layoutLines.map(async line => { if (!line.dbId) { const { data } = await axios.post('/api/stock-lines', { room_id: selectedRoom!.room_id, line_symbol: line.symbol, line_name: line.name }); line.dbId = data.line_id; } })); // 2️⃣ Create any new racks await Promise.all(layoutItems.map(async rack => { if (!rack.dbId) { const parentLine = layoutLines.find(l => l.id === rack.lineId)!; const { data } = await axios.post('/api/stock-racks', { line_id: parentLine.dbId, rack_symbol: rack.symbol, rack_name: rack.name }); rack.dbId = data.rack_id; } })); // 3️⃣ Update any existing rack whose parent‐line changed await Promise.all( layoutItems .filter(r => r.dbId !== null) .map(async rack => { const parent = layoutLines.find(l => l.id === rack.lineId)!; // send only if rack.line_id in DB !== parent.dbId // (you could cache original in state, but simplest to just PUT all) await axios.put(`/api/stock-racks/${rack.dbId}`, { line_id: parent.dbId, // you can also include name/symbol if you like: rack_name: rack.name, rack_symbol: rack.symbol, }); }) ); // 3️⃣ Delete any queued lines/racks await Promise.all([ ...deletedLineDbIds.map(id => axios.delete(`/api/stock-lines/${id}`)), ...deletedRackDbIds.map(id => axios.delete(`/api/stock-racks/${id}`)) ]); // reset the deletion queues setDeletedLineDbIds([]); setDeletedRackDbIds([]); // 4️⃣ Persist the full geometry const { data } = await axios.post('/api/floor-layouts/update', { room_id: selectedRoom!.room_id, contents: { lines: layoutLines, racks: layoutItems } }); alert(`Layout saved at ${new Date(data.updated_at).toLocaleTimeString()}`); } catch (e) { console.error(e); alert('Error saving layout'); } }; // ➊ add a new shelf const handleAddShelf = async (rackId: number) => { // compute next symbol & name from current editModal.item_obj const rack = editModal.item_obj as StockRack; const next = rack.shelves ? rack.shelves.length + 1 : 1; const symbol = String(next); const name = `Shelf ${symbol}`; try { await axios.post( '/api/stock-shelves', { rack_id: rackId, shelf_symbol: symbol, shelf_name: name }, { withCredentials: true } ); // refresh just that rack const { data: freshRack } = await axios.get( `/api/stock-racks/${rackId}`, { withCredentials: true } ); setEditModal(m => ({ ...m, item_obj: freshRack })); } catch (err) { console.error(err); alert('Could not add shelf.'); } }; // ➋ delete a shelf const handleDeleteShelf = async (shelfId: number) => { try { await axios.delete(`/api/stock-shelves/${shelfId}`, { withCredentials: true }); // after delete, re-fetch rack const rack = editModal.item_obj as StockRack; const { data: freshRack } = await axios.get( `/api/stock-racks/${rack.rack_id}`, { withCredentials: true } ); setEditModal(m => ({ ...m, item_obj: freshRack })); } catch (err) { console.error(err); alert('Could not delete shelf.'); } }; return (
{/* Toolbar */}
{rooms.map(r => ( ))}
{/* Canvas */} setSelectedId(null)}> {bgImage && ( )} {/* Lines */} {layoutLines.map(line => ( { onSelect(e, line.id, 'line'); openEditModal(line, 'line'); }} onDragEnd={e => onDragEnd(e, line.id, 'line')} onTransformEnd={e => onTransformEnd(e, line.id)} /> ))} {/* Racks */} {layoutItems.map(item => ( { onSelect(e, item.id, 'rack'); openEditModal(item, 'rack'); }} onDragEnd={e => onDragEnd(e, item.id, 'rack')} onTransformEnd={e => onTransformEnd(e, item.id)} /> ))} {/* Edit Modal */} {editModal.visible && (

Edit {editModal.type}

setEditModal(m => ({ ...m, name: e.target.value }))} placeholder="Name" /> setEditModal(m => ({ ...m, symbol: e.target.value }))} placeholder="Symbol" /> {editModal.type === 'rack' && (

Shelves & Sections

)}
)} {/* === Sections Editor Nested Modal === */} {showEditSectionsModal && selectedPositionForEditing && (
)} {/* Create Layout Modal */} {showCreateModal && (

New Layout for {selectedRoom?.room_name}

Layout does not exist yet, upload floorplan and create

{ const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => setNewBgFile(reader.result as string); reader.readAsDataURL(file); }} /> {newBgFile && ( Preview )}
)}
); }