Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 53 additions & 27 deletions frontend/app/(dashboard)/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
INFO: "bg-blue-100 text-blue-600",
Expand All @@ -29,27 +12,70 @@ const TYPE_ICONS: Record<string, string> = {
};

export default function NotificationsPage() {
const [notifications, setNotifications] = useState<Notification[]>(MOCK_NOTIFICATIONS);
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
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 (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-6">Notifications</h1>
<div className="bg-white border rounded-xl p-12 text-center">
<p className="text-gray-400">Loading notifications...</p>
</div>
</div>
);
}

if (error) {
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-6">Notifications</h1>
<div className="bg-white border rounded-xl p-12 text-center">
<Bell className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">Failed to load notifications. Please try again later.</p>
</div>
</div>
);
}

return (
<div>
<div className="flex items-center justify-between mb-6">
Expand Down Expand Up @@ -106,4 +132,4 @@ export default function NotificationsPage() {
</div>
</div>
);
}
}
63 changes: 34 additions & 29 deletions frontend/app/(dashboard)/reservations/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
PENDING: "bg-yellow-100 text-yellow-700",
Expand All @@ -34,19 +17,37 @@ type Tab = "my" | "pending";

export default function ReservationsPage() {
const [tab, setTab] = useState<Tab>("my");
const [reservations, setReservations] = useState<Reservation[]>(MOCK);
const [reservations, setReservations] = useState<Reservation[]>([]);
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 (
Expand Down Expand Up @@ -76,7 +77,11 @@ export default function ReservationsPage() {
</div>

<div className="space-y-3">
{currentList.length === 0 ? (
{loading ? (
<div className="bg-white border rounded-xl p-12 text-center">
<p className="text-gray-400">Loading reservations...</p>
</div>
) : currentList.length === 0 ? (
<div className="bg-white border rounded-xl p-12 text-center">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">No reservations</p>
Expand Down Expand Up @@ -137,4 +142,4 @@ export default function ReservationsPage() {
)}
</div>
);
}
}
59 changes: 37 additions & 22 deletions frontend/components/assets/depreciation-schedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState } from "react";
import { TrendingDown, DollarSign, BarChart3 } from "lucide-react";
import { api } from "@/lib/api";

interface DepreciationEntry {
period: number;
Expand All @@ -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<DepreciationData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expanded, setExpanded] = useState(false);

useState(() => {
api
.get<DepreciationData>(`/assets/${assetId}/depreciation`)
.then((r) => setData(r.data))
.catch(() => setError("Failed to load depreciation data"))
.finally(() => setLoading(false));
});

if (loading) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-5 animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/3 mb-4" />
<div className="grid grid-cols-3 gap-4">
<div className="h-16 bg-gray-100 rounded-lg" />
<div className="h-16 bg-gray-100 rounded-lg" />
<div className="h-16 bg-gray-100 rounded-lg" />
</div>
</div>
);
}

if (error || !data) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-2">Depreciation Schedule</h3>
<p className="text-sm text-gray-400">{error || "No depreciation configured for this asset."}</p>
</div>
);
}

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

return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-4">Depreciation Schedule</h3>

{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs text-gray-500">Purchase Cost</p>
Expand All @@ -57,7 +77,6 @@ export function DepreciationSchedule({ data = MOCK_DATA }: { data?: Depreciation
</div>
</div>

{/* Simple bar chart */}
<div className="mb-4">
<p className="text-xs text-gray-500 mb-2">Book Value Over Time</p>
<div className="flex items-end gap-1 h-32">
Expand All @@ -77,7 +96,6 @@ export function DepreciationSchedule({ data = MOCK_DATA }: { data?: Depreciation
</div>
</div>

{/* Table */}
<div className={`${expanded ? "" : "max-h-48 overflow-hidden"}`}>
<table className="w-full text-sm">
<thead>
Expand Down Expand Up @@ -136,7 +154,6 @@ export function DepreciationFinancialsTab() {
</div>
</div>

{/* Category breakdown placeholder */}
<div className="bg-white border rounded-xl p-5">
<h3 className="text-sm font-semibold mb-4">Book Value by Category</h3>
<div className="space-y-3">
Expand All @@ -158,8 +175,6 @@ export function DepreciationFinancialsTab() {
))}
</div>
</div>

<DepreciationSchedule />
</div>
);
}
}
Loading
Loading