76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import Link from 'next/link';
|
|
import { Card } from '@/components/ui';
|
|
import { requireStaff } from '@/lib/server/auth';
|
|
import { overview } from '@/lib/server/crm';
|
|
import { formatCurrency, formatDate } from '@/lib/utils';
|
|
import { t } from '@/lib/i18n';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
export default async function AdminOverviewPage() {
|
|
await requireStaff();
|
|
const data = await overview();
|
|
const maxStage = Math.max(1, ...data.byStage.map((s) => s.count));
|
|
|
|
const counters = [
|
|
{ label: t('admin.customersTotal'), value: String(data.customers), href: '/admin/customers' },
|
|
{ label: t('admin.leads'), value: String(data.leads), href: '/admin/customers?status=lead' },
|
|
{ label: t('admin.dealsTotal'), value: String(data.deals), href: '/admin/deals' },
|
|
{ label: t('admin.wonValue'), value: formatCurrency(data.wonValue), href: '/admin/deals?stage=won' },
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-2xl font-bold">{t('admin.overview')}</h1>
|
|
|
|
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
|
{counters.map((c) => (
|
|
<Link key={c.label} href={c.href} className="card block">
|
|
<p className="muted text-sm">{c.label}</p>
|
|
<p className="mt-2 text-3xl font-bold">{c.value}</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
<Card title={t('admin.dealStages')}>
|
|
<ul className="space-y-4">
|
|
{data.byStage.map((s) => (
|
|
<li key={s.stage}>
|
|
<div className="mb-1 flex justify-between text-sm">
|
|
<span>{t(`admin.${s.stage}`)}</span>
|
|
<span className="muted">{s.count}</span>
|
|
</div>
|
|
<div className="h-2 w-full overflow-hidden rounded-full" style={{ background: 'var(--border)' }}>
|
|
<div
|
|
className="h-full rounded-full"
|
|
style={{ width: `${Math.round((s.count / maxStage) * 100)}%`, background: 'var(--color-brand-600)' }}
|
|
/>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</Card>
|
|
|
|
<Card title={t('admin.recentActivity')} actions={<Link href="/admin/audit" className="btn-secondary">{t('admin.audit')}</Link>}>
|
|
{data.recent.length === 0 ? (
|
|
<p className="muted text-sm">{t('common.empty')}</p>
|
|
) : (
|
|
<ul className="divide-y" style={{ borderColor: 'var(--border)' }}>
|
|
{data.recent.map((entry) => (
|
|
<li key={entry.id} className="flex items-center justify-between gap-4 py-2 text-sm">
|
|
<span>
|
|
<span className="font-medium">{entry.action}</span>
|
|
{entry.target ? <span className="muted"> → {entry.target}</span> : null}
|
|
</span>
|
|
<span className="muted shrink-0 text-xs">
|
|
{entry.actor} · {formatDate(entry.createdAt)}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|