diff --git a/frontend/app/(dashboard)/assets/[id]/page.tsx b/frontend/app/(dashboard)/assets/[id]/page.tsx
index 4890b274..ed1f5d15 100644
--- a/frontend/app/(dashboard)/assets/[id]/page.tsx
+++ b/frontend/app/(dashboard)/assets/[id]/page.tsx
@@ -1 +1,740 @@
+// frontend/app/(dashboard)/assets/[id]/page.tsx
+"use client";
+
+import { useState } from "react";
+import { useParams, useRouter } from "next/navigation";
+import {
+ ArrowLeft,
+ Clock,
+ FileText,
+ Hash,
+ Wrench,
+ FolderOpen,
+ StickyNote,
+ Pencil,
+ ArrowRightLeft,
+ RefreshCw,
+ CheckCircle,
+ Upload,
+ Plus,
+ 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,
+ 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}
+
+ );
+}
+
+// ── 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");
+
+ // Confirm dialogs
+ 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: maintenance = [], isLoading: maintenanceLoading } = useMaintenanceRecords(id);
+ const { data: documents = [], isLoading: documentsLoading } = useAssetDocuments(id);
+ const { data: notes = [], isLoading: notesLoading } = useAssetNotes(id);
+
+ // Mutations
+ const { mutate: deleteDoc, isPending: deletingDoc } = useDeleteDocument(id);
+ const { mutate: deleteNote, isPending: deletingNote } = useDeleteNote(id);
+ const { mutate: addNote, isPending: addingNote } = useCreateNote(id, {
+ onSuccess: () => setNoteContent(""),
+ });
+
+ 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 */}
+
+
+ {(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" && (
+
+ )}
+
+ {/* ── Maintenance ── */}
+ {tab === "maintenance" && (
+
setShowScheduleMaintenance(true)}
+ />
+ )}
+
+ {/* ── Documents ── */}
+ {tab === "documents" && (
+ setShowUploadDoc(true)}
+ onDelete={(docId) => setConfirmDeleteDoc(docId)}
+ />
+ )}
+
+ {/* ── Notes ── */}
+ {tab === "notes" && (
+ addNote({ content: noteContent.trim() })}
+ onDeleteNote={(noteId) => setConfirmDeleteNote(noteId)}
+ />
+ )}
+
+ {/* ── Confirm Dialogs ── */}
+ {confirmDeleteDoc && (
+ {
+ deleteDoc(confirmDeleteDoc);
+ setConfirmDeleteDoc(null);
+ }}
+ onCancel={() => setConfirmDeleteDoc(null)}
+ />
+ )}
+
+ {confirmDeleteNote && (
+ {
+ deleteNote(confirmDeleteNote);
+ setConfirmDeleteNote(null);
+ }}
+ onCancel={() => setConfirmDeleteNote(null)}
+ />
+ )}
+
+ {/* ── Modals ── */}
+ {showScheduleMaintenance && (
+ setShowScheduleMaintenance(false)}
+ />
+ )}
+
+ {showUploadDoc && (
+ setShowUploadDoc(false)} />
+ )}
+
+ );
+}
+
+// ── Maintenance Tab ───────────────────────────────────────────
+function MaintenanceTabContent({
+ assetId,
+ maintenance,
+ loading,
+ onSchedule,
+}: {
+ assetId: string;
+ maintenance: any[];
+ loading: boolean;
+ onSchedule: () => void;
+}) {
+ const { mutate: markComplete, isPending: markingComplete } = useUpdateMaintenanceStatus(assetId);
+
+ return (
+
+
+
Maintenance Records
+
+
+ {loading ? (
+
+ {[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 ─────────────────────────────────────────────
+function DocumentsTabContent({
+ documents,
+ loading,
+ onUpload,
+ onDelete,
+}: {
+ documents: any[];
+ loading: boolean;
+ onUpload: () => void;
+ onDelete: (docId: string) => void;
+}) {
+ return (
+
+
+
Documents
+
+
+ {loading ? (
+
+ {[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 ─────────────────────────────────────────────────
+function NotesTabContent({
+ notes,
+ loading,
+ noteContent,
+ onNoteContentChange,
+ addingNote,
+ onAddNote,
+ onDeleteNote,
+}: {
+ notes: any[];
+ loading: boolean;
+ noteContent: string;
+ onNoteContentChange: (content: string) => void;
+ addingNote: boolean;
+ onAddNote: () => void;
+ onDeleteNote: (noteId: string) => void;
+}) {
+ return (
+
+
+
+
+
Notes
+ {loading ? (
+
+ {[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")}
+
+
+ ))}
+
+ )}
+
+
+ );
+}
+
export { default } from '@/opsce/features/reports/ReportsPage';
diff --git a/frontend/opsce/features/assets/DownloadAssetReportButton.tsx b/frontend/opsce/features/assets/DownloadAssetReportButton.tsx
new file mode 100644
index 00000000..c14aa003
--- /dev/null
+++ b/frontend/opsce/features/assets/DownloadAssetReportButton.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import { useState, useCallback } from 'react';
+import { FileDown } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { toast } from '@/components/ui/toast';
+import { api } from '@/lib/api';
+
+interface DownloadAssetReportButtonProps {
+ assetId: string;
+ assetName: string;
+}
+
+export function DownloadAssetReportButton({ assetId, assetName }: DownloadAssetReportButtonProps) {
+ const [loading, setLoading] = useState(false);
+
+ const handleDownload = useCallback(async () => {
+ setLoading(true);
+ try {
+ const response = await api.get(`/assets/${assetId}/report`, {
+ responseType: 'blob',
+ });
+
+ const blob = new Blob([response.data], { type: 'application/pdf' });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `${assetName.replace(/\s+/g, '_')}_report.pdf`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+
+ toast.success('Report downloaded successfully');
+ } catch (err) {
+ console.error('Failed to download asset report:', err);
+ toast.error('Failed to download report. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ }, [assetId, assetName]);
+
+ return (
+
+ );
+}
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": {