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>
This commit is contained in:
AutonetSellCar Deploy
2025-12-30 13:24:39 +09:00
commit 1f0dcb1ddb
224 changed files with 55119 additions and 0 deletions

View File

@@ -0,0 +1,408 @@
'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>
);
}