[MCH-FE] feat: Connect Dashboard to real API

Replace hardcoded mock data with TanStack Query hooks:
- dashboardApi.getStats() for stats cards (sales, orders, customers, fiado)
- ordersApi.getAll() for recent orders list
- inventoryApi.getLowStock() for low stock alerts

Add loading spinners and error states for each section.
Add TypeScript interfaces for API response types.
Add formatCurrency helper for MXN formatting.
Add Spanish labels for order status.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-20 02:15:04 -06:00
parent c8cf78e0db
commit 2c4db175c2

View File

@ -1,3 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import {
TrendingUp,
ShoppingCart,
@ -5,26 +6,35 @@ import {
CreditCard,
Package,
AlertCircle,
Loader2,
} from 'lucide-react';
import { dashboardApi, ordersApi, inventoryApi } from '../lib/api';
const stats = [
{ name: 'Ventas Hoy', value: '$1,240', change: '+12%', icon: TrendingUp, color: 'green' },
{ name: 'Pedidos', value: '23', change: '+5', icon: ShoppingCart, color: 'blue' },
{ name: 'Clientes', value: '156', change: '+3', icon: Users, color: 'purple' },
{ name: 'Fiados Pendientes', value: '$2,100', change: '-$450', icon: CreditCard, color: 'orange' },
];
interface DashboardStats {
salesToday: number;
ordersCount: number;
customersCount: number;
pendingFiado: number;
salesChange: string;
ordersChange: string;
customersChange: string;
fiadoChange: string;
}
const recentOrders = [
{ id: 'MCH-001', customer: 'Maria Lopez', total: 156.00, status: 'ready', time: '10:30 AM' },
{ id: 'MCH-002', customer: 'Juan Perez', total: 89.50, status: 'preparing', time: '10:45 AM' },
{ id: 'MCH-003', customer: 'Ana Garcia', total: 234.00, status: 'pending', time: '11:00 AM' },
];
interface Order {
id: string;
customer: { name: string } | null;
total: number;
status: string;
createdAt: string;
}
const lowStockProducts = [
{ name: 'Coca-Cola 600ml', stock: 5, minStock: 10 },
{ name: 'Pan Bimbo', stock: 2, minStock: 5 },
{ name: 'Leche Lala 1L', stock: 3, minStock: 8 },
];
interface LowStockProduct {
id: string;
name: string;
stock: number;
minStock: number;
}
const statusColors: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800',
@ -33,7 +43,111 @@ const statusColors: Record<string, string> = {
completed: 'bg-gray-100 text-gray-800',
};
const statusLabels: Record<string, string> = {
pending: 'Pendiente',
preparing: 'Preparando',
ready: 'Listo',
completed: 'Completado',
};
function formatCurrency(amount: number): string {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
}).format(amount);
}
function LoadingSpinner() {
return (
<div className="flex items-center justify-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary-600" />
</div>
);
}
function ErrorMessage({ message }: { message: string }) {
return (
<div className="flex items-center justify-center p-8 text-red-600">
<AlertCircle className="h-5 w-5 mr-2" />
<span>{message}</span>
</div>
);
}
export function Dashboard() {
// Fetch dashboard stats
const {
data: statsData,
isLoading: statsLoading,
error: statsError,
} = useQuery({
queryKey: ['dashboard-stats'],
queryFn: async () => {
const response = await dashboardApi.getStats();
return response.data as DashboardStats;
},
});
// Fetch recent orders
const {
data: ordersData,
isLoading: ordersLoading,
error: ordersError,
} = useQuery({
queryKey: ['recent-orders'],
queryFn: async () => {
const response = await ordersApi.getAll({ status: undefined });
return (response.data as Order[]).slice(0, 5);
},
});
// Fetch low stock products
const {
data: lowStockData,
isLoading: lowStockLoading,
error: lowStockError,
} = useQuery({
queryKey: ['low-stock'],
queryFn: async () => {
const response = await inventoryApi.getLowStock();
return response.data as LowStockProduct[];
},
});
// Build stats array from API data
const stats = statsData
? [
{
name: 'Ventas Hoy',
value: formatCurrency(statsData.salesToday),
change: statsData.salesChange,
icon: TrendingUp,
color: 'green',
},
{
name: 'Pedidos',
value: String(statsData.ordersCount),
change: statsData.ordersChange,
icon: ShoppingCart,
color: 'blue',
},
{
name: 'Clientes',
value: String(statsData.customersCount),
change: statsData.customersChange,
icon: Users,
color: 'purple',
},
{
name: 'Fiados Pendientes',
value: formatCurrency(statsData.pendingFiado),
change: statsData.fiadoChange,
icon: CreditCard,
color: 'orange',
},
]
: [];
return (
<div className="space-y-6">
<div>
@ -42,26 +156,32 @@ export function Dashboard() {
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => (
<div key={stat.name} className="card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">{stat.name}</p>
<p className="text-2xl font-bold">{stat.value}</p>
<p className={`text-sm ${
stat.change.startsWith('+') ? 'text-green-600' : 'text-red-600'
}`}>
{stat.change}
</p>
</div>
<div className={`p-3 rounded-full bg-${stat.color}-100`}>
<stat.icon className={`h-6 w-6 text-${stat.color}-600`} />
{statsLoading ? (
<LoadingSpinner />
) : statsError ? (
<ErrorMessage message="Error al cargar estadisticas" />
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => (
<div key={stat.name} className="card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">{stat.name}</p>
<p className="text-2xl font-bold">{stat.value}</p>
<p className={`text-sm ${
stat.change.startsWith('+') ? 'text-green-600' : 'text-red-600'
}`}>
{stat.change}
</p>
</div>
<div className={`p-3 rounded-full bg-${stat.color}-100`}>
<stat.icon className={`h-6 w-6 text-${stat.color}-600`} />
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
)}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Recent Orders */}
@ -75,25 +195,35 @@ export function Dashboard() {
Ver todos
</a>
</div>
<div className="space-y-3">
{recentOrders.map((order) => (
<div
key={order.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div>
<p className="font-medium">{order.id}</p>
<p className="text-sm text-gray-500">{order.customer}</p>
{ordersLoading ? (
<LoadingSpinner />
) : ordersError ? (
<ErrorMessage message="Error al cargar pedidos" />
) : ordersData && ordersData.length > 0 ? (
<div className="space-y-3">
{ordersData.map((order) => (
<div
key={order.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div>
<p className="font-medium">{order.id.slice(0, 8).toUpperCase()}</p>
<p className="text-sm text-gray-500">
{order.customer?.name || 'Cliente General'}
</p>
</div>
<div className="text-right">
<p className="font-medium">{formatCurrency(order.total)}</p>
<span className={`inline-block px-2 py-1 text-xs rounded-full ${statusColors[order.status] || 'bg-gray-100 text-gray-800'}`}>
{statusLabels[order.status] || order.status}
</span>
</div>
</div>
<div className="text-right">
<p className="font-medium">${order.total.toFixed(2)}</p>
<span className={`inline-block px-2 py-1 text-xs rounded-full ${statusColors[order.status]}`}>
{order.status}
</span>
</div>
</div>
))}
</div>
))}
</div>
) : (
<p className="text-center text-gray-500 py-4">No hay pedidos recientes</p>
)}
</div>
{/* Low Stock Alert */}
@ -107,23 +237,31 @@ export function Dashboard() {
Ver inventario
</a>
</div>
<div className="space-y-3">
{lowStockProducts.map((product) => (
<div
key={product.name}
className="flex items-center justify-between p-3 bg-orange-50 rounded-lg"
>
<div className="flex items-center gap-3">
<Package className="h-5 w-5 text-orange-500" />
<p className="font-medium">{product.name}</p>
{lowStockLoading ? (
<LoadingSpinner />
) : lowStockError ? (
<ErrorMessage message="Error al cargar inventario" />
) : lowStockData && lowStockData.length > 0 ? (
<div className="space-y-3">
{lowStockData.map((product) => (
<div
key={product.id}
className="flex items-center justify-between p-3 bg-orange-50 rounded-lg"
>
<div className="flex items-center gap-3">
<Package className="h-5 w-5 text-orange-500" />
<p className="font-medium">{product.name}</p>
</div>
<div className="text-right">
<p className="font-bold text-orange-600">{product.stock} unidades</p>
<p className="text-xs text-gray-500">Min: {product.minStock}</p>
</div>
</div>
<div className="text-right">
<p className="font-bold text-orange-600">{product.stock} unidades</p>
<p className="text-xs text-gray-500">Min: {product.minStock}</p>
</div>
</div>
))}
</div>
))}
</div>
) : (
<p className="text-center text-gray-500 py-4">No hay productos con stock bajo</p>
)}
</div>
</div>
</div>