import React, { useState, useEffect, useMemo } from "react";
|
import {
|
Box,
|
Card,
|
CardContent,
|
Grid,
|
Typography,
|
Table,
|
TableBody,
|
TableCell,
|
TableHead,
|
TableRow,
|
Divider,
|
CircularProgress,
|
Stack,
|
FormControl,
|
InputLabel,
|
Select,
|
MenuItem,
|
} from '@mui/material';
|
import {
|
useTranslate,
|
useRecordContext,
|
useDataProvider,
|
useGetMany,
|
} from 'react-admin';
|
import { format } from 'date-fns';
|
import * as Common from '@/utils/common'
|
|
const STATUS_FIELDS = [
|
{ key: 'autoing', labelKey: 'table.field.sta.autoing' },
|
{ key: 'loading', labelKey: 'table.field.sta.loading' },
|
{ key: 'inEnable', labelKey: 'table.field.sta.inEnable' },
|
{ key: 'outEnable', labelKey: 'table.field.sta.outEnable' },
|
];
|
|
const INFO_FIELDS = [
|
{ labelKey: 'table.field.sta.zoneId', valueKey: 'zoneId$' },
|
{ labelKey: 'table.field.sta.staType', valueKey: 'staType$' },
|
{ labelKey: 'table.field.sta.code', valueKey: 'code$' },
|
{ labelKey: 'table.field.sta.capacity', valueKey: 'capacity' },
|
{ labelKey: 'table.field.sta.rsvInCnt', valueKey: 'rsvInCnt' },
|
{ labelKey: 'table.field.sta.rsvOutCnt', valueKey: 'rsvOutCnt' },
|
{ labelKey: 'table.field.sta.offset', valueKey: 'offset' },
|
{ labelKey: 'table.field.sta.zpallet', valueKey: 'zpallet' },
|
];
|
|
const RESERVE_COLUMN_COUNT = 11;
|
const RESERVE_TYPE_OPTIONS = ['IN', 'OUT'];
|
const RESERVE_STATE_OPTIONS = ['reserved', 'waiting', 'confirmed', 'canceled', 'timeout'];
|
const StaPanel = () => {
|
const record = useRecordContext();
|
const translate = useTranslate();
|
const dataProvider = useDataProvider();
|
const [reserves, setReserves] = useState([]);
|
const [isReservesLoading, setIsReservesLoading] = useState(false);
|
const [typeFilter, setTypeFilter] = useState('');
|
const [stateFilter, setStateFilter] = useState('');
|
const taskIds = useMemo(() => extractIds(reserves, 'taskId'), [reserves]);
|
const segmentIds = useMemo(() => extractIds(reserves, 'segmentId'), [reserves]);
|
const agvIds = useMemo(() => extractIds(reserves, 'agvId'), [reserves]);
|
|
const { data: taskRecords = [] } = useGetMany('task', { ids: taskIds }, { enabled: taskIds.length > 0 });
|
const { data: segmentRecords = [] } = useGetMany('segment', { ids: segmentIds }, { enabled: segmentIds.length > 0 });
|
const { data: agvRecords = [] } = useGetMany('agv', { ids: agvIds }, { enabled: agvIds.length > 0 });
|
|
const taskLabelMap = useMemo(
|
() => createLabelMap(taskRecords, (item) => item.seqNum),
|
[taskRecords]
|
);
|
const segmentLabelMap = useMemo(
|
() => createLabelMap(segmentRecords, (item) => item.groupId + '-' + item.serial),
|
[segmentRecords]
|
);
|
const agvLabelMap = useMemo(
|
() => createLabelMap(agvRecords, (item) => item.uuid),
|
[agvRecords]
|
);
|
|
const filteredReserves = useMemo(() => {
|
if (!typeFilter && !stateFilter) {
|
return reserves;
|
}
|
return reserves.filter((reserve) => {
|
const matchesType =
|
!typeFilter || normalizeValueKey(reserve?.type) === normalizeValueKey(typeFilter);
|
const matchesState =
|
!stateFilter || normalizeValueKey(reserve?.state) === normalizeValueKey(stateFilter);
|
return matchesType && matchesState;
|
});
|
}, [reserves, typeFilter, stateFilter]);
|
|
useEffect(() => {
|
if (!record?.id) {
|
setReserves([]);
|
setIsReservesLoading(false);
|
return;
|
}
|
|
let isMounted = true;
|
setIsReservesLoading(true);
|
dataProvider.getList('staReserve', {
|
pagination: { page: 1, perPage: 10 },
|
sort: { field: 'updateTime', order: 'desc' },
|
filter: { staId: record.id },
|
}).then(({ data }) => {
|
if (!isMounted) return;
|
setReserves(data || []);
|
}).catch(() => {
|
if (!isMounted) return;
|
setReserves([]);
|
}).finally(() => {
|
if (!isMounted) return;
|
setIsReservesLoading(false);
|
});
|
|
return () => {
|
isMounted = false;
|
};
|
}, [record?.id, dataProvider]);
|
|
if (!record) return null;
|
|
return (
|
<>
|
<Card sx={{ maxWidth: '80%', margin: 'auto', mt: .5, mb: .5 }}>
|
<CardContent>
|
<Grid container spacing={2}>
|
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
<Typography variant="subtitle2" gutterBottom align="left" sx={{
|
maxWidth: { xs: '140px', sm: '220px', md: '300px', lg: '360px' },
|
whiteSpace: 'nowrap',
|
overflow: 'hidden',
|
textOverflow: 'ellipsis',
|
}}>
|
{Common.camelToPascalWithSpaces(translate('table.field.sta.staNo'))}: {record.staNo}
|
</Typography>
|
<Typography variant="caption" color="textSecondary" sx={{ wordWrap: 'break-word', wordBreak: 'break-all' }}>
|
{Common.camelToPascalWithSpaces(translate('common.field.memo'))}:{record.memo || '-'}
|
</Typography>
|
</Grid>
|
</Grid>
|
|
<Box height={12}> </Box>
|
|
<Grid container spacing={2}>
|
{INFO_FIELDS.map(({ labelKey, valueKey }) => (
|
<Grid item xs={12} sm={6} md={4} key={labelKey}>
|
<InfoItem
|
labelKey={labelKey}
|
value={getRecordValue(record, valueKey)}
|
/>
|
</Grid>
|
))}
|
</Grid>
|
|
<Divider sx={{ my: 2 }} />
|
|
{/* <Typography variant="subtitle2" color="textSecondary" gutterBottom>
|
{translate('common.field.status')}
|
</Typography> */}
|
<Grid container spacing={2}>
|
{STATUS_FIELDS.map(({ key, labelKey }) => (
|
<Grid item xs={6} sm={3} key={key}>
|
<StatusIndicator labelKey={labelKey} value={record[key]} />
|
</Grid>
|
))}
|
</Grid>
|
<Divider sx={{ my: 2 }} />
|
|
<Box>
|
<Typography variant="subtitle1" gutterBottom>
|
{translate('menu.staReserve')}
|
</Typography>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} mb={2}>
|
<FormControl size="small" sx={{ minWidth: 160 }}>
|
<InputLabel id="sta-panel-reserve-type-label">
|
{translate('table.field.staReserve.type')}
|
</InputLabel>
|
<Select
|
labelId="sta-panel-reserve-type-label"
|
value={typeFilter}
|
label={translate('table.field.staReserve.type')}
|
onChange={(event) => setTypeFilter(event.target.value)}
|
>
|
<MenuItem value="">
|
{translate('common.action.deselect')}
|
</MenuItem>
|
{RESERVE_TYPE_OPTIONS.map((option) => (
|
<MenuItem key={option} value={option}>
|
{formatReserveType(option, translate)}
|
</MenuItem>
|
))}
|
</Select>
|
</FormControl>
|
<FormControl size="small" sx={{ minWidth: 160 }}>
|
<InputLabel id="sta-panel-reserve-state-label">
|
{translate('table.field.staReserve.state')}
|
</InputLabel>
|
<Select
|
labelId="sta-panel-reserve-state-label"
|
value={stateFilter}
|
label={translate('table.field.staReserve.state')}
|
onChange={(event) => setStateFilter(event.target.value)}
|
>
|
<MenuItem value="">
|
{translate('common.action.deselect')}
|
</MenuItem>
|
{RESERVE_STATE_OPTIONS.map((option) => (
|
<MenuItem key={option} value={option}>
|
{formatReserveState(option, translate)}
|
</MenuItem>
|
))}
|
</Select>
|
</FormControl>
|
</Stack>
|
<Table size="small">
|
<TableHead>
|
<TableRow>
|
<TableCell>{translate('table.field.staReserve.uuid')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.taskId')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.segmentId')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.agvId')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.type')}</TableCell>
|
<TableCellRight>{translate('table.field.staReserve.qty')}</TableCellRight>
|
<TableCell>{translate('table.field.staReserve.state')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.expireTime')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.waitingAt')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.confirmedAt')}</TableCell>
|
<TableCell>{translate('table.field.staReserve.uniqKey')}</TableCell>
|
</TableRow>
|
</TableHead>
|
<TableBody>
|
{isReservesLoading && (
|
<TableRow>
|
<TableCell colSpan={RESERVE_COLUMN_COUNT}>
|
<Box display="flex" alignItems="center" gap={1}>
|
<CircularProgress size={16} />
|
<Typography variant="body2" color="textSecondary">
|
{translate('common.loading', { _: 'Loading...' })}
|
</Typography>
|
</Box>
|
</TableCell>
|
</TableRow>
|
)}
|
{!isReservesLoading && filteredReserves.length === 0 && (
|
<TableRow>
|
<TableCell colSpan={RESERVE_COLUMN_COUNT}>
|
<Typography variant="body2" color="textSecondary">
|
{translate('ra.navigation.no_results')}
|
</Typography>
|
</TableCell>
|
</TableRow>
|
)}
|
{filteredReserves.map((reserve) => (
|
<TableRow key={reserve.id}>
|
<TableCell>{reserve.uuid || '-'}</TableCell>
|
<TableCell>{getReferenceLabel(taskLabelMap, reserve.taskId)}</TableCell>
|
<TableCell>{getReferenceLabel(segmentLabelMap, reserve.segmentId)}</TableCell>
|
<TableCell>{getReferenceLabel(agvLabelMap, reserve.agvId)}</TableCell>
|
<TableCell>{formatReserveType(reserve.type, translate)}</TableCell>
|
<TableCellRight>{reserve.qty ?? '-'}</TableCellRight>
|
<TableCell>{formatReserveState(reserve.state, translate)}</TableCell>
|
<TableCell>{formatDateTime(reserve.expireTime)}</TableCell>
|
<TableCell>{formatDateTime(reserve.waitingAt)}</TableCell>
|
<TableCell>{formatDateTime(reserve.confirmedAt)}</TableCell>
|
<TableCell>{reserve.uniqKey || '-'}</TableCell>
|
</TableRow>
|
))}
|
</TableBody>
|
</Table>
|
</Box>
|
</CardContent>
|
</Card >
|
</>
|
);
|
};
|
|
const InfoItem = ({ labelKey, value }) => {
|
const translate = useTranslate();
|
return (
|
<Stack spacing={0.5}>
|
<Typography variant="caption" color="textSecondary">
|
{Common.camelToPascalWithSpaces(translate(labelKey))}
|
</Typography>
|
<Typography variant="body2" fontWeight={400}>
|
{formatInfoValue(value)}
|
</Typography>
|
</Stack>
|
);
|
};
|
|
const getRecordValue = (record, key) => {
|
if (!record) return undefined;
|
if (record[key] !== undefined && record[key] !== null) {
|
return record[key];
|
}
|
if (key.endsWith('$')) {
|
const fallbackKey = key.slice(0, -1);
|
return record[fallbackKey];
|
}
|
return record[key];
|
};
|
|
const formatInfoValue = (value) => {
|
if (value === null || value === undefined || value === '') {
|
return '-';
|
}
|
return value;
|
};
|
|
const StatusIndicator = ({ labelKey, value }) => {
|
const translate = useTranslate();
|
const color = getIndicatorColor(value);
|
return (
|
<Stack spacing={0.5}>
|
<Typography variant="caption" color="textSecondary">
|
{Common.camelToPascalWithSpaces(translate(labelKey))}
|
</Typography>
|
<Box display="flex" alignItems="center" gap={1}>
|
<Box sx={{
|
width: 14,
|
height: 14,
|
borderRadius: '50%',
|
backgroundColor: color,
|
boxShadow: `0 0 3px ${color}`,
|
border: '1px solid rgba(0,0,0,0.12)'
|
}} />
|
<Typography variant="body2">
|
{formatIndicatorValue(value)}
|
</Typography>
|
</Box>
|
</Stack>
|
);
|
};
|
|
const getIndicatorColor = (value) => {
|
if (isTruthy(value)) {
|
return '#2e7d32';
|
}
|
if (isFalsy(value)) {
|
return '#9e9e9e';
|
}
|
return '#d40000ff';
|
};
|
|
const isTruthy = (value) => {
|
if (typeof value === 'boolean') return value;
|
if (typeof value === 'number') return value > 0;
|
if (typeof value === 'string') {
|
const normalized = value.toLowerCase();
|
return ['1', 'true', 'y', 'yes', 'open', 'enable'].includes(normalized);
|
}
|
return false;
|
};
|
|
const isFalsy = (value) => {
|
if (typeof value === 'boolean') return !value;
|
if (typeof value === 'number') return value === 0;
|
if (typeof value === 'string') {
|
const normalized = value.toLowerCase();
|
return ['0', 'false', 'n', 'no', 'close', 'disable'].includes(normalized);
|
}
|
return false;
|
};
|
|
const formatIndicatorValue = (value) => {
|
if (value === null || value === undefined || value === '') {
|
return '-';
|
}
|
return String(value);
|
};
|
|
const extractIds = (items, key) => {
|
if (!items || items.length === 0) return [];
|
const unique = new Set();
|
items.forEach((item) => {
|
const value = item?.[key];
|
if (value !== undefined && value !== null && value !== '') {
|
unique.add(value);
|
}
|
});
|
return Array.from(unique);
|
};
|
|
const createLabelMap = (records, getLabel) => {
|
if (!records || records.length === 0) return {};
|
return records.reduce((acc, record) => {
|
if (record?.id === undefined || record?.id === null) {
|
return acc;
|
}
|
const label = getLabel(record);
|
acc[record.id] = label ?? record.id;
|
return acc;
|
}, {});
|
};
|
|
const getReferenceLabel = (map, id) => {
|
if (id === undefined || id === null || id === '') {
|
return '-';
|
}
|
return map?.[id] || '-';
|
};
|
|
const formatReserveType = (value, translate) =>
|
formatReserveEnum(value, translate, 'type');
|
|
const formatReserveState = (value, translate) =>
|
formatReserveEnum(value, translate, 'state', {
|
cancelled: 'canceled',
|
});
|
|
const getReserveRelationValue = (reserve, key) => {
|
if (!reserve) return '-';
|
return reserve[`${key}$`] || reserve[key] || '-';
|
};
|
|
const formatReserveEnum = (value, translate, enumType, overrides = {}) => {
|
const normalized = normalizeValueKey(value);
|
if (!normalized) {
|
return '-';
|
}
|
const translationKey = overrides[normalized] || normalized;
|
return translate(`page.sta.enums.${enumType}.${translationKey}`, { _: value });
|
};
|
|
const normalizeValueKey = (value) => {
|
if (typeof value === 'string') {
|
return value.trim().toLowerCase();
|
}
|
if (value === undefined || value === null) {
|
return '';
|
}
|
return String(value).trim().toLowerCase();
|
};
|
|
const formatDateTime = (value) => {
|
if (!value) return '-';
|
try {
|
const date = new Date(value);
|
if (Number.isNaN(date.getTime())) {
|
return value;
|
}
|
return format(date, 'MM-dd HH:mm');
|
} catch (error) {
|
return value;
|
}
|
};
|
|
const TableCellRight = (props) => <TableCell align="right" {...props} />;
|
|
export default StaPanel;
|