From b8a5536852289113d901d8384f04388a17094c24 Mon Sep 17 00:00:00 2001 From: kilodesodiq-arch Date: Sat, 30 May 2026 19:55:28 +0000 Subject: [PATCH] feat: implement PDF export for single asset report Closes #782 --- frontend/app/(dashboard)/assets/[id]/page.tsx | 522 ++++++++---------- .../assets/DownloadAssetReportButton.tsx | 54 ++ frontend/package-lock.json | 2 +- frontend/package.json | 2 +- 4 files changed, 293 insertions(+), 287 deletions(-) create mode 100644 frontend/opsce/features/assets/DownloadAssetReportButton.tsx diff --git a/frontend/app/(dashboard)/assets/[id]/page.tsx b/frontend/app/(dashboard)/assets/[id]/page.tsx index 3333bbbf..e89e0aec 100644 --- a/frontend/app/(dashboard)/assets/[id]/page.tsx +++ b/frontend/app/(dashboard)/assets/[id]/page.tsx @@ -1,7 +1,7 @@ // frontend/app/(dashboard)/assets/[id]/page.tsx "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { ArrowLeft, @@ -12,23 +12,24 @@ import { FolderOpen, StickyNote, Pencil, - Trash2, ArrowRightLeft, RefreshCw, CheckCircle, Upload, Plus, - Printer, - QrCode, + Trash2, } from "lucide-react"; import { format } from "date-fns"; import { Button } from "@/components/ui/button"; import { StatusBadge } from "@/components/assets/status-badge"; import { ConditionBadge } from "@/components/assets/condition-badge"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { AssetQRCode } from "@/opsce/features/assets/AssetQRCode"; +import { AssetHistoryTimeline } from "@/opsce/features/assets/AssetHistoryTimeline"; +import { DeleteAssetDialog } from "@/opsce/features/assets/DeleteAssetDialog"; +import { DownloadAssetReportButton } from "@/opsce/features/assets/DownloadAssetReportButton"; import { useAsset, - useAssetHistory, useAssetDocuments, useMaintenanceRecords, useAssetNotes, @@ -69,26 +70,6 @@ function DetailRow({ ); } -// ── ActionBadge ────────────────────────────────────────────────────────────── -const actionColors: Record = { - CREATED: "bg-green-100 text-green-700", - UPDATED: "bg-blue-100 text-blue-700", - STATUS_CHANGED: "bg-yellow-100 text-yellow-700", - TRANSFERRED: "bg-purple-100 text-purple-700", - MAINTENANCE: "bg-orange-100 text-orange-700", - NOTE_ADDED: "bg-gray-100 text-gray-600", - DOCUMENT_UPLOADED: "bg-teal-100 text-teal-700", -}; - -function ActionBadge({ action }: { action: string }) { - const cls = actionColors[action] ?? "bg-gray-100 text-gray-600"; - return ( - - {action.replace(/_/g, " ")} - - ); -} - // ── MaintenanceStatusBadge ─────────────────────────────────────────────────── const maintenanceStatusColors: Record = { SCHEDULED: "bg-blue-100 text-blue-700", @@ -255,10 +236,8 @@ export default function AssetDetailPage() { const { id } = useParams<{ id: string }>(); const router = useRouter(); const [tab, setTab] = useState("overview"); - const [qrCodeDataUri, setQrCodeDataUri] = useState(null); // Confirm dialogs - const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDeleteDoc, setConfirmDeleteDoc] = useState(null); const [confirmDeleteNote, setConfirmDeleteNote] = useState(null); @@ -271,60 +250,17 @@ export default function AssetDetailPage() { // Queries const { data: asset, isLoading } = useAsset(id); - const { data: history = [], isLoading: historyLoading } = useAssetHistory(id); const { data: maintenance = [], isLoading: maintenanceLoading } = useMaintenanceRecords(id); const { data: documents = [], isLoading: documentsLoading } = useAssetDocuments(id); const { data: notes = [], isLoading: notesLoading } = useAssetNotes(id); // Mutations - const { mutate: deleteAsset, isPending: deletingAsset } = useDeleteAsset(id, { - onSuccess: () => router.push("/assets"), - }); const { mutate: deleteDoc, isPending: deletingDoc } = useDeleteDocument(id); const { mutate: deleteNote, isPending: deletingNote } = useDeleteNote(id); - const { mutate: markComplete, isPending: markingComplete } = useUpdateMaintenanceStatus(id); const { mutate: addNote, isPending: addingNote } = useCreateNote(id, { onSuccess: () => setNoteContent(""), }); - // Fetch QR code when asset loads - useEffect(() => { - if (!asset?.id) return; - - const fetchQRCode = async () => { - try { - const response = await fetch(`/api/assets/${asset.id}/qr`); - if (response.ok) { - const blob = await response.blob(); - const reader = new FileReader(); - reader.onloadend = () => { - setQrCodeDataUri(reader.result as string); - }; - reader.readAsDataURL(blob); - } - } catch (error) { - console.error('Failed to fetch QR code:', error); - } - }; - - fetchQRCode(); - }, [asset?.id]); - - // Print handler - const handlePrint = () => { - if (!asset) return; - - // Set page title to asset name - const originalTitle = document.title; - document.title = asset.name; - - // Trigger print - window.print(); - - // Restore original title - document.title = originalTitle; - }; - if (isLoading) { return (
@@ -393,21 +329,8 @@ export default function AssetDetailPage() { - - + +
@@ -497,19 +420,8 @@ export default function AssetDetailPage() { - {/* QR Code - visible in print */} - {qrCodeDataUri && ( -
-

QR Code

-
- {`QR -
-
- )} + {/* QR Code */} + {(asset.tags?.length || asset.notes) && (
@@ -541,208 +453,43 @@ export default function AssetDetailPage() { {/* ── History ── */} {tab === "history" && ( -
-

Change History

- {historyLoading ? ( -
- {[1, 2, 3].map((i) => ( - - ))} -
- ) : history.length === 0 ? ( -

No history recorded yet.

- ) : ( -
    - {history.map((event) => ( -
  1. -
    -
    - -
    -

    {event.description}

    -

    - {format(new Date(event.createdAt), "MMM d, yyyy · h:mm a")} - {event.performedBy && ` · ${event.performedBy.name}`} -

    -
  2. - ))} -
- )} -
+ )} {/* ── Maintenance ── */} {tab === "maintenance" && ( -
-
-

Maintenance Records

- -
- {maintenanceLoading ? ( -
- {[1, 2].map((i) => )} -
- ) : maintenance.length === 0 ? ( -

No maintenance records.

- ) : ( -
- {maintenance.map((record) => ( -
-
-
- {record.type} - -
-

{record.description}

-

- Scheduled: {format(new Date(record.scheduledDate), "MMM d, yyyy")} - {record.cost != null && ` · $${Number(record.cost).toLocaleString()}`} -

-
- {record.status !== "COMPLETED" && record.status !== "CANCELLED" && ( - - )} -
- ))} -
- )} -
+ setShowScheduleMaintenance(true)} + /> )} {/* ── Documents ── */} {tab === "documents" && ( -
-
-

Documents

- -
- {documentsLoading ? ( -
- {[1, 2].map((i) => )} -
- ) : documents.length === 0 ? ( -

No documents uploaded.

- ) : ( -
- {documents.map((doc) => ( -
-
-

{doc.name}

-

- {doc.type} · {(doc.size / 1024).toFixed(1)} KB ·{" "} - {format(new Date(doc.createdAt), "MMM d, yyyy")} -

-
- -
- ))} -
- )} -
+ setShowUploadDoc(true)} + onDelete={(docId) => setConfirmDeleteDoc(docId)} + /> )} {/* ── Notes ── */} {tab === "notes" && ( -
-
-

Add Note

-