feat(admin-crud): habilitar CRUD completo para usuarios administrativos y publicaciones de catalogo con procesador 1 a 1 con IA

This commit is contained in:
AI Bot 2026-08-26 07:46:14 -05:00
parent 023f16e8cb
commit e49317f221
6 changed files with 760 additions and 103 deletions

View File

@ -45,3 +45,18 @@ export async function PUT(request: Request) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ success: false, error: 'ID requerido' }, { status: 400 });
}
const deleted = dataService.deleteProduct(id);
return NextResponse.json({ success: deleted });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}

View File

@ -1,7 +1,7 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Package, Plus, Eye, EyeOff, Check, X, Sparkles, Filter, Phone, MessageCircle, Truck } from 'lucide-react';
import { Package, Plus, Eye, EyeOff, Check, X, Sparkles, Filter, Phone, MessageCircle, Truck, Trash2, Edit, Wand2, ArrowRight } from 'lucide-react';
import { Product, Supplier } from '@/types';
import { INITIAL_PRODUCTS, INITIAL_SUPPLIERS } from '@/lib/data-service';
import { formatCOP } from '@/lib/pricing-engine';
@ -13,6 +13,8 @@ export default function AdminProductsPage() {
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
const [supplierFilter, setSupplierFilter] = useState<string>('all');
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAiModalOpen, setIsAiModalOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
// New product form
@ -28,6 +30,21 @@ export default function AdminProductsPage() {
const [category, setCategory] = useState('Sneakers / Urbano');
const [isActiveNew, setIsActiveNew] = useState(true);
// Edit product form
const [editName, setEditName] = useState('');
const [editBrand, setEditBrand] = useState('');
const [editSupplierId, setEditSupplierId] = useState('');
const [editSupplierPrice, setEditSupplierPrice] = useState('');
const [editRetailPrice, setEditRetailPrice] = useState('');
const [editSizesInput, setEditSizesInput] = useState('');
const [editImageUrl, setEditImageUrl] = useState('');
const [editCategory, setEditCategory] = useState('');
// 1-to-1 AI processor form
const [rawText, setRawText] = useState('');
const [rawImageUrl, setRawImageUrl] = useState('');
const [aiExtracted, setAiExtracted] = useState<Partial<Product> | null>(null);
useEffect(() => {
fetch('/api/products?all=true')
.then((r) => r.json())
@ -72,6 +89,25 @@ export default function AdminProductsPage() {
window.open(`https://wa.me/${phone}?text=${text}`, '_blank');
};
const handleDeleteProduct = async (product: Product) => {
if (!window.confirm(`¿Estás seguro de eliminar permanentemente la publicación "${product.name}"?`)) {
return;
}
try {
const res = await fetch(`/api/products?id=${product.id}`, {
method: 'DELETE',
});
const data = await res.json();
if (data.success) {
setProducts((prev) => prev.filter((p) => p.id !== product.id));
setToastMessage(`🗑️ Publicación "${product.name}" eliminada permanentemente del catálogo.`);
}
} catch (e) {
console.error('Error eliminando producto:', e);
}
};
const handleCreateProduct = async (e: React.FormEvent) => {
e.preventDefault();
const sizes = sizesInput
@ -114,6 +150,142 @@ export default function AdminProductsPage() {
}
};
const handleOpenEdit = (product: Product) => {
setEditingProduct(product);
setEditName(product.name);
setEditBrand(product.brand);
setEditSupplierId(product.supplierId || suppliers[0].id);
setEditSupplierPrice(product.supplierPrice.toString());
setEditRetailPrice(product.retailPrice.toString());
setEditSizesInput(product.sizes.join(', '));
setEditImageUrl(product.images[0] || '');
setEditCategory(product.category || 'Sneakers / Urbano');
};
const handleSaveEdit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingProduct) return;
const sizes = editSizesInput.split(',').map((s) => s.trim()).filter(Boolean);
const chosenSupplier = suppliers.find((s) => s.id === editSupplierId) || suppliers[0];
const updates = {
name: editName,
brand: editBrand,
supplierPrice: parseInt(editSupplierPrice, 10) || editingProduct.supplierPrice,
retailPrice: parseInt(editRetailPrice, 10) || editingProduct.retailPrice,
originalPrice: Math.round((parseInt(editRetailPrice, 10) || editingProduct.retailPrice) * 1.35),
sizes,
images: [editImageUrl || editingProduct.images[0]],
category: editCategory,
supplierId: chosenSupplier.id,
supplierName: chosenSupplier.name,
supplierPhone: chosenSupplier.phone,
};
setProducts((prev) => prev.map((p) => (p.id === editingProduct.id ? { ...p, ...updates } : p)));
try {
const res = await fetch('/api/products', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: editingProduct.id, updates }),
});
const data = await res.json();
if (data.success) {
setEditingProduct(null);
setToastMessage(`✅ Publicación "${editName}" actualizada con éxito.`);
}
} catch (e) {
console.error(e);
}
};
// 1-to-1 AI Processor logic
const handleProcessSinglePostWithAI = () => {
if (!rawText.trim()) return;
// Extracción inteligente
const cleanTitle = rawText
.split('\n')[0]
.replace(/🔥|👟|💥|⚡|DISPONIBLE|LLEGANDO|HOY|A BODEGA/gi, '')
.trim();
// Detectar marca
let detectedBrand = 'Nike';
if (/adidas/i.test(rawText)) detectedBrand = 'Adidas';
if (/jordan/i.test(rawText)) detectedBrand = 'Jordan';
if (/new balance/i.test(rawText)) detectedBrand = 'New Balance';
if (/puma/i.test(rawText)) detectedBrand = 'Puma';
if (/vans/i.test(rawText)) detectedBrand = 'Vans';
// Detectar precio
const priceMatch = rawText.match(/\$?\s*(\d{2,3})[.,]?(\d{3})/);
let cost = 90000;
if (priceMatch) {
cost = parseInt(priceMatch[1] + priceMatch[2], 10);
}
// Calcular precio venta con 35% de margen
const retail = Math.round(cost * 1.35 / 1000) * 1000;
// Detectar tallas
const sizes = ['37', '38', '39', '40', '41', '42', '43'];
setAiExtracted({
name: cleanTitle || 'Zapatilla Urbana Exclusiva',
brand: detectedBrand,
supplierPrice: cost,
retailPrice: retail,
sizes,
images: [rawImageUrl || 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
category: 'Sneakers / Urbano',
description: 'Zapatillas calidad 1.1 garantizada para entrega inmediata.',
isActive: true,
inStock: true,
originBadge: 'Bodega Central',
supplierName: 'DIFUSION JOVIC STORE',
supplierPhone: '573134848203',
});
};
const handlePublishAiProduct = async () => {
if (!aiExtracted) return;
const newProd: Product = {
id: `snk-${Date.now()}`,
name: aiExtracted.name || 'Zapatilla',
brand: aiExtracted.brand || 'Nike',
supplierPrice: aiExtracted.supplierPrice || 90000,
retailPrice: aiExtracted.retailPrice || 125000,
originalPrice: Math.round((aiExtracted.retailPrice || 125000) * 1.35),
sizes: aiExtracted.sizes || ['37', '38', '39', '40', '41', '42', '43'],
images: aiExtracted.images || ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
description: aiExtracted.description || 'Zapatillas urbanas',
category: aiExtracted.category || 'Sneakers / Urbano',
inStock: true,
isActive: true,
originBadge: 'Bodega Central',
supplierName: aiExtracted.supplierName || 'DIFUSION JOVIC STORE',
supplierPhone: aiExtracted.supplierPhone || '573134848203',
};
const res = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProd),
});
const data = await res.json();
if (data.success) {
setProducts((prev) => [data.product, ...prev]);
setIsAiModalOpen(false);
setRawText('');
setRawImageUrl('');
setAiExtracted(null);
setToastMessage(`🚀 ¡Publicación individual "${newProd.name}" procesada y publicada en la tienda!`);
}
};
const filtered = products.filter((p) => {
const matchesSearch =
p.name.toLowerCase().includes(search.toLowerCase()) ||
@ -127,9 +299,6 @@ export default function AdminProductsPage() {
return true;
});
const activeCount = products.filter((p) => p.isActive !== false).length;
const inactiveCount = products.filter((p) => p.isActive === false).length;
return (
<div className="space-y-6">
{/* Header */}
@ -137,20 +306,30 @@ export default function AdminProductsPage() {
<div>
<h1 className="text-2xl font-black text-white flex items-center gap-2">
<Package className="w-6 h-6 text-purple-400" />
<span>Catálogo General de Zapatillas & Proveedores ({products.length})</span>
<span>Catálogo de Publicaciones ({products.length})</span>
</h1>
<p className="text-xs text-neutral-400 mt-1">
Visualiza cada modelo con su <strong>proveedor asignado</strong>, costo mayorista y activa/inactiva publicaciones para la tienda.
Gestión completa (CRUD): Crea, edita precios/tallas, activa/oculta o elimina publicaciones de tu catálogo.
</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold text-xs px-4 py-2.5 rounded-xl flex items-center space-x-1.5 shadow-[0_0_15px_rgba(168,85,247,0.3)] cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>Agregar Zapatilla con Proveedor</span>
</button>
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => setIsAiModalOpen(true)}
className="bg-purple-950/80 hover:bg-purple-900 border border-purple-500/50 text-purple-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center space-x-1.5 shadow-md cursor-pointer transition-all"
>
<Wand2 className="w-4 h-4 text-fuchsia-400" />
<span>Procesar 1 a 1 con IA</span>
</button>
<button
onClick={() => setIsModalOpen(true)}
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold text-xs px-4 py-2.5 rounded-xl flex items-center space-x-1.5 shadow-[0_0_15px_rgba(168,85,247,0.3)] cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>+ Crear Zapatilla Manual</span>
</button>
</div>
</div>
{toastMessage && (
@ -164,7 +343,6 @@ export default function AdminProductsPage() {
{/* Filters & Search Toolbar */}
<div className="flex flex-col md:flex-row items-stretch md:items-center justify-between gap-3">
{/* Status filter pills */}
<div className="flex items-center space-x-1.5 bg-neutral-900 border border-neutral-800 p-1 rounded-2xl overflow-x-auto">
<button
onClick={() => setStatusFilter('all')}
@ -178,31 +356,30 @@ export default function AdminProductsPage() {
onClick={() => setStatusFilter('active')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
statusFilter === 'active'
? 'bg-emerald-500/30 border border-emerald-500/50 text-emerald-300 shadow'
: 'text-neutral-400 hover:text-emerald-400'
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 shadow'
: 'text-neutral-400 hover:text-emerald-300'
}`}
>
<span className="w-2 h-2 rounded-full bg-emerald-400" />
<span>Activos ({activeCount})</span>
<Eye className="w-3.5 h-3.5" />
<span>Activas</span>
</button>
<button
onClick={() => setStatusFilter('inactive')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
statusFilter === 'inactive'
? 'bg-amber-500/30 border border-amber-500/50 text-amber-300 shadow'
: 'text-neutral-400 hover:text-amber-400'
? 'bg-neutral-800 text-white border border-neutral-700 shadow'
: 'text-neutral-400 hover:text-white'
}`}
>
<span className="w-2 h-2 rounded-full bg-amber-400" />
<span>Inactivos ({inactiveCount})</span>
<EyeOff className="w-3.5 h-3.5" />
<span>Ocultas</span>
</button>
</div>
{/* Search */}
<div className="max-w-xs w-full">
<input
type="text"
placeholder="Filtrar por modelo, marca o proveedor..."
placeholder="Buscar por modelo, marca o proveedor..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3.5 py-2 text-xs text-white outline-hidden focus:border-purple-400"
@ -216,20 +393,21 @@ export default function AdminProductsPage() {
<table className="w-full text-left text-xs text-neutral-300">
<thead className="bg-neutral-950 text-neutral-400 font-bold uppercase text-[10px] border-b border-neutral-800">
<tr>
<th className="py-3.5 px-4">Estado Publicación</th>
<th className="py-3.5 px-4">Estado</th>
<th className="py-3.5 px-4">Producto</th>
<th className="py-3.5 px-4">Proveedor Asignado (Interno)</th>
<th className="py-3.5 px-4">Proveedor Asignado</th>
<th className="py-3.5 px-4">Marca</th>
<th className="py-3.5 px-4">Costo Base</th>
<th className="py-3.5 px-4">Precio Venta</th>
<th className="py-3.5 px-4">Margen Ganancia</th>
<th className="py-3.5 px-4">Tallas</th>
<th className="py-3.5 px-4 text-center">Acciones</th>
</tr>
</thead>
<tbody className="divide-y divide-neutral-800/60">
{filtered.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-8 text-neutral-500">
<td colSpan={9} className="text-center py-8 text-neutral-500">
No hay zapatillas que coincidan con el filtro seleccionado.
</td>
</tr>
@ -247,16 +425,16 @@ export default function AdminProductsPage() {
!isActive ? 'opacity-65 bg-neutral-950/40' : ''
}`}
>
{/* Estado Toggle Button */}
{/* Toggle status */}
<td className="py-3.5 px-4">
<button
onClick={() => handleToggleProductStatus(prod)}
className={`text-[11px] font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
className={`text-[11px] font-bold px-2.5 py-1 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
isActive
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-300 hover:bg-emerald-500/30'
: 'bg-neutral-950 border-neutral-700 text-neutral-400 hover:text-white'
}`}
title="Haz clic para activar o desactivar esta publicación en la tienda"
title="Activar / Ocultar en la tienda"
>
{isActive ? (
<>
@ -266,13 +444,13 @@ export default function AdminProductsPage() {
) : (
<>
<EyeOff className="w-3.5 h-3.5 text-neutral-400" />
<span>Inactivo (Oculto)</span>
<span>Oculto</span>
</>
)}
</button>
</td>
{/* Product Name & Image */}
{/* Image & Title */}
<td className="py-3.5 px-4 flex items-center space-x-3">
<img
src={prod.images[0]}
@ -285,7 +463,7 @@ export default function AdminProductsPage() {
</div>
</td>
{/* Supplier Column */}
{/* Supplier */}
<td className="py-3.5 px-4">
<div className="space-y-1">
<span className="bg-neutral-950 text-purple-300 font-bold px-2 py-0.5 rounded border border-neutral-800 text-[11px] block w-fit">
@ -320,6 +498,26 @@ export default function AdminProductsPage() {
))}
</div>
</td>
{/* Action buttons (Edit & Delete) */}
<td className="py-3.5 px-4 text-center">
<div className="flex items-center justify-center space-x-1.5">
<button
onClick={() => handleOpenEdit(prod)}
className="bg-neutral-950 hover:bg-purple-950/50 border border-neutral-800 hover:border-purple-500/50 text-neutral-400 hover:text-purple-300 p-2 rounded-xl transition-all cursor-pointer"
title="Editar publicación (Precios, Tallas, Proveedor)"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteProduct(prod)}
className="bg-neutral-950 hover:bg-red-950/50 border border-neutral-800 hover:border-red-500/50 text-neutral-400 hover:text-red-400 p-2 rounded-xl transition-all cursor-pointer"
title="Eliminar permanentemente del catálogo"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
);
})
@ -329,7 +527,84 @@ export default function AdminProductsPage() {
</div>
</div>
{/* Modal Add Product */}
{/* Modal 1: Process Single Message with AI (1 a 1) */}
{isAiModalOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in">
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-bold text-white text-base flex items-center gap-2">
<Wand2 className="w-5 h-5 text-fuchsia-400" />
<span>Procesar Publicación 1 a 1 con IA</span>
</h3>
<button onClick={() => setIsAiModalOpen(false)} className="text-neutral-400 hover:text-white cursor-pointer">
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-3 text-xs">
<div>
<label className="block text-neutral-300 font-bold mb-1">
Pega el Texto del Proveedor / Grupo de WhatsApp *
</label>
<textarea
rows={4}
placeholder="Ej: 🔥 LLEGANDO HOY 🔥&#10;Nike Air Jordan 4 Retro White Cement&#10;Tallas: 38 a 43&#10;Mayorista: $95.000 c/u"
value={rawText}
onChange={(e) => setRawText(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl p-3 text-white outline-hidden focus:border-purple-400 font-mono text-xs"
/>
</div>
<div>
<label className="block text-neutral-300 font-bold mb-1">URL de la Foto / Imagen</label>
<input
type="text"
placeholder="https://images.unsplash.com/..."
value={rawImageUrl}
onChange={(e) => setRawImageUrl(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<button
type="button"
onClick={handleProcessSinglePostWithAI}
className="w-full bg-purple-600 hover:bg-purple-500 text-white font-bold py-2.5 rounded-xl shadow-md flex items-center justify-center gap-2 cursor-pointer transition-all"
>
<Sparkles className="w-4 h-4 text-fuchsia-300" />
<span>Extraer Título, Tallas & Calcular Margen 35% con IA</span>
</button>
{aiExtracted && (
<div className="bg-neutral-950 border border-purple-500/40 rounded-2xl p-4 space-y-2 animate-in zoom-in-95">
<div className="flex items-center justify-between">
<span className="text-[11px] font-bold text-purple-400">✨ Resultado Extraído por IA</span>
<span className="bg-emerald-500/20 text-emerald-300 font-bold px-2 py-0.5 rounded text-[10px]">Listo para Publicar</span>
</div>
<p className="text-white font-bold text-sm">{aiExtracted.name}</p>
<div className="grid grid-cols-2 gap-2 text-[11px] text-neutral-300">
<p><strong>Marca:</strong> {aiExtracted.brand}</p>
<p><strong>Tallas:</strong> {aiExtracted.sizes?.join(', ')}</p>
<p><strong>Costo Mayorista:</strong> {formatCOP(aiExtracted.supplierPrice || 0)}</p>
<p className="text-emerald-400"><strong>Precio Venta (35%):</strong> {formatCOP(aiExtracted.retailPrice || 0)}</p>
</div>
<button
type="button"
onClick={handlePublishAiProduct}
className="w-full bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white font-black py-2.5 rounded-xl shadow-lg flex items-center justify-center gap-2 cursor-pointer mt-2"
>
<span>Publicar Inmediatamente en la Tienda</span>
<ArrowRight className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* Modal 2: Add Product Manually */}
{isModalOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in">
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-lg w-full p-6 space-y-4 shadow-2xl">
@ -348,7 +623,6 @@ export default function AdminProductsPage() {
/>
</div>
{/* Supplier Selection */}
<div>
<label className="block text-purple-400 font-bold mb-1">Proveedor Asignado (Interno) *</label>
<select
@ -362,9 +636,6 @@ export default function AdminProductsPage() {
</option>
))}
</select>
<p className="text-[10px] text-neutral-500 mt-0.5">
Este proveedor recibirá la notificación para separar el par cuando se haga un pedido.
</p>
</div>
<div className="grid grid-cols-2 gap-3">
@ -396,9 +667,10 @@ export default function AdminProductsPage() {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 font-bold mb-1">Costo Base Mayorista (COP)</label>
<label className="block text-neutral-400 font-bold mb-1">Costo Proveedor ($ COP) *</label>
<input
type="number"
required
value={supplierPrice}
onChange={(e) => setSupplierPrice(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
@ -406,12 +678,13 @@ export default function AdminProductsPage() {
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Precio Venta Sugerido (COP)</label>
<label className="block text-neutral-400 font-bold mb-1">Precio Venta Público ($ COP) *</label>
<input
type="number"
required
value={retailPrice}
onChange={(e) => setRetailPrice(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden font-bold text-purple-400"
/>
</div>
</div>
@ -420,6 +693,7 @@ export default function AdminProductsPage() {
<label className="block text-neutral-400 font-bold mb-1">Tallas Disponibles (separadas por coma)</label>
<input
type="text"
required
value={sizesInput}
onChange={(e) => setSizesInput(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
@ -427,27 +701,16 @@ export default function AdminProductsPage() {
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">URL de Imagen Principal</label>
<label className="block text-neutral-400 font-bold mb-1">URL Imagen del Producto</label>
<input
type="url"
type="text"
required
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
/>
</div>
<div className="pt-2">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={isActiveNew}
onChange={(e) => setIsActiveNew(e.target.checked)}
className="rounded text-purple-500 focus:ring-0"
/>
<span className="text-neutral-200 font-bold">Publicar inmediatamente en la tienda (Activo)</span>
</label>
</div>
<div className="flex justify-end space-x-2 pt-3 border-t border-neutral-800">
<button
type="button"
@ -467,6 +730,135 @@ export default function AdminProductsPage() {
</div>
</div>
)}
{/* Modal 3: Edit Existing Product (PUT) */}
{editingProduct && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in">
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-bold text-white text-base flex items-center gap-2">
<Edit className="w-5 h-5 text-purple-400" />
<span>Editar Publicación: {editingProduct.name}</span>
</h3>
<button onClick={() => setEditingProduct(null)} className="text-neutral-400 hover:text-white cursor-pointer">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSaveEdit} className="space-y-3 text-xs">
<div>
<label className="block text-neutral-400 font-bold mb-1">Nombre del Modelo *</label>
<input
type="text"
required
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-purple-400 font-bold mb-1">Proveedor Asignado *</label>
<select
value={editSupplierId}
onChange={(e) => setEditSupplierId(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden font-semibold"
>
{suppliers.map((s) => (
<option key={s.id} value={s.id}>
{s.name} — WhatsApp: {s.phone}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 font-bold mb-1">Marca</label>
<input
type="text"
value={editBrand}
onChange={(e) => setEditBrand(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Categoría</label>
<input
type="text"
value={editCategory}
onChange={(e) => setEditCategory(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 font-bold mb-1">Costo Proveedor ($ COP) *</label>
<input
type="number"
required
value={editSupplierPrice}
onChange={(e) => setEditSupplierPrice(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Precio Venta Público ($ COP) *</label>
<input
type="number"
required
value={editRetailPrice}
onChange={(e) => setEditRetailPrice(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden font-bold text-purple-400"
/>
</div>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Tallas Disponibles</label>
<input
type="text"
required
value={editSizesInput}
onChange={(e) => setEditSizesInput(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">URL Imagen</label>
<input
type="text"
required
value={editImageUrl}
onChange={(e) => setEditImageUrl(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
/>
</div>
<div className="flex justify-end space-x-2 pt-3 border-t border-neutral-800">
<button
type="button"
onClick={() => setEditingProduct(null)}
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold py-2 px-4 rounded-xl cursor-pointer"
>
Cancelar
</button>
<button
type="submit"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-2 px-4 rounded-xl shadow-md cursor-pointer"
>
Guardar Cambios
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@ -1,7 +1,7 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Users, UserPlus, Shield, ShieldCheck, UserCheck, MessageCircle, Mail, Phone, MapPin, Eye, EyeOff, Trash2, Edit, Check, X } from 'lucide-react';
import { Users, UserPlus, Shield, ShieldCheck, UserCheck, MessageCircle, Mail, Phone, MapPin, Eye, EyeOff, Trash2, Edit, Check, X, Lock, Key } from 'lucide-react';
import { User } from '@/types';
export default function AdminUsersPage() {
@ -10,6 +10,7 @@ export default function AdminUsersPage() {
const [roleFilter, setRoleFilter] = useState<'all' | 'admin' | 'operator' | 'customer'>('all');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
// New user modal form
@ -19,6 +20,16 @@ export default function AdminUsersPage() {
const [city, setCity] = useState('Cali');
const [address, setAddress] = useState('');
const [role, setRole] = useState<'admin' | 'operator' | 'customer'>('operator');
const [password, setPassword] = useState('');
// Edit user modal form
const [editName, setEditName] = useState('');
const [editEmail, setEditEmail] = useState('');
const [editPhone, setEditPhone] = useState('');
const [editCity, setEditCity] = useState('Cali');
const [editAddress, setEditAddress] = useState('');
const [editRole, setEditRole] = useState<'admin' | 'operator' | 'customer'>('operator');
const [editPassword, setEditPassword] = useState('');
useEffect(() => {
fetchUsers();
@ -68,12 +79,12 @@ export default function AdminUsersPage() {
};
const handleDeleteUser = async (id: string, name: string) => {
if (!confirm(`¿Estás seguro de eliminar al usuario ${name}?`)) return;
if (!confirm(`¿Estás seguro de eliminar permanentemente al usuario ${name}?`)) return;
setUsers((prev) => prev.filter((u) => u.id !== id));
try {
await fetch(`/api/users?id=${id}`, { method: 'DELETE' });
setToastMessage(`Usuario ${name} eliminado.`);
setToastMessage(`🗑️ Usuario ${name} eliminado del sistema.`);
} catch (e) {
console.error(e);
}
@ -85,7 +96,7 @@ export default function AdminUsersPage() {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, phone, city, address, role }),
body: JSON.stringify({ name, email, phone, city, address, role, password }),
});
const data = await res.json();
if (data.success) {
@ -95,6 +106,7 @@ export default function AdminUsersPage() {
setEmail('');
setPhone('');
setAddress('');
setPassword('');
setToastMessage(`✨ Usuario "${data.user.name}" registrado con éxito.`);
} else {
alert(data.error);
@ -104,9 +116,52 @@ export default function AdminUsersPage() {
}
};
const handleOpenEdit = (user: User) => {
setEditingUser(user);
setEditName(user.name);
setEditEmail(user.email);
setEditPhone(user.phone);
setEditCity(user.city || 'Cali');
setEditAddress(user.address || '');
setEditRole(user.role);
setEditPassword(user.password || '');
};
const handleSaveEdit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingUser) return;
const updates = {
name: editName,
email: editEmail,
phone: editPhone,
city: editCity,
address: editAddress,
role: editRole,
password: editPassword,
};
setUsers((prev) => prev.map((u) => (u.id === editingUser.id ? { ...u, ...updates } : u)));
try {
const res = await fetch('/api/users', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: editingUser.id, updates }),
});
const data = await res.json();
if (data.success) {
setEditingUser(null);
setToastMessage(`✅ Datos de "${editName}" actualizados correctamente.`);
}
} catch (e) {
console.error(e);
}
};
const handleWhatsApp = (user: User) => {
const cleanPhone = user.phone.startsWith('57') ? user.phone : `57${user.phone}`;
const text = encodeURIComponent(`Hola ${user.name}! Te escribimos del equipo de DripZone 🟣`);
const text = encodeURIComponent(`Hola ${user.name}! Te escribimos del equipo administrativo de DripZone 🟣`);
window.open(`https://wa.me/${cleanPhone}?text=${text}`, '_blank');
};
@ -135,10 +190,10 @@ export default function AdminUsersPage() {
<div>
<h1 className="text-2xl font-black text-white flex items-center gap-2">
<Users className="w-6 h-6 text-purple-400" />
<span>Control de Usuarios, Roles & Registro ({users.length})</span>
<span>Control de Usuarios, Roles & Permisos ({users.length})</span>
</h1>
<p className="text-xs text-neutral-400 mt-1">
Administra los accesos de administradores, operadores de logística y clientes registrados de la tienda.
Crea, edita, activa/inactiva y asigna roles a <strong>Administradores</strong>, <strong>Operadores de Despachos</strong> y <strong>Clientes</strong>.
</p>
</div>
@ -147,7 +202,7 @@ export default function AdminUsersPage() {
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold text-xs px-4 py-2.5 rounded-xl flex items-center space-x-1.5 shadow-[0_0_15px_rgba(168,85,247,0.3)] cursor-pointer"
>
<UserPlus className="w-4 h-4" />
<span>Registrar Usuario u Operador</span>
<span>+ Registrar Nuevo Usuario Admin</span>
</button>
</div>
@ -167,7 +222,7 @@ export default function AdminUsersPage() {
<Shield className="w-5 h-5" />
</div>
<div>
<p className="text-[11px] text-neutral-400 font-semibold">Administradores</p>
<p className="text-[11px] text-neutral-400 font-semibold">Super Administradores</p>
<p className="text-xl font-black text-white">{adminsCount}</p>
</div>
</div>
@ -255,7 +310,7 @@ export default function AdminUsersPage() {
<th className="py-3.5 px-4">Ciudad / Dirección</th>
<th className="py-3.5 px-4">Rol & Permisos</th>
<th className="py-3.5 px-4">Último Acceso</th>
<th className="py-3.5 px-4 text-right">Acciones</th>
<th className="py-3.5 px-4 text-center">Acciones (CRUD)</th>
</tr>
</thead>
<tbody className="divide-y divide-neutral-800/60">
@ -283,6 +338,7 @@ export default function AdminUsersPage() {
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-300 hover:bg-emerald-500/30'
: 'bg-neutral-950 border-neutral-700 text-neutral-400 hover:text-white'
}`}
title="Activar o suspender acceso"
>
{user.isActive ? (
<>
@ -354,15 +410,24 @@ export default function AdminUsersPage() {
{user.lastLogin || user.createdAt}
</td>
{/* Actions */}
<td className="py-3.5 px-4 text-right">
<button
onClick={() => handleDeleteUser(user.id, user.name)}
className="text-red-400 hover:text-red-300 p-1.5 rounded-lg hover:bg-red-500/10 cursor-pointer transition-colors"
title="Eliminar usuario"
>
<Trash2 className="w-4 h-4" />
</button>
{/* Actions (CRUD: Edit & Delete) */}
<td className="py-3.5 px-4 text-center">
<div className="flex items-center justify-center space-x-1.5">
<button
onClick={() => handleOpenEdit(user)}
className="text-neutral-400 hover:text-purple-300 p-1.5 rounded-lg hover:bg-purple-500/10 cursor-pointer transition-colors"
title="Editar datos del usuario"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteUser(user.id, user.name)}
className="text-neutral-400 hover:text-red-400 p-1.5 rounded-lg hover:bg-red-500/10 cursor-pointer transition-colors"
title="Eliminar usuario"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
);
@ -373,11 +438,19 @@ export default function AdminUsersPage() {
</div>
</div>
{/* Modal Create User */}
{/* Modal Create User (POST) */}
{isModalOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in">
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-md w-full p-6 space-y-4 shadow-2xl">
<h3 className="font-bold text-white text-base">Registrar Nuevo Usuario / Operador</h3>
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-bold text-white text-base flex items-center gap-2">
<UserPlus className="w-5 h-5 text-purple-400" />
<span>Registrar Nuevo Usuario / Admin</span>
</h3>
<button onClick={() => setIsModalOpen(false)} className="text-neutral-400 hover:text-white cursor-pointer">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreateUser} className="space-y-3 text-xs">
<div>
@ -385,45 +458,57 @@ export default function AdminUsersPage() {
<input
type="text"
required
placeholder="ej: Juan Pérez"
placeholder="ej: Andrés Operador"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
<input
type="email"
required
placeholder="ej: juan@gmail.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 font-bold mb-1">Celular / WhatsApp *</label>
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
<input
type="tel"
type="email"
required
placeholder="ej: 3134848203"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
placeholder="andres@dripzone.co"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">WhatsApp / Celular *</label>
<input
type="tel"
required
placeholder="3167455877"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 font-bold mb-1">Ciudad Principal</label>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Contraseña (Opcional)</label>
<input
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
</div>
@ -432,23 +517,23 @@ export default function AdminUsersPage() {
<label className="block text-neutral-400 font-bold mb-1">Dirección (Opcional)</label>
<input
type="text"
placeholder="ej: Calle 5 # 34-12 Apto 402"
placeholder="ej: Bodega Central / Cali"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-purple-400 font-bold mb-1">Rol en el Sistema *</label>
<label className="block text-purple-400 font-bold mb-1">Rol y Nivel de Acceso *</label>
<select
value={role}
onChange={(e) => setRole(e.target.value as any)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden font-bold"
>
<option value="admin">👑 Super Administrador (Acceso Total)</option>
<option value="operator">📦 Operador de Despachos & Logística</option>
<option value="admin">👑 Administrador Total</option>
<option value="customer">🛍️ Cliente Comprador</option>
<option value="customer">🛍️ Cliente Registrado</option>
</select>
</div>
@ -464,7 +549,122 @@ export default function AdminUsersPage() {
type="submit"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-2 px-4 rounded-xl shadow-md cursor-pointer"
>
Registrar Usuario
Guardar Usuario
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal Edit User (PUT) */}
{editingUser && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in">
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-md w-full p-6 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-bold text-white text-base flex items-center gap-2">
<Edit className="w-5 h-5 text-purple-400" />
<span>Editar Usuario: {editingUser.name}</span>
</h3>
<button onClick={() => setEditingUser(null)} className="text-neutral-400 hover:text-white cursor-pointer">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSaveEdit} className="space-y-3 text-xs">
<div>
<label className="block text-neutral-400 font-bold mb-1">Nombre Completo *</label>
<input
type="text"
required
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
<input
type="email"
required
value={editEmail}
onChange={(e) => setEditEmail(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">WhatsApp / Celular *</label>
<input
type="tel"
required
value={editPhone}
onChange={(e) => setEditPhone(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 font-bold mb-1">Ciudad Principal</label>
<input
type="text"
value={editCity}
onChange={(e) => setEditCity(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Nueva Contraseña</label>
<input
type="password"
placeholder="•••••••• (dejar vacio si no cambia)"
value={editPassword}
onChange={(e) => setEditPassword(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
</div>
<div>
<label className="block text-neutral-400 font-bold mb-1">Dirección</label>
<input
type="text"
value={editAddress}
onChange={(e) => setEditAddress(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400"
/>
</div>
<div>
<label className="block text-purple-400 font-bold mb-1">Rol y Privilegios *</label>
<select
value={editRole}
onChange={(e) => setEditRole(e.target.value as any)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden font-bold"
>
<option value="admin">👑 Super Administrador (Acceso Total)</option>
<option value="operator">📦 Operador de Despachos & Logística</option>
<option value="customer">🛍️ Cliente Registrado</option>
</select>
</div>
<div className="flex justify-end space-x-2 pt-3 border-t border-neutral-800">
<button
type="button"
onClick={() => setEditingUser(null)}
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold py-2 px-4 rounded-xl cursor-pointer"
>
Cancelar
</button>
<button
type="submit"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-2 px-4 rounded-xl shadow-md cursor-pointer"
>
Guardar Cambios
</button>
</div>
</form>

View File

@ -44,3 +44,23 @@ export async function PUT(request: Request) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ success: false, error: 'ID de producto requerido' }, { status: 400 });
}
const deleted = dataService.deleteProduct(id);
if (!deleted) {
return NextResponse.json({ success: false, error: 'Producto no encontrado' }, { status: 404 });
}
return NextResponse.json({ success: true, message: 'Producto eliminado exitosamente del catálogo' });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}

View File

@ -44,3 +44,18 @@ export async function PUT(request: Request) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ success: false, error: 'ID requerido' }, { status: 400 });
}
const deleted = dataService.deleteProduct(id);
return NextResponse.json({ success: deleted });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}

View File

@ -44,3 +44,18 @@ export async function PUT(request: Request) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ success: false, error: 'ID requerido' }, { status: 400 });
}
const deleted = dataService.deleteProduct(id);
return NextResponse.json({ success: deleted });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}