This commit is contained in:
Emmanuel Rojas committed 2026-09-30 14:45:10 -05:00
1 parent 90125aede9
commit 2d6c29a1f3
404 files changed
+126983 -85

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
"use client";
import { useMemo } from "react";
import { iconArchive, iconBox, iconCart, iconDelivery, iconTag, iconUsersGroup } from "../_core/icons";
import { useOperatingDay } from "../_core/hooks/use-operating-day";
import { productsResource, shopCartsResource, usersResource, warehousesResource } from "../_core/services/entities";
import { QuickActionCard } from "./_components/quick-action-card";
import { StatCard } from "./_components/stat-card";
const ROLE_LABELS: Record<string, string> = { admin: "administrador", cashier: "cajero" };
function money(value: number) {
return value.toLocaleString("es-AR", { style: "currency", currency: "USD" });
}
export default function AdminDashboardPage() {
const { data: carts, isLoading: loadingCarts } = shopCartsResource.useList();
const { data: products, isLoading: loadingProducts } = productsResource.useList();
const { data: warehouses } = warehousesResource.useList();
const { data: users, isLoading: loadingUsers } = usersResource.useList();
const { start: operatingDayStart } = useOperatingDay();
const salesToday = useMemo(() => {
if (!carts) return null;
return carts
.filter((c) => c.status === "paid" && new Date(c.createdAt) >= operatingDayStart)
.reduce((sum, c) => sum + Number(c.totalPrice), 0);
}, [carts, operatingDayStart]);
const stockByProduct = useMemo(() => {
const map = new Map<number, number>();
for (const w of warehouses ?? []) map.set(w.productId, (map.get(w.productId) ?? 0) + w.stock);
return map;
}, [warehouses]);
const lowStockCount = useMemo(() => {
if (!products) return null;
return products.filter((p) => (stockByProduct.get(p.id) ?? 0) <= p.minStock).length;
}, [products, stockByProduct]);
const usersByRole = useMemo(() => {
const counts: Record<string, number> = {};
for (const u of users ?? []) counts[u.role] = (counts[u.role] ?? 0) + 1;
return counts;
}, [users]);
const usersHint = Object.entries(usersByRole)
.map(([role, count]) => `${count} ${ROLE_LABELS[role] ?? role}${count === 1 ? "" : "s"}`)
.join(", ");
return (
<div className="flex flex-col gap-8">
<div>
<h1 className="text-xl font-semibold text-foreground">Resumen</h1>
<p className="text-sm text-muted">Estado general del negocio.</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<StatCard
icon={iconCart}
label="Ventas de hoy"
value={loadingCarts || salesToday === null ? "—" : money(salesToday)}
hint={salesToday ? "Total cobrado hoy" : "Sin ventas registradas aún"}
/>
<StatCard
icon={iconBox}
label="Productos activos"
value={loadingProducts || !products ? "—" : String(products.length)}
hint={products && products.length > 0 ? "En el catálogo" : "Conectá el catálogo para ver el total"}
/>
<StatCard
icon={iconArchive}
label="Alertas de stock"
value={lowStockCount === null ? "—" : String(lowStockCount)}
hint={lowStockCount ? "Productos bajo el mínimo" : "Sin datos de inventario aún"}
/>
<StatCard
icon={iconUsersGroup}
label="Usuarios activos"
value={loadingUsers || !users ? "—" : String(users.length)}
hint={usersHint || "Sin usuarios"}
/>
</div>
<div>
<h2 className="mb-3 text-sm font-semibold text-foreground">Accesos rápidos</h2>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
<QuickActionCard href="/admin/inventario" icon={iconArchive} label="Almacenes" description="Stock, entradas y salidas" />
<QuickActionCard href="/admin/inventario/productos" icon={iconBox} label="Productos" description="Catálogo y precios" />
<QuickActionCard href="/admin/inventario/categorias" icon={iconTag} label="Categorías" description="Organizá el catálogo" />
<QuickActionCard href="/admin/proveedores" icon={iconDelivery} label="Proveedores" description="Contactos y pedidos" />
<QuickActionCard href="/admin/usuarios" icon={iconUsersGroup} label="Usuarios" description="Roles y accesos" />
</div>
</div>
</div>
);
}