diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx index 4c64f92..3efaf70 100644 --- a/src/app/(main)/testing/page.tsx +++ b/src/app/(main)/testing/page.tsx @@ -4,13 +4,47 @@ import React, { useState } from "react"; import { Box, Grid, Paper, Typography, Button, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Stack, Table, - TableBody, TableCell, TableContainer, TableHead, TableRow + TableBody, TableCell, TableContainer, TableHead, TableRow, + Tabs, Tab // ← Added for tabs } from "@mui/material"; import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material"; import dayjs from "dayjs"; 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 ( + + ); +} + export default function TestingPage() { + // Tab state + const [tabValue, setTabValue] = useState(0); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + // --- 1. TSC Section States --- const [tscConfig, setTscConfig] = useState({ ip: '192.168.1.100', port: '9100' }); const [tscItems, setTscItems] = useState([ @@ -35,10 +69,22 @@ export default function TestingPage() { }); // --- 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 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); } }; + // Laser Print (Section 4 - original) const handleLaserPrint = async (row: any) => { const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; @@ -122,7 +169,6 @@ const [laserItems, setLaserItems] = useState([ const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; try { - // We'll create this endpoint in the backend next const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, @@ -132,24 +178,58 @@ const [laserItems, setLaserItems] = useState([ } 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 const Section = ({ title, children }: { title: string, children?: React.ReactNode }) => ( - - - - {title} - - {children || Waiting for implementation...} - - + + + {title} + + {children || Waiting for implementation...} + ); return ( - Printer Testing Dashboard + Printer Testing - - {/* 1. TSC Section */} + + + + + + + + +
setTscConfig({...tscConfig, ip: e.target.value})} /> @@ -181,8 +261,9 @@ const [laserItems, setLaserItems] = useState([
+
- {/* 2. DataFlex Section */} +
setDfConfig({...dfConfig, ip: e.target.value})} /> @@ -214,8 +295,9 @@ const [laserItems, setLaserItems] = useState([
+
- {/* 3. OnPack Section */} +
@@ -226,8 +308,9 @@ const [laserItems, setLaserItems] = useState([
+
- {/* 4. Laser Section (HANS600S-M) */} +
setLaserConfig({...laserConfig, ip: e.target.value})} /> @@ -283,7 +366,94 @@ const [laserItems, setLaserItems] = useState([ Note: HANS Laser requires pre-saved templates on the controller.
-
+ + + +
+ + setHansConfig({...hansConfig, ip: e.target.value})} + /> + setHansConfig({...hansConfig, port: e.target.value})} + /> + + + + + + + + Ch3 Text (SN) + Ch4 Text (Batch) + Obj3 Name + Obj4 Name + Action + + + + {hansItems.map(row => ( + + + handleItemChange(setHansItems, row.id, 'textChannel3', e.target.value)} + sx={{ minWidth: 180 }} + /> + + + handleItemChange(setHansItems, row.id, 'textChannel4', e.target.value)} + sx={{ minWidth: 140 }} + /> + + + handleItemChange(setHansItems, row.id, 'text3ObjectName', e.target.value)} + size="small" + /> + + + handleItemChange(setHansItems, row.id, 'text4ObjectName', e.target.value)} + size="small" + /> + + + + + + ))} + +
+
+ + TCP Push to EZCAD3 (Ch3/Ch4 via E3_SetTextObject) | IP:192.168.76.10:45678 | Backend: /print-laser-tcp + +
+
{/* Dialog for OnPack */} setIsPrinterModalOpen(false)} fullWidth maxWidth="sm"> diff --git a/src/components/CreateItem/CreateItem.tsx b/src/components/CreateItem/CreateItem.tsx index f4fd8e2..661cf6e 100644 --- a/src/components/CreateItem/CreateItem.tsx +++ b/src/components/CreateItem/CreateItem.tsx @@ -220,7 +220,7 @@ const CreateItem: React.FC = ({ variant="scrollable" > - + {/* */} {serverError && ( diff --git a/src/components/DashboardPage/DashboardPage.tsx b/src/components/DashboardPage/DashboardPage.tsx index d417a8a..840adcd 100644 --- a/src/components/DashboardPage/DashboardPage.tsx +++ b/src/components/DashboardPage/DashboardPage.tsx @@ -18,6 +18,7 @@ import CollapsibleCard from "../CollapsibleCard"; import { EscalationResult } from "@/app/api/escalation"; import EscalationLogTable from "./escalation/EscalationLogTable"; import { TruckScheduleDashboard } from "./truckSchedule"; +import { GoodsReceiptStatus } from "./goodsReceiptStatus"; type Props = { // iqc: IQCItems[] | undefined escalationLogs: EscalationResult[] @@ -50,20 +51,28 @@ const DashboardPage: React.FC = ({ + + + + + + + 0 ? getPendingLog().length : t("No")})`} showFilter={true} filterText={t("show completed logs")} - // defaultOpen={getPendingLog().length > 0} // TODO Fix default not opening + > - + {/* Hidden: Progress chart - not in use currently */} + {/* @@ -79,9 +88,10 @@ const DashboardPage: React.FC = ({ - + */} - + {/* Hidden: Warehouse status - not in use currently */} + {/* @@ -95,31 +105,10 @@ const DashboardPage: React.FC = ({ - {/* - - - - - - - - - - - - */} - + */} ); diff --git a/src/components/DashboardPage/goodsReceiptStatus/GoodsReceiptStatus.tsx b/src/components/DashboardPage/goodsReceiptStatus/GoodsReceiptStatus.tsx new file mode 100644 index 0000000..4e725d2 --- /dev/null +++ b/src/components/DashboardPage/goodsReceiptStatus/GoodsReceiptStatus.tsx @@ -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(""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [currentTime, setCurrentTime] = useState(null); + const [isClient, setIsClient] = useState(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 ( + + + {/* Filter */} + + + + {t("Filter")} + + + + + + {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} + + + + {/* Table */} + + {loading ? ( + + + + ) : ( + + + + + {t("Column 1")} + {t("Column 2")} + {t("Column 3")} + {/* TODO: Add table columns when implementing */} + + + + {filteredData.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + filteredData.map((row, index) => ( + + + {/* TODO: Add table cell content when implementing */} + - + + + - + + + - + + + )) + )} + +
+
+ )} +
+
+
+ ); +}; + +export default GoodsReceiptStatus; diff --git a/src/components/DashboardPage/goodsReceiptStatus/index.ts b/src/components/DashboardPage/goodsReceiptStatus/index.ts new file mode 100644 index 0000000..22142f8 --- /dev/null +++ b/src/components/DashboardPage/goodsReceiptStatus/index.ts @@ -0,0 +1 @@ +export { default as GoodsReceiptStatus } from './GoodsReceiptStatus'; diff --git a/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx b/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx index 2c2413c..00d3d7c 100644 --- a/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx +++ b/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx @@ -237,11 +237,6 @@ const TruckScheduleDashboard: React.FC = () => { return ( - {/* Title */} - - {t("Truck Schedule Dashboard")} - - {/* Filter */} diff --git a/src/components/ItemsSearch/ItemsSearch.tsx b/src/components/ItemsSearch/ItemsSearch.tsx index f8a2487..9da740b 100644 --- a/src/components/ItemsSearch/ItemsSearch.tsx +++ b/src/components/ItemsSearch/ItemsSearch.tsx @@ -7,14 +7,11 @@ import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import { EditNote } from "@mui/icons-material"; import { useRouter, useSearchParams } from "next/navigation"; -import { GridDeleteIcon } from "@mui/x-data-grid"; import { Chip } from "@mui/material"; import { TypeEnum } from "@/app/utils/typeEnum"; import axios from "axios"; import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; import axiosInstance from "@/app/(main)/axios/axiosInstance"; -import { deleteItem } from "@/app/api/settings/item/actions"; -import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; type Props = { items: ItemsResult[]; @@ -135,22 +132,6 @@ const ItemsSearch: React.FC = ({ items }) => { refetchData, ]); - const onDeleteClick = useCallback( - (item: ItemsResult) => { - deleteDialog(async () => { - if (item.id) { - const itemId = typeof item.id === "string" ? parseInt(item.id, 10) : item.id; - if (!isNaN(itemId)) { - await deleteItem(itemId); - await refetchData(filterObj); - await successDialog(t("Delete Success"), t); - } - } - }, t); - }, - [refetchData, filterObj, t], - ); - const columns = useMemo[]>( () => [ { @@ -158,22 +139,34 @@ const ItemsSearch: React.FC = ({ items }) => { label: t("Details"), onClick: onDetailClick, buttonIcon: , + sx: { width: 80 }, }, { name: "code", label: t("Code"), + sx: { width: 150 }, }, { name: "name", label: t("Name"), + sx: { width: 250 }, + }, + { + name: "LocationCode", + label: t("LocationCode"), + sx: { width: 150 }, }, { name: "type", label: t("Type"), + sx: { width: 120 }, }, { name: "status", label: t("Status"), + align: "center", + headerAlign: "center", + sx: { width: 120 }, renderCell: (item) => { const status = item.status || checkItemStatus(item); if (status === "complete") { @@ -183,14 +176,8 @@ const ItemsSearch: React.FC = ({ items }) => { } }, }, - { - name: "action", - label: t(""), - buttonIcon: , - onClick: onDeleteClick, - }, ], - [onDeleteClick, onDetailClick, t, checkItemStatus], + [onDetailClick, t, checkItemStatus], ); const onReset = useCallback(() => { diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index ba6400a..8b59df9 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -247,7 +247,7 @@ const NavigationContent: React.FC = () => { icon: , label: "PS", path: "/ps", - requiredAbility: AUTH.TESTING, + requiredAbility: [AUTH.TESTING, AUTH.ADMIN], isHidden: false, }, { diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 9381927..f4a242e 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -47,5 +47,20 @@ export const REPORTS: ReportDefinition[] = [ ], 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... ]; \ No newline at end of file diff --git a/src/i18n/en/dashboard.json b/src/i18n/en/dashboard.json index a6d2c2b..7d5f025 100644 --- a/src/i18n/en/dashboard.json +++ b/src/i18n/en/dashboard.json @@ -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" + "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" } diff --git a/src/i18n/zh/dashboard.json b/src/i18n/zh/dashboard.json index 9464706..6fdfaec 100644 --- a/src/i18n/zh/dashboard.json +++ b/src/i18n/zh/dashboard.json @@ -72,5 +72,12 @@ "Tickets Completed": "已完成成品出倉單", "Last Ticket End": "末單結束時間", "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": "暫無資料" } diff --git a/src/i18n/zh/items.json b/src/i18n/zh/items.json index bdd59ca..c2f200a 100644 --- a/src/i18n/zh/items.json +++ b/src/i18n/zh/items.json @@ -33,7 +33,7 @@ "Search": "搜尋", "Release": "發佈", "Actions": "操作", -"LocationCode": "位置", +"LocationCode": "預設位置", "DefaultLocationCode": "預設位置", "Special Type": "特殊類型", "None": "無",