| @@ -1,4 +1,5 @@ | |||||
| API_URL=http://localhost:8090/api | API_URL=http://localhost:8090/api | ||||
| NEXTAUTH_SECRET=secret | NEXTAUTH_SECRET=secret | ||||
| NEXT_PUBLIC_API_URL=http://localhost:8090/api | NEXT_PUBLIC_API_URL=http://localhost:8090/api | ||||
| NEXT_PUBLIC_MONITORING_ENABLED=false | |||||
| NEXT_PUBLIC_MONITORING_ENABLED=false | |||||
| NEXT_PUBLIC_QC_MEASUREMENT_ENABLED=false | |||||
| @@ -1,4 +1,5 @@ | |||||
| API_URL=http://10.10.0.81:8090/api | API_URL=http://10.10.0.81:8090/api | ||||
| NEXTAUTH_SECRET=secret | NEXTAUTH_SECRET=secret | ||||
| NEXTAUTH_URL=http://10.10.0.81:3000 | NEXTAUTH_URL=http://10.10.0.81:3000 | ||||
| NEXT_PUBLIC_API_URL=http://10.10.0.81:8090/api | |||||
| NEXT_PUBLIC_API_URL=http://10.10.0.81:8090/api | |||||
| NEXT_PUBLIC_QC_MEASUREMENT_ENABLED=false | |||||
| @@ -0,0 +1,93 @@ | |||||
| "use client"; | |||||
| import React from "react"; | |||||
| import { | |||||
| FormHelperText, | |||||
| Grid, | |||||
| MenuItem, | |||||
| TextField, | |||||
| } from "@mui/material"; | |||||
| export type QcItemFilter = "measurable" | "non_measurable"; | |||||
| export const REP010_DEFAULT_CRITERIA: Record<string, string> = { | |||||
| qcItemFilter: "measurable", | |||||
| }; | |||||
| interface ItemQcReportFiltersProps { | |||||
| criteria: Record<string, string>; | |||||
| onFieldChange: (name: string, value: string) => void; | |||||
| } | |||||
| const gridSize = { xs: 12, sm: 6 }; | |||||
| export default function ItemQcReportFilters({ | |||||
| criteria, | |||||
| onFieldChange, | |||||
| }: ItemQcReportFiltersProps) { | |||||
| const qcItemFilter = (criteria.qcItemFilter || "measurable") as QcItemFilter; | |||||
| return ( | |||||
| <Grid container spacing={3}> | |||||
| <Grid item {...gridSize}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="QC 檢測日期:由 Last In Date Start" | |||||
| type="date" | |||||
| value={criteria.lastInDateStart || ""} | |||||
| onChange={(e) => onFieldChange("lastInDateStart", e.target.value)} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item {...gridSize}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="QC 檢測日期:至 Last In Date End" | |||||
| type="date" | |||||
| value={criteria.lastInDateEnd || ""} | |||||
| onChange={(e) => onFieldChange("lastInDateEnd", e.target.value)} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item {...gridSize}> | |||||
| <TextField | |||||
| fullWidth | |||||
| select | |||||
| label="QC 類型" | |||||
| value={criteria.qcType || ""} | |||||
| onChange={(e) => onFieldChange("qcType", e.target.value)} | |||||
| > | |||||
| <MenuItem value="">全部</MenuItem> | |||||
| <MenuItem value="IQC">IQC</MenuItem> | |||||
| <MenuItem value="EPQC">EPQC</MenuItem> | |||||
| </TextField> | |||||
| </Grid> | |||||
| <Grid item {...gridSize}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="貨品編號 Item Code" | |||||
| value={criteria.itemCode || ""} | |||||
| onChange={(e) => onFieldChange("itemCode", e.target.value)} | |||||
| placeholder="e.g. MJ0364" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item {...gridSize}> | |||||
| <TextField | |||||
| fullWidth | |||||
| select | |||||
| label="溫濕度數據篩選" | |||||
| value={qcItemFilter} | |||||
| onChange={(e) => onFieldChange("qcItemFilter", e.target.value)} | |||||
| > | |||||
| <MenuItem value="measurable">只包含溫度濕度</MenuItem> | |||||
| <MenuItem value="non_measurable">不包含溫度濕度</MenuItem> | |||||
| </TextField> | |||||
| <FormHelperText> | |||||
| {qcItemFilter === "measurable" | |||||
| ? "僅匯出已填寫實測值的溫度/濕度 QC 項目。" | |||||
| : "僅匯出非溫度/濕度之其他 QC 檢驗項目。"} | |||||
| </FormHelperText> | |||||
| </Grid> | |||||
| </Grid> | |||||
| ); | |||||
| } | |||||
| @@ -393,7 +393,7 @@ export default function ReportPage() { | |||||
| 搜索條件: {currentReport.title} | 搜索條件: {currentReport.title} | ||||
| </Typography> | </Typography> | ||||
| <Divider sx={{ mb: 3 }} /> | <Divider sx={{ mb: 3 }} /> | ||||
| <Grid container spacing={3}> | <Grid container spacing={3}> | ||||
| {currentReport.fields.map((field) => { | {currentReport.fields.map((field) => { | ||||
| const options = field.dynamicOptions | const options = field.dynamicOptions | ||||
| @@ -650,30 +650,7 @@ export default function ReportPage() { | |||||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | {loading ? "生成 Excel..." : "下載報告 (Excel)"} | ||||
| </Button> | </Button> | ||||
| </> | </> | ||||
| ) : currentReport.id === 'rep-006' ? ( | |||||
| <> | |||||
| <Button | |||||
| variant="contained" | |||||
| size="large" | |||||
| startIcon={<DownloadIcon />} | |||||
| onClick={handlePrint} | |||||
| disabled={loading} | |||||
| sx={{ px: 4 }} | |||||
| > | |||||
| {loading ? "生成 PDF..." : "下載報告 (PDF)"} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="large" | |||||
| startIcon={<DownloadIcon />} | |||||
| onClick={handleExcelPrint} | |||||
| disabled={loading} | |||||
| sx={{ px: 4 }} | |||||
| > | |||||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||||
| </Button> | |||||
| </> | |||||
| ) : currentReport.id === 'rep-010' ? ( | |||||
| ) : currentReport.id === 'rep-006' || currentReport.id === 'rep-010' ? ( | |||||
| <> | <> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| @@ -16,6 +16,14 @@ export interface QcResult { | |||||
| stockInLineId?: number; | stockInLineId?: number; | ||||
| stockOutLineId?: number; | stockOutLineId?: number; | ||||
| failQty: number; | failQty: number; | ||||
| measurementValue?: number | null; | |||||
| measureType?: string; | |||||
| unit?: string; | |||||
| } | |||||
| export interface SaveQcMeasurementRequest { | |||||
| measureType?: string; | |||||
| value?: number | null; | |||||
| unit?: string; | |||||
| } | } | ||||
| export interface SaveQcResultRequest { | export interface SaveQcResultRequest { | ||||
| qcItemId: number; | qcItemId: number; | ||||
| @@ -26,6 +34,7 @@ export interface SaveQcResultRequest { | |||||
| type: string; | type: string; | ||||
| remarks: string; | remarks: string; | ||||
| qcPassed: boolean; | qcPassed: boolean; | ||||
| measurement?: SaveQcMeasurementRequest; | |||||
| } | } | ||||
| export interface SaveQcResultResponse { | export interface SaveQcResultResponse { | ||||
| @@ -32,6 +32,9 @@ export interface QcData { | |||||
| qcPassed?: boolean, | qcPassed?: boolean, | ||||
| failQty?: number, | failQty?: number, | ||||
| remarks?: string, | remarks?: string, | ||||
| measurementValue?: number | null, | |||||
| measureType?: string, | |||||
| unit?: string, | |||||
| } | } | ||||
| export interface QcResult extends QcData{ | export interface QcResult extends QcData{ | ||||
| id?: number; | id?: number; | ||||
| @@ -0,0 +1,51 @@ | |||||
| export const QC_MEASURE_TYPE = { | |||||
| TEMPERATURE: "TEMPERATURE", | |||||
| HUMIDITY: "HUMIDITY", | |||||
| } as const; | |||||
| export type QcMeasureType = (typeof QC_MEASURE_TYPE)[keyof typeof QC_MEASURE_TYPE]; | |||||
| export function isTemperatureQcItem(row: { name?: string; code?: string }): boolean { | |||||
| const name = (row.name ?? "").trim(); | |||||
| const code = (row.code ?? "").trim(); | |||||
| return name === "溫度" || code === "溫度" || name.toLowerCase() === "temperature"; | |||||
| } | |||||
| export function isHumidityQcItem(row: { name?: string; code?: string }): boolean { | |||||
| const name = (row.name ?? "").trim(); | |||||
| const code = (row.code ?? "").trim(); | |||||
| return name === "濕度" || code === "濕度" || name.toLowerCase() === "humidity"; | |||||
| } | |||||
| export function isMeasurableQcItem(row: { name?: string; code?: string }): boolean { | |||||
| return isTemperatureQcItem(row) || isHumidityQcItem(row); | |||||
| } | |||||
| export function resolveMeasureMeta( | |||||
| row: { name?: string; code?: string }, | |||||
| ): { measureType: QcMeasureType; unit: string } | null { | |||||
| if (isTemperatureQcItem(row)) { | |||||
| return { measureType: QC_MEASURE_TYPE.TEMPERATURE, unit: "°C" }; | |||||
| } | |||||
| if (isHumidityQcItem(row)) { | |||||
| return { measureType: QC_MEASURE_TYPE.HUMIDITY, unit: "%" }; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| export function buildQcMeasurementPayload( | |||||
| row: { | |||||
| name?: string; | |||||
| code?: string; | |||||
| measurementValue?: number | null; | |||||
| measureType?: string; | |||||
| unit?: string; | |||||
| }, | |||||
| ) { | |||||
| const meta = resolveMeasureMeta(row); | |||||
| return { | |||||
| measureType: row.measureType ?? meta?.measureType ?? QC_MEASURE_TYPE.TEMPERATURE, | |||||
| value: row.measurementValue ?? null, | |||||
| unit: row.unit ?? meta?.unit ?? "°C", | |||||
| }; | |||||
| } | |||||
| @@ -36,6 +36,8 @@ import { isEmpty } from "lodash"; | |||||
| import { EscalationCombo } from "@/app/api/user"; | import { EscalationCombo } from "@/app/api/user"; | ||||
| import { truncateSync } from "fs"; | import { truncateSync } from "fs"; | ||||
| import { ModalFormInput, StockInLineInput, StockInLine } from "@/app/api/stockIn"; | import { ModalFormInput, StockInLineInput, StockInLine } from "@/app/api/stockIn"; | ||||
| import { buildQcMeasurementPayload, isMeasurableQcItem } from "@/app/api/qc/measurement"; | |||||
| import { QC_MEASUREMENT_ENABLED } from "@/config/featureFlags"; | |||||
| import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions"; | import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions"; | ||||
| import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | ||||
| import { fetchQcResult } from "@/app/api/qc/actions"; | import { fetchQcResult } from "@/app/api/qc/actions"; | ||||
| @@ -391,6 +393,9 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||||
| failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | ||||
| // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | ||||
| remarks: item.remarks || '', | remarks: item.remarks || '', | ||||
| ...(QC_MEASUREMENT_ENABLED && isMeasurableQcItem(item) | |||||
| ? { measurement: buildQcMeasurementPayload(item) } | |||||
| : {}), | |||||
| })), | })), | ||||
| }; | }; | ||||
| // const qcData = data; | // const qcData = data; | ||||
| @@ -16,6 +16,8 @@ import { | |||||
| useGridApiRef, | useGridApiRef, | ||||
| } from "@mui/x-data-grid"; | } from "@mui/x-data-grid"; | ||||
| import { QcFormInput, QcResult } from "@/app/api/qc"; | import { QcFormInput, QcResult } from "@/app/api/qc"; | ||||
| import { isMeasurableQcItem, resolveMeasureMeta } from "@/app/api/qc/measurement"; | |||||
| import { QC_MEASUREMENT_ENABLED } from "@/config/featureFlags"; | |||||
| interface Props { | interface Props { | ||||
| rows: QcResult[]; | rows: QcResult[]; | ||||
| @@ -132,6 +134,52 @@ const QcForm: React.FC<Props> = ({ rows, disabled = false, denseLayout = false } | |||||
| ); | ); | ||||
| }, | }, | ||||
| }, | }, | ||||
| ...(QC_MEASUREMENT_ENABLED | |||||
| ? [ | |||||
| { | |||||
| field: "measurementValue", | |||||
| headerName: t("measuredValue"), | |||||
| flex: 0.9, | |||||
| renderCell: (params: GridRenderEditCellParams) => { | |||||
| if (!isMeasurableQcItem(params.row)) { | |||||
| return null; | |||||
| } | |||||
| const index = getRowIndex(params); | |||||
| const currentValue = params.row.measurementValue; | |||||
| const meta = resolveMeasureMeta(params.row); | |||||
| return ( | |||||
| <TextField | |||||
| type="number" | |||||
| size={denseLayout ? "small" : "medium"} | |||||
| value={currentValue ?? ""} | |||||
| disabled={qcDisabled(params.row)} | |||||
| onChange={(e) => { | |||||
| const v = e.target.value; | |||||
| const next = v === "" ? null : Number(v); | |||||
| if (v !== "" && Number.isNaN(next)) return; | |||||
| setValue(`qcResult.${index}.measurementValue`, next); | |||||
| if (meta) { | |||||
| setValue(`qcResult.${index}.measureType`, meta.measureType); | |||||
| setValue(`qcResult.${index}.unit`, meta.unit); | |||||
| } | |||||
| }} | |||||
| onClick={(e) => e.stopPropagation()} | |||||
| onMouseDown={(e) => e.stopPropagation()} | |||||
| onKeyDown={(e) => e.stopPropagation()} | |||||
| inputProps={{ step: "0.1" }} | |||||
| sx={{ | |||||
| width: "100%", | |||||
| "& .MuiInputBase-input": { | |||||
| padding: denseLayout ? "0.35rem 0.5rem" : "0.75rem", | |||||
| fontSize: denseLayout ? 14 : 24, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| ); | |||||
| }, | |||||
| } as GridColDef, | |||||
| ] | |||||
| : []), | |||||
| { | { | ||||
| field: "failQty", | field: "failQty", | ||||
| headerName: t("failedQty"), | headerName: t("failedQty"), | ||||
| @@ -36,6 +36,8 @@ import { isEmpty } from "lodash"; | |||||
| import { EscalationCombo } from "@/app/api/user"; | import { EscalationCombo } from "@/app/api/user"; | ||||
| import { truncateSync } from "fs"; | import { truncateSync } from "fs"; | ||||
| import { ModalFormInput, StockInLineInput, StockInLine, StockInStatus } from "@/app/api/stockIn"; | import { ModalFormInput, StockInLineInput, StockInLine, StockInStatus } from "@/app/api/stockIn"; | ||||
| import { buildQcMeasurementPayload, isMeasurableQcItem } from "@/app/api/qc/measurement"; | |||||
| import { QC_MEASUREMENT_ENABLED } from "@/config/featureFlags"; | |||||
| import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions"; | import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions"; | ||||
| import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | ||||
| import FgStockInForm from "../StockIn/FgStockInForm"; | import FgStockInForm from "../StockIn/FgStockInForm"; | ||||
| @@ -430,6 +432,9 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | ||||
| // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | ||||
| remarks: item.remarks || '', | remarks: item.remarks || '', | ||||
| ...(QC_MEASUREMENT_ENABLED && isMeasurableQcItem(item) | |||||
| ? { measurement: buildQcMeasurementPayload(item) } | |||||
| : {}), | |||||
| })), | })), | ||||
| }; | }; | ||||
| // const qcData = data; | // const qcData = data; | ||||
| @@ -0,0 +1,3 @@ | |||||
| /** When true, show temperature/humidity measured-value UI and send measurement on stock-in QC. */ | |||||
| export const QC_MEASUREMENT_ENABLED = | |||||
| process.env.NEXT_PUBLIC_QC_MEASUREMENT_ENABLED === "true"; | |||||
| @@ -236,9 +236,14 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| title: "庫存品質檢測報告", | title: "庫存品質檢測報告", | ||||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, | apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, | ||||
| fields: [ | fields: [ | ||||
| { label: "QC 不合格日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, | |||||
| { label: "QC 不合格日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, | |||||
| { label: "QC 檢測日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, | |||||
| { label: "QC 檢測日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, | |||||
| { label: "QC 類型", name: "qcType", type: "select", required: false, | |||||
| options: [ | |||||
| { label: "全部", value: "" }, | |||||
| { label: "IQC", value: "IQC" }, | |||||
| { label: "EPQC", value: "EPQC" }, | |||||
| ] }, | |||||
| { label: "貨品編號 Item Code", name: "itemCode", type: "text", required: false}, | { label: "貨品編號 Item Code", name: "itemCode", type: "text", required: false}, | ||||
| ] | ] | ||||
| }, | }, | ||||
| @@ -134,6 +134,7 @@ | |||||
| "passed": "Passed", | "passed": "Passed", | ||||
| "failed": "Failed", | "failed": "Failed", | ||||
| "failedQty": "Failed Qty", | "failedQty": "Failed Qty", | ||||
| "measuredValue": "Measured Value", | |||||
| "remarks": "Remarks", | "remarks": "Remarks", | ||||
| "Reject": "Reject", | "Reject": "Reject", | ||||
| "submit": "Submit", | "submit": "Submit", | ||||
| @@ -134,6 +134,7 @@ | |||||
| "passed": "接受", | "passed": "接受", | ||||
| "failed": "不接受", | "failed": "不接受", | ||||
| "failedQty": "不合格數", | "failedQty": "不合格數", | ||||
| "measuredValue": "實測值", | |||||
| "remarks": "備註", | "remarks": "備註", | ||||
| "Reject": "拒絕", | "Reject": "拒絕", | ||||
| "submit": "提交", | "submit": "提交", | ||||