| Автор | SHA1 | Сообщение | Дата |
|---|---|---|---|
|
|
e400b4a9c8 | Remove ability to use sensitive reports | 1 месяц назад |
|
|
2f7419207b | Remove hardcoded placeholders and codes | 1 месяц назад |
|
|
9140cbc9b2 | Remove Pings Logo | 1 месяц назад |
| @@ -4,7 +4,7 @@ import LaserPrintSearch from "@/components/LaserPrint/LaserPrintSearch"; | |||||
| import { Stack, Typography } from "@mui/material"; | import { Stack, Typography } from "@mui/material"; | ||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "檸檬機(激光機)", | |||||
| title: "激光機", | |||||
| }; | }; | ||||
| export default async function LaserPrintPage() { | export default async function LaserPrintPage() { | ||||
| @@ -12,7 +12,7 @@ export default async function LaserPrintPage() { | |||||
| <I18nProvider namespaces={["laserPrint", "navigation", "common"]}> | <I18nProvider namespaces={["laserPrint", "navigation", "common"]}> | ||||
| <Stack direction="row" justifyContent="space-between" flexWrap="wrap" rowGap={2}> | <Stack direction="row" justifyContent="space-between" flexWrap="wrap" rowGap={2}> | ||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| 檸檬機(激光機) | |||||
| 激光機 | |||||
| </Typography> | </Typography> | ||||
| </Stack> | </Stack> | ||||
| <LaserPrintSearch /> | <LaserPrintSearch /> | ||||
| @@ -318,7 +318,7 @@ export default function M18SynPage() { | |||||
| label="PO Code" | label="PO Code" | ||||
| value={m18PoCode} | value={m18PoCode} | ||||
| onChange={(e) => setM18PoCode(e.target.value)} | onChange={(e) => setM18PoCode(e.target.value)} | ||||
| placeholder="e.g. PFP002PO26030341" | |||||
| placeholder="e.g. DEMO-PO-001" | |||||
| sx={{ minWidth: 320 }} | sx={{ minWidth: 320 }} | ||||
| /> | /> | ||||
| <Button variant="contained" color="primary" onClick={handleSyncM18PoByCode} disabled={isSyncingM18Po}> | <Button variant="contained" color="primary" onClick={handleSyncM18PoByCode} disabled={isSyncingM18Po}> | ||||
| @@ -418,7 +418,7 @@ export default function M18SynPage() { | |||||
| label="Item / product code" | label="Item / product code" | ||||
| value={m18ProductCode} | value={m18ProductCode} | ||||
| onChange={(e) => setM18ProductCode(e.target.value)} | onChange={(e) => setM18ProductCode(e.target.value)} | ||||
| placeholder="e.g. PP1175 (M18 item code)" | |||||
| placeholder="e.g. DEMO-FG-01" | |||||
| sx={{ minWidth: 320 }} | sx={{ minWidth: 320 }} | ||||
| /> | /> | ||||
| <Button | <Button | ||||
| @@ -1179,9 +1179,9 @@ export default function ProductionSchedulePage() { | |||||
| <th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期庫存</th> | <th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期庫存</th> | ||||
| <th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">過去平均出貨量</th> | <th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">過去平均出貨量</th> | ||||
| <th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期每天出貨量</th> | <th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期每天出貨量</th> | ||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">咖啡</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">茶</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">檸檬</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">類別 A</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">類別 B</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">類別 C</th> | |||||
| </tr> | </tr> | ||||
| </thead> | </thead> | ||||
| <tbody> | <tbody> | ||||
| @@ -239,7 +239,7 @@ const AsyncItemCodeAutocomplete: React.FC<Props> = ({ | |||||
| {...params} | {...params} | ||||
| fullWidth | fullWidth | ||||
| label={label} | label={label} | ||||
| placeholder={hasSelection ? "" : (placeholder || "e.g. FA0591")} | |||||
| placeholder={hasSelection ? "" : (placeholder || "e.g. DEMO-FG-01")} | |||||
| helperText={t("typeToSearchItemCode", { min: minChars })} | helperText={t("typeToSearchItemCode", { min: minChars })} | ||||
| InputLabelProps={params.InputLabelProps} | InputLabelProps={params.InputLabelProps} | ||||
| InputProps={{ | InputProps={{ | ||||
| @@ -75,7 +75,7 @@ export default function ItemQcReportFilters({ | |||||
| label={field("itemCode", "貨品編號 Item Code")} | label={field("itemCode", "貨品編號 Item Code")} | ||||
| value={criteria.itemCode || ""} | value={criteria.itemCode || ""} | ||||
| onChange={(e) => onFieldChange("itemCode", e.target.value)} | onChange={(e) => onFieldChange("itemCode", e.target.value)} | ||||
| placeholder="e.g. MJ0364" | |||||
| placeholder="e.g. DEMO-FG-01" | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item {...gridSize}> | <Grid item {...gridSize}> | ||||
| @@ -17,7 +17,7 @@ import OutboundOutlinedIcon from "@mui/icons-material/OutboundOutlined"; | |||||
| import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined"; | import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined"; | ||||
| import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutlined"; | import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutlined"; | ||||
| import type { SvgIconComponent } from "@mui/icons-material"; | import type { SvgIconComponent } from "@mui/icons-material"; | ||||
| import { REPORTS } from "@/config/reportConfig"; | |||||
| import { DEMO_UNAVAILABLE_REPORT_IDS, REPORTS } from "@/config/reportConfig"; | |||||
| import { REPORT_CATEGORIES, type ReportCategoryConfig } from "./reportCategories"; | import { REPORT_CATEGORIES, type ReportCategoryConfig } from "./reportCategories"; | ||||
| import { useReportLabels } from "./reportI18n"; | import { useReportLabels } from "./reportI18n"; | ||||
| @@ -52,12 +52,16 @@ function ReportCard({ | |||||
| title, | title, | ||||
| accent, | accent, | ||||
| selected, | selected, | ||||
| unavailable, | |||||
| unavailableLabel, | |||||
| onClick, | onClick, | ||||
| }: { | }: { | ||||
| reportId: string; | reportId: string; | ||||
| title: string; | title: string; | ||||
| accent: string; | accent: string; | ||||
| selected: boolean; | selected: boolean; | ||||
| unavailable: boolean; | |||||
| unavailableLabel: string; | |||||
| onClick: () => void; | onClick: () => void; | ||||
| }) { | }) { | ||||
| const Icon = REPORT_ICON_MAP[reportId] ?? Inventory2OutlinedIcon; | const Icon = REPORT_ICON_MAP[reportId] ?? Inventory2OutlinedIcon; | ||||
| @@ -66,7 +70,9 @@ function ReportCard({ | |||||
| <Box | <Box | ||||
| component="button" | component="button" | ||||
| type="button" | type="button" | ||||
| onClick={onClick} | |||||
| disabled={unavailable} | |||||
| onClick={unavailable ? undefined : onClick} | |||||
| aria-disabled={unavailable} | |||||
| sx={{ | sx={{ | ||||
| display: "flex", | display: "flex", | ||||
| alignItems: "center", | alignItems: "center", | ||||
| @@ -74,17 +80,22 @@ function ReportCard({ | |||||
| width: "100%", | width: "100%", | ||||
| p: 1.5, | p: 1.5, | ||||
| textAlign: "left", | textAlign: "left", | ||||
| cursor: "pointer", | |||||
| cursor: unavailable ? "not-allowed" : "pointer", | |||||
| border: "1px solid", | border: "1px solid", | ||||
| borderColor: selected ? accent : "divider", | |||||
| borderColor: unavailable ? "action.disabledBackground" : selected ? accent : "divider", | |||||
| borderRadius: 1.5, | borderRadius: 1.5, | ||||
| bgcolor: "background.paper", | |||||
| boxShadow: selected ? 2 : "0 1px 3px rgba(0,0,0,0.06)", | |||||
| bgcolor: unavailable ? "action.hover" : "background.paper", | |||||
| color: unavailable ? "text.disabled" : "text.primary", | |||||
| boxShadow: !unavailable && selected ? 2 : "0 1px 3px rgba(0,0,0,0.06)", | |||||
| opacity: unavailable ? 0.72 : 1, | |||||
| filter: unavailable ? "grayscale(1)" : "none", | |||||
| transition: "border-color 0.15s, box-shadow 0.15s", | transition: "border-color 0.15s, box-shadow 0.15s", | ||||
| "&:hover": { | |||||
| borderColor: accent, | |||||
| boxShadow: 2, | |||||
| }, | |||||
| "&:hover": unavailable | |||||
| ? {} | |||||
| : { | |||||
| borderColor: accent, | |||||
| boxShadow: 2, | |||||
| }, | |||||
| }} | }} | ||||
| > | > | ||||
| <Box | <Box | ||||
| @@ -95,16 +106,23 @@ function ReportCard({ | |||||
| width: 40, | width: 40, | ||||
| height: 40, | height: 40, | ||||
| borderRadius: 1, | borderRadius: 1, | ||||
| bgcolor: `${accent}18`, | |||||
| color: accent, | |||||
| bgcolor: unavailable ? "action.disabledBackground" : `${accent}18`, | |||||
| color: unavailable ? "text.disabled" : accent, | |||||
| flexShrink: 0, | flexShrink: 0, | ||||
| }} | }} | ||||
| > | > | ||||
| <Icon fontSize="small" /> | <Icon fontSize="small" /> | ||||
| </Box> | </Box> | ||||
| <Typography variant="body2" fontWeight={selected ? 600 : 400} sx={{ lineHeight: 1.35 }}> | |||||
| {title} | |||||
| </Typography> | |||||
| <Box sx={{ minWidth: 0 }}> | |||||
| <Typography variant="body2" fontWeight={selected && !unavailable ? 600 : 400} sx={{ lineHeight: 1.35 }}> | |||||
| {title} | |||||
| </Typography> | |||||
| {unavailable && ( | |||||
| <Typography variant="caption" sx={{ display: "block", lineHeight: 1.3, color: "text.secondary" }}> | |||||
| {unavailableLabel} | |||||
| </Typography> | |||||
| )} | |||||
| </Box> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -118,7 +136,7 @@ function CategoryColumn({ | |||||
| selectedReportId: string; | selectedReportId: string; | ||||
| onSelectReport: (reportId: string) => void; | onSelectReport: (reportId: string) => void; | ||||
| }) { | }) { | ||||
| const { reportTitle, categoryTitle } = useReportLabels(); | |||||
| const { t, reportTitle, categoryTitle } = useReportLabels(); | |||||
| const reports = category.reportIds | const reports = category.reportIds | ||||
| .map((id) => reportById[id]) | .map((id) => reportById[id]) | ||||
| .filter(Boolean); | .filter(Boolean); | ||||
| @@ -161,6 +179,8 @@ function CategoryColumn({ | |||||
| title={reportTitle(report)} | title={reportTitle(report)} | ||||
| accent={category.accent} | accent={category.accent} | ||||
| selected={selectedReportId === report.id} | selected={selectedReportId === report.id} | ||||
| unavailable={DEMO_UNAVAILABLE_REPORT_IDS.has(report.id)} | |||||
| unavailableLabel={t("unavailableInDemo")} | |||||
| onClick={() => onSelectReport(report.id)} | onClick={() => onSelectReport(report.id)} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| @@ -205,7 +205,7 @@ function toMaterialExcelRow( | |||||
| export async function generateBomShopSyncReportExcel( | export async function generateBomShopSyncReportExcel( | ||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = "M18 BOM Shop 同步記錄", | |||||
| reportTitle: string = "BOM Shop 同步記錄", | |||||
| t?: TFunction, | t?: TFunction, | ||||
| ): Promise<void> { | ): Promise<void> { | ||||
| const L = bomSyncLabels(t); | const L = bomSyncLabels(t); | ||||
| @@ -33,12 +33,6 @@ export interface GrnReportRow { | |||||
| lineAmount?: number; | lineAmount?: number; | ||||
| /** PO currency code (currency.code) */ | /** PO currency code (currency.code) */ | ||||
| currencyCode?: string; | currencyCode?: string; | ||||
| /** M18 AN document code from m18_goods_receipt_note_log.grn_code */ | |||||
| grnCode?: string; | |||||
| /** M18 record id (m18_record_id) */ | |||||
| grnId?: number | string; | |||||
| /** From purchase_order.m18CreatedUId; e.g. "2569 (legato)" */ | |||||
| poM18CreatorDisplay?: string; | |||||
| [key: string]: unknown; | [key: string]: unknown; | ||||
| } | } | ||||
| @@ -54,7 +48,6 @@ export interface ListedPoAmounts { | |||||
| poCode?: string; | poCode?: string; | ||||
| currencyCode?: string; | currencyCode?: string; | ||||
| totalAmount?: number; | totalAmount?: number; | ||||
| grnCodes?: string; | |||||
| }[]; | }[]; | ||||
| } | } | ||||
| @@ -148,13 +141,9 @@ function grnLabels(t?: TFunction) { | |||||
| unitPrice: tx(t, "excel.grn.unitPrice", "單價"), | unitPrice: tx(t, "excel.grn.unitPrice", "單價"), | ||||
| currency: tx(t, "excel.grn.currency", "貨幣"), | currency: tx(t, "excel.grn.currency", "貨幣"), | ||||
| amount: tx(t, "excel.grn.amount", "金額"), | amount: tx(t, "excel.grn.amount", "金額"), | ||||
| grnCode: tx(t, "excel.grn.grnCode", "M18 入倉單號"), | |||||
| grnId: tx(t, "excel.grn.grnId", "M18 記錄編號"), | |||||
| poCreator: tx(t, "excel.grn.poCreator", "PO建立者(M18)"), | |||||
| note: tx(t, "excel.grn.note", "備註"), | note: tx(t, "excel.grn.note", "備註"), | ||||
| category: tx(t, "excel.grn.category", "類別"), | category: tx(t, "excel.grn.category", "類別"), | ||||
| totalAmount: tx(t, "excel.grn.totalAmount", "金額"), | totalAmount: tx(t, "excel.grn.totalAmount", "金額"), | ||||
| grnCodes: tx(t, "excel.grn.grnCodes", "M18 入倉單號"), | |||||
| noCompletedPo: tx(t, "excel.grn.noCompletedPo", "(篩選範圍內無已完成之 PO 行)"), | noCompletedPo: tx(t, "excel.grn.noCompletedPo", "(篩選範圍內無已完成之 PO 行)"), | ||||
| categoryCurrencyTotal: tx(t, "excel.grn.categoryCurrencyTotal", "貨幣小計"), | categoryCurrencyTotal: tx(t, "excel.grn.categoryCurrencyTotal", "貨幣小計"), | ||||
| categoryPo: tx(t, "excel.grn.categoryPo", "訂單"), | categoryPo: tx(t, "excel.grn.categoryPo", "訂單"), | ||||
| @@ -187,9 +176,6 @@ function toExcelRow( | |||||
| base[L.currency] = r.currencyCode ?? ""; | base[L.currency] = r.currencyCode ?? ""; | ||||
| base[L.amount] = moneyCellValue(r.lineAmount); | base[L.amount] = moneyCellValue(r.lineAmount); | ||||
| } | } | ||||
| base[L.grnCode] = r.grnCode ?? ""; | |||||
| base[L.grnId] = r.grnId ?? ""; | |||||
| base[L.poCreator] = r.poM18CreatorDisplay ?? ""; | |||||
| return base; | return base; | ||||
| } | } | ||||
| @@ -213,7 +199,6 @@ function buildListedPoAmountSheetRows( | |||||
| [L.poNo]: "", | [L.poNo]: "", | ||||
| [L.currency]: c.currencyCode ?? "", | [L.currency]: c.currencyCode ?? "", | ||||
| [L.totalAmount]: moneyCellValue(c.totalAmount), | [L.totalAmount]: moneyCellValue(c.totalAmount), | ||||
| [L.grnCodes]: "", | |||||
| }); | }); | ||||
| } | } | ||||
| for (const p of listed.byPurchaseOrder) { | for (const p of listed.byPurchaseOrder) { | ||||
| @@ -223,7 +208,6 @@ function buildListedPoAmountSheetRows( | |||||
| [L.poNo]: p.poCode ?? "", | [L.poNo]: p.poCode ?? "", | ||||
| [L.currency]: p.currencyCode ?? "", | [L.currency]: p.currencyCode ?? "", | ||||
| [L.totalAmount]: moneyCellValue(p.totalAmount), | [L.totalAmount]: moneyCellValue(p.totalAmount), | ||||
| [L.grnCodes]: p.grnCodes ?? "", | |||||
| }); | }); | ||||
| } | } | ||||
| return out; | return out; | ||||
| @@ -25,7 +25,7 @@ import { | |||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import DownloadIcon from '@mui/icons-material/Download'; | import DownloadIcon from '@mui/icons-material/Download'; | ||||
| import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | ||||
| import { REPORTS } from '@/config/reportConfig'; | |||||
| import { DEMO_UNAVAILABLE_REPORT_IDS, REPORTS } from '@/config/reportConfig'; | |||||
| import { mergePastedItemCodes, buildItemCodePasteRequestBody, parseItemCodeTokens } from './parseItemCodeTokens'; | import { mergePastedItemCodes, buildItemCodePasteRequestBody, parseItemCodeTokens } from './parseItemCodeTokens'; | ||||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | import { NEXT_PUBLIC_API_URL } from '@/config/api'; | ||||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | ||||
| @@ -152,6 +152,7 @@ export default function ReportPage() { | |||||
| [selectedReportId]); | [selectedReportId]); | ||||
| const handleSelectReport = (reportId: string) => { | const handleSelectReport = (reportId: string) => { | ||||
| if (DEMO_UNAVAILABLE_REPORT_IDS.has(reportId)) return; | |||||
| if (reportId === selectedReportId) return; | if (reportId === selectedReportId) return; | ||||
| setSelectedReportId(reportId); | setSelectedReportId(reportId); | ||||
| setFieldErrors({}); | setFieldErrors({}); | ||||
| @@ -159,7 +160,7 @@ export default function ReportPage() { | |||||
| if (reportId === 'rep-010') { | if (reportId === 'rep-010') { | ||||
| updateCriteria({ qcType: 'all', qcItemScope: 'all' }); | updateCriteria({ qcType: 'all', qcItemScope: 'all' }); | ||||
| } else if (reportId === 'rep-004') { | } else if (reportId === 'rep-004') { | ||||
| updateCriteria({ storeId: 'All', poPrefix: 'All' }); | |||||
| updateCriteria({ storeId: 'All' }); | |||||
| } else if (reportId === 'rep-021') { | } else if (reportId === 'rep-021') { | ||||
| updateCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' }); | updateCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' }); | ||||
| } else { | } else { | ||||
| @@ -452,6 +453,7 @@ export default function ReportPage() { | |||||
| const executeExcelReport = async () => { | const executeExcelReport = async () => { | ||||
| if (!currentReport) return; | if (!currentReport) return; | ||||
| if (DEMO_UNAVAILABLE_REPORT_IDS.has(currentReport.id)) return; | |||||
| if (excelInFlightRef.current) return; | if (excelInFlightRef.current) return; | ||||
| excelInFlightRef.current = true; | excelInFlightRef.current = true; | ||||
| setLoading(true); | setLoading(true); | ||||
| @@ -727,7 +729,7 @@ export default function ReportPage() { | |||||
| <Grid item {...gridSize} key={fieldKey}> | <Grid item {...gridSize} key={fieldKey}> | ||||
| <AsyncItemCodeAutocomplete | <AsyncItemCodeAutocomplete | ||||
| label={translatedLabel} | label={translatedLabel} | ||||
| placeholder={field.placeholder || "e.g. FA0591"} | |||||
| placeholder={field.placeholder || "e.g. DEMO-FG-01"} | |||||
| value={selectedCodes} | value={selectedCodes} | ||||
| onInputValueChange={(text) => { | onInputValueChange={(text) => { | ||||
| pendingItemCodeInputRef.current[field.name] = text; | pendingItemCodeInputRef.current[field.name] = text; | ||||
| @@ -37,8 +37,6 @@ import { | |||||
| type LaserBag2AutoSendReport, | type LaserBag2AutoSendReport, | ||||
| type LaserLastReceiveSuccess, | type LaserLastReceiveSuccess, | ||||
| } from "@/app/api/laserPrint/actions"; | } from "@/app/api/laserPrint/actions"; | ||||
| import * as XLSX from "xlsx"; | |||||
| interface TabPanelProps { | interface TabPanelProps { | ||||
| children?: React.ReactNode; | children?: React.ReactNode; | ||||
| index: number; | index: number; | ||||
| @@ -175,49 +173,6 @@ export default function TestingPage() { | |||||
| }; | }; | ||||
| }, [tabValue]); | }, [tabValue]); | ||||
| const handleDownloadGrnPreviewXlsx = async () => { | |||||
| try { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/report/grn-preview-m18?receiptDate=${encodeURIComponent( | |||||
| grnPreviewReceiptDate, | |||||
| )}`, | |||||
| { method: "GET" }, | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (!response.ok) throw new Error(`Download failed: ${response.status}`); | |||||
| const data = await response.json(); | |||||
| const rows = Array.isArray(data?.rows) ? data.rows : []; | |||||
| const ws = XLSX.utils.json_to_sheet(rows); | |||||
| const wb = XLSX.utils.book_new(); | |||||
| XLSX.utils.book_append_sheet(wb, ws, "GRN Preview"); | |||||
| const xlsxArrayBuffer = XLSX.write(wb, { | |||||
| bookType: "xlsx", | |||||
| type: "array", | |||||
| }); | |||||
| const blob = new Blob([xlsxArrayBuffer], { | |||||
| type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |||||
| }); | |||||
| const url = window.URL.createObjectURL(blob); | |||||
| const link = document.createElement("a"); | |||||
| link.href = url; | |||||
| link.setAttribute( | |||||
| "download", | |||||
| `grn-preview-m18-${grnPreviewReceiptDate}.xlsx`, | |||||
| ); | |||||
| document.body.appendChild(link); | |||||
| link.click(); | |||||
| link.remove(); | |||||
| window.URL.revokeObjectURL(url); | |||||
| } catch (e) { | |||||
| console.error("GRN Preview XLSX Download Error:", e); | |||||
| alert("GRN Preview XLSX download failed. Check console/network."); | |||||
| } | |||||
| }; | |||||
| const downloadBlob = (blob: Blob, filename: string) => { | const downloadBlob = (blob: Blob, filename: string) => { | ||||
| const url = window.URL.createObjectURL(blob); | const url = window.URL.createObjectURL(blob); | ||||
| const link = document.createElement("a"); | const link = document.createElement("a"); | ||||
| @@ -524,33 +479,35 @@ export default function TestingPage() { | |||||
| </Tabs> | </Tabs> | ||||
| <TabPanel value={tabValue} index={0}> | <TabPanel value={tabValue} index={0}> | ||||
| <Section title="1. GRN Preview (M18)"> | |||||
| <Stack | |||||
| direction="row" | |||||
| spacing={2} | |||||
| sx={{ mb: 2, alignItems: "center" }} | |||||
| > | |||||
| <TextField | |||||
| size="small" | |||||
| label="Receipt Date" | |||||
| type="date" | |||||
| value={grnPreviewReceiptDate} | |||||
| onChange={(e) => setGrnPreviewReceiptDate(e.target.value)} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| /> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="success" | |||||
| size="medium" | |||||
| startIcon={<FileDownload />} | |||||
| onClick={handleDownloadGrnPreviewXlsx} | |||||
| <Section title="1. GRN Preview"> | |||||
| <Box sx={{ opacity: 0.55, filter: "grayscale(1)", pointerEvents: "none" }}> | |||||
| <Stack | |||||
| direction="row" | |||||
| spacing={2} | |||||
| sx={{ mb: 2, alignItems: "center" }} | |||||
| > | > | ||||
| Download GRN Preview XLSX | |||||
| </Button> | |||||
| </Stack> | |||||
| <Typography variant="body2" color="textSecondary"> | |||||
| Backend endpoint:{" "} | |||||
| <code>/report/grn-preview-m18?receiptDate=YYYY-MM-DD</code> | |||||
| <TextField | |||||
| size="small" | |||||
| label="Receipt Date" | |||||
| type="date" | |||||
| value={grnPreviewReceiptDate} | |||||
| onChange={(e) => setGrnPreviewReceiptDate(e.target.value)} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| disabled | |||||
| /> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="success" | |||||
| size="medium" | |||||
| startIcon={<FileDownload />} | |||||
| disabled | |||||
| > | |||||
| Download GRN Preview XLSX | |||||
| </Button> | |||||
| </Stack> | |||||
| </Box> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| Unavailable in demo mode | |||||
| </Typography> | </Typography> | ||||
| </Section> | </Section> | ||||
| </TabPanel> | </TabPanel> | ||||
| @@ -560,7 +517,7 @@ export default function TestingPage() { | |||||
| <Alert severity="info" sx={{ mb: 2 }}> | <Alert severity="info" sx={{ mb: 2 }}> | ||||
| Uses <strong>GET /py/job-orders?planStart=</strong> for the day, | Uses <strong>GET /py/job-orders?planStart=</strong> for the day, | ||||
| then the same <code>jobOrders</code> payload as{" "} | then the same <code>jobOrders</code> payload as{" "} | ||||
| <strong>Bag Print → 下載 OnPack2023檸檬機</strong>. The ZIP contains | |||||
| <strong>Bag Print → 下載 OnPack 文字</strong>. The ZIP contains | |||||
| loose <code>.job</code> / <code>.image</code> / BMPs — extract | loose <code>.job</code> / <code>.image</code> / BMPs — extract | ||||
| before sending to NGE; the ZIP itself is only a transport bundle. | before sending to NGE; the ZIP itself is only a transport bundle. | ||||
| </Alert> | </Alert> | ||||
| @@ -973,12 +930,9 @@ export default function TestingPage() { | |||||
| <TabPanel value={tabValue} index={5}> | <TabPanel value={tabValue} index={5}> | ||||
| <Section title="6. M18 sync alert email"> | <Section title="6. M18 sync alert email"> | ||||
| <Alert severity="info" sx={{ mb: 2 }}> | <Alert severity="info" sx={{ mb: 2 }}> | ||||
| Production sync errors email{" "} | |||||
| <strong>[email protected]</strong> and{" "} | |||||
| <strong>[email protected]</strong> (see{" "} | |||||
| <code>scheduler.sync-alert.email.to-addresses</code>). WhatsApp/Twilio | |||||
| is disabled. SMTP from DB <code>MAIL.smtp.*</code> (e.g. Gmail{" "} | |||||
| <code>[email protected]</code> + app password). | |||||
| Production sync errors email the addresses in{" "} | |||||
| <code>scheduler.sync-alert.email.to-addresses</code>. WhatsApp/Twilio | |||||
| is disabled. SMTP comes from DB <code>MAIL.smtp.*</code>. | |||||
| </Alert> | </Alert> | ||||
| <Stack spacing={2} sx={{ mb: 2, maxWidth: 720 }}> | <Stack spacing={2} sx={{ mb: 2, maxWidth: 720 }}> | ||||
| <TextField | <TextField | ||||
| @@ -180,7 +180,7 @@ function skippedWithoutExpiryFromResponse(res: Response): string[] { | |||||
| .filter(Boolean); | .filter(Boolean); | ||||
| } | } | ||||
| /** 汁水機 OnPack — same as QR ZIP, plus LOGO_EXP BMP from item_default_shelf_life. */ | |||||
| /** OnPack QR — same as QR ZIP, plus LOGO_EXP BMP from item_default_shelf_life. */ | |||||
| export async function downloadOnPackQrZipWithExpiry( | export async function downloadOnPackQrZipWithExpiry( | ||||
| request: OnPackQrDownloadRequest, | request: OnPackQrDownloadRequest, | ||||
| ): Promise<OnPackZipDownload> { | ): Promise<OnPackZipDownload> { | ||||
| @@ -201,7 +201,7 @@ export async function downloadOnPackQrZipWithExpiry( | |||||
| }; | }; | ||||
| } | } | ||||
| /** OnPack2023 檸檬機 — text QR template (`onpack2030_2`), no separate .bmp */ | |||||
| /** OnPack text QR template (`onpack2030_2`), no separate .bmp */ | |||||
| export async function downloadOnPackTextQrZip( | export async function downloadOnPackTextQrZip( | ||||
| request: OnPackQrDownloadRequest, | request: OnPackQrDownloadRequest, | ||||
| ): Promise<Blob> { | ): Promise<Blob> { | ||||
| @@ -219,7 +219,7 @@ export async function downloadOnPackTextQrZip( | |||||
| return res.blob(); | return res.blob(); | ||||
| } | } | ||||
| /** OnPack2023 檸檬機 — same as text ZIP, plus TEXT_EXP from item_default_shelf_life. */ | |||||
| /** OnPack text QR — same as text ZIP, plus TEXT_EXP from item_default_shelf_life. */ | |||||
| export async function downloadOnPackTextQrZipWithExpiry( | export async function downloadOnPackTextQrZipWithExpiry( | ||||
| request: OnPackQrDownloadRequest, | request: OnPackQrDownloadRequest, | ||||
| ): Promise<OnPackZipDownload> { | ): Promise<OnPackZipDownload> { | ||||
| @@ -82,15 +82,23 @@ const SETTINGS_KEY = "bagPrint_settings"; | |||||
| const DEFAULT_SETTINGS = { | const DEFAULT_SETTINGS = { | ||||
| dabag_ip: "", | dabag_ip: "", | ||||
| dabag_port: "3008", | dabag_port: "3008", | ||||
| laser_ip: "192.168.17.10", | |||||
| laser_ip: "", | |||||
| laser_port: "45678", | laser_port: "45678", | ||||
| }; | }; | ||||
| const LIVE_PRINTER_HOSTS = new Set(["192.168.17.10", "192.168.18.77"]); | |||||
| function loadSettings(): typeof DEFAULT_SETTINGS { | function loadSettings(): typeof DEFAULT_SETTINGS { | ||||
| if (typeof window === "undefined") return DEFAULT_SETTINGS; | if (typeof window === "undefined") return DEFAULT_SETTINGS; | ||||
| try { | try { | ||||
| const s = localStorage.getItem(SETTINGS_KEY); | const s = localStorage.getItem(SETTINGS_KEY); | ||||
| if (s) return { ...DEFAULT_SETTINGS, ...JSON.parse(s) }; | |||||
| if (s) { | |||||
| const merged = { ...DEFAULT_SETTINGS, ...JSON.parse(s) }; | |||||
| if (LIVE_PRINTER_HOSTS.has(String(merged.laser_ip ?? "").trim())) { | |||||
| merged.laser_ip = ""; | |||||
| } | |||||
| return merged; | |||||
| } | |||||
| } catch {} | } catch {} | ||||
| return DEFAULT_SETTINGS; | return DEFAULT_SETTINGS; | ||||
| } | } | ||||
| @@ -467,11 +475,11 @@ const BagPrintSearch: React.FC = () => { | |||||
| link.remove(); | link.remove(); | ||||
| window.URL.revokeObjectURL(url); | window.URL.revokeObjectURL(url); | ||||
| setSnackbar(skippedExpirySnackbar("OnPack 汁水機(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||||
| setSnackbar(skippedExpirySnackbar("OnPack QR(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||||
| } catch (e) { | } catch (e) { | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: e instanceof Error ? e.message : "下載 OnPack 汁水機(含到期日)失敗", | |||||
| message: e instanceof Error ? e.message : "下載 OnPack QR(含到期日)失敗", | |||||
| severity: "error", | severity: "error", | ||||
| }); | }); | ||||
| } finally { | } finally { | ||||
| @@ -505,11 +513,11 @@ const BagPrintSearch: React.FC = () => { | |||||
| link.remove(); | link.remove(); | ||||
| window.URL.revokeObjectURL(url); | window.URL.revokeObjectURL(url); | ||||
| setSnackbar({ open: true, message: "OnPack2023檸檬機 ZIP 已下載", severity: "success" }); | |||||
| setSnackbar({ open: true, message: "OnPack 文字 ZIP 已下載", severity: "success" }); | |||||
| } catch (e) { | } catch (e) { | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: e instanceof Error ? e.message : "下載 OnPack2023檸檬機 失敗", | |||||
| message: e instanceof Error ? e.message : "下載 OnPack 文字 失敗", | |||||
| severity: "error", | severity: "error", | ||||
| }); | }); | ||||
| } finally { | } finally { | ||||
| @@ -544,11 +552,11 @@ const BagPrintSearch: React.FC = () => { | |||||
| link.remove(); | link.remove(); | ||||
| window.URL.revokeObjectURL(url); | window.URL.revokeObjectURL(url); | ||||
| setSnackbar(skippedExpirySnackbar("OnPack2023檸檬機(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||||
| setSnackbar(skippedExpirySnackbar("OnPack 文字(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||||
| } catch (e) { | } catch (e) { | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: e instanceof Error ? e.message : "下載 OnPack2023檸檬機(含到期日)失敗", | |||||
| message: e instanceof Error ? e.message : "下載 OnPack 文字(含到期日)失敗", | |||||
| severity: "error", | severity: "error", | ||||
| }); | }); | ||||
| } finally { | } finally { | ||||
| @@ -597,7 +605,7 @@ const BagPrintSearch: React.FC = () => { | |||||
| ), | ), | ||||
| ); | ); | ||||
| } catch { | } catch { | ||||
| /* 檸檬機標籤可沒有;不擋畫面 */ | |||||
| /* 激光機標籤可沒有;不擋畫面 */ | |||||
| } | } | ||||
| })(); | })(); | ||||
| }, []); | }, []); | ||||
| @@ -793,7 +801,7 @@ const BagPrintSearch: React.FC = () => { | |||||
| jobOrders.length === 0 | jobOrders.length === 0 | ||||
| } | } | ||||
| > | > | ||||
| {downloadingOnPack ? "下載中..." : "下載 OnPack 汁水機 QR code"} | |||||
| {downloadingOnPack ? "下載中..." : "下載 OnPack QR code"} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| @@ -808,7 +816,7 @@ const BagPrintSearch: React.FC = () => { | |||||
| jobOrders.length === 0 | jobOrders.length === 0 | ||||
| } | } | ||||
| > | > | ||||
| {downloadingOnPackExp ? "下載中..." : "下載 OnPack 汁水機(含到期日)"} | |||||
| {downloadingOnPackExp ? "下載中..." : "下載 OnPack QR(含到期日)"} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| @@ -824,7 +832,7 @@ const BagPrintSearch: React.FC = () => { | |||||
| jobOrders.length === 0 | jobOrders.length === 0 | ||||
| } | } | ||||
| > | > | ||||
| {downloadingOnPackText ? "下載中..." : "下載 OnPack2023檸檬機"} | |||||
| {downloadingOnPackText ? "下載中..." : "下載 OnPack 文字"} | |||||
| </Button> | </Button> | ||||
| {canSeeOnPackAdmin && ( | {canSeeOnPackAdmin && ( | ||||
| <Button | <Button | ||||
| @@ -841,7 +849,7 @@ const BagPrintSearch: React.FC = () => { | |||||
| jobOrders.length === 0 | jobOrders.length === 0 | ||||
| } | } | ||||
| > | > | ||||
| {downloadingOnPackTextExp ? "下載中..." : "下載 OnPack2023檸檬機(含到期日)"} | |||||
| {downloadingOnPackTextExp ? "下載中..." : "下載 OnPack 文字(含到期日)"} | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| @@ -903,8 +911,8 @@ const BagPrintSearch: React.FC = () => { | |||||
| {jo.itemCode || "—"} | {jo.itemCode || "—"} | ||||
| </Typography> | </Typography> | ||||
| <Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap sx={{ mt: 0.5 }}> | <Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap sx={{ mt: 0.5 }}> | ||||
| {juiceExpiryOk ? <Chip size="small" label="汁水機" color="primary" /> : null} | |||||
| {lemonOk ? <Chip size="small" label="檸檬機" color="secondary" /> : null} | |||||
| {juiceExpiryOk ? <Chip size="small" label="OnPack QR" color="primary" /> : null} | |||||
| {lemonOk ? <Chip size="small" label="激光機" color="secondary" /> : null} | |||||
| </Stack> | </Stack> | ||||
| </Box> | </Box> | ||||
| <Box sx={{ flex: 1, minWidth: 0 }}> | <Box sx={{ flex: 1, minWidth: 0 }}> | ||||
| @@ -1086,17 +1094,17 @@ const BagPrintSearch: React.FC = () => { | |||||
| > | > | ||||
| <Stack spacing={1.5} sx={{ flexShrink: 0, mb: 1 }}> | <Stack spacing={1.5} sx={{ flexShrink: 0, mb: 1 }}> | ||||
| <Typography variant="subtitle2" color="primary"> | <Typography variant="subtitle2" color="primary"> | ||||
| 汁水機({expiryCodes.length}) | |||||
| OnPack QR({expiryCodes.length}) | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| 「下載 OnPack 汁水機(含到期日)」只用此清單。名稱印在 Product 行,過長可改短後按儲存。 | |||||
| 「下載 OnPack QR(含到期日)」只用此清單。名稱印在 Product 行,過長可改短後按儲存。 | |||||
| 點欄位標題可排序;冷藏/-18/列印第一次點擊會把「未設」排到最前。 | 點欄位標題可排序;冷藏/-18/列印第一次點擊會把「未設」排到最前。 | ||||
| </Typography> | </Typography> | ||||
| <Stack direction="row" spacing={1} alignItems="center"> | <Stack direction="row" spacing={1} alignItems="center"> | ||||
| <TextField | <TextField | ||||
| label="新增品號" | label="新增品號" | ||||
| size="small" | size="small" | ||||
| placeholder="例如 PP2211" | |||||
| placeholder="例如 DEMO-FG-01" | |||||
| value={expiryCodeInput} | value={expiryCodeInput} | ||||
| onChange={(e) => setExpiryCodeInput(e.target.value)} | onChange={(e) => setExpiryCodeInput(e.target.value)} | ||||
| onKeyDown={(e) => { | onKeyDown={(e) => { | ||||
| @@ -1216,7 +1224,7 @@ const BagPrintSearch: React.FC = () => { | |||||
| onChange={(e) => | onChange={(e) => | ||||
| setNameDrafts((prev) => ({ ...prev, [row.itemCode]: e.target.value })) | setNameDrafts((prev) => ({ ...prev, [row.itemCode]: e.target.value })) | ||||
| } | } | ||||
| placeholder={row.defaultPrintName || "例如 豬肉水(1包X2千克)"} | |||||
| placeholder={row.defaultPrintName || "例如 鄉村麵包"} | |||||
| inputProps={{ maxLength: 255 }} | inputProps={{ maxLength: 255 }} | ||||
| fullWidth | fullWidth | ||||
| /> | /> | ||||
| @@ -1292,7 +1300,7 @@ const BagPrintSearch: React.FC = () => { | |||||
| </TableContainer> | </TableContainer> | ||||
| )} | )} | ||||
| <Typography variant="body2" color="text.secondary" sx={{ flexShrink: 0, mt: 1 }}> | <Typography variant="body2" color="text.secondary" sx={{ flexShrink: 0, mt: 1 }}> | ||||
| 檸檬機到期日 ZIP 品號稍後加入。 | |||||
| 激光機到期日 ZIP 品號稍後加入。 | |||||
| </Typography> | </Typography> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -358,8 +358,8 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||||
| onChange={onFloorSelectChange} | onChange={onFloorSelectChange} | ||||
| disabled={dialogSaving || comboLoading} | disabled={dialogSaving || comboLoading} | ||||
| > | > | ||||
| <MenuItem value="2F">2F</MenuItem> | |||||
| <MenuItem value="4F">4F</MenuItem> | |||||
| <MenuItem value="2F">2/F</MenuItem> | |||||
| <MenuItem value="4F">4/F</MenuItem> | |||||
| </Select> | </Select> | ||||
| </FormControl> | </FormControl> | ||||
| <Button | <Button | ||||
| @@ -83,21 +83,27 @@ const LaserPrintSearch: React.FC = () => { | |||||
| message: "", | message: "", | ||||
| }); | }); | ||||
| const [successSignal, setSuccessSignal] = useState<string | null>(null); | const [successSignal, setSuccessSignal] = useState<string | null>(null); | ||||
| const [laserHost, setLaserHost] = useState("192.168.18.77"); | |||||
| const [laserHost, setLaserHost] = useState(""); | |||||
| const [laserPort, setLaserPort] = useState("45678"); | const [laserPort, setLaserPort] = useState("45678"); | ||||
| const [laserItemCodes, setLaserItemCodes] = useState("PP1175"); | |||||
| const [laserItemCodes, setLaserItemCodes] = useState(""); | |||||
| const [lastLaserReceive, setLastLaserReceive] = useState<LaserLastReceiveSuccess | null>(null); | const [lastLaserReceive, setLastLaserReceive] = useState<LaserLastReceiveSuccess | null>(null); | ||||
| const [settingsLoaded, setSettingsLoaded] = useState(false); | const [settingsLoaded, setSettingsLoaded] = useState(false); | ||||
| const [printerConnected, setPrinterConnected] = useState(false); | const [printerConnected, setPrinterConnected] = useState(false); | ||||
| const [printerMessage, setPrinterMessage] = useState("檸檬機(激光機)未連接"); | |||||
| const [printerMessage, setPrinterMessage] = useState("激光機未連接"); | |||||
| const sendInFlightRef = useRef(false); | const sendInFlightRef = useRef(false); | ||||
| const loadSystemSettings = useCallback(async () => { | const loadSystemSettings = useCallback(async () => { | ||||
| try { | try { | ||||
| const s = await fetchLaserBag2Settings(); | const s = await fetchLaserBag2Settings(); | ||||
| setLaserHost(s.host); | |||||
| const host = (s.host ?? "").trim(); | |||||
| setLaserHost(host === "192.168.18.77" || host === "192.168.17.10" ? "" : host); | |||||
| setLaserPort(String(s.port)); | setLaserPort(String(s.port)); | ||||
| setLaserItemCodes(s.itemCodes ?? "PP1175"); | |||||
| const itemCodes = (s.itemCodes ?? "") | |||||
| .split(",") | |||||
| .map((code) => code.trim()) | |||||
| .filter((code) => code && code !== "PP1175" && code !== "PP2211") | |||||
| .join(","); | |||||
| setLaserItemCodes(itemCodes); | |||||
| setLastLaserReceive(s.lastReceiveSuccess ?? null); | setLastLaserReceive(s.lastReceiveSuccess ?? null); | ||||
| setSettingsLoaded(true); | setSettingsLoaded(true); | ||||
| } catch (e) { | } catch (e) { | ||||
| @@ -158,7 +164,7 @@ const LaserPrintSearch: React.FC = () => { | |||||
| setPrinterMessage(result.message); | setPrinterMessage(result.message); | ||||
| } catch (e) { | } catch (e) { | ||||
| setPrinterConnected(false); | setPrinterConnected(false); | ||||
| setPrinterMessage(e instanceof Error ? e.message : "檸檬機(激光機)狀態檢查失敗"); | |||||
| setPrinterMessage(e instanceof Error ? e.message : "激光機狀態檢查失敗"); | |||||
| } | } | ||||
| }, []); | }, []); | ||||
| @@ -201,7 +207,7 @@ const LaserPrintSearch: React.FC = () => { | |||||
| if (sendInFlightRef.current || sendingJobId !== null) return; | if (sendInFlightRef.current || sendingJobId !== null) return; | ||||
| if (!laserHost.trim()) { | if (!laserHost.trim()) { | ||||
| setErrorSnackbar({ open: true, message: "請在系統設定中填寫檸檬機(激光機) IP。" }); | |||||
| setErrorSnackbar({ open: true, message: "請在系統設定中填寫激光機 IP。" }); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -239,8 +245,8 @@ const LaserPrintSearch: React.FC = () => { | |||||
| : ""; | : ""; | ||||
| setSuccessSignal( | setSuccessSignal( | ||||
| laterFail | laterFail | ||||
| ? `已送出 ${sentOk} 次至檸檬機(激光機)${ackHint}(後續重送失敗:${laterFail})` | |||||
| : `已送出 ${sentOk} 次至檸檬機(激光機)${ackHint}`, | |||||
| ? `已送出 ${sentOk} 次至激光機${ackHint}(後續重送失敗:${laterFail})` | |||||
| : `已送出 ${sentOk} 次至激光機${ackHint}`, | |||||
| ); | ); | ||||
| await loadSystemSettings(); | await loadSystemSettings(); | ||||
| } catch (e) { | } catch (e) { | ||||
| @@ -287,7 +293,7 @@ const LaserPrintSearch: React.FC = () => { | |||||
| {lastLaserReceive.source ? ` (${lastLaserReceive.source === "AUTO" ? "自動送出" : "手動點選"})` : ""} | {lastLaserReceive.source ? ` (${lastLaserReceive.source === "AUTO" ? "自動送出" : "手動點選"})` : ""} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" sx={{ mt: 0.5 }}> | <Typography variant="body2" sx={{ mt: 0.5 }}> | ||||
| 此時間只會在檸檬機回覆 receive 時更新。之後送出失敗不會改這裡。 | |||||
| 此時間只會在激光機回覆 receive 時更新。之後送出失敗不會改這裡。 | |||||
| </Typography> | </Typography> | ||||
| </Alert> | </Alert> | ||||
| )} | )} | ||||
| @@ -332,7 +338,7 @@ const LaserPrintSearch: React.FC = () => { | |||||
| }} | }} | ||||
| title={printerMessage} | title={printerMessage} | ||||
| > | > | ||||
| 檸檬機(激光機): | |||||
| 激光機: | |||||
| </Box> | </Box> | ||||
| </Stack> | </Stack> | ||||
| </Stack> | </Stack> | ||||
| @@ -420,11 +426,11 @@ const LaserPrintSearch: React.FC = () => { | |||||
| </Paper> | </Paper> | ||||
| <Dialog open={settingsOpen} onClose={() => setSettingsOpen(false)} maxWidth="sm" fullWidth> | <Dialog open={settingsOpen} onClose={() => setSettingsOpen(false)} maxWidth="sm" fullWidth> | ||||
| <DialogTitle>檸檬機(激光機)(系統設定)</DialogTitle> | |||||
| <DialogTitle>激光機(系統設定)</DialogTitle> | |||||
| <DialogContent> | <DialogContent> | ||||
| <Stack spacing={2} sx={{ mt: 1 }}> | <Stack spacing={2} sx={{ mt: 1 }}> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| 儲存後寫入資料庫,後端送出走此 IP/埠(預設 192.168.18.77:45678)。 | |||||
| 儲存後寫入資料庫,後端送出走此 IP/埠。未填 IP 則不會送出。 | |||||
| </Typography> | </Typography> | ||||
| <TextField | <TextField | ||||
| label="IP" | label="IP" | ||||
| @@ -446,8 +452,8 @@ const LaserPrintSearch: React.FC = () => { | |||||
| value={laserItemCodes} | value={laserItemCodes} | ||||
| onChange={(e) => setLaserItemCodes(e.target.value)} | onChange={(e) => setLaserItemCodes(e.target.value)} | ||||
| fullWidth | fullWidth | ||||
| placeholder="PP1175" | |||||
| helperText="預設 PP1175;可輸入多個品號,例如 PP1175,AB999。留空則列表顯示當日全部包裝工單。" | |||||
| placeholder="例如 DEMO-FG-01" | |||||
| helperText="可輸入多個品號,例如 DEMO-FG-01,DEMO-FG-02。留空則列表顯示當日全部包裝工單。" | |||||
| /> | /> | ||||
| </Stack> | </Stack> | ||||
| </DialogContent> | </DialogContent> | ||||
| @@ -116,7 +116,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||||
| group: "default", | group: "default", | ||||
| }; | }; | ||||
| const options = useMemo(() => { | const options = useMemo(() => { | ||||
| const defaultLabel = suggestedLocationCode || t("W201 - 2F-A,B室"); | |||||
| const defaultLabel = suggestedLocationCode || t("Select warehouse"); | |||||
| return [ | return [ | ||||
| { value: 1, label: defaultLabel, group: "default" }, | { value: 1, label: defaultLabel, group: "default" }, | ||||
| ...filteredWarehouse.map((w) => ({ | ...filteredWarehouse.map((w) => ({ | ||||
| @@ -260,7 +260,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||||
| editable: false, | editable: false, | ||||
| renderCell(params) { | renderCell(params) { | ||||
| const value = (params.value as string) ?? ""; | const value = (params.value as string) ?? ""; | ||||
| // 目前格式像 "2F-W201-#L-08 - 2F-W201",只要左邊 LocationCode | |||||
| // Location label is "code - description"; keep the code on the left. | |||||
| const locationCode = value.split(" - ")[0] || value; | const locationCode = value.split(" - ")[0] || value; | ||||
| return ( | return ( | ||||
| @@ -5,72 +5,67 @@ export const PO_WORKBENCH_DETAILS_GRID_MOCK_ROWS: readonly PoWorkbenchDetailsGri | |||||
| [ | [ | ||||
| { | { | ||||
| id: "mock-line-1", | id: "mock-line-1", | ||||
| itemName: | |||||
| "澳洲和牛腱(冷凍)· A5級去筋切片裝 2.5kg×4包/箱(供應商批次:AU-WG-2026-Q1)", | |||||
| itemCode: "AB1234", | |||||
| itemName: "普通麵粉", | |||||
| itemCode: "DEMO-RM-01", | |||||
| category: "material", | category: "material", | ||||
| storageLocation: "YF-W201-#A-12", | |||||
| storageLocation: "DEMO-A-#A-01", | |||||
| receiveStatus: "pending", | receiveStatus: "pending", | ||||
| expectedQty: 100, | expectedQty: 100, | ||||
| purchaseUnitLabel: "1箱X100包X150克", | |||||
| purchaseUnitLabel: "1袋X25公斤", | |||||
| receivedQtyPurchase: 0, | receivedQtyPurchase: 0, | ||||
| receivedQtyInventory: 0, | receivedQtyInventory: 0, | ||||
| inventoryUnitLabel: "克", | |||||
| inventoryUnitLabel: "公斤", | |||||
| }, | }, | ||||
| { | { | ||||
| id: "mock-line-2", | id: "mock-line-2", | ||||
| itemName: | |||||
| "耐高温真空膠袋(食品級)· 300×450mm 厚度80μm 每卷500個(雙層密封條)", | |||||
| itemCode: "BC5678", | |||||
| itemName: "鮮奶", | |||||
| itemCode: "DEMO-RM-02", | |||||
| category: "consumable", | category: "consumable", | ||||
| storageLocation: "YF-C305-#B-04", | |||||
| storageLocation: "DEMO-B-#B-02", | |||||
| receiveStatus: "receiving", | receiveStatus: "receiving", | ||||
| expectedQty: 24, | expectedQty: 24, | ||||
| purchaseUnitLabel: "1箱X24卷X500個", | |||||
| receivedQtyPurchase: 26, | |||||
| receivedQtyInventory: 312000, | |||||
| inventoryUnitLabel: "個", | |||||
| purchaseUnitLabel: "1箱X12瓶X1升", | |||||
| receivedQtyPurchase: 12, | |||||
| receivedQtyInventory: 12, | |||||
| inventoryUnitLabel: "升", | |||||
| }, | }, | ||||
| { | { | ||||
| id: "mock-line-3", | id: "mock-line-3", | ||||
| itemName: | |||||
| "照燒汁(商用濃縮裝)· 日式醬油基底 5L/桶(開封後需冷藏,效期見標籤)", | |||||
| itemCode: "FG9012", | |||||
| itemName: "鄉村麵包", | |||||
| itemCode: "DEMO-FG-01", | |||||
| category: "finishedGoods", | category: "finishedGoods", | ||||
| storageLocation: "YF-D102-#C-08", | |||||
| storageLocation: "DEMO-A-#C-03", | |||||
| receiveStatus: "completed", | receiveStatus: "completed", | ||||
| expectedQty: 48, | expectedQty: 48, | ||||
| purchaseUnitLabel: "1板X4桶X5升", | |||||
| purchaseUnitLabel: "1箱X8個", | |||||
| receivedQtyPurchase: 48, | receivedQtyPurchase: 48, | ||||
| receivedQtyInventory: 960, | |||||
| inventoryUnitLabel: "升", | |||||
| receivedQtyInventory: 48, | |||||
| inventoryUnitLabel: "個", | |||||
| }, | }, | ||||
| { | { | ||||
| id: "mock-line-4", | id: "mock-line-4", | ||||
| itemName: | |||||
| "高筋麵粉預拌漿(半成品)· 中央廚房標準配方 20kg/袋(含酵母改良劑)", | |||||
| itemCode: "DE3456", | |||||
| itemName: "麵團", | |||||
| itemCode: "DEMO-SF-01", | |||||
| category: "semiFinished", | category: "semiFinished", | ||||
| storageLocation: "YF-W118-#D-22", | |||||
| storageLocation: "DEMO-A-#D-04", | |||||
| receiveStatus: "receiving", | receiveStatus: "receiving", | ||||
| expectedQty: 60, | expectedQty: 60, | ||||
| purchaseUnitLabel: "1板X3袋X20公斤", | |||||
| receivedQtyPurchase: 55, | |||||
| receivedQtyInventory: 3300, | |||||
| purchaseUnitLabel: "1袋X10公斤", | |||||
| receivedQtyPurchase: 40, | |||||
| receivedQtyInventory: 40, | |||||
| inventoryUnitLabel: "公斤", | inventoryUnitLabel: "公斤", | ||||
| }, | }, | ||||
| { | { | ||||
| id: "mock-line-5", | id: "mock-line-5", | ||||
| itemName: | |||||
| "廚房多功能清潔劑(濃縮原液)· 無磷配方 1L×12瓶/箱(勿與漂白水混用)", | |||||
| itemCode: "EF7890", | |||||
| itemName: "奶包", | |||||
| itemCode: "DEMO-FG-02", | |||||
| category: "miscNonConsumable", | category: "miscNonConsumable", | ||||
| storageLocation: "YF-M402-#E-01", | |||||
| storageLocation: "DEMO-B-#E-05", | |||||
| receiveStatus: "pending", | receiveStatus: "pending", | ||||
| expectedQty: 120, | expectedQty: 120, | ||||
| purchaseUnitLabel: "1扎X3箱X12包X350克", | |||||
| purchaseUnitLabel: "1箱X20個", | |||||
| receivedQtyPurchase: 0, | receivedQtyPurchase: 0, | ||||
| receivedQtyInventory: 0, | receivedQtyInventory: 0, | ||||
| inventoryUnitLabel: "瓶", | |||||
| inventoryUnitLabel: "個", | |||||
| }, | }, | ||||
| ] as const; | ] as const; | ||||
| @@ -315,7 +315,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| const processQrCode = useCallback((qrValue: string, lineId: number) => { | const processQrCode = useCallback((qrValue: string, lineId: number) => { | ||||
| // 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码 | // 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码 | ||||
| // 格式:{2fitesteXXX} = equipmentCode: "XXX" | // 格式:{2fitesteXXX} = equipmentCode: "XXX" | ||||
| // 例如:{2fiteste包裝機類-真空八爪魚機-1號} = equipmentCode: "包裝機類-真空八爪魚機-1號" | |||||
| // 例如:{2fitesteDEMO-EQ-01} = equipmentCode: "DEMO-EQ-01" | |||||
| if (qrValue.match(/\{2fiteste(.+)\}/)) { | if (qrValue.match(/\{2fiteste(.+)\}/)) { | ||||
| const match = qrValue.match(/\{2fiteste(.+)\}/); | const match = qrValue.match(/\{2fiteste(.+)\}/); | ||||
| const equipmentCode = match![1]; | const equipmentCode = match![1]; | ||||
| @@ -371,7 +371,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| // 可能是员工编号 | // 可能是员工编号 | ||||
| setScannedStaffNo(trimmedValue); | setScannedStaffNo(trimmedValue); | ||||
| } else if (trimmedValue.includes("-")) { | } else if (trimmedValue.includes("-")) { | ||||
| // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") | |||||
| // 可能包含 "-" 的是设备代码(如 "DEMO-EQ-01") | |||||
| setScannedEquipmentCode(trimmedValue); | setScannedEquipmentCode(trimmedValue); | ||||
| } | } | ||||
| } | } | ||||
| @@ -33,6 +33,9 @@ export interface ReportField { | |||||
| export type ReportResponseType = 'pdf' | 'excel'; | export type ReportResponseType = 'pdf' | 'excel'; | ||||
| /** Live-system reports. The demo page shows them greyed out. */ | |||||
| export const DEMO_UNAVAILABLE_REPORT_IDS = new Set(["rep-015"]); | |||||
| export interface ReportDefinition { | export interface ReportDefinition { | ||||
| id: string; | id: string; | ||||
| title: string; | title: string; | ||||
| @@ -54,9 +57,20 @@ const asyncItemCodeField = ( | |||||
| multiple: true, | multiple: true, | ||||
| allowInput: true, | allowInput: true, | ||||
| asyncSearch: true, | asyncSearch: true, | ||||
| placeholder: "e.g. FA0591", | |||||
| placeholder: "e.g. DEMO-FG-01", | |||||
| }); | }); | ||||
| const floorOptionsAll: ReportField["options"] = [ | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "2/F", value: "2/F" }, | |||||
| { label: "4/F", value: "4/F" }, | |||||
| ]; | |||||
| const floorOptions: ReportField["options"] = [ | |||||
| { label: "2/F", value: "2/F" }, | |||||
| { label: "4/F", value: "4/F" }, | |||||
| ]; | |||||
| const pasteItemCodeField = (): ReportField => ({ | const pasteItemCodeField = (): ReportField => ({ | ||||
| label: "貼上貨品編號 Paste Item Codes", | label: "貼上貨品編號 Paste Item Codes", | ||||
| name: "itemCodePaste", | name: "itemCodePaste", | ||||
| @@ -124,29 +138,12 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| name: "storeId", | name: "storeId", | ||||
| type: "select", | type: "select", | ||||
| required: false, | required: false, | ||||
| options: [ | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "1F", value: "1F" }, | |||||
| { label: "2F", value: "2F" }, | |||||
| { label: "3F", value: "3F" }, | |||||
| { label: "4F", value: "4F" }, | |||||
| ], | |||||
| options: floorOptionsAll, | |||||
| }, | }, | ||||
| { label: "倉庫 Warehouse", name: "warehouse", type: "text", required: false, placeholder: "e.g. W201" }, | |||||
| { label: "倉庫 Warehouse", name: "warehouse", type: "text", required: false, placeholder: "e.g. DEMO-A" }, | |||||
| { label: "區域 Area", name: "area", type: "text", required: false, placeholder: "e.g. #A" }, | { label: "區域 Area", name: "area", type: "text", required: false, placeholder: "e.g. #A" }, | ||||
| { label: "儲位 Slot", name: "slot", type: "text", required: false, placeholder: "e.g. 01" }, | { label: "儲位 Slot", name: "slot", type: "text", required: false, placeholder: "e.g. 01" }, | ||||
| { label: "批號 Lot No", name: "lotNo", type: "text", required: false, placeholder: "e.g. LT-202608" }, | { label: "批號 Lot No", name: "lotNo", type: "text", required: false, placeholder: "e.g. LT-202608" }, | ||||
| { | |||||
| label: "PP/PF 分類", | |||||
| name: "poPrefix", | |||||
| type: "select", | |||||
| required: false, | |||||
| options: [ | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "PP", value: "PP" }, | |||||
| { label: "PF", value: "PF" }, | |||||
| ], | |||||
| }, | |||||
| ] | ] | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -193,13 +190,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| name: "store_id", | name: "store_id", | ||||
| type: "select", | type: "select", | ||||
| required: false, | required: false, | ||||
| options: [ | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "1F", value: "1F" }, | |||||
| { label: "2F", value: "2F" }, | |||||
| { label: "3F", value: "3F" }, | |||||
| { label: "4F", value: "4F" } | |||||
| ], | |||||
| options: floorOptionsAll, | |||||
| }, | }, | ||||
| { | { | ||||
| label: "狀態", | label: "狀態", | ||||
| @@ -220,14 +211,11 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| multiple: true, | multiple: true, | ||||
| options: [ | options: [ | ||||
| { label: "全部", value: "All" }, | { label: "全部", value: "All" }, | ||||
| { label: "PP", value: "PP" }, | |||||
| { label: "PF", value: "PF" }, | |||||
| { label: "TOA", value: "TOA" }, | |||||
| { label: "工廠生產", value: "工廠生產" }, | |||||
| { label: "倉存調整", value: "倉存調整" }, | |||||
| { label: "期初存貨", value: "期初存貨" }, | |||||
| { label: "採購入倉", value: "採購入倉" }, | |||||
| { label: "其他入倉", value: "其他入倉" }, | |||||
| { label: "採購", value: "purchase" }, | |||||
| { label: "生產", value: "production" }, | |||||
| { label: "倉存調整", value: "adjustment" }, | |||||
| { label: "期初存貨", value: "opening" }, | |||||
| { label: "其他", value: "other" }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| ] | ] | ||||
| @@ -241,19 +229,13 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| fields: [ | fields: [ | ||||
| { label: "庫存日期 Stock Date(僅今天)", name: "stockDate", type: "date", required: true, disabled: true }, | { label: "庫存日期 Stock Date(僅今天)", name: "stockDate", type: "date", required: true, disabled: true }, | ||||
| { label: "貨品編號 Item Code", name: "itemCode", type: "text", required: false }, | { label: "貨品編號 Item Code", name: "itemCode", type: "text", required: false }, | ||||
| { label: "倉位 Warehouse Code", name: "warehouseCode", type: "text", required: false, placeholder: "e.g. W200 or 2F-W200-#A-00" }, | |||||
| { label: "倉位 Warehouse Code", name: "warehouseCode", type: "text", required: false, placeholder: "e.g. DEMO-A" }, | |||||
| { | { | ||||
| label: "樓層 Store ID", | label: "樓層 Store ID", | ||||
| name: "storeId", | name: "storeId", | ||||
| type: "select", | type: "select", | ||||
| required: false, | required: false, | ||||
| options: [ | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "1F", value: "1F" }, | |||||
| { label: "2F", value: "2F" }, | |||||
| { label: "3F", value: "3F" }, | |||||
| { label: "4F", value: "4F" }, | |||||
| ], | |||||
| options: floorOptionsAll, | |||||
| }, | }, | ||||
| { label: "批號 Lot No", name: "lotNo", type: "text", required: false, placeholder: "e.g. LT-202608" }, | { label: "批號 Lot No", name: "lotNo", type: "text", required: false, placeholder: "e.g. LT-202608" }, | ||||
| ], | ], | ||||
| @@ -271,15 +253,9 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| name: "storeId", | name: "storeId", | ||||
| type: "select", | type: "select", | ||||
| required: false, | required: false, | ||||
| options: [ | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "1F", value: "1F" }, | |||||
| { label: "2F", value: "2F" }, | |||||
| { label: "3F", value: "3F" }, | |||||
| { label: "4F", value: "4F" }, | |||||
| ], | |||||
| options: floorOptionsAll, | |||||
| }, | }, | ||||
| { label: "倉庫 Warehouse", name: "warehouse", type: "text", required: false, placeholder: "e.g. W201" }, | |||||
| { label: "倉庫 Warehouse", name: "warehouse", type: "text", required: false, placeholder: "e.g. DEMO-A" }, | |||||
| { label: "區域 Area", name: "area", type: "text", required: false, placeholder: "e.g. #A" }, | { label: "區域 Area", name: "area", type: "text", required: false, placeholder: "e.g. #A" }, | ||||
| { label: "儲位 Slot", name: "slot", type: "text", required: false, placeholder: "e.g. 02" }, | { label: "儲位 Slot", name: "slot", type: "text", required: false, placeholder: "e.g. 02" }, | ||||
| { | { | ||||
| @@ -299,8 +275,8 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| required: false, | required: false, | ||||
| options: [ | options: [ | ||||
| { label: "全部", value: "All" }, | { label: "全部", value: "All" }, | ||||
| { label: "PP", value: "PP" }, | |||||
| { label: "PF", value: "PF" }, | |||||
| { label: "採購", value: "purchase" }, | |||||
| { label: "生產", value: "production" }, | |||||
| { label: "其他", value: "other" }, | { label: "其他", value: "other" }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| @@ -442,12 +418,10 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "類別 Category", name: "stockCategory", type: "select", required: false, | { label: "類別 Category", name: "stockCategory", type: "select", required: false, | ||||
| multiple: true, | multiple: true, | ||||
| options: [ | options: [ | ||||
| { label: "All", value: "All" }, | |||||
| { label: "MAT", value: "MAT" }, | |||||
| { label: "WIP", value: "WIP" }, | |||||
| { label: "NM", value: "NM" }, | |||||
| { label: "FG", value: "FG" }, | |||||
| { label: "CMB", value: "CMB" } | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "原料", value: "mat" }, | |||||
| { label: "半成品", value: "sfg" }, | |||||
| { label: "成品", value: "fg" }, | |||||
| ] }, | ] }, | ||||
| { label: "貨品編號 Item Code", name: "itemCode", type: "select", required: false, | { label: "貨品編號 Item Code", name: "itemCode", type: "select", required: false, | ||||
| multiple: true, | multiple: true, | ||||
| @@ -472,9 +446,9 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "類別 Category", name: "stockCategory", type: "select", required: false, | { label: "類別 Category", name: "stockCategory", type: "select", required: false, | ||||
| multiple: true, | multiple: true, | ||||
| options: [ | options: [ | ||||
| { label: "All", value: "All" }, | |||||
| { label: "WIP", value: "WIP" }, | |||||
| { label: "FG", value: "FG" }, | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "半成品", value: "sfg" }, | |||||
| { label: "成品", value: "fg" }, | |||||
| ] }, | ] }, | ||||
| { label: "貨品編號 Item Code", name: "itemCode", type: "select", required: false, | { label: "貨品編號 Item Code", name: "itemCode", type: "select", required: false, | ||||
| multiple: true, | multiple: true, | ||||
| @@ -488,7 +462,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| }, | }, | ||||
| { | { | ||||
| id: "rep-015", | id: "rep-015", | ||||
| title: "M18 BOM Shop 同步記錄", | |||||
| title: "BOM Shop 同步記錄", | |||||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history`, | apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history`, | ||||
| responseType: "excel", | responseType: "excel", | ||||
| fields: [ | fields: [ | ||||
| @@ -533,10 +507,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| name: "storeId", | name: "storeId", | ||||
| type: "select", | type: "select", | ||||
| required: false, | required: false, | ||||
| options: [ | |||||
| { label: "2F", value: "2F" }, | |||||
| { label: "4F", value: "4F" }, | |||||
| ], | |||||
| options: floorOptions, | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| @@ -574,11 +545,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| name: "storeId", | name: "storeId", | ||||
| type: "select", | type: "select", | ||||
| required: false, | required: false, | ||||
| options: [ | |||||
| { label: "全部", value: "All" }, | |||||
| { label: "2F", value: "2F" }, | |||||
| { label: "4F", value: "4F" }, | |||||
| ], | |||||
| options: floorOptionsAll, | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| @@ -1,12 +1,12 @@ | |||||
| { | { | ||||
| "title": "Delivery order / workbench floor (supplier codes)", | "title": "Delivery order / workbench floor (supplier codes)", | ||||
| "Intro": "Manage supplier codes assigned to each floor. Use the edit button to add or remove suppliers from a floor.", | "Intro": "Manage supplier codes assigned to each floor. Use the edit button to add or remove suppliers from a floor.", | ||||
| "2F supplier": "2F supplier", | |||||
| "4F supplier": "4F supplier", | |||||
| "Edit 2F": "Edit 2F", | |||||
| "Edit 4F": "Edit 4F", | |||||
| "Edit 2F title": "Edit 2F supplier codes", | |||||
| "Edit 4F title": "Edit 4F supplier codes", | |||||
| "2F supplier": "2/F supplier", | |||||
| "4F supplier": "4/F supplier", | |||||
| "Edit 2F": "Edit 2/F", | |||||
| "Edit 4F": "Edit 4/F", | |||||
| "Edit 2F title": "Edit 2/F supplier codes", | |||||
| "Edit 4F title": "Edit 4/F supplier codes", | |||||
| "Edit dialog title": "Edit floor–supplier mapping", | "Edit dialog title": "Edit floor–supplier mapping", | ||||
| "Floor label": "Floor", | "Floor label": "Floor", | ||||
| "Add mapping": "Add mapping", | "Add mapping": "Add mapping", | ||||
| @@ -20,10 +20,10 @@ | |||||
| "btnSyncDoExtra": "Sync DO (Extra) from M18", | "btnSyncDoExtra": "Sync DO (Extra) from M18", | ||||
| "btnSyncProduct": "Sync product from M18", | "btnSyncProduct": "Sync product from M18", | ||||
| "syncing": "Syncing...", | "syncing": "Syncing...", | ||||
| "placeholderPoCode": "e.g. PFP002PO26030341", | |||||
| "placeholderPoCode": "e.g. DEMO-PO-001", | |||||
| "placeholderDoCode": "e.g. same document code as M18 shop PO", | "placeholderDoCode": "e.g. same document code as M18 shop PO", | ||||
| "placeholderDoExtraCode": "Enter multiple codes separated by comma or newline", | "placeholderDoExtraCode": "Enter multiple codes separated by comma or newline", | ||||
| "placeholderProductCode": "e.g. PP1175 (M18 item code)", | |||||
| "placeholderProductCode": "e.g. DEMO-FG-01", | |||||
| "waiting": "Waiting for implementation...", | "waiting": "Waiting for implementation...", | ||||
| "noResponse": "No response received", | "noResponse": "No response received", | ||||
| "alreadyExists": "{{docName}} already exists in system", | "alreadyExists": "{{docName}} already exists in system", | ||||
| @@ -15,6 +15,7 @@ | |||||
| "noDataFoundTitle": "No Data Found / 查無資料", | "noDataFoundTitle": "No Data Found / 查無資料", | ||||
| "noDataFoundHint": "No inventory records match your search. Please try adjusting the filters.", | "noDataFoundHint": "No inventory records match your search. Please try adjusting the filters.", | ||||
| "ok": "OK", | "ok": "OK", | ||||
| "unavailableInDemo": "Unavailable in demo mode", | |||||
| "missingRequired": "Missing required fields:\n- {{fields}}", | "missingRequired": "Missing required fields:\n- {{fields}}", | ||||
| "requiredField": "This is a required field", | "requiredField": "This is a required field", | ||||
| "dateNotBeforeToday": "Date cannot be before today:\n- {{fields}}", | "dateNotBeforeToday": "Date cannot be before today:\n- {{fields}}", | ||||
| @@ -57,17 +58,11 @@ | |||||
| "warehouse": "Warehouse", | "warehouse": "Warehouse", | ||||
| "area": "Area", | "area": "Area", | ||||
| "slot": "Slot", | "slot": "Slot", | ||||
| "lotNo": "Lot No", | |||||
| "poPrefix": "PP/PF" | |||||
| "lotNo": "Lot No" | |||||
| }, | }, | ||||
| "options": { | "options": { | ||||
| "storeId": { | "storeId": { | ||||
| "All": "All" | "All": "All" | ||||
| }, | |||||
| "poPrefix": { | |||||
| "All": "All", | |||||
| "PP": "PP", | |||||
| "PF": "PF" | |||||
| } | } | ||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| @@ -107,14 +102,11 @@ | |||||
| }, | }, | ||||
| "type": { | "type": { | ||||
| "All": "All", | "All": "All", | ||||
| "PP": "PP", | |||||
| "PF": "PF", | |||||
| "TOA": "TOA", | |||||
| "工廠生產": "Factory production", | |||||
| "倉存調整": "Inventory adjustment", | |||||
| "期初存貨": "Opening inventory", | |||||
| "採購入倉": "Purchase inbound", | |||||
| "其他入倉": "Other inbound" | |||||
| "purchase": "Purchase", | |||||
| "production": "Production", | |||||
| "adjustment": "Inventory adjustment", | |||||
| "opening": "Opening inventory", | |||||
| "other": "Other" | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -208,6 +200,14 @@ | |||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | ||||
| }, | |||||
| "options": { | |||||
| "stockCategory": { | |||||
| "All": "All", | |||||
| "mat": "Material", | |||||
| "sfg": "Semi-finished", | |||||
| "fg": "Finished goods" | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-005": { | "rep-005": { | ||||
| @@ -222,10 +222,17 @@ | |||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | ||||
| }, | |||||
| "options": { | |||||
| "stockCategory": { | |||||
| "All": "All", | |||||
| "sfg": "Semi-finished", | |||||
| "fg": "Finished goods" | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-015": { | "rep-015": { | ||||
| "title": "M18 BOM Shop Sync History", | |||||
| "title": "BOM Shop Sync History", | |||||
| "fields": { | "fields": { | ||||
| "syncDateStart": "Sync Date Start", | "syncDateStart": "Sync Date Start", | ||||
| "syncDateEnd": "Sync Date End", | "syncDateEnd": "Sync Date End", | ||||
| @@ -295,8 +302,8 @@ | |||||
| }, | }, | ||||
| "lotOrigin": { | "lotOrigin": { | ||||
| "All": "All", | "All": "All", | ||||
| "PP": "PP", | |||||
| "PF": "PF", | |||||
| "purchase": "Purchase", | |||||
| "production": "Production", | |||||
| "other": "Other" | "other": "Other" | ||||
| } | } | ||||
| } | } | ||||
| @@ -326,13 +333,9 @@ | |||||
| "unitPrice": "Unit Price", | "unitPrice": "Unit Price", | ||||
| "currency": "Currency", | "currency": "Currency", | ||||
| "amount": "Amount", | "amount": "Amount", | ||||
| "grnCode": "GRN Code / M18 Receipt No.", | |||||
| "grnId": "GRN Id / M18 Record Id", | |||||
| "poCreator": "PO creator (M18)", | |||||
| "note": "Note", | "note": "Note", | ||||
| "category": "Category", | "category": "Category", | ||||
| "totalAmount": "Total Amount", | |||||
| "grnCodes": "GRN Code(s) / M18 Receipt No." | |||||
| "totalAmount": "Total Amount" | |||||
| }, | }, | ||||
| "bomSync": { | "bomSync": { | ||||
| "sheetSync": "BOM Sync Log", | "sheetSync": "BOM Sync Log", | ||||
| @@ -1,12 +1,12 @@ | |||||
| { | { | ||||
| "title": "送貨單樓層設定(供應商代碼)", | "title": "送貨單樓層設定(供應商代碼)", | ||||
| "Intro": "管理各樓層的供應商代碼。點擊編輯按鈕可新增或移除供應商。", | "Intro": "管理各樓層的供應商代碼。點擊編輯按鈕可新增或移除供應商。", | ||||
| "2F supplier": "2F 供應商", | |||||
| "4F supplier": "4F 供應商", | |||||
| "Edit 2F": "編輯 2F", | |||||
| "Edit 4F": "編輯 4F", | |||||
| "Edit 2F title": "編輯 2F 供應商代碼", | |||||
| "Edit 4F title": "編輯 4F 供應商代碼", | |||||
| "2F supplier": "2/F 供應商", | |||||
| "4F supplier": "4/F 供應商", | |||||
| "Edit 2F": "編輯 2/F", | |||||
| "Edit 4F": "編輯 4/F", | |||||
| "Edit 2F title": "編輯 2/F 供應商代碼", | |||||
| "Edit 4F title": "編輯 4/F 供應商代碼", | |||||
| "Edit dialog title": "編輯樓層供應商映射", | "Edit dialog title": "編輯樓層供應商映射", | ||||
| "Floor label": "樓層", | "Floor label": "樓層", | ||||
| "Add mapping": "新增映射", | "Add mapping": "新增映射", | ||||
| @@ -1,3 +1,3 @@ | |||||
| { | { | ||||
| "title": "檸檬機(激光機)" | |||||
| "title": "激光機" | |||||
| } | } | ||||
| @@ -20,10 +20,10 @@ | |||||
| "btnSyncDoExtra": "從 M18 同步送貨訂單(加單)", | "btnSyncDoExtra": "從 M18 同步送貨訂單(加單)", | ||||
| "btnSyncProduct": "從 M18 同步貨品", | "btnSyncProduct": "從 M18 同步貨品", | ||||
| "syncing": "同步中…", | "syncing": "同步中…", | ||||
| "placeholderPoCode": "例:PFP002PO26030341", | |||||
| "placeholderPoCode": "例:DEMO-PO-001", | |||||
| "placeholderDoCode": "例:與 M18 店鋪訂單相同的文件編號", | "placeholderDoCode": "例:與 M18 店鋪訂單相同的文件編號", | ||||
| "placeholderDoExtraCode": "可輸入多個 code,用逗號或換行分隔", | "placeholderDoExtraCode": "可輸入多個 code,用逗號或換行分隔", | ||||
| "placeholderProductCode": "例:PP1175(M18 貨品編號)", | |||||
| "placeholderProductCode": "例:DEMO-FG-01", | |||||
| "waiting": "等待實作…", | "waiting": "等待實作…", | ||||
| "noResponse": "未收到回應", | "noResponse": "未收到回應", | ||||
| "alreadyExists": "{{docName}}已存在系統", | "alreadyExists": "{{docName}}已存在系統", | ||||
| @@ -25,7 +25,7 @@ | |||||
| "nav.breadcrumb.joEdit": "工單詳情", | "nav.breadcrumb.joEdit": "工單詳情", | ||||
| "nav.breadcrumb.joTesting": "工單測試", | "nav.breadcrumb.joTesting": "工單測試", | ||||
| "nav.breadcrumb.joWorkbench": "工單工作台", | "nav.breadcrumb.joWorkbench": "工單工作台", | ||||
| "nav.breadcrumb.laserPrint": "檸檬機(激光機)", | |||||
| "nav.breadcrumb.laserPrint": "激光機", | |||||
| "nav.breadcrumb.m18Sync": "M18 同步", | "nav.breadcrumb.m18Sync": "M18 同步", | ||||
| "nav.breadcrumb.poEdit": "編輯", | "nav.breadcrumb.poEdit": "編輯", | ||||
| "nav.breadcrumb.poWorkbench": "採購單工作台", | "nav.breadcrumb.poWorkbench": "採購單工作台", | ||||
| @@ -64,7 +64,7 @@ | |||||
| "nav.jobOrder.productionProcess": "工單生產流程", | "nav.jobOrder.productionProcess": "工單生產流程", | ||||
| "nav.jobOrder.searchCreate": "搜索工單/ 建立工單", | "nav.jobOrder.searchCreate": "搜索工單/ 建立工單", | ||||
| "nav.jobOrderManagement": "管理工單", | "nav.jobOrderManagement": "管理工單", | ||||
| "nav.laserPrint": "檸檬機(激光機)", | |||||
| "nav.laserPrint": "激光機", | |||||
| "nav.m18Sync": "M18 同步", | "nav.m18Sync": "M18 同步", | ||||
| "nav.productionServer": "正式伺服器", | "nav.productionServer": "正式伺服器", | ||||
| "nav.report": "報告管理", | "nav.report": "報告管理", | ||||
| @@ -15,6 +15,7 @@ | |||||
| "noDataFoundTitle": "查無資料", | "noDataFoundTitle": "查無資料", | ||||
| "noDataFoundHint": "找不到符合您搜尋條件的庫存資料,請嘗試調整篩選條件。", | "noDataFoundHint": "找不到符合您搜尋條件的庫存資料,請嘗試調整篩選條件。", | ||||
| "ok": "確定", | "ok": "確定", | ||||
| "unavailableInDemo": "示範模式無法使用", | |||||
| "missingRequired": "缺少必填條件:\n- {{fields}}", | "missingRequired": "缺少必填條件:\n- {{fields}}", | ||||
| "requiredField": "此為必填欄位", | "requiredField": "此為必填欄位", | ||||
| "dateNotBeforeToday": "日期不可早於今天:\n- {{fields}}", | "dateNotBeforeToday": "日期不可早於今天:\n- {{fields}}", | ||||
| @@ -57,17 +58,11 @@ | |||||
| "warehouse": "倉庫", | "warehouse": "倉庫", | ||||
| "area": "區域", | "area": "區域", | ||||
| "slot": "儲位", | "slot": "儲位", | ||||
| "lotNo": "批號", | |||||
| "poPrefix": "PP/PF 分類" | |||||
| "lotNo": "批號" | |||||
| }, | }, | ||||
| "options": { | "options": { | ||||
| "storeId": { | "storeId": { | ||||
| "All": "全部" | "All": "全部" | ||||
| }, | |||||
| "poPrefix": { | |||||
| "All": "全部", | |||||
| "PP": "PP", | |||||
| "PF": "PF" | |||||
| } | } | ||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| @@ -107,14 +102,11 @@ | |||||
| }, | }, | ||||
| "type": { | "type": { | ||||
| "All": "全部", | "All": "全部", | ||||
| "PP": "PP", | |||||
| "PF": "PF", | |||||
| "TOA": "TOA", | |||||
| "工廠生產": "工廠生產", | |||||
| "倉存調整": "倉存調整", | |||||
| "期初存貨": "期初存貨", | |||||
| "採購入倉": "採購入倉", | |||||
| "其他入倉": "其他入倉" | |||||
| "purchase": "採購", | |||||
| "production": "生產", | |||||
| "adjustment": "倉存調整", | |||||
| "opening": "期初存貨", | |||||
| "other": "其他" | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -208,6 +200,14 @@ | |||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | ||||
| }, | |||||
| "options": { | |||||
| "stockCategory": { | |||||
| "All": "全部", | |||||
| "mat": "原料", | |||||
| "sfg": "半成品", | |||||
| "fg": "成品" | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-005": { | "rep-005": { | ||||
| @@ -222,10 +222,17 @@ | |||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | ||||
| }, | |||||
| "options": { | |||||
| "stockCategory": { | |||||
| "All": "全部", | |||||
| "sfg": "半成品", | |||||
| "fg": "成品" | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-015": { | "rep-015": { | ||||
| "title": "M18 BOM Shop 同步記錄", | |||||
| "title": "BOM Shop 同步記錄", | |||||
| "fields": { | "fields": { | ||||
| "syncDateStart": "同步日期:由", | "syncDateStart": "同步日期:由", | ||||
| "syncDateEnd": "同步日期:至", | "syncDateEnd": "同步日期:至", | ||||
| @@ -295,8 +302,8 @@ | |||||
| }, | }, | ||||
| "lotOrigin": { | "lotOrigin": { | ||||
| "All": "全部", | "All": "全部", | ||||
| "PP": "PP", | |||||
| "PF": "PF", | |||||
| "purchase": "採購", | |||||
| "production": "生產", | |||||
| "other": "其他" | "other": "其他" | ||||
| } | } | ||||
| } | } | ||||
| @@ -326,13 +333,9 @@ | |||||
| "unitPrice": "單價", | "unitPrice": "單價", | ||||
| "currency": "貨幣", | "currency": "貨幣", | ||||
| "amount": "金額", | "amount": "金額", | ||||
| "grnCode": "M18 入倉單號", | |||||
| "grnId": "M18 記錄編號", | |||||
| "poCreator": "PO建立者(M18)", | |||||
| "note": "備註", | "note": "備註", | ||||
| "category": "類別", | "category": "類別", | ||||
| "totalAmount": "金額", | |||||
| "grnCodes": "M18 入倉單號" | |||||
| "totalAmount": "金額" | |||||
| }, | }, | ||||
| "bomSync": { | "bomSync": { | ||||
| "sheetSync": "BOM同步記錄", | "sheetSync": "BOM同步記錄", | ||||