[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:
parent
00691fd1f7
commit
c8cf78e0db
@ -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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user