Files
AutonetSellCar/frontend/src/app/admin/page.tsx
AutonetSellCar Deploy 1f0dcb1ddb Initial commit: AutonetSellCar platform with deployment system
- Frontend: Next.js 14 with TypeScript
- Backend: FastAPI with SQLAlchemy
- Agent: Carmodoo sync agent
- Deployment: Docker Compose based staging/production setup
- Scripts: Automated deployment with rollback support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 13:24:39 +09:00

409 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import {
dashboardApi,
DashboardStats,
RevenueStats,
ChartData,
RecentActivity,
TopDealer,
PendingActions,
} from '@/lib/api';
export default function AdminDashboard() {
const [stats, setStats] = useState<DashboardStats | null>(null);
const [revenue, setRevenue] = useState<RevenueStats | null>(null);
const [userChart, setUserChart] = useState<ChartData | null>(null);
const [requestChart, setRequestChart] = useState<ChartData | null>(null);
const [recentActivities, setRecentActivities] = useState<RecentActivity[]>([]);
const [topDealers, setTopDealers] = useState<TopDealer[]>([]);
const [pendingActions, setPendingActions] = useState<PendingActions | null>(null);
const [loading, setLoading] = useState(true);
const [chartDays, setChartDays] = useState(14);
useEffect(() => {
loadDashboardData();
}, []);
useEffect(() => {
loadChartData();
}, [chartDays]);
const loadDashboardData = async () => {
try {
const [statsData, revenueData, activitiesData, dealersData, pendingData] = await Promise.all([
dashboardApi.getStats(),
dashboardApi.getRevenue(),
dashboardApi.getRecentActivities(10),
dashboardApi.getTopDealers(5),
dashboardApi.getPendingActions(),
]);
setStats(statsData);
setRevenue(revenueData);
setRecentActivities(activitiesData);
setTopDealers(dealersData);
setPendingActions(pendingData);
} catch (error) {
console.error('Failed to load dashboard data:', error);
} finally {
setLoading(false);
}
};
const loadChartData = async () => {
try {
const [userData, requestData] = await Promise.all([
dashboardApi.getUserChart(chartDays),
dashboardApi.getRequestChart(chartDays),
]);
setUserChart(userData);
setRequestChart(requestData);
} catch (error) {
console.error('Failed to load chart data:', error);
}
};
const formatNumber = (num: number) => {
return new Intl.NumberFormat('ko-KR').format(num);
};
const formatCurrency = (num: number) => {
return new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(num);
};
const getActivityIcon = (icon: string) => {
switch (icon) {
case 'user': return '👤';
case 'car': return '🚗';
case 'message': return '💬';
case 'badge': return '🎫';
case 'wallet': return '💰';
default: return '📌';
}
};
const getTimeAgo = (time: string) => {
if (!time) return '';
const now = new Date();
const then = new Date(time);
const diff = Math.floor((now.getTime() - then.getTime()) / 1000);
if (diff < 60) return `${diff}초 전`;
if (diff < 3600) return `${Math.floor(diff / 60)}분 전`;
if (diff < 86400) return `${Math.floor(diff / 3600)}시간 전`;
return `${Math.floor(diff / 86400)}일 전`;
};
const SimpleBarChart = ({ data, color = 'bg-primary-500', height = 100 }: { data: ChartData | null; color?: string; height?: number }) => {
if (!data || data.values.length === 0) return <div className="text-gray-400 text-center py-4">No data</div>;
const maxValue = Math.max(...data.values, 1);
return (
<div className="flex items-end gap-1" style={{ height }}>
{data.values.map((value, index) => (
<div key={index} className="flex-1 flex flex-col items-center group">
<div className="hidden group-hover:block absolute -mt-6 bg-gray-800 text-white text-xs px-2 py-1 rounded">
{value}
</div>
<div
className={`w-full ${color} rounded-t transition-all hover:opacity-80`}
style={{ height: `${(value / maxValue) * 100}%`, minHeight: value > 0 ? '4px' : '0' }}
/>
</div>
))}
</div>
);
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-800">Dashboard</h1>
<button
onClick={loadDashboardData}
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm flex items-center gap-2"
>
<span>Refresh</span>
</button>
</div>
{/* Pending Actions Alert */}
{pendingActions && pendingActions.total_pending > 0 && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<div className="flex items-center gap-2 text-amber-700 font-medium mb-3">
<span className="text-xl"></span>
<span>Pending Actions Required ({pendingActions.total_pending})</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{pendingActions.pending_requests > 0 && (
<Link href="/admin/vehicle-requests" className="flex items-center gap-2 p-2 bg-white rounded-lg hover:shadow">
<span className="text-lg">🚗</span>
<span className="text-sm">Vehicle Requests: {pendingActions.pending_requests}</span>
</Link>
)}
{pendingActions.pending_inquiries > 0 && (
<Link href="/admin/inquiries" className="flex items-center gap-2 p-2 bg-white rounded-lg hover:shadow">
<span className="text-lg">💬</span>
<span className="text-sm">Inquiries: {pendingActions.pending_inquiries}</span>
</Link>
)}
{pendingActions.pending_dealer_applications > 0 && (
<Link href="/admin/dealers" className="flex items-center gap-2 p-2 bg-white rounded-lg hover:shadow">
<span className="text-lg">🎫</span>
<span className="text-sm">Dealer Apps: {pendingActions.pending_dealer_applications}</span>
</Link>
)}
{pendingActions.pending_withdrawals > 0 && (
<Link href="/admin/withdrawals" className="flex items-center gap-2 p-2 bg-white rounded-lg hover:shadow">
<span className="text-lg">💰</span>
<span className="text-sm">Withdrawals: {pendingActions.pending_withdrawals}</span>
</Link>
)}
</div>
</div>
)}
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Link href="/admin/users" className="bg-white rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm">Total Users</p>
<p className="text-2xl font-bold text-gray-800 mt-1">{formatNumber(stats?.total_users || 0)}</p>
<p className="text-xs text-green-600 mt-1">+{stats?.new_users_this_week || 0} this week</p>
</div>
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center text-2xl">
👥
</div>
</div>
</Link>
<Link href="/admin/dealers" className="bg-white rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm">Active Dealers</p>
<p className="text-2xl font-bold text-gray-800 mt-1">{formatNumber(stats?.total_dealers || 0)}</p>
{(stats?.pending_dealer_applications || 0) > 0 && (
<p className="text-xs text-amber-600 mt-1">{stats?.pending_dealer_applications} pending</p>
)}
</div>
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center text-2xl">
🎫
</div>
</div>
</Link>
<Link href="/admin/vehicle-requests" className="bg-white rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm">Vehicle Requests</p>
<p className="text-2xl font-bold text-gray-800 mt-1">{formatNumber(stats?.total_vehicle_requests || 0)}</p>
{(stats?.pending_requests || 0) > 0 && (
<p className="text-xs text-amber-600 mt-1">{stats?.pending_requests} pending</p>
)}
</div>
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center text-2xl">
🚗
</div>
</div>
</Link>
<Link href="/admin/purchased" className="bg-white rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm">Purchased Vehicles</p>
<p className="text-2xl font-bold text-gray-800 mt-1">{formatNumber(stats?.total_purchased_vehicles || 0)}</p>
</div>
<div className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center text-2xl">
📦
</div>
</div>
</Link>
</div>
{/* Revenue & CC Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl p-5 text-white">
<p className="text-blue-100 text-sm">Total CC Charged</p>
<p className="text-3xl font-bold mt-1">{formatNumber(stats?.total_cc_charged || 0)} CC</p>
<div className="mt-2 flex items-center gap-2 text-sm text-blue-100">
<span>Revenue this month:</span>
<span className="font-semibold text-white">{formatCurrency(revenue?.revenue_this_month || 0)}</span>
</div>
</div>
<div className="bg-gradient-to-r from-green-500 to-green-600 rounded-xl p-5 text-white">
<p className="text-green-100 text-sm">Share Rewards</p>
<p className="text-3xl font-bold mt-1">{formatNumber(stats?.total_shares || 0)}</p>
<div className="mt-2 flex items-center gap-2 text-sm text-green-100">
<span>Purchased:</span>
<span className="font-semibold text-white">{formatNumber(stats?.purchased_shares || 0)}</span>
</div>
</div>
<Link href="/admin/withdrawals" className="bg-gradient-to-r from-purple-500 to-purple-600 rounded-xl p-5 text-white hover:opacity-90">
<p className="text-purple-100 text-sm">Total Withdrawals</p>
<p className="text-3xl font-bold mt-1">{formatCurrency(stats?.total_withdrawal_amount || 0)}</p>
{(stats?.pending_withdrawals || 0) > 0 && (
<div className="mt-2 flex items-center gap-2 text-sm">
<span className="bg-white/20 px-2 py-0.5 rounded">{stats?.pending_withdrawals} pending</span>
</div>
)}
</Link>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-xl shadow-sm p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-800">User Registrations</h3>
<select
value={chartDays}
onChange={(e) => setChartDays(Number(e.target.value))}
className="text-sm border rounded px-2 py-1"
>
<option value={7}>7 days</option>
<option value={14}>14 days</option>
<option value={30}>30 days</option>
</select>
</div>
<SimpleBarChart data={userChart} color="bg-blue-500" height={120} />
{userChart && (
<div className="flex justify-between mt-2 text-xs text-gray-400 overflow-hidden">
<span>{userChart.labels[0]}</span>
<span>{userChart.labels[userChart.labels.length - 1]}</span>
</div>
)}
</div>
<div className="bg-white rounded-xl shadow-sm p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-800">Vehicle Requests</h3>
</div>
<SimpleBarChart data={requestChart} color="bg-purple-500" height={120} />
{requestChart && (
<div className="flex justify-between mt-2 text-xs text-gray-400 overflow-hidden">
<span>{requestChart.labels[0]}</span>
<span>{requestChart.labels[requestChart.labels.length - 1]}</span>
</div>
)}
</div>
</div>
{/* Two Column Layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Activity */}
<div className="bg-white rounded-xl shadow-sm p-5">
<h3 className="font-semibold text-gray-800 mb-4">Recent Activity</h3>
<div className="space-y-3">
{recentActivities.length === 0 ? (
<p className="text-gray-400 text-center py-4">No recent activity</p>
) : (
recentActivities.map((activity, index) => (
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<span className="text-xl">{getActivityIcon(activity.icon)}</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">{activity.title}</p>
<p className="text-xs text-gray-500 truncate">{activity.description}</p>
</div>
<span className="text-xs text-gray-400 whitespace-nowrap">{getTimeAgo(activity.time)}</span>
</div>
))
)}
</div>
</div>
{/* Top Dealers */}
<div className="bg-white rounded-xl shadow-sm p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-800">Top Dealers</h3>
<Link href="/admin/dealers" className="text-sm text-primary-600 hover:underline">View all</Link>
</div>
<div className="space-y-3">
{topDealers.length === 0 ? (
<p className="text-gray-400 text-center py-4">No dealers yet</p>
) : (
topDealers.map((dealer, index) => (
<div key={dealer.id} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-amber-400 to-amber-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
{index + 1}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800">{dealer.name}</p>
<p className="text-xs text-gray-500">{dealer.dealer_code}</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-gray-800">{dealer.total_sales} sales</p>
<p className="text-xs text-green-600">{formatCurrency(dealer.total_commission)}</p>
</div>
</div>
))
)}
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white rounded-xl shadow-sm p-5">
<h3 className="font-semibold text-gray-800 mb-4">Quick Actions</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Link
href="/admin/hero-banners"
className="flex items-center gap-3 p-4 border border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors"
>
<span className="text-2xl">🖼</span>
<div>
<p className="font-medium text-gray-800">Hero Banners</p>
<p className="text-xs text-gray-500">Manage slider</p>
</div>
</Link>
<Link
href="/admin/cars"
className="flex items-center gap-3 p-4 border border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors"
>
<span className="text-2xl">🚙</span>
<div>
<p className="font-medium text-gray-800">Car Listings</p>
<p className="text-xs text-gray-500">{formatNumber(stats?.total_cars || 0)} cars</p>
</div>
</Link>
<Link
href="/admin/settings"
className="flex items-center gap-3 p-4 border border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors"
>
<span className="text-2xl"></span>
<div>
<p className="font-medium text-gray-800">Settings</p>
<p className="text-xs text-gray-500">System config</p>
</div>
</Link>
<Link
href="/admin/notifications"
className="flex items-center gap-3 p-4 border border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors"
>
<span className="text-2xl">🔔</span>
<div>
<p className="font-medium text-gray-800">Notifications</p>
<p className="text-xs text-gray-500">Send alerts</p>
</div>
</Link>
</div>
</div>
</div>
);
}