diff --git a/frontend/src/pages/Admin/ReglasMapeeo.tsx b/frontend/src/pages/Admin/ReglasMapeeo.tsx index 6ed190a..9aa510d 100644 --- a/frontend/src/pages/Admin/ReglasMapeeo.tsx +++ b/frontend/src/pages/Admin/ReglasMapeeo.tsx @@ -1,34 +1,176 @@ import { useState, useEffect } from 'react'; import { adminApi } from '../../services/api'; -import { ReglaMapeo } from '../../types'; +import { ReglaMapeo, ReporteContable, CategoriaContable } from '../../types'; import toast from 'react-hot-toast'; +const SISTEMAS = ['contpaqi', 'aspel', 'sap', 'odoo', 'alegra', 'generico']; + +interface FormData { + sistema_origen: string; + cuenta_padre_codigo: string; + rango_inicio: string; + rango_fin: string; + patron_regex: string; + reporte_contable_id: number | null; + categoria_contable_id: number | null; + prioridad: number; + activo: boolean; +} + +const initialFormData: FormData = { + sistema_origen: 'contpaqi', + cuenta_padre_codigo: '', + rango_inicio: '', + rango_fin: '', + patron_regex: '', + reporte_contable_id: null, + categoria_contable_id: null, + prioridad: 10, + activo: true, +}; + export default function AdminReglasMapeeo() { const [reglas, setReglas] = useState([]); + const [reportesContables, setReportesContables] = useState([]); + const [categoriasContables, setCategoriasContables] = useState([]); const [loading, setLoading] = useState(true); const [filterSistema, setFilterSistema] = useState(''); - - const sistemas = ['contpaqi', 'aspel', 'sap', 'odoo', 'alegra', 'generico']; + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState(initialFormData); + const [saving, setSaving] = useState(false); useEffect(() => { - loadReglas(); + loadData(); }, []); - const loadReglas = async () => { + const loadData = async () => { try { - const data = await adminApi.reglasMapeeo.list(); - setReglas(data); + const [reglasData, reportesData, categoriasData] = await Promise.all([ + adminApi.reglasMapeeo.list(), + adminApi.catalogos.reportesContables(), + adminApi.catalogos.categoriasContables(), + ]); + setReglas(reglasData); + setReportesContables(reportesData); + setCategoriasContables(categoriasData); } catch { - toast.error('Error al cargar reglas'); + toast.error('Error al cargar datos'); } finally { setLoading(false); } }; + const handleOpenCreate = () => { + setFormData(initialFormData); + setEditingId(null); + setShowForm(true); + }; + + const handleOpenEdit = (regla: ReglaMapeo) => { + setFormData({ + sistema_origen: regla.sistema_origen, + cuenta_padre_codigo: regla.cuenta_padre_codigo || '', + rango_inicio: regla.rango_inicio || '', + rango_fin: regla.rango_fin || '', + patron_regex: regla.patron_regex || '', + reporte_contable_id: regla.reporte_contable_id, + categoria_contable_id: regla.categoria_contable_id, + prioridad: regla.prioridad, + activo: regla.activo, + }); + setEditingId(regla.id); + setShowForm(true); + }; + + const handleClose = () => { + setShowForm(false); + setEditingId(null); + setFormData(initialFormData); + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type } = e.target; + + if (type === 'checkbox') { + const checked = (e.target as HTMLInputElement).checked; + setFormData((prev) => ({ ...prev, [name]: checked })); + } else if (name === 'reporte_contable_id' || name === 'categoria_contable_id') { + setFormData((prev) => ({ ...prev, [name]: value ? Number(value) : null })); + } else if (name === 'prioridad') { + setFormData((prev) => ({ ...prev, [name]: Number(value) })); + } else { + setFormData((prev) => ({ ...prev, [name]: value })); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.sistema_origen) { + toast.error('Seleccione un sistema'); + return; + } + if (!formData.reporte_contable_id) { + toast.error('Seleccione un reporte contable'); + return; + } + if (!formData.categoria_contable_id) { + toast.error('Seleccione una categoría'); + return; + } + + setSaving(true); + try { + const dataToSend = { + sistema_origen: formData.sistema_origen, + cuenta_padre_codigo: formData.cuenta_padre_codigo || null, + rango_inicio: formData.rango_inicio || null, + rango_fin: formData.rango_fin || null, + patron_regex: formData.patron_regex || null, + reporte_contable_id: formData.reporte_contable_id, + categoria_contable_id: formData.categoria_contable_id, + prioridad: formData.prioridad, + activo: formData.activo, + }; + + if (editingId) { + await adminApi.reglasMapeeo.update(editingId, dataToSend); + toast.success('Regla actualizada'); + } else { + await adminApi.reglasMapeeo.create(dataToSend); + toast.success('Regla creada'); + } + + handleClose(); + loadData(); + } catch (error: any) { + toast.error(error.response?.data?.message || 'Error al guardar regla'); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: number) => { + if (!confirm('¿Estás seguro de eliminar esta regla?')) return; + + try { + await adminApi.reglasMapeeo.delete(id); + toast.success('Regla eliminada'); + loadData(); + } catch { + toast.error('Error al eliminar regla'); + } + }; + const filteredReglas = filterSistema ? reglas.filter((r) => r.sistema_origen === filterSistema) : reglas; + const categoriasFiltradas = formData.reporte_contable_id + ? categoriasContables.filter((c) => c.reporte_contable_id === formData.reporte_contable_id) + : []; + if (loading) { return (
@@ -41,7 +183,9 @@ export default function AdminReglasMapeeo() {

Reglas de Mapeo Contable

- +
{/* Filtro */} @@ -49,10 +193,10 @@ export default function AdminReglasMapeeo() { + {SISTEMAS.map((s) => ( + + ))} + +
+ + {/* Prioridad */} +
+ + +

Mayor número = mayor prioridad

+
+
+ + {/* Cuenta padre */} +
+ + +
+ + {/* Rango */} +
+
+ + +
+
+ + +
+
+ + {/* Patrón Regex */} +
+ + +

+ Use expresiones regulares para patrones complejos +

+
+ + {/* Reporte Contable */} +
+ + +
+ + {/* Categoría Contable */} +
+ + +
+ + {/* Activo */} +
+ +
+ + {/* Botones */} +
+ + +
+ + + + )} ); } diff --git a/frontend/src/pages/Admin/Umbrales.tsx b/frontend/src/pages/Admin/Umbrales.tsx index 9b9593f..9eec830 100644 --- a/frontend/src/pages/Admin/Umbrales.tsx +++ b/frontend/src/pages/Admin/Umbrales.tsx @@ -3,33 +3,158 @@ import { adminApi } from '../../services/api'; import { Umbral, Giro } from '../../types'; import toast from 'react-hot-toast'; +const METRICAS = [ + { value: 'margen_bruto', label: 'Margen Bruto' }, + { value: 'margen_ebitda', label: 'Margen EBITDA' }, + { value: 'margen_operativo', label: 'Margen Operativo' }, + { value: 'margen_neto', label: 'Margen Neto' }, + { value: 'margen_nopat', label: 'Margen NOPAT' }, + { value: 'margen_ocf', label: 'Margen OCF' }, + { value: 'margen_fcf', label: 'Margen FCF' }, + { value: 'roic', label: 'ROIC' }, + { value: 'roe', label: 'ROE' }, + { value: 'roa', label: 'ROA' }, + { value: 'roce', label: 'ROCE' }, + { value: 'current_ratio', label: 'Current Ratio' }, + { value: 'quick_ratio', label: 'Quick Ratio' }, + { value: 'cash_ratio', label: 'Cash Ratio' }, + { value: 'net_debt_ebitda', label: 'Net Debt/EBITDA' }, + { value: 'interest_coverage', label: 'Interest Coverage' }, + { value: 'debt_ratio', label: 'Debt Ratio' }, + { value: 'asset_turnover', label: 'Asset Turnover' }, + { value: 'inventory_turnover', label: 'Inventory Turnover' }, + { value: 'revenue_growth', label: 'Revenue Growth' }, +]; + +interface FormData { + metrica: string; + giro_id: number | null; + muy_positivo: string; + positivo: string; + neutral: string; + negativo: string; + muy_negativo: string; +} + +const initialFormData: FormData = { + metrica: '', + giro_id: null, + muy_positivo: '', + positivo: '', + neutral: '', + negativo: '', + muy_negativo: '', +}; + export default function AdminUmbrales() { const [umbrales, setUmbrales] = useState([]); const [giros, setGiros] = useState([]); const [loading, setLoading] = useState(true); const [filterGiro, setFilterGiro] = useState(''); + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState(initialFormData); + const [saving, setSaving] = useState(false); useEffect(() => { - Promise.all([loadUmbrales(), loadGiros()]); + loadData(); }, []); - const loadUmbrales = async () => { + const loadData = async () => { try { - const data = await adminApi.umbrales.list(); - setUmbrales(data); + const [umbralesData, girosData] = await Promise.all([ + adminApi.umbrales.list(), + adminApi.giros.list(), + ]); + setUmbrales(umbralesData); + setGiros(girosData); } catch { - toast.error('Error al cargar umbrales'); + toast.error('Error al cargar datos'); } finally { setLoading(false); } }; - const loadGiros = async () => { + const handleOpenCreate = () => { + setFormData(initialFormData); + setEditingId(null); + setShowForm(true); + }; + + const handleOpenEdit = (umbral: Umbral) => { + setFormData({ + metrica: umbral.metrica, + giro_id: umbral.giro_id || null, + muy_positivo: umbral.muy_positivo?.toString() || '', + positivo: umbral.positivo?.toString() || '', + neutral: umbral.neutral?.toString() || '', + negativo: umbral.negativo?.toString() || '', + muy_negativo: umbral.muy_negativo?.toString() || '', + }); + setEditingId(umbral.id); + setShowForm(true); + }; + + const handleClose = () => { + setShowForm(false); + setEditingId(null); + setFormData(initialFormData); + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: name === 'giro_id' ? (value ? Number(value) : null) : value, + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.metrica) { + toast.error('Seleccione una métrica'); + return; + } + + setSaving(true); try { - const data = await adminApi.giros.list(); - setGiros(data); + const dataToSend = { + metrica: formData.metrica, + giro_id: formData.giro_id, + muy_positivo: formData.muy_positivo ? parseFloat(formData.muy_positivo) : null, + positivo: formData.positivo ? parseFloat(formData.positivo) : null, + neutral: formData.neutral ? parseFloat(formData.neutral) : null, + negativo: formData.negativo ? parseFloat(formData.negativo) : null, + muy_negativo: formData.muy_negativo ? parseFloat(formData.muy_negativo) : null, + }; + + if (editingId) { + await adminApi.umbrales.update(editingId, dataToSend); + toast.success('Umbral actualizado'); + } else { + await adminApi.umbrales.create(dataToSend); + toast.success('Umbral creado'); + } + + handleClose(); + loadData(); + } catch (error: any) { + toast.error(error.response?.data?.message || 'Error al guardar umbral'); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: number) => { + if (!confirm('¿Estás seguro de eliminar este umbral?')) return; + + try { + await adminApi.umbrales.delete(id); + toast.success('Umbral eliminado'); + loadData(); } catch { - toast.error('Error al cargar giros'); + toast.error('Error al eliminar umbral'); } }; @@ -37,6 +162,11 @@ export default function AdminUmbrales() { ? umbrales.filter((u) => u.giro_id?.toString() === filterGiro || (!u.giro_id && filterGiro === 'general')) : umbrales; + const formatValue = (value: number | null | undefined) => { + if (value === null || value === undefined) return '-'; + return value.toString(); + }; + if (loading) { return (
@@ -49,7 +179,9 @@ export default function AdminUmbrales() {

Umbrales de Métricas

- +
{/* Filtro */} @@ -57,7 +189,7 @@ export default function AdminUmbrales() { + + {METRICAS.map((m) => ( + + ))} + +
+ + {/* Giro */} +
+ + +

+ Si selecciona un giro, este umbral solo aplicará a clientes de ese giro. +

+
+ + {/* Umbrales */} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

+ Para métricas de porcentaje, use decimales (ej: 0.20 = 20%). + Para ratios, use el valor directo (ej: 1.5). +

+ + {/* Botones */} +
+ + +
+ +
+ + )} ); } diff --git a/frontend/src/pages/Admin/Usuarios.tsx b/frontend/src/pages/Admin/Usuarios.tsx index 67c990f..3947f8f 100644 --- a/frontend/src/pages/Admin/Usuarios.tsx +++ b/frontend/src/pages/Admin/Usuarios.tsx @@ -1,41 +1,163 @@ import { useState, useEffect } from 'react'; -import { adminApi } from '../../services/api'; -import { User } from '../../types'; +import { adminApi, clientesApi } from '../../services/api'; +import { User, Cliente } from '../../types'; import toast from 'react-hot-toast'; +interface FormData { + nombre: string; + email: string; + password: string; + role: 'admin' | 'analista' | 'cliente' | 'empleado'; + cliente_id: number | null; +} + +const initialFormData: FormData = { + nombre: '', + email: '', + password: '', + role: 'analista', + cliente_id: null, +}; + export default function AdminUsuarios() { const [usuarios, setUsuarios] = useState([]); + const [clientes, setClientes] = useState([]); const [loading, setLoading] = useState(true); const [showForm, setShowForm] = useState(false); const [editingUser, setEditingUser] = useState(null); + const [formData, setFormData] = useState(initialFormData); + const [saving, setSaving] = useState(false); useEffect(() => { - loadUsuarios(); + loadData(); }, []); - const loadUsuarios = async () => { + const loadData = async () => { try { - const data = await adminApi.usuarios.list(); - setUsuarios(data); + const [usuariosData, clientesData] = await Promise.all([ + adminApi.usuarios.list(), + clientesApi.list(), + ]); + setUsuarios(usuariosData); + setClientes(clientesData); } catch { - toast.error('Error al cargar usuarios'); + toast.error('Error al cargar datos'); } finally { setLoading(false); } }; + const handleOpenCreate = () => { + setFormData(initialFormData); + setEditingUser(null); + setShowForm(true); + }; + + const handleOpenEdit = (usuario: User) => { + setFormData({ + nombre: usuario.nombre, + email: usuario.email, + password: '', + role: usuario.role, + cliente_id: usuario.cliente_id || null, + }); + setEditingUser(usuario); + setShowForm(true); + }; + + const handleClose = () => { + setShowForm(false); + setEditingUser(null); + setFormData(initialFormData); + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: name === 'cliente_id' ? (value ? Number(value) : null) : value, + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Validaciones + if (!formData.nombre.trim()) { + toast.error('El nombre es requerido'); + return; + } + if (!formData.email.trim()) { + toast.error('El email es requerido'); + return; + } + if (!editingUser && !formData.password) { + toast.error('La contraseña es requerida'); + return; + } + if ((formData.role === 'cliente' || formData.role === 'empleado') && !formData.cliente_id) { + toast.error('Debe seleccionar un cliente para este rol'); + return; + } + + setSaving(true); + try { + const dataToSend: any = { + nombre: formData.nombre, + email: formData.email, + role: formData.role, + cliente_id: formData.cliente_id, + }; + + if (formData.password) { + dataToSend.password = formData.password; + } + + if (editingUser) { + await adminApi.usuarios.update(editingUser.id, dataToSend); + toast.success('Usuario actualizado'); + } else { + await adminApi.usuarios.create(dataToSend); + toast.success('Usuario creado'); + } + + handleClose(); + loadData(); + } catch (error: any) { + const message = error.response?.data?.message || 'Error al guardar usuario'; + toast.error(message); + } finally { + setSaving(false); + } + }; + const handleDelete = async (id: number) => { if (!confirm('¿Estás seguro de eliminar este usuario?')) return; try { await adminApi.usuarios.delete(id); toast.success('Usuario eliminado'); - loadUsuarios(); + loadData(); } catch { toast.error('Error al eliminar usuario'); } }; + const getRoleBadgeColor = (role: string) => { + switch (role) { + case 'admin': + return 'bg-red-100 text-red-700'; + case 'analista': + return 'bg-blue-100 text-blue-700'; + case 'cliente': + return 'bg-green-100 text-green-700'; + case 'empleado': + return 'bg-yellow-100 text-yellow-700'; + default: + return 'bg-gray-100 text-gray-700'; + } + }; + if (loading) { return (
@@ -48,7 +170,7 @@ export default function AdminUsuarios() {

Usuarios

-
@@ -66,11 +188,11 @@ export default function AdminUsuarios() { {usuarios.map((usuario) => ( - - {usuario.nombre} + + {usuario.nombre} {usuario.email} - + {usuario.role} @@ -79,7 +201,7 @@ export default function AdminUsuarios() {
- {/* Modal placeholder - implementar formulario completo */} - {(showForm || editingUser) && ( + {/* Modal de formulario */} + {showForm && (
-

+

{editingUser ? 'Editar Usuario' : 'Nuevo Usuario'}

-

Formulario de usuario aquí...

-
- - -
+ +
+ {/* Nombre */} +
+ + +
+ + {/* Email */} +
+ + +
+ + {/* Password */} +
+ + +
+ + {/* Rol */} +
+ + +

+ {formData.role === 'admin' && 'Acceso total al sistema'} + {formData.role === 'analista' && 'Gestiona clientes y genera reportes'} + {formData.role === 'cliente' && 'Ve solo su dashboard y reportes'} + {formData.role === 'empleado' && 'Permisos configurables por cliente'} +

+
+ + {/* Cliente (solo para rol cliente o empleado) */} + {(formData.role === 'cliente' || formData.role === 'empleado') && ( +
+ + + {clientes.length === 0 && ( +

+ No hay clientes registrados. Cree un cliente primero. +

+ )} +
+ )} + + {/* Botones */} +
+ + +
+
)}