Procházet zdrojové kódy

no message

MergeProblem1
DESKTOP-064TTA1\Fai LUK před 3 dny
rodič
revize
cf9fb4c527
7 změnil soubory, kde provedl 392 přidání a 130 odebrání
  1. +22
    -0
      src/app/(main)/m18Syn/layout.tsx
  2. +239
    -0
      src/app/(main)/m18Syn/page.tsx
  3. +82
    -130
      src/app/(main)/testing/page.tsx
  4. +39
    -0
      src/app/api/laserPrint/actions.ts
  5. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  6. +8
    -0
      src/components/NavigationContent/NavigationContent.tsx
  7. +1
    -0
      src/routes.ts

+ 22
- 0
src/app/(main)/m18Syn/layout.tsx Zobrazit soubor

@@ -0,0 +1,22 @@
import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/config/authConfig";
import { AUTH } from "@/authorities";

export const metadata: Metadata = {
title: "M18 Sync",
};

export default async function M18SynLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const abilities = session?.user?.abilities ?? [];
if (!abilities.includes(AUTH.ADMIN)) {
redirect("/dashboard");
}
return <>{children}</>;
}

+ 239
- 0
src/app/(main)/m18Syn/page.tsx Zobrazit soubor

@@ -0,0 +1,239 @@
"use client";

import React, { useState } from "react";
import { Box, Button, Paper, Stack, Tab, Tabs, TextField, Typography } from "@mui/material";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";

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={`m18syn-tabpanel-${index}`}
aria-labelledby={`m18syn-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}

export default function M18SynPage() {
const [tabValue, setTabValue] = useState(0);

const [m18PoCode, setM18PoCode] = useState("");
const [isSyncingM18Po, setIsSyncingM18Po] = useState(false);
const [m18PoSyncResult, setM18PoSyncResult] = useState<string>("");

const [m18DoCode, setM18DoCode] = useState("");
const [isSyncingM18Do, setIsSyncingM18Do] = useState(false);
const [m18DoSyncResult, setM18DoSyncResult] = useState<string>("");

const [m18ProductCode, setM18ProductCode] = useState("");
const [isSyncingM18Product, setIsSyncingM18Product] = useState(false);
const [m18ProductSyncResult, setM18ProductSyncResult] = useState<string>("");

const handleSyncM18PoByCode = async () => {
if (!m18PoCode.trim()) {
alert("Please enter PO code.");
return;
}
setIsSyncingM18Po(true);
setM18PoSyncResult("");
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent(m18PoCode.trim())}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
const text = await response.text();
setM18PoSyncResult(text);
if (!response.ok) {
alert(`Sync failed: ${response.status}`);
}
} catch (e) {
console.error("M18 PO Sync By Code Error:", e);
alert("M18 PO sync failed. Check console/network.");
} finally {
setIsSyncingM18Po(false);
}
};

const handleSyncM18DoByCode = async () => {
if (!m18DoCode.trim()) {
alert("Please enter DO / shop PO code.");
return;
}
setIsSyncingM18Do(true);
setM18DoSyncResult("");
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/do-by-code?code=${encodeURIComponent(m18DoCode.trim())}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
const text = await response.text();
setM18DoSyncResult(text);
if (!response.ok) {
alert(`Sync failed: ${response.status}`);
}
} catch (e) {
console.error("M18 DO Sync By Code Error:", e);
alert("M18 DO sync failed. Check console/network.");
} finally {
setIsSyncingM18Do(false);
}
};

const handleSyncM18ProductByCode = async () => {
if (!m18ProductCode.trim()) {
alert("Please enter M18 item / product code.");
return;
}
setIsSyncingM18Product(true);
setM18ProductSyncResult("");
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/product-by-code?code=${encodeURIComponent(m18ProductCode.trim())}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
const text = await response.text();
setM18ProductSyncResult(text);
if (!response.ok) {
alert(`Sync failed: ${response.status}`);
}
} catch (e) {
console.error("M18 Product Sync By Code Error:", e);
alert("M18 product sync failed. Check console/network.");
} finally {
setIsSyncingM18Product(false);
}
};

const Section = ({ title, children }: { title: string; children?: React.ReactNode }) => (
<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 (
<Box sx={{ p: 4 }}>
<Typography variant="h4" sx={{ mb: 4, fontWeight: "bold" }}>
M18 Sync (by code)
</Typography>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
ADMIN only. Sync Purchase Order, Delivery Order, or product/material from M18 using document or item code.
</Typography>

<Tabs value={tabValue} onChange={(_, v) => setTabValue(v)} aria-label="M18 sync by code" centered variant="fullWidth">
<Tab label="1. PO" id="m18syn-tab-0" />
<Tab label="2. DO" id="m18syn-tab-1" />
<Tab label="3. Product" id="m18syn-tab-2" />
</Tabs>

<TabPanel value={tabValue} index={0}>
<Section title="M18 Purchase Order — sync by code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
label="PO Code"
value={m18PoCode}
onChange={(e) => setM18PoCode(e.target.value)}
placeholder="e.g. PFP002PO26030341"
sx={{ minWidth: 320 }}
/>
<Button variant="contained" color="primary" onClick={handleSyncM18PoByCode} disabled={isSyncingM18Po}>
{isSyncingM18Po ? "Syncing..." : "Sync PO from M18"}
</Button>
</Stack>
{m18PoSyncResult ? (
<TextField
fullWidth
multiline
minRows={4}
margin="normal"
label="Sync Result"
value={m18PoSyncResult}
InputProps={{ readOnly: true }}
/>
) : null}
</Section>
</TabPanel>

<TabPanel value={tabValue} index={1}>
<Section title="M18 Delivery Order — sync by code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
label="DO / Shop PO Code"
value={m18DoCode}
onChange={(e) => setM18DoCode(e.target.value)}
placeholder="e.g. same document code as M18 shop PO"
sx={{ minWidth: 320 }}
/>
<Button variant="contained" color="primary" onClick={handleSyncM18DoByCode} disabled={isSyncingM18Do}>
{isSyncingM18Do ? "Syncing..." : "Sync DO from M18"}
</Button>
</Stack>
{m18DoSyncResult ? (
<TextField
fullWidth
multiline
minRows={4}
margin="normal"
label="Sync Result"
value={m18DoSyncResult}
InputProps={{ readOnly: true }}
/>
) : null}
</Section>
</TabPanel>

<TabPanel value={tabValue} index={2}>
<Section title="M18 Product / material — sync by code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
label="Item / product code"
value={m18ProductCode}
onChange={(e) => setM18ProductCode(e.target.value)}
placeholder="e.g. PP1175 (M18 item code)"
sx={{ minWidth: 320 }}
/>
<Button
variant="contained"
color="primary"
onClick={handleSyncM18ProductByCode}
disabled={isSyncingM18Product}
>
{isSyncingM18Product ? "Syncing..." : "Sync product from M18"}
</Button>
</Stack>
{m18ProductSyncResult ? (
<TextField
fullWidth
multiline
minRows={4}
margin="normal"
label="Sync Result"
value={m18ProductSyncResult}
InputProps={{ readOnly: true }}
/>
) : null}
</Section>
</TabPanel>
</Box>
);
}

+ 82
- 130
src/app/(main)/testing/page.tsx Zobrazit soubor

@@ -29,6 +29,10 @@ import {
pushOnPackTextQrZipToNgpcl, pushOnPackTextQrZipToNgpcl,
type JobOrderListItem, type JobOrderListItem,
} from "@/app/api/bagPrint/actions"; } from "@/app/api/bagPrint/actions";
import {
runLaserBag2AutoSend,
type LaserBag2AutoSendReport,
} from "@/app/api/laserPrint/actions";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";


interface TabPanelProps { interface TabPanelProps {
@@ -61,15 +65,7 @@ export default function TestingPage() {


// --- 1. GRN Preview (M18) --- // --- 1. GRN Preview (M18) ---
const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] = useState("2026-03-16"); const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] = useState("2026-03-16");
// --- 2. M18 PO Sync by Code ---
const [m18PoCode, setM18PoCode] = useState("");
const [isSyncingM18Po, setIsSyncingM18Po] = useState(false);
const [m18PoSyncResult, setM18PoSyncResult] = useState<string>("");
// --- 3. M18 DO Sync by Code ---
const [m18DoCode, setM18DoCode] = useState("");
const [isSyncingM18Do, setIsSyncingM18Do] = useState(false);
const [m18DoSyncResult, setM18DoSyncResult] = useState<string>("");
// --- 4. OnPack NGPCL (same job-order → ZIP logic as /bagPrint) ---
// --- 2. OnPack NGPCL (same job-order → ZIP logic as /bagPrint) ---
const [onpackPlanDate, setOnpackPlanDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [onpackPlanDate, setOnpackPlanDate] = useState(() => dayjs().format("YYYY-MM-DD"));
const [onpackJobOrders, setOnpackJobOrders] = useState<JobOrderListItem[]>([]); const [onpackJobOrders, setOnpackJobOrders] = useState<JobOrderListItem[]>([]);
const [onpackLoading, setOnpackLoading] = useState(false); const [onpackLoading, setOnpackLoading] = useState(false);
@@ -77,11 +73,17 @@ export default function TestingPage() {
const [onpackLemonDownloading, setOnpackLemonDownloading] = useState(false); const [onpackLemonDownloading, setOnpackLemonDownloading] = useState(false);
const [onpackPushLoading, setOnpackPushLoading] = useState(false); const [onpackPushLoading, setOnpackPushLoading] = useState(false);
const [onpackPushResult, setOnpackPushResult] = useState<string | null>(null); const [onpackPushResult, setOnpackPushResult] = useState<string | null>(null);
// --- 3. Laser Bag2 auto-send (same as /laserPrint + DB LASER_PRINT.*) ---
const [laserAutoPlanDate, setLaserAutoPlanDate] = useState(() => dayjs().format("YYYY-MM-DD"));
const [laserAutoLimit, setLaserAutoLimit] = useState("1");
const [laserAutoLoading, setLaserAutoLoading] = useState(false);
const [laserAutoReport, setLaserAutoReport] = useState<LaserBag2AutoSendReport | null>(null);
const [laserAutoError, setLaserAutoError] = useState<string | null>(null);


const onpackPayload = useMemo(() => buildOnPackJobOrdersPayload(onpackJobOrders), [onpackJobOrders]); const onpackPayload = useMemo(() => buildOnPackJobOrdersPayload(onpackJobOrders), [onpackJobOrders]);


useEffect(() => { useEffect(() => {
if (tabValue !== 3) return;
if (tabValue !== 1) return;
let cancelled = false; let cancelled = false;
(async () => { (async () => {
setOnpackLoading(true); setOnpackLoading(true);
@@ -138,58 +140,6 @@ export default function TestingPage() {
} }
}; };


const handleSyncM18PoByCode = async () => {
if (!m18PoCode.trim()) {
alert("Please enter PO code.");
return;
}
setIsSyncingM18Po(true);
setM18PoSyncResult("");
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent(m18PoCode.trim())}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
const text = await response.text();
setM18PoSyncResult(text);
if (!response.ok) {
alert(`Sync failed: ${response.status}`);
}
} catch (e) {
console.error("M18 PO Sync By Code Error:", e);
alert("M18 PO sync failed. Check console/network.");
} finally {
setIsSyncingM18Po(false);
}
};

const handleSyncM18DoByCode = async () => {
if (!m18DoCode.trim()) {
alert("Please enter DO / shop PO code.");
return;
}
setIsSyncingM18Do(true);
setM18DoSyncResult("");
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/do-by-code?code=${encodeURIComponent(m18DoCode.trim())}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
const text = await response.text();
setM18DoSyncResult(text);
if (!response.ok) {
alert(`Sync failed: ${response.status}`);
}
} catch (e) {
console.error("M18 DO Sync By Code Error:", e);
alert("M18 DO sync failed. Check console/network.");
} finally {
setIsSyncingM18Do(false);
}
};

const downloadBlob = (blob: Blob, filename: string) => { const downloadBlob = (blob: Blob, filename: string) => {
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
@@ -218,6 +168,24 @@ export default function TestingPage() {
} }
}; };


const handleLaserBag2AutoSend = async () => {
setLaserAutoLoading(true);
setLaserAutoError(null);
setLaserAutoReport(null);
try {
const lim = parseInt(laserAutoLimit.trim(), 10);
const report = await runLaserBag2AutoSend({
planStart: laserAutoPlanDate,
limitPerRun: Number.isFinite(lim) ? lim : 0,
});
setLaserAutoReport(report);
} catch (e) {
setLaserAutoError(e instanceof Error ? e.message : String(e));
} finally {
setLaserAutoLoading(false);
}
};

const handleOnpackPushNgpcl = async () => { const handleOnpackPushNgpcl = async () => {
if (onpackPayload.length === 0) { if (onpackPayload.length === 0) {
alert("No job orders with item code for this plan date."); alert("No job orders with item code for this plan date.");
@@ -254,9 +222,8 @@ export default function TestingPage() {


<Tabs value={tabValue} onChange={handleTabChange} aria-label="testing sections tabs" centered variant="fullWidth"> <Tabs value={tabValue} onChange={handleTabChange} aria-label="testing sections tabs" centered variant="fullWidth">
<Tab label="1. GRN Preview" /> <Tab label="1. GRN Preview" />
<Tab label="2. M18 PO Sync" />
<Tab label="3. M18 DO Sync" />
<Tab label="4. OnPack NGPCL" />
<Tab label="2. OnPack NGPCL" />
<Tab label="3. Laser Bag2 自動送" />
</Tabs> </Tabs>


<TabPanel value={tabValue} index={0}> <TabPanel value={tabValue} index={0}>
@@ -287,71 +254,7 @@ export default function TestingPage() {
</TabPanel> </TabPanel>


<TabPanel value={tabValue} index={1}> <TabPanel value={tabValue} index={1}>
<Section title="2. M18 PO Sync by Code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
label="PO Code"
value={m18PoCode}
onChange={(e) => setM18PoCode(e.target.value)}
placeholder="e.g. PFP002PO26030341"
sx={{ minWidth: 320 }}
/>
<Button variant="contained" color="primary" onClick={handleSyncM18PoByCode} disabled={isSyncingM18Po}>
{isSyncingM18Po ? "Syncing..." : "Sync PO from M18"}
</Button>
</Stack>
<Typography variant="body2" color="textSecondary">
Backend endpoint: <code>/m18/test/po-by-code?code=YOUR_CODE</code>
</Typography>
{m18PoSyncResult ? (
<TextField
fullWidth
multiline
minRows={4}
margin="normal"
label="Sync Result"
value={m18PoSyncResult}
InputProps={{ readOnly: true }}
/>
) : null}
</Section>
</TabPanel>

<TabPanel value={tabValue} index={2}>
<Section title="3. M18 DO Sync by Code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
label="DO / Shop PO Code"
value={m18DoCode}
onChange={(e) => setM18DoCode(e.target.value)}
placeholder="e.g. same document code as M18 shop PO"
sx={{ minWidth: 320 }}
/>
<Button variant="contained" color="primary" onClick={handleSyncM18DoByCode} disabled={isSyncingM18Do}>
{isSyncingM18Do ? "Syncing..." : "Sync DO from M18"}
</Button>
</Stack>
<Typography variant="body2" color="textSecondary">
Backend endpoint: <code>/m18/test/do-by-code?code=YOUR_CODE</code>
</Typography>
{m18DoSyncResult ? (
<TextField
fullWidth
multiline
minRows={4}
margin="normal"
label="Sync Result"
value={m18DoSyncResult}
InputProps={{ readOnly: true }}
/>
) : null}
</Section>
</TabPanel>

<TabPanel value={tabValue} index={3}>
<Section title="4. OnPack NGPCL (same logic as /bagPrint)">
<Section title="2. OnPack NGPCL (same logic as /bagPrint)">
<Alert severity="info" sx={{ mb: 2 }}> <Alert severity="info" sx={{ mb: 2 }}>
Uses <strong>GET /py/job-orders?planStart=</strong> for the day, then the same <code>jobOrders</code> payload as{" "} Uses <strong>GET /py/job-orders?planStart=</strong> for the day, then the same <code>jobOrders</code> payload as{" "}
<strong>Bag Print → 下載 OnPack2023檸檬機</strong>. The ZIP contains loose <code>.job</code> / <code>.image</code> / BMPs — extract <strong>Bag Print → 下載 OnPack2023檸檬機</strong>. The ZIP contains loose <code>.job</code> / <code>.image</code> / BMPs — extract
@@ -445,6 +348,55 @@ export default function TestingPage() {
</Typography> </Typography>
</Section> </Section>
</TabPanel> </TabPanel>

<TabPanel value={tabValue} index={2}>
<Section title="3. Laser Bag2 自動送(與 /laserPrint 相同邏輯)">
<Alert severity="warning" sx={{ mb: 2 }}>
依資料庫 <strong>LASER_PRINT.host</strong>、<strong>LASER_PRINT.port</strong>、<strong>LASER_PRINT.itemCodes</strong> 查當日包裝工單並送 TCP(每筆工單預設 3 次、間隔 3 秒,與前端點列相同)。
排程預設關閉;啟用請設 <code>laser.bag2.auto-send.enabled=true</code>(後端 application.yml)。
</Alert>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}>
<TextField
size="small"
label="Plan date (planStart)"
type="date"
value={laserAutoPlanDate}
onChange={(e) => setLaserAutoPlanDate(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<TextField
size="small"
label="limitPerRun(0=全部符合)"
value={laserAutoLimit}
onChange={(e) => setLaserAutoLimit(e.target.value)}
sx={{ width: 200 }}
helperText="手動測試建議 1;排程預設每分鐘最多 1 筆"
/>
<Button variant="contained" color="primary" onClick={() => void handleLaserBag2AutoSend()} disabled={laserAutoLoading}>
{laserAutoLoading ? "送出中…" : "執行 POST /plastic/laser-bag2-auto-send"}
</Button>
</Stack>
{laserAutoError ? (
<Alert severity="error" sx={{ mb: 2 }}>
{laserAutoError}
</Alert>
) : null}
{laserAutoReport ? (
<TextField
fullWidth
multiline
minRows={8}
label="回應(LaserBag2AutoSendReport)"
value={JSON.stringify(laserAutoReport, null, 2)}
InputProps={{ readOnly: true }}
sx={{ fontFamily: "monospace" }}
/>
) : null}
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
<code>POST /api/plastic/laser-bag2-auto-send?planStart=YYYY-MM-DD&amp;limitPerRun=N</code>
</Typography>
</Section>
</TabPanel>
</Box> </Box>
); );
} }

+ 39
- 0
src/app/api/laserPrint/actions.ts Zobrazit soubor

@@ -128,6 +128,45 @@ export async function checkPrinterStatus(request: PrinterStatusRequest): Promise
return data; return data;
} }


export interface LaserBag2JobSendResult {
jobOrderId: number;
itemCode: string | null;
success: boolean;
message: string;
}

export interface LaserBag2AutoSendReport {
planStart: string;
jobOrdersFound: number;
jobOrdersProcessed: number;
results: LaserBag2JobSendResult[];
}

/**
* Same workflow as /laserPrint row click: list job orders (LASER_PRINT.itemCodes) for planStart,
* then TCP send(s) using DB LASER_PRINT.host / port. limitPerRun 0 = all matches.
*/
export async function runLaserBag2AutoSend(params?: {
planStart?: string;
limitPerRun?: number;
}): Promise<LaserBag2AutoSendReport> {
const base = (NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, "");
if (!base) {
throw new Error("NEXT_PUBLIC_API_URL is not set.");
}
const sp = new URLSearchParams();
if (params?.planStart) sp.set("planStart", params.planStart);
if (params?.limitPerRun != null) sp.set("limitPerRun", String(params.limitPerRun));
const q = sp.toString();
const url = `${base}/plastic/laser-bag2-auto-send${q ? `?${q}` : ""}`;
const res = await clientAuthFetch(url, { method: "POST" });
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(t || `HTTP ${res.status}`);
}
return res.json() as Promise<LaserBag2AutoSendReport>;
}

export async function patchSetting(name: string, value: string): Promise<void> { export async function patchSetting(name: string, value: string): Promise<void> {
const url = `${NEXT_PUBLIC_API_URL}/settings/${encodeURIComponent(name)}`; const url = `${NEXT_PUBLIC_API_URL}/settings/${encodeURIComponent(name)}`;
const res = await clientAuthFetch(url, { const res = await clientAuthFetch(url, {


+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Zobrazit soubor

@@ -46,6 +46,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/putAway": "Put Away", "/putAway": "Put Away",
"/stockIssue": "Stock Issue", "/stockIssue": "Stock Issue",
"/report": "Report", "/report": "Report",
"/m18Syn": "M18 Sync",
"/bagPrint": "打袋機", "/bagPrint": "打袋機",
"/laserPrint": "檸檬機(激光機)", "/laserPrint": "檸檬機(激光機)",
"/settings/itemPrice": "Price Inquiry", "/settings/itemPrice": "Price Inquiry",


+ 8
- 0
src/components/NavigationContent/NavigationContent.tsx Zobrazit soubor

@@ -37,6 +37,7 @@ import Label from "@mui/icons-material/Label";
import Checklist from "@mui/icons-material/Checklist"; import Checklist from "@mui/icons-material/Checklist";
import Science from "@mui/icons-material/Science"; import Science from "@mui/icons-material/Science";
import UploadFile from "@mui/icons-material/UploadFile"; import UploadFile from "@mui/icons-material/UploadFile";
import Sync from "@mui/icons-material/Sync";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
@@ -194,6 +195,13 @@ const NavigationContent: React.FC = () => {
requiredAbility: [AUTH.TESTING, AUTH.ADMIN], requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
isHidden: false, isHidden: false,
}, },
{
icon: <Sync />,
label: "M18 Sync",
path: "/m18Syn",
requiredAbility: [AUTH.ADMIN],
isHidden: false,
},
{ {
icon: <ShowChart />, icon: <ShowChart />,
label: "圖表報告", label: "圖表報告",


+ 1
- 0
src/routes.ts Zobrazit soubor

@@ -1,6 +1,7 @@
export const PRIVATE_ROUTES = [ export const PRIVATE_ROUTES = [
"/analytics", "/analytics",
"/dashboard", "/dashboard",
"/m18Syn",
"/testing", "/testing",
"/jo/testing", "/jo/testing",
"/po/workbench", "/po/workbench",


Načítá se…
Zrušit
Uložit