| 作成者 | 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"; | |||
| export const metadata: Metadata = { | |||
| title: "檸檬機(激光機)", | |||
| title: "激光機", | |||
| }; | |||
| export default async function LaserPrintPage() { | |||
| @@ -12,7 +12,7 @@ export default async function LaserPrintPage() { | |||
| <I18nProvider namespaces={["laserPrint", "navigation", "common"]}> | |||
| <Stack direction="row" justifyContent="space-between" flexWrap="wrap" rowGap={2}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| 檸檬機(激光機) | |||
| 激光機 | |||
| </Typography> | |||
| </Stack> | |||
| <LaserPrintSearch /> | |||
| @@ -318,7 +318,7 @@ export default function M18SynPage() { | |||
| label="PO Code" | |||
| value={m18PoCode} | |||
| onChange={(e) => setM18PoCode(e.target.value)} | |||
| placeholder="e.g. PFP002PO26030341" | |||
| placeholder="e.g. DEMO-PO-001" | |||
| sx={{ minWidth: 320 }} | |||
| /> | |||
| <Button variant="contained" color="primary" onClick={handleSyncM18PoByCode} disabled={isSyncingM18Po}> | |||
| @@ -418,7 +418,7 @@ export default function M18SynPage() { | |||
| label="Item / product code" | |||
| value={m18ProductCode} | |||
| onChange={(e) => setM18ProductCode(e.target.value)} | |||
| placeholder="e.g. PP1175 (M18 item code)" | |||
| placeholder="e.g. DEMO-FG-01" | |||
| sx={{ minWidth: 320 }} | |||
| /> | |||
| <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-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-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> | |||
| </thead> | |||
| <tbody> | |||
| @@ -239,7 +239,7 @@ const AsyncItemCodeAutocomplete: React.FC<Props> = ({ | |||
| {...params} | |||
| fullWidth | |||
| label={label} | |||
| placeholder={hasSelection ? "" : (placeholder || "e.g. FA0591")} | |||
| placeholder={hasSelection ? "" : (placeholder || "e.g. DEMO-FG-01")} | |||
| helperText={t("typeToSearchItemCode", { min: minChars })} | |||
| InputLabelProps={params.InputLabelProps} | |||
| InputProps={{ | |||
| @@ -75,7 +75,7 @@ export default function ItemQcReportFilters({ | |||
| label={field("itemCode", "貨品編號 Item Code")} | |||
| value={criteria.itemCode || ""} | |||
| onChange={(e) => onFieldChange("itemCode", e.target.value)} | |||
| placeholder="e.g. MJ0364" | |||
| placeholder="e.g. DEMO-FG-01" | |||
| /> | |||
| </Grid> | |||
| <Grid item {...gridSize}> | |||
| @@ -17,7 +17,7 @@ import OutboundOutlinedIcon from "@mui/icons-material/OutboundOutlined"; | |||
| import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined"; | |||
| import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutlined"; | |||
| 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 { useReportLabels } from "./reportI18n"; | |||
| @@ -52,12 +52,16 @@ function ReportCard({ | |||
| title, | |||
| accent, | |||
| selected, | |||
| unavailable, | |||
| unavailableLabel, | |||
| onClick, | |||
| }: { | |||
| reportId: string; | |||
| title: string; | |||
| accent: string; | |||
| selected: boolean; | |||
| unavailable: boolean; | |||
| unavailableLabel: string; | |||
| onClick: () => void; | |||
| }) { | |||
| const Icon = REPORT_ICON_MAP[reportId] ?? Inventory2OutlinedIcon; | |||
| @@ -66,7 +70,9 @@ function ReportCard({ | |||
| <Box | |||
| component="button" | |||
| type="button" | |||
| onClick={onClick} | |||
| disabled={unavailable} | |||
| onClick={unavailable ? undefined : onClick} | |||
| aria-disabled={unavailable} | |||
| sx={{ | |||
| display: "flex", | |||
| alignItems: "center", | |||
| @@ -74,17 +80,22 @@ function ReportCard({ | |||
| width: "100%", | |||
| p: 1.5, | |||
| textAlign: "left", | |||
| cursor: "pointer", | |||
| cursor: unavailable ? "not-allowed" : "pointer", | |||
| border: "1px solid", | |||
| borderColor: selected ? accent : "divider", | |||
| borderColor: unavailable ? "action.disabledBackground" : selected ? accent : "divider", | |||
| 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", | |||
| "&:hover": { | |||
| borderColor: accent, | |||
| boxShadow: 2, | |||
| }, | |||
| "&:hover": unavailable | |||
| ? {} | |||
| : { | |||
| borderColor: accent, | |||
| boxShadow: 2, | |||
| }, | |||
| }} | |||
| > | |||
| <Box | |||
| @@ -95,16 +106,23 @@ function ReportCard({ | |||
| width: 40, | |||
| height: 40, | |||
| borderRadius: 1, | |||
| bgcolor: `${accent}18`, | |||
| color: accent, | |||
| bgcolor: unavailable ? "action.disabledBackground" : `${accent}18`, | |||
| color: unavailable ? "text.disabled" : accent, | |||
| flexShrink: 0, | |||
| }} | |||
| > | |||
| <Icon fontSize="small" /> | |||
| </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> | |||
| ); | |||
| } | |||
| @@ -118,7 +136,7 @@ function CategoryColumn({ | |||
| selectedReportId: string; | |||
| onSelectReport: (reportId: string) => void; | |||
| }) { | |||
| const { reportTitle, categoryTitle } = useReportLabels(); | |||
| const { t, reportTitle, categoryTitle } = useReportLabels(); | |||
| const reports = category.reportIds | |||
| .map((id) => reportById[id]) | |||
| .filter(Boolean); | |||
| @@ -161,6 +179,8 @@ function CategoryColumn({ | |||
| title={reportTitle(report)} | |||
| accent={category.accent} | |||
| selected={selectedReportId === report.id} | |||
| unavailable={DEMO_UNAVAILABLE_REPORT_IDS.has(report.id)} | |||
| unavailableLabel={t("unavailableInDemo")} | |||
| onClick={() => onSelectReport(report.id)} | |||
| /> | |||
| </Grid> | |||
| @@ -205,7 +205,7 @@ function toMaterialExcelRow( | |||
| export async function generateBomShopSyncReportExcel( | |||
| criteria: Record<string, string>, | |||
| reportTitle: string = "M18 BOM Shop 同步記錄", | |||
| reportTitle: string = "BOM Shop 同步記錄", | |||
| t?: TFunction, | |||
| ): Promise<void> { | |||
| const L = bomSyncLabels(t); | |||
| @@ -33,12 +33,6 @@ export interface GrnReportRow { | |||
| lineAmount?: number; | |||
| /** PO currency code (currency.code) */ | |||
| 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; | |||
| } | |||
| @@ -54,7 +48,6 @@ export interface ListedPoAmounts { | |||
| poCode?: string; | |||
| currencyCode?: string; | |||
| totalAmount?: number; | |||
| grnCodes?: string; | |||
| }[]; | |||
| } | |||
| @@ -148,13 +141,9 @@ function grnLabels(t?: TFunction) { | |||
| unitPrice: tx(t, "excel.grn.unitPrice", "單價"), | |||
| currency: tx(t, "excel.grn.currency", "貨幣"), | |||
| 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", "備註"), | |||
| category: tx(t, "excel.grn.category", "類別"), | |||
| totalAmount: tx(t, "excel.grn.totalAmount", "金額"), | |||
| grnCodes: tx(t, "excel.grn.grnCodes", "M18 入倉單號"), | |||
| noCompletedPo: tx(t, "excel.grn.noCompletedPo", "(篩選範圍內無已完成之 PO 行)"), | |||
| categoryCurrencyTotal: tx(t, "excel.grn.categoryCurrencyTotal", "貨幣小計"), | |||
| categoryPo: tx(t, "excel.grn.categoryPo", "訂單"), | |||
| @@ -187,9 +176,6 @@ function toExcelRow( | |||
| base[L.currency] = r.currencyCode ?? ""; | |||
| base[L.amount] = moneyCellValue(r.lineAmount); | |||
| } | |||
| base[L.grnCode] = r.grnCode ?? ""; | |||
| base[L.grnId] = r.grnId ?? ""; | |||
| base[L.poCreator] = r.poM18CreatorDisplay ?? ""; | |||
| return base; | |||
| } | |||
| @@ -213,7 +199,6 @@ function buildListedPoAmountSheetRows( | |||
| [L.poNo]: "", | |||
| [L.currency]: c.currencyCode ?? "", | |||
| [L.totalAmount]: moneyCellValue(c.totalAmount), | |||
| [L.grnCodes]: "", | |||
| }); | |||
| } | |||
| for (const p of listed.byPurchaseOrder) { | |||
| @@ -223,7 +208,6 @@ function buildListedPoAmountSheetRows( | |||
| [L.poNo]: p.poCode ?? "", | |||
| [L.currency]: p.currencyCode ?? "", | |||
| [L.totalAmount]: moneyCellValue(p.totalAmount), | |||
| [L.grnCodes]: p.grnCodes ?? "", | |||
| }); | |||
| } | |||
| return out; | |||
| @@ -25,7 +25,7 @@ import { | |||
| } from '@mui/material'; | |||
| import DownloadIcon from '@mui/icons-material/Download'; | |||
| 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 { NEXT_PUBLIC_API_URL } from '@/config/api'; | |||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | |||
| @@ -152,6 +152,7 @@ export default function ReportPage() { | |||
| [selectedReportId]); | |||
| const handleSelectReport = (reportId: string) => { | |||
| if (DEMO_UNAVAILABLE_REPORT_IDS.has(reportId)) return; | |||
| if (reportId === selectedReportId) return; | |||
| setSelectedReportId(reportId); | |||
| setFieldErrors({}); | |||
| @@ -159,7 +160,7 @@ export default function ReportPage() { | |||
| if (reportId === 'rep-010') { | |||
| updateCriteria({ qcType: 'all', qcItemScope: 'all' }); | |||
| } else if (reportId === 'rep-004') { | |||
| updateCriteria({ storeId: 'All', poPrefix: 'All' }); | |||
| updateCriteria({ storeId: 'All' }); | |||
| } else if (reportId === 'rep-021') { | |||
| updateCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' }); | |||
| } else { | |||
| @@ -452,6 +453,7 @@ export default function ReportPage() { | |||
| const executeExcelReport = async () => { | |||
| if (!currentReport) return; | |||
| if (DEMO_UNAVAILABLE_REPORT_IDS.has(currentReport.id)) return; | |||
| if (excelInFlightRef.current) return; | |||
| excelInFlightRef.current = true; | |||
| setLoading(true); | |||
| @@ -727,7 +729,7 @@ export default function ReportPage() { | |||
| <Grid item {...gridSize} key={fieldKey}> | |||
| <AsyncItemCodeAutocomplete | |||
| label={translatedLabel} | |||
| placeholder={field.placeholder || "e.g. FA0591"} | |||
| placeholder={field.placeholder || "e.g. DEMO-FG-01"} | |||
| value={selectedCodes} | |||
| onInputValueChange={(text) => { | |||
| pendingItemCodeInputRef.current[field.name] = text; | |||
| @@ -37,8 +37,6 @@ import { | |||
| type LaserBag2AutoSendReport, | |||
| type LaserLastReceiveSuccess, | |||
| } from "@/app/api/laserPrint/actions"; | |||
| import * as XLSX from "xlsx"; | |||
| interface TabPanelProps { | |||
| children?: React.ReactNode; | |||
| index: number; | |||
| @@ -175,49 +173,6 @@ export default function TestingPage() { | |||
| }; | |||
| }, [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 url = window.URL.createObjectURL(blob); | |||
| const link = document.createElement("a"); | |||
| @@ -524,33 +479,35 @@ export default function TestingPage() { | |||
| </Tabs> | |||
| <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> | |||
| </Section> | |||
| </TabPanel> | |||
| @@ -560,7 +517,7 @@ export default function TestingPage() { | |||
| <Alert severity="info" sx={{ mb: 2 }}> | |||
| Uses <strong>GET /py/job-orders?planStart=</strong> for the day, | |||
| 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 | |||
| before sending to NGE; the ZIP itself is only a transport bundle. | |||
| </Alert> | |||
| @@ -973,12 +930,9 @@ export default function TestingPage() { | |||
| <TabPanel value={tabValue} index={5}> | |||
| <Section title="6. M18 sync alert email"> | |||
| <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> | |||
| <Stack spacing={2} sx={{ mb: 2, maxWidth: 720 }}> | |||
| <TextField | |||
| @@ -180,7 +180,7 @@ function skippedWithoutExpiryFromResponse(res: Response): string[] { | |||
| .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( | |||
| request: OnPackQrDownloadRequest, | |||
| ): 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( | |||
| request: OnPackQrDownloadRequest, | |||
| ): Promise<Blob> { | |||
| @@ -219,7 +219,7 @@ export async function downloadOnPackTextQrZip( | |||
| 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( | |||
| request: OnPackQrDownloadRequest, | |||
| ): Promise<OnPackZipDownload> { | |||
| @@ -82,15 +82,23 @@ const SETTINGS_KEY = "bagPrint_settings"; | |||
| const DEFAULT_SETTINGS = { | |||
| dabag_ip: "", | |||
| dabag_port: "3008", | |||
| laser_ip: "192.168.17.10", | |||
| laser_ip: "", | |||
| laser_port: "45678", | |||
| }; | |||
| const LIVE_PRINTER_HOSTS = new Set(["192.168.17.10", "192.168.18.77"]); | |||
| function loadSettings(): typeof DEFAULT_SETTINGS { | |||
| if (typeof window === "undefined") return DEFAULT_SETTINGS; | |||
| try { | |||
| const s = localStorage.getItem(SETTINGS_KEY); | |||
| if (s) return { ...DEFAULT_SETTINGS, ...JSON.parse(s) }; | |||
| 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 {} | |||
| return DEFAULT_SETTINGS; | |||
| } | |||
| @@ -467,11 +475,11 @@ const BagPrintSearch: React.FC = () => { | |||
| link.remove(); | |||
| window.URL.revokeObjectURL(url); | |||
| setSnackbar(skippedExpirySnackbar("OnPack 汁水機(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||
| setSnackbar(skippedExpirySnackbar("OnPack QR(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||
| } catch (e) { | |||
| setSnackbar({ | |||
| open: true, | |||
| message: e instanceof Error ? e.message : "下載 OnPack 汁水機(含到期日)失敗", | |||
| message: e instanceof Error ? e.message : "下載 OnPack QR(含到期日)失敗", | |||
| severity: "error", | |||
| }); | |||
| } finally { | |||
| @@ -505,11 +513,11 @@ const BagPrintSearch: React.FC = () => { | |||
| link.remove(); | |||
| window.URL.revokeObjectURL(url); | |||
| setSnackbar({ open: true, message: "OnPack2023檸檬機 ZIP 已下載", severity: "success" }); | |||
| setSnackbar({ open: true, message: "OnPack 文字 ZIP 已下載", severity: "success" }); | |||
| } catch (e) { | |||
| setSnackbar({ | |||
| open: true, | |||
| message: e instanceof Error ? e.message : "下載 OnPack2023檸檬機 失敗", | |||
| message: e instanceof Error ? e.message : "下載 OnPack 文字 失敗", | |||
| severity: "error", | |||
| }); | |||
| } finally { | |||
| @@ -544,11 +552,11 @@ const BagPrintSearch: React.FC = () => { | |||
| link.remove(); | |||
| window.URL.revokeObjectURL(url); | |||
| setSnackbar(skippedExpirySnackbar("OnPack2023檸檬機(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||
| setSnackbar(skippedExpirySnackbar("OnPack 文字(含到期日)ZIP 已下載", skippedWithoutExpiry)); | |||
| } catch (e) { | |||
| setSnackbar({ | |||
| open: true, | |||
| message: e instanceof Error ? e.message : "下載 OnPack2023檸檬機(含到期日)失敗", | |||
| message: e instanceof Error ? e.message : "下載 OnPack 文字(含到期日)失敗", | |||
| severity: "error", | |||
| }); | |||
| } finally { | |||
| @@ -597,7 +605,7 @@ const BagPrintSearch: React.FC = () => { | |||
| ), | |||
| ); | |||
| } catch { | |||
| /* 檸檬機標籤可沒有;不擋畫面 */ | |||
| /* 激光機標籤可沒有;不擋畫面 */ | |||
| } | |||
| })(); | |||
| }, []); | |||
| @@ -793,7 +801,7 @@ const BagPrintSearch: React.FC = () => { | |||
| jobOrders.length === 0 | |||
| } | |||
| > | |||
| {downloadingOnPack ? "下載中..." : "下載 OnPack 汁水機 QR code"} | |||
| {downloadingOnPack ? "下載中..." : "下載 OnPack QR code"} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| @@ -808,7 +816,7 @@ const BagPrintSearch: React.FC = () => { | |||
| jobOrders.length === 0 | |||
| } | |||
| > | |||
| {downloadingOnPackExp ? "下載中..." : "下載 OnPack 汁水機(含到期日)"} | |||
| {downloadingOnPackExp ? "下載中..." : "下載 OnPack QR(含到期日)"} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| @@ -824,7 +832,7 @@ const BagPrintSearch: React.FC = () => { | |||
| jobOrders.length === 0 | |||
| } | |||
| > | |||
| {downloadingOnPackText ? "下載中..." : "下載 OnPack2023檸檬機"} | |||
| {downloadingOnPackText ? "下載中..." : "下載 OnPack 文字"} | |||
| </Button> | |||
| {canSeeOnPackAdmin && ( | |||
| <Button | |||
| @@ -841,7 +849,7 @@ const BagPrintSearch: React.FC = () => { | |||
| jobOrders.length === 0 | |||
| } | |||
| > | |||
| {downloadingOnPackTextExp ? "下載中..." : "下載 OnPack2023檸檬機(含到期日)"} | |||
| {downloadingOnPackTextExp ? "下載中..." : "下載 OnPack 文字(含到期日)"} | |||
| </Button> | |||
| )} | |||
| </Stack> | |||
| @@ -903,8 +911,8 @@ const BagPrintSearch: React.FC = () => { | |||
| {jo.itemCode || "—"} | |||
| </Typography> | |||
| <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> | |||
| </Box> | |||
| <Box sx={{ flex: 1, minWidth: 0 }}> | |||
| @@ -1086,17 +1094,17 @@ const BagPrintSearch: React.FC = () => { | |||
| > | |||
| <Stack spacing={1.5} sx={{ flexShrink: 0, mb: 1 }}> | |||
| <Typography variant="subtitle2" color="primary"> | |||
| 汁水機({expiryCodes.length}) | |||
| OnPack QR({expiryCodes.length}) | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| 「下載 OnPack 汁水機(含到期日)」只用此清單。名稱印在 Product 行,過長可改短後按儲存。 | |||
| 「下載 OnPack QR(含到期日)」只用此清單。名稱印在 Product 行,過長可改短後按儲存。 | |||
| 點欄位標題可排序;冷藏/-18/列印第一次點擊會把「未設」排到最前。 | |||
| </Typography> | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <TextField | |||
| label="新增品號" | |||
| size="small" | |||
| placeholder="例如 PP2211" | |||
| placeholder="例如 DEMO-FG-01" | |||
| value={expiryCodeInput} | |||
| onChange={(e) => setExpiryCodeInput(e.target.value)} | |||
| onKeyDown={(e) => { | |||
| @@ -1216,7 +1224,7 @@ const BagPrintSearch: React.FC = () => { | |||
| onChange={(e) => | |||
| setNameDrafts((prev) => ({ ...prev, [row.itemCode]: e.target.value })) | |||
| } | |||
| placeholder={row.defaultPrintName || "例如 豬肉水(1包X2千克)"} | |||
| placeholder={row.defaultPrintName || "例如 鄉村麵包"} | |||
| inputProps={{ maxLength: 255 }} | |||
| fullWidth | |||
| /> | |||
| @@ -1292,7 +1300,7 @@ const BagPrintSearch: React.FC = () => { | |||
| </TableContainer> | |||
| )} | |||
| <Typography variant="body2" color="text.secondary" sx={{ flexShrink: 0, mt: 1 }}> | |||
| 檸檬機到期日 ZIP 品號稍後加入。 | |||
| 激光機到期日 ZIP 品號稍後加入。 | |||
| </Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -358,8 +358,8 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| onChange={onFloorSelectChange} | |||
| 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> | |||
| </FormControl> | |||
| <Button | |||
| @@ -83,21 +83,27 @@ const LaserPrintSearch: React.FC = () => { | |||
| message: "", | |||
| }); | |||
| 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 [laserItemCodes, setLaserItemCodes] = useState("PP1175"); | |||
| const [laserItemCodes, setLaserItemCodes] = useState(""); | |||
| const [lastLaserReceive, setLastLaserReceive] = useState<LaserLastReceiveSuccess | null>(null); | |||
| const [settingsLoaded, setSettingsLoaded] = useState(false); | |||
| const [printerConnected, setPrinterConnected] = useState(false); | |||
| const [printerMessage, setPrinterMessage] = useState("檸檬機(激光機)未連接"); | |||
| const [printerMessage, setPrinterMessage] = useState("激光機未連接"); | |||
| const sendInFlightRef = useRef(false); | |||
| const loadSystemSettings = useCallback(async () => { | |||
| try { | |||
| 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)); | |||
| 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); | |||
| setSettingsLoaded(true); | |||
| } catch (e) { | |||
| @@ -158,7 +164,7 @@ const LaserPrintSearch: React.FC = () => { | |||
| setPrinterMessage(result.message); | |||
| } catch (e) { | |||
| 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 (!laserHost.trim()) { | |||
| setErrorSnackbar({ open: true, message: "請在系統設定中填寫檸檬機(激光機) IP。" }); | |||
| setErrorSnackbar({ open: true, message: "請在系統設定中填寫激光機 IP。" }); | |||
| return; | |||
| } | |||
| @@ -239,8 +245,8 @@ const LaserPrintSearch: React.FC = () => { | |||
| : ""; | |||
| setSuccessSignal( | |||
| laterFail | |||
| ? `已送出 ${sentOk} 次至檸檬機(激光機)${ackHint}(後續重送失敗:${laterFail})` | |||
| : `已送出 ${sentOk} 次至檸檬機(激光機)${ackHint}`, | |||
| ? `已送出 ${sentOk} 次至激光機${ackHint}(後續重送失敗:${laterFail})` | |||
| : `已送出 ${sentOk} 次至激光機${ackHint}`, | |||
| ); | |||
| await loadSystemSettings(); | |||
| } catch (e) { | |||
| @@ -287,7 +293,7 @@ const LaserPrintSearch: React.FC = () => { | |||
| {lastLaserReceive.source ? ` (${lastLaserReceive.source === "AUTO" ? "自動送出" : "手動點選"})` : ""} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ mt: 0.5 }}> | |||
| 此時間只會在檸檬機回覆 receive 時更新。之後送出失敗不會改這裡。 | |||
| 此時間只會在激光機回覆 receive 時更新。之後送出失敗不會改這裡。 | |||
| </Typography> | |||
| </Alert> | |||
| )} | |||
| @@ -332,7 +338,7 @@ const LaserPrintSearch: React.FC = () => { | |||
| }} | |||
| title={printerMessage} | |||
| > | |||
| 檸檬機(激光機): | |||
| 激光機: | |||
| </Box> | |||
| </Stack> | |||
| </Stack> | |||
| @@ -420,11 +426,11 @@ const LaserPrintSearch: React.FC = () => { | |||
| </Paper> | |||
| <Dialog open={settingsOpen} onClose={() => setSettingsOpen(false)} maxWidth="sm" fullWidth> | |||
| <DialogTitle>檸檬機(激光機)(系統設定)</DialogTitle> | |||
| <DialogTitle>激光機(系統設定)</DialogTitle> | |||
| <DialogContent> | |||
| <Stack spacing={2} sx={{ mt: 1 }}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| 儲存後寫入資料庫,後端送出走此 IP/埠(預設 192.168.18.77:45678)。 | |||
| 儲存後寫入資料庫,後端送出走此 IP/埠。未填 IP 則不會送出。 | |||
| </Typography> | |||
| <TextField | |||
| label="IP" | |||
| @@ -446,8 +452,8 @@ const LaserPrintSearch: React.FC = () => { | |||
| value={laserItemCodes} | |||
| onChange={(e) => setLaserItemCodes(e.target.value)} | |||
| fullWidth | |||
| placeholder="PP1175" | |||
| helperText="預設 PP1175;可輸入多個品號,例如 PP1175,AB999。留空則列表顯示當日全部包裝工單。" | |||
| placeholder="例如 DEMO-FG-01" | |||
| helperText="可輸入多個品號,例如 DEMO-FG-01,DEMO-FG-02。留空則列表顯示當日全部包裝工單。" | |||
| /> | |||
| </Stack> | |||
| </DialogContent> | |||
| @@ -116,7 +116,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||
| group: "default", | |||
| }; | |||
| const options = useMemo(() => { | |||
| const defaultLabel = suggestedLocationCode || t("W201 - 2F-A,B室"); | |||
| const defaultLabel = suggestedLocationCode || t("Select warehouse"); | |||
| return [ | |||
| { value: 1, label: defaultLabel, group: "default" }, | |||
| ...filteredWarehouse.map((w) => ({ | |||
| @@ -260,7 +260,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||
| editable: false, | |||
| renderCell(params) { | |||
| 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; | |||
| return ( | |||
| @@ -5,72 +5,67 @@ export const PO_WORKBENCH_DETAILS_GRID_MOCK_ROWS: readonly PoWorkbenchDetailsGri | |||
| [ | |||
| { | |||
| id: "mock-line-1", | |||
| itemName: | |||
| "澳洲和牛腱(冷凍)· A5級去筋切片裝 2.5kg×4包/箱(供應商批次:AU-WG-2026-Q1)", | |||
| itemCode: "AB1234", | |||
| itemName: "普通麵粉", | |||
| itemCode: "DEMO-RM-01", | |||
| category: "material", | |||
| storageLocation: "YF-W201-#A-12", | |||
| storageLocation: "DEMO-A-#A-01", | |||
| receiveStatus: "pending", | |||
| expectedQty: 100, | |||
| purchaseUnitLabel: "1箱X100包X150克", | |||
| purchaseUnitLabel: "1袋X25公斤", | |||
| receivedQtyPurchase: 0, | |||
| receivedQtyInventory: 0, | |||
| inventoryUnitLabel: "克", | |||
| inventoryUnitLabel: "公斤", | |||
| }, | |||
| { | |||
| id: "mock-line-2", | |||
| itemName: | |||
| "耐高温真空膠袋(食品級)· 300×450mm 厚度80μm 每卷500個(雙層密封條)", | |||
| itemCode: "BC5678", | |||
| itemName: "鮮奶", | |||
| itemCode: "DEMO-RM-02", | |||
| category: "consumable", | |||
| storageLocation: "YF-C305-#B-04", | |||
| storageLocation: "DEMO-B-#B-02", | |||
| receiveStatus: "receiving", | |||
| expectedQty: 24, | |||
| purchaseUnitLabel: "1箱X24卷X500個", | |||
| receivedQtyPurchase: 26, | |||
| receivedQtyInventory: 312000, | |||
| inventoryUnitLabel: "個", | |||
| purchaseUnitLabel: "1箱X12瓶X1升", | |||
| receivedQtyPurchase: 12, | |||
| receivedQtyInventory: 12, | |||
| inventoryUnitLabel: "升", | |||
| }, | |||
| { | |||
| id: "mock-line-3", | |||
| itemName: | |||
| "照燒汁(商用濃縮裝)· 日式醬油基底 5L/桶(開封後需冷藏,效期見標籤)", | |||
| itemCode: "FG9012", | |||
| itemName: "鄉村麵包", | |||
| itemCode: "DEMO-FG-01", | |||
| category: "finishedGoods", | |||
| storageLocation: "YF-D102-#C-08", | |||
| storageLocation: "DEMO-A-#C-03", | |||
| receiveStatus: "completed", | |||
| expectedQty: 48, | |||
| purchaseUnitLabel: "1板X4桶X5升", | |||
| purchaseUnitLabel: "1箱X8個", | |||
| receivedQtyPurchase: 48, | |||
| receivedQtyInventory: 960, | |||
| inventoryUnitLabel: "升", | |||
| receivedQtyInventory: 48, | |||
| inventoryUnitLabel: "個", | |||
| }, | |||
| { | |||
| id: "mock-line-4", | |||
| itemName: | |||
| "高筋麵粉預拌漿(半成品)· 中央廚房標準配方 20kg/袋(含酵母改良劑)", | |||
| itemCode: "DE3456", | |||
| itemName: "麵團", | |||
| itemCode: "DEMO-SF-01", | |||
| category: "semiFinished", | |||
| storageLocation: "YF-W118-#D-22", | |||
| storageLocation: "DEMO-A-#D-04", | |||
| receiveStatus: "receiving", | |||
| expectedQty: 60, | |||
| purchaseUnitLabel: "1板X3袋X20公斤", | |||
| receivedQtyPurchase: 55, | |||
| receivedQtyInventory: 3300, | |||
| purchaseUnitLabel: "1袋X10公斤", | |||
| receivedQtyPurchase: 40, | |||
| receivedQtyInventory: 40, | |||
| inventoryUnitLabel: "公斤", | |||
| }, | |||
| { | |||
| id: "mock-line-5", | |||
| itemName: | |||
| "廚房多功能清潔劑(濃縮原液)· 無磷配方 1L×12瓶/箱(勿與漂白水混用)", | |||
| itemCode: "EF7890", | |||
| itemName: "奶包", | |||
| itemCode: "DEMO-FG-02", | |||
| category: "miscNonConsumable", | |||
| storageLocation: "YF-M402-#E-01", | |||
| storageLocation: "DEMO-B-#E-05", | |||
| receiveStatus: "pending", | |||
| expectedQty: 120, | |||
| purchaseUnitLabel: "1扎X3箱X12包X350克", | |||
| purchaseUnitLabel: "1箱X20個", | |||
| receivedQtyPurchase: 0, | |||
| receivedQtyInventory: 0, | |||
| inventoryUnitLabel: "瓶", | |||
| inventoryUnitLabel: "個", | |||
| }, | |||
| ] as const; | |||
| @@ -315,7 +315,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||
| const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| // 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码 | |||
| // 格式:{2fitesteXXX} = equipmentCode: "XXX" | |||
| // 例如:{2fiteste包裝機類-真空八爪魚機-1號} = equipmentCode: "包裝機類-真空八爪魚機-1號" | |||
| // 例如:{2fitesteDEMO-EQ-01} = equipmentCode: "DEMO-EQ-01" | |||
| if (qrValue.match(/\{2fiteste(.+)\}/)) { | |||
| const match = qrValue.match(/\{2fiteste(.+)\}/); | |||
| const equipmentCode = match![1]; | |||
| @@ -371,7 +371,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| // 可能是员工编号 | |||
| setScannedStaffNo(trimmedValue); | |||
| } else if (trimmedValue.includes("-")) { | |||
| // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") | |||
| // 可能包含 "-" 的是设备代码(如 "DEMO-EQ-01") | |||
| setScannedEquipmentCode(trimmedValue); | |||
| } | |||
| } | |||
| @@ -33,6 +33,9 @@ export interface ReportField { | |||
| 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 { | |||
| id: string; | |||
| title: string; | |||
| @@ -54,9 +57,20 @@ const asyncItemCodeField = ( | |||
| multiple: true, | |||
| allowInput: 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 => ({ | |||
| label: "貼上貨品編號 Paste Item Codes", | |||
| name: "itemCodePaste", | |||
| @@ -124,29 +138,12 @@ export const REPORTS: ReportDefinition[] = [ | |||
| name: "storeId", | |||
| type: "select", | |||
| 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: "儲位 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: "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", | |||
| type: "select", | |||
| 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: "狀態", | |||
| @@ -220,14 +211,11 @@ export const REPORTS: ReportDefinition[] = [ | |||
| multiple: true, | |||
| options: [ | |||
| { 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: [ | |||
| { label: "庫存日期 Stock Date(僅今天)", name: "stockDate", type: "date", required: true, disabled: true }, | |||
| { 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", | |||
| name: "storeId", | |||
| type: "select", | |||
| 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" }, | |||
| ], | |||
| @@ -271,15 +253,9 @@ export const REPORTS: ReportDefinition[] = [ | |||
| name: "storeId", | |||
| type: "select", | |||
| 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: "儲位 Slot", name: "slot", type: "text", required: false, placeholder: "e.g. 02" }, | |||
| { | |||
| @@ -299,8 +275,8 @@ export const REPORTS: ReportDefinition[] = [ | |||
| required: false, | |||
| options: [ | |||
| { label: "全部", value: "All" }, | |||
| { label: "PP", value: "PP" }, | |||
| { label: "PF", value: "PF" }, | |||
| { label: "採購", value: "purchase" }, | |||
| { label: "生產", value: "production" }, | |||
| { label: "其他", value: "other" }, | |||
| ], | |||
| }, | |||
| @@ -442,12 +418,10 @@ export const REPORTS: ReportDefinition[] = [ | |||
| { label: "類別 Category", name: "stockCategory", type: "select", required: false, | |||
| multiple: true, | |||
| 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, | |||
| multiple: true, | |||
| @@ -472,9 +446,9 @@ export const REPORTS: ReportDefinition[] = [ | |||
| { label: "類別 Category", name: "stockCategory", type: "select", required: false, | |||
| multiple: true, | |||
| 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, | |||
| multiple: true, | |||
| @@ -488,7 +462,7 @@ export const REPORTS: ReportDefinition[] = [ | |||
| }, | |||
| { | |||
| id: "rep-015", | |||
| title: "M18 BOM Shop 同步記錄", | |||
| title: "BOM Shop 同步記錄", | |||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history`, | |||
| responseType: "excel", | |||
| fields: [ | |||
| @@ -533,10 +507,7 @@ export const REPORTS: ReportDefinition[] = [ | |||
| name: "storeId", | |||
| type: "select", | |||
| required: false, | |||
| options: [ | |||
| { label: "2F", value: "2F" }, | |||
| { label: "4F", value: "4F" }, | |||
| ], | |||
| options: floorOptions, | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -574,11 +545,7 @@ export const REPORTS: ReportDefinition[] = [ | |||
| name: "storeId", | |||
| type: "select", | |||
| 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)", | |||
| "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", | |||
| "Floor label": "Floor", | |||
| "Add mapping": "Add mapping", | |||
| @@ -20,10 +20,10 @@ | |||
| "btnSyncDoExtra": "Sync DO (Extra) from M18", | |||
| "btnSyncProduct": "Sync product from M18", | |||
| "syncing": "Syncing...", | |||
| "placeholderPoCode": "e.g. PFP002PO26030341", | |||
| "placeholderPoCode": "e.g. DEMO-PO-001", | |||
| "placeholderDoCode": "e.g. same document code as M18 shop PO", | |||
| "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...", | |||
| "noResponse": "No response received", | |||
| "alreadyExists": "{{docName}} already exists in system", | |||
| @@ -15,6 +15,7 @@ | |||
| "noDataFoundTitle": "No Data Found / 查無資料", | |||
| "noDataFoundHint": "No inventory records match your search. Please try adjusting the filters.", | |||
| "ok": "OK", | |||
| "unavailableInDemo": "Unavailable in demo mode", | |||
| "missingRequired": "Missing required fields:\n- {{fields}}", | |||
| "requiredField": "This is a required field", | |||
| "dateNotBeforeToday": "Date cannot be before today:\n- {{fields}}", | |||
| @@ -57,17 +58,11 @@ | |||
| "warehouse": "Warehouse", | |||
| "area": "Area", | |||
| "slot": "Slot", | |||
| "lotNo": "Lot No", | |||
| "poPrefix": "PP/PF" | |||
| "lotNo": "Lot No" | |||
| }, | |||
| "options": { | |||
| "storeId": { | |||
| "All": "All" | |||
| }, | |||
| "poPrefix": { | |||
| "All": "All", | |||
| "PP": "PP", | |||
| "PF": "PF" | |||
| } | |||
| }, | |||
| "fieldHints": { | |||
| @@ -107,14 +102,11 @@ | |||
| }, | |||
| "type": { | |||
| "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": { | |||
| "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": { | |||
| @@ -222,10 +222,17 @@ | |||
| }, | |||
| "fieldHints": { | |||
| "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": { | |||
| "title": "M18 BOM Shop Sync History", | |||
| "title": "BOM Shop Sync History", | |||
| "fields": { | |||
| "syncDateStart": "Sync Date Start", | |||
| "syncDateEnd": "Sync Date End", | |||
| @@ -295,8 +302,8 @@ | |||
| }, | |||
| "lotOrigin": { | |||
| "All": "All", | |||
| "PP": "PP", | |||
| "PF": "PF", | |||
| "purchase": "Purchase", | |||
| "production": "Production", | |||
| "other": "Other" | |||
| } | |||
| } | |||
| @@ -326,13 +333,9 @@ | |||
| "unitPrice": "Unit Price", | |||
| "currency": "Currency", | |||
| "amount": "Amount", | |||
| "grnCode": "GRN Code / M18 Receipt No.", | |||
| "grnId": "GRN Id / M18 Record Id", | |||
| "poCreator": "PO creator (M18)", | |||
| "note": "Note", | |||
| "category": "Category", | |||
| "totalAmount": "Total Amount", | |||
| "grnCodes": "GRN Code(s) / M18 Receipt No." | |||
| "totalAmount": "Total Amount" | |||
| }, | |||
| "bomSync": { | |||
| "sheetSync": "BOM Sync Log", | |||
| @@ -1,12 +1,12 @@ | |||
| { | |||
| "title": "送貨單樓層設定(供應商代碼)", | |||
| "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": "編輯樓層供應商映射", | |||
| "Floor label": "樓層", | |||
| "Add mapping": "新增映射", | |||
| @@ -1,3 +1,3 @@ | |||
| { | |||
| "title": "檸檬機(激光機)" | |||
| "title": "激光機" | |||
| } | |||
| @@ -20,10 +20,10 @@ | |||
| "btnSyncDoExtra": "從 M18 同步送貨訂單(加單)", | |||
| "btnSyncProduct": "從 M18 同步貨品", | |||
| "syncing": "同步中…", | |||
| "placeholderPoCode": "例:PFP002PO26030341", | |||
| "placeholderPoCode": "例:DEMO-PO-001", | |||
| "placeholderDoCode": "例:與 M18 店鋪訂單相同的文件編號", | |||
| "placeholderDoExtraCode": "可輸入多個 code,用逗號或換行分隔", | |||
| "placeholderProductCode": "例:PP1175(M18 貨品編號)", | |||
| "placeholderProductCode": "例:DEMO-FG-01", | |||
| "waiting": "等待實作…", | |||
| "noResponse": "未收到回應", | |||
| "alreadyExists": "{{docName}}已存在系統", | |||
| @@ -25,7 +25,7 @@ | |||
| "nav.breadcrumb.joEdit": "工單詳情", | |||
| "nav.breadcrumb.joTesting": "工單測試", | |||
| "nav.breadcrumb.joWorkbench": "工單工作台", | |||
| "nav.breadcrumb.laserPrint": "檸檬機(激光機)", | |||
| "nav.breadcrumb.laserPrint": "激光機", | |||
| "nav.breadcrumb.m18Sync": "M18 同步", | |||
| "nav.breadcrumb.poEdit": "編輯", | |||
| "nav.breadcrumb.poWorkbench": "採購單工作台", | |||
| @@ -64,7 +64,7 @@ | |||
| "nav.jobOrder.productionProcess": "工單生產流程", | |||
| "nav.jobOrder.searchCreate": "搜索工單/ 建立工單", | |||
| "nav.jobOrderManagement": "管理工單", | |||
| "nav.laserPrint": "檸檬機(激光機)", | |||
| "nav.laserPrint": "激光機", | |||
| "nav.m18Sync": "M18 同步", | |||
| "nav.productionServer": "正式伺服器", | |||
| "nav.report": "報告管理", | |||
| @@ -15,6 +15,7 @@ | |||
| "noDataFoundTitle": "查無資料", | |||
| "noDataFoundHint": "找不到符合您搜尋條件的庫存資料,請嘗試調整篩選條件。", | |||
| "ok": "確定", | |||
| "unavailableInDemo": "示範模式無法使用", | |||
| "missingRequired": "缺少必填條件:\n- {{fields}}", | |||
| "requiredField": "此為必填欄位", | |||
| "dateNotBeforeToday": "日期不可早於今天:\n- {{fields}}", | |||
| @@ -57,17 +58,11 @@ | |||
| "warehouse": "倉庫", | |||
| "area": "區域", | |||
| "slot": "儲位", | |||
| "lotNo": "批號", | |||
| "poPrefix": "PP/PF 分類" | |||
| "lotNo": "批號" | |||
| }, | |||
| "options": { | |||
| "storeId": { | |||
| "All": "全部" | |||
| }, | |||
| "poPrefix": { | |||
| "All": "全部", | |||
| "PP": "PP", | |||
| "PF": "PF" | |||
| } | |||
| }, | |||
| "fieldHints": { | |||
| @@ -107,14 +102,11 @@ | |||
| }, | |||
| "type": { | |||
| "All": "全部", | |||
| "PP": "PP", | |||
| "PF": "PF", | |||
| "TOA": "TOA", | |||
| "工廠生產": "工廠生產", | |||
| "倉存調整": "倉存調整", | |||
| "期初存貨": "期初存貨", | |||
| "採購入倉": "採購入倉", | |||
| "其他入倉": "其他入倉" | |||
| "purchase": "採購", | |||
| "production": "生產", | |||
| "adjustment": "倉存調整", | |||
| "opening": "期初存貨", | |||
| "other": "其他" | |||
| } | |||
| } | |||
| }, | |||
| @@ -208,6 +200,14 @@ | |||
| }, | |||
| "fieldHints": { | |||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||
| }, | |||
| "options": { | |||
| "stockCategory": { | |||
| "All": "全部", | |||
| "mat": "原料", | |||
| "sfg": "半成品", | |||
| "fg": "成品" | |||
| } | |||
| } | |||
| }, | |||
| "rep-005": { | |||
| @@ -222,10 +222,17 @@ | |||
| }, | |||
| "fieldHints": { | |||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||
| }, | |||
| "options": { | |||
| "stockCategory": { | |||
| "All": "全部", | |||
| "sfg": "半成品", | |||
| "fg": "成品" | |||
| } | |||
| } | |||
| }, | |||
| "rep-015": { | |||
| "title": "M18 BOM Shop 同步記錄", | |||
| "title": "BOM Shop 同步記錄", | |||
| "fields": { | |||
| "syncDateStart": "同步日期:由", | |||
| "syncDateEnd": "同步日期:至", | |||
| @@ -295,8 +302,8 @@ | |||
| }, | |||
| "lotOrigin": { | |||
| "All": "全部", | |||
| "PP": "PP", | |||
| "PF": "PF", | |||
| "purchase": "採購", | |||
| "production": "生產", | |||
| "other": "其他" | |||
| } | |||
| } | |||
| @@ -326,13 +333,9 @@ | |||
| "unitPrice": "單價", | |||
| "currency": "貨幣", | |||
| "amount": "金額", | |||
| "grnCode": "M18 入倉單號", | |||
| "grnId": "M18 記錄編號", | |||
| "poCreator": "PO建立者(M18)", | |||
| "note": "備註", | |||
| "category": "類別", | |||
| "totalAmount": "金額", | |||
| "grnCodes": "M18 入倉單號" | |||
| "totalAmount": "金額" | |||
| }, | |||
| "bomSync": { | |||
| "sheetSync": "BOM同步記錄", | |||