diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx new file mode 100644 index 0000000..4c64f92 --- /dev/null +++ b/src/app/(main)/testing/page.tsx @@ -0,0 +1,306 @@ +"use client"; + +import React, { useState } from "react"; +import { + Box, Grid, Paper, Typography, Button, Dialog, DialogTitle, + DialogContent, DialogActions, TextField, Stack, Table, + TableBody, TableCell, TableContainer, TableHead, TableRow +} 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"; + +export default function TestingPage() { + // --- 1. TSC Section States --- + const [tscConfig, setTscConfig] = useState({ ip: '192.168.1.100', port: '9100' }); + const [tscItems, setTscItems] = useState([ + { id: 1, itemCode: 'FG-001', itemName: 'Yellow Curry Sauce', lotNo: 'LOT-TSC-01', expiryDate: '2025-12-01' }, + { id: 2, itemCode: 'FG-002', itemName: 'Red Curry Paste', lotNo: 'LOT-TSC-02', expiryDate: '2025-12-05' }, + ]); + + // --- 2. DataFlex Section States --- + const [dfConfig, setDfConfig] = useState({ ip: '192.168.1.101', port: '9100' }); + const [dfItems, setDfItems] = useState([ + { id: 1, itemCode: 'DF-101', itemName: 'Instant Noodle A', lotNo: 'LOT-DF-01', expiryDate: '2026-01-10' }, + { id: 2, itemCode: 'DF-102', itemName: 'Instant Noodle B', lotNo: 'LOT-DF-02', expiryDate: '2026-01-15' }, + ]); + + // --- 3. OnPack Section States --- + const [isPrinterModalOpen, setIsPrinterModalOpen] = useState(false); + const [printerFormData, setPrinterFormData] = useState({ + itemCode: '', + lotNo: '', + expiryDate: dayjs().format('YYYY-MM-DD'), + productName: '' + }); + + // --- 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' }, +]); + + // Generic handler for inline table edits + const handleItemChange = (setter: any, id: number, field: string, value: string) => { + setter((prev: any[]) => prev.map(item => + item.id === id ? { ...item, [field]: value } : item + )); + }; + + // --- API CALLS --- + + // TSC Print (Section 1) + const handleTscPrint = async (row: any) => { + const token = localStorage.getItem("accessToken"); + const payload = { ...row, printerIp: tscConfig.ip, printerPort: tscConfig.port }; + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (response.ok) alert(`TSC Print Command Sent for ${row.itemCode}!`); + else alert("TSC Print Failed"); + } catch (e) { console.error("TSC Error:", e); } + }; + + // DataFlex Print (Section 2) + const handleDfPrint = async (row: any) => { + const token = localStorage.getItem("accessToken"); + const payload = { ...row, printerIp: dfConfig.ip, printerPort: dfConfig.port }; + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (response.ok) alert(`DataFlex Print Command Sent for ${row.itemCode}!`); + else alert("DataFlex Print Failed"); + } catch (e) { console.error("DataFlex Error:", e); } + }; + + // OnPack Zip Download (Section 3) + const handleDownloadPrintJob = async () => { + const token = localStorage.getItem("accessToken"); + const params = new URLSearchParams(printerFormData); + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (!response.ok) throw new Error('Download failed'); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `${printerFormData.lotNo || 'OnPack'}.zip`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + setIsPrinterModalOpen(false); + } catch (e) { console.error("OnPack Error:", e); } + }; + + const handleLaserPrint = async (row: any) => { + const token = localStorage.getItem("accessToken"); + const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (response.ok) alert(`Laser Command Sent: ${row.templateId}`); + } catch (e) { console.error(e); } + }; + + const handleLaserPreview = async (row: any) => { + const token = localStorage.getItem("accessToken"); + const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; + try { + // 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' }, + body: JSON.stringify(payload) + }); + if (response.ok) alert("Red light preview active!"); + } catch (e) { console.error("Preview Error:", e); } + }; + + // Layout Helper + const Section = ({ title, children }: { title: string, children?: React.ReactNode }) => ( + + + + {title} + + {children || Waiting for implementation...} + + + ); + + return ( + + Printer Testing Dashboard + + + {/* 1. TSC Section */} +
+ + setTscConfig({...tscConfig, ip: e.target.value})} /> + setTscConfig({...tscConfig, port: e.target.value})} /> + + + + + + + Code + Name + Lot + Expiry + Action + + + + {tscItems.map(row => ( + + handleItemChange(setTscItems, row.id, 'itemCode', e.target.value)} /> + handleItemChange(setTscItems, row.id, 'itemName', e.target.value)} /> + handleItemChange(setTscItems, row.id, 'lotNo', e.target.value)} /> + handleItemChange(setTscItems, row.id, 'expiryDate', e.target.value)} /> + + + ))} + +
+
+
+ + {/* 2. DataFlex Section */} +
+ + setDfConfig({...dfConfig, ip: e.target.value})} /> + setDfConfig({...dfConfig, port: e.target.value})} /> + + + + + + + Code + Name + Lot + Expiry + Action + + + + {dfItems.map(row => ( + + handleItemChange(setDfItems, row.id, 'itemCode', e.target.value)} /> + handleItemChange(setDfItems, row.id, 'itemName', e.target.value)} /> + handleItemChange(setDfItems, row.id, 'lotNo', e.target.value)} /> + handleItemChange(setDfItems, row.id, 'expiryDate', e.target.value)} /> + + + ))} + +
+
+
+ + {/* 3. OnPack Section */} +
+ + + Calls /plastic/get-printer6 to generate CoLOS .job bundle. + + + +
+ + {/* 4. Laser Section (HANS600S-M) */} +
+ + setLaserConfig({...laserConfig, ip: e.target.value})} /> + setLaserConfig({...laserConfig, port: e.target.value})} /> + + + + + + + + Template + Lot + Exp + Pwr% + Action + + + + {laserItems.map(row => ( + + handleItemChange(setLaserItems, row.id, 'templateId', e.target.value)} /> + handleItemChange(setLaserItems, row.id, 'lotNo', e.target.value)} /> + handleItemChange(setLaserItems, row.id, 'expiryDate', e.target.value)} /> + handleItemChange(setLaserItems, row.id, 'power', e.target.value)} /> + + + + + + + + ))} + +
+
+ + Note: HANS Laser requires pre-saved templates on the controller. + +
+
+ + {/* Dialog for OnPack */} + setIsPrinterModalOpen(false)} fullWidth maxWidth="sm"> + OnPack Printer Job Details + + + setPrinterFormData({ ...printerFormData, itemCode: e.target.value })} /> + setPrinterFormData({ ...printerFormData, lotNo: e.target.value })} /> + setPrinterFormData({ ...printerFormData, productName: e.target.value })} /> + setPrinterFormData({ ...printerFormData, expiryDate: e.target.value })} /> + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/src/app/api/scheduling/actions.ts b/src/app/api/scheduling/actions.ts index 0ed01bb..ca5e48f 100644 --- a/src/app/api/scheduling/actions.ts +++ b/src/app/api/scheduling/actions.ts @@ -43,6 +43,13 @@ export interface ReleaseProdScheduleReq { id: number; } +export interface print6FilesReq { + itemCode: 'string', + lotNo: 'string', + expiryDate: 'string', + productName: 'string' +} + export interface ReleaseProdScheduleResponse { id: number; code: string; @@ -145,6 +152,23 @@ export const testDetailedSchedule = cache(async (date?: string) => { ); }); +export const getFile6 = cache(async ( + token: string | "", + data: print6FilesReq +) => { + const queryStr = convertObjToURLSearchParams(data) + return serverFetchJson( + `${BASE_API_URL}/plastic/get-printer6?${queryStr}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + }, + ); +}); + export const releaseProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { const response = serverFetchJson( `${BASE_API_URL}/productionSchedule/detail/detailed/releaseLine`, diff --git a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx index 2b53dd9..7b63578 100644 --- a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx +++ b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx @@ -7,6 +7,7 @@ import { EditNote } from "@mui/icons-material"; import { useRouter, useSearchParams } from "next/navigation"; import { useTranslation } from "react-i18next"; import { ScheduleType } from "@/app/api/scheduling"; +import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { ProdScheduleResult, SearchProdSchedule, @@ -14,6 +15,7 @@ import { fetchProdSchedules, exportProdSchedule, testDetailedSchedule, + getFile6, } from "@/app/api/scheduling/actions"; import { defaultPagingController } from "../SearchResults/SearchResults"; import { arrayToDateString, arrayToDayjs, decimalFormatter } from "@/app/utils/formatUtil"; @@ -23,6 +25,9 @@ import { Button, Stack } from "@mui/material"; import isToday from 'dayjs/plugin/isToday'; import useUploadContext from "../UploadProvider/useUploadContext"; import { FileDownload, CalendarMonth } from "@mui/icons-material"; +import { useSession } from "next-auth/react"; +import { VIEW_USER, VIEW_DO } from "@/authorities"; + dayjs.extend(isToday); // may need move to "index" or "actions" @@ -52,6 +57,10 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { const { setIsUploading } = useUploadContext(); const today = dayjs().format("YYYY-MM-DD"); + const { data: session } = useSession(); + // Extract abilities (safe fallback to empty array if not logged in / no abilities) + const abilities = session?.user?.abilities ?? []; + const router = useRouter(); // const [filterObj, setFilterObj] = useState({}); // const [tempSelectedValue, setTempSelectedValue] = useState({}); @@ -226,6 +235,48 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { refetchData(resetWithToday, "reset"); // Fetch data }, [defaultInputs, refetchData]); + const handleDownloadPrintJob = async () => { + const token = localStorage.getItem("accessToken"); + + const params = { + itemCode: 'TT173', + lotNo: 'LOT342989', + expiryDate: '2026-02-28', + productName: 'Name2342' + }; + + try { + // 1. Direct fetch call to avoid Next.js trying to parse JSON + const query = new URLSearchParams(params).toString(); + const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${query}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error('Network response was not ok'); + + // 2. GET THE DATA AS BLOB (This is the fix) + const blob = await response.blob(); + + // 3. Create a download link + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `${params.lotNo}.zip`); + document.body.appendChild(link); + link.click(); + + // Cleanup + link.parentNode?.removeChild(link); + window.URL.revokeObjectURL(url); + + } catch (error) { + console.error("Download failed", error); + } + }; + const testDetailedScheduleClick = useCallback(async () => { try { setIsUploading(true) @@ -332,6 +383,21 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { > {t("Export Schedule")} + + {false && abilities.includes(VIEW_USER) && ( + + )} { }, ], }, + { + icon: , + label: "Printer Testing", + path: "/testing", + isHidden: false, + }, { icon: , label: "Settings", @@ -396,6 +403,7 @@ const NavigationContent: React.FC = () => { {navigationItems + .filter(item => !item.isHidden) .map(renderNavigationItem) .filter(Boolean)} {/* {navigationItems.map(({ icon, label, path }, index) => {