| @@ -23,6 +23,8 @@ const pathToLabelKey: { [path: string]: string } = { | |||
| "/settings/qcItem": "nav.breadcrumb.qcItem", | |||
| "/settings/qcItemAll": "nav.breadcrumb.qcItemAll", | |||
| "/settings/qrCodeHandle": "nav.breadcrumb.qrCodeHandle", | |||
| "/settings/deliveryOrderFloor": "nav.breadcrumb.deliveryOrderFloor", | |||
| "/settings/masterDataIssues": "nav.breadcrumb.masterDataIssues", | |||
| "/settings/rss": "nav.breadcrumb.demandForecast", | |||
| "/settings/equipment": "nav.breadcrumb.equipment", | |||
| "/settings/equipment/MaintenanceEdit": "nav.breadcrumb.equipmentMaintenanceEdit", | |||
| @@ -116,8 +116,8 @@ const EscalationComponent: React.FC<Props> = ({ | |||
| defaultValue="pass" | |||
| name="radio-buttons-group" | |||
| > | |||
| <FormControlLabel value="pass" control={<Radio />} label="合格" /> | |||
| <FormControlLabel value="fail" control={<Radio />} label="不合格" /> | |||
| <FormControlLabel value="pass" control={<Radio />} label={t("passed")} /> | |||
| <FormControlLabel value="fail" control={<Radio />} label={t("failed")} /> | |||
| </RadioGroup> | |||
| </FormControl> | |||
| ): undefined} | |||
| @@ -140,24 +140,24 @@ const EscalationComponent: React.FC<Props> = ({ | |||
| fullWidth | |||
| id="quantity" | |||
| name="quantity" | |||
| label="數量" | |||
| label={t("Quantity")} | |||
| type="number" | |||
| value={formData.quantity} | |||
| onChange={handleInputChange} | |||
| InputProps={{ inputProps: { min: 1 } }} | |||
| placeholder="請輸入數量" | |||
| placeholder={t("Enter quantity")} | |||
| /> | |||
| <TextField | |||
| fullWidth | |||
| id="message" | |||
| name="message" | |||
| label="備註" | |||
| label={t("remarks")} | |||
| multiline | |||
| rows={4} | |||
| value={formData.message} | |||
| onChange={handleInputChange} | |||
| placeholder="請輸入您的備註" | |||
| placeholder={t("Enter your remark")} | |||
| /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| @@ -404,7 +404,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) { | |||
| submitDialogWithWarning(() => { | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); | |||
| }, t, {title: t("confirm_accept_with_fail"), confirmButtonText: t("Confirm"), html: ""}); | |||
| return; | |||
| } | |||
| @@ -463,7 +463,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| <FormControlLabel | |||
| value="true" | |||
| control={<Radio />} | |||
| label="合格" | |||
| label={t("passed")} | |||
| sx={{ | |||
| color: current.qcPassed === true ? "green" : "inherit", | |||
| "& .Mui-checked": {color: "green"} | |||
| @@ -472,7 +472,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| <FormControlLabel | |||
| value="false" | |||
| control={<Radio />} | |||
| label="不合格" | |||
| label={t("failed")} | |||
| sx={{ | |||
| color: current.qcPassed === false ? "red" : "inherit", | |||
| "& .Mui-checked": {color: "red"} | |||
| @@ -216,16 +216,16 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| <FormControlLabel | |||
| value="true" | |||
| control={<Radio />} | |||
| label="合格" | |||
| label={t("passed")} | |||
| sx={{ | |||
| color: currentValue === true ? "green" : "inherit", | |||
| "& .Mui-checked": {color: "green"} | |||
| }} | |||
| /> | |||
| <FormControlLabel | |||
| value="false" | |||
| control={<Radio />} | |||
| label="不合格" | |||
| <FormControlLabel | |||
| value="false" | |||
| control={<Radio />} | |||
| label={t("failed")} | |||
| sx={{ | |||
| color: currentValue === false ? "red" : "inherit", | |||
| "& .Mui-checked": {color: "red"} | |||
| @@ -419,7 +419,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| field.onChange(value); | |||
| }} | |||
| > | |||
| <FormControlLabel value="true" control={<Radio />} label="接受" /> | |||
| <FormControlLabel value="true" control={<Radio />} label={t("passed")} /> | |||
| <Box sx={{mr:2}}> | |||
| <TextField | |||
| type="number" | |||
| @@ -434,7 +434,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| helperText={errors.acceptQty?.message} | |||
| /> | |||
| </Box> | |||
| <FormControlLabel value="false" control={<Radio />} label="不接受及上報" /> | |||
| <FormControlLabel value="false" control={<Radio />} label={t("failed")} /> | |||
| </RadioGroup> | |||
| )} | |||
| /> | |||
| @@ -5,14 +5,6 @@ import { | |||
| Card, | |||
| CardContent, | |||
| Typography, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TablePagination, | |||
| TableRow, | |||
| Paper, | |||
| Button, | |||
| CircularProgress, | |||
| Alert, | |||
| @@ -30,14 +22,20 @@ import { | |||
| } from "@mui/material"; | |||
| import AddIcon from "@mui/icons-material/Add"; | |||
| import SaveIcon from "@mui/icons-material/Save"; | |||
| import { useState, useMemo } from "react"; | |||
| import { useState, useMemo, useCallback } from "react"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; | |||
| import type { Truck } from "@/app/api/shop/actions"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import SearchResults, { | |||
| Column, | |||
| defaultPagingController, | |||
| } from "../SearchResults/SearchResults"; | |||
| import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; | |||
| type TruckRow = Omit<Truck, "id"> & { id: string | number }; | |||
| type SearchQuery = { | |||
| truckLanceCode: string; | |||
| departureTime: string; | |||
| @@ -53,8 +51,7 @@ const TruckLane: React.FC = () => { | |||
| const [loading, setLoading] = useState<boolean>(false); | |||
| const [error, setError] = useState<string | null>(null); | |||
| const [filters, setFilters] = useState<Record<string, string>>({}); | |||
| const [page, setPage] = useState(0); | |||
| const [rowsPerPage, setRowsPerPage] = useState(10); | |||
| const [pagingController, setPagingController] = useState(defaultPagingController); | |||
| const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false); | |||
| const [newTruck, setNewTruck] = useState({ | |||
| truckLanceCode: "", | |||
| @@ -93,11 +90,80 @@ const TruckLane: React.FC = () => { | |||
| }); | |||
| }, [truckData, filters]); | |||
| // Paginated rows | |||
| const paginatedRows = useMemo(() => { | |||
| const startIndex = page * rowsPerPage; | |||
| return filteredRows.slice(startIndex, startIndex + rowsPerPage); | |||
| }, [filteredRows, page, rowsPerPage]); | |||
| const tableRows = useMemo<TruckRow[]>( | |||
| () => | |||
| filteredRows.map((truck) => ({ | |||
| ...truck, | |||
| id: truck.id ?? (String(truck.truckLanceCode ?? "").trim() || `truck-${truck.truckLanceCode}`), | |||
| })), | |||
| [filteredRows], | |||
| ); | |||
| const handleViewDetail = useCallback( | |||
| (truck: TruckRow) => { | |||
| const truckLanceCode = String(truck.truckLanceCode || "").trim(); | |||
| if (truckLanceCode) { | |||
| const url = new URL(`/settings/shop/truckdetail`, window.location.origin); | |||
| url.searchParams.set("truckLanceCode", truckLanceCode); | |||
| router.push(url.pathname + url.search); | |||
| } | |||
| }, | |||
| [router], | |||
| ); | |||
| const columns = useMemo<Column<TruckRow>[]>( | |||
| () => [ | |||
| { | |||
| name: "truckLanceCode", | |||
| label: t("TruckLance Code"), | |||
| sx: { width: "250px", minWidth: "250px", maxWidth: "250px" }, | |||
| renderCell: (item) => String(item.truckLanceCode ?? "-"), | |||
| }, | |||
| { | |||
| name: "departureTime", | |||
| label: t("Departure Time"), | |||
| sx: { width: "200px", minWidth: "200px", maxWidth: "200px" }, | |||
| renderCell: (item) => | |||
| formatDepartureTime( | |||
| Array.isArray(item.departureTime) | |||
| ? item.departureTime | |||
| : item.departureTime | |||
| ? String(item.departureTime) | |||
| : null, | |||
| ), | |||
| }, | |||
| { | |||
| name: "storeId", | |||
| label: t("Store ID"), | |||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||
| renderCell: (item) => | |||
| normalizeStoreId( | |||
| item.storeId | |||
| ? typeof item.storeId === "string" || item.storeId instanceof String | |||
| ? String(item.storeId) | |||
| : String(item.storeId) | |||
| : null, | |||
| ), | |||
| }, | |||
| { | |||
| name: "id", | |||
| label: t("Actions"), | |||
| align: "right", | |||
| headerAlign: "right", | |||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||
| renderCell: (item) => ( | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| onClick={() => handleViewDetail(item)} | |||
| > | |||
| {t("View Detail")} | |||
| </Button> | |||
| ), | |||
| }, | |||
| ], | |||
| [handleViewDetail, t], | |||
| ); | |||
| const handleSearch = async (inputs: Record<string, string>) => { | |||
| setLoading(true); | |||
| @@ -113,7 +179,7 @@ const TruckLane: React.FC = () => { | |||
| }); | |||
| setTruckData(Array.from(uniqueCodes.values())); | |||
| setFilters(inputs); | |||
| setPage(0); | |||
| setPagingController(defaultPagingController); | |||
| } catch (err: any) { | |||
| console.error("Failed to load truck lanes:", err); | |||
| setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); | |||
| @@ -122,26 +188,6 @@ const TruckLane: React.FC = () => { | |||
| } | |||
| }; | |||
| const handlePageChange = (event: unknown, newPage: number) => { | |||
| setPage(newPage); | |||
| }; | |||
| const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
| setRowsPerPage(parseInt(event.target.value, 10)); | |||
| setPage(0); // Reset to first page when changing rows per page | |||
| }; | |||
| const handleViewDetail = (truck: Truck) => { | |||
| // Navigate to truck lane detail page using truckLanceCode | |||
| const truckLanceCode = String(truck.truckLanceCode || "").trim(); | |||
| if (truckLanceCode) { | |||
| // Use router.push with proper URL encoding | |||
| const url = new URL(`/settings/shop/truckdetail`, window.location.origin); | |||
| url.searchParams.set("truckLanceCode", truckLanceCode); | |||
| router.push(url.pathname + url.search); | |||
| } | |||
| }; | |||
| const handleOpenAddDialog = () => { | |||
| setNewTruck({ | |||
| truckLanceCode: "", | |||
| @@ -270,77 +316,12 @@ const TruckLane: React.FC = () => { | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}> | |||
| {t("TruckLance Code")} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}> | |||
| {t("Departure Time")} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| {t("Store ID")} | |||
| </TableCell> | |||
| <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| {t("Actions")} | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {paginatedRows.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={4} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No Truck Lane data available")} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| paginatedRows.map((truck) => ( | |||
| <TableRow key={truck.id ?? `truck-${truck.truckLanceCode}`}> | |||
| <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}> | |||
| {String(truck.truckLanceCode ?? "-")} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}> | |||
| {formatDepartureTime( | |||
| Array.isArray(truck.departureTime) | |||
| ? truck.departureTime | |||
| : (truck.departureTime ? String(truck.departureTime) : null) | |||
| )} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| {normalizeStoreId( | |||
| truck.storeId ? (typeof truck.storeId === 'string' || truck.storeId instanceof String | |||
| ? String(truck.storeId) | |||
| : String(truck.storeId)) : null | |||
| )} | |||
| </TableCell> | |||
| <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| onClick={() => handleViewDetail(truck)} | |||
| > | |||
| {t("View Detail")} | |||
| </Button> | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| <TablePagination | |||
| component="div" | |||
| count={filteredRows.length} | |||
| page={page} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={rowsPerPage} | |||
| onRowsPerPageChange={handleRowsPerPageChange} | |||
| rowsPerPageOptions={[5, 10, 25, 50]} | |||
| <SearchResults | |||
| items={tableRows} | |||
| columns={columns} | |||
| pagingController={pagingController} | |||
| setPagingController={setPagingController} | |||
| /> | |||
| </TableContainer> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| @@ -38,7 +38,7 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | |||
| equipmentTabContent, | |||
| warehouseTabContent, | |||
| }) => { | |||
| const { t } = useTranslation("common"); | |||
| const { t: tQrCodeHandle } = useTranslation("qrCodeHandle"); | |||
| const { t: tUser } = useTranslation("user"); | |||
| const { t: tWarehouse } = useTranslation("warehouse"); | |||
| const searchParams = useSearchParams(); | |||
| @@ -80,7 +80,7 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | |||
| <Box sx={{ borderBottom: 1, borderColor: "divider" }}> | |||
| <Tabs value={currentTab} onChange={handleTabChange}> | |||
| <Tab label={tUser("User")} /> | |||
| <Tab label={t("Equipment")} /> | |||
| <Tab label={tQrCodeHandle("Equipment")} /> | |||
| <Tab label={tWarehouse("Warehouse")} /> | |||
| </Tabs> | |||
| </Box> | |||
| @@ -44,7 +44,7 @@ | |||
| "nav.settings.deliveryOrderFloor": "DO floor (supplier)", | |||
| "nav.settings.demandForecast": "Demand Forecast Setting", | |||
| "nav.settings.bomWeighting": "BOM Weighting Score List", | |||
| "nav.settings.masterDataIssues": "Master Data Issues", | |||
| "nav.settings.masterDataIssues": "BOM / Item UOM Issues", | |||
| "nav.settings.qrCodeHandle": "QR Code Handle", | |||
| "nav.settings.importTesting": "Import Testing", | |||
| "nav.settings.importExcel": "Import Excel", | |||
| @@ -65,6 +65,8 @@ | |||
| "nav.breadcrumb.qcItemAll": "QC Item All", | |||
| "nav.breadcrumb.qrCodeHandle": "QR Code Handle", | |||
| "nav.breadcrumb.demandForecast": "Demand Forecast Setting", | |||
| "nav.breadcrumb.deliveryOrderFloor": "Delivery Order Floor", | |||
| "nav.breadcrumb.masterDataIssues": "BOM / Item UOM Issues", | |||
| "nav.breadcrumb.equipment": "Equipment", | |||
| "nav.breadcrumb.equipmentMaintenanceEdit": "Maintenance Edit", | |||
| "nav.breadcrumb.shop": "Shop And Truck", | |||
| @@ -514,5 +514,8 @@ | |||
| "No lot rows. Select a line in the table above.": "No lot rows. Select a line in the table above.", | |||
| "No stock out line for this lot": "No stock out line for this lot", | |||
| "No data available for this pick order.": "No data available for this pick order.", | |||
| "Report missing or bad items": "Report missing or bad items" | |||
| "Report missing or bad items": "Report missing or bad items", | |||
| "passed": "Passed", | |||
| "failed": "Failed", | |||
| "confirm_accept_with_fail": "There are failed QC items. Confirm to accept stock out?" | |||
| } | |||
| @@ -71,6 +71,7 @@ | |||
| "Job Order Info": "Job Order Info", | |||
| "Job Order No.": "Job Order No.", | |||
| "Job Order and Product": "Job Order and Product", | |||
| "Job Order Production Process": "Job Order Production Process", | |||
| "Job Process Status Dashboard": "Job Process Status Dashboard", | |||
| "Job Type": "Job Type", | |||
| "Job process detail mode label": "Job process detail mode label", | |||
| @@ -178,5 +178,8 @@ | |||
| "Add Record": "Add Record", | |||
| "Clean Record": "Reset", | |||
| "Create Material": "Create Material", | |||
| "Will start binding procedure after scanning item qr code.": "Will start binding procedure after scanning item qr code." | |||
| "Will start binding procedure after scanning item qr code.": "Will start binding procedure after scanning item qr code.", | |||
| "Quantity": "Quantity", | |||
| "Enter quantity": "Enter quantity", | |||
| "Enter your remark": "Enter your remark" | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| { | |||
| "PDF Preview": "PDF Preview", | |||
| "QR Code Handle": "QR Code Handle" | |||
| "QR Code Handle": "QR Code Handle", | |||
| "Equipment": "Equipment" | |||
| } | |||
| @@ -17,5 +17,16 @@ | |||
| "Balance Qty": "Balance Qty", | |||
| "Loading...": "Loading...", | |||
| "Type": "Type", | |||
| "Status": "Status" | |||
| "Status": "Status", | |||
| "pending": "Pending", | |||
| "completed": "Completed", | |||
| "partially_completed": "Partially completed", | |||
| "receiving": "Receiving", | |||
| "rejected": "Rejected", | |||
| "checked": "Checked", | |||
| "determine1": "Determine 1", | |||
| "lot-change": "Lot change approval", | |||
| "qc1": "QC 1", | |||
| "qc2": "QC 2", | |||
| "qc3": "QC 3" | |||
| } | |||
| @@ -22,7 +22,7 @@ | |||
| "masterDataIssue_scope_BOM": "BOM 總表", | |||
| "masterDataIssue_scope_BOM_MATERIAL": "BOM 原材料", | |||
| "masterDataIssue_scope_ITEM": "貨品", | |||
| "masterDataIssue_nav": "數據問題", | |||
| "masterDataIssue_nav": "BOM/貨品單位問題", | |||
| "masterDataIssue_MISSING_BOM_CODE": "BOM 編號為空", | |||
| "masterDataIssue_MISSING_BOM_NAME": "BOM 名稱為空", | |||
| "masterDataIssue_MISSING_ITEM": "關聯貨品不存在或已刪除", | |||
| @@ -10,6 +10,7 @@ | |||
| "nav.breadcrumb.chartPurchase": "採購", | |||
| "nav.breadcrumb.chartWarehouse": "庫存與倉儲", | |||
| "nav.breadcrumb.demandForecast": "需求預測設定", | |||
| "nav.breadcrumb.deliveryOrderFloor": "送貨單樓層", | |||
| "nav.breadcrumb.doWorkbenchEdit": "DO Workbench 詳情", | |||
| "nav.breadcrumb.doWorkbenchPick": "DO Workbench 揀貨", | |||
| "nav.breadcrumb.doWorkbenchSearch": "DO Workbench 搜索", | |||
| @@ -514,5 +514,8 @@ | |||
| "No lot rows. Select a line in the table above.": "尚無批號資料。請在上方表格勾選一行提料單明細。", | |||
| "No stock out line for this lot": "此批號尚無出庫行,無法提交。", | |||
| "No data available for this pick order.": "此提料單沒有可用資料。", | |||
| "Report missing or bad items": "報告缺失或不良物品" | |||
| "Report missing or bad items": "報告缺失或不良物品", | |||
| "passed": "合格", | |||
| "failed": "不合格", | |||
| "confirm_accept_with_fail": "有不合格檢查項目,確認接受出庫?" | |||
| } | |||
| @@ -71,6 +71,7 @@ | |||
| "Job Order Info": "工單信息", | |||
| "Job Order No.": "工單編號", | |||
| "Job Order and Product": "工單及貨品", | |||
| "Job Order Production Process": "工單生產流程", | |||
| "Job Process Status Dashboard": "儀表板 - 工單狀態", | |||
| "Job Type": "工單類型", | |||
| "Job process detail mode label": "工序格顯示", | |||
| @@ -177,5 +177,8 @@ | |||
| "Total must equal Required Qty. Exceeds by": "總數量必須等於所需數量。超出:{{diff}}", | |||
| "Add Record": "新增", | |||
| "Clean Record": "重置", | |||
| "Will start binding procedure after scanning item qr code.": "掃描物品二維碼後將開始綁定流程。" | |||
| "Will start binding procedure after scanning item qr code.": "掃描物品二維碼後將開始綁定流程。", | |||
| "Quantity": "數量", | |||
| "Enter quantity": "請輸入數量", | |||
| "Enter your remark": "請輸入您的備註" | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| { | |||
| "PDF Preview": "PDF 預覽", | |||
| "QR Code Handle": "二維碼列印及下載" | |||
| "QR Code Handle": "二維碼列印及下載", | |||
| "Equipment": "設備" | |||
| } | |||
| @@ -17,5 +17,16 @@ | |||
| "Balance Qty": "庫存數量", | |||
| "Loading...": "載入中...", | |||
| "Type": "類別", | |||
| "Status": "狀態" | |||
| "Status": "狀態", | |||
| "pending": "待處理", | |||
| "completed": "已完成", | |||
| "partially_completed": "已部分完成", | |||
| "receiving": "收貨中", | |||
| "rejected": "已拒絕", | |||
| "checked": "已檢查", | |||
| "determine1": "上報1", | |||
| "lot-change": "批次更換審批", | |||
| "qc1": "質檢1", | |||
| "qc2": "質檢2", | |||
| "qc3": "質檢3" | |||
| } | |||