feat: metabase auto-registration + ui fixes + migration scripts

- Add metabase.service.ts for automatic DB registration on tenant creation
- Hook createTenant, addTenantToOwner and deleteTenant to sync with Metabase
- Add environment variables for Metabase integration
- Fix dashboard routing for global admin users
- Fix CFDI status casing (Vigente vs vigente)
- Fix sidebar empty nav crash
- Fix KPI null regimen_fiscal values
- Fix CFDI type mapping (EMITIDO/RECIBIDO)
- Update branding from Horux360 to Horux Despachos
- Add legacy migration scripts for central and tenant DBs
This commit is contained in:
Horux Dev
2026-04-28 00:34:41 +00:00
parent 56a05ba767
commit e8dc3aed67
18 changed files with 846 additions and 45 deletions

View File

@@ -7,7 +7,7 @@ import Image from 'next/image';
import { Button, Input, Label, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@horux/shared-ui';
import { login } from '@/lib/api/auth';
import { useAuthStore } from '@/stores/auth-store';
import { isGlobalAdminRfc } from '@horux/shared';
import { isGlobalAdminRfc, type PlatformRole } from '@horux/shared';
export default function LoginPage() {
const router = useRouter();
@@ -32,7 +32,7 @@ export default function LoginPage() {
const userRole = response.user?.role;
// Admin global aterriza directo en `/clientes` — su home natural es la
// gestión de tenants, no el dashboard operativo del despacho.
const platformRoles = (response.user as { platformRoles?: string[] }).platformRoles;
const platformRoles = (response.user as { platformRoles?: PlatformRole[] }).platformRoles;
const isGlobalAdmin = isGlobalAdminRfc(response.user?.tenantRfc, userRole, platformRoles);
if (isGlobalAdmin) {
router.push('/clientes');
@@ -58,7 +58,7 @@ export default function LoginPage() {
<div className="flex justify-center mb-4">
<Image
src="/logo.jpg"
alt="Horux360"
alt="Horux Despachos"
width={80}
height={80}
className="rounded-full"

View File

@@ -776,9 +776,9 @@ export default function CfdiPage() {
const calculateTotal = () => {
const subtotal = formData.subtotal || 0;
const descuento = formData.descuento || 0;
const iva = formData.ivaTrasladoTraslado || 0;
const iva = formData.ivaTraslado || 0;
const isrRetencion = formData.isrRetencion || 0;
const ivaRetencion = formData.ivaTrasladoRetencion || 0;
const ivaRetencion = formData.ivaRetencion || 0;
return subtotal - descuento + iva - isrRetencion - ivaRetencion;
};
@@ -1641,11 +1641,11 @@ export default function CfdiPage() {
<Button
variant="ghost"
size="icon"
onClick={() => handleViewCfdi(cfdi.id)}
disabled={loadingCfdi === cfdi.id}
onClick={() => handleViewCfdi(cfdi.id.toString())}
disabled={loadingCfdi === String(cfdi.id)}
title="Ver factura"
>
{loadingCfdi === cfdi.id ? (
{loadingCfdi === String(cfdi.id) ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Eye className="h-4 w-4" />
@@ -1679,7 +1679,7 @@ export default function CfdiPage() {
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(cfdi.id)}
onClick={() => handleDelete(cfdi.id.toString())}
className="text-destructive hover:text-destructive"
title="Eliminar registro (solo local)"
>

View File

@@ -77,7 +77,7 @@ function RegimenesActivosSection() {
useEffect(() => {
if (activos && catalogo) {
const ids = new Set(activos.map(a => catalogo.find(c => c.clave === a.clave)?.id).filter(Boolean) as number[]);
const ids = new Set(activos.map((a: { clave: string }) => catalogo.find(c => c.clave === a.clave)?.id).filter(Boolean) as number[]);
setSelected(ids);
}
}, [activos, catalogo]);

View File

@@ -1,7 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Header } from '@/components/layouts/header';
import { KpiCard } from '@horux/shared-ui';
import { BarChart } from '@/components/charts/bar-chart';
@@ -9,7 +8,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@horux/shared-ui';
import { PeriodSelector, RegimenSelector } from '@horux/shared-ui';
import { useKpis, useIngresosEgresos, useAlertas, useRegimenesDelPeriodo } from '@/lib/hooks/use-dashboard';
import { useAuthStore } from '@/stores/auth-store';
import { isGlobalAdminRfc } from '@horux/shared';
import {
TrendingUp,
TrendingDown,
@@ -44,16 +42,7 @@ function shiftDatesOneYear(fechaInicio: string, fechaFin: string, delta: number)
}
export default function DashboardPage() {
const router = useRouter();
const { user } = useAuthStore();
// Admin global no opera sobre datos de despacho — su home natural es
// `/clientes` (gestión de tenants). Redirige al primer render.
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles);
useEffect(() => {
if (isGlobalAdmin) router.replace('/clientes');
}, [isGlobalAdmin, router]);
const now = new Date();
const { user } = useAuthStore(); const now = new Date();
const defaultRange = getMonthRange(now.getFullYear(), now.getMonth() + 1);
const [fechaInicio, setFechaInicio] = useState(defaultRange.start);

View File

@@ -14,7 +14,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
import Link from 'next/link';
import { cn } from '@horux/shared-ui';
import { isDespachoTenant } from '@horux/shared';
import type { Role } from '@horux/shared';
import type { Role, UserInvite } from '@horux/shared';
// ── Horux360 legacy roles ─────────────────────────────────────────────────────
const legacyRoleLabels: Record<string, { label: string; icon: React.ElementType; color: string }> = {
@@ -175,7 +175,7 @@ export default function UsuariosPage() {
return;
}
try {
const newUser = await inviteUsuario.mutateAsync(inviteForm);
const newUser = await inviteUsuario.mutateAsync(inviteForm as UserInvite);
// If role is 'cliente' and RFCs were selected, grant access to each
if (inviteForm.role === 'cliente' && selectedRfcIds.length > 0) {
await Promise.all(
@@ -269,11 +269,11 @@ export default function UsuariosPage() {
<SelectValue />
</SelectTrigger>
<SelectContent>
{inviteRoles.map(r => (
{inviteRoles.map((r: any) => (
<SelectItem key={r.value} value={r.value}>
<div className="flex flex-col">
<span>{r.label}</span>
{'description' in r && r.description && (
{r.description && (
<span className="text-xs text-muted-foreground">{r.description}</span>
)}
</div>

View File

@@ -7,7 +7,7 @@ import { QueryProvider } from '@/components/providers/query-provider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Horux360 - Análisis Financiero',
title: 'Horux Despachos',
description: 'Plataforma de análisis financiero y gestión fiscal para empresas mexicanas',
};