Ver a proveniência

update dashboard, job order list

MergeProblem1
CANCERYS\kw093 há 3 dias
ascendente
cometimento
0947fd181d
4 ficheiros alterados com 104 adições e 53 eliminações
  1. +6
    -3
      src/app/api/jo/actions.ts
  2. +62
    -22
      src/components/Jodetail/JoPickOrderList.tsx
  3. +18
    -13
      src/components/ProductionProcess/JobProcessStatus.tsx
  4. +18
    -15
      src/components/ProductionProcess/OperatorKpiDashboard.tsx

+ 6
- 3
src/app/api/jo/actions.ts Ver ficheiro

@@ -692,16 +692,19 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder
},
);
});
export const fetchAllJoPickOrders = cache(async (isDrink?: boolean | null, floor?: string | null) => {
// NOTE: Do NOT wrap in `cache()` because the list needs to reflect just-completed lines
// immediately when navigating back from JobPickExecution.
export const fetchAllJoPickOrders = async (isDrink?: boolean | null, floor?: string | null) => {
const params = new URLSearchParams();
if (isDrink !== undefined && isDrink !== null) params.set("isDrink", String(isDrink));
if (floor) params.set("floor", floor);
const query = params.toString() ? `?${params.toString()}` : "";
return serverFetchJson<AllJoPickOrderResponse[]>(
`${BASE_API_URL}/jo/AllJoPickOrder${query}`,
{ method: "GET" }
// Force re-fetch. This page reflects real-time pick completion state.
{ method: "GET", cache: "no-store" }
);
});
};
export const fetchProductProcessLineDetail = cache(async (lineId: number) => {
return serverFetchJson<JobOrderProcessLineDetailResponse>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`,


+ 62
- 22
src/components/Jodetail/JoPickOrderList.tsx Ver ficheiro

@@ -27,7 +27,7 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);
type PickOrderFilter = "all" | "drink" | "other";
const [filter, setFilter] = useState<PickOrderFilter>("all");
type FloorFilter = "ALL" | "2F" | "3F" | "4F";
type FloorFilter = "ALL" | "2F" | "3F" | "4F" | "NO_LOT";
const [floorFilter, setFloorFilter] = useState<FloorFilter>("ALL");
const fetchPickOrders = useCallback(async () => {
setLoading(true);
@@ -60,10 +60,7 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
onClick={() => {
setSelectedPickOrderId(undefined);
setSelectedJobOrderId(undefined);
}}
onClick={handleBackToList}
startIcon={<ArrowBackIcon />}
>
{t("Back to List")}
@@ -140,6 +137,13 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
>
4F
</Button>
<Button
variant={floorFilter === "NO_LOT" ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("NO_LOT")}
>
{t("No Lot")}
</Button>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total pick orders")}: {pickOrders.length}
@@ -163,8 +167,8 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
<Grid key={pickOrder.id} item xs={12} sm={6} md={4}>
<Card
sx={{
minHeight: 160,
maxHeight: 240,
minHeight: 180,
maxHeight: 280,
display: "flex",
flexDirection: "column",
}}
@@ -196,21 +200,57 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
<Typography variant="body2" color="text.secondary">
{t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName})
</Typography>
{pickOrder.floorPickCounts?.map(({ floor, finishedCount, totalCount }) => (
<Typography key={floor} variant="body2" color="text.secondary" component="span" sx={{ mr: 1 }}>
{floor}: {finishedCount}/{totalCount}
</Typography>
))}
{!!pickOrder.noLotPickCount && (
<Typography
key="NO_LOT"
variant="body2"
color="text.secondary"
component="span"
sx={{ mr: 1 }}
>
{t("No Lot")}: {pickOrder.noLotPickCount.finishedCount}/{pickOrder.noLotPickCount.totalCount}
</Typography>
{floorFilter === "ALL" ? (
<>
{pickOrder.floorPickCounts?.map(({ floor, finishedCount, totalCount }) => (
<Typography
key={floor}
variant="body2"
color="text.secondary"
component="span"
sx={{ mr: 1 }}
>
{floor}: {finishedCount}/{totalCount}
</Typography>
))}
{!!pickOrder.noLotPickCount && (
<Typography
key="NO_LOT"
variant="body2"
color="text.secondary"
component="span"
sx={{ mr: 1 }}
>
{t("No Lot")}: {pickOrder.noLotPickCount.finishedCount}/{pickOrder.noLotPickCount.totalCount}
</Typography>
)}
</>
) : floorFilter === "NO_LOT" ? (
!!pickOrder.noLotPickCount && (
<Typography
key="NO_LOT"
variant="body2"
color="text.secondary"
component="span"
sx={{ mr: 1 }}
>
{t("No Lot")}: {pickOrder.noLotPickCount.finishedCount}/{pickOrder.noLotPickCount.totalCount}
</Typography>
)
) : (
pickOrder.floorPickCounts
?.filter((c) => c.floor === floorFilter)
.map(({ floor, finishedCount, totalCount }) => (
<Typography
key={floor}
variant="body2"
color="text.secondary"
component="span"
sx={{ mr: 1 }}
>
{floor}: {finishedCount}/{totalCount}
</Typography>
))
)}
{typeof pickOrder.suggestedFailCount === "number" && pickOrder.suggestedFailCount > 0 && (
<Typography variant="body2" color="error" sx={{ mt: 0.5 }}>


+ 18
- 13
src/components/ProductionProcess/JobProcessStatus.tsx Ver ficheiro

@@ -19,9 +19,11 @@ import {
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs';
import { fetchJobProcessStatus, JobProcessStatusResponse } from '@/app/api/jo/actions';
import { arrayToDayjs } from '@/app/utils/formatUtil';
import { FormControl, Select, MenuItem } from "@mui/material";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes

const JobProcessStatus: React.FC = () => {
@@ -30,8 +32,8 @@ const JobProcessStatus: React.FC = () => {
const [loading, setLoading] = useState<boolean>(true);
const refreshCountRef = useRef<number>(0);
const [currentTime, setCurrentTime] = useState(dayjs());
const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD"));
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null);
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<Dayjs | null>(null);

// Update current time every second for countdown
useEffect(() => {
@@ -44,7 +46,7 @@ const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | nul
const loadData = useCallback(async () => {
setLoading(true);
try {
const result = await fetchJobProcessStatus(selectedDate);
const result = await fetchJobProcessStatus(selectedDate.format("YYYY-MM-DD"));
setData(result);
refreshCountRef.current += 1;
setLastDataRefreshTime(dayjs());
@@ -181,16 +183,19 @@ const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | nul

{/* Filters */}
<Stack direction="row" spacing={2} sx={{ mb: 3 }}>
<FormControl size="small" sx={{ minWidth: 160 }}>
<Select
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label={t("Date")}
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
>
<MenuItem value={dayjs().format("YYYY-MM-DD")}>今天</MenuItem>
<MenuItem value={dayjs().subtract(1, "day").format("YYYY-MM-DD")}>昨天</MenuItem>
<MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>前天</MenuItem>
</Select>
</FormControl>
onChange={(newValue) => {
if (newValue) setSelectedDate(newValue);
}}
format="YYYY-MM-DD"
slotProps={{
textField: { size: "small", sx: { minWidth: 160 } },
}}
/>
</LocalizationProvider>

<Box sx={{ flexGrow: 1 }} />
<Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}>


+ 18
- 15
src/components/ProductionProcess/OperatorKpiDashboard.tsx Ver ficheiro

@@ -14,13 +14,13 @@ import {
TableRow,
Paper,
Typography,
FormControl,
Select,
MenuItem,
Stack
} from "@mui/material";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import type { Dayjs } from "dayjs";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { fetchOperatorKpi, OperatorKpiResponse, OperatorKpiProcessInfo } from "@/app/api/jo/actions";
import { arrayToDayjs } from "@/app/utils/formatUtil";

@@ -30,10 +30,10 @@ const OperatorKpiDashboard: React.FC = () => {
const { t } = useTranslation(["common", "jo"]);
const [data, setData] = useState<OperatorKpiResponse[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD"));
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const refreshCountRef = useRef<number>(0);
const [now, setNow] = useState(dayjs());
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null);
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<Dayjs | null>(null);

const formatTime = (timeData: any): string => {
if (!timeData) return "-";
@@ -69,7 +69,7 @@ const OperatorKpiDashboard: React.FC = () => {
const loadData = useCallback(async () => {
setLoading(true);
try {
const result = await fetchOperatorKpi(selectedDate);
const result = await fetchOperatorKpi(selectedDate.format("YYYY-MM-DD"));
setData(result);
setLastDataRefreshTime(dayjs());
refreshCountRef.current += 1;
@@ -165,16 +165,19 @@ const OperatorKpiDashboard: React.FC = () => {

{/* Filters */}
<Stack direction="row" spacing={2} sx={{ mb: 3 }}>
<FormControl size="small" sx={{ minWidth: 160 }}>
<Select
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label={t("Date")}
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
>
<MenuItem value={dayjs().format("YYYY-MM-DD")}>{t("Today")}</MenuItem>
<MenuItem value={dayjs().subtract(1, "day").format("YYYY-MM-DD")}>{t("Yesterday")}</MenuItem>
<MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>{t("Two Days Ago")}</MenuItem>
</Select>
</FormControl>
onChange={(newValue) => {
if (newValue) setSelectedDate(newValue);
}}
format="YYYY-MM-DD"
slotProps={{
textField: { size: "small", sx: { minWidth: 160 } },
}}
/>
</LocalizationProvider>

<Box sx={{ flexGrow: 1 }} />
<Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}>


Carregando…
Cancelar
Guardar