Files
mercadodevida/project/apps/admin/src/app/(dashboard)/page.tsx
2026-08-19 07:17:14 +02:00

249 lines
7.6 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { api } from '@/lib/api-client';
interface Stats {
ordersToday: number;
revenueTodayCents: number;
revenueTodayFormatted: string;
ordersByState: Record<string, number>;
outOfStockVariants: number;
totalActiveProducts: number;
newCustomersThisMonth: number;
generatedAt: string;
}
function formatCents(cents: number): string {
return `${(cents / 100).toFixed(2)}`;
}
const STATE_LABELS: Record<string, string> = {
PENDING: 'Pendientes',
AWAITING_PAYMENT: 'Esperando pago',
PAID: 'Pagados',
PROCESSING: 'Procesando',
SHIPPED: 'Enviados',
DELIVERED: 'Entregados',
CANCELLED: 'Cancelados',
REFUNDED: 'Reembolsados',
PARTIALLY_REFUNDED: 'Reembolso parcial',
};
const STATE_COLORS: Record<string, string> = {
PENDING: 'bg-amber-100 text-amber-700',
AWAITING_PAYMENT: 'bg-orange-100 text-orange-700',
PAID: 'bg-green-100 text-green-700',
PROCESSING: 'bg-blue-100 text-blue-700',
SHIPPED: 'bg-indigo-100 text-indigo-700',
DELIVERED: 'bg-emerald-100 text-emerald-700',
CANCELLED: 'bg-gray-100 text-gray-600',
REFUNDED: 'bg-red-100 text-red-700',
PARTIALLY_REFUNDED: 'bg-pink-100 text-pink-700',
};
function KPICard({
label,
value,
sub,
icon,
trend,
}: {
label: string;
value: string;
sub?: string;
icon: string;
trend?: 'up' | 'down' | 'neutral';
}) {
return (
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-medium text-gray-500">{label}</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{value}</p>
{sub && <p className="text-xs text-gray-400 mt-1">{sub}</p>}
</div>
<div className="text-3xl">{icon}</div>
</div>
</div>
);
}
function OrderStateBar({ state, count, total }: { state: string; count: number; total: number }) {
const pct = total > 0 ? (count / total) * 100 : 0;
const label = STATE_LABELS[state] ?? state;
const color = STATE_COLORS[state] ?? 'bg-gray-100 text-gray-700';
return (
<div className="flex items-center gap-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium min-w-[100px] ${color}`}>
{label}
</span>
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-[#2D6A4F] rounded-full transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-sm font-semibold text-gray-700 min-w-[32px] text-right">{count}</span>
</div>
);
}
export default function DashboardPage() {
const [stats, setStats] = useState<Stats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
api
.get<Stats>('/api/admin/stats')
.then(setStats)
.catch(() => setError('No se pudieron cargar las estadísticas'))
.finally(() => setLoading(false));
}, []);
if (loading) {
return (
<div className="p-8 space-y-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="bg-white border border-gray-200 rounded-xl p-5 animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/2 mb-3" />
<div className="h-8 bg-gray-200 rounded w-3/4" />
</div>
))}
</div>
<div className="bg-white border border-gray-200 rounded-xl p-6 animate-pulse">
<div className="h-5 bg-gray-200 rounded w-1/4 mb-4" />
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-8 bg-gray-100 rounded" />
))}
</div>
</div>
</div>
);
}
if (error || !stats) {
return (
<div className="p-8">
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-sm text-red-700">
{error ?? 'Error desconocido'}
</div>
</div>
);
}
const totalOrders = Object.values(stats.ordersByState).reduce((a, b) => a + b, 0);
const ordersByStateSorted = Object.entries(stats.ordersByState).sort(
([, a], [, b]) => b - a,
);
return (
<div className="p-8 space-y-6">
{/* KPI Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<KPICard
label="Pedidos hoy"
value={String(stats.ordersToday)}
sub="Órdenes del día"
icon="📦"
/>
<KPICard
label="Ingresos hoy"
value={formatCents(stats.revenueTodayCents)}
sub="Revenue del día"
icon="💶"
/>
<KPICard
label="Productos activos"
value={String(stats.totalActiveProducts)}
sub="En el catálogo"
icon="🌿"
/>
<KPICard
label="Sin stock"
value={String(stats.outOfStockVariants)}
sub="Variantes agotadas"
icon="⚠️"
/>
</div>
{/* Secondary KPIs */}
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
<KPICard
label="Clientes nuevos"
value={String(stats.newCustomersThisMonth)}
sub="Este mes"
icon="👥"
/>
<KPICard
label="Total pedidos"
value={String(totalOrders)}
sub="En el sistema"
icon="📋"
/>
<KPICard
label="Alertas"
value={
stats.outOfStockVariants > 0
? `${stats.outOfStockVariants} sin stock`
: 'Sin alertas'
}
sub={stats.outOfStockVariants > 0 ? 'Revisar inventario' : 'Todo OK'}
icon={stats.outOfStockVariants > 0 ? '🔴' : '✅'}
/>
</div>
{/* Orders by state */}
<div className="bg-white border border-gray-200 rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-semibold text-gray-900">Pedidos por estado</h2>
<span className="text-sm text-gray-500">{totalOrders} total</span>
</div>
{totalOrders === 0 ? (
<div className="py-8 text-center text-gray-400 text-sm">
<p className="text-3xl mb-2">📋</p>
<p>No hay pedidos en el sistema</p>
</div>
) : (
<div>
{ordersByStateSorted.map(([state, count]) => (
<OrderStateBar
key={state}
state={state}
count={count}
total={totalOrders}
/>
))}
</div>
)}
</div>
{/* Quick actions */}
<div className="bg-white border border-gray-200 rounded-xl p-6">
<h2 className="text-base font-semibold text-gray-900 mb-4">Acciones rápidas</h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
{[
{ href: '/products/new', label: '+ Nuevo producto', icon: '🌿' },
{ href: '/orders', label: 'Ver pedidos', icon: '📦' },
{ href: '/inventory', label: 'Revisar stock', icon: '📊' },
{ href: '/customers', label: 'Clientes', icon: '👥' },
].map(({ href, label, icon }) => (
<a
key={href}
href={href}
className="flex items-center gap-2 px-4 py-3 border border-gray-200 rounded-xl hover:bg-gray-50 hover:border-[#2D6A4F] transition-colors text-sm font-medium text-gray-700"
>
<span>{icon}</span>
<span>{label}</span>
</a>
))}
</div>
</div>
</div>
);
}