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
66 changes: 57 additions & 9 deletions app/(admin)/admin/analytics/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,61 @@
"use client";

import { useState, useEffect } from "react";
import { ChevronDown, UserPlus, ArrowUpDown, Clock, Coins } from "lucide-react";
import { AdminMetricCard } from "@/components/admin/AdminMetricCard";
import { RevenueChart } from "@/components/admin/RevenueChart";
import { mockAdminMetrics, mockAdminUsers } from "@/lib/admin-mock-data";

const recentUsers = mockAdminUsers.slice(0, 5);
import { getAdminMetrics, getAdminUsers, type AdminMetrics, type AdminUser } from "@/lib/api/admin";

export default function AnalyticsPage() {
const [metrics, setMetrics] = useState<AdminMetrics | null>(null);
const [recentUsers, setRecentUsers] = useState<AdminUser[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchAnalyticsData() {
try {
setLoading(true);
setError(null);
const [fetchedMetrics, fetchedUsers] = await Promise.all([
getAdminMetrics(),
getAdminUsers(),
]);
setMetrics(fetchedMetrics);
setRecentUsers(fetchedUsers.slice(0, 5));
} catch (err: unknown) {
console.error("Error fetching analytics data:", err);
setError(err instanceof Error ? err.message : "Failed to load analytics data.");
} finally {
setLoading(false);
}
}
fetchAnalyticsData();
}, []);

if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-400" />
</div>
);
}

if (error || !metrics) {
return (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg max-w-lg mx-auto mt-8">
<p className="font-semibold">Error Loading Analytics</p>
<p className="text-sm">{error || "Could not retrieve metrics"}</p>
<button
onClick={() => window.location.reload()}
className="mt-2 text-xs font-semibold underline hover:text-red-800"
>
Try Again
</button>
</div>
);
}

return (
<div className="space-y-6">
{/* Overview section header */}
Expand All @@ -24,22 +72,22 @@ export default function AnalyticsPage() {
<div className="flex flex-wrap gap-4">
<AdminMetricCard
label="Registered Users"
value={mockAdminMetrics.registeredUsers}
value={metrics.registeredUsers}
icon={UserPlus}
/>
<AdminMetricCard
label="Total Transaction"
value={mockAdminMetrics.totalTransactions}
value={metrics.totalTransactions}
icon={ArrowUpDown}
/>
<AdminMetricCard
label="Pending KYC"
value={mockAdminMetrics.pendingKyc}
value={metrics.pendingKyc}
icon={Clock}
/>
<AdminMetricCard
label="Currency"
value={mockAdminMetrics.currencies}
value={metrics.currencies}
icon={Coins}
/>
</div>
Expand All @@ -53,13 +101,13 @@ export default function AnalyticsPage() {
<div className="h-[126px] flex items-center pl-6 border-b border-gray-200">
<div className="flex flex-col gap-2">
<p className="text-xs font-medium leading-none text-gray-500">Total Deposits</p>
<p className="text-[32px] font-semibold leading-none text-gray-900">{mockAdminMetrics.totalDeposits.toLocaleString()}</p>
<p className="text-[32px] font-semibold leading-none text-gray-900">{metrics.totalDeposits.toLocaleString()}</p>
</div>
</div>
<div className="h-[126px] flex items-center pl-6">
<div className="flex flex-col gap-2">
<p className="text-xs font-medium leading-none text-gray-500">Total Withdrawals</p>
<p className="text-[32px] font-semibold leading-none text-gray-900">{mockAdminMetrics.totalWithdrawals.toLocaleString()}</p>
<p className="text-[32px] font-semibold leading-none text-gray-900">{metrics.totalWithdrawals.toLocaleString()}</p>
</div>
</div>
</div>
Expand Down
75 changes: 55 additions & 20 deletions app/(admin)/admin/push-notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
"use client";

import { useState, useMemo } from "react";
import { mockPushNotifications, PushNotification } from "@/lib/admin-mock-data";
import { useState, useMemo, useEffect } from "react";
import PushNotificationTable from "@/components/admin/push-notifications/PushNotificationTable";
import CreatePushNotificationModal from "@/components/admin/push-notifications/CreatePushNotificationModal";
import { getAdminPushNotifications, createAdminPushNotification, type PushNotification } from "@/lib/api/admin";
import { Search, Plus } from "lucide-react";

export default function TransactionsPage() {
const [notifications, setNotifications] = useState<PushNotification[]>(
mockPushNotifications,
);
export default function PushNotificationsPage() {
const [notifications, setNotifications] = useState<PushNotification[]>([]);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [search, setSearch] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchNotifications() {
try {
setLoading(true);
setError(null);
const data = await getAdminPushNotifications();
setNotifications(data);
} catch (err: unknown) {
console.error("Error fetching push notifications:", err);
setError(err instanceof Error ? err.message : "Failed to load push notifications.");
} finally {
setLoading(false);
}
}
fetchNotifications();
}, []);

const filteredData = useMemo(() => {
return notifications.filter(
Expand All @@ -23,6 +40,7 @@ export default function TransactionsPage() {
}, [notifications, search]);

const handleDeactivate = () => {
// Since backend does not explicitly support deactivate in simple REST, we simulate locally
setNotifications((prev) =>
prev.map((n) =>
selectedIds.includes(n.id) ? { ...n, status: "Inactive" } : n,
Expand All @@ -31,22 +49,39 @@ export default function TransactionsPage() {
setSelectedIds([]);
};

const handleCreate = (title: string, message: string) => {
const newNotification: PushNotification = {
id: Date.now().toString(),
title,
message,
status: "Active",
createdAt: new Date().toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
}),
};

setNotifications((prev) => [newNotification, ...prev]);
const handleCreate = async (title: string, message: string) => {
try {
const newNotification = await createAdminPushNotification({ title, message });
setNotifications((prev) => [newNotification, ...prev]);
} catch (err: unknown) {
console.error("Error creating push notification:", err);
alert(err instanceof Error ? err.message : "Failed to create push notification.");
}
};

if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-400" />
</div>
);
}

if (error) {
return (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg max-w-lg mx-auto mt-8">
<p className="font-semibold">Error Loading Push Notifications</p>
<p className="text-sm">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-2 text-xs font-semibold underline hover:text-red-800"
>
Try Again
</button>
</div>
);
}

return (
<div className="md:p-6 space-y-6">
{/* Top Bar */}
Expand Down
78 changes: 76 additions & 2 deletions app/(admin)/admin/transactions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,85 @@
"use client";

import { useState, useEffect, useMemo } from "react";
import { TableTransaction } from "@/components/admin/transaction/TableTransaction";
import { TransactionFilters } from "@/components/admin/transaction/TransactionFilters";
import { getAdminTransactions, type AdminTransaction } from "@/lib/api/admin";

export default function TransactionPage() {
const [transactions, setTransactions] = useState<AdminTransaction[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [activeFilter, setActiveFilter] = useState("All");

useEffect(() => {
async function fetchTransactions() {
try {
setLoading(true);
setError(null);
const data = await getAdminTransactions();
setTransactions(data);
} catch (err: unknown) {
console.error("Error fetching transactions:", err);
setError(err instanceof Error ? err.message : "Failed to load transactions.");
} finally {
setLoading(false);
}
}
fetchTransactions();
}, []);

const filteredTransactions = useMemo(() => {
return transactions.filter((tx) => {
// Search by username or txId
const matchesSearch =
!searchQuery.trim() ||
tx.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
tx.txId.toLowerCase().includes(searchQuery.toLowerCase());

// Filter by active type (backend withdrawal maps to Withdraw)
const matchesFilter =
activeFilter === "All" ||
tx.type.toLowerCase() === activeFilter.toLowerCase() ||
(activeFilter === "Withdrawal" && tx.type === "Withdraw");

return matchesSearch && matchesFilter;
});
}, [transactions, searchQuery, activeFilter]);

if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-400"></div>
</div>
);
}

if (error) {
return (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg max-w-lg mx-auto mt-8">
<p className="font-semibold">Error Loading Transactions</p>
<p className="text-sm">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-2 text-xs font-semibold underline hover:text-red-800"
>
Try Again
</button>
</div>
);
}

return (
<div className="space-y-6">
<TransactionFilters />
<TableTransaction />
<TransactionFilters
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
activeFilter={activeFilter}
onFilterChange={setActiveFilter}
totalCount={filteredTransactions.length}
/>
<TableTransaction transactions={filteredTransactions} />
</div>
);
}
56 changes: 50 additions & 6 deletions app/(admin)/admin/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
'use client';

import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { Search, Filter } from 'lucide-react';
import { mockAdminUsers, AdminUser } from '@/lib/admin-mock-data';
import { AdminUserTable } from '@/components/admin/AdminUserTable';
import { UserDetailPanel } from '@/components/admin/UserDetailPanel';
import { getAdminUsers, type AdminUser } from '@/lib/api/admin';

const ITEMS_PER_PAGE = 10;

export default function UsersPage() {
const [users, setUsers] = useState<AdminUser[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null);

useEffect(() => {
async function fetchUsers() {
try {
setLoading(true);
setError(null);
const fetchedUsers = await getAdminUsers();
setUsers(fetchedUsers);
} catch (err: unknown) {
console.error('Error fetching users:', err);
setError(err instanceof Error ? err.message : 'Failed to load users.');
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);

// Filter users based on search query
const filteredUsers = useMemo(() => {
if (!searchQuery.trim()) return mockAdminUsers;
if (!searchQuery.trim()) return users;

const query = searchQuery.toLowerCase();
return mockAdminUsers.filter(user =>
return users.filter(user =>
user.email.toLowerCase().includes(query) ||
user.firstName?.toLowerCase().includes(query) ||
user.lastName?.toLowerCase().includes(query)
);
}, [searchQuery]);
}, [users, searchQuery]);

// Paginate filtered users
const paginatedUsers = useMemo(() => {
Expand All @@ -33,7 +53,7 @@ export default function UsersPage() {
}, [filteredUsers, currentPage]);

const totalPages = Math.ceil(filteredUsers.length / ITEMS_PER_PAGE);
const startEntry = (currentPage - 1) * ITEMS_PER_PAGE + 1;
const startEntry = filteredUsers.length === 0 ? 0 : (currentPage - 1) * ITEMS_PER_PAGE + 1;
const endEntry = Math.min(currentPage * ITEMS_PER_PAGE, filteredUsers.length);

const handleNextPage = () => {
Expand All @@ -46,6 +66,30 @@ export default function UsersPage() {
setCurrentPage(1);
};

if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-400"></div>
</div>
);
}

if (error) {
return (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg max-w-lg mx-auto mt-8">
<p className="font-semibold">Error Loading Users</p>
<p className="text-sm">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-2 text-xs font-semibold underline hover:text-red-800"
>
Try Again
</button>
</div>
);
}


return (
<div className="space-y-6">
{/* Mobile Header */}
Expand Down
Loading
Loading