Selaa lähdekoodia

Merge remote-tracking branch 'origin/MergeProblem1' into MergeProblem1

reset-do-picking-order
CANCERYS\kw093 1 viikko sitten
vanhempi
commit
1e346fa9b8
11 muutettua tiedostoa jossa 637 lisäystä ja 32 poistoa
  1. +23
    -0
      src/app/(main)/bagPrint/page.tsx
  2. +82
    -0
      src/app/api/bagPrint/actions.ts
  3. +7
    -0
      src/app/api/settings/bomWeighting/client.ts
  4. +445
    -0
      src/components/BagPrint/BagPrintSearch.tsx
  5. +20
    -1
      src/components/BomWeightingScoreTable/BomWeightingScoreTable.tsx
  6. +52
    -24
      src/components/BomWeightingTabs/BomWeightingTabs.tsx
  7. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  8. +1
    -1
      src/components/CreateItem/CreateItemWrapper.tsx
  9. +3
    -3
      src/components/NavigationContent/NavigationContent.tsx
  10. +2
    -2
      src/i18n/zh/common.json
  11. +1
    -1
      src/i18n/zh/project.json

+ 23
- 0
src/app/(main)/bagPrint/page.tsx Näytä tiedosto

@@ -0,0 +1,23 @@
import BagPrintSearch from "@/components/BagPrint/BagPrintSearch";
import { Stack, Typography } from "@mui/material";
import { Metadata } from "next";
import React from "react";

export const metadata: Metadata = {
title: "打袋機",
};

const BagPrintPage: React.FC = () => {
return (
<>
<Stack direction="row" justifyContent="space-between" flexWrap="wrap" rowGap={2}>
<Typography variant="h4" marginInlineEnd={2}>
打袋機
</Typography>
</Stack>
<BagPrintSearch />
</>
);
};

export default BagPrintPage;

+ 82
- 0
src/app/api/bagPrint/actions.ts Näytä tiedosto

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

import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";

export interface JobOrderListItem {
id: number;
code: string | null;
planStart: string | null;
itemCode: string | null;
itemName: string | null;
reqQty: number | null;
stockInLineId: number | null;
itemId: number | null;
lotNo: string | null;
}

export interface PrinterStatusRequest {
printerType: "dataflex" | "laser";
printerIp?: string;
printerPort?: number;
}

export interface PrinterStatusResponse {
connected: boolean;
message: string;
}

export interface OnPackQrDownloadRequest {
jobOrders: {
jobOrderId: number;
itemCode: string;
}[];
}

/**
* Fetch job orders by plan date from GET /py/job-orders.
* Client-side only; uses auth token from localStorage.
*/
export async function fetchJobOrders(planStart: string): Promise<JobOrderListItem[]> {
const url = `${NEXT_PUBLIC_API_URL}/py/job-orders?planStart=${encodeURIComponent(planStart)}`;
const res = await clientAuthFetch(url, { method: "GET" });
if (!res.ok) {
throw new Error(`Failed to fetch job orders: ${res.status}`);
}
return res.json();
}

export async function checkPrinterStatus(
request: PrinterStatusRequest,
): Promise<PrinterStatusResponse> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/check-printer`;
const res = await clientAuthFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});

const data = (await res.json()) as PrinterStatusResponse;
if (!res.ok) {
return data;
}

return data;
}

export async function downloadOnPackQrZip(
request: OnPackQrDownloadRequest,
): Promise<Blob> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr`;
const res = await clientAuthFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});

if (!res.ok) {
throw new Error((await res.text()) || "Download failed");
}

return res.blob();
}

+ 7
- 0
src/app/api/settings/bomWeighting/client.ts Näytä tiedosto

@@ -12,6 +12,13 @@ export interface UpdateBomWeightingScoreInputs {
remarks?: string;
}

export const fetchBomWeightingScoresClient = async (): Promise<BomWeightingScoreResult[]> => {
const response = await axiosInstance.get<BomWeightingScoreResult[]>(
`${NEXT_PUBLIC_API_URL}/bomWeightingScores`
);
return response.data;
};

export const updateBomWeightingScoreClient = async (
data: UpdateBomWeightingScoreInputs
): Promise<BomWeightingScoreResult> => {


+ 445
- 0
src/components/BagPrint/BagPrintSearch.tsx Näytä tiedosto

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

import React, { useCallback, useEffect, useState } from "react";
import {
Box,
Button,
FormControl,
InputLabel,
MenuItem,
Select,
Stack,
Typography,
Paper,
CircularProgress,
SelectChangeEvent,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Snackbar,
} from "@mui/material";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import Settings from "@mui/icons-material/Settings";
import Print from "@mui/icons-material/Print";
import Download from "@mui/icons-material/Download";
import { checkPrinterStatus, downloadOnPackQrZip, fetchJobOrders, JobOrderListItem } from "@/app/api/bagPrint/actions";
import dayjs from "dayjs";

// Light blue theme (matching Python Bag1)
const BG_TOP = "#E8F4FC";
const BG_LIST = "#D4E8F7";
const BG_ROW = "#C5E1F5";
const BG_ROW_SELECTED = "#6BB5FF";
const BG_STATUS_ERROR = "#FFCCCB";
const BG_STATUS_OK = "#90EE90";
const FG_STATUS_ERROR = "#B22222";
const FG_STATUS_OK = "#006400";

const PRINTER_OPTIONS = [
{ value: "dataflex", label: "打袋機 DataFlex" },
{ value: "laser", label: "激光機" },
];

const REFRESH_MS = 60 * 1000;
const PRINTER_CHECK_MS = 60 * 1000;
const PRINTER_RETRY_MS = 30 * 1000;
const SETTINGS_KEY = "bagPrint_settings";

const DEFAULT_SETTINGS = {
dabag_ip: "",
dabag_port: "3008",
laser_ip: "192.168.17.10",
laser_port: "45678",
};

function loadSettings(): typeof DEFAULT_SETTINGS {
if (typeof window === "undefined") return DEFAULT_SETTINGS;
try {
const s = localStorage.getItem(SETTINGS_KEY);
if (s) return { ...DEFAULT_SETTINGS, ...JSON.parse(s) };
} catch {}
return DEFAULT_SETTINGS;
}

function saveSettings(s: typeof DEFAULT_SETTINGS) {
if (typeof window === "undefined") return;
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(s));
} catch {}
}

function formatQty(val: number | null | undefined): string {
if (val == null) return "—";
try {
const n = Number(val);
if (Number.isInteger(n)) return n.toLocaleString();
return n.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 }).replace(/\.?0+$/, "");
} catch {
return String(val);
}
}

function getBatch(jo: JobOrderListItem): string {
return (jo.lotNo || "—").trim() || "—";
}

const BagPrintSearch: React.FC = () => {
const [planDate, setPlanDate] = useState(() => dayjs().format("YYYY-MM-DD"));
const [jobOrders, setJobOrders] = useState<JobOrderListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [connected, setConnected] = useState(false);
const [printer, setPrinter] = useState<string>("dataflex");
const [selectedId, setSelectedId] = useState<number | null>(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity?: "success" | "info" | "error" }>({ open: false, message: "" });
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [printerConnected, setPrinterConnected] = useState(false);
const [printerMessage, setPrinterMessage] = useState("列印機未連接");
const [downloadingOnPack, setDownloadingOnPack] = useState(false);

useEffect(() => {
setSettings(loadSettings());
}, []);

const loadJobOrders = useCallback(async (fromUserChange = false) => {
setLoading(true);
setError(null);
try {
const data = await fetchJobOrders(planDate);
setJobOrders(data);
setConnected(true);
if (fromUserChange) setSelectedId(null);
} catch (e) {
setError(e instanceof Error ? e.message : "連接不到服務器");
setConnected(false);
setJobOrders([]);
} finally {
setLoading(false);
}
}, [planDate]);

useEffect(() => {
loadJobOrders(true);
}, [planDate]);

useEffect(() => {
if (!connected) return;
const id = setInterval(() => loadJobOrders(false), REFRESH_MS);
return () => clearInterval(id);
}, [connected, loadJobOrders]);

const checkCurrentPrinter = useCallback(async () => {
try {
const request =
printer === "dataflex"
? {
printerType: "dataflex" as const,
printerIp: settings.dabag_ip,
printerPort: Number(settings.dabag_port || 3008),
}
: {
printerType: "laser" as const,
printerIp: settings.laser_ip,
printerPort: Number(settings.laser_port || 45678),
};

const result = await checkPrinterStatus(request);
setPrinterConnected(result.connected);
setPrinterMessage(result.message);
} catch (e) {
setPrinterConnected(false);
setPrinterMessage(e instanceof Error ? e.message : "列印機狀態檢查失敗");
}
}, [printer, settings]);

useEffect(() => {
checkCurrentPrinter();
}, [checkCurrentPrinter]);

useEffect(() => {
const intervalMs = printerConnected ? PRINTER_CHECK_MS : PRINTER_RETRY_MS;
const id = setInterval(() => {
checkCurrentPrinter();
}, intervalMs);

return () => clearInterval(id);
}, [printerConnected, checkCurrentPrinter]);

const goPrevDay = () => {
setPlanDate((d) => dayjs(d).subtract(1, "day").format("YYYY-MM-DD"));
};

const goNextDay = () => {
setPlanDate((d) => dayjs(d).add(1, "day").format("YYYY-MM-DD"));
};

const handlePrinterChange = (e: SelectChangeEvent<string>) => {
setPrinter(e.target.value);
};

const handleRowClick = (jo: JobOrderListItem) => {
setSelectedId(jo.id);
const batch = getBatch(jo);
const itemCode = jo.itemCode || "—";
const itemName = jo.itemName || "—";
setSnackbar({ open: true, message: `已點選:批次 ${batch} 品號 ${itemCode} ${itemName}`, severity: "info" });
// TODO: Actual printing would require backend API to proxy to printer (TCP/serial)
// For now, show info. Backend could add /py/print-dataflex, /py/print-label, /py/print-laser
};

const handleDownloadOnPackQr = async () => {
const onPackJobOrders = jobOrders
.map((jobOrder) => ({
jobOrderId: jobOrder.id,
itemCode: jobOrder.itemCode?.trim() || "",
}))
.filter((jobOrder) => jobOrder.itemCode.length > 0);

if (onPackJobOrders.length === 0) {
setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" });
return;
}

setDownloadingOnPack(true);
try {
const blob = await downloadOnPackQrZip({
jobOrders: onPackJobOrders,
});

const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `onpack_qr_${planDate}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);

setSnackbar({ open: true, message: "OnPack QR code ZIP 已下載", severity: "success" });
} catch (e) {
setSnackbar({
open: true,
message: e instanceof Error ? e.message : "下載 OnPack QR code 失敗",
severity: "error",
});
} finally {
setDownloadingOnPack(false);
}
};

return (
<Box sx={{ minHeight: "70vh", display: "flex", flexDirection: "column" }}>
{/* Top: date nav + printer + settings */}
<Paper sx={{ p: 2, mb: 2, backgroundColor: BG_TOP }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={2}>
<Stack direction="row" alignItems="center" spacing={2}>
<Button variant="outlined" startIcon={<ChevronLeft />} onClick={goPrevDay}>
前一天
</Button>
<TextField
type="date"
value={planDate}
onChange={(e) => setPlanDate(e.target.value)}
size="small"
sx={{ width: 160 }}
InputLabelProps={{ shrink: true }}
/>
<Button variant="outlined" endIcon={<ChevronRight />} onClick={goNextDay}>
後一天
</Button>
</Stack>
<Stack direction="row" alignItems="center" spacing={2}>
<Button variant="outlined" startIcon={<Settings />} onClick={() => setSettingsOpen(true)}>
設定
</Button>
<Box
sx={{
px: 1.5,
py: 0.75,
borderRadius: 1,
backgroundColor: printerConnected ? BG_STATUS_OK : BG_STATUS_ERROR,
color: printerConnected ? FG_STATUS_OK : FG_STATUS_ERROR,
fontWeight: 600,
whiteSpace: "nowrap",
}}
title={printerMessage}
>
列印機:
</Box>
<FormControl size="small" sx={{ minWidth: 180 }}>
<InputLabel>列印機</InputLabel>
<Select value={printer} label="列印機" onChange={handlePrinterChange}>
{PRINTER_OPTIONS.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
</Stack>
<Typography variant="body2" sx={{ mt: 1, color: "text.secondary" }}>
{printerMessage}
</Typography>
<Stack direction="row" sx={{ mt: 2 }}>
<Button
variant="contained"
startIcon={<Download />}
onClick={handleDownloadOnPackQr}
disabled={loading || downloadingOnPack || jobOrders.length === 0}
>
{downloadingOnPack ? "下載中..." : "下載 OnPack 汁水機 QR code"}
</Button>
</Stack>
</Paper>

{/* Job orders list */}
<Paper sx={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column", backgroundColor: BG_LIST }}>
{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", py: 8 }}>
<CircularProgress />
</Box>
) : jobOrders.length === 0 ? (
<Box sx={{ py: 8, textAlign: "center" }}>
<Typography color="text.secondary">當日無工單</Typography>
</Box>
) : (
<Box sx={{ overflow: "auto", flex: 1, p: 2 }}>
<Stack spacing={1}>
{jobOrders.map((jo) => {
const batch = getBatch(jo);
const qtyStr = formatQty(jo.reqQty);
const isSelected = selectedId === jo.id;
return (
<Paper
key={jo.id}
elevation={1}
sx={{
p: 2,
display: "flex",
alignItems: "flex-start",
gap: 2,
cursor: "pointer",
backgroundColor: isSelected ? BG_ROW_SELECTED : BG_ROW,
"&:hover": { backgroundColor: isSelected ? BG_ROW_SELECTED : "#b8d4eb" },
transition: "background-color 0.2s",
}}
onClick={() => handleRowClick(jo)}
>
<Box sx={{ minWidth: 120, flexShrink: 0 }}>
<Typography variant="h6" sx={{ fontSize: "1.1rem" }}>
{batch}
</Typography>
{qtyStr !== "—" && (
<Typography variant="body2" color="text.secondary">
數量:{qtyStr}
</Typography>
)}
</Box>
<Box sx={{ minWidth: 140, flexShrink: 0 }}>
<Typography variant="h6" sx={{ fontSize: "1.1rem" }}>
{jo.code || "—"}
</Typography>
</Box>
<Box sx={{ minWidth: 140, flexShrink: 0 }}>
<Typography variant="h6" sx={{ fontSize: "1.35rem" }}>
{jo.itemCode || "—"}
</Typography>
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="h6" sx={{ fontSize: "1.35rem", wordBreak: "break-word" }}>
{jo.itemName || "—"}
</Typography>
</Box>
<Button
size="small"
variant="contained"
startIcon={<Print />}
onClick={(e) => {
e.stopPropagation();
handleRowClick(jo);
}}
>
列印
</Button>
</Paper>
);
})}
</Stack>
</Box>
)}
</Paper>

{/* Settings dialog */}
<Dialog open={settingsOpen} onClose={() => setSettingsOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>設定</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Typography variant="subtitle2" color="primary">
打袋機 DataFlex
</Typography>
<TextField
label="IP"
size="small"
value={settings.dabag_ip}
onChange={(e) => setSettings((s) => ({ ...s, dabag_ip: e.target.value }))}
fullWidth
/>
<TextField
label="Port"
size="small"
value={settings.dabag_port}
onChange={(e) => setSettings((s) => ({ ...s, dabag_port: e.target.value }))}
fullWidth
/>
<Typography variant="subtitle2" color="primary">
激光機
</Typography>
<TextField
label="IP"
size="small"
value={settings.laser_ip}
onChange={(e) => setSettings((s) => ({ ...s, laser_ip: e.target.value }))}
fullWidth
/>
<TextField
label="Port"
size="small"
value={settings.laser_port}
onChange={(e) => setSettings((s) => ({ ...s, laser_port: e.target.value }))}
fullWidth
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setSettingsOpen(false)}>取消</Button>
<Button
variant="contained"
onClick={() => {
saveSettings(settings);
setSnackbar({ open: true, message: "設定已儲存", severity: "success" });
setSettingsOpen(false);
checkCurrentPrinter();
}}
>
儲存
</Button>
</DialogActions>
</Dialog>

<Snackbar
open={snackbar.open}
autoHideDuration={3000}
onClose={() => setSnackbar((s) => ({ ...s, open: false }))}
message={snackbar.message}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
/>
</Box>
);
};

export default BagPrintSearch;

+ 20
- 1
src/components/BomWeightingScoreTable/BomWeightingScoreTable.tsx Näytä tiedosto

@@ -16,9 +16,10 @@ import Stack from "@mui/material/Stack";

interface Props {
bomWeightingScores: BomWeightingScoreResult[];
onWeightingUpdated?: () => void;
}

const BomWeightingScoreTable: React.FC<Props> & { Loading?: React.FC } = ({ bomWeightingScores: initialBomWeightingScores }) => {
const BomWeightingScoreTable: React.FC<Props> & { Loading?: React.FC } = ({ bomWeightingScores: initialBomWeightingScores, onWeightingUpdated }) => {
const { t } = useTranslation("common");
const [bomWeightingScores, setBomWeightingScores] = useState(initialBomWeightingScores);
const [isEditMode, setIsEditMode] = useState(false);
@@ -120,10 +121,28 @@ const BomWeightingScoreTable: React.FC<Props> & { Loading?: React.FC } = ({ bomW
prev.map((r) => updatedById.get(r.id) ?? r),
);

// Wait a bit to ensure all weighting updates are committed to database
// before recalculating base scores
console.log("Waiting for weighting updates to commit...");
await new Promise(resolve => setTimeout(resolve, 200));

// After weighting changes, trigger BOM baseScore recalculation on the server
try {
console.log("Triggering BOM base score recalculation...");
const result = await recalcBomScoresClient();
updatedCount = result?.updatedCount ?? null;
console.log(`BOM base scores recalculated: ${updatedCount} BOMs updated`);
// Wait a bit more to ensure recalculation transaction is committed
// before refreshing the frontend
await new Promise(resolve => setTimeout(resolve, 300));
// Notify parent component to refresh BOM scores if needed
if (onWeightingUpdated) {
console.log("Refreshing BOM scores in frontend...");
await onWeightingUpdated();
console.log("BOM scores refreshed");
}
} catch (recalcError) {
console.error("Failed to recalculate BOM base scores:", recalcError);
// We don't block the main save flow if recalculation fails


+ 52
- 24
src/components/BomWeightingTabs/BomWeightingTabs.tsx Näytä tiedosto

@@ -3,6 +3,7 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { BomWeightingScoreResult } from "@/app/api/settings/bomWeighting";
import { fetchBomWeightingScoresClient } from "@/app/api/settings/bomWeighting/client";
import type { BomScoreResult } from "@/app/api/bom";
import { fetchBomScoresClient } from "@/app/api/bom/client";
import BomWeightingScoreTable from "@/components/BomWeightingScoreTable";
@@ -16,37 +17,61 @@ interface Props {
bomWeightingScores: BomWeightingScoreResult[];
}

const BomWeightingTabs: React.FC<Props> = ({ bomWeightingScores }) => {
const BomWeightingTabs: React.FC<Props> = ({ bomWeightingScores: initialBomWeightingScores }) => {
const { t } = useTranslation("common");
const [tab, setTab] = useState(0);
const [bomWeightingScores, setBomWeightingScores] = useState<BomWeightingScoreResult[]>(initialBomWeightingScores);
const [bomScores, setBomScores] = useState<BomScoreResult[] | null>(null);
const [loadingScores, setLoadingScores] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);

useEffect(() => {
if (tab !== 1) return;
const loadBomScores = React.useCallback(async () => {
try {
setLoadingScores(true);
setLoadError(null);
console.log("Fetching BOM scores from /bom/scores...");
const data = await fetchBomScoresClient();
console.log("BOM scores received:", data);
setBomScores(data || []);
} catch (err: any) {
console.error("Failed to load BOM scores:", err);
const errorMsg =
err?.response?.data?.message || err?.message || t("Update Failed") || "Load failed";
setLoadError(errorMsg);
setBomScores([]);
} finally {
setLoadingScores(false);
}
}, [t]);

const loadBomWeightingScores = React.useCallback(async () => {
try {
console.log("Fetching BOM weighting scores...");
const data = await fetchBomWeightingScoresClient();
console.log("BOM weighting scores received:", data);
setBomWeightingScores(data || []);
} catch (err: any) {
console.error("Failed to load BOM weighting scores:", err);
}
}, []);

const handleWeightingUpdated = React.useCallback(async () => {
// Refresh both weighting scores and BOM scores
await Promise.all([
loadBomWeightingScores(),
loadBomScores()
]);
}, [loadBomWeightingScores, loadBomScores]);

const load = async () => {
try {
setLoadingScores(true);
setLoadError(null);
console.log("Fetching BOM scores from /bom/scores...");
const data = await fetchBomScoresClient();
console.log("BOM scores received:", data);
setBomScores(data || []);
} catch (err: any) {
console.error("Failed to load BOM scores:", err);
const errorMsg =
err?.response?.data?.message || err?.message || t("Update Failed") || "Load failed";
setLoadError(errorMsg);
setBomScores([]);
} finally {
setLoadingScores(false);
}
};
// Sync initial prop values
useEffect(() => {
setBomWeightingScores(initialBomWeightingScores);
}, [initialBomWeightingScores]);

void load();
}, [tab, t]);
useEffect(() => {
if (tab !== 1) return;
void loadBomScores();
}, [tab, loadBomScores]);

return (
<>
@@ -83,7 +108,10 @@ const BomWeightingTabs: React.FC<Props> = ({ bomWeightingScores }) => {

<Box>
{tab === 0 && (
<BomWeightingScoreTable bomWeightingScores={bomWeightingScores} />
<BomWeightingScoreTable
bomWeightingScores={bomWeightingScores}
onWeightingUpdated={handleWeightingUpdated}
/>
)}
{tab === 1 && (
loadingScores ? (


+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Näytä tiedosto

@@ -38,6 +38,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/putAway": "Put Away",
"/stockIssue": "Stock Issue",
"/report": "Report",
"/bagPrint": "打袋機",
};

const Breadcrumb = () => {


+ 1
- 1
src/components/CreateItem/CreateItemWrapper.tsx Näytä tiedosto

@@ -24,7 +24,7 @@ const CreateItemWrapper: React.FC<Props> & SubComponents = async ({ id }) => {
if (id) {
result = await fetchItem(id);
const item = result.item;
qcChecks = result.qcChecks;
qcChecks = result.qcChecks ?? [];
const activeRows = qcChecks.filter((it) => it.isActive).map((i) => i.id);
// Normalize LocationCode field (handle case sensitivity from MySQL)


+ 3
- 3
src/components/NavigationContent/NavigationContent.tsx Näytä tiedosto

@@ -172,9 +172,9 @@ const NavigationContent: React.FC = () => {
},
{
icon: <Print />,
label: "打袋機列印",
path: "/testing",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
label: "打袋機",
path: "/bagPrint",
requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
isHidden: false,
},
{


+ 2
- 2
src/i18n/zh/common.json Näytä tiedosto

@@ -92,7 +92,7 @@
"User": "用戶",
"user": "用戶",
"User Group": "用戶群組",
"Items": "物",
"Items": "物",
"BOM Weighting Score List": "物料清單權重得分",
"Material Weighting": "物料清單加權",
"Material Score": "物料清單得分",
@@ -190,7 +190,7 @@
"Inventory": "庫存",
"scheduling": "排程",
"settings": "設定",
"items": "物",
"items": "物",
"edit":"編輯",
"bag": "包裝袋",
"Bag Usage": "包裝袋使用記錄",


+ 1
- 1
src/i18n/zh/project.json Näytä tiedosto

@@ -3,7 +3,7 @@
"FG & Material Demand Forecast Detail": "FG 及材料需求預測詳情",
"Release": "發佈",
"Actions": "操作",
"Product": "品",
"Product": "品",
"Details": "詳情",
"View BoM": "查看 BoM",
"description": "描述",


Ladataan…
Peruuta
Tallenna