diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index cc8b6b8..0acf4da 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,15 +1,16 @@ +import { Navigate, useLocation } from "react-router-dom"; + interface ProtectedRouteProps { children: React.ReactNode; } export function ProtectedRoute({ children }: ProtectedRouteProps) { - // const location = useLocation() - // const token = localStorage.getItem('access_token') + const location = useLocation(); + const token = localStorage.getItem("access_token"); - // if (!token) { - // // Redirect to login page with return URL - // return - // } + if (!token) { + return ; + } return <>{children}; } diff --git a/src/layouts/app-shell.tsx b/src/layouts/app-shell.tsx index bef61bf..6fca881 100644 --- a/src/layouts/app-shell.tsx +++ b/src/layouts/app-shell.tsx @@ -40,7 +40,8 @@ import { } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { roleLabel, getPermissions } from "@/lib/admin-roles"; -import { authService } from "@/services"; +import { authService, notificationService } from "@/services"; + interface User { email: string; @@ -49,6 +50,7 @@ interface User { role: string; } + type NavItem = { icon?: ComponentType<{ className?: string }>; label: string; @@ -58,6 +60,7 @@ type NavItem = { visible?: (role: string | undefined) => boolean; }; + function navItemIsActive( item: NavItem, isActive: (path?: string) => boolean, @@ -66,6 +69,7 @@ function navItemIsActive( return item.children?.some((child) => navItemIsActive(child, isActive)) ?? false; } + function filterNavItems( items: NavItem[], role: string | undefined, @@ -83,6 +87,7 @@ function filterNavItems( .filter((item) => !item.children || item.children.length > 0); } + const adminNavigationItems: NavItem[] = [ { icon: LayoutDashboard, label: "Dashboard", path: "/admin/dashboard" }, { @@ -208,6 +213,7 @@ const adminNavigationItems: NavItem[] = [ }, ]; + const SidebarNavItem = ({ item, depth = 0, @@ -225,8 +231,10 @@ const SidebarNavItem = ({ const hasChildren = visibleChildren && visibleChildren.length > 0; const isCurrentlyActive = navItemIsActive(item, isActive); + const [isOpen, setIsOpen] = useState(isCurrentlyActive); + // Keep open if it becomes active from external navigation (e.g. breadcrumbs or search) useEffect(() => { if (isCurrentlyActive) { @@ -234,8 +242,10 @@ const SidebarNavItem = ({ } }, [isCurrentlyActive]); + const Icon = item.icon; + if (hasChildren) { return (
@@ -276,6 +286,7 @@ const SidebarNavItem = ({ ); } + return ( (() => { const userStr = localStorage.getItem("user"); @@ -311,24 +324,53 @@ export function AppShell() { return null; }); + + // ✅ NEW: Track unread notification count + const [unreadCount, setUnreadCount] = useState(0); + + const isActive = (path?: string) => { if (!path) return false; return location.pathname.startsWith(path); }; + + useEffect(() => { + const fetchUnreadCount = async () => { + try { + const unreadCount = + await notificationService.getUnreadCount(); + + setUnreadCount(unreadCount); + } catch (error) { + console.error( + "Failed to fetch unread notification count:", + error + ); + setUnreadCount(0); + } + }; + + fetchUnreadCount(); + }, []); + + const handleLogout = async () => { await authService.logout(); navigate("/login", { replace: true }); }; + const handleNotificationClick = () => { navigate("/notifications"); }; + const handleProfileClick = () => { navigate("/admin/settings"); }; + const getUserInitials = () => { if (user?.firstName && user?.lastName) { return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase(); @@ -339,6 +381,7 @@ export function AppShell() { return "AD"; }; + const getUserDisplayName = () => { if (user?.firstName && user?.lastName) { return `${user.firstName} ${user.lastName}`; @@ -346,6 +389,7 @@ export function AppShell() { return user?.email || "Admin User"; }; + return (
{/* Sidebar */} @@ -360,6 +404,7 @@ export function AppShell() {
+ {/* Navigation */} + {/* User Section */}
@@ -404,6 +450,7 @@ export function AppShell() {
+ {/* Main Content */}
{/* Top Header */} @@ -418,7 +465,10 @@ export function AppShell() { onClick={handleNotificationClick} > - + {/* ✅ FIXED: Show red badge only when unreadCount > 0 */} + {unreadCount > 0 && ( + + )} @@ -458,6 +508,7 @@ export function AppShell() {
+ {/* Page Content */}
@@ -465,4 +516,4 @@ export function AppShell() {
); -} +} \ No newline at end of file diff --git a/src/pages/admin/invoices/proforma-list.tsx b/src/pages/admin/invoices/proforma-list.tsx index 5d19931..fe7733d 100644 --- a/src/pages/admin/invoices/proforma-list.tsx +++ b/src/pages/admin/invoices/proforma-list.tsx @@ -37,6 +37,7 @@ import { useAdminRole } from "@/hooks/use-admin-role"; import { toast } from "sonner"; import type { Proforma, InvoiceItem } from "@/services/invoice.service"; + export default function ProformaPage() { const { canCreateBusinessData, canEditBusinessData, canDeleteBusinessData } = useAdminRole(); @@ -48,6 +49,7 @@ export default function ProformaPage() { const [editingProforma, setEditingProforma] = useState(null); const [proformaToDelete, setProformaToDelete] = useState(null); + // Form State const [formData, setFormData] = useState>({ proformaNumber: "", @@ -65,6 +67,7 @@ export default function ProformaPage() { items: [] as InvoiceItem[], }); + const { data: proformaData, isLoading } = useQuery({ queryKey: ["admin", "proforma", page, search], queryFn: () => @@ -75,6 +78,7 @@ export default function ProformaPage() { }), }); + const createMutation = useMutation({ mutationFn: (data: any) => invoiceService.createProforma(data), onSuccess: () => { @@ -89,6 +93,7 @@ export default function ProformaPage() { }, }); + const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: any }) => invoiceService.updateProforma(id, data), @@ -104,6 +109,7 @@ export default function ProformaPage() { }, }); + const deleteMutation = useMutation({ mutationFn: (id: string) => invoiceService.deleteProforma(id), onSuccess: () => { @@ -116,6 +122,7 @@ export default function ProformaPage() { }, }); + const handleOpenCreate = () => { setEditingProforma(null); setFormData({ @@ -136,16 +143,26 @@ export default function ProformaPage() { setIsModalOpen(true); }; + + // ✅ FIXED: Convert string values to numbers for items const handleOpenEdit = (item: Proforma) => { setEditingProforma(item); setFormData({ ...item, issueDate: new Date(item.issueDate).toISOString().split("T")[0], dueDate: new Date(item.dueDate).toISOString().split("T")[0], + // Convert all item string values to numbers + items: item.items.map(i => ({ + ...i, + quantity: Number(i.quantity), + unitPrice: Number(i.unitPrice), + total: Number(i.total), + })), }); setIsModalOpen(true); }; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (editingProforma) { @@ -155,18 +172,21 @@ export default function ProformaPage() { } }; + + // ✅ FIXED: Convert item.total to Number when calculating subtotal const calculateTotals = ( items: InvoiceItem[], tax: number, discount: number, ) => { const subtotal = items.reduce( - (acc: number, item: InvoiceItem) => acc + item.total, + (acc: number, item: InvoiceItem) => acc + Number(item.total), 0, ); return subtotal + tax - discount; }; + const handleAddItem = () => { const newItem: InvoiceItem = { id: Math.random().toString(36).substring(7), @@ -181,6 +201,7 @@ export default function ProformaPage() { }); }; + const handleUpdateItem = ( index: number, field: keyof InvoiceItem, @@ -189,11 +210,13 @@ export default function ProformaPage() { const newItems = [...(formData.items || [])]; newItems[index] = { ...newItems[index], [field]: value } as InvoiceItem; + if (field === "quantity" || field === "unitPrice") { newItems[index].total = Number(newItems[index].quantity) * Number(newItems[index].unitPrice); } + const newAmount = calculateTotals( newItems, formData.taxAmount || 0, @@ -202,6 +225,7 @@ export default function ProformaPage() { setFormData({ ...formData, items: newItems, amount: newAmount }); }; + const handleRemoveItem = (index: number) => { const newItems = (formData.items || []).filter( (_, i: number) => i !== index, @@ -214,6 +238,7 @@ export default function ProformaPage() { setFormData({ ...formData, items: newItems, amount: newAmount }); }; + const formatCurrency = (amount: number | any) => { const val = typeof amount === "number" ? amount : 0; return new Intl.NumberFormat("en-US", { @@ -222,6 +247,7 @@ export default function ProformaPage() { }).format(val); }; + return (
@@ -246,6 +272,7 @@ export default function ProformaPage() { )}
+ @@ -323,7 +350,7 @@ export default function ProformaPage() {
- {formatCurrency(item.amount)} + {formatCurrency(Number(item.amount))} {new Date(item.issueDate).toLocaleDateString()} @@ -405,6 +432,7 @@ export default function ProformaPage() { )} + {/* Create/Edit Modal */} @@ -418,6 +446,7 @@ export default function ProformaPage() { +
@@ -485,6 +514,7 @@ export default function ProformaPage() {
+
@@ -575,6 +605,7 @@ export default function ProformaPage() {
+ {/* Line Items */}
@@ -592,6 +623,7 @@ export default function ProformaPage() {
+
{formData.items?.map((item: InvoiceItem, idx: number) => (
+
@@ -707,6 +740,7 @@ export default function ProformaPage() {
+ {/* Delete Confirmation */} @@ -748,4 +782,4 @@ export default function ProformaPage() { ); -} +} \ No newline at end of file diff --git a/src/pages/admin/notifications/broadcast.tsx b/src/pages/admin/notifications/broadcast.tsx index ac4445a..3be4de3 100644 --- a/src/pages/admin/notifications/broadcast.tsx +++ b/src/pages/admin/notifications/broadcast.tsx @@ -52,7 +52,7 @@ export default function NotificationBroadcastPage() { }, onError: () => toast.error( - "Could not send. Ensure POST /admin/notifications/broadcast exists.", + "Could not send. Ensure POST /notifications/broadcast exists.", ), }) diff --git a/src/pages/admin/subscriptions/index.tsx b/src/pages/admin/subscriptions/index.tsx index 070d11c..fce064e 100644 --- a/src/pages/admin/subscriptions/index.tsx +++ b/src/pages/admin/subscriptions/index.tsx @@ -1,8 +1,13 @@ +import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, @@ -11,95 +16,542 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Edit, Loader2 } from "lucide-react"; +import { Edit, Loader2, Search, UserCheck, ChevronLeft, ChevronRight } from "lucide-react"; import { subscriptionService } from "@/services"; +import { toast } from "sonner"; +import type { ChapaTransactionStatus, BillingInterval } from "@/types/subscription.types"; +import type { ApiError } from "@/types/error.types"; -export default function SubscriptionsAdminPage() { +// ─── Status badge helper ────────────────────────────────────────────────────── +function TxStatusBadge({ status }: { status: ChapaTransactionStatus }) { + const map: Record = { + SUCCESS: "bg-green-500", + PENDING: "bg-yellow-500", + FAILED: "bg-red-500", + CANCELLED: "bg-gray-500", + }; + return {status}; +} + +function SubStatusBadge({ status }: { status: string }) { + const map: Record = { + ACTIVE: "bg-green-500", + TRIALING: "bg-blue-500", + PAST_DUE: "bg-yellow-500", + CANCELLED: "bg-gray-500", + EXPIRED: "bg-red-500", + }; + return {status}; +} + +// ─── Plans tab ──────────────────────────────────────────────────────────────── +function PlansTab() { const navigate = useNavigate(); - - const { data: plans, isLoading: plansLoading } = useQuery({ + const { data: plans, isLoading } = useQuery({ queryKey: ["admin", "subscription-plans"], queryFn: () => subscriptionService.getAdminPlans(), }); return ( -
-
-

Subscription Plans

-

- Manage plan pricing, feature flags, limits, and activation status. - Payment history is under{" "} - - . -

+ + + All Plans + + + {isLoading ? ( +
+ + Loading plans... +
+ ) : ( + + + + Plan + Monthly (ETB) + Yearly (ETB) + Status + Actions + + + + {plans?.map((plan) => ( + + +
+

{plan.displayName}

+

{plan.name}

+
+
+ + {plan.isFree ? "Free" : plan.monthlyPrice.toLocaleString()} + + + {plan.isFree ? "Free" : plan.yearlyPrice.toLocaleString()} + + + + {plan.isActive ? "Active" : "Inactive"} + + + + + +
+ ))} +
+
+ )} +
+
+ ); +} + +// ─── All Subscriptions / Transactions tab ──────────────────────────────────── +function AllSubscriptionsTab() { + const [page, setPage] = useState(1); + const [statusFilter, setStatusFilter] = useState("ALL"); + const limit = 20; + + const { data, isLoading } = useQuery({ + queryKey: ["admin", "subscription-transactions", page, statusFilter], + queryFn: () => + subscriptionService.getSubscriptionTransactions({ + page, + limit, + status: statusFilter === "ALL" ? undefined : statusFilter, + }), + }); + + const transactions = data?.data ?? []; + const meta = data?.meta; + + return ( +
+ {/* Filters */} +
+
- All Plans + Subscription Transactions + - {plansLoading ? ( + {isLoading ? (
- Loading plans... + Loading transactions...
) : ( - - - - Plan - Monthly (ETB) - Yearly (ETB) - Status - Actions - - - - {plans?.map((plan) => ( - - -
-

{plan.displayName}

-

{plan.name}

-
-
- - {plan.isFree ? "Free" : plan.monthlyPrice.toLocaleString()} - - - {plan.isFree ? "Free" : plan.yearlyPrice.toLocaleString()} - - - - {plan.isActive ? "Active" : "Inactive"} - - - - - + <> +
+ + + Ref + User + Plan + Amount + Interval + Status + Date - ))} - -
+ + + + {transactions.length === 0 ? ( + + + No transactions found. + + + ) : ( + transactions.map((tx) => ( + + + {tx.txRef} + + + +
+

+ {tx.user.firstName || tx.user.lastName + ? `${tx.user.firstName ?? ""} ${ + tx.user.lastName ?? "" + }`.trim() + : "—"} +

+ +

+ {tx.user.email} +

+
+
+ + + {tx.plan?.displayName ?? "—"} + + + + {Number(tx.totalAmount).toLocaleString()}{" "} + {tx.currency} + + + + {tx.billingInterval ?? "—"} + + + + + + + + {new Date(tx.createdAt).toLocaleDateString()} + +
+ )) + )} +
+ + + {meta && meta.totalPages > 1 && ( +
+ + Page {meta.page} of {meta.totalPages} · {meta.total} total + + +
+ + + +
+
+ )} + )}
); } + +// ─── User Lookup tab ────────────────────────────────────────────────────────── +function UserLookupTab() { + const queryClient = useQueryClient(); + const [userId, setUserId] = useState(""); + const [searchedId, setSearchedId] = useState(""); + const [assignUserId, setAssignUserId] = useState(""); + const [assignPlanId, setAssignPlanId] = useState(""); + const [assignInterval, setAssignInterval] = useState("MONTHLY"); + + const { data: plans } = useQuery({ + queryKey: ["admin", "subscription-plans"], + queryFn: () => subscriptionService.getAdminPlans(), + }); + + const { + data: sub, + isLoading, + error, + refetch, + } = useQuery({ + queryKey: ["admin", "user-subscription", searchedId], + queryFn: () => subscriptionService.getUserSubscription(searchedId), + enabled: !!searchedId, + retry: false, + }); + + const assignMutation = useMutation({ + mutationFn: () => + subscriptionService.assignPlan(assignUserId, { + planId: assignPlanId, + billingInterval: assignInterval, + }), + onSuccess: () => { + toast.success("Plan assigned successfully"); + queryClient.invalidateQueries({ + queryKey: ["admin", "user-subscription", assignUserId], + }); + // If the looked-up user is the same, refresh + if (searchedId === assignUserId) refetch(); + setAssignUserId(""); + setAssignPlanId(""); + }, + onError: (err) => { + const apiError = err as ApiError; + toast.error(apiError.response?.data?.message || "Failed to assign plan"); + }, + }); + + const handleSearch = () => { + const trimmed = userId.trim(); + if (!trimmed) return; + setSearchedId(trimmed); + }; + + return ( +
+ {/* Lookup */} + + + Per-User Subscription Lookup + + +
+ setUserId(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + /> + +
+ + {isLoading && ( +
+ + Loading subscription... +
+ )} + + {error && ( +

+ No subscription found for this user, or the user does not exist. +

+ )} + + {sub && ( +
+ {/* Plan */} +
+
+

Plan

+

{sub.plan.displayName}

+

{sub.plan.name}

+
+ +
+ + {/* Subscription details */} +
+
+

Billing

+

{sub.subscription.billingInterval}

+
+
+

Period Start

+

+ {new Date(sub.subscription.currentPeriodStart).toLocaleDateString()} +

+
+
+

Period End

+

+ {new Date(sub.subscription.currentPeriodEnd).toLocaleDateString()} +

+
+ {sub.subscription.nextBillingAt && ( +
+

Next Billing

+

+ {new Date(sub.subscription.nextBillingAt).toLocaleDateString()} +

+
+ )} + {sub.subscription.trialEndsAt && ( +
+

Trial Ends

+

+ {new Date(sub.subscription.trialEndsAt).toLocaleDateString()} +

+
+ )} +
+ + {/* Features */} + {Object.keys(sub.features).length > 0 && ( +
+

Features

+
+ {Object.entries(sub.features).map(([key, enabled]) => ( + + {key.replace(/_/g, " ")} + {enabled ? " ✓" : " ✗"} + + ))} +
+
+ )} + + {/* Usage */} + {Object.keys(sub.usage).length > 0 && ( +
+

Usage

+
+ {Object.entries(sub.usage).map(([key, { used, limit }]) => ( +
+

+ {key.replace(/_/g, " ")} +

+

+ {used} / {limit === null ? "∞" : limit} +

+
+ ))} +
+
+ )} +
+ )} +
+
+ + {/* Assign Plan */} + + + Assign Plan to User + + +
+
+ + setAssignUserId(e.target.value)} + /> +
+
+ + +
+
+ + +
+ +
+
+
+
+ ); +} + +// ─── Root page ──────────────────────────────────────────────────────────────── +export default function SubscriptionsAdminPage() { + return ( +
+
+

Subscriptions

+

+ Manage plans, view all subscription transactions, and look up or assign plans for + individual users. +

+
+ + + + Plans + All Subscriptions + User Lookup + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/pages/admin/transactions/subscription-transactions.tsx b/src/pages/admin/transactions/subscription-transactions.tsx index 432b10f..7310daa 100644 --- a/src/pages/admin/transactions/subscription-transactions.tsx +++ b/src/pages/admin/transactions/subscription-transactions.tsx @@ -183,10 +183,7 @@ export default function SubscriptionTransactionsPage() {
- {row.userEmail} - - - {row.userId} + {row.user.email}
@@ -197,26 +194,21 @@ export default function SubscriptionTransactionsPage() { variant="secondary" className="bg-slate-100 text-slate-700 hover:bg-slate-200 border-none rounded-lg px-2.5 py-0.5 text-[10px] font-black uppercase" > - {row.planName} + {row.plan?.name} - {formatMoney(row.amount, row.currency)} + {formatMoney(Number(row.totalAmount), row.currency)}
- {row.provider} + { "Default"}
- {row.providerRef && ( - - {row.providerRef} - - )}
@@ -227,13 +219,7 @@ export default function SubscriptionTransactionsPage() { - {tab === "failed" && ( - -

- {row.failureReason ?? "Unknown Error Logic"} -

- - )} + {/* Pagination Controls */} - {data && data.totalPages > 1 && ( + {data?.meta && data.meta.totalPages > 1 && (

Showing{" "} - {data.data.length} of{" "} - {data.total} entries + + {data.data.length} + {" "} + of{" "} + + {data.meta.total} + {" "} + entries

+
+
- {data.page} / {data.totalPages} + {data.meta.page} / {data.meta.totalPages}
+
diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 6839645..2979840 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -82,7 +82,7 @@ export default function LoginPage() { setEmail(e.target.value)} required diff --git a/src/services/api/client.ts b/src/services/api/client.ts index a47b7d5..7d4f146 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -62,25 +62,32 @@ apiClient.interceptors.response.use( if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; + const refreshToken = localStorage.getItem("refresh_token"); + + if (!refreshToken) { + // No refresh token available — clear session and redirect + localStorage.removeItem("access_token"); + localStorage.removeItem("refresh_token"); + localStorage.removeItem("user"); + window.location.href = "/login"; + return Promise.reject(error); + } + try { - // Try to refresh token - const refreshToken = localStorage.getItem("refresh_token"); - if (refreshToken) { - const response = await axios.post( - `${API_BASE_URL}/auth/refresh`, - { refreshToken }, - { withCredentials: true }, - ); + const response = await axios.post( + `${API_BASE_URL}/auth/refresh`, + { refreshToken }, + { withCredentials: true }, + ); - const { accessToken } = response.data; - localStorage.setItem("access_token", accessToken); + const { accessToken } = response.data; + localStorage.setItem("access_token", accessToken); - // Retry original request with new token - originalRequest.headers.Authorization = `Bearer ${accessToken}`; - return apiClient(originalRequest); - } + // Retry original request with new token + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + return apiClient(originalRequest); } catch (refreshError) { - // Refresh failed - logout user + // Refresh failed — clear session and redirect localStorage.removeItem("access_token"); localStorage.removeItem("refresh_token"); localStorage.removeItem("user"); diff --git a/src/services/faq.service.ts b/src/services/faq.service.ts index 050047e..3ce5a72 100644 --- a/src/services/faq.service.ts +++ b/src/services/faq.service.ts @@ -29,7 +29,7 @@ class FaqService { audience?: FaqAudience search?: string }): Promise { - const response = await apiClient.get("/admin/faq", { + const response = await apiClient.get("/faq", { params, }) return response.data @@ -42,7 +42,7 @@ class FaqService { sortOrder?: number isPublished?: boolean }): Promise { - const response = await apiClient.post("/admin/faq", data) + const response = await apiClient.post("/faq", data) return response.data } @@ -55,12 +55,12 @@ class FaqService { > >, ): Promise { - const response = await apiClient.patch(`/admin/faq/${id}`, data) + const response = await apiClient.patch(`/faq/${id}`, data) return response.data } async remove(id: string): Promise { - await apiClient.delete(`/admin/faq/${id}`) + await apiClient.delete(`/faq/${id}`) } } diff --git a/src/services/issue.service.ts b/src/services/issue.service.ts index 5e70a8d..3eb7db3 100644 --- a/src/services/issue.service.ts +++ b/src/services/issue.service.ts @@ -33,7 +33,7 @@ export interface PaginatedIssues { class IssueService { async list(filters: IssueFilters = {}): Promise { - const response = await apiClient.get("/admin/issues", { + const response = await apiClient.get("/issues", { params: filters, }) return response.data @@ -44,7 +44,7 @@ class IssueService { description: string priority?: SupportIssue["priority"] }): Promise { - const response = await apiClient.post("/admin/issues", data) + const response = await apiClient.post("/issues", data) return response.data } @@ -53,7 +53,7 @@ class IssueService { status: IssueStatus, ): Promise { const response = await apiClient.patch( - `/admin/issues/${id}`, + `/issues/${id}`, { status }, ) return response.data diff --git a/src/services/notification.service.ts b/src/services/notification.service.ts index 8a1252d..0086215 100644 --- a/src/services/notification.service.ts +++ b/src/services/notification.service.ts @@ -52,7 +52,10 @@ export interface SendBroadcastRequest { audience: "all_end_users" | "system_users_only" | "everyone_with_access"; channels: ("push" | "sms" | "email")[]; } - +export interface NotificationListResponse { + data: Notification[]; + total: number; +} class NotificationService { /** * Get all notifications for current user (Paginated) @@ -64,20 +67,20 @@ class NotificationService { status?: string; search?: string; }): Promise { - const response = await apiClient.get("/notifications", { + const response = await apiClient.get("/notifications", { params, }); - return response.data; + return response.data.data; } /** * Get unread notification count */ async getUnreadCount(): Promise { - const response = await apiClient.get<{ count: number }>( + const response = await apiClient.get<{ unreadCount: number }>( "/notifications/unread-count", ); - return response.data.count; + return response.data.unreadCount; } /** @@ -91,7 +94,7 @@ class NotificationService { * Mark all notifications as read */ async markAllAsRead(): Promise { - await apiClient.post("/notifications/read-all"); + await apiClient.put("/notifications/mark-all-read"); } /** @@ -101,7 +104,7 @@ class NotificationService { data: SendBroadcastRequest, ): Promise<{ success: boolean }> { const response = await apiClient.post<{ success: boolean }>( - "/admin/notifications/broadcast", + "/notifications/broadcast", data, ); return response.data; diff --git a/src/services/subscription-transaction.service.ts b/src/services/subscription-transaction.service.ts index ef9a382..75e7657 100644 --- a/src/services/subscription-transaction.service.ts +++ b/src/services/subscription-transaction.service.ts @@ -3,34 +3,64 @@ import apiClient from "./api/client" export type SubscriptionPaymentStatus = "COMPLETED" | "FAILED" | "PENDING" export interface SubscriptionTransaction { - id: string - userId: string - userEmail: string - planName: string - amount: number - currency: string - status: SubscriptionPaymentStatus - provider: string - providerRef?: string - failureReason?: string - createdAt: string + id: string; + txRef: string; + checkoutUrl: string; + + totalAmount: string; + currency: string; + + status: SubscriptionPaymentStatus; + purpose: string; + + chapaTransactionId: string | null; + chapaData: Record | null; + + returnUrl: string; + + userId: string; + invoiceId: string; + + planId: string | null; + billingInterval: string | null; + + completedAt: string | null; + createdAt: string; + updatedAt: string; + + user: { + id: string; + email: string; + firstName: string | null; + lastName: string | null; + }; + + plan: { + id: string; + name: string; + displayName: string; + } | null; } export interface SubscriptionTransactionFilters { - page?: number - limit?: number - status?: SubscriptionPaymentStatus - search?: string + page?: number; + limit?: number; + status?: SubscriptionPaymentStatus; + search?: string; } export interface PaginatedSubscriptionTx { - data: SubscriptionTransaction[] - total: number - page: number - limit: number - totalPages: number -} + data: SubscriptionTransaction[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; +} class SubscriptionTransactionService { async getTransactions( filters: SubscriptionTransactionFilters = {},