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

- Replace mock data with useQuery for fetching orders from ordersApi
- Add useMutation for updating order status
- Implement loading state with spinner
- Add error state with retry button
- Add empty state when no orders found
- Show individual loading state on status update buttons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-20 02:14:56 -06:00
parent 00691fd1f7
commit c8cf78e0db

View File

@ -1,47 +1,26 @@
import { useState } from 'react';
import { Clock, CheckCircle, XCircle, ChefHat, Package } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Clock, CheckCircle, XCircle, ChefHat, Package, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
import clsx from 'clsx';
import { ordersApi } from '../lib/api';
const mockOrders = [
{
id: 'MCH-001',
customer: 'Maria Lopez',
phone: '5551234567',
items: [
{ name: 'Coca-Cola 600ml', qty: 2, price: 18.00 },
{ name: 'Sabritas', qty: 3, price: 15.00 },
],
total: 81.00,
status: 'pending',
source: 'whatsapp',
createdAt: '2024-01-15T10:30:00',
},
{
id: 'MCH-002',
customer: 'Juan Perez',
phone: '5559876543',
items: [
{ name: 'Leche Lala 1L', qty: 2, price: 28.00 },
{ name: 'Pan Bimbo', qty: 1, price: 45.00 },
],
total: 101.00,
status: 'preparing',
source: 'pos',
createdAt: '2024-01-15T10:45:00',
},
{
id: 'MCH-003',
customer: 'Ana Garcia',
phone: '5555555555',
items: [
{ name: 'Fabuloso 1L', qty: 1, price: 32.00 },
],
total: 32.00,
status: 'ready',
source: 'whatsapp',
createdAt: '2024-01-15T11:00:00',
},
];
// Order types based on API response
interface OrderItem {
name: string;
qty: number;
price: number;
}
interface Order {
id: string;
customer: string;
phone: string;
items: OrderItem[];
total: number;
status: string;
source: string;
createdAt: string;
}
const statusConfig = {
pending: { label: 'Pendiente', color: 'yellow', icon: Clock },
@ -56,20 +35,85 @@ const statusFlow = ['pending', 'confirmed', 'preparing', 'ready', 'completed'];
export function Orders() {
const [filter, setFilter] = useState('all');
const queryClient = useQueryClient();
const filteredOrders = filter === 'all'
? mockOrders
: mockOrders.filter(o => o.status === filter);
// Fetch orders from API
const {
data: ordersResponse,
isLoading,
isError,
error,
refetch,
} = useQuery({
queryKey: ['orders', filter === 'all' ? undefined : filter],
queryFn: () => ordersApi.getAll(filter === 'all' ? undefined : { status: filter }),
});
const updateStatus = (orderId: string, currentStatus: string) => {
// Get orders array from response
const orders: Order[] = ordersResponse?.data || [];
// Fetch all orders for counting (unfiltered)
const { data: allOrdersResponse } = useQuery({
queryKey: ['orders', 'all'],
queryFn: () => ordersApi.getAll(),
enabled: filter !== 'all', // Only fetch when we have a filter active
});
// Use all orders for counting, or current orders if no filter
const allOrders: Order[] = filter === 'all' ? orders : (allOrdersResponse?.data || orders);
// Mutation for updating order status
const updateStatusMutation = useMutation({
mutationFn: ({ id, status }: { id: string; status: string }) =>
ordersApi.updateStatus(id, status),
onSuccess: () => {
// Invalidate and refetch orders
queryClient.invalidateQueries({ queryKey: ['orders'] });
},
onError: (err) => {
console.error('Error updating order status:', err);
// Could show a toast notification here
},
});
const handleUpdateStatus = (orderId: string, currentStatus: string) => {
const currentIndex = statusFlow.indexOf(currentStatus);
if (currentIndex < statusFlow.length - 1) {
const nextStatus = statusFlow[currentIndex + 1];
console.log(`Updating ${orderId} to ${nextStatus}`);
// API call would go here
updateStatusMutation.mutate({ id: orderId, status: nextStatus });
}
};
// Loading state
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary-600" />
<p className="mt-4 text-gray-500">Cargando pedidos...</p>
</div>
);
}
// Error state
if (isError) {
return (
<div className="flex flex-col items-center justify-center py-12">
<AlertCircle className="h-12 w-12 text-red-500" />
<h3 className="mt-4 text-lg font-semibold text-gray-900">Error al cargar pedidos</h3>
<p className="mt-2 text-gray-500">
{error instanceof Error ? error.message : 'Ocurrio un error inesperado'}
</p>
<button
onClick={() => refetch()}
className="mt-4 flex items-center gap-2 btn-primary"
>
<RefreshCw className="h-4 w-4" />
Reintentar
</button>
</div>
);
}
return (
<div className="space-y-6">
<div>
@ -88,17 +132,17 @@ export function Orders() {
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
)}
>
Todos ({mockOrders.length})
Todos ({allOrders.length})
</button>
{Object.entries(statusConfig).slice(0, 4).map(([status, config]) => {
const count = mockOrders.filter(o => o.status === status).length;
{Object.entries(statusConfig).slice(0, 4).map(([statusKey, config]) => {
const count = allOrders.filter(o => o.status === statusKey).length;
return (
<button
key={status}
onClick={() => setFilter(status)}
key={statusKey}
onClick={() => setFilter(statusKey)}
className={clsx(
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors',
filter === status
filter === statusKey
? `bg-${config.color}-100 text-${config.color}-700`
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
)}
@ -111,70 +155,94 @@ export function Orders() {
{/* Orders List */}
<div className="space-y-4">
{filteredOrders.map((order) => {
const status = statusConfig[order.status as keyof typeof statusConfig];
const StatusIcon = status.icon;
{orders.length === 0 ? (
<div className="text-center py-12">
<Package className="h-12 w-12 mx-auto text-gray-400" />
<h3 className="mt-4 text-lg font-semibold text-gray-900">No hay pedidos</h3>
<p className="mt-2 text-gray-500">
{filter === 'all'
? 'Aun no tienes pedidos registrados'
: `No hay pedidos con estado "${statusConfig[filter as keyof typeof statusConfig]?.label || filter}"`}
</p>
</div>
) : (
orders.map((order) => {
const status = statusConfig[order.status as keyof typeof statusConfig] || statusConfig.pending;
const StatusIcon = status.icon;
const isUpdating = updateStatusMutation.isPending &&
updateStatusMutation.variables?.id === order.id;
return (
<div key={order.id} className="card">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full bg-${status.color}-100`}>
<StatusIcon className={`h-6 w-6 text-${status.color}-600`} />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-bold text-lg">{order.id}</h3>
<span className={`px-2 py-0.5 text-xs rounded-full bg-${status.color}-100 text-${status.color}-700`}>
{status.label}
</span>
<span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600">
{order.source === 'whatsapp' ? 'WhatsApp' : 'POS'}
</span>
return (
<div key={order.id} className="card">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full bg-${status.color}-100`}>
<StatusIcon className={`h-6 w-6 text-${status.color}-600`} />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-bold text-lg">{order.id}</h3>
<span className={`px-2 py-0.5 text-xs rounded-full bg-${status.color}-100 text-${status.color}-700`}>
{status.label}
</span>
<span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600">
{order.source === 'whatsapp' ? 'WhatsApp' : 'POS'}
</span>
</div>
<p className="text-gray-600">{order.customer}</p>
<p className="text-sm text-gray-500">{order.phone}</p>
</div>
<p className="text-gray-600">{order.customer}</p>
<p className="text-sm text-gray-500">{order.phone}</p>
</div>
</div>
<div className="flex-1 lg:px-8">
<div className="text-sm text-gray-600">
{order.items.map((item, i) => (
<span key={i}>
{item.qty}x {item.name}
{i < order.items.length - 1 ? ', ' : ''}
</span>
))}
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-2xl font-bold">${order.total.toFixed(2)}</p>
<p className="text-sm text-gray-500">
{new Date(order.createdAt).toLocaleTimeString('es-MX', {
hour: '2-digit',
minute: '2-digit',
})}
</p>
</div>
{order.status !== 'completed' && order.status !== 'cancelled' && (
<button
onClick={() => updateStatus(order.id, order.status)}
className="btn-primary whitespace-nowrap"
>
{order.status === 'pending' && 'Confirmar'}
{order.status === 'confirmed' && 'Preparar'}
{order.status === 'preparing' && 'Listo'}
{order.status === 'ready' && 'Entregar'}
</button>
)}
<div className="flex-1 lg:px-8">
<div className="text-sm text-gray-600">
{order.items?.map((item, i) => (
<span key={i}>
{item.qty}x {item.name}
{i < order.items.length - 1 ? ', ' : ''}
</span>
))}
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-2xl font-bold">${order.total?.toFixed(2) || '0.00'}</p>
<p className="text-sm text-gray-500">
{new Date(order.createdAt).toLocaleTimeString('es-MX', {
hour: '2-digit',
minute: '2-digit',
})}
</p>
</div>
{order.status !== 'completed' && order.status !== 'cancelled' && (
<button
onClick={() => handleUpdateStatus(order.id, order.status)}
disabled={isUpdating}
className={clsx(
'btn-primary whitespace-nowrap',
isUpdating && 'opacity-50 cursor-not-allowed'
)}
>
{isUpdating ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
{order.status === 'pending' && 'Confirmar'}
{order.status === 'confirmed' && 'Preparar'}
{order.status === 'preparing' && 'Listo'}
{order.status === 'ready' && 'Entregar'}
</>
)}
</button>
)}
</div>
</div>
</div>
</div>
);
})}
);
})
)}
</div>
</div>
);