| @@ -1,5 +1,5 @@ | |||||
| import { SearchParams } from "@/app/utils/fetchUtil"; | import { SearchParams } from "@/app/utils/fetchUtil"; | ||||
| import DoDetail from "@/components/DoDetail/DodetailWrapper"; | |||||
| import DoDetail from "@/components/DoDetail/DoDetailWrapper"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
| import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
| import { isArray } from "lodash"; | import { isArray } from "lodash"; | ||||
| @@ -12,8 +12,8 @@ import { | |||||
| OnlinePrediction, FileDownload, SettingsEthernet | OnlinePrediction, FileDownload, SettingsEthernet | ||||
| } from "@mui/icons-material"; | } from "@mui/icons-material"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { redirect } from "next/navigation"; | |||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||||
| export default function ProductionSchedulePage() { | export default function ProductionSchedulePage() { | ||||
| // ── Main states ── | // ── Main states ── | ||||
| @@ -69,21 +69,14 @@ export default function ProductionSchedulePage() { | |||||
| // ── API Actions ── | // ── API Actions ── | ||||
| const handleSearch = async () => { | const handleSearch = async () => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | 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', | 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(); | const data = await response.json(); | ||||
| @@ -101,7 +94,6 @@ export default function ProductionSchedulePage() { | |||||
| return; | return; | ||||
| } | } | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| setLoading(true); | setLoading(true); | ||||
| setIsForecastDialogOpen(false); | setIsForecastDialogOpen(false); | ||||
| @@ -113,10 +105,9 @@ export default function ProductionSchedulePage() { | |||||
| const url = `${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule?${params.toString()}`; | 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) { | if (response.ok) { | ||||
| await handleSearch(); // refresh list | await handleSearch(); // refresh list | ||||
| @@ -140,7 +131,6 @@ export default function ProductionSchedulePage() { | |||||
| return; | return; | ||||
| } | } | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| setLoading(true); | setLoading(true); | ||||
| setIsExportDialogOpen(false); | setIsExportDialogOpen(false); | ||||
| @@ -149,11 +139,11 @@ export default function ProductionSchedulePage() { | |||||
| fromDate: exportFromDate, | 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}`); | if (!response.ok) throw new Error(`Export failed: ${response.status}`); | ||||
| const blob = await response.blob(); | const blob = await response.blob(); | ||||
| @@ -183,25 +173,15 @@ export default function ProductionSchedulePage() { | |||||
| return; | return; | ||||
| } | } | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| console.log("Token exists:", !!token); | |||||
| setSelectedPs(ps); | setSelectedPs(ps); | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`; | 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) { | if (!response.ok) { | ||||
| const errorText = await response.text().catch(() => "(no text)"); | const errorText = await response.text().catch(() => "(no text)"); | ||||
| @@ -229,19 +209,17 @@ export default function ProductionSchedulePage() { | |||||
| const handleAutoGenJob = async () => { | const handleAutoGenJob = async () => { | ||||
| //if (!isDateToday) return; | |||||
| const token = localStorage.getItem("accessToken"); | |||||
| //if (!isDateToday) return; | |||||
| setIsGenerating(true); | setIsGenerating(true); | ||||
| try { | 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', | method: 'POST', | ||||
| headers: { | |||||
| 'Authorization': `Bearer ${token}`, | |||||
| 'Content-Type': 'application/json' | |||||
| }, | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify({ id: selectedPs.id }) | body: JSON.stringify({ id: selectedPs.id }) | ||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) { | if (response.ok) { | ||||
| const data = await response.json(); | const data = await response.json(); | ||||
| const displayMessage = data.message || "Operation completed."; | const displayMessage = data.message || "Operation completed."; | ||||
| @@ -17,6 +17,7 @@ import { | |||||
| import PrintIcon from '@mui/icons-material/Print'; | import PrintIcon from '@mui/icons-material/Print'; | ||||
| import { REPORTS, ReportDefinition } from '@/config/reportConfig'; | import { REPORTS, ReportDefinition } from '@/config/reportConfig'; | ||||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | import { NEXT_PUBLIC_API_URL } from '@/config/api'; | ||||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | |||||
| import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | ||||
| import { | import { | ||||
| fetchSemiFGItemCodes, | fetchSemiFGItemCodes, | ||||
| @@ -90,25 +91,17 @@ export default function ReportPage() { | |||||
| } | } | ||||
| // Handle other reports with dynamic options | // 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; | let url = field.dynamicOptionsEndpoint; | ||||
| if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) { | if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) { | ||||
| // Multiple categories selected (e.g., "FG,WIP") | |||||
| url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`; | url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`; | ||||
| } | } | ||||
| const response = await fetch(url, { | |||||
| const response = await clientAuthFetch(url, { | |||||
| method: 'GET', | 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}`); | if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | ||||
| const data = await response.json(); | const data = await response.json(); | ||||
| @@ -159,21 +152,18 @@ export default function ReportPage() { | |||||
| const executePrint = async () => { | const executePrint = async () => { | ||||
| if (!currentReport) return; | if (!currentReport) return; | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const queryParams = new URLSearchParams(criteria).toString(); | const queryParams = new URLSearchParams(criteria).toString(); | ||||
| const url = `${currentReport.apiEndpoint}?${queryParams}`; | const url = `${currentReport.apiEndpoint}?${queryParams}`; | ||||
| const response = await fetch(url, { | |||||
| const response = await clientAuthFetch(url, { | |||||
| method: 'GET', | method: 'GET', | ||||
| headers: { | |||||
| 'Authorization': `Bearer ${token}`, | |||||
| 'Accept': 'application/pdf', | |||||
| }, | |||||
| headers: { 'Accept': 'application/pdf' }, | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (!response.ok) { | if (!response.ok) { | ||||
| const errorText = await response.text(); | const errorText = await response.text(); | ||||
| console.error("Response error:", errorText); | console.error("Response error:", errorText); | ||||
| @@ -1,4 +1,7 @@ | |||||
| "use client"; | |||||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | import { NEXT_PUBLIC_API_URL } from '@/config/api'; | ||||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | |||||
| export interface ItemCodeWithName { | export interface ItemCodeWithName { | ||||
| code: string; | code: string; | ||||
| @@ -19,24 +22,18 @@ export interface ItemCodeWithCategory { | |||||
| export const fetchSemiFGItemCodes = async ( | export const fetchSemiFGItemCodes = async ( | ||||
| stockCategory: string = '' | stockCategory: string = '' | ||||
| ): Promise<ItemCodeWithName[]> => { | ): Promise<ItemCodeWithName[]> => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`; | let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`; | ||||
| if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { | if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { | ||||
| url = `${url}?stockCategory=${stockCategory}`; | url = `${url}?stockCategory=${stockCategory}`; | ||||
| } | } | ||||
| const response = await fetch(url, { | |||||
| const response = await clientAuthFetch(url, { | |||||
| method: 'GET', | 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(); | return await response.json(); | ||||
| }; | }; | ||||
| @@ -49,24 +46,18 @@ export const fetchSemiFGItemCodes = async ( | |||||
| export const fetchSemiFGItemCodesWithCategory = async ( | export const fetchSemiFGItemCodesWithCategory = async ( | ||||
| stockCategory: string = '' | stockCategory: string = '' | ||||
| ): Promise<ItemCodeWithCategory[]> => { | ): Promise<ItemCodeWithCategory[]> => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes-with-category`; | let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes-with-category`; | ||||
| if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { | if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { | ||||
| url = `${url}?stockCategory=${stockCategory}`; | url = `${url}?stockCategory=${stockCategory}`; | ||||
| } | } | ||||
| const response = await fetch(url, { | |||||
| const response = await clientAuthFetch(url, { | |||||
| method: 'GET', | 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(); | return await response.json(); | ||||
| }; | }; | ||||
| @@ -81,21 +72,16 @@ export const generateSemiFGProductionAnalysisReport = async ( | |||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = '成品/半成品生產分析報告' | reportTitle: string = '成品/半成品生產分析報告' | ||||
| ): Promise<void> => { | ): Promise<void> => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const queryParams = new URLSearchParams(criteria).toString(); | const queryParams = new URLSearchParams(criteria).toString(); | ||||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis?${queryParams}`; | 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', | 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 blob = await response.blob(); | ||||
| const downloadUrl = window.URL.createObjectURL(blob); | const downloadUrl = window.URL.createObjectURL(blob); | ||||
| @@ -10,6 +10,7 @@ import { | |||||
| import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material"; | import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||||
| // Simple TabPanel component for conditional rendering | // Simple TabPanel component for conditional rendering | ||||
| interface TabPanelProps { | interface TabPanelProps { | ||||
| @@ -97,14 +98,14 @@ export default function TestingPage() { | |||||
| // TSC Print (Section 1) | // TSC Print (Section 1) | ||||
| const handleTscPrint = async (row: any) => { | const handleTscPrint = async (row: any) => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const payload = { ...row, printerIp: tscConfig.ip, printerPort: tscConfig.port }; | const payload = { ...row, printerIp: tscConfig.ip, printerPort: tscConfig.port }; | ||||
| try { | 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', | method: 'POST', | ||||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify(payload) | body: JSON.stringify(payload) | ||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) alert(`TSC Print Command Sent for ${row.itemCode}!`); | if (response.ok) alert(`TSC Print Command Sent for ${row.itemCode}!`); | ||||
| else alert("TSC Print Failed"); | else alert("TSC Print Failed"); | ||||
| } catch (e) { console.error("TSC Error:", e); } | } catch (e) { console.error("TSC Error:", e); } | ||||
| @@ -112,14 +113,14 @@ export default function TestingPage() { | |||||
| // DataFlex Print (Section 2) | // DataFlex Print (Section 2) | ||||
| const handleDfPrint = async (row: any) => { | const handleDfPrint = async (row: any) => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const payload = { ...row, printerIp: dfConfig.ip, printerPort: dfConfig.port }; | const payload = { ...row, printerIp: dfConfig.ip, printerPort: dfConfig.port }; | ||||
| try { | 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', | method: 'POST', | ||||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify(payload) | body: JSON.stringify(payload) | ||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) alert(`DataFlex Print Command Sent for ${row.itemCode}!`); | if (response.ok) alert(`DataFlex Print Command Sent for ${row.itemCode}!`); | ||||
| else alert("DataFlex Print Failed"); | else alert("DataFlex Print Failed"); | ||||
| } catch (e) { console.error("DataFlex Error:", e); } | } catch (e) { console.error("DataFlex Error:", e); } | ||||
| @@ -127,14 +128,13 @@ export default function TestingPage() { | |||||
| // OnPack Zip Download (Section 3) | // OnPack Zip Download (Section 3) | ||||
| const handleDownloadPrintJob = async () => { | const handleDownloadPrintJob = async () => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const params = new URLSearchParams(printerFormData); | const params = new URLSearchParams(printerFormData); | ||||
| try { | 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', | method: 'GET', | ||||
| headers: { 'Authorization': `Bearer ${token}` } | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (!response.ok) throw new Error('Download failed'); | if (!response.ok) throw new Error('Download failed'); | ||||
| const blob = await response.blob(); | const blob = await response.blob(); | ||||
| @@ -153,34 +153,33 @@ export default function TestingPage() { | |||||
| // Laser Print (Section 4 - original) | // Laser Print (Section 4 - original) | ||||
| const handleLaserPrint = async (row: any) => { | const handleLaserPrint = async (row: any) => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; | const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; | ||||
| try { | 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', | method: 'POST', | ||||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify(payload) | body: JSON.stringify(payload) | ||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) alert(`Laser Command Sent: ${row.templateId}`); | if (response.ok) alert(`Laser Command Sent: ${row.templateId}`); | ||||
| } catch (e) { console.error(e); } | } catch (e) { console.error(e); } | ||||
| }; | }; | ||||
| const handleLaserPreview = async (row: any) => { | const handleLaserPreview = async (row: any) => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; | const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; | ||||
| try { | 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', | method: 'POST', | ||||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify(payload) | body: JSON.stringify(payload) | ||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) alert("Red light preview active!"); | if (response.ok) alert("Red light preview active!"); | ||||
| } catch (e) { console.error("Preview Error:", e); } | } catch (e) { console.error("Preview Error:", e); } | ||||
| }; | }; | ||||
| // HANS600S-M TCP Print (Section 5) | // HANS600S-M TCP Print (Section 5) | ||||
| const handleHansPrint = async (row: any) => { | const handleHansPrint = async (row: any) => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| const payload = { | const payload = { | ||||
| printerIp: hansConfig.ip, | printerIp: hansConfig.ip, | ||||
| printerPort: hansConfig.port, | printerPort: hansConfig.port, | ||||
| @@ -190,11 +189,12 @@ export default function TestingPage() { | |||||
| text4ObjectName: row.text4ObjectName | text4ObjectName: row.text4ObjectName | ||||
| }; | }; | ||||
| try { | 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', | method: 'POST', | ||||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||||
| headers: { 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify(payload) | body: JSON.stringify(payload) | ||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| const result = await response.text(); | const result = await response.text(); | ||||
| if (response.ok) { | if (response.ok) { | ||||
| alert(`HANS600S-M Mark Success: ${result}`); | alert(`HANS600S-M Mark Success: ${result}`); | ||||
| @@ -29,9 +29,9 @@ export interface QcData { | |||||
| name?: string, | name?: string, | ||||
| order?: number, | order?: number, | ||||
| description?: string, | description?: string, | ||||
| // qcPassed: boolean | undefined | |||||
| // failQty: number | undefined | |||||
| // remarks: string | undefined | |||||
| qcPassed?: boolean, | |||||
| failQty?: number, | |||||
| remarks?: string, | |||||
| } | } | ||||
| export interface QcResult extends QcData{ | export interface QcResult extends QcData{ | ||||
| id?: number; | id?: number; | ||||
| @@ -2,7 +2,7 @@ | |||||
| // import { serverFetchWithNoContent } from '@/app/utils/fetchUtil'; | // import { serverFetchWithNoContent } from '@/app/utils/fetchUtil'; | ||||
| // import { BASE_API_URL } from "@/config/api"; | // 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"; | import { BASE_API_URL } from "../../../../config/api"; | ||||
| export interface M18ImportPoForm { | export interface M18ImportPoForm { | ||||
| @@ -85,13 +85,13 @@ export const triggerScheduler = async (type: 'po' | 'do1' | 'do2' | 'master-data | |||||
| console.log("Fetching URL:", url); | console.log("Fetching URL:", url); | ||||
| const response = await serverFetchWithNoContent(url, { | |||||
| const response = await serverFetch(url, { | |||||
| method: "GET", | method: "GET", | ||||
| cache: "no-store", | cache: "no-store", | ||||
| }); | }); | ||||
| if (!response.ok) throw new Error(`Failed: ${response.status}`); | if (!response.ok) throw new Error(`Failed: ${response.status}`); | ||||
| return await response.text(); | return await response.text(); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Scheduler Action Error:", error); | console.error("Scheduler Action Error:", error); | ||||
| @@ -103,13 +103,13 @@ export const refreshCronSchedules = async () => { | |||||
| // Simply reuse the triggerScheduler logic to avoid duplication | // Simply reuse the triggerScheduler logic to avoid duplication | ||||
| // or call serverFetch directly as shown below: | // or call serverFetch directly as shown below: | ||||
| try { | try { | ||||
| const response = await serverFetchWithNoContent(`${BASE_API_URL}/scheduler/refresh-cron`, { | |||||
| const response = await serverFetch(`${BASE_API_URL}/scheduler/refresh-cron`, { | |||||
| method: "GET", | method: "GET", | ||||
| cache: "no-store", | cache: "no-store", | ||||
| }); | }); | ||||
| if (!response.ok) throw new Error(`Failed to refresh: ${response.status}`); | if (!response.ok) throw new Error(`Failed to refresh: ${response.status}`); | ||||
| return await response.text(); | return await response.text(); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Refresh Cron Error:", error); | console.error("Refresh Cron Error:", error); | ||||
| @@ -13,10 +13,11 @@ export interface UserInputs { | |||||
| username: string; | username: string; | ||||
| name: string; | name: string; | ||||
| staffNo?: string; | staffNo?: string; | ||||
| locked?: boolean; | |||||
| addAuthIds?: number[]; | addAuthIds?: number[]; | ||||
| removeAuthIds?: number[]; | removeAuthIds?: number[]; | ||||
| password?: string; | password?: string; | ||||
| confirmPassword?: string; | |||||
| confirmPassword?: string; | |||||
| } | } | ||||
| export interface PasswordInputs { | 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, | username: data.username, | ||||
| // name: data.name, | |||||
| name: data.name ?? "", | |||||
| staffNo: data.staffNo, | |||||
| locked: false, | locked: false, | ||||
| addAuthIds: data.addAuthIds || [], | addAuthIds: data.addAuthIds || [], | ||||
| removeAuthIds: data.removeAuthIds || [], | removeAuthIds: data.removeAuthIds || [], | ||||
| @@ -26,7 +26,7 @@ import isToday from 'dayjs/plugin/isToday'; | |||||
| import useUploadContext from "../UploadProvider/useUploadContext"; | import useUploadContext from "../UploadProvider/useUploadContext"; | ||||
| import { FileDownload, CalendarMonth } from "@mui/icons-material"; | import { FileDownload, CalendarMonth } from "@mui/icons-material"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { VIEW_USER } from "@/authorities"; | |||||
| import { AUTH } from "@/authorities"; | |||||
| dayjs.extend(isToday); | dayjs.extend(isToday); | ||||
| @@ -384,7 +384,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| {t("Export Schedule")} | {t("Export Schedule")} | ||||
| </Button> | </Button> | ||||
| {false && abilities.includes(VIEW_USER) && ( | |||||
| {false && abilities.includes(AUTH.VIEW_USER) && ( | |||||
| <Button | <Button | ||||
| variant="contained" // Solid button for the "Export" action | variant="contained" // Solid button for the "Export" action | ||||
| color="success" // Green color often signifies a successful action/download | color="success" // Green color often signifies a successful action/download | ||||
| @@ -9,7 +9,7 @@ interface SubComponents { | |||||
| const DoSearchWrapper: React.FC & SubComponents = async () => { | const DoSearchWrapper: React.FC & SubComponents = async () => { | ||||
| // const [dos] = await Promise.all([fetchDoList()]); | // const [dos] = await Promise.all([fetchDoList()]); | ||||
| return <DoSearch />; | |||||
| return <DoSearch onDeliveryOrderSearch={() => {}} />; | |||||
| }; | }; | ||||
| DoSearchWrapper.Loading = GeneralLoading; | DoSearchWrapper.Loading = GeneralLoading; | ||||
| @@ -61,7 +61,7 @@ const EscalationComponent: React.FC<Props> = ({ | |||||
| ]; | ]; | ||||
| const handleInputChange = ( | const handleInputChange = ( | ||||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SelectChangeEvent<string> | |||||
| ): void => { | ): void => { | ||||
| const { name, value } = event.target; | const { name, value } = event.target; | ||||
| setFormData((prev) => ({ | setFormData((prev) => ({ | ||||
| @@ -22,7 +22,7 @@ import { | |||||
| import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | ||||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; | |||||
| import { dummyQCData, QcData } from "./dummyQcTemplate"; | |||||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | ||||
| const style = { | const style = { | ||||
| @@ -149,17 +149,17 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { | if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { | ||||
| submitDialogWithWarning(() => { | submitDialogWithWarning(() => { | ||||
| console.log("QC accepted with failed items"); | console.log("QC accepted with failed items"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); | }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); | ||||
| return; | return; | ||||
| } | } | ||||
| if (qcData.qcAccept) { | if (qcData.qcAccept) { | ||||
| console.log("QC accepted"); | console.log("QC accepted"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| } else { | } else { | ||||
| console.log("QC rejected"); | console.log("QC rejected"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| } | } | ||||
| }, | }, | ||||
| [qcItems, onClose, t], | [qcItems, onClose, t], | ||||
| @@ -260,7 +260,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| color="warning" | color="warning" | ||||
| onClick={() => { | onClick={() => { | ||||
| console.log("Sort to accept"); | console.log("Sort to accept"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| }} | }} | ||||
| > | > | ||||
| Sort to Accept | Sort to Accept | ||||
| @@ -270,7 +270,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| color="error" | color="error" | ||||
| onClick={() => { | onClick={() => { | ||||
| console.log("Reject and pick another lot"); | console.log("Reject and pick another lot"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| }} | }} | ||||
| > | > | ||||
| Reject and Pick Another Lot | Reject and Pick Another Lot | ||||
| @@ -24,7 +24,7 @@ import { | |||||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | ||||
| import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; | import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { dummyQCData } from "../PoDetail/dummyQcTemplate"; | |||||
| import { dummyQCData } from "./dummyQcTemplate"; | |||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| import { GridColDef } from "@mui/x-data-grid"; | import { GridColDef } from "@mui/x-data-grid"; | ||||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | ||||
| @@ -111,7 +111,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||||
| if (isPickOrderData) { | if (isPickOrderData) { | ||||
| // PickOrder 数据 | // PickOrder 数据 | ||||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | ||||
| return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; | |||||
| return pickOrderItem.uomDesc || (pickOrderItem as { uomCode?: string }).uomCode || ''; | |||||
| } else { | } else { | ||||
| // StockIn 数据 | // StockIn 数据 | ||||
| const stockInItem = itemDetail as StockInLine; | const stockInItem = itemDetail as StockInLine; | ||||
| @@ -169,7 +169,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||||
| <TextField | <TextField | ||||
| label={t("itemNo")} | label={t("itemNo")} | ||||
| fullWidth | fullWidth | ||||
| {...register("itemNo", { | |||||
| {...(register as (name: string, opts?: object) => ReturnType<typeof register>)("itemNo", { | |||||
| required: "itemNo required!", | required: "itemNo required!", | ||||
| })} | })} | ||||
| value={getItemDisplayValue()} | value={getItemDisplayValue()} | ||||
| @@ -1346,709 +1346,6 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| setSearchResultsPagingController(newPagingController); | 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 | // Add helper function to get group range text | ||||
| const getGroupRangeText = useCallback(() => { | const getGroupRangeText = useCallback(() => { | ||||
| if (groups.length === 0) return ""; | if (groups.length === 0) return ""; | ||||
| @@ -3,6 +3,7 @@ | |||||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | ||||
| import { QcItemWithChecks } from "@/app/api/qc"; | import { QcItemWithChecks } from "@/app/api/qc"; | ||||
| import { PurchaseQcResult } from "@/app/api/po/actions"; | import { PurchaseQcResult } from "@/app/api/po/actions"; | ||||
| import { StockInLine } from "@/app/api/po"; | |||||
| import { | import { | ||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| @@ -187,7 +188,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| if (qcData.qcAccept) { | if (qcData.qcAccept) { | ||||
| onOpenPutaway(); | onOpenPutaway(); | ||||
| } else { | } else { | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| } | } | ||||
| }, | }, | ||||
| [onOpenPutaway, qcItems], | [onOpenPutaway, qcItems], | ||||
| @@ -281,7 +282,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| onSubmit={formProps.handleSubmit(onSubmitPutaway)} | onSubmit={formProps.handleSubmit(onSubmitPutaway)} | ||||
| > | > | ||||
| <PutawayForm | <PutawayForm | ||||
| itemDetail={itemDetail} | |||||
| itemDetail={itemDetail as unknown as StockInLine} | |||||
| warehouse={warehouse!} | warehouse={warehouse!} | ||||
| disabled={false} | disabled={false} | ||||
| /> | /> | ||||
| @@ -341,7 +342,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| > | > | ||||
| <QcFormVer2 | <QcFormVer2 | ||||
| qc={qc!} | qc={qc!} | ||||
| itemDetail={itemDetail} | |||||
| itemDetail={itemDetail as unknown as StockInLine} | |||||
| disabled={false} | disabled={false} | ||||
| qcItems={qcItems} | qcItems={qcItems} | ||||
| setQcItems={setQcItems} | setQcItems={setQcItems} | ||||
| @@ -58,6 +58,7 @@ const InventorySearch: React.FC<Props> = ({ inventories }) => { | |||||
| currencyName: "", | currencyName: "", | ||||
| status: "", | status: "", | ||||
| baseUom: "", | baseUom: "", | ||||
| uomShortDesc: "", | |||||
| }), []) | }), []) | ||||
| const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs); | const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs); | ||||
| @@ -195,7 +195,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| setFilterObj({ | setFilterObj({ | ||||
| ...query, | ...query, | ||||
| }); | }); | ||||
| refetchData(query); | |||||
| refetchData(query as unknown as SearchQuery); | |||||
| }} | }} | ||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| @@ -61,7 +61,7 @@ const EscalationComponent: React.FC<Props> = ({ | |||||
| ]; | ]; | ||||
| const handleInputChange = ( | const handleInputChange = ( | ||||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SelectChangeEvent<string> | |||||
| ): void => { | ): void => { | ||||
| const { name, value } = event.target; | const { name, value } = event.target; | ||||
| setFormData((prev) => ({ | setFormData((prev) => ({ | ||||
| @@ -111,7 +111,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||||
| if (isPickOrderData) { | if (isPickOrderData) { | ||||
| // PickOrder 数据 | // PickOrder 数据 | ||||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | ||||
| return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; | |||||
| return pickOrderItem.uomDesc || (pickOrderItem as { uomCode?: string }).uomCode || ''; | |||||
| } else { | } else { | ||||
| // StockIn 数据 | // StockIn 数据 | ||||
| const stockInItem = itemDetail as StockInLine; | const stockInItem = itemDetail as StockInLine; | ||||
| @@ -169,7 +169,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||||
| <TextField | <TextField | ||||
| label={t("itemNo")} | label={t("itemNo")} | ||||
| fullWidth | fullWidth | ||||
| {...register("itemNo", { | |||||
| {...(register as (name: string, opts?: object) => ReturnType<typeof register>)("itemNo", { | |||||
| required: "itemNo required!", | required: "itemNo required!", | ||||
| })} | })} | ||||
| value={getItemDisplayValue()} | value={getItemDisplayValue()} | ||||
| @@ -61,7 +61,7 @@ const EscalationComponent: React.FC<Props> = ({ | |||||
| ]; | ]; | ||||
| const handleInputChange = ( | const handleInputChange = ( | ||||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||||
| event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SelectChangeEvent<string> | |||||
| ): void => { | ): void => { | ||||
| const { name, value } = event.target; | const { name, value } = event.target; | ||||
| setFormData((prev) => ({ | setFormData((prev) => ({ | ||||
| @@ -75,7 +75,8 @@ interface LotTableProps { | |||||
| selectedLotForInput: LotPickData | null; | selectedLotForInput: LotPickData | null; | ||||
| generateInputBody: () => any; | generateInputBody: () => any; | ||||
| onDataRefresh: () => Promise<void>; | onDataRefresh: () => Promise<void>; | ||||
| onLotDataRefresh: () => Promise<void>; | |||||
| onLotDataRefresh: () => Promise<void>; | |||||
| onIssueNoLotStockOutLine?: (stockOutLineId: number) => void | Promise<void>; | |||||
| } | } | ||||
| // QR Code Modal Component | // QR Code Modal Component | ||||
| @@ -964,7 +964,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) | |||||
| {lotData.length > 0 ? ( | {lotData.length > 0 ? ( | ||||
| <LotTable | <LotTable | ||||
| lotData={lotData} | |||||
| lotData={lotData as Parameters<typeof LotTable>[0]["lotData"]} | |||||
| selectedRowId={selectedRowId} | selectedRowId={selectedRowId} | ||||
| selectedRow={selectedRow} | selectedRow={selectedRow} | ||||
| pickQtyData={pickQtyData} | pickQtyData={pickQtyData} | ||||
| @@ -981,7 +981,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) | |||||
| showInputBody={showInputBody} | showInputBody={showInputBody} | ||||
| onIssueNoLotStockOutLine={handleIssueNoLotStockOutLine} | onIssueNoLotStockOutLine={handleIssueNoLotStockOutLine} | ||||
| setShowInputBody={setShowInputBody} | setShowInputBody={setShowInputBody} | ||||
| //selectedLotForInput={selectedLotForInput} | |||||
| selectedLotForInput={selectedLotForInput as Parameters<typeof LotTable>[0]["selectedLotForInput"]} | |||||
| generateInputBody={generateInputBody} | generateInputBody={generateInputBody} | ||||
| // Add missing props | // Add missing props | ||||
| totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed | totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed | ||||
| @@ -22,7 +22,7 @@ import { | |||||
| import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | ||||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; | |||||
| import { dummyQCData, QcData } from "./dummyQcTemplate"; | |||||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | ||||
| const style = { | const style = { | ||||
| @@ -149,17 +149,17 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { | if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { | ||||
| submitDialogWithWarning(() => { | submitDialogWithWarning(() => { | ||||
| console.log("QC accepted with failed items"); | console.log("QC accepted with failed items"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); | }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); | ||||
| return; | return; | ||||
| } | } | ||||
| if (qcData.qcAccept) { | if (qcData.qcAccept) { | ||||
| console.log("QC accepted"); | console.log("QC accepted"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| } else { | } else { | ||||
| console.log("QC rejected"); | console.log("QC rejected"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| } | } | ||||
| }, | }, | ||||
| [qcItems, onClose, t], | [qcItems, onClose, t], | ||||
| @@ -260,7 +260,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| color="warning" | color="warning" | ||||
| onClick={() => { | onClick={() => { | ||||
| console.log("Sort to accept"); | console.log("Sort to accept"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| }} | }} | ||||
| > | > | ||||
| Sort to Accept | Sort to Accept | ||||
| @@ -270,7 +270,7 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| color="error" | color="error" | ||||
| onClick={() => { | onClick={() => { | ||||
| console.log("Reject and pick another lot"); | console.log("Reject and pick another lot"); | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| }} | }} | ||||
| > | > | ||||
| Reject and Pick Another Lot | Reject and Pick Another Lot | ||||
| @@ -111,7 +111,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||||
| if (isPickOrderData) { | if (isPickOrderData) { | ||||
| // PickOrder 数据 | // PickOrder 数据 | ||||
| const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; | ||||
| return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; | |||||
| return pickOrderItem.uomDesc || (pickOrderItem as { uomCode?: string }).uomCode || ''; | |||||
| } else { | } else { | ||||
| // StockIn 数据 | // StockIn 数据 | ||||
| const stockInItem = itemDetail as StockInLine; | const stockInItem = itemDetail as StockInLine; | ||||
| @@ -169,7 +169,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||||
| <TextField | <TextField | ||||
| label={t("itemNo")} | label={t("itemNo")} | ||||
| fullWidth | fullWidth | ||||
| {...register("itemNo", { | |||||
| {...(register as (name: string, opts?: object) => ReturnType<typeof register>)("itemNo", { | |||||
| required: "itemNo required!", | required: "itemNo required!", | ||||
| })} | })} | ||||
| value={getItemDisplayValue()} | value={getItemDisplayValue()} | ||||
| @@ -3,6 +3,7 @@ | |||||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | ||||
| import { QcItemWithChecks } from "@/app/api/qc"; | import { QcItemWithChecks } from "@/app/api/qc"; | ||||
| import { PurchaseQcResult } from "@/app/api/po/actions"; | import { PurchaseQcResult } from "@/app/api/po/actions"; | ||||
| import { StockInLine } from "@/app/api/po"; | |||||
| import { | import { | ||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| @@ -187,7 +188,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| if (qcData.qcAccept) { | if (qcData.qcAccept) { | ||||
| onOpenPutaway(); | onOpenPutaway(); | ||||
| } else { | } else { | ||||
| onClose(); | |||||
| onClose?.({} as object, "backdropClick"); | |||||
| } | } | ||||
| }, | }, | ||||
| [onOpenPutaway, qcItems], | [onOpenPutaway, qcItems], | ||||
| @@ -281,7 +282,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| onSubmit={formProps.handleSubmit(onSubmitPutaway)} | onSubmit={formProps.handleSubmit(onSubmitPutaway)} | ||||
| > | > | ||||
| <PutawayForm | <PutawayForm | ||||
| itemDetail={itemDetail} | |||||
| itemDetail={itemDetail as unknown as StockInLine} | |||||
| warehouse={warehouse!} | warehouse={warehouse!} | ||||
| disabled={false} | disabled={false} | ||||
| /> | /> | ||||
| @@ -341,7 +342,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| > | > | ||||
| <QcFormVer2 | <QcFormVer2 | ||||
| qc={qc!} | qc={qc!} | ||||
| itemDetail={itemDetail} | |||||
| itemDetail={itemDetail as unknown as StockInLine} | |||||
| disabled={false} | disabled={false} | ||||
| qcItems={qcItems} | qcItems={qcItems} | ||||
| setQcItems={setQcItems} | setQcItems={setQcItems} | ||||
| @@ -1,7 +1,7 @@ | |||||
| import StockInFormOld from "./StockInFormOld"; | import StockInFormOld from "./StockInFormOld"; | ||||
| import EscalationLog from "./EscalationLog"; | import EscalationLog from "./EscalationLog"; | ||||
| import EscalationComponent from "./EscalationComponent"; | import EscalationComponent from "./EscalationComponent"; | ||||
| import React from "react"; | |||||
| import React, { useState } from "react"; | |||||
| import { PurchaseQcResult } from "@/app/api/po/actions"; | import { PurchaseQcResult } from "@/app/api/po/actions"; | ||||
| import { StockInLine } from "@/app/api/po"; | import { StockInLine } from "@/app/api/po"; | ||||
| @@ -13,10 +13,11 @@ interface Props { | |||||
| } | } | ||||
| const EscalationTab:React.FC<Props> = ({itemDetail, disabled}) => { | const EscalationTab:React.FC<Props> = ({itemDetail, disabled}) => { | ||||
| const [isCollapsed, setIsCollapsed] = useState(false); | |||||
| return <> | return <> | ||||
| <StockInFormOld itemDetail={itemDetail} disabled={disabled}/> | <StockInFormOld itemDetail={itemDetail} disabled={disabled}/> | ||||
| <EscalationLog/> | <EscalationLog/> | ||||
| <EscalationComponent/> | |||||
| <EscalationComponent forSupervisor={false} isCollapsed={isCollapsed} setIsCollapsed={setIsCollapsed}/> | |||||
| </> | </> | ||||
| }; | }; | ||||
| @@ -37,7 +37,7 @@ import { | |||||
| GridApiCommunity, | GridApiCommunity, | ||||
| GridSlotsComponentsProps, | GridSlotsComponentsProps, | ||||
| } from "@mui/x-data-grid/internals"; | } 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 | // T == CreatexxxInputs map of the form's fields | ||||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | ||||
| // E == error | // E == error | ||||
| @@ -69,11 +69,11 @@ const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||||
| console.log(defaultValues); | console.log(defaultValues); | ||||
| //// validate form | //// validate form | ||||
| const accQty = watch("acceptedQty"); | |||||
| const accQty = watch("acceptQty"); | |||||
| const validateForm = useCallback(() => { | const validateForm = useCallback(() => { | ||||
| console.log(accQty); | console.log(accQty); | ||||
| if (accQty > itemDetail.acceptedQty) { | if (accQty > itemDetail.acceptedQty) { | ||||
| setError("acceptedQty", { | |||||
| setError("acceptQty", { | |||||
| message: `${t("acceptedQty must not greater than")} ${ | message: `${t("acceptedQty must not greater than")} ${ | ||||
| itemDetail.acceptedQty | itemDetail.acceptedQty | ||||
| }`, | }`, | ||||
| @@ -81,13 +81,13 @@ const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||||
| }); | }); | ||||
| } | } | ||||
| if (accQty < 1) { | if (accQty < 1) { | ||||
| setError("acceptedQty", { | |||||
| setError("acceptQty", { | |||||
| message: t("minimal value is 1"), | message: t("minimal value is 1"), | ||||
| type: "required", | type: "required", | ||||
| }); | }); | ||||
| } | } | ||||
| if (isNaN(accQty)) { | if (isNaN(accQty)) { | ||||
| setError("acceptedQty", { | |||||
| setError("acceptQty", { | |||||
| message: t("value must be a number"), | message: t("value must be a number"), | ||||
| type: "required", | type: "required", | ||||
| }); | }); | ||||
| @@ -224,14 +224,14 @@ const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||||
| label={t("accepted Qty")} | label={t("accepted Qty")} | ||||
| fullWidth | fullWidth | ||||
| // value={itemDetail.acceptedQty} | // value={itemDetail.acceptedQty} | ||||
| {...register("acceptedQty", { | |||||
| {...register("acceptQty", { | |||||
| required: "acceptedQty required!", | required: "acceptedQty required!", | ||||
| valueAsNumber: true, | valueAsNumber: true, | ||||
| max: itemDetail.acceptedQty, | max: itemDetail.acceptedQty, | ||||
| })} | })} | ||||
| disabled={disabled} | disabled={disabled} | ||||
| error={Boolean(errors.acceptedQty)} | |||||
| helperText={errors.acceptedQty?.message} | |||||
| error={Boolean(errors.acceptQty)} | |||||
| helperText={errors.acceptQty?.message} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| {/* <Grid item xs={12} lg={6}> | {/* <Grid item xs={12} lg={6}> | ||||
| @@ -20,7 +20,7 @@ import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react- | |||||
| import { StockInLineRow } from "./PoInputGrid"; | import { StockInLineRow } from "./PoInputGrid"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import StockInForm from "../StockIn/StockInForm"; | import StockInForm from "../StockIn/StockInForm"; | ||||
| import QcComponent from "./QcComponent"; | |||||
| import QcComponent from "../Qc/QcComponent"; | |||||
| import PutAwayForm from "./PutAwayForm"; | import PutAwayForm from "./PutAwayForm"; | ||||
| import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; | import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; | ||||
| import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; | import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; | ||||
| @@ -10,6 +10,7 @@ import { | |||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | import { useCallback, useEffect, useMemo, useState } from "react"; | ||||
| import { GridRowModesModel, GridRowSelectionModel } from "@mui/x-data-grid"; | |||||
| import ReactQrCodeScanner, { | import ReactQrCodeScanner, { | ||||
| ScannerConfig, | ScannerConfig, | ||||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | ||||
| @@ -107,6 +108,8 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||||
| }, [scanner.values]); | }, [scanner.values]); | ||||
| const [itemDetail, setItemDetail] = useState<StockInLine>(); | const [itemDetail, setItemDetail] = useState<StockInLine>(); | ||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||||
| const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>([]); | |||||
| const [disabledSubmit, setDisabledSubmit] = useState(false); | const [disabledSubmit, setDisabledSubmit] = useState(false); | ||||
| const [unavailableText, setUnavailableText] = useState<string | undefined>( | const [unavailableText, setUnavailableText] = useState<string | undefined>( | ||||
| undefined, | undefined, | ||||
| @@ -208,6 +211,8 @@ const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||||
| itemDetail={itemDetail} | itemDetail={itemDetail} | ||||
| warehouse={warehouse} | warehouse={warehouse} | ||||
| disabled={false} | disabled={false} | ||||
| setRowModesModel={setRowModesModel} | |||||
| setRowSelectionModel={setRowSelectionModel} | |||||
| /> | /> | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| <Button | <Button | ||||
| @@ -35,6 +35,13 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||||
| outputFromProcessUom: "", | outputFromProcessUom: "", | ||||
| defectQty: 0, | defectQty: 0, | ||||
| defectUom: "", | defectUom: "", | ||||
| defect2Qty: 0, | |||||
| defect2Uom: "", | |||||
| defect3Qty: 0, | |||||
| defect3Uom: "", | |||||
| defectDescription: "", | |||||
| defectDescription2: "", | |||||
| defectDescription3: "", | |||||
| scrapQty: 0, | scrapQty: 0, | ||||
| scrapUom: "", | scrapUom: "", | ||||
| byproductName: "", | byproductName: "", | ||||
| @@ -75,16 +82,8 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||||
| try { | try { | ||||
| await updateProductProcessLineQty({ | await updateProductProcessLineQty({ | ||||
| ...outputData, | |||||
| productProcessLineId: lineDetail.id || 0, | 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"); | console.log("Output data submitted successfully"); | ||||
| @@ -65,7 +65,7 @@ export default function NextAppDirEmotionCacheProvider( | |||||
| inserted.forEach(({ name, isGlobal }) => { | inserted.forEach(({ name, isGlobal }) => { | ||||
| const style = registry.cache.inserted[name]; | const style = registry.cache.inserted[name]; | ||||
| if (typeof style !== "boolean") { | |||||
| if (typeof style !== "boolean" && style != null) { | |||||
| if (isGlobal) { | if (isGlobal) { | ||||
| globals.push({ name, style }); | globals.push({ name, style }); | ||||
| } else { | } else { | ||||
| @@ -24,5 +24,5 @@ | |||||
| "baseUrl": "." | "baseUrl": "." | ||||
| }, | }, | ||||
| "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], | "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"] | |||||
| } | } | ||||