diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx deleted file mode 100644 index c1b3edb..0000000 --- a/src/app/(main)/report/page.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; - -import React, { useState, useMemo } from 'react'; -import { - Box, - Card, - CardContent, - Typography, - MenuItem, - TextField, - Button, - Grid, - Divider -} from '@mui/material'; -import PrintIcon from '@mui/icons-material/Print'; -import { REPORTS, ReportDefinition } from '@/config/reportConfig'; -import { getSession } from "next-auth/react"; - -export default function ReportPage() { - const [selectedReportId, setSelectedReportId] = useState(''); - const [criteria, setCriteria] = useState>({}); - const [loading, setLoading] = useState(false); - - // Find the configuration for the currently selected report - const currentReport = useMemo(() => - REPORTS.find((r) => r.id === selectedReportId), - [selectedReportId]); - - const handleReportChange = (event: React.ChangeEvent) => { - setSelectedReportId(event.target.value); - setCriteria({}); // Clear criteria when switching reports - }; - - const handleFieldChange = (name: string, value: string) => { - setCriteria((prev) => ({ ...prev, [name]: value })); - }; - - const handlePrint = async () => { - if (!currentReport) return; - - // 1. Mandatory Field Validation - const missingFields = currentReport.fields - .filter(field => field.required && !criteria[field.name]) - .map(field => field.label); - - if (missingFields.length > 0) { - alert(`Please enter the following mandatory fields:\n- ${missingFields.join('\n- ')}`); - return; - } - - setLoading(true); - try { - const token = localStorage.getItem("accessToken"); - const queryParams = new URLSearchParams(criteria).toString(); - const url = `${currentReport.apiEndpoint}?${queryParams}`; - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/pdf', - }, - }); - - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - - const blob = await response.blob(); - const downloadUrl = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = downloadUrl; - - const contentDisposition = response.headers.get('Content-Disposition'); - let fileName = `${currentReport.title}.pdf`; - if (contentDisposition?.includes('filename=')) { - fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); - } - - link.setAttribute('download', fileName); - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(downloadUrl); - } catch (error) { - console.error("Failed to generate report:", error); - alert("An error occurred while generating the report. Please try again."); - } finally { - setLoading(false); - } - }; - - return ( - - - Report Management - - - - - - Select Report Type - - - {REPORTS.map((report) => ( - - {report.title} - - ))} - - - - - {currentReport && ( - - - - Search Criteria: {currentReport.title} - - - - - {currentReport.fields.map((field) => ( - - handleFieldChange(field.name, e.target.value)} - value={criteria[field.name] || ''} - select={field.type === 'select'} - > - {field.type === 'select' && field.options?.map((opt) => ( - - {opt.label} - - ))} - - - ))} - - - - - - - - )} - - ); -} \ No newline at end of file diff --git a/src/app/(main)/settings/qrCodeHandle/page.tsx b/src/app/(main)/settings/qrCodeHandle/page.tsx index d363561..e0a84c7 100644 --- a/src/app/(main)/settings/qrCodeHandle/page.tsx +++ b/src/app/(main)/settings/qrCodeHandle/page.tsx @@ -4,7 +4,6 @@ import Typography from "@mui/material/Typography"; import { getServerI18n } from "@/i18n"; import QrCodeHandleSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleSearchWrapper"; import QrCodeHandleEquipmentSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper"; -import QrCodeHandleWarehouseSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper"; import QrCodeHandleTabs from "@/components/qrCodeHandles/qrCodeHandleTabs"; import { I18nProvider } from "@/i18n"; import Box from "@mui/material/Box"; @@ -20,7 +19,7 @@ const QrCodeHandlePage: React.FC = async () => { {t("QR Code Handle")} - + }> @@ -36,13 +35,6 @@ const QrCodeHandlePage: React.FC = async () => { } - warehouseTabContent={ - }> - - - - - } /> diff --git a/src/app/(main)/stockRecord/page.tsx b/src/app/(main)/stockRecord/page.tsx deleted file mode 100644 index d144167..0000000 --- a/src/app/(main)/stockRecord/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import SearchPage from "@/components/StockRecord/index"; -import { getServerI18n } from "@/i18n"; -import { I18nProvider } from "@/i18n"; -import { Metadata } from "next"; -import { Suspense } from "react"; - -export const metadata: Metadata = { - title: "Stock Record", -}; - -const SearchView: React.FC = async () => { - const { t } = await getServerI18n("inventory"); - - return ( - <> - - }> - - - - - ); -}; - -export default SearchView; \ No newline at end of file diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index ff20f0a..5007a80 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -131,21 +131,6 @@ export interface getTicketReleaseTable { handlerName: string | null; numberOfFGItems: number; } - -export interface TruckScheduleDashboardItem { - storeId: string | null; - truckId: number | null; - truckLanceCode: string | null; - truckDepartureTime: string | number[] | null; - numberOfShopsToServe: number; - numberOfPickTickets: number; - totalItemsToPick: number; - numberOfTicketsReleased: number; - firstTicketStartTime: string | number[] | null; - numberOfTicketsCompleted: number; - lastTicketEndTime: string | number[] | null; - pickTimeTakenMinutes: number | null; -} export interface SearchDeliveryOrderInfoRequest { code: string; shopName: string; @@ -196,15 +181,6 @@ export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: } ); }); - -export const fetchTruckScheduleDashboard = cache(async () => { - return await serverFetchJson( - `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`, - { - method: "GET", - } - ); -}); export const startBatchReleaseAsyncSingle = cache(async (data: { doId: number; userId: number }) => { const { doId, userId } = data; return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( diff --git a/src/app/api/do/client.ts b/src/app/api/do/client.ts deleted file mode 100644 index 8adddde..0000000 --- a/src/app/api/do/client.ts +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import { - fetchTruckScheduleDashboard, - type TruckScheduleDashboardItem -} from "./actions"; - -export const fetchTruckScheduleDashboardClient = async (): Promise => { - return await fetchTruckScheduleDashboard(); -}; - -export type { TruckScheduleDashboardItem }; - -export default fetchTruckScheduleDashboardClient; - - diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 1ccfb2a..e2db571 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -734,7 +734,6 @@ export const fetchAllJoborderProductProcessInfo = cache(async () => { } ); }); - /* export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => { return serverFetchJson( @@ -1178,62 +1177,4 @@ export const updateProductProcessLineProcessingTimeSetupTimeChangeoverTime = asy headers: { "Content-Type": "application/json" }, } ); - -}; -export interface MaterialPickStatusItem { - id: number; - pickOrderId: number | null; - pickOrderCode: string | null; - jobOrderId: number | null; - jobOrderCode: string | null; - itemId: number | null; - itemCode: string | null; - itemName: string | null; - jobOrderQty: number | null; - uom: string | null; - pickStartTime: string | null; // ISO datetime string - pickEndTime: string | null; // ISO datetime string - numberOfItemsToPick: number; - numberOfItemsWithIssue: number; - pickStatus: string | null; -} - -export const fetchMaterialPickStatus = cache(async (): Promise => { - return await serverFetchJson( - `${BASE_API_URL}/jo/material-pick-status`, - { - method: "GET", - } - ); -}) -export interface ProcessStatusInfo { - startTime?: string | null; - endTime?: string | null; - equipmentCode?: string | null; - isRequired: boolean; -} - -export interface JobProcessStatusResponse { - jobOrderId: number; - jobOrderCode: string; - itemCode: string; - itemName: string; - processingTime: number | null; - setupTime: number | null; - changeoverTime: number | null; - planEndTime?: string | null; - processes: ProcessStatusInfo[]; -} - -// 添加API调用函数 -export const fetchJobProcessStatus = cache(async () => { - return serverFetchJson( - `${BASE_API_URL}/product-process/Demo/JobProcessStatus`, - { - method: "GET", - next: { tags: ["jobProcessStatus"] }, - } - ); -}); - -; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/app/api/settings/item/actions.ts b/src/app/api/settings/item/actions.ts index 1340c6e..d74c089 100644 --- a/src/app/api/settings/item/actions.ts +++ b/src/app/api/settings/item/actions.ts @@ -4,7 +4,7 @@ import { serverFetchJson, serverFetchWithNoContent, } from "@/app/utils/fetchUtil"; -import { revalidateTag, revalidatePath } from "next/cache"; +import { revalidateTag } from "next/cache"; import { BASE_API_URL } from "@/config/api"; import { CreateItemResponse, RecordsRes } from "../../utils"; import { ItemQc, ItemsResult } from "."; @@ -60,21 +60,6 @@ export const saveItem = async (data: CreateItemInputs) => { return item; }; -export const deleteItem = async (id: number) => { - const response = await serverFetchJson( - `${BASE_API_URL}/items/${id}`, - { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - }, - ); - - revalidateTag("items"); - revalidatePath("/(main)/settings/items"); - - return response; -}; - export interface ItemCombo { id: number, label: string, diff --git a/src/app/api/settings/item/index.ts b/src/app/api/settings/item/index.ts index a30af00..a5bf7de 100644 --- a/src/app/api/settings/item/index.ts +++ b/src/app/api/settings/item/index.ts @@ -58,7 +58,6 @@ export type ItemsResult = { area?: string | undefined; slot?: string | undefined; LocationCode?: string | undefined; - locationCode?: string | undefined; // Backend may return lowercase version isEgg?: boolean | undefined; isFee?: boolean | undefined; isBag?: boolean | undefined; diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index e54376a..c092195 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -3,11 +3,6 @@ import { cache } from 'react'; import { serverFetchJson } from "@/app/utils/fetchUtil"; // 改为 serverFetchJson import { BASE_API_URL } from "@/config/api"; - -export interface RecordsRes { - records: T[]; - total: number; -} export interface InventoryLotDetailResponse { id: number; inventoryLotId: number; @@ -44,34 +39,30 @@ export interface InventoryLotDetailResponse { export const getInventoryLotDetailsBySection = async ( stockTakeSection: string, - stockTakeId?: number | null, - pageNum?: number, - pageSize?: number + stockTakeId?: number | null ) => { console.log('🌐 [API] getInventoryLotDetailsBySection called with:', { stockTakeSection, - stockTakeId, - pageNum, - pageSize + stockTakeId }); const encodedSection = encodeURIComponent(stockTakeSection); - let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySection?stockTakeSection=${encodedSection}&pageNum=${pageNum}&pageSize=${pageSize}`; + let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySection?stockTakeSection=${encodedSection}`; if (stockTakeId != null && stockTakeId > 0) { url += `&stockTakeId=${stockTakeId}`; } console.log(' [API] Full URL:', url); - const response = await serverFetchJson>( + const details = await serverFetchJson( url, { method: "GET", }, ); - console.log('[API] Response received:', response); - return response; + console.log('[API] Response received:', details); + return details; } export interface SaveStockTakeRecordRequest { stockTakeRecordId?: number | null; @@ -109,7 +100,6 @@ export const importStockTake = async (data: FormData) => { } export const getStockTakeRecords = async () => { - const stockTakeRecords = await serverFetchJson( // 改为 serverFetchJson `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList`, { @@ -287,86 +277,28 @@ export const updateStockTakeRecordStatusToNotMatch = async ( export const getInventoryLotDetailsBySectionNotMatch = async ( stockTakeSection: string, - stockTakeId?: number | null, - pageNum: number = 0, - pageSize: number = 10 + stockTakeId?: number | null ) => { - const encodedSection = encodeURIComponent(stockTakeSection); - let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}&pageNum=${pageNum}`; - - // Only add pageSize if it's not "all" (which would be a large number) - if (pageSize < 100000) { - url += `&pageSize=${pageSize}`; - } - // If pageSize is large (meaning "all"), don't send it - backend will return all + console.log('🌐 [API] getInventoryLotDetailsBySectionNotMatch called with:', { + stockTakeSection, + stockTakeId + }); + const encodedSection = encodeURIComponent(stockTakeSection); + let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}`; if (stockTakeId != null && stockTakeId > 0) { url += `&stockTakeId=${stockTakeId}`; } - const response = await serverFetchJson>( + console.log(' [API] Full URL:', url); + + const details = await serverFetchJson( url, { method: "GET", }, ); - return response; -} - -export interface SearchStockTransactionRequest { - startDate: string | null; - endDate: string | null; - itemCode: string | null; - itemName: string | null; - type: string | null; - pageNum: number; - pageSize: number; -} -export interface StockTransactionResponse { - id: number; - transactionType: string; - itemId: number; - itemCode: string | null; - itemName: string | null; - balanceQty: number | null; - qty: number; - type: string | null; - status: string; - transactionDate: string | null; - date: string | null; // 添加这个字段 - lotNo: string | null; - stockInId: number | null; - stockOutId: number | null; - remarks: string | null; -} - -export interface StockTransactionListResponse { - records: RecordsRes; -} - -export const searchStockTransactions = cache(async (request: SearchStockTransactionRequest) => { - // 构建查询字符串 - const params = new URLSearchParams(); - - if (request.itemCode) params.append("itemCode", request.itemCode); - if (request.itemName) params.append("itemName", request.itemName); - if (request.type) params.append("type", request.type); - if (request.startDate) params.append("startDate", request.startDate); - if (request.endDate) params.append("endDate", request.endDate); - params.append("pageNum", String(request.pageNum || 0)); - params.append("pageSize", String(request.pageSize || 100)); - - const queryString = params.toString(); - const url = `${BASE_API_URL}/stockTakeRecord/searchStockTransactions${queryString ? `?${queryString}` : ''}`; - const response = await serverFetchJson>( - url, - { - method: "GET", - next: { tags: ["Stock Transaction List"] }, - } - ); - // 确保返回正确的格式 - return response?.records || []; -}); - + console.log('[API] Response received:', details); + return details; +} diff --git a/src/app/api/warehouse/client.ts b/src/app/api/warehouse/client.ts index 454d48a..fc90335 100644 --- a/src/app/api/warehouse/client.ts +++ b/src/app/api/warehouse/client.ts @@ -3,31 +3,23 @@ import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { WarehouseResult } from "./index"; -export const exportWarehouseQrCode = async (warehouseIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => { - +export const fetchWarehouseListClient = async (): Promise => { const token = localStorage.getItem("accessToken"); - const response = await fetch(`${NEXT_PUBLIC_API_URL}/warehouse/export-qrcode`, { - method: "POST", + const response = await fetch(`${NEXT_PUBLIC_API_URL}/warehouse`, { + method: "GET", headers: { "Content-Type": "application/json", ...(token && { Authorization: `Bearer ${token}` }), }, - body: JSON.stringify({ warehouseIds }), }); if (!response.ok) { if (response.status === 401) { throw new Error("Unauthorized: Please log in again"); } - throw new Error(`Failed to export QR code: ${response.status} ${response.statusText}`); + throw new Error(`Failed to fetch warehouses: ${response.status} ${response.statusText}`); } - const filename = response.headers.get("Content-Disposition")?.split("filename=")[1]?.replace(/"/g, "") || "warehouse_qrcode.pdf"; - - const blob = await response.blob(); - const arrayBuffer = await blob.arrayBuffer(); - const blobValue = new Uint8Array(arrayBuffer); - - return { blobValue, filename }; -}; + return response.json(); +}; \ No newline at end of file diff --git a/src/authorities.ts b/src/authorities.ts index 227b0a2..7a412a7 100644 --- a/src/authorities.ts +++ b/src/authorities.ts @@ -1,14 +1,21 @@ -export const [VIEW_USER,MAINTAIN_USER, VIEW_GROUP, MAINTAIN_GROUP, - TESTING, PROD, PACK, ADMIN, STOCK, Driver] = [ - "VIEW_USER", - "MAINTAIN_USER", - "VIEW_GROUP", - "MAINTAIN_GROUP", - //below auth act as role - "TESTING", - "PROD", - "PACK", - "ADMIN", - "STOCK", - "Driver", -]; +export const AUTH = { + VIEW_USER: "VIEW_USER", + MAINTAIN_USER: "MAINTAIN_USER", + VIEW_GROUP: "VIEW_GROUP", + MAINTAIN_GROUP: "MAINTAIN_GROUP", + TESTING: "TESTING", + PROD: "PROD", + PACK: "PACK", + ADMIN: "ADMIN", + STOCK: "STOCK", + PURCHASE: "PURCHASE", + STOCK_TAKE: "STOCK_TAKE", + STOCK_IN_BIND: "STOCK_IN_BIND", + STOCK_FG: "STOCK_FG", + FORECAST: "FORECAST", + JOB_CREATE: "JOB_CREATE", + JOB_PICK: "JOB_PICK", + JOB_MAT: "JOB_MAT", + JOB_PROD: "JOB_PROD", + +} as const; \ No newline at end of file diff --git a/src/components/CreateItem/CreateItem.tsx b/src/components/CreateItem/CreateItem.tsx index f4fd8e2..69b8e9e 100644 --- a/src/components/CreateItem/CreateItem.tsx +++ b/src/components/CreateItem/CreateItem.tsx @@ -159,8 +159,9 @@ const CreateItem: React.FC = ({ console.log(qcCheck); // return // do api + console.log("asdad"); const responseI = await saveItem(data); - + console.log("asdad"); const responseQ = await saveItemQcChecks(qcCheck); if (responseI && responseQ) { if (!Boolean(responseI.id)) { diff --git a/src/components/CreateItem/CreateItemWrapper.tsx b/src/components/CreateItem/CreateItemWrapper.tsx index 095eff8..b3e2d6c 100644 --- a/src/components/CreateItem/CreateItemWrapper.tsx +++ b/src/components/CreateItem/CreateItemWrapper.tsx @@ -26,18 +26,7 @@ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { const item = result.item; qcChecks = result.qcChecks; const activeRows = qcChecks.filter((it) => it.isActive).map((i) => i.id); - - // Normalize LocationCode field (handle case sensitivity from MySQL) - const locationCode = item?.LocationCode || item?.locationCode; - - console.log("Fetched item data for edit:", { - id: item?.id, - code: item?.code, - name: item?.name, - LocationCode: locationCode, - rawItem: item - }); - + console.log(qcChecks); defaultValues = { type: item?.type, id: item?.id, @@ -55,7 +44,7 @@ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { warehouse: item?.warehouse, area: item?.area, slot: item?.slot, - LocationCode: locationCode, + LocationCode: item?.LocationCode, isEgg: item?.isEgg, isFee: item?.isFee, isBag: item?.isBag, diff --git a/src/components/CreateItem/ProductDetails.tsx b/src/components/CreateItem/ProductDetails.tsx index 0903a54..2bdda49 100644 --- a/src/components/CreateItem/ProductDetails.tsx +++ b/src/components/CreateItem/ProductDetails.tsx @@ -23,7 +23,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import InputDataGrid from "../InputDataGrid"; -import { SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react"; +import { SyntheticEvent, useCallback, useMemo, useState } from "react"; import { GridColDef, GridRowModel } from "@mui/x-data-grid"; import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; import { TypeEnum } from "@/app/utils/typeEnum"; @@ -114,13 +114,6 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehous onChange(value.id) }, []) - // Ensure LocationCode is set from defaultValues when component mounts - useEffect(() => { - if (initialDefaultValues?.LocationCode && !getValues("LocationCode")) { - setValue("LocationCode", initialDefaultValues.LocationCode); - } - }, [initialDefaultValues, setValue, getValues]); - return ( diff --git a/src/components/DashboardPage/DashboardPage.tsx b/src/components/DashboardPage/DashboardPage.tsx index d417a8a..f2d0dad 100644 --- a/src/components/DashboardPage/DashboardPage.tsx +++ b/src/components/DashboardPage/DashboardPage.tsx @@ -17,7 +17,6 @@ import CollapsibleCard from "../CollapsibleCard"; // import SupervisorQcApproval, { IQCItems } from "./QC/SupervisorQcApproval"; import { EscalationResult } from "@/app/api/escalation"; import EscalationLogTable from "./escalation/EscalationLogTable"; -import { TruckScheduleDashboard } from "./truckSchedule"; type Props = { // iqc: IQCItems[] | undefined escalationLogs: EscalationResult[] @@ -43,13 +42,6 @@ const DashboardPage: React.FC = ({ return ( - - - - - - - { - const { t } = useTranslation("dashboard"); - const [selectedStore, setSelectedStore] = useState(""); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - // Initialize as null to avoid SSR/client hydration mismatch - const [currentTime, setCurrentTime] = useState(null); - const [isClient, setIsClient] = useState(false); - const completedTrackerRef = useRef>(new Map()); - const refreshCountRef = useRef(0); - - // Set client flag and time on mount - useEffect(() => { - setIsClient(true); - setCurrentTime(dayjs()); - }, []); - - // Format time from array or string to HH:mm - const formatTime = (timeData: string | number[] | null): string => { - if (!timeData) return '-'; - - if (Array.isArray(timeData)) { - if (timeData.length >= 2) { - const hour = timeData[0] || 0; - const minute = timeData[1] || 0; - return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; - } - return '-'; - } - - if (typeof timeData === 'string') { - const parts = timeData.split(':'); - if (parts.length >= 2) { - const hour = parseInt(parts[0], 10); - const minute = parseInt(parts[1], 10); - return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; - } - } - - return '-'; - }; - - // Format datetime from array or string - const formatDateTime = (dateTimeData: string | number[] | null): string => { - if (!dateTimeData) return '-'; - - if (Array.isArray(dateTimeData)) { - return arrayToDayjs(dateTimeData, true).format('HH:mm'); - } - - const parsed = dayjs(dateTimeData); - if (parsed.isValid()) { - return parsed.format('HH:mm'); - } - - return '-'; - }; - - // Calculate time remaining for truck departure - const calculateTimeRemaining = useCallback((departureTime: string | number[] | null): string => { - if (!departureTime || !currentTime) return '-'; - - const now = currentTime; - let departureHour: number; - let departureMinute: number; - - if (Array.isArray(departureTime)) { - if (departureTime.length < 2) return '-'; - departureHour = departureTime[0] || 0; - departureMinute = departureTime[1] || 0; - } else if (typeof departureTime === 'string') { - const parts = departureTime.split(':'); - if (parts.length < 2) return '-'; - departureHour = parseInt(parts[0], 10); - departureMinute = parseInt(parts[1], 10); - } else { - return '-'; - } - - // Create departure datetime for today - const departure = now.clone().hour(departureHour).minute(departureMinute).second(0); - const diffMinutes = departure.diff(now, 'minute'); - - if (diffMinutes < 0) { - // Past departure time - const absDiff = Math.abs(diffMinutes); - const hours = Math.floor(absDiff / 60); - const minutes = absDiff % 60; - return `-${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; - } else { - const hours = Math.floor(diffMinutes / 60); - const minutes = diffMinutes % 60; - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; - } - }, [currentTime]); - - // Generate unique key for tracking completed items - const getItemKey = (item: TruckScheduleDashboardItem): string => { - return `${item.storeId}-${item.truckLanceCode}-${item.truckDepartureTime}`; - }; - - // Load data from API - const loadData = useCallback(async () => { - try { - const result = await fetchTruckScheduleDashboardClient(); - - // Update completed tracker - refreshCountRef.current += 1; - const currentRefresh = refreshCountRef.current; - - result.forEach(item => { - const key = getItemKey(item); - // If all tickets are completed, track it - if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) { - const existing = completedTrackerRef.current.get(key); - if (!existing) { - completedTrackerRef.current.set(key, { key, refreshCount: currentRefresh }); - } - } else { - // Remove from tracker if no longer completed - completedTrackerRef.current.delete(key); - } - }); - - // Filter out items that have been completed for 2+ refresh cycles - const filteredResult = result.filter(item => { - const key = getItemKey(item); - const tracker = completedTrackerRef.current.get(key); - if (tracker) { - // Hide if completed for 2 or more refresh cycles - if (currentRefresh - tracker.refreshCount >= 2) { - return false; - } - } - return true; - }); - - setData(filteredResult); - } catch (error) { - console.error('Error fetching truck schedule dashboard:', error); - } finally { - setLoading(false); - } - }, []); - - // Initial load and auto-refresh every 5 minutes - useEffect(() => { - loadData(); - - const refreshInterval = setInterval(() => { - loadData(); - }, 5 * 60 * 1000); // 5 minutes - - return () => clearInterval(refreshInterval); - }, [loadData]); - - // Update current time every 1 minute for time remaining calculation - useEffect(() => { - if (!isClient) return; - - const timeInterval = setInterval(() => { - setCurrentTime(dayjs()); - }, 60 * 1000); // 1 minute - - return () => clearInterval(timeInterval); - }, [isClient]); - - // Filter data by selected store - const filteredData = useMemo(() => { - if (!selectedStore) return data; - return data.filter(item => item.storeId === selectedStore); - }, [data, selectedStore]); - - // Get chip color based on time remaining - const getTimeChipColor = (departureTime: string | number[] | null): "success" | "warning" | "error" | "default" => { - if (!departureTime || !currentTime) return "default"; - - const now = currentTime; - let departureHour: number; - let departureMinute: number; - - if (Array.isArray(departureTime)) { - if (departureTime.length < 2) return "default"; - departureHour = departureTime[0] || 0; - departureMinute = departureTime[1] || 0; - } else if (typeof departureTime === 'string') { - const parts = departureTime.split(':'); - if (parts.length < 2) return "default"; - departureHour = parseInt(parts[0], 10); - departureMinute = parseInt(parts[1], 10); - } else { - return "default"; - } - - const departure = now.clone().hour(departureHour).minute(departureMinute).second(0); - const diffMinutes = departure.diff(now, 'minute'); - - if (diffMinutes < 0) return "error"; // Past due - if (diffMinutes <= 30) return "warning"; // Within 30 minutes - return "success"; // More than 30 minutes - }; - - return ( - - - {/* Title */} - - {t("Truck Schedule Dashboard")} - - - {/* Filter */} - - - - {t("Store ID")} - - - - - - {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} - - - - {/* Table */} - - {loading ? ( - - - - ) : ( - - - - - {t("Store ID")} - {t("Truck Schedule")} - {t("Time Remaining")} - {t("No. of Shops")} - {t("Total Items")} - {t("Tickets Released")} - {t("First Ticket Start")} - {t("Tickets Completed")} - {t("Last Ticket End")} - {t("Pick Time (min)")} - - - - {filteredData.length === 0 ? ( - - - - {t("No truck schedules available for today")} - - - - ) : ( - filteredData.map((row, index) => { - const timeRemaining = calculateTimeRemaining(row.truckDepartureTime); - const chipColor = getTimeChipColor(row.truckDepartureTime); - - return ( - 0 && row.numberOfTicketsCompleted >= row.numberOfPickTickets - ? 'success.light' - : 'inherit' - }} - > - - - - - - - {row.truckLanceCode || '-'} - - - ETD: {formatTime(row.truckDepartureTime)} - - - - - - - - - {row.numberOfShopsToServe} [{row.numberOfPickTickets}] - - - - - {row.totalItemsToPick} - - - - 0 ? 'info' : 'default'} - /> - - - {formatDateTime(row.firstTicketStartTime)} - - - 0 ? 'success' : 'default'} - /> - - - {formatDateTime(row.lastTicketEndTime)} - - - - {row.pickTimeTakenMinutes !== null ? row.pickTimeTakenMinutes : '-'} - - - - ); - }) - )} - -
-
- )} -
-
-
- ); -}; - -export default TruckScheduleDashboard; diff --git a/src/components/DashboardPage/truckSchedule/index.ts b/src/components/DashboardPage/truckSchedule/index.ts deleted file mode 100644 index b3609a6..0000000 --- a/src/components/DashboardPage/truckSchedule/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as TruckScheduleDashboard } from './TruckScheduleDashboard'; - - diff --git a/src/components/EquipmentSearch/EquipmentSearchLoading.tsx b/src/components/EquipmentSearch/EquipmentSearchLoading.tsx index 40a4253..100feb0 100644 --- a/src/components/EquipmentSearch/EquipmentSearchLoading.tsx +++ b/src/components/EquipmentSearch/EquipmentSearchLoading.tsx @@ -6,6 +6,7 @@ import Skeleton from "@mui/material/Skeleton"; import Stack from "@mui/material/Stack"; import React from "react"; +// Can make this nicer export const EquipmentTypeSearchLoading: React.FC = () => { return ( <> diff --git a/src/components/EquipmentSearch/EquipmentSearchResults.tsx b/src/components/EquipmentSearch/EquipmentSearchResults.tsx index dd2c45e..d35b83b 100644 --- a/src/components/EquipmentSearch/EquipmentSearchResults.tsx +++ b/src/components/EquipmentSearch/EquipmentSearchResults.tsx @@ -139,6 +139,7 @@ function isCheckboxColumn( return column.type === "checkbox"; } +// Icon Component Functions function convertObjectKeysToLowercase( obj: T, ): object | undefined { @@ -206,6 +207,7 @@ function EquipmentSearchResults({ const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); + /// this const handleChangePage: TablePaginationProps["onPageChange"] = ( _event, newPage, @@ -236,6 +238,7 @@ function EquipmentSearchResults({ } }; + // checkbox const currItems = useMemo(() => { return items.length > 10 ? items .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) @@ -251,6 +254,7 @@ function EquipmentSearchResults({ const handleRowClick = useCallback( (event: MouseEvent, item: T, columns: Column[]) => { + // check is disabled or not let disabled = false; columns.forEach((col) => { if (isCheckboxColumn(col) && col.disabled) { @@ -265,6 +269,7 @@ function EquipmentSearchResults({ return; } + // set id const id = item.id; if (setCheckboxIds) { const selectedIndex = checkboxIds.indexOf(id); @@ -330,7 +335,7 @@ function EquipmentSearchResults({ column.renderHeader() ) : ( column.label.split('\n').map((line, index) => ( -
{line}
+
{line}
// Render each line in a div )) )} @@ -437,6 +442,7 @@ function EquipmentSearchResults({ return noWrapper ? table : {table}; } +// Table cells interface TableCellsProps { column: Column; columnName: keyof T; diff --git a/src/components/EquipmentTypeSearch/EquipmentTypeSearchLoading.tsx b/src/components/EquipmentTypeSearch/EquipmentTypeSearchLoading.tsx index 75f1b1b..838189b 100644 --- a/src/components/EquipmentTypeSearch/EquipmentTypeSearchLoading.tsx +++ b/src/components/EquipmentTypeSearch/EquipmentTypeSearchLoading.tsx @@ -4,6 +4,7 @@ import Skeleton from "@mui/material/Skeleton"; import Stack from "@mui/material/Stack"; import React from "react"; +// Can make this nicer export const EquipmentTypeSearchLoading: React.FC = () => { return ( <> diff --git a/src/components/InventorySearch/InventoryLotLineTable.tsx b/src/components/InventorySearch/InventoryLotLineTable.tsx index f293ea4..b325aff 100644 --- a/src/components/InventorySearch/InventoryLotLineTable.tsx +++ b/src/components/InventorySearch/InventoryLotLineTable.tsx @@ -1,16 +1,21 @@ import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; -import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Column } from "../SearchResults"; import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; -import { CheckCircleOutline, DoDisturb, EditNote } from "@mui/icons-material"; import { arrayToDateString } from "@/app/utils/formatUtil"; -import { Typography } from "@mui/material"; -import { isFinite } from "lodash"; +import { Box, Card, Grid, IconButton, Modal, TextField, Typography, Button } from "@mui/material"; import useUploadContext from "../UploadProvider/useUploadContext"; import { downloadFile } from "@/app/utils/commonUtil"; import { fetchQrCodeByLotLineId, LotLineToQrcode } from "@/app/api/pdf/actions"; import QrCodeIcon from "@mui/icons-material/QrCode"; +import PrintIcon from "@mui/icons-material/Print"; +import SwapHoriz from "@mui/icons-material/SwapHoriz"; +import CloseIcon from "@mui/icons-material/Close"; +import { Autocomplete } from "@mui/material"; +import { WarehouseResult } from "@/app/api/warehouse"; +import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; +import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; interface Props { inventoryLotLines: InventoryLotLineResult[] | null; @@ -23,8 +28,26 @@ interface Props { const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory }) => { const { t } = useTranslation(["inventory"]); const { setIsUploading } = useUploadContext(); + const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); + const [selectedLotLine, setSelectedLotLine] = useState(null); + const [startLocation, setStartLocation] = useState(""); + const [targetLocation, setTargetLocation] = useState(""); + const [targetLocationInput, setTargetLocationInput] = useState(""); + const [qtyToBeTransferred, setQtyToBeTransferred] = useState(0); + const [warehouses, setWarehouses] = useState([]); - const printQrcode = useCallback(async (lotLineId: number) => { + useEffect(() => { + if (stockTransferModalOpen) { + fetchWarehouseListClient() + .then(setWarehouses) + .catch(console.error); + } + }, [stockTransferModalOpen]); + + const originalQty = selectedLotLine?.availableQty || 0; + const remainingQty = originalQty - qtyToBeTransferred; + + const downloadQrCode = useCallback(async (lotLineId: number) => { setIsUploading(true); // const postData = { stockInLineIds: [42,43,44] }; const postData: LotLineToQrcode = { @@ -37,12 +60,24 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr setIsUploading(false); }, [setIsUploading]); + const handleStockTransfer = useCallback( + (lotLine: InventoryLotLineResult) => { + setSelectedLotLine(lotLine); + setStockTransferModalOpen(true); + setStartLocation(lotLine.warehouse.code || ""); + setTargetLocation(""); + setTargetLocationInput(""); + setQtyToBeTransferred(0); + }, + [], + ); + const onDetailClick = useCallback( (lotLine: InventoryLotLineResult) => { - printQrcode(lotLine.id) + downloadQrCode(lotLine.id) // lot line id to find stock in line }, - [printQrcode], + [downloadQrCode], ); const columns = useMemo[]>( () => [ @@ -108,14 +143,32 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr name: "warehouse", label: t("Warehouse"), renderCell: (params) => { - return `${params.warehouse.code} - ${params.warehouse.name}` + return `${params.warehouse.code}` }, }, { name: "id", - label: t("qrcode"), + label: t("Download QR Code"), onClick: onDetailClick, buttonIcon: , + align: "center", + headerAlign: "center", + }, + { + name: "id", + label: t("Print QR Code"), + onClick: () => {}, + buttonIcon: , + align: "center", + headerAlign: "center", + }, + { + name: "id", + label: t("Stock Transfer"), + onClick: handleStockTransfer, + buttonIcon: , + align: "center", + headerAlign: "center", }, // { // name: "status", @@ -131,8 +184,39 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr // } // }, ], - [t], + [t, onDetailClick, downloadQrCode, handleStockTransfer], ); + + const handleCloseStockTransferModal = useCallback(() => { + setStockTransferModalOpen(false); + setSelectedLotLine(null); + setStartLocation(""); + setTargetLocation(""); + setTargetLocationInput(""); + setQtyToBeTransferred(0); + }, []); + + const handleSubmitStockTransfer = useCallback(async () => { + try { + setIsUploading(true); + + // Decrease the inQty (availableQty) in the source inventory lot line + + + // TODO: Add logic to increase qty in target location warehouse + + alert(t("Stock transfer successful")); + handleCloseStockTransferModal(); + + // TODO: Refresh the inventory lot lines list + } catch (error: any) { + console.error("Error transferring stock:", error); + alert(error?.message || t("Failed to transfer stock. Please try again.")); + } finally { + setIsUploading(false); + } + }, [selectedLotLine, targetLocation, qtyToBeTransferred, originalQty, handleCloseStockTransferModal, setIsUploading, t]); + return <> {inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")} @@ -142,6 +226,191 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr setPagingController={setPagingController} totalCount={totalCount} /> + + + + + + {inventory && selectedLotLine + ? `${inventory.itemCode} ${inventory.itemName} (${selectedLotLine.lotNo})` + : t("Stock Transfer") + } + + + + + + + + + + + {t("to")} + + + w.code !== startLocation)} + getOptionLabel={(option) => option.code || ""} + value={targetLocation ? warehouses.find(w => w.code === targetLocation) || null : null} + inputValue={targetLocationInput} + onInputChange={(event, newInputValue) => { + setTargetLocationInput(newInputValue); + if (targetLocation && newInputValue !== targetLocation) { + setTargetLocation(""); + } + }} + onChange={(event, newValue) => { + if (newValue) { + setTargetLocation(newValue.code); + setTargetLocationInput(newValue.code); + } else { + setTargetLocation(""); + setTargetLocationInput(""); + } + }} + filterOptions={(options, { inputValue }) => { + if (!inputValue || inputValue.trim() === "") return options; + const searchTerm = inputValue.toLowerCase().trim(); + return options.filter((option) => + (option.code || "").toLowerCase().includes(searchTerm) || + (option.name || "").toLowerCase().includes(searchTerm) || + (option.description || "").toLowerCase().includes(searchTerm) + ); + }} + isOptionEqualToValue={(option, value) => option.code === value.code} + autoHighlight={false} + autoSelect={false} + clearOnBlur={false} + renderOption={(props, option) => ( +
  • + {option.code} +
  • + )} + renderInput={(params) => ( + + )} + /> +
    +
    + + + + + + - + + + { + const value = parseInt(e.target.value) || 0; + const maxValue = Math.max(0, originalQty); + setQtyToBeTransferred(Math.min(Math.max(0, value), maxValue)); + }} + inputProps={{ min: 0, max: originalQty, step: 1 }} + InputLabelProps={{ + shrink: true, + sx: { fontSize: "0.9375rem" }, + }} + /> + + + = + + + + + + + + + + + +
    +
    + } diff --git a/src/components/ItemsSearch/ItemsSearch.tsx b/src/components/ItemsSearch/ItemsSearch.tsx index f8a2487..a178cd2 100644 --- a/src/components/ItemsSearch/ItemsSearch.tsx +++ b/src/components/ItemsSearch/ItemsSearch.tsx @@ -13,8 +13,6 @@ import { TypeEnum } from "@/app/utils/typeEnum"; import axios from "axios"; import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; import axiosInstance from "@/app/(main)/axios/axiosInstance"; -import { deleteItem } from "@/app/api/settings/item/actions"; -import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; type Props = { items: ItemsResult[]; @@ -52,6 +50,8 @@ const ItemsSearch: React.FC = ({ items }) => { [router], ); + const onDeleteClick = useCallback((item: ItemsResult) => {}, [router]); + const checkItemStatus = useCallback((item: ItemsResult): "complete" | "missing" => { // Check if type exists and is not empty const hasType = item.type != null && String(item.type).trim() !== ""; @@ -76,6 +76,48 @@ const ItemsSearch: React.FC = ({ items }) => { return "missing"; }, []); + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: onDetailClick, + buttonIcon: , + }, + { + name: "code", + label: t("Code"), + }, + { + name: "name", + label: t("Name"), + }, + { + name: "type", + label: t("Type"), + }, + { + name: "status", + label: t("Status"), + renderCell: (item) => { + const status = item.status || checkItemStatus(item); + if (status === "complete") { + return ; + } else { + return ; + } + }, + }, + { + name: "action", + label: t(""), + buttonIcon: , + onClick: onDeleteClick, + }, + ], + [onDeleteClick, onDetailClick, t, checkItemStatus], + ); + const refetchData = useCallback( async (filterObj: SearchQuery) => { const authHeader = axiosInstance.defaults.headers["Authorization"]; @@ -92,6 +134,8 @@ const ItemsSearch: React.FC = ({ items }) => { `${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { params }, ); + console.log("API Response:", response); + console.log("First record keys:", response.data?.records?.[0] ? Object.keys(response.data.records[0]) : "No records"); if (response.status == 200) { // Normalize field names and add status to each item const itemsWithStatus: ItemsResultWithStatus[] = response.data.records.map((item: any) => { @@ -106,12 +150,18 @@ const ItemsSearch: React.FC = ({ items }) => { qcCategory: item.qcCategory || (qcCategoryId ? { id: qcCategoryId } : undefined), }; + console.log("Normalized item:", { + id: normalizedItem.id, + LocationCode: normalizedItem.LocationCode, + qcCategoryId: qcCategoryId, + qcCategory: normalizedItem.qcCategory + }); + return { ...normalizedItem, status: checkItemStatus(normalizedItem), }; }); - console.log("Fetched items data:", itemsWithStatus); setFilteredItems(itemsWithStatus as ItemsResult[]); setTotalCount(response.data.total); return response; // Return the data from the response @@ -135,64 +185,6 @@ const ItemsSearch: React.FC = ({ items }) => { refetchData, ]); - const onDeleteClick = useCallback( - (item: ItemsResult) => { - deleteDialog(async () => { - if (item.id) { - const itemId = typeof item.id === "string" ? parseInt(item.id, 10) : item.id; - if (!isNaN(itemId)) { - await deleteItem(itemId); - await refetchData(filterObj); - await successDialog(t("Delete Success"), t); - } - } - }, t); - }, - [refetchData, filterObj, t], - ); - - const columns = useMemo[]>( - () => [ - { - name: "id", - label: t("Details"), - onClick: onDetailClick, - buttonIcon: , - }, - { - name: "code", - label: t("Code"), - }, - { - name: "name", - label: t("Name"), - }, - { - name: "type", - label: t("Type"), - }, - { - name: "status", - label: t("Status"), - renderCell: (item) => { - const status = item.status || checkItemStatus(item); - if (status === "complete") { - return ; - } else { - return ; - } - }, - }, - { - name: "action", - label: t(""), - buttonIcon: , - onClick: onDeleteClick, - }, - ], - [onDeleteClick, onDetailClick, t, checkItemStatus], - ); - const onReset = useCallback(() => { setFilteredItems(items); }, [items]); diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index 81f5b1e..22165c8 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -37,7 +37,6 @@ import { import { fetchPrinterCombo } from "@/app/api/settings/printer"; import { PrinterCombo } from "@/app/api/settings/printer"; import JoPickOrderDetail from "./JoPickOrderDetail"; -import MaterialPickStatusTable from "./MaterialPickStatusTable"; interface Props { pickOrders: PickOrderResult[]; printerCombo: PrinterCombo[]; @@ -490,7 +489,6 @@ const JodetailSearch: React.FC = ({ pickOrders, printerCombo }) => { - @@ -505,7 +503,6 @@ const JodetailSearch: React.FC = ({ pickOrders, printerCombo }) => { printQty={printQty} /> )} - {tabIndex === 2 && } ); diff --git a/src/components/Jodetail/MaterialPickStatusTable.tsx b/src/components/Jodetail/MaterialPickStatusTable.tsx deleted file mode 100644 index 4246138..0000000 --- a/src/components/Jodetail/MaterialPickStatusTable.tsx +++ /dev/null @@ -1,381 +0,0 @@ -"use client"; - -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { - Box, - Typography, - Card, - CardContent, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - CircularProgress, - TablePagination, -} from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import dayjs from 'dayjs'; -import { arrayToDayjs } from '@/app/utils/formatUtil'; -import { fetchMaterialPickStatus, MaterialPickStatusItem } from '@/app/api/jo/actions'; - -const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds - -const MaterialPickStatusTable: React.FC = () => { - const { t } = useTranslation("jo"); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const refreshCountRef = useRef(0); - const [paginationController, setPaginationController] = useState({ - pageNum: 0, - pageSize: 10, - }); - - const loadData = useCallback(async () => { - setLoading(true); - try { - const result = await fetchMaterialPickStatus(); - // On second refresh, clear completed pick orders - if (refreshCountRef.current >= 1) { - // const filtered = result.filter(item => - // item.pickStatus?.toLowerCase() !== 'completed' - //); - setData(result); - } else { - setData(result || []); - } - refreshCountRef.current += 1; - } catch (error) { - console.error('Error fetching material pick status:', error); - setData([]); // Set empty array on error to stop loading - } finally { - setLoading(false); - } - }, []); // Remove refreshCount from dependencies - - useEffect(() => { - // Initial load - loadData(); - - // Set up auto-refresh every 10 minutes - const interval = setInterval(() => { - loadData(); - }, REFRESH_INTERVAL); - - return () => clearInterval(interval); - }, [loadData]); // Only depend on loadData, which is now stable - - const formatTime = (timeData: any): string => { - if (!timeData) return ''; - - // Handle LocalDateTime ISO string format (e.g., "2026-01-09T18:01:54") - if (typeof timeData === 'string') { - // Try parsing as ISO string first (most common format from LocalDateTime) - const parsed = dayjs(timeData); - if (parsed.isValid()) { - return parsed.format('HH:mm'); - } - - // Try parsing as custom format YYYYMMDDHHmmss - const customParsed = dayjs(timeData, 'YYYYMMDDHHmmss'); - if (customParsed.isValid()) { - return customParsed.format('HH:mm'); - } - - // Try parsing as time string (HH:mm or HH:mm:ss) - const parts = timeData.split(':'); - if (parts.length >= 2) { - const hour = parseInt(parts[0], 10); - const minute = parseInt(parts[1] || '0', 10); - if (!isNaN(hour) && !isNaN(minute)) { - return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; - } - } - } else if (Array.isArray(timeData)) { - // Handle array format [year, month, day, hour, minute, second] - const hour = timeData[3] ?? timeData[0] ?? 0; - const minute = timeData[4] ?? timeData[1] ?? 0; - return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; - } - - return ''; - }; - - const calculatePickTime = (startTime: any, endTime: any): number => { - if (!startTime || !endTime) return 0; - - let start: dayjs.Dayjs; - let end: dayjs.Dayjs; - - // Parse start time - if (Array.isArray(startTime)) { - // Array format: [year, month, day, hour, minute, second] - if (startTime.length >= 5) { - const year = startTime[0] || 0; - const month = (startTime[1] || 1) - 1; // month is 0-indexed in JS Date - const day = startTime[2] || 1; - const hour = startTime[3] || 0; - const minute = startTime[4] || 0; - const second = startTime[5] || 0; - - // Create Date object and convert to dayjs - const date = new Date(year, month, day, hour, minute, second); - start = dayjs(date); - console.log('Parsed start time:', { - array: startTime, - date: date.toISOString(), - dayjs: start.format('YYYY-MM-DD HH:mm:ss'), - isValid: start.isValid() - }); - } else { - // Fallback to arrayToDayjs for shorter arrays - start = arrayToDayjs(startTime, true); - } - } else if (typeof startTime === 'string') { - // Try ISO format first - start = dayjs(startTime); - if (!start.isValid()) { - // Try custom format - start = dayjs(startTime, 'YYYYMMDDHHmmss'); - } - } else { - start = dayjs(startTime); - } - - // Parse end time - if (Array.isArray(endTime)) { - // Array format: [year, month, day, hour, minute, second] - if (endTime.length >= 5) { - const year = endTime[0] || 0; - const month = (endTime[1] || 1) - 1; // month is 0-indexed in JS Date - const day = endTime[2] || 1; - const hour = endTime[3] || 0; - const minute = endTime[4] || 0; - const second = endTime[5] || 0; - - // Create Date object and convert to dayjs - const date = new Date(year, month, day, hour, minute, second); - end = dayjs(date); - console.log('Parsed end time:', { - array: endTime, - date: date.toISOString(), - dayjs: end.format('YYYY-MM-DD HH:mm:ss'), - isValid: end.isValid() - }); - } else { - // Fallback to arrayToDayjs for shorter arrays - end = arrayToDayjs(endTime, true); - } - } else if (typeof endTime === 'string') { - // Try ISO format first - end = dayjs(endTime); - if (!end.isValid()) { - // Try custom format - end = dayjs(endTime, 'YYYYMMDDHHmmss'); - } - } else { - end = dayjs(endTime); - } - - if (!start.isValid() || !end.isValid()) { - console.warn('Invalid time values:', { - startTime, - endTime, - startValid: start.isValid(), - endValid: end.isValid(), - startFormat: start.isValid() ? start.format() : 'invalid', - endFormat: end.isValid() ? end.format() : 'invalid' - }); - return 0; - } - - // Calculate difference in seconds first, then convert to minutes - // This handles sub-minute differences correctly - const diffSeconds = end.diff(start, 'second'); - const diffMinutes = Math.ceil(diffSeconds / 60); // Round up to nearest minute - - console.log('Time calculation:', { - start: start.format('YYYY-MM-DD HH:mm:ss'), - end: end.format('YYYY-MM-DD HH:mm:ss'), - diffSeconds, - diffMinutes - }); - - return diffMinutes > 0 ? diffMinutes : 0; - }; - - const handlePageChange = useCallback((event: unknown, newPage: number) => { - setPaginationController(prev => ({ - ...prev, - pageNum: newPage, - })); - }, []); - - const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { - 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 data.slice(startIndex, endIndex); - }, [data, paginationController]); - - return ( - - - {/* Title */} - - - {t("Material Pick Status")} - - - - - - - - {loading ? ( - - - - ) : ( - <> - - - - - - - - {t("Pick Order No.- Job Order No.- Item")} - - - - - - - - - {t("Job Order Qty")} - - - - - - - - - {t("No. of Items to be Picked")} - - - - - - - - {t("No. of Items with Issue During Pick")} - - - - - - - - {t("Pick Start Time")} - - - - - - - - {t("Pick End Time")} - - - - - - - - {t("Pick Time Taken (minutes)")} - - - - - - - - {paginatedData.length === 0 ? ( - - - {t("No data available")} - - - ) : ( - paginatedData.map((row) => { - const pickTimeTaken = calculatePickTime(row.pickStartTime, row.pickEndTime); - - return ( - - - {row.pickOrderCode || '-'} -
    - {row.jobOrderCode || '-'} -
    - {row.itemCode || '-'} {row.itemName || '-'} - - -
    - - - {row.jobOrderQty !== null && row.jobOrderQty !== undefined - ? `${row.jobOrderQty} ${row.uom || ''}` - : '-'} - - {row.numberOfItemsToPick ?? 0} - {row.numberOfItemsWithIssue ?? 0} - {formatTime(row.pickStartTime) || '-'} - {formatTime(row.pickEndTime) || '-'} - - {pickTimeTaken > 0 ? `${pickTimeTaken} ${t("minutes")}` : '-'} - -
    - ); - }) - )} -
    -
    -
    - {data.length > 0 && ( - - )} - - )} -
    -
    -
    - ); -}; - -export default MaterialPickStatusTable; \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 5448a6d..6f2a516 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -129,11 +129,6 @@ const NavigationContent: React.FC = () => { label: "Finished Good Order", path: "/finishedGood", }, - { - icon: , - label: "Stock Record", - path: "/stockRecord", - }, ], }, { @@ -248,13 +243,6 @@ const NavigationContent: React.FC = () => { requiredAbility: TESTING, isHidden: false, }, - { - icon: , - label: "Report Management", - path: "/report", - requiredAbility: TESTING, - isHidden: false, - }, { icon: , label: "Settings", diff --git a/src/components/ProductionProcess/JobProcessStatus.tsx b/src/components/ProductionProcess/JobProcessStatus.tsx deleted file mode 100644 index e0fc43b..0000000 --- a/src/components/ProductionProcess/JobProcessStatus.tsx +++ /dev/null @@ -1,324 +0,0 @@ -"use client"; - -import React, { useState, useEffect, useCallback, useRef } from 'react'; - -import { - Box, - Typography, - Card, - CardContent, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - CircularProgress, -} from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import dayjs from 'dayjs'; -import { fetchJobProcessStatus, JobProcessStatusResponse } from '@/app/api/jo/actions'; -import { arrayToDayjs } from '@/app/utils/formatUtil'; - -const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes - -const JobProcessStatus: React.FC = () => { - const { t } = useTranslation(["common", "jo"]); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const refreshCountRef = useRef(0); - const [currentTime, setCurrentTime] = useState(dayjs()); - - // Update current time every second for countdown - useEffect(() => { - const timer = setInterval(() => { - setCurrentTime(dayjs()); - }, 1000); - return () => clearInterval(timer); - }, []); - - const loadData = useCallback(async () => { - setLoading(true); - try { - const result = await fetchJobProcessStatus(); - - // On second refresh, filter out completed jobs - if (refreshCountRef.current >= 1) { - const filtered = result.filter(item => { - // Check if all required processes are completed - const allCompleted = item.processes - .filter(p => p.isRequired) - .every(p => p.endTime != null); - return !allCompleted; - }); - setData(filtered); - } else { - setData(result); - } - refreshCountRef.current += 1; - } catch (error) { - console.error('Error fetching job process status:', error); - setData([]); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - loadData(); - const interval = setInterval(() => { - loadData(); - }, REFRESH_INTERVAL); - return () => clearInterval(interval); - }, [loadData]); - - const formatTime = (timeData: any): string => { - if (!timeData) return '-'; // 改为返回 '-' 而不是 'N/A' - - // Handle array format [year, month, day, hour, minute, second] - if (Array.isArray(timeData)) { - try { - const parsed = arrayToDayjs(timeData, true); - if (parsed.isValid()) { - return parsed.format('HH:mm'); - } - } catch (error) { - console.error('Error parsing array time:', error); - } - } - - // Handle LocalDateTime ISO string format (e.g., "2026-01-09T18:01:54") - if (typeof timeData === 'string') { - const parsed = dayjs(timeData); - if (parsed.isValid()) { - return parsed.format('HH:mm'); - } - } - - return '-'; - }; - - const calculateRemainingTime = (planEndTime: any, processingTime: number | null, setupTime: number | null, changeoverTime: number | null): string => { - if (!planEndTime) return '-'; - - let endTime: dayjs.Dayjs; - - // Handle array format [year, month, day, hour, minute, second] - // Use arrayToDayjs for consistency with other parts of the codebase - if (Array.isArray(planEndTime)) { - try { - endTime = arrayToDayjs(planEndTime, true); - console.log('Parsed planEndTime array:', { - array: planEndTime, - parsed: endTime.format('YYYY-MM-DD HH:mm:ss'), - isValid: endTime.isValid() - }); - } catch (error) { - console.error('Error parsing array planEndTime:', error); - return '-'; - } - } else if (typeof planEndTime === 'string') { - endTime = dayjs(planEndTime); - console.log('Parsed planEndTime string:', { - string: planEndTime, - parsed: endTime.format('YYYY-MM-DD HH:mm:ss'), - isValid: endTime.isValid() - }); - } else { - return '-'; - } - - if (!endTime.isValid()) { - console.error('Invalid endTime:', planEndTime); - return '-'; - } - - const diff = endTime.diff(currentTime, 'minute'); - console.log('Remaining time calculation:', { - endTime: endTime.format('YYYY-MM-DD HH:mm:ss'), - currentTime: currentTime.format('YYYY-MM-DD HH:mm:ss'), - diffMinutes: diff - }); - - // If the planned end time is in the past, show 0 (or you could show negative time) - if (diff < 0) return '0'; - - const hours = Math.floor(diff / 60); - const minutes = diff % 60; - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; - }; - - const calculateWaitTime = ( - currentProcessEndTime: any, - nextProcessStartTime: any, - isLastProcess: boolean - ): string => { - if (isLastProcess) return '-'; - if (!currentProcessEndTime) return '-'; - if (nextProcessStartTime) return '0'; // Next process has started, stop counting - - let endTime: dayjs.Dayjs; - - // Handle array format - if (Array.isArray(currentProcessEndTime)) { - try { - endTime = arrayToDayjs(currentProcessEndTime, true); - } catch (error) { - console.error('Error parsing array endTime:', error); - return '-'; - } - } else if (typeof currentProcessEndTime === 'string') { - endTime = dayjs(currentProcessEndTime); - } else { - return '-'; - } - - if (!endTime.isValid()) return '-'; - - const diff = currentTime.diff(endTime, 'minute'); - return diff > 0 ? diff.toString() : '0'; - }; - - return ( - - - - - {t("Job Process Status", )} - - - - - - {loading ? ( - - - - ) : ( - - - - - - - {t("Job Order No.")} - - - - - {t("FG / WIP Item")} - - - - - {t("Production Time Remaining")} - - - - - - {[1, 2, 3, 4, 5, 6].map((num) => ( - - - {t("Process")} {num} - - - ))} - - - {[1, 2, 3, 4, 5, 6].map((num) => ( - - - - {t("Start")} - - - {t("Finish")} - - - {t("Wait Time [minutes]")} - - - - ))} - - - - {data.length === 0 ? ( - - - {t("No data available")} - - - ) : ( - data.map((row) => ( - - - {row.jobOrderCode || '-'} - - - {row.itemCode || '-'} - {row.itemName || '-'} - - - - {calculateRemainingTime(row.planEndTime, row.processingTime, row.setupTime, row.changeoverTime)} - - {row.processes.map((process, index) => { - const isLastProcess = index === row.processes.length - 1 || - !row.processes.slice(index + 1).some(p => p.isRequired); - const nextProcess = index < row.processes.length - 1 ? row.processes[index + 1] : null; - const waitTime = calculateWaitTime( - process.endTime, - nextProcess?.startTime, - isLastProcess - ); - - // 如果工序不是必需的,只显示一个 N/A - if (!process.isRequired) { - return ( - - - N/A - - - ); - } - - // 如果工序是必需的,显示三行(Start、Finish、Wait Time) - return ( - - - {process.equipmentCode || '-'} - - {formatTime(process.startTime)} - - - {formatTime(process.endTime)} - - 0 ? 'warning.main' : 'text.primary' - }}> - {waitTime} - - - - ); - })} - - )) - )} - -
    -
    - )} -
    - - -
    -
    - ); -}; - -export default JobProcessStatus; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 9a724d7..aed2342 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -142,7 +142,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess } // 3) 更新 JO 状态 - // await updateJo({ id: process.jobOrderId, status: "completed" }); + await updateJo({ id: process.jobOrderId, status: "completed" }); // 4) 刷新列表 await fetchProcesses(); diff --git a/src/components/ProductionProcess/ProductionProcessPage.tsx b/src/components/ProductionProcess/ProductionProcessPage.tsx index 3297d79..bdad5e6 100644 --- a/src/components/ProductionProcess/ProductionProcessPage.tsx +++ b/src/components/ProductionProcess/ProductionProcessPage.tsx @@ -8,7 +8,6 @@ import ProductionProcessDetail from "@/components/ProductionProcess/ProductionPr import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; -import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; import { fetchProductProcesses, fetchProductProcessesByJobOrderId, @@ -165,7 +164,6 @@ const ProductionProcessPage: React.FC = ({ printerCo - {tabIndex === 0 && ( @@ -192,9 +190,6 @@ const ProductionProcessPage: React.FC = ({ printerCo selectedPrinter={selectedPrinter} /> )} - {tabIndex === 2 && ( - - )} ); }; diff --git a/src/components/Qc/QcComponent.tsx b/src/components/Qc/QcComponent.tsx index ae194a2..474b4ba 100644 --- a/src/components/Qc/QcComponent.tsx +++ b/src/components/Qc/QcComponent.tsx @@ -285,7 +285,7 @@ const fetchNewQcData = useCallback( } },[fetchEscalationLogsByStockInLines] ); - +// // Set QC Data useEffect(() => { if (itemDetail) { diff --git a/src/components/Qc/QcForm.tsx b/src/components/Qc/QcForm.tsx index 10d46d4..30a35f6 100644 --- a/src/components/Qc/QcForm.tsx +++ b/src/components/Qc/QcForm.tsx @@ -16,7 +16,7 @@ import { useGridApiRef, } from "@mui/x-data-grid"; import { QcFormInput, QcResult } from "@/app/api/qc"; - +// interface Props { rows: QcResult[]; disabled?: boolean; diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index 035325b..2fb4250 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -54,6 +54,7 @@ const style = { display: "block", width: { xs: "90%", sm: "90%", md: "90%" }, height: { xs: "90%", sm: "90%", md: "90%" }, + // }; interface CommonProps extends Omit { // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] } | undefined; diff --git a/src/components/Shop/TruckLaneDetail.tsx b/src/components/Shop/TruckLaneDetail.tsx index e8eef0f..21b5536 100644 --- a/src/components/Shop/TruckLaneDetail.tsx +++ b/src/components/Shop/TruckLaneDetail.tsx @@ -69,7 +69,6 @@ const TruckLaneDetail: React.FC = () => { const [uniqueRemarks, setUniqueRemarks] = useState([]); const [uniqueShopCodes, setUniqueShopCodes] = useState([]); const [uniqueShopNames, setUniqueShopNames] = useState([]); - const [shopNameByCodeMap, setShopNameByCodeMap] = useState>(new Map()); const [addShopDialogOpen, setAddShopDialogOpen] = useState(false); const [newShop, setNewShop] = useState({ shopName: "", @@ -87,12 +86,11 @@ const TruckLaneDetail: React.FC = () => { useEffect(() => { const fetchAutocompleteData = async () => { try { - const [shopData, remarks, codes, names, allShopsFromShopTable] = await Promise.all([ + const [shopData, remarks, codes, names] = await Promise.all([ findAllUniqueShopNamesAndCodesFromTrucksClient() as Promise>, findAllUniqueRemarksFromTrucksClient() as Promise, findAllUniqueShopCodesFromTrucksClient() as Promise, findAllUniqueShopNamesFromTrucksClient() as Promise, - fetchAllShopsClient() as Promise, ]); // Convert to Shop format (id will be 0 since we don't have shop IDs from truck table) @@ -107,15 +105,6 @@ const TruckLaneDetail: React.FC = () => { setUniqueRemarks(remarks || []); setUniqueShopCodes(codes || []); setUniqueShopNames(names || []); - - // Create lookup map: shopCode -> shopName from shop table - const shopNameMap = new Map(); - (allShopsFromShopTable || []).forEach((shop) => { - if (shop.code) { - shopNameMap.set(String(shop.code).trim().toLowerCase(), String(shop.name || "").trim()); - } - }); - setShopNameByCodeMap(shopNameMap); } catch (err) { console.error("Failed to load autocomplete data:", err); } @@ -711,7 +700,6 @@ const TruckLaneDetail: React.FC = () => { {t("Shop Name")} - {t("Shop Branch")} {t("Shop Code")} {t("Remark")} {t("Loading Sequence")} @@ -721,7 +709,7 @@ const TruckLaneDetail: React.FC = () => { {shopsData.length === 0 ? ( - + {t("No shops found using this truck lane")} @@ -731,14 +719,6 @@ const TruckLaneDetail: React.FC = () => { shopsData.map((shop, index) => ( - {/* Shop Name from shop table (read-only, looked up by shop code) */} - {(() => { - const shopCode = String(shop.code || "").trim().toLowerCase(); - return shopNameByCodeMap.get(shopCode) || "-"; - })()} - - - {/* Shop Branch from truck table (editable) */} {editingRowIndex === index ? ( )} */} - { - const expiryDateValue = watch("expiryDate"); - return ( - + { + return ( + { - if (!date) { - setValue("expiryDate", ""); - return; - } + if (!date) return; + console.log(date.format(INPUT_DATE_FORMAT)); setValue("expiryDate", date.format(INPUT_DATE_FORMAT)); }} inputRef={field.ref} @@ -418,10 +416,10 @@ return ( }, }} /> - - ); - }} -/> + + ); + }} + /> {/* {putawayMode ? ( diff --git a/src/components/StockRecord/SearchPage.tsx b/src/components/StockRecord/SearchPage.tsx deleted file mode 100644 index 8d1c02a..0000000 --- a/src/components/StockRecord/SearchPage.tsx +++ /dev/null @@ -1,444 +0,0 @@ -"use client"; - -import SearchBox, { Criterion } from "../SearchBox"; -import { useCallback, useMemo, useState, useEffect, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import SearchResults, { Column } from "../SearchResults/index"; -import { StockTransactionResponse, SearchStockTransactionRequest } from "@/app/api/stockTake/actions"; -import { decimalFormatter } from "@/app/utils/formatUtil"; -import { Stack, Box } from "@mui/material"; -import { searchStockTransactions } from "@/app/api/stockTake/actions"; - -interface Props { - dataList: StockTransactionResponse[]; -} - -type SearchQuery = { - itemCode?: string; - itemName?: string; - type?: string; - startDate?: string; - endDate?: string; -}; - -// 扩展类型以包含计算字段 -interface ExtendedStockTransaction extends StockTransactionResponse { - formattedDate: string; - inQty: number; - outQty: number; - balanceQty: number; -} - -const SearchPage: React.FC = ({ dataList: initialDataList }) => { - const { t } = useTranslation("inventory"); - - // 添加数据状态 - const [dataList, setDataList] = useState(initialDataList); - const [loading, setLoading] = useState(false); - const [filterArgs, setFilterArgs] = useState>({}); - const isInitialMount = useRef(true); - - // 添加分页状态 - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(10); - const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 }); - const [hasSearchQuery, setHasSearchQuery] = useState(false); - const [totalCount, setTotalCount] = useState(initialDataList.length); - - const processedData = useMemo(() => { - // 按日期和 itemId 排序 - 优先使用 date 字段,如果没有则使用 transactionDate - const sorted = [...dataList].sort((a, b) => { - // 优先使用 date 字段,如果没有则使用 transactionDate 的日期部分 - const getDateValue = (item: StockTransactionResponse): number => { - if (item.date) { - return new Date(item.date).getTime(); - } - if (item.transactionDate) { - if (Array.isArray(item.transactionDate)) { - const [year, month, day] = item.transactionDate; - return new Date(year, month - 1, day).getTime(); - } else { - return new Date(item.transactionDate).getTime(); - } - } - return 0; - }; - - const dateA = getDateValue(a); - const dateB = getDateValue(b); - - if (dateA !== dateB) return dateA - dateB; // 从旧到新排序 - return a.itemId - b.itemId; - }); - - // 计算每个 item 的累计余额 - const balanceMap = new Map(); // itemId -> balance - const processed: ExtendedStockTransaction[] = []; - - sorted.forEach((item) => { - const currentBalance = balanceMap.get(item.itemId) || 0; - let newBalance = currentBalance; - - // 根据类型计算余额 - if (item.transactionType === "IN") { - newBalance = currentBalance + item.qty; - } else if (item.transactionType === "OUT") { - newBalance = currentBalance - item.qty; - } - - balanceMap.set(item.itemId, newBalance); - - // 格式化日期 - 优先使用 date 字段 - let formattedDate = ""; - if (item.date) { - // 如果 date 是字符串格式 "yyyy-MM-dd" - const date = new Date(item.date); - if (!isNaN(date.getTime())) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - formattedDate = `${year}-${month}-${day}`; - } - } else if (item.transactionDate) { - // 回退到 transactionDate - if (Array.isArray(item.transactionDate)) { - const [year, month, day] = item.transactionDate; - formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; - } else if (typeof item.transactionDate === 'string') { - const date = new Date(item.transactionDate); - if (!isNaN(date.getTime())) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - formattedDate = `${year}-${month}-${day}`; - } - } else { - const date = new Date(item.transactionDate); - if (!isNaN(date.getTime())) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - formattedDate = `${year}-${month}-${day}`; - } - } - } - - processed.push({ - ...item, - formattedDate, - inQty: item.transactionType === "IN" ? item.qty : 0, - outQty: item.transactionType === "OUT" ? item.qty : 0, - balanceQty: item.balanceQty ? item.balanceQty : newBalance, - }); - }); - - return processed; - }, [dataList]); - // 修复:使用 processedData 初始化 filteredList - const [filteredList, setFilteredList] = useState(processedData); - - // 当 processedData 变化时更新 filteredList(不更新 pagingController,避免循环) - useEffect(() => { - setFilteredList(processedData); - setTotalCount(processedData.length); - // 只在初始加载时设置 pageSize - if (isInitialMount.current && processedData.length > 0) { - setPageSize("all"); - setPagingController(prev => ({ ...prev, pageSize: processedData.length })); - setPage(0); - isInitialMount.current = false; - } - }, [processedData]); - - // API 调用函数(参考 PoSearch 的实现) - // API 调用函数(参考 PoSearch 的实现) -const newPageFetch = useCallback( - async ( - pagingController: Record, - filterArgs: Record, - ) => { - setLoading(true); - try { - // 处理空字符串,转换为 null - const itemCode = filterArgs.itemCode?.trim() || null; - const itemName = filterArgs.itemName?.trim() || null; - - // 验证:至少需要 itemCode 或 itemName - if (!itemCode && !itemName) { - console.warn("Search requires at least itemCode or itemName"); - setDataList([]); - setTotalCount(0); - return; - } - - const params: SearchStockTransactionRequest = { - itemCode: itemCode, - itemName: itemName, - type: filterArgs.type?.trim() || null, - startDate: filterArgs.startDate || null, - endDate: filterArgs.endDate || null, - pageNum: pagingController.pageNum - 1 || 0, - pageSize: pagingController.pageSize || 100, - }; - - console.log("Search params:", params); // 添加调试日志 - - const res = await searchStockTransactions(params); - console.log("Search response:", res); // 添加调试日志 - - if (res && Array.isArray(res)) { - setDataList(res); - } else { - console.error("Invalid response format:", res); - setDataList([]); - } - } catch (error) { - console.error("Fetch error:", error); - setDataList([]); - } finally { - setLoading(false); - } - }, - [], -); - - // 使用 useRef 来存储上一次的值,避免不必要的 API 调用 - const prevPagingControllerRef = useRef(pagingController); - const prevFilterArgsRef = useRef(filterArgs); - const hasSearchedRef = useRef(false); - // 当 filterArgs 或 pagingController 变化时调用 API(只在真正变化时调用) - useEffect(() => { - // 检查是否有有效的搜索条件 - const hasValidSearch = filterArgs.itemCode || filterArgs.itemName; - - if (!hasValidSearch) { - // 如果没有有效搜索条件,只更新 ref,不调用 API - if (isInitialMount.current) { - isInitialMount.current = false; - } - prevFilterArgsRef.current = filterArgs; - return; - } - - // 检查是否真的变化了 - const pagingChanged = - prevPagingControllerRef.current.pageNum !== pagingController.pageNum || - prevPagingControllerRef.current.pageSize !== pagingController.pageSize; - - const filterChanged = JSON.stringify(prevFilterArgsRef.current) !== JSON.stringify(filterArgs); - - // 如果是第一次有效搜索,或者条件/分页发生变化,则调用 API - if (!hasSearchedRef.current || pagingChanged || filterChanged) { - newPageFetch(pagingController, filterArgs); - prevPagingControllerRef.current = pagingController; - prevFilterArgsRef.current = filterArgs; - hasSearchedRef.current = true; - isInitialMount.current = false; - } - }, [newPageFetch, pagingController, filterArgs]); - - // 分页处理函数 - const handleChangePage = useCallback((event: unknown, newPage: number) => { - setPage(newPage); - setPagingController(prev => ({ ...prev, pageNum: newPage + 1 })); - }, []); - - const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => { - const newSize = parseInt(event.target.value, 10); - if (newSize === -1) { - setPageSize("all"); - setPagingController(prev => ({ ...prev, pageSize: filteredList.length, pageNum: 1 })); - } else if (!isNaN(newSize)) { - setPageSize(newSize); - setPagingController(prev => ({ ...prev, pageSize: newSize, pageNum: 1 })); - } - setPage(0); - }, [filteredList.length]); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Item Code"), - paramName: "itemCode", - type: "text", - }, - { - label: t("Item Name"), - paramName: "itemName", - type: "text", - }, - { - label: t("Type"), - paramName: "type", - type: "text", - }, - { - label: t("Start Date"), - paramName: "startDate", - type: "date", - }, - { - label: t("End Date"), - paramName: "endDate", - type: "date", - }, - ], - [t], - ); - - const columns = useMemo[]>( - () => [ - { - name: "formattedDate" as keyof ExtendedStockTransaction, - label: t("Date"), - align: "left", - }, - { - name: "itemCode" as keyof ExtendedStockTransaction, - label: t("Item-lotNo"), - align: "left", - renderCell: (item) => ( - - - {item.itemCode || "-"} {item.itemName || "-"} - {item.lotNo || "-"} - - - ), - }, - { - name: "inQty" as keyof ExtendedStockTransaction, - label: t("In Qty"), - align: "left", - type: "decimal", - renderCell: (item) => ( - <>{item.inQty > 0 ? decimalFormatter.format(item.inQty) : ""} - ), - }, - { - name: "outQty" as keyof ExtendedStockTransaction, - label: t("Out Qty"), - align: "left", - type: "decimal", - renderCell: (item) => ( - <>{item.outQty > 0 ? decimalFormatter.format(item.outQty) : ""} - ), - }, - { - name: "balanceQty" as keyof ExtendedStockTransaction, - label: t("Balance Qty"), - align: "left", - type: "decimal", - }, - { - name: "type", - label: t("Type"), - align: "left", - renderCell: (item) => { - if (!item.type) return "-"; - return t(item.type.toLowerCase()); - }, - }, - { - name: "status", - label: t("Status"), - align: "left", - renderCell: (item) => { - if (!item.status) return "-"; - return t(item.status.toLowerCase()); - }, - }, - ], - [t], - ); - - const handleSearch = useCallback((query: Record) => { - // 检查是否有搜索条件 - const itemCode = query.itemCode?.trim(); - const itemName = query.itemName?.trim(); - const type = query.type?.trim(); - const startDate = query.startDate === "Invalid Date" ? "" : query.startDate; - const endDate = query.endDate === "Invalid Date" ? "" : query.endDate; - - // 验证:至少需要 itemCode 或 itemName - if (!itemCode && !itemName) { - // 可以显示提示信息 - console.warn("Please enter at least Item Code or Item Name"); - return; - } - - const hasQuery = !!(itemCode || itemName || type || startDate || endDate); - setHasSearchQuery(hasQuery); - - // 更新 filterArgs,触发 useEffect 调用 API - setFilterArgs({ - itemCode: itemCode || undefined, - itemName: itemName || undefined, - type: type || undefined, - startDate: startDate || undefined, - endDate: endDate || undefined, - }); - - // 重置分页 - setPage(0); - setPagingController(prev => ({ ...prev, pageNum: 1 })); - }, []); - - const handleReset = useCallback(() => { - setHasSearchQuery(false); - // 重置 filterArgs,触发 useEffect 调用 API - setFilterArgs({}); - setPage(0); - setPagingController(prev => ({ ...prev, pageNum: 1 })); - }, []); - - // 计算实际显示的 items(分页) - const paginatedItems = useMemo(() => { - if (pageSize === "all") { - return filteredList; - } - const actualPageSize = typeof pageSize === 'number' ? pageSize : 10; - const startIndex = page * actualPageSize; - const endIndex = startIndex + actualPageSize; - return filteredList.slice(startIndex, endIndex); - }, [filteredList, page, pageSize]); - - // 计算传递给 SearchResults 的 pageSize(确保在选项中) - const actualPageSizeForTable = useMemo(() => { - if (pageSize === "all") { - return filteredList.length; - } - const size = typeof pageSize === 'number' ? pageSize : 10; - // 如果 size 不在标准选项中,使用 "all" 模式 - if (![10, 25, 100].includes(size)) { - return filteredList.length; - } - return size; - }, [pageSize, filteredList.length]); - - return ( - <> - - {loading && {t("Loading...")}} - - items={paginatedItems} - columns={columns} - pagingController={{ ...pagingController, pageSize: actualPageSizeForTable }} - setPagingController={setPagingController} - totalCount={totalCount} - isAutoPaging={false} - /> - - ); -}; - -export default SearchPage; \ No newline at end of file diff --git a/src/components/StockRecord/index.tsx b/src/components/StockRecord/index.tsx deleted file mode 100644 index e5b59a6..0000000 --- a/src/components/StockRecord/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import GeneralLoading from "../General/GeneralLoading"; -import SearchPage from "./SearchPage"; -import { searchStockTransactions } from "@/app/api/stockTake/actions"; - -interface SubComponents { - Loading: typeof GeneralLoading; -} - -const Wrapper: React.FC & SubComponents = async () => { - // 初始加载时使用空参数,SearchPage 会在用户搜索时调用 API - const dataList = await searchStockTransactions({ - startDate: null, - endDate: null, - itemCode: null, - itemName: null, - type: null, - pageNum: 0, - pageSize: 100, - }); - - return ; -}; - -Wrapper.Loading = GeneralLoading; - -export default Wrapper; \ No newline at end of file diff --git a/src/components/StockTakeManagement/ApproverCardList.tsx b/src/components/StockTakeManagement/ApproverCardList.tsx index 8c92cdf..153f5a7 100644 --- a/src/components/StockTakeManagement/ApproverCardList.tsx +++ b/src/components/StockTakeManagement/ApproverCardList.tsx @@ -201,7 +201,23 @@ const ApproverCardList: React.FC = ({ onCardClick }) => { {t("Control Time")}: - + {session.totalInventoryLotNumber > 0 && ( + + + + {t("Progress")} + + + {completionRate}% + + + + + )}
    diff --git a/src/components/StockTakeManagement/ApproverStockTake.tsx b/src/components/StockTakeManagement/ApproverStockTake.tsx index 512898a..a036bd0 100644 --- a/src/components/StockTakeManagement/ApproverStockTake.tsx +++ b/src/components/StockTakeManagement/ApproverStockTake.tsx @@ -14,14 +14,10 @@ import { TableHead, TableRow, Paper, - Checkbox, TextField, - FormControlLabel, Radio, - TablePagination, - ToggleButton } from "@mui/material"; -import { useState, useCallback, useEffect, useRef, useMemo } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { AllPickedStockTakeListReponse, @@ -56,8 +52,7 @@ const ApproverStockTake: React.FC = ({ const [inventoryLotDetails, setInventoryLotDetails] = useState([]); const [loadingDetails, setLoadingDetails] = useState(false); - const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(false); - + // 每个记录的选择状态,key 为 detail.id const [qtySelection, setQtySelection] = useState>({}); const [approverQty, setApproverQty] = useState>({}); @@ -65,111 +60,28 @@ const ApproverStockTake: React.FC = ({ const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [updatingStatus, setUpdatingStatus] = useState(false); - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState("all"); - const [total, setTotal] = useState(0); - const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); - const handleChangePage = useCallback((event: unknown, newPage: number) => { - setPage(newPage); - }, []); - - const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => { - const newSize = parseInt(event.target.value, 10); - if (newSize === -1) { - setPageSize("all"); - } else if (!isNaN(newSize)) { - setPageSize(newSize); - } - setPage(0); - }, []); - - const loadDetails = useCallback(async (pageNum: number, size: number | string) => { - setLoadingDetails(true); - try { - let actualSize: number; - if (size === "all") { - if (selectedSession.totalInventoryLotNumber > 0) { - actualSize = selectedSession.totalInventoryLotNumber; - } else if (total > 0) { - actualSize = total; - } else { - actualSize = 10000; - } - } else { - actualSize = typeof size === 'string' ? parseInt(size, 10) : size; - } - - const response = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, - pageNum, - actualSize - ); - setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); - setTotal(response.total || 0); - } catch (e) { - console.error(e); - setInventoryLotDetails([]); - setTotal(0); - } finally { - setLoadingDetails(false); - } - }, [selectedSession, total]); - - useEffect(() => { - loadDetails(page, pageSize); - }, [page, pageSize, loadDetails]); - const calculateDifference = useCallback((detail: InventoryLotDetailResponse, selection: QtySelectionType): number => { - let selectedQty = 0; - - if (selection === "first") { - selectedQty = detail.firstStockTakeQty || 0; - } else if (selection === "second") { - selectedQty = detail.secondStockTakeQty || 0; - } else if (selection === "approver") { - selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0")) || 0; - } - - const bookQty = detail.availableQty || 0; - return selectedQty - bookQty; - }, [approverQty, approverBadQty]); - - // 3. 修改默认选择逻辑(在 loadDetails 的 useEffect 中,或创建一个新的 useEffect) useEffect(() => { - // 初始化默认选择:如果 second 存在则选择 second,否则选择 first - const newSelections: Record = {}; - inventoryLotDetails.forEach(detail => { - if (!qtySelection[detail.id]) { - // 如果 second 不为 null 且大于 0,默认选择 second,否则选择 first - if (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0) { - newSelections[detail.id] = "second"; - } else { - newSelections[detail.id] = "first"; - } + const loadDetails = async () => { + setLoadingDetails(true); + try { + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + } finally { + setLoadingDetails(false); } - }); - - if (Object.keys(newSelections).length > 0) { - setQtySelection(prev => ({ ...prev, ...newSelections })); - } - }, [inventoryLotDetails]); - - // 4. 添加过滤逻辑(在渲染表格之前) - const filteredDetails = useMemo(() => { - if (!showOnlyWithDifference) { - return inventoryLotDetails; - } - - return inventoryLotDetails.filter(detail => { - const selection = qtySelection[detail.id] || (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first"); - const difference = calculateDifference(detail, selection); - return difference !== 0; - }); - }, [inventoryLotDetails, showOnlyWithDifference, qtySelection, calculateDifference]); - + }; + loadDetails(); + }, [selectedSession]); + const handleSaveApproverStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { if (!selectedSession || !currentUserId) { return; @@ -223,7 +135,11 @@ const ApproverStockTake: React.FC = ({ onSnackbar(t("Approver stock take record saved successfully"), "success"); - await loadDetails(page, pageSize); + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("Save approver stock take record error:", e); let errorMessage = t("Failed to save approver stock take record"); @@ -243,8 +159,7 @@ const ApproverStockTake: React.FC = ({ } finally { setSaving(false); } - }, [selectedSession, qtySelection, approverQty, approverBadQty, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); - + }, [selectedSession, qtySelection, approverQty, approverBadQty, t, currentUserId, onSnackbar]); const handleUpdateStatusToNotMatch = useCallback(async (detail: InventoryLotDetailResponse) => { if (!detail.stockTakeRecordId) { onSnackbar(t("Stock take record ID is required"), "error"); @@ -256,6 +171,12 @@ const ApproverStockTake: React.FC = ({ await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId); onSnackbar(t("Stock take record status updated to not match"), "success"); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("Update stock take record status error:", e); let errorMessage = t("Failed to update stock take record status"); @@ -274,20 +195,8 @@ const ApproverStockTake: React.FC = ({ onSnackbar(errorMessage, "error"); } finally { setUpdatingStatus(false); - // Reload after status update - the useEffect will handle it with current page/pageSize - // Or explicitly reload: - setPage((currentPage) => { - setPageSize((currentPageSize) => { - setTimeout(() => { - loadDetails(currentPage, currentPageSize); - }, 0); - return currentPageSize; - }); - return currentPage; - }); } - }, [selectedSession, t, onSnackbar, loadDetails]); - + }, [selectedSession, t, onSnackbar]); const handleBatchSubmitAll = useCallback(async () => { if (!selectedSession || !currentUserId) { console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); @@ -314,7 +223,11 @@ const ApproverStockTake: React.FC = ({ result.errorCount > 0 ? "warning" : "success" ); - await loadDetails(page, pageSize); + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("handleBatchSubmitAll: Error:", e); let errorMessage = t("Failed to batch save approver stock take records"); @@ -334,12 +247,11 @@ const ApproverStockTake: React.FC = ({ } finally { setBatchSaving(false); } - }, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); + }, [selectedSession, t, currentUserId, onSnackbar]); useEffect(() => { handleBatchSubmitAllRef.current = handleBatchSubmitAll; }, [handleBatchSubmitAll]); - const formatNumber = (num: number | null | undefined): string => { if (num == null) return "0.00"; return num.toLocaleString('en-US', { @@ -347,7 +259,6 @@ const ApproverStockTake: React.FC = ({ maximumFractionDigits: 2 }); }; - const uniqueWarehouses = Array.from( new Set( inventoryLotDetails @@ -355,7 +266,6 @@ const ApproverStockTake: React.FC = ({ .filter(warehouse => warehouse && warehouse.trim() !== "") ) ).join(", "); - const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { // Only allow editing if there's a first stock take qty if (!detail.firstStockTakeQty || detail.firstStockTakeQty === 0) { @@ -370,270 +280,232 @@ const ApproverStockTake: React.FC = ({ {t("Back to List")} - - {t("Stock Take Section")}: {selectedSession.stockTakeSession} - {uniqueWarehouses && ( - <> {t("Warehouse")}: {uniqueWarehouses} - )} - + + {t("Stock Take Section")}: {selectedSession.stockTakeSession} + {uniqueWarehouses && ( + <> {t("Warehouse")}: {uniqueWarehouses} + )} + - - - - - + + {loadingDetails ? ( ) : ( - <> - - - + +
    + + + {t("Warehouse Location")} + {t("Item-lotNo-ExpiryDate")} + {t("Stock Take Qty(include Bad Qty)= Available Qty")} + {t("Remark")} + {t("UOM")} + {t("Record Status")} + {t("Action")} + + + + {inventoryLotDetails.length === 0 ? ( - {t("Warehouse Location")} - {t("Item-lotNo-ExpiryDate")} - {t("Stock Take Qty(include Bad Qty)= Available Qty")} - {t("Remark")} - {t("UOM")} - {t("Record Status")} - {t("Action")} + + + {t("No data")} + + - - - {filteredDetails.length === 0 ? ( - - - - {t("No data")} - - - - ) : ( - filteredDetails.map((detail) => { - const submitDisabled = isSubmitDisabled(detail); - const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty > 0; - const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0; - const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); + ) : ( + inventoryLotDetails.map((detail) => { + const submitDisabled = isSubmitDisabled(detail); + const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty > 0; + const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0; + const selection = qtySelection[detail.id] || "first"; - return ( - - {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} - + return ( + + {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} + + + {detail.itemCode || "-"} {detail.itemName || "-"} + {detail.lotNo || "-"} + {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} + {/**/} + + + + + {detail.finalQty != null ? ( + - {detail.itemCode || "-"} {detail.itemName || "-"} - {detail.lotNo || "-"} - {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} + + {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber((detail.finalQty || 0) - (detail.availableQty || 0))} + - - - - {detail.finalQty != null ? ( - - {(() => { - const finalDifference = (detail.finalQty || 0) - (detail.availableQty || 0); - const differenceColor = finalDifference > 0 - ? 'error.main' - : finalDifference < 0 - ? 'error.main' - : 'success.main'; - - return ( - - {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber(finalDifference)} - - ); - })()} - - ) : ( - - {hasFirst && ( - - setQtySelection({ ...qtySelection, [detail.id]: "first" })} - /> - - {t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)} - - - )} - - {hasSecond && ( - - setQtySelection({ ...qtySelection, [detail.id]: "second" })} - /> - - {t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)} - - - )} - - {hasSecond && ( - - setQtySelection({ ...qtySelection, [detail.id]: "approver" })} - /> - {t("Approver Input")}: - setApproverQty({ ...approverQty, [detail.id]: e.target.value })} - sx={{ - width: 130, - minWidth: 130, - '& .MuiInputBase-input': { - height: '1.4375em', - padding: '4px 8px' - } - }} - placeholder={t("Stock Take Qty") } - disabled={selection !== "approver"} - /> - - setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} - sx={{ - width: 130, - minWidth: 130, - '& .MuiInputBase-input': { - height: '1.4375em', - padding: '4px 8px' - } - }} - placeholder={t("Bad Qty")} - disabled={selection !== "approver"} - /> - - ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} - - - )} - - {(() => { - let selectedQty = 0; - - if (selection === "first") { - selectedQty = detail.firstStockTakeQty || 0; - } else if (selection === "second") { - selectedQty = detail.secondStockTakeQty || 0; - } else if (selection === "approver") { - selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0; - } - - const bookQty = detail.availableQty || 0; - const difference = selectedQty - bookQty; - const differenceColor = difference > 0 - ? 'error.main' - : difference < 0 - ? 'error.main' - : 'success.main'; - - return ( - - {t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)} - - ); - })()} - - )} - - - - - {detail.remarks || "-"} - - - - {detail.uom || "-"} - - - {detail.stockTakeRecordStatus === "pass" ? ( - - ) : detail.stockTakeRecordStatus === "notMatch" ? ( - - ) : ( - - )} - - - {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( - - - - )} -
    - {detail.finalQty == null && ( - - - - )} -
    -
    - ); - }) - )} -
    -
    -
    - - + ) : ( + + + {hasFirst && ( + + setQtySelection({ ...qtySelection, [detail.id]: "first" })} + /> + + {t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)} + + + )} + + + {hasSecond && ( + + setQtySelection({ ...qtySelection, [detail.id]: "second" })} + /> + + {t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)} + + + )} + + + {hasSecond && ( + + setQtySelection({ ...qtySelection, [detail.id]: "approver" })} + /> + {t("Approver Input")}: + setApproverQty({ ...approverQty, [detail.id]: e.target.value })} + sx={{ + width: 130, + minWidth: 130, + '& .MuiInputBase-input': { + height: '1.4375em', + + padding: '4px 8px' + } + }} + placeholder={t("Stock Take Qty") } + disabled={selection !== "approver"} + /> + + setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} + sx={{ + width: 130, + minWidth: 130, + '& .MuiInputBase-input': { + height: '1.4375em', + padding: '4px 8px' + } + }} + placeholder={t("Bad Qty")} + disabled={selection !== "approver"} + /> + + ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} + + + )} + + + {(() => { + let selectedQty = 0; + + if (selection === "first") { + selectedQty = detail.firstStockTakeQty || 0; + } else if (selection === "second") { + selectedQty = detail.secondStockTakeQty || 0; + } else if (selection === "approver") { + selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0; + } + + const bookQty = detail.availableQty || 0; + const difference = selectedQty - bookQty; + + return ( + + {t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)} + + ); + })()} + + )} + + + + + {detail.remarks || "-"} + + + + {detail.uom || "-"} + + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( + + + + )} +
    + {detail.finalQty == null && ( + + + + )} +
    + + ); + }) + )} + + + )} ); diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx index 15c437a..a6affe8 100644 --- a/src/components/StockTakeManagement/PickerCardList.tsx +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -224,7 +224,23 @@ const PickerCardList: React.FC = ({ onCardClick, onReStockT {t("Control Time")}: {t("Total Item Number")}: {session.totalItemNumber} - + {session.totalInventoryLotNumber > 0 && ( + + + + {t("Progress")} + + + {completionRate}% + + + + + )} diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx index e186194..e47dbe8 100644 --- a/src/components/StockTakeManagement/PickerReStockTake.tsx +++ b/src/components/StockTakeManagement/PickerReStockTake.tsx @@ -15,7 +15,6 @@ import { TableRow, Paper, TextField, - TablePagination, } from "@mui/material"; import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; @@ -34,13 +33,13 @@ import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; -interface PickerReStockTakeProps { +interface PickerStockTakeProps { selectedSession: AllPickedStockTakeListReponse; onBack: () => void; onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; } -const PickerReStockTake: React.FC = ({ +const PickerStockTake: React.FC = ({ selectedSession, onBack, onSnackbar, @@ -61,63 +60,28 @@ const PickerReStockTake: React.FC = ({ const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [shortcutInput, setShortcutInput] = useState(""); - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState("all"); - const [total, setTotal] = useState(0); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); - - const handleChangePage = useCallback((event: unknown, newPage: number) => { - setPage(newPage); - }, []); - - const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => { - const newSize = parseInt(event.target.value, 10); - if (newSize === -1) { - setPageSize("all"); - } else if (!isNaN(newSize)) { - setPageSize(newSize); - } - setPage(0); - }, []); - const loadDetails = useCallback(async (pageNum: number, size: number | string) => { - setLoadingDetails(true); - try { - let actualSize: number; - if (size === "all") { - if (selectedSession.totalInventoryLotNumber > 0) { - actualSize = selectedSession.totalInventoryLotNumber; - } else if (total > 0) { - actualSize = total; - } else { - actualSize = 10000; - } - } else { - actualSize = typeof size === 'string' ? parseInt(size, 10) : size; - } - - const response = await getInventoryLotDetailsBySectionNotMatch( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, - pageNum, - actualSize - ); - setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); - setTotal(response.total || 0); - } catch (e) { - console.error(e); - setInventoryLotDetails([]); - setTotal(0); - } finally { - setLoadingDetails(false); - } - }, [selectedSession, total]); - useEffect(() => { - loadDetails(page, pageSize); - }, [page, pageSize, loadDetails]); + const loadDetails = async () => { + setLoadingDetails(true); + try { + const details = await getInventoryLotDetailsBySectionNotMatch( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + } finally { + setLoadingDetails(false); + } + }; + loadDetails(); + }, [selectedSession]); const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { setEditingRecord(detail); @@ -167,9 +131,9 @@ const PickerReStockTake: React.FC = ({ badQty: parseFloat(badQty), remark: isSecondSubmit ? (remark || null) : null, }; - console.log('handleSaveStockTake: request:', request); - console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); - console.log('handleSaveStockTake: currentUserId:', currentUserId); + console.log('handleSaveStockTake: request:', request); + console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); + console.log('handleSaveStockTake: currentUserId:', currentUserId); await saveStockTakeRecord( request, selectedSession.stockTakeId, @@ -179,7 +143,11 @@ const PickerReStockTake: React.FC = ({ onSnackbar(t("Stock take record saved successfully"), "success"); handleCancelEdit(); - await loadDetails(page, pageSize); + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("Save stock take record error:", e); let errorMessage = t("Failed to save stock take record"); @@ -199,7 +167,7 @@ const PickerReStockTake: React.FC = ({ } finally { setSaving(false); } - }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); + }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]); const handleBatchSubmitAll = useCallback(async () => { if (!selectedSession || !currentUserId) { @@ -227,7 +195,11 @@ const PickerReStockTake: React.FC = ({ result.errorCount > 0 ? "warning" : "success" ); - await loadDetails(page, pageSize); + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("handleBatchSubmitAll: Error:", e); let errorMessage = t("Failed to batch save stock take records"); @@ -247,7 +219,7 @@ const PickerReStockTake: React.FC = ({ } finally { setBatchSaving(false); } - }, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); + }, [selectedSession, t, currentUserId, onSnackbar]); useEffect(() => { handleBatchSubmitAllRef.current = handleBatchSubmitAll; @@ -353,213 +325,213 @@ const PickerReStockTake: React.FC = ({ ) : ( - <> - - - + +
    + + + {t("Warehouse Location")} + {t("Item-lotNo-ExpiryDate")} + + {t("Qty")} + {t("Bad Qty")} + {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} + {t("Remark")} + + {t("UOM")} + + {t("Record Status")} + {t("Action")} + + + + {inventoryLotDetails.length === 0 ? ( - {t("Warehouse Location")} - {t("Item-lotNo-ExpiryDate")} - {t("Qty")} - {t("Bad Qty")} - {t("Remark")} - {t("UOM")} - {t("Record Status")} - {t("Action")} + + + {t("No data")} + + - - - {inventoryLotDetails.length === 0 ? ( - - - - {t("No data")} - - - - ) : ( - inventoryLotDetails.map((detail) => { - const isEditing = editingRecord?.id === detail.id; - const submitDisabled = isSubmitDisabled(detail); - const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; - const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; + ) : ( + inventoryLotDetails.map((detail) => { + const isEditing = editingRecord?.id === detail.id; + const submitDisabled = isSubmitDisabled(detail); + const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; - return ( - - {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} - - - {detail.itemCode || "-"} {detail.itemName || "-"} - {detail.lotNo || "-"} - {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} - - - - - - {isEditing && isFirstSubmit ? ( - setFirstQty(e.target.value)} - sx={{ width: 100 }} - /> - ) : detail.firstStockTakeQty ? ( - - {t("First")}: {detail.firstStockTakeQty.toFixed(2)} - - ) : null} - - {isEditing && isSecondSubmit ? ( - setSecondQty(e.target.value)} - sx={{ width: 100 }} - /> - ) : detail.secondStockTakeQty ? ( - - {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} - - ) : null} - - {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( - - - - - )} - - - - - {isEditing && isFirstSubmit ? ( - setFirstBadQty(e.target.value)} - sx={{ width: 100 }} - /> - ) : detail.firstBadQty != null && detail.firstBadQty > 0 ? ( - - {t("First")}: {detail.firstBadQty.toFixed(2)} - - ) : ( - - {t("First")}: 0.00 - - )} - - {isEditing && isSecondSubmit ? ( - setSecondBadQty(e.target.value)} - sx={{ width: 100 }} - /> - ) : detail.secondBadQty != null && detail.secondBadQty > 0 ? ( - - {t("Second")}: {detail.secondBadQty.toFixed(2)} - - ) : null} - - {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( - - - - - )} - - - + return ( + + {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} + + + {detail.itemCode || "-"} {detail.itemName || "-"} + {detail.lotNo || "-"} + {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} + {/**/} + + + + + + {isEditing && isFirstSubmit ? ( + setFirstQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.firstStockTakeQty ? ( + + {t("First")}: {detail.firstStockTakeQty.toFixed(2)} + + ) : null} + {isEditing && isSecondSubmit ? ( - <> - {t("Remark")} - setRemark(e.target.value)} - sx={{ width: 150 }} - /> - - ) : ( + setSecondQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.secondStockTakeQty ? ( - {detail.remarks || "-"} + {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} + + ) : null} + + {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( + + - )} - - {detail.uom || "-"} + + + + + {isEditing && isFirstSubmit ? ( + setFirstBadQty(e.target.value)} + sx={{ width: 100 }} + /> + ) : detail.firstBadQty != null && detail.firstBadQty > 0 ? ( + + {t("First")}: {detail.firstBadQty.toFixed(2)} + + ) : ( + + + {t("First")}: 0.00 + + )} + + {isEditing && isSecondSubmit ? ( + setSecondBadQty(e.target.value)} + sx={{ width: 100 }} + /> + ) : detail.secondBadQty != null && detail.secondBadQty > 0 ? ( + + {t("Second")}: {detail.secondBadQty.toFixed(2)} + + ) : null} + + {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( + + - + + )} + + + + {isEditing && isSecondSubmit ? ( + <> + {t("Remark")} + setRemark(e.target.value)} + sx={{ width: 150 }} + // If you want a single-line input, remove multiline/rows: + // multiline + // rows={2} + /> + + ) : ( + + {detail.remarks || "-"} + + )} + + {detail.uom || "-"} - - {detail.stockTakeRecordStatus === "pass" ? ( - - ) : detail.stockTakeRecordStatus === "notMatch" ? ( - - ) : ( - - )} - - - {isEditing ? ( - - - - - ) : ( + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + {isEditing ? ( + - )} - - - ); - }) - )} - -
    -
    - - + + + + ) : ( + + )} + + + ); + }) + )} + + + )} ); }; -export default PickerReStockTake; \ No newline at end of file +export default PickerStockTake; \ No newline at end of file diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index 9c49f44..e1dfa1b 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -15,13 +15,7 @@ import { TableRow, Paper, TextField, - TablePagination, - Select, // Add this - MenuItem, // Add this - FormControl, // Add this - InputLabel, } from "@mui/material"; -import { SelectChangeEvent } from "@mui/material/Select"; import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { @@ -66,76 +60,29 @@ const PickerStockTake: React.FC = ({ const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [shortcutInput, setShortcutInput] = useState(""); - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState("all"); - - const [total, setTotal] = useState(0); - const totalPages = pageSize === "all" ? 1 : Math.ceil(total / (pageSize as number)); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); - const handleChangePage = useCallback((event: unknown, newPage: number) => { - setPage(newPage); - }, []); - const handlePageSelectChange = useCallback((event: SelectChangeEvent) => { - const newPage = parseInt(event.target.value as string, 10) - 1; // Convert to 0-indexed - setPage(Math.max(0, Math.min(newPage, totalPages - 1))); - }, [totalPages]); - - const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => { - const newSize = parseInt(event.target.value, 10); - if (newSize === -1) { - setPageSize("all"); - } else if (!isNaN(newSize)) { - setPageSize(newSize); - } - setPage(0); - }, []); - const loadDetails = useCallback(async (pageNum: number, size: number | string) => { - console.log('loadDetails called with:', { pageNum, size, selectedSessionTotal: selectedSession.totalInventoryLotNumber }); - setLoadingDetails(true); - try { - let actualSize: number; - if (size === "all") { - // Use totalInventoryLotNumber from selectedSession if available - if (selectedSession.totalInventoryLotNumber > 0) { - actualSize = selectedSession.totalInventoryLotNumber; - console.log('Using "all" - actualSize set to totalInventoryLotNumber:', actualSize); - } else if (total > 0) { - // Fallback to total from previous response - actualSize = total; - console.log('Using "all" - actualSize set to total from state:', actualSize); - } else { - // Last resort: use a large number - actualSize = 10000; - console.log('Using "all" - actualSize set to default 10000'); - } - } else { - actualSize = typeof size === 'string' ? parseInt(size, 10) : size; - console.log('Using specific size - actualSize set to:', actualSize); - } - - console.log('Calling getInventoryLotDetailsBySection with actualSize:', actualSize); - const response = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, - pageNum, - actualSize - ); - setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); - setTotal(response.total || 0); - } catch (e) { - console.error(e); - setInventoryLotDetails([]); - setTotal(0); - } finally { - setLoadingDetails(false); - } - }, [selectedSession, total]); - + useEffect(() => { - loadDetails(page, pageSize); - }, [page, pageSize, loadDetails]); + const loadDetails = async () => { + setLoadingDetails(true); + try { + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + } finally { + setLoadingDetails(false); + } + }; + loadDetails(); + }, [selectedSession]); + const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { setEditingRecord(detail); @@ -229,9 +176,12 @@ const PickerStockTake: React.FC = ({ onSnackbar(t("Stock take record saved successfully"), "success"); handleCancelEdit(); - - await loadDetails(page, pageSize); - + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("Save stock take record error:", e); let errorMessage = t("Failed to save stock take record"); @@ -263,9 +213,6 @@ const PickerStockTake: React.FC = ({ t, currentUserId, onSnackbar, - loadDetails, - page, - pageSize, ] ); @@ -296,7 +243,11 @@ const PickerStockTake: React.FC = ({ result.errorCount > 0 ? "warning" : "success" ); - await loadDetails(page, pageSize); + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("handleBatchSubmitAll: Error:", e); let errorMessage = t("Failed to batch save stock take records"); @@ -442,290 +393,278 @@ const PickerStockTake: React.FC = ({ ) : ( - <> - - - + +
    + + + {t("Warehouse Location")} + {t("Item-lotNo-ExpiryDate")} + {t("Stock Take Qty(include Bad Qty)= Available Qty")} + {t("Remark")} + {t("UOM")} + {t("Record Status")} + {t("Action")} + + + + {inventoryLotDetails.length === 0 ? ( - {t("Warehouse Location")} - {t("Item-lotNo-ExpiryDate")} - {t("Stock Take Qty(include Bad Qty)= Available Qty")} - {t("Remark")} - {t("UOM")} - {t("Record Status")} - {t("Action")} + + + {t("No data")} + + - - - {inventoryLotDetails.length === 0 ? ( - - - - {t("No data")} - - - - ) : ( - inventoryLotDetails.map((detail) => { - const isEditing = editingRecord?.id === detail.id; - const submitDisabled = isSubmitDisabled(detail); - const isFirstSubmit = - !detail.stockTakeRecordId || !detail.firstStockTakeQty; - const isSecondSubmit = - detail.stockTakeRecordId && - detail.firstStockTakeQty && - !detail.secondStockTakeQty; - - return ( - - - {detail.warehouseArea || "-"} - {detail.warehouseSlot || "-"} - - - - - {detail.itemCode || "-"} {detail.itemName || "-"} - - {detail.lotNo || "-"} - - {detail.expiryDate - ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) - : "-"} - - - - - {/* Qty + Bad Qty 合并显示/输入 */} - - - {/* First */} - {isEditing && isFirstSubmit ? ( - - {t("First")}: - setFirstQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Stock Take Qty")} - /> - setFirstBadQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Bad Qty")} - /> - - = - {formatNumber( - parseFloat(firstQty || "0") - - parseFloat(firstBadQty || "0") - )} - - - ) : detail.firstStockTakeQty != null ? ( - - {t("First")}:{" "} - {formatNumber( - (detail.firstStockTakeQty ?? 0) + - (detail.firstBadQty ?? 0) - )}{" "} - ( - {formatNumber( - detail.firstBadQty ?? 0 - )} - ) ={" "} - {formatNumber(detail.firstStockTakeQty ?? 0)} - - ) : null} - - {/* Second */} - {isEditing && isSecondSubmit ? ( - - {t("Second")}: - setSecondQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Stock Take Qty")} - /> - setSecondBadQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Bad Qty")} - /> - - = - {formatNumber( - parseFloat(secondQty || "0") - - parseFloat(secondBadQty || "0") - )} - - - ) : detail.secondStockTakeQty != null ? ( + ) : ( + inventoryLotDetails.map((detail) => { + const isEditing = editingRecord?.id === detail.id; + const submitDisabled = isSubmitDisabled(detail); + const isFirstSubmit = + !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = + detail.stockTakeRecordId && + detail.firstStockTakeQty && + !detail.secondStockTakeQty; + + return ( + + + {detail.warehouseArea || "-"} + {detail.warehouseSlot || "-"} + + + + + {detail.itemCode || "-"} {detail.itemName || "-"} + + {detail.lotNo || "-"} + + {detail.expiryDate + ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) + : "-"} + + + + + {/* Qty + Bad Qty 合并显示/输入 */} + + + {/* First */} + {isEditing && isFirstSubmit ? ( + + {t("First")}: + setFirstQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Stock Take Qty")} + /> + setFirstBadQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Bad Qty")} + /> - {t("Second")}:{" "} - {formatNumber( - (detail.secondStockTakeQty ?? 0) + - (detail.secondBadQty ?? 0) - )}{" "} - ( + = {formatNumber( - detail.secondBadQty ?? 0 + parseFloat(firstQty || "0") - + parseFloat(firstBadQty || "0") )} - ) ={" "} - {formatNumber(detail.secondStockTakeQty ?? 0)} - ) : null} - - {!detail.firstStockTakeQty && - !detail.secondStockTakeQty && - !isEditing && ( - - - - + + ) : detail.firstStockTakeQty != null ? ( + + {t("First")}:{" "} + {formatNumber( + (detail.firstStockTakeQty ?? 0) + + (detail.firstBadQty ?? 0) + )}{" "} + ( + {formatNumber( + detail.firstBadQty ?? 0 )} - - + ) ={" "} + {formatNumber(detail.firstStockTakeQty ?? 0)} + + ) : null} - {/* Remark */} - + {/* Second */} {isEditing && isSecondSubmit ? ( - <> - {t("Remark")} + + {t("Second")}: setRemark(e.target.value)} - sx={{ width: 150 }} + type="number" + value={secondQty} + onChange={(e) => setSecondQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Stock Take Qty")} /> - - ) : ( + setSecondBadQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Bad Qty")} + /> + + = + {formatNumber( + parseFloat(secondQty || "0") - + parseFloat(secondBadQty || "0") + )} + + + ) : detail.secondStockTakeQty != null ? ( - {detail.remarks || "-"} + {t("Second")}:{" "} + {formatNumber( + (detail.secondStockTakeQty ?? 0) + + (detail.secondBadQty ?? 0) + )}{" "} + ( + {formatNumber( + detail.secondBadQty ?? 0 + )} + ) ={" "} + {formatNumber(detail.secondStockTakeQty ?? 0)} - )} - - - {detail.uom || "-"} - - - {detail.stockTakeRecordStatus === "pass" ? ( - - ) : detail.stockTakeRecordStatus === "notMatch" ? ( - - ) : ( - + - + + )} + + + + {/* Remark */} + + {isEditing && isSecondSubmit ? ( + <> + {t("Remark")} + setRemark(e.target.value)} + sx={{ width: 150 }} /> - )} - - - - {isEditing ? ( - - - - - ) : ( + + ) : ( + + {detail.remarks || "-"} + + )} + + + {detail.uom || "-"} + + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + + {isEditing ? ( + + - )} - - - ); - }) - )} - -
    -
    - - + + ) : ( + + )} + + + ); + }) + )} + + + )} ); diff --git a/src/components/WarehouseHandle/WarehouseHandle.tsx b/src/components/WarehouseHandle/WarehouseHandle.tsx index 97e471b..453de68 100644 --- a/src/components/WarehouseHandle/WarehouseHandle.tsx +++ b/src/components/WarehouseHandle/WarehouseHandle.tsx @@ -53,6 +53,8 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { successDialog(t("Delete Success"), t); } catch (error) { console.error("Failed to delete warehouse:", error); + // Don't redirect on error, just show error message + // The error will be logged but user stays on the page } }, t); }, [t, router]); @@ -74,14 +76,18 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { try { let results: WarehouseResult[] = warehouses; + // Build search pattern from the four fields: store_idF-warehouse-area-slot + // Only search by code field - match the code that follows this pattern const storeId = searchInputs.store_id?.trim() || ""; const warehouse = searchInputs.warehouse?.trim() || ""; const area = searchInputs.area?.trim() || ""; const slot = searchInputs.slot?.trim() || ""; const stockTakeSection = searchInputs.stockTakeSection?.trim() || ""; + // If any field has a value, filter by code pattern and stockTakeSection if (storeId || warehouse || area || slot || stockTakeSection) { results = warehouses.filter((warehouseItem) => { + // Filter by stockTakeSection if provided if (stockTakeSection) { const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); if (!itemStockTakeSection.includes(stockTakeSection.toLowerCase())) { @@ -89,6 +95,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { } } + // Filter by code pattern if any code-related field is provided if (storeId || warehouse || area || slot) { if (!warehouseItem.code) { return false; @@ -96,6 +103,8 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { const codeValue = String(warehouseItem.code).toLowerCase(); + // Check if code matches the pattern: store_id-warehouse-area-slot + // Match each part if provided const codeParts = codeValue.split("-"); if (codeParts.length >= 4) { @@ -112,6 +121,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { return storeIdMatch && warehouseMatch && areaMatch && slotMatch; } + // Fallback: if code doesn't follow the pattern, check if it contains any of the search terms const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase()); const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase()); const areaMatch = !area || codeValue.includes(area.toLowerCase()); @@ -120,9 +130,11 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { return storeIdMatch && warehouseMatch && areaMatch && slotMatch; } + // If only stockTakeSection is provided, return true (already filtered above) return true; }); } else { + // If no search terms, show all warehouses results = warehouses; } @@ -130,6 +142,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); } catch (error) { console.error("Error searching warehouses:", error); + // Fallback: filter by code pattern and stockTakeSection const storeId = searchInputs.store_id?.trim().toLowerCase() || ""; const warehouse = searchInputs.warehouse?.trim().toLowerCase() || ""; const area = searchInputs.area?.trim().toLowerCase() || ""; @@ -138,6 +151,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { setFilteredWarehouse( warehouses.filter((warehouseItem) => { + // Filter by stockTakeSection if provided if (stockTakeSection) { const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); if (!itemStockTakeSection.includes(stockTakeSection)) { @@ -145,6 +159,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { } } + // Filter by code if any code-related field is provided if (storeId || warehouse || area || slot) { if (!warehouseItem.code) { return false; @@ -252,6 +267,7 @@ const WarehouseHandle: React.FC = ({ warehouses }) => { justifyContent: "flex-start", }} > + {/* 樓層 field with F inside on the right */} = ({ warehouses }) => { - + {/* 倉庫 field */} = ({ warehouses }) => { - + {/* 區域 field */} = ({ warehouses }) => { - + {/* 儲位 field */} = ({ warehouses }) => { size="small" sx={{ width: "150px", minWidth: "120px" }} /> + {/* 盤點區域 field */} = ({ userTabContent, equipmentTabContent, - warehouseTabContent, }) => { const { t } = useTranslation("common"); const { t: tUser } = useTranslation("user"); - const { t: tWarehouse } = useTranslation("warehouse"); const searchParams = useSearchParams(); const router = useRouter(); const getInitialTab = () => { const tab = searchParams.get("tab"); if (tab === "equipment") return 1; - if (tab === "warehouse") return 2; if (tab === "user") return 0; return 0; }; @@ -58,8 +54,6 @@ const QrCodeHandleTabs: React.FC = ({ const tab = searchParams.get("tab"); if (tab === "equipment") { setCurrentTab(1); - } else if (tab === "warehouse") { - setCurrentTab(2); } else if (tab === "user") { setCurrentTab(0); } @@ -67,9 +61,7 @@ const QrCodeHandleTabs: React.FC = ({ const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setCurrentTab(newValue); - let tabName = "user"; - if (newValue === 1) tabName = "equipment"; - else if (newValue === 2) tabName = "warehouse"; + const tabName = newValue === 1 ? "equipment" : "user"; const params = new URLSearchParams(searchParams.toString()); params.set("tab", tabName); router.push(`?${params.toString()}`, { scroll: false }); @@ -81,7 +73,6 @@ const QrCodeHandleTabs: React.FC = ({ - @@ -92,10 +83,6 @@ const QrCodeHandleTabs: React.FC = ({ {equipmentTabContent} - - - {warehouseTabContent} - ); }; diff --git a/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx deleted file mode 100644 index 7bed7ec..0000000 --- a/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx +++ /dev/null @@ -1,675 +0,0 @@ -"use client"; - -import { useCallback, useMemo, useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import SearchResults, { Column } from "../SearchResults"; -import { successDialog } from "../Swal/CustomAlerts"; -import useUploadContext from "../UploadProvider/useUploadContext"; -import { downloadFile } from "@/app/utils/commonUtil"; -import { WarehouseResult } from "@/app/api/warehouse"; -import { exportWarehouseQrCode } from "@/app/api/warehouse/client"; -import { - Checkbox, - Box, - Button, - TextField, - Stack, - Autocomplete, - Modal, - Card, - CardContent, - CardActions, - IconButton, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - Typography, - InputAdornment -} from "@mui/material"; -import DownloadIcon from "@mui/icons-material/Download"; -import PrintIcon from "@mui/icons-material/Print"; -import CloseIcon from "@mui/icons-material/Close"; -import RestartAlt from "@mui/icons-material/RestartAlt"; -import Search from "@mui/icons-material/Search"; -import { PrinterCombo } from "@/app/api/settings/printer"; - -interface Props { - warehouses: WarehouseResult[]; - printerCombo: PrinterCombo[]; -} - -const QrCodeHandleWarehouseSearch: React.FC = ({ warehouses, printerCombo }) => { - const { t } = useTranslation(["warehouse", "common"]); - const [filteredWarehouses, setFilteredWarehouses] = useState(warehouses); - const { setIsUploading } = useUploadContext(); - const [pagingController, setPagingController] = useState({ - pageNum: 1, - pageSize: 10, - }); - - const [checkboxIds, setCheckboxIds] = useState([]); - const [selectAll, setSelectAll] = useState(false); - const [printQty, setPrintQty] = useState(1); - const [isSearching, setIsSearching] = useState(false); - - const [previewOpen, setPreviewOpen] = useState(false); - const [previewUrl, setPreviewUrl] = useState(null); - - const [selectedWarehousesModalOpen, setSelectedWarehousesModalOpen] = useState(false); - - const [searchInputs, setSearchInputs] = useState({ - store_id: "", - warehouse: "", - area: "", - slot: "", - }); - - const filteredPrinters = useMemo(() => { - return printerCombo.filter((printer) => { - return printer.type === "A4"; - }); - }, [printerCombo]); - - const [selectedPrinter, setSelectedPrinter] = useState( - filteredPrinters.length > 0 ? filteredPrinters[0] : undefined - ); - - useEffect(() => { - if (!selectedPrinter || !filteredPrinters.find(p => p.id === selectedPrinter.id)) { - setSelectedPrinter(filteredPrinters.length > 0 ? filteredPrinters[0] : undefined); - } - }, [filteredPrinters, selectedPrinter]); - - const handleReset = useCallback(() => { - setSearchInputs({ - store_id: "", - warehouse: "", - area: "", - slot: "", - }); - setFilteredWarehouses(warehouses); - setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); - }, [warehouses, pagingController.pageSize]); - - const handleSearch = useCallback(() => { - setIsSearching(true); - try { - let results: WarehouseResult[] = warehouses; - - const storeId = searchInputs.store_id?.trim() || ""; - const warehouse = searchInputs.warehouse?.trim() || ""; - const area = searchInputs.area?.trim() || ""; - const slot = searchInputs.slot?.trim() || ""; - - if (storeId || warehouse || area || slot) { - results = warehouses.filter((warehouseItem) => { - if (storeId || warehouse || area || slot) { - if (!warehouseItem.code) { - return false; - } - - const codeValue = String(warehouseItem.code).toLowerCase(); - - const codeParts = codeValue.split("-"); - - if (codeParts.length >= 4) { - const codeStoreId = codeParts[0] || ""; - const codeWarehouse = codeParts[1] || ""; - const codeArea = codeParts[2] || ""; - const codeSlot = codeParts[3] || ""; - - const storeIdMatch = !storeId || codeStoreId.includes(storeId.toLowerCase()); - const warehouseMatch = !warehouse || codeWarehouse.includes(warehouse.toLowerCase()); - const areaMatch = !area || codeArea.includes(area.toLowerCase()); - const slotMatch = !slot || codeSlot.includes(slot.toLowerCase()); - - return storeIdMatch && warehouseMatch && areaMatch && slotMatch; - } - - const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase()); - const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase()); - const areaMatch = !area || codeValue.includes(area.toLowerCase()); - const slotMatch = !slot || codeValue.includes(slot.toLowerCase()); - - return storeIdMatch && warehouseMatch && areaMatch && slotMatch; - } - - return true; - }); - } else { - results = warehouses; - } - - setFilteredWarehouses(results); - setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); - } catch (error) { - console.error("Error searching warehouses:", error); - const storeId = searchInputs.store_id?.trim().toLowerCase() || ""; - const warehouse = searchInputs.warehouse?.trim().toLowerCase() || ""; - const area = searchInputs.area?.trim().toLowerCase() || ""; - const slot = searchInputs.slot?.trim().toLowerCase() || ""; - - setFilteredWarehouses( - warehouses.filter((warehouseItem) => { - if (storeId || warehouse || area || slot) { - if (!warehouseItem.code) { - return false; - } - - const codeValue = String(warehouseItem.code).toLowerCase(); - const codeParts = codeValue.split("-"); - - if (codeParts.length >= 4) { - const storeIdMatch = !storeId || codeParts[0].includes(storeId); - const warehouseMatch = !warehouse || codeParts[1].includes(warehouse); - const areaMatch = !area || codeParts[2].includes(area); - const slotMatch = !slot || codeParts[3].includes(slot); - return storeIdMatch && warehouseMatch && areaMatch && slotMatch; - } - - return (!storeId || codeValue.includes(storeId)) && - (!warehouse || codeValue.includes(warehouse)) && - (!area || codeValue.includes(area)) && - (!slot || codeValue.includes(slot)); - } - - return true; - }) - ); - } finally { - setIsSearching(false); - } - }, [searchInputs, warehouses, pagingController.pageSize]); - - const handleSelectWarehouse = useCallback((warehouseId: number, checked: boolean) => { - if (checked) { - setCheckboxIds(prev => [...prev, warehouseId]); - } else { - setCheckboxIds(prev => prev.filter(id => id !== warehouseId)); - setSelectAll(false); - } - }, []); - - const handleSelectAll = useCallback((checked: boolean) => { - if (checked) { - setCheckboxIds(filteredWarehouses.map(warehouse => warehouse.id)); - setSelectAll(true); - } else { - setCheckboxIds([]); - setSelectAll(false); - } - }, [filteredWarehouses]); - - const showPdfPreview = useCallback(async (warehouseIds: number[]) => { - if (warehouseIds.length === 0) { - return; - } - try { - setIsUploading(true); - const response = await exportWarehouseQrCode(warehouseIds); - - const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" }); - const url = URL.createObjectURL(blob); - - setPreviewUrl(`${url}#toolbar=0`); - setPreviewOpen(true); - } catch (error) { - console.error("Error exporting QR code:", error); - } finally { - setIsUploading(false); - } - }, [setIsUploading]); - - const handleClosePreview = useCallback(() => { - setPreviewOpen(false); - if (previewUrl) { - URL.revokeObjectURL(previewUrl); - setPreviewUrl(null); - } - }, [previewUrl]); - - const handleDownloadQrCode = useCallback(async (warehouseIds: number[]) => { - if (warehouseIds.length === 0) { - return; - } - try { - setIsUploading(true); - const response = await exportWarehouseQrCode(warehouseIds); - downloadFile(response.blobValue, response.filename); - setSelectedWarehousesModalOpen(false); - successDialog("二維碼已下載", t); - } catch (error) { - console.error("Error exporting QR code:", error); - } finally { - setIsUploading(false); - } - }, [setIsUploading, t]); - - const handlePrint = useCallback(async () => { - if (checkboxIds.length === 0) { - return; - } - try { - setIsUploading(true); - const response = await exportWarehouseQrCode(checkboxIds); - - const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" }); - const url = URL.createObjectURL(blob); - - const printWindow = window.open(url, '_blank'); - if (printWindow) { - printWindow.onload = () => { - for (let i = 0; i < printQty; i++) { - setTimeout(() => { - printWindow.print(); - }, i * 500); - } - }; - } - - setTimeout(() => { - URL.revokeObjectURL(url); - }, 1000); - setSelectedWarehousesModalOpen(false); - successDialog("二維碼已列印", t); - } catch (error) { - console.error("Error printing QR code:", error); - } finally { - setIsUploading(false); - } - }, [checkboxIds, printQty, setIsUploading, t]); - - const handleViewSelectedQrCodes = useCallback(() => { - if (checkboxIds.length === 0) { - return; - } - setSelectedWarehousesModalOpen(true); - }, [checkboxIds]); - - const selectedWarehouses = useMemo(() => { - return warehouses.filter(warehouse => checkboxIds.includes(warehouse.id)); - }, [warehouses, checkboxIds]); - - const handleCloseSelectedWarehousesModal = useCallback(() => { - setSelectedWarehousesModalOpen(false); - }, []); - - const columns = useMemo[]>( - () => [ - { - name: "id", - label: "", - sx: { width: "50px", minWidth: "50px" }, - renderCell: (params) => ( - handleSelectWarehouse(params.id, e.target.checked)} - onClick={(e) => e.stopPropagation()} - /> - ), - }, - { - name: "code", - label: t("code"), - align: "left", - headerAlign: "left", - sx: { width: "200px", minWidth: "200px" }, - }, - { - name: "store_id", - label: t("store_id"), - align: "left", - headerAlign: "left", - sx: { width: "150px", minWidth: "150px" }, - }, - { - name: "warehouse", - label: t("warehouse"), - align: "left", - headerAlign: "left", - sx: { width: "150px", minWidth: "150px" }, - }, - { - name: "area", - label: t("area"), - align: "left", - headerAlign: "left", - sx: { width: "150px", minWidth: "150px" }, - }, - { - name: "slot", - label: t("slot"), - align: "left", - headerAlign: "left", - sx: { width: "150px", minWidth: "150px" }, - }, - ], - [t, checkboxIds, handleSelectWarehouse], - ); - - return ( - <> - - - {t("Search Criteria")} - - - setSearchInputs((prev) => ({ ...prev, store_id: e.target.value })) - } - size="small" - sx={{ width: "150px", minWidth: "120px" }} - InputProps={{ - endAdornment: ( - F - ), - }} - /> - - - - - - setSearchInputs((prev) => ({ ...prev, warehouse: e.target.value })) - } - size="small" - sx={{ width: "150px", minWidth: "120px" }} - /> - - - - - - setSearchInputs((prev) => ({ ...prev, area: e.target.value })) - } - size="small" - sx={{ width: "150px", minWidth: "120px" }} - /> - - - - - - setSearchInputs((prev) => ({ ...prev, slot: e.target.value })) - } - size="small" - sx={{ width: "150px", minWidth: "120px" }} - /> - - - - - - - - - items={filteredWarehouses} - columns={columns} - pagingController={pagingController} - setPagingController={setPagingController} - totalCount={filteredWarehouses.length} - isAutoPaging={true} - /> - - - - - - - - - - 已選擇倉庫 ({selectedWarehouses.length}) - - - - - - - - - - - - - {t("code")} - - - {t("store_id")} - - - {t("warehouse")} - - - {t("area")} - - - {t("slot")} - - - - - {selectedWarehouses.length === 0 ? ( - - - 沒有選擇的倉庫 - - - ) : ( - selectedWarehouses.map((warehouse) => ( - - {warehouse.code || '-'} - {warehouse.store_id || '-'} - {warehouse.warehouse || '-'} - {warehouse.area || '-'} - {warehouse.slot || '-'} - - )) - )} - -
    -
    -
    - - - - - options={filteredPrinters} - value={selectedPrinter ?? null} - onChange={(event, value) => { - setSelectedPrinter(value ?? undefined); - }} - getOptionLabel={(option) => option.name || option.label || option.code || String(option.id)} - isOptionEqualToValue={(option, value) => option.id === value.id} - renderInput={(params) => ( - - )} - /> - { - const value = parseInt(e.target.value) || 1; - setPrintQty(Math.max(1, value)); - }} - inputProps={{ min: 1 }} - sx={{ width: 120 }} - /> - - - - -
    -
    - - - - - - - - - - - {previewUrl && ( -