Bläddra i källkod

Remove ability to use sensitive reports

debrand
Harry Groves 1 månad sedan
förälder
incheckning
e400b4a9c8
9 ändrade filer med 79 tillägg och 119 borttagningar
  1. +36
    -16
      src/app/(main)/report/ReportSelectionDashboard.tsx
  2. +1
    -1
      src/app/(main)/report/bomShopSyncReportApi.ts
  3. +0
    -16
      src/app/(main)/report/grnReportApi.ts
  4. +3
    -1
      src/app/(main)/report/page.tsx
  5. +1
    -1
      src/app/(main)/report/reportCategories.ts
  6. +28
    -71
      src/app/(main)/testing/page.tsx
  7. +4
    -1
      src/config/reportConfig.ts
  8. +3
    -6
      src/i18n/en/report.json
  9. +3
    -6
      src/i18n/zh/report.json

+ 36
- 16
src/app/(main)/report/ReportSelectionDashboard.tsx Visa fil

@@ -17,7 +17,7 @@ import OutboundOutlinedIcon from "@mui/icons-material/OutboundOutlined";
import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined";
import PieChartOutlineOutlinedIcon from "@mui/icons-material/PieChartOutlineOutlined";
import type { SvgIconComponent } from "@mui/icons-material";
import { REPORTS } from "@/config/reportConfig";
import { DEMO_UNAVAILABLE_REPORT_IDS, REPORTS } from "@/config/reportConfig";
import { REPORT_CATEGORIES, type ReportCategoryConfig } from "./reportCategories";
import { useReportLabels } from "./reportI18n";

@@ -52,12 +52,16 @@ function ReportCard({
title,
accent,
selected,
unavailable,
unavailableLabel,
onClick,
}: {
reportId: string;
title: string;
accent: string;
selected: boolean;
unavailable: boolean;
unavailableLabel: string;
onClick: () => void;
}) {
const Icon = REPORT_ICON_MAP[reportId] ?? Inventory2OutlinedIcon;
@@ -66,7 +70,9 @@ function ReportCard({
<Box
component="button"
type="button"
onClick={onClick}
disabled={unavailable}
onClick={unavailable ? undefined : onClick}
aria-disabled={unavailable}
sx={{
display: "flex",
alignItems: "center",
@@ -74,17 +80,22 @@ function ReportCard({
width: "100%",
p: 1.5,
textAlign: "left",
cursor: "pointer",
cursor: unavailable ? "not-allowed" : "pointer",
border: "1px solid",
borderColor: selected ? accent : "divider",
borderColor: unavailable ? "action.disabledBackground" : selected ? accent : "divider",
borderRadius: 1.5,
bgcolor: "background.paper",
boxShadow: selected ? 2 : "0 1px 3px rgba(0,0,0,0.06)",
bgcolor: unavailable ? "action.hover" : "background.paper",
color: unavailable ? "text.disabled" : "text.primary",
boxShadow: !unavailable && selected ? 2 : "0 1px 3px rgba(0,0,0,0.06)",
opacity: unavailable ? 0.72 : 1,
filter: unavailable ? "grayscale(1)" : "none",
transition: "border-color 0.15s, box-shadow 0.15s",
"&:hover": {
borderColor: accent,
boxShadow: 2,
},
"&:hover": unavailable
? {}
: {
borderColor: accent,
boxShadow: 2,
},
}}
>
<Box
@@ -95,16 +106,23 @@ function ReportCard({
width: 40,
height: 40,
borderRadius: 1,
bgcolor: `${accent}18`,
color: accent,
bgcolor: unavailable ? "action.disabledBackground" : `${accent}18`,
color: unavailable ? "text.disabled" : accent,
flexShrink: 0,
}}
>
<Icon fontSize="small" />
</Box>
<Typography variant="body2" fontWeight={selected ? 600 : 400} sx={{ lineHeight: 1.35 }}>
{title}
</Typography>
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" fontWeight={selected && !unavailable ? 600 : 400} sx={{ lineHeight: 1.35 }}>
{title}
</Typography>
{unavailable && (
<Typography variant="caption" sx={{ display: "block", lineHeight: 1.3, color: "text.secondary" }}>
{unavailableLabel}
</Typography>
)}
</Box>
</Box>
);
}
@@ -118,7 +136,7 @@ function CategoryColumn({
selectedReportId: string;
onSelectReport: (reportId: string) => void;
}) {
const { reportTitle, categoryTitle } = useReportLabels();
const { t, reportTitle, categoryTitle } = useReportLabels();
const reports = category.reportIds
.map((id) => reportById[id])
.filter(Boolean);
@@ -161,6 +179,8 @@ function CategoryColumn({
title={reportTitle(report)}
accent={category.accent}
selected={selectedReportId === report.id}
unavailable={DEMO_UNAVAILABLE_REPORT_IDS.has(report.id)}
unavailableLabel={t("unavailableInDemo")}
onClick={() => onSelectReport(report.id)}
/>
</Grid>


+ 1
- 1
src/app/(main)/report/bomShopSyncReportApi.ts Visa fil

@@ -205,7 +205,7 @@ function toMaterialExcelRow(

export async function generateBomShopSyncReportExcel(
criteria: Record<string, string>,
reportTitle: string = "M18 BOM Shop 同步記錄",
reportTitle: string = "BOM Shop 同步記錄",
t?: TFunction,
): Promise<void> {
const L = bomSyncLabels(t);


+ 0
- 16
src/app/(main)/report/grnReportApi.ts Visa fil

@@ -33,12 +33,6 @@ export interface GrnReportRow {
lineAmount?: number;
/** PO currency code (currency.code) */
currencyCode?: string;
/** M18 AN document code from m18_goods_receipt_note_log.grn_code */
grnCode?: string;
/** M18 record id (m18_record_id) */
grnId?: number | string;
/** From purchase_order.m18CreatedUId; e.g. "2569 (legato)" */
poM18CreatorDisplay?: string;
[key: string]: unknown;
}

@@ -54,7 +48,6 @@ export interface ListedPoAmounts {
poCode?: string;
currencyCode?: string;
totalAmount?: number;
grnCodes?: string;
}[];
}

@@ -148,13 +141,9 @@ function grnLabels(t?: TFunction) {
unitPrice: tx(t, "excel.grn.unitPrice", "單價"),
currency: tx(t, "excel.grn.currency", "貨幣"),
amount: tx(t, "excel.grn.amount", "金額"),
grnCode: tx(t, "excel.grn.grnCode", "M18 入倉單號"),
grnId: tx(t, "excel.grn.grnId", "M18 記錄編號"),
poCreator: tx(t, "excel.grn.poCreator", "PO建立者(M18)"),
note: tx(t, "excel.grn.note", "備註"),
category: tx(t, "excel.grn.category", "類別"),
totalAmount: tx(t, "excel.grn.totalAmount", "金額"),
grnCodes: tx(t, "excel.grn.grnCodes", "M18 入倉單號"),
noCompletedPo: tx(t, "excel.grn.noCompletedPo", "(篩選範圍內無已完成之 PO 行)"),
categoryCurrencyTotal: tx(t, "excel.grn.categoryCurrencyTotal", "貨幣小計"),
categoryPo: tx(t, "excel.grn.categoryPo", "訂單"),
@@ -187,9 +176,6 @@ function toExcelRow(
base[L.currency] = r.currencyCode ?? "";
base[L.amount] = moneyCellValue(r.lineAmount);
}
base[L.grnCode] = r.grnCode ?? "";
base[L.grnId] = r.grnId ?? "";
base[L.poCreator] = r.poM18CreatorDisplay ?? "";
return base;
}

@@ -213,7 +199,6 @@ function buildListedPoAmountSheetRows(
[L.poNo]: "",
[L.currency]: c.currencyCode ?? "",
[L.totalAmount]: moneyCellValue(c.totalAmount),
[L.grnCodes]: "",
});
}
for (const p of listed.byPurchaseOrder) {
@@ -223,7 +208,6 @@ function buildListedPoAmountSheetRows(
[L.poNo]: p.poCode ?? "",
[L.currency]: p.currencyCode ?? "",
[L.totalAmount]: moneyCellValue(p.totalAmount),
[L.grnCodes]: p.grnCodes ?? "",
});
}
return out;


+ 3
- 1
src/app/(main)/report/page.tsx Visa fil

@@ -25,7 +25,7 @@ import {
} from '@mui/material';
import DownloadIcon from '@mui/icons-material/Download';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { REPORTS } from '@/config/reportConfig';
import { DEMO_UNAVAILABLE_REPORT_IDS, REPORTS } from '@/config/reportConfig';
import { mergePastedItemCodes, buildItemCodePasteRequestBody, parseItemCodeTokens } from './parseItemCodeTokens';
import { NEXT_PUBLIC_API_URL } from '@/config/api';
import { clientAuthFetch } from '@/app/utils/clientAuthFetch';
@@ -152,6 +152,7 @@ export default function ReportPage() {
[selectedReportId]);

const handleSelectReport = (reportId: string) => {
if (DEMO_UNAVAILABLE_REPORT_IDS.has(reportId)) return;
if (reportId === selectedReportId) return;
setSelectedReportId(reportId);
setFieldErrors({});
@@ -452,6 +453,7 @@ export default function ReportPage() {

const executeExcelReport = async () => {
if (!currentReport) return;
if (DEMO_UNAVAILABLE_REPORT_IDS.has(currentReport.id)) return;
if (excelInFlightRef.current) return;
excelInFlightRef.current = true;
setLoading(true);


+ 1
- 1
src/app/(main)/report/reportCategories.ts Visa fil

@@ -33,6 +33,6 @@ export const REPORT_CATEGORIES: ReportCategoryConfig[] = [
headerBg: "#f5d4a8",
bodyBg: "#fdf6ec",
accent: "#e65100",
reportIds: ["rep-006", "rep-005"],
reportIds: ["rep-006", "rep-005", "rep-015"],
},
];

+ 28
- 71
src/app/(main)/testing/page.tsx Visa fil

@@ -37,8 +37,6 @@ import {
type LaserBag2AutoSendReport,
type LaserLastReceiveSuccess,
} from "@/app/api/laserPrint/actions";
import * as XLSX from "xlsx";

interface TabPanelProps {
children?: React.ReactNode;
index: number;
@@ -175,49 +173,6 @@ export default function TestingPage() {
};
}, [tabValue]);

const handleDownloadGrnPreviewXlsx = async () => {
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/report/grn-preview-m18?receiptDate=${encodeURIComponent(
grnPreviewReceiptDate,
)}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
if (!response.ok) throw new Error(`Download failed: ${response.status}`);

const data = await response.json();
const rows = Array.isArray(data?.rows) ? data.rows : [];

const ws = XLSX.utils.json_to_sheet(rows);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "GRN Preview");

const xlsxArrayBuffer = XLSX.write(wb, {
bookType: "xlsx",
type: "array",
});
const blob = new Blob([xlsxArrayBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});

const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute(
"download",
`grn-preview-m18-${grnPreviewReceiptDate}.xlsx`,
);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (e) {
console.error("GRN Preview XLSX Download Error:", e);
alert("GRN Preview XLSX download failed. Check console/network.");
}
};

const downloadBlob = (blob: Blob, filename: string) => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
@@ -524,33 +479,35 @@ export default function TestingPage() {
</Tabs>

<TabPanel value={tabValue} index={0}>
<Section title="1. GRN Preview (M18)">
<Stack
direction="row"
spacing={2}
sx={{ mb: 2, alignItems: "center" }}
>
<TextField
size="small"
label="Receipt Date"
type="date"
value={grnPreviewReceiptDate}
onChange={(e) => setGrnPreviewReceiptDate(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<Button
variant="contained"
color="success"
size="medium"
startIcon={<FileDownload />}
onClick={handleDownloadGrnPreviewXlsx}
<Section title="1. GRN Preview">
<Box sx={{ opacity: 0.55, filter: "grayscale(1)", pointerEvents: "none" }}>
<Stack
direction="row"
spacing={2}
sx={{ mb: 2, alignItems: "center" }}
>
Download GRN Preview XLSX
</Button>
</Stack>
<Typography variant="body2" color="textSecondary">
Backend endpoint:{" "}
<code>/report/grn-preview-m18?receiptDate=YYYY-MM-DD</code>
<TextField
size="small"
label="Receipt Date"
type="date"
value={grnPreviewReceiptDate}
onChange={(e) => setGrnPreviewReceiptDate(e.target.value)}
InputLabelProps={{ shrink: true }}
disabled
/>
<Button
variant="contained"
color="success"
size="medium"
startIcon={<FileDownload />}
disabled
>
Download GRN Preview XLSX
</Button>
</Stack>
</Box>
<Typography variant="body2" color="text.secondary">
Unavailable in demo mode
</Typography>
</Section>
</TabPanel>


+ 4
- 1
src/config/reportConfig.ts Visa fil

@@ -33,6 +33,9 @@ export interface ReportField {

export type ReportResponseType = 'pdf' | 'excel';

/** Live-system reports. The demo page shows them greyed out. */
export const DEMO_UNAVAILABLE_REPORT_IDS = new Set(["rep-015"]);

export interface ReportDefinition {
id: string;
title: string;
@@ -459,7 +462,7 @@ export const REPORTS: ReportDefinition[] = [
},
{
id: "rep-015",
title: "M18 BOM Shop 同步記錄",
title: "BOM Shop 同步記錄",
apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history`,
responseType: "excel",
fields: [


+ 3
- 6
src/i18n/en/report.json Visa fil

@@ -15,6 +15,7 @@
"noDataFoundTitle": "No Data Found / 查無資料",
"noDataFoundHint": "No inventory records match your search. Please try adjusting the filters.",
"ok": "OK",
"unavailableInDemo": "Unavailable in demo mode",
"missingRequired": "Missing required fields:\n- {{fields}}",
"requiredField": "This is a required field",
"dateNotBeforeToday": "Date cannot be before today:\n- {{fields}}",
@@ -231,7 +232,7 @@
}
},
"rep-015": {
"title": "M18 BOM Shop Sync History",
"title": "BOM Shop Sync History",
"fields": {
"syncDateStart": "Sync Date Start",
"syncDateEnd": "Sync Date End",
@@ -332,13 +333,9 @@
"unitPrice": "Unit Price",
"currency": "Currency",
"amount": "Amount",
"grnCode": "GRN Code / M18 Receipt No.",
"grnId": "GRN Id / M18 Record Id",
"poCreator": "PO creator (M18)",
"note": "Note",
"category": "Category",
"totalAmount": "Total Amount",
"grnCodes": "GRN Code(s) / M18 Receipt No."
"totalAmount": "Total Amount"
},
"bomSync": {
"sheetSync": "BOM Sync Log",


+ 3
- 6
src/i18n/zh/report.json Visa fil

@@ -15,6 +15,7 @@
"noDataFoundTitle": "查無資料",
"noDataFoundHint": "找不到符合您搜尋條件的庫存資料,請嘗試調整篩選條件。",
"ok": "確定",
"unavailableInDemo": "示範模式無法使用",
"missingRequired": "缺少必填條件:\n- {{fields}}",
"requiredField": "此為必填欄位",
"dateNotBeforeToday": "日期不可早於今天:\n- {{fields}}",
@@ -231,7 +232,7 @@
}
},
"rep-015": {
"title": "M18 BOM Shop 同步記錄",
"title": "BOM Shop 同步記錄",
"fields": {
"syncDateStart": "同步日期:由",
"syncDateEnd": "同步日期:至",
@@ -332,13 +333,9 @@
"unitPrice": "單價",
"currency": "貨幣",
"amount": "金額",
"grnCode": "M18 入倉單號",
"grnId": "M18 記錄編號",
"poCreator": "PO建立者(M18)",
"note": "備註",
"category": "類別",
"totalAmount": "金額",
"grnCodes": "M18 入倉單號"
"totalAmount": "金額"
},
"bomSync": {
"sheetSync": "BOM同步記錄",


Laddar…
Avbryt
Spara