diff --git a/frontend/app/(dashboard)/notifications/page.tsx b/frontend/app/(dashboard)/notifications/page.tsx index e1c9f49c..f91eed59 100644 --- a/frontend/app/(dashboard)/notifications/page.tsx +++ b/frontend/app/(dashboard)/notifications/page.tsx @@ -1,26 +1,9 @@ "use client"; -import { useState } from "react"; -import { Bell, Check, CheckCheck, Filter } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Bell, CheckCheck, Filter } from "lucide-react"; import { Button } from "@/components/ui/button"; - -interface Notification { - id: string; - title: string; - message?: string; - type: "INFO" | "WARNING" | "ALERT"; - isRead: boolean; - resourceType?: string; - resourceId?: string; - createdAt: string; -} - -const MOCK_NOTIFICATIONS: Notification[] = [ - { id: "n1", title: "Transfer Approved", message: "Your transfer request for MacBook Pro M2 has been approved.", type: "INFO", isRead: false, resourceType: "transfer", resourceId: "tr-1", createdAt: "2026-08-25T10:30:00Z" }, - { id: "n2", title: "Maintenance Due", message: "Scheduled maintenance for Dell Monitor is due in 3 days.", type: "WARNING", isRead: false, resourceType: "maintenance", createdAt: "2026-08-24T14:00:00Z" }, - { id: "n3", title: "New Asset Assigned", message: "You have been assigned a new asset: Standing Desk.", type: "INFO", isRead: true, resourceType: "asset", resourceId: "a-1", createdAt: "2026-08-22T09:00:00Z" }, - { id: "n4", title: "Transfer Rejected", message: "Your transfer request for Standing Desk was rejected.", type: "ALERT", isRead: true, resourceType: "transfer", resourceId: "tr-3", createdAt: "2026-08-20T16:00:00Z" }, -]; +import { notificationsApiClient, type NotificationItem } from "@/lib/api/notifications"; const TYPE_ICONS: Record = { INFO: "bg-blue-100 text-blue-600", @@ -29,27 +12,70 @@ const TYPE_ICONS: Record = { }; export default function NotificationsPage() { - const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS); + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); const [filter, setFilter] = useState<"all" | "unread">("all"); + useEffect(() => { + notificationsApiClient + .list() + .then(setNotifications) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }, []); + const unreadCount = notifications.filter((n) => !n.isRead).length; const filtered = filter === "all" ? notifications : notifications.filter((n) => !n.isRead); - const markRead = (id: string) => { - setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, isRead: true } : n)); + const markRead = async (id: string) => { + try { + await notificationsApiClient.markRead(id); + setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))); + } catch { + // ignore on error + } }; - const markAllRead = () => { - setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))); + const markAllRead = async () => { + try { + await notificationsApiClient.markAllRead(); + setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))); + } catch { + // ignore on error + } }; - const getLink = (n: Notification) => { + const getLink = (n: NotificationItem) => { if (n.resourceType === "asset" && n.resourceId) return `/assets/${n.resourceId}`; if (n.resourceType === "transfer") return "/transfers"; if (n.resourceType === "maintenance") return "/maintenance"; return "#"; }; + if (loading) { + return ( +
+

Notifications

+
+

Loading notifications...

+
+
+ ); + } + + if (error) { + return ( +
+

Notifications

+
+ +

Failed to load notifications. Please try again later.

+
+
+ ); + } + return (
@@ -106,4 +132,4 @@ export default function NotificationsPage() {
); -} +} \ No newline at end of file diff --git a/frontend/app/(dashboard)/reservations/page.tsx b/frontend/app/(dashboard)/reservations/page.tsx index c0494fbd..37524965 100644 --- a/frontend/app/(dashboard)/reservations/page.tsx +++ b/frontend/app/(dashboard)/reservations/page.tsx @@ -1,27 +1,10 @@ "use client"; -import { useState } from "react"; -import { Calendar, Plus, Check, X, Clock } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Calendar, Plus, Check, X } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; -import { ConfirmDialog } from "@/components/ui/confirm-dialog"; - -interface Reservation { - id: string; - assetName: string; - assetId: string; - purpose: string; - startsAt: string; - endsAt: string; - status: "PENDING" | "CONFIRMED" | "CANCELLED" | "COMPLETED"; - requester: string; -} - -const MOCK: Reservation[] = [ - { id: "r1", assetName: "Projector A", assetId: "a-1", purpose: "Client demo", startsAt: "2026-08-26T09:00:00Z", endsAt: "2026-08-26T17:00:00Z", status: "PENDING", requester: "Alex" }, - { id: "r2", assetName: "Test Vehicle", assetId: "a-2", purpose: "Field audit", startsAt: "2026-08-27T08:00:00Z", endsAt: "2026-08-27T18:00:00Z", status: "CONFIRMED", requester: "Sam" }, -]; +import { reservationsApiClient, type Reservation } from "@/lib/api/reservations"; const STATUS_COLORS: Record = { PENDING: "bg-yellow-100 text-yellow-700", @@ -34,19 +17,37 @@ type Tab = "my" | "pending"; export default function ReservationsPage() { const [tab, setTab] = useState("my"); - const [reservations, setReservations] = useState(MOCK); + const [reservations, setReservations] = useState([]); + const [loading, setLoading] = useState(true); const [showBooking, setShowBooking] = useState(false); - const myReservations = reservations; + useEffect(() => { + const fetchFn = tab === "my" ? reservationsApiClient.listMine : reservationsApiClient.listAll; + fetchFn() + .then(setReservations) + .catch(() => setReservations([])) + .finally(() => setLoading(false)); + }, [tab]); + const pendingConfirmations = reservations.filter((r) => r.status === "PENDING"); - const currentList = tab === "my" ? myReservations : pendingConfirmations; + const currentList = tab === "my" ? reservations : pendingConfirmations; - const handleConfirm = (id: string) => { - setReservations((prev) => prev.map((r) => r.id === id ? { ...r, status: "CONFIRMED" as const } : r)); + const handleConfirm = async (id: string) => { + try { + await reservationsApiClient.confirm(id); + setReservations((prev) => prev.map((r) => (r.id === id ? { ...r, status: "CONFIRMED" as const } : r))); + } catch { + // ignore on error + } }; - const handleCancel = (id: string) => { - setReservations((prev) => prev.map((r) => r.id === id ? { ...r, status: "CANCELLED" as const } : r)); + const handleCancel = async (id: string) => { + try { + await reservationsApiClient.cancel(id); + setReservations((prev) => prev.map((r) => (r.id === id ? { ...r, status: "CANCELLED" as const } : r))); + } catch { + // ignore on error + } }; return ( @@ -76,7 +77,11 @@ export default function ReservationsPage() {
- {currentList.length === 0 ? ( + {loading ? ( +
+

Loading reservations...

+
+ ) : currentList.length === 0 ? (

No reservations

@@ -137,4 +142,4 @@ export default function ReservationsPage() { )}
); -} +} \ No newline at end of file diff --git a/frontend/components/assets/depreciation-schedule.tsx b/frontend/components/assets/depreciation-schedule.tsx index 04ad03f6..ea939e1c 100644 --- a/frontend/components/assets/depreciation-schedule.tsx +++ b/frontend/components/assets/depreciation-schedule.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { TrendingDown, DollarSign, BarChart3 } from "lucide-react"; +import { api } from "@/lib/api"; interface DepreciationEntry { period: number; @@ -19,29 +20,48 @@ interface DepreciationData { schedule: DepreciationEntry[]; } -const MOCK_DATA: DepreciationData = { - method: "STRAIGHT_LINE", - usefulLifeMonths: 60, - salvageValue: 500, - purchaseCost: 5000, - currentValue: 3200, - schedule: Array.from({ length: 12 }, (_, i) => ({ - period: i + 1, - openingValue: 5000 - i * 375, - depreciation: 375, - closingValue: 5000 - (i + 1) * 375, - })), -}; - -export function DepreciationSchedule({ data = MOCK_DATA }: { data?: DepreciationData }) { +export function DepreciationSchedule({ assetId }: { assetId: string }) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const [expanded, setExpanded] = useState(false); + + useState(() => { + api + .get(`/assets/${assetId}/depreciation`) + .then((r) => setData(r.data)) + .catch(() => setError("Failed to load depreciation data")) + .finally(() => setLoading(false)); + }); + + if (loading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + if (error || !data) { + return ( +
+

Depreciation Schedule

+

{error || "No depreciation configured for this asset."}

+
+ ); + } + const visibleSchedule = expanded ? data.schedule : data.schedule.slice(0, 6); return (

Depreciation Schedule

- {/* Stats */}

Purchase Cost

@@ -57,7 +77,6 @@ export function DepreciationSchedule({ data = MOCK_DATA }: { data?: Depreciation
- {/* Simple bar chart */}

Book Value Over Time

@@ -77,7 +96,6 @@ export function DepreciationSchedule({ data = MOCK_DATA }: { data?: Depreciation
- {/* Table */}
@@ -136,7 +154,6 @@ export function DepreciationFinancialsTab() { - {/* Category breakdown placeholder */}

Book Value by Category

@@ -158,8 +175,6 @@ export function DepreciationFinancialsTab() { ))}
- - ); -} +} \ No newline at end of file diff --git a/frontend/hooks/useWebSocket.ts b/frontend/hooks/useWebSocket.ts index 8d5fbd3d..54948f72 100644 --- a/frontend/hooks/useWebSocket.ts +++ b/frontend/hooks/useWebSocket.ts @@ -24,6 +24,17 @@ interface UseWebSocketOptions { onTransferExecuted?: (data: any) => void; } +/** Derive the WebSocket URL from the API base URL, falling back to port 6003 */ +function getWsUrl(): string { + const apiBase = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:6003/api'; + try { + const url = new URL(apiBase); + return `${url.protocol === 'https:' ? 'wss' : 'ws'}://${url.host}`; + } catch { + return 'ws://localhost:6003'; + } +} + export const useWebSocket = (options: UseWebSocketOptions = {}) => { const [socket, setSocket] = useState(null); const [isConnected, setIsConnected] = useState(false); @@ -32,57 +43,37 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { const connect = useCallback(() => { if (!options.userId) return; - const newSocket = io(process.env.NEXT_PUBLIC_WS_URL || 'http://localhost:8000', { + const token = typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null; + const newSocket = io(process.env.NEXT_PUBLIC_WS_URL || getWsUrl(), { transports: ['websocket'], - query: { - userId: options.userId - } + auth: { token }, + query: { userId: options.userId }, }); newSocket.on('connect', () => { setIsConnected(true); - console.log('Connected to WebSocket server'); - - // Join user room newSocket.emit('join_room', { userId: options.userId }); }); - newSocket.on('disconnect', () => { - setIsConnected(false); - console.log('Disconnected from WebSocket server'); - }); + newSocket.on('disconnect', () => setIsConnected(false)); newSocket.on('notification', (notification: Notification) => { - setNotifications(prev => [notification, ...prev]); + setNotifications((prev) => [notification, ...prev]); options.onNotification?.(notification); }); - newSocket.on('transfer_created', (data: any) => { - options.onTransferCreated?.(data); - }); - - newSocket.on('transfer_approved', (data: any) => { - options.onTransferApproved?.(data); - }); - - newSocket.on('transfer_rejected', (data: any) => { - options.onTransferRejected?.(data); - }); - - newSocket.on('transfer_cancelled', (data: any) => { - options.onTransferCancelled?.(data); - }); - - newSocket.on('transfer_executed', (data: any) => { - options.onTransferExecuted?.(data); - }); + newSocket.on('transfer_created', (data: any) => options.onTransferCreated?.(data)); + newSocket.on('transfer_approved', (data: any) => options.onTransferApproved?.(data)); + newSocket.on('transfer_rejected', (data: any) => options.onTransferRejected?.(data)); + newSocket.on('transfer_cancelled', (data: any) => options.onTransferCancelled?.(data)); + newSocket.on('transfer_executed', (data: any) => options.onTransferExecuted?.(data)); newSocket.on('connect_error', (error) => { console.error('WebSocket connection error:', error); }); setSocket(newSocket); - }, [options.userId, options]); + }, [options.userId]); const disconnect = useCallback(() => { if (socket) { @@ -93,75 +84,41 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { }, [socket]); const joinRoom = useCallback((roomId: string) => { - if (socket) { - socket.emit('join_room', { roomId }); - } + if (socket) socket.emit('join_room', { roomId }); }, [socket]); const leaveRoom = useCallback((roomId: string) => { - if (socket) { - socket.emit('leave_room', { roomId }); - } + if (socket) socket.emit('leave_room', { roomId }); }, [socket]); const sendNotification = useCallback((userId: string, notification: any) => { - if (socket) { - socket.emit('send_notification', { userId, notification }); - } + if (socket) socket.emit('send_notification', { userId, notification }); }, [socket]); useEffect(() => { - if (options.userId) { - connect(); - } - - return () => { - disconnect(); - }; + if (options.userId) connect(); + return () => disconnect(); }, [options.userId, connect, disconnect]); - return { - socket, - isConnected, - notifications, - setNotifications, - connect, - disconnect, - joinRoom, - leaveRoom, - sendNotification - }; + return { socket, isConnected, notifications, setNotifications, connect, disconnect, joinRoom, leaveRoom, sendNotification }; }; -// Hook for managing notification state export const useNotifications = (userId?: string) => { const [unreadCount, setUnreadCount] = useState(0); const [showNotifications, setShowNotifications] = useState(false); - const { - notifications, - setNotifications, - isConnected - } = useWebSocket({ + const { notifications, setNotifications, isConnected } = useWebSocket({ userId, - onNotification: (notification) => { - setUnreadCount(prev => prev + 1); - } + onNotification: () => setUnreadCount((prev) => prev + 1), }); const markAsRead = useCallback((notificationId: string) => { - setNotifications(prev => - prev.map(notif => - notif.id === notificationId ? { ...notif, isRead: true } : notif - ) - ); - setUnreadCount(prev => Math.max(0, prev - 1)); + setNotifications((prev) => prev.map((n) => (n.id === notificationId ? { ...n, isRead: true } : n))); + setUnreadCount((prev) => Math.max(0, prev - 1)); }, []); const markAllAsRead = useCallback(() => { - setNotifications(prev => - prev.map(notif => ({ ...notif, isRead: true })) - ); + setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))); setUnreadCount(0); }, []); @@ -170,14 +127,5 @@ export const useNotifications = (userId?: string) => { setUnreadCount(0); }, []); - return { - notifications, - unreadCount, - showNotifications, - setShowNotifications, - markAsRead, - markAllAsRead, - clearNotifications, - isConnected - }; + return { notifications, unreadCount, showNotifications, setShowNotifications, markAsRead, markAllAsRead, clearNotifications, isConnected }; }; \ No newline at end of file diff --git a/frontend/lib/api/notifications.ts b/frontend/lib/api/notifications.ts new file mode 100644 index 00000000..9beadf57 --- /dev/null +++ b/frontend/lib/api/notifications.ts @@ -0,0 +1,26 @@ +import { api } from '../api'; + +export interface NotificationItem { + id: string; + title: string; + message?: string; + type: 'INFO' | 'WARNING' | 'ALERT'; + isRead: boolean; + resourceType?: string; + resourceId?: string; + createdAt: string; +} + +export const notificationsApiClient = { + list: (): Promise => + api.get('/notifications').then((r) => r.data), + + getUnreadCount: (): Promise<{ count: number }> => + api.get<{ count: number }>('/notifications/unread-count').then((r) => r.data), + + markRead: (id: string): Promise => + api.patch(`/notifications/${id}/read`).then(() => undefined), + + markAllRead: (): Promise => + api.patch('/notifications/mark-all-read').then(() => undefined), +}; \ No newline at end of file diff --git a/frontend/lib/api/reservations.ts b/frontend/lib/api/reservations.ts new file mode 100644 index 00000000..876254e7 --- /dev/null +++ b/frontend/lib/api/reservations.ts @@ -0,0 +1,39 @@ +import { api } from '../api'; + +export interface Reservation { + id: string; + assetName: string; + assetId: string; + purpose: string; + startsAt: string; + endsAt: string; + status: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'; + requester: string; +} + +export interface CreateReservationInput { + assetId: string; + purpose: string; + startsAt: string; + endsAt: string; +} + +export const reservationsApiClient = { + listMine: (): Promise => + api.get('/reservations/mine').then((r) => r.data), + + listAll: (): Promise => + api.get('/reservations').then((r) => r.data), + + create: (data: CreateReservationInput): Promise => + api.post('/reservations', data).then((r) => r.data), + + cancel: (id: string, reason?: string): Promise => + api.patch(`/reservations/${id}/cancel`, { reason }).then((r) => r.data), + + confirm: (id: string): Promise => + api.patch(`/reservations/${id}/confirm`).then((r) => r.data), + + getAvailability: (assetId: string, startsAt: string, endsAt: string): Promise<{ available: boolean; conflicts?: Reservation[] }> => + api.get(`/reservations/availability`, { params: { assetId, startsAt, endsAt } }).then((r) => r.data), +}; \ No newline at end of file