fase 3.5: licenciamiento por plan (clave firmada, planes basico/profesional/enterprise/custom, modulos incluidos y limites de usuarios/sucursales/citas/almacenamiento, modo aviso o bloqueo, panel de licencia) + API keys gestionables con alcances y revocacion (arregla la API publica que estaba rota) + webhooks salientes firmados con outbox, reintentos y bitacora; herramientas de vendedor y docs

This commit is contained in:
2026-10-11 19:53:44 +00:00
parent ab9d84cb67
commit 3ed0d327d2
30 changed files with 3705 additions and 89 deletions

View File

@@ -49,7 +49,7 @@ import { useGlosario, traducirMenu, traducirEtiqueta } from '../lib/glosario';
import { useClinicSettings } from '../lib/clinic-settings';
import { useChatUnread } from '../lib/chat';
import { useQuickLinks } from '../lib/quicklinks';
import { odooApi } from '../services/odoo';
import { odooApi, licenciaModuloIncluido } from '../services/odoo';
interface LayoutProps {
children: ReactNode;
@@ -122,7 +122,7 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
const navigate = useNavigate();
const { user, canSee, hasRole, logout, brand } = useAuth();
const { term } = useGlosario();
const { membresias: membresiasCfg, compras: comprasCfg, comisiones: comisionesCfg } = useClinicSettings();
const { membresias: membresiasCfg, compras: comprasCfg, comisiones: comisionesCfg, licencia: licenciaCfg } = useClinicSettings();
const topNavItems = useMemo(
() => TOP_NAV_ITEMS.map((it) => ({ ...it, label: traducirEtiqueta(it.label, term) })),
[term]
@@ -144,8 +144,15 @@ export const HomeNestLayout: FC<LayoutProps> = ({ children, title: _title, subti
? { ...s, items: [...s.items, HOME_COMISIONES_ITEM] }
: s));
}
// Licencia en modo bloqueo sin WhatsApp: se ocultan el chat y la sección WACRM.
if (!licenciaModuloIncluido(licenciaCfg, 'whatsapp')) {
base = base
.filter((s) => s.label !== 'WACRM')
.map((s) => ({ ...s, items: s.items.filter((i) => i.href !== '/chat') }))
.filter((s) => s.items.length > 0);
}
return traducirMenu(base, term);
}, [term, membresiasCfg.activo, comprasCfg.activo, comisionesCfg.activo]);
}, [term, membresiasCfg.activo, comprasCfg.activo, comisionesCfg.activo, licenciaCfg]);
const [menuOpen, setMenuOpen] = useState(false);
const [expanded, setExpanded] = useState<string[]>(['Clínica']);
const chatUnread = useChatUnread();