diff --git a/src/App.tsx b/src/App.tsx index 1164c6e..8383a30 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,6 +30,11 @@ import PaymentRequestsPage from "@/pages/admin/payments/payment-requests"; import InvoicesPage from "@/pages/admin/invoices/invoices-list"; import ProformaPage from "@/pages/admin/invoices/proforma-list"; import ProformaRequestsPage from "@/pages/admin/invoices/proforma-requests"; +import SubscriptionTransactionsPage from "@/pages/admin/transactions/subscription-transactions"; +import SystemMembersPage from "@/pages/admin/system-members"; +import IssuesPage from "@/pages/admin/issues"; +import FaqSupportPage from "@/pages/admin/support/faq"; +import NotificationBroadcastPage from "@/pages/admin/notifications/broadcast"; function App() { return ( @@ -87,6 +92,17 @@ function App() { path="admin/payment-requests" element={} /> + } + /> + } /> + } /> + } /> + } + /> } /> } /> { const onKey = (e: KeyboardEvent) => { @@ -32,7 +34,15 @@ export function AdminQuickSearch() { return () => window.removeEventListener("keydown", onKey); }, []); - const grouped = groupAdminSearchRoutes(ADMIN_SEARCH_ROUTES); + const searchRoutes = ADMIN_SEARCH_ROUTES.filter((r) => { + if (r.path === "/admin/system-members" && !canAccessSystemMembers) + return false; + if (r.path === "/admin/notifications/broadcast" && !canSendBroadcast) + return false; + return true; + }); + + const grouped = groupAdminSearchRoutes(searchRoutes); const groups = [...grouped.keys()]; return ( diff --git a/src/config/admin-search-routes.ts b/src/config/admin-search-routes.ts index 82cff7b..6296288 100644 --- a/src/config/admin-search-routes.ts +++ b/src/config/admin-search-routes.ts @@ -21,6 +21,11 @@ import { Gauge, DollarSign, HardDrive, + ArrowRightLeft, + UserCog, + LifeBuoy, + HelpCircle, + Send, } from "lucide-react" export interface AdminSearchRoute { @@ -92,6 +97,15 @@ export const ADMIN_SEARCH_ROUTES: AdminSearchRoute[] = [ group: "Commerce", icon: FileClock, }, + { + id: "subscription-txns", + path: "/admin/transactions/subscriptions", + title: "Subscription transactions", + description: + "Successful and failed subscription charges for the platform.", + group: "Commerce", + icon: ArrowRightLeft, + }, { id: "users", path: "/admin/users", @@ -100,6 +114,15 @@ export const ADMIN_SEARCH_ROUTES: AdminSearchRoute[] = [ group: "People & activity", icon: Users, }, + { + id: "system-members", + path: "/admin/system-members", + title: "System users", + description: + "Internal panel accounts (system admin, admin, customer support).", + group: "People & activity", + icon: UserCog, + }, { id: "logs", path: "/admin/logs", @@ -116,6 +139,24 @@ export const ADMIN_SEARCH_ROUTES: AdminSearchRoute[] = [ group: "Operations", icon: Settings, }, + { + id: "issues", + path: "/admin/issues", + title: "Issues", + description: + "Support tickets reported by customers or internal system users.", + group: "Support", + icon: LifeBuoy, + }, + { + id: "faq-support", + path: "/admin/support/faq", + title: "FAQ & support", + description: + "Published Q&A for end users and staff; editors manage entries.", + group: "Support", + icon: HelpCircle, + }, { id: "maintenance", path: "/admin/maintenance", @@ -244,13 +285,24 @@ export const ADMIN_SEARCH_ROUTES: AdminSearchRoute[] = [ group: "Operations", icon: Heart, }, + { + id: "broadcast", + path: "/admin/notifications/broadcast", + title: "Send notification", + description: + "Broadcast push, SMS, and email to selected audiences (admins).", + group: "Communications", + icon: Send, + }, ] const GROUP_ORDER = [ "Overview", "Commerce", "People & activity", + "Support", "Operations", + "Communications", "Security", "Analytics", ] diff --git a/src/hooks/use-admin-role.ts b/src/hooks/use-admin-role.ts new file mode 100644 index 0000000..d646bc8 --- /dev/null +++ b/src/hooks/use-admin-role.ts @@ -0,0 +1,29 @@ +import { useMemo } from "react" +import { authService } from "@/services" +import { + canAccessSystemMembers, + canEdit, + canSendBroadcast, + hasPanelAccess, + isSuperAdmin, + roleLabel, +} from "@/lib/admin-roles" + +export function useAdminRole() { + return useMemo(() => { + const user = authService.getCurrentUser() as + | { role?: string; email?: string } + | null + const role = user?.role + + return { + role, + roleLabel: roleLabel(role), + isSuperAdmin: isSuperAdmin(role), + canEdit: canEdit(role), + canAccessSystemMembers: canAccessSystemMembers(role), + canSendBroadcast: canSendBroadcast(role), + hasPanelAccess: hasPanelAccess(role), + } + }, []) +} diff --git a/src/layouts/app-shell.tsx b/src/layouts/app-shell.tsx index 2eaa5ff..42da1bf 100644 --- a/src/layouts/app-shell.tsx +++ b/src/layouts/app-shell.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, type ComponentType } from "react"; import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"; import { LayoutDashboard, @@ -18,6 +18,11 @@ import { Receipt, FileSearch, ClipboardList, + ArrowRightLeft, + UserCog, + LifeBuoy, + HelpCircle, + Send, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { AdminQuickSearch } from "@/components/admin-quick-search"; @@ -31,6 +36,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; +import { roleLabel } from "@/lib/admin-roles"; import { authService } from "@/services"; interface User { @@ -40,7 +46,15 @@ interface User { role: string; } -const adminNavigationItems = [ +type NavItem = { + icon: ComponentType<{ className?: string }>; + label: string; + path: string; + /** Omit = visible to all panel roles */ + visible?: (role: string | undefined) => boolean; +}; + +const adminNavigationItems: NavItem[] = [ { icon: LayoutDashboard, label: "Dashboard", path: "/admin/dashboard" }, { icon: Receipt, label: "Invoices", path: "/admin/invoices" }, { icon: FileSearch, label: "Proforma", path: "/admin/proforma" }, @@ -55,8 +69,21 @@ const adminNavigationItems = [ label: "Payment Requests", path: "/admin/payment-requests", }, + { + icon: ArrowRightLeft, + label: "Subscription transactions", + path: "/admin/transactions/subscriptions", + }, { icon: Users, label: "Users", path: "/admin/users" }, + { + icon: UserCog, + label: "System users", + path: "/admin/system-members", + visible: (role) => role === "SUPER_ADMIN" || role === "ADMIN", + }, { icon: FileText, label: "Logs", path: "/admin/logs" }, + { icon: LifeBuoy, label: "Issues", path: "/admin/issues" }, + { icon: HelpCircle, label: "FAQ & support", path: "/admin/support/faq" }, { icon: Settings, label: "Settings", path: "/admin/settings" }, { icon: Wrench, label: "Maintenance", path: "/admin/maintenance" }, { icon: Megaphone, label: "Announcements", path: "/admin/announcements" }, @@ -64,6 +91,12 @@ const adminNavigationItems = [ { icon: Shield, label: "Security", path: "/admin/security" }, { icon: BarChart3, label: "Analytics", path: "/admin/analytics" }, { icon: Heart, label: "System Health", path: "/admin/health" }, + { + icon: Send, + label: "Send notification", + path: "/admin/notifications/broadcast", + visible: (role) => role === "SUPER_ADMIN" || role === "ADMIN", + }, ]; export function AppShell() { @@ -136,24 +169,28 @@ export function AppShell() { {/* Navigation */} {/* User Section */} @@ -169,6 +206,11 @@ export function AppShell() {

{user?.email || "admin@example.com"}

+ {user?.role && ( +

+ {roleLabel(user.role)} +

+ )} + + + + + + Queue + +
+ + { + setSearch(e.target.value) + setPage(1) + }} + /> +
+
+ + {error && ( +

+ Wire up GET /admin/issues on your API + to list tickets. +

+ )} +
+ + + + + + + + + + + + + {isLoading ? ( + + + + ) : data?.data?.length ? ( + data.data.map((issue) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
+ Title + + Reporter + + Type + + Priority + + Updated + + Status +
+ Loading… +
+ {issue.title} +

+ {issue.description} +

+
+
{issue.reporterEmail}
+ + {issue.reporterType} + +
+ {issue.priority} + + {issue.updatedAt + ? new Date(issue.updatedAt).toLocaleString() + : new Date(issue.createdAt).toLocaleString()} + + {canEdit ? ( + + ) : ( + + {issue.status} + + )} +
+ No issues loaded. +
+
+
+
+ + + + + Report an issue + +
+
+ + + setNewIssue((n) => ({ ...n, title: e.target.value })) + } + className="rounded-none" + /> +
+
+ +