| @@ -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"> | ||||
| @@ -220,7 +220,7 @@ const CreateItem: React.FC<Props> = ({ | |||||
| variant="scrollable" | variant="scrollable" | ||||
| > | > | ||||
| <Tab label={t("Product / Material Details")} iconPosition="end" /> | <Tab label={t("Product / Material Details")} iconPosition="end" /> | ||||
| <Tab label={t("Qc items")} iconPosition="end" /> | |||||
| {/* <Tab label={t("Qc items")} iconPosition="end" /> */} | |||||
| </Tabs> | </Tabs> | ||||
| {serverError && ( | {serverError && ( | ||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | <Typography variant="body2" color="error" alignSelf="flex-end"> | ||||
| @@ -18,6 +18,7 @@ import CollapsibleCard from "../CollapsibleCard"; | |||||
| import { EscalationResult } from "@/app/api/escalation"; | import { EscalationResult } from "@/app/api/escalation"; | ||||
| import EscalationLogTable from "./escalation/EscalationLogTable"; | import EscalationLogTable from "./escalation/EscalationLogTable"; | ||||
| import { TruckScheduleDashboard } from "./truckSchedule"; | import { TruckScheduleDashboard } from "./truckSchedule"; | ||||
| import { GoodsReceiptStatus } from "./goodsReceiptStatus"; | |||||
| type Props = { | type Props = { | ||||
| // iqc: IQCItems[] | undefined | // iqc: IQCItems[] | undefined | ||||
| escalationLogs: EscalationResult[] | escalationLogs: EscalationResult[] | ||||
| @@ -50,20 +51,28 @@ const DashboardPage: React.FC<Props> = ({ | |||||
| </CardContent> | </CardContent> | ||||
| </CollapsibleCard> | </CollapsibleCard> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | |||||
| <CollapsibleCard title={t("Goods Receipt Status")} defaultOpen={true}> | |||||
| <CardContent> | |||||
| <GoodsReceiptStatus /> | |||||
| </CardContent> | |||||
| </CollapsibleCard> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <CollapsibleCard | <CollapsibleCard | ||||
| title={`${t("Responsible Escalation List")} (${t("pending")} : ${ | title={`${t("Responsible Escalation List")} (${t("pending")} : ${ | ||||
| getPendingLog().length > 0 ? getPendingLog().length : t("No")})`} | getPendingLog().length > 0 ? getPendingLog().length : t("No")})`} | ||||
| showFilter={true} | showFilter={true} | ||||
| filterText={t("show completed logs")} | filterText={t("show completed logs")} | ||||
| // defaultOpen={getPendingLog().length > 0} // TODO Fix default not opening | |||||
| > | > | ||||
| <CardContent> | <CardContent> | ||||
| <EscalationLogTable items={escLog}/> | <EscalationLogTable items={escLog}/> | ||||
| </CardContent> | </CardContent> | ||||
| </CollapsibleCard> | </CollapsibleCard> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | |||||
| {/* Hidden: Progress chart - not in use currently */} | |||||
| {/* <Grid item xs={12}> | |||||
| <CollapsibleCard title={t("Progress chart")}> | <CollapsibleCard title={t("Progress chart")}> | ||||
| <CardContent> | <CardContent> | ||||
| <Grid container spacing={3}> | <Grid container spacing={3}> | ||||
| @@ -79,9 +88,10 @@ const DashboardPage: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| </CardContent> | </CardContent> | ||||
| </CollapsibleCard> | </CollapsibleCard> | ||||
| </Grid> | |||||
| </Grid> */} | |||||
| <Grid item xs={12}> | |||||
| {/* Hidden: Warehouse status - not in use currently */} | |||||
| {/* <Grid item xs={12}> | |||||
| <CollapsibleCard title={t("Warehouse status")}> | <CollapsibleCard title={t("Warehouse status")}> | ||||
| <CardContent> | <CardContent> | ||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| @@ -95,31 +105,10 @@ const DashboardPage: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| {/*<Grid item xs={12} md={6}> | |||||
| <Grid container spacing={2}> | |||||
| <Grid item xs={12} sm={6}> | |||||
| <DashboardBox | |||||
| title={t("Temperature status")} | |||||
| value="--" | |||||
| unit="°C" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} sm={6}> | |||||
| <DashboardBox | |||||
| title={t("Humidity status")} | |||||
| value="--" | |||||
| unit="%" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <DashboardLineChart /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Grid>*/} | |||||
| </Grid> | </Grid> | ||||
| </CardContent> | </CardContent> | ||||
| </CollapsibleCard> | </CollapsibleCard> | ||||
| </Grid> | |||||
| </Grid> */} | |||||
| </Grid> | </Grid> | ||||
| </ThemeProvider> | </ThemeProvider> | ||||
| ); | ); | ||||
| @@ -0,0 +1,163 @@ | |||||
| "use client"; | |||||
| import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| FormControl, | |||||
| InputLabel, | |||||
| Select, | |||||
| MenuItem, | |||||
| Card, | |||||
| CardContent, | |||||
| Stack, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| CircularProgress, | |||||
| Chip | |||||
| } from '@mui/material'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import dayjs from 'dayjs'; | |||||
| interface GoodsReceiptStatusItem { | |||||
| id: string; | |||||
| } | |||||
| const GoodsReceiptStatus: React.FC = () => { | |||||
| const { t } = useTranslation("dashboard"); | |||||
| const [selectedFilter, setSelectedFilter] = useState<string>(""); | |||||
| const [data, setData] = useState<GoodsReceiptStatusItem[]>([]); | |||||
| const [loading, setLoading] = useState<boolean>(false); | |||||
| const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null); | |||||
| const [isClient, setIsClient] = useState<boolean>(false); | |||||
| useEffect(() => { | |||||
| setIsClient(true); | |||||
| setCurrentTime(dayjs()); | |||||
| }, []); | |||||
| const loadData = useCallback(async () => { | |||||
| try { | |||||
| setData([]); | |||||
| } catch (error) { | |||||
| console.error('Error fetching goods receipt status:', error); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| loadData(); | |||||
| const refreshInterval = setInterval(() => { | |||||
| loadData(); | |||||
| }, 5 * 60 * 1000); | |||||
| return () => clearInterval(refreshInterval); | |||||
| }, [loadData]); | |||||
| useEffect(() => { | |||||
| if (!isClient) return; | |||||
| const timeInterval = setInterval(() => { | |||||
| setCurrentTime(dayjs()); | |||||
| }, 60 * 1000); | |||||
| return () => clearInterval(timeInterval); | |||||
| }, [isClient]); | |||||
| const filteredData = useMemo(() => { | |||||
| if (!selectedFilter) return data; | |||||
| return data.filter(item => true); | |||||
| }, [data, selectedFilter]); | |||||
| return ( | |||||
| <Card sx={{ mb: 2 }}> | |||||
| <CardContent> | |||||
| {/* Filter */} | |||||
| <Stack direction="row" spacing={2} sx={{ mb: 3 }}> | |||||
| <FormControl sx={{ minWidth: 150 }} size="small"> | |||||
| <InputLabel id="filter-select-label" shrink={true}> | |||||
| {t("Filter")} | |||||
| </InputLabel> | |||||
| <Select | |||||
| labelId="filter-select-label" | |||||
| id="filter-select" | |||||
| value={selectedFilter} | |||||
| label={t("Filter")} | |||||
| onChange={(e) => setSelectedFilter(e.target.value)} | |||||
| displayEmpty | |||||
| > | |||||
| <MenuItem value="">{t("All")}</MenuItem> | |||||
| {/* TODO: Add filter options when implementing */} | |||||
| </Select> | |||||
| </FormControl> | |||||
| <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') : '--:--:--'} | |||||
| </Typography> | |||||
| </Stack> | |||||
| {/* Table */} | |||||
| <Box sx={{ mt: 2 }}> | |||||
| {loading ? ( | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <TableContainer component={Paper}> | |||||
| <Table size="small" sx={{ minWidth: 1200 }}> | |||||
| <TableHead> | |||||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("Column 1")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("Column 2")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("Column 3")}</TableCell> | |||||
| {/* TODO: Add table columns when implementing */} | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {filteredData.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={3} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| filteredData.map((row, index) => ( | |||||
| <TableRow | |||||
| key={row.id || index} | |||||
| sx={{ | |||||
| '&:hover': { backgroundColor: 'grey.50' } | |||||
| }} | |||||
| > | |||||
| <TableCell> | |||||
| {/* TODO: Add table cell content when implementing */} | |||||
| - | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| - | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| - | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| )} | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default GoodsReceiptStatus; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default as GoodsReceiptStatus } from './GoodsReceiptStatus'; | |||||
| @@ -237,11 +237,6 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| return ( | return ( | ||||
| <Card sx={{ mb: 2 }}> | <Card sx={{ mb: 2 }}> | ||||
| <CardContent> | <CardContent> | ||||
| {/* Title */} | |||||
| <Typography variant="h5" sx={{ mb: 3, fontWeight: 600 }}> | |||||
| {t("Truck Schedule Dashboard")} | |||||
| </Typography> | |||||
| {/* Filter */} | {/* Filter */} | ||||
| <Stack direction="row" spacing={2} sx={{ mb: 3 }}> | <Stack direction="row" spacing={2} sx={{ mb: 3 }}> | ||||
| <FormControl sx={{ minWidth: 150 }} size="small"> | <FormControl sx={{ minWidth: 150 }} size="small"> | ||||
| @@ -7,14 +7,11 @@ import { useTranslation } from "react-i18next"; | |||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | |||||
| import { Chip } from "@mui/material"; | import { Chip } from "@mui/material"; | ||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import axios from "axios"; | import axios from "axios"; | ||||
| import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | import axiosInstance from "@/app/(main)/axios/axiosInstance"; | ||||
| import { deleteItem } from "@/app/api/settings/item/actions"; | |||||
| import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | |||||
| type Props = { | type Props = { | ||||
| items: ItemsResult[]; | items: ItemsResult[]; | ||||
| @@ -135,22 +132,6 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| refetchData, | refetchData, | ||||
| ]); | ]); | ||||
| const onDeleteClick = useCallback( | |||||
| (item: ItemsResult) => { | |||||
| deleteDialog(async () => { | |||||
| if (item.id) { | |||||
| const itemId = typeof item.id === "string" ? parseInt(item.id, 10) : item.id; | |||||
| if (!isNaN(itemId)) { | |||||
| await deleteItem(itemId); | |||||
| await refetchData(filterObj); | |||||
| await successDialog(t("Delete Success"), t); | |||||
| } | |||||
| } | |||||
| }, t); | |||||
| }, | |||||
| [refetchData, filterObj, t], | |||||
| ); | |||||
| const columns = useMemo<Column<ItemsResultWithStatus>[]>( | const columns = useMemo<Column<ItemsResultWithStatus>[]>( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| @@ -158,22 +139,34 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| label: t("Details"), | label: t("Details"), | ||||
| onClick: onDetailClick, | onClick: onDetailClick, | ||||
| buttonIcon: <EditNote />, | buttonIcon: <EditNote />, | ||||
| sx: { width: 80 }, | |||||
| }, | }, | ||||
| { | { | ||||
| name: "code", | name: "code", | ||||
| label: t("Code"), | label: t("Code"), | ||||
| sx: { width: 150 }, | |||||
| }, | }, | ||||
| { | { | ||||
| name: "name", | name: "name", | ||||
| label: t("Name"), | label: t("Name"), | ||||
| sx: { width: 250 }, | |||||
| }, | |||||
| { | |||||
| name: "LocationCode", | |||||
| label: t("LocationCode"), | |||||
| sx: { width: 150 }, | |||||
| }, | }, | ||||
| { | { | ||||
| name: "type", | name: "type", | ||||
| label: t("Type"), | label: t("Type"), | ||||
| sx: { width: 120 }, | |||||
| }, | }, | ||||
| { | { | ||||
| name: "status", | name: "status", | ||||
| label: t("Status"), | label: t("Status"), | ||||
| align: "center", | |||||
| headerAlign: "center", | |||||
| sx: { width: 120 }, | |||||
| renderCell: (item) => { | renderCell: (item) => { | ||||
| const status = item.status || checkItemStatus(item); | const status = item.status || checkItemStatus(item); | ||||
| if (status === "complete") { | if (status === "complete") { | ||||
| @@ -183,14 +176,8 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| } | } | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | |||||
| name: "action", | |||||
| label: t(""), | |||||
| buttonIcon: <GridDeleteIcon />, | |||||
| onClick: onDeleteClick, | |||||
| }, | |||||
| ], | ], | ||||
| [onDeleteClick, onDetailClick, t, checkItemStatus], | |||||
| [onDetailClick, t, checkItemStatus], | |||||
| ); | ); | ||||
| const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
| @@ -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, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -47,5 +47,20 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| ], required: false} | ], required: false} | ||||
| ] | ] | ||||
| }, | }, | ||||
| { | |||||
| id: "rep-003", | |||||
| title: "Report 3", | |||||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-report3`, | |||||
| fields: [ | |||||
| { label: "From Date", name: "fromDate", type: "date", required: true }, // Mandatory | |||||
| { label: "To Date", name: "toDate", type: "date", required: true }, // Mandatory | |||||
| { label: "Item Code", name: "itemCode", type: "text", required: false, placeholder: "e.g. FG"}, | |||||
| { label: "Item Type", name: "itemType", type: "select", required: false, | |||||
| options: [ | |||||
| { label: "FG", value: "FG" }, | |||||
| { label: "Material", value: "Mat" } | |||||
| ] }, | |||||
| ] | |||||
| } | |||||
| // Add Report 3 to 10 following the same pattern... | // Add Report 3 to 10 following the same pattern... | ||||
| ]; | ]; | ||||
| @@ -72,5 +72,12 @@ | |||||
| "Tickets Completed": "Tickets Completed", | "Tickets Completed": "Tickets Completed", | ||||
| "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", | |||||
| "Goods Receipt Status": "Goods Receipt Status", | |||||
| "Filter": "Filter", | |||||
| "All": "All", | |||||
| "Column 1": "Column 1", | |||||
| "Column 2": "Column 2", | |||||
| "Column 3": "Column 3", | |||||
| "No data available": "No data available" | |||||
| } | } | ||||
| @@ -72,5 +72,12 @@ | |||||
| "Tickets Completed": "已完成成品出倉單", | "Tickets Completed": "已完成成品出倉單", | ||||
| "Last Ticket End": "末單結束時間", | "Last Ticket End": "末單結束時間", | ||||
| "Pick Time (min)": "揀貨時間(分鐘)", | "Pick Time (min)": "揀貨時間(分鐘)", | ||||
| "No truck schedules available for today": "今日無車輛調度計劃" | |||||
| "No truck schedules available for today": "今日無車輛調度計劃", | |||||
| "Goods Receipt Status": "貨物接收狀態", | |||||
| "Filter": "篩選", | |||||
| "All": "全部", | |||||
| "Column 1": "欄位1", | |||||
| "Column 2": "欄位2", | |||||
| "Column 3": "欄位3", | |||||
| "No data available": "暫無資料" | |||||
| } | } | ||||
| @@ -33,7 +33,7 @@ | |||||
| "Search": "搜尋", | "Search": "搜尋", | ||||
| "Release": "發佈", | "Release": "發佈", | ||||
| "Actions": "操作", | "Actions": "操作", | ||||
| "LocationCode": "位置", | |||||
| "LocationCode": "預設位置", | |||||
| "DefaultLocationCode": "預設位置", | "DefaultLocationCode": "預設位置", | ||||
| "Special Type": "特殊類型", | "Special Type": "特殊類型", | ||||
| "None": "無", | "None": "無", | ||||