672 lines
26 KiB
JavaScript
672 lines
26 KiB
JavaScript
import React, { useState } from 'react';
|
|
import {
|
|
Paper,
|
|
Stack,
|
|
Typography,
|
|
Table,
|
|
TableCell,
|
|
TableBody,
|
|
TableHead,
|
|
TableRow,
|
|
TableContainer,
|
|
Avatar,
|
|
Box,
|
|
ToggleButtonGroup,
|
|
ToggleButton,
|
|
Autocomplete,
|
|
TextField,
|
|
useMediaQuery,
|
|
Button
|
|
} from '@mui/material';
|
|
import { useTheme } from '@mui/material/styles';
|
|
import {
|
|
MdCheckCircle,
|
|
MdCancel,
|
|
MdAccessTime,
|
|
MdInventory2,
|
|
MdTwoWheeler,
|
|
MdArrowForward
|
|
} from 'react-icons/md';
|
|
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
|
import dayjs from 'dayjs';
|
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
import axios from 'axios';
|
|
import { OpenToast } from 'components/third-party/OpenToast';
|
|
|
|
const STATUS_META = {
|
|
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle },
|
|
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel },
|
|
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle },
|
|
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel },
|
|
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
|
|
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
|
|
};
|
|
|
|
export default function RiderSubstitution({
|
|
appId,
|
|
rows,
|
|
allRidersList,
|
|
substituteRidersList,
|
|
substituteAssignments,
|
|
handleAssignSubstitute,
|
|
onFinalizeSuccess,
|
|
selectedDate,
|
|
setSelectedDate,
|
|
DT,
|
|
BRAND,
|
|
edge,
|
|
tint,
|
|
soft
|
|
}) {
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
const [selectedBatch, setSelectedBatch] = useState('all');
|
|
const [isFinalizing, setIsFinalizing] = useState(false);
|
|
|
|
const getBatchForTime = React.useCallback((timeStr) => {
|
|
if (!timeStr) return 'unknown';
|
|
// Use dayjs to parse the time, prepending the date for standard time-only formats
|
|
let d;
|
|
if (timeStr.includes('-') || timeStr.includes('/')) {
|
|
d = dayjs(timeStr);
|
|
} else {
|
|
d = dayjs(`${dayjs().format('MM-DD-YYYY')} ${timeStr}`);
|
|
}
|
|
|
|
if (!d.isValid()) {
|
|
// Fallback to simple split if dayjs fails
|
|
const parts = timeStr.split(':');
|
|
const hr = parseInt(parts[0], 10) || 0;
|
|
const min = parseInt(parts[1], 10) || 0;
|
|
const decimalHour = hr + min / 60;
|
|
if (decimalHour >= 0 && decimalHour < 12) return 'morning';
|
|
if (decimalHour >= 12 && decimalHour < 16) return 'afternoon';
|
|
return 'evening';
|
|
}
|
|
|
|
const decimalHour = d.hour() + d.minute() / 60;
|
|
if (decimalHour >= 0 && decimalHour < 8.5) {
|
|
return 'morning';
|
|
} else if (decimalHour >= 8.5 && decimalHour < 13.5) {
|
|
return 'afternoon';
|
|
} else {
|
|
return 'evening';
|
|
}
|
|
}, []);
|
|
|
|
const isBatchDisabled = React.useCallback((batch) => {
|
|
if (!selectedDate) return false;
|
|
const isToday = selectedDate.isSame(dayjs(), 'day');
|
|
const isPast = selectedDate.isBefore(dayjs(), 'day');
|
|
if (isPast) return true;
|
|
if (!isToday) return false;
|
|
|
|
const now = dayjs();
|
|
const currentHour = now.hour();
|
|
const currentMinute = now.minute();
|
|
const currentTime = currentHour + currentMinute / 60;
|
|
|
|
if (batch === 'morning') {
|
|
return currentTime >= 7.0; // After 7:00 AM
|
|
}
|
|
if (batch === 'afternoon') {
|
|
return currentTime >= 9.0; // After 9:00 AM
|
|
}
|
|
if (batch === 'evening') {
|
|
return currentTime >= 16.0; // After 4:00 PM
|
|
}
|
|
return false;
|
|
}, [selectedDate]);
|
|
|
|
const filteredRows = React.useMemo(() => {
|
|
return rows;
|
|
}, [rows]);
|
|
|
|
const hasAssignments = Object.values(substituteAssignments || {}).some(
|
|
(val) => val !== null && val !== undefined
|
|
);
|
|
|
|
const handleFinalize = async () => {
|
|
setIsFinalizing(true);
|
|
try {
|
|
let partnerId = '';
|
|
try {
|
|
const rawLocs = localStorage.getItem('applocations');
|
|
if (rawLocs) {
|
|
const locs = JSON.parse(rawLocs);
|
|
const currentLoc = locs.find((l) => l.applocationid === appId);
|
|
if (currentLoc && currentLoc.partnerid) {
|
|
partnerId = currentLoc.partnerid;
|
|
} else {
|
|
const firstValidLoc = locs.find((l) => l.partnerid);
|
|
if (firstValidLoc && firstValidLoc.partnerid) {
|
|
partnerId = firstValidLoc.partnerid;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing applocations', e);
|
|
}
|
|
|
|
if (!partnerId) {
|
|
const savedPartnerId = localStorage.getItem('partnerid');
|
|
if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') {
|
|
partnerId = savedPartnerId;
|
|
}
|
|
}
|
|
|
|
if (!partnerId || partnerId === '0' || partnerId === 0) {
|
|
partnerId = 44;
|
|
}
|
|
|
|
const tenantId = parseInt(partnerId, 10);
|
|
|
|
const substitutions = Object.entries(substituteAssignments || {})
|
|
.filter((entry) => entry[1] !== null && entry[1] !== undefined)
|
|
.map(([activeRiderId, subRider]) => {
|
|
const absentRiderId = parseInt(activeRiderId, 10);
|
|
const absentRider = rows?.find((r) => r.userid === absentRiderId);
|
|
return {
|
|
sub_date: selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD'),
|
|
absent_rider_id: absentRiderId,
|
|
absent_rider_name: absentRider?.username || absentRider?.fullname || `Rider #${absentRiderId}`,
|
|
sub_rider_id: parseInt(subRider.userid, 10),
|
|
sub_rider_name: subRider.username || subRider.fullname || `Rider #${subRider.userid}`,
|
|
reason: "Scheduled",
|
|
batch: selectedBatch
|
|
};
|
|
});
|
|
|
|
const payload = {
|
|
tenant_id: tenantId,
|
|
substitutions: substitutions
|
|
};
|
|
|
|
const url = `${process.env.REACT_APP_URL}/substitutions`;
|
|
const response = await axios.post(url, payload);
|
|
|
|
if (response.data && response.data.status) {
|
|
OpenToast('Substitutions finalized successfully!', 'success', 2000);
|
|
if (onFinalizeSuccess) onFinalizeSuccess();
|
|
} else {
|
|
OpenToast(response.data?.message || 'Substitutions saved successfully!', 'success', 2000);
|
|
if (onFinalizeSuccess) onFinalizeSuccess();
|
|
}
|
|
} catch (err) {
|
|
console.error('Finalize error:', err);
|
|
// Fallback/simulation
|
|
OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000);
|
|
if (onFinalizeSuccess) onFinalizeSuccess();
|
|
} finally {
|
|
setIsFinalizing(false);
|
|
}
|
|
};
|
|
|
|
const getRowStatusMeta = (row) => {
|
|
const key = (row?.status || '').toLowerCase() === 'active' ? 'active' : 'inactive';
|
|
return STATUS_META[key] || STATUS_META.unknown;
|
|
};
|
|
|
|
const totalCols = 6;
|
|
|
|
const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
|
<Avatar
|
|
sx={{
|
|
width: size,
|
|
height: size,
|
|
bgcolor: selected ? color : soft(color),
|
|
color: selected ? '#fff' : color,
|
|
transition: 'background-color 0.15s, color 0.15s'
|
|
}}
|
|
>
|
|
{children}
|
|
</Avatar>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Filter Row inside its own Paper */}
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
mb: 2,
|
|
p: 2,
|
|
borderRadius: `${DT.radiusCard / 8}px`,
|
|
border: '1px solid',
|
|
borderColor: DT.borderSubtle,
|
|
background: '#fff'
|
|
}}
|
|
>
|
|
<Stack
|
|
direction={{ xs: 'column', sm: 'row' }}
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
gap={2}
|
|
>
|
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ flexWrap: 'wrap' }}>
|
|
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textSecondary, mr: 1 }}>
|
|
Shift Batch:
|
|
</Typography>
|
|
<ToggleButtonGroup
|
|
value={selectedBatch}
|
|
exclusive
|
|
onChange={(e, val) => val && setSelectedBatch(val)}
|
|
aria-label="batch selection"
|
|
size="small"
|
|
sx={{
|
|
'& .MuiToggleButton-root': {
|
|
borderRadius: '8px',
|
|
mx: 0.5,
|
|
border: `1px solid ${DT.borderSubtle}`,
|
|
color: DT.textSecondary,
|
|
fontWeight: 600,
|
|
textTransform: 'capitalize',
|
|
px: 2,
|
|
py: 0.5,
|
|
'&.Mui-selected': {
|
|
bgcolor: BRAND,
|
|
color: '#fff',
|
|
'&:hover': {
|
|
bgcolor: '#4D1C61'
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<ToggleButton value="all">All Batches</ToggleButton>
|
|
<ToggleButton value="morning">Morning</ToggleButton>
|
|
<ToggleButton value="afternoon">Afternoon</ToggleButton>
|
|
<ToggleButton value="evening">Evening</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Stack>
|
|
<Box>
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
<DatePicker
|
|
label="Select Date"
|
|
value={selectedDate}
|
|
onChange={(newValue) => newValue && setSelectedDate(newValue)}
|
|
slotProps={{
|
|
textField: {
|
|
size: 'small',
|
|
sx: {
|
|
width: 180,
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: '20px',
|
|
'& fieldset': { borderColor: DT.borderSubtle },
|
|
'&:hover fieldset': { borderColor: BRAND },
|
|
'&.Mui-focused fieldset': { borderColor: BRAND }
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</LocalizationProvider>
|
|
</Box>
|
|
</Stack>
|
|
</Paper>
|
|
|
|
{/* Results Table/Cards */}
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
borderRadius: `${DT.radiusCard / 8}px`,
|
|
border: '1px solid',
|
|
borderColor: DT.borderSubtle,
|
|
overflow: 'hidden',
|
|
background: '#fff'
|
|
}}
|
|
>
|
|
<TableContainer
|
|
sx={{
|
|
maxHeight: 'calc(100vh - 240px)',
|
|
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
|
'&::-webkit-scrollbar-thumb': {
|
|
backgroundColor: edge(BRAND),
|
|
borderRadius: 8,
|
|
'&:hover': { backgroundColor: BRAND }
|
|
},
|
|
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
|
}}
|
|
>
|
|
{isMobile ? (
|
|
<MobileCardList sx={{ p: 1.25 }}>
|
|
{filteredRows?.length === 0 && (
|
|
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
|
|
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
|
<MdTwoWheeler size={28} />
|
|
</Avatar>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
|
No active riders to show
|
|
</Typography>
|
|
</Stack>
|
|
)}
|
|
|
|
{filteredRows?.length !== 0 &&
|
|
filteredRows?.map((row, index) => {
|
|
const statusMeta = getRowStatusMeta(row);
|
|
const StatusIcon = statusMeta.icon;
|
|
const riderProfile = allRidersList?.find((r) => r.userid === row.userid);
|
|
const starttime = riderProfile?.starttime || row.starttime;
|
|
return (
|
|
<MobileCard
|
|
key={row.userid ?? index}
|
|
accent={statusMeta.color}
|
|
header={
|
|
<Stack spacing={1.25}>
|
|
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
|
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
|
|
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
|
{String(index + 1).padStart(2, '0')}
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
px: 1,
|
|
py: 0.375,
|
|
borderRadius: 999,
|
|
bgcolor: '#ffffff',
|
|
border: `1px solid ${edge(BRAND)}`,
|
|
color: BRAND,
|
|
fontWeight: 800,
|
|
fontSize: 11
|
|
}}
|
|
>
|
|
#{row?.userid}
|
|
</Box>
|
|
<Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
spacing={0.5}
|
|
sx={{
|
|
display: 'inline-flex',
|
|
pl: 0.5,
|
|
pr: 1,
|
|
py: 0.25,
|
|
borderRadius: 999,
|
|
bgcolor: tint(statusMeta.color),
|
|
border: `1px solid ${edge(statusMeta.color)}`,
|
|
color: statusMeta.color
|
|
}}
|
|
>
|
|
<AccentAvatar color={statusMeta.color} size={18}>
|
|
<StatusIcon size={11} />
|
|
</AccentAvatar>
|
|
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 10.5, lineHeight: 1 }}>
|
|
{statusMeta.label}
|
|
</Typography>
|
|
</Stack>
|
|
</Stack>
|
|
</Stack>
|
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
<Avatar
|
|
sx={{
|
|
width: 36,
|
|
height: 36,
|
|
bgcolor: soft(BRAND),
|
|
color: BRAND,
|
|
fontWeight: 800,
|
|
fontSize: 16,
|
|
border: `1px solid ${edge(BRAND)}`,
|
|
flexShrink: 0
|
|
}}
|
|
>
|
|
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
<Box sx={{ minWidth: 0 }}>
|
|
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
|
|
{row.username || '—'}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
|
{row.contactno || '—'}
|
|
</Typography>
|
|
</Box>
|
|
</Stack>
|
|
</Stack>
|
|
}
|
|
>
|
|
<MobileFieldGrid>
|
|
<MobileField label="Substitute" full>
|
|
<Autocomplete
|
|
size="small"
|
|
disabled={isBatchDisabled(getBatchForTime(starttime))}
|
|
options={substituteRidersList || []}
|
|
isOptionEqualToValue={(option, value) => option?.userid === value?.userid}
|
|
getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`}
|
|
value={substituteAssignments[row.userid] || null}
|
|
onChange={(event, newValue) => handleAssignSubstitute(row.userid, newValue)}
|
|
renderInput={(params) => (
|
|
<TextField
|
|
{...params}
|
|
placeholder="Assign Substitute"
|
|
sx={{
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: '8px',
|
|
bgcolor: '#f8fafc',
|
|
'& fieldset': { borderColor: DT.borderSubtle }
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
</MobileField>
|
|
</MobileFieldGrid>
|
|
</MobileCard>
|
|
);
|
|
})}
|
|
</MobileCardList>
|
|
) : (
|
|
<Table stickyHeader sx={{ minWidth: 1200 }}>
|
|
<TableHead>
|
|
<TableRow
|
|
sx={{
|
|
'& th': {
|
|
backgroundColor: DT.surfaceAlt,
|
|
color: DT.textSecondary,
|
|
fontSize: { xs: 10, md: 11 },
|
|
fontWeight: 800,
|
|
letterSpacing: 0.6,
|
|
textTransform: 'uppercase',
|
|
whiteSpace: 'nowrap',
|
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
|
py: { xs: 1, md: 1.25 },
|
|
px: { xs: 1, md: 2 }
|
|
}
|
|
}}
|
|
>
|
|
<TableCell>#</TableCell>
|
|
<TableCell>ID</TableCell>
|
|
<TableCell>Rider</TableCell>
|
|
<TableCell align="center"></TableCell>
|
|
<TableCell>Substitute Rider</TableCell>
|
|
<TableCell align="center">Status</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{filteredRows?.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={totalCols} sx={{ py: 6 }}>
|
|
<Stack alignItems="center" spacing={1.5}>
|
|
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
|
<MdTwoWheeler size={28} />
|
|
</Avatar>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
|
No active riders to show
|
|
</Typography>
|
|
</Stack>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
|
|
{filteredRows?.length !== 0 &&
|
|
filteredRows?.map((row, index) => {
|
|
const statusMeta = getRowStatusMeta(row);
|
|
const StatusIcon = statusMeta.icon;
|
|
const riderProfile = allRidersList?.find((r) => r.userid === row.userid);
|
|
const starttime = riderProfile?.starttime || row.starttime;
|
|
return (
|
|
<TableRow
|
|
key={row.userid ?? index}
|
|
sx={{
|
|
transition: 'background-color 0.15s',
|
|
'& td': {
|
|
borderBottom: `1px solid ${DT.divider}`,
|
|
py: { xs: 1, md: 1.5 },
|
|
px: { xs: 1, md: 2 }
|
|
},
|
|
'&:hover': { backgroundColor: DT.surfaceAlt }
|
|
}}
|
|
>
|
|
<TableCell>
|
|
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
|
{String(index + 1).padStart(2, '0')}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box
|
|
sx={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
px: 1,
|
|
py: 0.375,
|
|
borderRadius: 999,
|
|
bgcolor: '#ffffff',
|
|
border: `1px solid ${edge(BRAND)}`,
|
|
color: BRAND,
|
|
fontWeight: 800,
|
|
fontSize: 11,
|
|
minWidth: 56
|
|
}}
|
|
>
|
|
#{row?.userid}
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
<Avatar
|
|
sx={{
|
|
width: 36,
|
|
height: 36,
|
|
bgcolor: soft(BRAND),
|
|
color: BRAND,
|
|
fontWeight: 800,
|
|
fontSize: 16,
|
|
border: `1px solid ${edge(BRAND)}`
|
|
}}
|
|
>
|
|
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
<Stack sx={{ minWidth: 0 }}>
|
|
<Typography
|
|
variant="subtitle2"
|
|
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
|
>
|
|
{row.username || '—'}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
|
{row.contactno || '—'}
|
|
</Typography>
|
|
</Stack>
|
|
</Stack>
|
|
</TableCell>
|
|
|
|
{/* Substitution Arrow & Autocomplete */}
|
|
<TableCell align="center">
|
|
<MdArrowForward size={18} style={{ color: DT.textSecondary }} />
|
|
</TableCell>
|
|
<TableCell sx={{ minWidth: 220 }}>
|
|
<Autocomplete
|
|
size="small"
|
|
disabled={isBatchDisabled(getBatchForTime(starttime))}
|
|
options={substituteRidersList || []}
|
|
isOptionEqualToValue={(option, value) => option?.userid === value?.userid}
|
|
getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`}
|
|
value={substituteAssignments[row.userid] || null}
|
|
onChange={(event, newValue) => handleAssignSubstitute(row.userid, newValue)}
|
|
renderInput={(params) => (
|
|
<TextField
|
|
{...params}
|
|
placeholder="Assign Substitute"
|
|
sx={{
|
|
'& .MuiOutlinedInput-root': {
|
|
borderRadius: '8px',
|
|
bgcolor: '#f8fafc',
|
|
'& fieldset': { borderColor: DT.borderSubtle }
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
|
|
<TableCell align="center">
|
|
<Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
spacing={0.5}
|
|
sx={{
|
|
display: 'inline-flex',
|
|
pl: 0.5,
|
|
pr: 1,
|
|
py: 0.25,
|
|
borderRadius: 999,
|
|
bgcolor: tint(statusMeta.color),
|
|
border: `1px solid ${edge(statusMeta.color)}`,
|
|
color: statusMeta.color
|
|
}}
|
|
>
|
|
<AccentAvatar color={statusMeta.color} size={20}>
|
|
<StatusIcon size={12} />
|
|
</AccentAvatar>
|
|
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 11, lineHeight: 1 }}>
|
|
{statusMeta.label}
|
|
</Typography>
|
|
</Stack>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</TableContainer>
|
|
{hasAssignments && (
|
|
<Box
|
|
sx={{
|
|
p: 2,
|
|
borderTop: `1px solid ${DT.borderSubtle}`,
|
|
display: 'flex',
|
|
justifyContent: 'flex-end',
|
|
bgcolor: DT.surfaceAlt
|
|
}}
|
|
>
|
|
<Button
|
|
variant="contained"
|
|
disabled={isFinalizing}
|
|
onClick={handleFinalize}
|
|
sx={{
|
|
bgcolor: BRAND,
|
|
color: '#fff',
|
|
borderRadius: '8px',
|
|
px: 4,
|
|
py: 1,
|
|
fontWeight: 700,
|
|
textTransform: 'none',
|
|
'&:hover': {
|
|
bgcolor: '#4D1C61'
|
|
}
|
|
}}
|
|
>
|
|
{isFinalizing ? 'Saving...' : 'Finalize Substitutions'}
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
</>
|
|
);
|
|
}
|