From 8199d622b19fc2f4270d46f59ac8351b43919bad Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Tue, 20 Jan 2026 02:15:31 -0600 Subject: [PATCH] [MCH-FE] feat: Connect Products to real API Replace mock data with real API calls using React Query: - useQuery for fetching products with category/search filters - useMutation for create, update, and delete operations - Add loading, error, and empty states - Add create/edit modal with form validation - Add delete confirmation modal - Maintain existing UI structure Co-Authored-By: Claude Opus 4.5 --- src/pages/Products.tsx | 502 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 455 insertions(+), 47 deletions(-) diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx index 8014c11..d4c844b 100644 --- a/src/pages/Products.tsx +++ b/src/pages/Products.tsx @@ -1,14 +1,31 @@ import { useState } from 'react'; -import { Search, Plus, Edit, Trash2, Package } from 'lucide-react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Search, Plus, Edit, Trash2, Package, X, AlertCircle, Loader2 } from 'lucide-react'; +import { productsApi } from '../lib/api'; -const mockProducts = [ - { id: '1', name: 'Coca-Cola 600ml', category: 'bebidas', price: 18.00, stock: 24, barcode: '7501055300051' }, - { id: '2', name: 'Sabritas Original', category: 'botanas', price: 15.00, stock: 12, barcode: '7501000111111' }, - { id: '3', name: 'Leche Lala 1L', category: 'lacteos', price: 28.00, stock: 8, barcode: '7501020500001' }, - { id: '4', name: 'Pan Bimbo Grande', category: 'panaderia', price: 45.00, stock: 5, barcode: '7501030400001' }, - { id: '5', name: 'Fabuloso 1L', category: 'limpieza', price: 32.00, stock: 6, barcode: '7501040300001' }, - { id: '6', name: 'Pepsi 600ml', category: 'bebidas', price: 17.00, stock: 20, barcode: '7501055300052' }, -]; +// Type definitions +interface Product { + id: string; + name: string; + category: string; + price: number; + stock: number; + barcode?: string; + description?: string; + minStock?: number; + sku?: string; +} + +interface ProductFormData { + name: string; + category: string; + price: number; + stock: number; + barcode?: string; + description?: string; + minStock?: number; + sku?: string; +} const categories = [ { id: 'all', name: 'Todos' }, @@ -17,26 +34,162 @@ const categories = [ { id: 'lacteos', name: 'Lacteos' }, { id: 'panaderia', name: 'Panaderia' }, { id: 'limpieza', name: 'Limpieza' }, + { id: 'abarrotes', name: 'Abarrotes' }, + { id: 'otros', name: 'Otros' }, ]; +const initialFormData: ProductFormData = { + name: '', + category: 'bebidas', + price: 0, + stock: 0, + barcode: '', + description: '', + minStock: 5, + sku: '', +}; + export function Products() { const [search, setSearch] = useState(''); const [category, setCategory] = useState('all'); + const [showModal, setShowModal] = useState(false); + const [editingProduct, setEditingProduct] = useState(null); + const [deletingProduct, setDeletingProduct] = useState(null); + const [formData, setFormData] = useState(initialFormData); + const [formError, setFormError] = useState(null); - const filteredProducts = mockProducts.filter((product) => { - const matchesSearch = product.name.toLowerCase().includes(search.toLowerCase()); - const matchesCategory = category === 'all' || product.category === category; - return matchesSearch && matchesCategory; + const queryClient = useQueryClient(); + + // Fetch products from API + const { data: productsResponse, isLoading, isError, error } = useQuery({ + queryKey: ['products', { category: category === 'all' ? undefined : category, search: search || undefined }], + queryFn: async () => { + const response = await productsApi.getAll({ + category: category === 'all' ? undefined : category, + search: search || undefined, + }); + return response.data; + }, }); + // Extract products array from response + const products: Product[] = Array.isArray(productsResponse) + ? productsResponse + : productsResponse?.data || productsResponse?.products || []; + + // Create product mutation + const createMutation = useMutation({ + mutationFn: (data: ProductFormData) => productsApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }); + handleCloseModal(); + }, + onError: (err: any) => { + setFormError(err.response?.data?.message || 'Error al crear el producto'); + }, + }); + + // Update product mutation + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: ProductFormData }) => + productsApi.update(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }); + handleCloseModal(); + }, + onError: (err: any) => { + setFormError(err.response?.data?.message || 'Error al actualizar el producto'); + }, + }); + + // Delete product mutation + const deleteMutation = useMutation({ + mutationFn: (id: string) => productsApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }); + setDeletingProduct(null); + }, + onError: (err: any) => { + alert(err.response?.data?.message || 'Error al eliminar el producto'); + }, + }); + + const handleOpenCreate = () => { + setEditingProduct(null); + setFormData(initialFormData); + setFormError(null); + setShowModal(true); + }; + + const handleOpenEdit = (product: Product) => { + setEditingProduct(product); + setFormData({ + name: product.name, + category: product.category, + price: product.price, + stock: product.stock, + barcode: product.barcode || '', + description: product.description || '', + minStock: product.minStock || 5, + sku: product.sku || '', + }); + setFormError(null); + setShowModal(true); + }; + + const handleCloseModal = () => { + setShowModal(false); + setEditingProduct(null); + setFormData(initialFormData); + setFormError(null); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setFormError(null); + + // Validation + if (!formData.name.trim()) { + setFormError('El nombre es requerido'); + return; + } + if (formData.price <= 0) { + setFormError('El precio debe ser mayor a 0'); + return; + } + if (formData.stock < 0) { + setFormError('El stock no puede ser negativo'); + return; + } + + if (editingProduct) { + updateMutation.mutate({ id: editingProduct.id, data: formData }); + } else { + createMutation.mutate(formData); + } + }; + + const handleDelete = () => { + if (deletingProduct) { + deleteMutation.mutate(deletingProduct.id); + } + }; + + const isSubmitting = createMutation.isPending || updateMutation.isPending; + return (

Productos

-

{mockProducts.length} productos en catalogo

+

+ {isLoading ? 'Cargando...' : `${products.length} productos en catalogo`} +

- @@ -65,39 +218,294 @@ export function Products() {
- {/* Products Grid */} -
- {filteredProducts.map((product) => ( -
-
-
- -
-
- - -
-
-

{product.name}

-

{product.category}

-
- - ${product.price.toFixed(2)} - - 10 ? 'text-green-600' : product.stock > 5 ? 'text-yellow-600' : 'text-red-600' - }`}> - {product.stock} en stock - -
-

{product.barcode}

+ {/* Error State */} + {isError && ( +
+ +
+

Error al cargar productos

+

+ {(error as any)?.response?.data?.message || 'No se pudieron cargar los productos. Intenta de nuevo.'} +

- ))} -
+
+ )} + + {/* Loading State */} + {isLoading && ( +
+ +

Cargando productos...

+
+ )} + + {/* Empty State */} + {!isLoading && !isError && products.length === 0 && ( +
+ +

No hay productos

+

+ {search || category !== 'all' + ? 'No se encontraron productos con esos filtros' + : 'Agrega tu primer producto para empezar'} +

+ {!search && category === 'all' && ( + + )} +
+ )} + + {/* Products Grid */} + {!isLoading && !isError && products.length > 0 && ( +
+ {products.map((product) => ( +
+
+
+ +
+
+ + +
+
+

{product.name}

+

{product.category}

+
+ + ${Number(product.price).toFixed(2)} + + 10 ? 'text-green-600' : product.stock > 5 ? 'text-yellow-600' : 'text-red-600' + }`}> + {product.stock} en stock + +
+ {product.barcode && ( +

{product.barcode}

+ )} +
+ ))} +
+ )} + + {/* Create/Edit Modal */} + {showModal && ( +
+
+
+

+ {editingProduct ? 'Editar Producto' : 'Nuevo Producto'} +

+ +
+ +
+ {formError && ( +
+ +

{formError}

+
+ )} + +
+ + setFormData({ ...formData, name: e.target.value })} + className="input" + placeholder="Coca-Cola 600ml" + required + /> +
+ +
+ + +
+ +
+
+ + setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} + className="input" + placeholder="0.00" + step="0.01" + min="0" + required + /> +
+
+ + setFormData({ ...formData, stock: parseInt(e.target.value) || 0 })} + className="input" + placeholder="0" + min="0" + required + /> +
+
+ +
+ + setFormData({ ...formData, barcode: e.target.value })} + className="input" + placeholder="7501055300051" + /> +
+ +
+ + setFormData({ ...formData, sku: e.target.value })} + className="input" + placeholder="COCA-600ML" + /> +
+ +
+ + setFormData({ ...formData, minStock: parseInt(e.target.value) || 0 })} + className="input" + placeholder="5" + min="0" + /> +
+ +
+ +