232 lines
7.5 KiB
TypeScript
232 lines
7.5 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Box,
|
|
Grid,
|
|
Paper,
|
|
Typography,
|
|
Chip,
|
|
Button,
|
|
TextField,
|
|
InputAdornment,
|
|
ButtonGroup,
|
|
} from '@mui/material';
|
|
import SearchIcon from '@mui/icons-material/Search';
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
import RestaurantMenuIcon from '@mui/icons-material/RestaurantMenu';
|
|
import type { KDSOrder, OrderStatus, OrderType } from '../../types';
|
|
import { KDSTicketCard } from './KDSTicketCard';
|
|
|
|
interface KDSKanbanProps {
|
|
orders: KDSOrder[];
|
|
busyOrderId?: string | null;
|
|
onStatusChange: (id: string, newStatus: OrderStatus) => void;
|
|
onToggleItem: (orderId: string, itemId: string) => void;
|
|
onAddSampleOrder: () => void;
|
|
}
|
|
|
|
export const KDSKanban: React.FC<KDSKanbanProps> = ({
|
|
orders,
|
|
busyOrderId = null,
|
|
onStatusChange,
|
|
onToggleItem,
|
|
onAddSampleOrder,
|
|
}) => {
|
|
const [filterType, setFilterType] = useState<'ALL' | OrderType>('ALL');
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
const filteredOrders = orders.filter((order) => {
|
|
const matchesType = filterType === 'ALL' || order.orderType === filterType;
|
|
const matchesSearch =
|
|
order.ticketNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
(order.tableNumber && order.tableNumber.includes(searchQuery)) ||
|
|
(order.customerName && order.customerName.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
|
order.items.some((i) => i.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
return matchesType && matchesSearch;
|
|
});
|
|
|
|
const columns: { status: OrderStatus; title: string; color: string; bg: string }[] = [
|
|
{ status: 'pending', title: 'PENDING', color: '#ba1a1a', bg: '#ffdad6' },
|
|
{ status: 'preparing', title: 'IN PREPARATION', color: '#845000', bg: '#ffddba' },
|
|
{ status: 'ready', title: 'READY FOR PICKUP / SERVE', color: '#11651d', bg: '#a3f69c' },
|
|
{ status: 'served', title: 'SERVED & COMPLETED', color: '#546067', bg: '#e2e2e2' },
|
|
];
|
|
|
|
return (
|
|
<Box sx={{ width: '100%', maxWidth: '100%' }}>
|
|
{/* Top Filter Bar */}
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: { xs: 1.5, md: 2 },
|
|
mb: 2,
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
gap: 1.5,
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
border: '1px solid #e2e2e2',
|
|
borderRadius: '12px',
|
|
bgcolor: '#fff',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap', minWidth: 0 }}>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{
|
|
fontWeight: 800,
|
|
color: '#ac2d00',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
fontSize: '1.05rem',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
<RestaurantMenuIcon fontSize="small" />
|
|
KDS Board
|
|
</Typography>
|
|
|
|
<ButtonGroup variant="outlined" size="small">
|
|
{(['ALL', 'Dine-In', 'Takeaway', 'Delivery'] as const).map((type) => (
|
|
<Button
|
|
key={type}
|
|
variant={filterType === type ? 'contained' : 'outlined'}
|
|
onClick={() => setFilterType(type)}
|
|
sx={{
|
|
fontWeight: 700,
|
|
color: filterType === type ? '#ffffff' : '#ac2d00',
|
|
borderColor: '#ac2d00',
|
|
'&:hover': {
|
|
borderColor: '#ac2d00',
|
|
},
|
|
}}
|
|
>
|
|
{type}
|
|
</Button>
|
|
))}
|
|
</ButtonGroup>
|
|
</Box>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
|
<TextField
|
|
placeholder="Search Ticket, Table, Item..."
|
|
size="small"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
slotProps={{
|
|
input: {
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon fontSize="small" />
|
|
</InputAdornment>
|
|
),
|
|
},
|
|
}}
|
|
sx={{ width: { xs: 200, sm: 260 } }}
|
|
/>
|
|
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
startIcon={<AddIcon />}
|
|
onClick={onAddSampleOrder}
|
|
sx={{ fontWeight: 800 }}
|
|
>
|
|
NEW TEST ORDER
|
|
</Button>
|
|
</Box>
|
|
</Paper>
|
|
|
|
{/* 4 Column Kanban Board Grid */}
|
|
<Grid container spacing={1.5} alignItems="stretch">
|
|
{columns.map((col) => {
|
|
const colOrders = filteredOrders.filter((o) => o.status === col.status);
|
|
return (
|
|
<Grid size={{ xs: 12, sm: 6, lg: 3 }} key={col.status}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 1.25,
|
|
minHeight: { xs: 320, lg: 'calc(100vh - 200px)' },
|
|
height: '100%',
|
|
backgroundColor: '#f6f7f8',
|
|
border: '1px solid #e4beb4',
|
|
borderRadius: '12px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
{/* Column Header */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 1,
|
|
mb: 1.5,
|
|
pb: 1,
|
|
borderBottom: '2px solid #e2e2e2',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="subtitle2"
|
|
sx={{
|
|
fontWeight: 800,
|
|
color: col.color,
|
|
letterSpacing: '0.04em',
|
|
fontSize: '0.7rem',
|
|
lineHeight: 1.3,
|
|
}}
|
|
>
|
|
{col.title}
|
|
</Typography>
|
|
<Chip
|
|
label={colOrders.length}
|
|
size="small"
|
|
sx={{
|
|
fontWeight: 800,
|
|
backgroundColor: col.bg,
|
|
color: col.color,
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Column Tickets */}
|
|
<Box sx={{ flexGrow: 1, overflowY: 'auto', minHeight: 0, pr: 0.25 }}>
|
|
{colOrders.length === 0 ? (
|
|
<Box
|
|
sx={{
|
|
p: 3,
|
|
textAlign: 'center',
|
|
color: '#8f7068',
|
|
border: '2px dashed #e2e2e2',
|
|
borderRadius: '8px',
|
|
mt: 1,
|
|
}}
|
|
>
|
|
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
|
No orders in {col.title.toLowerCase()}
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
colOrders.map((order) => (
|
|
<KDSTicketCard
|
|
key={order.id}
|
|
order={order}
|
|
busy={busyOrderId === order.id}
|
|
onStatusChange={onStatusChange}
|
|
onToggleItem={onToggleItem}
|
|
/>
|
|
))
|
|
)}
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
);
|
|
})}
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
};
|