소스 검색

Merge branch 'MergeProblem1' of http://svn.2fi-solutions.com:8300/derek/FPSMS-frontend into MergeProblem1

reset-do-picking-order
CANCERYS\kw093 2 주 전
부모
커밋
66b8912ff0
6개의 변경된 파일325개의 추가작업 그리고 10개의 파일을 삭제
  1. +6
    -0
      src/app/api/dashboard/actions.ts
  2. +3
    -3
      src/components/DashboardPage/DashboardPage.tsx
  3. +287
    -0
      src/components/DashboardPage/goodsReceiptStatus/GoodsReceiptStatusNew.tsx
  4. +10
    -5
      src/config/reportConfig.ts
  5. +9
    -1
      src/i18n/en/dashboard.json
  6. +10
    -1
      src/i18n/zh/dashboard.json

+ 6
- 0
src/app/api/dashboard/actions.ts 파일 보기

@@ -193,12 +193,18 @@ export const testing = cache(async (queryParams?: Record<string, any>) => {

export interface GoodsReceiptStatusRow {
supplierId: number | null;
supplierCode: string | null;
supplierName: string;
purchaseOrderCode: string | null;
statistics: string;
expectedNoOfDelivery: number;
noOfOrdersReceivedAtDock: number;
noOfItemsInspected: number;
noOfItemsWithIqcIssue: number;
noOfItemsCompletedPutAwayAtStore: number;
// When true, this PO should be hidden from the dashboard table,
// but still counted in the overall statistics (訂單已處理).
hideFromDashboard?: boolean;
}

export const fetchGoodsReceiptStatus = cache(async (date?: string) => {


+ 3
- 3
src/components/DashboardPage/DashboardPage.tsx 파일 보기

@@ -15,7 +15,7 @@ import OrderCompletionChart from "./chart/OrderCompletionChart";
import { EscalationResult } from "@/app/api/escalation";
import EscalationLogTable from "./escalation/EscalationLogTable";
import { TruckScheduleDashboard } from "./truckSchedule";
import { GoodsReceiptStatus } from "./goodsReceiptStatus";
import GoodsReceiptStatusNew from "./goodsReceiptStatus/GoodsReceiptStatusNew";
import { CardFilterContext } from "../CollapsibleCard/CollapsibleCard";

interface TabPanelProps {
@@ -85,7 +85,7 @@ const DashboardPage: React.FC<Props> = ({
aria-label="dashboard tabs"
>
<Tab label={t("Truck Schedule Dashboard")} id="dashboard-tab-0" aria-controls="dashboard-tabpanel-0" />
<Tab label={t("Goods Receipt Status")} id="dashboard-tab-1" aria-controls="dashboard-tabpanel-1" />
<Tab label={t("Goods Receipt Status New")} id="dashboard-tab-1" aria-controls="dashboard-tabpanel-1" />
<Tab
label={`${t("Responsible Escalation List")} (${t("pending")} : ${
getPendingLog().length > 0 ? getPendingLog().length : t("No")})`}
@@ -99,7 +99,7 @@ const DashboardPage: React.FC<Props> = ({
<TruckScheduleDashboard />
</TabPanel>
<TabPanel value={currentTab} index={1}>
<GoodsReceiptStatus />
<GoodsReceiptStatusNew />
</TabPanel>
<TabPanel value={currentTab} index={2}>
<CardFilterContext.Provider value={{


+ 287
- 0
src/components/DashboardPage/goodsReceiptStatus/GoodsReceiptStatusNew.tsx 파일 보기

@@ -0,0 +1,287 @@
"use client";

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
CircularProgress,
Button,
Chip
} from '@mui/material';
import { useTranslation } from 'react-i18next';
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 { fetchGoodsReceiptStatusClient, type GoodsReceiptStatusRow } from '@/app/api/dashboard/client';

const REFRESH_MS = 15 * 60 * 1000;

const GoodsReceiptStatusNew: React.FC = () => {
const { t } = useTranslation("dashboard");
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs());
const [data, setData] = useState<GoodsReceiptStatusRow[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [lastUpdated, setLastUpdated] = useState<dayjs.Dayjs | null>(null);
const [screenCleared, setScreenCleared] = useState<boolean>(false);

const loadData = useCallback(async () => {
if (screenCleared) return;
try {
setLoading(true);
const dateParam = selectedDate.format('YYYY-MM-DD');
const result = await fetchGoodsReceiptStatusClient(dateParam);
setData(result ?? []);
setLastUpdated(dayjs());
} catch (error) {
console.error('Error fetching goods receipt status:', error);
setData([]);
} finally {
setLoading(false);
}
}, [selectedDate, screenCleared]);

useEffect(() => {
if (screenCleared) return;
loadData();
const refreshInterval = setInterval(() => {
loadData();
}, REFRESH_MS);
return () => clearInterval(refreshInterval);
}, [loadData, screenCleared]);


const selectedDateLabel = useMemo(() => {
return selectedDate.format('YYYY-MM-DD');
}, [selectedDate]);

// Sort rows by supplier code alphabetically (A -> Z)
const sortedData = useMemo(() => {
return [...data].sort((a, b) => {
const codeA = (a.supplierCode || '').toUpperCase();
const codeB = (b.supplierCode || '').toUpperCase();
if (codeA < codeB) return -1;
if (codeA > codeB) return 1;
return 0;
});
}, [data]);

const totalStatistics = useMemo(() => {
// Overall statistics should count ALL POs, including those hidden from the table
const totalReceived = sortedData.reduce((sum, row) => sum + (row.noOfOrdersReceivedAtDock || 0), 0);
const totalExpected = sortedData.reduce((sum, row) => sum + (row.expectedNoOfDelivery || 0), 0);
return { received: totalReceived, expected: totalExpected };
}, [sortedData]);

type StatusKey = 'pending' | 'receiving' | 'accepted';

const getStatusKey = useCallback((row: GoodsReceiptStatusRow): StatusKey => {
// Only when the whole PO is processed (all items finished IQC and PO completed)
// should we treat it as "accepted" (已收貨).
if (row.noOfOrdersReceivedAtDock === 1) {
return 'accepted';
}

// If some items have been inspected or put away but the order is not fully processed,
// treat as "receiving" / "processing".
if ((row.noOfItemsInspected ?? 0) > 0 || (row.noOfItemsCompletedPutAwayAtStore ?? 0) > 0) {
return 'receiving';
}

// Otherwise, nothing has started yet -> "pending".
return 'pending';
}, []);

const renderStatusChip = useCallback((row: GoodsReceiptStatusRow) => {
const statusKey = getStatusKey(row);
const label = t(statusKey);

// Color mapping: pending -> red, receiving -> yellow, accepted -> default/green-ish
const color =
statusKey === 'pending'
? 'error'
: statusKey === 'receiving'
? 'warning'
: 'success';

return (
<Chip
label={label}
color={color}
size="small"
sx={{
minWidth: 64,
fontWeight: 500,
...(statusKey === 'pending'
? {
bgcolor: 'error.light',
color: 'common.white',
}
: {}),
}}
/>
);
}, [getStatusKey, t]);

if (screenCleared) {
return (
<Card sx={{ mb: 2 }}>
<CardContent>
<Stack direction="row" spacing={2} justifyContent="space-between" alignItems="center">
<Typography variant="body2" color="text.secondary">
{t("Screen cleared")}
</Typography>
<Button variant="contained" onClick={() => setScreenCleared(false)}>
{t("Restore Screen")}
</Button>
</Stack>
</CardContent>
</Card>
);
}

return (
<Card sx={{ mb: 2 }}>
<CardContent>
{/* Header */}
<Stack direction="row" spacing={2} sx={{ mb: 2 }} alignItems="center" flexWrap="wrap">
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{t("Date")}:
</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
value={selectedDate}
onChange={(value) => {
if (!value) return;
setSelectedDate(value);
}}
slotProps={{
textField: {
size: "small",
sx: { minWidth: 160 }
}
}}
/>
</LocalizationProvider>
<Typography variant="body2" sx={{ ml: 1 }}>
訂單已處理: {totalStatistics.received}/{totalStatistics.expected}
</Typography>
</Stack>

<Box sx={{ flexGrow: 1 }} />

<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'}
</Typography>

<Button variant="outlined" color="inherit" onClick={() => setScreenCleared(true)}>
{t("Exit Screen")}
</Button>
</Stack>

{/* Table */}
<Box sx={{ mt: 2 }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table
size="small"
sx={{
minWidth: 560,
tableLayout: 'fixed',
}}
>
<TableHead>
<TableRow sx={{ backgroundColor: 'grey.100' }}>
<TableCell sx={{ fontWeight: 600, width: '32%', padding: '4px 6px' }}>{t("Supplier")}</TableCell>
<TableCell sx={{ fontWeight: 600, width: '30%', padding: '4px 6px' }}>{t("Purchase Order Code")}</TableCell>
<TableCell sx={{ fontWeight: 600, width: '14%', padding: '4px 4px' }}>{t("Status")}</TableCell>
<TableCell sx={{ fontWeight: 600, width: '24%', padding: '4px 4px' }} align="right">{t("No. of Items with IQC Issue")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedData.length === 0 ? (
<TableRow>
<TableCell colSpan={4} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")} ({selectedDateLabel})
</Typography>
</TableCell>
</TableRow>
) : (
sortedData
.filter((row) => !row.hideFromDashboard) // hide completed/rejected POs from table only
.map((row, index) => (
<TableRow
key={`${row.supplierId ?? 'na'}-${index}`}
sx={{
'&:hover': { backgroundColor: 'grey.50' }
}}
>
<TableCell sx={{ padding: '4px 6px' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography
component="span"
variant="body2"
sx={{
fontWeight: 500,
minWidth: '60px'
}}
>
{row.supplierCode || '-'}
</Typography>
<Typography
component="span"
variant="body2"
sx={{ color: 'text.secondary',ml: 0.5, mr: 1 }}
>
-
</Typography>
<Typography
component="span"
variant="body2"
>
{row.supplierName || '-'}
</Typography>
</Box>
</TableCell>
<TableCell sx={{ padding: '4px 6px' }}>
{row.purchaseOrderCode || '-'}
</TableCell>
<TableCell sx={{ padding: '4px 4px' }}>
{renderStatusChip(row)}
</TableCell>
<TableCell sx={{ padding: '4px 6px' }} align="right">
{row.noOfItemsWithIqcIssue ?? 0}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</CardContent>
</Card>
);
};

export default GoodsReceiptStatusNew;
4

+ 10
- 5
src/config/reportConfig.ts 파일 보기

@@ -115,10 +115,6 @@ export const REPORTS: ReportDefinition[] = [
title: "庫存結餘報告",
apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-balance`,
fields: [
{ label: "最後入倉日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false },
{ label: "最後入倉日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false },
{ label: "最後出倉日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false },
{ label: "最後出倉日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false },
{ label: "物料編號 Item Code", name: "itemCode", type: "text", required: false},
]
},
@@ -129,8 +125,12 @@ export const REPORTS: ReportDefinition[] = [
fields: [
{ label: "出貨日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false },
{ label: "出貨日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false },
{ label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" },
{ label: "物料編號 Item Code", name: "itemCode", type: "text", required: false},
{ label: "提料人 Handler", name: "handler", type: "select", required: false,
multiple: true,
dynamicOptions: true,
dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/fg-stock-out-traceability-handlers`,
options: [] },
]
},

@@ -151,6 +151,11 @@ export const REPORTS: ReportDefinition[] = [
{ label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false },
{ label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false },
{ label: "物料編號 Item Code", name: "itemCode", type: "text", required: false},
{ label: "提料人 Handler", name: "handler", type: "select", required: false,
multiple: true,
dynamicOptions: true,
dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/material-stock-out-traceability-handlers`,
options: [] },
]
},
{


+ 9
- 1
src/i18n/en/dashboard.json 파일 보기

@@ -103,5 +103,13 @@
"Column 1": "Column 1",
"Column 2": "Column 2",
"Column 3": "Column 3",
"No data available": "No data available"
"No data available": "No data available",
"Supplier Code": "Supplier Code",
"Supplier Name": "Supplier Name",
"Purchase Order Code": "Purchase Order Code",
"Statistics": "Statistics",
"Show Supplier Code": "Show Supplier Code",
"Show Purchase Order Codes": "Show Purchase Order Codes",
"x/y orders received": "x/y orders received",
"Goods Receipt Status New": "Goods Receipt Status"
}

+ 10
- 1
src/i18n/zh/dashboard.json 파일 보기

@@ -103,5 +103,14 @@
"Column 1": "欄位1",
"Column 2": "欄位2",
"Column 3": "欄位3",
"No data available": "暫無資料"
"No data available": "暫無資料",
"Supplier Code": "供應商編號",
"Supplier Name": "供應商名稱",
"Purchase Order Code": "採購訂單編號",
"Statistics": "統計",
"Show Supplier Code": "顯示供應商編號",
"Show Purchase Order Codes": "顯示採購訂單編號",
"x/y orders received": "x/y張單已處理",
"Goods Receipt Status New": "採購單接收狀態",
"Status": "狀態"
}

불러오는 중...
취소
저장