fix(impuestos): desactivar JIT en queries con subplans correlacionados

- Agrega helper withJitOff en impuestos.service.ts
- Ejecuta getResumenIva, getIvaMensual y readResumenIvaFromCache con SET LOCAL jit = off
- Evita compilación JIT de ~17s en queries con costo estimado alto

feat(contribuyentes): auto-asignar a cartera del supervisor

- Al crear contribuyente con supervisorUserId, se agrega automáticamente
  a todas las carteras top-level del supervisor

feat(permisos): restricciones de UI por rol en contribuyentes

- Oculta botón Add-ons para roles distintos de owner/cfo
- Oculta botón Eliminar contribuyente para no-owner
- Oculta botón Agregar RFC para auxiliar/visor/cliente/contador

feat(cfdi): ver CFDI desde conceptos y forma de pago en Excel

- Agrega botón Ver CFDI en cada fila de la tabla de Conceptos
- Agrega columna Forma de Pago en export Excel de CFDIs
- Agrega columna Forma de Pago en export individual de CFDI

chore(migraciones): índices GIN para relaciones de activos

- 048: índices btree parciales para activos
- 049: índices GIN para cfdis_relacionados y uuid_relacionado
This commit is contained in:
Horux Dev
2026-05-28 02:38:30 +00:00
parent 138e223361
commit 2208cee87f
14 changed files with 390 additions and 152 deletions

View File

@@ -14,6 +14,7 @@ import {
useDesasignarObligacion,
useAsignarTarea,
useDesasignarTarea,
useAuxiliaresElegibles,
} from '@/lib/hooks/use-asignaciones';
import { useUsuarios } from '@/lib/hooks/use-usuarios';
import { useAuthStore } from '@/stores/auth-store';
@@ -36,6 +37,11 @@ export default function SeguimientoAuxiliares() {
const auxiliares = (usuarios ?? []).filter((u: any) => u.role === 'auxiliar');
const { data: elegiblesData, isLoading: loadingElegibles } = useAuxiliaresElegibles(modalItem?.contribuyenteId);
const auxiliaresIdsElegibles = elegiblesData?.auxiliares ?? [];
const auxiliaresFiltrados = auxiliares.filter((a: any) => auxiliaresIdsElegibles.includes(a.id));
const puedeAsignar = !loadingElegibles && auxiliaresFiltrados.length > 0;
const openAssignModal = (type: 'obligacion' | 'tarea', item: any) => {
setModalType(type);
setModalItem(item);
@@ -169,20 +175,28 @@ export default function SeguimientoAuxiliares() {
<p className="text-xs text-muted-foreground mb-4">
Contribuyente: {modalItem?.contribuyenteRazonSocial} ({modalItem?.contribuyenteRfc})
</p>
<Select value={selectedAuxiliar} onValueChange={setSelectedAuxiliar}>
<SelectTrigger>
<SelectValue placeholder="Selecciona un auxiliar" />
</SelectTrigger>
<SelectContent>
{auxiliares.map((a: any) => (
<SelectItem key={a.id} value={a.id}>{a.nombre} ({a.email})</SelectItem>
))}
</SelectContent>
</Select>
{loadingElegibles ? (
<p className="text-sm text-muted-foreground">Verificando subcarteras...</p>
) : auxiliaresFiltrados.length === 0 ? (
<p className="text-sm text-red-600">
Ningún auxiliar tiene este contribuyente en su subcartera. No se puede asignar.
</p>
) : (
<Select value={selectedAuxiliar} onValueChange={setSelectedAuxiliar}>
<SelectTrigger>
<SelectValue placeholder="Selecciona un auxiliar" />
</SelectTrigger>
<SelectContent>
{auxiliaresFiltrados.map((a: any) => (
<SelectItem key={a.id} value={a.id}>{a.nombre} ({a.email})</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setModalOpen(false)}>Cancelar</Button>
<Button onClick={handleAssign} disabled={!selectedAuxiliar}>
<Button onClick={handleAssign} disabled={!selectedAuxiliar || !puedeAsignar}>
{modalItem?.auxiliarUserId ? 'Reasignar' : 'Asignar'}
</Button>
</DialogFooter>

View File

@@ -425,6 +425,7 @@ export default function CfdiPage() {
'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision),
'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante),
'Uso CFDI': (cfdi as any).usoCfdi || '',
'Forma de Pago': cfdi.formaPago || '',
'Serie': cfdi.serie || '',
'Folio': cfdi.folio || '',
'RFC Emisor': cfdi.rfcEmisor,
@@ -541,6 +542,7 @@ export default function CfdiPage() {
'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision),
'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante),
'Uso CFDI': (cfdi as any).usoCfdi || '',
'Forma de Pago': cfdi.formaPago || '',
'Serie': cfdi.serie || '',
'Folio': cfdi.folio || '',
'RFC Emisor': cfdi.rfcEmisor,
@@ -1699,6 +1701,7 @@ export default function CfdiPage() {
)}
</button>
</th>
<th className="pb-3 font-medium"></th>
</tr>
</thead>
<tbody className="text-sm text-center">
@@ -1715,6 +1718,21 @@ export default function CfdiPage() {
<td className="py-2 text-xs" title={row.unidad || ''}>{row.clave_unidad || '-'}</td>
<td className="py-2 text-right">${Number(row.valor_unitario ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>
<td className="py-2 text-right font-medium">${Number(row.importe ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>
<td className="py-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleViewCfdi(row.cfdi_id)}
disabled={loadingCfdi === row.cfdi_id}
title="Ver CFDI"
>
{loadingCfdi === row.cfdi_id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</td>
</tr>
))}
</tbody>

View File

@@ -88,6 +88,8 @@ export default function ContribuyentesPage() {
setShowDialog(true);
};
const canCreate = user?.role === 'owner' || user?.role === 'cfo' || user?.role === 'supervisor';
return (
<div className="p-6 space-y-6 max-w-7xl mx-auto">
<div className="flex items-center justify-between">
@@ -95,14 +97,16 @@ export default function ContribuyentesPage() {
<h1 className="text-2xl font-bold">Contribuyentes</h1>
<p className="text-sm text-muted-foreground">RFCs que gestiona tu despacho · {rfcCounterText}</p>
</div>
<Button
onClick={() => { resetForm(); setShowDialog(true); }}
disabled={trialAtLimit}
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" /> Agregar RFC
</Button>
{canCreate && (
<Button
onClick={() => { resetForm(); setShowDialog(true); }}
disabled={trialAtLimit}
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" /> Agregar RFC
</Button>
)}
</div>
{isLoading ? <p className="text-muted-foreground">Cargando...</p> : !contribuyentes || contribuyentes.length === 0 ? (
@@ -110,13 +114,15 @@ export default function ContribuyentesPage() {
<Building2 className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold">Sin contribuyentes</h3>
<p className="text-sm text-muted-foreground mt-1 mb-4">Agrega el primer RFC para empezar.</p>
<Button
onClick={() => { resetForm(); setShowDialog(true); }}
disabled={trialAtLimit}
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
>
Agregar primer RFC
</Button>
{canCreate && (
<Button
onClick={() => { resetForm(); setShowDialog(true); }}
disabled={trialAtLimit}
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
>
Agregar primer RFC
</Button>
)}
</CardContent></Card>
) : (
<div className="grid gap-3 lg:grid-cols-2 3xl:grid-cols-3 4xl:grid-cols-4">{contribuyentes.map((c) => (
@@ -127,9 +133,13 @@ export default function ContribuyentesPage() {
{c.regimenFiscal && <p className="text-xs text-muted-foreground mt-1">Régimen: {c.regimenFiscal}</p>}
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setAddonsTarget({ id: c.id, nombre: c.nombre })} title="Add-ons"><Sparkles className="h-4 w-4" /></Button>
{(user?.role === 'owner' || user?.role === 'cfo') && (
<Button variant="ghost" size="sm" onClick={() => setAddonsTarget({ id: c.id, nombre: c.nombre })} title="Add-ons"><Sparkles className="h-4 w-4" /></Button>
)}
<Button variant="ghost" size="sm" onClick={() => openEdit(c)}><Pencil className="h-4 w-4" /></Button>
<Button variant="ghost" size="sm" onClick={() => handleDeactivate(c.id, c.rfc)} className="text-destructive hover:text-destructive"><Trash2 className="h-4 w-4" /></Button>
{user?.role === 'owner' && (
<Button variant="ghost" size="sm" onClick={() => handleDeactivate(c.id, c.rfc)} className="text-destructive hover:text-destructive"><Trash2 className="h-4 w-4" /></Button>
)}
</div>
</CardContent></Card>
))}</div>

View File

@@ -56,3 +56,6 @@ export const asignarTarea = (tareaId: string, auxiliarUserId: string) =>
export const desasignarTarea = (tareaId: string) =>
apiClient.delete(`/tareas/${tareaId}/asignar`).then(r => r.data);
export const getAuxiliaresElegibles = (contribuyenteId: string) =>
apiClient.get<{ auxiliares: string[] }>(`/carteras/asignaciones/auxiliares-elegibles/${contribuyenteId}`).then(r => r.data);

View File

@@ -7,6 +7,7 @@ import {
desasignarObligacion,
asignarTarea,
desasignarTarea,
getAuxiliaresElegibles,
} from '../api/asignaciones';
export function useAsignacionesSupervisor() {
@@ -87,3 +88,11 @@ export function useDesasignarTarea() {
},
});
}
export function useAuxiliaresElegibles(contribuyenteId: string | undefined) {
return useQuery({
queryKey: ['auxiliares-elegibles', contribuyenteId],
queryFn: () => getAuxiliaresElegibles(contribuyenteId!),
enabled: !!contribuyenteId,
});
}