diff --git a/frontend/app/(dashboard)/assets/page.tsx b/frontend/app/(dashboard)/assets/page.tsx
index 3209bdda2..47e9b02e4 100644
--- a/frontend/app/(dashboard)/assets/page.tsx
+++ b/frontend/app/(dashboard)/assets/page.tsx
@@ -2,22 +2,46 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
-import { Plus, Search, SlidersHorizontal } from "lucide-react";
+import { Plus, Search, SlidersHorizontal, ScanLine } from "lucide-react";
import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/assets/status-badge";
import { ConditionBadge } from "@/components/assets/condition-badge";
import { CreateAssetModal } from "@/components/assets/create-asset-modal";
+import { ScannerModal } from "@/components/assets/ScannerModal";
import { useAssets } from "@/lib/query/hooks/useAssets";
import { AssetStatus, AssetCondition } from "@/lib/query/types/asset";
+import { useToast } from "@/components/ui/use-toast";
+import { apiClient } from "@/lib/api/client";
const STATUS_OPTIONS = ["All", ...Object.values(AssetStatus)];
export default function AssetsPage() {
const router = useRouter();
- const [showModal, setShowModal] = useState(false);
- const [search, setSearch] = useState("");
- const [status, setStatus] = useState("");
- const [page, setPage] = useState(1);
+ const [showScanner, setShowScanner] = useState(false);
+ const { toast } = useToast();
+
+ const handleScanSuccess = async (decodedText: string) => {
+ setShowScanner(false);
+ try {
+ const res = await apiClient.get(`/assets/scan?code=${decodedText}`);
+ if (res.data) {
+ router.push(`/assets/${res.data.id}`);
+ } else {
+ toast({
+ title: "Not Found",
+ description: "No asset found for this code.",
+ variant: "destructive",
+ });
+ }
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: "Failed to fetch asset information.",
+ variant: "destructive",
+ });
+ }
+ };
+
const { data, isLoading, refetch } = useAssets({
search: search || undefined,
@@ -42,12 +66,32 @@ export default function AssetsPage() {
: "No assets yet"}
-
+
+
+
+
+ {/* FAB for mobile */}
+
+
{/* Filters */}
@@ -204,6 +248,13 @@ export default function AssetsPage() {
onSuccess={() => refetch()}
/>
)}
+
+ {showScanner && (
+ setShowScanner(false)}
+ onScanSuccess={handleScanSuccess}
+ />
+ )}
);
-}
+}
\ No newline at end of file
diff --git a/frontend/components/assets/ScannerModal.tsx b/frontend/components/assets/ScannerModal.tsx
new file mode 100644
index 000000000..dfc757d31
--- /dev/null
+++ b/frontend/components/assets/ScannerModal.tsx
@@ -0,0 +1,69 @@
+
+"use client";
+
+import { useEffect, useState } from "react";
+import { Html5Qrcode } from "html5-qrcode";
+import { X } from "lucide-react";
+
+interface ScannerModalProps {
+ onClose: () => void;
+ onScanSuccess: (decodedText: string) => void;
+}
+
+export function ScannerModal({ onClose, onScanSuccess }: ScannerModalProps) {
+ const [cameraId, setCameraId] = useState
(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const scanner = new Html5Qrcode("reader");
+
+ const startScanner = async () => {
+ try {
+ const cameras = await Html5Qrcode.getCameras();
+ if (cameras && cameras.length) {
+ const selectedCameraId = cameras[0].id;
+ setCameraId(selectedCameraId);
+
+ await scanner.start(
+ selectedCameraId,
+ {
+ fps: 10,
+ qrbox: { width: 250, height: 250 },
+ },
+ (decodedText, decodedResult) => {
+ onScanSuccess(decodedText);
+ },
+ (errorMessage) => {}
+ );
+ } else {
+ setError("No cameras found.");
+ }
+ } catch (err) {
+ setError("Failed to start scanner. Please grant camera permissions.");
+ }
+ };
+
+ startScanner();
+
+ return () => {
+ if (scanner.isScanning) {
+ scanner.stop().catch((err) => console.error("Failed to stop scanner", err));
+ }
+ };
+ }, [onScanSuccess]);
+
+ return (
+
+
+
+
Scan QR/Barcode
+
+
+
+ {error &&
{error}
}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/layout/topbar.tsx b/frontend/components/layout/topbar.tsx
index 9aca58ee6..1154f2a07 100644
--- a/frontend/components/layout/topbar.tsx
+++ b/frontend/components/layout/topbar.tsx
@@ -2,8 +2,19 @@
import { useRouter, usePathname } from "next/navigation";
import { useState, useRef, useEffect } from "react";
-import { Menu, User, ChevronDown } from "lucide-react";
+import { Menu, User, ChevronDown, Bell } from "lucide-react";
import { useAuthStore } from "@/store/auth.store";
+import { useSocket } from "@/hooks/useSocket";
+import { useToast } from "@/components/ui/use-toast";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
const pageTitles: Record = {
"/dashboard": "Dashboard",
@@ -49,9 +60,46 @@ export function Topbar({ onMenuClick }: TopbarProps) {
return () => document.removeEventListener("mousedown", handleClick);
}, []);
- const initials = user
- ? `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
- : "?";
+ const { toast } = useToast();
+ const { on, off } = useSocket();
+ const [notifications, setNotifications] = useState([]);
+ const [unreadCount, setUnreadCount] = useState(0);
+
+ useEffect(() => {
+ const handleNewNotification = (notification: any) => {
+ setNotifications((prev) => [notification, ...prev]);
+ setUnreadCount((prev) => prev + 1);
+ };
+
+ const handleMaintenanceDue = (data: any) => {
+ toast({
+ title: "Maintenance Due",
+ description: `Asset ${data.assetName} is due for maintenance.`,
+ action: (
+
+ ),
+ });
+ };
+
+ const handleAssetStatusChanged = (data: any) => {
+ if (pathname.includes(`/assets/${data.assetId}`)) {
+ router.refresh();
+ }
+ };
+
+ on("notification.new", handleNewNotification);
+ on("maintenance.due", handleMaintenanceDue);
+ on("asset.status_changed", handleAssetStatusChanged);
+
+ return () => {
+ off("notification.new", handleNewNotification);
+ off("maintenance.due", handleMaintenanceDue);
+ off("asset.status_changed", handleAssetStatusChanged);
+ };
+ }, [on, off, toast, router, pathname]);
+
return (
@@ -69,47 +117,87 @@ export function Topbar({ onMenuClick }: TopbarProps) {
- {/* Right: user dropdown */}
-
-