| | |
| | | import React, { useState, useEffect } from "react"; |
| | | import React, { useState, useEffect, useMemo } from "react"; |
| | | import { |
| | | Box, |
| | | Card, |
| | |
| | | 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 INFO_FIELDS = [ |
| | | { labelKey: 'table.field.sta.uuid', valueKey: 'uuid' }, |
| | | { 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.offset', valueKey: 'offset' }, |
| | | { labelKey: 'table.field.sta.rsvInCnt', valueKey: 'rsvInCnt' }, |
| | | { labelKey: 'table.field.sta.rsvOutCnt', valueKey: 'rsvOutCnt' }, |
| | | { labelKey: 'table.field.sta.staSts', valueKey: 'staSts$' }, |
| | | { 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) { |
| | |
| | | setIsReservesLoading(true); |
| | | dataProvider.getList('staReserve', { |
| | | pagination: { page: 1, perPage: 10 }, |
| | | sort: { field: 'updateTime', order: 'DESC' }, |
| | | 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); |
| | | }); |
| | | }).then(({ data }) => { |
| | | if (!isMounted) return; |
| | | setReserves(data || []); |
| | | }).catch(() => { |
| | | if (!isMounted) return; |
| | | setReserves([]); |
| | | }).finally(() => { |
| | | if (!isMounted) return; |
| | | setIsReservesLoading(false); |
| | | }); |
| | | |
| | | return () => { |
| | | isMounted = false; |
| | |
| | | |
| | | return ( |
| | | <> |
| | | <Card sx={{ width: { xs: 320, sm: 560, md: 680, lg: 900 }, margin: 'auto', mt: .5, mb: .5 }}> |
| | | <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="h6" gutterBottom align="left" sx={{ |
| | | <Typography variant="subtitle2" gutterBottom align="left" sx={{ |
| | | maxWidth: { xs: '140px', sm: '220px', md: '300px', lg: '360px' }, |
| | | whiteSpace: 'nowrap', |
| | | overflow: 'hidden', |
| | |
| | | }}> |
| | | {Common.camelToPascalWithSpaces(translate('table.field.sta.staNo'))}: {record.staNo} |
| | | </Typography> |
| | | <Typography variant="h6" gutterBottom align="right" > |
| | | ID: {record.id} |
| | | </Typography> |
| | | </Grid> |
| | | </Grid> |
| | | <Grid container spacing={2}> |
| | | <Grid item xs={12} container alignContent="flex-end"> |
| | | <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={16}> </Box> |
| | | |
| | | <Box height={12}> </Box> |
| | | |
| | | <Grid container spacing={2}> |
| | | {INFO_FIELDS.map(({ labelKey, valueKey }) => ( |
| | | <Grid item xs={12} sm={6} md={4} key={labelKey}> |
| | |
| | | |
| | | <Divider sx={{ my: 2 }} /> |
| | | |
| | | <Typography variant="subtitle2" color="textSecondary" gutterBottom> |
| | | {translate('common.field.status')} / {translate('table.field.sta.staNo')} |
| | | </Typography> |
| | | {/* <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}> |
| | |
| | | </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.name')}</TableCell> |
| | | <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.agvId')}</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={7}> |
| | | <TableCell colSpan={RESERVE_COLUMN_COUNT}> |
| | | <Box display="flex" alignItems="center" gap={1}> |
| | | <CircularProgress size={16} /> |
| | | <Typography variant="body2" color="textSecondary"> |
| | |
| | | </TableCell> |
| | | </TableRow> |
| | | )} |
| | | {!isReservesLoading && reserves.length === 0 && ( |
| | | {!isReservesLoading && filteredReserves.length === 0 && ( |
| | | <TableRow> |
| | | <TableCell colSpan={7}> |
| | | <TableCell colSpan={RESERVE_COLUMN_COUNT}> |
| | | <Typography variant="body2" color="textSecondary"> |
| | | {translate('ra.navigation.no_results')} |
| | | </Typography> |
| | | </TableCell> |
| | | </TableRow> |
| | | )} |
| | | {reserves.map((reserve) => ( |
| | | {filteredReserves.map((reserve) => ( |
| | | <TableRow key={reserve.id}> |
| | | <TableCell>{reserve.name || '-'}</TableCell> |
| | | <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>{reserve.agvId$ || reserve.agvId || '-'}</TableCell> |
| | | <TableCell>{formatDateTime(reserve.expireTime)}</TableCell> |
| | | <TableCell>{formatDateTime(reserve.waitingAt)}</TableCell> |
| | | <TableCell>{formatDateTime(reserve.confirmedAt)}</TableCell> |
| | | <TableCell>{reserve.uniqKey || '-'}</TableCell> |
| | | </TableRow> |
| | | ))} |
| | | </TableBody> |
| | |
| | | <Typography variant="caption" color="textSecondary"> |
| | | {Common.camelToPascalWithSpaces(translate(labelKey))} |
| | | </Typography> |
| | | <Typography variant="body2" fontWeight={600}> |
| | | <Typography variant="body2" fontWeight={400}> |
| | | {formatInfoValue(value)} |
| | | </Typography> |
| | | </Stack> |
| | |
| | | height: 14, |
| | | borderRadius: '50%', |
| | | backgroundColor: color, |
| | | boxShadow: `0 0 6px ${color}`, |
| | | boxShadow: `0 0 3px ${color}`, |
| | | border: '1px solid rgba(0,0,0,0.12)' |
| | | }} /> |
| | | <Typography variant="body2"> |
| | |
| | | if (isFalsy(value)) { |
| | | return '#9e9e9e'; |
| | | } |
| | | return '#ff9800'; |
| | | return '#d40000ff'; |
| | | }; |
| | | |
| | | const isTruthy = (value) => { |
| | |
| | | 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'); |
| | | |
| | |
| | | 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) { |