Kaynağa Gözat

added printer testing

master
ebeveyn
işleme
26bca09116
4 değiştirilmiş dosya ile 405 ekleme ve 1 silme
  1. +306
    -0
      src/app/(main)/testing/page.tsx
  2. +24
    -0
      src/app/api/scheduling/actions.ts
  3. +66
    -0
      src/components/DetailedSchedule/DetailedScheduleSearchView.tsx
  4. +9
    -1
      src/components/NavigationContent/NavigationContent.tsx

+ 306
- 0
src/app/(main)/testing/page.tsx Dosyayı Görüntüle

@@ -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 }) => (
<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>
);

return (
<Box sx={{ p: 4 }}>
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Printer Testing Dashboard</Typography>
<Grid container spacing={3}>
{/* 1. TSC Section */}
<Section title="1. TSC">
<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="Port" value={tscConfig.port} onChange={e => setTscConfig({...tscConfig, port: e.target.value})} />
<SettingsEthernet color="action" />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Code</TableCell>
<TableCell>Name</TableCell>
<TableCell>Lot</TableCell>
<TableCell>Expiry</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tscItems.map(row => (
<TableRow key={row.id}>
<TableCell><TextField variant="standard" value={row.itemCode} onChange={e => handleItemChange(setTscItems, row.id, 'itemCode', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.itemName} onChange={e => handleItemChange(setTscItems, row.id, 'itemName', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.lotNo} onChange={e => handleItemChange(setTscItems, row.id, 'lotNo', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" type="date" value={row.expiryDate} onChange={e => handleItemChange(setTscItems, row.id, 'expiryDate', e.target.value)} /></TableCell>
<TableCell align="center"><Button variant="contained" size="small" startIcon={<Print />} onClick={() => handleTscPrint(row)}>Print</Button></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Section>

{/* 2. DataFlex Section */}
<Section title="2. DataFlex">
<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="Port" value={dfConfig.port} onChange={e => setDfConfig({...dfConfig, port: e.target.value})} />
<Lan color="action" />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Code</TableCell>
<TableCell>Name</TableCell>
<TableCell>Lot</TableCell>
<TableCell>Expiry</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{dfItems.map(row => (
<TableRow key={row.id}>
<TableCell><TextField variant="standard" value={row.itemCode} onChange={e => handleItemChange(setDfItems, row.id, 'itemCode', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.itemName} onChange={e => handleItemChange(setDfItems, row.id, 'itemName', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.lotNo} onChange={e => handleItemChange(setDfItems, row.id, 'lotNo', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" type="date" value={row.expiryDate} onChange={e => handleItemChange(setDfItems, row.id, 'expiryDate', e.target.value)} /></TableCell>
<TableCell align="center"><Button variant="contained" color="secondary" size="small" startIcon={<Print />} onClick={() => handleDfPrint(row)}>Print</Button></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Section>

{/* 3. OnPack Section */}
<Section title="3. OnPack">
<Box sx={{ m: 'auto', textAlign: 'center' }}>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Calls /plastic/get-printer6 to generate CoLOS .job bundle.
</Typography>
<Button variant="contained" color="success" size="large" startIcon={<FileDownload />} onClick={() => setIsPrinterModalOpen(true)}>
Generate CoLOS Files
</Button>
</Box>
</Section>

{/* 4. Laser Section (HANS600S-M) */}
<Section title="4. Laser">
<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="Port" value={laserConfig.port} onChange={e => setLaserConfig({...laserConfig, port: e.target.value})} />
<Router color="action" />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Template</TableCell>
<TableCell>Lot</TableCell>
<TableCell>Exp</TableCell>
<TableCell>Pwr%</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{laserItems.map(row => (
<TableRow key={row.id}>
<TableCell><TextField variant="standard" value={row.templateId} onChange={e => handleItemChange(setLaserItems, row.id, 'templateId', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.lotNo} onChange={e => handleItemChange(setLaserItems, row.id, 'lotNo', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" type="date" value={row.expiryDate} onChange={e => handleItemChange(setLaserItems, row.id, 'expiryDate', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.power} sx={{ width: 40 }} onChange={e => handleItemChange(setLaserItems, row.id, 'power', e.target.value)} /></TableCell>
<TableCell align="center">
<Stack direction="row" spacing={1} justifyContent="center">
<Button
variant="outlined"
color="info"
size="small"
onClick={() => handleLaserPreview(row)}
>
Preview
</Button>
<Button
variant="contained"
color="warning"
size="small"
startIcon={<Print />}
onClick={() => handleLaserPrint(row)}
>
Mark
</Button>
</Stack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Typography variant="caption" sx={{ mt: 2, display: 'block', color: 'text.secondary' }}>
Note: HANS Laser requires pre-saved templates on the controller.
</Typography>
</Section>
</Grid>

{/* Dialog for OnPack */}
<Dialog open={isPrinterModalOpen} onClose={() => setIsPrinterModalOpen(false)} fullWidth maxWidth="sm">
<DialogTitle sx={{ bgcolor: 'success.main', color: 'white' }}>OnPack Printer Job Details</DialogTitle>
<DialogContent sx={{ mt: 2 }}>
<Stack spacing={3}>
<TextField label="Item Code" fullWidth value={printerFormData.itemCode} onChange={(e) => setPrinterFormData({ ...printerFormData, itemCode: e.target.value })} />
<TextField label="Lot Number" fullWidth value={printerFormData.lotNo} onChange={(e) => setPrinterFormData({ ...printerFormData, lotNo: e.target.value })} />
<TextField label="Product Name" fullWidth value={printerFormData.productName} onChange={(e) => setPrinterFormData({ ...printerFormData, productName: e.target.value })} />
<TextField label="Expiry Date" type="date" fullWidth InputLabelProps={{ shrink: true }} value={printerFormData.expiryDate} onChange={(e) => setPrinterFormData({ ...printerFormData, expiryDate: e.target.value })} />
</Stack>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button onClick={() => setIsPrinterModalOpen(false)} variant="outlined" color="inherit">Cancel</Button>
<Button variant="contained" color="success" onClick={handleDownloadPrintJob}>Generate & Download</Button>
</DialogActions>
</Dialog>
</Box>
);
}

+ 24
- 0
src/app/api/scheduling/actions.ts Dosyayı Görüntüle

@@ -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<ReleaseProdScheduleResponse>(
`${BASE_API_URL}/productionSchedule/detail/detailed/releaseLine`,


+ 66
- 0
src/components/DetailedSchedule/DetailedScheduleSearchView.tsx Dosyayı Görüntüle

@@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ type, defaultInputs }) => {
>
{t("Export Schedule")}
</Button>

{false && abilities.includes(VIEW_USER) && (
<Button
variant="contained" // Solid button for the "Export" action
color="success" // Green color often signifies a successful action/download
startIcon={<FileDownload />}
onClick={handleDownloadPrintJob}
sx={{
boxShadow: 2,
'&:hover': { backgroundColor: 'success.dark', boxShadow: 4 }
}}
>
Get Printer File API
</Button>
)}
</Stack>
<SearchBox
criteria={searchCriteria}


+ 9
- 1
src/components/NavigationContent/NavigationContent.tsx Dosyayı Görüntüle

@@ -25,6 +25,7 @@ import { usePathname } from "next/navigation";
import Link from "next/link";
import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
import Logo from "../Logo";
import BugReportIcon from "@mui/icons-material/BugReport";
import {
VIEW_USER,
VIEW_DO,
@@ -40,7 +41,7 @@ interface NavigationItem {
label: string;
path: string;
children?: NavigationItem[];
isHidden?: true | undefined;
isHidden?: boolean | undefined;
requiredAbility?: string | string[];
}

@@ -230,6 +231,12 @@ const NavigationContent: React.FC = () => {
},
],
},
{
icon: <BugReportIcon />,
label: "Printer Testing",
path: "/testing",
isHidden: false,
},
{
icon: <RequestQuote />,
label: "Settings",
@@ -396,6 +403,7 @@ const NavigationContent: React.FC = () => {
<Divider />
<List component="nav">
{navigationItems
.filter(item => !item.isHidden)
.map(renderNavigationItem)
.filter(Boolean)}
{/* {navigationItems.map(({ icon, label, path }, index) => {


Yükleniyor…
İptal
Kaydet