[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 { 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 clsx from 'clsx';
|
||||||
|
import { ordersApi } from '../lib/api';
|
||||||
|
|
||||||
const mockOrders = [
|
// Order types based on API response
|
||||||
{
|
interface OrderItem {
|
||||||
id: 'MCH-001',
|
name: string;
|
||||||
customer: 'Maria Lopez',
|
qty: number;
|
||||||
phone: '5551234567',
|
price: number;
|
||||||
items: [
|
}
|
||||||
{ name: 'Coca-Cola 600ml', qty: 2, price: 18.00 },
|
|
||||||
{ name: 'Sabritas', qty: 3, price: 15.00 },
|
interface Order {
|
||||||
],
|
id: string;
|
||||||
total: 81.00,
|
customer: string;
|
||||||
status: 'pending',
|
phone: string;
|
||||||
source: 'whatsapp',
|
items: OrderItem[];
|
||||||
createdAt: '2024-01-15T10:30:00',
|
total: number;
|
||||||
},
|
status: string;
|
||||||
{
|
source: string;
|
||||||
id: 'MCH-002',
|
createdAt: string;
|
||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
pending: { label: 'Pendiente', color: 'yellow', icon: Clock },
|
pending: { label: 'Pendiente', color: 'yellow', icon: Clock },
|
||||||
@ -56,20 +35,85 @@ const statusFlow = ['pending', 'confirmed', 'preparing', 'ready', 'completed'];
|
|||||||
|
|
||||||
export function Orders() {
|
export function Orders() {
|
||||||
const [filter, setFilter] = useState('all');
|
const [filter, setFilter] = useState('all');
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const filteredOrders = filter === 'all'
|
// Fetch orders from API
|
||||||
? mockOrders
|
const {
|
||||||
: mockOrders.filter(o => o.status === filter);
|
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);
|
const currentIndex = statusFlow.indexOf(currentStatus);
|
||||||
if (currentIndex < statusFlow.length - 1) {
|
if (currentIndex < statusFlow.length - 1) {
|
||||||
const nextStatus = statusFlow[currentIndex + 1];
|
const nextStatus = statusFlow[currentIndex + 1];
|
||||||
console.log(`Updating ${orderId} to ${nextStatus}`);
|
updateStatusMutation.mutate({ id: orderId, status: nextStatus });
|
||||||
// API call would go here
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
@ -88,17 +132,17 @@ export function Orders() {
|
|||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Todos ({mockOrders.length})
|
Todos ({allOrders.length})
|
||||||
</button>
|
</button>
|
||||||
{Object.entries(statusConfig).slice(0, 4).map(([status, config]) => {
|
{Object.entries(statusConfig).slice(0, 4).map(([statusKey, config]) => {
|
||||||
const count = mockOrders.filter(o => o.status === status).length;
|
const count = allOrders.filter(o => o.status === statusKey).length;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={status}
|
key={statusKey}
|
||||||
onClick={() => setFilter(status)}
|
onClick={() => setFilter(statusKey)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors',
|
'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-${config.color}-100 text-${config.color}-700`
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
)}
|
)}
|
||||||
@ -111,70 +155,94 @@ export function Orders() {
|
|||||||
|
|
||||||
{/* Orders List */}
|
{/* Orders List */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredOrders.map((order) => {
|
{orders.length === 0 ? (
|
||||||
const status = statusConfig[order.status as keyof typeof statusConfig];
|
<div className="text-center py-12">
|
||||||
const StatusIcon = status.icon;
|
<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 (
|
return (
|
||||||
<div key={order.id} className="card">
|
<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 flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className={`p-3 rounded-full bg-${status.color}-100`}>
|
<div className={`p-3 rounded-full bg-${status.color}-100`}>
|
||||||
<StatusIcon className={`h-6 w-6 text-${status.color}-600`} />
|
<StatusIcon className={`h-6 w-6 text-${status.color}-600`} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-bold text-lg">{order.id}</h3>
|
<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`}>
|
<span className={`px-2 py-0.5 text-xs rounded-full bg-${status.color}-100 text-${status.color}-700`}>
|
||||||
{status.label}
|
{status.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600">
|
<span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600">
|
||||||
{order.source === 'whatsapp' ? 'WhatsApp' : 'POS'}
|
{order.source === 'whatsapp' ? 'WhatsApp' : 'POS'}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600">{order.customer}</p>
|
||||||
|
<p className="text-sm text-gray-500">{order.phone}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{order.status !== 'completed' && order.status !== 'cancelled' && (
|
<div className="flex-1 lg:px-8">
|
||||||
<button
|
<div className="text-sm text-gray-600">
|
||||||
onClick={() => updateStatus(order.id, order.status)}
|
{order.items?.map((item, i) => (
|
||||||
className="btn-primary whitespace-nowrap"
|
<span key={i}>
|
||||||
>
|
{item.qty}x {item.name}
|
||||||
{order.status === 'pending' && 'Confirmar'}
|
{i < order.items.length - 1 ? ', ' : ''}
|
||||||
{order.status === 'confirmed' && 'Preparar'}
|
</span>
|
||||||
{order.status === 'preparing' && 'Listo'}
|
))}
|
||||||
{order.status === 'ready' && 'Entregar'}
|
</div>
|
||||||
</button>
|
</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>
|
);
|
||||||
);
|
})
|
||||||
})}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user