| @@ -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; | |||||
| @@ -6,6 +6,7 @@ export interface BomCombo { | |||||
| id: number; | id: number; | ||||
| value: number; | value: number; | ||||
| label: string; | label: string; | ||||
| outputQty: number; | |||||
| } | } | ||||
| export const preloadBomCombo = (() => { | export const preloadBomCombo = (() => { | ||||
| @@ -26,6 +26,7 @@ export interface SearchJoResultRequest extends Pageable { | |||||
| itemName?: string; | itemName?: string; | ||||
| planStart?: string; | planStart?: string; | ||||
| planStartTo?: string; | planStartTo?: string; | ||||
| jobTypeName?: string; | |||||
| } | } | ||||
| export interface productProcessLineQtyRequest { | export interface productProcessLineQtyRequest { | ||||
| @@ -96,6 +97,8 @@ export interface JobOrderDetail { | |||||
| reqQty: number; | reqQty: number; | ||||
| uom: string; | uom: string; | ||||
| pickLines: any[]; | pickLines: any[]; | ||||
| jobTypeName: string; | |||||
| status: string; | status: string; | ||||
| } | } | ||||
| @@ -183,6 +186,7 @@ export interface ProductProcessLineResponse { | |||||
| name: string, | name: string, | ||||
| description: string, | description: string, | ||||
| equipment_name: string, | equipment_name: string, | ||||
| equipmentDetailCode: string, | |||||
| status: string, | status: string, | ||||
| byproductId: number, | byproductId: number, | ||||
| byproductName: string, | byproductName: string, | ||||
| @@ -215,6 +219,8 @@ export interface ProductProcessWithLinesResponse { | |||||
| isDark: string; | isDark: string; | ||||
| isDense: number; | isDense: number; | ||||
| isFloat: string; | isFloat: string; | ||||
| scrapRate: number; | |||||
| allergicSubstance: string; | |||||
| itemId: number; | itemId: number; | ||||
| itemCode: string; | itemCode: string; | ||||
| itemName: string; | itemName: string; | ||||
| @@ -301,8 +307,10 @@ export interface ProductProcessInfoResponse { | |||||
| } | } | ||||
| export interface ProductProcessLineQrscanUpadteRequest { | export interface ProductProcessLineQrscanUpadteRequest { | ||||
| productProcessLineId: number; | productProcessLineId: number; | ||||
| operatorId?: number; | |||||
| equipmentId?: number; | |||||
| //operatorId?: number; | |||||
| //equipmentId?: number; | |||||
| equipmentTypeSubTypeEquipmentNo?: string; | |||||
| staffNo?: string; | |||||
| } | } | ||||
| export interface ProductProcessLineDetailResponse { | export interface ProductProcessLineDetailResponse { | ||||
| @@ -403,6 +411,7 @@ export interface ProductProcessLineInfoResponse { | |||||
| name: string, | name: string, | ||||
| description: string, | description: string, | ||||
| equipment_name: string, | equipment_name: string, | ||||
| equipmentDetailCode: string, | |||||
| status: string, | status: string, | ||||
| byproductId: number, | byproductId: number, | ||||
| byproductName: string, | byproductName: string, | ||||
| @@ -419,8 +428,74 @@ export interface ProductProcessLineInfoResponse { | |||||
| startTime: string, | startTime: string, | ||||
| endTime: 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) => { | export const fetchProductProcessLineDetail = cache(async (lineId: number) => { | ||||
| return serverFetchJson<JobOrderProcessLineDetailResponse>( | return serverFetchJson<JobOrderProcessLineDetailResponse>( | ||||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`, | `${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) => { | 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>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/product-process/Demo/update`, | `${BASE_API_URL}/product-process/Demo/update`, | ||||
| { | { | ||||
| method: "POST", | method: "POST", | ||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| body: JSON.stringify(request), | |||||
| body: JSON.stringify(requestBody), | |||||
| } | } | ||||
| ); | ); | ||||
| }); | }); | ||||
| @@ -28,6 +28,8 @@ export interface JobOrder { | |||||
| planStartTo?: string; | planStartTo?: string; | ||||
| planEnd?: number[]; | planEnd?: number[]; | ||||
| type: string; | type: string; | ||||
| jobTypeId: number; | |||||
| jobTypeName: string; | |||||
| // TODO pack below into StockInLineInfo | // TODO pack below into StockInLineInfo | ||||
| stockInLineId?: number; | stockInLineId?: number; | ||||
| stockInLineStatus?: string; | stockInLineStatus?: string; | ||||
| @@ -452,6 +452,10 @@ export interface LaneBtn { | |||||
| unassigned: number; | unassigned: number; | ||||
| total: number; | total: number; | ||||
| } | } | ||||
| export const fetchDoPickOrderDetail = async ( | export const fetchDoPickOrderDetail = async ( | ||||
| doPickOrderId: number, | doPickOrderId: number, | ||||
| selectedPickOrderId?: number | selectedPickOrderId?: number | ||||
| @@ -1,6 +1,6 @@ | |||||
| "use client"; | "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 { useCallback, useEffect, useState } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| @@ -217,7 +217,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| }} | }} | ||||
| > | > | ||||
| {isLoadingSummary ? ( | {isLoadingSummary ? ( | ||||
| <Typography variant="caption">Loading...</Typography> | |||||
| <Typography variant="caption"> {t("Loading...")}</Typography> | |||||
| ) : !summary2F?.rows || summary2F.rows.length === 0 ? ( | ) : !summary2F?.rows || summary2F.rows.length === 0 ? ( | ||||
| <Typography | <Typography | ||||
| variant="body2" | variant="body2" | ||||
| @@ -39,6 +39,9 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | ||||
| onChange(value.id) | 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) => { | const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => { | ||||
| @@ -156,16 +159,28 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12} sm={12} md={6}> | <Grid item xs={12} sm={12} md={6}> | ||||
| <TextField | |||||
| {...register("reqQty", { | |||||
| <Controller | |||||
| control={control} | |||||
| name="reqQty" | |||||
| rules={{ | |||||
| required: "Req. Qty. required!", | required: "Req. Qty. required!", | ||||
| validate: (value) => value > 0 | 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> | ||||
| <Grid item xs={12} sm={12} md={6}> | <Grid item xs={12} sm={12} md={6}> | ||||
| @@ -26,18 +26,19 @@ import dayjs from "dayjs"; | |||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | import { fetchInventories } from "@/app/api/inventory/actions"; | ||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | |||||
| interface Props { | interface Props { | ||||
| defaultInputs: SearchJoResultRequest, | defaultInputs: SearchJoResultRequest, | ||||
| bomCombo: BomCombo[] | bomCombo: BomCombo[] | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| jobTypes: JobTypeResponse[]; | |||||
| } | } | ||||
| type SearchQuery = Partial<Omit<JobOrder, "id">>; | type SearchQuery = Partial<Omit<JobOrder, "id">>; | ||||
| type SearchParamNames = keyof SearchQuery; | 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 { t } = useTranslation("jo"); | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]); | const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]); | ||||
| @@ -139,7 +140,16 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ | const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ | ||||
| { label: t("Code"), paramName: "code", type: "text" }, | { label: t("Code"), paramName: "code", type: "text" }, | ||||
| { label: t("Item Name"), paramName: "itemName", 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]) | ], [t]) | ||||
| const columns = useMemo<Column<JobOrder>[]>( | 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 | // TODO put it inside Action Buttons | ||||
| name: "id", | name: "id", | ||||
| @@ -271,6 +288,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => | |||||
| planStartTo: query.planStartTo, | planStartTo: query.planStartTo, | ||||
| pageNum: pagingController.pageNum - 1, | pageNum: pagingController.pageNum - 1, | ||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| jobTypeName: query.jobTypeName||"", | |||||
| } | } | ||||
| const response = await fetchJos(params) | const response = await fetchJos(params) | ||||
| @@ -363,14 +381,16 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => | |||||
| const transformedQuery = { | const transformedQuery = { | ||||
| ...query, | ...query, | ||||
| planStart: query.planStart ? `${query.planStart}T00:00:00` : query.planStart, | 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(() => ({ | setInputs(() => ({ | ||||
| code: transformedQuery.code, | code: transformedQuery.code, | ||||
| itemName: transformedQuery.itemName, | itemName: transformedQuery.itemName, | ||||
| planStart: transformedQuery.planStart, | planStart: transformedQuery.planStart, | ||||
| planStartTo: transformedQuery.planStartTo | |||||
| planStartTo: transformedQuery.planStartTo, | |||||
| jobTypeName: transformedQuery.jobTypeName | |||||
| })) | })) | ||||
| refetchData(transformedQuery, "search"); | refetchData(transformedQuery, "search"); | ||||
| }, []) | }, []) | ||||
| @@ -4,7 +4,7 @@ import JoSearch from "./JoSearch"; | |||||
| import { SearchJoResultRequest } from "@/app/api/jo/actions"; | import { SearchJoResultRequest } from "@/app/api/jo/actions"; | ||||
| import { fetchBomCombo } from "@/app/api/bom"; | import { fetchBomCombo } from "@/app/api/bom"; | ||||
| import { fetchPrinterCombo } from "@/app/api/settings/printer"; | import { fetchPrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { fetchAllJobTypes } from "@/app/api/jo/actions"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof GeneralLoading; | Loading: typeof GeneralLoading; | ||||
| } | } | ||||
| @@ -17,13 +17,15 @@ const JoSearchWrapper: React.FC & SubComponents = async () => { | |||||
| const [ | const [ | ||||
| bomCombo, | bomCombo, | ||||
| printerCombo | |||||
| printerCombo, | |||||
| jobTypes | |||||
| ] = await Promise.all([ | ] = await Promise.all([ | ||||
| fetchBomCombo(), | 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; | JoSearchWrapper.Loading = GeneralLoading; | ||||
| @@ -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; | |||||
| @@ -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; | |||||
| @@ -457,7 +457,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | ||||
| // TODO: Implement QR code functionality | // TODO: Implement QR code functionality | ||||
| }; | }; | ||||
| const getPickOrderId = useCallback(() => { | |||||
| return filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | |||||
| }, [filterArgs?.pickOrderId]); | |||||
| // 修改:使用 Job Order API 获取数据 | // 修改:使用 Job Order API 获取数据 | ||||
| const fetchJobOrderData = useCallback(async (userId?: number) => { | const fetchJobOrderData = useCallback(async (userId?: number) => { | ||||
| setCombinedDataLoading(true); | setCombinedDataLoading(true); | ||||
| @@ -26,6 +26,7 @@ import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan"; | |||||
| import FInishedJobOrderRecord from "./FInishedJobOrderRecord"; | import FInishedJobOrderRecord from "./FInishedJobOrderRecord"; | ||||
| import JobPickExecution from "./JobPickExecution"; | import JobPickExecution from "./JobPickExecution"; | ||||
| import CompleteJobOrderRecord from "./completeJobOrderRecord"; | import CompleteJobOrderRecord from "./completeJobOrderRecord"; | ||||
| import JoPickOrderList from "./JoPickOrderList"; | |||||
| import { | import { | ||||
| fetchUnassignedJobOrderPickOrders, | fetchUnassignedJobOrderPickOrders, | ||||
| assignJobOrderPickOrder, | assignJobOrderPickOrder, | ||||
| @@ -35,6 +36,7 @@ import { | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { fetchPrinterCombo } from "@/app/api/settings/printer"; | import { fetchPrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import JoPickOrderDetail from "./JoPickOrderDetail"; | |||||
| interface Props { | interface Props { | ||||
| pickOrders: PickOrderResult[]; | pickOrders: PickOrderResult[]; | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| @@ -474,6 +476,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
| <Tab label={t("Pick Order Detail")} iconPosition="end" /> | <Tab label={t("Pick Order Detail")} iconPosition="end" /> | ||||
| <Tab label={t("Complete Job Order Record")} 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("Job Order Match")} iconPosition="end" /> */} | ||||
| {/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */} | {/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */} | ||||
| </Tabs> | </Tabs> | ||||
| @@ -486,6 +489,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| }}> | }}> | ||||
| {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} | {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} | ||||
| {tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />} | {tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />} | ||||
| {/* {tabIndex === 2 && <JoPickOrderList />} */} | |||||
| {/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */} | {/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */} | ||||
| {/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */} | {/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */} | ||||
| </Box> | </Box> | ||||
| @@ -196,11 +196,13 @@ const NavigationContent: React.FC = () => { | |||||
| label: "Detail Scheduling", | label: "Detail Scheduling", | ||||
| path: "/scheduling/detailed", | path: "/scheduling/detailed", | ||||
| }, | }, | ||||
| /* | |||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "Production", | label: "Production", | ||||
| path: "/production", | path: "/production", | ||||
| }, | }, | ||||
| */ | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -218,6 +220,11 @@ const NavigationContent: React.FC = () => { | |||||
| label: "Job Order Pickexcution", | label: "Job Order Pickexcution", | ||||
| path: "/jodetail", | path: "/jodetail", | ||||
| }, | }, | ||||
| { | |||||
| icon: <RequestQuote />, | |||||
| label: "Job Order Production Process", | |||||
| path: "/productionProcess", | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -303,7 +303,7 @@ const QrCodeModal: React.FC<{ | |||||
| {/* Manual Input with Submit-Triggered Helper Text */} | {/* Manual Input with Submit-Triggered Helper Text */} | ||||
| {false &&( | |||||
| {true &&( | |||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| <Typography variant="body2" gutterBottom> | <Typography variant="body2" gutterBottom> | ||||
| <strong>{t("Manual Input")}:</strong> | <strong>{t("Manual Input")}:</strong> | ||||
| @@ -588,7 +588,44 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| console.error("Error submitting pick execution form:", error); | console.error("Error submitting pick execution form:", error); | ||||
| } | } | ||||
| }, [onDataRefresh, onLotDataRefresh]); | }, [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 ( | return ( | ||||
| <> | <> | ||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| @@ -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; | |||||
| @@ -49,15 +49,17 @@ import { | |||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; | import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; | ||||
| import ProductionOutputFormPage from "./ProductionOutputFormPage"; | import ProductionOutputFormPage from "./ProductionOutputFormPage"; | ||||
| import ProcessSummaryHeader from "./ProcessSummaryHeader"; | |||||
| interface ProductProcessDetailProps { | interface ProductProcessDetailProps { | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| onBack: () => void; | onBack: () => void; | ||||
| fromJosave?: boolean; | |||||
| } | } | ||||
| const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | ||||
| jobOrderId, | jobOrderId, | ||||
| onBack, | onBack, | ||||
| fromJosave, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | 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 [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | ||||
| const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); | const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); | ||||
| const [scannedEquipmentId, setScannedEquipmentId] = 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 [scanningLineId, setScanningLineId] = useState<number | null>(null); | ||||
| const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null); | const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null); | ||||
| const [showScanDialog, setShowScanDialog] = useState(false); | const [showScanDialog, setShowScanDialog] = useState(false); | ||||
| @@ -146,6 +150,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| // 提交产出数据 | // 提交产出数据 | ||||
| /* | |||||
| const processQrCode = useCallback((qrValue: string, lineId: number) => { | const processQrCode = useCallback((qrValue: string, lineId: number) => { | ||||
| // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用) | // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用) | ||||
| if (qrValue.match(/\{2fitestu(\d+)\}/)) { | if (qrValue.match(/\{2fitestu(\d+)\}/)) { | ||||
| @@ -205,7 +210,92 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| // TODO: 处理普通文本格式 | // 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 码扫描效果 | // 处理 QR 码扫描效果 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (isManualScanning && qrValues.length > 0 && scanningLineId) { | if (isManualScanning && qrValues.length > 0 && scanningLineId) { | ||||
| @@ -219,74 +309,72 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| processQrCode(latestQr, scanningLineId); | processQrCode(latestQr, scanningLineId); | ||||
| } | } | ||||
| }, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]); | }, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]); | ||||
| const submitScanAndStart = useCallback(async (lineId: number) => { | const submitScanAndStart = useCallback(async (lineId: number) => { | ||||
| console.log("submitScanAndStart called with:", { | console.log("submitScanAndStart called with:", { | ||||
| lineId, | lineId, | ||||
| scannedOperatorId, | |||||
| scannedEquipmentId, | |||||
| scannedStaffNo, | |||||
| scannedEquipmentTypeSubTypeEquipmentNo, | |||||
| }); | }); | ||||
| if (!scannedOperatorId) { | |||||
| console.log("No operatorId, cannot submit"); | |||||
| if (!scannedStaffNo) { | |||||
| console.log("No staffNo, cannot submit"); | |||||
| setIsAutoSubmitting(false); | setIsAutoSubmitting(false); | ||||
| return false; // 没有 operatorId,不能提交 | |||||
| return false; // 没有 staffNo,不能提交 | |||||
| } | } | ||||
| try { | try { | ||||
| // 获取 line detail 以检查 bomProcessEquipmentId | // 获取 line detail 以检查 bomProcessEquipmentId | ||||
| const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); | const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); | ||||
| // 提交 operatorId 和 equipmentId | |||||
| // 提交 staffNo 和 equipmentTypeSubTypeEquipmentNo | |||||
| console.log("Submitting scan data:", { | console.log("Submitting scan data:", { | ||||
| productProcessLineId: lineId, | productProcessLineId: lineId, | ||||
| operatorId: scannedOperatorId, | |||||
| equipmentId: scannedEquipmentId || undefined, | |||||
| staffNo: scannedStaffNo, | |||||
| equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo, | |||||
| }); | }); | ||||
| const response = await updateProductProcessLineQrscan({ | const response = await updateProductProcessLineQrscan({ | ||||
| productProcessLineId: lineId, | productProcessLineId: lineId, | ||||
| operatorId: scannedOperatorId, | |||||
| equipmentId: scannedEquipmentId || undefined, | |||||
| equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo || undefined, | |||||
| staffNo: scannedStaffNo || undefined, | |||||
| }); | }); | ||||
| console.log("Scan submit response:", response); | console.log("Scan submit response:", response); | ||||
| // 检查响应中的 message 字段来判断是否成功 | // 检查响应中的 message 字段来判断是否成功 | ||||
| // 如果后端返回 message 不为 null,说明验证失败 | |||||
| if (response && response.message) { | if (response && response.message) { | ||||
| setIsAutoSubmitting(false); | setIsAutoSubmitting(false); | ||||
| // 清除定时器 | |||||
| if (autoSubmitTimerRef.current) { | if (autoSubmitTimerRef.current) { | ||||
| clearTimeout(autoSubmitTimerRef.current); | clearTimeout(autoSubmitTimerRef.current); | ||||
| autoSubmitTimerRef.current = null; | 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; | 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) => { | const handleSubmitScanAndStart = useCallback(async (lineId: number) => { | ||||
| console.log("handleSubmitScanAndStart called with lineId:", lineId); | console.log("handleSubmitScanAndStart called with lineId:", lineId); | ||||
| if (!scannedOperatorId) { | |||||
| if (!scannedStaffNo) { | |||||
| //alert(t("Please scan operator code first")); | //alert(t("Please scan operator code first")); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -316,8 +404,11 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| setLineDetailForScan(null); | setLineDetailForScan(null); | ||||
| // 获取 line detail 以获取 bomProcessEquipmentId | // 获取 line detail 以获取 bomProcessEquipmentId | ||||
| fetchProductProcessLineDetail(lineId) | 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(); | ||||
| }, [startScan]); | }, [startScan]); | ||||
| @@ -351,16 +442,16 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("Auto-submit check:", { | console.log("Auto-submit check:", { | ||||
| scanningLineId, | scanningLineId, | ||||
| scannedOperatorId, | |||||
| scannedEquipmentId, | |||||
| scannedStaffNo, | |||||
| scannedEquipmentTypeSubTypeEquipmentNo, | |||||
| isAutoSubmitting, | isAutoSubmitting, | ||||
| isManualScanning, | isManualScanning, | ||||
| }); | }); | ||||
| if ( | if ( | ||||
| scanningLineId && | scanningLineId && | ||||
| scannedOperatorId !== null && | |||||
| scannedEquipmentId !== null && | |||||
| scannedStaffNo !== null && | |||||
| scannedEquipmentTypeSubTypeEquipmentNo !== null && | |||||
| !isAutoSubmitting && | !isAutoSubmitting && | ||||
| isManualScanning | isManualScanning | ||||
| ) { | ) { | ||||
| @@ -385,7 +476,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| // 注意:这里不立即清除定时器,因为我们需要它执行 | // 注意:这里不立即清除定时器,因为我们需要它执行 | ||||
| // 只在组件卸载时清除 | // 只在组件卸载时清除 | ||||
| }; | }; | ||||
| }, [scanningLineId, scannedOperatorId, scannedEquipmentId, isAutoSubmitting, isManualScanning, submitScanAndStart]); | |||||
| }, [scanningLineId, scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, isAutoSubmitting, isManualScanning, submitScanAndStart]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| return () => { | return () => { | ||||
| if (autoSubmitTimerRef.current) { | if (autoSubmitTimerRef.current) { | ||||
| @@ -477,7 +568,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| <Typography variant="h6" gutterBottom fontWeight="bold"> | <Typography variant="h6" gutterBottom fontWeight="bold"> | ||||
| {t("Production Process Steps")} | {t("Production Process Steps")} | ||||
| </Typography> | </Typography> | ||||
| <ProcessSummaryHeader t={t} processData={processData} /> | |||||
| {!isExecutingLine ? ( | {!isExecutingLine ? ( | ||||
| /* ========== 步骤列表视图 ========== */ | /* ========== 步骤列表视图 ========== */ | ||||
| <TableContainer> | <TableContainer> | ||||
| @@ -509,7 +600,8 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| </Box> | </Box> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="center">{t("Status")}</TableCell> | <TableCell align="center">{t("Status")}</TableCell> | ||||
| <TableCell align="center">{t("Action")}</TableCell> | |||||
| {!fromJosave&&(<TableCell align="center">{t("Action")}</TableCell>)} | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| @@ -529,7 +621,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| <Typography fontWeight={500}>{line.name}</Typography> | <Typography fontWeight={500}>{line.name}</Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell><Typography fontWeight={500}>{line.description || "-"}</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.operatorName}</Typography></TableCell> | ||||
| {/* | {/* | ||||
| <TableCell><Typography fontWeight={500}>{line.durationInMinutes} </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" /> | <Chip label={t("Unknown")} color="error" size="small" /> | ||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| {!fromJosave&&( | |||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| {statusLower === 'pending' ? ( | {statusLower === 'pending' ? ( | ||||
| <Button | <Button | ||||
| @@ -598,6 +691,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| )} | |||||
| </TableRow> | </TableRow> | ||||
| ); | ); | ||||
| })} | })} | ||||
| @@ -635,17 +729,17 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| <Stack spacing={2} sx={{ mt: 2 }}> | <Stack spacing={2} sx={{ mt: 2 }}> | ||||
| <Box> | <Box> | ||||
| <Typography variant="body2" color="text.secondary"> | <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> | </Typography> | ||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Typography variant="body2" color="text.secondary"> | <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)") | : t("Please scan equipment code (optional if not required)") | ||||
| } | } | ||||
| </Typography> | </Typography> | ||||
| @@ -672,7 +766,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)} | onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)} | ||||
| disabled={!scannedOperatorId} | |||||
| disabled={!scannedStaffNo} | |||||
| > | > | ||||
| {t("Submit & Start")} | {t("Submit & Start")} | ||||
| </Button> | </Button> | ||||
| @@ -17,7 +17,7 @@ import { | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import { useTranslation } from "react-i18next"; | 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 ProductionProcessDetail from "./ProductionProcessDetail"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; | 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 { InventoryResult } from "@/app/api/inventory"; | ||||
| import { releaseJo, startJo } from "@/app/api/jo/actions"; | import { releaseJo, startJo } from "@/app/api/jo/actions"; | ||||
| import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | ||||
| import ProcessSummaryHeader from "./ProcessSummaryHeader"; | |||||
| interface JobOrderLine { | interface JobOrderLine { | ||||
| id: number; | id: number; | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| @@ -127,7 +128,12 @@ const stockCounts = useMemo(() => { | |||||
| }; | }; | ||||
| }, [jobOrderLines, inventoryData]); | }, [jobOrderLines, inventoryData]); | ||||
| const status = processData?.status?.toLowerCase?.() ?? ""; | 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) => { | const handleRelease = useCallback(async ( jobOrderId: number) => { | ||||
| // TODO: 替换为实际的 release 调用 | // TODO: 替换为实际的 release 调用 | ||||
| console.log("Release clicked for jobOrderId:", jobOrderId); | console.log("Release clicked for jobOrderId:", jobOrderId); | ||||
| @@ -256,6 +262,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| align: "left", | align: "left", | ||||
| headerAlign: "center", | headerAlign: "center", | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params) => { | |||||
| return <Typography sx={{ fontSize: "14px" }}>{params.value}</Typography>; | |||||
| }, | |||||
| }, | }, | ||||
| { | { | ||||
| field: "description", | field: "description", | ||||
| @@ -263,6 +272,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| flex: 1, | flex: 1, | ||||
| align: "left", | align: "left", | ||||
| headerAlign: "center", | headerAlign: "center", | ||||
| renderCell: (params) => { | |||||
| return <Typography sx={{ fontSize: "14px" }}>{params.value || ""}</Typography>; | |||||
| }, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| const productionProcessesLineRemarkTableRows = | const productionProcessesLineRemarkTableRows = | ||||
| @@ -270,6 +282,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| id: line.seqNo, | id: line.seqNo, | ||||
| seqNo: line.seqNo, | seqNo: line.seqNo, | ||||
| description: line.description ?? "", | description: line.description ?? "", | ||||
| })) ?? []; | })) ?? []; | ||||
| @@ -356,11 +369,13 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| const pickTableRows = jobOrderLines.map((line, index) => ({ | const pickTableRows = jobOrderLines.map((line, index) => ({ | ||||
| ...line, | ...line, | ||||
| id: line.id || index, | |||||
| //id: line.id || index, | |||||
| id: index + 1, | |||||
| })); | })); | ||||
| const PickTableContent = () => ( | const PickTableContent = () => ( | ||||
| <Box sx={{ mt: 2 }}> | <Box sx={{ mt: 2 }}> | ||||
| <ProcessSummaryHeader processData={processData} /> | |||||
| <Card sx={{ mb: 2 }}> | <Card sx={{ mb: 2 }}> | ||||
| <CardContent> | <CardContent> | ||||
| <Stack | <Stack | ||||
| @@ -380,7 +395,17 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | ||||
| {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong> | {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong> | ||||
| </Typography> | </Typography> | ||||
| {fromJosave && ( | |||||
| {fromJosave && ( | |||||
| <Button | |||||
| variant="contained" | |||||
| color="error" | |||||
| onClick={() => handleDeleteJobOrder(jobOrderId)} | |||||
| disabled={processData?.jobOrderStatus !== "planning"} | |||||
| > | |||||
| {t("Delete Job Order")} | |||||
| </Button> | |||||
| )} | |||||
| {fromJosave && ( | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| color="primary" | color="primary" | ||||
| @@ -406,6 +431,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| ); | ); | ||||
| const ProductionProcessesLineRemarkTableContent = () => ( | const ProductionProcessesLineRemarkTableContent = () => ( | ||||
| <Box sx={{ mt: 2 }}> | <Box sx={{ mt: 2 }}> | ||||
| <ProcessSummaryHeader processData={processData} /> | |||||
| <StyledDataGrid | <StyledDataGrid | ||||
| sx={{ | sx={{ | ||||
| "--DataGrid-overlayHeight": "100px", | "--DataGrid-overlayHeight": "100px", | ||||
| @@ -414,6 +440,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| rows={productionProcessesLineRemarkTableRows ?? []} | rows={productionProcessesLineRemarkTableRows ?? []} | ||||
| columns={productionProcessesLineRemarkTableColumns} | columns={productionProcessesLineRemarkTableColumns} | ||||
| getRowHeight={() => 'auto'} | getRowHeight={() => 'auto'} | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -427,7 +454,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| {/* 标签页 */} | {/* 标签页 */} | ||||
| <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
| @@ -455,7 +482,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| onBack={() => { | onBack={() => { | ||||
| // 切换回第一个标签页,或者什么都不做 | // 切换回第一个标签页,或者什么都不做 | ||||
| setTabIndex(0); | setTabIndex(0); | ||||
| }} | }} | ||||
| fromJosave={fromJosave} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | ||||
| @@ -166,21 +166,41 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
| if (qrCodeScannerValues.length > 0) { | if (qrCodeScannerValues.length > 0) { | ||||
| const scannedValues = qrCodeScannerValues[0]; | const scannedValues = qrCodeScannerValues[0]; | ||||
| console.log("%c Scanned Result: ", "color:cyan", scannedValues); | console.log("%c Scanned Result: ", "color:cyan", scannedValues); | ||||
| if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING | 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 { | try { | ||||
| const data: QrCodeInfo = JSON.parse(scannedValues); | const data: QrCodeInfo = JSON.parse(scannedValues); | ||||
| console.log("%c Parsed scan data", "color:green", data); | 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); | const content = scannedValues.substring(1, scannedValues.length - 1); | ||||
| data.value = content; | data.value = content; | ||||
| setScanResult(data); | 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; | const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0; | ||||
| if (silId == 0) { | if (silId == 0) { | ||||
| const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0; | const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0; | ||||
| setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()}); | setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()}); | ||||
| } else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); } | } else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); } | ||||
| resetQrCodeScanner(String(error)); | resetQrCodeScanner(String(error)); | ||||
| } | } | ||||
| // resetQrCodeScanner(); | // resetQrCodeScanner(); | ||||
| } | } | ||||
| }, [qrCodeScannerValues]); | }, [qrCodeScannerValues]); | ||||
| @@ -2,7 +2,8 @@ | |||||
| "dashboard": "資訊展示面板", | "dashboard": "資訊展示面板", | ||||
| "Edit": "編輯", | "Edit": "編輯", | ||||
| "Job Order Production Process": "工單生產流程", | |||||
| "productionProcess": "生產流程", | |||||
| "Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
| "All": "全部", | "All": "全部", | ||||
| "No options": "沒有選項", | "No options": "沒有選項", | ||||
| @@ -12,11 +13,12 @@ | |||||
| "code": "編號", | "code": "編號", | ||||
| "Name": "名稱", | "Name": "名稱", | ||||
| "Type": "類型", | "Type": "類型", | ||||
| "WIP": "半成品", | "WIP": "半成品", | ||||
| "R&D": "研發", | "R&D": "研發", | ||||
| "STF": "樣品", | "STF": "樣品", | ||||
| "Other": "其他", | "Other": "其他", | ||||
| "Add some entries!": "添加條目", | "Add some entries!": "添加條目", | ||||
| "Add Record": "新增", | "Add Record": "新增", | ||||
| "Clean Record": "重置", | "Clean Record": "重置", | ||||
| @@ -54,12 +56,15 @@ | |||||
| "sfg": "半成品", | "sfg": "半成品", | ||||
| "item": "貨品", | "item": "貨品", | ||||
| "FG":"成品", | "FG":"成品", | ||||
| "Qty":"數量", | |||||
| "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", | "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", | ||||
| "View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌", | "View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌", | ||||
| "Delivery Order":"送貨訂單", | "Delivery Order":"送貨訂單", | ||||
| "Detail Scheduling":"詳細排程", | "Detail Scheduling":"詳細排程", | ||||
| "Customer":"客戶", | "Customer":"客戶", | ||||
| "qcItem":"品檢項目", | "qcItem":"品檢項目", | ||||
| "Item":"物料", | |||||
| "Production Date":"生產日期", | |||||
| "QC Check Item":"QC品檢項目", | "QC Check Item":"QC品檢項目", | ||||
| "QC Category":"QC品檢模板", | "QC Category":"QC品檢模板", | ||||
| "qcCategory":"品檢模板", | "qcCategory":"品檢模板", | ||||
| @@ -12,6 +12,7 @@ | |||||
| "UoM": "銷售單位", | "UoM": "銷售單位", | ||||
| "Status": "工單狀態", | "Status": "工單狀態", | ||||
| "Lot No.": "批號", | "Lot No.": "批號", | ||||
| "Delete Job Order": "刪除工單", | |||||
| "Bom": "半成品/成品編號", | "Bom": "半成品/成品編號", | ||||
| "Release": "放單", | "Release": "放單", | ||||
| "Pending": "待掃碼", | "Pending": "待掃碼", | ||||
| @@ -276,10 +277,11 @@ | |||||
| "success": "成功", | "success": "成功", | ||||
| "Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量", | "Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量", | ||||
| "BOM Status": "材料預備狀況", | "BOM Status": "材料預備狀況", | ||||
| "Estimated Production Date": "預計生產日期及時間", | |||||
| "Estimated Production Date": "預計生產日期", | |||||
| "Plan Start": "預計生產日期", | "Plan Start": "預計生產日期", | ||||
| "Plan Start From": "預計生產日期及時間", | |||||
| "Plan Start To": "預計生產日期及時間至", | |||||
| "Plan Start From": "預計生產日期", | |||||
| "Delivery Note Code": "送貨單編號", | |||||
| "Plan Start To": "預計生產日期至", | |||||
| "By-product": "副產品", | "By-product": "副產品", | ||||
| "Complete Step": "完成步驟", | "Complete Step": "完成步驟", | ||||
| "Defect": "缺陷", | "Defect": "缺陷", | ||||
| @@ -329,7 +331,8 @@ | |||||
| "Total Steps": "總步驟數", | "Total Steps": "總步驟數", | ||||
| "Unknown": "", | "Unknown": "", | ||||
| "Job Type": "工單類型", | "Job Type": "工單類型", | ||||
| "Production Date":"生產日期", | |||||
| "Jo Pick Order Detail":"工單提料詳情", | |||||
| "WIP": "半成品", | "WIP": "半成品", | ||||
| "R&D": "研發", | "R&D": "研發", | ||||
| "STF": "員工餐", | "STF": "員工餐", | ||||
| @@ -204,6 +204,7 @@ | |||||
| "Report and Pick another lot": "上報並需重新選擇批號", | "Report and Pick another lot": "上報並需重新選擇批號", | ||||
| "Accept Stock Out": "接受出庫", | "Accept Stock Out": "接受出庫", | ||||
| "Pick Another Lot": "欠數,並重新選擇批號", | "Pick Another Lot": "欠數,並重新選擇批號", | ||||
| "Delivery Note Code": "送貨單編號", | |||||
| "Lot No": "批號", | "Lot No": "批號", | ||||
| "Expiry Date": "到期日", | "Expiry Date": "到期日", | ||||
| "Location": "位置", | "Location": "位置", | ||||