| @@ -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; | |||
| value: number; | |||
| label: string; | |||
| outputQty: number; | |||
| } | |||
| export const preloadBomCombo = (() => { | |||
| @@ -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), | |||
| } | |||
| ); | |||
| }); | |||
| @@ -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; | |||
| @@ -452,6 +452,10 @@ export interface LaneBtn { | |||
| unassigned: number; | |||
| total: number; | |||
| } | |||
| export const fetchDoPickOrderDetail = async ( | |||
| doPickOrderId: number, | |||
| selectedPickOrderId?: number | |||
| @@ -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" | |||
| @@ -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}> | |||
| @@ -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"); | |||
| }, []) | |||
| @@ -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; | |||
| @@ -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}`); | |||
| // 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); | |||
| @@ -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> | |||
| @@ -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", | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -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}> | |||
| @@ -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 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> | |||
| @@ -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 />} | |||
| @@ -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]); | |||
| @@ -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":"品檢模板", | |||
| @@ -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": "員工餐", | |||
| @@ -204,6 +204,7 @@ | |||
| "Report and Pick another lot": "上報並需重新選擇批號", | |||
| "Accept Stock Out": "接受出庫", | |||
| "Pick Another Lot": "欠數,並重新選擇批號", | |||
| "Delivery Note Code": "送貨單編號", | |||
| "Lot No": "批號", | |||
| "Expiry Date": "到期日", | |||
| "Location": "位置", | |||