Browse Source

update

master
CANCERYS\kw093 1 week ago
parent
commit
4b9cd0f54d
23 changed files with 2688 additions and 110 deletions
  1. +48
    -0
      src/app/(main)/productionProcess/page.tsx
  2. +1
    -0
      src/app/api/bom/index.ts
  3. +91
    -5
      src/app/api/jo/actions.ts
  4. +2
    -0
      src/app/api/jo/index.ts
  5. +4
    -0
      src/app/api/pickOrder/actions.ts
  6. +2
    -2
      src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx
  7. +23
    -8
      src/components/JoSearch/JoCreateFormModal.tsx
  8. +25
    -5
      src/components/JoSearch/JoSearch.tsx
  9. +6
    -4
      src/components/JoSearch/JoSearchWrapper.tsx
  10. +34
    -0
      src/components/Jodetail/JoPickOrderDetail.tsx
  11. +176
    -0
      src/components/Jodetail/JoPickOrderList.tsx
  12. +3
    -1
      src/components/Jodetail/JobPickExecution.tsx
  13. +4
    -0
      src/components/Jodetail/JodetailSearch.tsx
  14. +1942
    -0
      src/components/Jodetail/newJobPickExecution.tsx
  15. +7
    -0
      src/components/NavigationContent/NavigationContent.tsx
  16. +39
    -2
      src/components/PickOrderSearch/LotTable.tsx
  17. +46
    -0
      src/components/ProductionProcess/ProcessSummaryHeader.tsx
  18. +150
    -56
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  19. +34
    -5
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  20. +36
    -16
      src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx
  21. +7
    -2
      src/i18n/zh/common.json
  22. +7
    -4
      src/i18n/zh/jo.json
  23. +1
    -0
      src/i18n/zh/pickOrder.json

+ 48
- 0
src/app/(main)/productionProcess/page.tsx View File

@@ -0,0 +1,48 @@
import ProductionProcessPage from "../../../components/ProductionProcess/ProductionProcessPage";
import { I18nProvider, getServerI18n } from "../../../i18n";

import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
import { fetchPrinterCombo } from "@/app/api/settings/printer";

export const metadata: Metadata = {
title: "Job Order Production Process",
};

const productionProcess: React.FC = async () => {
const { t } = await getServerI18n("common");
const printerCombo = await fetchPrinterCombo();
return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Job Order Production Process")}
</Typography>
{/* Optional: Remove or modify create button, because creation is done via API automatically */}
{/* <Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/productionProcess/create"
>
{t("Create Process")}
</Button> */}
</Stack>
<I18nProvider namespaces={["common", "production","purchaseOrder","jo"]}>
<ProductionProcessPage printerCombo={printerCombo} />
</I18nProvider>
</>
);
};

export default productionProcess;

+ 1
- 0
src/app/api/bom/index.ts View File

@@ -6,6 +6,7 @@ export interface BomCombo {
id: number;
value: number;
label: string;
outputQty: number;
}

export const preloadBomCombo = (() => {


+ 91
- 5
src/app/api/jo/actions.ts View File

@@ -26,6 +26,7 @@ export interface SearchJoResultRequest extends Pageable {
itemName?: string;
planStart?: string;
planStartTo?: string;
jobTypeName?: string;
}

export interface productProcessLineQtyRequest {
@@ -96,6 +97,8 @@ export interface JobOrderDetail {
reqQty: number;
uom: string;
pickLines: any[];

jobTypeName: string;
status: string;
}

@@ -183,6 +186,7 @@ export interface ProductProcessLineResponse {
name: string,
description: string,
equipment_name: string,
equipmentDetailCode: string,
status: string,
byproductId: number,
byproductName: string,
@@ -215,6 +219,8 @@ export interface ProductProcessWithLinesResponse {
isDark: string;
isDense: number;
isFloat: string;
scrapRate: number;
allergicSubstance: string;
itemId: number;
itemCode: string;
itemName: string;
@@ -301,8 +307,10 @@ export interface ProductProcessInfoResponse {
}
export interface ProductProcessLineQrscanUpadteRequest {
productProcessLineId: number;
operatorId?: number;
equipmentId?: number;
//operatorId?: number;
//equipmentId?: number;
equipmentTypeSubTypeEquipmentNo?: string;
staffNo?: string;
}

export interface ProductProcessLineDetailResponse {
@@ -403,6 +411,7 @@ export interface ProductProcessLineInfoResponse {
name: string,
description: string,
equipment_name: string,
equipmentDetailCode: string,
status: string,
byproductId: number,
byproductName: string,
@@ -419,8 +428,74 @@ export interface ProductProcessLineInfoResponse {
startTime: string,
endTime: string
}


export interface AllJoPickOrderResponse {
id: number;
pickOrderId: number | null;
pickOrderCode: string | null;
jobOrderId: number | null;
jobOrderCode: string | null;
jobOrderTypeId: number | null;
jobOrderType: string | null;
itemId: number;
itemName: string;
reqQty: number;
uomId: number;
uomName: string;
jobOrderStatus: string;
finishedPickOLineCount: number;
}
export interface UpdateJoPickOrderHandledByRequest {
pickOrderId: number;
itemId: number;
userId: number;
}
export interface JobTypeResponse {
id: number;
name: string;
}
export const deleteJobOrder=cache(async (jobOrderId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/demo/deleteJobOrder/${jobOrderId}`,
{
method: "POST",
}
);
});
export const fetchAllJobTypes = cache(async () => {
return serverFetchJson<JobTypeResponse[]>(
`${BASE_API_URL}/jo/jobTypes`,
{
method: "GET",
}
);
});
export const updateJoPickOrderHandledBy = cache(async (request: UpdateJoPickOrderHandledByRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/update-jo-pick-order-handled-by`,
{
method: "POST",
body: JSON.stringify(request),
headers: { "Content-Type": "application/json" },
},
);
});
export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrderId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`,
{
method: "GET",
next: { tags: ["jo-hierarchical"] },
},
);
});
export const fetchAllJoPickOrders = cache(async () => {
return serverFetchJson<AllJoPickOrderResponse[]>(
`${BASE_API_URL}/jo/AllJoPickOrder`,
{
method: "GET",
}
);
});
export const fetchProductProcessLineDetail = cache(async (lineId: number) => {
return serverFetchJson<JobOrderProcessLineDetailResponse>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`,
@@ -441,12 +516,23 @@ export const updateProductProcessLineQty = cache(async (request: UpdateProductPr
});

export const updateProductProcessLineQrscan = cache(async (request: ProductProcessLineQrscanUpadteRequest) => {
const requestBody: any = {
productProcessLineId: request.productProcessLineId,
//operatorId: request.operatorId,
//equipmentId: request.equipmentId,
equipmentTypeSubTypeEquipmentNo: request.equipmentTypeSubTypeEquipmentNo,
staffNo: request.staffNo,
};
if (request.equipmentTypeSubTypeEquipmentNo !== undefined) {
requestBody["EquipmentType-SubType-EquipmentNo"] = request.equipmentTypeSubTypeEquipmentNo;
}
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/update`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
body: JSON.stringify(requestBody),
}
);
});


+ 2
- 0
src/app/api/jo/index.ts View File

@@ -28,6 +28,8 @@ export interface JobOrder {
planStartTo?: string;
planEnd?: number[];
type: string;
jobTypeId: number;
jobTypeName: string;
// TODO pack below into StockInLineInfo
stockInLineId?: number;
stockInLineStatus?: string;


+ 4
- 0
src/app/api/pickOrder/actions.ts View File

@@ -452,6 +452,10 @@ export interface LaneBtn {
unassigned: number;
total: number;
}



export const fetchDoPickOrderDetail = async (
doPickOrderId: number,
selectedPickOrderId?: number


+ 2
- 2
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx View File

@@ -1,6 +1,6 @@
"use client";

import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material";
import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSession } from "next-auth/react";
@@ -217,7 +217,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
}}
>
{isLoadingSummary ? (
<Typography variant="caption">Loading...</Typography>
<Typography variant="caption"> {t("Loading...")}</Typography>
) : !summary2F?.rows || summary2F.rows.length === 0 ? (
<Typography
variant="body2"


+ 23
- 8
src/components/JoSearch/JoCreateFormModal.tsx View File

@@ -39,6 +39,9 @@ const JoCreateFormModal: React.FC<Props> = ({

const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
onChange(value.id)
if (value.outputQty != null) {
formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true })
}
}, [])

const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
@@ -156,16 +159,28 @@ const JoCreateFormModal: React.FC<Props> = ({
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<TextField
{...register("reqQty", {
<Controller
control={control}
name="reqQty"
rules={{
required: "Req. Qty. required!",
validate: (value) => value > 0
})}
label={t("Req. Qty")}
fullWidth
error={Boolean(errors.reqQty)}
variant="outlined"
type="number"
}}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label={t("Req. Qty")}
fullWidth
error={Boolean(error)}
variant="outlined"
type="number"
value={field.value ?? ""}
onChange={(e) => {
const val = e.target.value === "" ? undefined : Number(e.target.value);
field.onChange(val);
}}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>


+ 25
- 5
src/components/JoSearch/JoSearch.tsx View File

@@ -26,18 +26,19 @@ import dayjs from "dayjs";
import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { PrinterCombo } from "@/app/api/settings/printer";
import { JobTypeResponse } from "@/app/api/jo/actions";
interface Props {
defaultInputs: SearchJoResultRequest,
bomCombo: BomCombo[]
printerCombo: PrinterCombo[];
jobTypes: JobTypeResponse[];
}

type SearchQuery = Partial<Omit<JobOrder, "id">>;

type SearchParamNames = keyof SearchQuery;

const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => {
const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => {
const { t } = useTranslation("jo");
const router = useRouter()
const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
@@ -139,7 +140,16 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) =>
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [
{ label: t("Code"), paramName: "code", type: "text" },
{ label: t("Item Name"), paramName: "itemName", type: "text" },
{ label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "dateRange", preFilledValue: dayjsToDateString(dayjs(), "input") },
{ label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "dateRange", preFilledValue: {
from: dayjsToDateString(dayjs(), "input"),
to: dayjsToDateString(dayjs(), "input")
} },
{
label: t("Job Type"),
paramName: "jobTypeName",
type: "select",
options: jobTypes.map(jt => jt.name)
},
], [t])

const columns = useMemo<Column<JobOrder>[]>(
@@ -205,6 +215,13 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) =>
);
}
},
{
name: "jobTypeName",
label: t("Job Type"),
renderCell: (row) => {
return row.jobTypeName ? t(row.jobTypeName) : '-'
}
},
{
// TODO put it inside Action Buttons
name: "id",
@@ -271,6 +288,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) =>
planStartTo: query.planStartTo,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
jobTypeName: query.jobTypeName||"",
}
const response = await fetchJos(params)

@@ -363,14 +381,16 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) =>
const transformedQuery = {
...query,
planStart: query.planStart ? `${query.planStart}T00:00:00` : query.planStart,
planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo
planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo,
jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : ""
};
setInputs(() => ({
code: transformedQuery.code,
itemName: transformedQuery.itemName,
planStart: transformedQuery.planStart,
planStartTo: transformedQuery.planStartTo
planStartTo: transformedQuery.planStartTo,
jobTypeName: transformedQuery.jobTypeName
}))
refetchData(transformedQuery, "search");
}, [])


+ 6
- 4
src/components/JoSearch/JoSearchWrapper.tsx View File

@@ -4,7 +4,7 @@ import JoSearch from "./JoSearch";
import { SearchJoResultRequest } from "@/app/api/jo/actions";
import { fetchBomCombo } from "@/app/api/bom";
import { fetchPrinterCombo } from "@/app/api/settings/printer";
import { fetchAllJobTypes } from "@/app/api/jo/actions";
interface SubComponents {
Loading: typeof GeneralLoading;
}
@@ -17,13 +17,15 @@ const JoSearchWrapper: React.FC & SubComponents = async () => {

const [
bomCombo,
printerCombo
printerCombo,
jobTypes
] = await Promise.all([
fetchBomCombo(),
fetchPrinterCombo()
fetchPrinterCombo(),
fetchAllJobTypes()
])
return <JoSearch defaultInputs={defaultInputs} bomCombo={bomCombo} printerCombo={printerCombo}/>
return <JoSearch defaultInputs={defaultInputs} bomCombo={bomCombo} printerCombo={printerCombo} jobTypes={jobTypes}/>
}

JoSearchWrapper.Loading = GeneralLoading;


+ 34
- 0
src/components/Jodetail/JoPickOrderDetail.tsx View File

@@ -0,0 +1,34 @@
"use client";
import React, { useCallback } from "react";
import { Box, Button, Stack } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useTranslation } from "react-i18next";
import JobPickExecution from "./JobPickExecution";

interface JoPickOrderDetailProps {
pickOrderId: number | undefined;
jobOrderId: number | undefined;
onBack: () => void;
}

const JoPickOrderDetail: React.FC<JoPickOrderDetailProps> = ({
pickOrderId,
jobOrderId,
onBack,
}) => {
const { t } = useTranslation("jo");

return (
<Box>
<Box sx={{ mb: 2 }}>
<Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
{t("Back to List")}
</Button>
</Box>

<JobPickExecution filterArgs={{ pickOrderId, jobOrderId }} />
</Box>
);
};

export default JoPickOrderDetail;

+ 176
- 0
src/components/Jodetail/JoPickOrderList.tsx View File

@@ -0,0 +1,176 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import {
Box,
Button,
Card,
CardContent,
CardActions,
Stack,
Typography,
Chip,
CircularProgress,
TablePagination,
Grid,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useTranslation } from "react-i18next";
import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions";
import JobPickExecution from "./newJobPickExecution";

const PER_PAGE = 6;

const JoPickOrderList: React.FC = () => {
const { t } = useTranslation(["common", "jo"]);
const [loading, setLoading] = useState(false);
const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]);
const [page, setPage] = useState(0);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined);
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);

const fetchPickOrders = useCallback(async () => {
setLoading(true);
try {
const data = await fetchAllJoPickOrders();
setPickOrders(Array.isArray(data) ? data : []);
setPage(0);
} catch (e) {
console.error(e);
setPickOrders([]);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
fetchPickOrders();
}, [fetchPickOrders]);

// If a pick order is selected, show JobPickExecution detail view
if (selectedPickOrderId !== undefined) {
return (
<Box>
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
onClick={() => {
setSelectedPickOrderId(undefined);
setSelectedJobOrderId(undefined);
}}
startIcon={<ArrowBackIcon />}
>
{t("Back to List")}
</Button>
</Box>
<JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} />
</Box>
);
}

const startIdx = page * PER_PAGE;
const paged = pickOrders.slice(startIdx, startIdx + PER_PAGE);

return (
<Box>
{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total pick orders")}: {pickOrders.length}
</Typography>

<Grid container spacing={2}>
{paged.map((pickOrder) => {
const status = String(pickOrder.jobOrderStatus || "");
const statusLower = status.toLowerCase();
const statusColor =
statusLower === "completed"
? "success"
: statusLower === "pending" || statusLower === "processing"
? "primary"
: "default";

const finishedCount = pickOrder.finishedPickOLineCount ?? 0;

return (
<Grid key={pickOrder.id} item xs={12} sm={6} md={4}>
<Card
sx={{
minHeight: 160,
maxHeight: 240,
display: "flex",
flexDirection: "column",
}}
>
<CardContent
sx={{
pb: 1,
flexGrow: 1,
overflow: "auto",
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1">
{t("Job Order")}: {pickOrder.jobOrderCode || "-"}
</Typography>
</Box>
<Chip size="small" label={t(status)} color={statusColor as any} />
</Stack>

<Typography variant="body2" color="text.secondary">
{t("Pick Order")}: {pickOrder.pickOrderCode || "-"}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Item Name")}: {pickOrder.itemName}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName})
</Typography>
{statusLower !== "pending" && finishedCount > 0 && (
<Box sx={{ mt: 1 }}>
<Typography variant="body2" fontWeight={600}>
{t("Finished lines")}: {finishedCount}
</Typography>
</Box>
)}
</CardContent>

<CardActions sx={{ pt: 0.5 }}>
<Button
variant="contained"
size="small"
onClick={() => {
setSelectedPickOrderId(pickOrder.pickOrderId ?? undefined);
setSelectedJobOrderId(pickOrder.jobOrderId ?? undefined);
}}
>
{t("View Details")}
</Button>
<Box sx={{ flex: 1 }} />
</CardActions>
</Card>
</Grid>
);
})}
</Grid>
{pickOrders.length > 0 && (
<TablePagination
component="div"
count={pickOrders.length}
page={page}
rowsPerPage={PER_PAGE}
onPageChange={(e, p) => setPage(p)}
rowsPerPageOptions={[PER_PAGE]}
/>
)}
</Box>
)}
</Box>
);
};

export default JoPickOrderList;

+ 3
- 1
src/components/Jodetail/JobPickExecution.tsx View File

@@ -457,7 +457,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
// TODO: Implement QR code functionality
};

const getPickOrderId = useCallback(() => {
return filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
}, [filterArgs?.pickOrderId]);
// 修改:使用 Job Order API 获取数据
const fetchJobOrderData = useCallback(async (userId?: number) => {
setCombinedDataLoading(true);


+ 4
- 0
src/components/Jodetail/JodetailSearch.tsx View File

@@ -26,6 +26,7 @@ import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan";
import FInishedJobOrderRecord from "./FInishedJobOrderRecord";
import JobPickExecution from "./JobPickExecution";
import CompleteJobOrderRecord from "./completeJobOrderRecord";
import JoPickOrderList from "./JoPickOrderList";
import {
fetchUnassignedJobOrderPickOrders,
assignJobOrderPickOrder,
@@ -35,6 +36,7 @@ import {
} from "@/app/api/jo/actions";
import { fetchPrinterCombo } from "@/app/api/settings/printer";
import { PrinterCombo } from "@/app/api/settings/printer";
import JoPickOrderDetail from "./JoPickOrderDetail";
interface Props {
pickOrders: PickOrderResult[];
printerCombo: PrinterCombo[];
@@ -474,6 +476,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Pick Order Detail")} iconPosition="end" />
<Tab label={t("Complete Job Order Record")} iconPosition="end" />
{/* <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> */}
{/* <Tab label={t("Job Order Match")} iconPosition="end" /> */}
{/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */}
</Tabs>
@@ -486,6 +489,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
}}>
{tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />}
{tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />}
{/* {tabIndex === 2 && <JoPickOrderList />} */}
{/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */}
{/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */}
</Box>


+ 1942
- 0
src/components/Jodetail/newJobPickExecution.tsx
File diff suppressed because it is too large
View File


+ 7
- 0
src/components/NavigationContent/NavigationContent.tsx View File

@@ -196,11 +196,13 @@ const NavigationContent: React.FC = () => {
label: "Detail Scheduling",
path: "/scheduling/detailed",
},
/*
{
icon: <RequestQuote />,
label: "Production",
path: "/production",
},
*/
],
},
{
@@ -218,6 +220,11 @@ const NavigationContent: React.FC = () => {
label: "Job Order Pickexcution",
path: "/jodetail",
},
{
icon: <RequestQuote />,
label: "Job Order Production Process",
path: "/productionProcess",
},
],
},
{


+ 39
- 2
src/components/PickOrderSearch/LotTable.tsx View File

@@ -303,7 +303,7 @@ const QrCodeModal: React.FC<{
{/* Manual Input with Submit-Triggered Helper Text */}
{false &&(
{true &&(
<Box sx={{ mb: 2 }}>
<Typography variant="body2" gutterBottom>
<strong>{t("Manual Input")}:</strong>
@@ -588,7 +588,44 @@ const LotTable: React.FC<LotTableProps> = ({
console.error("Error submitting pick execution form:", error);
}
}, [onDataRefresh, onLotDataRefresh]);

const allLotsUnavailable = useMemo(() => {
if (!paginatedLotTableData || paginatedLotTableData.length === 0) return false;
return paginatedLotTableData.every((lot) =>
['rejected', 'expired', 'insufficient_stock', 'status_unavailable']
.includes(lot.lotAvailability)
);
}, [paginatedLotTableData]);
// 完成当前行(无可用批次)的点击处理
const handleCompleteWithoutLot = useCallback(async (lot: LotPickData) => {
try {
if (!lot.stockOutLineId) {
alert("No stock out line for this lot. Please contact administrator.");
return;
}
// 这里建议调用你自己在 actions 里封装的 API,例如:
// await completeStockOutLineWithoutLot(lot.stockOutLineId);
// 简单点可以复用 updateStockOutLineStatus,直接标记 COMPLETE、数量为 0:
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: 'completed',
qty: lot.stockOutLineQty || 0,
});
// 刷新数据
if (onLotDataRefresh) {
await onLotDataRefresh();
}
if (onDataRefresh) {
await onDataRefresh();
}
} catch (e) {
console.error("Error completing stock out line without lot", e);
alert("Failed to complete this line. Please try again.");
}
}, [onDataRefresh, onLotDataRefresh]);
return (
<>
<TableContainer component={Paper}>


+ 46
- 0
src/components/ProductionProcess/ProcessSummaryHeader.tsx View File

@@ -0,0 +1,46 @@
import { Card, CardContent, Stack, Typography } from "@mui/material";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { useTranslation } from "react-i18next";

interface Props {
processData?: {
jobOrderCode?: string;
itemCode?: string;
itemName?: string;
jobType?: string;
outputQty?: number | string;
date?: string;
};
}

const ProcessSummaryHeader: React.FC<Props> = ({ processData }) => {
const { t } = useTranslation();
return (
<Card sx={{ mb: 2 }}>
<CardContent>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={2}>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Job Order Code")}: <strong>{processData?.jobOrderCode}</strong>
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Item")}: <strong>{processData?.itemCode+"-"+processData?.itemName}</strong>
</Typography>

<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Job Type")}: <strong style={{ color: "green" }}>{t(processData?.jobType ?? "")}</strong>
</Typography>

<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Qty")}: <strong style={{ color: "green" }}>{processData?.outputQty}</strong>
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Production Date")}: <strong style={{ color: "green" }}>{processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}</strong>
</Typography>
</Stack>
</CardContent>
</Card>
);
};

export default ProcessSummaryHeader;

+ 150
- 56
src/components/ProductionProcess/ProductionProcessDetail.tsx View File

@@ -49,15 +49,17 @@ import {
import { fetchNameList, NameList } from "@/app/api/user/actions";
import ProductionProcessStepExecution from "./ProductionProcessStepExecution";
import ProductionOutputFormPage from "./ProductionOutputFormPage";
import ProcessSummaryHeader from "./ProcessSummaryHeader";
interface ProductProcessDetailProps {
jobOrderId: number;
onBack: () => void;
fromJosave?: boolean;
}

const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
jobOrderId,
onBack,
fromJosave,
}) => {
const { t } = useTranslation();
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -78,6 +80,8 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null);
const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null);
const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null);
const [scannedStaffNo, setScannedStaffNo] = useState<string | null>(null);
const [scanningLineId, setScanningLineId] = useState<number | null>(null);
const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null);
const [showScanDialog, setShowScanDialog] = useState(false);
@@ -146,6 +150,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({

// 提交产出数据
/*
const processQrCode = useCallback((qrValue: string, lineId: number) => {
// 操作员格式:{2fitestu1} - 键盘模拟输入(测试用)
if (qrValue.match(/\{2fitestu(\d+)\}/)) {
@@ -205,7 +210,92 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
// TODO: 处理普通文本格式
}
}, []);
*/
// 提交产出数据
const processQrCode = useCallback((qrValue: string, lineId: number) => {
// 设备快捷格式:{2fiteste数字} - 自动生成 equipmentTypeSubTypeEquipmentNo
// 格式:{2fiteste数字} = line.equipment_name + "-数字號"
// 例如:{2fiteste1} = "包裝機類-真空八爪魚機-1號"
if (qrValue.match(/\{2fiteste(\d+)\}/)) {
const match = qrValue.match(/\{2fiteste(\d+)\}/);
const equipmentNo = parseInt(match![1]);
// 根据 lineId 找到对应的 line
const currentLine = lines.find(l => l.id === lineId);
if (currentLine && currentLine.equipment_name) {
const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`;
setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo);
console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`);
} else {
// 如果找不到 line,尝试从 API 获取 line detail
console.warn(`Line with ID ${lineId} not found in current lines, fetching from API...`);
fetchProductProcessLineDetail(lineId)
.then((lineDetail) => {
// 从 lineDetail 中获取 equipment_name
// 注意:lineDetail 的结构可能不同,需要根据实际 API 响应调整
const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || "";
if (equipmentName) {
const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`;
setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo);
console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`);
} else {
console.warn(`Equipment name not found in line detail for lineId: ${lineId}`);
}
})
.catch((err) => {
console.error(`Failed to fetch line detail for lineId ${lineId}:`, err);
});
}
return;
}
// 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo
// 例如:{2fitestu123} = staffNo: "123"
// 例如:{2fitestustaff001} = staffNo: "staff001"
if (qrValue.match(/\{2fitestu(.+)\}/)) {
const match = qrValue.match(/\{2fitestu(.+)\}/);
const staffNo = match![1];
setScannedStaffNo(staffNo);
return;
}

// 正常 QR 扫描器扫描格式
const trimmedValue = qrValue.trim();
// 检查 staffNo 格式:"staffNo: STAFF001" 或 "staffNo:STAFF001"
const staffNoMatch = trimmedValue.match(/^staffNo:\s*(.+)$/i);
if (staffNoMatch) {
const staffNo = staffNoMatch[1].trim();
setScannedStaffNo(staffNo);
return;
}
// 检查 equipmentTypeSubTypeEquipmentNo 格式
const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo):\s*(.+)$/i);
if (equipmentCodeMatch) {
const equipmentCode = equipmentCodeMatch[1].trim();
setScannedEquipmentTypeSubTypeEquipmentNo(equipmentCode);
return;
}
// 其他格式处理(JSON、普通文本等)
try {
const qrData = JSON.parse(qrValue);
// TODO: 处理 JSON 格式的 QR 码
} catch {
// 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode
if (trimmedValue.length > 0) {
if (trimmedValue.toUpperCase().startsWith("STAFF") || /^\d+$/.test(trimmedValue)) {
// 可能是员工编号
setScannedStaffNo(trimmedValue);
} else if (trimmedValue.includes("-")) {
// 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號")
setScannedEquipmentTypeSubTypeEquipmentNo(trimmedValue);
}
}
}
}, [lines]);
// 处理 QR 码扫描效果
useEffect(() => {
if (isManualScanning && qrValues.length > 0 && scanningLineId) {
@@ -219,74 +309,72 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
processQrCode(latestQr, scanningLineId);
}
}, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]);

const submitScanAndStart = useCallback(async (lineId: number) => {
console.log("submitScanAndStart called with:", {
lineId,
scannedOperatorId,
scannedEquipmentId,
scannedStaffNo,
scannedEquipmentTypeSubTypeEquipmentNo,
});
if (!scannedOperatorId) {
console.log("No operatorId, cannot submit");
if (!scannedStaffNo) {
console.log("No staffNo, cannot submit");
setIsAutoSubmitting(false);
return false; // 没有 operatorId,不能提交
return false; // 没有 staffNo,不能提交
}
try {
// 获取 line detail 以检查 bomProcessEquipmentId
const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId);
// 提交 operatorId 和 equipmentId
// 提交 staffNo 和 equipmentTypeSubTypeEquipmentNo
console.log("Submitting scan data:", {
productProcessLineId: lineId,
operatorId: scannedOperatorId,
equipmentId: scannedEquipmentId || undefined,
staffNo: scannedStaffNo,
equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo,
});
const response = await updateProductProcessLineQrscan({
productProcessLineId: lineId,
operatorId: scannedOperatorId,
equipmentId: scannedEquipmentId || undefined,
equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo || undefined,
staffNo: scannedStaffNo || undefined,
});
console.log("Scan submit response:", response);
// 检查响应中的 message 字段来判断是否成功
// 如果后端返回 message 不为 null,说明验证失败
if (response && response.message) {
setIsAutoSubmitting(false);
// 清除定时器
if (autoSubmitTimerRef.current) {
clearTimeout(autoSubmitTimerRef.current);
autoSubmitTimerRef.current = null;
}
//alert(response.message || t("Validation failed. Please check operator and equipment."));
return false;
}
// 验证通过,继续执行后续步骤
console.log("Validation passed, starting line...");
handleStopScan();
setShowScanDialog(false);
setIsAutoSubmitting(false);
await handleStartLine(lineId);
setSelectedLineId(lineId);
setIsExecutingLine(true);
await fetchProcessDetail();
return true;
} catch (error) {
console.error("Error submitting scan:", error);
//alert(t("Failed to submit scan data. Please try again."));
setIsAutoSubmitting(false);
return false;
}
}, [scannedOperatorId, scannedEquipmentId, lineDetailForScan, t, fetchProcessDetail]);
// 验证通过,继续执行后续步骤
console.log("Validation passed, starting line...");
handleStopScan();
setShowScanDialog(false);
setIsAutoSubmitting(false);
await handleStartLine(lineId);
setSelectedLineId(lineId);
setIsExecutingLine(true);
await fetchProcessDetail();
return true;
} catch (error) {
console.error("Error submitting scan:", error);
setIsAutoSubmitting(false);
return false;
}
}, [scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, lineDetailForScan, t, fetchProcessDetail]);
const handleSubmitScanAndStart = useCallback(async (lineId: number) => {
console.log("handleSubmitScanAndStart called with lineId:", lineId);
if (!scannedOperatorId) {
if (!scannedStaffNo) {
//alert(t("Please scan operator code first"));
return;
}
@@ -316,8 +404,11 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
setLineDetailForScan(null);
// 获取 line detail 以获取 bomProcessEquipmentId
fetchProductProcessLineDetail(lineId)
.then(setLineDetailForScan)
.catch(err => console.error("Failed to load line detail", err));
.then(setLineDetailForScan)
.catch(err => {
console.error("Failed to load line detail", err);
// 不阻止扫描继续,line detail 不是必需的
});
startScan();
}, [startScan]);

@@ -351,16 +442,16 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
useEffect(() => {
console.log("Auto-submit check:", {
scanningLineId,
scannedOperatorId,
scannedEquipmentId,
scannedStaffNo,
scannedEquipmentTypeSubTypeEquipmentNo,
isAutoSubmitting,
isManualScanning,
});

if (
scanningLineId &&
scannedOperatorId !== null &&
scannedEquipmentId !== null &&
scannedStaffNo !== null &&
scannedEquipmentTypeSubTypeEquipmentNo !== null &&
!isAutoSubmitting &&
isManualScanning
) {
@@ -385,7 +476,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
// 注意:这里不立即清除定时器,因为我们需要它执行
// 只在组件卸载时清除
};
}, [scanningLineId, scannedOperatorId, scannedEquipmentId, isAutoSubmitting, isManualScanning, submitScanAndStart]);
}, [scanningLineId, scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, isAutoSubmitting, isManualScanning, submitScanAndStart]);
useEffect(() => {
return () => {
if (autoSubmitTimerRef.current) {
@@ -477,7 +568,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<Typography variant="h6" gutterBottom fontWeight="bold">
{t("Production Process Steps")}
</Typography>
<ProcessSummaryHeader t={t} processData={processData} />
{!isExecutingLine ? (
/* ========== 步骤列表视图 ========== */
<TableContainer>
@@ -509,7 +600,8 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
</Box>
</TableCell>
<TableCell align="center">{t("Status")}</TableCell>
<TableCell align="center">{t("Action")}</TableCell>
{!fromJosave&&(<TableCell align="center">{t("Action")}</TableCell>)}
</TableRow>
</TableHead>
<TableBody>
@@ -529,7 +621,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<Typography fontWeight={500}>{line.name}</Typography>
</TableCell>
<TableCell><Typography fontWeight={500}>{line.description || "-"}</Typography></TableCell>
<TableCell><Typography fontWeight={500}>{equipmentName}</Typography></TableCell>
<TableCell><Typography fontWeight={500}>{line.equipmentDetailCode||equipmentName}</Typography></TableCell>
<TableCell><Typography fontWeight={500}>{line.operatorName}</Typography></TableCell>
{/*
<TableCell><Typography fontWeight={500}>{line.durationInMinutes} </Typography></TableCell>
@@ -561,6 +653,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<Chip label={t("Unknown")} color="error" size="small" />
)}
</TableCell>
{!fromJosave&&(
<TableCell align="center">
{statusLower === 'pending' ? (
<Button
@@ -598,6 +691,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
</Button>
)}
</TableCell>
)}
</TableRow>
);
})}
@@ -635,17 +729,17 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<Stack spacing={2} sx={{ mt: 2 }}>
<Box>
<Typography variant="body2" color="text.secondary">
{scannedOperatorId
? `${t("Operator")}: ${scannedOperatorId}`
: t("Please scan operator code")
{scannedStaffNo
? `${t("Staff No")}: ${scannedStaffNo}`
: t("Please scan staff no")
}
</Typography>
</Box>
<Box>
<Typography variant="body2" color="text.secondary">
{scannedEquipmentId
? `${t("Equipment")}: ${scannedEquipmentId}`
{scannedEquipmentTypeSubTypeEquipmentNo
? `${t("Equipment Type/Code")}: ${scannedEquipmentTypeSubTypeEquipmentNo}`
: t("Please scan equipment code (optional if not required)")
}
</Typography>
@@ -672,7 +766,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<Button
variant="contained"
onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)}
disabled={!scannedOperatorId}
disabled={!scannedStaffNo}
>
{t("Submit & Start")}
</Button>


+ 34
- 5
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx View File

@@ -17,7 +17,7 @@ import {
} from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTranslation } from "react-i18next";
import { fetchProductProcessesByJobOrderId } from "@/app/api/jo/actions";
import { fetchProductProcessesByJobOrderId ,deleteJobOrder} from "@/app/api/jo/actions";
import ProductionProcessDetail from "./ProductionProcessDetail";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
@@ -30,6 +30,7 @@ import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { releaseJo, startJo } from "@/app/api/jo/actions";
import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
import ProcessSummaryHeader from "./ProcessSummaryHeader";
interface JobOrderLine {
id: number;
jobOrderId: number;
@@ -127,7 +128,12 @@ const stockCounts = useMemo(() => {
};
}, [jobOrderLines, inventoryData]);
const status = processData?.status?.toLowerCase?.() ?? "";

const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
const response = await deleteJobOrder(jobOrderId)
if (response) {
setProcessData(response.entity);
}
}, [jobOrderId]);
const handleRelease = useCallback(async ( jobOrderId: number) => {
// TODO: 替换为实际的 release 调用
console.log("Release clicked for jobOrderId:", jobOrderId);
@@ -256,6 +262,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
align: "left",
headerAlign: "center",
type: "number",
renderCell: (params) => {
return <Typography sx={{ fontSize: "14px" }}>{params.value}</Typography>;
},
},
{
field: "description",
@@ -263,6 +272,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
flex: 1,
align: "left",
headerAlign: "center",
renderCell: (params) => {
return <Typography sx={{ fontSize: "14px" }}>{params.value || ""}</Typography>;
},
},
];
const productionProcessesLineRemarkTableRows =
@@ -270,6 +282,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
id: line.seqNo,
seqNo: line.seqNo,
description: line.description ?? "",

})) ?? [];


@@ -356,11 +369,13 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {

const pickTableRows = jobOrderLines.map((line, index) => ({
...line,
id: line.id || index,
//id: line.id || index,
id: index + 1,
}));

const PickTableContent = () => (
<Box sx={{ mt: 2 }}>
<ProcessSummaryHeader processData={processData} />
<Card sx={{ mb: 2 }}>
<CardContent>
<Stack
@@ -380,7 +395,17 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
</Typography>
{fromJosave && (
{fromJosave && (
<Button
variant="contained"
color="error"
onClick={() => handleDeleteJobOrder(jobOrderId)}
disabled={processData?.jobOrderStatus !== "planning"}
>
{t("Delete Job Order")}
</Button>
)}
{fromJosave && (
<Button
variant="contained"
color="primary"
@@ -406,6 +431,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
);
const ProductionProcessesLineRemarkTableContent = () => (
<Box sx={{ mt: 2 }}>
<ProcessSummaryHeader processData={processData} />
<StyledDataGrid
sx={{
"--DataGrid-overlayHeight": "100px",
@@ -414,6 +440,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
rows={productionProcessesLineRemarkTableRows ?? []}
columns={productionProcessesLineRemarkTableColumns}
getRowHeight={() => 'auto'}
/>
</Box>
);
@@ -427,7 +454,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
{t("Back to List")}
</Button>
</Box>
{/* 标签页 */}
<Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
@@ -455,7 +482,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
onBack={() => {
// 切换回第一个标签页,或者什么都不做
setTabIndex(0);

}}
fromJosave={fromJosave}
/>
)}
{tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}


+ 36
- 16
src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx View File

@@ -166,21 +166,41 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
if (qrCodeScannerValues.length > 0) {
const scannedValues = qrCodeScannerValues[0];
console.log("%c Scanned Result: ", "color:cyan", scannedValues);
if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING
const number = scannedValues.substring(8, scannedValues.length - 1);
if (/^\d+$/.test(number)) { // Check if number contains only digits
console.log("%c DEBUG: detected ID: ", "color:pink", number);
const debugValue = {
value: number
}
setScanResult(debugValue);
} else {
resetQrCodeScanner("DEBUG -- Invalid number format: " + number);
// 先检查是否是 {2fiteste...} 或 {2fitestu...} 格式
// 这些格式需要传递完整值给 processQrCode 处理
if (scannedValues.length > 9) {
const ninthChar = scannedValues.substring(8, 9);
if (ninthChar === "e" || ninthChar === "u") {
// {2fiteste数字} 或 {2fitestu任何内容} 格式
console.log("%c DEBUG: detected shortcut format: ", "color:pink", scannedValues);
const debugValue = {
value: scannedValues // 传递完整值,让 processQrCode 处理
}
setScanResult(debugValue);
return;
}
}
// 原有的 {2fitest数字} 格式(纯数字,向后兼容)
const number = scannedValues.substring(8, scannedValues.length - 1);
if (/^\d+$/.test(number)) { // Check if number contains only digits
console.log("%c DEBUG: detected ID: ", "color:pink", number);
const debugValue = {
value: number
}
return;
setScanResult(debugValue);
} else {
// 如果不是纯数字,传递完整值让 processQrCode 处理
const debugValue = {
value: scannedValues
}
setScanResult(debugValue);
}
return;
}

try {
const data: QrCodeInfo = JSON.parse(scannedValues);
console.log("%c Parsed scan data", "color:green", data);
@@ -188,18 +208,18 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
const content = scannedValues.substring(1, scannedValues.length - 1);
data.value = content;
setScanResult(data);
} catch (error) { // Rought match for other scanner input -- Pending Review
} catch (error) { // Rough match for other scanner input -- Pending Review
const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0;
if (silId == 0) {
const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0;
setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()});
} else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); }
resetQrCodeScanner(String(error));
}
// resetQrCodeScanner();
}
}, [qrCodeScannerValues]);


+ 7
- 2
src/i18n/zh/common.json View File

@@ -2,7 +2,8 @@

"dashboard": "資訊展示面板",
"Edit": "編輯",

"Job Order Production Process": "工單生產流程",
"productionProcess": "生產流程",
"Search Criteria": "搜尋條件",
"All": "全部",
"No options": "沒有選項",
@@ -12,11 +13,12 @@
"code": "編號",
"Name": "名稱",
"Type": "類型",
"WIP": "半成品",
"R&D": "研發",
"STF": "樣品",
"Other": "其他",
"Add some entries!": "添加條目",
"Add Record": "新增",
"Clean Record": "重置",
@@ -54,12 +56,15 @@
"sfg": "半成品",
"item": "貨品",
"FG":"成品",
"Qty":"數量",
"FG & Material Demand Forecast Detail":"成品及材料需求預測詳情",
"View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌",
"Delivery Order":"送貨訂單",
"Detail Scheduling":"詳細排程",
"Customer":"客戶",
"qcItem":"品檢項目",
"Item":"物料",
"Production Date":"生產日期",
"QC Check Item":"QC品檢項目",
"QC Category":"QC品檢模板",
"qcCategory":"品檢模板",


+ 7
- 4
src/i18n/zh/jo.json View File

@@ -12,6 +12,7 @@
"UoM": "銷售單位",
"Status": "工單狀態",
"Lot No.": "批號",
"Delete Job Order": "刪除工單",
"Bom": "半成品/成品編號",
"Release": "放單",
"Pending": "待掃碼",
@@ -276,10 +277,11 @@
"success": "成功",
"Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量",
"BOM Status": "材料預備狀況",
"Estimated Production Date": "預計生產日期及時間",
"Estimated Production Date": "預計生產日期",
"Plan Start": "預計生產日期",
"Plan Start From": "預計生產日期及時間",
"Plan Start To": "預計生產日期及時間至",
"Plan Start From": "預計生產日期",
"Delivery Note Code": "送貨單編號",
"Plan Start To": "預計生產日期至",
"By-product": "副產品",
"Complete Step": "完成步驟",
"Defect": "缺陷",
@@ -329,7 +331,8 @@
"Total Steps": "總步驟數",
"Unknown": "",
"Job Type": "工單類型",

"Production Date":"生產日期",
"Jo Pick Order Detail":"工單提料詳情",
"WIP": "半成品",
"R&D": "研發",
"STF": "員工餐",


+ 1
- 0
src/i18n/zh/pickOrder.json View File

@@ -204,6 +204,7 @@
"Report and Pick another lot": "上報並需重新選擇批號",
"Accept Stock Out": "接受出庫",
"Pick Another Lot": "欠數,並重新選擇批號",
"Delivery Note Code": "送貨單編號",
"Lot No": "批號",
"Expiry Date": "到期日",
"Location": "位置",


Loading…
Cancel
Save