vat_wms/resources/js/Pages/FloorPlan.tsx
2025-07-23 10:53:24 +02:00

894 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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() {
const [showEditSectionsModal, setShowEditSectionsModal] = useState(false);
const [selectedPositionForEditing, setSelectedPositionForEditing] = useState<StockPosition | null>(null);
// Multi-select & copy/paste
const [selectedShapes, setSelectedShapes] = useState<
{ id: string; type: 'rack' | 'line' }[]
>([]);
const [copyBuffer, setCopyBuffer] = useState<{
lines: LayoutLine[];
racks: LayoutItem[];
} | null>(null);
// Marqueeselection rectangle
const [marquee, setMarquee] = useState({
visible: false,
x: 0,
y: 0,
width: 0,
height: 0,
});
const stageRef = useRef<any>(null);
// record where the drag began
const dragStart = useRef<{ x: number; y: number } | null>(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<StockPosition>(
`/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<StockRoom[]>([]);
const [selectedRoom, setSelectedRoom] = useState<StockRoom | null>(null);
// Layout data
const [layoutItems, setLayoutItems] = useState<LayoutItem[]>([]);
const [layoutLines, setLayoutLines] = useState<LayoutLine[]>([]);
const [bgFile, setBgFile] = useState<string | null>(null);
// Track which existing DB items were deleted client-side
const [deletedLineDbIds, setDeletedLineDbIds] = useState<number[]>([]);
const [deletedRackDbIds, setDeletedRackDbIds] = useState<number[]>([]);
// New-layout modal state
const [showCreateModal, setShowCreateModal] = useState(false);
const [newBgFile, setNewBgFile] = useState<string | null>(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<any>(null);
const transformerRef = useRef<any>(null);
const [selectedId, setSelectedId] = useState<string | null>(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<StockRack>(`/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]);
useEffect(() => {
const tr = transformerRef.current;
if (!tr) return;
if (selectedShapes.length) {
const nodes = selectedShapes
.map(s => layerRef.current.findOne(`#${s.id}`))
.filter(n => !!n);
tr.nodes(nodes);
} else {
tr.nodes([]);
}
tr.getLayer().batchDraw();
}, [selectedShapes]);
// 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]);
// 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);
// In onTransformEnd, after node.scale reset:
const rot = node.rotation();
if (selectedType === 'line') {
setLayoutLines(ls => ls.map(l =>
l.id === id ? { ...l, x: node.x(), y: node.y(), width: w, height: h, rotation: rot } : l
));
} else {
setLayoutItems(is => is.map(i =>
i.id === id ? { ...i, x: node.x(), y: node.y(), width: w, height: h, rotation: rot } : i
));
}
};
// Helpers for new symbols/numbers
const nextLineLetter = () => {
const used = new Set<string>();
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',
rotation: 0
}]);
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,
rotation: 0
}]);
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 parentline 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<StockRack>(
`/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<StockRack>(
`/api/stock-racks/${rack.rack_id}`,
{ withCredentials: true }
);
setEditModal(m => ({ ...m, item_obj: freshRack }));
} catch (err) {
console.error(err);
alert('Could not delete shelf.');
}
};
// Shift-click or single-click to build multiselection
const onShapeClick = (e: any, id: string, type: 'rack' | 'line') => {
e.cancelBubble = true;
const isMulti = e.evt.shiftKey;
setSelectedShapes(current => {
if (!isMulti) return [{ id, type }];
const exists = current.find(s => s.id === id);
if (exists) return current.filter(s => s.id !== id);
return [...current, { id, type }];
});
};
const onStageClick = (e: any) => {
const stage = stageRef.current.getStage();
if (e.target === stage) setSelectedShapes([]);
};
const onStageMouseDown = (e: any) => {
if (e.target !== e.target.getStage()) return;
const stage = stageRef.current.getStage();
const pos = stage.getPointerPosition()!;
dragStart.current = pos;
// dont show marquee yet
setMarquee({ visible: false, x: pos.x, y: pos.y, width: 0, height: 0 });
setSelectedShapes([]);
};
const onStageMouseMove = (e: any) => {
if (!dragStart.current) return;
const stage = stageRef.current.getStage();
const pos = stage.getPointerPosition()!;
const dx = pos.x - dragStart.current.x;
const dy = pos.y - dragStart.current.y;
// only start showing after a small threshold
if (!marquee.visible) {
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
setMarquee({
visible: true,
x: dragStart.current.x,
y: dragStart.current.y,
width: dx,
height: dy,
});
} else {
setMarquee(m => ({ ...m, width: dx, height: dy }));
}
};
const onStageMouseUp = () => {
if (marquee.visible) {
const box = {
x: Math.min(marquee.x, marquee.x + marquee.width),
y: Math.min(marquee.y, marquee.y + marquee.height),
width: Math.abs(marquee.width),
height: Math.abs(marquee.height),
};
const rectIntersect = (r1: any, r2: any) =>
r1.x < r2.x + r2.width &&
r1.x + r1.width > r2.x &&
r1.y < r2.y + r2.height &&
r1.y + r1.height > r2.y;
// get *all* Rect nodes
const allRects = layerRef.current.find('Rect');
const hits = allRects
.filter((node: any) => {
const id = node.id();
// only pick actual racks/lines (they all start with "rack-" or "line-")
if (!id.startsWith('rack-') && !id.startsWith('line-')) return false;
const r = node.getClientRect();
return rectIntersect(r, box);
});
setSelectedShapes(
hits.map((n: any) => ({
id: n.id(),
type: n.id().startsWith('line-') ? 'line' : 'rack',
}))
);
}
dragStart.current = null;
setMarquee(m => ({ ...m, visible: false }));
};
const handleCopy = () => {
if (!selectedShapes.length) return;
setCopyBuffer({
lines: selectedShapes
.filter(s => s.type === 'line')
.map(s => ({ ...layoutLines.find(l => l.id === s.id)! })),
racks: selectedShapes
.filter(s => s.type === 'rack')
.map(s => ({ ...layoutItems.find(r => r.id === s.id)! })),
});
};
const handlePaste = () => {
if (!copyBuffer) return;
// nextletter for lines
const usedLetters = new Set(layoutLines.map(l => l.symbol));
const nextLetter = () => {
for (let c of ALPHABET) if (!usedLetters.has(c)) {
usedLetters.add(c);
return c;
}
return ALPHABET[0];
};
// map old line IDs → new IDs
const lineMap: Record<string,string> = {};
// create new lines
const newLines = copyBuffer.lines.map(orig => {
const sym = nextLetter();
const id = `line-${Date.now()}-${Math.random().toString(36).slice(2)}`;
lineMap[orig.id] = id;
return {
...orig,
rotation: orig.rotation ?? 0,
id,
dbId: null,
symbol: sym,
name: `Line ${sym}`,
x: orig.x + 10,
y: orig.y + 10,
};
});
setLayoutLines(ls => [...ls, ...newLines]);
// helper for next rack number per line
const usedNums: Record<string, Set<number>> = {};
layoutItems.forEach(r => {
const n = parseInt(r.symbol) || 0;
(usedNums[r.lineId] ||= new Set()).add(n);
});
const nextRackNum = (lineId: string) => {
const set = (usedNums[lineId] ||= new Set());
let n = 1;
while (set.has(n)) n++;
set.add(n);
return n;
};
// create new racks
const newRacks = copyBuffer.racks.map(orig => {
const target = lineMap[orig.lineId] || orig.lineId;
const num = nextRackNum(target);
const id = `rack-${Date.now()}-${Math.random().toString(36).slice(2)}`;
return {
...orig,
rotation: orig.rotation ?? 0,
id,
dbId: null,
lineId: target,
symbol: String(num),
name: `Rack ${num}`,
x: orig.x + 10,
y: orig.y + 10,
};
});
setLayoutItems(rs => [...rs, ...newRacks]);
// select the new shapes
setSelectedShapes([
...newLines.map(l => ({ id: l.id, type: 'line' as const })),
...newRacks.map(r => ({ id: r.id, type: 'rack' as const })),
]);
};
useEffect(() => {
const down = (e: KeyboardEvent) => {
const isMac = navigator.platform.startsWith('Mac');
const mod = isMac ? e.metaKey : e.ctrlKey;
if (mod && e.key === 'c') {
e.preventDefault();
handleCopy();
}
if (mod && e.key === 'v') {
e.preventDefault();
handlePaste();
}
};
window.addEventListener('keydown', down);
return () => window.removeEventListener('keydown', down);
}, [copyBuffer, selectedShapes, layoutLines, layoutItems]);
return (
<AppLayout title="Floor Plan">
<div className="p-6 flex flex-col items-center space-y-4">
{/* Toolbar */}
<div className="w-full flex justify-between items-center mb-4">
<div className="tabs">
{rooms.map(r => (
<button
key={r.room_id}
className={`tab ${selectedRoom?.room_id === r.room_id ? 'tab-active' : ''}`}
onClick={() => setSelectedRoom(r)}
>
{r.room_name}
</button>
))}
</div>
<div>
<button className="btn mr-2" onClick={addLine}>Add Line</button>
<button className="btn mr-2" onClick={addRack}>Add Rack</button>
<button className="btn mr-2" onClick={() => setShowCreateModal(true)}>New Layout</button>
<button className="btn" onClick={saveLayout}>Save Layout</button>
</div>
</div>
{/* Canvas */}
<Stage
width={1300}
height={900}
ref={stageRef}
onMouseDown={onStageMouseDown}
onMouseMove={onStageMouseMove}
onMouseUp={onStageMouseUp}
onClick={onStageClick}
>
<Layer ref={layerRef}>
<Rect x={0} y={0} width={1300} height={900} fill="white" listening={false}/>
{bgImage && (
<KonvaImage
image={bgImage}
x={0} y={0}
width={1300} height={900}
listening={false}
/>
)}
{/* Lines */}
{layoutLines.map(line => (
<React.Fragment key={line.id}>
<Rect
id={line.id}
x={line.x} y={line.y}
width={line.width} height={line.height}
fill={line.fill} stroke="black"
rotation={line.rotation}
draggable
// onClick={e => { onSelect(e, line.id, 'line'); openEditModal(line, 'line'); }}
onClick={e => {
onShapeClick(e, line.id, 'line');
openEditModal(line, 'line');
}}
onDragEnd={e => onDragEnd(e, line.id, 'line')}
onTransformEnd={e => onTransformEnd(e, line.id)}
/>
<Text text={line.name} x={line.x + 5} y={line.y + 2} fontSize={14} />
</React.Fragment>
))}
{/* Racks */}
{layoutItems.map(item => (
<React.Fragment key={item.id}>
<Rect
id={item.id}
x={item.x} y={item.y}
width={item.width} height={item.height}
fill={item.fill}
stroke={item.lineId ? 'black' : 'red'}
strokeWidth={item.lineId ? 1 : 2}
rotation={item.rotation}
draggable
// onClick={e => { onSelect(e, item.id, 'rack'); openEditModal(item, 'rack'); }}
onClick={e => {
onShapeClick(e, item.id, 'rack');
openEditModal(item, 'rack');
}}
onDragEnd={e => onDragEnd(e, item.id, 'rack')}
onTransformEnd={e => onTransformEnd(e, item.id)}
/>
<Text text={item.name} x={item.x + 5} y={item.y + 5} fontSize={14} />
</React.Fragment>
))}
{marquee.visible && (() => {
const x = marquee.width < 0 ? marquee.x + marquee.width : marquee.x;
const y = marquee.height < 0 ? marquee.y + marquee.height : marquee.y;
const w = Math.abs(marquee.width);
const h = Math.abs(marquee.height);
return (
<Rect
x={x}
y={y}
width={w}
height={h}
fill="rgba(0,0,255,0.1)"
stroke="blue"
dash={[4,4]}
listening={false}
/>
);
})()}
<Transformer ref={transformerRef} />
</Layer>
</Stage>
{/* Edit Modal */}
{editModal.visible && (
<div className="modal modal-open">
<div className="modal-box max-w-[50%]">
<h3 className="font-bold text-lg">Edit {editModal.type}</h3>
<input
type="text"
className="input w-full my-2"
value={editModal.name}
onChange={e => setEditModal(m => ({ ...m, name: e.target.value }))}
placeholder="Name"
/>
<input
type="text"
className="input w-full my-2"
value={editModal.symbol}
onChange={e => setEditModal(m => ({ ...m, symbol: e.target.value }))}
placeholder="Symbol"
/>
{editModal.type === 'rack' && (
<div className="mt-4">
<h4 className="font-bold mb-2">Shelves &amp; Sections</h4>
<RackModalDetails rack={editModal.item_obj} onPositionClick={handleOpenSections} onAddShelf={handleAddShelf} onDeleteShelf={handleDeleteShelf} />
</div>
)}
<div className="modal-action">
<button className="btn" onClick={saveEdit}>Save</button>
<button className="btn btn-error" onClick={deleteItem}>Delete</button>
<button className="btn btn-outline" onClick={() => setEditModal(m => ({ ...m, visible: false }))}>Cancel</button>
</div>
</div>
</div>
)}
{/* === Sections Editor Nested Modal === */}
{showEditSectionsModal && selectedPositionForEditing && (
<div className="modal modal-open">
<div className="modal-box max-w-[50%]">
<EditStockSections
selectedPosition={selectedPositionForEditing}
onClose={handleCloseSections}
/>
</div>
</div>
)}
{/* Create Layout Modal */}
{showCreateModal && (
<div className="modal modal-open">
<div className="modal-box">
<h3 className="font-bold text-lg">New Layout for {selectedRoom?.room_name}</h3>
<h3 className="text-secondary">Layout does not exist yet, upload floorplan and create</h3>
<input
type="file"
accept="image/*"
className="file-input w-full my-2"
onChange={e => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => setNewBgFile(reader.result as string);
reader.readAsDataURL(file);
}}
/>
{newBgFile && (
<img src={newBgFile} alt="Preview" className="w-full mb-2 rounded" />
)}
<div className="modal-action">
<button
className="btn btn-primary"
onClick={createLayout}
disabled={!newBgFile}
>
Create
</button>
<button
className="btn btn-outline"
onClick={() => {
setShowCreateModal(false);
setNewBgFile(null);
}}
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
</AppLayout>
);
}