| | |
| | | import React, { useState, useRef, useEffect, useMemo } from "react"; |
| | | import { Box, Card, CardContent, Grid, Typography, Tooltip } from '@mui/material'; |
| | | import React, { useState, useEffect } from "react"; |
| | | import { |
| | | Box, |
| | | Card, |
| | | CardContent, |
| | | Grid, |
| | | Typography, |
| | | Table, |
| | | TableBody, |
| | | TableCell, |
| | | TableHead, |
| | | TableRow, |
| | | Divider, |
| | | CircularProgress, |
| | | Stack, |
| | | } from '@mui/material'; |
| | | import { |
| | | useTranslate, |
| | | useRecordContext, |
| | | useDataProvider, |
| | | } from 'react-admin'; |
| | | import { format } from 'date-fns'; |
| | | import PanelTypography from "../components/PanelTypography"; |
| | | 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 StaPanel = () => { |
| | | const record = useRecordContext(); |
| | | if (!record) return null; |
| | | const translate = useTranslate(); |
| | | const dataProvider = useDataProvider(); |
| | | const [reserves, setReserves] = useState([]); |
| | | const [isReservesLoading, setIsReservesLoading] = useState(false); |
| | | |
| | | 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={{ width: { xs: 300, sm: 500, md: 600, lg: 800 }, margin: 'auto' }}> |
| | | <Card sx={{ width: { xs: 320, sm: 560, md: 680, lg: 900 }, 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={{ |
| | | maxWidth: { xs: '100px', sm: '180px', md: '260px', lg: '360px' }, |
| | | 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> |
| | | {/* inherit, primary, secondary, textPrimary, textSecondary, error */} |
| | | <Typography variant="h6" gutterBottom align="right" > |
| | | ID: {record.id} |
| | | </Typography> |
| | |
| | | <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} |
| | | {Common.camelToPascalWithSpaces(translate('common.field.memo'))}:{record.memo || '-'} |
| | | </Typography> |
| | | </Grid> |
| | | </Grid> |
| | | <Box height={20}> </Box> |
| | | <Box height={16}> </Box> |
| | | <Grid container spacing={2}> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.uuid" |
| | | title="table.field.sta.uuid" |
| | | property={record.uuid} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.zoneId" |
| | | title="table.field.sta.zoneId" |
| | | property={record.zoneId$} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.staNo" |
| | | property={record.staNo} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.name" |
| | | property={record.name} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.staType" |
| | | title="table.field.sta.staType" |
| | | property={record.staType$} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.code" |
| | | title="table.field.sta.code" |
| | | property={record.code$} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.offset" |
| | | title="table.field.sta.capacity" |
| | | property={record.capacity} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.offset" |
| | | property={record.offset} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.autoing" |
| | | property={record.autoing} |
| | | title="table.field.sta.rsvInCnt" |
| | | property={record.rsvInCnt} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.loading" |
| | | property={record.loading} |
| | | title="table.field.sta.rsvOutCnt" |
| | | property={record.rsvOutCnt} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.inEnable" |
| | | property={record.inEnable} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.outEnable" |
| | | property={record.outEnable} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.zpallet" |
| | | property={record.zpallet} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.staSts" |
| | | title="table.field.sta.staSts" |
| | | property={record.staSts$} |
| | | /> |
| | | </Grid> |
| | | |
| | | <Grid item xs={6}> |
| | | <PanelTypography |
| | | title="table.field.sta.zpallet" |
| | | property={record.zpallet} |
| | | /> |
| | | </Grid> |
| | | </Grid> |
| | | |
| | | <Divider sx={{ my: 2 }} /> |
| | | |
| | | <Typography variant="subtitle2" color="textSecondary" gutterBottom> |
| | | {translate('common.field.status')} / {translate('table.field.sta.staNo')} |
| | | </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> |
| | | <Table size="small"> |
| | | <TableHead> |
| | | <TableRow> |
| | | <TableCell>{translate('table.field.staReserve.name')}</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.waitingAt')}</TableCell> |
| | | <TableCell>{translate('table.field.staReserve.confirmedAt')}</TableCell> |
| | | </TableRow> |
| | | </TableHead> |
| | | <TableBody> |
| | | {isReservesLoading && ( |
| | | <TableRow> |
| | | <TableCell colSpan={7}> |
| | | <Box display="flex" alignItems="center" gap={1}> |
| | | <CircularProgress size={16} /> |
| | | <Typography variant="body2" color="textSecondary"> |
| | | {translate('common.loading', { _: 'Loading...' })} |
| | | </Typography> |
| | | </Box> |
| | | </TableCell> |
| | | </TableRow> |
| | | )} |
| | | {!isReservesLoading && reserves.length === 0 && ( |
| | | <TableRow> |
| | | <TableCell colSpan={7}> |
| | | <Typography variant="body2" color="textSecondary"> |
| | | No reservations |
| | | </Typography> |
| | | </TableCell> |
| | | </TableRow> |
| | | )} |
| | | {reserves.map((reserve) => ( |
| | | <TableRow key={reserve.id}> |
| | | <TableCell>{reserve.name || '-'}</TableCell> |
| | | <TableCell>{reserve.type || '-'}</TableCell> |
| | | <TableCellRight>{reserve.qty ?? '-'}</TableCellRight> |
| | | <TableCell>{reserve.state || '-'}</TableCell> |
| | | <TableCell>{reserve.agvId$ || reserve.agvId || '-'}</TableCell> |
| | | <TableCell>{formatDateTime(reserve.waitingAt)}</TableCell> |
| | | <TableCell>{formatDateTime(reserve.confirmedAt)}</TableCell> |
| | | </TableRow> |
| | | ))} |
| | | </TableBody> |
| | | </Table> |
| | | </Box> |
| | | </CardContent> |
| | | </Card > |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | 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 6px ${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 '#ff9800'; |
| | | }; |
| | | |
| | | 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 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; |