| @@ -87,7 +87,7 @@ export default function ProductionSchedulePage() { | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule`, { | const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule`, { | ||||
| method: 'POST', | |||||
| method: 'GET', | |||||
| headers: { 'Authorization': `Bearer ${token}` } | headers: { 'Authorization': `Bearer ${token}` } | ||||
| }); | }); | ||||
| if (response.ok) { | if (response.ok) { | ||||
| @@ -104,7 +104,7 @@ export default function ProductionSchedulePage() { | |||||
| const handleExport = async () => { | const handleExport = async () => { | ||||
| const token = localStorage.getItem("accessToken"); | const token = localStorage.getItem("accessToken"); | ||||
| try { | try { | ||||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/export-prod-schedule`, { | |||||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule`, { | |||||
| method: 'POST', | method: 'POST', | ||||
| headers: { 'Authorization': `Bearer ${token}` } | headers: { 'Authorization': `Bearer ${token}` } | ||||
| }); | }); | ||||
| @@ -0,0 +1,19 @@ | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import { Stack, Typography, Link } from "@mui/material"; | |||||
| import NextLink from "next/link"; | |||||
| export default async function NotFound() { | |||||
| const { t } = await getServerI18n("qcItem", "common"); | |||||
| return ( | |||||
| <Stack spacing={2}> | |||||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||||
| <Typography variant="body1"> | |||||
| {t("The create qc item page was not found!")} | |||||
| </Typography> | |||||
| <Link href="/qcItems" component={NextLink} variant="body2"> | |||||
| {t("Return to all qc items")} | |||||
| </Link> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n, I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { preloadQcItem } from "@/app/api/settings/qcItem"; | |||||
| import QcItemSave from "@/components/QcItemSave"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Qc Item", | |||||
| }; | |||||
| const qcItem: React.FC = async () => { | |||||
| const { t } = await getServerI18n("qcItem"); | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Create Qc Item")} | |||||
| </Typography> | |||||
| <I18nProvider namespaces={["qcItem"]}> | |||||
| <QcItemSave /> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default qcItem; | |||||
| @@ -0,0 +1,19 @@ | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import { Stack, Typography, Link } from "@mui/material"; | |||||
| import NextLink from "next/link"; | |||||
| export default async function NotFound() { | |||||
| const { t } = await getServerI18n("qcItem", "common"); | |||||
| return ( | |||||
| <Stack spacing={2}> | |||||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||||
| <Typography variant="body1"> | |||||
| {t("The edit qc item page was not found!")} | |||||
| </Typography> | |||||
| <Link href="/settings/qcItems" component={NextLink} variant="body2"> | |||||
| {t("Return to all qc items")} | |||||
| </Link> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,53 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n, I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { fetchQcItemDetails, preloadQcItem } from "@/app/api/settings/qcItem"; | |||||
| import QcItemSave from "@/components/QcItemSave"; | |||||
| import { isArray } from "lodash"; | |||||
| import { notFound } from "next/navigation"; | |||||
| import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Qc Item", | |||||
| }; | |||||
| interface Props { | |||||
| searchParams: { [key: string]: string | string[] | undefined }; | |||||
| } | |||||
| const qcItem: React.FC<Props> = async ({ searchParams }) => { | |||||
| const { t } = await getServerI18n("qcItem"); | |||||
| const id = searchParams["id"]; | |||||
| if (!id || isArray(id)) { | |||||
| notFound(); | |||||
| } | |||||
| try { | |||||
| console.log("first"); | |||||
| await fetchQcItemDetails(id); | |||||
| console.log("firsts"); | |||||
| } catch (e) { | |||||
| if ( | |||||
| e instanceof ServerFetchError && | |||||
| (e.response?.status === 404 || e.response?.status === 400) | |||||
| ) { | |||||
| console.log(e); | |||||
| notFound(); | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Edit Qc Item")} | |||||
| </Typography> | |||||
| <I18nProvider namespaces={["qcItem"]}> | |||||
| <QcItemSave id={id} /> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default qcItem; | |||||
| @@ -0,0 +1,48 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n, I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { Button, Link, Stack } from "@mui/material"; | |||||
| import { Add } from "@mui/icons-material"; | |||||
| import { Suspense } from "react"; | |||||
| import { preloadQcItem } from "@/app/api/settings/qcItem"; | |||||
| import QcItemSearch from "@/components/QcItemSearch"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Qc Item", | |||||
| }; | |||||
| const qcItem: React.FC = async () => { | |||||
| const { t } = await getServerI18n("qcItem"); | |||||
| preloadQcItem(); | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Qc Item")} | |||||
| </Typography> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="qcItem/create" | |||||
| > | |||||
| {t("Create Qc Item")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <Suspense fallback={<QcItemSearch.Loading />}> | |||||
| <I18nProvider namespaces={["common", "qcItem"]}> | |||||
| <QcItemSearch /> | |||||
| </I18nProvider> | |||||
| </Suspense> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default qcItem; | |||||
| @@ -0,0 +1,47 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n, I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { Stack } from "@mui/material"; | |||||
| import { Suspense } from "react"; | |||||
| import QcItemAllTabs from "@/components/QcItemAll/QcItemAllTabs"; | |||||
| import Tab0ItemQcCategoryMapping from "@/components/QcItemAll/Tab0ItemQcCategoryMapping"; | |||||
| import Tab1QcCategoryQcItemMapping from "@/components/QcItemAll/Tab1QcCategoryQcItemMapping"; | |||||
| import Tab2QcCategoryManagement from "@/components/QcItemAll/Tab2QcCategoryManagement"; | |||||
| import Tab3QcItemManagement from "@/components/QcItemAll/Tab3QcItemManagement"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Qc Item All", | |||||
| }; | |||||
| const qcItemAll: React.FC = async () => { | |||||
| const { t } = await getServerI18n("qcItemAll"); | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| sx={{ mb: 3 }} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Qc Item All")} | |||||
| </Typography> | |||||
| </Stack> | |||||
| <Suspense fallback={<div>Loading...</div>}> | |||||
| <I18nProvider namespaces={["common", "qcItemAll", "qcCategory", "qcItem"]}> | |||||
| <QcItemAllTabs | |||||
| tab0Content={<Tab0ItemQcCategoryMapping />} | |||||
| tab1Content={<Tab1QcCategoryQcItemMapping />} | |||||
| tab2Content={<Tab2QcCategoryManagement />} | |||||
| tab3Content={<Tab3QcItemManagement />} | |||||
| /> | |||||
| </I18nProvider> | |||||
| </Suspense> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default qcItemAll; | |||||
| @@ -4,13 +4,47 @@ import React, { useState } from "react"; | |||||
| import { | import { | ||||
| Box, Grid, Paper, Typography, Button, Dialog, DialogTitle, | Box, Grid, Paper, Typography, Button, Dialog, DialogTitle, | ||||
| DialogContent, DialogActions, TextField, Stack, Table, | DialogContent, DialogActions, TextField, Stack, Table, | ||||
| TableBody, TableCell, TableContainer, TableHead, TableRow | |||||
| TableBody, TableCell, TableContainer, TableHead, TableRow, | |||||
| Tabs, Tab // ← Added for tabs | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| 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"; | ||||
| // Simple TabPanel component for conditional rendering | |||||
| interface TabPanelProps { | |||||
| children?: React.ReactNode; | |||||
| index: number; | |||||
| value: number; | |||||
| } | |||||
| function TabPanel(props: TabPanelProps) { | |||||
| const { children, value, index, ...other } = props; | |||||
| return ( | |||||
| <div | |||||
| role="tabpanel" | |||||
| hidden={value !== index} | |||||
| id={`simple-tabpanel-${index}`} | |||||
| aria-labelledby={`simple-tab-${index}`} | |||||
| {...other} | |||||
| > | |||||
| {value === index && ( | |||||
| <Box sx={{ p: 3 }}> | |||||
| {children} | |||||
| </Box> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default function TestingPage() { | export default function TestingPage() { | ||||
| // Tab state | |||||
| const [tabValue, setTabValue] = useState(0); | |||||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | |||||
| setTabValue(newValue); | |||||
| }; | |||||
| // --- 1. TSC Section States --- | // --- 1. TSC Section States --- | ||||
| const [tscConfig, setTscConfig] = useState({ ip: '192.168.1.100', port: '9100' }); | const [tscConfig, setTscConfig] = useState({ ip: '192.168.1.100', port: '9100' }); | ||||
| const [tscItems, setTscItems] = useState([ | const [tscItems, setTscItems] = useState([ | ||||
| @@ -35,10 +69,22 @@ export default function TestingPage() { | |||||
| }); | }); | ||||
| // --- 4. Laser Section States --- | // --- 4. Laser Section States --- | ||||
| const [laserConfig, setLaserConfig] = useState({ ip: '192.168.1.102', port: '8080' }); | |||||
| const [laserItems, setLaserItems] = useState([ | |||||
| { id: 1, templateId: 'JOB_001', lotNo: 'L-LASER-01', expiryDate: '2025-12-31', power: '50' }, | |||||
| ]); | |||||
| const [laserConfig, setLaserConfig] = useState({ ip: '192.168.1.102', port: '8080' }); | |||||
| const [laserItems, setLaserItems] = useState([ | |||||
| { id: 1, templateId: 'JOB_001', lotNo: 'L-LASER-01', expiryDate: '2025-12-31', power: '50' }, | |||||
| ]); | |||||
| // --- 5. HANS600S-M Section States --- | |||||
| const [hansConfig, setHansConfig] = useState({ ip: '192.168.76.10', port: '45678' }); | |||||
| const [hansItems, setHansItems] = useState([ | |||||
| { | |||||
| id: 1, | |||||
| textChannel3: 'SN-HANS-001-20260117', // channel 3 (e.g. serial / text1) | |||||
| textChannel4: 'BATCH-HK-TEST-OK', // channel 4 (e.g. batch / text2) | |||||
| text3ObjectName: 'Text3', // EZCAD object name for channel 3 | |||||
| text4ObjectName: 'Text4' // EZCAD object name for channel 4 | |||||
| }, | |||||
| ]); | |||||
| // Generic handler for inline table edits | // Generic handler for inline table edits | ||||
| const handleItemChange = (setter: any, id: number, field: string, value: string) => { | const handleItemChange = (setter: any, id: number, field: string, value: string) => { | ||||
| @@ -105,6 +151,7 @@ const [laserItems, setLaserItems] = useState([ | |||||
| } catch (e) { console.error("OnPack Error:", e); } | } catch (e) { console.error("OnPack Error:", e); } | ||||
| }; | }; | ||||
| // Laser Print (Section 4 - original) | |||||
| const handleLaserPrint = async (row: any) => { | const handleLaserPrint = async (row: any) => { | ||||
| const token = localStorage.getItem("accessToken"); | const token = localStorage.getItem("accessToken"); | ||||
| const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; | const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; | ||||
| @@ -122,7 +169,6 @@ const [laserItems, setLaserItems] = useState([ | |||||
| const token = localStorage.getItem("accessToken"); | 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 { | ||||
| // We'll create this endpoint in the backend next | |||||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { | const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { | ||||
| method: 'POST', | method: 'POST', | ||||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | ||||
| @@ -132,24 +178,58 @@ const [laserItems, setLaserItems] = useState([ | |||||
| } catch (e) { console.error("Preview Error:", e); } | } 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, | |||||
| textChannel3: row.textChannel3, | |||||
| textChannel4: row.textChannel4, | |||||
| text3ObjectName: row.text3ObjectName, | |||||
| text4ObjectName: row.text4ObjectName | |||||
| }; | |||||
| try { | |||||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { | |||||
| method: 'POST', | |||||
| headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, | |||||
| body: JSON.stringify(payload) | |||||
| }); | |||||
| const result = await response.text(); | |||||
| if (response.ok) { | |||||
| alert(`HANS600S-M Mark Success: ${result}`); | |||||
| } else { | |||||
| alert(`HANS600S-M Failed: ${result}`); | |||||
| } | |||||
| } catch (e) { | |||||
| console.error("HANS600S-M Error:", e); | |||||
| alert("HANS600S-M Connection Error"); | |||||
| } | |||||
| }; | |||||
| // Layout Helper | // Layout Helper | ||||
| const Section = ({ title, children }: { title: string, children?: React.ReactNode }) => ( | const Section = ({ title, children }: { title: string, children?: React.ReactNode }) => ( | ||||
| <Grid item xs={12} md={6}> | |||||
| <Paper sx={{ p: 3, minHeight: '450px', display: 'flex', flexDirection: 'column' }}> | |||||
| <Typography variant="h5" gutterBottom color="primary" sx={{ borderBottom: '2px solid #f0f0f0', pb: 1, mb: 2 }}> | |||||
| {title} | |||||
| </Typography> | |||||
| {children || <Typography color="textSecondary" sx={{ m: 'auto' }}>Waiting for implementation...</Typography>} | |||||
| </Paper> | |||||
| </Grid> | |||||
| <Paper sx={{ p: 3, minHeight: '450px', display: 'flex', flexDirection: 'column' }}> | |||||
| <Typography variant="h5" gutterBottom color="primary" sx={{ borderBottom: '2px solid #f0f0f0', pb: 1, mb: 2 }}> | |||||
| {title} | |||||
| </Typography> | |||||
| {children || <Typography color="textSecondary" sx={{ m: 'auto' }}>Waiting for implementation...</Typography>} | |||||
| </Paper> | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| <Box sx={{ p: 4 }}> | <Box sx={{ p: 4 }}> | ||||
| <Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Printer Testing Dashboard</Typography> | |||||
| <Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Printer Testing</Typography> | |||||
| <Grid container spacing={3}> | |||||
| {/* 1. TSC Section */} | |||||
| <Tabs value={tabValue} onChange={handleTabChange} aria-label="printer sections tabs" centered variant="fullWidth"> | |||||
| <Tab label="1. TSC" /> | |||||
| <Tab label="2. DataFlex" /> | |||||
| <Tab label="3. OnPack" /> | |||||
| <Tab label="4. Laser" /> | |||||
| <Tab label="5. HANS600S-M" /> | |||||
| </Tabs> | |||||
| <TabPanel value={tabValue} index={0}> | |||||
| <Section title="1. TSC"> | <Section title="1. TSC"> | ||||
| <Stack direction="row" spacing={2} sx={{ mb: 2 }}> | <Stack direction="row" spacing={2} sx={{ mb: 2 }}> | ||||
| <TextField size="small" label="Printer IP" value={tscConfig.ip} onChange={e => setTscConfig({...tscConfig, ip: e.target.value})} /> | <TextField size="small" label="Printer IP" value={tscConfig.ip} onChange={e => setTscConfig({...tscConfig, ip: e.target.value})} /> | ||||
| @@ -181,8 +261,9 @@ const [laserItems, setLaserItems] = useState([ | |||||
| </Table> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| </Section> | </Section> | ||||
| </TabPanel> | |||||
| {/* 2. DataFlex Section */} | |||||
| <TabPanel value={tabValue} index={1}> | |||||
| <Section title="2. DataFlex"> | <Section title="2. DataFlex"> | ||||
| <Stack direction="row" spacing={2} sx={{ mb: 2 }}> | <Stack direction="row" spacing={2} sx={{ mb: 2 }}> | ||||
| <TextField size="small" label="Printer IP" value={dfConfig.ip} onChange={e => setDfConfig({...dfConfig, ip: e.target.value})} /> | <TextField size="small" label="Printer IP" value={dfConfig.ip} onChange={e => setDfConfig({...dfConfig, ip: e.target.value})} /> | ||||
| @@ -214,8 +295,9 @@ const [laserItems, setLaserItems] = useState([ | |||||
| </Table> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| </Section> | </Section> | ||||
| </TabPanel> | |||||
| {/* 3. OnPack Section */} | |||||
| <TabPanel value={tabValue} index={2}> | |||||
| <Section title="3. OnPack"> | <Section title="3. OnPack"> | ||||
| <Box sx={{ m: 'auto', textAlign: 'center' }}> | <Box sx={{ m: 'auto', textAlign: 'center' }}> | ||||
| <Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}> | <Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}> | ||||
| @@ -226,8 +308,9 @@ const [laserItems, setLaserItems] = useState([ | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| </Section> | </Section> | ||||
| </TabPanel> | |||||
| {/* 4. Laser Section (HANS600S-M) */} | |||||
| <TabPanel value={tabValue} index={3}> | |||||
| <Section title="4. Laser"> | <Section title="4. Laser"> | ||||
| <Stack direction="row" spacing={2} sx={{ mb: 2 }}> | <Stack direction="row" spacing={2} sx={{ mb: 2 }}> | ||||
| <TextField size="small" label="Laser IP" value={laserConfig.ip} onChange={e => setLaserConfig({...laserConfig, ip: e.target.value})} /> | <TextField size="small" label="Laser IP" value={laserConfig.ip} onChange={e => setLaserConfig({...laserConfig, ip: e.target.value})} /> | ||||
| @@ -283,7 +366,94 @@ const [laserItems, setLaserItems] = useState([ | |||||
| Note: HANS Laser requires pre-saved templates on the controller. | Note: HANS Laser requires pre-saved templates on the controller. | ||||
| </Typography> | </Typography> | ||||
| </Section> | </Section> | ||||
| </Grid> | |||||
| </TabPanel> | |||||
| <TabPanel value={tabValue} index={4}> | |||||
| <Section title="5. HANS600S-M"> | |||||
| <Stack direction="row" spacing={2} sx={{ mb: 2 }}> | |||||
| <TextField | |||||
| size="small" | |||||
| label="Laser IP" | |||||
| value={hansConfig.ip} | |||||
| onChange={e => setHansConfig({...hansConfig, ip: e.target.value})} | |||||
| /> | |||||
| <TextField | |||||
| size="small" | |||||
| label="Port" | |||||
| value={hansConfig.port} | |||||
| onChange={e => setHansConfig({...hansConfig, port: e.target.value})} | |||||
| /> | |||||
| <Router color="action" sx={{ ml: 'auto' }} /> | |||||
| </Stack> | |||||
| <TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}> | |||||
| <Table size="small" stickyHeader> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>Ch3 Text (SN)</TableCell> | |||||
| <TableCell>Ch4 Text (Batch)</TableCell> | |||||
| <TableCell>Obj3 Name</TableCell> | |||||
| <TableCell>Obj4 Name</TableCell> | |||||
| <TableCell align="center">Action</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {hansItems.map(row => ( | |||||
| <TableRow key={row.id}> | |||||
| <TableCell> | |||||
| <TextField | |||||
| variant="standard" | |||||
| value={row.textChannel3} | |||||
| onChange={e => handleItemChange(setHansItems, row.id, 'textChannel3', e.target.value)} | |||||
| sx={{ minWidth: 180 }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| variant="standard" | |||||
| value={row.textChannel4} | |||||
| onChange={e => handleItemChange(setHansItems, row.id, 'textChannel4', e.target.value)} | |||||
| sx={{ minWidth: 140 }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| variant="standard" | |||||
| value={row.text3ObjectName} | |||||
| onChange={e => handleItemChange(setHansItems, row.id, 'text3ObjectName', e.target.value)} | |||||
| size="small" | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| variant="standard" | |||||
| value={row.text4ObjectName} | |||||
| onChange={e => handleItemChange(setHansItems, row.id, 'text4ObjectName', e.target.value)} | |||||
| size="small" | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="error" | |||||
| size="small" | |||||
| startIcon={<Print />} | |||||
| onClick={() => handleHansPrint(row)} | |||||
| sx={{ minWidth: 80 }} | |||||
| > | |||||
| TCP Mark | |||||
| </Button> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ))} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <Typography variant="caption" sx={{ mt: 2, display: 'block', color: 'text.secondary', fontSize: '0.75rem' }}> | |||||
| TCP Push to EZCAD3 (Ch3/Ch4 via E3_SetTextObject) | IP:192.168.76.10:45678 | Backend: /print-laser-tcp | |||||
| </Typography> | |||||
| </Section> | |||||
| </TabPanel> | |||||
| {/* Dialog for OnPack */} | {/* Dialog for OnPack */} | ||||
| <Dialog open={isPrinterModalOpen} onClose={() => setIsPrinterModalOpen(false)} fullWidth maxWidth="sm"> | <Dialog open={isPrinterModalOpen} onClose={() => setIsPrinterModalOpen(false)} fullWidth maxWidth="sm"> | ||||
| @@ -118,4 +118,27 @@ export const fetchBagLotLines = cache(async (bagId: number) => | |||||
| export const fetchBagConsumptions = cache(async (bagLotLineId: number) => | export const fetchBagConsumptions = cache(async (bagLotLineId: number) => | ||||
| serverFetchJson<BagConsumptionResponse[]>(`${BASE_API_URL}/bag/lot-lines/${bagLotLineId}/consumptions`, { method: "GET" }) | serverFetchJson<BagConsumptionResponse[]>(`${BASE_API_URL}/bag/lot-lines/${bagLotLineId}/consumptions`, { method: "GET" }) | ||||
| ); | |||||
| ); | |||||
| export interface SoftDeleteBagResponse { | |||||
| id: number | null; | |||||
| code: string | null; | |||||
| name: string | null; | |||||
| type: string | null; | |||||
| message: string | null; | |||||
| errorPosition: string | null; | |||||
| entity: any | null; | |||||
| } | |||||
| export const softDeleteBagByItemId = async (itemId: number): Promise<SoftDeleteBagResponse> => { | |||||
| const response = await serverFetchJson<SoftDeleteBagResponse>( | |||||
| `${BASE_API_URL}/bag/by-item/${itemId}/soft-delete`, | |||||
| { | |||||
| method: "PUT", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| } | |||||
| ); | |||||
| revalidateTag("bagInfo"); | |||||
| revalidateTag("bags"); | |||||
| return response; | |||||
| }; | |||||
| @@ -197,9 +197,12 @@ export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: | |||||
| ); | ); | ||||
| }); | }); | ||||
| export const fetchTruckScheduleDashboard = cache(async () => { | |||||
| export const fetchTruckScheduleDashboard = cache(async (date?: string) => { | |||||
| const url = date | |||||
| ? `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard?date=${date}` | |||||
| : `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`; | |||||
| return await serverFetchJson<TruckScheduleDashboardItem[]>( | return await serverFetchJson<TruckScheduleDashboardItem[]>( | ||||
| `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`, | |||||
| url, | |||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| } | } | ||||
| @@ -5,8 +5,8 @@ import { | |||||
| type TruckScheduleDashboardItem | type TruckScheduleDashboardItem | ||||
| } from "./actions"; | } from "./actions"; | ||||
| export const fetchTruckScheduleDashboardClient = async (): Promise<TruckScheduleDashboardItem[]> => { | |||||
| return await fetchTruckScheduleDashboard(); | |||||
| export const fetchTruckScheduleDashboardClient = async (date?: string): Promise<TruckScheduleDashboardItem[]> => { | |||||
| return await fetchTruckScheduleDashboard(date); | |||||
| }; | }; | ||||
| export type { TruckScheduleDashboardItem }; | export type { TruckScheduleDashboardItem }; | ||||
| @@ -152,3 +152,33 @@ export const updateInventoryLotLineQuantities = async (data: { | |||||
| revalidateTag("pickorder"); | revalidateTag("pickorder"); | ||||
| return result; | return result; | ||||
| }; | }; | ||||
| //STOCK TRANSFER | |||||
| export interface CreateStockTransferRequest { | |||||
| inventoryLotLineId: number; | |||||
| transferredQty: number; | |||||
| warehouseId: number; | |||||
| } | |||||
| export interface MessageResponse { | |||||
| id: number | null; | |||||
| name: string; | |||||
| code: string; | |||||
| type: string; | |||||
| message: string | null; | |||||
| errorPosition: string | null; | |||||
| } | |||||
| export const createStockTransfer = async (data: CreateStockTransferRequest) => { | |||||
| const result = await serverFetchJson<MessageResponse>( | |||||
| `${BASE_API_URL}/stockTransferRecord/create`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| revalidateTag("inventoryLotLines"); | |||||
| revalidateTag("inventories"); | |||||
| return result; | |||||
| }; | |||||
| @@ -45,6 +45,7 @@ export type CreateItemInputs = { | |||||
| isEgg?: boolean | undefined; | isEgg?: boolean | undefined; | ||||
| isFee?: boolean | undefined; | isFee?: boolean | undefined; | ||||
| isBag?: boolean | undefined; | isBag?: boolean | undefined; | ||||
| qcType?: string | undefined; | |||||
| }; | }; | ||||
| export const saveItem = async (data: CreateItemInputs) => { | export const saveItem = async (data: CreateItemInputs) => { | ||||
| @@ -67,6 +67,7 @@ export type ItemsResult = { | |||||
| export type Result = { | export type Result = { | ||||
| item: ItemsResult; | item: ItemsResult; | ||||
| qcChecks: ItemQc[]; | qcChecks: ItemQc[]; | ||||
| qcType?: string; | |||||
| }; | }; | ||||
| export const fetchAllItems = cache(async () => { | export const fetchAllItems = cache(async () => { | ||||
| return serverFetchJson<ItemsResult[]>(`${BASE_API_URL}/items`, { | return serverFetchJson<ItemsResult[]>(`${BASE_API_URL}/items`, { | ||||
| @@ -8,11 +8,15 @@ import { BASE_API_URL } from "../../../../config/api"; | |||||
| export interface M18ImportPoForm { | export interface M18ImportPoForm { | ||||
| modifiedDateFrom: string; | modifiedDateFrom: string; | ||||
| modifiedDateTo: string; | modifiedDateTo: string; | ||||
| dDateFrom: string; | |||||
| dDateTo: string; | |||||
| } | } | ||||
| export interface M18ImportDoForm { | export interface M18ImportDoForm { | ||||
| modifiedDateFrom: string; | modifiedDateFrom: string; | ||||
| modifiedDateTo: string; | modifiedDateTo: string; | ||||
| dDateFrom: string; | |||||
| dDateTo: string; | |||||
| } | } | ||||
| export interface M18ImportPqForm { | export interface M18ImportPqForm { | ||||
| @@ -49,19 +53,67 @@ export const testM18ImportDo = async (data: M18ImportDoForm) => { | |||||
| }; | }; | ||||
| export const testM18ImportPq = async (data: M18ImportPqForm) => { | export const testM18ImportPq = async (data: M18ImportPqForm) => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| return serverFetchWithNoContent(`${BASE_API_URL}/m18/pq`, { | return serverFetchWithNoContent(`${BASE_API_URL}/m18/pq`, { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| headers: { "Content-Type": "application/json" }, | |||||
| headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, | |||||
| }); | }); | ||||
| }; | }; | ||||
| export const testM18ImportMasterData = async ( | export const testM18ImportMasterData = async ( | ||||
| data: M18ImportMasterDataForm, | data: M18ImportMasterDataForm, | ||||
| ) => { | ) => { | ||||
| const token = localStorage.getItem("accessToken"); | |||||
| return serverFetchWithNoContent(`${BASE_API_URL}/m18/master-data`, { | return serverFetchWithNoContent(`${BASE_API_URL}/m18/master-data`, { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| headers: { "Content-Type": "application/json" }, | |||||
| headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, | |||||
| }); | }); | ||||
| }; | }; | ||||
| export const triggerScheduler = async (type: 'po' | 'do1' | 'do2' | 'master-data' | 'refresh-cron') => { | |||||
| try { | |||||
| // IMPORTANT: 'refresh-cron' is a direct endpoint /api/scheduler/refresh-cron | |||||
| // Others are /api/scheduler/trigger/{type} | |||||
| const path = type === 'refresh-cron' | |||||
| ? 'refresh-cron' | |||||
| : `trigger/${type}`; | |||||
| const url = `${BASE_API_URL}/scheduler/${path}`; | |||||
| console.log("Fetching URL:", url); | |||||
| const response = await serverFetchWithNoContent(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); | |||||
| return null; | |||||
| } | |||||
| }; | |||||
| 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`, { | |||||
| 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); | |||||
| return "Refresh failed. Check server logs."; | |||||
| } | |||||
| }; | |||||
| @@ -0,0 +1,28 @@ | |||||
| "use client"; | |||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||||
| import { QcItemInfo } from "./index"; | |||||
| export const fetchQcItemsByCategoryId = async (categoryId: number): Promise<QcItemInfo[]> => { | |||||
| const token = localStorage.getItem("accessToken"); | |||||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/qcCategories/${categoryId}/items`, { | |||||
| method: "GET", | |||||
| headers: { | |||||
| "Content-Type": "application/json", | |||||
| ...(token && { Authorization: `Bearer ${token}` }), | |||||
| }, | |||||
| }); | |||||
| if (!response.ok) { | |||||
| if (response.status === 401) { | |||||
| throw new Error("Unauthorized: Please log in again"); | |||||
| } | |||||
| throw new Error(`Failed to fetch QC items: ${response.status} ${response.statusText}`); | |||||
| } | |||||
| return response.json(); | |||||
| }; | |||||
| @@ -17,6 +17,15 @@ export interface QcCategoryCombo { | |||||
| label: string; | label: string; | ||||
| } | } | ||||
| export interface QcItemInfo { | |||||
| id: number; | |||||
| qcItemId: number; | |||||
| code: string; | |||||
| name?: string; | |||||
| order: number; | |||||
| description?: string; | |||||
| } | |||||
| export const preloadQcCategory = () => { | export const preloadQcCategory = () => { | ||||
| fetchQcCategories(); | fetchQcCategories(); | ||||
| }; | }; | ||||
| @@ -0,0 +1,265 @@ | |||||
| "use server"; | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { revalidatePath, revalidateTag } from "next/cache"; | |||||
| import { | |||||
| ItemQcCategoryMappingInfo, | |||||
| QcItemInfo, | |||||
| DeleteResponse, | |||||
| QcCategoryResult, | |||||
| ItemsResult, | |||||
| QcItemResult, | |||||
| } from "."; | |||||
| export interface SaveQcCategoryInputs { | |||||
| id?: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| } | |||||
| export interface SaveQcCategoryResponse { | |||||
| id?: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| errors: Record<string, string> | null; | |||||
| } | |||||
| export interface SaveQcItemInputs { | |||||
| id?: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| } | |||||
| export interface SaveQcItemResponse { | |||||
| id?: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| errors: Record<string, string> | null; | |||||
| } | |||||
| // Item and QcCategory mapping | |||||
| export const getItemQcCategoryMappings = async ( | |||||
| qcCategoryId?: number, | |||||
| itemId?: number | |||||
| ): Promise<ItemQcCategoryMappingInfo[]> => { | |||||
| const params = new URLSearchParams(); | |||||
| if (qcCategoryId) params.append("qcCategoryId", qcCategoryId.toString()); | |||||
| if (itemId) params.append("itemId", itemId.toString()); | |||||
| return serverFetchJson<ItemQcCategoryMappingInfo[]>( | |||||
| `${BASE_API_URL}/qcItemAll/itemMappings?${params.toString()}` | |||||
| ); | |||||
| }; | |||||
| export const saveItemQcCategoryMapping = async ( | |||||
| itemId: number, | |||||
| qcCategoryId: number, | |||||
| type: string | |||||
| ): Promise<ItemQcCategoryMappingInfo> => { | |||||
| const params = new URLSearchParams(); | |||||
| params.append("itemId", itemId.toString()); | |||||
| params.append("qcCategoryId", qcCategoryId.toString()); | |||||
| params.append("type", type); | |||||
| const response = await serverFetchJson<ItemQcCategoryMappingInfo>( | |||||
| `${BASE_API_URL}/qcItemAll/itemMapping?${params.toString()}`, | |||||
| { | |||||
| method: "POST", | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcItemAll"); | |||||
| return response; | |||||
| }; | |||||
| export const deleteItemQcCategoryMapping = async ( | |||||
| mappingId: number | |||||
| ): Promise<void> => { | |||||
| await serverFetchJson<void>( | |||||
| `${BASE_API_URL}/qcItemAll/itemMapping/${mappingId}`, | |||||
| { | |||||
| method: "DELETE", | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcItemAll"); | |||||
| }; | |||||
| // QcCategory and QcItem mapping | |||||
| export const getQcCategoryQcItemMappings = async ( | |||||
| qcCategoryId: number | |||||
| ): Promise<QcItemInfo[]> => { | |||||
| return serverFetchJson<QcItemInfo[]>( | |||||
| `${BASE_API_URL}/qcItemAll/qcItemMappings/${qcCategoryId}` | |||||
| ); | |||||
| }; | |||||
| export const saveQcCategoryQcItemMapping = async ( | |||||
| qcCategoryId: number, | |||||
| qcItemId: number, | |||||
| order: number, | |||||
| description?: string | |||||
| ): Promise<QcItemInfo> => { | |||||
| const params = new URLSearchParams(); | |||||
| params.append("qcCategoryId", qcCategoryId.toString()); | |||||
| params.append("qcItemId", qcItemId.toString()); | |||||
| params.append("order", order.toString()); | |||||
| if (description) params.append("description", description); | |||||
| const response = await serverFetchJson<QcItemInfo>( | |||||
| `${BASE_API_URL}/qcItemAll/qcItemMapping?${params.toString()}`, | |||||
| { | |||||
| method: "POST", | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcItemAll"); | |||||
| return response; | |||||
| }; | |||||
| export const deleteQcCategoryQcItemMapping = async ( | |||||
| mappingId: number | |||||
| ): Promise<void> => { | |||||
| await serverFetchJson<void>( | |||||
| `${BASE_API_URL}/qcItemAll/qcItemMapping/${mappingId}`, | |||||
| { | |||||
| method: "DELETE", | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcItemAll"); | |||||
| }; | |||||
| // Counts | |||||
| export const getItemCountByQcCategory = async ( | |||||
| qcCategoryId: number | |||||
| ): Promise<number> => { | |||||
| return serverFetchJson<number>( | |||||
| `${BASE_API_URL}/qcItemAll/itemCount/${qcCategoryId}` | |||||
| ); | |||||
| }; | |||||
| export const getQcItemCountByQcCategory = async ( | |||||
| qcCategoryId: number | |||||
| ): Promise<number> => { | |||||
| return serverFetchJson<number>( | |||||
| `${BASE_API_URL}/qcItemAll/qcItemCount/${qcCategoryId}` | |||||
| ); | |||||
| }; | |||||
| // Validation | |||||
| export const canDeleteQcCategory = async (id: number): Promise<boolean> => { | |||||
| return serverFetchJson<boolean>( | |||||
| `${BASE_API_URL}/qcItemAll/canDeleteQcCategory/${id}` | |||||
| ); | |||||
| }; | |||||
| export const canDeleteQcItem = async (id: number): Promise<boolean> => { | |||||
| return serverFetchJson<boolean>( | |||||
| `${BASE_API_URL}/qcItemAll/canDeleteQcItem/${id}` | |||||
| ); | |||||
| }; | |||||
| // Save and delete with validation | |||||
| export const saveQcCategoryWithValidation = async ( | |||||
| data: SaveQcCategoryInputs | |||||
| ): Promise<SaveQcCategoryResponse> => { | |||||
| const response = await serverFetchJson<SaveQcCategoryResponse>( | |||||
| `${BASE_API_URL}/qcItemAll/saveQcCategory`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcCategories"); | |||||
| revalidateTag("qcItemAll"); | |||||
| return response; | |||||
| }; | |||||
| export const deleteQcCategoryWithValidation = async ( | |||||
| id: number | |||||
| ): Promise<DeleteResponse> => { | |||||
| const response = await serverFetchJson<DeleteResponse>( | |||||
| `${BASE_API_URL}/qcItemAll/deleteQcCategory/${id}`, | |||||
| { | |||||
| method: "DELETE", | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcCategories"); | |||||
| revalidateTag("qcItemAll"); | |||||
| revalidatePath("/(main)/settings/qcItemAll"); | |||||
| return response; | |||||
| }; | |||||
| export const saveQcItemWithValidation = async ( | |||||
| data: SaveQcItemInputs | |||||
| ): Promise<SaveQcItemResponse> => { | |||||
| const response = await serverFetchJson<SaveQcItemResponse>( | |||||
| `${BASE_API_URL}/qcItemAll/saveQcItem`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcItems"); | |||||
| revalidateTag("qcItemAll"); | |||||
| return response; | |||||
| }; | |||||
| export const deleteQcItemWithValidation = async ( | |||||
| id: number | |||||
| ): Promise<DeleteResponse> => { | |||||
| const response = await serverFetchJson<DeleteResponse>( | |||||
| `${BASE_API_URL}/qcItemAll/deleteQcItem/${id}`, | |||||
| { | |||||
| method: "DELETE", | |||||
| } | |||||
| ); | |||||
| revalidateTag("qcItems"); | |||||
| revalidateTag("qcItemAll"); | |||||
| revalidatePath("/(main)/settings/qcItemAll"); | |||||
| return response; | |||||
| }; | |||||
| // Server actions for fetching data (to be used in client components) | |||||
| export const fetchQcCategoriesForAll = async (): Promise<QcCategoryResult[]> => { | |||||
| return serverFetchJson<QcCategoryResult[]>(`${BASE_API_URL}/qcCategories`, { | |||||
| next: { tags: ["qcCategories"] }, | |||||
| }); | |||||
| }; | |||||
| export const fetchItemsForAll = async (): Promise<ItemsResult[]> => { | |||||
| return serverFetchJson<ItemsResult[]>(`${BASE_API_URL}/items`, { | |||||
| next: { tags: ["items"] }, | |||||
| }); | |||||
| }; | |||||
| export const fetchQcItemsForAll = async (): Promise<QcItemResult[]> => { | |||||
| return serverFetchJson<QcItemResult[]>(`${BASE_API_URL}/qcItems`, { | |||||
| next: { tags: ["qcItems"] }, | |||||
| }); | |||||
| }; | |||||
| // Get item by code (for Tab 0 - validate item code input) | |||||
| export const getItemByCode = async (code: string): Promise<ItemsResult | null> => { | |||||
| try { | |||||
| return await serverFetchJson<ItemsResult>(`${BASE_API_URL}/qcItemAll/itemByCode/${encodeURIComponent(code)}`); | |||||
| } catch (error) { | |||||
| // Item not found | |||||
| return null; | |||||
| } | |||||
| }; | |||||
| @@ -0,0 +1,101 @@ | |||||
| // Type definitions that can be used in both client and server components | |||||
| export interface ItemQcCategoryMappingInfo { | |||||
| id: number; | |||||
| itemId: number; | |||||
| itemCode?: string; | |||||
| itemName?: string; | |||||
| qcCategoryId: number; | |||||
| qcCategoryCode?: string; | |||||
| qcCategoryName?: string; | |||||
| type?: string; | |||||
| } | |||||
| export interface QcItemInfo { | |||||
| id: number; | |||||
| order: number; | |||||
| qcItemId: number; | |||||
| code: string; | |||||
| name?: string; | |||||
| description?: string; | |||||
| } | |||||
| export interface DeleteResponse { | |||||
| success: boolean; | |||||
| message?: string; | |||||
| canDelete: boolean; | |||||
| } | |||||
| export interface QcCategoryWithCounts { | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| itemCount: number; | |||||
| qcItemCount: number; | |||||
| } | |||||
| export interface QcCategoryWithItemCount { | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| itemCount: number; | |||||
| } | |||||
| export interface QcCategoryWithQcItemCount { | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| qcItemCount: number; | |||||
| } | |||||
| export interface QcItemWithCounts { | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| qcCategoryCount: number; | |||||
| } | |||||
| // Type definitions that match the server-only types | |||||
| export interface QcCategoryResult { | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description?: string; | |||||
| } | |||||
| export interface QcItemResult { | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description: string; | |||||
| } | |||||
| export interface ItemsResult { | |||||
| id: string | number; | |||||
| code: string; | |||||
| name: string; | |||||
| description: string | undefined; | |||||
| remarks: string | undefined; | |||||
| shelfLife: number | undefined; | |||||
| countryOfOrigin: string | undefined; | |||||
| maxQty: number | undefined; | |||||
| type: string; | |||||
| qcChecks: any[]; | |||||
| action?: any; | |||||
| fgName?: string; | |||||
| excludeDate?: string; | |||||
| qcCategory?: QcCategoryResult; | |||||
| store_id?: string | undefined; | |||||
| warehouse?: string | undefined; | |||||
| area?: string | undefined; | |||||
| slot?: string | undefined; | |||||
| LocationCode?: string | undefined; | |||||
| locationCode?: string | undefined; | |||||
| isEgg?: boolean | undefined; | |||||
| isFee?: boolean | undefined; | |||||
| isBag?: boolean | undefined; | |||||
| } | |||||
| @@ -16,15 +16,16 @@ export interface StockIssueResult { | |||||
| storeLocation: string | null; | storeLocation: string | null; | ||||
| requiredQty: number | null; | requiredQty: number | null; | ||||
| actualPickQty: number | null; | actualPickQty: number | null; | ||||
| missQty: number; | |||||
| badItemQty: number; | |||||
| missQty: number; | |||||
| badItemQty: number; | |||||
| bookQty: number; | |||||
| issueQty: number; | |||||
| issueRemark: string | null; | issueRemark: string | null; | ||||
| pickerName: string | null; | pickerName: string | null; | ||||
| handleStatus: string; | handleStatus: string; | ||||
| handleDate: string | null; | handleDate: string | null; | ||||
| handledBy: number | null; | handledBy: number | null; | ||||
| } | } | ||||
| export interface ExpiryItemResult { | export interface ExpiryItemResult { | ||||
| id: number; | id: number; | ||||
| itemId: number; | itemId: number; | ||||
| @@ -31,4 +31,25 @@ export const exportWarehouseQrCode = async (warehouseIds: number[]): Promise<{ b | |||||
| return { blobValue, filename }; | return { blobValue, filename }; | ||||
| }; | }; | ||||
| export const fetchWarehouseListClient = async (): Promise<WarehouseResult[]> => { | |||||
| const token = localStorage.getItem("accessToken"); | |||||
| const response = await fetch(`${NEXT_PUBLIC_API_URL}/warehouse`, { | |||||
| method: "GET", | |||||
| headers: { | |||||
| "Content-Type": "application/json", | |||||
| ...(token && { Authorization: `Bearer ${token}` }), | |||||
| }, | |||||
| }); | |||||
| if (!response.ok) { | |||||
| if (response.status === 401) { | |||||
| throw new Error("Unauthorized: Please log in again"); | |||||
| } | |||||
| throw new Error(`Failed to fetch warehouse list: ${response.status} ${response.statusText}`); | |||||
| } | |||||
| return response.json(); | |||||
| }; | |||||
| //test | //test | ||||
| @@ -31,6 +31,7 @@ import { saveItemQcChecks } from "@/app/api/settings/qcCheck/actions"; | |||||
| import { useGridApiRef } from "@mui/x-data-grid"; | import { useGridApiRef } from "@mui/x-data-grid"; | ||||
| import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; | import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; | ||||
| import { WarehouseResult } from "@/app/api/warehouse"; | import { WarehouseResult } from "@/app/api/warehouse"; | ||||
| import { softDeleteBagByItemId } from "@/app/api/bag/action"; | |||||
| type Props = { | type Props = { | ||||
| isEditMode: boolean; | isEditMode: boolean; | ||||
| @@ -173,6 +174,16 @@ const CreateItem: React.FC<Props> = ({ | |||||
| ); | ); | ||||
| } else if (!Boolean(responseQ.id)) { | } else if (!Boolean(responseQ.id)) { | ||||
| } else if (Boolean(responseI.id) && Boolean(responseQ.id)) { | } else if (Boolean(responseI.id) && Boolean(responseQ.id)) { | ||||
| // If special type is not "isBag", soft-delete the bag record if it exists | |||||
| if (data.isBag !== true && data.id) { | |||||
| try { | |||||
| const itemId = typeof data.id === "string" ? parseInt(data.id) : data.id; | |||||
| await softDeleteBagByItemId(itemId); | |||||
| } catch (bagError) { | |||||
| // Log error but don't block the save operation | |||||
| console.log("Error soft-deleting bag:", bagError); | |||||
| } | |||||
| } | |||||
| router.replace(redirPath); | router.replace(redirPath); | ||||
| } | } | ||||
| } | } | ||||
| @@ -51,6 +51,7 @@ const CreateItemWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | |||||
| qcChecks: qcChecks, | qcChecks: qcChecks, | ||||
| qcChecks_active: activeRows, | qcChecks_active: activeRows, | ||||
| qcCategoryId: item.qcCategory?.id, | qcCategoryId: item.qcCategory?.id, | ||||
| qcType: result.qcType, | |||||
| store_id: item?.store_id, | store_id: item?.store_id, | ||||
| warehouse: item?.warehouse, | warehouse: item?.warehouse, | ||||
| area: item?.area, | area: item?.area, | ||||
| @@ -29,8 +29,10 @@ import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; | |||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import { CreateItemInputs } from "@/app/api/settings/item/actions"; | import { CreateItemInputs } from "@/app/api/settings/item/actions"; | ||||
| import { ItemQc } from "@/app/api/settings/item"; | import { ItemQc } from "@/app/api/settings/item"; | ||||
| import { QcCategoryCombo } from "@/app/api/settings/qcCategory"; | |||||
| import { QcCategoryCombo, QcItemInfo } from "@/app/api/settings/qcCategory"; | |||||
| import { fetchQcItemsByCategoryId } from "@/app/api/settings/qcCategory/client"; | |||||
| import { WarehouseResult } from "@/app/api/warehouse"; | import { WarehouseResult } from "@/app/api/warehouse"; | ||||
| import QcItemsList from "./QcItemsList"; | |||||
| type Props = { | type Props = { | ||||
| // isEditMode: boolean; | // isEditMode: boolean; | ||||
| // type: TypeEnum; | // type: TypeEnum; | ||||
| @@ -43,11 +45,13 @@ type Props = { | |||||
| }; | }; | ||||
| const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => { | const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => { | ||||
| const [qcItems, setQcItems] = useState<QcItemInfo[]>([]); | |||||
| const [qcItemsLoading, setQcItemsLoading] = useState(false); | |||||
| const { | const { | ||||
| t, | t, | ||||
| i18n: { language }, | i18n: { language }, | ||||
| } = useTranslation(); | |||||
| } = useTranslation("items"); | |||||
| const { | const { | ||||
| register, | register, | ||||
| @@ -121,6 +125,30 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehous | |||||
| } | } | ||||
| }, [initialDefaultValues, setValue, getValues]); | }, [initialDefaultValues, setValue, getValues]); | ||||
| // Watch qcCategoryId and fetch QC items when it changes | |||||
| const qcCategoryId = watch("qcCategoryId"); | |||||
| useEffect(() => { | |||||
| const fetchItems = async () => { | |||||
| if (qcCategoryId) { | |||||
| setQcItemsLoading(true); | |||||
| try { | |||||
| const items = await fetchQcItemsByCategoryId(qcCategoryId); | |||||
| setQcItems(items); | |||||
| } catch (error) { | |||||
| console.error("Failed to fetch QC items:", error); | |||||
| setQcItems([]); | |||||
| } finally { | |||||
| setQcItemsLoading(false); | |||||
| } | |||||
| } else { | |||||
| setQcItems([]); | |||||
| } | |||||
| }; | |||||
| fetchItems(); | |||||
| }, [qcCategoryId]); | |||||
| return ( | return ( | ||||
| <Card sx={{ display: "block" }}> | <Card sx={{ display: "block" }}> | ||||
| <CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
| @@ -216,6 +244,26 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehous | |||||
| )} | )} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| control={control} | |||||
| name="qcType" | |||||
| render={({ field }) => ( | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{t("QC Type")}</InputLabel> | |||||
| <Select | |||||
| value={field.value || ""} | |||||
| label={t("QC Type")} | |||||
| onChange={field.onChange} | |||||
| onBlur={field.onBlur} | |||||
| > | |||||
| <MenuItem value="IPQC">{t("IPQC")}</MenuItem> | |||||
| <MenuItem value="EPQC">{t("EPQC")}</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| )} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| @@ -292,6 +340,13 @@ const ProductDetails: React.FC<Props> = ({ isEditMode, qcCategoryCombo, warehous | |||||
| </RadioGroup> | </RadioGroup> | ||||
| </FormControl> | </FormControl> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | |||||
| <QcItemsList | |||||
| qcItems={qcItems} | |||||
| loading={qcItemsLoading} | |||||
| categorySelected={!!qcCategoryId} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Stack | <Stack | ||||
| direction="row" | direction="row" | ||||
| @@ -0,0 +1,200 @@ | |||||
| "use client"; | |||||
| import { QcItemInfo } from "@/app/api/settings/qcCategory"; | |||||
| import { | |||||
| Box, | |||||
| Card, | |||||
| CircularProgress, | |||||
| Divider, | |||||
| List, | |||||
| ListItem, | |||||
| Stack, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { CheckCircleOutline, FormatListNumbered } from "@mui/icons-material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| type Props = { | |||||
| qcItems: QcItemInfo[]; | |||||
| loading?: boolean; | |||||
| categorySelected?: boolean; | |||||
| }; | |||||
| const QcItemsList: React.FC<Props> = ({ | |||||
| qcItems, | |||||
| loading = false, | |||||
| categorySelected = false, | |||||
| }) => { | |||||
| const { t } = useTranslation("items"); | |||||
| // Sort items by order | |||||
| const sortedItems = [...qcItems].sort((a, b) => a.order - b.order); | |||||
| if (loading) { | |||||
| return ( | |||||
| <Box | |||||
| display="flex" | |||||
| justifyContent="center" | |||||
| alignItems="center" | |||||
| py={4} | |||||
| sx={{ | |||||
| backgroundColor: "grey.50", | |||||
| borderRadius: 2, | |||||
| border: "1px dashed", | |||||
| borderColor: "grey.300", | |||||
| }} | |||||
| > | |||||
| <CircularProgress size={24} sx={{ mr: 1.5 }} /> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Loading QC items...")} | |||||
| </Typography> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (!categorySelected) { | |||||
| return ( | |||||
| <Box | |||||
| display="flex" | |||||
| flexDirection="column" | |||||
| alignItems="center" | |||||
| py={4} | |||||
| sx={{ | |||||
| backgroundColor: "grey.50", | |||||
| borderRadius: 2, | |||||
| border: "1px dashed", | |||||
| borderColor: "grey.300", | |||||
| }} | |||||
| > | |||||
| <FormatListNumbered | |||||
| sx={{ fontSize: 40, color: "grey.400", mb: 1 }} | |||||
| /> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Select a QC template to view items")} | |||||
| </Typography> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (sortedItems.length === 0) { | |||||
| return ( | |||||
| <Box | |||||
| display="flex" | |||||
| flexDirection="column" | |||||
| alignItems="center" | |||||
| py={4} | |||||
| sx={{ | |||||
| backgroundColor: "grey.50", | |||||
| borderRadius: 2, | |||||
| border: "1px dashed", | |||||
| borderColor: "grey.300", | |||||
| }} | |||||
| > | |||||
| <CheckCircleOutline | |||||
| sx={{ fontSize: 40, color: "grey.400", mb: 1 }} | |||||
| /> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No QC items in this template")} | |||||
| </Typography> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <Card | |||||
| variant="outlined" | |||||
| sx={{ | |||||
| borderRadius: 2, | |||||
| backgroundColor: "background.paper", | |||||
| overflow: "hidden", | |||||
| }} | |||||
| > | |||||
| <Box | |||||
| sx={{ | |||||
| px: 2, | |||||
| py: 1.5, | |||||
| backgroundColor: "primary.main", | |||||
| color: "primary.contrastText", | |||||
| }} | |||||
| > | |||||
| <Stack direction="row" alignItems="center" spacing={1}> | |||||
| <FormatListNumbered fontSize="small" /> | |||||
| <Typography variant="subtitle2" fontWeight={600}> | |||||
| {t("QC Checklist")} ({sortedItems.length}) | |||||
| </Typography> | |||||
| </Stack> | |||||
| </Box> | |||||
| <List disablePadding> | |||||
| {sortedItems.map((item, index) => ( | |||||
| <Box key={item.id}> | |||||
| {index > 0 && <Divider />} | |||||
| <ListItem | |||||
| sx={{ | |||||
| py: 1.5, | |||||
| px: 2, | |||||
| "&:hover": { | |||||
| backgroundColor: "action.hover", | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Stack | |||||
| direction="row" | |||||
| spacing={2} | |||||
| alignItems="flex-start" | |||||
| width="100%" | |||||
| > | |||||
| {/* Order Number */} | |||||
| <Typography | |||||
| variant="body1" | |||||
| fontWeight={600} | |||||
| color="text.secondary" | |||||
| sx={{ minWidth: 24 }} | |||||
| > | |||||
| {item.order}. | |||||
| </Typography> | |||||
| {/* Content */} | |||||
| <Stack | |||||
| direction="row" | |||||
| alignItems="center" | |||||
| spacing={2} | |||||
| flex={1} | |||||
| minWidth={0} | |||||
| > | |||||
| <Typography | |||||
| variant="body1" | |||||
| fontWeight={500} | |||||
| sx={{ | |||||
| overflow: "hidden", | |||||
| textOverflow: "ellipsis", | |||||
| whiteSpace: "nowrap", | |||||
| flexShrink: 0, | |||||
| }} | |||||
| > | |||||
| {item.name || item.code} | |||||
| </Typography> | |||||
| {item.description && ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| color="text.secondary" | |||||
| sx={{ | |||||
| overflow: "hidden", | |||||
| textOverflow: "ellipsis", | |||||
| whiteSpace: "nowrap", | |||||
| }} | |||||
| > | |||||
| {item.description} | |||||
| </Typography> | |||||
| )} | |||||
| </Stack> | |||||
| </Stack> | |||||
| </ListItem> | |||||
| </Box> | |||||
| ))} | |||||
| </List> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default QcItemsList; | |||||
| @@ -35,6 +35,7 @@ interface CompletedTracker { | |||||
| const TruckScheduleDashboard: React.FC = () => { | const TruckScheduleDashboard: React.FC = () => { | ||||
| const { t } = useTranslation("dashboard"); | const { t } = useTranslation("dashboard"); | ||||
| const [selectedStore, setSelectedStore] = useState<string>(""); | const [selectedStore, setSelectedStore] = useState<string>(""); | ||||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | |||||
| const [data, setData] = useState<TruckScheduleDashboardItem[]>([]); | const [data, setData] = useState<TruckScheduleDashboardItem[]>([]); | ||||
| const [loading, setLoading] = useState<boolean>(true); | const [loading, setLoading] = useState<boolean>(true); | ||||
| // Initialize as null to avoid SSR/client hydration mismatch | // Initialize as null to avoid SSR/client hydration mismatch | ||||
| @@ -43,6 +44,23 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| const completedTrackerRef = useRef<Map<string, CompletedTracker>>(new Map()); | const completedTrackerRef = useRef<Map<string, CompletedTracker>>(new Map()); | ||||
| const refreshCountRef = useRef<number>(0); | const refreshCountRef = useRef<number>(0); | ||||
| // Get date label for display (e.g., "2026-01-17") | |||||
| const getDateLabel = (offset: number): string => { | |||||
| return dayjs().add(offset, 'day').format('YYYY-MM-DD'); | |||||
| }; | |||||
| // Convert date option to YYYY-MM-DD format for API | |||||
| const getDateParam = (dateOption: string): string => { | |||||
| if (dateOption === "today") { | |||||
| return dayjs().format('YYYY-MM-DD'); | |||||
| } else if (dateOption === "tomorrow") { | |||||
| return dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||||
| } else if (dateOption === "dayAfterTomorrow") { | |||||
| return dayjs().add(2, 'day').format('YYYY-MM-DD'); | |||||
| } | |||||
| return dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||||
| }; | |||||
| // Set client flag and time on mount | // Set client flag and time on mount | ||||
| useEffect(() => { | useEffect(() => { | ||||
| setIsClient(true); | setIsClient(true); | ||||
| @@ -136,7 +154,8 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| // Load data from API | // Load data from API | ||||
| const loadData = useCallback(async () => { | const loadData = useCallback(async () => { | ||||
| try { | try { | ||||
| const result = await fetchTruckScheduleDashboardClient(); | |||||
| const dateParam = getDateParam(selectedDate); | |||||
| const result = await fetchTruckScheduleDashboardClient(dateParam); | |||||
| // Update completed tracker | // Update completed tracker | ||||
| refreshCountRef.current += 1; | refreshCountRef.current += 1; | ||||
| @@ -175,7 +194,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, []); | |||||
| }, [selectedDate]); | |||||
| // Initial load and auto-refresh every 5 minutes | // Initial load and auto-refresh every 5 minutes | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -183,7 +202,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| const refreshInterval = setInterval(() => { | const refreshInterval = setInterval(() => { | ||||
| loadData(); | loadData(); | ||||
| }, 5 * 60 * 1000); // 5 minutes | |||||
| }, 0.1 * 60 * 1000); // 5 minutes | |||||
| return () => clearInterval(refreshInterval); | return () => clearInterval(refreshInterval); | ||||
| }, [loadData]); | }, [loadData]); | ||||
| @@ -256,6 +275,23 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| <MenuItem value="4/F">4/F</MenuItem> | <MenuItem value="4/F">4/F</MenuItem> | ||||
| </Select> | </Select> | ||||
| </FormControl> | </FormControl> | ||||
| <FormControl sx={{ minWidth: 200 }} size="small"> | |||||
| <InputLabel id="date-select-label" shrink={true}> | |||||
| {t("Select Date")} | |||||
| </InputLabel> | |||||
| <Select | |||||
| labelId="date-select-label" | |||||
| id="date-select" | |||||
| value={selectedDate} | |||||
| label={t("Select Date")} | |||||
| onChange={(e) => setSelectedDate(e.target.value)} | |||||
| > | |||||
| <MenuItem value="today">{t("Today")} ({getDateLabel(0)})</MenuItem> | |||||
| <MenuItem value="tomorrow">{t("Tomorrow")} ({getDateLabel(1)})</MenuItem> | |||||
| <MenuItem value="dayAfterTomorrow">{t("Day After Tomorrow")} ({getDateLabel(2)})</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| <Typography variant="body2" sx={{ alignSelf: 'center', color: 'text.secondary' }}> | <Typography variant="body2" sx={{ alignSelf: 'center', color: 'text.secondary' }}> | ||||
| {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} | {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} | ||||
| @@ -290,7 +326,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={10} align="center"> | <TableCell colSpan={10} align="center"> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No truck schedules available for today")} | |||||
| {t("No truck schedules available")} ({getDateParam(selectedDate)}) | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -15,7 +15,7 @@ import CloseIcon from "@mui/icons-material/Close"; | |||||
| import { Autocomplete } from "@mui/material"; | import { Autocomplete } from "@mui/material"; | ||||
| import { WarehouseResult } from "@/app/api/warehouse"; | import { WarehouseResult } from "@/app/api/warehouse"; | ||||
| import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; | import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; | ||||
| import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; | |||||
| import { createStockTransfer } from "@/app/api/inventory/actions"; | |||||
| interface Props { | interface Props { | ||||
| inventoryLotLines: InventoryLotLineResult[] | null; | inventoryLotLines: InventoryLotLineResult[] | null; | ||||
| @@ -31,7 +31,7 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); | const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); | ||||
| const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null); | const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null); | ||||
| const [startLocation, setStartLocation] = useState<string>(""); | const [startLocation, setStartLocation] = useState<string>(""); | ||||
| const [targetLocation, setTargetLocation] = useState<string>(""); | |||||
| const [targetLocation, setTargetLocation] = useState<number | null>(null); // Store warehouse ID instead of code | |||||
| const [targetLocationInput, setTargetLocationInput] = useState<string>(""); | const [targetLocationInput, setTargetLocationInput] = useState<string>(""); | ||||
| const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0); | const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0); | ||||
| const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]); | const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]); | ||||
| @@ -65,7 +65,7 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| setSelectedLotLine(lotLine); | setSelectedLotLine(lotLine); | ||||
| setStockTransferModalOpen(true); | setStockTransferModalOpen(true); | ||||
| setStartLocation(lotLine.warehouse.code || ""); | setStartLocation(lotLine.warehouse.code || ""); | ||||
| setTargetLocation(""); | |||||
| setTargetLocation(null); | |||||
| setTargetLocationInput(""); | setTargetLocationInput(""); | ||||
| setQtyToBeTransferred(0); | setQtyToBeTransferred(0); | ||||
| }, | }, | ||||
| @@ -188,34 +188,46 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| ); | ); | ||||
| const handleCloseStockTransferModal = useCallback(() => { | const handleCloseStockTransferModal = useCallback(() => { | ||||
| setStockTransferModalOpen(false); | |||||
| setSelectedLotLine(null); | |||||
| setStartLocation(""); | |||||
| setTargetLocation(""); | |||||
| setTargetLocationInput(""); | |||||
| setQtyToBeTransferred(0); | |||||
| setStockTransferModalOpen(false); | |||||
| setSelectedLotLine(null); | |||||
| setStartLocation(""); | |||||
| setTargetLocation(null); | |||||
| setTargetLocationInput(""); | |||||
| setQtyToBeTransferred(0); | |||||
| }, []); | }, []); | ||||
| const handleSubmitStockTransfer = useCallback(async () => { | const handleSubmitStockTransfer = useCallback(async () => { | ||||
| try { | |||||
| setIsUploading(true); | |||||
| // Decrease the inQty (availableQty) in the source inventory lot line | |||||
| if (!selectedLotLine || !targetLocation || qtyToBeTransferred <= 0) { | |||||
| return; | |||||
| } | |||||
| try { | |||||
| setIsUploading(true); | |||||
| const request = { | |||||
| inventoryLotLineId: selectedLotLine.id, | |||||
| transferredQty: qtyToBeTransferred, | |||||
| warehouseId: targetLocation, // targetLocation now contains warehouse ID | |||||
| }; | |||||
| // TODO: Add logic to increase qty in target location warehouse | |||||
| alert(t("Stock transfer successful")); | |||||
| handleCloseStockTransferModal(); | |||||
| // TODO: Refresh the inventory lot lines list | |||||
| } catch (error: any) { | |||||
| console.error("Error transferring stock:", error); | |||||
| alert(error?.message || t("Failed to transfer stock. Please try again.")); | |||||
| } finally { | |||||
| setIsUploading(false); | |||||
| const response = await createStockTransfer(request); | |||||
| if (response && response.type === "success") { | |||||
| alert(t("Stock transfer successful")); | |||||
| handleCloseStockTransferModal(); | |||||
| // Refresh the inventory lot lines list | |||||
| window.location.reload(); // Or use your preferred refresh method | |||||
| } else { | |||||
| throw new Error(response?.message || t("Failed to transfer stock")); | |||||
| } | } | ||||
| }, [selectedLotLine, targetLocation, qtyToBeTransferred, originalQty, handleCloseStockTransferModal, setIsUploading, t]); | |||||
| } catch (error: any) { | |||||
| console.error("Error transferring stock:", error); | |||||
| alert(error?.message || t("Failed to transfer stock. Please try again.")); | |||||
| } finally { | |||||
| setIsUploading(false); | |||||
| } | |||||
| }, [selectedLotLine, targetLocation, qtyToBeTransferred, handleCloseStockTransferModal, setIsUploading, t]); | |||||
| return <> | return <> | ||||
| <Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography> | <Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography> | ||||
| @@ -276,55 +288,55 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={5.5}> | <Grid item xs={5.5}> | ||||
| <Autocomplete | <Autocomplete | ||||
| options={warehouses.filter(w => w.code !== startLocation)} | |||||
| getOptionLabel={(option) => option.code || ""} | |||||
| value={targetLocation ? warehouses.find(w => w.code === targetLocation) || null : null} | |||||
| inputValue={targetLocationInput} | |||||
| onInputChange={(event, newInputValue) => { | |||||
| setTargetLocationInput(newInputValue); | |||||
| if (targetLocation && newInputValue !== targetLocation) { | |||||
| setTargetLocation(""); | |||||
| } | |||||
| }} | |||||
| onChange={(event, newValue) => { | |||||
| if (newValue) { | |||||
| setTargetLocation(newValue.code); | |||||
| setTargetLocationInput(newValue.code); | |||||
| } else { | |||||
| setTargetLocation(""); | |||||
| setTargetLocationInput(""); | |||||
| } | |||||
| }} | |||||
| filterOptions={(options, { inputValue }) => { | |||||
| if (!inputValue || inputValue.trim() === "") return options; | |||||
| const searchTerm = inputValue.toLowerCase().trim(); | |||||
| return options.filter((option) => | |||||
| (option.code || "").toLowerCase().includes(searchTerm) || | |||||
| (option.name || "").toLowerCase().includes(searchTerm) || | |||||
| (option.description || "").toLowerCase().includes(searchTerm) | |||||
| ); | |||||
| options={warehouses.filter(w => w.code !== startLocation)} | |||||
| getOptionLabel={(option) => option.code || ""} | |||||
| value={targetLocation ? warehouses.find(w => w.id === targetLocation) || null : null} | |||||
| inputValue={targetLocationInput} | |||||
| onInputChange={(event, newInputValue) => { | |||||
| setTargetLocationInput(newInputValue); | |||||
| if (targetLocation && newInputValue !== warehouses.find(w => w.id === targetLocation)?.code) { | |||||
| setTargetLocation(null); | |||||
| } | |||||
| }} | |||||
| onChange={(event, newValue) => { | |||||
| if (newValue) { | |||||
| setTargetLocation(newValue.id); | |||||
| setTargetLocationInput(newValue.code); | |||||
| } else { | |||||
| setTargetLocation(null); | |||||
| setTargetLocationInput(""); | |||||
| } | |||||
| }} | |||||
| filterOptions={(options, { inputValue }) => { | |||||
| if (!inputValue || inputValue.trim() === "") return options; | |||||
| const searchTerm = inputValue.toLowerCase().trim(); | |||||
| return options.filter((option) => | |||||
| (option.code || "").toLowerCase().includes(searchTerm) || | |||||
| (option.name || "").toLowerCase().includes(searchTerm) || | |||||
| (option.description || "").toLowerCase().includes(searchTerm) | |||||
| ); | |||||
| }} | |||||
| isOptionEqualToValue={(option, value) => option.id === value.id} | |||||
| autoHighlight={false} | |||||
| autoSelect={false} | |||||
| clearOnBlur={false} | |||||
| renderOption={(props, option) => ( | |||||
| <li {...props}> | |||||
| {option.code} | |||||
| </li> | |||||
| )} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={t("Target Location")} | |||||
| variant="outlined" | |||||
| fullWidth | |||||
| InputLabelProps={{ | |||||
| shrink: !!targetLocation || !!targetLocationInput, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | }} | ||||
| isOptionEqualToValue={(option, value) => option.code === value.code} | |||||
| autoHighlight={false} | |||||
| autoSelect={false} | |||||
| clearOnBlur={false} | |||||
| renderOption={(props, option) => ( | |||||
| <li {...props}> | |||||
| {option.code} | |||||
| </li> | |||||
| )} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={t("Target Location")} | |||||
| variant="outlined" | |||||
| fullWidth | |||||
| InputLabelProps={{ | |||||
| shrink: !!targetLocation || !!targetLocationInput, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| )} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -124,12 +124,13 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| refetchData(filterObj); | |||||
| // Only refetch when paging changes AND we have already searched (filterObj has been set by search) | |||||
| if (Object.keys(filterObj).length > 0 || filteredItems.length > 0) { | |||||
| refetchData(filterObj); | |||||
| } | |||||
| }, [ | }, [ | ||||
| filterObj, | |||||
| pagingController.pageNum, | pagingController.pageNum, | ||||
| pagingController.pageSize, | pagingController.pageSize, | ||||
| refetchData, | |||||
| ]); | ]); | ||||
| const columns = useMemo<Column<ItemsResultWithStatus>[]>( | const columns = useMemo<Column<ItemsResultWithStatus>[]>( | ||||
| @@ -181,25 +182,20 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| ); | ); | ||||
| const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
| setFilteredItems(items); | |||||
| }, [items]); | |||||
| setFilteredItems([]); | |||||
| setFilterObj({}); | |||||
| setTotalCount(0); | |||||
| }, []); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| // setFilteredItems( | |||||
| // items.filter((pm) => { | |||||
| // return ( | |||||
| // pm.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| // pm.name.toLowerCase().includes(query.name.toLowerCase()) | |||||
| // ); | |||||
| // }) | |||||
| // ); | |||||
| setFilterObj({ | setFilterObj({ | ||||
| ...query, | ...query, | ||||
| }); | }); | ||||
| refetchData(query); | |||||
| }} | }} | ||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| @@ -70,7 +70,7 @@ const M18ImportDo: React.FC<Props> = ({}) => { | |||||
| <Box display="flex"> | <Box display="flex"> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| name="do.modifiedDateFrom" | |||||
| name="do.dDateFrom" | |||||
| // rules={{ | // rules={{ | ||||
| // required: "Please input the date From!", | // required: "Please input the date From!", | ||||
| // validate: { | // validate: { | ||||
| @@ -80,7 +80,7 @@ const M18ImportDo: React.FC<Props> = ({}) => { | |||||
| // }} | // }} | ||||
| render={({ field, fieldState: { error } }) => ( | render={({ field, fieldState: { error } }) => ( | ||||
| <DateTimePicker | <DateTimePicker | ||||
| label={t("Modified Date From *")} | |||||
| label={t("Delivery Date From *")} | |||||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | ||||
| onChange={(newValue: Dayjs | null) => | onChange={(newValue: Dayjs | null) => | ||||
| handleDateTimePickerOnChange(newValue, field.onChange) | handleDateTimePickerOnChange(newValue, field.onChange) | ||||
| @@ -104,7 +104,7 @@ const M18ImportDo: React.FC<Props> = ({}) => { | |||||
| </Box> | </Box> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| name="do.modifiedDateTo" | |||||
| name="do.dDateTo" | |||||
| // rules={{ | // rules={{ | ||||
| // required: "Please input the date to!", | // required: "Please input the date to!", | ||||
| // validate: { | // validate: { | ||||
| @@ -116,7 +116,7 @@ const M18ImportDo: React.FC<Props> = ({}) => { | |||||
| // }} | // }} | ||||
| render={({ field, fieldState: { error } }) => ( | render={({ field, fieldState: { error } }) => ( | ||||
| <DateTimePicker | <DateTimePicker | ||||
| label={t("Modified Date To *")} | |||||
| label={t("Delivery Date To *")} | |||||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | ||||
| onChange={(newValue: Dayjs | null) => | onChange={(newValue: Dayjs | null) => | ||||
| handleDateTimePickerOnChange(newValue, field.onChange) | handleDateTimePickerOnChange(newValue, field.onChange) | ||||
| @@ -70,7 +70,7 @@ const M18ImportPo: React.FC<Props> = ({}) => { | |||||
| <Box display="flex"> | <Box display="flex"> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| name="po.modifiedDateFrom" | |||||
| name="po.dDateFrom" | |||||
| // rules={{ | // rules={{ | ||||
| // required: "Please input the date From!", | // required: "Please input the date From!", | ||||
| // validate: { | // validate: { | ||||
| @@ -80,7 +80,7 @@ const M18ImportPo: React.FC<Props> = ({}) => { | |||||
| // }} | // }} | ||||
| render={({ field, fieldState: { error } }) => ( | render={({ field, fieldState: { error } }) => ( | ||||
| <DateTimePicker | <DateTimePicker | ||||
| label={t("Modified Date From *")} | |||||
| label={t("Delivery Date From *")} | |||||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | ||||
| onChange={(newValue: Dayjs | null) => | onChange={(newValue: Dayjs | null) => | ||||
| handleDateTimePickerOnChange(newValue, field.onChange) | handleDateTimePickerOnChange(newValue, field.onChange) | ||||
| @@ -104,7 +104,7 @@ const M18ImportPo: React.FC<Props> = ({}) => { | |||||
| </Box> | </Box> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| name="po.modifiedDateTo" | |||||
| name="po.dDateTo" | |||||
| // rules={{ | // rules={{ | ||||
| // required: "Please input the date to!", | // required: "Please input the date to!", | ||||
| // validate: { | // validate: { | ||||
| @@ -116,7 +116,7 @@ const M18ImportPo: React.FC<Props> = ({}) => { | |||||
| // }} | // }} | ||||
| render={({ field, fieldState: { error } }) => ( | render={({ field, fieldState: { error } }) => ( | ||||
| <DateTimePicker | <DateTimePicker | ||||
| label={t("Modified Date To *")} | |||||
| label={t("Delivery Date To *")} | |||||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | ||||
| onChange={(newValue: Dayjs | null) => | onChange={(newValue: Dayjs | null) => | ||||
| handleDateTimePickerOnChange(newValue, field.onChange) | handleDateTimePickerOnChange(newValue, field.onChange) | ||||
| @@ -8,7 +8,7 @@ import { | |||||
| testM18ImportMasterData, | testM18ImportMasterData, | ||||
| testM18ImportDo, | testM18ImportDo, | ||||
| } from "@/app/api/settings/m18ImportTesting/actions"; | } from "@/app/api/settings/m18ImportTesting/actions"; | ||||
| import { Card, CardContent, Grid, Stack, Typography } from "@mui/material"; | |||||
| import { Card, CardContent, Grid, Stack, Typography, Button } from "@mui/material"; | |||||
| import React, { | import React, { | ||||
| BaseSyntheticEvent, | BaseSyntheticEvent, | ||||
| FormEvent, | FormEvent, | ||||
| @@ -22,6 +22,8 @@ import M18ImportPq from "./M18ImportPq"; | |||||
| import { dateTimeStringToDayjs } from "@/app/utils/formatUtil"; | import { dateTimeStringToDayjs } from "@/app/utils/formatUtil"; | ||||
| import M18ImportMasterData from "./M18ImportMasterData"; | import M18ImportMasterData from "./M18ImportMasterData"; | ||||
| import M18ImportDo from "./M18ImportDo"; | import M18ImportDo from "./M18ImportDo"; | ||||
| import { PlayArrow, Refresh as RefreshIcon } from "@mui/icons-material"; | |||||
| import { triggerScheduler, refreshCronSchedules } from "@/app/api/settings/m18ImportTesting/actions"; | |||||
| interface Props {} | interface Props {} | ||||
| @@ -166,9 +168,80 @@ const M18ImportTesting: React.FC<Props> = ({}) => { | |||||
| // [], | // [], | ||||
| // ); | // ); | ||||
| const handleManualTrigger = async (type: any) => { | |||||
| setIsLoading(true); | |||||
| setLoadingType(`Manual ${type}`); | |||||
| try { | |||||
| const result = await triggerScheduler(type); | |||||
| if (result) alert(result); | |||||
| } catch (error) { | |||||
| console.error(error); | |||||
| alert("Trigger failed. Check server logs."); | |||||
| } finally { | |||||
| setIsLoading(false); | |||||
| } | |||||
| }; | |||||
| const handleRefreshSchedules = async () => { | |||||
| // Re-use the manual trigger logic which we know works | |||||
| await handleManualTrigger('refresh-cron'); | |||||
| }; | |||||
| return ( | return ( | ||||
| <Card> | <Card> | ||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
| <Typography variant="h6">{t("Manual Scheduler Triggers")}</Typography> | |||||
| <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<PlayArrow />} | |||||
| onClick={() => handleManualTrigger('po')} | |||||
| disabled={isLoading} | |||||
| > | |||||
| Trigger PO | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<PlayArrow />} | |||||
| onClick={() => handleManualTrigger('do1')} | |||||
| disabled={isLoading} | |||||
| > | |||||
| Trigger DO1 | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<PlayArrow />} | |||||
| onClick={() => handleManualTrigger('do2')} | |||||
| disabled={isLoading} | |||||
| > | |||||
| Trigger DO2 | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<PlayArrow />} | |||||
| onClick={() => handleManualTrigger('master-data')} | |||||
| disabled={isLoading} | |||||
| > | |||||
| Trigger Master | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="secondary" | |||||
| startIcon={<RefreshIcon />} | |||||
| onClick={handleRefreshSchedules} // This now uses the logic that works | |||||
| disabled={isLoading} | |||||
| > | |||||
| Reload Cron Settings | |||||
| </Button> | |||||
| </Stack> | |||||
| <hr style={{ opacity: 0.2 }} /> | |||||
| <Typography variant="overline"> | |||||
| {t("Status: ")} | |||||
| {isLoading ? t(`Processing ${loadingType}...`) : t("Ready")} | |||||
| </Typography> | |||||
| <Typography variant="overline"> | <Typography variant="overline"> | ||||
| {t("Status: ")} | {t("Status: ")} | ||||
| {isLoading ? t(`Importing ${loadingType}...`) : t("Ready to import")} | {isLoading ? t(`Importing ${loadingType}...`) : t("Ready to import")} | ||||
| @@ -247,7 +247,7 @@ const NavigationContent: React.FC = () => { | |||||
| icon: <BugReportIcon />, | icon: <BugReportIcon />, | ||||
| label: "PS", | label: "PS", | ||||
| path: "/ps", | path: "/ps", | ||||
| requiredAbility: AUTH.TESTING, | |||||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN], | |||||
| isHidden: false, | isHidden: false, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -353,9 +353,14 @@ const NavigationContent: React.FC = () => { | |||||
| path: "/settings/user", | path: "/settings/user", | ||||
| }, | }, | ||||
| { | { | ||||
| icon: <QrCodeIcon />, | |||||
| label: "QR Code Handle", | |||||
| path: "/settings/qrCodeHandle", | |||||
| icon: <RequestQuote />, | |||||
| label: "QC Check Template", | |||||
| path: "/settings/user", | |||||
| }, | |||||
| { | |||||
| icon: <RequestQuote />, | |||||
| label: "QC Item All", | |||||
| path: "/settings/qcItemAll", | |||||
| }, | }, | ||||
| // { | // { | ||||
| // icon: <RequestQuote />, | // icon: <RequestQuote />, | ||||
| @@ -484,7 +484,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| }, | }, | ||||
| { | { | ||||
| field: "reqQty", | field: "reqQty", | ||||
| headerName: t("Req. Qty"), | |||||
| headerName: t("Bom Req. Qty"), | |||||
| flex: 0.7, | flex: 0.7, | ||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| @@ -0,0 +1,105 @@ | |||||
| "use client"; | |||||
| import { useState, ReactNode, useEffect } from "react"; | |||||
| import { Box, Tabs, Tab } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { useSearchParams, useRouter } from "next/navigation"; | |||||
| interface TabPanelProps { | |||||
| children?: ReactNode; | |||||
| index: number; | |||||
| value: number; | |||||
| } | |||||
| function TabPanel(props: TabPanelProps) { | |||||
| const { children, value, index, ...other } = props; | |||||
| return ( | |||||
| <div | |||||
| role="tabpanel" | |||||
| hidden={value !== index} | |||||
| id={`qc-item-all-tabpanel-${index}`} | |||||
| aria-labelledby={`qc-item-all-tab-${index}`} | |||||
| {...other} | |||||
| > | |||||
| {value === index && <Box sx={{ py: 3 }}>{children}</Box>} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| interface QcItemAllTabsProps { | |||||
| tab0Content: ReactNode; | |||||
| tab1Content: ReactNode; | |||||
| tab2Content: ReactNode; | |||||
| tab3Content: ReactNode; | |||||
| } | |||||
| const QcItemAllTabs: React.FC<QcItemAllTabsProps> = ({ | |||||
| tab0Content, | |||||
| tab1Content, | |||||
| tab2Content, | |||||
| tab3Content, | |||||
| }) => { | |||||
| const { t } = useTranslation("qcItemAll"); | |||||
| const searchParams = useSearchParams(); | |||||
| const router = useRouter(); | |||||
| const getInitialTab = () => { | |||||
| const tab = searchParams.get("tab"); | |||||
| if (tab === "1") return 1; | |||||
| if (tab === "2") return 2; | |||||
| if (tab === "3") return 3; | |||||
| return 0; | |||||
| }; | |||||
| const [currentTab, setCurrentTab] = useState(getInitialTab); | |||||
| useEffect(() => { | |||||
| const tab = searchParams.get("tab"); | |||||
| const tabIndex = tab ? parseInt(tab, 10) : 0; | |||||
| setCurrentTab(tabIndex); | |||||
| }, [searchParams]); | |||||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | |||||
| setCurrentTab(newValue); | |||||
| const params = new URLSearchParams(searchParams.toString()); | |||||
| if (newValue === 0) { | |||||
| params.delete("tab"); | |||||
| } else { | |||||
| params.set("tab", newValue.toString()); | |||||
| } | |||||
| router.push(`?${params.toString()}`, { scroll: false }); | |||||
| }; | |||||
| return ( | |||||
| <Box sx={{ width: "100%" }}> | |||||
| <Box sx={{ borderBottom: 1, borderColor: "divider" }}> | |||||
| <Tabs value={currentTab} onChange={handleTabChange}> | |||||
| <Tab label={t("Item and Qc Category Mapping")} /> | |||||
| <Tab label={t("Qc Category and Qc Item Mapping")} /> | |||||
| <Tab label={t("Qc Category Management")} /> | |||||
| <Tab label={t("Qc Item Management")} /> | |||||
| </Tabs> | |||||
| </Box> | |||||
| <TabPanel value={currentTab} index={0}> | |||||
| {tab0Content} | |||||
| </TabPanel> | |||||
| <TabPanel value={currentTab} index={1}> | |||||
| {tab1Content} | |||||
| </TabPanel> | |||||
| <TabPanel value={currentTab} index={2}> | |||||
| {tab2Content} | |||||
| </TabPanel> | |||||
| <TabPanel value={currentTab} index={3}> | |||||
| {tab3Content} | |||||
| </TabPanel> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default QcItemAllTabs; | |||||
| @@ -0,0 +1,351 @@ | |||||
| "use client"; | |||||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Dialog, | |||||
| DialogActions, | |||||
| DialogContent, | |||||
| DialogTitle, | |||||
| Grid, | |||||
| Stack, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| TextField, | |||||
| Typography, | |||||
| IconButton, | |||||
| CircularProgress, | |||||
| } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { Add, Delete, Edit } from "@mui/icons-material"; | |||||
| import SearchBox, { Criterion } from "../SearchBox/SearchBox"; | |||||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||||
| import { | |||||
| saveItemQcCategoryMapping, | |||||
| deleteItemQcCategoryMapping, | |||||
| getItemQcCategoryMappings, | |||||
| fetchQcCategoriesForAll, | |||||
| fetchItemsForAll, | |||||
| getItemByCode, | |||||
| } from "@/app/api/settings/qcItemAll/actions"; | |||||
| import { | |||||
| QcCategoryResult, | |||||
| ItemsResult, | |||||
| } from "@/app/api/settings/qcItemAll"; | |||||
| import { ItemQcCategoryMappingInfo } from "@/app/api/settings/qcItemAll"; | |||||
| import { | |||||
| deleteDialog, | |||||
| errorDialogWithContent, | |||||
| submitDialog, | |||||
| successDialog, | |||||
| } from "../Swal/CustomAlerts"; | |||||
| type SearchQuery = Partial<Omit<QcCategoryResult, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const Tab0ItemQcCategoryMapping: React.FC = () => { | |||||
| const { t } = useTranslation("qcItemAll"); | |||||
| const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]); | |||||
| const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]); | |||||
| const [selectedCategory, setSelectedCategory] = useState<QcCategoryResult | null>(null); | |||||
| const [mappings, setMappings] = useState<ItemQcCategoryMappingInfo[]>([]); | |||||
| const [openDialog, setOpenDialog] = useState(false); | |||||
| const [openAddDialog, setOpenAddDialog] = useState(false); | |||||
| const [itemCode, setItemCode] = useState<string>(""); | |||||
| const [validatedItem, setValidatedItem] = useState<ItemsResult | null>(null); | |||||
| const [itemCodeError, setItemCodeError] = useState<string>(""); | |||||
| const [validatingItemCode, setValidatingItemCode] = useState<boolean>(false); | |||||
| const [selectedType, setSelectedType] = useState<string>("IQC"); | |||||
| const [loading, setLoading] = useState(true); | |||||
| useEffect(() => { | |||||
| const loadData = async () => { | |||||
| setLoading(true); | |||||
| try { | |||||
| // Only load categories list (same as Tab 2) - fast! | |||||
| const categories = await fetchQcCategoriesForAll(); | |||||
| setQcCategories(categories || []); | |||||
| setFilteredQcCategories(categories || []); | |||||
| } catch (error) { | |||||
| console.error("Tab0: Error loading data:", error); | |||||
| setQcCategories([]); | |||||
| setFilteredQcCategories([]); | |||||
| if (error instanceof Error) { | |||||
| errorDialogWithContent(t("Error"), error.message, t); | |||||
| } | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }; | |||||
| loadData(); | |||||
| }, []); | |||||
| const handleViewMappings = useCallback(async (category: QcCategoryResult) => { | |||||
| setSelectedCategory(category); | |||||
| const mappingData = await getItemQcCategoryMappings(category.id); | |||||
| setMappings(mappingData); | |||||
| setOpenDialog(true); | |||||
| }, []); | |||||
| const handleAddMapping = useCallback(() => { | |||||
| if (!selectedCategory) return; | |||||
| setItemCode(""); | |||||
| setValidatedItem(null); | |||||
| setItemCodeError(""); | |||||
| setOpenAddDialog(true); | |||||
| }, [selectedCategory]); | |||||
| const handleItemCodeChange = useCallback(async (code: string) => { | |||||
| setItemCode(code); | |||||
| setValidatedItem(null); | |||||
| setItemCodeError(""); | |||||
| if (!code || code.trim() === "") { | |||||
| return; | |||||
| } | |||||
| setValidatingItemCode(true); | |||||
| try { | |||||
| const item = await getItemByCode(code.trim()); | |||||
| if (item) { | |||||
| setValidatedItem(item); | |||||
| setItemCodeError(""); | |||||
| } else { | |||||
| setValidatedItem(null); | |||||
| setItemCodeError(t("Item code not found")); | |||||
| } | |||||
| } catch (error) { | |||||
| setValidatedItem(null); | |||||
| setItemCodeError(t("Error validating item code")); | |||||
| } finally { | |||||
| setValidatingItemCode(false); | |||||
| } | |||||
| }, [t]); | |||||
| const handleSaveMapping = useCallback(async () => { | |||||
| if (!selectedCategory || !validatedItem) return; | |||||
| await submitDialog(async () => { | |||||
| try { | |||||
| await saveItemQcCategoryMapping( | |||||
| validatedItem.id as number, | |||||
| selectedCategory.id, | |||||
| selectedType | |||||
| ); | |||||
| // Close add dialog first | |||||
| setOpenAddDialog(false); | |||||
| setItemCode(""); | |||||
| setValidatedItem(null); | |||||
| setItemCodeError(""); | |||||
| // Reload mappings to update the view | |||||
| const mappingData = await getItemQcCategoryMappings(selectedCategory.id); | |||||
| setMappings(mappingData); | |||||
| // Show success message after closing dialogs | |||||
| await successDialog(t("Submit Success"), t); | |||||
| // Keep the view dialog open to show updated data | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Submit Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, [selectedCategory, validatedItem, selectedType, t]); | |||||
| const handleDeleteMapping = useCallback( | |||||
| async (mappingId: number) => { | |||||
| if (!selectedCategory) return; | |||||
| deleteDialog(async () => { | |||||
| try { | |||||
| await deleteItemQcCategoryMapping(mappingId); | |||||
| await successDialog(t("Delete Success"), t); | |||||
| // Reload mappings | |||||
| const mappingData = await getItemQcCategoryMappings(selectedCategory.id); | |||||
| setMappings(mappingData); | |||||
| // No need to reload categories list - it doesn't change | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Delete Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, | |||||
| [selectedCategory, t] | |||||
| ); | |||||
| const typeOptions = ["IQC", "IPQC", "OQC", "FQC"]; | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: t("Code"), paramName: "code", type: "text" }, | |||||
| { label: t("Name"), paramName: "name", type: "text" }, | |||||
| ], | |||||
| [t] | |||||
| ); | |||||
| const onReset = useCallback(() => { | |||||
| setFilteredQcCategories(qcCategories); | |||||
| }, [qcCategories]); | |||||
| const columnWidthSx = (width = "10%") => { | |||||
| return { width: width, whiteSpace: "nowrap" }; | |||||
| }; | |||||
| const columns = useMemo<Column<QcCategoryResult>[]>( | |||||
| () => [ | |||||
| { name: "code", label: t("Qc Category Code"), sx: columnWidthSx("20%") }, | |||||
| { name: "name", label: t("Qc Category Name"), sx: columnWidthSx("40%") }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Actions"), | |||||
| onClick: (category) => handleViewMappings(category), | |||||
| buttonIcon: <Edit />, | |||||
| buttonIcons: {} as any, | |||||
| sx: columnWidthSx("10%"), | |||||
| }, | |||||
| ], | |||||
| [t, handleViewMappings] | |||||
| ); | |||||
| if (loading) { | |||||
| return ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "200px" }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <Box> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| setFilteredQcCategories( | |||||
| qcCategories.filter( | |||||
| (qc) => | |||||
| (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) && | |||||
| (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase())) | |||||
| ) | |||||
| ); | |||||
| }} | |||||
| onReset={onReset} | |||||
| /> | |||||
| <SearchResults<QcCategoryResult> | |||||
| items={filteredQcCategories} | |||||
| columns={columns} | |||||
| /> | |||||
| {/* View Mappings Dialog */} | |||||
| <Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth> | |||||
| <DialogTitle> | |||||
| {t("Mapping Details")} - {selectedCategory?.name} | |||||
| </DialogTitle> | |||||
| <DialogContent> | |||||
| <Stack spacing={2} sx={{ mt: 1 }}> | |||||
| <Box sx={{ display: "flex", justifyContent: "flex-end" }}> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| onClick={handleAddMapping} | |||||
| > | |||||
| {t("Add Mapping")} | |||||
| </Button> | |||||
| </Box> | |||||
| <TableContainer> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Item Code")}</TableCell> | |||||
| <TableCell>{t("Item Name")}</TableCell> | |||||
| <TableCell>{t("Type")}</TableCell> | |||||
| <TableCell>{t("Actions")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {mappings.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={4} align="center"> | |||||
| {t("No mappings found")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| mappings.map((mapping) => ( | |||||
| <TableRow key={mapping.id}> | |||||
| <TableCell>{mapping.itemCode}</TableCell> | |||||
| <TableCell>{mapping.itemName}</TableCell> | |||||
| <TableCell>{mapping.type}</TableCell> | |||||
| <TableCell> | |||||
| <IconButton | |||||
| color="error" | |||||
| size="small" | |||||
| onClick={() => handleDeleteMapping(mapping.id)} | |||||
| > | |||||
| <Delete /> | |||||
| </IconButton> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| {/* Add Mapping Dialog */} | |||||
| <Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth> | |||||
| <DialogTitle>{t("Add Mapping")}</DialogTitle> | |||||
| <DialogContent> | |||||
| <Stack spacing={2} sx={{ mt: 2 }}> | |||||
| <TextField | |||||
| label={t("Item Code")} | |||||
| value={itemCode} | |||||
| onChange={(e) => handleItemCodeChange(e.target.value)} | |||||
| error={!!itemCodeError} | |||||
| helperText={itemCodeError || (validatedItem ? `${validatedItem.code} - ${validatedItem.name}` : t("Enter item code to validate"))} | |||||
| fullWidth | |||||
| disabled={validatingItemCode} | |||||
| InputProps={{ | |||||
| endAdornment: validatingItemCode ? <CircularProgress size={20} /> : null, | |||||
| }} | |||||
| /> | |||||
| <TextField | |||||
| select | |||||
| label={t("Select Type")} | |||||
| value={selectedType} | |||||
| onChange={(e) => setSelectedType(e.target.value)} | |||||
| SelectProps={{ | |||||
| native: true, | |||||
| }} | |||||
| fullWidth | |||||
| > | |||||
| {typeOptions.map((type) => ( | |||||
| <option key={type} value={type}> | |||||
| {type} | |||||
| </option> | |||||
| ))} | |||||
| </TextField> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={handleSaveMapping} | |||||
| disabled={!validatedItem} | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default Tab0ItemQcCategoryMapping; | |||||
| @@ -0,0 +1,304 @@ | |||||
| "use client"; | |||||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Dialog, | |||||
| DialogActions, | |||||
| DialogContent, | |||||
| DialogTitle, | |||||
| IconButton, | |||||
| Stack, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| TextField, | |||||
| Autocomplete, | |||||
| CircularProgress, | |||||
| } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { Add, Delete, Edit } from "@mui/icons-material"; | |||||
| import SearchBox, { Criterion } from "../SearchBox/SearchBox"; | |||||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||||
| import { | |||||
| saveQcCategoryQcItemMapping, | |||||
| deleteQcCategoryQcItemMapping, | |||||
| getQcCategoryQcItemMappings, | |||||
| fetchQcCategoriesForAll, | |||||
| fetchQcItemsForAll, | |||||
| } from "@/app/api/settings/qcItemAll/actions"; | |||||
| import { | |||||
| QcCategoryResult, | |||||
| QcItemResult, | |||||
| } from "@/app/api/settings/qcItemAll"; | |||||
| import { QcItemInfo } from "@/app/api/settings/qcItemAll"; | |||||
| import { | |||||
| deleteDialog, | |||||
| errorDialogWithContent, | |||||
| submitDialog, | |||||
| successDialog, | |||||
| } from "../Swal/CustomAlerts"; | |||||
| type SearchQuery = Partial<Omit<QcCategoryResult, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const Tab1QcCategoryQcItemMapping: React.FC = () => { | |||||
| const { t } = useTranslation("qcItemAll"); | |||||
| const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]); | |||||
| const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]); | |||||
| const [selectedCategory, setSelectedCategory] = useState<QcCategoryResult | null>(null); | |||||
| const [mappings, setMappings] = useState<QcItemInfo[]>([]); | |||||
| const [openDialog, setOpenDialog] = useState(false); | |||||
| const [openAddDialog, setOpenAddDialog] = useState(false); | |||||
| const [qcItems, setQcItems] = useState<QcItemResult[]>([]); | |||||
| const [selectedQcItem, setSelectedQcItem] = useState<QcItemResult | null>(null); | |||||
| const [order, setOrder] = useState<number>(0); | |||||
| const [loading, setLoading] = useState(true); | |||||
| useEffect(() => { | |||||
| const loadData = async () => { | |||||
| setLoading(true); | |||||
| try { | |||||
| // Only load categories list (same as Tab 2) - fast! | |||||
| const categories = await fetchQcCategoriesForAll(); | |||||
| setQcCategories(categories || []); | |||||
| setFilteredQcCategories(categories || []); | |||||
| } catch (error) { | |||||
| console.error("Error loading data:", error); | |||||
| setQcCategories([]); // Ensure it's always an array | |||||
| setFilteredQcCategories([]); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }; | |||||
| loadData(); | |||||
| }, []); | |||||
| const handleViewMappings = useCallback(async (category: QcCategoryResult) => { | |||||
| setSelectedCategory(category); | |||||
| // Load mappings when user clicks View (lazy loading) | |||||
| const mappingData = await getQcCategoryQcItemMappings(category.id); | |||||
| setMappings(mappingData); | |||||
| setOpenDialog(true); | |||||
| }, []); | |||||
| const handleAddMapping = useCallback(async () => { | |||||
| if (!selectedCategory) return; | |||||
| // Load qc items list when opening add dialog | |||||
| try { | |||||
| const itemsData = await fetchQcItemsForAll(); | |||||
| setQcItems(itemsData); | |||||
| } catch (error) { | |||||
| console.error("Error loading qc items:", error); | |||||
| } | |||||
| setOpenAddDialog(true); | |||||
| setOrder(0); | |||||
| setSelectedQcItem(null); | |||||
| }, [selectedCategory]); | |||||
| const handleSaveMapping = useCallback(async () => { | |||||
| if (!selectedCategory || !selectedQcItem) return; | |||||
| await submitDialog(async () => { | |||||
| try { | |||||
| await saveQcCategoryQcItemMapping( | |||||
| selectedCategory.id, | |||||
| selectedQcItem.id, | |||||
| order, | |||||
| undefined // No description needed - qcItem already has description | |||||
| ); | |||||
| // Close add dialog first | |||||
| setOpenAddDialog(false); | |||||
| setSelectedQcItem(null); | |||||
| setOrder(0); | |||||
| // Reload mappings to update the view | |||||
| const mappingData = await getQcCategoryQcItemMappings(selectedCategory.id); | |||||
| setMappings(mappingData); | |||||
| // Show success message after closing dialogs | |||||
| await successDialog(t("Submit Success"), t); | |||||
| // Keep the view dialog open to show updated data | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Submit Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, [selectedCategory, selectedQcItem, order, t]); | |||||
| const handleDeleteMapping = useCallback( | |||||
| async (mappingId: number) => { | |||||
| if (!selectedCategory) return; | |||||
| deleteDialog(async () => { | |||||
| try { | |||||
| await deleteQcCategoryQcItemMapping(mappingId); | |||||
| await successDialog(t("Delete Success"), t); | |||||
| // Reload mappings | |||||
| const mappingData = await getQcCategoryQcItemMappings(selectedCategory.id); | |||||
| setMappings(mappingData); | |||||
| // No need to reload categories list - it doesn't change | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Delete Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, | |||||
| [selectedCategory, t] | |||||
| ); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: t("Code"), paramName: "code", type: "text" }, | |||||
| { label: t("Name"), paramName: "name", type: "text" }, | |||||
| ], | |||||
| [t] | |||||
| ); | |||||
| const onReset = useCallback(() => { | |||||
| setFilteredQcCategories(qcCategories); | |||||
| }, [qcCategories]); | |||||
| const columnWidthSx = (width = "10%") => { | |||||
| return { width: width, whiteSpace: "nowrap" }; | |||||
| }; | |||||
| const columns = useMemo<Column<QcCategoryResult>[]>( | |||||
| () => [ | |||||
| { name: "code", label: t("Qc Category Code"), sx: columnWidthSx("20%") }, | |||||
| { name: "name", label: t("Qc Category Name"), sx: columnWidthSx("40%") }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Actions"), | |||||
| onClick: (category) => handleViewMappings(category), | |||||
| buttonIcon: <Edit />, | |||||
| buttonIcons: {} as any, | |||||
| sx: columnWidthSx("10%"), | |||||
| }, | |||||
| ], | |||||
| [t, handleViewMappings] | |||||
| ); | |||||
| return ( | |||||
| <Box> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| setFilteredQcCategories( | |||||
| qcCategories.filter( | |||||
| (qc) => | |||||
| (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) && | |||||
| (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase())) | |||||
| ) | |||||
| ); | |||||
| }} | |||||
| onReset={onReset} | |||||
| /> | |||||
| <SearchResults<QcCategoryResult> | |||||
| items={filteredQcCategories} | |||||
| columns={columns} | |||||
| /> | |||||
| {/* View Mappings Dialog */} | |||||
| <Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth> | |||||
| <DialogTitle> | |||||
| {t("Association Details")} - {selectedCategory?.name} | |||||
| </DialogTitle> | |||||
| <DialogContent> | |||||
| <Stack spacing={2} sx={{ mt: 1 }}> | |||||
| <Box sx={{ display: "flex", justifyContent: "flex-end" }}> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| onClick={handleAddMapping} | |||||
| > | |||||
| {t("Add Association")} | |||||
| </Button> | |||||
| </Box> | |||||
| <TableContainer> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Order")}</TableCell> | |||||
| <TableCell>{t("Qc Item Code")}</TableCell> | |||||
| <TableCell>{t("Qc Item Name")}</TableCell> | |||||
| <TableCell>{t("Description")}</TableCell> | |||||
| <TableCell>{t("Actions")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {mappings.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={5} align="center"> | |||||
| {t("No associations found")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| mappings.map((mapping) => ( | |||||
| <TableRow key={mapping.id}> | |||||
| <TableCell>{mapping.order}</TableCell> | |||||
| <TableCell>{mapping.code}</TableCell> | |||||
| <TableCell>{mapping.name}</TableCell> | |||||
| <TableCell>{mapping.description || "-"}</TableCell> | |||||
| <TableCell> | |||||
| <IconButton | |||||
| color="error" | |||||
| size="small" | |||||
| onClick={() => handleDeleteMapping(mapping.id)} | |||||
| > | |||||
| <Delete /> | |||||
| </IconButton> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| {/* Add Mapping Dialog */} | |||||
| <Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth> | |||||
| <DialogTitle>{t("Add Association")}</DialogTitle> | |||||
| <DialogContent> | |||||
| <Stack spacing={2} sx={{ mt: 2 }}> | |||||
| <Autocomplete | |||||
| options={qcItems} | |||||
| getOptionLabel={(option) => `${option.code} - ${option.name}`} | |||||
| value={selectedQcItem} | |||||
| onChange={(_, newValue) => setSelectedQcItem(newValue)} | |||||
| renderInput={(params) => ( | |||||
| <TextField {...params} label={t("Select Qc Item")} /> | |||||
| )} | |||||
| /> | |||||
| <TextField | |||||
| type="number" | |||||
| label={t("Order")} | |||||
| value={order} | |||||
| onChange={(e) => setOrder(parseInt(e.target.value) || 0)} | |||||
| fullWidth | |||||
| /> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={handleSaveMapping} | |||||
| disabled={!selectedQcItem} | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default Tab1QcCategoryQcItemMapping; | |||||
| @@ -0,0 +1,226 @@ | |||||
| "use client"; | |||||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import SearchResults, { Column } from "../SearchResults"; | |||||
| import EditNote from "@mui/icons-material/EditNote"; | |||||
| import { fetchQcCategoriesForAll } from "@/app/api/settings/qcItemAll/actions"; | |||||
| import { QcCategoryResult } from "@/app/api/settings/qcItemAll"; | |||||
| import { | |||||
| deleteDialog, | |||||
| errorDialogWithContent, | |||||
| submitDialog, | |||||
| successDialog, | |||||
| } from "../Swal/CustomAlerts"; | |||||
| import { | |||||
| deleteQcCategoryWithValidation, | |||||
| canDeleteQcCategory, | |||||
| saveQcCategoryWithValidation, | |||||
| SaveQcCategoryInputs, | |||||
| } from "@/app/api/settings/qcItemAll/actions"; | |||||
| import Delete from "@mui/icons-material/Delete"; | |||||
| import { Add } from "@mui/icons-material"; | |||||
| import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from "@mui/material"; | |||||
| import QcCategoryDetails from "../QcCategorySave/QcCategoryDetails"; | |||||
| import { FormProvider, useForm } from "react-hook-form"; | |||||
| type SearchQuery = Partial<Omit<QcCategoryResult, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const Tab2QcCategoryManagement: React.FC = () => { | |||||
| const { t } = useTranslation("qcItemAll"); | |||||
| const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]); | |||||
| const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]); | |||||
| const [openDialog, setOpenDialog] = useState(false); | |||||
| const [editingCategory, setEditingCategory] = useState<QcCategoryResult | null>(null); | |||||
| useEffect(() => { | |||||
| loadCategories(); | |||||
| }, []); | |||||
| const loadCategories = async () => { | |||||
| const categories = await fetchQcCategoriesForAll(); | |||||
| setQcCategories(categories); | |||||
| setFilteredQcCategories(categories); | |||||
| }; | |||||
| const formProps = useForm<SaveQcCategoryInputs>({ | |||||
| defaultValues: { | |||||
| code: "", | |||||
| name: "", | |||||
| description: "", | |||||
| }, | |||||
| }); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: t("Code"), paramName: "code", type: "text" }, | |||||
| { label: t("Name"), paramName: "name", type: "text" }, | |||||
| ], | |||||
| [t] | |||||
| ); | |||||
| const onReset = useCallback(() => { | |||||
| setFilteredQcCategories(qcCategories); | |||||
| }, [qcCategories]); | |||||
| const handleEdit = useCallback((qcCategory: QcCategoryResult) => { | |||||
| setEditingCategory(qcCategory); | |||||
| formProps.reset({ | |||||
| id: qcCategory.id, | |||||
| code: qcCategory.code, | |||||
| name: qcCategory.name, | |||||
| description: qcCategory.description || "", | |||||
| }); | |||||
| setOpenDialog(true); | |||||
| }, [formProps]); | |||||
| const handleAdd = useCallback(() => { | |||||
| setEditingCategory(null); | |||||
| formProps.reset({ | |||||
| code: "", | |||||
| name: "", | |||||
| description: "", | |||||
| }); | |||||
| setOpenDialog(true); | |||||
| }, [formProps]); | |||||
| const handleSubmit = useCallback(async (data: SaveQcCategoryInputs) => { | |||||
| await submitDialog(async () => { | |||||
| try { | |||||
| const response = await saveQcCategoryWithValidation(data); | |||||
| if (response.errors) { | |||||
| let errorContents = ""; | |||||
| for (const [key, value] of Object.entries(response.errors)) { | |||||
| formProps.setError(key as keyof SaveQcCategoryInputs, { | |||||
| type: "custom", | |||||
| message: value, | |||||
| }); | |||||
| errorContents = errorContents + t(value) + "<br>"; | |||||
| } | |||||
| errorDialogWithContent(t("Submit Error"), errorContents, t); | |||||
| } else { | |||||
| await successDialog(t("Submit Success"), t); | |||||
| setOpenDialog(false); | |||||
| await loadCategories(); | |||||
| } | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Submit Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, [formProps, t]); | |||||
| const handleDelete = useCallback(async (qcCategory: QcCategoryResult) => { | |||||
| // Check if can delete first | |||||
| const canDelete = await canDeleteQcCategory(qcCategory.id); // This is a server action, token handled server-side | |||||
| if (!canDelete) { | |||||
| errorDialogWithContent( | |||||
| t("Cannot Delete"), | |||||
| t("Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.").replace("{itemCount}", "some").replace("{qcItemCount}", "some"), | |||||
| t | |||||
| ); | |||||
| return; | |||||
| } | |||||
| deleteDialog(async () => { | |||||
| try { | |||||
| const response = await deleteQcCategoryWithValidation(qcCategory.id); | |||||
| if (!response.success || !response.canDelete) { | |||||
| errorDialogWithContent( | |||||
| t("Delete Error"), | |||||
| response.message || t("Cannot Delete"), | |||||
| t | |||||
| ); | |||||
| } else { | |||||
| await successDialog(t("Delete Success"), t); | |||||
| await loadCategories(); | |||||
| } | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Delete Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, [t]); | |||||
| const columnWidthSx = (width = "10%") => { | |||||
| return { width: width, whiteSpace: "nowrap" }; | |||||
| }; | |||||
| const columns = useMemo<Column<QcCategoryResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "id", | |||||
| label: t("Details"), | |||||
| onClick: handleEdit, | |||||
| buttonIcon: <EditNote />, | |||||
| sx: columnWidthSx("5%"), | |||||
| }, | |||||
| { name: "code", label: t("Code"), sx: columnWidthSx("15%") }, | |||||
| { name: "name", label: t("Name"), sx: columnWidthSx("30%") }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Delete"), | |||||
| onClick: handleDelete, | |||||
| buttonIcon: <Delete />, | |||||
| buttonColor: "error", | |||||
| sx: columnWidthSx("5%"), | |||||
| }, | |||||
| ], | |||||
| [t, handleEdit, handleDelete] | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <Stack direction="row" justifyContent="flex-end" sx={{ mb: 2 }}> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| onClick={handleAdd} | |||||
| > | |||||
| {t("Create Qc Category")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| setFilteredQcCategories( | |||||
| qcCategories.filter( | |||||
| (qc) => | |||||
| (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) && | |||||
| (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase())) | |||||
| ) | |||||
| ); | |||||
| }} | |||||
| onReset={onReset} | |||||
| /> | |||||
| <SearchResults<QcCategoryResult> | |||||
| items={filteredQcCategories} | |||||
| columns={columns} | |||||
| /> | |||||
| {/* Add/Edit Dialog */} | |||||
| <Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth> | |||||
| <DialogTitle> | |||||
| {editingCategory ? t("Edit Qc Category") : t("Create Qc Category")} | |||||
| </DialogTitle> | |||||
| <FormProvider {...formProps}> | |||||
| <form onSubmit={formProps.handleSubmit(handleSubmit)}> | |||||
| <DialogContent> | |||||
| <QcCategoryDetails /> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button> | |||||
| <Button type="submit" variant="contained"> | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </form> | |||||
| </FormProvider> | |||||
| </Dialog> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Tab2QcCategoryManagement; | |||||
| @@ -0,0 +1,226 @@ | |||||
| "use client"; | |||||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import SearchResults, { Column } from "../SearchResults"; | |||||
| import EditNote from "@mui/icons-material/EditNote"; | |||||
| import { fetchQcItemsForAll } from "@/app/api/settings/qcItemAll/actions"; | |||||
| import { QcItemResult } from "@/app/api/settings/qcItemAll"; | |||||
| import { | |||||
| deleteDialog, | |||||
| errorDialogWithContent, | |||||
| submitDialog, | |||||
| successDialog, | |||||
| } from "../Swal/CustomAlerts"; | |||||
| import { | |||||
| deleteQcItemWithValidation, | |||||
| canDeleteQcItem, | |||||
| saveQcItemWithValidation, | |||||
| SaveQcItemInputs, | |||||
| } from "@/app/api/settings/qcItemAll/actions"; | |||||
| import Delete from "@mui/icons-material/Delete"; | |||||
| import { Add } from "@mui/icons-material"; | |||||
| import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from "@mui/material"; | |||||
| import QcItemDetails from "../QcItemSave/QcItemDetails"; | |||||
| import { FormProvider, useForm } from "react-hook-form"; | |||||
| type SearchQuery = Partial<Omit<QcItemResult, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const Tab3QcItemManagement: React.FC = () => { | |||||
| const { t } = useTranslation("qcItemAll"); | |||||
| const [qcItems, setQcItems] = useState<QcItemResult[]>([]); | |||||
| const [filteredQcItems, setFilteredQcItems] = useState<QcItemResult[]>([]); | |||||
| const [openDialog, setOpenDialog] = useState(false); | |||||
| const [editingItem, setEditingItem] = useState<QcItemResult | null>(null); | |||||
| useEffect(() => { | |||||
| loadItems(); | |||||
| }, []); | |||||
| const loadItems = async () => { | |||||
| const items = await fetchQcItemsForAll(); | |||||
| setQcItems(items); | |||||
| setFilteredQcItems(items); | |||||
| }; | |||||
| const formProps = useForm<SaveQcItemInputs>({ | |||||
| defaultValues: { | |||||
| code: "", | |||||
| name: "", | |||||
| description: "", | |||||
| }, | |||||
| }); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: t("Code"), paramName: "code", type: "text" }, | |||||
| { label: t("Name"), paramName: "name", type: "text" }, | |||||
| ], | |||||
| [t] | |||||
| ); | |||||
| const onReset = useCallback(() => { | |||||
| setFilteredQcItems(qcItems); | |||||
| }, [qcItems]); | |||||
| const handleEdit = useCallback((qcItem: QcItemResult) => { | |||||
| setEditingItem(qcItem); | |||||
| formProps.reset({ | |||||
| id: qcItem.id, | |||||
| code: qcItem.code, | |||||
| name: qcItem.name, | |||||
| description: qcItem.description || "", | |||||
| }); | |||||
| setOpenDialog(true); | |||||
| }, [formProps]); | |||||
| const handleAdd = useCallback(() => { | |||||
| setEditingItem(null); | |||||
| formProps.reset({ | |||||
| code: "", | |||||
| name: "", | |||||
| description: "", | |||||
| }); | |||||
| setOpenDialog(true); | |||||
| }, [formProps]); | |||||
| const handleSubmit = useCallback(async (data: SaveQcItemInputs) => { | |||||
| await submitDialog(async () => { | |||||
| try { | |||||
| const response = await saveQcItemWithValidation(data); | |||||
| if (response.errors) { | |||||
| let errorContents = ""; | |||||
| for (const [key, value] of Object.entries(response.errors)) { | |||||
| formProps.setError(key as keyof SaveQcItemInputs, { | |||||
| type: "custom", | |||||
| message: value, | |||||
| }); | |||||
| errorContents = errorContents + t(value) + "<br>"; | |||||
| } | |||||
| errorDialogWithContent(t("Submit Error"), errorContents, t); | |||||
| } else { | |||||
| await successDialog(t("Submit Success"), t); | |||||
| setOpenDialog(false); | |||||
| await loadItems(); | |||||
| } | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Submit Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, [formProps, t]); | |||||
| const handleDelete = useCallback(async (qcItem: QcItemResult) => { | |||||
| // Check if can delete first | |||||
| const canDelete = await canDeleteQcItem(qcItem.id); | |||||
| if (!canDelete) { | |||||
| errorDialogWithContent( | |||||
| t("Cannot Delete"), | |||||
| t("Cannot delete QcItem. It is linked to one or more QcCategories."), | |||||
| t | |||||
| ); | |||||
| return; | |||||
| } | |||||
| deleteDialog(async () => { | |||||
| try { | |||||
| const response = await deleteQcItemWithValidation(qcItem.id); | |||||
| if (!response.success || !response.canDelete) { | |||||
| errorDialogWithContent( | |||||
| t("Delete Error"), | |||||
| response.message || t("Cannot Delete"), | |||||
| t | |||||
| ); | |||||
| } else { | |||||
| await successDialog(t("Delete Success"), t); | |||||
| await loadItems(); | |||||
| } | |||||
| } catch (error) { | |||||
| errorDialogWithContent(t("Delete Error"), String(error), t); | |||||
| } | |||||
| }, t); | |||||
| }, [t]); | |||||
| const columnWidthSx = (width = "10%") => { | |||||
| return { width: width, whiteSpace: "nowrap" }; | |||||
| }; | |||||
| const columns = useMemo<Column<QcItemResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "id", | |||||
| label: t("Details"), | |||||
| onClick: handleEdit, | |||||
| buttonIcon: <EditNote />, | |||||
| sx: columnWidthSx("150px"), | |||||
| }, | |||||
| { name: "code", label: t("Code"), sx: columnWidthSx() }, | |||||
| { name: "name", label: t("Name"), sx: columnWidthSx() }, | |||||
| { name: "description", label: t("Description") }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Delete"), | |||||
| onClick: handleDelete, | |||||
| buttonIcon: <Delete />, | |||||
| buttonColor: "error", | |||||
| }, | |||||
| ], | |||||
| [t, handleEdit, handleDelete] | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <Stack direction="row" justifyContent="flex-end" sx={{ mb: 2 }}> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| onClick={handleAdd} | |||||
| > | |||||
| {t("Create Qc Item")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| setFilteredQcItems( | |||||
| qcItems.filter( | |||||
| (qi) => | |||||
| (!query.code || qi.code.toLowerCase().includes(query.code.toLowerCase())) && | |||||
| (!query.name || qi.name.toLowerCase().includes(query.name.toLowerCase())) | |||||
| ) | |||||
| ); | |||||
| }} | |||||
| onReset={onReset} | |||||
| /> | |||||
| <SearchResults<QcItemResult> | |||||
| items={filteredQcItems} | |||||
| columns={columns} | |||||
| /> | |||||
| {/* Add/Edit Dialog */} | |||||
| <Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth> | |||||
| <DialogTitle> | |||||
| {editingItem ? t("Edit Qc Item") : t("Create Qc Item")} | |||||
| </DialogTitle> | |||||
| <FormProvider {...formProps}> | |||||
| <form onSubmit={formProps.handleSubmit(handleSubmit)}> | |||||
| <DialogContent> | |||||
| <QcItemDetails /> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button> | |||||
| <Button type="submit" variant="contained"> | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </form> | |||||
| </FormProvider> | |||||
| </Dialog> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Tab3QcItemManagement; | |||||
| @@ -303,12 +303,6 @@ const Shop: React.FC = () => { | |||||
| } | } | ||||
| }, [searchParams]); | }, [searchParams]); | ||||
| useEffect(() => { | |||||
| if (activeTab === 0) { | |||||
| fetchAllShops(); | |||||
| } | |||||
| }, [activeTab]); | |||||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | ||||
| setActiveTab(newValue); | setActiveTab(newValue); | ||||
| // Update URL to reflect the selected tab | // Update URL to reflect the selected tab | ||||
| @@ -30,7 +30,7 @@ import { | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import AddIcon from "@mui/icons-material/Add"; | import AddIcon from "@mui/icons-material/Add"; | ||||
| import SaveIcon from "@mui/icons-material/Save"; | import SaveIcon from "@mui/icons-material/Save"; | ||||
| import { useState, useEffect, useMemo } from "react"; | |||||
| import { useState, useMemo } from "react"; | |||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; | import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; | ||||
| @@ -50,7 +50,7 @@ const TruckLane: React.FC = () => { | |||||
| const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [truckData, setTruckData] = useState<Truck[]>([]); | const [truckData, setTruckData] = useState<Truck[]>([]); | ||||
| const [loading, setLoading] = useState<boolean>(true); | |||||
| const [loading, setLoading] = useState<boolean>(false); | |||||
| const [error, setError] = useState<string | null>(null); | const [error, setError] = useState<string | null>(null); | ||||
| const [filters, setFilters] = useState<Record<string, string>>({}); | const [filters, setFilters] = useState<Record<string, string>>({}); | ||||
| const [page, setPage] = useState(0); | const [page, setPage] = useState(0); | ||||
| @@ -65,32 +65,6 @@ const TruckLane: React.FC = () => { | |||||
| const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false); | const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false); | ||||
| const [snackbarMessage, setSnackbarMessage] = useState<string>(""); | const [snackbarMessage, setSnackbarMessage] = useState<string>(""); | ||||
| useEffect(() => { | |||||
| const fetchTruckLanes = async () => { | |||||
| setLoading(true); | |||||
| setError(null); | |||||
| try { | |||||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | |||||
| // Get unique truckLanceCodes only | |||||
| const uniqueCodes = new Map<string, Truck>(); | |||||
| (data || []).forEach((truck) => { | |||||
| const code = String(truck.truckLanceCode || "").trim(); | |||||
| if (code && !uniqueCodes.has(code)) { | |||||
| uniqueCodes.set(code, truck); | |||||
| } | |||||
| }); | |||||
| setTruckData(Array.from(uniqueCodes.values())); | |||||
| } catch (err: any) { | |||||
| console.error("Failed to load truck lanes:", err); | |||||
| setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }; | |||||
| fetchTruckLanes(); | |||||
| }, [t]); | |||||
| // Client-side filtered rows (contains-matching) | // Client-side filtered rows (contains-matching) | ||||
| const filteredRows = useMemo(() => { | const filteredRows = useMemo(() => { | ||||
| const fKeys = Object.keys(filters).filter((k) => String(filters[k] ?? "").trim() !== ""); | const fKeys = Object.keys(filters).filter((k) => String(filters[k] ?? "").trim() !== ""); | ||||
| @@ -125,9 +99,27 @@ const TruckLane: React.FC = () => { | |||||
| return filteredRows.slice(startIndex, startIndex + rowsPerPage); | return filteredRows.slice(startIndex, startIndex + rowsPerPage); | ||||
| }, [filteredRows, page, rowsPerPage]); | }, [filteredRows, page, rowsPerPage]); | ||||
| const handleSearch = (inputs: Record<string, string>) => { | |||||
| setFilters(inputs); | |||||
| setPage(0); // Reset to first page when searching | |||||
| const handleSearch = async (inputs: Record<string, string>) => { | |||||
| setLoading(true); | |||||
| setError(null); | |||||
| try { | |||||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | |||||
| const uniqueCodes = new Map<string, Truck>(); | |||||
| (data || []).forEach((truck) => { | |||||
| const code = String(truck.truckLanceCode ?? "").trim(); | |||||
| if (code && !uniqueCodes.has(code)) { | |||||
| uniqueCodes.set(code, truck); | |||||
| } | |||||
| }); | |||||
| setTruckData(Array.from(uniqueCodes.values())); | |||||
| setFilters(inputs); | |||||
| setPage(0); | |||||
| } catch (err: any) { | |||||
| console.error("Failed to load truck lanes:", err); | |||||
| setError(err?.message ?? String(err) ?? t("Failed to load truck lanes")); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }; | }; | ||||
| const handlePageChange = (event: unknown, newPage: number) => { | const handlePageChange = (event: unknown, newPage: number) => { | ||||
| @@ -233,24 +225,6 @@ const TruckLane: React.FC = () => { | |||||
| } | } | ||||
| }; | }; | ||||
| if (loading) { | |||||
| return ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (error) { | |||||
| return ( | |||||
| <Box> | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | |||||
| {error} | |||||
| </Alert> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| const criteria: Criterion<SearchParamNames>[] = [ | const criteria: Criterion<SearchParamNames>[] = [ | ||||
| { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" }, | { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" }, | ||||
| { type: "time", label: t("Departure Time"), paramName: "departureTime" }, | { type: "time", label: t("Departure Time"), paramName: "departureTime" }, | ||||
| @@ -265,6 +239,7 @@ const TruckLane: React.FC = () => { | |||||
| criteria={criteria as Criterion<string>[]} | criteria={criteria as Criterion<string>[]} | ||||
| onSearch={handleSearch} | onSearch={handleSearch} | ||||
| onReset={() => { | onReset={() => { | ||||
| setTruckData([]); | |||||
| setFilters({}); | setFilters({}); | ||||
| }} | }} | ||||
| /> | /> | ||||
| @@ -284,7 +259,17 @@ const TruckLane: React.FC = () => { | |||||
| {t("Add Truck Lane")} | {t("Add Truck Lane")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| {error && ( | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | |||||
| {error} | |||||
| </Alert> | |||||
| )} | |||||
| {loading ? ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| @@ -356,6 +341,7 @@ const TruckLane: React.FC = () => { | |||||
| rowsPerPageOptions={[5, 10, 25, 50]} | rowsPerPageOptions={[5, 10, 25, 50]} | ||||
| /> | /> | ||||
| </TableContainer> | </TableContainer> | ||||
| )} | |||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| @@ -150,7 +150,7 @@ const SearchPage: React.FC<Props> = ({ dataList }) => { | |||||
| { name: "itemDescription", label: t("Item") }, | { name: "itemDescription", label: t("Item") }, | ||||
| { name: "lotNo", label: t("Lot No.") }, | { name: "lotNo", label: t("Lot No.") }, | ||||
| { name: "storeLocation", label: t("Location") }, | { name: "storeLocation", label: t("Location") }, | ||||
| { name: "missQty", label: t("Miss Qty") }, | |||||
| { name: "issueQty", label: t("Miss Qty") }, | |||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| label: t("Action"), | label: t("Action"), | ||||
| @@ -176,7 +176,7 @@ const SearchPage: React.FC<Props> = ({ dataList }) => { | |||||
| { name: "itemDescription", label: t("Item") }, | { name: "itemDescription", label: t("Item") }, | ||||
| { name: "lotNo", label: t("Lot No.") }, | { name: "lotNo", label: t("Lot No.") }, | ||||
| { name: "storeLocation", label: t("Location") }, | { name: "storeLocation", label: t("Location") }, | ||||
| { name: "badItemQty", label: t("Defective Qty") }, | |||||
| { name: "issueQty", label: t("Defective Qty") }, | |||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| label: t("Action"), | label: t("Action"), | ||||
| @@ -77,16 +77,10 @@ const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => { | |||||
| sorted.forEach((item) => { | sorted.forEach((item) => { | ||||
| const currentBalance = balanceMap.get(item.itemId) || 0; | const currentBalance = balanceMap.get(item.itemId) || 0; | ||||
| let newBalance = currentBalance; | |||||
| // 根据类型计算余额 | |||||
| if (item.transactionType === "IN") { | |||||
| newBalance = currentBalance + item.qty; | |||||
| } else if (item.transactionType === "OUT") { | |||||
| newBalance = currentBalance - item.qty; | |||||
| } | |||||
| balanceMap.set(item.itemId, newBalance); | |||||
| // 格式化日期 - 优先使用 date 字段 | // 格式化日期 - 优先使用 date 字段 | ||||
| let formattedDate = ""; | let formattedDate = ""; | ||||
| @@ -128,7 +122,7 @@ const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => { | |||||
| formattedDate, | formattedDate, | ||||
| inQty: item.transactionType === "IN" ? item.qty : 0, | inQty: item.transactionType === "IN" ? item.qty : 0, | ||||
| outQty: item.transactionType === "OUT" ? item.qty : 0, | outQty: item.transactionType === "OUT" ? item.qty : 0, | ||||
| balanceQty: item.balanceQty ? item.balanceQty : newBalance, | |||||
| balanceQty: item.balanceQty ?? 0, | |||||
| }); | }); | ||||
| }); | }); | ||||
| @@ -404,6 +404,17 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| </Box> | </Box> | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| <TablePagination | |||||
| component="div" | |||||
| count={total} | |||||
| page={page} | |||||
| onPageChange={handleChangePage} | |||||
| rowsPerPage={pageSize === "all" ? total : (pageSize as number)} | |||||
| onRowsPerPageChange={handleChangeRowsPerPage} | |||||
| rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| /> | |||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| @@ -354,6 +354,17 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| </Box> | </Box> | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| <TablePagination | |||||
| component="div" | |||||
| count={total} | |||||
| page={page} | |||||
| onPageChange={handleChangePage} | |||||
| rowsPerPage={pageSize === "all" ? total : (pageSize as number)} | |||||
| onRowsPerPageChange={handleChangeRowsPerPage} | |||||
| rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| /> | |||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| @@ -443,6 +443,17 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| </Box> | </Box> | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| <TablePagination | |||||
| component="div" | |||||
| count={total} | |||||
| page={page} | |||||
| onPageChange={handleChangePage} | |||||
| rowsPerPage={pageSize === "all" ? total : (pageSize as number)} | |||||
| onRowsPerPageChange={handleChangeRowsPerPage} | |||||
| rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| /> | |||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| @@ -73,6 +73,11 @@ | |||||
| "Last Ticket End": "Last Ticket End", | "Last Ticket End": "Last Ticket End", | ||||
| "Pick Time (min)": "Pick Time (min)", | "Pick Time (min)": "Pick Time (min)", | ||||
| "No truck schedules available for today": "No truck schedules available for today", | "No truck schedules available for today": "No truck schedules available for today", | ||||
| "No truck schedules available": "No truck schedules available", | |||||
| "Select Date": "Select Date", | |||||
| "Today": "Today", | |||||
| "Tomorrow": "Tomorrow", | |||||
| "Day After Tomorrow": "Day After Tomorrow", | |||||
| "Goods Receipt Status": "Goods Receipt Status", | "Goods Receipt Status": "Goods Receipt Status", | ||||
| "Filter": "Filter", | "Filter": "Filter", | ||||
| "All": "All", | "All": "All", | ||||
| @@ -9,5 +9,12 @@ | |||||
| "Back": "Back", | "Back": "Back", | ||||
| "Status": "Status", | "Status": "Status", | ||||
| "Complete": "Complete", | "Complete": "Complete", | ||||
| "Missing Data": "Missing Data" | |||||
| "Missing Data": "Missing Data", | |||||
| "Loading QC items...": "Loading QC items...", | |||||
| "Select a QC template to view items": "Select a QC template to view items", | |||||
| "No QC items in this template": "No QC items in this template", | |||||
| "QC Checklist": "QC Checklist", | |||||
| "QC Type": "QC Type", | |||||
| "IPQC": "IPQC", | |||||
| "EPQC": "EPQC" | |||||
| } | } | ||||
| @@ -0,0 +1,58 @@ | |||||
| { | |||||
| "Qc Item All": "QC Management", | |||||
| "Item and Qc Category Mapping": "Item and Qc Category Mapping", | |||||
| "Qc Category and Qc Item Mapping": "Qc Category and Qc Item Mapping", | |||||
| "Qc Category Management": "Qc Category Management", | |||||
| "Qc Item Management": "Qc Item Management", | |||||
| "Qc Category": "Qc Category", | |||||
| "Qc Item": "Qc Item", | |||||
| "Item": "Item", | |||||
| "Code": "Code", | |||||
| "Name": "Name", | |||||
| "Description": "Description", | |||||
| "Type": "Type", | |||||
| "Order": "Order", | |||||
| "Item Count": "Item Count", | |||||
| "Qc Item Count": "Qc Item Count", | |||||
| "Qc Category Count": "Qc Category Count", | |||||
| "Actions": "Actions", | |||||
| "View": "View", | |||||
| "Edit": "Edit", | |||||
| "Delete": "Delete", | |||||
| "Add": "Add", | |||||
| "Add Mapping": "Add Mapping", | |||||
| "Add Association": "Add Association", | |||||
| "Save": "Save", | |||||
| "Cancel": "Cancel", | |||||
| "Submit": "Submit", | |||||
| "Details": "Details", | |||||
| "Create Qc Category": "Create Qc Category", | |||||
| "Edit Qc Category": "Edit Qc Category", | |||||
| "Create Qc Item": "Create Qc Item", | |||||
| "Edit Qc Item": "Edit Qc Item", | |||||
| "Delete Success": "Delete Success", | |||||
| "Delete Error": "Delete Error", | |||||
| "Submit Success": "Submit Success", | |||||
| "Submit Error": "Submit Error", | |||||
| "Cannot Delete": "Cannot Delete", | |||||
| "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.", | |||||
| "Cannot delete QcItem. It is linked to one or more QcCategories.": "Cannot delete QcItem. It is linked to one or more QcCategories.", | |||||
| "Select Item": "Select Item", | |||||
| "Select Qc Category": "Select Qc Category", | |||||
| "Select Qc Item": "Select Qc Item", | |||||
| "Select Type": "Select Type", | |||||
| "Item Code": "Item Code", | |||||
| "Item Name": "Item Name", | |||||
| "Qc Category Code": "Qc Category Code", | |||||
| "Qc Category Name": "Qc Category Name", | |||||
| "Qc Item Code": "Qc Item Code", | |||||
| "Qc Item Name": "Qc Item Name", | |||||
| "Mapping Details": "Mapping Details", | |||||
| "Association Details": "Association Details", | |||||
| "No mappings found": "No mappings found", | |||||
| "No associations found": "No associations found", | |||||
| "No data available": "No data available", | |||||
| "Confirm Delete": "Confirm Delete", | |||||
| "Are you sure you want to delete this item?": "Are you sure you want to delete this item?" | |||||
| } | |||||
| @@ -65,7 +65,7 @@ | |||||
| "Last updated": "最後更新", | "Last updated": "最後更新", | ||||
| "Truck Schedule": "車輛班次", | "Truck Schedule": "車輛班次", | ||||
| "Time Remaining": "剩餘時間", | "Time Remaining": "剩餘時間", | ||||
| "No. of Shops": "門店數量", | |||||
| "No. of Shops": "門店數量[提票數量]", | |||||
| "Total Items": "總貨品數", | "Total Items": "總貨品數", | ||||
| "Tickets Released": "已發放成品出倉單", | "Tickets Released": "已發放成品出倉單", | ||||
| "First Ticket Start": "首單開始時間", | "First Ticket Start": "首單開始時間", | ||||
| @@ -73,6 +73,11 @@ | |||||
| "Last Ticket End": "末單結束時間", | "Last Ticket End": "末單結束時間", | ||||
| "Pick Time (min)": "揀貨時間(分鐘)", | "Pick Time (min)": "揀貨時間(分鐘)", | ||||
| "No truck schedules available for today": "今日無車輛調度計劃", | "No truck schedules available for today": "今日無車輛調度計劃", | ||||
| "No truck schedules available": "無車輛調度計劃", | |||||
| "Select Date": "請選擇日期", | |||||
| "Today": "是日", | |||||
| "Tomorrow": "翌日", | |||||
| "Day After Tomorrow": "後日", | |||||
| "Goods Receipt Status": "貨物接收狀態", | "Goods Receipt Status": "貨物接收狀態", | ||||
| "Filter": "篩選", | "Filter": "篩選", | ||||
| "All": "全部", | "All": "全部", | ||||
| @@ -33,6 +33,11 @@ | |||||
| "Start Time": "開始時間", | "Start Time": "開始時間", | ||||
| "Difference": "差異", | "Difference": "差異", | ||||
| "stockTaking": "盤點中", | "stockTaking": "盤點中", | ||||
| "rejected": "已拒絕", | |||||
| "miss": "缺貨", | |||||
| "bad": "不良", | |||||
| "expiry": "過期", | |||||
| "Bom Req. Qty": "需求數(BOM單位)", | |||||
| "selected stock take qty": "已選擇盤點數量", | "selected stock take qty": "已選擇盤點數量", | ||||
| "book qty": "帳面庫存", | "book qty": "帳面庫存", | ||||
| "start time": "開始時間", | "start time": "開始時間", | ||||
| @@ -36,12 +36,19 @@ | |||||
| "LocationCode": "預設位置", | "LocationCode": "預設位置", | ||||
| "DefaultLocationCode": "預設位置", | "DefaultLocationCode": "預設位置", | ||||
| "Special Type": "特殊類型", | "Special Type": "特殊類型", | ||||
| "None": "無", | |||||
| "None": "正常", | |||||
| "isEgg": "雞蛋", | "isEgg": "雞蛋", | ||||
| "isFee": "費用", | "isFee": "費用", | ||||
| "isBag": "袋子", | "isBag": "袋子", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| "Status": "狀態", | "Status": "狀態", | ||||
| "Complete": "完成", | "Complete": "完成", | ||||
| "Missing Data": "缺少資料" | |||||
| "Missing Data": "缺少資料", | |||||
| "Loading QC items...": "正在加載質檢項目...", | |||||
| "Select a QC template to view items": "選擇質檢模板以查看項目", | |||||
| "No QC items in this template": "此模板無質檢項目", | |||||
| "QC Checklist": "質檢項目", | |||||
| "QC Type": "質檢種類", | |||||
| "IPQC": "IPQC", | |||||
| "EPQC": "EPQC" | |||||
| } | } | ||||
| @@ -203,6 +203,7 @@ | |||||
| "No Group": "沒有組", | "No Group": "沒有組", | ||||
| "No created items": "沒有創建物料", | "No created items": "沒有創建物料", | ||||
| "Order Quantity": "需求數", | "Order Quantity": "需求數", | ||||
| "Bom Req. Qty": "需求數(BOM單位)", | |||||
| "Selected": "已選擇", | "Selected": "已選擇", | ||||
| "Are you sure you want to delete this procoess?": "您確定要刪除此工序嗎?", | "Are you sure you want to delete this procoess?": "您確定要刪除此工序嗎?", | ||||
| "Please select item": "請選擇物料", | "Please select item": "請選擇物料", | ||||
| @@ -0,0 +1,58 @@ | |||||
| { | |||||
| "Qc Item All": "QC 綜合管理", | |||||
| "Item and Qc Category Mapping": "物料與品檢模板映射", | |||||
| "Qc Category and Qc Item Mapping": "品檢模板與品檢項目映射", | |||||
| "Qc Category Management": "品檢模板管理", | |||||
| "Qc Item Management": "品檢項目管理", | |||||
| "Qc Category": "品檢模板", | |||||
| "Qc Item": "品檢項目", | |||||
| "Item": "物料", | |||||
| "Code": "編號", | |||||
| "Name": "名稱", | |||||
| "Description": "描述", | |||||
| "Type": "類型", | |||||
| "Order": "順序", | |||||
| "Item Count": "關聯物料數量", | |||||
| "Qc Item Count": "關聯品檢項目數量", | |||||
| "Qc Category Count": "關聯品檢模板數量", | |||||
| "Actions": "操作", | |||||
| "View": "查看", | |||||
| "Edit": "編輯", | |||||
| "Delete": "刪除", | |||||
| "Add": "新增", | |||||
| "Add Mapping": "新增映射", | |||||
| "Add Association": "新增關聯", | |||||
| "Save": "儲存", | |||||
| "Cancel": "取消", | |||||
| "Submit": "提交", | |||||
| "Details": "詳情", | |||||
| "Create Qc Category": "新增品檢模板", | |||||
| "Edit Qc Category": "編輯品檢模板", | |||||
| "Create Qc Item": "新增品檢項目", | |||||
| "Edit Qc Item": "編輯品檢項目", | |||||
| "Delete Success": "刪除成功", | |||||
| "Delete Error": "刪除失敗", | |||||
| "Submit Success": "提交成功", | |||||
| "Submit Error": "提交失敗", | |||||
| "Cannot Delete": "無法刪除", | |||||
| "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "無法刪除品檢模板。它有 {itemCount} 個物料和 {qcItemCount} 個品檢項目與其關聯。", | |||||
| "Cannot delete QcItem. It is linked to one or more QcCategories.": "無法刪除品檢項目。它與一個或多個品檢模板關聯。", | |||||
| "Select Item": "選擇物料", | |||||
| "Select Qc Category": "選擇品檢模板", | |||||
| "Select Qc Item": "選擇品檢項目", | |||||
| "Select Type": "選擇類型", | |||||
| "Item Code": "物料編號", | |||||
| "Item Name": "物料名稱", | |||||
| "Qc Category Code": "品檢模板編號", | |||||
| "Qc Category Name": "品檢模板名稱", | |||||
| "Qc Item Code": "品檢項目編號", | |||||
| "Qc Item Name": "品檢項目名稱", | |||||
| "Mapping Details": "映射詳情", | |||||
| "Association Details": "關聯詳情", | |||||
| "No mappings found": "未找到映射", | |||||
| "No associations found": "未找到關聯", | |||||
| "No data available": "暫無數據", | |||||
| "Confirm Delete": "確認刪除", | |||||
| "Are you sure you want to delete this item?": "您確定要刪除此項目嗎?" | |||||
| } | |||||