| @@ -1,4 +1,5 @@ | |||
| API_URL=http://localhost:8090/api | |||
| NEXTAUTH_SECRET=secret | |||
| 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 | |||
| NEXTAUTH_SECRET=secret | |||
| 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} | |||
| </Typography> | |||
| <Divider sx={{ mb: 3 }} /> | |||
| <Grid container spacing={3}> | |||
| {currentReport.fields.map((field) => { | |||
| const options = field.dynamicOptions | |||
| @@ -650,30 +650,7 @@ export default function ReportPage() { | |||
| {loading ? "生成 Excel..." : "下載報告 (Excel)"} | |||
| </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 | |||
| variant="contained" | |||
| @@ -16,6 +16,14 @@ export interface QcResult { | |||
| stockInLineId?: number; | |||
| stockOutLineId?: number; | |||
| failQty: number; | |||
| measurementValue?: number | null; | |||
| measureType?: string; | |||
| unit?: string; | |||
| } | |||
| export interface SaveQcMeasurementRequest { | |||
| measureType?: string; | |||
| value?: number | null; | |||
| unit?: string; | |||
| } | |||
| export interface SaveQcResultRequest { | |||
| qcItemId: number; | |||
| @@ -26,6 +34,7 @@ export interface SaveQcResultRequest { | |||
| type: string; | |||
| remarks: string; | |||
| qcPassed: boolean; | |||
| measurement?: SaveQcMeasurementRequest; | |||
| } | |||
| export interface SaveQcResultResponse { | |||
| @@ -32,6 +32,9 @@ export interface QcData { | |||
| qcPassed?: boolean, | |||
| failQty?: number, | |||
| remarks?: string, | |||
| measurementValue?: number | null, | |||
| measureType?: string, | |||
| unit?: string, | |||
| } | |||
| export interface QcResult extends QcData{ | |||
| 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 { truncateSync } from "fs"; | |||
| 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 { fetchStockInLineInfo } from "@/app/api/stockIn/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, | |||
| // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | |||
| remarks: item.remarks || '', | |||
| ...(QC_MEASUREMENT_ENABLED && isMeasurableQcItem(item) | |||
| ? { measurement: buildQcMeasurementPayload(item) } | |||
| : {}), | |||
| })), | |||
| }; | |||
| // const qcData = data; | |||
| @@ -16,6 +16,8 @@ import { | |||
| useGridApiRef, | |||
| } from "@mui/x-data-grid"; | |||
| import { QcFormInput, QcResult } from "@/app/api/qc"; | |||
| import { isMeasurableQcItem, resolveMeasureMeta } from "@/app/api/qc/measurement"; | |||
| import { QC_MEASUREMENT_ENABLED } from "@/config/featureFlags"; | |||
| interface Props { | |||
| 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", | |||
| headerName: t("failedQty"), | |||
| @@ -36,6 +36,8 @@ import { isEmpty } from "lodash"; | |||
| import { EscalationCombo } from "@/app/api/user"; | |||
| import { truncateSync } from "fs"; | |||
| 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 { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | |||
| import FgStockInForm from "../StockIn/FgStockInForm"; | |||
| @@ -430,6 +432,9 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | |||
| // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | |||
| remarks: item.remarks || '', | |||
| ...(QC_MEASUREMENT_ENABLED && isMeasurableQcItem(item) | |||
| ? { measurement: buildQcMeasurementPayload(item) } | |||
| : {}), | |||
| })), | |||
| }; | |||
| // 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: "庫存品質檢測報告", | |||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, | |||
| 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}, | |||
| ] | |||
| }, | |||
| @@ -134,6 +134,7 @@ | |||
| "passed": "Passed", | |||
| "failed": "Failed", | |||
| "failedQty": "Failed Qty", | |||
| "measuredValue": "Measured Value", | |||
| "remarks": "Remarks", | |||
| "Reject": "Reject", | |||
| "submit": "Submit", | |||
| @@ -134,6 +134,7 @@ | |||
| "passed": "接受", | |||
| "failed": "不接受", | |||
| "failedQty": "不合格數", | |||
| "measuredValue": "實測值", | |||
| "remarks": "備註", | |||
| "Reject": "拒絕", | |||
| "submit": "提交", | |||