[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 <noreply@anthropic.com>
This commit is contained in:
parent
0385695d27
commit
8199d622b1
@ -1,14 +1,31 @@
|
|||||||
import { useState } from 'react';
|
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 = [
|
// Type definitions
|
||||||
{ id: '1', name: 'Coca-Cola 600ml', category: 'bebidas', price: 18.00, stock: 24, barcode: '7501055300051' },
|
interface Product {
|
||||||
{ id: '2', name: 'Sabritas Original', category: 'botanas', price: 15.00, stock: 12, barcode: '7501000111111' },
|
id: string;
|
||||||
{ id: '3', name: 'Leche Lala 1L', category: 'lacteos', price: 28.00, stock: 8, barcode: '7501020500001' },
|
name: string;
|
||||||
{ id: '4', name: 'Pan Bimbo Grande', category: 'panaderia', price: 45.00, stock: 5, barcode: '7501030400001' },
|
category: string;
|
||||||
{ id: '5', name: 'Fabuloso 1L', category: 'limpieza', price: 32.00, stock: 6, barcode: '7501040300001' },
|
price: number;
|
||||||
{ id: '6', name: 'Pepsi 600ml', category: 'bebidas', price: 17.00, stock: 20, barcode: '7501055300052' },
|
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 = [
|
const categories = [
|
||||||
{ id: 'all', name: 'Todos' },
|
{ id: 'all', name: 'Todos' },
|
||||||
@ -17,26 +34,162 @@ const categories = [
|
|||||||
{ id: 'lacteos', name: 'Lacteos' },
|
{ id: 'lacteos', name: 'Lacteos' },
|
||||||
{ id: 'panaderia', name: 'Panaderia' },
|
{ id: 'panaderia', name: 'Panaderia' },
|
||||||
{ id: 'limpieza', name: 'Limpieza' },
|
{ 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() {
|
export function Products() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [category, setCategory] = useState('all');
|
const [category, setCategory] = useState('all');
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||||||
|
const [deletingProduct, setDeletingProduct] = useState<Product | null>(null);
|
||||||
|
const [formData, setFormData] = useState<ProductFormData>(initialFormData);
|
||||||
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
|
||||||
const filteredProducts = mockProducts.filter((product) => {
|
const queryClient = useQueryClient();
|
||||||
const matchesSearch = product.name.toLowerCase().includes(search.toLowerCase());
|
|
||||||
const matchesCategory = category === 'all' || product.category === category;
|
// Fetch products from API
|
||||||
return matchesSearch && matchesCategory;
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Productos</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Productos</h1>
|
||||||
<p className="text-gray-500">{mockProducts.length} productos en catalogo</p>
|
<p className="text-gray-500">
|
||||||
|
{isLoading ? 'Cargando...' : `${products.length} productos en catalogo`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-primary flex items-center gap-2">
|
<button
|
||||||
|
onClick={handleOpenCreate}
|
||||||
|
className="btn-primary flex items-center gap-2"
|
||||||
|
>
|
||||||
<Plus className="h-5 w-5" />
|
<Plus className="h-5 w-5" />
|
||||||
Agregar Producto
|
Agregar Producto
|
||||||
</button>
|
</button>
|
||||||
@ -65,39 +218,294 @@ export function Products() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Products Grid */}
|
{/* Error State */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
{isError && (
|
||||||
{filteredProducts.map((product) => (
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-3">
|
||||||
<div key={product.id} className="card hover:shadow-md transition-shadow">
|
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div>
|
||||||
<div className="p-2 bg-gray-100 rounded-lg">
|
<h3 className="font-medium text-red-800">Error al cargar productos</h3>
|
||||||
<Package className="h-8 w-8 text-gray-600" />
|
<p className="text-red-600 text-sm">
|
||||||
</div>
|
{(error as any)?.response?.data?.message || 'No se pudieron cargar los productos. Intenta de nuevo.'}
|
||||||
<div className="flex gap-1">
|
</p>
|
||||||
<button className="p-1 hover:bg-gray-100 rounded">
|
|
||||||
<Edit className="h-4 w-4 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
<button className="p-1 hover:bg-red-100 rounded">
|
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 className="font-semibold text-gray-900">{product.name}</h3>
|
|
||||||
<p className="text-sm text-gray-500 capitalize">{product.category}</p>
|
|
||||||
<div className="mt-3 flex items-center justify-between">
|
|
||||||
<span className="text-xl font-bold text-primary-600">
|
|
||||||
${product.price.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
<span className={`text-sm font-medium ${
|
|
||||||
product.stock > 10 ? 'text-green-600' : product.stock > 5 ? 'text-yellow-600' : 'text-red-600'
|
|
||||||
}`}>
|
|
||||||
{product.stock} en stock
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-400 mt-2">{product.barcode}</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Loader2 className="h-8 w-8 text-primary-600 animate-spin mx-auto" />
|
||||||
|
<p className="text-gray-500 mt-2">Cargando productos...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{!isLoading && !isError && products.length === 0 && (
|
||||||
|
<div className="text-center py-12 card">
|
||||||
|
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">No hay productos</h3>
|
||||||
|
<p className="text-gray-500 mb-4">
|
||||||
|
{search || category !== 'all'
|
||||||
|
? 'No se encontraron productos con esos filtros'
|
||||||
|
: 'Agrega tu primer producto para empezar'}
|
||||||
|
</p>
|
||||||
|
{!search && category === 'all' && (
|
||||||
|
<button onClick={handleOpenCreate} className="btn-primary">
|
||||||
|
<Plus className="h-4 w-4 inline mr-2" />
|
||||||
|
Agregar Producto
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Products Grid */}
|
||||||
|
{!isLoading && !isError && products.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
|
{products.map((product) => (
|
||||||
|
<div key={product.id} className="card hover:shadow-md transition-shadow">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="p-2 bg-gray-100 rounded-lg">
|
||||||
|
<Package className="h-8 w-8 text-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenEdit(product)}
|
||||||
|
className="p-1 hover:bg-gray-100 rounded"
|
||||||
|
title="Editar producto"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeletingProduct(product)}
|
||||||
|
className="p-1 hover:bg-red-100 rounded"
|
||||||
|
title="Eliminar producto"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-red-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-gray-900">{product.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500 capitalize">{product.category}</p>
|
||||||
|
<div className="mt-3 flex items-center justify-between">
|
||||||
|
<span className="text-xl font-bold text-primary-600">
|
||||||
|
${Number(product.price).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span className={`text-sm font-medium ${
|
||||||
|
product.stock > 10 ? 'text-green-600' : product.stock > 5 ? 'text-yellow-600' : 'text-red-600'
|
||||||
|
}`}>
|
||||||
|
{product.stock} en stock
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{product.barcode && (
|
||||||
|
<p className="text-xs text-gray-400 mt-2">{product.barcode}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create/Edit Modal */}
|
||||||
|
{showModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="p-6 border-b flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-bold">
|
||||||
|
{editingProduct ? 'Editar Producto' : 'Nuevo Producto'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||||
|
{formError && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0" />
|
||||||
|
<p className="text-red-600 text-sm">{formError}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nombre *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
className="input"
|
||||||
|
placeholder="Coca-Cola 600ml"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Categoria *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
||||||
|
className="input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{categories.filter(c => c.id !== 'all').map((cat) => (
|
||||||
|
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Precio *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.price}
|
||||||
|
onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })}
|
||||||
|
className="input"
|
||||||
|
placeholder="0.00"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Stock *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.stock}
|
||||||
|
onChange={(e) => setFormData({ ...formData, stock: parseInt(e.target.value) || 0 })}
|
||||||
|
className="input"
|
||||||
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Codigo de Barras
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.barcode}
|
||||||
|
onChange={(e) => setFormData({ ...formData, barcode: e.target.value })}
|
||||||
|
className="input"
|
||||||
|
placeholder="7501055300051"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
SKU
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.sku}
|
||||||
|
onChange={(e) => setFormData({ ...formData, sku: e.target.value })}
|
||||||
|
className="input"
|
||||||
|
placeholder="COCA-600ML"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Stock Minimo (alerta)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.minStock}
|
||||||
|
onChange={(e) => setFormData({ ...formData, minStock: parseInt(e.target.value) || 0 })}
|
||||||
|
className="input"
|
||||||
|
placeholder="5"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Descripcion
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
className="input"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Descripcion opcional del producto..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="btn-secondary flex-1"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="btn-primary flex-1 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{isSubmitting && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||||
|
{isSubmitting
|
||||||
|
? 'Guardando...'
|
||||||
|
: editingProduct
|
||||||
|
? 'Guardar Cambios'
|
||||||
|
: 'Crear Producto'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
{deletingProduct && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-xl max-w-sm w-full p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
||||||
|
<Trash2 className="h-6 w-6 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
Eliminar Producto
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 mb-6">
|
||||||
|
Estas seguro de eliminar <strong>{deletingProduct.name}</strong>?
|
||||||
|
Esta accion no se puede deshacer.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeletingProduct(null)}
|
||||||
|
className="btn-secondary flex-1"
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{deleteMutation.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||||
|
{deleteMutation.isPending ? 'Eliminando...' : 'Eliminar'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user