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
58 changes: 33 additions & 25 deletions frontend/app/(dashboard)/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { adminApi } from "@/lib/api/admin.api";
import { adminApi, type AdminStatsResponse } from "@/lib/api/admin.api";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { useUser } from "@/lib/hooks/useUser"; // assumes you have a user hook
import { useAuthStore } from "@/stores/auth.store";

export default function AdminStatsPage() {
const router = useRouter();
const { user, loading: userLoading } = useUser();
const [stats, setStats] = useState<any>(null);
const { user, isLoading: userLoading } = useAuthStore();
const [stats, setStats] = useState<AdminStatsResponse | null>(null);
const [loading, setLoading] = useState(true);

// Redirect non-admins
Expand All @@ -22,8 +21,16 @@ export default function AdminStatsPage() {
}
}, [user, userLoading, router]);

// Fetch stats
useEffect(() => {
if (userLoading) {
return;
}

if (user?.role !== "admin") {
setLoading(false);
return;
}

async function fetchStats() {
try {
const data = await adminApi.getStats();
Expand All @@ -34,59 +41,60 @@ export default function AdminStatsPage() {
setLoading(false);
}
}

fetchStats();
}, []);
}, [user, userLoading]);

if (loading) {
if (userLoading || loading) {
return (
<div className="grid grid-cols-3 gap-4 p-6">
{Array.from({ length: 10 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-full rounded-md" />
<div key={i} className="h-24 w-full animate-pulse rounded-md bg-muted" />
))}
</div>
);
}

if (!user || user.role !== "admin" || !stats) {
return null;
}

return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Admin Overview</h1>

{/* User stats */}
<div className="grid grid-cols-3 gap-4">
<StatCard title="Total Users" value={stats.users.total} />
<StatCard title="Active Users" value={stats.users.active} />
<StatCard title="Inactive Users" value={stats.users.inactive} />
<StatCard title="Shippers" value={stats.users.shippers} />
<StatCard title="Carriers" value={stats.users.carriers} />
<StatCard title="Admins" value={stats.users.admins} />
<StatCard title="Shippers" value={stats.users.byRole.shipper} />
<StatCard title="Carriers" value={stats.users.byRole.carrier} />
<StatCard title="Admins" value={stats.users.byRole.admin} />
</div>

{/* Shipment stats */}
<div className="grid grid-cols-3 gap-4">
<StatCard title="Total Shipments" value={stats.shipments.total} />
<StatCard title="Pending" value={stats.shipments.pending} />
<StatCard title="In Transit" value={stats.shipments.inTransit} />
<StatCard title="Completed" value={stats.shipments.completed} />
<StatCard title="Pending" value={stats.shipments.byStatus.pending} />
<StatCard title="In Transit" value={stats.shipments.byStatus.in_transit} />
<StatCard title="Completed" value={stats.shipments.byStatus.completed} />
<StatCard
title="Disputed"
value={stats.shipments.disputed}
destructive={stats.shipments.disputed > 0}
value={stats.shipments.disputesPending}
destructive={stats.shipments.disputesPending > 0}
/>
<StatCard title="Cancelled" value={stats.shipments.cancelled} />
<StatCard title="Cancelled" value={stats.shipments.byStatus.cancelled} />
</div>

{/* Revenue */}
<div className="grid grid-cols-1 gap-4">
<StatCard
title="Total Revenue"
value={new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(stats.revenue.completed)}
currency: stats.revenue.currency,
}).format(stats.revenue.totalCompleted)}
/>
</div>

{/* Quick navigation */}
<div className="flex gap-4">
<Button onClick={() => router.push("/admin/users")}>Manage Users</Button>
<Button onClick={() => router.push("/admin/shipments")}>Manage Shipments</Button>
Expand All @@ -101,7 +109,7 @@ function StatCard({
destructive = false,
}: {
title: string;
value: any;
value: string | number;
destructive?: boolean;
}) {
return (
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/(dashboard)/marketplace/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { shipmentApi } from '../../../lib/api/shipment.api';
import { PaginatedShipments } from '../../../types/shipment.types';
import { ShipmentCard } from '../../../components/shipment/shipment-card';
Expand All @@ -15,7 +15,7 @@ export default function MarketplacePage() {
const [destination, setDestination] = useState('');
const [page, setPage] = useState(1);

const fetchMarketplace = async (pg = 1) => {
const fetchMarketplace = useCallback(async (pg = 1) => {
setLoading(true);
try {
const data = await shipmentApi.marketplace({
Expand All @@ -31,11 +31,11 @@ export default function MarketplacePage() {
} finally {
setLoading(false);
}
};
}, [destination, origin]);

useEffect(() => {
fetchMarketplace(1);
}, []);
}, [fetchMarketplace]);

const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
Expand Down
25 changes: 16 additions & 9 deletions frontend/app/(dashboard)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import { authApi } from "@/lib/api/auth.api";
import { logout } from "@/stores/auth.store";
import { changePassword } from "@/lib/api/auth.api";
import { useAuthStore } from "@/stores/auth.store";
import {
Card,
CardHeader,
Expand All @@ -29,7 +29,17 @@ const schema = z

type FormValues = z.infer<typeof schema>;

function getErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}

return "Failed to change password";
}

export default function SettingsPage() {
const logout = useAuthStore((state) => state.logout);

const {
register,
handleSubmit,
Expand All @@ -41,19 +51,16 @@ export default function SettingsPage() {

const onSubmit = async (values: FormValues) => {
try {
await authApi.changePassword({
currentPassword: values.currentPassword,
newPassword: values.newPassword,
});
await changePassword(values.currentPassword, values.newPassword);

toast.success("Password changed successfully. You will be signed out.");
reset();

setTimeout(() => {
logout();
void logout();
}, 1500);
} catch (err: any) {
toast.error(err.message || "Failed to change password");
} catch (error) {
toast.error(getErrorMessage(error));
}
};

Expand Down
Loading
Loading