Преглед изворни кода

Merge branch 'MergeProblem1' of https://git.2fi-solutions.com/derek/FPSMS-frontend into MergeProblem1

MergeProblem1
kelvin.yau пре 12 часа
родитељ
комит
de25934531
8 измењених фајлова са 129 додато и 27 уклоњено
  1. +1
    -1
      src/app/(main)/report/page.tsx
  2. +6
    -0
      src/app/api/jo/actions.ts
  3. +25
    -7
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  4. +49
    -9
      src/components/InventorySearch/LotLabelPrintModal.tsx
  5. +8
    -2
      src/components/ProductionProcess/ProductionProcessList.tsx
  6. +36
    -7
      src/components/ProductionProcess/ProductionProcessPage.tsx
  7. +2
    -1
      src/components/PutAwayScan/PutAwayModal.tsx
  8. +2
    -0
      src/i18n/zh/common.json

+ 1
- 1
src/app/(main)/report/page.tsx Прегледај датотеку

@@ -504,7 +504,7 @@ export default function ReportPage() {
setLoading={setLoading}
reportTitle={currentReport.title}
/>
) : currentReport.id === 'rep-013' || currentReport.id === 'rep-009' || currentReport.id === 'rep-012' ? (
) : currentReport.id === 'rep-013' || currentReport.id === 'rep-009' || currentReport.id === 'rep-012' || currentReport.id === 'rep-004' || currentReport.id === 'rep-007' || currentReport.id === 'rep-008' || currentReport.id === 'rep-011' ? (
<>
<Button
variant="contained"


+ 6
- 0
src/app/api/jo/actions.ts Прегледај датотеку

@@ -352,6 +352,8 @@ export interface AllJoborderProductProcessInfoResponse {
uom: string;
isDrink?: boolean | null;
stockInLineId: number;
/** Stock-in-line current status (e.g. receiving/received/partially_completed/completed/rejected). */
stockInLineStatus?: string | null;
jobOrderCode: string;
productProcessLineCount: number;
FinishedProductProcessLineCount: number;
@@ -798,6 +800,8 @@ export const fetchJoborderProductProcessesPage = cache(async (params: {
qcReady?: boolean | null;
type?: string | null;
includePutaway?: boolean | null;
/** all | completed | notCompleted */
putawayStatus?: string | null;
page?: number;
size?: number;
}) => {
@@ -808,6 +812,7 @@ export const fetchJoborderProductProcessesPage = cache(async (params: {
bomIds,
qcReady,
includePutaway,
putawayStatus,
type,
page = 0,
size = 50,
@@ -825,6 +830,7 @@ export const fetchJoborderProductProcessesPage = cache(async (params: {
if (includePutaway !== undefined && includePutaway !== null) {
queryParts.push(`includePutaway=${includePutaway}`);
}
if (putawayStatus) queryParts.push(`putawayStatus=${encodeURIComponent(putawayStatus)}`);
queryParts.push(`page=${page}`);
queryParts.push(`size=${size}`);



+ 25
- 7
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Прегледај датотеку

@@ -77,7 +77,6 @@ import QrCodeIcon from "@mui/icons-material/QrCode";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { AUTH } from "@/authorities";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
import GoodPickExecutionForm from "./GoodPickExecutionForm";
import FGPickOrderCard from "./FGPickOrderCard";
@@ -687,8 +686,6 @@ const PickExecution: React.FC<Props> = ({
const { t } = useTranslation("pickOrder");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
const abilities = session?.abilities ?? session?.user?.abilities ?? [];
const isAdmin = abilities.some((a) => String(a).trim() === AUTH.ADMIN);
const [doPickOrderDetail, setDoPickOrderDetail] =
useState<DoPickOrderDetail | null>(null);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(
@@ -783,6 +780,25 @@ const PickExecution: React.FC<Props> = ({
useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
const lotFloorPrefixFilter = useMemo(() => {
const storeId = String(fgPickOrders?.[0]?.storeId ?? "")
.trim()
.toUpperCase()
.replace(/\s/g, "");
// e.g. "2/F" -> "2F-", "4/F" -> "4F-"
const floorKey = storeId.replace(/\//g, "");
return floorKey ? `${floorKey}-` : "";
}, [fgPickOrders]);
const defaultLabelPrinterName = useMemo(() => {
const storeId = String(fgPickOrders?.[0]?.storeId ?? "")
.trim()
.toUpperCase()
.replace(/\s/g, "");
const floorKey = storeId.replace(/\//g, "");
if (floorKey === "2F") return "Label機 2F A+B";
if (floorKey === "4F") return "Label機 4F 乾貨 C, D";
return undefined;
}, [fgPickOrders]);
// Add these missing state variables after line 352
const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
// Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
@@ -1690,7 +1706,7 @@ const PickExecution: React.FC<Props> = ({
return;
}

if (switchedToUnavailable && isAdmin) {
if (switchedToUnavailable) {
const itemId = Number(sel?.itemId ?? exp?.itemId);
const stockInLineId = Number(newStockInLineId);
if (Number.isFinite(itemId) && Number.isFinite(stockInLineId)) {
@@ -1745,7 +1761,6 @@ const PickExecution: React.FC<Props> = ({
resetScan,
clearLotConfirmationState,
t,
isAdmin,
],
);

@@ -2138,7 +2153,7 @@ const PickExecution: React.FC<Props> = ({
const byLotId = new Map<number, any>();
const byLotNo = new Map<string, any[]>();
const byStockInLineId = new Map<number, any[]>();
// Cache active lots separately to avoid filtering on every scan
const activeLotsByItemId = new Map<number, any[]>();
const rejectedStatuses = new Set(["rejected"]);
@@ -5055,9 +5070,12 @@ const PickExecution: React.FC<Props> = ({
setLotLabelPrintReminderText(null);
}}
initialPayload={lotLabelPrintInitialPayload}
defaultPrinterName="Label機 2F A+B"
defaultPrinterName={defaultLabelPrinterName}
hideScanSection
reminderText={lotLabelPrintReminderText ?? undefined}
statusTitleText="此批號的已用完/已過期"
warehouseCodePrefixFilter={lotFloorPrefixFilter}
hideTriggeredLot
/>
</FormProvider>
</TestQrCodeProvider>


+ 49
- 9
src/components/InventorySearch/LotLabelPrintModal.tsx Прегледај датотеку

@@ -75,6 +75,12 @@ export interface LotLabelPrintModalProps {
hideScanSection?: boolean;
/** 額外提醒(顯示在最上方) */
reminderText?: string;
/** 額外標題(顯示在最上方,reminderText 之下) */
statusTitleText?: string;
/** 只顯示特定倉位前綴(例如 "2F-") */
warehouseCodePrefixFilter?: string;
/** 不顯示觸發視窗的批號(analysis.scanned) */
hideTriggeredLot?: boolean;
}

function safeParseScanPayload(raw: string): ScanPayload | null {
@@ -115,6 +121,9 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({
defaultPrinterName,
hideScanSection,
reminderText,
statusTitleText,
warehouseCodePrefixFilter,
hideTriggeredLot,
}) => {
const scanInputRef = useRef<HTMLInputElement | null>(null);
const [scanInput, setScanInput] = useState("");
@@ -126,6 +135,7 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({

const [analysisLoading, setAnalysisLoading] = useState(false);
const [analysis, setAnalysis] = useState<QrCodeAnalysisResponse | null>(null);
const [lastPayload, setLastPayload] = useState<ScanPayload | null>(null);

const [printQty, setPrintQty] = useState(1);
const [printingLotLineId, setPrintingLotLineId] = useState<number | null>(
@@ -234,6 +244,7 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({
return;
}

setLastPayload(payload);
setScanError(null);
setAnalysisLoading(true);
try {
@@ -279,6 +290,19 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({
await analyzePayload(payload);
}, [scanInput, analyzePayload]);

const handleRefreshLots = useCallback(async () => {
const payload = lastPayload ?? safeParseScanPayload(scanInput.trim());
if (!payload) {
setSnackbar({
open: true,
message: "請先掃碼或查詢一次,才可刷新批號清單。",
severity: "info",
});
return;
}
await analyzePayload(payload);
}, [analyzePayload, lastPayload, scanInput]);

useEffect(() => {
if (!open) return;
if (!initialPayload) return;
@@ -310,14 +334,22 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({
: null;

const merged = [
...(scannedLot ? [scannedLot] : []),
...(!hideTriggeredLot && scannedLot ? [scannedLot] : []),
...list
.filter((x) => x.inventoryLotLineId !== scannedLotLineId)
.map((x) => ({ ...x, _scanned: false as const })),
];

return merged;
}, [analysis]);
}, [analysis, hideTriggeredLot]);

const filteredLots = useMemo(() => {
const prefix = String(warehouseCodePrefixFilter ?? "").trim();
if (!prefix) return availableLots;
return availableLots.filter((lot) =>
String(lot.warehouseCode ?? "").startsWith(prefix),
);
}, [availableLots, warehouseCodePrefixFilter]);

const selectedPrinter = useMemo(() => {
if (selectedPrinterId === "") return null;
@@ -390,6 +422,14 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({
<DialogTitle>批號標籤列印</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
{statusTitleText ? (
<Typography
variant="h6"
sx={{ fontWeight: 800, color: "error.main" }}
>
{statusTitleText}
</Typography>
) : null}
{reminderText ? (
<Alert severity="warning">{reminderText}</Alert>
) : null}
@@ -485,13 +525,13 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({

<Button
variant="outlined"
onClick={() => void loadPrinters()}
disabled={printersLoading}
onClick={() => void handleRefreshLots()}
disabled={analysisLoading}
>
{printersLoading ? (
{analysisLoading ? (
<CircularProgress size={18} />
) : (
"重新載入印表機"
"刷新批號清單"
)}
</Button>

@@ -512,13 +552,13 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({
品號:{analysis.itemCode} {analysis.itemName}
</Typography>

{availableLots.length === 0 ? (
{filteredLots.length === 0 ? (
<Alert severity="warning">
找不到可用批號(availableQty &gt; 0)。
找不到該樓層有可用批號(availableQty &gt; 0)。
</Alert>
) : (
<Stack spacing={1}>
{availableLots.map((lot) => {
{filteredLots.map((lot) => {
const isPrinting =
printingLotLineId === lot.inventoryLotLineId;
const loc = String(lot.warehouseCode ?? "").trim();


+ 8
- 2
src/components/ProductionProcess/ProductionProcessList.tsx Прегледај датотеку

@@ -60,6 +60,9 @@ interface ProductProcessListProps {
onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void;
printerCombo: PrinterCombo[];
qcReady: boolean;
includePutaway?: boolean | null;
/** all | completed | notCompleted */
putawayStatus?: string | null;
listPersistedState: ProductionProcessListPersistedState;
onListPersistedStateChange: React.Dispatch<
React.SetStateAction<ProductionProcessListPersistedState>
@@ -93,6 +96,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
printerCombo,
onSelectMatchingStock,
qcReady,
includePutaway,
putawayStatus,
listPersistedState,
onListPersistedStateChange,
}) => {
@@ -258,7 +263,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
itemCode: appliedSearch.itemCode,
jobOrderCode: appliedSearch.jobOrderCode,
qcReady,
includePutaway: qcReady ? true : null,
includePutaway: includePutaway ?? (qcReady ? true : null),
putawayStatus,
type: typeParam,
page,
size: PAGE_SIZE,
@@ -273,7 +279,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
} finally {
setLoading(false);
}
}, [listPersistedState, qcReady]);
}, [listPersistedState, qcReady, includePutaway, putawayStatus]);

useEffect(() => {
fetchProcesses();


+ 36
- 7
src/components/ProductionProcess/ProductionProcessPage.tsx Прегледај датотеку

@@ -36,7 +36,10 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
const [productionListState, setProductionListState] = useState(
createDefaultProductionProcessListPersistedState,
);
const [finishedQcListState, setFinishedQcListState] = useState(
const [waitingPutawayListState, setWaitingPutawayListState] = useState(
createDefaultProductionProcessListPersistedState,
);
const [putawayedListState, setPutawayedListState] = useState(
createDefaultProductionProcessListPersistedState,
);
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -199,7 +202,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo

<Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}>
<Tab label={t("Production Process")} />
<Tab label={t("Finished QC Job Orders")} />
<Tab label={t("Waiting QC Put Away Job Orders")} />
<Tab label={t("Put Awayed Job Orders")} />
<Tab label={t("Job Process Status Dashboard")} />
<Tab label={t("Operator KPI Dashboard")} />
<Tab label={t("Production Equipment Status Dashboard")} />
@@ -231,8 +235,10 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
<ProductionProcessList
printerCombo={printerCombo}
qcReady={true}
listPersistedState={finishedQcListState}
onListPersistedStateChange={setFinishedQcListState}
includePutaway={true}
putawayStatus="notCompleted"
listPersistedState={waitingPutawayListState}
onListPersistedStateChange={setWaitingPutawayListState}
onSelectProcess={(jobOrderId) => {
const id = jobOrderId ?? null;
if (id !== null) {
@@ -248,13 +254,36 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
}}
/>
)}
{tabIndex === 2 && (
<JobProcessStatus />
{tabIndex === 2 && (
<ProductionProcessList
printerCombo={printerCombo}
qcReady={true}
includePutaway={true}
putawayStatus="completed"
listPersistedState={putawayedListState}
onListPersistedStateChange={setPutawayedListState}
onSelectProcess={(jobOrderId) => {
const id = jobOrderId ?? null;
if (id !== null) {
setSelectedProcessId(id);
}
}}
onSelectMatchingStock={(jobOrderId, productProcessId, pickOrderId) => {
setSelectedMatchingStock({
jobOrderId: jobOrderId || 0,
productProcessId: productProcessId || 0,
pickOrderId: pickOrderId || 0,
});
}}
/>
)}
{tabIndex === 3 && (
<OperatorKpiDashboard />
<JobProcessStatus />
)}
{tabIndex === 4 && (
<OperatorKpiDashboard />
)}
{tabIndex === 5 && (
<EquipmentStatusDashboard />
)}
</Box>


+ 2
- 1
src/components/PutAwayScan/PutAwayModal.tsx Прегледај датотеку

@@ -128,6 +128,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
...defaultNewValue,
},
});
const { isSubmitting } = formProps.formState;
const errors = formProps.formState.errors;

useEffect(() => {
@@ -646,7 +647,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
},
}}
// onClick={formProps.handleSubmit()}
disabled={!verified || qtyError != ""}
disabled={!verified || qtyError != "" || isSubmitting}
>
{t("confirm putaway")}
</Button>


+ 2
- 0
src/i18n/zh/common.json Прегледај датотеку

@@ -12,6 +12,8 @@
"Please Select BOM": "請選擇 BOM",
"No Lot": "沒有批號",
"Select All": "全選",
"Waiting QC Put Away Job Orders": "待QC上架工單",
"Put Awayed Job Orders": "已上架工單",
"Loading BOM Detail...": "正在載入 BOM 明細…",
"Output Quantity": "使用數量",
"Process & Equipment": "製程與設備",


Loading…
Откажи
Сачувај