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
131 changes: 131 additions & 0 deletions frontend/app/(auth)/admin/dashboard/DashboardClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { adminApi } from "@/lib/api/admin";
import { useAuthStore } from "@/lib/store/authStore";
import { AdminRole } from "@/lib/types/user";
import RoleProtectedPage from "@/app/components/admin/RoleProtectedPage";
import PopularItemsChart from "@/app/components/admin/dashboard/PopularItemsChart";
import RecentActivity from "@/app/components/admin/dashboard/RecentActivity";

function StatCard({ label, value }: { label: string; value: number | string }) {
return (
<div className="rounded-xl border bg-white p-5 shadow-sm">
<p className="text-sm text-gray-500">{label}</p>
<p className="mt-1 text-2xl font-bold">{value}</p>
</div>
);
}

function QuickActions() {
const links = [
{ label: "Add Menu Item", href: "/admin/menu/new" },
{ label: "View Contacts", href: "/admin/contacts" },
{ label: "Manage Users", href: "/admin/users" },
];
return (
<div className="rounded-xl border bg-white p-5 shadow-sm">
<h2 className="mb-3 font-semibold text-gray-700">Quick Actions</h2>
<ul className="space-y-2">
{links.map((l) => (
<li key={l.href}>
<a href={l.href} className="text-sm text-blue-600 hover:underline">
{l.label}
</a>
</li>
))}
</ul>
</div>
);
}

export default function DashboardClient() {
const user = useAuthStore((s) => s.user);
const isStaff = user?.role === AdminRole.STAFF;

const {
data: stats,
isLoading: loadingStats,
error: statsError,
} = useQuery({
queryKey: ["admin-dashboard"],
queryFn: adminApi.getDashboard,
staleTime: 60_000,
});

const {
data: analytics,
isLoading: loadingAnalytics,
error: analyticsError,
} = useQuery({
queryKey: ["menu-analytics"],
queryFn: adminApi.getMenuAnalytics,
staleTime: 60_000,
});

const {
data: auditLogs,
isLoading: loadingLogs,
error: logsError,
} = useQuery({
queryKey: ["audit-logs", 10],
queryFn: () => adminApi.getAuditLogs(10),
staleTime: 30_000,
});

const isLoading = loadingStats || loadingAnalytics || loadingLogs;
const error = statsError || analyticsError || logsError;

return (
<RoleProtectedPage
allowedRoles={[
AdminRole.SUPER_ADMIN,
AdminRole.ADMIN,
AdminRole.MANAGER,
AdminRole.STAFF,
]}
>
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold text-gray-800">Dashboard</h1>

{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700">
Failed to load dashboard data.
</div>
)}

{isLoading ? (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-24 animate-pulse rounded-xl bg-gray-200" />
))}
</div>
) : (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<StatCard label="Total Menu Items" value={stats?.menu.total ?? 0} />
<StatCard label="Available Items" value={stats?.menu.available ?? 0} />
<StatCard label="Last 7 Days Views" value={stats?.analytics.last7Days ?? 0} />
{!isStaff && (
<StatCard label="New Contact Submissions" value={stats?.contacts.new ?? 0} />
)}
</div>
)}

<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{loadingAnalytics ? (
<div className="h-64 animate-pulse rounded-xl bg-gray-200" />
) : (
<PopularItemsChart data={analytics?.popularItems ?? []} />
)}
<QuickActions />
</div>

{loadingLogs ? (
<div className="h-40 animate-pulse rounded-xl bg-gray-200" />
) : (
<RecentActivity logs={auditLogs ?? []} />
)}
</div>
</RoleProtectedPage>
);
}
8 changes: 8 additions & 0 deletions frontend/app/(auth)/admin/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Metadata } from "next";
import DashboardClient from "./DashboardClient";

export const metadata: Metadata = { title: "Dashboard" };

export default function DashboardPage() {
return <DashboardClient />;
}
83 changes: 83 additions & 0 deletions frontend/app/(auth)/admin/menu/MenuClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { adminApi } from "@/lib/api/admin";
import MenuTable from "@/app/components/admin/menu/MenuTable";

function StatCard({ label, value }: { label: string; value: number }) {
return (
<div className="rounded-xl border bg-white p-5 shadow-sm">
<p className="text-sm text-gray-500">{label}</p>
<p className="mt-1 text-2xl font-bold">{value}</p>
</div>
);
}

export default function MenuClient() {
const queryClient = useQueryClient();

const { data: items = [], isLoading, error } = useQuery({
queryKey: ["admin-menu-items"],
queryFn: adminApi.getAllMenuItems,
staleTime: 60_000,
});

const toggleMutation = useMutation({
mutationFn: (id: string) => adminApi.toggleMenuItemAvailability(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["admin-menu-items"] }),
});

const deleteMutation = useMutation({
mutationFn: (id: string) => adminApi.deleteMenuItem(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["admin-menu-items"] }),
});

const total = items.length;
const available = items.filter((i: any) => i.isAvailable).length;
const unavailable = total - available;

return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-800">Menu Management</h1>
<Link
href="/admin/menu/new"
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Add Item
</Link>
</div>

{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700">
Failed to load menu items.
</div>
)}

{isLoading ? (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-24 animate-pulse rounded-xl bg-gray-200" />
))}
</div>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<StatCard label="Total Items" value={total} />
<StatCard label="Available" value={available} />
<StatCard label="Unavailable" value={unavailable} />
</div>
)}

{isLoading ? (
<div className="h-64 animate-pulse rounded-xl bg-gray-200" />
) : (
<MenuTable
items={items}
onToggleAvailability={(id) => toggleMutation.mutate(id)}
onDelete={(id) => deleteMutation.mutateAsync(id)}
/>
)}
</div>
);
}
8 changes: 8 additions & 0 deletions frontend/app/(auth)/admin/menu/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Metadata } from "next";
import MenuClient from "./MenuClient";

export const metadata: Metadata = { title: "Menu" };

export default function MenuPage() {
return <MenuClient />;
}
105 changes: 105 additions & 0 deletions frontend/app/(auth)/admin/users/UsersClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { adminApi } from "@/lib/api/admin";
import { useAuthStore } from "@/lib/store/authStore";
import { AdminRole, AdminUser } from "@/lib/types/user";
import RoleProtectedPage from "@/app/components/admin/RoleProtectedPage";
import UserTable from "@/app/components/admin/users/UserTable";

function StatCard({ label, value }: { label: string; value: number }) {
return (
<div className="rounded-xl border bg-white p-5 shadow-sm">
<p className="text-sm text-gray-500">{label}</p>
<p className="mt-1 text-2xl font-bold">{value}</p>
</div>
);
}

export default function UsersClient() {
const queryClient = useQueryClient();
const user = useAuthStore((s) => s.user);
const isSuperAdmin = user?.role === AdminRole.SUPER_ADMIN;

const { data: allUsers = [], isLoading, error } = useQuery<AdminUser[]>({
queryKey: ["admin-users"],
queryFn: adminApi.getAdmins,
staleTime: 60_000,
});

const users = isSuperAdmin
? allUsers
: allUsers.filter((u) => u.role !== AdminRole.SUPER_ADMIN);

const toggleMutation = useMutation({
mutationFn: ({ id, isActive }: { id: string; isActive: boolean }) =>
adminApi.toggleAdminStatus(id, isActive),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["admin-users"] }),
});

const roleMutation = useMutation({
mutationFn: ({ id, role }: { id: string; role: AdminRole }) =>
adminApi.updateAdminRole(id, role),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["admin-users"] }),
});

const total = users.length;
const active = users.filter((u) => u.isActive).length;
const inactive = total - active;
const adminCount = users.filter(
(u) => u.role === AdminRole.ADMIN || u.role === AdminRole.SUPER_ADMIN
).length;

return (
<RoleProtectedPage
allowedRoles={[AdminRole.SUPER_ADMIN, AdminRole.ADMIN, AdminRole.MANAGER]}
>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-800">Users Management</h1>
{isSuperAdmin && (
<Link
href="/admin/users/new"
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Add User
</Link>
)}
</div>

{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700">
Failed to load users.
</div>
)}

{isLoading ? (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-24 animate-pulse rounded-xl bg-gray-200" />
))}
</div>
) : (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<StatCard label="Total Users" value={total} />
<StatCard label="Active" value={active} />
<StatCard label="Inactive" value={inactive} />
<StatCard label="Admin Count" value={adminCount} />
</div>
)}

{isLoading ? (
<div className="h-64 animate-pulse rounded-xl bg-gray-200" />
) : (
<UserTable
users={users}
currentUserId={user?.id ?? ""}
onToggleStatus={(id, isActive) => toggleMutation.mutate({ id, isActive })}
onRoleChange={(id, role) => roleMutation.mutate({ id, role })}
/>
)}
</div>
</RoleProtectedPage>
);
}
Loading
Loading