نسخ من RaghadAlkhous/RestaurantDash
520 أسطر
20 KiB
JavaScript
520 أسطر
20 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
useMediaQuery,
|
|
Box,
|
|
Typography,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Paper,
|
|
Button,
|
|
Chip,
|
|
Menu,
|
|
MenuItem,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
} from '@mui/material';
|
|
import TuneIcon from '@mui/icons-material/Tune';
|
|
import AddEmployModal from './AddEmployModal';
|
|
import authService from '../../../../services/authService';
|
|
import IOSSwitch from './IOSSwitch';
|
|
import { useTheme } from '@mui/material/styles';
|
|
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
|
import EmployeeDetailsModal from './EmployeeDetailsModal';
|
|
|
|
const Waiter = ({ restaurantId }) => {
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [showAll, setShowAll] = useState(false);
|
|
const [switchStates, setSwitchStates] = useState({});
|
|
const [shiftFilter, setShiftFilter] = useState('all');
|
|
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
|
|
const [waiters, setWaiters] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
|
const [waiterDetails, setWaiterDetails] = useState(null);
|
|
|
|
|
|
// حالات الحذف
|
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
const [selectedWaiter, setSelectedWaiter] = useState(null);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
// استدعاء API
|
|
useEffect(() => {
|
|
const fetchWaiters = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await authService.getAllWaiters(restaurantId);
|
|
if (response.waiters) {
|
|
setWaiters(response.waiters);
|
|
|
|
// تهيئة switchStates حسب قيمة active
|
|
const initialStates = {};
|
|
response.waiters.forEach(waiter => {
|
|
initialStates[waiter.id] = waiter.active === 1; // أو Boolean(waiter.active)
|
|
});
|
|
setSwitchStates(initialStates);
|
|
} else {
|
|
setWaiters([]);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch waiters:", error);
|
|
setWaiters([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchWaiters();
|
|
}, [restaurantId]);
|
|
|
|
|
|
const handleSwitchChange = async (id, email) => {
|
|
try {
|
|
const response = await authService.toggleAccountStatus(email, "Waiter");
|
|
|
|
if (response.success) {
|
|
setSwitchStates((prev) => ({
|
|
...prev,
|
|
[id]: response.data,
|
|
}));
|
|
// alert(response.message);
|
|
} else {
|
|
alert("Failed: " + response.message);
|
|
}
|
|
} catch (error) {
|
|
alert("Error: " + error.message);
|
|
}
|
|
};
|
|
|
|
|
|
const handleOpenModal = () => setModalOpen(true);
|
|
const handleCloseModal = () => setModalOpen(false);
|
|
|
|
const handleConfirm = async (formData) => {
|
|
try {
|
|
const waiterData = {
|
|
...formData,
|
|
restaurant_id: restaurantId,
|
|
};
|
|
|
|
const response = await authService.registerWaiter(waiterData);
|
|
|
|
if (response.success) {
|
|
alert('Waiter registered successfully!');
|
|
setModalOpen(false);
|
|
// إعادة التحميل بعد الإضافة
|
|
const updated = await authService.getAllWaiters(restaurantId);
|
|
if (updated.waiters) setWaiters(updated.waiters);
|
|
} else {
|
|
if (response.errors) {
|
|
const errorsList = Object.entries(response.errors)
|
|
.map(([field, msgs]) => `${field}: ${msgs.join(', ')}`)
|
|
.join('\n');
|
|
alert(`Failed to register waiter due to validation errors:\n${errorsList}`);
|
|
} else {
|
|
alert('Failed to register waiter: ' + response.message);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
alert('An error occurred: ' + error.message);
|
|
}
|
|
};
|
|
|
|
const getShiftChipProps = (shiftType) => {
|
|
switch (shiftType?.toLowerCase()) {
|
|
case 'morning':
|
|
return {
|
|
label: 'Morning',
|
|
sx: {
|
|
backgroundColor: '#E7F4EE',
|
|
color: '#0D894F',
|
|
fontWeight: 600,
|
|
fontSize: '14px',
|
|
minWidth: 80,
|
|
textTransform: 'capitalize',
|
|
},
|
|
};
|
|
case 'evening':
|
|
return {
|
|
label: 'Evening',
|
|
sx: {
|
|
backgroundColor: '#FDF1E8',
|
|
color: '#E46A11',
|
|
fontWeight: 600,
|
|
fontSize: '14px',
|
|
minWidth: 80,
|
|
textTransform: 'capitalize',
|
|
},
|
|
};
|
|
default:
|
|
return {
|
|
label: shiftType,
|
|
sx: {
|
|
backgroundColor: '#E0E0E0',
|
|
color: '#424242',
|
|
fontWeight: 600,
|
|
fontSize: '14px',
|
|
minWidth: 80,
|
|
textTransform: 'capitalize',
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
const filteredWaiters =
|
|
shiftFilter === 'all'
|
|
? waiters
|
|
: waiters.filter((w) => w.shift_type.toLowerCase() === shiftFilter);
|
|
|
|
const displayedWaiters = showAll ? filteredWaiters : filteredWaiters.slice(0, 3);
|
|
|
|
const handleFilterClick = (event) => {
|
|
setFilterAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleFilterClose = () => {
|
|
setFilterAnchorEl(null);
|
|
};
|
|
|
|
const handleFilterSelect = (value) => {
|
|
setShiftFilter(value);
|
|
setFilterAnchorEl(null);
|
|
};
|
|
|
|
// فتح مودال الحذف
|
|
const handleDeleteClick = (waiter) => {
|
|
setSelectedWaiter(waiter);
|
|
setConfirmDeleteOpen(true);
|
|
};
|
|
|
|
// تأكيد الحذف
|
|
const handleDeleteConfirm = async () => {
|
|
if (!selectedWaiter) return;
|
|
setIsDeleting(true);
|
|
try {
|
|
const response = await authService.deleteWaiter(selectedWaiter.id);
|
|
|
|
// هنا تحقق من وجود message بدل success
|
|
if (response.message && response.message.toLowerCase().includes("successfully")) {
|
|
// alert(response.message);
|
|
setWaiters((prev) => prev.filter((w) => w.id !== selectedWaiter.id));
|
|
} else {
|
|
alert("Failed to delete waiter: " + (response.message || "Unknown error"));
|
|
}
|
|
} catch (error) {
|
|
alert("An error occurred: " + error.message);
|
|
} finally {
|
|
setIsDeleting(false);
|
|
setConfirmDeleteOpen(false);
|
|
setSelectedWaiter(null);
|
|
}
|
|
};
|
|
|
|
const handleOpenDetails = async (id) => {
|
|
try {
|
|
const response = await authService.getWaiterById(id);
|
|
if (response.waiter) {
|
|
setWaiterDetails(response.waiter);
|
|
setDetailsModalOpen(true);
|
|
} else {
|
|
alert("Failed to fetch waiter details");
|
|
}
|
|
} catch (error) {
|
|
alert("Error: " + error.message);
|
|
}
|
|
};
|
|
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
width: { xs: '100%', sm: '93.5%' },
|
|
p: { xs: 2, sm: 3 },
|
|
backgroundColor: 'white',
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
<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' },
|
|
mb: { xs: 1, sm: 0 },
|
|
textAlign: { xs: 'center', sm: 'left' },
|
|
}}
|
|
>
|
|
Waiter
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: 1,
|
|
justifyContent: { xs: 'center', sm: 'flex-start' },
|
|
width: { xs: '100%', sm: 'auto' },
|
|
}}
|
|
>
|
|
{waiters.length > 3 && (
|
|
<Button
|
|
onClick={() => setShowAll(!showAll)}
|
|
sx={{
|
|
borderRadius: '8px',
|
|
fontWeight: 600,
|
|
fontSize: '14px',
|
|
height: '40px',
|
|
textTransform: 'none',
|
|
minWidth: 90,
|
|
}}
|
|
variant="outlined"
|
|
size="small"
|
|
>
|
|
{showAll ? 'See Less' : 'See All'}
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
variant="outlined"
|
|
sx={{
|
|
textTransform: 'none',
|
|
color: '#667085',
|
|
borderColor: '#e0e0e0',
|
|
backgroundColor: '#fff',
|
|
borderRadius: '8px',
|
|
height: '40px',
|
|
width: { xs: '120px', sm: '120px', md: '99px' },
|
|
fontSize: { xs: '0', sm: '13px', md: '14px' },
|
|
fontWeight: 600,
|
|
p: 0,
|
|
m: 0,
|
|
whiteSpace: 'nowrap',
|
|
minWidth: 'unset',
|
|
}}
|
|
startIcon={<TuneIcon fontSize={isMobile ? 'small' : 'medium'} />}
|
|
onClick={handleFilterClick}
|
|
>
|
|
{isMobile ? '' : 'Filters'}
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={handleOpenModal}
|
|
sx={{
|
|
color: 'white',
|
|
borderRadius: '8px',
|
|
fontWeight: 600,
|
|
fontSize: '14px',
|
|
height: '40px',
|
|
width: { xs: '100%', sm: '135px' },
|
|
textTransform: 'none',
|
|
minWidth: { xs: 'unset', sm: '135px' },
|
|
}}
|
|
variant="contained"
|
|
size="small"
|
|
>
|
|
Add Waiter
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
|
|
<TableContainer
|
|
component={Paper}
|
|
sx={{
|
|
boxShadow: 'none',
|
|
border: '1px solid #e0e0e0',
|
|
borderRadius: 2,
|
|
minWidth: 320,
|
|
}}
|
|
>
|
|
<Table
|
|
sx={{
|
|
minWidth: { sm: 550, md: 650 },
|
|
tableLayout: 'auto',
|
|
}}
|
|
aria-label="recent activity table"
|
|
size={isMobile ? 'small' : 'medium'}
|
|
>
|
|
<TableHead sx={{ backgroundColor: '#f5f5f5', color: '#61677F' }}>
|
|
<TableRow sx={{ '& th': { borderBottom: 'none' } }}>
|
|
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
|
Name
|
|
</TableCell>
|
|
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
|
Email
|
|
</TableCell>
|
|
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 10 } }}>
|
|
Shift Type
|
|
</TableCell>
|
|
<TableCell sx={{ fontWeight: 500, fontSize: '16px', color: '#61677F', width: '25%', pl: { xs: 1, sm: 8 } }}>
|
|
Actions
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
|
Loading...
|
|
</TableCell>
|
|
</TableRow>
|
|
) : displayedWaiters.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={4} align="center" sx={{ color: '#999', fontStyle: 'italic' }}>
|
|
No activities found.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
displayedWaiters.map((waiter) => (
|
|
<TableRow key={waiter.id}>
|
|
<TableCell>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
color: "#296adaff",
|
|
fontSize: "14px",
|
|
fontWeight: 500,
|
|
pl: { xs: 1, sm: 6 },
|
|
wordBreak: "break-word",
|
|
cursor: "pointer",
|
|
"&:hover": { color: "#71a5ffff" },
|
|
}}
|
|
onClick={() => handleOpenDetails(waiter.id)}
|
|
>
|
|
{waiter.name}
|
|
</Box>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
color: '#4F5867',
|
|
fontSize: '14px',
|
|
fontWeight: 500,
|
|
pl: { xs: 1, sm: 3 },
|
|
wordBreak: 'break-word',
|
|
}}
|
|
>
|
|
{waiter.email}
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box sx={{ pl: { xs: 1, sm: 8 } }}>
|
|
<Chip {...getShiftChipProps(waiter.shift_type)} />
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell sx={{ width: '28%', pl: { xs: 1, sm: 8 } }}>
|
|
<Box sx={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
|
<IOSSwitch
|
|
checked={!!switchStates[waiter.id]}
|
|
onChange={() => handleSwitchChange(waiter.id, waiter.email)}
|
|
inputProps={{ 'aria-label': 'accountant switch' }}
|
|
/>
|
|
|
|
<DeleteForeverIcon
|
|
sx={{ cursor: 'pointer', color: '#e53935' }}
|
|
onClick={() => handleDeleteClick(waiter)}
|
|
/>
|
|
</Box>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
|
|
<AddEmployModal
|
|
open={modalOpen}
|
|
onClose={handleCloseModal}
|
|
onConfirm={handleConfirm}
|
|
title="Add Waiter"
|
|
buttonText="Add Waiter"
|
|
/>
|
|
|
|
<Menu
|
|
anchorEl={filterAnchorEl}
|
|
open={Boolean(filterAnchorEl)}
|
|
onClose={handleFilterClose}
|
|
PaperProps={{
|
|
sx: {
|
|
backgroundColor: 'white',
|
|
px: 1,
|
|
position: 'relative',
|
|
width: '120px',
|
|
},
|
|
}}
|
|
>
|
|
{['all', 'morning', 'evening'].map((shift) => (
|
|
<MenuItem
|
|
key={shift}
|
|
selected={shiftFilter === shift}
|
|
onClick={() => handleFilterSelect(shift)}
|
|
sx={{
|
|
color: shiftFilter === shift ? '#FF914D' : '#4F5867',
|
|
backgroundColor: shiftFilter === shift ? '#fffcf9d5' : 'transparent',
|
|
'&:hover': {
|
|
backgroundColor: '#f0f0f0ff',
|
|
transition: 'background-color 150ms ease-in-out',
|
|
},
|
|
}}
|
|
>
|
|
{shift.charAt(0).toUpperCase() + shift.slice(1)}
|
|
</MenuItem>
|
|
))}
|
|
</Menu>
|
|
|
|
{/* مودال تأكيد الحذف */}
|
|
<Dialog open={confirmDeleteOpen} onClose={() => setConfirmDeleteOpen(false)}>
|
|
<DialogTitle>Confirm Delete</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>
|
|
Are you sure you want to delete "{selectedWaiter?.name}"? This action cannot be undone.
|
|
</Typography>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setConfirmDeleteOpen(false)}>Cancel</Button>
|
|
<Button
|
|
onClick={handleDeleteConfirm}
|
|
color="error"
|
|
variant="contained"
|
|
disabled={isDeleting}
|
|
>
|
|
{isDeleting ? "Deleting..." : "Delete"}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
<EmployeeDetailsModal
|
|
open={detailsModalOpen}
|
|
onClose={() => setDetailsModalOpen(false)}
|
|
employee={waiterDetails} // بدل waiter
|
|
type="waiter"
|
|
/>
|
|
|
|
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default Waiter;
|