ux: menú lateral más descriptivo y PricingManager muestra productos del rubro con precios

This commit is contained in:
2026-02-21 20:10:40 -03:00
parent e21028ee9f
commit a9ad545fbb
2 changed files with 131 additions and 66 deletions

View File

@@ -49,20 +49,20 @@ export default function ProtectedLayout() {
{
title: "Gestión Diaria",
items: [
{ label: 'Dashboard', href: '/', icon: <LayoutDashboard size={18} />, roles: ['Admin', 'Cajero'] },
{ label: 'Moderación', href: '/moderation', icon: <Eye size={18} />, roles: ['Admin', 'Moderador'], badge: unreadCount },
{ label: 'Explorador', href: '/listings', icon: <Search size={18} />, roles: ['Admin', 'Cajero', 'Moderador'] },
{ label: 'Inicio', href: '/', icon: <LayoutDashboard size={18} />, roles: ['Admin', 'Cajero'] },
{ label: 'Moderación de Avisos', href: '/moderation', icon: <Eye size={18} />, roles: ['Admin', 'Moderador'], badge: unreadCount },
{ label: 'Buscar Publicaciones', href: '/listings', icon: <Search size={18} />, roles: ['Admin', 'Cajero', 'Moderador'] },
{ label: 'Clientes', href: '/clients', icon: <ClientIcon size={18} />, roles: ['Admin', 'Cajero'] },
]
},
{
title: "Comercial & Catálogo",
title: "Catálogo & Precios",
items: [
{ label: 'Productos', href: '/products', icon: <Box size={18} />, roles: ['Admin'] },
{ label: 'Tarifas', href: '/pricing', icon: <DollarSign size={18} />, roles: ['Admin'] },
{ label: 'Productos & Tarifas', href: '/products', icon: <Box size={18} />, roles: ['Admin'] },
{ label: 'Reglas por Rubro', href: '/pricing', icon: <DollarSign size={18} />, roles: ['Admin'] },
{ label: 'Promociones', href: '/promotions', icon: <Tag size={18} />, roles: ['Admin'] },
{ label: 'Cupones', href: '/coupons', icon: <Tag size={18} />, roles: ['Admin'] },
{ label: 'Categorías', href: '/categories', icon: <FolderTree size={18} />, roles: ['Admin'] },
{ label: 'Cupones de Descuento', href: '/coupons', icon: <Tag size={18} />, roles: ['Admin'] },
{ label: 'Rubros & Categorías', href: '/categories', icon: <FolderTree size={18} />, roles: ['Admin'] },
]
},
{
@@ -70,16 +70,16 @@ export default function ProtectedLayout() {
items: [
{ label: 'Empresas', href: '/companies', icon: <Building2 size={18} />, roles: ['Admin'] },
{ label: 'Riesgo Crediticio', href: '/finance/credit', icon: <ShieldAlert size={18} />, roles: ['Admin', 'Gerente'] },
{ label: 'Liquidación', href: '/reports/settlement', icon: <ArrowRightLeft size={18} />, roles: ['Admin', 'Contador'] },
{ label: 'Liquidación de Cuentas', href: '/reports/settlement', icon: <ArrowRightLeft size={18} />, roles: ['Admin', 'Contador'] },
]
},
{
title: "Operaciones & Sistema",
title: "Sistemas & Administración",
items: [
{ label: 'Diagramación', href: '/diagram', icon: <FileText size={18} />, roles: ['Admin', 'Diagramador'] },
{ label: 'Calendario', href: '/companies/calendar', icon: <Calendar size={18} />, roles: ['Admin'] },
{ label: 'Usuarios', href: '/users', icon: <Users size={18} />, roles: ['Admin'] },
{ label: 'Auditoría', href: '/audit', icon: <History size={18} />, roles: ['Admin'] },
{ label: 'Diagramación de Página', href: '/diagram', icon: <FileText size={18} />, roles: ['Admin', 'Diagramador'] },
{ label: 'Calendario de Ediciones', href: '/companies/calendar', icon: <Calendar size={18} />, roles: ['Admin'] },
{ label: 'Gestión de Usuarios', href: '/users', icon: <Users size={18} />, roles: ['Admin'] },
{ label: 'Registro de Auditoría', href: '/audit', icon: <History size={18} />, roles: ['Admin'] },
]
}
];

View File

@@ -1,8 +1,17 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import api from '../../services/api';
import { Save, DollarSign, FileText, Type, AlertCircle } from 'lucide-react';
import { Save, DollarSign, FileText, Package, ExternalLink, ArrowRight } from 'lucide-react';
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
// Datos mínimos de un Producto del Catálogo
interface ProductSummary {
id: number;
name: string;
typeCode: string;
basePrice: number;
}
interface PricingConfig {
baseWordCount: number;
extraWordPrice: number;
@@ -21,6 +30,8 @@ const defaultConfig: PricingConfig = {
export default function PricingManager() {
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
const [selectedCat, setSelectedCat] = useState<number | null>(null);
const [categoryProducts, setCategoryProducts] = useState<ProductSummary[]>([]);
const [loadingProducts, setLoadingProducts] = useState(false);
const [config, setConfig] = useState<PricingConfig>(defaultConfig);
const [saving, setSaving] = useState(false);
@@ -35,12 +46,21 @@ export default function PricingManager() {
useEffect(() => {
if (selectedCat) {
// Cargar config existente
// Cargar config existente del rubro
api.get(`/pricing/${selectedCat}`)
.then(res => {
if (res.data) setConfig(res.data);
else setConfig(defaultConfig); // Reset si es nuevo
else setConfig(defaultConfig);
});
// Cargar productos vinculados a este rubro
setLoadingProducts(true);
api.get(`/products/by-category/${selectedCat}`)
.then(res => setCategoryProducts(res.data || []))
.catch(() => setCategoryProducts([]))
.finally(() => setLoadingProducts(false));
} else {
setCategoryProducts([]);
}
}, [selectedCat]);
@@ -64,7 +84,7 @@ export default function PricingManager() {
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 text-gray-800">
<DollarSign className="text-green-600" />
Gestor de Tarifas y Reglas
Reglas de Tarifación por Rubro
</h2>
{/* SELECTOR DE RUBRO */}
@@ -81,7 +101,7 @@ export default function PricingManager() {
<option
key={cat.id}
value={cat.id}
disabled={!cat.isSelectable} // Bloqueamos padres para forzar config en hojas
disabled={!cat.isSelectable}
className={cat.isSelectable ? "text-gray-900 font-medium" : "text-gray-400 font-bold bg-gray-50"}
>
{'\u00A0\u00A0'.repeat(cat.level)}
@@ -89,7 +109,6 @@ export default function PricingManager() {
</option>
))}
</select>
{/* Flecha custom para estilo */}
<div className="absolute right-4 top-3.5 pointer-events-none text-gray-500"></div>
</div>
@@ -105,17 +124,63 @@ export default function PricingManager() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* TARIFA BASE */}
{/* TARJETA: PRECIO BASE - muestra productos vinculados */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 className="font-bold text-lg mb-1 flex items-center gap-2 text-gray-800">
<Package size={20} className="text-blue-500" /> Precio Base del Rubro
</h3>
<p className="text-xs text-gray-400 mb-4">
El precio mínimo por aviso se define en cada Producto del Catálogo vinculado a este rubro.
</p>
{loadingProducts ? (
<div className="text-xs text-slate-400 animate-pulse py-3">Cargando productos...</div>
) : categoryProducts.length === 0 ? (
<div className="flex flex-col gap-3">
<div className="flex items-start gap-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<span className="text-amber-500 text-xl leading-none"></span>
<div>
<p className="text-xs font-bold text-amber-700">Sin productos vinculados</p>
<p className="text-xs text-amber-600 mt-0.5">
Este rubro no tiene productos en el Catálogo. El precio base será $0 al calcular tarifas.
</p>
</div>
</div>
<Link
to="/products"
className="inline-flex items-center gap-2 text-xs font-bold text-blue-600 hover:text-blue-800 transition-colors bg-blue-50 px-3 py-2 rounded-lg border border-blue-200"
>
<ExternalLink size={12} /> Ir al Catálogo de Productos para agregar uno
</Link>
</div>
) : (
<div className="space-y-2">
{categoryProducts.map(prod => (
<div key={prod.id} className="flex items-center justify-between py-2.5 px-3 bg-slate-50 rounded-lg border border-slate-100 hover:border-blue-200 transition-colors">
<div className="flex items-center gap-2">
<span className="text-[9px] font-black uppercase text-slate-400 bg-slate-200 px-1.5 py-0.5 rounded">{prod.typeCode}</span>
<span className="text-sm font-bold text-slate-700">{prod.name}</span>
</div>
<span className="font-mono font-black text-blue-600 text-sm">${prod.basePrice.toLocaleString()}</span>
</div>
))}
<Link
to="/products"
className="mt-2 inline-flex items-center gap-1.5 text-[11px] font-bold text-blue-500 hover:text-blue-700 transition-colors"
>
<ArrowRight size={11} /> Administrar precios en el Catálogo de Productos
</Link>
</div>
)}
</div>
{/* TARJETA: REGLAS POR PALABRAS */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 className="font-bold text-lg mb-4 pb-2 border-b border-gray-100 flex items-center gap-2 text-gray-800">
<FileText size={20} className="text-blue-500" /> Tarifa Base (Texto)
<FileText size={20} className="text-blue-500" /> Reglas por Cantidad de Palabras
</h3>
<div className="space-y-4">
<p className="text-xs text-blue-600 mb-2 bg-blue-50/50 p-2 rounded-lg border border-blue-100">
El Precio Base (Precio Mínimo) ahora se define directamente en los Productos del Catálogo.
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">Palabras Incluidas</label>
@@ -131,18 +196,20 @@ export default function PricingManager() {
</div>
</div>
{/* CONTENIDO ESPECIAL */}
</div>
{/* TARJETA: CARACTERES ESPECIALES */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 className="font-bold text-lg mb-4 pb-2 border-b border-gray-100 flex items-center gap-2 text-gray-800">
<AlertCircle size={20} className="text-orange-500" /> Caracteres Especiales
<DollarSign size={20} className="text-orange-500" /> Caracteres Especiales
</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">Caracteres a cobrar (ej: !$%)</label>
<input type="text" className="border p-2 rounded w-full font-mono tracking-widest focus:ring-2 focus:ring-orange-500 outline-none"
value={config.specialChars} onChange={e => setConfig({ ...config, specialChars: e.target.value })} />
<p className="text-xs text-gray-400 mt-1">Cada uno de estos símbolos se cobrará aparte.</p>
<p className="text-xs text-gray-400 mt-1">Cada uno de estos símbolos se cobrará aparte del precio por palabra.</p>
</div>
<div>
@@ -156,10 +223,10 @@ export default function PricingManager() {
</div>
</div>
{/* ESTILOS VISUALES */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 lg:col-span-2">
{/* TARJETA: ESTILOS VISUALES */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 className="font-bold text-lg mb-4 pb-2 border-b border-gray-100 flex items-center gap-2 text-gray-800">
<Type size={20} className="text-purple-500" /> Estilos Visuales (Recargos)
<Save size={20} className="text-purple-500" /> Estilos Visuales (Recargos)
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
@@ -183,8 +250,6 @@ export default function PricingManager() {
</div>
</div>
</div>
{/* BARRA DE ACCIÓN FLOTANTE */}
<div className="sticky bottom-4 bg-gray-900 text-white p-4 rounded-lg shadow-lg flex justify-between items-center z-10">
<div className="text-sm">