RestroAi/src/components/kds/KDSKanban.tsx

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>
);
};