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:
parent
023f16e8cb
commit
e49317f221
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 🔥 Nike Air Jordan 4 Retro White Cement Tallas: 38 a 43 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user