# Conflicts: # src/components/JoSave/JoSave.tsx # src/i18n/zh/jo.json # src/i18n/zh/pickOrder.jsonmaster
| @@ -0,0 +1,19 @@ | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import { Stack, Typography, Link } from "@mui/material"; | |||||
| import NextLink from "next/link"; | |||||
| export default async function NotFound() { | |||||
| const { t } = await getServerI18n("schedule", "common"); | |||||
| return ( | |||||
| <Stack spacing={2}> | |||||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||||
| <Typography variant="body1"> | |||||
| {t("The job order page was not found!")} | |||||
| </Typography> | |||||
| <Link href="/settings/scheduling" component={NextLink} variant="body2"> | |||||
| {t("Return to all job orders")} | |||||
| </Link> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,49 @@ | |||||
| import { fetchJoDetail } from "@/app/api/jo"; | |||||
| import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
| import JoSave from "@/components/JoSave/JoSave"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { Typography } from "@mui/material"; | |||||
| import { isArray } from "lodash"; | |||||
| import { Metadata } from "next"; | |||||
| import { notFound } from "next/navigation"; | |||||
| import { Suspense } from "react"; | |||||
| import GeneralLoading from "@/components/General/GeneralLoading"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Edit Job Order Detail" | |||||
| } | |||||
| type Props = SearchParams; | |||||
| const JoEdit: React.FC<Props> = async ({ searchParams }) => { | |||||
| const { t } = await getServerI18n("jo"); | |||||
| const id = searchParams["id"]; | |||||
| if (!id || isArray(id) || !isFinite(parseInt(id))) { | |||||
| notFound(); | |||||
| } | |||||
| try { | |||||
| await fetchJoDetail(parseInt(id)) | |||||
| } catch (e) { | |||||
| if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { | |||||
| console.log(e) | |||||
| notFound(); | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Edit Job Order Detail")} | |||||
| </Typography> | |||||
| <I18nProvider namespaces={["jo", "common"]}> | |||||
| <Suspense fallback={<GeneralLoading />}> | |||||
| <JoSave id={parseInt(id)} defaultValues={undefined} /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default JoEdit; | |||||
| @@ -0,0 +1,39 @@ | |||||
| import { preloadBomCombo } from "@/app/api/bom"; | |||||
| import JodetailSearch from "@/components/Jodetail/JodetailSearch"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { Stack, Typography } from "@mui/material"; | |||||
| import { Metadata } from "next"; | |||||
| import React, { Suspense } from "react"; | |||||
| import GeneralLoading from "@/components/General/GeneralLoading"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Job Order Pickexcution" | |||||
| } | |||||
| const jo: React.FC = async () => { | |||||
| const { t } = await getServerI18n("jo"); | |||||
| preloadBomCombo() | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Job Order Pickexcution")} | |||||
| </Typography> | |||||
| </Stack> | |||||
| <I18nProvider namespaces={["jo", "common"]}> | |||||
| <Suspense fallback={<GeneralLoading />}> | |||||
| <JodetailSearch pickOrders={[]} /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default jo; | |||||
| @@ -38,15 +38,17 @@ export interface SearchJoResult { | |||||
| status: JoStatus; | status: JoStatus; | ||||
| } | } | ||||
| export interface ReleaseJoRequest { | |||||
| // For Jo Button Actions | |||||
| export interface CommonActionJoRequest { | |||||
| id: number; | id: number; | ||||
| } | } | ||||
| export interface ReleaseJoResponse { | |||||
| export interface CommonActionJoResponse { | |||||
| id: number; | id: number; | ||||
| entity: { status: JoStatus } | entity: { status: JoStatus } | ||||
| } | } | ||||
| // For Jo Process | |||||
| export interface IsOperatorExistResponse<T> { | export interface IsOperatorExistResponse<T> { | ||||
| id: number | null; | id: number | null; | ||||
| name: string; | name: string; | ||||
| @@ -76,7 +78,129 @@ export interface JobOrderDetail { | |||||
| pickLines: any[]; | pickLines: any[]; | ||||
| status: string; | status: string; | ||||
| } | } | ||||
| export interface UnassignedJobOrderPickOrder { | |||||
| pickOrderId: number; | |||||
| pickOrderCode: string; | |||||
| pickOrderConsoCode: string; | |||||
| pickOrderTargetDate: string; | |||||
| pickOrderStatus: string; | |||||
| jobOrderId: number; | |||||
| jobOrderCode: string; | |||||
| jobOrderName: string; | |||||
| reqQty: number; | |||||
| uom: string; | |||||
| planStart: string; | |||||
| planEnd: string; | |||||
| } | |||||
| export interface AssignJobOrderResponse { | |||||
| id: number | null; | |||||
| code: string | null; | |||||
| name: string | null; | |||||
| type: string | null; | |||||
| message: string | null; | |||||
| errorPosition: string | null; | |||||
| } | |||||
| export const recordSecondScanIssue = cache(async ( | |||||
| pickOrderId: number, | |||||
| itemId: number, | |||||
| data: { | |||||
| qty: number; | |||||
| isMissing: boolean; | |||||
| isBad: boolean; | |||||
| reason: string; | |||||
| createdBy: number; | |||||
| } | |||||
| ) => { | |||||
| return serverFetchJson<any>( | |||||
| `${BASE_API_URL}/jo/second-scan-issue/${pickOrderId}/${itemId}`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify(data), | |||||
| next: { tags: ["jo-second-scan"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number) => { | |||||
| return serverFetchJson<any>( | |||||
| `${BASE_API_URL}/jo/second-scan-qr/${pickOrderId}/${itemId}`, | |||||
| { | |||||
| method: "POST", | |||||
| next: { tags: ["jo-second-scan"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| export const submitSecondScanQuantity = cache(async ( | |||||
| pickOrderId: number, | |||||
| itemId: number, | |||||
| data: { qty: number; isMissing?: boolean; isBad?: boolean; reason?: string } | |||||
| ) => { | |||||
| return serverFetchJson<any>( | |||||
| `${BASE_API_URL}/jo/second-scan-submit/${pickOrderId}/${itemId}`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify(data), | |||||
| next: { tags: ["jo-second-scan"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| // 获取未分配的 Job Order pick orders | |||||
| export const fetchUnassignedJobOrderPickOrders = cache(async () => { | |||||
| return serverFetchJson<UnassignedJobOrderPickOrder[]>( | |||||
| `${BASE_API_URL}/jo/unassigned-job-order-pick-orders`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["jo-unassigned"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| // 分配 Job Order pick order 给用户 | |||||
| export const assignJobOrderPickOrder = async (pickOrderId: number, userId: number) => { | |||||
| return serverFetchJson<AssignJobOrderResponse>( | |||||
| `${BASE_API_URL}/jo/assign-job-order-pick-order/${pickOrderId}/${userId}`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| } | |||||
| ); | |||||
| }; | |||||
| // 获取 Job Order 分层数据 | |||||
| export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | |||||
| return serverFetchJson<any>( | |||||
| `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["jo-hierarchical"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| // 获取已完成的 Job Order pick orders | |||||
| export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => { | |||||
| return serverFetchJson<any>( | |||||
| `${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["jo-completed"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| // 获取已完成的 Job Order pick order records | |||||
| export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { | |||||
| return serverFetchJson<any[]>( | |||||
| `${BASE_API_URL}/jo/completed-job-order-pick-order-records/${userId}`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["jo-records"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| export const fetchJobOrderDetailByCode = cache(async (code: string) => { | export const fetchJobOrderDetailByCode = cache(async (code: string) => { | ||||
| return serverFetchJson<JobOrderDetail>( | return serverFetchJson<JobOrderDetail>( | ||||
| `${BASE_API_URL}/jo/detailByCode/${code}`, | `${BASE_API_URL}/jo/detailByCode/${code}`, | ||||
| @@ -129,8 +253,17 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => { | |||||
| return response | return response | ||||
| }) | }) | ||||
| export const releaseJo = cache(async (data: ReleaseJoRequest) => { | |||||
| return serverFetchJson<ReleaseJoResponse>(`${BASE_API_URL}/jo/release`, | |||||
| export const releaseJo = cache(async (data: CommonActionJoRequest) => { | |||||
| return serverFetchJson<CommonActionJoResponse>(`${BASE_API_URL}/jo/release`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }) | |||||
| }) | |||||
| export const startJo = cache(async (data: CommonActionJoRequest) => { | |||||
| return serverFetchJson<CommonActionJoResponse>(`${BASE_API_URL}/jo/start`, | |||||
| { | { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| @@ -139,10 +272,22 @@ export const releaseJo = cache(async (data: ReleaseJoRequest) => { | |||||
| }) | }) | ||||
| export const manualCreateJo = cache(async (data: SaveJo) => { | export const manualCreateJo = cache(async (data: SaveJo) => { | ||||
| console.log(data) | |||||
| return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/manualCreate`, { | return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/manualCreate`, { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| headers: { "Content-Type": "application/json" } | headers: { "Content-Type": "application/json" } | ||||
| }) | }) | ||||
| }) | |||||
| export const fetchCompletedJobOrderPickOrdersWithCompletedSecondScan = cache(async (userId: number) => { | |||||
| return serverFetchJson<any[]>(`${BASE_API_URL}/jo/completed-job-order-pick-orders-with-completed-second-scan/${userId}`, { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json" } | |||||
| }) | |||||
| }) | |||||
| export const fetchCompletedJobOrderPickOrderLotDetails = cache(async (pickOrderId: number) => { | |||||
| return serverFetchJson<any[]>(`${BASE_API_URL}/jo/completed-job-order-pick-order-lot-details/${pickOrderId}`, { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json" } | |||||
| }) | |||||
| }) | }) | ||||
| @@ -690,8 +690,8 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| }}> | }}> | ||||
| <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("Pick Execution Detail")} iconPosition="end" /> | |||||
| <Tab label={t("Pick Execution Record")} iconPosition="end" /> | |||||
| <Tab label={t("Finished Good Detail")} iconPosition="end" /> | |||||
| <Tab label={t("Finished Good Record")} iconPosition="end" /> | |||||
| </Tabs> | </Tabs> | ||||
| </Box> | </Box> | ||||
| @@ -20,7 +20,7 @@ import { useCallback, useEffect, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions"; | import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions"; | ||||
| import { fetchEscalationCombo } from "@/app/api/user/actions"; | import { fetchEscalationCombo } from "@/app/api/user/actions"; | ||||
| import { useRef } from "react"; | |||||
| interface LotPickData { | interface LotPickData { | ||||
| id: number; | id: number; | ||||
| lotId: number; | lotId: number; | ||||
| @@ -81,7 +81,6 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| const [errors, setErrors] = useState<FormErrors>({}); | const [errors, setErrors] = useState<FormErrors>({}); | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | ||||
| // 计算剩余可用数量 | // 计算剩余可用数量 | ||||
| const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | ||||
| const remainingQty = lot.inQty - lot.outQty; | const remainingQty = lot.inQty - lot.outQty; | ||||
| @@ -92,7 +91,18 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| // The actualPickQty in the form should be independent of the database value | // The actualPickQty in the form should be independent of the database value | ||||
| return lot.requiredQty || 0; | return lot.requiredQty || 0; | ||||
| }, []); | }, []); | ||||
| const remaining = selectedLot ? calculateRemainingAvailableQty(selectedLot) : 0; | |||||
| const req = selectedLot ? calculateRequiredQty(selectedLot) : 0; | |||||
| const ap = Number(formData.actualPickQty) || 0; | |||||
| const miss = Number(formData.missQty) || 0; | |||||
| const bad = Number(formData.badItemQty) || 0; | |||||
| // Max the user can type | |||||
| const maxPick = Math.min(remaining, req); | |||||
| const maxIssueTotal = Math.max(0, req - ap); // remaining room for miss+bad | |||||
| const clamp0 = (v: any) => Math.max(0, Number(v) || 0); | |||||
| // 获取处理人员列表 | // 获取处理人员列表 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| const fetchHandlers = async () => { | const fetchHandlers = async () => { | ||||
| @@ -107,55 +117,49 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| fetchHandlers(); | fetchHandlers(); | ||||
| }, []); | }, []); | ||||
| // 初始化表单数据 - 每次打开时都重新初始化 | |||||
| const initKeyRef = useRef<string | null>(null); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (open && selectedLot && selectedPickOrderLine && pickOrderId) { | |||||
| const getSafeDate = (dateValue: any): string => { | |||||
| if (!dateValue) return new Date().toISOString().split('T')[0]; | |||||
| try { | |||||
| const date = new Date(dateValue); | |||||
| if (isNaN(date.getTime())) { | |||||
| return new Date().toISOString().split('T')[0]; | |||||
| } | |||||
| return date.toISOString().split('T')[0]; | |||||
| } catch { | |||||
| return new Date().toISOString().split('T')[0]; | |||||
| } | |||||
| }; | |||||
| if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return; | |||||
| // 计算剩余可用数量 | |||||
| const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); | |||||
| const requiredQty = calculateRequiredQty(selectedLot); | |||||
| console.log("=== PickExecutionForm Debug ==="); | |||||
| console.log("selectedLot:", selectedLot); | |||||
| console.log("inQty:", selectedLot.inQty); | |||||
| console.log("outQty:", selectedLot.outQty); | |||||
| console.log("holdQty:", selectedLot.holdQty); | |||||
| console.log("availableQty:", selectedLot.availableQty); | |||||
| console.log("calculated remainingAvailableQty:", remainingAvailableQty); | |||||
| console.log("=== End Debug ==="); | |||||
| setFormData({ | |||||
| pickOrderId: pickOrderId, | |||||
| pickOrderCode: selectedPickOrderLine.pickOrderCode, | |||||
| pickOrderCreateDate: getSafeDate(pickOrderCreateDate), | |||||
| pickExecutionDate: new Date().toISOString().split('T')[0], | |||||
| pickOrderLineId: selectedPickOrderLine.id, | |||||
| itemId: selectedPickOrderLine.itemId, | |||||
| itemCode: selectedPickOrderLine.itemCode, | |||||
| itemDescription: selectedPickOrderLine.itemName, | |||||
| lotId: selectedLot.lotId, | |||||
| lotNo: selectedLot.lotNo, | |||||
| storeLocation: selectedLot.location, | |||||
| requiredQty: selectedLot.requiredQty, | |||||
| actualPickQty: selectedLot.actualPickQty || 0, | |||||
| missQty: 0, | |||||
| badItemQty: 0, // 初始化为 0,用户需要手动输入 | |||||
| issueRemark: '', | |||||
| pickerName: '', | |||||
| handledBy: undefined, | |||||
| }); | |||||
| } | |||||
| }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); | |||||
| // Only initialize once per (pickOrderLineId + lotId) while dialog open | |||||
| const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`; | |||||
| if (initKeyRef.current === key) return; | |||||
| const getSafeDate = (dateValue: any): string => { | |||||
| if (!dateValue) return new Date().toISOString().split('T')[0]; | |||||
| try { | |||||
| const d = new Date(dateValue); | |||||
| return isNaN(d.getTime()) ? new Date().toISOString().split('T')[0] : d.toISOString().split('T')[0]; | |||||
| } catch { | |||||
| return new Date().toISOString().split('T')[0]; | |||||
| } | |||||
| }; | |||||
| setFormData({ | |||||
| pickOrderId: pickOrderId, | |||||
| pickOrderCode: selectedPickOrderLine.pickOrderCode, | |||||
| pickOrderCreateDate: getSafeDate(pickOrderCreateDate), | |||||
| pickExecutionDate: new Date().toISOString().split('T')[0], | |||||
| pickOrderLineId: selectedPickOrderLine.id, | |||||
| itemId: selectedPickOrderLine.itemId, | |||||
| itemCode: selectedPickOrderLine.itemCode, | |||||
| itemDescription: selectedPickOrderLine.itemName, | |||||
| lotId: selectedLot.lotId, | |||||
| lotNo: selectedLot.lotNo, | |||||
| storeLocation: selectedLot.location, | |||||
| requiredQty: selectedLot.requiredQty, | |||||
| actualPickQty: selectedLot.actualPickQty || 0, | |||||
| missQty: 0, | |||||
| badItemQty: 0, | |||||
| issueRemark: '', | |||||
| pickerName: '', | |||||
| handledBy: undefined, | |||||
| }); | |||||
| initKeyRef.current = key; | |||||
| }, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]); | |||||
| // Mutually exclusive inputs: picking vs reporting issues | |||||
| const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { | const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { | ||||
| setFormData(prev => ({ ...prev, [field]: value })); | setFormData(prev => ({ ...prev, [field]: value })); | ||||
| @@ -168,30 +172,23 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 | // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 | ||||
| const validateForm = (): boolean => { | const validateForm = (): boolean => { | ||||
| const newErrors: FormErrors = {}; | const newErrors: FormErrors = {}; | ||||
| if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { | |||||
| newErrors.actualPickQty = t('Qty is required'); | |||||
| } | |||||
| // ✅ FIXED: Check if actual pick qty exceeds remaining available qty | |||||
| if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) { | |||||
| newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty'); | |||||
| } | |||||
| // ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty) | |||||
| if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) { | |||||
| newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); | |||||
| const req = selectedLot?.requiredQty || 0; | |||||
| const ap = formData.actualPickQty || 0; | |||||
| const miss = formData.missQty || 0; | |||||
| const bad = formData.badItemQty || 0; | |||||
| if (ap < 0) newErrors.actualPickQty = t('Qty is required'); | |||||
| if (ap > Math.min(remainingAvailableQty, req)) newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty'); | |||||
| if (miss < 0) newErrors.missQty = t('Invalid qty'); | |||||
| if (bad < 0) newErrors.badItemQty = t('Invalid qty'); | |||||
| if (ap + miss + bad > req) { | |||||
| newErrors.actualPickQty = t('Total exceeds required qty'); | |||||
| newErrors.missQty = t('Total exceeds required qty'); | |||||
| } | } | ||||
| // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) | |||||
| const hasMissQty = formData.missQty && formData.missQty > 0; | |||||
| const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; | |||||
| if (!hasMissQty && !hasBadItemQty) { | |||||
| newErrors.missQty = t('At least one issue must be reported'); | |||||
| newErrors.badItemQty = t('At least one issue must be reported'); | |||||
| if (ap === 0 && miss === 0 && bad === 0) { | |||||
| newErrors.actualPickQty = t('Enter pick qty or issue qty'); | |||||
| newErrors.missQty = t('Enter pick qty or issue qty'); | |||||
| } | } | ||||
| setErrors(newErrors); | setErrors(newErrors); | ||||
| return Object.keys(newErrors).length === 0; | return Object.keys(newErrors).length === 0; | ||||
| }; | }; | ||||
| @@ -266,42 +263,42 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Actual Pick Qty')} | |||||
| type="number" | |||||
| value={formData.actualPickQty || 0} | |||||
| onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} | |||||
| error={!!errors.actualPickQty} | |||||
| helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} | |||||
| variant="outlined" | |||||
| /> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Actual Pick Qty')} | |||||
| type="number" | |||||
| value={formData.actualPickQty ?? ''} | |||||
| onChange={(e) => handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} | |||||
| error={!!errors.actualPickQty} | |||||
| helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Missing item Qty')} | |||||
| type="number" | |||||
| value={formData.missQty || 0} | |||||
| onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)} | |||||
| error={!!errors.missQty} | |||||
| // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} | |||||
| variant="outlined" | |||||
| /> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Missing item Qty')} | |||||
| type="number" | |||||
| value={formData.missQty || 0} | |||||
| onChange={(e) => handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} | |||||
| error={!!errors.missQty} | |||||
| variant="outlined" | |||||
| //disabled={(formData.actualPickQty || 0) > 0} | |||||
| /> | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Bad Item Qty')} | |||||
| type="number" | |||||
| value={formData.badItemQty || 0} | |||||
| onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} | |||||
| error={!!errors.badItemQty} | |||||
| // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} | |||||
| variant="outlined" | |||||
| /> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Bad Item Qty')} | |||||
| type="number" | |||||
| value={formData.badItemQty || 0} | |||||
| onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} | |||||
| error={!!errors.badItemQty} | |||||
| variant="outlined" | |||||
| //disabled={(formData.actualPickQty || 0) > 0} | |||||
| /> | |||||
| </Grid> | </Grid> | ||||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | {/* ✅ Show issue description and handler fields when bad items > 0 */} | ||||
| @@ -118,7 +118,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| // ✅ 新增:搜索状态 | // ✅ 新增:搜索状态 | ||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | ||||
| const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrder[]>([]); | const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrder[]>([]); | ||||
| // ✅ 新增:分页状态 | // ✅ 新增:分页状态 | ||||
| const [paginationController, setPaginationController] = useState({ | const [paginationController, setPaginationController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| @@ -358,10 +358,10 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| {/* 加载状态 */} | {/* 加载状态 */} | ||||
| {completedDoPickOrdersLoading ? ( | {completedDoPickOrdersLoading ? ( | ||||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <Box> | <Box> | ||||
| {/* 结果统计 */} | {/* 结果统计 */} | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||||
| @@ -370,12 +370,12 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| {/* 列表 */} | {/* 列表 */} | ||||
| {filteredDoPickOrders.length === 0 ? ( | {filteredDoPickOrders.length === 0 ? ( | ||||
| <Box sx={{ p: 3, textAlign: 'center' }}> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| <Box sx={{ p: 3, textAlign: 'center' }}> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No completed DO pick orders found")} | {t("No completed DO pick orders found")} | ||||
| </Typography> | |||||
| </Box> | |||||
| ) : ( | |||||
| </Typography> | |||||
| </Box> | |||||
| ) : ( | |||||
| <Stack spacing={2}> | <Stack spacing={2}> | ||||
| {paginatedData.map((doPickOrder) => ( | {paginatedData.map((doPickOrder) => ( | ||||
| <Card key={doPickOrder.id}> | <Card key={doPickOrder.id}> | ||||
| @@ -429,10 +429,10 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| onRowsPerPageChange={handlePageSizeChange} | onRowsPerPageChange={handlePageSizeChange} | ||||
| rowsPerPageOptions={[5, 10, 25, 50]} | rowsPerPageOptions={[5, 10, 25, 50]} | ||||
| /> | /> | ||||
| )} | |||||
| </Box> | |||||
| )} | |||||
| </Box> | |||||
| )} | |||||
| </Box> | |||||
| )} | |||||
| </Box> | |||||
| </FormProvider> | </FormProvider> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -586,12 +586,20 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||||
| setQrScanError(false); | setQrScanError(false); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| setQrScanInput(''); | setQrScanInput(''); | ||||
| setIsManualScanning(false); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| //setIsManualScanning(false); | |||||
| //stopScan(); | |||||
| //resetScan(); | |||||
| setProcessedQrCodes(new Set()); | setProcessedQrCodes(new Set()); | ||||
| setLastProcessedQr(''); | setLastProcessedQr(''); | ||||
| setQrModalOpen(false); | |||||
| setPickExecutionFormOpen(false); | |||||
| if(selectedLotForQr?.stockOutLineId){ | |||||
| const stockOutLineUpdate = await updateStockOutLineStatus({ | |||||
| id: selectedLotForQr.stockOutLineId, | |||||
| status: 'checked', | |||||
| qty: 0 | |||||
| }); | |||||
| } | |||||
| setLotConfirmationOpen(false); | setLotConfirmationOpen(false); | ||||
| setExpectedLotData(null); | setExpectedLotData(null); | ||||
| setScannedLotData(null); | setScannedLotData(null); | ||||
| @@ -709,9 +717,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||||
| setQrScanSuccess(true); | setQrScanSuccess(true); | ||||
| setQrScanError(false); | setQrScanError(false); | ||||
| setQrScanInput(''); // Clear input after successful processing | setQrScanInput(''); // Clear input after successful processing | ||||
| setIsManualScanning(false); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| //setIsManualScanning(false); | |||||
| // stopScan(); | |||||
| // resetScan(); | |||||
| // ✅ Clear success state after a delay | // ✅ Clear success state after a delay | ||||
| //setTimeout(() => { | //setTimeout(() => { | ||||
| @@ -14,6 +14,7 @@ type Props = { | |||||
| interface ErrorEntry { | interface ErrorEntry { | ||||
| qtyErr: boolean; | qtyErr: boolean; | ||||
| scanErr: boolean; | scanErr: boolean; | ||||
| pickErr: boolean; | |||||
| } | } | ||||
| const ActionButtons: React.FC<Props> = ({ | const ActionButtons: React.FC<Props> = ({ | ||||
| @@ -36,24 +37,31 @@ const ActionButtons: React.FC<Props> = ({ | |||||
| const errors: ErrorEntry = useMemo(() => { | const errors: ErrorEntry = useMemo(() => { | ||||
| let qtyErr = false; | let qtyErr = false; | ||||
| let scanErr = false; | let scanErr = false; | ||||
| let pickErr = false | |||||
| pickLines.forEach((line) => { | pickLines.forEach((line) => { | ||||
| if (!qtyErr) { | if (!qtyErr) { | ||||
| const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0 | const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0 | ||||
| qtyErr = pickedQty > 0 && pickedQty >= line.reqQty | |||||
| qtyErr = pickedQty <= 0 || pickedQty < line.reqQty | |||||
| } | } | ||||
| if (!scanErr) { | if (!scanErr) { | ||||
| scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false | scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false | ||||
| } | } | ||||
| if (!pickErr) { | |||||
| pickErr = line.pickedLotNo === null | |||||
| } | |||||
| }) | }) | ||||
| return { | return { | ||||
| qtyErr: qtyErr, | qtyErr: qtyErr, | ||||
| scanErr: scanErr | |||||
| scanErr: scanErr, | |||||
| pickErr: pickErr | |||||
| } | } | ||||
| }, [pickLines]) | |||||
| }, [pickLines, status]) | |||||
| console.log(pickLines) | |||||
| return ( | return ( | ||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | <Stack direction="row" justifyContent="flex-start" gap={1}> | ||||
| {status === "planning" && ( | {status === "planning" && ( | ||||
| @@ -71,12 +79,13 @@ const ActionButtons: React.FC<Props> = ({ | |||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<PlayCircleFilledWhiteIcon />} | startIcon={<PlayCircleFilledWhiteIcon />} | ||||
| onClick={handleStart} | onClick={handleStart} | ||||
| disabled={errors.qtyErr || errors.scanErr} | |||||
| disabled={errors.qtyErr || errors.scanErr || errors.pickErr} | |||||
| > | > | ||||
| {t("Start Job Order")} | {t("Start Job Order")} | ||||
| </Button> | </Button> | ||||
| {errors.scanErr && (<Typography variant="h3" color="error">{t("Please scan the item qr code.")}</Typography>)} | |||||
| {errors.qtyErr && (<Typography variant="h3" color="error">{t("Please make sure the qty is enough.")}</Typography>)} | |||||
| {errors.pickErr && (<Typography variant="h3" color="error">{t("Please make sure all required items are picked")}</Typography>)} | |||||
| {errors.scanErr && (<Typography variant="h3" color="error">{t("Please scan the item qr code")}</Typography>)} | |||||
| {errors.qtyErr && (<Typography variant="h3" color="error">{t("Please make sure the qty is enough")}</Typography>)} | |||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| @@ -8,13 +8,17 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } fro | |||||
| import { Button, Stack, Typography } from "@mui/material"; | import { Button, Stack, Typography } from "@mui/material"; | ||||
| import StartIcon from "@mui/icons-material/Start"; | import StartIcon from "@mui/icons-material/Start"; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import { releaseJo } from "@/app/api/jo/actions"; | |||||
| import { releaseJo, startJo } from "@/app/api/jo/actions"; | |||||
| import InfoCard from "./InfoCard"; | import InfoCard from "./InfoCard"; | ||||
| import PickTable from "./PickTable"; | import PickTable from "./PickTable"; | ||||
| import ActionButtons from "./ActionButtons"; | import ActionButtons from "./ActionButtons"; | ||||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | ||||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | import { fetchStockInLineInfo } from "@/app/api/po/actions"; | ||||
| <<<<<<< HEAD | |||||
| import JoRelease from "./JoRelease"; | import JoRelease from "./JoRelease"; | ||||
| ======= | |||||
| import { submitDialog } from "../Swal/CustomAlerts"; | |||||
| >>>>>>> 5ef2a717b8e76f98fdf437b56fa641e990ef106b | |||||
| type Props = { | type Props = { | ||||
| id?: number; | id?: number; | ||||
| @@ -94,12 +98,18 @@ const JoSave: React.FC<Props> = ({ | |||||
| shouldValidate: true, | shouldValidate: true, | ||||
| shouldDirty: true, | shouldDirty: true, | ||||
| }); | }); | ||||
| // Ask user and confirm to start JO | |||||
| await submitDialog(() => handleStart(), t, { | |||||
| title: t("Do you want to start job order"), | |||||
| confirmButtonText: t("Start Job Order") | |||||
| }) | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } finally { | } finally { | ||||
| scanner.resetScan() | |||||
| setIsUploading(false) | |||||
| scanner.resetScan() | |||||
| setIsUploading(false) | |||||
| } | } | ||||
| }, []) | }, []) | ||||
| @@ -127,7 +137,6 @@ const JoSave: React.FC<Props> = ({ | |||||
| formProps.setValue("status", response.entity.status) | formProps.setValue("status", response.entity.status) | ||||
| } | } | ||||
| } | } | ||||
| } catch (e) { | } catch (e) { | ||||
| // backend error | // backend error | ||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| @@ -138,7 +147,25 @@ const JoSave: React.FC<Props> = ({ | |||||
| }, []) | }, []) | ||||
| const handleStart = useCallback(async () => { | const handleStart = useCallback(async () => { | ||||
| console.log("first") | |||||
| try { | |||||
| setIsUploading(true) | |||||
| if (id) { | |||||
| const response = await startJo({ id: id }) | |||||
| if (response) { | |||||
| formProps.setValue("status", response.entity.status) | |||||
| pickLines.map((line) => ({...line, status: "completed"})) | |||||
| handleBack() | |||||
| } | |||||
| } | |||||
| } catch (e) { | |||||
| // backend error | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| console.log(e); | |||||
| } finally { | |||||
| setIsUploading(false) | |||||
| } | |||||
| }, []) | }, []) | ||||
| // --------------------------------------------- Form Submit --------------------------------------------- // | // --------------------------------------------- Form Submit --------------------------------------------- // | ||||
| @@ -141,8 +141,8 @@ const PickTable: React.FC<Props> = ({ | |||||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | ||||
| return notPickedStatusColumn | return notPickedStatusColumn | ||||
| } | } | ||||
| const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned)) | |||||
| return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack> | |||||
| const scanStatus = params.row.pickedLotNo.map((pln) => params.row.status === "completed" ? true : Boolean(pln.isScanned)) | |||||
| return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack key={`${params.id}-scan`} direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack> | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -213,10 +213,11 @@ const PickTable: React.FC<Props> = ({ | |||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | ||||
| const status = Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))) || params.row.status === "completed" | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))} | {params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))} | ||||
| {scanStatusColumn(Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))))} | |||||
| {scanStatusColumn(status)} | |||||
| </> | </> | ||||
| ) | ) | ||||
| }, | }, | ||||
| @@ -0,0 +1,231 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| CircularProgress, | |||||
| Paper, | |||||
| Stack, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| TextField, | |||||
| Typography, | |||||
| TablePagination, | |||||
| } from "@mui/material"; | |||||
| import { useCallback, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| interface CombinedLotTableProps { | |||||
| combinedLotData: any[]; | |||||
| combinedDataLoading: boolean; | |||||
| pickQtyData: Record<string, number>; | |||||
| paginationController: { | |||||
| pageNum: number; | |||||
| pageSize: number; | |||||
| }; | |||||
| onPickQtyChange: (lotKey: string, value: number | string) => void; | |||||
| onSubmitPickQty: (lot: any) => void; | |||||
| onRejectLot: (lot: any) => void; | |||||
| onPageChange: (event: unknown, newPage: number) => void; | |||||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||||
| } | |||||
| // ✅ Simple helper function to check if item is completed | |||||
| const isItemCompleted = (lot: any) => { | |||||
| const actualPickQty = Number(lot.actualPickQty) || 0; | |||||
| const requiredQty = Number(lot.requiredQty) || 0; | |||||
| return lot.stockOutLineStatus === 'completed' || | |||||
| (actualPickQty > 0 && requiredQty > 0 && actualPickQty >= requiredQty); | |||||
| }; | |||||
| const isItemRejected = (lot: any) => { | |||||
| return lot.stockOutLineStatus === 'rejected'; | |||||
| }; | |||||
| const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||||
| combinedLotData, | |||||
| combinedDataLoading, | |||||
| pickQtyData, | |||||
| paginationController, | |||||
| onPickQtyChange, | |||||
| onSubmitPickQty, | |||||
| onRejectLot, | |||||
| onPageChange, | |||||
| onPageSizeChange, | |||||
| }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| // ✅ Paginated data | |||||
| const paginatedLotData = useMemo(() => { | |||||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||||
| const endIndex = startIndex + paginationController.pageSize; | |||||
| return combinedLotData.slice(startIndex, endIndex); | |||||
| }, [combinedLotData, paginationController]); | |||||
| if (combinedDataLoading) { | |||||
| return ( | |||||
| <Box display="flex" justifyContent="center" alignItems="center" minHeight="200px"> | |||||
| <CircularProgress size={40} /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Pick Order Code")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | |||||
| <TableCell>{t("Item Name")}</TableCell> | |||||
| <TableCell>{t("Lot No")}</TableCell> | |||||
| {/* <TableCell>{t("Expiry Date")}</TableCell> */} | |||||
| <TableCell>{t("Location")}</TableCell> | |||||
| <TableCell align="right">{t("Current Stock")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Qty Already Picked")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell> | |||||
| <TableCell>{t("Stock Unit")}</TableCell> | |||||
| <TableCell align="center">{t("Submit")}</TableCell> | |||||
| <TableCell align="center">{t("Reject")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedLotData.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={13} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| paginatedLotData.map((lot: any) => { | |||||
| const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; | |||||
| const currentPickQty = pickQtyData[lotKey] ?? ''; | |||||
| const isCompleted = isItemCompleted(lot); | |||||
| const isRejected = isItemRejected(lot); | |||||
| // ✅ Green text color for completed items | |||||
| const textColor = isCompleted ? 'success.main' : isRejected ? 'error.main' : 'inherit'; | |||||
| return ( | |||||
| <TableRow | |||||
| key={lotKey} | |||||
| sx={{ | |||||
| '&:hover': { | |||||
| backgroundColor: 'action.hover', | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <TableCell sx={{ color: textColor }}>{lot.pickOrderCode}</TableCell> | |||||
| <TableCell sx={{ color: textColor }}>{lot.itemCode}</TableCell> | |||||
| <TableCell sx={{ color: textColor }}>{lot.itemName}</TableCell> | |||||
| <TableCell sx={{ color: textColor }}>{lot.lotNo}</TableCell> | |||||
| {/* <TableCell sx={{ color: textColor }}> | |||||
| {lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A'} | |||||
| </TableCell> | |||||
| */} | |||||
| <TableCell sx={{ color: textColor }}>{lot.location}</TableCell> | |||||
| <TableCell align="right" sx={{ color: textColor }}>{lot.availableQty}</TableCell> | |||||
| <TableCell align="right" sx={{ color: textColor }}>{lot.requiredQty}</TableCell> | |||||
| <TableCell align="right" sx={{ color: textColor }}>{lot.actualPickQty || 0}</TableCell> | |||||
| <TableCell align="right"> | |||||
| <TextField | |||||
| type="number" | |||||
| value={currentPickQty} | |||||
| onChange={(e) => { | |||||
| onPickQtyChange(lotKey, e.target.value); | |||||
| }} | |||||
| onFocus={(e) => { | |||||
| e.target.select(); | |||||
| }} | |||||
| inputProps={{ | |||||
| min: 0, | |||||
| max: lot.availableQty, | |||||
| step: 0.01 | |||||
| }} | |||||
| disabled={ | |||||
| isCompleted || | |||||
| isRejected || | |||||
| lot.lotAvailability === 'expired' || | |||||
| lot.lotAvailability === 'status_unavailable' | |||||
| } | |||||
| sx={{ | |||||
| width: '80px', | |||||
| '& .MuiInputBase-input': { | |||||
| textAlign: 'right', | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell sx={{ color: textColor }}>{lot.stockUnit}</TableCell> | |||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="contained" | |||||
| size="small" | |||||
| disabled={isCompleted || isRejected || !currentPickQty || currentPickQty <= 0} | |||||
| onClick={() => onSubmitPickQty(lot)} | |||||
| sx={{ | |||||
| backgroundColor: isCompleted ? 'success.main' : 'primary.main', | |||||
| color: 'white', | |||||
| '&:disabled': { | |||||
| backgroundColor: 'grey.300', | |||||
| color: 'grey.500', | |||||
| }, | |||||
| }} | |||||
| > | |||||
| {isCompleted ? t("Completed") : t("Submit")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| color="error" | |||||
| disabled={isCompleted || isRejected} | |||||
| onClick={() => onRejectLot(lot)} | |||||
| sx={{ | |||||
| '&:disabled': { | |||||
| borderColor: 'grey.300', | |||||
| color: 'grey.500', | |||||
| }, | |||||
| }} | |||||
| > | |||||
| {t("Reject")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ); | |||||
| }) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={combinedLotData.length} | |||||
| page={paginationController.pageNum} | |||||
| rowsPerPage={paginationController.pageSize} | |||||
| onPageChange={onPageChange} | |||||
| onRowsPerPageChange={onPageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CombinedLotTable; | |||||
| @@ -0,0 +1,321 @@ | |||||
| "use client"; | |||||
| import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions"; | |||||
| import { | |||||
| Autocomplete, | |||||
| Box, | |||||
| Card, | |||||
| CardContent, | |||||
| FormControl, | |||||
| Grid, | |||||
| Stack, | |||||
| TextField, | |||||
| Tooltip, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import StyledDataGrid from "../StyledDataGrid"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| GridColDef, | |||||
| GridRowIdGetter, | |||||
| GridRowModel, | |||||
| useGridApiContext, | |||||
| GridRenderCellParams, | |||||
| GridRenderEditCellParams, | |||||
| useGridApiRef, | |||||
| } from "@mui/x-data-grid"; | |||||
| import InputDataGrid from "../InputDataGrid"; | |||||
| import { TableRow } from "../InputDataGrid/InputDataGrid"; | |||||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||||
| import { StockInLine } from "@/app/api/po"; | |||||
| import { INPUT_DATE_FORMAT, stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||||
| import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; | |||||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||||
| import axios from "@/app/(main)/axios/axiosInstance"; | |||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||||
| import { SavePickOrderLineRequest, SavePickOrderRequest } from "@/app/api/pickOrder/actions"; | |||||
| import TwoLineCell from "../PoDetail/TwoLineCell"; | |||||
| import ItemSelect from "./ItemSelect"; | |||||
| import { ItemCombo } from "@/app/api/settings/item/actions"; | |||||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
| import dayjs from "dayjs"; | |||||
| interface Props { | |||||
| items: ItemCombo[]; | |||||
| // disabled: boolean; | |||||
| } | |||||
| type EntryError = | |||||
| | { | |||||
| [field in keyof SavePickOrderLineRequest]?: string; | |||||
| } | |||||
| | undefined; | |||||
| type PolRow = TableRow<Partial<SavePickOrderLineRequest>, EntryError>; | |||||
| // fetchQcItemCheck | |||||
| const CreateForm: React.FC<Props> = ({ items }) => { | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation("pickOrder"); | |||||
| const apiRef = useGridApiRef(); | |||||
| const { | |||||
| formState: { errors, defaultValues, touchedFields }, | |||||
| watch, | |||||
| control, | |||||
| setValue, | |||||
| } = useFormContext<SavePickOrderRequest>(); | |||||
| console.log(defaultValues); | |||||
| const targetDate = watch("targetDate"); | |||||
| //// validate form | |||||
| // const accQty = watch("acceptedQty"); | |||||
| // const validateForm = useCallback(() => { | |||||
| // console.log(accQty); | |||||
| // if (accQty > itemDetail.acceptedQty) { | |||||
| // setError("acceptedQty", { | |||||
| // message: `${t("acceptedQty must not greater than")} ${ | |||||
| // itemDetail.acceptedQty | |||||
| // }`, | |||||
| // type: "required", | |||||
| // }); | |||||
| // } | |||||
| // if (accQty < 1) { | |||||
| // setError("acceptedQty", { | |||||
| // message: t("minimal value is 1"), | |||||
| // type: "required", | |||||
| // }); | |||||
| // } | |||||
| // if (isNaN(accQty)) { | |||||
| // setError("acceptedQty", { | |||||
| // message: t("value must be a number"), | |||||
| // type: "required", | |||||
| // }); | |||||
| // } | |||||
| // }, [accQty]); | |||||
| // useEffect(() => { | |||||
| // clearErrors(); | |||||
| // validateForm(); | |||||
| // }, [clearErrors, validateForm]); | |||||
| const columns = useMemo<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| field: "itemId", | |||||
| headerName: t("Item"), | |||||
| // width: 100, | |||||
| flex: 1, | |||||
| editable: true, | |||||
| valueFormatter(params) { | |||||
| const row = params.id ? params.api.getRow<PolRow>(params.id) : null; | |||||
| if (!row) { | |||||
| return null; | |||||
| } | |||||
| const Item = items.find((q) => q.id === row.itemId); | |||||
| return Item ? Item.label : t("Please select item"); | |||||
| }, | |||||
| renderCell(params: GridRenderCellParams<PolRow, number>) { | |||||
| console.log(params.value); | |||||
| return <TwoLineCell>{params.formattedValue}</TwoLineCell>; | |||||
| }, | |||||
| renderEditCell(params: GridRenderEditCellParams<PolRow, number>) { | |||||
| const errorMessage = | |||||
| params.row._error?.[params.field as keyof SavePickOrderLineRequest]; | |||||
| console.log(errorMessage); | |||||
| const content = ( | |||||
| // <></> | |||||
| <ItemSelect | |||||
| allItems={items} | |||||
| value={params.row.itemId} | |||||
| onItemSelect={async (itemId, uom, uomId) => { | |||||
| console.log(uom) | |||||
| await params.api.setEditCellValue({ | |||||
| id: params.id, | |||||
| field: "itemId", | |||||
| value: itemId, | |||||
| }); | |||||
| await params.api.setEditCellValue({ | |||||
| id: params.id, | |||||
| field: "uom", | |||||
| value: uom | |||||
| }) | |||||
| await params.api.setEditCellValue({ | |||||
| id: params.id, | |||||
| field: "uomId", | |||||
| value: uomId | |||||
| }) | |||||
| }} | |||||
| /> | |||||
| ); | |||||
| return errorMessage ? ( | |||||
| <Tooltip title={errorMessage}> | |||||
| <Box width="100%">{content}</Box> | |||||
| </Tooltip> | |||||
| ) : ( | |||||
| content | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "qty", | |||||
| headerName: t("qty"), | |||||
| // width: 100, | |||||
| flex: 1, | |||||
| type: "number", | |||||
| editable: true, | |||||
| renderEditCell(params: GridRenderEditCellParams<PolRow>) { | |||||
| const errorMessage = | |||||
| params.row._error?.[params.field as keyof SavePickOrderLineRequest]; | |||||
| const content = <GridEditInputCell {...params} />; | |||||
| return errorMessage ? ( | |||||
| <Tooltip title={t(errorMessage)}> | |||||
| <Box width="100%">{content}</Box> | |||||
| </Tooltip> | |||||
| ) : ( | |||||
| content | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "uom", | |||||
| headerName: t("uom"), | |||||
| // width: 100, | |||||
| flex: 1, | |||||
| editable: true, | |||||
| // renderEditCell(params: GridRenderEditCellParams<PolRow>) { | |||||
| // console.log(params.row) | |||||
| // const errorMessage = | |||||
| // params.row._error?.[params.field as keyof SavePickOrderLineRequest]; | |||||
| // const content = <GridEditInputCell {...params} />; | |||||
| // return errorMessage ? ( | |||||
| // <Tooltip title={t(errorMessage)}> | |||||
| // <Box width="100%">{content}</Box> | |||||
| // </Tooltip> | |||||
| // ) : ( | |||||
| // content | |||||
| // ); | |||||
| // } | |||||
| } | |||||
| ], | |||||
| [items, t], | |||||
| ); | |||||
| /// validate datagrid | |||||
| const validation = useCallback( | |||||
| (newRow: GridRowModel<PolRow>): EntryError => { | |||||
| const error: EntryError = {}; | |||||
| const { itemId, qty } = newRow; | |||||
| if (!itemId || itemId <= 0) { | |||||
| error["itemId"] = t("select qc"); | |||||
| } | |||||
| if (!qty || qty <= 0) { | |||||
| error["qty"] = t("enter a qty"); | |||||
| } | |||||
| return Object.keys(error).length > 0 ? error : undefined; | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const typeList = [ | |||||
| { | |||||
| type: "Consumable" | |||||
| } | |||||
| ] | |||||
| const onChange = useCallback( | |||||
| (event: React.SyntheticEvent, newValue: {type: string}) => { | |||||
| console.log(newValue); | |||||
| setValue("type", newValue.type); | |||||
| }, | |||||
| [setValue], | |||||
| ); | |||||
| return ( | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Pick Order Detail")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Grid item xs={6} lg={6}> | |||||
| <FormControl fullWidth> | |||||
| <Autocomplete | |||||
| disableClearable | |||||
| fullWidth | |||||
| getOptionLabel={(option) => option.type} | |||||
| options={typeList} | |||||
| onChange={onChange} | |||||
| renderInput={(params) => <TextField {...params} label={t("type")}/>} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| control={control} | |||||
| name="targetDate" | |||||
| // rules={{ required: !Boolean(productionDate) }} | |||||
| render={({ field }) => { | |||||
| return ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale={`${language}-hk`} | |||||
| > | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={{ width: "100%" }} | |||||
| label={t("targetDate")} | |||||
| value={targetDate ? dayjs(targetDate) : undefined} | |||||
| onChange={(date) => { | |||||
| console.log(date); | |||||
| if (!date) return; | |||||
| console.log(date.format(INPUT_DATE_FORMAT)); | |||||
| setValue("targetDate", date.format(INPUT_DATE_FORMAT)); | |||||
| // field.onChange(date); | |||||
| }} | |||||
| inputRef={field.ref} | |||||
| slotProps={{ | |||||
| textField: { | |||||
| // required: true, | |||||
| error: Boolean(errors.targetDate?.message), | |||||
| helperText: errors.targetDate?.message, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Grid item xs={12}> | |||||
| <InputDataGrid<SavePickOrderRequest, SavePickOrderLineRequest, EntryError> | |||||
| apiRef={apiRef} | |||||
| checkboxSelection={false} | |||||
| _formKey={"pickOrderLine"} | |||||
| columns={columns} | |||||
| validateRow={validation} | |||||
| needAdd={true} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default CreateForm; | |||||
| @@ -0,0 +1,209 @@ | |||||
| import React, { useCallback } from 'react'; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| Checkbox, | |||||
| TextField, | |||||
| TablePagination, | |||||
| FormControl, | |||||
| Select, | |||||
| MenuItem, | |||||
| } from '@mui/material'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| interface CreatedItem { | |||||
| itemId: number; | |||||
| itemName: string; | |||||
| itemCode: string; | |||||
| qty: number; | |||||
| uom: string; | |||||
| uomId: number; | |||||
| uomDesc: string; | |||||
| isSelected: boolean; | |||||
| currentStockBalance?: number; | |||||
| targetDate?: string | null; | |||||
| groupId?: number | null; | |||||
| } | |||||
| interface Group { | |||||
| id: number; | |||||
| name: string; | |||||
| targetDate: string; | |||||
| } | |||||
| interface CreatedItemsTableProps { | |||||
| items: CreatedItem[]; | |||||
| groups: Group[]; | |||||
| onItemSelect: (itemId: number, checked: boolean) => void; | |||||
| onQtyChange: (itemId: number, qty: number) => void; | |||||
| onGroupChange: (itemId: number, groupId: string) => void; | |||||
| pageNum: number; | |||||
| pageSize: number; | |||||
| onPageChange: (event: unknown, newPage: number) => void; | |||||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||||
| } | |||||
| const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | |||||
| items, | |||||
| groups, | |||||
| onItemSelect, | |||||
| onQtyChange, | |||||
| onGroupChange, | |||||
| pageNum, | |||||
| pageSize, | |||||
| onPageChange, | |||||
| onPageSizeChange, | |||||
| }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| // Calculate pagination | |||||
| const startIndex = (pageNum - 1) * pageSize; | |||||
| const endIndex = startIndex + pageSize; | |||||
| const paginatedItems = items.slice(startIndex, endIndex); | |||||
| const handleQtyChange = useCallback((itemId: number, value: string) => { | |||||
| const numValue = Number(value); | |||||
| if (!isNaN(numValue) && numValue >= 1) { | |||||
| onQtyChange(itemId, numValue); | |||||
| } | |||||
| }, [onQtyChange]); | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}> | |||||
| {t("Selected")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Item")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Group")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Current Stock")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Stock Unit")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Order Quantity")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Target Date")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedItems.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={12} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No created items")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| paginatedItems.map((item) => ( | |||||
| <TableRow key={item.itemId}> | |||||
| <TableCell padding="checkbox"> | |||||
| <Checkbox | |||||
| checked={item.isSelected} | |||||
| onChange={(e) => onItemSelect(item.itemId, e.target.checked)} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2">{item.itemName}</Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| {item.itemCode} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <FormControl size="small" sx={{ minWidth: 120 }}> | |||||
| <Select | |||||
| value={item.groupId?.toString() || ""} | |||||
| onChange={(e) => onGroupChange(item.itemId, e.target.value)} | |||||
| displayEmpty | |||||
| > | |||||
| <MenuItem value=""> | |||||
| <em>{t("No Group")}</em> | |||||
| </MenuItem> | |||||
| {groups.map((group) => ( | |||||
| <MenuItem key={group.id} value={group.id.toString()}> | |||||
| {group.name} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | |||||
| </FormControl> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography | |||||
| variant="body2" | |||||
| color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"} | |||||
| > | |||||
| {item.currentStockBalance?.toLocaleString() || 0} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2">{item.uomDesc}</Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={item.qty || ""} | |||||
| onChange={(e) => handleQtyChange(item.itemId, e.target.value)} | |||||
| inputProps={{ | |||||
| min: 1, | |||||
| step: 1, | |||||
| style: { textAlign: 'center' } | |||||
| }} | |||||
| sx={{ | |||||
| width: '80px', | |||||
| '& .MuiInputBase-input': { | |||||
| textAlign: 'center', | |||||
| cursor: 'text' | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={items.length} | |||||
| page={(pageNum - 1)} | |||||
| rowsPerPage={pageSize} | |||||
| onPageChange={onPageChange} | |||||
| onRowsPerPageChange={onPageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreatedItemsTable; | |||||
| @@ -0,0 +1,179 @@ | |||||
| import React, { useState, ChangeEvent, FormEvent, Dispatch } from 'react'; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Collapse, | |||||
| FormControl, | |||||
| InputLabel, | |||||
| Select, | |||||
| MenuItem, | |||||
| TextField, | |||||
| Checkbox, | |||||
| FormControlLabel, | |||||
| Paper, | |||||
| Typography, | |||||
| RadioGroup, | |||||
| Radio, | |||||
| Stack, | |||||
| Autocomplete, | |||||
| } from '@mui/material'; | |||||
| import { SelectChangeEvent } from '@mui/material/Select'; | |||||
| import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||||
| import ExpandLessIcon from '@mui/icons-material/ExpandLess'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| interface NameOption { | |||||
| value: string; | |||||
| label: string; | |||||
| } | |||||
| interface FormData { | |||||
| name: string; | |||||
| quantity: string; | |||||
| message: string; | |||||
| } | |||||
| interface Props { | |||||
| forSupervisor: boolean | |||||
| isCollapsed: boolean | |||||
| setIsCollapsed: Dispatch<React.SetStateAction<boolean>> | |||||
| } | |||||
| const EscalationComponent: React.FC<Props> = ({ | |||||
| forSupervisor, | |||||
| isCollapsed, | |||||
| setIsCollapsed | |||||
| }) => { | |||||
| const { t } = useTranslation("purchaseOrder"); | |||||
| const [formData, setFormData] = useState<FormData>({ | |||||
| name: '', | |||||
| quantity: '', | |||||
| message: '', | |||||
| }); | |||||
| const nameOptions: NameOption[] = [ | |||||
| { value: '', label: '請選擇姓名...' }, | |||||
| { value: 'john', label: '張大明' }, | |||||
| { value: 'jane', label: '李小美' }, | |||||
| { value: 'mike', label: '王志強' }, | |||||
| { value: 'sarah', label: '陳淑華' }, | |||||
| { value: 'david', label: '林建國' }, | |||||
| ]; | |||||
| const handleInputChange = ( | |||||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||||
| ): void => { | |||||
| const { name, value } = event.target; | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| [name]: value, | |||||
| })); | |||||
| }; | |||||
| const handleSubmit = (e: FormEvent<HTMLFormElement>): void => { | |||||
| e.preventDefault(); | |||||
| console.log('表單已提交:', formData); | |||||
| // 處理表單提交 | |||||
| }; | |||||
| const handleCollapseToggle = (e: ChangeEvent<HTMLInputElement>): void => { | |||||
| setIsCollapsed(e.target.checked); | |||||
| }; | |||||
| return ( | |||||
| // <Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', p: 3 }}> | |||||
| <> | |||||
| <Paper> | |||||
| {/* <Paper elevation={3} sx={{ mx: 'auto', p: 3 }}> */} | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> | |||||
| <FormControlLabel | |||||
| control={ | |||||
| <Checkbox | |||||
| checked={isCollapsed} | |||||
| onChange={handleCollapseToggle} | |||||
| color="primary" | |||||
| /> | |||||
| } | |||||
| label={ | |||||
| <Box sx={{ display: 'flex', alignItems: 'center' }}> | |||||
| <Typography variant="body1">上報結果</Typography> | |||||
| {isCollapsed ? ( | |||||
| <ExpandLessIcon sx={{ ml: 1 }} /> | |||||
| ) : ( | |||||
| <ExpandMoreIcon sx={{ ml: 1 }} /> | |||||
| )} | |||||
| </Box> | |||||
| } | |||||
| /> | |||||
| </Box> | |||||
| <Collapse in={isCollapsed}> | |||||
| <Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> | |||||
| {forSupervisor ? ( | |||||
| <FormControl> | |||||
| <RadioGroup | |||||
| row | |||||
| aria-labelledby="demo-radio-buttons-group-label" | |||||
| defaultValue="pass" | |||||
| name="radio-buttons-group" | |||||
| > | |||||
| <FormControlLabel value="pass" control={<Radio />} label="合格" /> | |||||
| <FormControlLabel value="fail" control={<Radio />} label="不合格" /> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| ): undefined} | |||||
| <FormControl fullWidth> | |||||
| <select | |||||
| id="name" | |||||
| name="name" | |||||
| value={formData.name} | |||||
| onChange={handleInputChange} | |||||
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" | |||||
| > | |||||
| {nameOptions.map((option: NameOption) => ( | |||||
| <option key={option.value} value={option.value}> | |||||
| {option.label} | |||||
| </option> | |||||
| ))} | |||||
| </select> | |||||
| </FormControl> | |||||
| <TextField | |||||
| fullWidth | |||||
| id="quantity" | |||||
| name="quantity" | |||||
| label="數量" | |||||
| type="number" | |||||
| value={formData.quantity} | |||||
| onChange={handleInputChange} | |||||
| InputProps={{ inputProps: { min: 1 } }} | |||||
| placeholder="請輸入數量" | |||||
| /> | |||||
| <TextField | |||||
| fullWidth | |||||
| id="message" | |||||
| name="message" | |||||
| label="備註" | |||||
| multiline | |||||
| rows={4} | |||||
| value={formData.message} | |||||
| onChange={handleInputChange} | |||||
| placeholder="請輸入您的備註" | |||||
| /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| type="submit" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| > | |||||
| {t("update qc info")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Box> | |||||
| </Collapse> | |||||
| </Paper> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default EscalationComponent; | |||||
| @@ -0,0 +1,120 @@ | |||||
| "use client"; | |||||
| import { FGPickOrderResponse } from "@/app/api/pickOrder/actions"; | |||||
| import { Box, Card, CardContent, Grid, Stack, TextField, Button } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | |||||
| type Props = { | |||||
| fgOrder: FGPickOrderResponse; | |||||
| onQrCodeClick: (pickOrderId: number) => void; | |||||
| }; | |||||
| const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| return ( | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Delivery Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.deliveryNo} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Pick Order Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.pickOrderCode} | |||||
| //helperText={fgOrder.pickOrderConsoCode} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Shop PO Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.shopPoNo} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Store ID")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.storeId} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Shop ID")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.shopCode} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Shop Name")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.shopName} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Delivery Date")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={new Date(fgOrder.deliveryDate).toLocaleDateString()} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| label={t("Shop Address")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.shopAddress} | |||||
| multiline | |||||
| rows={2} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Departure Time")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.DepartureTime} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Truck No.")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.truckNo} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Ticket No.")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.ticketNo} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default FGPickOrderCard; | |||||
| @@ -0,0 +1,556 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Stack, | |||||
| TextField, | |||||
| Typography, | |||||
| Alert, | |||||
| CircularProgress, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| TablePagination, | |||||
| Modal, | |||||
| Card, | |||||
| CardContent, | |||||
| CardActions, | |||||
| Chip, | |||||
| Accordion, | |||||
| AccordionSummary, | |||||
| AccordionDetails, | |||||
| Checkbox, // ✅ Add Checkbox import | |||||
| } from "@mui/material"; | |||||
| import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||||
| import { useCallback, useEffect, useState, useRef, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { | |||||
| fetchCompletedJobOrderPickOrdersWithCompletedSecondScan, | |||||
| fetchCompletedJobOrderPickOrderLotDetails | |||||
| } from "@/app/api/jo/actions"; | |||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||||
| import { | |||||
| FormProvider, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { useSession } from "next-auth/react"; | |||||
| import { SessionWithTokens } from "@/config/authConfig"; | |||||
| interface Props { | |||||
| filterArgs: Record<string, any>; | |||||
| } | |||||
| // ✅ 修改:已完成的 Job Order Pick Order 接口 | |||||
| interface CompletedJobOrderPickOrder { | |||||
| id: number; | |||||
| pickOrderId: number; | |||||
| pickOrderCode: string; | |||||
| pickOrderConsoCode: string; | |||||
| pickOrderTargetDate: string; | |||||
| pickOrderStatus: string; | |||||
| completedDate: string; | |||||
| jobOrderId: number; | |||||
| jobOrderCode: string; | |||||
| jobOrderName: string; | |||||
| reqQty: number; | |||||
| uom: string; | |||||
| planStart: string; | |||||
| planEnd: string; | |||||
| secondScanCompleted: boolean; | |||||
| totalItems: number; | |||||
| completedItems: number; | |||||
| } | |||||
| // ✅ 新增:Lot 详情接口 | |||||
| interface LotDetail { | |||||
| lotId: number; | |||||
| lotNo: string; | |||||
| expiryDate: string; | |||||
| location: string; | |||||
| availableQty: number; | |||||
| requiredQty: number; | |||||
| actualPickQty: number; | |||||
| processingStatus: string; | |||||
| lotAvailability: string; | |||||
| pickOrderId: number; | |||||
| pickOrderCode: string; | |||||
| pickOrderConsoCode: string; | |||||
| pickOrderLineId: number; | |||||
| stockOutLineId: number; | |||||
| stockOutLineStatus: string; | |||||
| routerIndex: number; | |||||
| routerArea: string; | |||||
| routerRoute: string; | |||||
| uomShortDesc: string; | |||||
| secondQrScanStatus: string; | |||||
| itemId: number; | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| uomCode: string; | |||||
| uomDesc: string; | |||||
| } | |||||
| const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| const { t } = useTranslation("jo"); | |||||
| const router = useRouter(); | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||||
| // ✅ 修改:已完成 Job Order Pick Orders 状态 | |||||
| const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||||
| const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false); | |||||
| // ✅ 修改:详情视图状态 | |||||
| const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState<CompletedJobOrderPickOrder | null>(null); | |||||
| const [showDetailView, setShowDetailView] = useState(false); | |||||
| const [detailLotData, setDetailLotData] = useState<LotDetail[]>([]); | |||||
| const [detailLotDataLoading, setDetailLotDataLoading] = useState(false); | |||||
| // ✅ 修改:搜索状态 | |||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||||
| const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||||
| // ✅ 修改:分页状态 | |||||
| const [paginationController, setPaginationController] = useState({ | |||||
| pageNum: 0, | |||||
| pageSize: 10, | |||||
| }); | |||||
| const formProps = useForm(); | |||||
| const errors = formProps.formState.errors; | |||||
| // ✅ 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders | |||||
| const fetchCompletedJobOrderPickOrdersData = useCallback(async () => { | |||||
| if (!currentUserId) return; | |||||
| setCompletedJobOrderPickOrdersLoading(true); | |||||
| try { | |||||
| console.log("🔍 Fetching completed Job Order pick orders..."); | |||||
| const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersWithCompletedSecondScan(currentUserId); | |||||
| setCompletedJobOrderPickOrders(completedJobOrderPickOrders); | |||||
| setFilteredJobOrderPickOrders(completedJobOrderPickOrders); | |||||
| console.log("✅ Fetched completed Job Order pick orders:", completedJobOrderPickOrders); | |||||
| } catch (error) { | |||||
| console.error("❌ Error fetching completed Job Order pick orders:", error); | |||||
| setCompletedJobOrderPickOrders([]); | |||||
| setFilteredJobOrderPickOrders([]); | |||||
| } finally { | |||||
| setCompletedJobOrderPickOrdersLoading(false); | |||||
| } | |||||
| }, [currentUserId]); | |||||
| // ✅ 新增:获取 lot 详情数据 | |||||
| const fetchLotDetailsData = useCallback(async (pickOrderId: number) => { | |||||
| setDetailLotDataLoading(true); | |||||
| try { | |||||
| console.log("🔍 Fetching lot details for pick order:", pickOrderId); | |||||
| const lotDetails = await fetchCompletedJobOrderPickOrderLotDetails(pickOrderId); | |||||
| setDetailLotData(lotDetails); | |||||
| console.log("✅ Fetched lot details:", lotDetails); | |||||
| } catch (error) { | |||||
| console.error("❌ Error fetching lot details:", error); | |||||
| setDetailLotData([]); | |||||
| } finally { | |||||
| setDetailLotDataLoading(false); | |||||
| } | |||||
| }, []); | |||||
| // ✅ 修改:初始化时获取数据 | |||||
| useEffect(() => { | |||||
| if (currentUserId) { | |||||
| fetchCompletedJobOrderPickOrdersData(); | |||||
| } | |||||
| }, [currentUserId, fetchCompletedJobOrderPickOrdersData]); | |||||
| // ✅ 修改:搜索功能 | |||||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||||
| setSearchQuery({ ...query }); | |||||
| console.log("Search query:", query); | |||||
| const filtered = completedJobOrderPickOrders.filter((pickOrder) => { | |||||
| const pickOrderCodeMatch = !query.pickOrderCode || | |||||
| pickOrder.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); | |||||
| const jobOrderCodeMatch = !query.jobOrderCode || | |||||
| pickOrder.jobOrderCode?.toLowerCase().includes((query.jobOrderCode || "").toLowerCase()); | |||||
| const jobOrderNameMatch = !query.jobOrderName || | |||||
| pickOrder.jobOrderName?.toLowerCase().includes((query.jobOrderName || "").toLowerCase()); | |||||
| return pickOrderCodeMatch && jobOrderCodeMatch && jobOrderNameMatch; | |||||
| }); | |||||
| setFilteredJobOrderPickOrders(filtered); | |||||
| console.log("Filtered Job Order pick orders count:", filtered.length); | |||||
| }, [completedJobOrderPickOrders]); | |||||
| // ✅ 修改:重置搜索 | |||||
| const handleSearchReset = useCallback(() => { | |||||
| setSearchQuery({}); | |||||
| setFilteredJobOrderPickOrders(completedJobOrderPickOrders); | |||||
| }, [completedJobOrderPickOrders]); | |||||
| // ✅ 修改:分页功能 | |||||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||||
| setPaginationController(prev => ({ | |||||
| ...prev, | |||||
| pageNum: newPage, | |||||
| })); | |||||
| }, []); | |||||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||||
| const newPageSize = parseInt(event.target.value, 10); | |||||
| setPaginationController({ | |||||
| pageNum: 0, | |||||
| pageSize: newPageSize, | |||||
| }); | |||||
| }, []); | |||||
| // ✅ 修改:分页数据 | |||||
| const paginatedData = useMemo(() => { | |||||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||||
| const endIndex = startIndex + paginationController.pageSize; | |||||
| return filteredJobOrderPickOrders.slice(startIndex, endIndex); | |||||
| }, [filteredJobOrderPickOrders, paginationController]); | |||||
| // ✅ 修改:搜索条件 | |||||
| const searchCriteria: Criterion<any>[] = [ | |||||
| { | |||||
| label: t("Pick Order Code"), | |||||
| paramName: "pickOrderCode", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Job Order Code"), | |||||
| paramName: "jobOrderCode", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Job Order Item Name"), | |||||
| paramName: "jobOrderName", | |||||
| type: "text", | |||||
| } | |||||
| ]; | |||||
| // ✅ 修改:详情点击处理 | |||||
| const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => { | |||||
| setSelectedJobOrderPickOrder(jobOrderPickOrder); | |||||
| setShowDetailView(true); | |||||
| // ✅ 获取 lot 详情数据 | |||||
| await fetchLotDetailsData(jobOrderPickOrder.pickOrderId); | |||||
| // ✅ 触发打印按钮状态更新 - 基于详情数据 | |||||
| const allCompleted = jobOrderPickOrder.secondScanCompleted; | |||||
| // ✅ 发送事件,包含标签页信息 | |||||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||||
| detail: { | |||||
| allLotsCompleted: allCompleted, | |||||
| tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件 | |||||
| } | |||||
| })); | |||||
| }, [fetchLotDetailsData]); | |||||
| // ✅ 修改:返回列表视图 | |||||
| const handleBackToList = useCallback(() => { | |||||
| setShowDetailView(false); | |||||
| setSelectedJobOrderPickOrder(null); | |||||
| setDetailLotData([]); | |||||
| // ✅ 返回列表时禁用打印按钮 | |||||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||||
| detail: { | |||||
| allLotsCompleted: false, | |||||
| tabIndex: 2 | |||||
| } | |||||
| })); | |||||
| }, []); | |||||
| // ✅ 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 | |||||
| if (showDetailView && selectedJobOrderPickOrder) { | |||||
| return ( | |||||
| <FormProvider {...formProps}> | |||||
| <Box> | |||||
| {/* 返回按钮和标题 */} | |||||
| <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}> | |||||
| <Button variant="outlined" onClick={handleBackToList}> | |||||
| {t("Back to List")} | |||||
| </Button> | |||||
| <Typography variant="h6"> | |||||
| {t("Job Order Pick Order Details")}: {selectedJobOrderPickOrder.pickOrderCode} | |||||
| </Typography> | |||||
| </Box> | |||||
| {/* Job Order 信息卡片 */} | |||||
| <Card sx={{ mb: 2 }}> | |||||
| <CardContent> | |||||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap"> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Pick Order Code")}:</strong> {selectedJobOrderPickOrder.pickOrderCode} | |||||
| </Typography> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Job Order Code")}:</strong> {selectedJobOrderPickOrder.jobOrderCode} | |||||
| </Typography> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Job Order Item Name")}:</strong> {selectedJobOrderPickOrder.jobOrderName} | |||||
| </Typography> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Target Date")}:</strong> {selectedJobOrderPickOrder.pickOrderTargetDate} | |||||
| </Typography> | |||||
| </Stack> | |||||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap" sx={{ mt: 2 }}> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} | |||||
| </Typography> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| {/* ✅ 修改:Lot 详情表格 - 添加复选框列 */} | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Typography variant="h6" gutterBottom> | |||||
| {t("Lot Details")} | |||||
| </Typography> | |||||
| {detailLotDataLoading ? ( | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Index")}</TableCell> | |||||
| <TableCell>{t("Route")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | |||||
| <TableCell>{t("Item Name")}</TableCell> | |||||
| <TableCell>{t("Lot No")}</TableCell> | |||||
| <TableCell>{t("Location")}</TableCell> | |||||
| <TableCell align="right">{t("Required Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Actual Pick Qty")}</TableCell> | |||||
| <TableCell align="center">{t("Processing Status")}</TableCell> | |||||
| <TableCell align="center">{t("Second Scan Status")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {detailLotData.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={10} align="center"> {/* ✅ 恢复原来的 colSpan */} | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No lot details available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| detailLotData.map((lot, index) => ( | |||||
| <TableRow key={lot.lotId}> | |||||
| <TableCell> | |||||
| <Typography variant="body2" fontWeight="bold"> | |||||
| {index + 1} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2"> | |||||
| {lot.routerRoute || '-'} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell>{lot.itemCode}</TableCell> | |||||
| <TableCell>{lot.itemName}</TableCell> | |||||
| <TableCell>{lot.lotNo}</TableCell> | |||||
| <TableCell>{lot.location}</TableCell> | |||||
| <TableCell align="right"> | |||||
| {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||||
| </TableCell> | |||||
| {/* ✅ 修改:Processing Status 使用复选框 */} | |||||
| <TableCell align="center"> | |||||
| <Box sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'center', | |||||
| alignItems: 'center', | |||||
| width: '100%', | |||||
| height: '100%' | |||||
| }}> | |||||
| <Checkbox | |||||
| checked={lot.processingStatus === 'completed'} | |||||
| disabled={true} | |||||
| readOnly={true} | |||||
| size="large" | |||||
| sx={{ | |||||
| color: lot.processingStatus === 'completed' ? 'success.main' : 'grey.400', | |||||
| '&.Mui-checked': { | |||||
| color: 'success.main', | |||||
| }, | |||||
| transform: 'scale(1.3)', | |||||
| '& .MuiSvgIcon-root': { | |||||
| fontSize: '1.5rem', | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </Box> | |||||
| </TableCell> | |||||
| {/* ✅ 修改:Second Scan Status 使用复选框 */} | |||||
| <TableCell align="center"> | |||||
| <Box sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'center', | |||||
| alignItems: 'center', | |||||
| width: '100%', | |||||
| height: '100%' | |||||
| }}> | |||||
| <Checkbox | |||||
| checked={lot.secondQrScanStatus === 'completed'} | |||||
| disabled={true} | |||||
| readOnly={true} | |||||
| size="large" | |||||
| sx={{ | |||||
| color: lot.secondQrScanStatus === 'completed' ? 'success.main' : 'grey.400', | |||||
| '&.Mui-checked': { | |||||
| color: 'success.main', | |||||
| }, | |||||
| transform: 'scale(1.3)', | |||||
| '& .MuiSvgIcon-root': { | |||||
| fontSize: '1.5rem', | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </Box> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| )} | |||||
| </CardContent> | |||||
| </Card> | |||||
| </Box> | |||||
| </FormProvider> | |||||
| ); | |||||
| } | |||||
| // ✅ 修改:默认列表视图 | |||||
| return ( | |||||
| <FormProvider {...formProps}> | |||||
| <Box> | |||||
| {/* 搜索框 */} | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={handleSearch} | |||||
| onReset={handleSearchReset} | |||||
| /> | |||||
| </Box> | |||||
| {/* 加载状态 */} | |||||
| {completedJobOrderPickOrdersLoading ? ( | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <Box> | |||||
| {/* 结果统计 */} | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||||
| {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} | |||||
| </Typography> | |||||
| {/* 列表 */} | |||||
| {filteredJobOrderPickOrders.length === 0 ? ( | |||||
| <Box sx={{ p: 3, textAlign: 'center' }}> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No completed Job Order pick orders with matching found")} | |||||
| </Typography> | |||||
| </Box> | |||||
| ) : ( | |||||
| <Stack spacing={2}> | |||||
| {paginatedData.map((jobOrderPickOrder) => ( | |||||
| <Card key={jobOrderPickOrder.id}> | |||||
| <CardContent> | |||||
| <Stack direction="row" justifyContent="space-between" alignItems="center"> | |||||
| <Box> | |||||
| <Typography variant="h6"> | |||||
| {jobOrderPickOrder.pickOrderCode} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.jobOrderCode} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} | |||||
| </Typography> | |||||
| </Box> | |||||
| <Box> | |||||
| <Chip | |||||
| label={jobOrderPickOrder.pickOrderStatus} | |||||
| color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | |||||
| size="small" | |||||
| sx={{ mb: 1 }} | |||||
| /> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} | |||||
| </Typography> | |||||
| <Chip | |||||
| label={jobOrderPickOrder.secondScanCompleted ? t("Second Scan Completed") : t("Second Scan Pending")} | |||||
| color={jobOrderPickOrder.secondScanCompleted ? 'success' : 'warning'} | |||||
| size="small" | |||||
| sx={{ mt: 1 }} | |||||
| /> | |||||
| </Box> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| <CardActions> | |||||
| <Button | |||||
| variant="outlined" | |||||
| onClick={() => handleDetailClick(jobOrderPickOrder)} | |||||
| > | |||||
| {t("View Details")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </Card> | |||||
| ))} | |||||
| </Stack> | |||||
| )} | |||||
| {/* 分页 */} | |||||
| {filteredJobOrderPickOrders.length > 0 && ( | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={filteredJobOrderPickOrders.length} | |||||
| page={paginationController.pageNum} | |||||
| rowsPerPage={paginationController.pageSize} | |||||
| onPageChange={handlePageChange} | |||||
| onRowsPerPageChange={handlePageSizeChange} | |||||
| rowsPerPageOptions={[5, 10, 25, 50]} | |||||
| /> | |||||
| )} | |||||
| </Box> | |||||
| )} | |||||
| </Box> | |||||
| </FormProvider> | |||||
| ); | |||||
| }; | |||||
| export default FInishedJobOrderRecord; | |||||
| @@ -0,0 +1,26 @@ | |||||
| import { fetchPickOrders } from "@/app/api/pickOrder"; | |||||
| import GeneralLoading from "../General/GeneralLoading"; | |||||
| import PickOrderSearch from "./FinishedGoodSearchWrapper"; | |||||
| interface SubComponents { | |||||
| Loading: typeof GeneralLoading; | |||||
| } | |||||
| const FinishedGoodSearchWrapper: React.FC & SubComponents = async () => { | |||||
| const [pickOrders] = await Promise.all([ | |||||
| fetchPickOrders({ | |||||
| code: undefined, | |||||
| targetDateFrom: undefined, | |||||
| targetDateTo: undefined, | |||||
| type: undefined, | |||||
| status: undefined, | |||||
| itemName: undefined, | |||||
| }), | |||||
| ]); | |||||
| return <FinishedGoodSearchWrapper pickOrders={pickOrders} />; | |||||
| }; | |||||
| FinishedGoodSearchWrapper.Loading = GeneralLoading; | |||||
| export default FinishedGoodSearchWrapper; | |||||
| @@ -0,0 +1,79 @@ | |||||
| import { ItemCombo } from "@/app/api/settings/item/actions"; | |||||
| import { Autocomplete, TextField } from "@mui/material"; | |||||
| import { useCallback, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| interface CommonProps { | |||||
| allItems: ItemCombo[]; | |||||
| error?: boolean; | |||||
| } | |||||
| interface SingleAutocompleteProps extends CommonProps { | |||||
| value: number | string | undefined; | |||||
| onItemSelect: (itemId: number, uom: string, uomId: number) => void | Promise<void>; | |||||
| // multiple: false; | |||||
| } | |||||
| type Props = SingleAutocompleteProps; | |||||
| const ItemSelect: React.FC<Props> = ({ | |||||
| allItems, | |||||
| value, | |||||
| error, | |||||
| onItemSelect | |||||
| }) => { | |||||
| const { t } = useTranslation("item"); | |||||
| const filteredItems = useMemo(() => { | |||||
| return allItems | |||||
| }, [allItems]) | |||||
| const options = useMemo(() => { | |||||
| return [ | |||||
| { | |||||
| value: -1, // think think sin | |||||
| label: t("None"), | |||||
| uom: "", | |||||
| uomId: -1, | |||||
| group: "default", | |||||
| }, | |||||
| ...filteredItems.map((i) => ({ | |||||
| value: i.id as number, | |||||
| label: i.label, | |||||
| uom: i.uom, | |||||
| uomId: i.uomId, | |||||
| group: "existing", | |||||
| })), | |||||
| ]; | |||||
| }, [t, filteredItems]); | |||||
| const currentValue = options.find((o) => o.value === value) || options[0]; | |||||
| const onChange = useCallback( | |||||
| ( | |||||
| event: React.SyntheticEvent, | |||||
| newValue: { value: number; uom: string; uomId: number; group: string } | { uom: string; uomId: number; value: number }[], | |||||
| ) => { | |||||
| const singleNewVal = newValue as { | |||||
| value: number; | |||||
| uom: string; | |||||
| uomId: number; | |||||
| group: string; | |||||
| }; | |||||
| onItemSelect(singleNewVal.value, singleNewVal.uom, singleNewVal.uomId) | |||||
| } | |||||
| , [onItemSelect]) | |||||
| return ( | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Item")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| value={currentValue} | |||||
| onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => <TextField {...params} error={error} />} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| export default ItemSelect | |||||
| @@ -0,0 +1,383 @@ | |||||
| // FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx | |||||
| "use client"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Dialog, | |||||
| DialogActions, | |||||
| DialogContent, | |||||
| DialogTitle, | |||||
| FormControl, | |||||
| Grid, | |||||
| InputLabel, | |||||
| MenuItem, | |||||
| Select, | |||||
| TextField, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { useCallback, useEffect, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions"; | |||||
| import { fetchEscalationCombo } from "@/app/api/user/actions"; | |||||
| interface LotPickData { | |||||
| id: number; | |||||
| lotId: number; | |||||
| lotNo: string; | |||||
| expiryDate: string; | |||||
| location: string; | |||||
| stockUnit: string; | |||||
| inQty: number; | |||||
| outQty: number; | |||||
| holdQty: number; | |||||
| totalPickedByAllPickOrders: number; | |||||
| availableQty: number; | |||||
| requiredQty: number; | |||||
| actualPickQty: number; | |||||
| lotStatus: string; | |||||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; | |||||
| stockOutLineId?: number; | |||||
| stockOutLineStatus?: string; | |||||
| stockOutLineQty?: number; | |||||
| } | |||||
| interface PickExecutionFormProps { | |||||
| open: boolean; | |||||
| onClose: () => void; | |||||
| onSubmit: (data: PickExecutionIssueData) => Promise<void>; | |||||
| selectedLot: LotPickData | null; | |||||
| selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | |||||
| pickOrderId?: number; | |||||
| pickOrderCreateDate: any; | |||||
| // ✅ Remove these props since we're not handling normal cases | |||||
| // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>; | |||||
| // selectedRowId?: number | null; | |||||
| } | |||||
| // 定义错误类型 | |||||
| interface FormErrors { | |||||
| actualPickQty?: string; | |||||
| missQty?: string; | |||||
| badItemQty?: string; | |||||
| issueRemark?: string; | |||||
| handledBy?: string; | |||||
| } | |||||
| const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| open, | |||||
| onClose, | |||||
| onSubmit, | |||||
| selectedLot, | |||||
| selectedPickOrderLine, | |||||
| pickOrderId, | |||||
| pickOrderCreateDate, | |||||
| // ✅ Remove these props | |||||
| // onNormalPickSubmit, | |||||
| // selectedRowId, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({}); | |||||
| const [errors, setErrors] = useState<FormErrors>({}); | |||||
| const [loading, setLoading] = useState(false); | |||||
| const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | |||||
| const [verifiedQty, setVerifiedQty] = useState<number>(0); | |||||
| // 计算剩余可用数量 | |||||
| const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | |||||
| const remainingQty = lot.inQty - lot.outQty; | |||||
| return Math.max(0, remainingQty); | |||||
| }, []); | |||||
| const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||||
| // ✅ Use the original required quantity, not subtracting actualPickQty | |||||
| // The actualPickQty in the form should be independent of the database value | |||||
| return lot.requiredQty || 0; | |||||
| }, []); | |||||
| // 获取处理人员列表 | |||||
| useEffect(() => { | |||||
| const fetchHandlers = async () => { | |||||
| try { | |||||
| const escalationCombo = await fetchEscalationCombo(); | |||||
| setHandlers(escalationCombo); | |||||
| } catch (error) { | |||||
| console.error("Error fetching handlers:", error); | |||||
| } | |||||
| }; | |||||
| fetchHandlers(); | |||||
| }, []); | |||||
| // 初始化表单数据 - 每次打开时都重新初始化 | |||||
| useEffect(() => { | |||||
| if (open && selectedLot && selectedPickOrderLine && pickOrderId) { | |||||
| const getSafeDate = (dateValue: any): string => { | |||||
| if (!dateValue) return new Date().toISOString().split('T')[0]; | |||||
| try { | |||||
| const date = new Date(dateValue); | |||||
| if (isNaN(date.getTime())) { | |||||
| return new Date().toISOString().split('T')[0]; | |||||
| } | |||||
| return date.toISOString().split('T')[0]; | |||||
| } catch { | |||||
| return new Date().toISOString().split('T')[0]; | |||||
| } | |||||
| }; | |||||
| // ✅ Initialize verified quantity to the received quantity (actualPickQty) | |||||
| const initialVerifiedQty = selectedLot.actualPickQty || 0; | |||||
| setVerifiedQty(initialVerifiedQty); | |||||
| console.log("=== PickExecutionForm Debug ==="); | |||||
| console.log("selectedLot:", selectedLot); | |||||
| console.log("initialVerifiedQty:", initialVerifiedQty); | |||||
| console.log("=== End Debug ==="); | |||||
| setFormData({ | |||||
| pickOrderId: pickOrderId, | |||||
| pickOrderCode: selectedPickOrderLine.pickOrderCode, | |||||
| pickOrderCreateDate: getSafeDate(pickOrderCreateDate), | |||||
| pickExecutionDate: new Date().toISOString().split('T')[0], | |||||
| pickOrderLineId: selectedPickOrderLine.id, | |||||
| itemId: selectedPickOrderLine.itemId, | |||||
| itemCode: selectedPickOrderLine.itemCode, | |||||
| itemDescription: selectedPickOrderLine.itemName, | |||||
| lotId: selectedLot.lotId, | |||||
| lotNo: selectedLot.lotNo, | |||||
| storeLocation: selectedLot.location, | |||||
| requiredQty: selectedLot.requiredQty, | |||||
| actualPickQty: initialVerifiedQty, // ✅ Use the initial value | |||||
| missQty: 0, | |||||
| badItemQty: 0, | |||||
| issueRemark: '', | |||||
| pickerName: '', | |||||
| handledBy: undefined, | |||||
| }); | |||||
| } | |||||
| }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]); | |||||
| const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { | |||||
| setFormData(prev => ({ ...prev, [field]: value })); | |||||
| // ✅ Update verified quantity state when actualPickQty changes | |||||
| if (field === 'actualPickQty') { | |||||
| setVerifiedQty(value); | |||||
| } | |||||
| // 清除错误 | |||||
| if (errors[field as keyof FormErrors]) { | |||||
| setErrors(prev => ({ ...prev, [field]: undefined })); | |||||
| } | |||||
| }, [errors]); | |||||
| // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 | |||||
| const validateForm = (): boolean => { | |||||
| const newErrors: FormErrors = {}; | |||||
| if (verifiedQty === undefined || verifiedQty < 0) { | |||||
| newErrors.actualPickQty = t('Qty is required'); | |||||
| } | |||||
| // ✅ Check if verified qty exceeds received qty | |||||
| if (verifiedQty > (selectedLot?.actualPickQty || 0)) { | |||||
| newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity'); | |||||
| } | |||||
| // ✅ Check if verified qty exceeds required qty | |||||
| if (verifiedQty > (selectedLot?.requiredQty || 0)) { | |||||
| newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); | |||||
| } | |||||
| // ✅ Require either missQty > 0 OR badItemQty > 0 | |||||
| const hasMissQty = formData.missQty && formData.missQty > 0; | |||||
| const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; | |||||
| if (!hasMissQty && !hasBadItemQty) { | |||||
| newErrors.missQty = t('At least one issue must be reported'); | |||||
| newErrors.badItemQty = t('At least one issue must be reported'); | |||||
| } | |||||
| setErrors(newErrors); | |||||
| return Object.keys(newErrors).length === 0; | |||||
| }; | |||||
| const handleSubmit = async () => { | |||||
| if (!validateForm() || !formData.pickOrderId) { | |||||
| return; | |||||
| } | |||||
| setLoading(true); | |||||
| try { | |||||
| // ✅ Use the verified quantity in the submission | |||||
| const submissionData = { | |||||
| ...formData, | |||||
| actualPickQty: verifiedQty | |||||
| } as PickExecutionIssueData; | |||||
| await onSubmit(submissionData); | |||||
| onClose(); | |||||
| } catch (error) { | |||||
| console.error('Error submitting pick execution issue:', error); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }; | |||||
| const handleClose = () => { | |||||
| setFormData({}); | |||||
| setErrors({}); | |||||
| setVerifiedQty(0); | |||||
| onClose(); | |||||
| }; | |||||
| if (!selectedLot || !selectedPickOrderLine) { | |||||
| return null; | |||||
| } | |||||
| const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); | |||||
| const requiredQty = calculateRequiredQty(selectedLot); | |||||
| return ( | |||||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | |||||
| <DialogTitle> | |||||
| {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} | |||||
| </DialogTitle> | |||||
| <DialogContent> | |||||
| <Box sx={{ mt: 2 }}> | |||||
| {/* ✅ Add instruction text */} | |||||
| <Grid container spacing={2}> | |||||
| <Grid item xs={12}> | |||||
| <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}> | |||||
| <Typography variant="body2" color="warning.main"> | |||||
| <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Grid> | |||||
| {/* ✅ Keep the existing form fields */} | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Required Qty')} | |||||
| value={selectedLot?.requiredQty || 0} | |||||
| disabled | |||||
| variant="outlined" | |||||
| // helperText={t('Still need to pick')} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Received Qty')} | |||||
| value={formData.actualPickQty || 0} | |||||
| disabled | |||||
| variant="outlined" | |||||
| // helperText={t('Available in warehouse')} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Verified Qty')} | |||||
| type="number" | |||||
| value={verifiedQty} // ✅ Use the separate state | |||||
| onChange={(e) => { | |||||
| const newValue = parseFloat(e.target.value) || 0; | |||||
| setVerifiedQty(newValue); | |||||
| handleInputChange('actualPickQty', newValue); | |||||
| }} | |||||
| error={!!errors.actualPickQty} | |||||
| helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(selectedLot?.actualPickQty || 0, selectedLot?.requiredQty || 0)}`} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Missing item Qty')} | |||||
| type="number" | |||||
| value={formData.missQty || 0} | |||||
| onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)} | |||||
| error={!!errors.missQty} | |||||
| // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t('Bad Item Qty')} | |||||
| type="number" | |||||
| value={formData.badItemQty || 0} | |||||
| onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} | |||||
| error={!!errors.badItemQty} | |||||
| // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | |||||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||||
| <> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| fullWidth | |||||
| id="issueRemark" | |||||
| label={t('Issue Remark')} | |||||
| multiline | |||||
| rows={4} | |||||
| value={formData.issueRemark || ''} | |||||
| onChange={(e) => handleInputChange('issueRemark', e.target.value)} | |||||
| error={!!errors.issueRemark} | |||||
| helperText={errors.issueRemark} | |||||
| //placeholder={t('Describe the issue with bad items')} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <FormControl fullWidth error={!!errors.handledBy}> | |||||
| <InputLabel>{t('handler')}</InputLabel> | |||||
| <Select | |||||
| value={formData.handledBy ? formData.handledBy.toString() : ''} | |||||
| onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)} | |||||
| label={t('handler')} | |||||
| > | |||||
| {handlers.map((handler) => ( | |||||
| <MenuItem key={handler.id} value={handler.id.toString()}> | |||||
| {handler.name} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | |||||
| {errors.handledBy && ( | |||||
| <Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}> | |||||
| {errors.handledBy} | |||||
| </Typography> | |||||
| )} | |||||
| </FormControl> | |||||
| </Grid> | |||||
| </> | |||||
| ) : (<></>)} | |||||
| </Grid> | |||||
| </Box> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={handleClose} disabled={loading}> | |||||
| {t('Cancel')} | |||||
| </Button> | |||||
| <Button | |||||
| onClick={handleSubmit} | |||||
| variant="contained" | |||||
| disabled={loading} | |||||
| > | |||||
| {loading ? t('submitting') : t('submit')} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| ); | |||||
| }; | |||||
| export default PickExecutionForm; | |||||
| @@ -0,0 +1,167 @@ | |||||
| import { Button, CircularProgress, Grid } from "@mui/material"; | |||||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { isEmpty, upperCase, upperFirst } from "lodash"; | |||||
| import { arrayToDateString, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import { | |||||
| consolidatePickOrder, | |||||
| fetchPickOrderClient, | |||||
| } from "@/app/api/pickOrder/actions"; | |||||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||||
| import dayjs from "dayjs"; | |||||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||||
| dayjs.extend(arraySupport); | |||||
| interface Props { | |||||
| filteredPickOrders: PickOrderResult[]; | |||||
| filterArgs: Record<string, any>; | |||||
| } | |||||
| const Jodetail: React.FC<Props> = ({ filteredPickOrders, filterArgs }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]); | |||||
| const [filteredPickOrder, setFilteredPickOrder] = useState( | |||||
| [] as PickOrderResult[], | |||||
| ); | |||||
| const { setIsUploading } = useUploadContext(); | |||||
| const [isLoading, setIsLoading] = useState(false); | |||||
| const [pagingController, setPagingController] = useState({ | |||||
| pageNum: 0, | |||||
| pageSize: 10, | |||||
| }); | |||||
| const [totalCount, setTotalCount] = useState<number>(); | |||||
| const fetchNewPagePickOrder = useCallback( | |||||
| async ( | |||||
| pagingController: Record<string, number>, | |||||
| filterArgs: Record<string, number>, | |||||
| ) => { | |||||
| setIsLoading(true); | |||||
| const params = { | |||||
| ...pagingController, | |||||
| ...filterArgs, | |||||
| }; | |||||
| const res = await fetchPickOrderClient(params); | |||||
| if (res) { | |||||
| console.log(res); | |||||
| setFilteredPickOrder(res.records); | |||||
| setTotalCount(res.total); | |||||
| } | |||||
| setIsLoading(false); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const handleConsolidatedRows = useCallback(async () => { | |||||
| console.log(selectedRows); | |||||
| setIsUploading(true); | |||||
| try { | |||||
| const res = await consolidatePickOrder(selectedRows as number[]); | |||||
| if (res) { | |||||
| console.log(res); | |||||
| } | |||||
| } catch { | |||||
| setIsUploading(false); | |||||
| } | |||||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||||
| setIsUploading(false); | |||||
| }, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]); | |||||
| useEffect(() => { | |||||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||||
| }, [fetchNewPagePickOrder, pagingController, filterArgs]); | |||||
| const columns = useMemo<Column<PickOrderResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "id", | |||||
| label: "", | |||||
| type: "checkbox", | |||||
| disabled: (params) => { | |||||
| return !isEmpty(params.consoCode); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "code", | |||||
| label: t("Code"), | |||||
| }, | |||||
| { | |||||
| name: "consoCode", | |||||
| label: t("Consolidated Code"), | |||||
| renderCell: (params) => { | |||||
| return params.consoCode ?? ""; | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "type", | |||||
| label: t("type"), | |||||
| renderCell: (params) => { | |||||
| return upperCase(params.type); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "items", | |||||
| label: t("Items"), | |||||
| renderCell: (params) => { | |||||
| return params.items?.map((i) => i.name).join(", "); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "targetDate", | |||||
| label: t("Target Date"), | |||||
| renderCell: (params) => { | |||||
| return ( | |||||
| dayjs(params.targetDate) | |||||
| .add(-1, "month") | |||||
| .format(OUTPUT_DATE_FORMAT) | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "releasedBy", | |||||
| label: t("Released By"), | |||||
| }, | |||||
| { | |||||
| name: "status", | |||||
| label: t("Status"), | |||||
| renderCell: (params) => { | |||||
| return upperFirst(params.status); | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| return ( | |||||
| <Grid container rowGap={1}> | |||||
| <Grid item xs={3}> | |||||
| <Button | |||||
| disabled={selectedRows.length < 1} | |||||
| variant="outlined" | |||||
| onClick={handleConsolidatedRows} | |||||
| > | |||||
| {t("Consolidate")} | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| {isLoading ? ( | |||||
| <CircularProgress size={40} /> | |||||
| ) : ( | |||||
| <SearchResults<PickOrderResult> | |||||
| items={filteredPickOrder} | |||||
| columns={columns} | |||||
| pagingController={pagingController} | |||||
| setPagingController={setPagingController} | |||||
| totalCount={totalCount} | |||||
| checkboxIds={selectedRows!} | |||||
| setCheckboxIds={setSelectedRows} | |||||
| /> | |||||
| )} | |||||
| </Grid> | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default Jodetail; | |||||
| @@ -0,0 +1,443 @@ | |||||
| "use client"; | |||||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { | |||||
| flatten, | |||||
| intersectionWith, | |||||
| isEmpty, | |||||
| sortBy, | |||||
| uniqBy, | |||||
| upperCase, | |||||
| upperFirst, | |||||
| } from "lodash"; | |||||
| import { | |||||
| arrayToDayjs, | |||||
| } from "@/app/utils/formatUtil"; | |||||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material"; | |||||
| import Jodetail from "./Jodetail" | |||||
| import PickExecution from "./JobPickExecution"; | |||||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | |||||
| import { fetchPickOrderClient, autoAssignAndReleasePickOrder, autoAssignAndReleasePickOrderByStore } from "@/app/api/pickOrder/actions"; | |||||
| import { useSession } from "next-auth/react"; | |||||
| import { SessionWithTokens } from "@/config/authConfig"; | |||||
| import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan"; | |||||
| import FInishedJobOrderRecord from "./FInishedJobOrderRecord"; | |||||
| import JobPickExecution from "./JobPickExecution"; | |||||
| import { | |||||
| fetchUnassignedJobOrderPickOrders, | |||||
| assignJobOrderPickOrder, | |||||
| fetchJobOrderLotsHierarchical, | |||||
| fetchCompletedJobOrderPickOrders, | |||||
| fetchCompletedJobOrderPickOrderRecords | |||||
| } from "@/app/api/jo/actions"; | |||||
| interface Props { | |||||
| pickOrders: PickOrderResult[]; | |||||
| } | |||||
| type SearchQuery = Partial< | |||||
| Omit<PickOrderResult, "id" | "consoCode" | "completeDate"> | |||||
| >; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const JodetailSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| const { t } = useTranslation("jo"); | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||||
| const [isOpenCreateModal, setIsOpenCreateModal] = useState(false) | |||||
| const [items, setItems] = useState<ItemCombo[]>([]) | |||||
| const [printButtonsEnabled, setPrintButtonsEnabled] = useState(false); | |||||
| const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | |||||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | |||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const [totalCount, setTotalCount] = useState<number>(); | |||||
| const [isAssigning, setIsAssigning] = useState(false); | |||||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||||
| const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | |||||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | |||||
| ); | |||||
| useEffect(() => { | |||||
| const onAssigned = () => { | |||||
| localStorage.removeItem('hideCompletedUntilNext'); | |||||
| setHideCompletedUntilNext(false); | |||||
| }; | |||||
| window.addEventListener('pickOrderAssigned', onAssigned); | |||||
| return () => window.removeEventListener('pickOrderAssigned', onAssigned); | |||||
| }, []); | |||||
| // ... existing code ... | |||||
| useEffect(() => { | |||||
| const handleCompletionStatusChange = (event: CustomEvent) => { | |||||
| const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail; | |||||
| // ✅ 修复:根据标签页和事件来源决定是否更新打印按钮状态 | |||||
| if (eventTabIndex === undefined || eventTabIndex === tabIndex) { | |||||
| setPrintButtonsEnabled(allLotsCompleted); | |||||
| console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted); | |||||
| } | |||||
| }; | |||||
| window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||||
| return () => { | |||||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||||
| }; | |||||
| }, [tabIndex]); // ✅ 添加 tabIndex 依赖 | |||||
| // ✅ 新增:处理标签页切换时的打印按钮状态重置 | |||||
| useEffect(() => { | |||||
| // 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态 | |||||
| if (tabIndex === 2) { | |||||
| setPrintButtonsEnabled(false); | |||||
| console.log("Reset print buttons for Pick Execution Record tab"); | |||||
| } | |||||
| }, [tabIndex]); | |||||
| // ... existing code ... | |||||
| const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | |||||
| if (!currentUserId) { | |||||
| console.error("Missing user id in session"); | |||||
| return; | |||||
| } | |||||
| setIsAssigning(true); | |||||
| try { | |||||
| const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId); | |||||
| console.log("Assign by store result:", res); | |||||
| // ✅ Handle different response codes | |||||
| if (res.code === "SUCCESS") { | |||||
| console.log("✅ Successfully assigned pick order to store", storeId); | |||||
| // ✅ Trigger refresh to show newly assigned data | |||||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||||
| } else if (res.code === "USER_BUSY") { | |||||
| console.warn("⚠️ User already has pick orders in progress:", res.message); | |||||
| // ✅ Show warning but still refresh to show existing orders | |||||
| alert(`Warning: ${res.message}`); | |||||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||||
| } else if (res.code === "NO_ORDERS") { | |||||
| console.log("ℹ️ No available pick orders for store", storeId); | |||||
| alert(`Info: ${res.message}`); | |||||
| } else { | |||||
| console.log("ℹ️ Assignment result:", res.message); | |||||
| alert(`Info: ${res.message}`); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("❌ Error assigning by store:", error); | |||||
| alert("Error occurred during assignment"); | |||||
| } finally { | |||||
| setIsAssigning(false); | |||||
| } | |||||
| }; | |||||
| // ✅ Manual assignment handler - uses the action function | |||||
| const loadUnassignedOrders = useCallback(async () => { | |||||
| setIsLoadingUnassigned(true); | |||||
| try { | |||||
| const orders = await fetchUnassignedJobOrderPickOrders(); | |||||
| setUnassignedOrders(orders); | |||||
| } catch (error) { | |||||
| console.error("Error loading unassigned orders:", error); | |||||
| } finally { | |||||
| setIsLoadingUnassigned(false); | |||||
| } | |||||
| }, []); | |||||
| // 分配订单给当前用户 | |||||
| const handleAssignOrder = useCallback(async (pickOrderId: number) => { | |||||
| if (!currentUserId) { | |||||
| console.error("Missing user id in session"); | |||||
| return; | |||||
| } | |||||
| try { | |||||
| const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); | |||||
| if (result.message === "Successfully assigned") { | |||||
| console.log("✅ Successfully assigned pick order"); | |||||
| // 刷新数据 | |||||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||||
| // 重新加载未分配订单列表 | |||||
| loadUnassignedOrders(); | |||||
| } else { | |||||
| console.warn("⚠️ Assignment failed:", result.message); | |||||
| alert(`Assignment failed: ${result.message}`); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("❌ Error assigning order:", error); | |||||
| alert("Error occurred during assignment"); | |||||
| } | |||||
| }, [currentUserId, loadUnassignedOrders]); | |||||
| // 在组件加载时获取未分配订单 | |||||
| useEffect(() => { | |||||
| loadUnassignedOrders(); | |||||
| }, [loadUnassignedOrders]); | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const openCreateModal = useCallback(async () => { | |||||
| console.log("testing") | |||||
| const res = await fetchAllItemsInClient() | |||||
| console.log(res) | |||||
| setItems(res) | |||||
| setIsOpenCreateModal(true) | |||||
| }, []) | |||||
| const closeCreateModal = useCallback(() => { | |||||
| setIsOpenCreateModal(false) | |||||
| }, []) | |||||
| useEffect(() => { | |||||
| if (tabIndex === 3) { | |||||
| const loadItems = async () => { | |||||
| try { | |||||
| const itemsData = await fetchAllItemsInClient(); | |||||
| console.log("PickOrderSearch loaded items:", itemsData.length); | |||||
| setItems(itemsData); | |||||
| } catch (error) { | |||||
| console.error("Error loading items in PickOrderSearch:", error); | |||||
| } | |||||
| }; | |||||
| // 如果还没有数据,则加载 | |||||
| if (items.length === 0) { | |||||
| loadItems(); | |||||
| } | |||||
| } | |||||
| }, [tabIndex, items.length]); | |||||
| useEffect(() => { | |||||
| const handleCompletionStatusChange = (event: CustomEvent) => { | |||||
| const { allLotsCompleted } = event.detail; | |||||
| setPrintButtonsEnabled(allLotsCompleted); | |||||
| console.log("Print buttons enabled:", allLotsCompleted); | |||||
| }; | |||||
| window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||||
| return () => { | |||||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||||
| }; | |||||
| }, []); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => { | |||||
| const baseCriteria: Criterion<SearchParamNames>[] = [ | |||||
| { | |||||
| label: tabIndex === 3 ? t("Item Code") : t("Code"), | |||||
| paramName: "code", | |||||
| type: "text" | |||||
| }, | |||||
| { | |||||
| label: t("Type"), | |||||
| paramName: "type", | |||||
| type: "autocomplete", | |||||
| options: tabIndex === 3 | |||||
| ? | |||||
| [ | |||||
| { value: "Consumable", label: t("Consumable") }, | |||||
| { value: "Material", label: t("Material") }, | |||||
| { value: "Product", label: t("Product") } | |||||
| ] | |||||
| : | |||||
| sortBy( | |||||
| uniqBy( | |||||
| pickOrders.map((po) => ({ | |||||
| value: po.type, | |||||
| label: t(upperCase(po.type)), | |||||
| })), | |||||
| "value", | |||||
| ), | |||||
| "label", | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| // Add Job Order search for Create Item tab (tabIndex === 3) | |||||
| if (tabIndex === 3) { | |||||
| baseCriteria.splice(1, 0, { | |||||
| label: t("Job Order"), | |||||
| paramName: "jobOrderCode" as any, // Type assertion for now | |||||
| type: "text", | |||||
| }); | |||||
| baseCriteria.splice(2, 0, { | |||||
| label: t("Target Date"), | |||||
| paramName: "targetDate", | |||||
| type: "date", | |||||
| }); | |||||
| } else { | |||||
| baseCriteria.splice(1, 0, { | |||||
| label: t("Target Date From"), | |||||
| label2: t("Target Date To"), | |||||
| paramName: "targetDate", | |||||
| type: "dateRange", | |||||
| }); | |||||
| } | |||||
| // Add Items/Item Name criteria | |||||
| baseCriteria.push({ | |||||
| label: tabIndex === 3 ? t("Item Name") : t("Items"), | |||||
| paramName: "items", | |||||
| type: tabIndex === 3 ? "text" : "autocomplete", | |||||
| options: tabIndex === 3 | |||||
| ? [] | |||||
| : | |||||
| uniqBy( | |||||
| flatten( | |||||
| sortBy( | |||||
| pickOrders.map((po) => | |||||
| po.items | |||||
| ? po.items.map((item) => ({ | |||||
| value: item.name, | |||||
| label: item.name, | |||||
| })) | |||||
| : [], | |||||
| ), | |||||
| "label", | |||||
| ), | |||||
| ), | |||||
| "value", | |||||
| ), | |||||
| }); | |||||
| // Add Status criteria for non-Create Item tabs | |||||
| if (tabIndex !== 3) { | |||||
| baseCriteria.push({ | |||||
| label: t("Status"), | |||||
| paramName: "status", | |||||
| type: "autocomplete", | |||||
| options: sortBy( | |||||
| uniqBy( | |||||
| pickOrders.map((po) => ({ | |||||
| value: po.status, | |||||
| label: t(upperFirst(po.status)), | |||||
| })), | |||||
| "value", | |||||
| ), | |||||
| "label", | |||||
| ), | |||||
| }); | |||||
| } | |||||
| return baseCriteria; | |||||
| }, | |||||
| [pickOrders, t, tabIndex, items], | |||||
| ); | |||||
| const fetchNewPagePickOrder = useCallback( | |||||
| async ( | |||||
| pagingController: Record<string, number>, | |||||
| filterArgs: Record<string, number>, | |||||
| ) => { | |||||
| const params = { | |||||
| ...pagingController, | |||||
| ...filterArgs, | |||||
| }; | |||||
| const res = await fetchPickOrderClient(params); | |||||
| if (res) { | |||||
| console.log(res); | |||||
| setFilteredPickOrders(res.records); | |||||
| setTotalCount(res.total); | |||||
| } | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const onReset = useCallback(() => { | |||||
| setFilteredPickOrders(pickOrders); | |||||
| }, [pickOrders]); | |||||
| useEffect(() => { | |||||
| if (!isOpenCreateModal) { | |||||
| setTabIndex(1) | |||||
| setTimeout(async () => { | |||||
| setTabIndex(0) | |||||
| }, 200) | |||||
| } | |||||
| }, [isOpenCreateModal]) | |||||
| // 添加处理提料单创建成功的函数 | |||||
| const handlePickOrderCreated = useCallback(() => { | |||||
| // 切换到 Assign & Release 标签页 (tabIndex = 1) | |||||
| setTabIndex(2); | |||||
| }, []); | |||||
| return ( | |||||
| <Box sx={{ | |||||
| height: '100vh', // Full viewport height | |||||
| overflow: 'auto' // Single scrollbar for the whole page | |||||
| }}> | |||||
| {/* Header section */} | |||||
| <Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}> | |||||
| <Stack rowGap={2}> | |||||
| <Grid container alignItems="center"> | |||||
| <Grid item xs={8}> | |||||
| </Grid> | |||||
| {/* Last 2 buttons aligned right */} | |||||
| <Grid item xs={6} > | |||||
| {/* Unassigned Job Orders */} | |||||
| {unassignedOrders.length > 0 && ( | |||||
| <Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}> | |||||
| <Typography variant="h6" gutterBottom> | |||||
| {t("Unassigned Job Orders")} ({unassignedOrders.length}) | |||||
| </Typography> | |||||
| <Stack direction="row" spacing={1} flexWrap="wrap"> | |||||
| {unassignedOrders.map((order) => ( | |||||
| <Button | |||||
| key={order.pickOrderId} | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => handleAssignOrder(order.pickOrderId)} | |||||
| disabled={isLoadingUnassigned} | |||||
| > | |||||
| {order.pickOrderCode} - {order.jobOrderName} | |||||
| </Button> | |||||
| ))} | |||||
| </Stack> | |||||
| </Box> | |||||
| )} | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Stack> | |||||
| </Box> | |||||
| {/* Tabs section - ✅ Move the click handler here */} | |||||
| <Box sx={{ | |||||
| borderBottom: '1px solid #e0e0e0' | |||||
| }}> | |||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||||
| <Tab label={t("Pick Order Detail")} iconPosition="end" /> | |||||
| <Tab label={t("Job Order Match")} iconPosition="end" /> | |||||
| <Tab label={t("Finished Job Order Record")} iconPosition="end" /> | |||||
| </Tabs> | |||||
| </Box> | |||||
| {/* Content section - NO overflow: 'auto' here */} | |||||
| <Box sx={{ | |||||
| p: 2 | |||||
| }}> | |||||
| {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} | |||||
| {tabIndex === 1 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} | |||||
| {tabIndex === 2 && <FInishedJobOrderRecord filterArgs={filterArgs} />} | |||||
| </Box> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default JodetailSearch; | |||||
| @@ -0,0 +1,124 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Dialog, | |||||
| DialogActions, | |||||
| DialogContent, | |||||
| DialogTitle, | |||||
| Typography, | |||||
| Alert, | |||||
| Stack, | |||||
| Divider, | |||||
| } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| interface LotConfirmationModalProps { | |||||
| open: boolean; | |||||
| onClose: () => void; | |||||
| onConfirm: () => void; | |||||
| expectedLot: { | |||||
| lotNo: string; | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| }; | |||||
| scannedLot: { | |||||
| lotNo: string; | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| }; | |||||
| isLoading?: boolean; | |||||
| } | |||||
| const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({ | |||||
| open, | |||||
| onClose, | |||||
| onConfirm, | |||||
| expectedLot, | |||||
| scannedLot, | |||||
| isLoading = false, | |||||
| }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| return ( | |||||
| <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> | |||||
| <DialogTitle> | |||||
| <Typography variant="h6" component="div" color="warning.main"> | |||||
| {t("Lot Number Mismatch")} | |||||
| </Typography> | |||||
| </DialogTitle> | |||||
| <DialogContent> | |||||
| <Stack spacing={3}> | |||||
| <Alert severity="warning"> | |||||
| {t("The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?")} | |||||
| </Alert> | |||||
| <Box> | |||||
| <Typography variant="subtitle1" gutterBottom color="primary"> | |||||
| {t("Expected Lot:")} | |||||
| </Typography> | |||||
| <Box sx={{ pl: 2, py: 1, backgroundColor: 'grey.50', borderRadius: 1 }}> | |||||
| <Typography variant="body2"> | |||||
| <strong>{t("Item Code")}:</strong> {expectedLot.itemCode} | |||||
| </Typography> | |||||
| <Typography variant="body2"> | |||||
| <strong>{t("Item Name")}:</strong> {expectedLot.itemName} | |||||
| </Typography> | |||||
| <Typography variant="body2"> | |||||
| <strong>{t("Lot No")}:</strong> {expectedLot.lotNo} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Box> | |||||
| <Divider /> | |||||
| <Box> | |||||
| <Typography variant="subtitle1" gutterBottom color="warning.main"> | |||||
| {t("Scanned Lot:")} | |||||
| </Typography> | |||||
| <Box sx={{ pl: 2, py: 1, backgroundColor: 'warning.50', borderRadius: 1 }}> | |||||
| <Typography variant="body2"> | |||||
| <strong>{t("Item Code")}:</strong> {scannedLot.itemCode} | |||||
| </Typography> | |||||
| <Typography variant="body2"> | |||||
| <strong>{t("Item Name")}:</strong> {scannedLot.itemName} | |||||
| </Typography> | |||||
| <Typography variant="body2"> | |||||
| <strong>{t("Lot No")}:</strong> {scannedLot.lotNo} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Box> | |||||
| <Alert severity="info"> | |||||
| {t("If you confirm, the system will:")} | |||||
| <ul style={{ margin: '8px 0 0 16px' }}> | |||||
| <li>{t("Update your suggested lot to the this scanned lot")}</li> | |||||
| </ul> | |||||
| </Alert> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button | |||||
| onClick={onClose} | |||||
| variant="outlined" | |||||
| disabled={isLoading} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button | |||||
| onClick={onConfirm} | |||||
| variant="contained" | |||||
| color="warning" | |||||
| disabled={isLoading} | |||||
| > | |||||
| {isLoading ? t("Processing...") : t("Confirm")} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| ); | |||||
| }; | |||||
| export default LotConfirmationModal; | |||||
| @@ -0,0 +1,527 @@ | |||||
| "use client"; | |||||
| import { PurchaseQcResult, PutAwayInput, PutAwayLine } from "@/app/api/po/actions"; | |||||
| import { | |||||
| Autocomplete, | |||||
| Box, | |||||
| Button, | |||||
| Card, | |||||
| CardContent, | |||||
| FormControl, | |||||
| Grid, | |||||
| Modal, | |||||
| ModalProps, | |||||
| Stack, | |||||
| TextField, | |||||
| Tooltip, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import StyledDataGrid from "../StyledDataGrid"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| GridColDef, | |||||
| GridRowIdGetter, | |||||
| GridRowModel, | |||||
| useGridApiContext, | |||||
| GridRenderCellParams, | |||||
| GridRenderEditCellParams, | |||||
| useGridApiRef, | |||||
| } from "@mui/x-data-grid"; | |||||
| import InputDataGrid from "../InputDataGrid"; | |||||
| import { TableRow } from "../InputDataGrid/InputDataGrid"; | |||||
| import TwoLineCell from "./TwoLineCell"; | |||||
| import QcSelect from "./QcSelect"; | |||||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||||
| import { StockInLine } from "@/app/api/po"; | |||||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||||
| import { | |||||
| OUTPUT_DATE_FORMAT, | |||||
| stockInLineStatusMap, | |||||
| } from "@/app/utils/formatUtil"; | |||||
| import { QRCodeSVG } from "qrcode.react"; | |||||
| import { QrCode } from "../QrCode"; | |||||
| import ReactQrCodeScanner, { | |||||
| ScannerConfig, | |||||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
| import dayjs from "dayjs"; | |||||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||||
| import { dummyPutawayLine } from "./dummyQcTemplate"; | |||||
| dayjs.extend(arraySupport); | |||||
| interface Props { | |||||
| itemDetail: StockInLine; | |||||
| warehouse: WarehouseResult[]; | |||||
| disabled: boolean; | |||||
| // qc: QcItemWithChecks[]; | |||||
| } | |||||
| type EntryError = | |||||
| | { | |||||
| [field in keyof PutAwayLine]?: string; | |||||
| } | |||||
| | undefined; | |||||
| type PutawayRow = TableRow<Partial<PutAwayLine>, EntryError>; | |||||
| const style = { | |||||
| position: "absolute", | |||||
| top: "50%", | |||||
| left: "50%", | |||||
| transform: "translate(-50%, -50%)", | |||||
| bgcolor: "background.paper", | |||||
| pt: 5, | |||||
| px: 5, | |||||
| pb: 10, | |||||
| width: "auto", | |||||
| }; | |||||
| const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled }) => { | |||||
| const { t } = useTranslation("purchaseOrder"); | |||||
| const apiRef = useGridApiRef(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors, defaultValues, touchedFields }, | |||||
| watch, | |||||
| control, | |||||
| setValue, | |||||
| getValues, | |||||
| reset, | |||||
| resetField, | |||||
| setError, | |||||
| clearErrors, | |||||
| } = useFormContext<PutAwayInput>(); | |||||
| console.log(itemDetail); | |||||
| // const [recordQty, setRecordQty] = useState(0); | |||||
| const [warehouseId, setWarehouseId] = useState(itemDetail.defaultWarehouseId); | |||||
| const filteredWarehouse = useMemo(() => { | |||||
| // do filtering here if any | |||||
| return warehouse; | |||||
| }, []); | |||||
| const defaultOption = { | |||||
| value: 0, // think think sin | |||||
| label: t("Select warehouse"), | |||||
| group: "default", | |||||
| }; | |||||
| const options = useMemo(() => { | |||||
| return [ | |||||
| // { | |||||
| // value: 0, // think think sin | |||||
| // label: t("Select warehouse"), | |||||
| // group: "default", | |||||
| // }, | |||||
| ...filteredWarehouse.map((w) => ({ | |||||
| value: w.id, | |||||
| label: `${w.code} - ${w.name}`, | |||||
| group: "existing", | |||||
| })), | |||||
| ]; | |||||
| }, [filteredWarehouse]); | |||||
| const currentValue = | |||||
| warehouseId > 0 | |||||
| ? options.find((o) => o.value === warehouseId) | |||||
| : options.find((o) => o.value === getValues("warehouseId")) || | |||||
| defaultOption; | |||||
| const onChange = useCallback( | |||||
| ( | |||||
| event: React.SyntheticEvent, | |||||
| newValue: { value: number; group: string } | { value: number }[], | |||||
| ) => { | |||||
| const singleNewVal = newValue as { | |||||
| value: number; | |||||
| group: string; | |||||
| }; | |||||
| console.log(singleNewVal); | |||||
| console.log("onChange"); | |||||
| // setValue("warehouseId", singleNewVal.value); | |||||
| setWarehouseId(singleNewVal.value); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| console.log(watch("putAwayLines")) | |||||
| // const accQty = watch("acceptedQty"); | |||||
| // const validateForm = useCallback(() => { | |||||
| // console.log(accQty); | |||||
| // if (accQty > itemDetail.acceptedQty) { | |||||
| // setError("acceptedQty", { | |||||
| // message: `acceptedQty must not greater than ${itemDetail.acceptedQty}`, | |||||
| // type: "required", | |||||
| // }); | |||||
| // } | |||||
| // if (accQty < 1) { | |||||
| // setError("acceptedQty", { | |||||
| // message: `minimal value is 1`, | |||||
| // type: "required", | |||||
| // }); | |||||
| // } | |||||
| // if (isNaN(accQty)) { | |||||
| // setError("acceptedQty", { | |||||
| // message: `value must be a number`, | |||||
| // type: "required", | |||||
| // }); | |||||
| // } | |||||
| // }, [accQty]); | |||||
| // useEffect(() => { | |||||
| // clearErrors(); | |||||
| // validateForm(); | |||||
| // }, [validateForm]); | |||||
| const qrContent = useMemo( | |||||
| () => ({ | |||||
| stockInLineId: itemDetail.id, | |||||
| itemId: itemDetail.itemId, | |||||
| lotNo: itemDetail.lotNo, | |||||
| // warehouseId: 2 // for testing | |||||
| // expiryDate: itemDetail.expiryDate, | |||||
| // productionDate: itemDetail.productionDate, | |||||
| // supplier: itemDetail.supplier, | |||||
| // poCode: itemDetail.poCode, | |||||
| }), | |||||
| [itemDetail], | |||||
| ); | |||||
| const [isOpenScanner, setOpenScanner] = useState(false); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (...args) => { | |||||
| setOpenScanner(false); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const onOpenScanner = useCallback(() => { | |||||
| setOpenScanner(true); | |||||
| }, []); | |||||
| const onCloseScanner = useCallback(() => { | |||||
| setOpenScanner(false); | |||||
| }, []); | |||||
| const scannerConfig = useMemo<ScannerConfig>( | |||||
| () => ({ | |||||
| onUpdate: (err, result) => { | |||||
| console.log(result); | |||||
| console.log(Boolean(result)); | |||||
| if (result) { | |||||
| const data: QrCodeInfo = JSON.parse(result.getText()); | |||||
| console.log(data); | |||||
| if (data.warehouseId) { | |||||
| console.log(data.warehouseId); | |||||
| setWarehouseId(data.warehouseId); | |||||
| onCloseScanner(); | |||||
| } | |||||
| } else return; | |||||
| }, | |||||
| }), | |||||
| [onCloseScanner], | |||||
| ); | |||||
| // QR Code Scanner | |||||
| const scanner = useQrCodeScannerContext(); | |||||
| useEffect(() => { | |||||
| if (isOpenScanner) { | |||||
| scanner.startScan(); | |||||
| } else if (!isOpenScanner) { | |||||
| scanner.stopScan(); | |||||
| } | |||||
| }, [isOpenScanner]); | |||||
| useEffect(() => { | |||||
| if (scanner.values.length > 0) { | |||||
| console.log(scanner.values[0]); | |||||
| const data: QrCodeInfo = JSON.parse(scanner.values[0]); | |||||
| console.log(data); | |||||
| if (data.warehouseId) { | |||||
| console.log(data.warehouseId); | |||||
| setWarehouseId(data.warehouseId); | |||||
| onCloseScanner(); | |||||
| } | |||||
| scanner.resetScan(); | |||||
| } | |||||
| }, [scanner.values]); | |||||
| useEffect(() => { | |||||
| setValue("status", "completed"); | |||||
| setValue("warehouseId", options[0].value); | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| if (warehouseId > 0) { | |||||
| setValue("warehouseId", warehouseId); | |||||
| clearErrors("warehouseId"); | |||||
| } | |||||
| }, [warehouseId]); | |||||
| const getWarningTextHardcode = useCallback((): string | undefined => { | |||||
| console.log(options) | |||||
| if (options.length === 0) return undefined | |||||
| const defaultWarehouseId = options[0].value; | |||||
| const currWarehouseId = watch("warehouseId"); | |||||
| if (defaultWarehouseId !== currWarehouseId) { | |||||
| return t("not default warehosue"); | |||||
| } | |||||
| return undefined; | |||||
| }, [options]); | |||||
| const columns = useMemo<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| field: "qty", | |||||
| headerName: t("qty"), | |||||
| flex: 1, | |||||
| // renderCell(params) { | |||||
| // return <>100</> | |||||
| // }, | |||||
| }, | |||||
| { | |||||
| field: "warehouse", | |||||
| headerName: t("warehouse"), | |||||
| flex: 1, | |||||
| // renderCell(params) { | |||||
| // return <>{filteredWarehouse[0].name}</> | |||||
| // }, | |||||
| }, | |||||
| { | |||||
| field: "printQty", | |||||
| headerName: t("printQty"), | |||||
| flex: 1, | |||||
| // renderCell(params) { | |||||
| // return <>100</> | |||||
| // }, | |||||
| }, | |||||
| ], []) | |||||
| const validation = useCallback( | |||||
| (newRow: GridRowModel<PutawayRow>): EntryError => { | |||||
| const error: EntryError = {}; | |||||
| const { qty, warehouseId, printQty } = newRow; | |||||
| return Object.keys(error).length > 0 ? error : undefined; | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| return ( | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Putaway Detail")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| label={t("LotNo")} | |||||
| fullWidth | |||||
| value={itemDetail.lotNo} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Supplier")} | |||||
| fullWidth | |||||
| value={itemDetail.supplier} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Po Code")} | |||||
| fullWidth | |||||
| value={itemDetail.poCode} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemName")} | |||||
| fullWidth | |||||
| value={itemDetail.itemName} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemNo")} | |||||
| fullWidth | |||||
| value={itemDetail.itemNo} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("qty")} | |||||
| fullWidth | |||||
| value={itemDetail.acceptedQty} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("productionDate")} | |||||
| fullWidth | |||||
| value={ | |||||
| // dayjs(itemDetail.productionDate) | |||||
| dayjs() | |||||
| // .add(-1, "month") | |||||
| .format(OUTPUT_DATE_FORMAT)} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("expiryDate")} | |||||
| fullWidth | |||||
| value={ | |||||
| // dayjs(itemDetail.expiryDate) | |||||
| dayjs() | |||||
| .add(20, "day") | |||||
| .format(OUTPUT_DATE_FORMAT)} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <FormControl fullWidth> | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Warehouse")} | |||||
| disableClearable | |||||
| disabled | |||||
| fullWidth | |||||
| defaultValue={options[0]} /// modify this later | |||||
| // onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => ( | |||||
| <TextField {...params} label={t("Default Warehouse")} /> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| {/* <Grid item xs={5.5}> | |||||
| <TextField | |||||
| label={t("acceptedQty")} | |||||
| fullWidth | |||||
| {...register("acceptedQty", { | |||||
| required: "acceptedQty required!", | |||||
| min: 1, | |||||
| max: itemDetail.acceptedQty, | |||||
| valueAsNumber: true, | |||||
| })} | |||||
| // defaultValue={itemDetail.acceptedQty} | |||||
| disabled={disabled} | |||||
| error={Boolean(errors.acceptedQty)} | |||||
| helperText={errors.acceptedQty?.message} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={1}> | |||||
| <Button disabled={disabled} onClick={onOpenScanner}> | |||||
| {t("bind")} | |||||
| </Button> | |||||
| </Grid> */} | |||||
| {/* <Grid item xs={5.5}> | |||||
| <Controller | |||||
| control={control} | |||||
| name="warehouseId" | |||||
| render={({ field }) => { | |||||
| console.log(field); | |||||
| return ( | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Warehouse")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| value={options.find((o) => o.value == field.value)} | |||||
| onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={"Select warehouse"} | |||||
| error={Boolean(errors.warehouseId?.message)} | |||||
| helperText={warehouseHelperText} | |||||
| // helperText={errors.warehouseId?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| <FormControl fullWidth> | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Warehouse")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| // value={warehouseId > 0 | |||||
| // ? options.find((o) => o.value === warehouseId) | |||||
| // : undefined} | |||||
| defaultValue={options[0]} | |||||
| // defaultValue={options.find((o) => o.value === 1)} | |||||
| value={currentValue} | |||||
| onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| // label={"Select warehouse"} | |||||
| disabled={disabled} | |||||
| error={Boolean(errors.warehouseId?.message)} | |||||
| helperText={ | |||||
| errors.warehouseId?.message ?? getWarningTextHardcode() | |||||
| } | |||||
| // helperText={warehouseHelperText} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> */} | |||||
| <Grid | |||||
| item | |||||
| xs={12} | |||||
| style={{ display: "flex", justifyContent: "center" }} | |||||
| > | |||||
| {/* <QrCode content={qrContent} sx={{ width: 200, height: 200 }} /> */} | |||||
| <InputDataGrid<PutAwayInput, PutAwayLine, EntryError> | |||||
| apiRef={apiRef} | |||||
| checkboxSelection={false} | |||||
| _formKey={"putAwayLines"} | |||||
| columns={columns} | |||||
| validateRow={validation} | |||||
| needAdd={true} | |||||
| showRemoveBtn={false} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| {/* <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Button onClick={onOpenScanner}>bind</Button> | |||||
| </Grid> */} | |||||
| <Modal open={isOpenScanner} onClose={closeHandler}> | |||||
| <Box sx={style}> | |||||
| <Typography variant="h4"> | |||||
| {t("Please scan warehouse qr code.")} | |||||
| </Typography> | |||||
| {/* <ReactQrCodeScanner scannerConfig={scannerConfig} /> */} | |||||
| </Box> | |||||
| </Modal> | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default PutawayForm; | |||||
| @@ -0,0 +1,395 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Dispatch, | |||||
| MutableRefObject, | |||||
| SetStateAction, | |||||
| useCallback, | |||||
| useEffect, | |||||
| useMemo, | |||||
| useState, | |||||
| } from "react"; | |||||
| import StyledDataGrid from "../StyledDataGrid"; | |||||
| import { | |||||
| FooterPropsOverrides, | |||||
| GridActionsCellItem, | |||||
| GridCellParams, | |||||
| GridColDef, | |||||
| GridEventListener, | |||||
| GridRowEditStopReasons, | |||||
| GridRowId, | |||||
| GridRowIdGetter, | |||||
| GridRowModel, | |||||
| GridRowModes, | |||||
| GridRowModesModel, | |||||
| GridRowSelectionModel, | |||||
| GridToolbarContainer, | |||||
| GridValidRowModel, | |||||
| useGridApiRef, | |||||
| } from "@mui/x-data-grid"; | |||||
| import { set, useFormContext } from "react-hook-form"; | |||||
| import SaveIcon from "@mui/icons-material/Save"; | |||||
| import DeleteIcon from "@mui/icons-material/Delete"; | |||||
| import CancelIcon from "@mui/icons-material/Cancel"; | |||||
| import { Add } from "@mui/icons-material"; | |||||
| import { Box, Button, Typography } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| GridApiCommunity, | |||||
| GridSlotsComponentsProps, | |||||
| } from "@mui/x-data-grid/internals"; | |||||
| import { dummyQCData } from "./dummyQcTemplate"; | |||||
| // T == CreatexxxInputs map of the form's fields | |||||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||||
| // E == error | |||||
| interface ResultWithId { | |||||
| id: string | number; | |||||
| } | |||||
| // export type InputGridProps = { | |||||
| // [key: string]: any | |||||
| // } | |||||
| interface DefaultResult<E> { | |||||
| _isNew: boolean; | |||||
| _error: E; | |||||
| } | |||||
| interface SelectionResult<E> { | |||||
| active: boolean; | |||||
| _isNew: boolean; | |||||
| _error: E; | |||||
| } | |||||
| type Result<E> = DefaultResult<E> | SelectionResult<E>; | |||||
| export type TableRow<V, E> = Partial< | |||||
| V & { | |||||
| isActive: boolean | undefined; | |||||
| _isNew: boolean; | |||||
| _error: E; | |||||
| } & ResultWithId | |||||
| >; | |||||
| export interface InputDataGridProps<T, V, E> { | |||||
| apiRef: MutableRefObject<GridApiCommunity>; | |||||
| // checkboxSelection: false | undefined; | |||||
| _formKey: keyof T; | |||||
| columns: GridColDef[]; | |||||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||||
| needAdd?: boolean; | |||||
| } | |||||
| export interface SelectionInputDataGridProps<T, V, E> { | |||||
| // thinking how do | |||||
| apiRef: MutableRefObject<GridApiCommunity>; | |||||
| // checkboxSelection: true; | |||||
| _formKey: keyof T; | |||||
| columns: GridColDef[]; | |||||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||||
| } | |||||
| export type Props<T, V, E> = | |||||
| | InputDataGridProps<T, V, E> | |||||
| | SelectionInputDataGridProps<T, V, E>; | |||||
| export class ProcessRowUpdateError<T, E> extends Error { | |||||
| public readonly row: T; | |||||
| public readonly errors: E | undefined; | |||||
| constructor(row: T, message?: string, errors?: E) { | |||||
| super(message); | |||||
| this.row = row; | |||||
| this.errors = errors; | |||||
| Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); | |||||
| } | |||||
| } | |||||
| // T == CreatexxxInputs map of the form's fields | |||||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||||
| // E == error | |||||
| function InputDataGrid<T, V, E>({ | |||||
| apiRef, | |||||
| // checkboxSelection = false, | |||||
| _formKey, | |||||
| columns, | |||||
| validateRow, | |||||
| }: Props<T, V, E>) { | |||||
| const { | |||||
| t, | |||||
| // i18n: { language }, | |||||
| } = useTranslation("purchaseOrder"); | |||||
| const formKey = _formKey.toString(); | |||||
| const { setValue, getValues } = useFormContext(); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||||
| // const apiRef = useGridApiRef(); | |||||
| const getRowId = useCallback<GridRowIdGetter<TableRow<V, E>>>( | |||||
| (row) => row.id! as number, | |||||
| [], | |||||
| ); | |||||
| const formValue = getValues(formKey) | |||||
| const list: TableRow<V, E>[] = !formValue || formValue.length == 0 ? dummyQCData : getValues(formKey); | |||||
| console.log(list) | |||||
| const [rows, setRows] = useState<TableRow<V, E>[]>(() => { | |||||
| // const list: TableRow<V, E>[] = getValues(formKey); | |||||
| console.log(list) | |||||
| return list && list.length > 0 ? list : []; | |||||
| }); | |||||
| console.log(rows) | |||||
| // const originalRows = list && list.length > 0 ? list : []; | |||||
| const originalRows = useMemo(() => ( | |||||
| list && list.length > 0 ? list : [] | |||||
| ), [list]) | |||||
| // const originalRowModel = originalRows.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel | |||||
| const [rowSelectionModel, setRowSelectionModel] = | |||||
| useState<GridRowSelectionModel>(() => { | |||||
| // const rowModel = list.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel | |||||
| const rowModel: GridRowSelectionModel = getValues( | |||||
| `${formKey}_active`, | |||||
| ) as GridRowSelectionModel; | |||||
| console.log(rowModel); | |||||
| return rowModel; | |||||
| }); | |||||
| useEffect(() => { | |||||
| for (let i = 0; i < rows.length; i++) { | |||||
| const currRow = rows[i] | |||||
| setRowModesModel((prevRowModesModel) => ({ | |||||
| ...prevRowModesModel, | |||||
| [currRow.id as number]: { mode: GridRowModes.View }, | |||||
| })); | |||||
| } | |||||
| }, [rows]) | |||||
| const handleSave = useCallback( | |||||
| (id: GridRowId) => () => { | |||||
| setRowModesModel((prevRowModesModel) => ({ | |||||
| ...prevRowModesModel, | |||||
| [id]: { mode: GridRowModes.View }, | |||||
| })); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const onProcessRowUpdateError = useCallback( | |||||
| (updateError: ProcessRowUpdateError<T, E>) => { | |||||
| const errors = updateError.errors; | |||||
| const row = updateError.row; | |||||
| console.log(errors); | |||||
| apiRef.current.updateRows([{ ...row, _error: errors }]); | |||||
| }, | |||||
| [apiRef], | |||||
| ); | |||||
| const processRowUpdate = useCallback( | |||||
| ( | |||||
| newRow: GridRowModel<TableRow<V, E>>, | |||||
| originalRow: GridRowModel<TableRow<V, E>>, | |||||
| ) => { | |||||
| ///////////////// | |||||
| // validation here | |||||
| const errors = validateRow(newRow); | |||||
| console.log(newRow); | |||||
| if (errors) { | |||||
| throw new ProcessRowUpdateError( | |||||
| originalRow, | |||||
| "validation error", | |||||
| errors, | |||||
| ); | |||||
| } | |||||
| ///////////////// | |||||
| const { _isNew, _error, ...updatedRow } = newRow; | |||||
| const rowToSave = { | |||||
| ...updatedRow, | |||||
| } as TableRow<V, E>; /// test | |||||
| console.log(rowToSave); | |||||
| setRows((rw) => | |||||
| rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)), | |||||
| ); | |||||
| return rowToSave; | |||||
| }, | |||||
| [validateRow, getRowId], | |||||
| ); | |||||
| const addRow = useCallback(() => { | |||||
| const newEntry = { id: Date.now(), _isNew: true } as TableRow<V, E>; | |||||
| setRows((prev) => [...prev, newEntry]); | |||||
| setRowModesModel((model) => ({ | |||||
| ...model, | |||||
| [getRowId(newEntry)]: { | |||||
| mode: GridRowModes.Edit, | |||||
| // fieldToFocus: "team", /// test | |||||
| }, | |||||
| })); | |||||
| }, [getRowId]); | |||||
| const reset = useCallback(() => { | |||||
| setRowModesModel({}); | |||||
| setRows(originalRows); | |||||
| }, [originalRows]); | |||||
| const handleCancel = useCallback( | |||||
| (id: GridRowId) => () => { | |||||
| setRowModesModel((model) => ({ | |||||
| ...model, | |||||
| [id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||||
| })); | |||||
| const editedRow = rows.find((row) => getRowId(row) === id); | |||||
| if (editedRow?._isNew) { | |||||
| setRows((rw) => rw.filter((r) => getRowId(r) !== id)); | |||||
| } else { | |||||
| setRows((rw) => | |||||
| rw.map((r) => (getRowId(r) === id ? { ...r, _error: undefined } : r)), | |||||
| ); | |||||
| } | |||||
| }, | |||||
| [rows, getRowId], | |||||
| ); | |||||
| const handleDelete = useCallback( | |||||
| (id: GridRowId) => () => { | |||||
| setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); | |||||
| }, | |||||
| [getRowId], | |||||
| ); | |||||
| const _columns = useMemo<GridColDef[]>( | |||||
| () => [ | |||||
| ...columns, | |||||
| { | |||||
| field: "actions", | |||||
| type: "actions", | |||||
| headerName: "", | |||||
| flex: 0.5, | |||||
| cellClassName: "actions", | |||||
| getActions: ({ id }: { id: GridRowId }) => { | |||||
| const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||||
| if (isInEditMode) { | |||||
| return [ | |||||
| <GridActionsCellItem | |||||
| icon={<SaveIcon />} | |||||
| label="Save" | |||||
| key="edit" | |||||
| sx={{ | |||||
| color: "primary.main", | |||||
| }} | |||||
| onClick={handleSave(id)} | |||||
| />, | |||||
| <GridActionsCellItem | |||||
| icon={<CancelIcon />} | |||||
| label="Cancel" | |||||
| key="edit" | |||||
| onClick={handleCancel(id)} | |||||
| />, | |||||
| ]; | |||||
| } | |||||
| return [ | |||||
| <GridActionsCellItem | |||||
| icon={<DeleteIcon />} | |||||
| label="Delete" | |||||
| sx={{ | |||||
| color: "error.main", | |||||
| }} | |||||
| onClick={handleDelete(id)} | |||||
| color="inherit" | |||||
| key="edit" | |||||
| />, | |||||
| ]; | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| [columns, rowModesModel, handleSave, handleCancel, handleDelete], | |||||
| ); | |||||
| // sync useForm | |||||
| useEffect(() => { | |||||
| // console.log(formKey) | |||||
| // console.log(rows) | |||||
| setValue(formKey, rows); | |||||
| }, [formKey, rows, setValue]); | |||||
| const footer = ( | |||||
| <Box display="flex" gap={2} alignItems="center"> | |||||
| <Button | |||||
| disableRipple | |||||
| variant="outlined" | |||||
| startIcon={<Add />} | |||||
| onClick={addRow} | |||||
| size="small" | |||||
| > | |||||
| 新增 | |||||
| {/* {t("Add Record")} */} | |||||
| </Button> | |||||
| <Button | |||||
| disableRipple | |||||
| variant="outlined" | |||||
| startIcon={<Add />} | |||||
| onClick={reset} | |||||
| size="small" | |||||
| > | |||||
| {/* {t("Clean Record")} */} | |||||
| 清除 | |||||
| </Button> | |||||
| </Box> | |||||
| ); | |||||
| // const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { | |||||
| // if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||||
| // event.defaultMuiPrevented = true; | |||||
| // } | |||||
| // }; | |||||
| return ( | |||||
| <StyledDataGrid | |||||
| // {...props} | |||||
| // getRowId={getRowId as GridRowIdGetter<GridValidRowModel>} | |||||
| rowSelectionModel={rowSelectionModel} | |||||
| apiRef={apiRef} | |||||
| rows={rows} | |||||
| columns={columns} | |||||
| editMode="row" | |||||
| autoHeight | |||||
| sx={{ | |||||
| "--DataGrid-overlayHeight": "100px", | |||||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||||
| border: "1px solid", | |||||
| borderColor: "error.main", | |||||
| }, | |||||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||||
| border: "1px solid", | |||||
| borderColor: "warning.main", | |||||
| }, | |||||
| }} | |||||
| disableColumnMenu | |||||
| processRowUpdate={processRowUpdate as any} | |||||
| // onRowEditStop={handleRowEditStop} | |||||
| rowModesModel={rowModesModel} | |||||
| onRowModesModelChange={setRowModesModel} | |||||
| onProcessRowUpdateError={onProcessRowUpdateError} | |||||
| getCellClassName={(params: GridCellParams<TableRow<T, E>>) => { | |||||
| let classname = ""; | |||||
| if (params.row._error) { | |||||
| classname = "hasError"; | |||||
| } | |||||
| return classname; | |||||
| }} | |||||
| slots={{ | |||||
| // footer: FooterToolbar, | |||||
| noRowsOverlay: NoRowsOverlay, | |||||
| }} | |||||
| // slotProps={{ | |||||
| // footer: { child: footer }, | |||||
| // } | |||||
| // } | |||||
| /> | |||||
| ); | |||||
| } | |||||
| const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||||
| return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||||
| }; | |||||
| const NoRowsOverlay: React.FC = () => { | |||||
| const { t } = useTranslation("home"); | |||||
| return ( | |||||
| <Box | |||||
| display="flex" | |||||
| justifyContent="center" | |||||
| alignItems="center" | |||||
| height="100%" | |||||
| > | |||||
| <Typography variant="caption">{t("Add some entries!")}</Typography> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default InputDataGrid; | |||||
| @@ -0,0 +1,460 @@ | |||||
| "use client"; | |||||
| import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions"; | |||||
| import { | |||||
| Box, | |||||
| Card, | |||||
| CardContent, | |||||
| Checkbox, | |||||
| FormControl, | |||||
| FormControlLabel, | |||||
| Grid, | |||||
| Radio, | |||||
| RadioGroup, | |||||
| Stack, | |||||
| Tab, | |||||
| Tabs, | |||||
| TabsProps, | |||||
| TextField, | |||||
| Tooltip, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { useFormContext, Controller } from "react-hook-form"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import StyledDataGrid from "../StyledDataGrid"; | |||||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| GridColDef, | |||||
| GridRowIdGetter, | |||||
| GridRowModel, | |||||
| useGridApiContext, | |||||
| GridRenderCellParams, | |||||
| GridRenderEditCellParams, | |||||
| useGridApiRef, | |||||
| GridRowSelectionModel, | |||||
| } from "@mui/x-data-grid"; | |||||
| import InputDataGrid from "../InputDataGrid"; | |||||
| import { TableRow } from "../InputDataGrid/InputDataGrid"; | |||||
| import TwoLineCell from "./TwoLineCell"; | |||||
| import QcSelect from "./QcSelect"; | |||||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||||
| import { StockInLine } from "@/app/api/po"; | |||||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||||
| import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; | |||||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||||
| import axios from "@/app/(main)/axios/axiosInstance"; | |||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||||
| import EscalationComponent from "./EscalationComponent"; | |||||
| import QcDataGrid from "./QCDatagrid"; | |||||
| import StockInFormVer2 from "./StockInFormVer2"; | |||||
| import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate"; | |||||
| import { ModalFormInput } from "@/app/api/po/actions"; | |||||
| import { escape } from "lodash"; | |||||
| interface Props { | |||||
| itemDetail: StockInLine; | |||||
| qc: QcItemWithChecks[]; | |||||
| disabled: boolean; | |||||
| qcItems: QcData[] | |||||
| setQcItems: Dispatch<SetStateAction<QcData[]>> | |||||
| } | |||||
| type EntryError = | |||||
| | { | |||||
| [field in keyof QcData]?: string; | |||||
| } | |||||
| | undefined; | |||||
| type QcRow = TableRow<Partial<QcData>, EntryError>; | |||||
| // fetchQcItemCheck | |||||
| const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcItems }) => { | |||||
| const { t } = useTranslation("purchaseOrder"); | |||||
| const apiRef = useGridApiRef(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors, defaultValues, touchedFields }, | |||||
| watch, | |||||
| control, | |||||
| setValue, | |||||
| getValues, | |||||
| reset, | |||||
| resetField, | |||||
| setError, | |||||
| clearErrors, | |||||
| } = useFormContext<PurchaseQCInput>(); | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>(); | |||||
| const [escalationHistory, setEscalationHistory] = useState(dummyEscalationHistory); | |||||
| const [qcResult, setQcResult] = useState(); | |||||
| const qcAccept = watch("qcAccept"); | |||||
| // const [qcAccept, setQcAccept] = useState(true); | |||||
| // const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| const column = useMemo<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| field: "escalation", | |||||
| headerName: t("escalation"), | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: "supervisor", | |||||
| headerName: t("supervisor"), | |||||
| flex: 1, | |||||
| }, | |||||
| ], [] | |||||
| ) | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| //// validate form | |||||
| const accQty = watch("acceptQty"); | |||||
| const validateForm = useCallback(() => { | |||||
| console.log(accQty); | |||||
| if (accQty > itemDetail.acceptedQty) { | |||||
| setError("acceptQty", { | |||||
| message: `${t("acceptQty must not greater than")} ${ | |||||
| itemDetail.acceptedQty | |||||
| }`, | |||||
| type: "required", | |||||
| }); | |||||
| } | |||||
| if (accQty < 1) { | |||||
| setError("acceptQty", { | |||||
| message: t("minimal value is 1"), | |||||
| type: "required", | |||||
| }); | |||||
| } | |||||
| if (isNaN(accQty)) { | |||||
| setError("acceptQty", { | |||||
| message: t("value must be a number"), | |||||
| type: "required", | |||||
| }); | |||||
| } | |||||
| }, [accQty]); | |||||
| useEffect(() => { | |||||
| clearErrors(); | |||||
| validateForm(); | |||||
| }, [clearErrors, validateForm]); | |||||
| const columns = useMemo<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| field: "escalation", | |||||
| headerName: t("escalation"), | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: "supervisor", | |||||
| headerName: t("supervisor"), | |||||
| flex: 1, | |||||
| }, | |||||
| ], | |||||
| [], | |||||
| ); | |||||
| /// validate datagrid | |||||
| const validation = useCallback( | |||||
| (newRow: GridRowModel<QcRow>): EntryError => { | |||||
| const error: EntryError = {}; | |||||
| // const { qcItemId, failQty } = newRow; | |||||
| return Object.keys(error).length > 0 ? error : undefined; | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| function BooleanEditCell(params: GridRenderEditCellParams) { | |||||
| const apiRef = useGridApiContext(); | |||||
| const { id, field, value } = params; | |||||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); | |||||
| apiRef.current.stopCellEditMode({ id, field }); // commit immediately | |||||
| }; | |||||
| return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | |||||
| } | |||||
| const qcColumns: GridColDef[] = [ | |||||
| { | |||||
| field: "qcItem", | |||||
| headerName: t("qcItem"), | |||||
| flex: 2, | |||||
| renderCell: (params) => ( | |||||
| <Box> | |||||
| <b>{params.value}</b><br/> | |||||
| {params.row.qcDescription}<br/> | |||||
| </Box> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| field: 'isPassed', | |||||
| headerName: t("qcResult"), | |||||
| flex: 1.5, | |||||
| renderCell: (params) => { | |||||
| const currentValue = params.value; | |||||
| return ( | |||||
| <FormControl> | |||||
| <RadioGroup | |||||
| row | |||||
| aria-labelledby="demo-radio-buttons-group-label" | |||||
| value={currentValue === undefined ? "" : (currentValue ? "true" : "false")} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| setQcItems((prev) => | |||||
| prev.map((r): QcData => (r.id === params.id ? { ...r, isPassed: value === "true" } : r)) | |||||
| ); | |||||
| }} | |||||
| name={`isPassed-${params.id}`} | |||||
| > | |||||
| <FormControlLabel | |||||
| value="true" | |||||
| control={<Radio />} | |||||
| label="合格" | |||||
| sx={{ | |||||
| color: currentValue === true ? "green" : "inherit", | |||||
| "& .Mui-checked": {color: "green"} | |||||
| }} | |||||
| /> | |||||
| <FormControlLabel | |||||
| value="false" | |||||
| control={<Radio />} | |||||
| label="不合格" | |||||
| sx={{ | |||||
| color: currentValue === false ? "red" : "inherit", | |||||
| "& .Mui-checked": {color: "red"} | |||||
| }} | |||||
| /> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "failedQty", | |||||
| headerName: t("failedQty"), | |||||
| flex: 1, | |||||
| // editable: true, | |||||
| renderCell: (params) => ( | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={!params.row.isPassed? (params.value ?? '') : '0'} | |||||
| disabled={params.row.isPassed} | |||||
| onChange={(e) => { | |||||
| const v = e.target.value; | |||||
| const next = v === '' ? undefined : Number(v); | |||||
| if (Number.isNaN(next)) return; | |||||
| setQcItems((prev) => | |||||
| prev.map((r) => (r.id === params.id ? { ...r, failedQty: next } : r)) | |||||
| ); | |||||
| }} | |||||
| onClick={(e) => e.stopPropagation()} | |||||
| onMouseDown={(e) => e.stopPropagation()} | |||||
| onKeyDown={(e) => e.stopPropagation()} | |||||
| inputProps={{ min: 0 }} | |||||
| sx={{ width: '100%' }} | |||||
| /> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| field: "remarks", | |||||
| headerName: t("remarks"), | |||||
| flex: 2, | |||||
| renderCell: (params) => ( | |||||
| <TextField | |||||
| size="small" | |||||
| value={params.value ?? ''} | |||||
| onChange={(e) => { | |||||
| const remarks = e.target.value; | |||||
| // const next = v === '' ? undefined : Number(v); | |||||
| // if (Number.isNaN(next)) return; | |||||
| setQcItems((prev) => | |||||
| prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r)) | |||||
| ); | |||||
| }} | |||||
| onClick={(e) => e.stopPropagation()} | |||||
| onMouseDown={(e) => e.stopPropagation()} | |||||
| onKeyDown={(e) => e.stopPropagation()} | |||||
| inputProps={{ min: 0 }} | |||||
| sx={{ width: '100%' }} | |||||
| /> | |||||
| ), | |||||
| }, | |||||
| ] | |||||
| useEffect(() => { | |||||
| console.log(itemDetail); | |||||
| }, [itemDetail]); | |||||
| // Set initial value for acceptQty | |||||
| useEffect(() => { | |||||
| if (itemDetail?.acceptedQty !== undefined) { | |||||
| setValue("acceptQty", itemDetail.acceptedQty); | |||||
| } | |||||
| }, [itemDetail?.acceptedQty, setValue]); | |||||
| // const [openCollapse, setOpenCollapse] = useState(false) | |||||
| const [isCollapsed, setIsCollapsed] = useState<boolean>(false); | |||||
| const onFailedOpenCollapse = useCallback((qcItems: QcData[]) => { | |||||
| const isFailed = qcItems.some((qc) => !qc.isPassed) | |||||
| console.log(isFailed) | |||||
| if (isFailed) { | |||||
| setIsCollapsed(true) | |||||
| } else { | |||||
| setIsCollapsed(false) | |||||
| } | |||||
| }, []) | |||||
| // const handleRadioChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||||
| // const value = event.target.value === 'true'; | |||||
| // setValue("qcAccept", value); | |||||
| // }, [setValue]); | |||||
| useEffect(() => { | |||||
| console.log(itemDetail); | |||||
| }, [itemDetail]); | |||||
| useEffect(() => { | |||||
| // onFailedOpenCollapse(qcItems) // This function is no longer needed | |||||
| }, [qcItems]); // Removed onFailedOpenCollapse from dependency array | |||||
| return ( | |||||
| <> | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Grid item xs={12}> | |||||
| <Tabs | |||||
| value={tabIndex} | |||||
| onChange={handleTabChange} | |||||
| variant="scrollable" | |||||
| > | |||||
| <Tab label={t("QC Info")} iconPosition="end" /> | |||||
| <Tab label={t("Escalation History")} iconPosition="end" /> | |||||
| </Tabs> | |||||
| </Grid> | |||||
| {tabIndex == 0 && ( | |||||
| <> | |||||
| <Grid item xs={12}> | |||||
| {/* <QcDataGrid<ModalFormInput, QcData, EntryError> | |||||
| apiRef={apiRef} | |||||
| columns={qcColumns} | |||||
| _formKey="qcResult" | |||||
| validateRow={validation} | |||||
| /> */} | |||||
| <StyledDataGrid | |||||
| columns={qcColumns} | |||||
| rows={qcItems} | |||||
| autoHeight | |||||
| /> | |||||
| </Grid> | |||||
| {/* <Grid item xs={12}> | |||||
| <EscalationComponent | |||||
| forSupervisor={false} | |||||
| isCollapsed={isCollapsed} | |||||
| setIsCollapsed={setIsCollapsed} | |||||
| /> | |||||
| </Grid> */} | |||||
| </> | |||||
| )} | |||||
| {tabIndex == 1 && ( | |||||
| <> | |||||
| {/* <Grid item xs={12}> | |||||
| <StockInFormVer2 | |||||
| itemDetail={itemDetail} | |||||
| disabled={false} | |||||
| /> | |||||
| </Grid> */} | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Escalation Info")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <StyledDataGrid | |||||
| rows={escalationHistory} | |||||
| columns={columns} | |||||
| onRowSelectionModelChange={(newRowSelectionModel) => { | |||||
| setRowSelectionModel(newRowSelectionModel); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| </> | |||||
| )} | |||||
| <Grid item xs={12}> | |||||
| <FormControl> | |||||
| <Controller | |||||
| name="qcAccept" | |||||
| control={control} | |||||
| defaultValue={true} | |||||
| render={({ field }) => ( | |||||
| <RadioGroup | |||||
| row | |||||
| aria-labelledby="demo-radio-buttons-group-label" | |||||
| {...field} | |||||
| value={field.value?.toString() || "true"} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value === 'true'; | |||||
| if (!value && Boolean(errors.acceptQty)) { | |||||
| setValue("acceptQty", itemDetail.acceptedQty); | |||||
| } | |||||
| field.onChange(value); | |||||
| }} | |||||
| > | |||||
| <FormControlLabel value="true" control={<Radio />} label="接受" /> | |||||
| <Box sx={{mr:2}}> | |||||
| <TextField | |||||
| type="number" | |||||
| label={t("acceptQty")} | |||||
| sx={{ width: '150px' }} | |||||
| defaultValue={accQty} | |||||
| disabled={!qcAccept} | |||||
| {...register("acceptQty", { | |||||
| required: "acceptQty required!", | |||||
| })} | |||||
| error={Boolean(errors.acceptQty)} | |||||
| helperText={errors.acceptQty?.message} | |||||
| /> | |||||
| </Box> | |||||
| <FormControlLabel value="false" control={<Radio />} label="不接受及上報" /> | |||||
| </RadioGroup> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| {/* <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Escalation Result")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <EscalationComponent | |||||
| forSupervisor={true} | |||||
| isCollapsed={isCollapsed} | |||||
| setIsCollapsed={setIsCollapsed} | |||||
| /> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| </Grid> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default QcFormVer2; | |||||
| @@ -0,0 +1,78 @@ | |||||
| import React, { useCallback, useMemo } from "react"; | |||||
| import { | |||||
| Autocomplete, | |||||
| Box, | |||||
| Checkbox, | |||||
| Chip, | |||||
| ListSubheader, | |||||
| MenuItem, | |||||
| TextField, | |||||
| Tooltip, | |||||
| } from "@mui/material"; | |||||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| interface CommonProps { | |||||
| allQcs: QcItemWithChecks[]; | |||||
| error?: boolean; | |||||
| } | |||||
| interface SingleAutocompleteProps extends CommonProps { | |||||
| value: number | string | undefined; | |||||
| onQcSelect: (qcItemId: number) => void | Promise<void>; | |||||
| // multiple: false; | |||||
| } | |||||
| type Props = SingleAutocompleteProps; | |||||
| const QcSelect: React.FC<Props> = ({ allQcs, value, error, onQcSelect }) => { | |||||
| const { t } = useTranslation("home"); | |||||
| const filteredQc = useMemo(() => { | |||||
| // do filtering here if any | |||||
| return allQcs; | |||||
| }, [allQcs]); | |||||
| const options = useMemo(() => { | |||||
| return [ | |||||
| { | |||||
| value: -1, // think think sin | |||||
| label: t("None"), | |||||
| group: "default", | |||||
| }, | |||||
| ...filteredQc.map((q) => ({ | |||||
| value: q.id, | |||||
| label: `${q.code} - ${q.name}`, | |||||
| group: "existing", | |||||
| })), | |||||
| ]; | |||||
| }, [t, filteredQc]); | |||||
| const currentValue = options.find((o) => o.value === value) || options[0]; | |||||
| const onChange = useCallback( | |||||
| ( | |||||
| event: React.SyntheticEvent, | |||||
| newValue: { value: number; group: string } | { value: number }[], | |||||
| ) => { | |||||
| const singleNewVal = newValue as { | |||||
| value: number; | |||||
| group: string; | |||||
| }; | |||||
| onQcSelect(singleNewVal.value); | |||||
| }, | |||||
| [onQcSelect], | |||||
| ); | |||||
| return ( | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Qc")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| value={currentValue} | |||||
| onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => <TextField {...params} error={error} />} | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| export default QcSelect; | |||||
| @@ -0,0 +1,243 @@ | |||||
| import React, { useCallback } from 'react'; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| Checkbox, | |||||
| TextField, | |||||
| TablePagination, | |||||
| FormControl, | |||||
| Select, | |||||
| MenuItem, | |||||
| } from '@mui/material'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| interface SearchItemWithQty { | |||||
| id: number; | |||||
| label: string; | |||||
| qty: number | null; | |||||
| currentStockBalance?: number; | |||||
| uomDesc?: string; | |||||
| targetDate?: string | null; | |||||
| groupId?: number | null; | |||||
| } | |||||
| interface Group { | |||||
| id: number; | |||||
| name: string; | |||||
| targetDate: string; | |||||
| } | |||||
| interface SearchResultsTableProps { | |||||
| items: SearchItemWithQty[]; | |||||
| selectedItemIds: (string | number)[]; | |||||
| groups: Group[]; | |||||
| onItemSelect: (itemId: number, checked: boolean) => void; | |||||
| onQtyChange: (itemId: number, qty: number | null) => void; | |||||
| onQtyBlur: (itemId: number) => void; | |||||
| onGroupChange: (itemId: number, groupId: string) => void; | |||||
| isItemInCreated: (itemId: number) => boolean; | |||||
| pageNum: number; | |||||
| pageSize: number; | |||||
| onPageChange: (event: unknown, newPage: number) => void; | |||||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||||
| } | |||||
| const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | |||||
| items, | |||||
| selectedItemIds, | |||||
| groups, | |||||
| onItemSelect, | |||||
| onQtyChange, | |||||
| onGroupChange, | |||||
| onQtyBlur, | |||||
| isItemInCreated, | |||||
| pageNum, | |||||
| pageSize, | |||||
| onPageChange, | |||||
| onPageSizeChange, | |||||
| }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| // Calculate pagination | |||||
| const startIndex = (pageNum - 1) * pageSize; | |||||
| const endIndex = startIndex + pageSize; | |||||
| const paginatedResults = items.slice(startIndex, endIndex); | |||||
| const handleQtyChange = useCallback((itemId: number, value: string) => { | |||||
| // Only allow numbers | |||||
| if (value === "" || /^\d+$/.test(value)) { | |||||
| const numValue = value === "" ? null : Number(value); | |||||
| onQtyChange(itemId, numValue); | |||||
| } | |||||
| }, [onQtyChange]); | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}> | |||||
| {t("Selected")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Item")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Group")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Current Stock")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Stock Unit")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Order Quantity")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Target Date")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedResults.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={12} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| paginatedResults.map((item) => ( | |||||
| <TableRow key={item.id}> | |||||
| <TableCell padding="checkbox"> | |||||
| <Checkbox | |||||
| checked={selectedItemIds.includes(item.id)} | |||||
| onChange={(e) => onItemSelect(item.id, e.target.checked)} | |||||
| disabled={isItemInCreated(item.id)} | |||||
| /> | |||||
| </TableCell> | |||||
| {/* Item */} | |||||
| <TableCell> | |||||
| <Box> | |||||
| <Typography variant="body2"> | |||||
| {item.label.split(' - ')[1] || item.label} | |||||
| </Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| {item.label.split(' - ')[0] || ''} | |||||
| </Typography> | |||||
| </Box> | |||||
| </TableCell> | |||||
| {/* Group */} | |||||
| <TableCell> | |||||
| <FormControl size="small" sx={{ minWidth: 120 }}> | |||||
| <Select | |||||
| value={item.groupId?.toString() || ""} | |||||
| onChange={(e) => onGroupChange(item.id, e.target.value)} | |||||
| displayEmpty | |||||
| disabled={isItemInCreated(item.id)} | |||||
| > | |||||
| <MenuItem value=""> | |||||
| <em>{t("No Group")}</em> | |||||
| </MenuItem> | |||||
| {groups.map((group) => ( | |||||
| <MenuItem key={group.id} value={group.id.toString()}> | |||||
| {group.name} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | |||||
| </FormControl> | |||||
| </TableCell> | |||||
| {/* Current Stock */} | |||||
| <TableCell align="right"> | |||||
| <Typography | |||||
| variant="body2" | |||||
| color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"} | |||||
| sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }} | |||||
| > | |||||
| {item.currentStockBalance?.toLocaleString()||0} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| {/* Stock Unit */} | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {item.uomDesc || "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {/* Order Quantity */} | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={item.qty || ""} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| // Only allow numbers | |||||
| if (value === "" || /^\d+$/.test(value)) { | |||||
| const numValue = value === "" ? null : Number(value); | |||||
| onQtyChange(item.id, numValue); | |||||
| } | |||||
| }} | |||||
| onBlur={() => { | |||||
| // Trigger auto-add check when user finishes input (clicks elsewhere) | |||||
| onQtyBlur(item.id); // ← Change this to call onQtyBlur instead! | |||||
| }} | |||||
| inputProps={{ | |||||
| style: { textAlign: 'center' } | |||||
| }} | |||||
| sx={{ | |||||
| width: '80px', | |||||
| '& .MuiInputBase-input': { | |||||
| textAlign: 'center', | |||||
| cursor: 'text' | |||||
| } | |||||
| }} | |||||
| disabled={isItemInCreated(item.id)} | |||||
| /> | |||||
| </TableCell> | |||||
| {/* Target Date */} | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={items.length} | |||||
| page={(pageNum - 1)} | |||||
| rowsPerPage={pageSize} | |||||
| onPageChange={onPageChange} | |||||
| onRowsPerPageChange={onPageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default SearchResultsTable; | |||||
| @@ -0,0 +1,321 @@ | |||||
| "use client"; | |||||
| import { | |||||
| PurchaseQcResult, | |||||
| PurchaseQCInput, | |||||
| StockInInput, | |||||
| } from "@/app/api/po/actions"; | |||||
| import { | |||||
| Box, | |||||
| Card, | |||||
| CardContent, | |||||
| Grid, | |||||
| Stack, | |||||
| TextField, | |||||
| Tooltip, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import StyledDataGrid from "../StyledDataGrid"; | |||||
| import { useCallback, useEffect, useMemo } from "react"; | |||||
| import { | |||||
| GridColDef, | |||||
| GridRowIdGetter, | |||||
| GridRowModel, | |||||
| useGridApiContext, | |||||
| GridRenderCellParams, | |||||
| GridRenderEditCellParams, | |||||
| useGridApiRef, | |||||
| } from "@mui/x-data-grid"; | |||||
| import InputDataGrid from "../InputDataGrid"; | |||||
| import { TableRow } from "../InputDataGrid/InputDataGrid"; | |||||
| import TwoLineCell from "./TwoLineCell"; | |||||
| import QcSelect from "./QcSelect"; | |||||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||||
| import { StockInLine } from "@/app/api/po"; | |||||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import dayjs from "dayjs"; | |||||
| // 修改接口以支持 PickOrder 数据 | |||||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | |||||
| // change PurchaseQcResult to stock in entry props | |||||
| interface Props { | |||||
| itemDetail: StockInLine | (GetPickOrderLineInfo & { pickOrderCode: string }); | |||||
| // qc: QcItemWithChecks[]; | |||||
| disabled: boolean; | |||||
| } | |||||
| type EntryError = | |||||
| | { | |||||
| [field in keyof StockInInput]?: string; | |||||
| } | |||||
| | undefined; | |||||
| // type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | |||||
| const StockInFormVer2: React.FC<Props> = ({ | |||||
| // qc, | |||||
| itemDetail, | |||||
| disabled, | |||||
| }) => { | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation("purchaseOrder"); | |||||
| const apiRef = useGridApiRef(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors, defaultValues, touchedFields }, | |||||
| watch, | |||||
| control, | |||||
| setValue, | |||||
| getValues, | |||||
| reset, | |||||
| resetField, | |||||
| setError, | |||||
| clearErrors, | |||||
| } = useFormContext<StockInInput>(); | |||||
| // console.log(itemDetail); | |||||
| useEffect(() => { | |||||
| console.log("triggered"); | |||||
| // receiptDate default tdy | |||||
| setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT)); | |||||
| setValue("status", "received"); | |||||
| }, [setValue]); | |||||
| useEffect(() => { | |||||
| console.log(errors); | |||||
| }, [errors]); | |||||
| const productionDate = watch("productionDate"); | |||||
| const expiryDate = watch("expiryDate"); | |||||
| const uom = watch("uom"); | |||||
| useEffect(() => { | |||||
| console.log(uom); | |||||
| console.log(productionDate); | |||||
| console.log(expiryDate); | |||||
| if (expiryDate) clearErrors(); | |||||
| if (productionDate) clearErrors(); | |||||
| }, [expiryDate, productionDate, clearErrors]); | |||||
| // 检查是否为 PickOrder 数据 | |||||
| const isPickOrderData = 'pickOrderCode' in itemDetail; | |||||
| // 获取 UOM 显示值 | |||||
| const getUomDisplayValue = () => { | |||||
| if (isPickOrderData) { | |||||
| // PickOrder 数据 | |||||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | |||||
| return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; | |||||
| } else { | |||||
| // StockIn 数据 | |||||
| const stockInItem = itemDetail as StockInLine; | |||||
| return uom?.code || stockInItem.uom?.code || ''; | |||||
| } | |||||
| }; | |||||
| // 获取 Item 显示值 | |||||
| const getItemDisplayValue = () => { | |||||
| if (isPickOrderData) { | |||||
| // PickOrder 数据 | |||||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | |||||
| return pickOrderItem.itemCode || ''; | |||||
| } else { | |||||
| // StockIn 数据 | |||||
| const stockInItem = itemDetail as StockInLine; | |||||
| return stockInItem.itemNo || ''; | |||||
| } | |||||
| }; | |||||
| // 获取 Item Name 显示值 | |||||
| const getItemNameDisplayValue = () => { | |||||
| if (isPickOrderData) { | |||||
| // PickOrder 数据 | |||||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | |||||
| return pickOrderItem.itemName || ''; | |||||
| } else { | |||||
| // StockIn 数据 | |||||
| const stockInItem = itemDetail as StockInLine; | |||||
| return stockInItem.itemName || ''; | |||||
| } | |||||
| }; | |||||
| // 获取 Quantity 显示值 | |||||
| const getQuantityDisplayValue = () => { | |||||
| if (isPickOrderData) { | |||||
| // PickOrder 数据 | |||||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | |||||
| return pickOrderItem.requiredQty || 0; | |||||
| } else { | |||||
| // StockIn 数据 | |||||
| const stockInItem = itemDetail as StockInLine; | |||||
| return stockInItem.acceptedQty || 0; | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <Grid container spacing={2}> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("stock in information")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemNo")} | |||||
| fullWidth | |||||
| {...register("itemNo", { | |||||
| required: "itemNo required!", | |||||
| })} | |||||
| value={getItemDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemName")} | |||||
| fullWidth | |||||
| {...register("itemName", { | |||||
| required: "itemName required!", | |||||
| })} | |||||
| value={getItemNameDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| name="productionDate" | |||||
| control={control} | |||||
| rules={{ | |||||
| required: "productionDate required!", | |||||
| }} | |||||
| render={({ field }) => { | |||||
| return ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale={`${language}-hk`} | |||||
| > | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={{ width: "100%" }} | |||||
| label={t("productionDate")} | |||||
| value={productionDate ? dayjs(productionDate) : undefined} | |||||
| disabled={disabled} | |||||
| onChange={(date) => { | |||||
| console.log(date); | |||||
| if (!date) return; | |||||
| console.log(date.format(INPUT_DATE_FORMAT)); | |||||
| setValue("productionDate", date.format(INPUT_DATE_FORMAT)); | |||||
| // field.onChange(date); | |||||
| }} | |||||
| inputRef={field.ref} | |||||
| slotProps={{ | |||||
| textField: { | |||||
| // required: true, | |||||
| error: Boolean(errors.productionDate?.message), | |||||
| helperText: errors.productionDate?.message, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| name="expiryDate" | |||||
| control={control} | |||||
| rules={{ | |||||
| required: "expiryDate required!", | |||||
| }} | |||||
| render={({ field }) => { | |||||
| return ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale={`${language}-hk`} | |||||
| > | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={{ width: "100%" }} | |||||
| label={t("expiryDate")} | |||||
| value={expiryDate ? dayjs(expiryDate) : undefined} | |||||
| disabled={disabled} | |||||
| onChange={(date) => { | |||||
| console.log(date); | |||||
| if (!date) return; | |||||
| console.log(date.format(INPUT_DATE_FORMAT)); | |||||
| setValue("expiryDate", date.format(INPUT_DATE_FORMAT)); | |||||
| // field.onChange(date); | |||||
| }} | |||||
| inputRef={field.ref} | |||||
| slotProps={{ | |||||
| textField: { | |||||
| // required: true, | |||||
| error: Boolean(errors.expiryDate?.message), | |||||
| helperText: errors.expiryDate?.message, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("receivedQty")} | |||||
| fullWidth | |||||
| {...register("receivedQty", { | |||||
| required: "receivedQty required!", | |||||
| })} | |||||
| value={getQuantityDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("uom")} | |||||
| fullWidth | |||||
| {...register("uom", { | |||||
| required: "uom required!", | |||||
| })} | |||||
| value={getUomDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("acceptedQty")} | |||||
| fullWidth | |||||
| {...register("acceptedQty", { | |||||
| required: "acceptedQty required!", | |||||
| })} | |||||
| value={getQuantityDisplayValue()} | |||||
| disabled={true} | |||||
| // disabled={disabled} | |||||
| // error={Boolean(errors.acceptedQty)} | |||||
| // helperText={errors.acceptedQty?.message} | |||||
| /> | |||||
| </Grid> | |||||
| {/* <Grid item xs={4}> | |||||
| <TextField | |||||
| label={t("acceptedWeight")} | |||||
| fullWidth | |||||
| // {...register("acceptedWeight", { | |||||
| // required: "acceptedWeight required!", | |||||
| // })} | |||||
| disabled={disabled} | |||||
| error={Boolean(errors.acceptedWeight)} | |||||
| helperText={errors.acceptedWeight?.message} | |||||
| /> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default StockInFormVer2; | |||||
| @@ -0,0 +1,24 @@ | |||||
| import { Box, Tooltip } from "@mui/material"; | |||||
| import React from "react"; | |||||
| const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => { | |||||
| return ( | |||||
| <Tooltip title={children}> | |||||
| <Box | |||||
| sx={{ | |||||
| whiteSpace: "normal", | |||||
| overflow: "hidden", | |||||
| textOverflow: "ellipsis", | |||||
| display: "-webkit-box", | |||||
| WebkitLineClamp: 2, | |||||
| WebkitBoxOrient: "vertical", | |||||
| lineHeight: "22px", | |||||
| }} | |||||
| > | |||||
| {children} | |||||
| </Box> | |||||
| </Tooltip> | |||||
| ); | |||||
| }; | |||||
| export default TwoLineCell; | |||||
| @@ -0,0 +1,73 @@ | |||||
| import { ItemCombo } from "@/app/api/settings/item/actions"; | |||||
| import { Autocomplete, TextField } from "@mui/material"; | |||||
| import { useCallback, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| interface CommonProps { | |||||
| allUom: ItemCombo[]; | |||||
| error?: boolean; | |||||
| } | |||||
| interface SingleAutocompleteProps extends CommonProps { | |||||
| value: number | string | undefined; | |||||
| onUomSelect: (itemId: number) => void | Promise<void>; | |||||
| // multiple: false; | |||||
| } | |||||
| type Props = SingleAutocompleteProps; | |||||
| const UomSelect: React.FC<Props> = ({ | |||||
| allUom, | |||||
| value, | |||||
| error, | |||||
| onUomSelect | |||||
| }) => { | |||||
| const { t } = useTranslation("item"); | |||||
| const filteredUom = useMemo(() => { | |||||
| return allUom | |||||
| }, [allUom]) | |||||
| const options = useMemo(() => { | |||||
| return [ | |||||
| { | |||||
| value: -1, // think think sin | |||||
| label: t("None"), | |||||
| group: "default", | |||||
| }, | |||||
| ...filteredUom.map((i) => ({ | |||||
| value: i.id as number, | |||||
| label: i.label, | |||||
| group: "existing", | |||||
| })), | |||||
| ]; | |||||
| }, [t, filteredUom]); | |||||
| const currentValue = options.find((o) => o.value === value) || options[0]; | |||||
| const onChange = useCallback( | |||||
| ( | |||||
| event: React.SyntheticEvent, | |||||
| newValue: { value: number; group: string } | { value: number }[], | |||||
| ) => { | |||||
| const singleNewVal = newValue as { | |||||
| value: number; | |||||
| group: string; | |||||
| }; | |||||
| onUomSelect(singleNewVal.value) | |||||
| } | |||||
| , [onUomSelect]) | |||||
| return ( | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Uom")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| value={currentValue} | |||||
| onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => <TextField {...params} error={error} />} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| export default UomSelect | |||||
| @@ -0,0 +1,85 @@ | |||||
| import { Criterion } from "@/components/SearchBox/SearchBox"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { useState } from "react"; | |||||
| import { Card, CardContent, Typography, Grid, TextField, Button, Stack } from "@mui/material"; | |||||
| import { RestartAlt, Search } from "@mui/icons-material"; | |||||
| import { Autocomplete } from "@mui/material"; | |||||
| const VerticalSearchBox = ({ criteria, onSearch, onReset }: { | |||||
| criteria: Criterion<any>[]; | |||||
| onSearch: (inputs: Record<string, any>) => void; | |||||
| onReset?: () => void; | |||||
| }) => { | |||||
| const { t } = useTranslation("common"); | |||||
| const [inputs, setInputs] = useState<Record<string, any>>({}); | |||||
| const handleInputChange = (paramName: string, value: any) => { | |||||
| setInputs(prev => ({ ...prev, [paramName]: value })); | |||||
| }; | |||||
| const handleSearch = () => { | |||||
| onSearch(inputs); | |||||
| }; | |||||
| const handleReset = () => { | |||||
| setInputs({}); | |||||
| onReset?.(); | |||||
| }; | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
| <Typography variant="overline">{t("Search Criteria")}</Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 12, sm: 12 }}> | |||||
| {criteria.map((c) => { | |||||
| return ( | |||||
| <Grid key={c.paramName} item xs={12}> | |||||
| {c.type === "text" && ( | |||||
| <TextField | |||||
| label={t(c.label)} | |||||
| fullWidth | |||||
| onChange={(e) => handleInputChange(c.paramName, e.target.value)} | |||||
| value={inputs[c.paramName] || ""} | |||||
| /> | |||||
| )} | |||||
| {c.type === "autocomplete" && ( | |||||
| <Autocomplete | |||||
| options={c.options || []} | |||||
| getOptionLabel={(option: any) => option.label} | |||||
| onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")} | |||||
| value={c.options?.find(option => option.value === inputs[c.paramName]) || null} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={t(c.label)} | |||||
| fullWidth | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| )} | |||||
| </Grid> | |||||
| ); | |||||
| })} | |||||
| </Grid> | |||||
| <Stack direction="row" spacing={2} sx={{ mt: 2 }}> | |||||
| <Button | |||||
| variant="text" | |||||
| startIcon={<RestartAlt />} | |||||
| onClick={handleReset} | |||||
| > | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Search />} | |||||
| onClick={handleSearch} | |||||
| > | |||||
| {t("Search")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default VerticalSearchBox; | |||||
| @@ -0,0 +1,78 @@ | |||||
| import { PutAwayLine } from "@/app/api/po/actions" | |||||
| export interface QcData { | |||||
| id: number, | |||||
| qcItem: string, | |||||
| qcDescription: string, | |||||
| isPassed: boolean | undefined | |||||
| failedQty: number | undefined | |||||
| remarks: string | undefined | |||||
| } | |||||
| export const dummyQCData: QcData[] = [ | |||||
| { | |||||
| id: 1, | |||||
| qcItem: "包裝", | |||||
| qcDescription: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格", | |||||
| isPassed: undefined, | |||||
| failedQty: undefined, | |||||
| remarks: undefined, | |||||
| }, | |||||
| { | |||||
| id: 2, | |||||
| qcItem: "肉質", | |||||
| qcDescription: "肉質鬆散,則不合格", | |||||
| isPassed: undefined, | |||||
| failedQty: undefined, | |||||
| remarks: undefined, | |||||
| }, | |||||
| { | |||||
| id: 3, | |||||
| qcItem: "顔色", | |||||
| qcDescription: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,", | |||||
| isPassed: undefined, | |||||
| failedQty: undefined, | |||||
| remarks: undefined, | |||||
| }, | |||||
| { | |||||
| id: 4, | |||||
| qcItem: "狀態", | |||||
| qcDescription: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格", | |||||
| isPassed: undefined, | |||||
| failedQty: undefined, | |||||
| remarks: undefined, | |||||
| }, | |||||
| { | |||||
| id: 5, | |||||
| qcItem: "異物", | |||||
| qcDescription: "有不屬於本食材的雜質,則不合格", | |||||
| isPassed: undefined, | |||||
| failedQty: undefined, | |||||
| remarks: undefined, | |||||
| }, | |||||
| ] | |||||
| export interface EscalationData { | |||||
| id: number, | |||||
| escalation: string, | |||||
| supervisor: string, | |||||
| } | |||||
| export const dummyEscalationHistory: EscalationData[] = [ | |||||
| { | |||||
| id: 1, | |||||
| escalation: "上報1", | |||||
| supervisor: "陳大文" | |||||
| }, | |||||
| ] | |||||
| export const dummyPutawayLine: PutAwayLine[] = [ | |||||
| { | |||||
| id: 1, | |||||
| qty: 100, | |||||
| warehouseId: 1, | |||||
| warehouse: "W001 - 憶兆 3樓A倉", | |||||
| printQty: 100 | |||||
| } | |||||
| ] | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./FinishedGoodSearchWrapper"; | |||||
| @@ -208,6 +208,11 @@ const NavigationContent: React.FC = () => { | |||||
| label: "Job Order", | label: "Job Order", | ||||
| path: "/jo", | path: "/jo", | ||||
| }, | }, | ||||
| { | |||||
| icon: <RequestQuote />, | |||||
| label: "Job Order Pickexcution", | |||||
| path: "/jodetail", | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -89,6 +89,20 @@ | |||||
| "Put Away Scan": "上架掃碼", | "Put Away Scan": "上架掃碼", | ||||
| "Finished Good Order": "成品出倉", | "Finished Good Order": "成品出倉", | ||||
| "finishedGood": "成品", | "finishedGood": "成品", | ||||
| "Router": "執貨路線" | |||||
| "Router": "執貨路線", | |||||
| "Job Order Pickexcution": "工單提料", | |||||
| "No data available": "沒有資料", | |||||
| "Start Scan": "開始掃碼", | |||||
| "Stop Scan": "停止掃碼", | |||||
| "Scan Result": "掃碼結果", | |||||
| "Expiry Date": "有效期", | |||||
| "Pick Order Code": "提料單編號", | |||||
| "Target Date": "需求日期", | |||||
| "Lot Required Pick Qty": "批號需求數量", | |||||
| "Job Order Match": "工單匹配", | |||||
| "All Pick Order Lots": "所有提料單批號", | |||||
| "Row per page": "每頁行數", | |||||
| "No data available": "沒有資料", | |||||
| "jodetail": "工單細節" | |||||
| } | } | ||||
| @@ -16,8 +16,14 @@ | |||||
| "Pending for pick": "待提料", | "Pending for pick": "待提料", | ||||
| "Planning": "計劃中", | "Planning": "計劃中", | ||||
| "Scanned": "已掃碼", | "Scanned": "已掃碼", | ||||
| "Processing": "已開始工序", | |||||
| "Storing": "入倉中", | |||||
| "completed": "已完成", | |||||
| "Completed": "已完成", | |||||
| "Cancel": "取消", | |||||
| "Scan Status": "掃碼狀態", | "Scan Status": "掃碼狀態", | ||||
| "Start Job Order": "開始工單", | "Start Job Order": "開始工單", | ||||
| <<<<<<< HEAD | |||||
| "Target Production Date" : "預計生產日期", | "Target Production Date" : "預計生產日期", | ||||
| "Production Priority" : "生產優先度", | "Production Priority" : "生產優先度", | ||||
| "Sequence" : "序", | "Sequence" : "序", | ||||
| @@ -28,4 +34,56 @@ | |||||
| "Lines with sufficient stock: ": "可提料項目數量: ", | "Lines with sufficient stock: ": "可提料項目數量: ", | ||||
| "Lines with insufficient stock: ": "未能提料項目數量: ", | "Lines with insufficient stock: ": "未能提料項目數量: ", | ||||
| "Item Name" : "原材料/半成品名稱" | "Item Name" : "原材料/半成品名稱" | ||||
| ======= | |||||
| "Job Order Pickexcution": "工單提料", | |||||
| "Pick Order Detail": "提料單細節", | |||||
| "Finished Job Order Record": "已完成工單記錄", | |||||
| "Index": "編號", | |||||
| "Route": "路線", | |||||
| "Item Code": "成品/半成品編號", | |||||
| "Item Name": "成品/半成品名稱", | |||||
| "Qty": "數量", | |||||
| "Unit": "單位", | |||||
| "Location": "位置", | |||||
| "Scan Result": "掃碼結果", | |||||
| "Expiry Date": "有效期", | |||||
| "Target Date": "需求日期", | |||||
| "Lot Required Pick Qty": "批號需求數量", | |||||
| "Job Order Match": "工單對料", | |||||
| "Lot No": "批號", | |||||
| "Submit Required Pick Qty": "提交需求數量", | |||||
| "All Pick Order Lots": "所有提料單批號", | |||||
| "Row per page": "每頁行數", | |||||
| "No data available": "沒有資料", | |||||
| "jodetail": "工單細節", | |||||
| "Start QR Scan": "開始QR掃碼", | |||||
| "Stop QR Scan": "停止QR掃碼", | |||||
| "Rows per page": "每頁行數", | |||||
| "Job Order Item Name": "工單物料名稱", | |||||
| "Job Order Code": "工單編號", | |||||
| "View Details": "查看詳情", | |||||
| "Required Qty": "需求數量", | |||||
| "completed Job Order pick orders with Matching": "工單已完成提料和對料", | |||||
| "No completed Job Order pick orders with matching found": "沒有匹配的工單", | |||||
| "completed Job Order pick orders with matching": "工單已完成提料和對料", | |||||
| "Total": "總數", | |||||
| "Back to List": "返回列表", | |||||
| "second Scan Status": "對料狀態", | |||||
| "Actual Pick Qty": "實際提料數量", | |||||
| "Processing Status": "處理狀態", | |||||
| "Lot Availability": "批號可用性", | |||||
| "Pick Order Id": "提料單編號", | |||||
| "Pick Order Code": "提料單編號", | |||||
| "Pick Order Conso Code": "提料單組合編號", | |||||
| "Pick Order Target Date": "提料單需求日期", | |||||
| "Pick Order Status": "提料單狀態", | |||||
| "Second Scan Status": "對料狀態", | |||||
| "Job Order Pick Order Details": "工單提料單詳情", | |||||
| "Scanning...": "掃碼中", | |||||
| "Unassigned Job Orders": "未分配工單", | |||||
| "Please scan the item qr code": "請掃描物料二維碼", | |||||
| "Please make sure the qty is enough": "請確保物料數量是足夠", | |||||
| "Please make sure all required items are picked": "請確保所有物料已被提取", | |||||
| "Do you want to start job order": "是否開始工單" | |||||
| >>>>>>> 5ef2a717b8e76f98fdf437b56fa641e990ef106b | |||||
| } | } | ||||
| @@ -172,7 +172,7 @@ | |||||
| "Job Order Code": "工單編號", | "Job Order Code": "工單編號", | ||||
| "QC Check": "QC 檢查", | "QC Check": "QC 檢查", | ||||
| "QR Code Scan": "QR Code掃描", | "QR Code Scan": "QR Code掃描", | ||||
| "Pick Order Details": "提料單詳情", | |||||
| "Pick Order Details": "提料單資料", | |||||
| "Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。", | "Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。", | ||||
| "Pick order completed successfully!": "提料單完成成功!", | "Pick order completed successfully!": "提料單完成成功!", | ||||
| "Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。", | "Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。", | ||||
| @@ -252,16 +252,16 @@ | |||||
| "Shop Name":"商店名稱", | "Shop Name":"商店名稱", | ||||
| "Shop Address":"商店地址", | "Shop Address":"商店地址", | ||||
| "Delivery Date":"目標日期", | "Delivery Date":"目標日期", | ||||
| "Pick Execution 2/F":"進行提料 2/F", | |||||
| "Pick Execution 4/F":"進行提料 4/F", | |||||
| "Pick Execution Detail":"進行提料詳情", | |||||
| "Pick Execution 2/F":"取單 2/F", | |||||
| "Pick Execution 4/F":"取單 4/F", | |||||
| "Finished Good Detail":"成品資料", | |||||
| "Submit Required Pick Qty":"提交所需提料數量", | "Submit Required Pick Qty":"提交所需提料數量", | ||||
| "Scan Result":"掃描結果", | "Scan Result":"掃描結果", | ||||
| "Ticket No.":"提票號碼", | "Ticket No.":"提票號碼", | ||||
| "Start QR Scan":"開始QR掃描", | "Start QR Scan":"開始QR掃描", | ||||
| "Stop QR Scan":"停止QR掃描", | "Stop QR Scan":"停止QR掃描", | ||||
| "Scanning...":"掃描中...", | "Scanning...":"掃描中...", | ||||
| "Print DN/Label":"列印送貨單/標籤", | |||||
| "Store ID":"儲存編號", | "Store ID":"儲存編號", | ||||
| "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。", | "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。", | ||||
| "Lot Number Mismatch":"批次號碼不符", | "Lot Number Mismatch":"批次號碼不符", | ||||
| @@ -270,15 +270,15 @@ | |||||
| "Scanned Lot:":"掃描批次:", | "Scanned Lot:":"掃描批次:", | ||||
| "Confirm":"確認", | "Confirm":"確認", | ||||
| "Update your suggested lot to the this scanned lot":"更新您的建議批次為此掃描的批次", | "Update your suggested lot to the this scanned lot":"更新您的建議批次為此掃描的批次", | ||||
| "Print Draft":"列印草稿", | |||||
| "Print Pick Order and DN Label":"列印提料單和送貨單標貼", | |||||
| "Print Pick Order":"列印提料單", | |||||
| "Print DN Label":"列印送貨單標貼", | |||||
| "Print Draft":"列印送貨單草稿", | |||||
| "Print Pick Order and DN Label":"列印送貨單和標貼", | |||||
| "Print Pick Order":"列印送貨單", | |||||
| "Print DN Label":"列印標貼", | |||||
| "If you confirm, the system will:":"如果您確認,系統將:", | "If you confirm, the system will:":"如果您確認,系統將:", | ||||
| "QR code verified.":"QR 碼驗證成功。", | "QR code verified.":"QR 碼驗證成功。", | ||||
| "Order Finished":"訂單完成", | "Order Finished":"訂單完成", | ||||
| "Submitted Status":"提交狀態", | "Submitted Status":"提交狀態", | ||||
| "Pick Execution Record":"提料執行記錄", | |||||
| "Finished Good Record":"已完成出倉記錄", | |||||
| "Delivery No.":"送貨單編號", | "Delivery No.":"送貨單編號", | ||||
| "Total":"總數", | "Total":"總數", | ||||
| "completed DO pick orders":"已完成送貨單提料單", | "completed DO pick orders":"已完成送貨單提料單", | ||||
| @@ -288,12 +288,19 @@ | |||||
| "COMPLETED":"已完成", | "COMPLETED":"已完成", | ||||
| "FG orders":"成品提料單", | "FG orders":"成品提料單", | ||||
| "Back to List":"返回列表", | "Back to List":"返回列表", | ||||
| <<<<<<< HEAD | |||||
| "Enter the number of cartons: ": "請輸入總箱數", | "Enter the number of cartons: ": "請輸入總箱數", | ||||
| "Number of cartons": "箱數", | "Number of cartons": "箱數", | ||||
| "You need to enter a number": "箱數不能為空", | "You need to enter a number": "箱數不能為空", | ||||
| "Number must be at least 1": "箱數最少為一", | "Number must be at least 1": "箱數最少為一", | ||||
| "Printed Successfully.": "已成功列印" | "Printed Successfully.": "已成功列印" | ||||
| ======= | |||||
| "No completed DO pick orders found":"沒有已完成送貨單提料單", | |||||
| "Enter the number of cartons: ": "請輸入總箱數", | |||||
| "Number of cartons": "箱數", | |||||
| "Total exceeds required qty":"總數超出所需數量" | |||||
| >>>>>>> 5ef2a717b8e76f98fdf437b56fa641e990ef106b | |||||