initial: standalone repo from monorepo split
This commit is contained in:
187
src/app/(admin)/page.tsx
Normal file
187
src/app/(admin)/page.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import api from '@/lib/api';
|
||||
|
||||
interface DashboardStats {
|
||||
totalTenants: number;
|
||||
activeTenants: number;
|
||||
inactiveTenants: number;
|
||||
totalUsers: number;
|
||||
totalClients: number;
|
||||
totalSubscriptions: number;
|
||||
activeSubscriptions: number;
|
||||
totalRevenue: number;
|
||||
openSupportTickets: number;
|
||||
}
|
||||
|
||||
interface RecentTenant {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface RecentTicket {
|
||||
id: string;
|
||||
subject: string;
|
||||
tenantName: string;
|
||||
category: string;
|
||||
priority: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [recentTenants, setRecentTenants] = useState<RecentTenant[]>([]);
|
||||
const [recentTickets, setRecentTickets] = useState<RecentTicket[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const [statsRes, activityRes] = await Promise.all([
|
||||
api.get('/dashboard/stats'),
|
||||
api.get('/dashboard/recent-activity'),
|
||||
]);
|
||||
setStats(statsRes.data.data);
|
||||
setRecentTenants(activityRes.data.data.recentTenants || []);
|
||||
setRecentTickets(activityRes.data.data.recentTickets || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load dashboard:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <DashboardSkeleton />;
|
||||
}
|
||||
|
||||
const statCards = [
|
||||
{ label: 'Total Tenants', value: stats?.totalTenants ?? 0, sub: `${stats?.activeTenants ?? 0} active` },
|
||||
{ label: 'Total Users', value: stats?.totalUsers ?? 0 },
|
||||
{ label: 'Active Subscriptions', value: stats?.activeSubscriptions ?? 0, sub: `${stats?.totalSubscriptions ?? 0} total` },
|
||||
{ label: 'Platform Revenue', value: `₱${Number(stats?.totalRevenue ?? 0).toLocaleString()}` },
|
||||
{ label: 'Open Tickets', value: stats?.openSupportTickets ?? 0 },
|
||||
{ label: 'Total Clients', value: stats?.totalClients ?? 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{statCards.map((card) => (
|
||||
<div
|
||||
key={card.label}
|
||||
className="bg-white rounded-xl border border-surface-200 p-5"
|
||||
>
|
||||
<p className="text-sm text-surface-500">{card.label}</p>
|
||||
<p className="text-2xl font-bold text-surface-900 mt-1">{card.value}</p>
|
||||
{card.sub && <p className="text-xs text-surface-400 mt-1">{card.sub}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Recent Tenants */}
|
||||
<div className="bg-white rounded-xl border border-surface-200 p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-base font-semibold text-surface-900">Recent Tenants</h2>
|
||||
<Link href="/tenants" className="text-sm text-primary-600 hover:text-primary-700">
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{recentTenants.map((t) => (
|
||||
<Link
|
||||
key={t.id}
|
||||
href={`/tenants/${t.id}`}
|
||||
className="flex items-center justify-between p-3 rounded-lg hover:bg-surface-50 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-surface-900">{t.name}</p>
|
||||
<p className="text-xs text-surface-400">{t.slug}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded-full ${
|
||||
t.isActive
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'bg-red-50 text-red-700'
|
||||
}`}
|
||||
>
|
||||
{t.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
{recentTenants.length === 0 && (
|
||||
<p className="text-sm text-surface-400 text-center py-4">No tenants yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Support Tickets */}
|
||||
<div className="bg-white rounded-xl border border-surface-200 p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-base font-semibold text-surface-900">Open Support Tickets</h2>
|
||||
<Link href="/support" className="text-sm text-primary-600 hover:text-primary-700">
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{recentTickets.map((t) => (
|
||||
<Link
|
||||
key={t.id}
|
||||
href={`/support/${t.id}`}
|
||||
className="flex items-center justify-between p-3 rounded-lg hover:bg-surface-50 transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-surface-900 truncate">{t.subject}</p>
|
||||
<p className="text-xs text-surface-400">{t.tenantName}</p>
|
||||
</div>
|
||||
<PriorityBadge priority={t.priority} />
|
||||
</Link>
|
||||
))}
|
||||
{recentTickets.length === 0 && (
|
||||
<p className="text-sm text-surface-400 text-center py-4">No open tickets</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PriorityBadge({ priority }: { priority: string }) {
|
||||
const styles: Record<string, string> = {
|
||||
urgent: 'bg-red-100 text-red-700',
|
||||
high: 'bg-orange-100 text-orange-700',
|
||||
normal: 'bg-blue-100 text-blue-700',
|
||||
low: 'bg-surface-100 text-surface-600',
|
||||
};
|
||||
return (
|
||||
<span className={`text-xs px-2 py-1 rounded-full capitalize ${styles[priority] || styles.normal}`}>
|
||||
{priority}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6 animate-pulse">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="bg-white rounded-xl border border-surface-200 p-5 h-24" />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white rounded-xl border border-surface-200 p-5 h-64" />
|
||||
<div className="bg-white rounded-xl border border-surface-200 p-5 h-64" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user