| @@ -1,6 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import React, { useState, useEffect } from 'react'; | |||||
| import React, { useState, useEffect, useRef } from 'react'; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { | import { | ||||
| Dialog, | Dialog, | ||||
| @@ -28,6 +28,38 @@ import { | |||||
| } from './semiFGProductionAnalysisApi'; | } from './semiFGProductionAnalysisApi'; | ||||
| import { parseItemCodeTokens } from './parseItemCodeTokens'; | import { parseItemCodeTokens } from './parseItemCodeTokens'; | ||||
| function validateSemiFgCriteria( | |||||
| criteria: Record<string, string>, | |||||
| t: (key: string) => string, | |||||
| ): string | null { | |||||
| const view = criteria.view || 'year'; | |||||
| if (view === 'day' && !/^\d{4}-\d{2}-\d{2}$/.test(criteria.reportDate || '')) { | |||||
| return t('semiFgNeedDate'); | |||||
| } | |||||
| if (view === 'week' && !/^\d{4}-\d{2}-\d{2}$/.test(criteria.reportWeek || '')) { | |||||
| return t('semiFgNeedWeek'); | |||||
| } | |||||
| if (view === 'month' && !/^\d{4}-\d{2}$/.test(criteria.reportMonth || '')) { | |||||
| return t('semiFgNeedMonth'); | |||||
| } | |||||
| if (view === 'year' && !/^\d{4}$/.test(criteria.year || '')) { | |||||
| return t('semiFgNeedYear'); | |||||
| } | |||||
| if (view === 'range') { | |||||
| const start = criteria.lastOutDateStart || ''; | |||||
| const end = criteria.lastOutDateEnd || ''; | |||||
| if (!/^\d{4}-\d{2}-\d{2}$/.test(start) || !/^\d{4}-\d{2}-\d{2}$/.test(end)) { | |||||
| return t('semiFgNeedRange'); | |||||
| } | |||||
| if (start > end) return t('semiFgRangeOrder'); | |||||
| const [fromYear, fromMonth] = start.split('-').map(Number); | |||||
| const [toYear, toMonth] = end.split('-').map(Number); | |||||
| const span = (toYear - fromYear) * 12 + (toMonth - fromMonth) + 1; | |||||
| if (span > 24) return t('semiFgRangeSpan'); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| interface SemiFGProductionAnalysisReportProps { | interface SemiFGProductionAnalysisReportProps { | ||||
| criteria: Record<string, string>; | criteria: Record<string, string>; | ||||
| requiredFieldLabels: string[]; | requiredFieldLabels: string[]; | ||||
| @@ -45,7 +77,8 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| reportTitle = '成品/半成品生產分析報告', | reportTitle = '成品/半成品生產分析報告', | ||||
| onExportSuccess, | onExportSuccess, | ||||
| }: SemiFGProductionAnalysisReportProps) { | }: SemiFGProductionAnalysisReportProps) { | ||||
| const { t } = useTranslation("report"); | |||||
| const { t, i18n } = useTranslation("report"); | |||||
| const inFlightRef = useRef(false); | |||||
| const [showConfirmDialog, setShowConfirmDialog] = useState(false); | const [showConfirmDialog, setShowConfirmDialog] = useState(false); | ||||
| const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]); | const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]); | ||||
| const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({}); | const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({}); | ||||
| @@ -70,8 +103,13 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| }, [criteria.stockCategory]); | }, [criteria.stockCategory]); | ||||
| const handleExportClick = async (format: 'pdf' | 'excel') => { | const handleExportClick = async (format: 'pdf' | 'excel') => { | ||||
| if (inFlightRef.current) return; | |||||
| setExportFormat(format); | setExportFormat(format); | ||||
| // Validate required fields | |||||
| const viewError = validateSemiFgCriteria(criteria, t); | |||||
| if (viewError) { | |||||
| alert(viewError); | |||||
| return; | |||||
| } | |||||
| if (requiredFieldLabels.length > 0) { | if (requiredFieldLabels.length > 0) { | ||||
| alert(t('missingRequired', { fields: requiredFieldLabels.join('\n- ') })); | alert(t('missingRequired', { fields: requiredFieldLabels.join('\n- ') })); | ||||
| return; | return; | ||||
| @@ -98,20 +136,24 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| }; | }; | ||||
| const executeExport = async (format: 'pdf' | 'excel' = exportFormat) => { | const executeExport = async (format: 'pdf' | 'excel' = exportFormat) => { | ||||
| if (inFlightRef.current) return; | |||||
| inFlightRef.current = true; | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| if (format === 'excel') { | if (format === 'excel') { | ||||
| await generateSemiFGProductionAnalysisReportExcel(criteria, reportTitle); | |||||
| await generateSemiFGProductionAnalysisReportExcel(criteria, reportTitle, i18n.language); | |||||
| } else { | } else { | ||||
| await generateSemiFGProductionAnalysisReport(criteria, reportTitle); | |||||
| await generateSemiFGProductionAnalysisReport(criteria, reportTitle, i18n.language); | |||||
| } | } | ||||
| onExportSuccess?.(format); | onExportSuccess?.(format); | ||||
| setShowConfirmDialog(false); | setShowConfirmDialog(false); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error('Failed to generate report:', error); | console.error('Failed to generate report:', error); | ||||
| alert(t('generateError')); | |||||
| const message = error instanceof Error && error.message ? error.message : t('generateError'); | |||||
| alert(message); | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| inFlightRef.current = false; | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -130,6 +130,8 @@ export default function ReportPage() { | |||||
| setCriteria({ storeId: 'All', poPrefix: 'All' }); | setCriteria({ storeId: 'All', poPrefix: 'All' }); | ||||
| } else if (reportId === 'rep-021') { | } else if (reportId === 'rep-021') { | ||||
| setCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' }); | setCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' }); | ||||
| } else if (reportId === 'rep-005') { | |||||
| setCriteria({ view: 'year', year: String(new Date().getFullYear()) }); | |||||
| } else { | } else { | ||||
| setCriteria({}); | setCriteria({}); | ||||
| } | } | ||||
| @@ -149,6 +151,14 @@ export default function ReportPage() { | |||||
| const m = stringValue.trim().match(/^w(\d)/i); | const m = stringValue.trim().match(/^w(\d)/i); | ||||
| if (m) next.storeId = `${m[1]}F`; | if (m) next.storeId = `${m[1]}F`; | ||||
| } | } | ||||
| if (currentReport?.id === 'rep-005' && name === 'view') { | |||||
| if (stringValue === 'year' && !next.year) { | |||||
| next.year = String(new Date().getFullYear()); | |||||
| } | |||||
| if (stringValue === 'week' && !next.reportWeek) { | |||||
| next.reportWeek = dayjs().format('YYYY-MM-DD'); | |||||
| } | |||||
| } | |||||
| return next; | return next; | ||||
| }); | }); | ||||
| @@ -595,6 +605,9 @@ export default function ReportPage() { | |||||
| > | > | ||||
| <Grid container spacing={3}> | <Grid container spacing={3}> | ||||
| {currentReport.fields.map((field) => { | {currentReport.fields.map((field) => { | ||||
| if (field.showWhen && !field.showWhen.values.includes(criteria[field.showWhen.field] || '')) { | |||||
| return null; | |||||
| } | |||||
| const fieldKey = `${currentReport.id}-${field.name}`; | const fieldKey = `${currentReport.id}-${field.name}`; | ||||
| const translatedLabel = fieldLabel(currentReport.id, field); | const translatedLabel = fieldLabel(currentReport.id, field); | ||||
| const rawOptions = field.dynamicOptions | const rawOptions = field.dynamicOptions | ||||
| @@ -624,21 +637,35 @@ export default function ReportPage() { | |||||
| field.name === 'status' && | field.name === 'status' && | ||||
| rep012MultiRound; | rep012MultiRound; | ||||
| if (field.type === 'date') { | |||||
| const parsed = currentValue ? dayjs(currentValue) : null; | |||||
| if (field.type === 'date' || field.type === 'month' || field.type === 'year') { | |||||
| const isMonth = field.type === 'month'; | |||||
| const isYear = field.type === 'year'; | |||||
| const parsed = currentValue | |||||
| ? dayjs(isYear ? `${currentValue}-01-01` : isMonth ? `${currentValue}-01` : currentValue) | |||||
| : null; | |||||
| const storedFormat = isYear ? 'YYYY' : isMonth ? 'YYYY-MM' : OUTPUT_DATE_FORMAT; | |||||
| const dateError = fieldErrors[field.name]; | const dateError = fieldErrors[field.name]; | ||||
| const semiFgBounds = currentReport.id === 'rep-005'; | |||||
| return ( | return ( | ||||
| <Grid item {...gridSize} key={fieldKey}> | <Grid item {...gridSize} key={fieldKey}> | ||||
| <DatePicker | <DatePicker | ||||
| label={translatedLabel} | label={translatedLabel} | ||||
| format={dateDisplayFormat} | |||||
| views={isYear ? ['year'] : isMonth ? ['year', 'month'] : undefined} | |||||
| openTo={isYear ? 'year' : isMonth ? 'month' : undefined} | |||||
| format={isYear ? 'YYYY' : isMonth ? 'YYYY-MM' : dateDisplayFormat} | |||||
| value={parsed?.isValid() ? parsed : null} | value={parsed?.isValid() ? parsed : null} | ||||
| minDate={field.minDate === 'today' ? dayjs().startOf('day') : undefined} | |||||
| minDate={ | |||||
| field.minDate === 'today' | |||||
| ? dayjs().startOf('day') | |||||
| : semiFgBounds | |||||
| ? dayjs('2025-01-01') | |||||
| : undefined | |||||
| } | |||||
| disabled={disabledByCheckedCheckbox || disabledRep012Status || !!field.disabled} | disabled={disabledByCheckedCheckbox || disabledRep012Status || !!field.disabled} | ||||
| onChange={(date) => { | onChange={(date) => { | ||||
| handleFieldChange( | handleFieldChange( | ||||
| field.name, | field.name, | ||||
| date?.isValid() ? date.format(OUTPUT_DATE_FORMAT) : '', | |||||
| date?.isValid() ? date.format(storedFormat) : '', | |||||
| ); | ); | ||||
| }} | }} | ||||
| slotProps={{ | slotProps={{ | ||||
| @@ -646,7 +673,7 @@ export default function ReportPage() { | |||||
| fullWidth: true, | fullWidth: true, | ||||
| required: field.required, | required: field.required, | ||||
| error: Boolean(dateError), | error: Boolean(dateError), | ||||
| helperText: dateError || undefined, | |||||
| helperText: dateError || hintText || undefined, | |||||
| sx: { | sx: { | ||||
| ...FIELD_ERROR_SX, | ...FIELD_ERROR_SX, | ||||
| ...(currentReport.id === 'rep-005' ? { | ...(currentReport.id === 'rep-005' ? { | ||||
| @@ -71,7 +71,8 @@ export const fetchSemiFGItemCodesWithCategory = async ( | |||||
| */ | */ | ||||
| export const generateSemiFGProductionAnalysisReport = async ( | export const generateSemiFGProductionAnalysisReport = async ( | ||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = '成品/半成品生產分析報告' | |||||
| reportTitle: string = '成品/半成品生產分析報告', | |||||
| language?: string, | |||||
| ): Promise<void> => { | ): Promise<void> => { | ||||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`; | const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`; | ||||
| @@ -81,11 +82,11 @@ export const generateSemiFGProductionAnalysisReport = async ( | |||||
| Accept: 'application/pdf', | Accept: 'application/pdf', | ||||
| 'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||||
| }, | }, | ||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, 'itemCode')), | |||||
| body: JSON.stringify(reportRequestBody(criteria, language)), | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); | if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); | ||||
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |||||
| if (!response.ok) throw new Error(await readReportError(response)); | |||||
| const blob = await response.blob(); | const blob = await response.blob(); | ||||
| const downloadUrl = window.URL.createObjectURL(blob); | const downloadUrl = window.URL.createObjectURL(blob); | ||||
| @@ -113,7 +114,8 @@ export const generateSemiFGProductionAnalysisReport = async ( | |||||
| */ | */ | ||||
| export const generateSemiFGProductionAnalysisReportExcel = async ( | export const generateSemiFGProductionAnalysisReportExcel = async ( | ||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = '成品/半成品生產分析報告' | |||||
| reportTitle: string = '成品/半成品生產分析報告', | |||||
| language?: string, | |||||
| ): Promise<void> => { | ): Promise<void> => { | ||||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis-excel`; | const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis-excel`; | ||||
| @@ -123,11 +125,11 @@ export const generateSemiFGProductionAnalysisReportExcel = async ( | |||||
| Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | ||||
| 'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||||
| }, | }, | ||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, 'itemCode')), | |||||
| body: JSON.stringify(reportRequestBody(criteria, language)), | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) throw new Error('Unauthorized'); | if (response.status === 401 || response.status === 403) throw new Error('Unauthorized'); | ||||
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |||||
| if (!response.ok) throw new Error(await readReportError(response)); | |||||
| const blob = await response.blob(); | const blob = await response.blob(); | ||||
| const downloadUrl = window.URL.createObjectURL(blob); | const downloadUrl = window.URL.createObjectURL(blob); | ||||
| @@ -146,3 +148,20 @@ export const generateSemiFGProductionAnalysisReportExcel = async ( | |||||
| link.remove(); | link.remove(); | ||||
| window.URL.revokeObjectURL(downloadUrl); | window.URL.revokeObjectURL(downloadUrl); | ||||
| }; | }; | ||||
| function reportRequestBody(criteria: Record<string, string>, language?: string): Record<string, unknown> { | |||||
| const body = buildItemCodePasteRequestBody(criteria, 'itemCode'); | |||||
| body.lang = language?.toLowerCase().startsWith('en') ? 'en' : 'zh'; | |||||
| return body; | |||||
| } | |||||
| async function readReportError(response: Response): Promise<string> { | |||||
| const text = await response.text(); | |||||
| try { | |||||
| const json = JSON.parse(text) as { message?: string }; | |||||
| if (json.message) return json.message; | |||||
| } catch { | |||||
| // Response body is plain text. | |||||
| } | |||||
| return text || `HTTP ${response.status}`; | |||||
| } | |||||
| @@ -1,5 +1,5 @@ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.0 | 2026-08-03 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.0 | 2026-08-03 */ | ||||
| export type FieldType = 'date' | 'text' | 'select' | 'number' | 'checkbox'; | |||||
| export type FieldType = 'date' | 'month' | 'year' | 'text' | 'select' | 'number' | 'checkbox'; | |||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| @@ -29,6 +29,8 @@ export interface ReportField { | |||||
| multiline?: boolean; | multiline?: boolean; | ||||
| /** Rows for multiline text areas. Default 4. */ | /** Rows for multiline text areas. Default 4. */ | ||||
| minRows?: number; | minRows?: number; | ||||
| /** Show this field only when another field has one of these values. */ | |||||
| showWhen?: { field: string; values: string[] }; | |||||
| } | } | ||||
| export type ReportResponseType = 'pdf' | 'excel'; | export type ReportResponseType = 'pdf' | 'excel'; | ||||
| @@ -466,15 +468,33 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| title: "成品/半成品生產分析報告", | title: "成品/半成品生產分析報告", | ||||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`, | apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`, | ||||
| fields: [ | fields: [ | ||||
| { label: "完成生產日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false, placeholder: "dd/mm/yyyy" }, | |||||
| { label: "完成生產日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false, placeholder: "dd/mm/yyyy" }, | |||||
| { label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, | |||||
| { label: "檢視 View", name: "view", type: "select", required: true, | |||||
| options: [ | |||||
| { label: "單日", value: "day" }, | |||||
| { label: "單週", value: "week" }, | |||||
| { label: "單月", value: "month" }, | |||||
| { label: "全年", value: "year" }, | |||||
| { label: "自訂日期範圍", value: "range" }, | |||||
| ] }, | |||||
| { label: "日期 Date", name: "reportDate", type: "date", required: false, placeholder: "dd/mm/yyyy", | |||||
| showWhen: { field: "view", values: ["day"] } }, | |||||
| { label: "週 Week", name: "reportWeek", type: "date", required: false, placeholder: "dd/mm/yyyy", | |||||
| showWhen: { field: "view", values: ["week"] } }, | |||||
| { label: "月份 Month", name: "reportMonth", type: "month", required: false, placeholder: "yyyy-mm", | |||||
| showWhen: { field: "view", values: ["month"] } }, | |||||
| { label: "年份 Year", name: "year", type: "year", required: false, | |||||
| showWhen: { field: "view", values: ["year"] } }, | |||||
| { label: "完成生產日期:由 Date From", name: "lastOutDateStart", type: "date", required: false, placeholder: "dd/mm/yyyy", | |||||
| showWhen: { field: "view", values: ["range"] } }, | |||||
| { label: "完成生產日期:至 Date To", name: "lastOutDateEnd", type: "date", required: false, placeholder: "dd/mm/yyyy", | |||||
| showWhen: { field: "view", values: ["range"] } }, | |||||
| { 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: "All", value: "All" }, | ||||
| { label: "WIP", value: "WIP" }, | { label: "WIP", value: "WIP" }, | ||||
| { label: "FG", value: "FG" }, | { label: "FG", value: "FG" }, | ||||
| { label: "Material", value: "mat" }, | |||||
| ] }, | ] }, | ||||
| { label: "貨品編號 Item Code", name: "itemCode", type: "select", required: false, | { label: "貨品編號 Item Code", name: "itemCode", type: "select", required: false, | ||||
| multiple: true, | multiple: true, | ||||
| @@ -29,6 +29,13 @@ | |||||
| "semiFgConfirmHint": "Please confirm the selected item codes and their categories:", | "semiFgConfirmHint": "Please confirm the selected item codes and their categories:", | ||||
| "semiFgColItem": "Item code and name", | "semiFgColItem": "Item code and name", | ||||
| "semiFgColCategory": "Category", | "semiFgColCategory": "Category", | ||||
| "semiFgNeedDate": "Please choose a date", | |||||
| "semiFgNeedWeek": "Please choose a date in the week", | |||||
| "semiFgNeedMonth": "Please choose a month", | |||||
| "semiFgNeedYear": "Please enter a 4-digit year (for example 2026)", | |||||
| "semiFgNeedRange": "Please choose a production date from and to", | |||||
| "semiFgRangeOrder": "The start date cannot be after the end date", | |||||
| "semiFgRangeSpan": "A custom date range can cover at most 24 months", | |||||
| "qcScopeHelpAll": "Export all QC inspection items (including temperature / humidity).", | "qcScopeHelpAll": "Export all QC inspection items (including temperature / humidity).", | ||||
| "qcScopeHelpMeasurable": "Export temperature / humidity QC items only (same as the previous production default).", | "qcScopeHelpMeasurable": "Export temperature / humidity QC items only (same as the previous production default).", | ||||
| "categories": { | "categories": { | ||||
| @@ -213,15 +220,40 @@ | |||||
| "rep-005": { | "rep-005": { | ||||
| "title": "FG / Semi-FG Production Analysis Report", | "title": "FG / Semi-FG Production Analysis Report", | ||||
| "fields": { | "fields": { | ||||
| "lastOutDateStart": "Production Complete Date Start", | |||||
| "lastOutDateEnd": "Production Complete Date End", | |||||
| "view": "View", | |||||
| "reportDate": "Date", | |||||
| "reportWeek": "Week", | |||||
| "reportMonth": "Month", | |||||
| "year": "Year", | "year": "Year", | ||||
| "lastOutDateStart": "Production date from", | |||||
| "lastOutDateEnd": "Production date to", | |||||
| "stockCategory": "Category", | "stockCategory": "Category", | ||||
| "itemCode": "Item Code", | "itemCode": "Item Code", | ||||
| "itemCodePaste": "Paste Item Codes" | "itemCodePaste": "Paste Item Codes" | ||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| "view": "One day, one week, one month, one year, or a custom date range", | |||||
| "reportWeek": "Shows Monday to Sunday of the date you pick.", | |||||
| "lastOutDateStart": "Up to one month is shown by day. A longer range is shown by month.", | |||||
| "lastOutDateEnd": "Up to one month is shown by day. A longer range is shown by month.", | |||||
| "stockCategory": "All is finished goods and semi-finished goods. Material is separate.", | |||||
| "itemCode": "A full code matches that item only. Category still applies.", | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines. A full code matches that item only." | |||||
| }, | |||||
| "options": { | |||||
| "view": { | |||||
| "day": "Day", | |||||
| "week": "Week", | |||||
| "month": "Month", | |||||
| "year": "Year", | |||||
| "range": "Custom date range" | |||||
| }, | |||||
| "stockCategory": { | |||||
| "All": "All", | |||||
| "WIP": "WIP", | |||||
| "FG": "FG", | |||||
| "mat": "Material" | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-015": { | "rep-015": { | ||||
| @@ -29,6 +29,13 @@ | |||||
| "semiFgConfirmHint": "請確認以下已選擇的物料編號及其類別:", | "semiFgConfirmHint": "請確認以下已選擇的物料編號及其類別:", | ||||
| "semiFgColItem": "物料編號及名稱", | "semiFgColItem": "物料編號及名稱", | ||||
| "semiFgColCategory": "類別", | "semiFgColCategory": "類別", | ||||
| "semiFgNeedDate": "請選擇日期", | |||||
| "semiFgNeedWeek": "請選擇該週的其中一天", | |||||
| "semiFgNeedMonth": "請選擇月份", | |||||
| "semiFgNeedYear": "請輸入四位數年份(例如 2026)", | |||||
| "semiFgNeedRange": "請選擇完成生產日期(由/至)", | |||||
| "semiFgRangeOrder": "開始日期不可晚於結束日期", | |||||
| "semiFgRangeSpan": "自訂日期最多 24 個月", | |||||
| "qcScopeHelpAll": "匯出全部 QC 檢驗項目(含溫度/濕度)。", | "qcScopeHelpAll": "匯出全部 QC 檢驗項目(含溫度/濕度)。", | ||||
| "qcScopeHelpMeasurable": "僅匯出溫度/濕度 QC 項目(與 production 舊預設相同)。", | "qcScopeHelpMeasurable": "僅匯出溫度/濕度 QC 項目(與 production 舊預設相同)。", | ||||
| "categories": { | "categories": { | ||||
| @@ -213,15 +220,40 @@ | |||||
| "rep-005": { | "rep-005": { | ||||
| "title": "成品/半成品生產分析報告", | "title": "成品/半成品生產分析報告", | ||||
| "fields": { | "fields": { | ||||
| "view": "檢視", | |||||
| "reportDate": "日期", | |||||
| "reportWeek": "週", | |||||
| "reportMonth": "月份", | |||||
| "year": "年份", | |||||
| "lastOutDateStart": "完成生產日期:由", | "lastOutDateStart": "完成生產日期:由", | ||||
| "lastOutDateEnd": "完成生產日期:至", | "lastOutDateEnd": "完成生產日期:至", | ||||
| "year": "年份", | |||||
| "stockCategory": "類別", | "stockCategory": "類別", | ||||
| "itemCode": "貨品編號", | "itemCode": "貨品編號", | ||||
| "itemCodePaste": "貼上貨品編號" | "itemCodePaste": "貼上貨品編號" | ||||
| }, | }, | ||||
| "fieldHints": { | "fieldHints": { | ||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| "view": "單日、單週、單月、全年,或自訂日期範圍", | |||||
| "reportWeek": "以所選日期的星期一至星期日顯示。", | |||||
| "lastOutDateStart": "一個月以內按日顯示,超過一個月按月顯示。", | |||||
| "lastOutDateEnd": "一個月以內按日顯示,超過一個月按月顯示。", | |||||
| "stockCategory": "全部只包含成品與半成品。材料請另選「材料」。", | |||||
| "itemCode": "完整貨品編號只會對上該編號,類別仍然有效。", | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔。完整編號只會對上該編號。" | |||||
| }, | |||||
| "options": { | |||||
| "view": { | |||||
| "day": "單日", | |||||
| "week": "單週", | |||||
| "month": "單月", | |||||
| "year": "全年", | |||||
| "range": "自訂日期範圍" | |||||
| }, | |||||
| "stockCategory": { | |||||
| "All": "全部", | |||||
| "WIP": "半成品", | |||||
| "FG": "成品", | |||||
| "mat": "材料" | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-015": { | "rep-015": { | ||||