diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 19cd2dd..8fea306 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -13,7 +13,7 @@ import { Label } from "@/components/ui/label"; import { toast } from "sonner"; export default function LoginPage() { - const [username, setUsername] = useState(""); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isRedirecting, setIsRedirecting] = useState(false); @@ -26,6 +26,7 @@ export default function LoginPage() { progress: 0, message: "Preparing your session..." }); + const [error, setError] = useState(""); const router = useRouter(); // Check if already logged in diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx new file mode 100644 index 0000000..579c561 --- /dev/null +++ b/app/dashboard/admin/page.tsx @@ -0,0 +1,172 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + AlertCircle, Users, MessageSquare, BarChart as BarChartIcon, Trash2, Bot, Database, + DollarSign, CreditCard, Landmark, Shield, Tag +} from "lucide-react"; +import AdminAnalytics from "@/components/admin/AdminAnalytics"; + +export default function AdminDashboardPage() { + const [activeTab, setActiveTab] = useState("overview"); + const [isAdmin, setIsAdmin] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if the current user is an admin + const checkAdminStatus = async () => { + try { + // Get auth token from cookies + const token = document.cookie + .split("; ") + .find((row) => row.startsWith("Authorization=")) + ?.split("=")[1]; + + if (!token) { + setIsAdmin(false); + setLoading(false); + return; + } + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/me`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const data = await response.json(); + // Check if vendor has admin privileges + setIsAdmin(data.vendor?.isAdmin === true); + } catch (error) { + console.error("Error checking admin status:", error); + setIsAdmin(false); + } finally { + setLoading(false); + } + }; + + checkAdminStatus(); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!isAdmin) { + return ( +
+ + + Access Denied + + You do not have permission to access the admin dashboard. + + +
+ ); + } + + return ( +
+

Admin Dashboard

+ + + + Overview + Vendors + Bot Management + Sessions + Analytics + + + +
+ setActiveTab("vendors")}> + + Vendor Management + + + + + View and manage vendors across the platform. + + + + + setActiveTab("bot")}> + + Bot Management + + + + + Configure and manage the Telegram bot settings. + + + + + setActiveTab("sessions")}> + + Session Management + + + + + Manage active sessions and clear inactive ones. + + + + + setActiveTab("analytics")}> + + Analytics + + + + + View platform statistics and performance metrics. + + + + + setActiveTab("analytics")}> + + Promotions + + + + + Track promotion usage and performance metrics. + + + + + setActiveTab("analytics")}> + + Payments & Escrow + + + + + Monitor escrow status and financial operations. + + + +
+
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/components/admin/AdminAnalytics.tsx b/components/admin/AdminAnalytics.tsx new file mode 100644 index 0000000..f2c1357 --- /dev/null +++ b/components/admin/AdminAnalytics.tsx @@ -0,0 +1,937 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { AlertCircle, BarChart as BarChartIcon, LineChart, RefreshCw, Users, ShoppingCart, + TrendingUp, TrendingDown, DollarSign, MessageSquare, Clock, Landmark } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip, Legend, XAxis, YAxis, BarChart, Bar } from "recharts"; + +// API response data structure +interface AnalyticsData { + vendors?: { + total?: number; + newToday?: number; + newThisWeek?: number; + activeToday?: number; + active?: number; + stores?: number; + dailyGrowth?: { date: string; count: number }[]; + data?: { date: string; count: number }[]; + }; + orders?: { + total?: number; + totalToday?: number; + totalThisWeek?: number; + recent?: number; + pending?: number; + completed?: number; + dailyOrders?: { date: string; count: number }[]; + data?: { date: string; count: number }[]; + }; + revenue?: { + total?: number; + today?: number; + thisWeek?: number; + dailyRevenue?: { date: string; amount: number }[]; + }; + engagement?: { + totalMessages?: number; + activeChats?: number; + avgResponseTime?: number; + dailyMessages?: { date: string; count: number }[]; + }; + products?: { + total?: number; + recent?: number; + }; + stores?: { + total?: number; + active?: number; + }; + sessions?: { + total?: number; + active?: number; + }; + promotions?: { + total?: number; + active?: number; + used?: number; + totalDiscountAmount?: number; + discountPercentage?: number; + topPromotions?: { _id: string; count: number; totalDiscount: number }[]; + discountByType?: { + percentage?: { count: number; totalDiscount: number }; + fixed?: { count: number; totalDiscount: number }; + }; + }; + chats?: { + totalChats?: number; + activeChats?: number; + totalMessages?: number; + recentMessages?: number; + messagesBySender?: { buyer: number; vendor: number }; + avgResponseTimeMin?: number; + messagesByHour?: number[]; + }; + telegram?: { + totalUsers?: number; + totalStoreConnections?: number; + avgStoresPerUser?: number; + multiStoreUsers?: number; + }; + escrow?: { + total?: number; + active?: number; + released?: number; + disputed?: number; + disputeRate?: number; + totalByCurrency?: { ltc: number; btc: number; xmr: number }; + heldByCurrency?: { ltc: number; btc: number; xmr: number }; + avgReleaseTimeHours?: number; + }; + security?: { + blockedUsers?: { + total?: number; + recentlyBlocked?: number; + } + }; +} + +export default function AdminAnalytics() { + const [analyticsData, setAnalyticsData] = useState(null); + const [loading, setLoading] = useState(true); + const [dateRange, setDateRange] = useState("7days"); + const [errorMessage, setErrorMessage] = useState(null); + const [refreshing, setRefreshing] = useState(false); + + const fetchAnalyticsData = async () => { + try { + setLoading(true); + setErrorMessage(null); + + const token = document.cookie + .split("; ") + .find((row) => row.startsWith("Authorization=")) + ?.split("=")[1]; + + console.log("Token from cookie:", token ? token.substring(0, 10) + "..." : "not found"); + console.log("API URL:", `${process.env.NEXT_PUBLIC_API_URL}/admin/analytics?range=${dateRange}`); + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/admin/analytics?range=${dateRange}`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + console.log("Response status:", response.status); + console.log("Response ok:", response.ok); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Error response:", errorText); + throw new Error(`Failed to fetch analytics data: ${response.status} ${errorText}`); + } + + const data = await response.json(); + console.log("Analytics data received:", data); + setAnalyticsData(data); + } catch (error: any) { + console.error("Error fetching analytics data:", error); + setErrorMessage(`Failed to load analytics data: ${error.message}`); + + // For demo purposes, load mock data if API fails + setAnalyticsData({ + vendors: { + total: 1248, + newToday: 24, + newThisWeek: 124, + activeToday: 356, + stores: 100, + dailyGrowth: [ + { date: "2023-11-01", count: 15 }, + { date: "2023-11-02", count: 18 }, + { date: "2023-11-03", count: 12 }, + { date: "2023-11-04", count: 22 }, + { date: "2023-11-05", count: 26 }, + { date: "2023-11-06", count: 17 }, + { date: "2023-11-07", count: 14 }, + ], + }, + orders: { + total: 5672, + totalToday: 86, + totalThisWeek: 432, + pending: 47, + completed: 5625, + dailyOrders: [ + { date: "2023-11-01", count: 54 }, + { date: "2023-11-02", count: 62 }, + { date: "2023-11-03", count: 58 }, + { date: "2023-11-04", count: 71 }, + { date: "2023-11-05", count: 68 }, + { date: "2023-11-06", count: 65 }, + { date: "2023-11-07", count: 54 }, + ], + }, + revenue: { + total: 156320, + today: 3280, + thisWeek: 18642, + dailyRevenue: [ + { date: "2023-11-01", amount: 2345 }, + { date: "2023-11-02", amount: 2762 }, + { date: "2023-11-03", amount: 2458 }, + { date: "2023-11-04", amount: 3121 }, + { date: "2023-11-05", amount: 2968 }, + { date: "2023-11-06", amount: 2708 }, + { date: "2023-11-07", amount: 2280 }, + ], + }, + engagement: { + totalMessages: 32450, + activeChats: 123, + avgResponseTime: 5.2, + dailyMessages: [ + { date: "2023-11-01", count: 432 }, + { date: "2023-11-02", count: 512 }, + { date: "2023-11-03", count: 478 }, + { date: "2023-11-04", count: 541 }, + { date: "2023-11-05", count: 498 }, + { date: "2023-11-06", count: 465 }, + { date: "2023-11-07", count: 423 }, + ], + }, + }); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchAnalyticsData(); + }, [dateRange]); + + const handleRefresh = () => { + setRefreshing(true); + fetchAnalyticsData(); + }; + + if (loading && !analyticsData) { + return ( +
+
+
+ ); + } + + // These would normally be real chart components + // For this example, we'll use simplified representations + const SimpleAreaChart = ({ data, label, color }: { data: any[]; label: string; color: string }) => { + // Handle empty data or undefined + if (!data || data.length === 0) { + return ( +
+ No data available +
+ ); + } + + return ( +
+ {/* Simplified chart visualization */} +
+ {data.map((item, index) => { + // Normalize height between 10% and 90% + const values = data.map(d => d.count || d.amount); + const max = Math.max(...values); + const min = Math.min(...values); + const range = max - min; + const value = item.count || item.amount; + const normalizedHeight = range === 0 + ? 50 + : 10 + (((value - min) / range) * 80); + + return ( +
+ ); + })} +
+
+
+ {new Date(data[0].date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})} + {new Date(data[data.length - 1].date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})} +
+
+ ); + }; + + // Function to render trend indicator + const TrendIndicator = ({ value, prefix = "", suffix = "" }: { value: number, prefix?: string, suffix?: string }) => { + if (value === 0) return null; + + return ( +
0 ? 'text-green-500' : 'text-red-500'}`}> + {value > 0 ? : } + {prefix}{Math.abs(value).toFixed(1)}{suffix} +
+ ); + }; + + // Mini sparkline component for metric cards + const MiniSparkline = ({ data, color = "#3b82f6" }: { data: any[], color?: string }) => { + if (!data || data.length === 0) return null; + + const values = data.map(item => item.count || item.amount || 0); + const max = Math.max(...values, 1); + + return ( +
+ {values.map((value, index) => { + const height = (value / max) * 100; + return ( +
+ ); + })} +
+ ); + }; + + return ( +
+
+

Platform Analytics

+
+ + +
+
+ + {errorMessage && ( + + + Error + {errorMessage} + + )} + + + + Orders + Vendors + Revenue + Promotions + Communication + Payments + + + + + + Order Analytics + Order volume and revenue metrics + + + {analyticsData && ( +
+
+
Total Orders
+
{analyticsData.orders?.total?.toLocaleString() || '0'}
+
+ +
+
Orders Today
+
{analyticsData.orders?.totalToday?.toLocaleString() || '0'}
+
+ +
+
Pending Orders
+
{analyticsData.orders?.pending?.toLocaleString() || '0'}
+
+ +
+
Revenue
+
${analyticsData.revenue?.total?.toLocaleString() || '0'}
+
+
+ )} + +
+

Order & Revenue Trends

+ {analyticsData && ( +
+

Order and revenue charts would render here

+
+ )} +
+
+
+
+ + + + + Vendor Analytics + Detailed vendor metrics and store statistics + + + {analyticsData && ( +
+
+
Total Vendors
+
{analyticsData.vendors?.total?.toLocaleString() || '0'}
+
+ +
+
New Today
+
{analyticsData.vendors?.newToday?.toLocaleString() || '0'}
+
+ +
+
New This Week
+
{analyticsData.vendors?.newThisWeek?.toLocaleString() || '0'}
+
+ +
+
Active Stores
+
{analyticsData.vendors?.stores?.toLocaleString() || analyticsData.stores?.total?.toLocaleString() || '0'}
+
+
+ )} + +
+

Vendor Growth Trend

+ {analyticsData && ( +
+

Vendor growth chart would render here

+
+ )} +
+
+
+
+ + + + + Revenue Analytics + Revenue and financial metrics + + + {analyticsData && ( +
+
+
Total Processed
+
${analyticsData.revenue?.total?.toLocaleString() || '0'}
+
+ +
+
Today's Revenue
+
${analyticsData.revenue?.today?.toLocaleString() || '0'}
+
+ +
+
This Week
+
${analyticsData.revenue?.thisWeek?.toLocaleString() || '0'}
+
+ +
+
Average Per Order
+
+ ${analyticsData.revenue && analyticsData.orders?.total + ? Math.round((analyticsData.revenue.total || 0) / analyticsData.orders.total).toLocaleString() + : '0'} +
+
+
+ )} + +
+

Revenue Trend

+ {analyticsData && ( + + )} +
+
+
+
+ + +
+ + + + Active Promotions + + + + +
+ {analyticsData?.promotions?.active || 0} +
+

+ out of {analyticsData?.promotions?.total || 0} total +

+
+
+ + + + Total Discount Amount + + + + +
+ ${analyticsData?.promotions?.totalDiscountAmount?.toLocaleString() || 0} +
+

+ {analyticsData?.promotions?.discountPercentage || 0}% of revenue +

+
+
+ + + + Promotion Uses + + + + +
+ {analyticsData?.promotions?.used || 0} +
+
+
+
+ +
+ + + Top Promotions + Most used promotion codes + + +
+ {analyticsData?.promotions?.topPromotions?.map((promo, i) => ( +
+
+

{promo._id}

+

+ {promo.count} uses ยท ${promo.totalDiscount.toLocaleString()} +

+
+
+ ${(promo.totalDiscount / promo.count).toFixed(2)} + avg +
+
+ ))} +
+
+
+ + + + Discount Types + Breakdown by discount type + + +
+ {analyticsData?.promotions?.discountByType && ( +
+
+
+

Percentage Discounts

+

+ {analyticsData.promotions.discountByType.percentage?.count || 0} uses +

+
+
+ ${analyticsData.promotions.discountByType.percentage?.totalDiscount?.toLocaleString() || 0} +
+
+
+
+

Fixed Amount Discounts

+

+ {analyticsData.promotions.discountByType.fixed?.count || 0} uses +

+
+
+ ${analyticsData.promotions.discountByType.fixed?.totalDiscount?.toLocaleString() || 0} +
+
+
+ )} +
+
+
+
+
+ + +
+ + + + Total Chats + + + + +
+ {analyticsData?.chats?.totalChats || 0} +
+

+ {analyticsData?.chats?.activeChats || 0} active in selected period +

+
+
+ + + + Total Messages + + + + +
+ {analyticsData?.chats?.totalMessages?.toLocaleString() || 0} +
+

+ {analyticsData?.chats?.recentMessages?.toLocaleString() || 0} in selected period +

+
+
+ + + + Avg Response Time + + + + +
+ {analyticsData?.chats?.avgResponseTimeMin || 0} min +
+
+
+
+ +
+ + + Message Volume by Hour + Chat activity distribution + + +
+ {analyticsData?.chats?.messagesByHour && ( + + ({ hour, count }))}> + + + + + + + )} +
+
+
+ + + + Message Breakdown + By sender type + + +
+ {analyticsData?.chats?.messagesBySender && ( + + + + + + + + + + + )} +
+
+
+
+ +
+ + + Telegram Users + Bot user statistics + + +
+
+
+

Total Users

+
+
+ {analyticsData?.telegram?.totalUsers || 0} +
+
+
+
+

Store Connections

+
+
+ {analyticsData?.telegram?.totalStoreConnections || 0} +
+
+
+
+

Avg Stores per User

+
+
+ {analyticsData?.telegram?.avgStoresPerUser?.toFixed(2) || 0} +
+
+
+
+

Multi-Store Users

+
+
+ {analyticsData?.telegram?.multiStoreUsers || 0} +
+
+
+
+
+ + + + Security + Blocked users and security metrics + + +
+
+
+

Total Blocked Users

+
+
+ {analyticsData?.security?.blockedUsers?.total || 0} +
+
+
+
+

Recently Blocked

+

+ In selected period +

+
+
+ {analyticsData?.security?.blockedUsers?.recentlyBlocked || 0} +
+
+
+
+
+
+
+ + +
+ + + + Active Escrows + + + + +
+ {analyticsData?.escrow?.active || 0} +
+

+ out of {analyticsData?.escrow?.total || 0} total +

+
+
+ + + + Released Escrows + + + + +
+ {analyticsData?.escrow?.released || 0} +
+
+
+ + + + Dispute Rate + + + + +
+ {analyticsData?.escrow?.disputeRate?.toFixed(1) || 0}% +
+

+ {analyticsData?.escrow?.disputed || 0} disputed escrows +

+
+
+
+ +
+ + + Escrow by Currency + Currently held amounts + + +
+ {analyticsData?.escrow?.heldByCurrency && ( + <> +
+
+

Bitcoin (BTC)

+
+
+ {analyticsData.escrow.heldByCurrency.btc.toFixed(4)} BTC +
+
+
+
+

Litecoin (LTC)

+
+
+ {analyticsData.escrow.heldByCurrency.ltc.toFixed(2)} LTC +
+
+
+
+

Monero (XMR)

+
+
+ {analyticsData.escrow.heldByCurrency.xmr.toFixed(2)} XMR +
+
+ + )} +
+
+
+ + + + Escrow Metrics + Release time and amounts + + +
+
+
+

Avg Release Time

+
+
+ {analyticsData?.escrow?.avgReleaseTimeHours ? ( + <> + {Math.floor(analyticsData.escrow.avgReleaseTimeHours / 24)} days, {analyticsData.escrow.avgReleaseTimeHours % 24} hours + + ) : ( + "8 days, 0 hours" + )} +
+
+ + {analyticsData?.escrow?.totalByCurrency && ( +
+

Total Processed (lifetime)

+
+
+ BTC: + {analyticsData.escrow.totalByCurrency.btc.toFixed(4)} BTC +
+
+ LTC: + {analyticsData.escrow.totalByCurrency.ltc.toFixed(2)} LTC +
+
+ XMR: + {analyticsData.escrow.totalByCurrency.xmr.toFixed(2)} XMR +
+
+
+ )} +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29