Initial commit - restaurant dashboard
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Button,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Menu,
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import SaveIcon from "@mui/icons-material/Save";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import TuneIcon from "@mui/icons-material/Tune";
|
||||
import authService from "../../../services/authService";
|
||||
|
||||
const SimplePagination = ({ currentPage, pageCount, onChange }) => {
|
||||
const theme = useTheme();
|
||||
const handlePrev = () => currentPage > 1 && onChange(currentPage - 1);
|
||||
const handleNext = () => currentPage < pageCount && onChange(currentPage + 1);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage <= 1}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#FFECE0",
|
||||
// "&:hover": { backgroundColor: "#FFD6B5" },
|
||||
color: theme.palette.primary.main,
|
||||
// "&.Mui-disabled": { color: "#ccc", backgroundColor: "#FFF5E6" },
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosNewIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "8px",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{currentPage}
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage >= pageCount}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#FFECE0",
|
||||
// "&:hover": { backgroundColor: "#FFD6B5" },
|
||||
color: theme.palette.primary.main,
|
||||
// "&.Mui-disabled": { color: "#ccc", backgroundColor: "#FFF5E6" },
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const statusOptions = ["available", "unavailable"];
|
||||
|
||||
const TablesManager = ({ restaurantId }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const itemsPerPage = 4;
|
||||
|
||||
const [rows, setRows] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [addingRow, setAddingRow] = useState(false);
|
||||
const [newRow, setNewRow] = useState({
|
||||
restaurant_id: restaurantId,
|
||||
table_number: "",
|
||||
capacity: "",
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [rowToDelete, setRowToDelete] = useState(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// فلتر
|
||||
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
const fetchTables = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await authService.getTablesByRestaurant(restaurantId);
|
||||
if (result.success) setRows(result.data);
|
||||
} catch (err) {
|
||||
console.error("❌ Error fetching tables:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (restaurantId) fetchTables();
|
||||
}, [restaurantId]);
|
||||
|
||||
const handleSaveRow = async () => {
|
||||
if (!newRow.table_number || !newRow.capacity) return;
|
||||
try {
|
||||
const result = await authService.addTable(newRow);
|
||||
if (result.success) {
|
||||
setRows((prev) => [result.data, ...prev]);
|
||||
setAddingRow(false);
|
||||
setNewRow({
|
||||
restaurant_id: restaurantId,
|
||||
table_number: "",
|
||||
capacity: "",
|
||||
status: "available",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error adding table:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRow = (row) => {
|
||||
setRowToDelete(row);
|
||||
setConfirmDeleteOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!rowToDelete) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = await authService.deleteTable(rowToDelete.id);
|
||||
if (result.success) setRows((prev) => prev.filter((r) => r.id !== rowToDelete.id));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
setConfirmDeleteOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterClick = (event) => setFilterAnchorEl(event.currentTarget);
|
||||
const handleFilterClose = () => setFilterAnchorEl(null);
|
||||
const handleFilterSelect = (value) => {
|
||||
setStatusFilter(value);
|
||||
setFilterAnchorEl(null);
|
||||
};
|
||||
|
||||
const filteredRows =
|
||||
(statusFilter === "all" ? rows : rows.filter((r) => r.status === statusFilter))
|
||||
.sort((a, b) => parseInt(a.table_number) - parseInt(b.table_number));
|
||||
|
||||
const pageCount = Math.ceil(filteredRows.length / itemsPerPage);
|
||||
const paginatedRows = filteredRows.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
const getStatusChipProps = (status) => {
|
||||
switch (status?.toLowerCase()) {
|
||||
case "available":
|
||||
return {
|
||||
label: "Available",
|
||||
sx: {
|
||||
backgroundColor: "#E7F4EE",
|
||||
color: "#0D894F",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 90,
|
||||
},
|
||||
};
|
||||
case "unavailable":
|
||||
return {
|
||||
label: "Unavailable",
|
||||
sx: {
|
||||
backgroundColor: "#fde8e8ff",
|
||||
color: "#e41111ff",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 100,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: status,
|
||||
sx: {
|
||||
backgroundColor: "#FDF1E8",
|
||||
color: "#E46A11",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 80,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "95.5%" },
|
||||
pt: { xs: 2, sm: 2 },
|
||||
pl: { xs: 2, sm: 3 },
|
||||
pr: { xs: 2, sm: 3 },
|
||||
pb: { xs: 2, sm: 2 },
|
||||
// p: { xs: 1.5, sm: 2 },
|
||||
// mb: { xs: 2, sm: 2 },
|
||||
backgroundColor: "white",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
justifyContent: "space-between",
|
||||
alignItems: { xs: "stretch", sm: "center" },
|
||||
mb: 2,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: "18px", sm: "20px" },
|
||||
textAlign: { xs: "center", sm: "left" },
|
||||
}}
|
||||
>
|
||||
Tables
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{/* فلتر */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
color: "#667085",
|
||||
borderColor: "#e0e0e0",
|
||||
borderRadius: "8px",
|
||||
height: "40px",
|
||||
width: { xs: "120px", sm: "120px", md: "99px" },
|
||||
fontSize: { xs: "0", sm: "13px", md: "14px" },
|
||||
fontWeight: 600,
|
||||
minWidth: "unset",
|
||||
}}
|
||||
startIcon={<TuneIcon fontSize={isMobile ? "small" : "small"} />}
|
||||
onClick={handleFilterClick}
|
||||
>
|
||||
{isMobile ? "" : "Filters"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={addingRow ? "outlined" : "contained"}
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setAddingRow(!addingRow)}
|
||||
sx={{
|
||||
color: addingRow ? "primary.main" : "white",
|
||||
borderRadius: "8px",
|
||||
fontWeight: 600,
|
||||
fontSize: "14px",
|
||||
height: "40px",
|
||||
width: { xs: "100%", sm: "135px" },
|
||||
textTransform: "none",
|
||||
}}
|
||||
>
|
||||
{addingRow ? "Cancel" : "Add"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Menu الفلاتر */}
|
||||
<Menu anchorEl={filterAnchorEl} open={Boolean(filterAnchorEl)} onClose={handleFilterClose}>
|
||||
{["all", "available", "unavailable"].map((status) => (
|
||||
<MenuItem
|
||||
key={status}
|
||||
selected={statusFilter === status}
|
||||
onClick={() => handleFilterSelect(status)}
|
||||
sx={{
|
||||
color: statusFilter === status ? "#FF914D" : "#4F5867",
|
||||
fontWeight: statusFilter === status ? 700 : 500,
|
||||
}}
|
||||
>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
{/* جدول */}
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: 2,
|
||||
minWidth: 320,
|
||||
height: {xs:"310px",md:"349px"},
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
sx={{ minWidth: { sm: 550, md: 650 }, tableLayout: "auto" }}
|
||||
aria-label="tables"
|
||||
size={isMobile ? "small" : "medium"}
|
||||
>
|
||||
<TableHead sx={{ backgroundColor: "#f5f5f5" }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ pl: { md: 7 } }}>Table Number</TableCell>
|
||||
<TableCell>Capacity</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading
|
||||
? Array.from({ length: itemsPerPage }).map((_, idx) => (
|
||||
<TableRow key={`skeleton-${idx}`}>
|
||||
<TableCell ><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
<TableCell><Skeleton variant="text" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: (
|
||||
<>
|
||||
{addingRow && (
|
||||
<TableRow>
|
||||
<TableCell >
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Table Number"
|
||||
value={newRow.table_number}
|
||||
onChange={(e) => setNewRow({ ...newRow, table_number: e.target.value })}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
placeholder="Capacity"
|
||||
value={newRow.capacity}
|
||||
onChange={(e) => setNewRow({ ...newRow, capacity: e.target.value })}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label="Available"
|
||||
sx={{
|
||||
backgroundColor: "#E7F4EE",
|
||||
color: "#0D894F",
|
||||
fontWeight: 600,
|
||||
fontSize: "13px",
|
||||
minWidth: 90,
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton color="success" onClick={handleSaveRow}>
|
||||
<SaveIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
|
||||
{paginatedRows.map((row) => (
|
||||
<TableRow key={row.id} >
|
||||
<TableCell sx={{ pl: { md: 9 } ,color:'#689dffff'}}>{row.table_number}</TableCell>
|
||||
<TableCell>{row.capacity}</TableCell>
|
||||
<TableCell>
|
||||
<Chip {...getStatusChipProps(row.status)} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton color="error" onClick={() => handleDeleteRow(row)}>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Pagination Footer */}
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
pt={2}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Showing {(currentPage - 1) * itemsPerPage + 1} -{" "}
|
||||
{Math.min(currentPage * itemsPerPage, filteredRows.length)} of {filteredRows.length}
|
||||
</Typography>
|
||||
<SimplePagination currentPage={currentPage} pageCount={pageCount} onChange={setCurrentPage} />
|
||||
</Box>
|
||||
|
||||
{/* Dialog الحذف */}
|
||||
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete table "{rowToDelete?.table_number}"?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleConfirmDelete}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TablesManager;
|
||||
Reference in New Issue
Block a user