'use client';
import { useState, useEffect, useCallback, useMemo, memo, useReducer, Suspense } from 'react';
import { createClient } from '@/utils/supabase/client';
import { FaPlus, FaEdit, FaTrash, FaChair } from 'react-icons/fa';
import { Database } from '@/types_db';
import { debounce } from 'lodash';
// Types
type TableRow = Omit<Database['public']['Tables']['tables']['Row'], 'width' | 'height'> & {
width?: number;
height?: number;
};
type FormDataType = Omit<TableRow, 'id' | 'created_at' | 'updated_at'>;
// Action types for reducer
type TableAction =
| { type: 'SET_TABLES'; payload: TableRow[] }
| { type: 'ADD_TABLE'; payload: TableRow }
| { type: 'UPDATE_TABLE'; payload: { id: number; table: Partial<TableRow> } }
| { type: 'DELETE_TABLE'; payload: number }
| { type: 'TEMP_UPDATE_POSITION'; payload: { id: number; x: number; y: number } }
| { type: 'TEMP_UPDATE_SIZE'; payload: { id: number; width: number; height: number; capacity: number } };
// Reducer for table state management
function tableReducer(state: TableRow[], action: TableAction): TableRow[] {
switch (action.type) {
case 'SET_TABLES':
return action.payload;
case 'ADD_TABLE':
return [...state, action.payload];
case 'UPDATE_TABLE':
return state.map(table =>
table.id === action.payload.id
? { ...table, ...action.payload.table }
: table
);
case 'DELETE_TABLE':
return state.filter(table => table.id !== action.payload);
case 'TEMP_UPDATE_POSITION':
return state.map(table =>
table.id === action.payload.id
? { ...table, position_x: action.payload.x, position_y: action.payload.y }
: table
);
case 'TEMP_UPDATE_SIZE':
return state.map(table =>
table.id === action.payload.id
? {
...table,
width: action.payload.width,
height: action.payload.height,
capacity: action.payload.capacity
}
: table
);
default:
return state;
}
}
// Custom hook for table data management
function useTableData() {
const supabase = createClient();
const [tables, dispatch] = useReducer(tableReducer, []);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Fetch tables
const fetchTables = useCallback(async () => {
setLoading(true);
try {
const { data, error } = await supabase
.from('tables')
.select('*')
.order('id');
if (error) throw error;
// Transform the data to match the TableRow type
const transformedData: TableRow[] = (data || []).map(table => ({
...table,
width: table.width || undefined,
height: table.height || undefined
}));
dispatch({ type: 'SET_TABLES', payload: transformedData });
} catch (err: any) {
setError(err.message);
console.error('Error fetching tables:', err);
} finally {
setLoading(false);
}
}, [supabase]);
// Create table
const createTable = useCallback(async (tableData: FormDataType) => {
try {
const { data, error } = await supabase
.from('tables')
.insert(tableData)
.select()
.single();
if (error) throw error;
// Transform the data to match the TableRow type
const transformedData: TableRow = {
...data,
width: data.width || undefined,
height: data.height || undefined
};
dispatch({ type: 'ADD_TABLE', payload: transformedData });
return { success: true };
} catch (err: any) {
setError(err.message);
console.error('Error creating table:', err);
return { success: false, error: err.message };
}
}, [supabase]);
// Update table
const updateTable = useCallback(async (id: number, tableData: Partial<Omit<TableRow, 'id'>>) => {
try {
const { error } = await supabase
.from('tables')
.update({
...tableData,
updated_at: new Date().toISOString()
})
.eq('id', id);
if (error) throw error;
dispatch({
type: 'UPDATE_TABLE',
payload: { id, table: { ...tableData, updated_at: new Date().toISOString() }}
});
return { success: true };
} catch (err: any) {
setError(err.message);
console.error('Error updating table:', err);
return { success: false, error: err.message };
}
}, [supabase]);
// Delete table
const deleteTable = useCallback(async (id: number) => {
try {
const { error } = await supabase
.from('tables')
.delete()
.eq('id', id);
if (error) throw error;
dispatch({ type: 'DELETE_TABLE', payload: id });
return { success: true };
} catch (err: any) {
setError(err.message);
console.error('Error deleting table:', err);
return { success: false, error: err.message };
}
}, [supabase]);
// Update table position temporarily (for dragging)
const updateTablePosition = useCallback((id: number, x: number, y: number) => {
dispatch({ type: 'TEMP_UPDATE_POSITION', payload: { id, x, y } });
}, []);
// Update table size temporarily (for resizing)
const updateTableSize = useCallback((id: number, width: number, height: number, capacity: number) => {
dispatch({ type: 'TEMP_UPDATE_SIZE', payload: { id, width, height, capacity } });
}, []);
return {
tables,
loading,
error,
fetchTables,
createTable,
updateTable,
deleteTable,
updateTablePosition,
updateTableSize,
setError
};
}
// Memoized Grid component
const Grid = memo(({ width, height }: { width: number; height: number }) => {
// Calculate grid cells only when dimensions change
const gridCells = useMemo(() => {
const rows = Math.floor(height / 20);
const cols = Math.floor(width / 20);
return Array.from({ length: rows }).map((_, rowIndex) => (
Array.from({ length: cols }).map((_, colIndex) => (
<div
key={`grid-${rowIndex}-${colIndex}`}
className="border border-gray-100 select-none"
style={{
width: 20,
height: 20,
gridRow: rowIndex + 1,
gridColumn: colIndex + 1
}}
/>
))
));
}, [width, height]);
return (
<div className="absolute inset-0 grid grid-cols-[repeat(auto-fill,_20px)] grid-rows-[repeat(auto-fill,_20px)] select-none">
{gridCells}
</div>
);
});
Grid.displayName = 'Grid';
// Table component
const Table = memo(({
table,
isSelected,
isDragging,
isResizing,
onSelect,
onDragStart,
onDragEnd,
onResizeStart
}: {
table: TableRow;
isSelected: boolean;
isDragging: boolean;
isResizing: boolean;
onSelect: (table: TableRow) => void;
onDragStart: (id: number) => void;
onDragEnd: (e: React.MouseEvent, id: number) => void;
onResizeStart: (e: React.MouseEvent, id: number, direction: string) => void;
}) => {
// Track mouse position to distinguish between clicks and drags
const [mouseDownPos, setMouseDownPos] = useState<{x: number, y: number} | null>(null);
return (
<div
className={`absolute flex flex-col items-center justify-center border-2 rounded shadow-md cursor-move select-none ${
isSelected
? 'border-blue-500 bg-blue-100'
: 'border-amber-500 bg-amber-50'
}`}
style={{
left: table.position_x,
top: table.position_y,
width: table.width || (table.capacity >= 6 ? 80 : 60),
height: table.height || (table.capacity >= 6 ? 80 : 60),
zIndex: isDragging || isResizing ? 10 : 1,
userSelect: 'none' // Prevent text selection
}}
onClick={(e) => {
if (!isResizing) {
e.stopPropagation();
// Only handle as a click if we haven't moved much
if (mouseDownPos) {
const dx = Math.abs(e.clientX - mouseDownPos.x);
const dy = Math.abs(e.clientY - mouseDownPos.y);
if (dx < 5 && dy < 5) {
onSelect(table);
}
}
}
}}
onMouseDown={(e) => {
e.stopPropagation();
// Save the position where mouse down happened
setMouseDownPos({x: e.clientX, y: e.clientY});
// We'll initiate drag only if mouse actually moves
// This prevents drag start on simple click
}}
onMouseMove={(e) => {
if (mouseDownPos) {
const dx = Math.abs(e.clientX - mouseDownPos.x);
const dy = Math.abs(e.clientY - mouseDownPos.y);
// Start drag only if mouse has moved a bit
if (dx > 5 || dy > 5) {
onDragStart(table.id);
setMouseDownPos(null); // Reset the position
}
}
}}
onMouseUp={(e) => {
e.stopPropagation();
setMouseDownPos(null); // Reset the position
if (isDragging) {
onDragEnd(e, table.id);
}
}}
onMouseLeave={() => {
// Reset on mouse leave to avoid state issues
setMouseDownPos(null);
}}
>
<FaChair className="text-amber-600 text-xl mb-1 pointer-events-none" />
<span className="text-sm font-medium text-amber-800 pointer-events-none">{table.label}</span>
<span className="text-xs text-amber-600 pointer-events-none">{table.capacity} места</span>
{/* Resize handle */}
<div
className="absolute bottom-0 right-0 w-4 h-4 bg-amber-500 rounded-bl cursor-se-resize"
style={{
transform: 'translate(50%, 50%)',
zIndex: 20
}}
onMouseDown={(e) => {
e.stopPropagation();
onResizeStart(e, table.id, 'bottomRight');
}}
/>
</div>
);
});
Table.displayName = 'Table';
// TableForm component
const TableForm = memo(({
formData,
isEditing,
selectedTable,
onInputChange,
onSubmit,
onCancel
}: {
formData: FormDataType;
isEditing: boolean;
selectedTable: TableRow | null;
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: () => void;
onCancel: () => void;
}) => {
return (
<div className="border rounded p-4 bg-white shadow-sm">
<h2 className="text-lg font-semibold mb-4">
{isEditing ? `Редактиране на маса ${selectedTable?.label}` : 'Добавяне на нова маса'}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Име на масата</label>
<input
type="text"
name="label"
value={formData.label}
onChange={onInputChange}
className="w-full border rounded py-2 px-3 bg-white"
placeholder="напр. Т1, Маса 1"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Позиция X</label>
<input
type="number"
name="position_x"
value={formData.position_x}
onChange={onInputChange}
className="w-full border rounded py-2 px-3 bg-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Позиция Y</label>
<input
type="number"
name="position_y"
value={formData.position_y}
onChange={onInputChange}
className="w-full border rounded py-2 px-3 bg-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Брой места</label>
<input
type="number"
name="capacity"
min="1"
value={formData.capacity}
onChange={onInputChange}
className="w-full border rounded py-2 px-3 bg-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Ширина (px)</label>
<input
type="number"
name="width"
min="40"
value={formData.width}
onChange={onInputChange}
className="w-full border rounded py-2 px-3 bg-white"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Височина (px)</label>
<input
type="number"
name="height"
min="40"
value={formData.height}
onChange={onInputChange}
className="w-full border rounded py-2 px-3 bg-white"
/>
</div>
<div className="flex gap-2">
<button
onClick={onSubmit}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded flex-1"
>
{isEditing ? 'Запази промените' : 'Създай маса'}
</button>
<button
onClick={onCancel}
className="bg-gray-300 hover:bg-gray-400 px-4 py-2 rounded"
>
Отказ
</button>
</div>
</div>
</div>
);
});
TableForm.displayName = 'TableForm';
// TableInfo component
const TableInfo = memo(({
table,
onEdit,
onDelete,
onBack
}: {
table: TableRow;
onEdit?: (table: TableRow) => void;
onDelete?: (id: number) => void;
onBack: () => void;
}) => {
// Get the base URL for the current environment
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
const orderUrl = `${baseUrl}/menu?table=${table.id}`;
// State for copy notification
const [showCopyNotification, setShowCopyNotification] = useState(false);
const handleCopyUrl = () => {
navigator.clipboard.writeText(orderUrl);
setShowCopyNotification(true);
setTimeout(() => setShowCopyNotification(false), 2000);
};
return (
<div>
<div className="flex items-center mb-4">
<button
onClick={onBack}
className="text-gray-600 hover:text-gray-800 mr-2"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L7.414 9H15a1 1 0 110 2H7.414l2.293 2.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
</button>
<h2 className="text-lg font-semibold">Информация за маса</h2>
</div>
<div className="space-y-2">
<p><span className="font-medium">Име:</span> {table.label}</p>
<p><span className="font-medium">Брой места:</span> {table.capacity} места</p>
<div className="mt-4 p-3 bg-amber-50 rounded border border-amber-200 relative">
<p className="text-sm font-medium text-amber-800 mb-2">Клиентски URL за поръчка:</p>
<div className="flex items-center gap-2">
<input
type="text"
readOnly
value={orderUrl}
className="flex-1 text-sm p-2 border rounded bg-white font-mono"
/>
<button
onClick={handleCopyUrl}
className="text-amber-600 hover:text-amber-800 p-2 hover:bg-amber-100 rounded"
title="Copy URL"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
</svg>
</button>
</div>
{/* Copy notification toast */}
{showCopyNotification && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-medium shadow-sm">
URL копиран!
</div>
)}
</div>
<div className="pt-4 flex gap-2">
{onEdit && (
<button
onClick={() => onEdit(table)}
className="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
>
<FaEdit /> Редактирай
</button>
)}
{onDelete && (
<button
onClick={() => onDelete(table.id)}
className="flex items-center gap-1 bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded"
>
<FaTrash /> Изтрий
</button>
)}
</div>
</div>
</div>
);
});
TableInfo.displayName = 'TableInfo';
// Fix the TablesList component type
type TableListItem = Pick<TableRow, 'id' | 'label' | 'capacity'>;
// TablesList component
const TablesList = memo(({
tables,
onEdit,
onDelete,
onSelect
}: {
tables: TableListItem[];
onEdit?: (table: TableListItem) => void;
onDelete?: (id: number) => void;
onSelect: (table: TableListItem) => void;
}) => {
return (
<div>
<h2 className="text-lg font-semibold mb-4">Списък с маси</h2>
<div className="space-y-2 max-h-[500px] overflow-auto">
{tables.map(table => (
<div
key={table.id}
className="p-2 border rounded hover:bg-amber-50 cursor-pointer flex justify-between items-center"
onClick={() => onSelect(table)}
>
<div>
<span className="font-medium">{table.label}</span>
<span className="text-sm text-gray-500 ml-2">({table.capacity} места)</span>
</div>
<div className="flex gap-2">
{onEdit && (
<button
onClick={(e) => {
e.stopPropagation();
onEdit(table);
}}
className="text-gray-600 hover:text-gray-800"
>
<FaEdit />
</button>
)}
{onDelete && (
<button
onClick={(e) => {
e.stopPropagation();
onDelete(table.id);
}}
className="text-gray-600 hover:text-gray-800"
>
<FaTrash />
</button>
)}
</div>
</div>
))}
</div>
</div>
);
});
TablesList.displayName = 'TablesList';
// Main component
function TableLayoutContent() {
// State from custom hook
const {
tables,
loading,
error,
fetchTables,
createTable,
updateTable,
deleteTable,
updateTablePosition,
updateTableSize,
setError
} = useTableData();
// UI state
const [selectedTable, setSelectedTable] = useState<TableRow | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [draggingTable, setDraggingTable] = useState<number | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [zoom, setZoom] = useState(1);
const [isResizing, setIsResizing] = useState(false);
const [resizingTable, setResizingTable] = useState<number | null>(null);
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'diagram' | 'list'>('diagram'); // View mode toggle
const [isEditMode, setIsEditMode] = useState(false); // New state for edit mode toggle
// Form state with default values
const [formData, setFormData] = useState<FormDataType>({
label: '',
position_x: 0,
position_y: 0,
capacity: 1,
width: 60,
height: 60
});
// Calculate canvas dimensions (memoized)
const { width, height } = useMemo(() => {
if (tables.length === 0) return { width: 800, height: 600 };
const maxX = Math.max(...tables.map(t => t.position_x)) + 100;
const maxY = Math.max(...tables.map(t => t.position_y)) + 100;
return {
width: Math.max(800, maxX),
height: Math.max(600, maxY)
};
}, [tables]);
// Load tables on component mount
useEffect(() => {
fetchTables();
}, [fetchTables]);
// Debounced position update to reduce API calls during drag
const debouncedUpdatePosition = useCallback(
debounce(async (id: number, x: number, y: number) => {
await updateTable(id, { position_x: x, position_y: y });
}, 500),
[updateTable]
);
// Debounced size update to reduce API calls during resize
const debouncedUpdateSize = useCallback(
debounce(async (id: number, width: number, height: number, capacity: number) => {
await updateTable(id, { width, height, capacity });
}, 500),
[updateTable]
);
// Handle form input changes
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: name === 'capacity' || name === 'position_x' || name === 'position_y' || name === 'width' || name === 'height'
? parseInt(value) || 0
: value
}));
}, []);
// Reset form to default values
const resetForm = useCallback(() => {
setFormData({
label: '',
position_x: 0,
position_y: 0,
capacity: 1,
width: 60,
height: 60
});
}, []);
// Submit form handler (create or update)
const handleSubmitForm = useCallback(async () => {
if (isEditing && selectedTable) {
const result = await updateTable(selectedTable.id, formData);
if (result.success) {
setIsEditing(false);
setSelectedTable(null);
resetForm();
}
} else if (isCreating) {
const result = await createTable(formData);
if (result.success) {
setIsCreating(false);
resetForm();
}
}
}, [isEditing, isCreating, selectedTable, formData, updateTable, createTable, resetForm]);
// Cancel form handler
const handleCancelForm = useCallback(() => {
setIsEditing(false);
setIsCreating(false);
setSelectedTable(null);
resetForm();
}, [resetForm]);
// Handle delete table with confirmation
const handleDeleteTable = useCallback((id: number) => {
if (confirm('Сигурен ли сте, че искате да изтриете тази маса?')) {
deleteTable(id);
if (selectedTable?.id === id) {
setSelectedTable(null);
setIsEditing(false);
}
}
}, [deleteTable, selectedTable]);
// Modified to split table selection from table editing
const handleSelectTable = useCallback((table: TableRow) => {
setSelectedTable(table);
setFormData({
label: table.label,
position_x: table.position_x,
position_y: table.position_y,
capacity: table.capacity,
width: table.width || 60,
height: table.height || 60
});
setIsEditing(false);
setIsCreating(false);
}, []);
// Start editing a table
const handleEditTable = useCallback((table: TableRow) => {
handleSelectTable(table);
setIsEditing(true);
}, [handleSelectTable]);
// Handle drag start - only if in edit mode
const handleDragStart = useCallback((e: React.MouseEvent | number, id?: number) => {
// Don't allow dragging if not in edit mode
if (!isEditMode) return;
// Accept either (event, id) or just (id)
const tableId = typeof e === 'number' ? e : id;
if (typeof tableId !== 'number') return;
setDraggingTable(tableId);
setIsDragging(true);
}, [isEditMode]);
// Handle drag end - save position to database
const handleDragEnd = useCallback((e: React.MouseEvent, id: number) => {
if (!isDragging) return;
setIsDragging(false);
setDraggingTable(null);
const canvas = document.getElementById('layout-canvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const newX = Math.round((e.clientX - rect.left) / zoom);
const newY = Math.round((e.clientY - rect.top) / zoom);
// Update in Supabase (debounced)
debouncedUpdatePosition(id, newX, newY);
}, [isDragging, zoom, debouncedUpdatePosition]);
// Handle canvas click to create a new table
const handleCanvasClick = useCallback((e: React.MouseEvent) => {
if (isDragging || isResizing || isEditing || isCreating) return;
const canvas = document.getElementById('layout-canvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const posX = Math.round((e.clientX - rect.left) / zoom);
const posY = Math.round((e.clientY - rect.top) / zoom);
setFormData(prev => ({
...prev,
position_x: posX,
position_y: posY
}));
setIsCreating(true);
setSelectedTable(null);
}, [isDragging, isResizing, isEditing, isCreating, zoom]);
// Handle resize start - only if in edit mode
const handleResizeStart = useCallback((e: React.MouseEvent, id: number, direction: string) => {
// Don't allow resizing if not in edit mode
if (!isEditMode) return;
e.stopPropagation();
setResizingTable(id);
setIsResizing(true);
setResizeDirection(direction);
}, [isEditMode]);
// Handle mouse move for dragging
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (isDragging && draggingTable !== null) {
const canvas = document.getElementById('layout-canvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const newX = Math.round((e.clientX - rect.left) / zoom);
const newY = Math.round((e.clientY - rect.top) / zoom);
updateTablePosition(draggingTable, newX, newY);
}
}, [isDragging, draggingTable, zoom, updateTablePosition]);
// Add document event listeners for resize
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing || resizingTable === null) return;
const canvas = document.getElementById('layout-canvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const table = tables.find(t => t.id === resizingTable);
if (!table) return;
const mouseX = (e.clientX - rect.left) / zoom;
const mouseY = (e.clientY - rect.top) / zoom;
if (resizeDirection === 'bottomRight') {
const width = Math.max(40, Math.round(mouseX - table.position_x));
const height = Math.max(40, Math.round(mouseY - table.position_y));
// Calculate capacity based on area
const area = width * height;
const newCapacity = Math.max(1, Math.round(area / 900));
updateTableSize(resizingTable, width, height, newCapacity);
}
};
const handleMouseUp = async () => {
if (!isResizing || resizingTable === null) return;
const table = tables.find(t => t.id === resizingTable);
if (table) {
debouncedUpdateSize(
resizingTable,
table.width || 60,
table.height || 60,
table.capacity
);
}
setIsResizing(false);
setResizingTable(null);
setResizeDirection(null);
};
if (isResizing) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isResizing, resizingTable, resizeDirection, tables, zoom, updateTableSize, debouncedUpdateSize]);
// Update the tablesList useMemo to use the new type
const tablesList = useMemo<TableListItem[]>(() => {
return tables.map(({ id, label, capacity }) => ({ id, label, capacity }));
}, [tables]);
// Add this function that was missing
const handleCreateTable = () => {
setIsCreating(true);
setIsEditing(false);
setSelectedTable(null);
// Reset form data
setFormData({
label: '',
position_x: 100,
position_y: 100,
capacity: 1,
width: 60,
height: 60
});
};
// Toggle edit mode
const toggleEditMode = useCallback(() => {
setIsEditMode(prev => !prev);
// If turning off edit mode, cancel any ongoing edits
if (isEditMode) {
setIsEditing(false);
setIsCreating(false);
setSelectedTable(null);
setIsDragging(false);
setDraggingTable(null);
setIsResizing(false);
setResizingTable(null);
}
}, [isEditMode]);
return (
<div className="container mx-auto px-0 py-0">
{error && (
<div className="bg-red-50 text-red-700 p-4 rounded-lg mb-6">
{error}
</div>
)}
{/* View mode and edit mode toggles */}
<div className="mb-6 p-3 shadow bg-white rounded-lg">
<div className="flex justify-between items-center">
<div className="flex gap-3 items-center">
{/* Edit mode toggle - changed to ghost button */}
<button
onClick={toggleEditMode}
className={`flex items-center gap-1 px-3 py-1 rounded-md text-sm border ${
isEditMode
? 'border-gray-300 text-gray-700'
: 'border-gray-200 text-gray-600'
} hover:bg-gray-50`}
title={isEditMode ? "Режим на редактиране" : "Режим на преглед"}
>
{isEditMode ? (
<>
<FaEdit className="h-4 w-4" />
<span className="hidden sm:inline">Режим на редактиране</span>
</>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
</svg>
<span className="hidden sm:inline">Режим на преглед</span>
</>
)}
</button>
{/* View mode toggle - only shown on mobile */}
<button
onClick={() => setViewMode(viewMode === 'diagram' ? 'list' : 'diagram')}
className="md:hidden flex items-center gap-1 px-3 py-1 rounded-md text-sm border border-gray-300 text-gray-700 hover:bg-gray-50"
title={viewMode === 'diagram' ? "Превключи към списък" : "Превключи към диаграма"}
>
{viewMode === 'diagram' ? (
<>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
</svg>
<span className="hidden sm:inline md:hidden">Превключи към списък</span>
</>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5zm2-2h10v10H5V3z" clipRule="evenodd" />
</svg>
<span className="hidden sm:inline md:hidden">Превключи към диаграма</span>
</>
)}
</button>
{/* Zoom controls - only visible when in diagram view (hidden on mobile when in list view) */}
<div className={`flex items-center border border-gray-300 rounded-lg overflow-hidden ${viewMode === 'list' ? 'hidden md:flex' : ''}`}>
<button
onClick={() => setZoom(prev => Math.max(0.5, prev - 0.1))}
className="px-2 py-1 bg-gray-50 hover:bg-gray-100 text-gray-700 text-sm"
>
-
</button>
<span className="px-2 py-1 bg-white text-xs sm:text-sm">
{Math.round(zoom * 100)}%
</span>
<button
onClick={() => setZoom(prev => Math.min(2, prev + 0.1))}
className="px-2 py-1 bg-gray-50 hover:bg-gray-100 text-gray-700 text-sm"
>
+
</button>
</div>
</div>
</div>
</div>
<div className="flex flex-col md:flex-row gap-6">
{/* Canvas for layout - hidden when in list view */}
<div className={`w-full md:w-[70%] bg-white rounded-lg shadow-md overflow-hidden ${viewMode === 'list' ? 'hidden md:block' : ''}`}>
{loading ? (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-orange-500"></div>
<span className="ml-3 text-gray-600">Зареждане...</span>
</div>
) : (
<div
className="relative overflow-auto border border-gray-200 bg-gray-50"
style={{ height: '650px' }}
onMouseMove={handleMouseMove}
onMouseUp={() => {
setIsDragging(false);
setDraggingTable(null);
}}
onClick={isEditMode ? handleCanvasClick : undefined}
>
<div
id="layout-canvas"
className="relative"
style={{
width: `${width * zoom}px`,
height: `${height * zoom}px`,
transformOrigin: 'top left',
transform: `scale(${zoom})`,
background: 'white'
}}
>
{tables.map(table => (
<div
key={table.id}
className={`absolute border ${
selectedTable?.id === table.id
? 'border-orange-500 bg-orange-50'
: 'border-gray-400 bg-gray-100'
} rounded-md text-center flex flex-col justify-center items-center ${isEditMode ? 'cursor-pointer' : ''} transition-colors hover:bg-gray-200`}
style={{
left: `${table.position_x}px`,
top: `${table.position_y}px`,
width: `${table.width || 60}px`,
height: `${table.height || 60}px`
}}
onClick={e => {
e.stopPropagation();
// Allow selecting tables even in view mode, but not for editing
handleSelectTable(table);
}}
onMouseDown={e => {
if (isEditMode && !isResizing) handleDragStart(e, table.id);
}}
>
<span className="font-medium text-sm truncate max-w-full px-2">
{table.label}
</span>
<span className="text-xs text-gray-600">
{table.capacity} {table.capacity === 1 ? 'място' : 'места'}
</span>
{/* Resize handle - only visible in edit mode */}
{isEditMode && (
<div
className="absolute bottom-0 right-0 w-4 h-4 bg-gray-300 cursor-se-resize rounded-bl-md"
onMouseDown={e => {
e.stopPropagation();
handleResizeStart(e, table.id, 'bottomRight');
}}
/>
)}
</div>
))}
</div>
</div>
)}
</div>
{/* Sidebar - shown differently on mobile based on viewMode */}
<div className={`w-full md:w-[30%] bg-white p-4 rounded-lg shadow-md ${viewMode === 'diagram' ? 'hidden md:block' : ''}`}>
{/* Controls - only show Add Table button in edit mode */}
{!isEditing && !isCreating && isEditMode && (
<div className="mb-4">
<button
onClick={handleCreateTable}
className="flex items-center justify-center gap-2 px-4 py-2.5 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors w-full"
>
<FaPlus className="text-sm" />
<span>Добави Маса</span>
</button>
</div>
)}
{/* Only show form when in edit mode */}
{isEditMode && (isEditing || isCreating) && (
<TableForm
formData={formData}
isEditing={isEditing}
selectedTable={selectedTable}
onInputChange={handleInputChange}
onSubmit={handleSubmitForm}
onCancel={handleCancelForm}
/>
)}
{selectedTable && !isEditing && (
<TableInfo
table={selectedTable}
onEdit={isEditMode ? handleEditTable : undefined}
onDelete={isEditMode ? handleDeleteTable : undefined}
onBack={() => setSelectedTable(null)}
/>
)}
{!selectedTable && !isEditing && !isCreating && tables.length > 0 && (
<TablesList
tables={tablesList}
onEdit={isEditMode ? (tableInfo) => {
const fullTable = tables.find(t => t.id === tableInfo.id);
if (fullTable) handleEditTable(fullTable);
} : undefined}
onDelete={isEditMode ? handleDeleteTable : undefined}
onSelect={(tableInfo) => {
const fullTable = tables.find(t => t.id === tableInfo.id);
if (fullTable) handleSelectTable(fullTable);
}}
/>
)}
</div>
</div>
</div>
);
}
export default function TableLayoutPage() {
return (
<Suspense fallback={<></>}>
<TableLayoutContent />
</Suspense>
);
}