diff --git a/frontend/app/(auth)/admin/dashboard/DashboardClient.tsx b/frontend/app/(auth)/admin/dashboard/DashboardClient.tsx new file mode 100644 index 0000000..438907f --- /dev/null +++ b/frontend/app/(auth)/admin/dashboard/DashboardClient.tsx @@ -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 ( +
+

{label}

+

{value}

+
+ ); +} + +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 ( +
+

Quick Actions

+ +
+ ); +} + +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 ( + +
+

Dashboard

+ + {error && ( +
+ Failed to load dashboard data. +
+ )} + + {isLoading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+ + + + {!isStaff && ( + + )} +
+ )} + +
+ {loadingAnalytics ? ( +
+ ) : ( + + )} + +
+ + {loadingLogs ? ( +
+ ) : ( + + )} +
+ + ); +} diff --git a/frontend/app/(auth)/admin/dashboard/page.tsx b/frontend/app/(auth)/admin/dashboard/page.tsx new file mode 100644 index 0000000..134d7f5 --- /dev/null +++ b/frontend/app/(auth)/admin/dashboard/page.tsx @@ -0,0 +1,8 @@ +import { Metadata } from "next"; +import DashboardClient from "./DashboardClient"; + +export const metadata: Metadata = { title: "Dashboard" }; + +export default function DashboardPage() { + return ; +} diff --git a/frontend/app/(auth)/admin/menu/MenuClient.tsx b/frontend/app/(auth)/admin/menu/MenuClient.tsx new file mode 100644 index 0000000..b2805b6 --- /dev/null +++ b/frontend/app/(auth)/admin/menu/MenuClient.tsx @@ -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 ( +
+

{label}

+

{value}

+
+ ); +} + +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 ( +
+
+

Menu Management

+ + Add Item + +
+ + {error && ( +
+ Failed to load menu items. +
+ )} + + {isLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+ + + +
+ )} + + {isLoading ? ( +
+ ) : ( + toggleMutation.mutate(id)} + onDelete={(id) => deleteMutation.mutateAsync(id)} + /> + )} +
+ ); +} diff --git a/frontend/app/(auth)/admin/menu/page.tsx b/frontend/app/(auth)/admin/menu/page.tsx new file mode 100644 index 0000000..8e2393e --- /dev/null +++ b/frontend/app/(auth)/admin/menu/page.tsx @@ -0,0 +1,8 @@ +import { Metadata } from "next"; +import MenuClient from "./MenuClient"; + +export const metadata: Metadata = { title: "Menu" }; + +export default function MenuPage() { + return ; +} diff --git a/frontend/app/(auth)/admin/users/UsersClient.tsx b/frontend/app/(auth)/admin/users/UsersClient.tsx new file mode 100644 index 0000000..a3f7d34 --- /dev/null +++ b/frontend/app/(auth)/admin/users/UsersClient.tsx @@ -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 ( +
+

{label}

+

{value}

+
+ ); +} + +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({ + 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 ( + +
+
+

Users Management

+ {isSuperAdmin && ( + + Add User + + )} +
+ + {error && ( +
+ Failed to load users. +
+ )} + + {isLoading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+ + + + +
+ )} + + {isLoading ? ( +
+ ) : ( + toggleMutation.mutate({ id, isActive })} + onRoleChange={(id, role) => roleMutation.mutate({ id, role })} + /> + )} +
+ + ); +} diff --git a/frontend/app/(auth)/admin/users/new/page.tsx b/frontend/app/(auth)/admin/users/new/page.tsx new file mode 100644 index 0000000..1b3871d --- /dev/null +++ b/frontend/app/(auth)/admin/users/new/page.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useMutation } from "@tanstack/react-query"; +import { Eye, EyeOff } from "lucide-react"; +import { adminApi } from "@/lib/api/admin"; +import { AdminRole } from "@/lib/types/user"; + +const schema = z.object({ + username: z.string().min(1, "Username is required"), + email: z.string().email("Valid email required"), + password: z.string().min(8, "Password must be at least 8 characters"), + role: z.enum([AdminRole.STAFF, AdminRole.MANAGER, AdminRole.ADMIN], { + required_error: "Role is required", + }), +}); + +type FormData = z.infer; + +const rolePermissions = [ + { role: "Staff", desc: "View orders, manage table status, limited menu access." }, + { role: "Manager", desc: "All Staff permissions + manage menu items and view reports." }, + { role: "Admin", desc: "All Manager permissions + manage users and system settings." }, +]; + +export default function CreateUserPage() { + const router = useRouter(); + const [showPassword, setShowPassword] = useState(false); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ resolver: zodResolver(schema) }); + + const mutation = useMutation({ + mutationFn: (data: FormData) => adminApi.createUser(data), + onSuccess: () => { + alert("User created successfully!"); + router.push("/admin/users"); + }, + onError: (err: any) => { + alert(err?.message ?? "Failed to create user."); + }, + }); + + return ( +
+

Create Admin User

+ +
mutation.mutate(d))} className="space-y-4"> +
+ + + {errors.username &&

{errors.username.message}

} +
+ +
+ + + {errors.email &&

{errors.email.message}

} +
+ +
+ +
+ + +
+ {errors.password &&

{errors.password.message}

} +
+ +
+ + + {errors.role &&

{errors.role.message}

} +
+ +
+ + +
+
+ +
+

Role Permissions

+
    + {rolePermissions.map(({ role, desc }) => ( +
  • + {role}: + {desc} +
  • + ))} +
+
+
+ ); +} diff --git a/frontend/app/(auth)/admin/users/page.tsx b/frontend/app/(auth)/admin/users/page.tsx new file mode 100644 index 0000000..8734132 --- /dev/null +++ b/frontend/app/(auth)/admin/users/page.tsx @@ -0,0 +1,8 @@ +import { Metadata } from "next"; +import UsersClient from "./UsersClient"; + +export const metadata: Metadata = { title: "Users" }; + +export default function UsersPage() { + return ; +}