Initial commit - restaurant dashboard

This commit is contained in:
RaghadMAlkous
2025-09-04 01:17:15 +03:00
parent 13891b47fd
commit 7b2f8840cb
136 changed files with 16637 additions and 6032 deletions
@@ -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;