michangarrito-frontend-v2/src/pages/Dashboard.tsx
rckrdmrd 1b2fca85f8 [MCH-FE] feat: Connect Settings to real API
- Add settingsApi in lib/api.ts with get, update, getWhatsAppStatus,
  testWhatsApp and getSubscription endpoints
- Rewrite Settings.tsx to use React Query for data fetching
- Implement useQuery for loading settings and subscription data
- Implement useMutation for saving settings changes
- Add form state management with controlled inputs
- Add loading states, error handling and success notifications
- Add individual save buttons per section plus global save
- Add WhatsApp connection test functionality
- Display subscription details with token usage

Also includes exports API and export button components added by linter.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 02:34:59 -06:00

291 lines
8.9 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import {
TrendingUp,
ShoppingCart,
Users,
CreditCard,
Package,
AlertCircle,
Loader2,
} from 'lucide-react';
import { dashboardApi, ordersApi, inventoryApi, exportsApi, downloadBlob } from '../lib/api';
import { ExportButton } from '../components/ExportButton';
interface DashboardStats {
salesToday: number;
ordersCount: number;
customersCount: number;
pendingFiado: number;
salesChange: string;
ordersChange: string;
customersChange: string;
fiadoChange: string;
}
interface Order {
id: string;
customer: { name: string } | null;
total: number;
status: string;
createdAt: string;
}
interface LowStockProduct {
id: string;
name: string;
stock: number;
minStock: number;
}
const statusColors: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800',
preparing: 'bg-blue-100 text-blue-800',
ready: 'bg-green-100 text-green-800',
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',
},
]
: [];
// Get today's date for export filename
const today = new Date().toISOString().split('T')[0];
const handleExportSalesPdf = async () => {
const response = await exportsApi.sales('pdf', { startDate: today, endDate: today });
downloadBlob(response.data, `ventas-${today}.pdf`);
};
const handleExportSalesExcel = async () => {
const response = await exportsApi.sales('xlsx', { startDate: today, endDate: today });
downloadBlob(response.data, `ventas-${today}.xlsx`);
};
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-500">Bienvenido a MiChangarrito</p>
</div>
<ExportButton
onExportPdf={handleExportSalesPdf}
onExportExcel={handleExportSalesExcel}
disabled={statsLoading}
/>
</div>
{/* Stats Grid */}
{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 className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Recent Orders */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<ShoppingCart className="h-5 w-5" />
Pedidos Recientes
</h2>
<a href="/orders" className="text-sm text-primary-600 hover:underline">
Ver todos
</a>
</div>
{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>
) : (
<p className="text-center text-gray-500 py-4">No hay pedidos recientes</p>
)}
</div>
{/* Low Stock Alert */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-orange-500" />
Stock Bajo
</h2>
<a href="/inventory" className="text-sm text-primary-600 hover:underline">
Ver inventario
</a>
</div>
{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>
) : (
<p className="text-center text-gray-500 py-4">No hay productos con stock bajo</p>
)}
</div>
</div>
</div>
);
}