Bläddra i källkod

排產 Excel

在「物料用量總結」加入「需採購/生產數量」=下週用量 − 庫存(不扣 PO,可為負)
欄寬加寬;不放在每日分頁
工單
列表顯示 BOM 工序標準工時(預估分鐘)
「BOM材料」→「材料」
耗材「不適用」→「不用提料」
BOM 工藝
新增 bom.details(含 Liquibase)
「工藝明細」改為可儲存的自由文字;「工藝流程」仍用名稱
看板 / 分頁
工單狀態看板經手人:無 handler 時回退顯示 operator
隱藏飲料生產數量分頁
排產規則文案
「一般日」→「一般規則」
「多備日」等「多備」相關 →「特別規則」
「多備星期」→「特別規則適用星期」(前後端同步)
debrand
tommy 2 dagar sedan
förälder
incheckning
1a61303907
12 ändrade filer med 144 tillägg och 96 borttagningar
  1. +12
    -0
      src/app/api/bom/client.ts
  2. +2
    -0
      src/app/api/jo/index.ts
  3. +16
    -0
      src/components/JoSearch/JoSearch.tsx
  4. +16
    -0
      src/components/JoWorkbench/JoWorkbenchSearch.tsx
  5. +67
    -75
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  6. +1
    -7
      src/components/ProductionProcess/ProductionProcessPage.tsx
  7. +6
    -2
      src/i18n/en/jo.json
  8. +4
    -0
      src/i18n/en/productionProcess.json
  9. +6
    -6
      src/i18n/en/scheduling.json
  10. +4
    -0
      src/i18n/zh/jo.json
  11. +4
    -0
      src/i18n/zh/productionProcess.json
  12. +6
    -6
      src/i18n/zh/scheduling.json

+ 12
- 0
src/app/api/bom/client.ts Visa fil

@@ -107,6 +107,18 @@ export async function fetchBomComboClient(options?: {
);
return response.data;
}

/** Update free-text craft details (工藝明細) on BOM. */
export async function updateBomDetailsClient(
id: number,
details: string | null,
): Promise<{ id: number; details: string | null }> {
const response = await axiosInstance.put<{ id: number; details: string | null }>(
`${NEXT_PUBLIC_API_URL}/bom/${id}/details`,
{ details },
);
return response.data;
}
export type BomExcelCheckProgress = {
batchId: string;
totalFiles: number;


+ 2
- 0
src/app/api/jo/index.ts Visa fil

@@ -39,6 +39,8 @@ export interface JobOrder {
stockInLineStatus?: string;
silHandlerId?: number;
lotNo?: string;
/** Standard BOM process minutes (prep + duration + post). */
estimatedMinutes?: number | null;
}

export interface Machine {


+ 16
- 0
src/components/JoSearch/JoSearch.tsx Visa fil

@@ -459,6 +459,22 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
return row.item?.uom ? t(row.item.uom.udfudesc) : '-'
}
},
{
name: "estimatedMinutes" as keyof JobOrder,
label: t("Estimated Process Time"),
align: "left",
headerAlign: "left",
renderCell: (row) => {
const mins = row.estimatedMinutes;
if (mins == null || mins <= 0) return "—";
const hours = Math.floor(mins / 60);
const minutes = mins % 60;
const parts: string[] = [];
if (hours > 0) parts.push(t("Duration hours", { count: hours }));
if (minutes > 0 || hours === 0) parts.push(t("Duration minutes", { count: minutes }));
return parts.join(" ");
}
},
{
name: "stockStatus" as keyof JobOrder,
label: t("BOM Status"),


+ 16
- 0
src/components/JoWorkbench/JoWorkbenchSearch.tsx Visa fil

@@ -460,6 +460,22 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo
return row.item?.uom ? t(row.item.uom.udfudesc) : '-'
}
},
{
name: "estimatedMinutes" as keyof JobOrder,
label: t("Estimated Process Time"),
align: "left",
headerAlign: "left",
renderCell: (row) => {
const mins = row.estimatedMinutes;
if (mins == null || mins <= 0) return "—";
const hours = Math.floor(mins / 60);
const minutes = mins % 60;
const parts: string[] = [];
if (hours > 0) parts.push(t("Duration hours", { count: hours }));
if (minutes > 0 || hours === 0) parts.push(t("Duration minutes", { count: minutes }));
return parts.join(" ");
}
},
{
name: "stockStatus" as keyof JobOrder,
label: t("BOM Status"),


+ 67
- 75
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx Visa fil

@@ -27,6 +27,7 @@ import { fetchProductProcessesByJobOrderId , setJobOrderHidden, updateProductPro
import ProductionProcessDetail from "./ProductionProcessDetail";
import { BomCombo } from "@/app/api/bom";
import { fetchBomCombo } from "@/app/api/bom/index";
import { updateBomDetailsClient } from "@/app/api/bom/client";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
import StyledDataGrid from "../StyledDataGrid/StyledDataGrid";
@@ -44,6 +45,7 @@ import EditIcon from "@mui/icons-material/Edit";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { dayjsToDateString } from "@/app/utils/formatUtil";
import { msg } from "../Swal/CustomAlerts";



@@ -77,6 +79,8 @@ const [reqQtyMultiplier, setReqQtyMultiplier] = useState<number>(1);
const [selectedBomForReqQty, setSelectedBomForReqQty] = useState<BomCombo | null>(null);
const [bomCombo, setBomCombo] = useState<BomCombo[]>([]);
const [showBaseQty, setShowBaseQty] = useState<boolean>(false);
const [bomDetailsText, setBomDetailsText] = useState<string>("");
const [savingBomDetails, setSavingBomDetails] = useState(false);
const fetchData = useCallback(async () => {
setLoading(true);
@@ -86,6 +90,7 @@ const [showBaseQty, setShowBaseQty] = useState<boolean>(false);
const firstProcess = data[0];
setProcessData(firstProcess);
setJobOrderLines((firstProcess as any).jobOrderLines || []);
setBomDetailsText((firstProcess as any).bomDetails ?? "");
}
} catch (error) {
console.error("Error loading data:", error);
@@ -93,6 +98,27 @@ const [showBaseQty, setShowBaseQty] = useState<boolean>(false);
setLoading(false);
}
}, [jobOrderId]);

const handleSaveBomDetails = useCallback(async () => {
const bomId = processData?.bomId as number | undefined;
if (!bomId) {
msg(t("update failed"));
return;
}
setSavingBomDetails(true);
try {
const result = await updateBomDetailsClient(bomId, bomDetailsText);
setProcessData((prev: any) =>
prev ? { ...prev, bomDetails: result.details ?? "" } : prev,
);
msg(t("update success"));
} catch (e) {
console.error(e);
msg(t("update failed"));
} finally {
setSavingBomDetails(false);
}
}, [bomDetailsText, processData?.bomId, t]);
const toggleBaseQty = useCallback(() => {
setShowBaseQty(prev => !prev);
}, []);
@@ -456,52 +482,6 @@ const handleRelease = useCallback(async (jobOrderId: number) => {
</Card>
);

const productionProcessesLineRemarkTableColumns: GridColDef[] = [
{
field: "seqNo",
headerName: t("SEQ"),
flex: 0.2,
align: "left",
headerAlign: "left",
type: "number",
renderCell: (params) => {
return <Typography sx={{ fontWeight: 500 }}>{params.value}</Typography>;
},
},
{
field: "description",
headerName: t("Remark"),
flex: 1,
align: "left",
headerAlign: "left",
renderCell: (params) => {
return(
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
<Typography sx={{ fontWeight: 500 }}>{params.value || ""}</Typography>
<Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
</Box>
)
},
},

];
const productionProcessesLineRemarkTableRows =
processData?.productProcessLines?.map((line: any) => ({
id: line.seqNo,
seqNo: line.seqNo,

description: line.description ?? "",



})) ?? [];




const pickTableColumns: GridColDef[] = [
{
field: "id",
@@ -649,7 +629,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => {
return (
<Box sx={{ textAlign: "right" }}>
<Typography variant="body2" color="text.secondary">
{t("N/A")}
{t("No pick required")}
</Typography>
</Box>
);
@@ -687,7 +667,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => {
if (isExcludedFromPickStock(params.row.type)) {
return (
<Typography variant="body2" color="text.secondary">
{t("N/A")}
{t("No pick required")}
</Typography>
);
}
@@ -764,32 +744,44 @@ const handleRelease = useCallback(async (jobOrderId: number) => {
const ProductionProcessesLineRemarkTableContent = () => (
<Box sx={{ mt: 2 }}>
<ProcessSummaryHeader processData={processData} />
<StyledDataGrid
sx={{
"--DataGrid-overlayHeight": "100px",
// ✅ Match ProductionProcessDetail font size (default body2 = 0.875rem)
"& .MuiDataGrid-cell": {
fontSize: "0.875rem", // ✅ Match default body2 size
fontWeight: 500,
},
"& .MuiDataGrid-columnHeader": {
fontSize: "0.875rem", // ✅ Match header size
fontWeight: 600,
},
// ✅ Ensure empty columns are visible
"& .MuiDataGrid-columnHeaders": {
display: "flex",
},
"& .MuiDataGrid-row": {
display: "flex",
},
}}
disableColumnMenu
rows={productionProcessesLineRemarkTableRows ?? []}
columns={productionProcessesLineRemarkTableColumns}
getRowHeight={() => 'auto'}
hideFooter={false} // ✅ Ensure footer is visible
/>
<Card sx={{ mb: 2 }}>
<CardContent>
<Stack spacing={2}>
<TextField
multiline
minRows={8}
fullWidth
value={bomDetailsText}
onChange={(e) => setBomDetailsText(e.target.value)}
placeholder={t("Production Process Line Remark")}
InputLabelProps={{ shrink: false }}
sx={{
"& .MuiInputBase-input::placeholder": {
color: "text.secondary",
opacity: 1,
},
"& .MuiInputBase-input": {
color: "text.primary",
},
}}
/>
<Box>
<Button
variant="contained"
onClick={handleSaveBomDetails}
disabled={savingBomDetails || !processData?.bomId}
startIcon={
savingBomDetails ? (
<CircularProgress size={16} color="inherit" />
) : undefined
}
>
{t("Save")}
</Button>
</Box>
</Stack>
</CardContent>
</Card>
</Box>
);
@@ -807,7 +799,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => {
<Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Job Order Info")} />
<Tab label={t("BoM Material")} />
<Tab label={t("Job Order Materials Tab")} />
<Tab label={t("Production Process")} />


+ 1
- 7
src/components/ProductionProcess/ProductionProcessPage.tsx Visa fil

@@ -13,7 +13,6 @@ import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionse
import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus";
import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard";
import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard";
import DrinkProductionQtyDashboard from "@/components/ProductionProcess/DrinkProductionQtyDashboard";
import JobOrderOpsTable from "@/components/ProductionProcess/JobOrderOpsTable";
import type { PrinterCombo } from "@/app/api/settings/printer";
import { useTranslation } from "react-i18next";
@@ -30,8 +29,7 @@ type PageTab =
| "ops"
| "jobProcessStatus"
| "operatorKpi"
| "equipmentStatus"
| "drinkQty";
| "equipmentStatus";

/**
* FP-MTMS Version Checklist | Functions Ref. No. 26 | v1.0.0 | 2026-07-20
@@ -169,7 +167,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
<Tab value="jobProcessStatus" label={t("Job Process Status Dashboard")} />
<Tab value="operatorKpi" label={t("Operator KPI Dashboard")} />
<Tab value="equipmentStatus" label={t("Production Equipment Status Dashboard")} />
<Tab value="drinkQty" label={t("Drink Production Qty Dashboard")} />
</Tabs>

{pageTab === "list" && (
@@ -211,9 +208,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
{pageTab === "equipmentStatus" && (
<EquipmentStatusDashboard />
)}
{pageTab === "drinkQty" && (
<DrinkProductionQtyDashboard />
)}
</Box>
<QcStockInModal
session={sessionToken}


+ 6
- 2
src/i18n/en/jo.json Visa fil

@@ -48,6 +48,8 @@
"Base UOM": "Base UOM",
"Batch Count": "Batch Count",
"BoM Material": "BOM material",
"Job Order Materials Tab": "Materials",
"No pick required": "No pick required",
"Bom": "BOM",
"Bom Req. Qty": "BOM required qty",
"Bom Uom": "BOM UOM",
@@ -106,8 +108,9 @@
"Do you want to start job order": "Start this job order?",
"Download Excel": "Download Excel",
"Drink": "Drink",
"Duration hours": "Hours",
"Duration minutes": "Minutes",
"Duration hours": "{{count}} hour(s)",
"Duration minutes": "{{count}} minute(s)",
"Estimated Process Time": "Estimated time",
"Duration seconds": "Seconds",
"Edit": "Edit",
"Edit Equipment": "Edit Equipment",
@@ -650,6 +653,7 @@
"update production priority": "Update production priority",
"update qc info": "Update QC details",
"update success": "Updated successfully",
"update failed": "Update failed",
"value must be a number": "Enter a number",
"view putaway": "View put-away details",
"view stockin": "QC",


+ 4
- 0
src/i18n/en/productionProcess.json Visa fil

@@ -17,6 +17,8 @@
"Base UOM": "Base UOM",
"Batch Count": "Batch Count",
"BoM Material": "BOM material",
"Job Order Materials Tab": "Materials",
"No pick required": "No pick required",
"Bom Req. Qty": "BOM required qty",
"Bom Uom": "BOM UOM",
"By-product": "By-product",
@@ -211,6 +213,8 @@
"Required Time": "Required Time",
"SEQ": "SEQ",
"Save": "Save",
"update success": "Updated successfully",
"update failed": "Update failed",
"Scan Operator & Equipment": "Scan Operator & Equipment",
"Scrap": "Scrap",
"Scrap Qty": "Scrap Qty",


+ 6
- 6
src/i18n/en/scheduling.json Visa fil

@@ -91,15 +91,15 @@
"rulesSaved": "Saved. Run Forecast schedule again to apply these rules.",
"rulesIntro": "Days of cover = stock ÷ average daily outbound. Production is scheduled when cover falls below the trigger, and stock is topped up to the replenish days. Items left blank follow these defaults. Schedules already created change only after you run Forecast schedule again.",
"defaultRules": "Default rules",
"normalDays": "Normal days",
"extraDays": "Extra-stock days",
"normalDays": "Normal rules",
"extraDays": "Special rules",
"below": "Below",
"produceUpTo": "days",
"topUpTo": "Top up to",
"days": "days",
"extraWeekdays": "Extra-stock weekdays",
"extraWeekdays": "Special-rule weekdays",
"noProductionUncategorised": "No production for unclassified items",
"skipExtra": "Skip extra stock",
"skipExtra": "Skip special rules",
"saveDefaults": "Save defaults",
"perItem": "Per item",
"searchItem": "Search item code or name",
@@ -107,13 +107,13 @@
"triggerDays": "Trigger days",
"replenishDays": "Replenish days",
"productionDays": "Production days",
"extraStock": "Extra stock",
"extraStock": "Special rules",
"daysBlankFollowDefault": "Days of cover must be greater than 0, or leave the field blank to follow the default.",
"customDaysRequired": "Choose at least one custom production day.",
"defaultPlaceholder": "Default",
"custom": "Custom",
"followDefault": "Follow default",
"applyExtra": "Apply extra stock",
"applyExtra": "Apply special rules",
"clearItemRule": "Clear this item's custom rule",
"weekdayMon": "Mon",
"weekdayTue": "Tue",


+ 4
- 0
src/i18n/zh/jo.json Visa fil

@@ -50,6 +50,8 @@
"Base UOM": "基本單位",
"Batch Count": "批數",
"BoM Material": "BOM 材料",
"Job Order Materials Tab": "材料",
"No pick required": "不用提料",
"Bom": "BOM",
"Bom Req. Qty": "BOM",
"Bom Uom": "BOM 單位",
@@ -520,6 +522,7 @@
"Unassigned Job Orders": "未分配工單",
"Unit": "單位",
"Assume Time Need": "預計所需時間",
"Estimated Process Time": "所需時間",
"Unknown": "未知",
"Unknown error": "未知錯誤",
"Unknown error: ": "未知錯誤: ",
@@ -653,6 +656,7 @@
"update production priority": "更新生產優先序",
"update qc info": "更新QC資訊",
"update success": "成功更新資料",
"update failed": "更新失敗",
"value must be a number": "值必須是數字",
"view putaway": "查看上架詳情",
"view stockin": "品檢",


+ 4
- 0
src/i18n/zh/productionProcess.json Visa fil

@@ -17,6 +17,8 @@
"Base UOM": "基本單位",
"Batch Count": "批數",
"BoM Material": "BOM 材料",
"Job Order Materials Tab": "材料",
"No pick required": "不用提料",
"Bom Req. Qty": "BOM",
"Bom Uom": "BOM 單位",
"By-product": "副產品",
@@ -211,6 +213,8 @@
"Required Time": "所需時間",
"SEQ": "步驟",
"Save": "儲存",
"update success": "成功更新資料",
"update failed": "更新失敗",
"Scan Operator & Equipment": "掃描操作員和設備",
"Scrap": "損耗",
"Scrap Qty": "損耗數量",


+ 6
- 6
src/i18n/zh/scheduling.json Visa fil

@@ -91,15 +91,15 @@
"rulesSaved": "已儲存。下次按「預測排期」會用新規則。",
"rulesIntro": "可用日 = 存貨 ÷ 每日平均出貨量。低於觸發日就安排生產,並補到補貨日。留空的物料跟隨這裡的預設。已產生的排期要再按「預測排期」才會更新。",
"defaultRules": "預設規則",
"normalDays": "一般日",
"extraDays": "多備日",
"normalDays": "一般規則",
"extraDays": "特別規則",
"below": "低於",
"produceUpTo": "日生產",
"topUpTo": "補至",
"days": "日",
"extraWeekdays": "多備星期",
"extraWeekdays": "特別規則適用星期",
"noProductionUncategorised": "未分類物料不生產",
"skipExtra": "不套用多備",
"skipExtra": "不套用特別規則",
"saveDefaults": "儲存預設",
"perItem": "個別物料",
"searchItem": "搜尋物料編號或名稱",
@@ -107,13 +107,13 @@
"triggerDays": "觸發日",
"replenishDays": "補貨日",
"productionDays": "可生產日",
"extraStock": "多備",
"extraStock": "特別規則",
"daysBlankFollowDefault": "可用日必須是大於 0 的數字,或留空跟隨預設。",
"customDaysRequired": "自訂可生產日至少要選一天。",
"defaultPlaceholder": "預設",
"custom": "自訂",
"followDefault": "跟隨預設",
"applyExtra": "套用多備",
"applyExtra": "套用特別規則",
"clearItemRule": "清除此物料的自訂規則",
"weekdayMon": "一",
"weekdayTue": "二",


Laddar…
Avbryt
Spara