Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 38 additions & 21 deletions web-frontend/src/components/Canvas/ExportReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import { TbFileSpreadsheet } from "react-icons/tb";
import * as XLSX from "xlsx";

import { useEditorStore } from "@/store/useEditorStore";
import { getReportFooterHTML } from "@/utils/exportFooter";

interface ExportReportModalProps {
editorId: string;
open: boolean;
projectName?: string;
onClose: () => void;
}

Expand All @@ -51,11 +53,16 @@ interface FormatOption {
export const ExportReportModal: React.FC<ExportReportModalProps> = ({
editorId,
open,
projectName,
onClose,
}) => {
const editorState = useEditorStore((s) => s.editors[editorId]);
const [exportFormat, setExportFormat] = useState<ExportFormat>("csv");

const createdBy = localStorage.getItem("username") || "Unknown User";
const dateStr = new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
const footerHTML = getReportFooterHTML(projectName || "Untitled Project", createdBy, dateStr);

// Transform editor items to report items
const items = useMemo(() => {
if (!editorState?.items) return [];
Expand Down Expand Up @@ -242,9 +249,13 @@ export const ExportReportModal: React.FC<ExportReportModalProps> = ({
.stat-item { text-align: center; }
.stat-value { font-size: 24px; font-weight: bold; color: #4F46E5; }
.stat-label { font-size: 12px; color: #666; }
/* Title block table must not inherit outer table styles */
.title-block table { margin-top: 0; border-collapse: collapse; }
.title-block td { border: none !important; padding: 5px 0; }
@media print {
body { margin: 0; }
.no-print { display: none; }
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
}
</style>
</head>
Expand All @@ -264,11 +275,10 @@ export const ExportReportModal: React.FC<ExportReportModalProps> = ({
<div class="stat-label">Unique Types</div>
</div>
<div class="stat-item">
<div class="stat-value">${
items.filter(
(i) => i.description && i.description !== "No description",
).length
}</div>
<div class="stat-value">${items.filter(
(i) => i.description && i.description !== "No description",
).length
}</div>
<div class="stat-label">With Description</div>
</div>
</div>
Expand All @@ -284,19 +294,20 @@ export const ExportReportModal: React.FC<ExportReportModalProps> = ({
</thead>
<tbody>
${items
.map(
(item) => `
.map(
(item) => `
<tr>
<td>${item.slNo}</td>
<td><strong>${item.tagNo}</strong></td>
<td>${item.type}</td>
<td>${item.description}</td>
</tr>
`,
)
.join("")}
)
.join("")}
</tbody>
</table>
${footerHTML}
<div class="footer">
Total Items: ${items.length}
</div>
Expand Down Expand Up @@ -362,6 +373,12 @@ export const ExportReportModal: React.FC<ExportReportModalProps> = ({
th { background-color: #4F46E5; color: white; font-weight: 600; padding: 12px 10px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #e5e7eb; }
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center; color: #6b7280; font-size: 9pt; }
/* Title block must not inherit outer table styles */
.title-block table { margin-top: 0; border-collapse: collapse; }
.title-block td { border: none !important; padding: 5px 0; }
@media print {
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
}
</style>
</head>
<body>
Expand All @@ -380,11 +397,10 @@ export const ExportReportModal: React.FC<ExportReportModalProps> = ({
<div class="meta-label">Equipment Types</div>
</div>
<div class="meta-item">
<div class="meta-value">${
items.filter(
(i) => i.description && i.description !== "No description",
).length
}</div>
<div class="meta-value">${items.filter(
(i) => i.description && i.description !== "No description",
).length
}</div>
<div class="meta-label">Documented Items</div>
</div>
<div class="meta-item">
Expand All @@ -404,20 +420,22 @@ export const ExportReportModal: React.FC<ExportReportModalProps> = ({
</thead>
<tbody>
${items
.map(
(item) => `
.map(
(item) => `
<tr>
<td>${item.slNo}</td>
<td><strong>${item.tagNo}</strong></td>
<td>${item.type}</td>
<td>${item.description}</td>
</tr>
`,
)
.join("")}
)
.join("")}
</tbody>
</table>

${footerHTML}

<div class="footer">
<p>Report ID: EQ-${new Date().getTime().toString().slice(-6)} | Page 1 of 1</p>
<p>This is a system-generated report. For official use, please verify with the equipment database.</p>
Expand Down Expand Up @@ -532,11 +550,10 @@ export const ExportReportModal: React.FC<ExportReportModalProps> = ({
key={format.key}
isHoverable
isPressable
className={`cursor-pointer transition-all ${
exportFormat === format.key
className={`cursor-pointer transition-all ${exportFormat === format.key
? `ring-2 ring-blue-500 ${format.color} border-2 ${format.borderColor}`
: ""
}`}
}`}
onPress={() => handleFormatSelect(format.key)}
>
<CardBody className="p-3">
Expand Down
20 changes: 11 additions & 9 deletions web-frontend/src/pages/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,7 @@ import {
importFromDiagramFile,
migrateExportData,
} from "@/utils/diagramExport";
// import {
// getProject,
// saveProject,
// createProject,
// type SavedProject,
// convertToBackendFormat,
// } from "@/utils/projectStorage";
import { appendFooterToImage } from "@/utils/exportFooter";
import {
createProject,
fetchProject,
Expand Down Expand Up @@ -603,7 +597,7 @@ export default function Editor() {
tempStage.batchDraw();

// Generate data URL from temp stage
const dataUrl = tempStage.toDataURL({
let dataUrl = tempStage.toDataURL({
pixelRatio,
mimeType,
quality,
Expand All @@ -613,6 +607,13 @@ export default function Editor() {
tempStage.destroy();
document.body.removeChild(tempContainer);

// Append Footer Block (mimicking PyQt title block)
const projectName = projectMetadata?.name || "Untitled Project";
const createdBy = localStorage.getItem("username") || "Unknown User";
const exportDate = new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });

dataUrl = await appendFooterToImage(dataUrl, projectName, createdBy, exportDate, backgroundFill, pixelRatio);

/* =========================
PDF EXPORT - FIXED
========================= */
Expand Down Expand Up @@ -2354,8 +2355,9 @@ export default function Editor() {
/>

<ExportReportModal
editorId={projectId ?? ""}
editorId={projectId || ""}
open={showReportModal}
projectName={projectMetadata?.name || "Untitled Project"}
onClose={() => setShowReportModal(false)}
/>

Expand Down
114 changes: 114 additions & 0 deletions web-frontend/src/utils/exportFooter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
export async function appendFooterToImage(
dataUrl: string,
projectName: string,
createdBy: string,
date: string,
backgroundColor: string,
scale: number = 2
): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image();

img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

if (!ctx) {
return reject(new Error("Failed to get 2d context"));
}

const blockWidth = Math.min(360 * scale, Math.max(250 * scale, img.width * 0.34));
const blockHeight = 92 * scale;
const margin = 24 * scale;

// Keep original image dimensions; draw footer inside the existing bounds
canvas.width = img.width;
canvas.height = img.height;

// Fill background
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Draw original diagram image
ctx.drawImage(img, 0, 0);

// Calculate position for the footer block (bottom-right of existing image bounds)
const x = canvas.width - blockWidth - margin;
const y = canvas.height - blockHeight - margin;

// Draw footer block background and border
ctx.fillStyle = "#FFF8F2";
ctx.strokeStyle = "#C97B5A";
ctx.lineWidth = 1.5 * scale;

// Rounded rectangle
ctx.beginPath();
ctx.roundRect(x, y, blockWidth, blockHeight, 8 * scale);
ctx.fill();
ctx.stroke();

// Text setup
ctx.fillStyle = "#6B4A3B";
const labelFontSize = 11 * scale;
const valueFontSize = 12 * scale;
const lineGap = 24 * scale;
const textX = x + 16 * scale;
const labelY = y + 26 * scale;

const rows = [
{ label: "Project Name:", value: projectName },
{ label: "Created By:", value: createdBy },
{ label: "Date:", value: date },
];

rows.forEach((row, index) => {
const rowY = labelY + index * lineGap;

ctx.font = `bold ${labelFontSize}px sans-serif`;
ctx.fillText(row.label, textX, rowY);

ctx.font = `normal ${valueFontSize}px sans-serif`;
ctx.fillText(row.value, textX + 110 * scale, rowY);
});

resolve(canvas.toDataURL("image/png", 1.0));
};
img.onerror = reject;
img.src = dataUrl;
});
}

export function getReportFooterHTML(
projectName: string,
createdBy: string,
date: string
): string {
return `
<div class="title-block" style="
width: 320px;
background-color: #FFF8F2;
border: 2px solid #C97B5A;
border-radius: 8px;
padding: 14px 16px;
margin-left: auto;
margin-top: 40px;
page-break-inside: avoid;
box-sizing: border-box;
">
<table style="width: 100%; border-collapse: collapse; border: none;">
<tr style="border: none;">
<td style="padding: 5px 0; font-weight: bold; color: #6B4A3B; font-size: 10pt; width: 110px; white-space: nowrap; border: none;">Project Name:</td>
<td style="padding: 5px 0; color: #6B4A3B; font-size: 10pt; border: none;">${projectName}</td>
</tr>
<tr style="border: none;">
<td style="padding: 5px 0; font-weight: bold; color: #6B4A3B; font-size: 10pt; width: 110px; white-space: nowrap; border: none;">Created By:</td>
<td style="padding: 5px 0; color: #6B4A3B; font-size: 10pt; border: none;">${createdBy}</td>
</tr>
<tr style="border: none;">
<td style="padding: 5px 0; font-weight: bold; color: #6B4A3B; font-size: 10pt; width: 110px; white-space: nowrap; border: none;">Date:</td>
<td style="padding: 5px 0; color: #6B4A3B; font-size: 10pt; border: none;">${date}</td>
</tr>
</table>
</div>
`;
}
Loading