| @@ -1,5 +1,5 @@ | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import DoDetail from "@/components/DoDetail/DodetailWrapper"; | |||
| import DoDetail from "@/components/DoDetail/DoDetailWrapper"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Typography } from "@mui/material"; | |||
| import { isArray } from "lodash"; | |||
| @@ -12,8 +12,8 @@ import { | |||
| OnlinePrediction, FileDownload, SettingsEthernet | |||
| } from "@mui/icons-material"; | |||
| import dayjs from "dayjs"; | |||
| import { redirect } from "next/navigation"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||
| export default function ProductionSchedulePage() { | |||
| // ── Main states ── | |||
| @@ -69,21 +69,14 @@ export default function ProductionSchedulePage() { | |||
| // ── API Actions ── | |||
| const handleSearch = async () => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| setLoading(true); | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { | |||
| method: 'GET', | |||
| headers: { 'Authorization': `Bearer ${token}` } | |||
| }); | |||
| if (response.status === 401 || response.status === 403) { | |||
| console.warn(`Auth error ${response.status} → clearing token & redirecting`); | |||
| window.location.href = "/login?session=expired"; | |||
| return; // ← stops execution here | |||
| } | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const data = await response.json(); | |||
| @@ -101,7 +94,6 @@ export default function ProductionSchedulePage() { | |||
| return; | |||
| } | |||
| const token = localStorage.getItem("accessToken"); | |||
| setLoading(true); | |||
| setIsForecastDialogOpen(false); | |||
| @@ -113,10 +105,9 @@ export default function ProductionSchedulePage() { | |||
| const url = `${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule?${params.toString()}`; | |||
| const response = await fetch(url, { | |||
| method: 'GET', | |||
| headers: { 'Authorization': `Bearer ${token}` } | |||
| }); | |||
| const response = await clientAuthFetch(url, { method: 'GET' }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (response.ok) { | |||
| await handleSearch(); // refresh list | |||
| @@ -140,7 +131,6 @@ export default function ProductionSchedulePage() { | |||
| return; | |||
| } | |||
| const token = localStorage.getItem("accessToken"); | |||
| setLoading(true); | |||
| setIsExportDialogOpen(false); | |||
| @@ -149,11 +139,11 @@ export default function ProductionSchedulePage() { | |||
| fromDate: exportFromDate, | |||
| }); | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { | |||
| method: 'GET', // or keep POST if backend requires it | |||
| headers: { 'Authorization': `Bearer ${token}` } | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { | |||
| method: 'GET', | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (!response.ok) throw new Error(`Export failed: ${response.status}`); | |||
| const blob = await response.blob(); | |||
| @@ -183,25 +173,15 @@ export default function ProductionSchedulePage() { | |||
| return; | |||
| } | |||
| const token = localStorage.getItem("accessToken"); | |||
| console.log("Token exists:", !!token); | |||
| setSelectedPs(ps); | |||
| setLoading(true); | |||
| try { | |||
| const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`; | |||
| console.log("Sending request to:", url); | |||
| const response = await fetch(url, { | |||
| method: 'GET', | |||
| headers: { | |||
| 'Authorization': `Bearer ${token}`, | |||
| }, | |||
| }); | |||
| const response = await clientAuthFetch(url, { method: 'GET' }); | |||
| console.log("Response status:", response.status); | |||
| console.log("Response ok?", response.ok); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (!response.ok) { | |||
| const errorText = await response.text().catch(() => "(no text)"); | |||
| @@ -229,19 +209,17 @@ export default function ProductionSchedulePage() { | |||
| const handleAutoGenJob = async () => { | |||
| //if (!isDateToday) return; | |||
| const token = localStorage.getItem("accessToken"); | |||
| //if (!isDateToday) return; | |||
| setIsGenerating(true); | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { | |||
| method: 'POST', | |||
| headers: { | |||
| 'Authorization': `Bearer ${token}`, | |||
| 'Content-Type': 'application/json' | |||
| }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| body: JSON.stringify({ id: selectedPs.id }) | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (response.ok) { | |||
| const data = await response.json(); | |||
| const displayMessage = data.message || "Operation completed."; | |||
| @@ -17,6 +17,7 @@ import { | |||
| import PrintIcon from '@mui/icons-material/Print'; | |||
| import { REPORTS, ReportDefinition } from '@/config/reportConfig'; | |||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | |||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | |||
| import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | |||
| import { | |||
| fetchSemiFGItemCodes, | |||
| @@ -90,25 +91,17 @@ export default function ReportPage() { | |||
| } | |||
| // Handle other reports with dynamic options | |||
| const token = localStorage.getItem("accessToken"); | |||
| // Handle multiple stockCategory values (comma-separated) | |||
| // If "All" is included or no value, fetch all | |||
| // Otherwise, fetch for all selected categories | |||
| let url = field.dynamicOptionsEndpoint; | |||
| if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) { | |||
| // Multiple categories selected (e.g., "FG,WIP") | |||
| url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`; | |||
| } | |||
| const response = await fetch(url, { | |||
| const response = await clientAuthFetch(url, { | |||
| method: 'GET', | |||
| headers: { | |||
| 'Authorization': `Bearer ${token}`, | |||
| 'Content-Type': 'application/json', | |||
| }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |||
| const data = await response.json(); | |||
| @@ -159,21 +152,18 @@ export default function ReportPage() { | |||
| const executePrint = async () => { | |||
| if (!currentReport) 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, { | |||
| const response = await clientAuthFetch(url, { | |||
| method: 'GET', | |||
| headers: { | |||
| 'Authorization': `Bearer ${token}`, | |||
| 'Accept': 'application/pdf', | |||
| }, | |||
| headers: { 'Accept': 'application/pdf' }, | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (!response.ok) { | |||
| const errorText = await response.text(); | |||
| console.error("Response error:", errorText); | |||
| @@ -1,4 +1,7 @@ | |||
| "use client"; | |||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | |||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | |||
| export interface ItemCodeWithName { | |||
| code: string; | |||
| @@ -19,24 +22,18 @@ export interface ItemCodeWithCategory { | |||
| export const fetchSemiFGItemCodes = async ( | |||
| stockCategory: string = '' | |||
| ): Promise<ItemCodeWithName[]> => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`; | |||
| if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { | |||
| url = `${url}?stockCategory=${stockCategory}`; | |||
| } | |||
| const response = await fetch(url, { | |||
| const response = await clientAuthFetch(url, { | |||
| method: 'GET', | |||
| headers: { | |||
| 'Authorization': `Bearer ${token}`, | |||
| 'Content-Type': 'application/json', | |||
| }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| }); | |||
| if (!response.ok) { | |||
| throw new Error(`HTTP error! status: ${response.status}`); | |||
| } | |||
| if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); | |||
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |||
| return await response.json(); | |||
| }; | |||
| @@ -49,24 +46,18 @@ export const fetchSemiFGItemCodes = async ( | |||
| export const fetchSemiFGItemCodesWithCategory = async ( | |||
| stockCategory: string = '' | |||
| ): Promise<ItemCodeWithCategory[]> => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes-with-category`; | |||
| if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { | |||
| url = `${url}?stockCategory=${stockCategory}`; | |||
| } | |||
| const response = await fetch(url, { | |||
| const response = await clientAuthFetch(url, { | |||
| method: 'GET', | |||
| headers: { | |||
| 'Authorization': `Bearer ${token}`, | |||
| 'Content-Type': 'application/json', | |||
| }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| }); | |||
| if (!response.ok) { | |||
| throw new Error(`HTTP error! status: ${response.status}`); | |||
| } | |||
| if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); | |||
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |||
| return await response.json(); | |||
| }; | |||
| @@ -81,21 +72,16 @@ export const generateSemiFGProductionAnalysisReport = async ( | |||
| criteria: Record<string, string>, | |||
| reportTitle: string = '成品/半成品生產分析報告' | |||
| ): Promise<void> => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| const queryParams = new URLSearchParams(criteria).toString(); | |||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis?${queryParams}`; | |||
| const response = await fetch(url, { | |||
| const response = await clientAuthFetch(url, { | |||
| method: 'GET', | |||
| headers: { | |||
| 'Authorization': `Bearer ${token}`, | |||
| 'Accept': 'application/pdf', | |||
| }, | |||
| headers: { 'Accept': 'application/pdf' }, | |||
| }); | |||
| if (!response.ok) { | |||
| throw new Error(`HTTP error! status: ${response.status}`); | |||
| } | |||
| if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); | |||
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |||
| const blob = await response.blob(); | |||
| const downloadUrl = window.URL.createObjectURL(blob); | |||
| @@ -10,6 +10,7 @@ import { | |||
| import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material"; | |||
| import dayjs from "dayjs"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||
| // Simple TabPanel component for conditional rendering | |||
| interface TabPanelProps { | |||
| @@ -97,14 +98,14 @@ export default function TestingPage() { | |||
| // TSC Print (Section 1) | |||
| const handleTscPrint = async (row: any) => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| const payload = { ...row, printerIp: tscConfig.ip, printerPort: tscConfig.port }; | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, { | |||
| method: 'POST', | |||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| body: JSON.stringify(payload) | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (response.ok) alert(`TSC Print Command Sent for ${row.itemCode}!`); | |||
| else alert("TSC Print Failed"); | |||
| } catch (e) { console.error("TSC Error:", e); } | |||
| @@ -112,14 +113,14 @@ export default function TestingPage() { | |||
| // DataFlex Print (Section 2) | |||
| const handleDfPrint = async (row: any) => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| const payload = { ...row, printerIp: dfConfig.ip, printerPort: dfConfig.port }; | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { | |||
| method: 'POST', | |||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| body: JSON.stringify(payload) | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (response.ok) alert(`DataFlex Print Command Sent for ${row.itemCode}!`); | |||
| else alert("DataFlex Print Failed"); | |||
| } catch (e) { console.error("DataFlex Error:", e); } | |||
| @@ -127,14 +128,13 @@ export default function TestingPage() { | |||
| // OnPack Zip Download (Section 3) | |||
| const handleDownloadPrintJob = async () => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| const params = new URLSearchParams(printerFormData); | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, { | |||
| method: 'GET', | |||
| headers: { 'Authorization': `Bearer ${token}` } | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (!response.ok) throw new Error('Download failed'); | |||
| const blob = await response.blob(); | |||
| @@ -153,34 +153,33 @@ export default function TestingPage() { | |||
| // Laser Print (Section 4 - original) | |||
| const handleLaserPrint = async (row: any) => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, { | |||
| method: 'POST', | |||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| body: JSON.stringify(payload) | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (response.ok) alert(`Laser Command Sent: ${row.templateId}`); | |||
| } catch (e) { console.error(e); } | |||
| }; | |||
| const handleLaserPreview = async (row: any) => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { | |||
| method: 'POST', | |||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| body: JSON.stringify(payload) | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| if (response.ok) alert("Red light preview active!"); | |||
| } catch (e) { console.error("Preview Error:", e); } | |||
| }; | |||
| // HANS600S-M TCP Print (Section 5) | |||
| const handleHansPrint = async (row: any) => { | |||
| const token = localStorage.getItem("accessToken"); | |||
| const payload = { | |||
| printerIp: hansConfig.ip, | |||
| printerPort: hansConfig.port, | |||
| @@ -190,11 +189,12 @@ export default function TestingPage() { | |||
| text4ObjectName: row.text4ObjectName | |||
| }; | |||
| try { | |||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { | |||
| const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { | |||
| method: 'POST', | |||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| body: JSON.stringify(payload) | |||
| }); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const result = await response.text(); | |||
| if (response.ok) { | |||
| alert(`HANS600S-M Mark Success: ${result}`); | |||
| @@ -29,9 +29,9 @@ export interface QcData { | |||
| name?: string, | |||
| order?: number, | |||
| description?: string, | |||
| // qcPassed: boolean | undefined | |||
| // failQty: number | undefined | |||
| // remarks: string | undefined | |||
| qcPassed?: boolean, | |||
| failQty?: number, | |||
| remarks?: string, | |||
| } | |||
| export interface QcResult extends QcData{ | |||
| id?: number; | |||
| @@ -2,7 +2,7 @@ | |||
| // import { serverFetchWithNoContent } from '@/app/utils/fetchUtil'; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| import { serverFetchWithNoContent } from "../../../utils/fetchUtil"; | |||
| import { serverFetch, serverFetchWithNoContent } from "../../../utils/fetchUtil"; | |||
| import { BASE_API_URL } from "../../../../config/api"; | |||
| export interface M18ImportPoForm { | |||
| @@ -85,13 +85,13 @@ export const triggerScheduler = async (type: 'po' | 'do1' | 'do2' | 'master-data | |||
| console.log("Fetching URL:", url); | |||
| const response = await serverFetchWithNoContent(url, { | |||
| const response = await serverFetch(url, { | |||
| method: "GET", | |||
| cache: "no-store", | |||
| }); | |||
| if (!response.ok) throw new Error(`Failed: ${response.status}`); | |||
| return await response.text(); | |||
| } catch (error) { | |||
| console.error("Scheduler Action Error:", error); | |||
| @@ -103,13 +103,13 @@ export const refreshCronSchedules = async () => { | |||
| // Simply reuse the triggerScheduler logic to avoid duplication | |||
| // or call serverFetch directly as shown below: | |||
| try { | |||
| const response = await serverFetchWithNoContent(`${BASE_API_URL}/scheduler/refresh-cron`, { | |||
| const response = await serverFetch(`${BASE_API_URL}/scheduler/refresh-cron`, { | |||
| method: "GET", | |||
| cache: "no-store", | |||
| }); | |||
| if (!response.ok) throw new Error(`Failed to refresh: ${response.status}`); | |||
| return await response.text(); | |||
| } catch (error) { | |||
| console.error("Refresh Cron Error:", error); | |||
| @@ -13,10 +13,11 @@ export interface UserInputs { | |||
| username: string; | |||
| name: string; | |||
| staffNo?: string; | |||
| locked?: boolean; | |||
| addAuthIds?: number[]; | |||
| removeAuthIds?: number[]; | |||
| password?: string; | |||
| confirmPassword?: string; | |||
| confirmPassword?: string; | |||
| } | |||
| export interface PasswordInputs { | |||
| @@ -0,0 +1,31 @@ | |||
| "use client"; | |||
| const LOGIN_REDIRECT = "/login?session=expired"; | |||
| /** | |||
| * Client-side fetch that adds Bearer token from localStorage and redirects | |||
| * to /login?session=expired on 401 or 403 (session timeout / unauthorized). | |||
| * Use this for all authenticated API requests so session expiry is handled consistently. | |||
| */ | |||
| export async function clientAuthFetch( | |||
| input: RequestInfo | URL, | |||
| init?: RequestInit | |||
| ): Promise<Response> { | |||
| const token = | |||
| typeof window !== "undefined" ? localStorage.getItem("accessToken") : null; | |||
| const headers = new Headers(init?.headers); | |||
| if (token) { | |||
| headers.set("Authorization", `Bearer ${token}`); | |||
| } | |||
| const response = await fetch(input, { ...init, headers }); | |||
| if (response.status === 401 || response.status === 403) { | |||
| if (typeof window !== "undefined") { | |||
| console.warn(`Auth error ${response.status} → redirecting to login`); | |||
| window.location.href = LOGIN_REDIRECT; | |||
| } | |||
| } | |||
| return response; | |||
| } | |||
| @@ -143,9 +143,10 @@ const CreateUser: React.FC<Props> = ({ rules, auths }) => { | |||
| }); | |||
| } | |||
| } | |||
| const userData = { | |||
| const userData: UserInputs = { | |||
| username: data.username, | |||
| // name: data.name, | |||
| name: data.name ?? "", | |||
| staffNo: data.staffNo, | |||
| locked: false, | |||
| addAuthIds: data.addAuthIds || [], | |||
| removeAuthIds: data.removeAuthIds || [], | |||
| @@ -26,7 +26,7 @@ import isToday from 'dayjs/plugin/isToday'; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import { FileDownload, CalendarMonth } from "@mui/icons-material"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { VIEW_USER } from "@/authorities"; | |||
| import { AUTH } from "@/authorities"; | |||
| dayjs.extend(isToday); | |||
| @@ -384,7 +384,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||
| {t("Export Schedule")} | |||
| </Button> | |||
| {false && abilities.includes(VIEW_USER) && ( | |||
| {false && abilities.includes(AUTH.VIEW_USER) && ( | |||
| <Button | |||
| variant="contained" // Solid button for the "Export" action | |||
| color="success" // Green color often signifies a successful action/download | |||
| @@ -9,7 +9,7 @@ interface SubComponents { | |||
| const DoSearchWrapper: React.FC & SubComponents = async () => { | |||
| // const [dos] = await Promise.all([fetchDoList()]); | |||
| return <DoSearch />; | |||
| return <DoSearch onDeliveryOrderSearch={() => {}} />; | |||
| }; | |||
| DoSearchWrapper.Loading = GeneralLoading; | |||
| @@ -61,7 +61,7 @@ const EscalationComponent: React.FC<Props> = ({ | |||
| ]; | |||
| const handleInputChange = ( | |||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SelectChangeEvent<string> | |||
| ): void => { | |||
| const { name, value } = event.target; | |||
| setFormData((prev) => ({ | |||
| @@ -22,7 +22,7 @@ import { | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; | |||
| import { dummyQCData, QcData } from "./dummyQcTemplate"; | |||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | |||
| const style = { | |||
| @@ -149,17 +149,17 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { | |||
| submitDialogWithWarning(() => { | |||
| console.log("QC accepted with failed items"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); | |||
| return; | |||
| } | |||
| if (qcData.qcAccept) { | |||
| console.log("QC accepted"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| } else { | |||
| console.log("QC rejected"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| } | |||
| }, | |||
| [qcItems, onClose, t], | |||
| @@ -260,7 +260,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| color="warning" | |||
| onClick={() => { | |||
| console.log("Sort to accept"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| }} | |||
| > | |||
| Sort to Accept | |||
| @@ -270,7 +270,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| color="error" | |||
| onClick={() => { | |||
| console.log("Reject and pick another lot"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| }} | |||
| > | |||
| Reject and Pick Another Lot | |||
| @@ -24,7 +24,7 @@ import { | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { dummyQCData } from "../PoDetail/dummyQcTemplate"; | |||
| import { dummyQCData } from "./dummyQcTemplate"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { GridColDef } from "@mui/x-data-grid"; | |||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | |||
| @@ -111,7 +111,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
| if (isPickOrderData) { | |||
| // PickOrder 数据 | |||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | |||
| return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; | |||
| return pickOrderItem.uomDesc || (pickOrderItem as { uomCode?: string }).uomCode || ''; | |||
| } else { | |||
| // StockIn 数据 | |||
| const stockInItem = itemDetail as StockInLine; | |||
| @@ -169,7 +169,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
| <TextField | |||
| label={t("itemNo")} | |||
| fullWidth | |||
| {...register("itemNo", { | |||
| {...(register as (name: string, opts?: object) => ReturnType<typeof register>)("itemNo", { | |||
| required: "itemNo required!", | |||
| })} | |||
| value={getItemDisplayValue()} | |||
| @@ -1346,709 +1346,6 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||
| setSearchResultsPagingController(newPagingController); | |||
| }, []); | |||
| // Add pagination state for created items | |||
| const [createdItemsPagingController, setCreatedItemsPagingController] = useState({ | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }); | |||
| // Add pagination handlers for created items | |||
| const handleCreatedItemsPageChange = useCallback((event: unknown, newPage: number) => { | |||
| const newPagingController = { | |||
| ...createdItemsPagingController, | |||
| pageNum: newPage + 1, | |||
| }; | |||
| setCreatedItemsPagingController(newPagingController); | |||
| }, [createdItemsPagingController]); | |||
| const handleCreatedItemsPageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||
| const newPageSize = parseInt(event.target.value, 10); | |||
| const newPagingController = { | |||
| pageNum: 1, | |||
| pageSize: newPageSize, | |||
| }; | |||
| setCreatedItemsPagingController(newPagingController); | |||
| }, []); | |||
| // Create a custom table for created items with pagination | |||
| const CustomCreatedItemsTable = () => { | |||
| const startIndex = (createdItemsPagingController.pageNum - 1) * createdItemsPagingController.pageSize; | |||
| const endIndex = startIndex + createdItemsPagingController.pageSize; | |||
| const paginatedCreatedItems = createdItems.slice(startIndex, endIndex); | |||
| return ( | |||
| <> | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}> | |||
| {t("Selected")} | |||
| </TableCell> | |||
| <TableCell> | |||
| {t("Item")} | |||
| </TableCell> | |||
| <TableCell> | |||
| {t("Group")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Current Stock")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Stock Unit")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Order Quantity")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Target Date")} | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {paginatedCreatedItems.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={12} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No created items")} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| paginatedCreatedItems.map((item) => ( | |||
| <TableRow key={item.itemId}> | |||
| <TableCell padding="checkbox"> | |||
| <Checkbox | |||
| checked={item.isSelected} | |||
| onChange={(e) => handleCreatedItemSelect(item.itemId, e.target.checked)} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Typography variant="body2">{item.itemName}</Typography> | |||
| <Typography variant="caption" color="textSecondary"> | |||
| {item.itemCode} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <FormControl size="small" sx={{ minWidth: 120 }}> | |||
| <Select | |||
| value={item.groupId?.toString() || ""} | |||
| onChange={(e) => handleCreatedItemGroupChange(item.itemId, e.target.value)} | |||
| displayEmpty | |||
| > | |||
| <MenuItem value=""> | |||
| <em>{t("No Group")}</em> | |||
| </MenuItem> | |||
| {groups.map((group) => ( | |||
| <MenuItem key={group.id} value={group.id.toString()}> | |||
| {group.name} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Typography | |||
| variant="body2" | |||
| color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"} | |||
| > | |||
| {item.currentStockBalance?.toLocaleString() || 0} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Typography variant="body2">{item.uomDesc}</Typography> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={item.qty || ""} | |||
| onChange={(e) => { | |||
| const newQty = Number(e.target.value); | |||
| handleQtyChange(item.itemId, newQty); | |||
| }} | |||
| inputProps={{ | |||
| min: 1, | |||
| step: 1, | |||
| style: { textAlign: 'center' } | |||
| }} | |||
| sx={{ | |||
| width: '80px', | |||
| '& .MuiInputBase-input': { | |||
| textAlign: 'center', | |||
| cursor: 'text' | |||
| } | |||
| }} | |||
| /> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Typography variant="body2"> | |||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| {/* Pagination for created items */} | |||
| <TablePagination | |||
| component="div" | |||
| count={createdItems.length} | |||
| page={(createdItemsPagingController.pageNum - 1)} | |||
| rowsPerPage={createdItemsPagingController.pageSize} | |||
| onPageChange={handleCreatedItemsPageChange} | |||
| onRowsPerPageChange={handleCreatedItemsPageSizeChange} | |||
| rowsPerPageOptions={[10, 25, 50]} | |||
| labelRowsPerPage={t("Rows per page")} | |||
| labelDisplayedRows={({ from, to, count }) => | |||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||
| } | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| // Define columns for SearchResults | |||
| const searchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [ | |||
| { | |||
| name: "id", | |||
| label: "", | |||
| type: "checkbox", | |||
| disabled: (item) => isItemInCreated(item.id), // Disable if already in created items | |||
| }, | |||
| { | |||
| name: "label", | |||
| label: t("Item"), | |||
| renderCell: (item) => { | |||
| const parts = item.label.split(' - '); | |||
| const code = parts[0] || ''; | |||
| const name = parts[1] || ''; | |||
| return ( | |||
| <Box> | |||
| <Typography variant="body2"> | |||
| {name} {/* 显示项目名称 */} | |||
| </Typography> | |||
| <Typography variant="caption" color="textSecondary"> | |||
| {code} {/* 显示项目代码 */} | |||
| </Typography> | |||
| </Box> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| name: "qty", | |||
| label: t("Order Quantity"), | |||
| renderCell: (item) => ( | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={item.qty || ""} // Show empty string if qty is null | |||
| onChange={(e) => { | |||
| const value = e.target.value; | |||
| const numValue = value === "" ? null : Number(value); | |||
| handleSearchQtyChange(item.id, numValue); | |||
| }} | |||
| inputProps={{ | |||
| min: 1, | |||
| step: 1, | |||
| style: { textAlign: 'center' } // Center the text | |||
| }} | |||
| sx={{ | |||
| width: '80px', | |||
| '& .MuiInputBase-input': { | |||
| textAlign: 'center', | |||
| cursor: 'text' | |||
| } | |||
| }} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| name: "currentStockBalance", | |||
| label: t("Current Stock"), | |||
| renderCell: (item) => { | |||
| const stockBalance = item.currentStockBalance || 0; | |||
| return ( | |||
| <Typography | |||
| variant="body2" | |||
| color={stockBalance > 0 ? "success.main" : "error.main"} | |||
| sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal' }} | |||
| > | |||
| {stockBalance} | |||
| </Typography> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| name: "targetDate", | |||
| label: t("Target Date"), | |||
| renderCell: (item) => ( | |||
| <Typography variant="body2"> | |||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||
| </Typography> | |||
| ), | |||
| }, | |||
| { | |||
| name: "uom", | |||
| label: t("Stock Unit"), | |||
| renderCell: (item) => item.uom || "-", | |||
| }, | |||
| ], [t, isItemInCreated, handleSearchQtyChange]); | |||
| // 修改搜索条件为3行,每行一个 - 确保SearchBox组件能正确处理 | |||
| const pickOrderSearchCriteria: Criterion<any>[] = useMemo( | |||
| () => [ | |||
| { | |||
| label: t("Item Code"), | |||
| paramName: "code", | |||
| type: "text" | |||
| }, | |||
| { | |||
| label: t("Item Name"), | |||
| paramName: "name", | |||
| type: "text" | |||
| }, | |||
| { | |||
| label: t("Product Type"), | |||
| paramName: "type", | |||
| type: "autocomplete", | |||
| options: [ | |||
| { value: "Consumable", label: t("Consumable") }, | |||
| { value: "MATERIAL", label: t("Material") }, | |||
| { value: "End_product", label: t("End Product") } | |||
| ], | |||
| }, | |||
| ], | |||
| [t], | |||
| ); | |||
| // 添加重置函数 | |||
| const handleSecondReset = useCallback(() => { | |||
| console.log("Second search reset"); | |||
| setSecondSearchQuery({}); | |||
| setSecondSearchResults([]); | |||
| setHasSearchedSecond(false); | |||
| // 清空表单中的类型,但保留今天的日期 | |||
| formProps.setValue("type", ""); | |||
| const today = dayjs().format(INPUT_DATE_FORMAT); | |||
| formProps.setValue("targetDate", today); | |||
| }, [formProps]); | |||
| // 添加数量变更处理函数 | |||
| const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { | |||
| setSecondSearchResults(prev => | |||
| prev.map(item => | |||
| item.id === itemId ? { ...item, qty: newQty } : item | |||
| ) | |||
| ); | |||
| // Auto-update created items if this item exists there | |||
| setCreatedItems(prev => | |||
| prev.map(item => | |||
| item.itemId === itemId ? { ...item, qty: newQty || 1 } : item | |||
| ) | |||
| ); | |||
| }, []); | |||
| // Add checkbox change handler for second search | |||
| const handleSecondSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => { | |||
| if (typeof ids === 'function') { | |||
| const newIds = ids(selectedSecondSearchItemIds); | |||
| setSelectedSecondSearchItemIds(newIds); | |||
| // 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面 | |||
| if (newIds.length === secondSearchResults.length) { | |||
| // 全选:将所有搜索结果添加到创建项目 | |||
| secondSearchResults.forEach(item => { | |||
| if (!isItemInCreated(item.id)) { | |||
| handleSecondSearchItemSelect(item.id, true); | |||
| } | |||
| }); | |||
| } else { | |||
| // 部分选择:只处理当前页面的选择 | |||
| secondSearchResults.forEach(item => { | |||
| const isSelected = newIds.includes(item.id); | |||
| const isCurrentlyInCreated = isItemInCreated(item.id); | |||
| if (isSelected && !isCurrentlyInCreated) { | |||
| handleSecondSearchItemSelect(item.id, true); | |||
| } else if (!isSelected && isCurrentlyInCreated) { | |||
| setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); | |||
| } | |||
| }); | |||
| } | |||
| } else { | |||
| const previousIds = selectedSecondSearchItemIds; | |||
| setSelectedSecondSearchItemIds(ids); | |||
| const newlySelected = ids.filter(id => !previousIds.includes(id)); | |||
| const newlyDeselected = previousIds.filter(id => !ids.includes(id)); | |||
| newlySelected.forEach(id => { | |||
| if (!isItemInCreated(id as number)) { | |||
| handleSecondSearchItemSelect(id as number, true); | |||
| } | |||
| }); | |||
| newlyDeselected.forEach(id => { | |||
| setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); | |||
| }); | |||
| } | |||
| }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]); | |||
| // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity | |||
| const secondSearchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [ | |||
| { | |||
| name: "id", | |||
| label: "", | |||
| type: "checkbox", | |||
| disabled: (item) => isItemInCreated(item.id), | |||
| }, | |||
| { | |||
| name: "label", | |||
| label: t("Item"), | |||
| renderCell: (item) => { | |||
| const parts = item.label.split(' - '); | |||
| const code = parts[0] || ''; | |||
| const name = parts[1] || ''; | |||
| return ( | |||
| <Box> | |||
| <Typography variant="body2"> | |||
| {name} | |||
| </Typography> | |||
| <Typography variant="caption" color="textSecondary"> | |||
| {code} | |||
| </Typography> | |||
| </Box> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| name: "currentStockBalance", | |||
| label: t("Current Stock"), | |||
| align: "right", // Add right alignment for the label | |||
| renderCell: (item) => { | |||
| const stockBalance = item.currentStockBalance || 0; | |||
| return ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}> | |||
| <Typography | |||
| variant="body2" | |||
| color={stockBalance > 0 ? "success.main" : "error.main"} | |||
| sx={{ | |||
| fontWeight: stockBalance > 0 ? 'bold' : 'normal', | |||
| textAlign: 'right' // Add right alignment for the value | |||
| }} | |||
| > | |||
| {stockBalance} | |||
| </Typography> | |||
| </Box> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| name: "uom", | |||
| label: t("Stock Unit"), | |||
| align: "right", // Add right alignment for the label | |||
| renderCell: (item) => ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}> | |||
| <Typography sx={{ textAlign: 'right' }}> {/* Add right alignment for the value */} | |||
| {item.uom || "-"} | |||
| </Typography> | |||
| </Box> | |||
| ), | |||
| }, | |||
| { | |||
| name: "qty", | |||
| label: t("Order Quantity"), | |||
| align: "right", | |||
| renderCell: (item) => ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}> | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={item.qty || ""} | |||
| onChange={(e) => { | |||
| const value = e.target.value; | |||
| // Only allow numbers | |||
| if (value === "" || /^\d+$/.test(value)) { | |||
| const numValue = value === "" ? null : Number(value); | |||
| handleSecondSearchQtyChange(item.id, numValue); | |||
| } | |||
| }} | |||
| inputProps={{ | |||
| style: { textAlign: 'center' } | |||
| }} | |||
| sx={{ | |||
| width: '80px', | |||
| '& .MuiInputBase-input': { | |||
| textAlign: 'center', | |||
| cursor: 'text' | |||
| } | |||
| }} | |||
| onBlur={(e) => { | |||
| const value = e.target.value; | |||
| const numValue = value === "" ? null : Number(value); | |||
| if (numValue !== null && numValue < 1) { | |||
| handleSecondSearchQtyChange(item.id, 1); // Enforce min value | |||
| } | |||
| }} | |||
| /> | |||
| </Box> | |||
| ), | |||
| } | |||
| ], [t, isItemInCreated, handleSecondSearchQtyChange, groups]); | |||
| // 添加缺失的 handleSecondSearch 函数 | |||
| const handleSecondSearch = useCallback((query: Record<string, any>) => { | |||
| console.log("Second search triggered with query:", query); | |||
| setSecondSearchQuery({ ...query }); | |||
| setIsLoadingSecondSearch(true); | |||
| // Sync second search box info to form - ensure type value is correct | |||
| if (query.type) { | |||
| // Ensure type value matches backend enum format | |||
| let correctType = query.type; | |||
| if (query.type === "consumable") { | |||
| correctType = "Consumable"; | |||
| } else if (query.type === "material") { | |||
| correctType = "MATERIAL"; | |||
| } else if (query.type === "jo") { | |||
| correctType = "JOB_ORDER"; | |||
| } | |||
| formProps.setValue("type", correctType); | |||
| } | |||
| setTimeout(() => { | |||
| let filtered = items; | |||
| // Same filtering logic as first search | |||
| if (query.code && query.code.trim()) { | |||
| filtered = filtered.filter(item => | |||
| item.label.toLowerCase().includes(query.code.toLowerCase()) | |||
| ); | |||
| } | |||
| if (query.name && query.name.trim()) { | |||
| filtered = filtered.filter(item => | |||
| item.label.toLowerCase().includes(query.name.toLowerCase()) | |||
| ); | |||
| } | |||
| if (query.type && query.type !== "All") { | |||
| // Filter by type if needed | |||
| } | |||
| // Convert to SearchItemWithQty with NO group/targetDate initially | |||
| const filteredWithQty = filtered.slice(0, 100).map(item => ({ | |||
| ...item, | |||
| qty: null, | |||
| targetDate: undefined, // No target date initially | |||
| groupId: undefined, // No group initially | |||
| })); | |||
| setSecondSearchResults(filteredWithQty); | |||
| setHasSearchedSecond(true); | |||
| setIsLoadingSecondSearch(false); | |||
| }, 500); | |||
| }, [items, formProps]); | |||
| // Add pagination state for search results | |||
| const [searchResultsPagingController, setSearchResultsPagingController] = useState({ | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }); | |||
| // Add pagination handlers for search results | |||
| const handleSearchResultsPageChange = useCallback((event: unknown, newPage: number) => { | |||
| const newPagingController = { | |||
| ...searchResultsPagingController, | |||
| pageNum: newPage + 1, // API uses 1-based pagination | |||
| }; | |||
| setSearchResultsPagingController(newPagingController); | |||
| }, [searchResultsPagingController]); | |||
| const handleSearchResultsPageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||
| const newPageSize = parseInt(event.target.value, 10); | |||
| const newPagingController = { | |||
| pageNum: 1, // Reset to first page | |||
| pageSize: newPageSize, | |||
| }; | |||
| setSearchResultsPagingController(newPagingController); | |||
| }, []); | |||
| // Add pagination state for created items | |||
| const [createdItemsPagingController, setCreatedItemsPagingController] = useState({ | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }); | |||
| // Add pagination handlers for created items | |||
| const handleCreatedItemsPageChange = useCallback((event: unknown, newPage: number) => { | |||
| const newPagingController = { | |||
| ...createdItemsPagingController, | |||
| pageNum: newPage + 1, | |||
| }; | |||
| setCreatedItemsPagingController(newPagingController); | |||
| }, [createdItemsPagingController]); | |||
| const handleCreatedItemsPageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||
| const newPageSize = parseInt(event.target.value, 10); | |||
| const newPagingController = { | |||
| pageNum: 1, | |||
| pageSize: newPageSize, | |||
| }; | |||
| setCreatedItemsPagingController(newPagingController); | |||
| }, []); | |||
| // Create a custom table for created items with pagination | |||
| const CustomCreatedItemsTable = () => { | |||
| const startIndex = (createdItemsPagingController.pageNum - 1) * createdItemsPagingController.pageSize; | |||
| const endIndex = startIndex + createdItemsPagingController.pageSize; | |||
| const paginatedCreatedItems = createdItems.slice(startIndex, endIndex); | |||
| return ( | |||
| <> | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}> | |||
| {t("Selected")} | |||
| </TableCell> | |||
| <TableCell> | |||
| {t("Item")} | |||
| </TableCell> | |||
| <TableCell> | |||
| {t("Group")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Current Stock")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Stock Unit")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Order Quantity")} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| {t("Target Date")} | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {paginatedCreatedItems.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={12} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No created items")} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| paginatedCreatedItems.map((item) => ( | |||
| <TableRow key={item.itemId}> | |||
| <TableCell padding="checkbox"> | |||
| <Checkbox | |||
| checked={item.isSelected} | |||
| onChange={(e) => handleCreatedItemSelect(item.itemId, e.target.checked)} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Typography variant="body2">{item.itemName}</Typography> | |||
| <Typography variant="caption" color="textSecondary"> | |||
| {item.itemCode} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <FormControl size="small" sx={{ minWidth: 120 }}> | |||
| <Select | |||
| value={item.groupId?.toString() || ""} | |||
| onChange={(e) => handleCreatedItemGroupChange(item.itemId, e.target.value)} | |||
| displayEmpty | |||
| > | |||
| <MenuItem value=""> | |||
| <em>{t("No Group")}</em> | |||
| </MenuItem> | |||
| {groups.map((group) => ( | |||
| <MenuItem key={group.id} value={group.id.toString()}> | |||
| {group.name} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Typography | |||
| variant="body2" | |||
| color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"} | |||
| > | |||
| {item.currentStockBalance?.toLocaleString() || 0} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Typography variant="body2">{item.uomDesc}</Typography> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={item.qty || ""} | |||
| onChange={(e) => { | |||
| const newQty = Number(e.target.value); | |||
| handleQtyChange(item.itemId, newQty); | |||
| }} | |||
| inputProps={{ | |||
| min: 1, | |||
| step: 1, | |||
| style: { textAlign: 'center' } | |||
| }} | |||
| sx={{ | |||
| width: '80px', | |||
| '& .MuiInputBase-input': { | |||
| textAlign: 'center', | |||
| cursor: 'text' | |||
| } | |||
| }} | |||
| /> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Typography variant="body2"> | |||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| {/* Pagination for created items */} | |||
| <TablePagination | |||
| component="div" | |||
| count={createdItems.length} | |||
| page={(createdItemsPagingController.pageNum - 1)} | |||
| rowsPerPage={createdItemsPagingController.pageSize} | |||
| onPageChange={handleCreatedItemsPageChange} | |||
| onRowsPerPageChange={handleCreatedItemsPageSizeChange} | |||
| rowsPerPageOptions={[10, 25, 50]} | |||
| labelRowsPerPage={t("Rows per page")} | |||
| labelDisplayedRows={({ from, to, count }) => | |||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||
| } | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| // Add helper function to get group range text | |||
| const getGroupRangeText = useCallback(() => { | |||
| if (groups.length === 0) return ""; | |||
| @@ -3,6 +3,7 @@ | |||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { PurchaseQcResult } from "@/app/api/po/actions"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -187,7 +188,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| if (qcData.qcAccept) { | |||
| onOpenPutaway(); | |||
| } else { | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| } | |||
| }, | |||
| [onOpenPutaway, qcItems], | |||
| @@ -281,7 +282,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| onSubmit={formProps.handleSubmit(onSubmitPutaway)} | |||
| > | |||
| <PutawayForm | |||
| itemDetail={itemDetail} | |||
| itemDetail={itemDetail as unknown as StockInLine} | |||
| warehouse={warehouse!} | |||
| disabled={false} | |||
| /> | |||
| @@ -341,7 +342,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| > | |||
| <QcFormVer2 | |||
| qc={qc!} | |||
| itemDetail={itemDetail} | |||
| itemDetail={itemDetail as unknown as StockInLine} | |||
| disabled={false} | |||
| qcItems={qcItems} | |||
| setQcItems={setQcItems} | |||
| @@ -58,6 +58,7 @@ const InventorySearch: React.FC<Props> = ({ inventories }) => { | |||
| currencyName: "", | |||
| status: "", | |||
| baseUom: "", | |||
| uomShortDesc: "", | |||
| }), []) | |||
| const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs); | |||
| @@ -195,7 +195,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||
| setFilterObj({ | |||
| ...query, | |||
| }); | |||
| refetchData(query); | |||
| refetchData(query as unknown as SearchQuery); | |||
| }} | |||
| onReset={onReset} | |||
| /> | |||
| @@ -61,7 +61,7 @@ const EscalationComponent: React.FC<Props> = ({ | |||
| ]; | |||
| const handleInputChange = ( | |||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SelectChangeEvent<string> | |||
| ): void => { | |||
| const { name, value } = event.target; | |||
| setFormData((prev) => ({ | |||
| @@ -111,7 +111,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
| if (isPickOrderData) { | |||
| // PickOrder 数据 | |||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | |||
| return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; | |||
| return pickOrderItem.uomDesc || (pickOrderItem as { uomCode?: string }).uomCode || ''; | |||
| } else { | |||
| // StockIn 数据 | |||
| const stockInItem = itemDetail as StockInLine; | |||
| @@ -169,7 +169,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
| <TextField | |||
| label={t("itemNo")} | |||
| fullWidth | |||
| {...register("itemNo", { | |||
| {...(register as (name: string, opts?: object) => ReturnType<typeof register>)("itemNo", { | |||
| required: "itemNo required!", | |||
| })} | |||
| value={getItemDisplayValue()} | |||
| @@ -61,7 +61,7 @@ const EscalationComponent: React.FC<Props> = ({ | |||
| ]; | |||
| const handleInputChange = ( | |||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SelectChangeEvent<string> | |||
| ): void => { | |||
| const { name, value } = event.target; | |||
| setFormData((prev) => ({ | |||
| @@ -75,7 +75,8 @@ interface LotTableProps { | |||
| selectedLotForInput: LotPickData | null; | |||
| generateInputBody: () => any; | |||
| onDataRefresh: () => Promise<void>; | |||
| onLotDataRefresh: () => Promise<void>; | |||
| onLotDataRefresh: () => Promise<void>; | |||
| onIssueNoLotStockOutLine?: (stockOutLineId: number) => void | Promise<void>; | |||
| } | |||
| // QR Code Modal Component | |||
| @@ -964,7 +964,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) | |||
| {lotData.length > 0 ? ( | |||
| <LotTable | |||
| lotData={lotData} | |||
| lotData={lotData as Parameters<typeof LotTable>[0]["lotData"]} | |||
| selectedRowId={selectedRowId} | |||
| selectedRow={selectedRow} | |||
| pickQtyData={pickQtyData} | |||
| @@ -981,7 +981,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) | |||
| showInputBody={showInputBody} | |||
| onIssueNoLotStockOutLine={handleIssueNoLotStockOutLine} | |||
| setShowInputBody={setShowInputBody} | |||
| //selectedLotForInput={selectedLotForInput} | |||
| selectedLotForInput={selectedLotForInput as Parameters<typeof LotTable>[0]["selectedLotForInput"]} | |||
| generateInputBody={generateInputBody} | |||
| // Add missing props | |||
| totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed | |||
| @@ -22,7 +22,7 @@ import { | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; | |||
| import { dummyQCData, QcData } from "./dummyQcTemplate"; | |||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | |||
| const style = { | |||
| @@ -149,17 +149,17 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { | |||
| submitDialogWithWarning(() => { | |||
| console.log("QC accepted with failed items"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); | |||
| return; | |||
| } | |||
| if (qcData.qcAccept) { | |||
| console.log("QC accepted"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| } else { | |||
| console.log("QC rejected"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| } | |||
| }, | |||
| [qcItems, onClose, t], | |||
| @@ -260,7 +260,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| color="warning" | |||
| onClick={() => { | |||
| console.log("Sort to accept"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| }} | |||
| > | |||
| Sort to Accept | |||
| @@ -270,7 +270,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| color="error" | |||
| onClick={() => { | |||
| console.log("Reject and pick another lot"); | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| }} | |||
| > | |||
| Reject and Pick Another Lot | |||
| @@ -111,7 +111,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
| if (isPickOrderData) { | |||
| // PickOrder 数据 | |||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | |||
| return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; | |||
| return pickOrderItem.uomDesc || (pickOrderItem as { uomCode?: string }).uomCode || ''; | |||
| } else { | |||
| // StockIn 数据 | |||
| const stockInItem = itemDetail as StockInLine; | |||
| @@ -169,7 +169,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
| <TextField | |||
| label={t("itemNo")} | |||
| fullWidth | |||
| {...register("itemNo", { | |||
| {...(register as (name: string, opts?: object) => ReturnType<typeof register>)("itemNo", { | |||
| required: "itemNo required!", | |||
| })} | |||
| value={getItemDisplayValue()} | |||
| @@ -3,6 +3,7 @@ | |||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { PurchaseQcResult } from "@/app/api/po/actions"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -187,7 +188,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| if (qcData.qcAccept) { | |||
| onOpenPutaway(); | |||
| } else { | |||
| onClose(); | |||
| onClose?.({} as object, "backdropClick"); | |||
| } | |||
| }, | |||
| [onOpenPutaway, qcItems], | |||
| @@ -281,7 +282,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| onSubmit={formProps.handleSubmit(onSubmitPutaway)} | |||
| > | |||
| <PutawayForm | |||
| itemDetail={itemDetail} | |||
| itemDetail={itemDetail as unknown as StockInLine} | |||
| warehouse={warehouse!} | |||
| disabled={false} | |||
| /> | |||
| @@ -341,7 +342,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| > | |||
| <QcFormVer2 | |||
| qc={qc!} | |||
| itemDetail={itemDetail} | |||
| itemDetail={itemDetail as unknown as StockInLine} | |||
| disabled={false} | |||
| qcItems={qcItems} | |||
| setQcItems={setQcItems} | |||
| @@ -1,7 +1,7 @@ | |||
| import StockInFormOld from "./StockInFormOld"; | |||
| import EscalationLog from "./EscalationLog"; | |||
| import EscalationComponent from "./EscalationComponent"; | |||
| import React from "react"; | |||
| import React, { useState } from "react"; | |||
| import { PurchaseQcResult } from "@/app/api/po/actions"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| @@ -13,10 +13,11 @@ interface Props { | |||
| } | |||
| const EscalationTab:React.FC<Props> = ({itemDetail, disabled}) => { | |||
| const [isCollapsed, setIsCollapsed] = useState(false); | |||
| return <> | |||
| <StockInFormOld itemDetail={itemDetail} disabled={disabled}/> | |||
| <EscalationLog/> | |||
| <EscalationComponent/> | |||
| <EscalationComponent forSupervisor={false} isCollapsed={isCollapsed} setIsCollapsed={setIsCollapsed}/> | |||
| </> | |||
| }; | |||
| @@ -37,7 +37,7 @@ import { | |||
| GridApiCommunity, | |||
| GridSlotsComponentsProps, | |||
| } from "@mui/x-data-grid/internals"; | |||
| import { dummyQCData } from "../Qc/dummyQcTemplate"; | |||
| import { dummyQcData_A1 as dummyQCData } from "../Qc/dummyQcTemplate"; | |||
| // T == CreatexxxInputs map of the form's fields | |||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||
| // E == error | |||
| @@ -69,11 +69,11 @@ const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| console.log(defaultValues); | |||
| //// validate form | |||
| const accQty = watch("acceptedQty"); | |||
| const accQty = watch("acceptQty"); | |||
| const validateForm = useCallback(() => { | |||
| console.log(accQty); | |||
| if (accQty > itemDetail.acceptedQty) { | |||
| setError("acceptedQty", { | |||
| setError("acceptQty", { | |||
| message: `${t("acceptedQty must not greater than")} ${ | |||
| itemDetail.acceptedQty | |||
| }`, | |||
| @@ -81,13 +81,13 @@ const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| }); | |||
| } | |||
| if (accQty < 1) { | |||
| setError("acceptedQty", { | |||
| setError("acceptQty", { | |||
| message: t("minimal value is 1"), | |||
| type: "required", | |||
| }); | |||
| } | |||
| if (isNaN(accQty)) { | |||
| setError("acceptedQty", { | |||
| setError("acceptQty", { | |||
| message: t("value must be a number"), | |||
| type: "required", | |||
| }); | |||
| @@ -224,14 +224,14 @@ const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| label={t("accepted Qty")} | |||
| fullWidth | |||
| // value={itemDetail.acceptedQty} | |||
| {...register("acceptedQty", { | |||
| {...register("acceptQty", { | |||
| required: "acceptedQty required!", | |||
| valueAsNumber: true, | |||
| max: itemDetail.acceptedQty, | |||
| })} | |||
| disabled={disabled} | |||
| error={Boolean(errors.acceptedQty)} | |||
| helperText={errors.acceptedQty?.message} | |||
| error={Boolean(errors.acceptQty)} | |||
| helperText={errors.acceptQty?.message} | |||
| /> | |||
| </Grid> | |||
| {/* <Grid item xs={12} lg={6}> | |||
| @@ -20,7 +20,7 @@ import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react- | |||
| import { StockInLineRow } from "./PoInputGrid"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StockInForm from "../StockIn/StockInForm"; | |||
| import QcComponent from "./QcComponent"; | |||
| import QcComponent from "../Qc/QcComponent"; | |||
| import PutAwayForm from "./PutAwayForm"; | |||
| import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; | |||
| import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; | |||
| @@ -10,6 +10,7 @@ import { | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { GridRowModesModel, GridRowSelectionModel } from "@mui/x-data-grid"; | |||
| import ReactQrCodeScanner, { | |||
| ScannerConfig, | |||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||
| @@ -107,6 +108,8 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||
| }, [scanner.values]); | |||
| const [itemDetail, setItemDetail] = useState<StockInLine>(); | |||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
| const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>([]); | |||
| const [disabledSubmit, setDisabledSubmit] = useState(false); | |||
| const [unavailableText, setUnavailableText] = useState<string | undefined>( | |||
| undefined, | |||
| @@ -208,6 +211,8 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse} | |||
| disabled={false} | |||
| setRowModesModel={setRowModesModel} | |||
| setRowSelectionModel={setRowSelectionModel} | |||
| /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| @@ -35,6 +35,13 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| outputFromProcessUom: "", | |||
| defectQty: 0, | |||
| defectUom: "", | |||
| defect2Qty: 0, | |||
| defect2Uom: "", | |||
| defect3Qty: 0, | |||
| defect3Uom: "", | |||
| defectDescription: "", | |||
| defectDescription2: "", | |||
| defectDescription3: "", | |||
| scrapQty: 0, | |||
| scrapUom: "", | |||
| byproductName: "", | |||
| @@ -75,16 +82,8 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| try { | |||
| await updateProductProcessLineQty({ | |||
| ...outputData, | |||
| productProcessLineId: lineDetail.id || 0, | |||
| byproductName: outputData.byproductName, | |||
| byproductQty: outputData.byproductQty, | |||
| byproductUom: outputData.byproductUom, | |||
| outputFromProcessQty: outputData.outputFromProcessQty, | |||
| outputFromProcessUom: outputData.outputFromProcessUom, | |||
| defectQty: outputData.defectQty, | |||
| defectUom: outputData.defectUom, | |||
| scrapQty: outputData.scrapQty, | |||
| scrapUom: outputData.scrapUom, | |||
| }); | |||
| console.log("Output data submitted successfully"); | |||
| @@ -65,7 +65,7 @@ export default function NextAppDirEmotionCacheProvider( | |||
| inserted.forEach(({ name, isGlobal }) => { | |||
| const style = registry.cache.inserted[name]; | |||
| if (typeof style !== "boolean") { | |||
| if (typeof style !== "boolean" && style != null) { | |||
| if (isGlobal) { | |||
| globals.push({ name, style }); | |||
| } else { | |||
| @@ -24,5 +24,5 @@ | |||
| "baseUrl": "." | |||
| }, | |||
| "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], | |||
| "exclude": ["node_modules"] | |||
| "exclude": ["node_modules", "src/components/FinishedGoodSearch/newcreatitem copy.tsx", "src/components/PickOrderSearch/newcreatitem copy.tsx"] | |||
| } | |||