Solucionado submodulos y subiendo codigo real
This commit is contained in:
73
frontend/src/lib/api.js
Normal file
73
frontend/src/lib/api.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// src/lib/api.js
|
||||
// Cliente para el backend FastAPI. Cambia BASE_URL si el backend corre en otra IP.
|
||||
const BASE_URL = "http://localhost:8000";
|
||||
|
||||
async function get(path, params = {}) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
const url = `${BASE_URL}${path}${qs ? "?" + qs : ""}`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`Error ${r.status} en ${path}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function postJson(path, body) {
|
||||
const r = await fetch(`${BASE_URL}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) throw new Error(`Error ${r.status} en ${path}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function post(path, params = {}) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
const url = `${BASE_URL}${path}${qs ? "?" + qs : ""}`;
|
||||
const r = await fetch(url, { method: "POST" });
|
||||
if (!r.ok) throw new Error(`Error ${r.status} en ${path}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
periodoActual: () => get("/api/periodo-actual"),
|
||||
ocupabilidad: (ano, mes, sede = "TODOS", programa = "TODOS") =>
|
||||
get("/api/ocupabilidad", { ano, mes, sede, programa }),
|
||||
ventas: (ano, mes, sede = "TODOS", programa = "TODOS") => get("/api/ventas", { ano, mes, sede, programa }),
|
||||
ventasDetalle: (vendedor, ano, mes, tipo, sede = "TODOS", programa = "TODOS") =>
|
||||
get("/api/ventas/detalle", { vendedor, ano, mes, tipo, sede, programa }),
|
||||
cobranza: (ano, mes, sectorista = "TODOS", agrupacion = "SEDE") =>
|
||||
get("/api/cobranza", { ano, mes, sectorista, agrupacion }),
|
||||
cobranzaDetalle: (grupo, ano, mes, sectorista = "TODOS", agrupacion = "PROGRAMA") =>
|
||||
get("/api/cobranza/detalle", { grupo, ano, mes, sectorista, agrupacion }),
|
||||
rentabilidad: (ano, mes, sede = "TODOS", programa = "TODOS") =>
|
||||
get("/api/rentabilidad", { ano, mes, sede, programa }),
|
||||
rentabilidadDetalle: (programa, ano, mes) =>
|
||||
get("/api/rentabilidad/detalle", { programa, ano, mes }),
|
||||
rentabilidadCostos: (programa, ano, mes) =>
|
||||
get("/api/rentabilidad/costos", { programa, ano, mes }),
|
||||
saldoPendiente: (tipo_cuota) => get("/api/saldo-pendiente", { tipo_cuota }),
|
||||
asesores: () => get("/api/asesores"),
|
||||
asesorEstado: (agent_id, online) =>
|
||||
post("/api/asesores/estado", { agent_id, online }),
|
||||
guardarCostos: (num_indice, inicial, actual) =>
|
||||
postJson("/api/rentabilidad/costos/guardar", { num_indice, inicial, actual }),
|
||||
clasificarSede: (programas) =>
|
||||
postJson("/api/cobranza/clasificar-sede", { programas }),
|
||||
cobranzaDetalleTodos: (ano, mes, sectorista = "TODOS") =>
|
||||
get("/api/cobranza/detalle-todos", { ano, mes, sectorista }),
|
||||
usuariosListar: () => get("/api/usuarios"),
|
||||
usuariosCrear: (email, password, nombre, rol) =>
|
||||
postJson("/api/usuarios/crear", { email, password, nombre, rol }),
|
||||
usuariosActualizarRol: (id, rol) => postJson("/api/usuarios/rol", { id, rol }),
|
||||
usuariosEliminar: (id) => postJson("/api/usuarios/eliminar", { id }),
|
||||
vendedoresManualesListar: (ano, mes) => get("/api/comisiones/vendedores", { ano, mes }),
|
||||
vendedoresManualesCrear: (datos) => postJson("/api/comisiones/vendedores/crear", datos),
|
||||
vendedoresManualesEliminar: (id) => postJson("/api/comisiones/vendedores/eliminar", { id }),
|
||||
vendedoresManualesActualizar: (datos) => postJson("/api/comisiones/vendedores/actualizar", datos),
|
||||
vendedoresManualesLote: (datos) => postJson("/api/comisiones/vendedores/lote", datos),
|
||||
comisionesOverrideGuardar: (registros) => postJson("/api/comisiones/override/guardar", { registros }),
|
||||
comisionesOverrideRestaurar: (num_matricula) => postJson("/api/comisiones/override/restaurar", { num_matricula }),
|
||||
comisionesDetalleTodos: (ano, mes) => get("/api/comisiones/detalle-todos", { ano, mes }),
|
||||
comisionesConfig: (ano, mes) => get("/api/comisiones/config", { ano, mes }),
|
||||
comisionesConfigGuardar: (datos) => postJson("/api/comisiones/config/guardar", datos),
|
||||
};
|
||||
106
frontend/src/lib/auth.jsx
Normal file
106
frontend/src/lib/auth.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
// src/lib/auth.jsx
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from "react";
|
||||
import { supabase } from "./supabase";
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [session, setSession] = useState(null);
|
||||
const [perfil, setPerfil] = useState(null); // { nombre, rol }
|
||||
const [permisos, setPermisos] = useState([]); // ["ocupabilidad", "cobranza", ...]
|
||||
const [cargando, setCargando] = useState(true);
|
||||
|
||||
// Cargar perfil (rol) y permisos del usuario logueado
|
||||
const cargarPerfil = useCallback(async (userId) => {
|
||||
// 1) Traer el rol del usuario desde la tabla "perfiles"
|
||||
const { data: perf, error: e1 } = await supabase
|
||||
.from("perfiles")
|
||||
.select("nombre, rol, activo")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (e1 || !perf || perf.activo === false) {
|
||||
setPerfil(null);
|
||||
setPermisos([]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) Traer los módulos permitidos para ese rol desde "permisos"
|
||||
const { data: perms } = await supabase
|
||||
.from("permisos")
|
||||
.select("modulo, puede_ver")
|
||||
.eq("rol", perf.rol);
|
||||
|
||||
const modulos = (perms || [])
|
||||
.filter((p) => p.puede_ver)
|
||||
.map((p) => p.modulo);
|
||||
|
||||
setPerfil({ nombre: perf.nombre, rol: perf.rol });
|
||||
setPermisos(modulos);
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
// Al iniciar: revisar si ya hay sesión activa
|
||||
useEffect(() => {
|
||||
let activo = true;
|
||||
supabase.auth.getSession().then(async ({ data }) => {
|
||||
if (!activo) return;
|
||||
const s = data?.session || null;
|
||||
setSession(s);
|
||||
if (s?.user) await cargarPerfil(s.user.id);
|
||||
setCargando(false);
|
||||
});
|
||||
|
||||
// Escuchar cambios de sesión (login/logout)
|
||||
const { data: sub } = supabase.auth.onAuthStateChange(async (_evt, s) => {
|
||||
setSession(s);
|
||||
if (s?.user) await cargarPerfil(s.user.id);
|
||||
else { setPerfil(null); setPermisos([]); }
|
||||
});
|
||||
|
||||
return () => { activo = false; sub?.subscription?.unsubscribe(); };
|
||||
}, [cargarPerfil]);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (error) return { ok: false, msg: traducirError(error.message) };
|
||||
// Verificar que tenga perfil/rol válido
|
||||
const ok = await cargarPerfil(data.user.id);
|
||||
if (!ok) {
|
||||
await supabase.auth.signOut();
|
||||
return { ok: false, msg: "Tu usuario no tiene un perfil asignado. Contacta al administrador." };
|
||||
}
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setPerfil(null);
|
||||
setPermisos([]);
|
||||
};
|
||||
|
||||
const value = {
|
||||
session,
|
||||
perfil,
|
||||
permisos,
|
||||
cargando,
|
||||
login,
|
||||
logout,
|
||||
autenticado: !!session && !!perfil,
|
||||
puedeVer: (modulo) => permisos.includes(modulo),
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth debe usarse dentro de AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function traducirError(msg) {
|
||||
if (/invalid login credentials/i.test(msg)) return "Correo o contraseña incorrectos.";
|
||||
if (/email not confirmed/i.test(msg)) return "El correo no está confirmado.";
|
||||
return msg || "Error al iniciar sesión.";
|
||||
}
|
||||
10
frontend/src/lib/supabase.js
Normal file
10
frontend/src/lib/supabase.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// src/lib/supabase.js
|
||||
// Cliente de Supabase para autenticación (login).
|
||||
// IMPORTANTE: aquí va la clave PÚBLICA (anon), nunca la secreta (service_role).
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
// Estos valores se leen de frontend/.env (ver .env.example)
|
||||
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || "";
|
||||
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY || "";
|
||||
|
||||
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||
63
frontend/src/lib/useColumnasAjustables.jsx
Normal file
63
frontend/src/lib/useColumnasAjustables.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
// src/lib/useColumnasAjustables.jsx
|
||||
// Hook reutilizable para columnas redimensionables (estilo Excel) en cualquier tabla.
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useColumnasAjustables(anchosIniciales) {
|
||||
const [anchos, setAnchos] = useState(anchosIniciales);
|
||||
|
||||
// Si cambia el número de columnas (ej. Cobranza Sede↔Programa), reiniciar anchos
|
||||
useEffect(() => {
|
||||
if (anchos.length !== anchosIniciales.length) {
|
||||
setAnchos(anchosIniciales);
|
||||
}
|
||||
}, [anchosIniciales.length]);
|
||||
|
||||
function iniciarResize(e, i) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const xInicial = e.clientX;
|
||||
const anchoInicial = anchos[i];
|
||||
function onMove(ev) {
|
||||
const nuevo = Math.max(50, anchoInicial + (ev.clientX - xInicial));
|
||||
setAnchos((prev) => { const c = [...prev]; c[i] = nuevo; return c; });
|
||||
}
|
||||
function onUp() {
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", onUp);
|
||||
}
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", onUp);
|
||||
}
|
||||
|
||||
// Devuelve los elementos <col> para el <colgroup> de la tabla
|
||||
function ColGroup() {
|
||||
return (
|
||||
<colgroup>
|
||||
{anchos.map((w, i) => <col key={i} style={{ width: w, minWidth: w }} />)}
|
||||
</colgroup>
|
||||
);
|
||||
}
|
||||
|
||||
// Devuelve el divisor arrastrable para poner dentro de cada <th>
|
||||
function Resizer({ index }) {
|
||||
return (
|
||||
<span
|
||||
onMouseDown={(e) => iniciarResize(e, index)}
|
||||
className="col-resizer"
|
||||
style={{ position: "absolute", top: 0, right: 0, width: 8, height: "100%",
|
||||
cursor: "col-resize", userSelect: "none", zIndex: 5 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const anchoTotal = anchos.reduce((a, b) => a + b, 0);
|
||||
|
||||
// width:100% → llena el contenedor desde el inicio (sin huecos a la derecha).
|
||||
// minWidth:anchoTotal → si las columnas crecen más que el contenedor, aparece scroll.
|
||||
// tableLayout:fixed + colgroup → encabezados y celdas alineados.
|
||||
const tableProps = {
|
||||
style: { tableLayout: "fixed", width: "100%", minWidth: anchoTotal },
|
||||
};
|
||||
|
||||
return { anchos, ColGroup, Resizer, anchoTotal, tableProps };
|
||||
}
|
||||
Reference in New Issue
Block a user