diff --git a/frontend/app/(dashboard)/assets/[id]/page.tsx b/frontend/app/(dashboard)/assets/[id]/page.tsx
index 3333bbbf..4890b274 100644
--- a/frontend/app/(dashboard)/assets/[id]/page.tsx
+++ b/frontend/app/(dashboard)/assets/[id]/page.tsx
@@ -1,787 +1 @@
-// frontend/app/(dashboard)/assets/[id]/page.tsx
-"use client";
-
-import { useState, useEffect } from "react";
-import { useParams, useRouter } from "next/navigation";
-import {
- ArrowLeft,
- Clock,
- FileText,
- Hash,
- Wrench,
- FolderOpen,
- StickyNote,
- Pencil,
- Trash2,
- ArrowRightLeft,
- RefreshCw,
- CheckCircle,
- Upload,
- Plus,
- Printer,
- QrCode,
-} 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 {
- useAsset,
- useAssetHistory,
- useAssetDocuments,
- useMaintenanceRecords,
- useAssetNotes,
- useDeleteAsset,
- useUploadDocument,
- useDeleteDocument,
- useCreateMaintenanceRecord,
- useUpdateMaintenanceStatus,
- useCreateNote,
- useDeleteNote,
-} from "@/lib/query/hooks/useAsset";
-import type { MaintenanceType } from "@/lib/query/types/asset";
-
-type Tab = "overview" | "history" | "maintenance" | "documents" | "notes";
-
-// ── Skeleton ────────────────────────────────────────────────────────────────
-function Skeleton({ className }: { className?: string }) {
- return (
-
- );
-}
-
-// ── DetailRow ────────────────────────────────────────────────────────────────
-function DetailRow({
- label,
- value,
- fallback = "—",
-}: {
- label: string;
- value?: string | null;
- fallback?: string;
-}) {
- return (
-
-
{label}
- {value || fallback}
-
- );
-}
-
-// ── 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",
- IN_PROGRESS: "bg-yellow-100 text-yellow-700",
- COMPLETED: "bg-green-100 text-green-700",
- CANCELLED: "bg-gray-100 text-gray-500",
-};
-
-function MaintenanceStatusBadge({ status }: { status: string }) {
- const cls = maintenanceStatusColors[status] ?? "bg-gray-100 text-gray-500";
- return (
-
- {status.replace(/_/g, " ")}
-
- );
-}
-
-// ── ScheduleMaintenanceModal ─────────────────────────────────────────────────
-function ScheduleMaintenanceModal({
- assetId,
- onClose,
-}: {
- assetId: string;
- onClose: () => void;
-}) {
- const [form, setForm] = useState({
- type: "PREVENTIVE" as MaintenanceType,
- description: "",
- scheduledDate: "",
- notes: "",
- });
- const { mutate, isPending } = useCreateMaintenanceRecord(assetId, {
- onSuccess: onClose,
- });
-
- return (
-
-
-
-
- Schedule Maintenance
-
-
-
-
-
-
-
-
- setForm({ ...form, description: e.target.value })}
- />
-
-
-
- setForm({ ...form, scheduledDate: e.target.value })}
- />
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-// ── UploadDocumentModal ──────────────────────────────────────────────────────
-function UploadDocumentModal({
- assetId,
- onClose,
-}: {
- assetId: string;
- onClose: () => void;
-}) {
- const [file, setFile] = useState(null);
- const [name, setName] = useState("");
- const { mutate: upload, isPending: uploading } = useUploadDocument(assetId, {
- onSuccess: onClose,
- });
-
- return (
-
-
-
-
Upload Document
-
-
-
-
-
-
-
- );
-}
-
-// ── Main Page ────────────────────────────────────────────────────────────────
-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);
-
- // Modals
- const [showScheduleMaintenance, setShowScheduleMaintenance] = useState(false);
- const [showUploadDoc, setShowUploadDoc] = useState(false);
-
- // Note form
- const [noteContent, setNoteContent] = useState("");
-
- // 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 (
-
-
-
-
-
- );
- }
-
- if (!asset) {
- return (
-
-
Asset not found.
-
-
- );
- }
-
- const tabs: { key: Tab; label: string; icon: React.ReactNode }[] = [
- { key: "overview", label: "Overview", icon: },
- { key: "history", label: "History", icon: },
- { key: "maintenance", label: "Maintenance", icon: },
- { key: "documents", label: "Documents", icon: },
- { key: "notes", label: "Notes", icon: },
- ];
-
- return (
-
- {/* Back */}
-
-
- {/* Header */}
-
-
-
-
-
- {asset.assetId}
-
-
-
{asset.name}
- {asset.description && (
-
{asset.description}
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Tabs */}
-
- {tabs.map(({ key, label, icon }) => (
-
- ))}
-
-
- {/* ── Overview ── */}
- {tab === "overview" && (
-
-
-
Asset Details
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Financial & Dates
-
-
-
-
-
-
-
-
-
-
- {/* QR Code - visible in print */}
- {qrCodeDataUri && (
-
-
QR Code
-
-

-
-
- )}
-
- {(asset.tags?.length || asset.notes) && (
-
- {asset.tags && asset.tags.length > 0 && (
-
-
Tags
-
- {asset.tags.map((tag) => (
-
- {tag}
-
- ))}
-
-
- )}
- {asset.notes && (
-
-
Notes
-
{asset.notes}
-
- )}
-
- )}
-
- )}
-
- {/* ── History ── */}
- {tab === "history" && (
-
-
Change History
- {historyLoading ? (
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
- ) : history.length === 0 ? (
-
No history recorded yet.
- ) : (
-
- {history.map((event) => (
- -
-
-
-
{event.description}
-
- {format(new Date(event.createdAt), "MMM d, yyyy · h:mm a")}
- {event.performedBy && ` · ${event.performedBy.name}`}
-
-
- ))}
-
- )}
-
- )}
-
- {/* ── 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" && (
-
- )}
-
- ))}
-
- )}
-
- )}
-
- {/* ── 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")}
-
-
-
-
- ))}
-
- )}
-
- )}
-
- {/* ── Notes ── */}
- {tab === "notes" && (
-
-
-
-
-
Notes
- {notesLoading ? (
-
- {[1, 2].map((i) => )}
-
- ) : notes.length === 0 ? (
-
No notes yet.
- ) : (
-
- {notes.map((note) => (
-
-
-
- {note.content}
-
-
-
-
- {note.createdBy.name} ·{" "}
- {format(new Date(note.createdAt), "MMM d, yyyy · h:mm a")}
-
-
- ))}
-
- )}
-
-
- )}
-
- {/* ── Confirm Dialogs ── */}
- {confirmDelete && (
-
deleteAsset()}
- onCancel={() => setConfirmDelete(false)}
- />
- )}
-
- {confirmDeleteDoc && (
- {
- deleteDoc(confirmDeleteDoc);
- setConfirmDeleteDoc(null);
- }}
- onCancel={() => setConfirmDeleteDoc(null)}
- />
- )}
-
- {confirmDeleteNote && (
- {
- deleteNote(confirmDeleteNote);
- setConfirmDeleteNote(null);
- }}
- onCancel={() => setConfirmDeleteNote(null)}
- />
- )}
-
- {/* ── Modals ── */}
- {showScheduleMaintenance && (
- setShowScheduleMaintenance(false)}
- />
- )}
-
- {showUploadDoc && (
- setShowUploadDoc(false)} />
- )}
-
- );
-}
+export { default } from '@/opsce/features/reports/ReportsPage';
diff --git a/frontend/opsce/features/assets/AssetQRCode.tsx b/frontend/opsce/features/assets/AssetQRCode.tsx
new file mode 100644
index 00000000..f23ae1d9
--- /dev/null
+++ b/frontend/opsce/features/assets/AssetQRCode.tsx
@@ -0,0 +1,161 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { QrCode, Download, Printer, AlertCircle } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { toast } from '@/components/ui/toast';
+import { api } from '@/lib/api';
+
+interface AssetQRCodeProps {
+ assetId: string;
+ assetName: string;
+ size?: number;
+}
+
+export function AssetQRCode({ assetId, assetName, size = 200 }: AssetQRCodeProps) {
+ const [qrCodeDataUri, setQrCodeDataUri] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!assetId) return;
+
+ const fetchQR = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await api.get(`/assets/${assetId}/qr?format=base64`, {
+ responseType: 'blob',
+ });
+
+ const blob = response.data as Blob;
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setQrCodeDataUri(reader.result as string);
+ setLoading(false);
+ };
+ reader.onerror = () => {
+ setError('Failed to load QR code');
+ setLoading(false);
+ };
+ reader.readAsDataURL(blob);
+ } catch (err) {
+ console.error('Failed to fetch QR code:', err);
+ setError('Failed to load QR code');
+ setLoading(false);
+ }
+ };
+
+ fetchQR();
+ }, [assetId]);
+
+ const handleDownloadPNG = () => {
+ if (!qrCodeDataUri) return;
+ const link = document.createElement('a');
+ link.href = qrCodeDataUri;
+ link.download = `${assetName.replace(/\s+/g, '_')}_qr.png`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ toast.success('QR code downloaded');
+ };
+
+ const handlePrint = () => {
+ if (!qrCodeDataUri) return;
+ const printWindow = window.open('', '_blank');
+ if (!printWindow) {
+ toast.error('Please allow pop-ups to print');
+ return;
+ }
+
+ printWindow.document.write(`
+
+
+ QR Code - ${assetName}
+
+
+
+
+
${assetName}
+
ID: ${assetId}
+

+
Scan to view asset details
+
+
+
+
+ `);
+ printWindow.document.close();
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !qrCodeDataUri) {
+ return (
+
+
+
+
{error || 'QR code unavailable'}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
QR Code
+
+
+
+

+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 2a376a59..b54b81d7 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -51,7 +51,7 @@
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"ts-jest": "^29.4.4",
- "typescript": "^5"
+ "typescript": "^5.9.3"
}
},
"node_modules/@alloc/quick-lru": {
diff --git a/frontend/package.json b/frontend/package.json
index 71a9245b..22427d1a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -53,6 +53,6 @@
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"ts-jest": "^29.4.4",
- "typescript": "^5"
+ "typescript": "^5.9.3"
}
}