ux: menú lateral más descriptivo y PricingManager muestra productos del rubro con precios
This commit is contained in:
@@ -49,20 +49,20 @@ export default function ProtectedLayout() {
|
|||||||
{
|
{
|
||||||
title: "Gestión Diaria",
|
title: "Gestión Diaria",
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Dashboard', href: '/', icon: <LayoutDashboard size={18} />, roles: ['Admin', 'Cajero'] },
|
{ label: 'Inicio', href: '/', icon: <LayoutDashboard size={18} />, roles: ['Admin', 'Cajero'] },
|
||||||
{ label: 'Moderación', href: '/moderation', icon: <Eye size={18} />, roles: ['Admin', 'Moderador'], badge: unreadCount },
|
{ label: 'Moderación de Avisos', href: '/moderation', icon: <Eye size={18} />, roles: ['Admin', 'Moderador'], badge: unreadCount },
|
||||||
{ label: 'Explorador', href: '/listings', icon: <Search size={18} />, roles: ['Admin', 'Cajero', 'Moderador'] },
|
{ label: 'Buscar Publicaciones', href: '/listings', icon: <Search size={18} />, roles: ['Admin', 'Cajero', 'Moderador'] },
|
||||||
{ label: 'Clientes', href: '/clients', icon: <ClientIcon size={18} />, roles: ['Admin', 'Cajero'] },
|
{ label: 'Clientes', href: '/clients', icon: <ClientIcon size={18} />, roles: ['Admin', 'Cajero'] },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Comercial & Catálogo",
|
title: "Catálogo & Precios",
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Productos', href: '/products', icon: <Box size={18} />, roles: ['Admin'] },
|
{ label: 'Productos & Tarifas', href: '/products', icon: <Box size={18} />, roles: ['Admin'] },
|
||||||
{ label: 'Tarifas', href: '/pricing', icon: <DollarSign 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: 'Promociones', href: '/promotions', icon: <Tag size={18} />, roles: ['Admin'] },
|
||||||
{ label: 'Cupones', href: '/coupons', icon: <Tag size={18} />, roles: ['Admin'] },
|
{ label: 'Cupones de Descuento', href: '/coupons', icon: <Tag size={18} />, roles: ['Admin'] },
|
||||||
{ label: 'Categorías', href: '/categories', icon: <FolderTree 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: [
|
items: [
|
||||||
{ label: 'Empresas', href: '/companies', icon: <Building2 size={18} />, roles: ['Admin'] },
|
{ label: 'Empresas', href: '/companies', icon: <Building2 size={18} />, roles: ['Admin'] },
|
||||||
{ label: 'Riesgo Crediticio', href: '/finance/credit', icon: <ShieldAlert size={18} />, roles: ['Admin', 'Gerente'] },
|
{ 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: [
|
items: [
|
||||||
{ label: 'Diagramación', href: '/diagram', icon: <FileText size={18} />, roles: ['Admin', 'Diagramador'] },
|
{ label: 'Diagramación de Página', href: '/diagram', icon: <FileText size={18} />, roles: ['Admin', 'Diagramador'] },
|
||||||
{ label: 'Calendario', href: '/companies/calendar', icon: <Calendar size={18} />, roles: ['Admin'] },
|
{ label: 'Calendario de Ediciones', href: '/companies/calendar', icon: <Calendar size={18} />, roles: ['Admin'] },
|
||||||
{ label: 'Usuarios', href: '/users', icon: <Users size={18} />, roles: ['Admin'] },
|
{ label: 'Gestión de Usuarios', href: '/users', icon: <Users size={18} />, roles: ['Admin'] },
|
||||||
{ label: 'Auditoría', href: '/audit', icon: <History size={18} />, roles: ['Admin'] },
|
{ label: 'Registro de Auditoría', href: '/audit', icon: <History size={18} />, roles: ['Admin'] },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import api from '../../services/api';
|
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';
|
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 {
|
interface PricingConfig {
|
||||||
baseWordCount: number;
|
baseWordCount: number;
|
||||||
extraWordPrice: number;
|
extraWordPrice: number;
|
||||||
@@ -21,6 +30,8 @@ const defaultConfig: PricingConfig = {
|
|||||||
export default function PricingManager() {
|
export default function PricingManager() {
|
||||||
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
|
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
|
||||||
const [selectedCat, setSelectedCat] = useState<number | null>(null);
|
const [selectedCat, setSelectedCat] = useState<number | null>(null);
|
||||||
|
const [categoryProducts, setCategoryProducts] = useState<ProductSummary[]>([]);
|
||||||
|
const [loadingProducts, setLoadingProducts] = useState(false);
|
||||||
|
|
||||||
const [config, setConfig] = useState<PricingConfig>(defaultConfig);
|
const [config, setConfig] = useState<PricingConfig>(defaultConfig);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -35,12 +46,21 @@ export default function PricingManager() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCat) {
|
if (selectedCat) {
|
||||||
// Cargar config existente
|
// Cargar config existente del rubro
|
||||||
api.get(`/pricing/${selectedCat}`)
|
api.get(`/pricing/${selectedCat}`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.data) setConfig(res.data);
|
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]);
|
}, [selectedCat]);
|
||||||
|
|
||||||
@@ -64,7 +84,7 @@ export default function PricingManager() {
|
|||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 text-gray-800">
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2 text-gray-800">
|
||||||
<DollarSign className="text-green-600" />
|
<DollarSign className="text-green-600" />
|
||||||
Gestor de Tarifas y Reglas
|
Reglas de Tarifación por Rubro
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* SELECTOR DE RUBRO */}
|
{/* SELECTOR DE RUBRO */}
|
||||||
@@ -81,7 +101,7 @@ export default function PricingManager() {
|
|||||||
<option
|
<option
|
||||||
key={cat.id}
|
key={cat.id}
|
||||||
value={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"}
|
className={cat.isSelectable ? "text-gray-900 font-medium" : "text-gray-400 font-bold bg-gray-50"}
|
||||||
>
|
>
|
||||||
{'\u00A0\u00A0'.repeat(cat.level)}
|
{'\u00A0\u00A0'.repeat(cat.level)}
|
||||||
@@ -89,7 +109,6 @@ export default function PricingManager() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{/* Flecha custom para estilo */}
|
|
||||||
<div className="absolute right-4 top-3.5 pointer-events-none text-gray-500">▼</div>
|
<div className="absolute right-4 top-3.5 pointer-events-none text-gray-500">▼</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,17 +124,63 @@ export default function PricingManager() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<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">
|
<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">
|
<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>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<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 className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">Palabras Incluidas</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CONTENIDO ESPECIAL */}
|
</div>
|
||||||
|
|
||||||
|
{/* TARJETA: CARACTERES ESPECIALES */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<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">
|
<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>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">Caracteres a cobrar (ej: !$%)</label>
|
<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"
|
<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 })} />
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -156,10 +223,10 @@ export default function PricingManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ESTILOS VISUALES */}
|
{/* TARJETA: ESTILOS VISUALES */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 lg:col-span-2">
|
<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">
|
<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>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
@@ -183,8 +250,6 @@ export default function PricingManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* BARRA DE ACCIÓN FLOTANTE */}
|
{/* 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="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">
|
<div className="text-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user