Просмотр исходного кода

QC添加溫度/濕度儲存

production
B.E.N.S.O.N 2 часов назад
Родитель
Сommit
d6d1cd1e5b
14 измененных файлов: 233 добавлений и 30 удалений
  1. +2
    -1
      .env.development
  2. +2
    -1
      .env.production
  3. +93
    -0
      src/app/(main)/report/ItemQcReportFilters.tsx
  4. +2
    -25
      src/app/(main)/report/page.tsx
  5. +9
    -0
      src/app/api/qc/actions.ts
  6. +3
    -0
      src/app/api/qc/index.ts
  7. +51
    -0
      src/app/api/qc/measurement.ts
  8. +5
    -0
      src/components/PoDetail/QcStockInModal.tsx
  9. +48
    -0
      src/components/Qc/QcForm.tsx
  10. +5
    -0
      src/components/Qc/QcStockInModal.tsx
  11. +3
    -0
      src/config/featureFlags.ts
  12. +8
    -3
      src/config/reportConfig.ts
  13. +1
    -0
      src/i18n/en/purchaseOrder.json
  14. +1
    -0
      src/i18n/zh/purchaseOrder.json

+ 2
- 1
.env.development Просмотреть файл

@@ -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

+ 2
- 1
.env.production Просмотреть файл

@@ -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

+ 93
- 0
src/app/(main)/report/ItemQcReportFilters.tsx Просмотреть файл

@@ -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>
);
}

+ 2
- 25
src/app/(main)/report/page.tsx Просмотреть файл

@@ -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"


+ 9
- 0
src/app/api/qc/actions.ts Просмотреть файл

@@ -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 {


+ 3
- 0
src/app/api/qc/index.ts Просмотреть файл

@@ -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;


+ 51
- 0
src/app/api/qc/measurement.ts Просмотреть файл

@@ -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",
};
}

+ 5
- 0
src/components/PoDetail/QcStockInModal.tsx Просмотреть файл

@@ -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;


+ 48
- 0
src/components/Qc/QcForm.tsx Просмотреть файл

@@ -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"),


+ 5
- 0
src/components/Qc/QcStockInModal.tsx Просмотреть файл

@@ -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;


+ 3
- 0
src/config/featureFlags.ts Просмотреть файл

@@ -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";

+ 8
- 3
src/config/reportConfig.ts Просмотреть файл

@@ -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},
] ]
}, },


+ 1
- 0
src/i18n/en/purchaseOrder.json Просмотреть файл

@@ -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",


+ 1
- 0
src/i18n/zh/purchaseOrder.json Просмотреть файл

@@ -134,6 +134,7 @@
"passed": "接受", "passed": "接受",
"failed": "不接受", "failed": "不接受",
"failedQty": "不合格數", "failedQty": "不合格數",
"measuredValue": "實測值",
"remarks": "備註", "remarks": "備註",
"Reject": "拒絕", "Reject": "拒絕",
"submit": "提交", "submit": "提交",


Загрузка…
Отмена
Сохранить