+
diff --git a/frontend/_admin_payments_page.txt b/frontend/_admin_payments_page.txt
new file mode 100644
index 00000000..c5594fcf
--- /dev/null
+++ b/frontend/_admin_payments_page.txt
@@ -0,0 +1,260 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import DashboardLayout from "@/components/dashboard/DashboardLayout";
+import { useAuthState } from "@/lib/store/authStore";
+import { useGetAdminPayments } from "@/lib/react-query/hooks/admin/payments/useGetAdminPayments";
+import { useRefundPayment } from "@/lib/react-query/hooks/admin/payments/useRefundPayment";
+import { useGetAdminAnalytics } from "@/lib/react-query/hooks/admin/analytics/useGetAdminAnalytics";
+import { PaymentStatus, PaymentProvider } from "@/lib/types/payment";
+import {
+ CreditCard,
+ TrendingUp,
+ Clock,
+ XCircle,
+ ChevronLeft,
+ ChevronRight,
+ X,
+} from "lucide-react";
+
+const STATUS_TABS: { value: PaymentStatus | ""; label: string }[] = [
+ { value: "", label: "All" },
+ { value: "pending", label: "Pending" },
+ { value: "success", label: "Success" },
+ { value: "failed", label: "Failed" },
+ { value: "refunded", label: "Refunded" },
+];
+
+const PROVIDER_OPTIONS: { value: PaymentProvider | ""; label: string }[] = [
+ { value: "", label: "All Providers" },
+ { value: "paystack", label: "Paystack" },
+ { value: "soroban", label: "Soroban" },
+];
+
+const STATUS_COLORS: Record
= {
+ pending: "bg-amber-50 text-amber-700",
+ success: "bg-emerald-50 text-emerald-600",
+ failed: "bg-red-50 text-red-600",
+ refunded: "bg-blue-50 text-blue-700",
+};
+
+function formatNaira(kobo: number): string {
+ return new Intl.NumberFormat("en-NG", {
+ style: "currency",
+ currency: "NGN",
+ minimumFractionDigits: 0,
+ notation: kobo >= 100_000_000 ? "compact" : "standard",
+ }).format(kobo / 100);
+}
+
+function formatDate(iso: string): string {
+ return new Date(iso).toLocaleDateString("en-NG", { dateStyle: "medium" });
+}
+
+function StatCard({
+ label,
+ value,
+ icon: Icon,
+ color,
+}: {
+ label: string;
+ value: string;
+ icon: React.ElementType;
+ color: string;
+}) {
+ return (
+
+
+
+
+
{value}
+
{label}
+
+ );
+}
+
+export default function AdminPaymentsPage() {
+ const router = useRouter();
+ const { user } = useAuthState();
+
+ const [page, setPage] = useState(1);
+ const [statusFilter, setStatusFilter] = useState("");
+ const [providerFilter, setProviderFilter] = useState("");
+ const [from, setFrom] = useState("");
+ const [to, setTo] = useState("");
+ const [appliedFrom, setAppliedFrom] = useState("");
+ const [appliedTo, setAppliedTo] = useState("");
+ const [refundTarget, setRefundTarget] = useState(null);
+
+ const isAdmin = user?.role === "admin";
+
+ useEffect(() => {
+ if (user && !isAdmin) {
+ router.replace("/dashboard");
+ }
+ }, [user, isAdmin, router]);
+
+ const { data, isLoading } = useGetAdminPayments({
+ page,
+ limit: 20,
+ status: statusFilter || undefined,
+ provider: providerFilter || undefined,
+ from: appliedFrom || undefined,
+ to: appliedTo || undefined,
+ });
+
+ const { data: analyticsData } = useGetAdminAnalytics();
+
+ const { data: pendingData } = useGetAdminPayments({ status: "pending", limit: 1 });
+ const { data: failedData } = useGetAdminPayments({ status: "failed", limit: 1 });
+
+ const refund = useRefundPayment();
+
+ if (!user || !isAdmin) return null;
+
+ const payments = data?.data ?? [];
+ const meta = data?.meta;
+ const revenue = analyticsData?.data?.revenue;
+
+ const handleApplyFilter = () => {
+ setAppliedFrom(from);
+ setAppliedTo(to);
+ setPage(1);
+ };
+
+ const handleClearFilter = () => {
+ setFrom("");
+ setTo("");
+ setAppliedFrom("");
+ setAppliedTo("");
+ setPage(1);
+ };
+
+ const handleRefund = async (id: string) => {
+ await refund.mutateAsync(id);
+ setRefundTarget(null);
+ };
+
+ return (
+
+
+
Payments
+
+ {meta?.total ?? 0} total payments
+
+
+
+ {/* Stat cards */}
+
+
+
+
+
+
+
+ {/* Filters */}
+
+ {/* Provider dropdown */}
+
+
+
+
+
+ {/* Date range */}
+
+
+ setFrom(e.target.value)}
+ className="text-sm border border-gray-200 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-1 focus:ring-gray-300"
+ />
+
+
+
+ setTo(e.target.value)}
+ className="text-sm border border-gray-200 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-1 focus:ring-gray-300"
+ />
+
+
+ {(appliedFrom || appliedTo) && (
+
+ )}
+
+
+ {/* Status filter tabs */}
+
+ {STATUS_TABS.map(({ value, label }) => (
+
+ ))}
+
+
+ {/* Confirmation modal */}
+ {refundTarget && (
+
+
+
diff --git a/frontend/_analytics_hook.txt b/frontend/_analytics_hook.txt
new file mode 100644
index 00000000..e0a7e88d
--- /dev/null
+++ b/frontend/_analytics_hook.txt
@@ -0,0 +1,69 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { apiClient } from "@/lib/apiClient";
+import { queryKeys } from "@/lib/react-query/keys/queryKeys";
+
+interface RevenueTrendPoint {
+ month: string;
+ totalKobo: number;
+ totalNaira: number;
+}
+
+interface BookingTrendPoint {
+ month: string;
+ count: number;
+}
+
+export interface AdminAnalytics {
+ revenue: {
+ total: number;
+ thisMonth: number;
+ lastMonth: number;
+ trend: RevenueTrendPoint[];
+ };
+ bookings: {
+ byStatus: Record;
+ trend: BookingTrendPoint[];
+ };
+ topWorkspaces: {
+ id: string;
+ name: string;
+ bookings: string; // raw string from SQL COUNT
+ revenueKobo: string; // raw string from SQL SUM
+ }[];
+ topMembers: {
+ id: string;
+ fullName: string;
+ totalKobo: string; // raw string from SQL SUM
+ }[];
+ invoices: {
+ total: number;
+ totalAmountKobo: number;
+ totalAmountNaira: number;
+ paid: number;
+ pending: number;
+ };
+ occupancy: {
+ totalSeats: number;
+ occupiedSeats: number;
+ availableSeats: number;
+ occupancyPercent: number;
+ activeWorkspaces: number;
+ };
+}
+
+export const useGetAdminAnalytics = (from?: string, to?: string) => {
+ const params = new URLSearchParams();
+ if (from) params.set("from", from);
+ if (to) params.set("to", to);
+ const qs = params.toString();
+
+ return useQuery({
+ queryKey: queryKeys.admin.analytics({ from, to }),
+ queryFn: () =>
+ apiClient.get<{ success: boolean; data: AdminAnalytics }>(
+ `/dashboard/admin/analytics${qs ? `?${qs}` : ""}`
+ ),
+ });
+};
diff --git a/frontend/_api_client.txt b/frontend/_api_client.txt
new file mode 100644
index 00000000..bd9d4ade
--- /dev/null
+++ b/frontend/_api_client.txt
@@ -0,0 +1,81 @@
+const API_BASE_URL =
+ process.env.NEXT_PUBLIC_API_URL || "http://localhost:6001/api";
+
+class ApiClient {
+ private baseURL: string;
+ private token: string | null = null;
+
+ constructor(baseURL: string) {
+ this.baseURL = baseURL;
+ }
+
+ setToken(token: string | null) {
+ this.token = token;
+ }
+
+ private async request(
+ endpoint: string,
+ options: RequestInit = {}
+ ): Promise {
+ const url = `${this.baseURL}${endpoint}`;
+
+ const headers: Record = {
+ "Content-Type": "application/json",
+ ...(options.headers as Record),
+ };
+
+ if (this.token) {
+ headers["Authorization"] = `Bearer ${this.token}`;
+ }
+
+ const config: RequestInit = {
+ ...options,
+ headers,
+ };
+
+ try {
+ const response = await fetch(url, config);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || "An API error occurred");
+ }
+
+ return await response.json();
+ } catch (error) {
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error("Network error occurred");
+ }
+ }
+
+ async get(endpoint: string): Promise {
+ return this.request(endpoint, {
+ method: "GET",
+ });
+ }
+
+ async post(endpoint: string, data?: D): Promise {
+ return this.request(endpoint, {
+ method: "POST",
+ credentials: "include",
+ body: data ? JSON.stringify(data) : undefined,
+ });
+ }
+
+ async patch(endpoint: string, data?: D): Promise {
+ return this.request(endpoint, {
+ method: "PATCH",
+ body: data ? JSON.stringify(data) : undefined,
+ });
+ }
+
+ async delete(endpoint: string): Promise {
+ return this.request(endpoint, {
+ method: "DELETE",
+ });
+ }
+}
+
+export const apiClient = new ApiClient(API_BASE_URL);
diff --git a/frontend/_auth_store.txt b/frontend/_auth_store.txt
new file mode 100644
index 00000000..8ecde49f
--- /dev/null
+++ b/frontend/_auth_store.txt
@@ -0,0 +1,220 @@
+import { create } from "zustand";
+import { useShallow } from "zustand/shallow";
+import { persist, devtools } from "zustand/middleware";
+import {
+ AuthResponse,
+ AuthState,
+ LoginUser,
+ RegisterUser,
+ User,
+} from "../types/user";
+import { apiClient } from "../apiClient";
+import { storage } from "../storage";
+
+interface AuthActions {
+ // Authentication actions
+ login: (data: LoginUser) => Promise;
+ register: (data: RegisterUser) => Promise;
+ logout: () => void;
+ refreshAccessToken: () => Promise;
+
+ //User actions
+ updateProfile: (userData: Partial) => Promise;
+
+ // State management actions
+ setUser: (user: User | null) => void;
+ setToken: (token: string | null) => void;
+ setLoading: (loading: boolean) => void;
+ initializeAuth: () => void;
+ clearAuth: () => void;
+}
+
+type AuthStore = AuthState & AuthActions;
+
+export const useAuthStore = create()(
+ devtools(
+ persist(
+ (set, get) => ({
+ // INITIAL STATE
+ user: null,
+ accessToken: null,
+ isAuthenticated: false,
+ isLoading: false,
+
+ // AUTHENTICATION ACTIONS
+ login: async (data: LoginUser) => {
+ try {
+ set({ isLoading: true });
+
+ const response = await apiClient.post<
+ AuthResponse & { message?: string; requiresTwoFactor?: boolean; tempToken?: string }
+ >("/auth/login", data);
+
+ const { user, accessToken, message, requiresTwoFactor, tempToken } = response;
+
+ // If 2FA is required, surface a typed error so useLoginUser can redirect
+ if (requiresTwoFactor && tempToken) {
+ set({ isLoading: false });
+ throw { twoFactorRequired: true, tempToken, email: data.email };
+ }
+
+ // If user is not verified, the backend returns a message without accessToken
+ if (!accessToken && message) {
+ set({ isLoading: false });
+ throw { unverified: true, email: data.email, message };
+ }
+
+ // set the token in the api request headers authorization so that it always sends it to the backend when sending a request.
+ apiClient.setToken(accessToken);
+
+ // update the local ui store state
+ set({
+ user,
+ accessToken,
+ isAuthenticated: true,
+ isLoading: false,
+ });
+
+ // persist token to local storage and cookie for middleware usage
+ storage.setToken(accessToken);
+
+ // persist user to local storage
+ storage.setUser(user);
+ } catch (error) {
+ set({ isLoading: false });
+ throw error;
+ }
+ },
+ register: async (data: RegisterUser) => {
+ try {
+ set({ isLoading: true });
+
+ const { user, accessToken } = await apiClient.post(
+ "/auth/register",
+ data
+ );
+
+ apiClient.setToken(accessToken);
+
+ set({
+ user,
+ accessToken,
+ isAuthenticated: true,
+ isLoading: false,
+ });
+
+ storage.setToken(accessToken);
+ storage.setUser(user);
+ } catch (error) {
+ set({ isLoading: false });
+ throw error;
+ }
+ },
+ logout: () => {
+ set({
+ user: null,
+ accessToken: null,
+ isAuthenticated: false,
+ isLoading: false,
+ });
+ apiClient.setToken(null);
+ storage.clear();
+ },
+ refreshAccessToken: async () => {
+ try {
+ const currentToken = get().accessToken;
+
+ if (!currentToken) {
+ throw new Error("No token available");
+ }
+
+ const response = await apiClient.post(
+ "/auth/refresh-token"
+ );
+
+ const { accessToken, user } = response;
+
+ apiClient.setToken(accessToken);
+
+ set({
+ user,
+ accessToken,
+ isAuthenticated: true,
+ });
+
+ storage.setToken(accessToken);
+ storage.setUser(user);
+ } catch (error) {
+ get().logout();
+ throw error;
+ }
+ },
+
+ // USER ACTIONS
+ updateProfile: async (userData: Partial) => {
+ try {
+ set({ isLoading: true });
+
+ const currentUser = get().user;
+ if (!currentUser) throw new Error("Not authenticated");
+
+ const user = await apiClient.patch(
+ `/users/${currentUser.id}`,
+ userData
+ );
+
+ set({
+ user,
+ isLoading: false,
+ });
+
+ storage.setUser(user);
+ } catch (error) {
+ set({ isLoading: false });
+ throw error;
+ }
+ },
+
+ // STATE MANAGEMENT ACTIONS
+ setUser: (user: User | null) => {
+ set({ user, isAuthenticated: !!user });
+ },
+ setToken: (accessToken: string | null) => {
+ set({ accessToken });
+ apiClient.setToken(accessToken);
+ },
+ setLoading: (loading: boolean) => {
+ set({ isLoading: loading });
+ },
+ initializeAuth: () => {
+ // Get the persisted data from localstorage
+ const accessToken = storage.getToken();
+ const user = storage.getUser();
+
+ if (accessToken && user) {
+ apiClient.setToken(accessToken);
+
+ set({
+ user,
+ accessToken,
+ isAuthenticated: true,
+ });
+ }
+ },
+ clearAuth: () => {
+ set({
+ user: null,
+ accessToken: null,
+ isAuthenticated: false,
+ isLoading: false,
+ });
+ apiClient.setToken(null);
+ storage.clear();
+ },
+ }),
+ {
+ name: "AuthStore",
+ partialize: (state) => ({
+ user: state.user,
+ accessToken: state.accessToken,
+ isAuthenticated: state.isAuthenticated,
diff --git a/frontend/_dashboard_sidebar.txt b/frontend/_dashboard_sidebar.txt
new file mode 100644
index 00000000..088cc823
--- /dev/null
+++ b/frontend/_dashboard_sidebar.txt
@@ -0,0 +1,194 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import {
+ Building2,
+ LayoutDashboard,
+ User,
+ Settings,
+ LogOut,
+ Users,
+ Mail,
+ Menu,
+ X,
+ BookOpen,
+ FileText,
+ BriefcaseBusiness,
+ LogIn,
+ Bell,
+ BarChart3,
+ CreditCard,
+} from "lucide-react";
+import { useState } from "react";
+import { useAuthState, useAuthActions } from "@/lib/store/authStore";
+import NotificationBell from "@/components/notifications/NotificationBell";
+
+const navItems = [
+ { label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
+ { label: "Workspaces", href: "/workspaces", icon: BriefcaseBusiness },
+ { label: "My Bookings", href: "/bookings", icon: BookOpen },
+ { label: "Check In / Out", href: "/check-in", icon: LogIn },
+ { label: "Notifications", href: "/notifications", icon: Bell },
+ { label: "Invoices", href: "/invoices", icon: FileText },
+ { label: "Profile", href: "/profile", icon: User },
+ { label: "Settings", href: "/settings", icon: Settings },
+];
+
+const adminItems = [
+ { label: "Analytics", href: "/admin/analytics", icon: BarChart3 },
+ { label: "Workspaces", href: "/admin/workspaces", icon: Building2 },
+ { label: "All Bookings", href: "/admin/bookings", icon: BookOpen },
+ { label: "Payments", href: "/admin/payments", icon: CreditCard },
+ { label: "Members", href: "/admin/members", icon: Users },
+ { label: "Invoices", href: "/admin/invoices", icon: FileText },
+ { label: "Newsletter", href: "/dashboard?tab=newsletter", icon: Mail },
+];
+
+export default function DashboardSidebar() {
+ const pathname = usePathname();
+ const { user } = useAuthState();
+ const { logout } = useAuthActions();
+ const [mobileOpen, setMobileOpen] = useState(false);
+ const isAdmin = user?.role === "admin";
+
+ const handleLogout = () => {
+ logout();
+ window.location.href = "/login";
+ };
+
+ const sidebar = (
+
+ {/* Brand */}
+
+
+
+
+
+
+ ManageHub
+
+
+
+
+ {/* Nav */}
+
+
+ {/* User + Logout */}
+
+
+ Notifications
+
+
+
+
+ {user?.firstname?.[0]}
+ {user?.lastname?.[0]}
+
+
+
+ {user?.firstname} {user?.lastname}
+
+
{user?.email}
+
+
+
+
+
+ );
+
+ return (
+ <>
+ {/* Mobile hamburger */}
+
+
+ {/* Mobile overlay */}
+ {mobileOpen && (
+ setMobileOpen(false)}
+ />
+ )}
+
+ {/* Mobile drawer */}
+
+
+ {sidebar}
+
+
+ {/* Desktop sidebar */}
+
+ >
+ );
+}