chore: catálogo obligaciones, cierre automático, fixes SAT y facturación
- Catálogo de obligaciones fiscales expandido a 30 entradas con campo requierePago. - Soporte de frecuencia cuatrimestral en obligaciones y declaraciones. - Automatización de cierre de obligaciones fiscales desde Documentos › Declaraciones. - Nuevas tablas obligacion_evidencias, obligacion_periodos estados y declaracion_obligaciones. - Nuevo servicio obligacion-evidencias.service.ts y endpoints REST. - Refactor de declaraciones.service.ts para vincular obligaciones y crear evidencias. - Notificaciones por email para evidencias de obligaciones. - Adjuntar PDFs en correo de declaración subida. - Fix drill-down de CFDIs: carga completa al visualizar. - Fix sincronización SAT: tipos P/N, UUID case-insensitive, no reutilizar requestId. - Fix suscripciones pending en /configuracion/planes-despacho. - Fix sugerencias de Clave Producto SAT: importar catálogo y robustecer autocomplete. - Quitar toggle manual de completado en Configuración › Obligaciones fiscales › Tareas. - Scripts de soporte para Demo Ventas y utilerías (change-user-email, resend-welcome, import-clave-prod-serv). - Documentación de cambios en docs/CAMBIOS-2026-05-04.md.
This commit is contained in:
@@ -23,9 +23,11 @@ import {
|
||||
import { PapeleriaTab } from '@/components/documentos/papeleria-tab';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as docsApi from '@/lib/api/documentos';
|
||||
import { getObligacionesPorPeriodo, type ObligacionPeriodo } from '@/lib/api/obligaciones';
|
||||
|
||||
const MESES = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
|
||||
const IMPUESTOS: Impuesto[] = ['IVA', 'ISR', 'IEPS', 'ISN', 'DIOT', 'OTRO', 'ISH'];
|
||||
const OBLIGACIONES_ROLES_UPLOAD = ['owner', 'cfo', 'contador', 'auxiliar', 'supervisor'];
|
||||
const PERIODICIDADES: { value: Periodicidad; label: string }[] = [
|
||||
{ value: 'mensual', label: 'Mensual' },
|
||||
{ value: 'bimestral', label: 'Bimestral' },
|
||||
@@ -504,7 +506,7 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
|
||||
const [tipo, setTipo] = useState<'normal' | 'complementaria'>('normal');
|
||||
const [periodicidad, setPeriodicidad] = useState<Periodicidad>('mensual');
|
||||
const yearsOptions = Array.from({ length: 6 }, (_, i) => currentYear - i);
|
||||
const [impuestos, setImpuestos] = useState<Impuesto[]>([]);
|
||||
const [obligacionesIds, setObligacionesIds] = useState<string[]>([]);
|
||||
const [montoPago, setMontoPago] = useState('');
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [ligaFile, setLigaFile] = useState<File | null>(null);
|
||||
@@ -512,6 +514,15 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
const periodOptions = getPeriodOptions(periodicidad);
|
||||
const periodo = `${año}-${String(mes).padStart(2, '0')}`;
|
||||
|
||||
const obligacionesQ = useQuery({
|
||||
queryKey: ['obligaciones-periodo-declaracion', selectedContribuyenteId, periodo],
|
||||
queryFn: () => selectedContribuyenteId
|
||||
? getObligacionesPorPeriodo(selectedContribuyenteId, periodo, false)
|
||||
: Promise.resolve({ data: [], periodo }),
|
||||
enabled: !!selectedContribuyenteId,
|
||||
});
|
||||
|
||||
const handlePeriodicidadChange = (p: Periodicidad) => {
|
||||
setPeriodicidad(p);
|
||||
@@ -522,21 +533,21 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleImpuesto = (i: Impuesto) => {
|
||||
setImpuestos(prev => prev.includes(i) ? prev.filter(x => x !== i) : [...prev, i]);
|
||||
const toggleObligacion = (id: string) => {
|
||||
setObligacionesIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
|
||||
};
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErr(null);
|
||||
if (!file) return setErr('Selecciona el PDF de la declaración');
|
||||
if (impuestos.length === 0) return setErr('Selecciona al menos un impuesto');
|
||||
if (obligacionesIds.length === 0) return setErr('Selecciona al menos una obligación fiscal');
|
||||
try {
|
||||
const pdfBase64 = await fileToBase64(file);
|
||||
const ligaPagoBase64 = ligaFile ? await fileToBase64(ligaFile) : undefined;
|
||||
const montoNum = montoPago.trim() !== '' ? parseFloat(montoPago) : undefined;
|
||||
await create.mutateAsync({
|
||||
año, mes, tipo, periodicidad, impuestos,
|
||||
año, mes, tipo, periodicidad, obligacionesIds,
|
||||
montoPago: montoNum,
|
||||
pdfBase64, pdfFilename: file.name,
|
||||
ligaPagoBase64,
|
||||
@@ -606,16 +617,51 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Impuestos cubiertos</Label>
|
||||
<div className="grid grid-cols-3 gap-2 mt-1">
|
||||
{IMPUESTOS.map(i => (
|
||||
<label key={i} className={`flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer text-sm ${impuestos.includes(i) ? 'bg-primary/10 border-primary' : 'hover:bg-muted'}`}>
|
||||
<input type="checkbox" checked={impuestos.includes(i)} onChange={() => toggleImpuesto(i)} className="accent-primary" />
|
||||
{i}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Selecciona todos los impuestos que incluye esta declaración — definen qué recordatorios se desactivan.</p>
|
||||
<Label>Obligaciones fiscales cubiertas</Label>
|
||||
{!selectedContribuyenteId ? (
|
||||
<p className="text-sm text-muted-foreground mt-1">Selecciona un contribuyente para ver sus obligaciones.</p>
|
||||
) : obligacionesQ.isLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Cargando obligaciones...
|
||||
</div>
|
||||
) : obligacionesQ.error ? (
|
||||
<p className="text-sm text-red-600 mt-1">Error al cargar obligaciones.</p>
|
||||
) : obligacionesQ.data?.data.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground mt-1">No hay obligaciones fiscales configuradas para este periodo.</p>
|
||||
) : (
|
||||
<div className="space-y-3 mt-2 max-h-60 overflow-y-auto rounded-md border p-3">
|
||||
{Array.from(new Set((obligacionesQ.data?.data || []).map(o => o.categoria || 'Sin categoría'))).map((categoria) => (
|
||||
<div key={categoria}>
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground mb-1.5">{categoria}</p>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{(obligacionesQ.data?.data || [])
|
||||
.filter(o => (o.categoria || 'Sin categoría') === categoria)
|
||||
.map((o) => (
|
||||
<label
|
||||
key={o.id}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded-md border cursor-pointer text-sm ${obligacionesIds.includes(o.id) ? 'bg-primary/10 border-primary' : 'hover:bg-muted'}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={obligacionesIds.includes(o.id)}
|
||||
onChange={() => toggleObligacion(o.id)}
|
||||
className="accent-primary mt-0.5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium">{o.nombre}</span>
|
||||
<span className="text-xs text-muted-foreground ml-2 capitalize">({o.frecuencia || '—'})</span>
|
||||
{o.requierePago && (
|
||||
<span className="block text-[10px] text-muted-foreground">Requiere comprobante de pago</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">Selecciona las obligaciones fiscales que cubre esta declaración. Al guardar se marcarán como presentadas y, si aplica, quedarán a la espera de su comprobante de pago.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user