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:
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -177,8 +177,8 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Moneda</p>
|
||||
<p className="text-sm font-semibold text-gray-800 mt-1">{cfdi.moneda || 'MXN'}</p>
|
||||
{cfdi.typeCambio && cfdi.typeCambio !== 1 && (
|
||||
<p className="text-xs text-gray-500">TC: {cfdi.typeCambio}</p>
|
||||
{cfdi.tipoCambio && cfdi.tipoCambio !== 1 && (
|
||||
<p className="text-xs text-gray-500">TC: {cfdi.tipoCambio}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
|
||||
@@ -121,7 +121,7 @@ export function CfdiViewerModal({ cfdi, open, onClose }: CfdiViewerModalProps) {
|
||||
let xml = xmlContent;
|
||||
|
||||
if (!xml) {
|
||||
xml = await getCfdiXml(cfdi.id);
|
||||
xml = await getCfdiXml(cfdi.id.toString());
|
||||
}
|
||||
|
||||
if (!xml) {
|
||||
|
||||
@@ -51,13 +51,13 @@ export function SidebarCompact() {
|
||||
const role = user?.role || 'visor';
|
||||
const filteredNav = navigation.filter((item) => {
|
||||
if ('feature' in item && item.feature && !hasFeature(plan, item.feature)) return false;
|
||||
if ('roles' in item && item.roles && !item.roles.includes(role)) return false;
|
||||
if ('roles' in item && item.roles && !item.roles.includes(role as any)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, role, user?.platformRoles);
|
||||
const allNavigation = isGlobalAdmin
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]]
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]].filter(Boolean)
|
||||
: filteredNav;
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -86,7 +86,7 @@ export function SidebarCompact() {
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<Image
|
||||
src="/logo.jpg"
|
||||
alt="Horux360"
|
||||
alt="Horux Despachos"
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full flex-shrink-0"
|
||||
@@ -95,7 +95,7 @@ export function SidebarCompact() {
|
||||
'font-bold text-lg whitespace-nowrap transition-opacity duration-300',
|
||||
expanded ? 'opacity-100' : 'opacity-0 w-0 overflow-hidden'
|
||||
)}>
|
||||
Horux360
|
||||
Horux Despachos
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -49,13 +49,13 @@ export function SidebarFloating() {
|
||||
const role = user?.role || 'visor';
|
||||
const filteredNav = navigation.filter((item) => {
|
||||
if ('feature' in item && item.feature && !hasFeature(plan, item.feature)) return false;
|
||||
if ('roles' in item && item.roles && !item.roles.includes(role)) return false;
|
||||
if ('roles' in item && item.roles && !item.roles.includes(role as any)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, role, user?.platformRoles);
|
||||
const allNavigation = isGlobalAdmin
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]]
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]].filter(Boolean)
|
||||
: filteredNav;
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -76,13 +76,13 @@ export function SidebarFloating() {
|
||||
<div className="flex items-center gap-3 mb-6 px-2">
|
||||
<Image
|
||||
src="/logo.jpg"
|
||||
alt="Horux360"
|
||||
alt="Horux Despachos"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full shadow-lg shadow-primary/25"
|
||||
/>
|
||||
<div>
|
||||
<span className="font-bold text-lg block">Horux360</span>
|
||||
<span className="font-bold text-lg block">Horux Despachos</span>
|
||||
<span className="text-xs text-muted-foreground">Análisis Fiscal</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,7 +108,7 @@ export function Sidebar() {
|
||||
// (Horux 360) no tiene contribuyentes propios y nunca los tendrá.
|
||||
const showOnboarding = (!contribuyentes || contribuyentes.length === 0) && role !== 'cliente' && !isGlobalAdmin;
|
||||
const allNavigation = isGlobalAdmin
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]]
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]].filter(Boolean)
|
||||
: filteredNav;
|
||||
|
||||
return (
|
||||
|
||||
@@ -51,13 +51,13 @@ export function TopNav() {
|
||||
const role = user?.role || 'visor';
|
||||
const filteredNav = navigation.filter((item) => {
|
||||
if ('feature' in item && item.feature && !hasFeature(plan, item.feature)) return false;
|
||||
if ('roles' in item && item.roles && !item.roles.includes(role)) return false;
|
||||
if ('roles' in item && item.roles && !item.roles.includes(role as any)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, role, user?.platformRoles);
|
||||
const allNavigation = isGlobalAdmin
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]]
|
||||
? [...filteredNav.slice(0, -1), ...adminNavigation, filteredNav[filteredNav.length - 1]].filter(Boolean)
|
||||
: filteredNav;
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -79,7 +79,7 @@ export function TopNav() {
|
||||
<div className="h-8 w-8 rounded-lg bg-primary flex items-center justify-center">
|
||||
<span className="text-primary-foreground font-bold text-lg">H</span>
|
||||
</div>
|
||||
<span className="font-bold text-xl">Horux360</span>
|
||||
<span className="font-bold text-xl">Horux Despachos</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
|
||||
Reference in New Issue
Block a user