Update: nueva version Horux Despachos

This commit is contained in:
consultoria-as
2026-04-27 22:09:36 -06:00
commit 6b36db1403
614 changed files with 125926 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
import { apiClient } from './client';
export interface SubscriptionAddon {
id: string;
codename: string;
nombre: string;
precio: number;
quantity: number;
contribuyenteId: string | null;
status: string;
currentPeriodStart: string | null;
currentPeriodEnd: string | null;
}
export interface AddonsResponse {
subscription: { id: string; plan: string; status: string } | null;
addons: SubscriptionAddon[];
}
export async function listMyAddons(contribuyenteId?: string): Promise<AddonsResponse> {
const params = contribuyenteId ? { contribuyenteId } : undefined;
const { data } = await apiClient.get<AddonsResponse>('/subscriptions/me/addons', { params });
return data;
}
export async function subscribeAddon(params: {
addonCodename: string;
quantity?: number;
contribuyenteId?: string | null;
}): Promise<{ addon: SubscriptionAddon; paymentUrl: string }> {
const { data } = await apiClient.post('/subscriptions/me/addons', params);
return data;
}
export async function cancelAddon(addonId: string): Promise<void> {
await apiClient.delete(`/subscriptions/me/addons/${addonId}`);
}

View File

@@ -0,0 +1,44 @@
import { apiClient } from './client';
export interface ClientesStats {
suscripcionesPorPlan: Array<{ plan: string; count: number }>;
ingresos: { total: number; paymentsCount: number };
noRenovaciones: Array<{
tenantId: string;
tenantNombre: string;
rfc: string;
plan: string;
currentPeriodEnd: string;
statusActual: string;
}>;
usuariosPorCliente: Array<{
tenantId: string;
tenantNombre: string;
rfc: string;
activeUsers: number;
owners: number;
}>;
}
export interface TenantUsuario {
userId: string;
email: string;
nombre: string;
rol: string;
isOwner: boolean;
joinedAt: string;
lastLogin: string | null;
}
export async function getClientesStats(from?: string, to?: string): Promise<ClientesStats> {
const params = new URLSearchParams();
if (from) params.set('from', from);
if (to) params.set('to', to);
const res = await apiClient.get<ClientesStats>(`/admin/clientes/stats?${params}`);
return res.data;
}
export async function getTenantUsuarios(tenantId: string): Promise<TenantUsuario[]> {
const res = await apiClient.get<{ data: TenantUsuario[] }>(`/admin/clientes/${tenantId}/usuarios`);
return res.data.data;
}

View File

@@ -0,0 +1,40 @@
import { apiClient } from './client';
import type { AlertaFull, AlertaCreate, AlertaUpdate, AlertasStats } from '@horux/shared';
export async function getAlertas(filters?: { leida?: boolean; resuelta?: boolean; contribuyenteId?: string }): Promise<AlertaFull[]> {
const params = new URLSearchParams();
if (filters?.leida !== undefined) params.set('leida', String(filters.leida));
if (filters?.resuelta !== undefined) params.set('resuelta', String(filters.resuelta));
if (filters?.contribuyenteId) params.set('contribuyenteId', filters.contribuyenteId);
const response = await apiClient.get<AlertaFull[]>(`/alertas?${params}`);
return response.data;
}
export async function getAlertasAutomaticas(contribuyenteId?: string): Promise<any[]> {
const params = contribuyenteId ? `?contribuyenteId=${encodeURIComponent(contribuyenteId)}` : '';
const response = await apiClient.get<any[]>(`/alertas/automaticas${params}`);
return response.data;
}
export async function getStats(): Promise<AlertasStats> {
const response = await apiClient.get<AlertasStats>('/alertas/stats');
return response.data;
}
export async function createAlerta(data: AlertaCreate): Promise<AlertaFull> {
const response = await apiClient.post<AlertaFull>('/alertas', data);
return response.data;
}
export async function updateAlerta(id: number, data: AlertaUpdate): Promise<AlertaFull> {
const response = await apiClient.patch<AlertaFull>(`/alertas/${id}`, data);
return response.data;
}
export async function deleteAlerta(id: number): Promise<void> {
await apiClient.delete(`/alertas/${id}`);
}
export async function markAllAsRead(): Promise<void> {
await apiClient.post('/alertas/mark-all-read');
}

View File

@@ -0,0 +1,37 @@
import { apiClient } from './client';
export interface AuditLogEntry {
id: string;
userId: string | null;
tenantId: string | null;
action: string;
entityType: string | null;
entityId: string | null;
metadata: Record<string, any> | null;
createdAt: string;
user: { id: string; email: string; nombre: string } | null;
tenant: { id: string; nombre: string; rfc: string } | null;
}
export interface AuditLogFilters {
action?: string;
tenantId?: string;
userId?: string;
from?: string;
to?: string;
page?: number;
limit?: number;
}
export interface AuditLogResponse {
data: AuditLogEntry[];
page: number;
limit: number;
total: number;
totalPages: number;
}
export async function listAuditLog(filters: AuditLogFilters = {}): Promise<AuditLogResponse> {
const response = await apiClient.get<AuditLogResponse>('/audit-log', { params: filters });
return response.data;
}

48
apps/web/lib/api/auth.ts Normal file
View File

@@ -0,0 +1,48 @@
import { apiClient } from './client';
import type { LoginRequest, RegisterRequest, LoginResponse } from '@horux/shared';
export async function login(data: LoginRequest): Promise<LoginResponse> {
const response = await apiClient.post<LoginResponse>('/auth/login', data);
return response.data;
}
export async function register(data: RegisterRequest): Promise<LoginResponse> {
const response = await apiClient.post<LoginResponse>('/auth/register', data);
return response.data;
}
export async function logout(): Promise<void> {
const refreshToken = localStorage.getItem('refreshToken');
await apiClient.post('/auth/logout', { refreshToken });
}
export async function getMe(): Promise<LoginResponse['user']> {
const response = await apiClient.get('/auth/me');
return response.data.user;
}
export async function requestPasswordReset(email: string): Promise<{ message: string }> {
const response = await apiClient.post('/auth/password-reset/request', { email });
return response.data;
}
export async function confirmPasswordReset(token: string, newPassword: string): Promise<{ message: string }> {
const response = await apiClient.post('/auth/password-reset/confirm', { token, newPassword });
return response.data;
}
export async function changePassword(currentPassword: string, newPassword: string): Promise<{ message: string }> {
const response = await apiClient.post('/auth/password-change', { currentPassword, newPassword });
return response.data;
}
export async function logoutAll(): Promise<{ message: string }> {
const response = await apiClient.post('/auth/logout-all');
return response.data;
}
export async function switchTenant(tenantId: string): Promise<LoginResponse> {
const refreshToken = localStorage.getItem('refreshToken') || '';
const response = await apiClient.post<LoginResponse>('/auth/switch-tenant', { tenantId, refreshToken });
return response.data;
}

View File

@@ -0,0 +1,28 @@
import { apiClient } from './client';
export interface Banco {
id: number;
banco: string;
terminacionCuenta: string;
}
export async function getBancos(contribuyenteId?: string | null): Promise<Banco[]> {
const params = new URLSearchParams();
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const res = await apiClient.get<Banco[]>(`/bancos?${params}`);
return res.data;
}
export async function createBanco(data: { banco: string; terminacionCuenta: string; contribuyenteId?: string }): Promise<Banco> {
const res = await apiClient.post<Banco>('/bancos', data);
return res.data;
}
export async function updateBanco(id: number, data: { banco?: string; terminacionCuenta?: string }): Promise<Banco> {
const res = await apiClient.put<Banco>(`/bancos/${id}`, data);
return res.data;
}
export async function deleteBanco(id: number): Promise<void> {
await apiClient.delete(`/bancos/${id}`);
}

View File

@@ -0,0 +1,28 @@
import { apiClient } from './client';
import type { EventoFiscal, EventoCreate, EventoUpdate } from '@horux/shared';
export async function getEventos(año: number, contribuyenteId?: string | null): Promise<EventoFiscal[]> {
const params = new URLSearchParams({ año: año.toString() });
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<EventoFiscal[]>(`/calendario/generados?${params}`);
return response.data;
}
export async function getProximos(dias = 30): Promise<EventoFiscal[]> {
const response = await apiClient.get<EventoFiscal[]>(`/calendario/proximos?dias=${dias}`);
return response.data;
}
export async function createEvento(data: EventoCreate): Promise<EventoFiscal> {
const response = await apiClient.post<EventoFiscal>('/calendario', data);
return response.data;
}
export async function updateEvento(id: number, data: EventoUpdate): Promise<EventoFiscal> {
const response = await apiClient.patch<EventoFiscal>(`/calendario/${id}`, data);
return response.data;
}
export async function deleteEvento(id: number): Promise<void> {
await apiClient.delete(`/calendario/${id}`);
}

View File

@@ -0,0 +1,73 @@
import { apiClient } from './client';
export interface Cartera {
id: string;
supervisorUserId: string | null;
auxiliarUserId: string | null;
parentId: string | null;
nombre: string;
descripcion: string | null;
createdAt: string;
entidadesCount: number;
subcarterasCount: number;
}
export interface SupervisorOption {
userId: string;
nombre: string;
email: string;
}
export async function getCarteras(): Promise<{ data: Cartera[] }> {
const { data } = await apiClient.get('/carteras');
return data;
}
export async function getSupervisores(): Promise<{ data: SupervisorOption[] }> {
const { data } = await apiClient.get('/carteras/supervisores');
return data;
}
export async function createCartera(payload: { nombre: string; descripcion?: string; supervisorUserId?: string }): Promise<Cartera> {
const { data } = await apiClient.post('/carteras', payload);
return data;
}
export async function updateCartera(id: string, payload: { nombre?: string; descripcion?: string }): Promise<Cartera> {
const { data } = await apiClient.put(`/carteras/${id}`, payload);
return data;
}
export async function deleteCartera(id: string): Promise<void> {
await apiClient.delete(`/carteras/${id}`);
}
export async function getCarteraEntidades(id: string): Promise<{ data: string[] }> {
const { data } = await apiClient.get(`/carteras/${id}/entidades`);
return data;
}
export async function addEntidadToCartera(carteraId: string, entidadId: string): Promise<void> {
await apiClient.post(`/carteras/${carteraId}/entidades`, { entidadId });
}
export async function removeEntidadFromCartera(carteraId: string, entidadId: string): Promise<void> {
await apiClient.delete(`/carteras/${carteraId}/entidades/${entidadId}`);
}
// Subcarteras
export async function getSubcarteras(carteraId: string): Promise<{ data: Cartera[] }> {
const { data } = await apiClient.get(`/carteras/${carteraId}/subcarteras`);
return data;
}
export async function createSubcartera(carteraId: string, payload: { nombre: string; descripcion?: string; auxiliarUserId: string }): Promise<Cartera> {
const { data } = await apiClient.post(`/carteras/${carteraId}/subcarteras`, payload);
return data;
}
// Auxiliares del supervisor (for subcartera assignment)
export async function getAuxiliaresDelSupervisor(supervisorId: string): Promise<{ data: Array<{ auxiliarUserId: string }> }> {
const { data } = await apiClient.get(`/carteras/${supervisorId}/auxiliares-disponibles`);
return data;
}

View File

@@ -0,0 +1,24 @@
import { apiClient } from './client';
export interface CatalogoItem {
id: number;
clave: string;
descripcion: string;
}
export interface UsoCfdiItem extends CatalogoItem {
personaFisica: boolean;
personaMoral: boolean;
}
export interface MonedaItem extends CatalogoItem {
decimales: number;
}
export const getFormasPago = () => apiClient.get<CatalogoItem[]>('/catalogos/forma-pago').then(r => r.data);
export const getMetodosPago = () => apiClient.get<CatalogoItem[]>('/catalogos/metodo-pago').then(r => r.data);
export const getUsosCfdi = () => apiClient.get<UsoCfdiItem[]>('/catalogos/uso-cfdi').then(r => r.data);
export const getMonedas = () => apiClient.get<MonedaItem[]>('/catalogos/moneda').then(r => r.data);
export const getClavesUnidad = () => apiClient.get<CatalogoItem[]>('/catalogos/clave-unidad').then(r => r.data);
export const searchClaveProdServ = (q: string) => apiClient.get<CatalogoItem[]>(`/catalogos/clave-prod-serv?q=${encodeURIComponent(q)}`).then(r => r.data);
export const getObjetosImp = () => apiClient.get<CatalogoItem[]>('/catalogos/objeto-imp').then(r => r.data);

135
apps/web/lib/api/cfdi.ts Normal file
View File

@@ -0,0 +1,135 @@
import { apiClient } from './client';
import type { CfdiListResponse, CfdiFilters, Cfdi } from '@horux/shared';
export async function getCfdis(filters: CfdiFilters): Promise<CfdiListResponse> {
const params = new URLSearchParams();
if (filters.tipo) params.set('tipo', filters.tipo);
if (filters.tipoComprobante) params.set('tipoComprobante', filters.tipoComprobante);
if (filters.estado) params.set('estado', filters.estado);
if (filters.fechaInicio) params.set('fechaInicio', filters.fechaInicio);
if (filters.fechaFin) params.set('fechaFin', filters.fechaFin);
if (filters.rfc) params.set('rfc', filters.rfc);
if (filters.emisor) params.set('emisor', filters.emisor);
if (filters.receptor) params.set('receptor', filters.receptor);
if (filters.search) params.set('search', filters.search);
if (filters.page) params.set('page', filters.page.toString());
if (filters.limit) params.set('limit', filters.limit.toString());
if (filters.contribuyenteId) params.set('contribuyenteId', filters.contribuyenteId);
const response = await apiClient.get<CfdiListResponse>(`/cfdi?${params}`);
return response.data;
}
export async function getCfdiById(id: string): Promise<Cfdi> {
const response = await apiClient.get<Cfdi>(`/cfdi/${id}`);
return response.data;
}
export async function getResumenCfdi(año?: number, mes?: number, contribuyenteId?: string) {
const params = new URLSearchParams();
if (año) params.set('año', año.toString());
if (mes) params.set('mes', mes.toString());
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get(`/cfdi/resumen?${params}`);
return response.data;
}
export interface CreateCfdiData {
uuid: string;
type: 'EMITIDO' | 'RECIBIDO';
serie?: string;
folio?: string;
status?: string;
fechaEmision: string;
rfcEmisor: string;
nombreEmisor: string;
rfcReceptor: string;
nombreReceptor: string;
subtotal: number;
subtotalMxn?: number;
descuento?: number;
descuentoMxn?: number;
total: number;
totalMxn?: number;
moneda?: string;
tipoCambio?: number;
tipoComprobante?: string;
metodoPago?: string;
formaPago?: string;
usoCfdi?: string;
ivaTraslado?: number;
ivaTrasladoMxn?: number;
isrRetencion?: number;
isrRetencionMxn?: number;
ivaRetencion?: number;
ivaRetencionMxn?: number;
}
export async function createCfdi(data: CreateCfdiData): Promise<Cfdi> {
const response = await apiClient.post<Cfdi>('/cfdi', data);
return response.data;
}
export interface BatchUploadResult {
message: string;
batchNumber: number;
totalBatches: number;
inserted: number;
duplicates: number;
errors: number;
errorMessages?: string[];
}
export async function createManyCfdis(
cfdis: CreateCfdiData[],
batchNumber?: number,
totalBatches?: number,
totalFiles?: number
): Promise<BatchUploadResult> {
const response = await apiClient.post<BatchUploadResult>('/cfdi/bulk', {
cfdis,
batchNumber: batchNumber || 1,
totalBatches: totalBatches || 1,
totalFiles: totalFiles || cfdis.length
});
return response.data;
}
export async function getCfdiConceptos(id: number | string): Promise<any[]> {
const response = await apiClient.get<any[]>(`/cfdi/${id}/conceptos`);
return response.data;
}
export async function deleteCfdi(id: string): Promise<void> {
await apiClient.delete(`/cfdi/${id}`);
}
export async function getCfdiXml(id: string): Promise<string> {
const response = await apiClient.get<string>(`/cfdi/${id}/xml`, {
responseType: 'text'
});
return response.data;
}
export interface EmisorReceptor {
rfc: string;
nombre: string;
}
export async function searchEmisores(search: string, contribuyenteId?: string): Promise<EmisorReceptor[]> {
if (search.length < 2) return [];
const params = new URLSearchParams({ search });
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<EmisorReceptor[]>(`/cfdi/emisores?${params}`);
return response.data;
}
export async function searchReceptores(search: string, contribuyenteId?: string): Promise<EmisorReceptor[]> {
if (search.length < 2) return [];
const params = new URLSearchParams({ search });
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<EmisorReceptor[]>(`/cfdi/receptores?${params}`);
return response.data;
}

View File

@@ -0,0 +1,79 @@
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api',
headers: {
'Content-Type': 'application/json',
},
});
apiClient.interceptors.request.use((config) => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Add viewing tenant header for admin users
const tenantViewStore = localStorage.getItem('horux-tenant-view');
if (tenantViewStore) {
try {
const { state } = JSON.parse(tenantViewStore);
if (state?.viewingTenantId) {
config.headers['X-View-Tenant'] = state.viewingTenantId;
}
} catch {
// Ignore parse errors
}
}
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Rate limit hit. El backend envía { message } — lo preservamos para que los
// try/catch existentes (que leen err.response.data.message) muestren la razón
// correcta. Además mostramos un alert visible como fallback si nadie maneja.
if (error.response?.status === 429) {
const msg =
error.response?.data?.message ||
'Demasiadas solicitudes. Espera unos minutos e intenta de nuevo.';
if (typeof window !== 'undefined' && !originalRequest?._rateLimitHandled) {
originalRequest._rateLimitHandled = true;
console.warn('[rate-limit]', msg);
}
return Promise.reject(error);
}
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api'}/auth/refresh`,
{ refreshToken }
);
const { accessToken, refreshToken: newRefreshToken } = response.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return apiClient(originalRequest);
}
} catch {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);

View File

@@ -0,0 +1,58 @@
import { apiClient } from './client';
export interface ConciliacionCfdi {
id: number;
uuid: string;
type: string;
fechaEmision: string;
rfcEmisor: string;
nombreEmisor: string;
rfcReceptor: string;
nombreReceptor: string;
total: number;
totalMxn: number;
tipoComprobante: string | null;
montoPagoMxn: number;
montoMxn: number;
metodoPago: string | null;
conciliado: string | null;
idConciliacion: number | null;
conciliacion: {
id: number;
fechaDePago: string;
banco: string;
terminacionCuenta: string;
} | null;
}
export async function getCfdisConConciliacion(params: {
tipo: string;
fechaInicio?: string;
fechaFin?: string;
regimen?: string;
estado?: string;
contribuyenteId?: string;
}): Promise<ConciliacionCfdi[]> {
const q = new URLSearchParams();
q.set('tipo', params.tipo);
if (params.fechaInicio) q.set('fechaInicio', params.fechaInicio);
if (params.fechaFin) q.set('fechaFin', params.fechaFin);
if (params.regimen) q.set('regimen', params.regimen);
if (params.estado) q.set('estado', params.estado);
if (params.contribuyenteId) q.set('contribuyenteId', params.contribuyenteId);
const res = await apiClient.get<ConciliacionCfdi[]>(`/conciliacion?${q}`);
return res.data;
}
export async function conciliar(data: {
cfdiIds: number[];
fechaDePago: string;
idBanco: number;
}): Promise<{ count: number }> {
const res = await apiClient.post<{ count: number }>('/conciliacion', data);
return res.data;
}
export async function desconciliar(id: number): Promise<void> {
await apiClient.delete(`/conciliacion/${id}`);
}

View File

@@ -0,0 +1,78 @@
import { apiClient } from './client';
export interface DomicilioCsf {
codigoPostal?: string;
tipoVialidad?: string;
nombreVialidad?: string;
numeroExterior?: string;
numeroInterior?: string;
colonia?: string;
localidad?: string;
municipio?: string;
entidadFederativa?: string;
entreCalle?: string;
yCalle?: string;
}
export interface RegimenCsf {
nombre: string;
fechaInicio: string;
fechaFin?: string;
}
export interface ObligacionCsf {
descripcion: string;
descripcionVencimiento: string;
fechaInicio: string;
fechaFin?: string;
}
export interface ActividadEconomicaCsf {
orden: number;
descripcion: string;
porcentaje: number;
fechaInicio: string;
fechaFin?: string;
}
export interface ConstanciaDatos {
rfc: string;
curp?: string;
idCIF: string;
nombre?: string;
primerApellido?: string;
segundoApellido?: string;
razonSocial?: string;
nombreComercial?: string;
fechaInicioOperaciones: string;
estatusPadron: string;
fechaUltimoCambioEstado?: string;
lugarFechaEmision: string;
domicilio: DomicilioCsf;
actividadesEconomicas: ActividadEconomicaCsf[];
regimenes: RegimenCsf[];
obligaciones: ObligacionCsf[];
}
export interface Constancia {
id: number;
rfc: string;
idCif: string | null;
razonSocial: string | null;
estatusPadron: string | null;
fechaEmision: string | null;
datos: ConstanciaDatos;
fechaConsulta: string;
createdAt: string;
}
export const listConstancias = (contribuyenteId?: string) => {
const params = contribuyenteId ? `?contribuyenteId=${encodeURIComponent(contribuyenteId)}` : '';
return apiClient.get<Constancia[]>(`/documentos/constancias${params}`).then(r => r.data);
};
export const consultarConstancia = () =>
apiClient.post<Constancia>('/documentos/constancias/consultar').then(r => r.data);
export const descargarConstanciaPdf = (id: number) =>
apiClient.get(`/documentos/constancias/${id}/pdf`, { responseType: 'blob' }).then(r => r.data as Blob);

View File

@@ -0,0 +1,66 @@
import { apiClient } from './client';
export interface Contribuyente {
id: string;
tipo: string;
nombre: string;
identificador: string;
supervisorUserId: string | null;
active: boolean;
createdAt: string;
rfc: string;
regimenFiscal: string | null;
codigoPostal: string | null;
domicilio: Record<string, unknown> | null;
}
export interface CreateContribuyenteData {
rfc: string;
razonSocial: string;
regimenFiscal?: string;
codigoPostal?: string;
supervisorUserId?: string;
}
/**
* Resultado del ajuste automático de overage de Business Cloud al crear o
* desactivar un contribuyente. Si `action === 'created'`, el frontend debe
* abrir `paymentUrl` en una pestaña para que el usuario autorice el cobro.
*/
export interface OverageAdjustResult {
action: 'none' | 'created' | 'updated' | 'cancelled' | 'skipped';
overageCount: number;
paymentUrl?: string;
reason?: string;
}
export type ContribuyenteWithOverage = Contribuyente & { overage?: OverageAdjustResult };
export async function getContribuyentes(): Promise<{ data: Contribuyente[] }> {
const { data } = await apiClient.get('/contribuyentes');
return data;
}
export async function getContribuyente(id: string): Promise<Contribuyente> {
const { data } = await apiClient.get(`/contribuyentes/${id}`);
return data;
}
export async function createContribuyente(payload: CreateContribuyenteData): Promise<ContribuyenteWithOverage> {
const { data } = await apiClient.post('/contribuyentes', payload);
return data;
}
export async function updateContribuyente(id: string, payload: Partial<CreateContribuyenteData>): Promise<Contribuyente> {
const { data } = await apiClient.put(`/contribuyentes/${id}`, payload);
return data;
}
export async function deactivateContribuyente(id: string): Promise<{ message: string; overage?: OverageAdjustResult }> {
const { data } = await apiClient.delete(`/contribuyentes/${id}`);
return data;
}
export async function addClienteAcceso(contribuyenteId: string, userId: string): Promise<void> {
await apiClient.post(`/contribuyentes/${contribuyenteId}/cliente-acceso`, { userId });
}

View File

@@ -0,0 +1,41 @@
import { apiClient } from './client';
import type { KpiData, IngresosEgresosData, Alerta } from '@horux/shared';
export async function getKpis(fechaInicio: string, fechaFin: string, conciliacion?: boolean, contribuyenteId?: string | null): Promise<KpiData> {
const params = new URLSearchParams();
params.set('fechaInicio', fechaInicio);
params.set('fechaFin', fechaFin);
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<KpiData>(`/dashboard/kpis?${params}`);
return response.data;
}
export async function getIngresosEgresos(año?: number, conciliacion?: boolean, contribuyenteId?: string | null): Promise<IngresosEgresosData[]> {
const params = new URLSearchParams();
if (año) params.set('año', año.toString());
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<IngresosEgresosData[]>(`/dashboard/ingresos-egresos?${params}`);
return response.data;
}
export async function getRegimenesDelPeriodo(fechaInicio: string, fechaFin: string, conciliacion?: boolean, contribuyenteId?: string | null): Promise<{ clave: string; descripcion: string }[]> {
const params = new URLSearchParams();
params.set('fechaInicio', fechaInicio);
params.set('fechaFin', fechaFin);
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<{ clave: string; descripcion: string }[]>(`/dashboard/regimenes-periodo?${params}`);
return response.data;
}
export async function getAlertas(limit = 5, contribuyenteId?: string | null): Promise<Alerta[]> {
const params = new URLSearchParams({ limit: String(limit) });
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<Alerta[]>(`/dashboard/alertas?${params}`);
return response.data;
}

View File

@@ -0,0 +1,87 @@
import { apiClient } from './client';
export type Impuesto = 'IVA' | 'ISR' | 'IEPS' | 'SUELDOS' | 'DIOT' | 'OTRO';
export type Periodicidad = 'mensual' | 'bimestral' | 'trimestral' | 'semestral' | 'anual';
export interface Declaracion {
id: number;
año: number;
mes: number;
tipo: 'normal' | 'complementaria';
periodicidad: Periodicidad;
impuestos: Impuesto[];
montoPago: number | null;
pdfFilename: string | null;
ligaPagoFilename: string | null;
pdfPagoFilename: string | null;
pagadoAt: string | null;
creadoPor: string | null;
notas: string | null;
createdAt: string;
updatedAt: string;
tieneLigaPago: boolean;
tienePagoPdf: boolean;
}
export interface CreateDeclaracionData {
año: number;
mes: number;
tipo: 'normal' | 'complementaria';
periodicidad?: Periodicidad;
impuestos: Impuesto[];
montoPago?: number;
pdfBase64: string;
pdfFilename: string;
ligaPagoBase64?: string;
ligaPagoFilename?: string;
notas?: string;
contribuyenteId?: string;
}
export const listDeclaraciones = (fechaDesde?: string, fechaHasta?: string, contribuyenteId?: string | null) => {
const params = new URLSearchParams();
if (fechaDesde) params.set('fechaDesde', fechaDesde);
if (fechaHasta) params.set('fechaHasta', fechaHasta);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
return apiClient.get<Declaracion[]>(`/documentos/declaraciones?${params}`).then(r => r.data);
};
export const createDeclaracion = (data: CreateDeclaracionData) =>
apiClient.post<{ declaracion: Declaracion; alertasResueltas: number }>('/documentos/declaraciones', data).then(r => r.data);
export const uploadComprobantePago = (id: number, pdfBase64: string, pdfFilename: string) =>
apiClient.post<{ declaracion: Declaracion; alertasResueltas: number }>(
`/documentos/declaraciones/${id}/comprobante-pago`,
{ pdfBase64, pdfFilename },
).then(r => r.data);
export const deleteDeclaracion = (id: number) =>
apiClient.delete(`/documentos/declaraciones/${id}`).then(r => r.data);
export const downloadDeclaracionPdf = (id: number, variant: 'declaracion' | 'liga' | 'pago') =>
apiClient.get(`/documentos/declaraciones/${id}/pdf/${variant}`, { responseType: 'blob' }).then(r => r.data as Blob);
export function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// result format: "data:application/pdf;base64,..."
const base64 = result.split(',')[1] || '';
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
export function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}

View File

@@ -0,0 +1,90 @@
import { apiClient } from './client';
import type { OpinionCumplimiento } from '@horux/shared';
export async function getOpiniones(contribuyenteId?: string): Promise<OpinionCumplimiento[]> {
const params = contribuyenteId ? `?contribuyenteId=${encodeURIComponent(contribuyenteId)}` : '';
const { data } = await apiClient.get(`/documentos/opiniones${params}`);
return data;
}
export async function descargarOpinionPdf(id: number): Promise<Blob> {
const { data } = await apiClient.get(`/documentos/opiniones/${id}/pdf`, {
responseType: 'blob',
});
return data;
}
export async function consultarOpinion(contribuyenteId?: string): Promise<OpinionCumplimiento> {
const params = contribuyenteId ? `?contribuyenteId=${encodeURIComponent(contribuyenteId)}` : '';
const { data } = await apiClient.post(`/documentos/opiniones/consultar${params}`);
return data;
}
export async function consultarConstancia(contribuyenteId?: string): Promise<any> {
const params = contribuyenteId ? `?contribuyenteId=${encodeURIComponent(contribuyenteId)}` : '';
const { data } = await apiClient.post(`/documentos/constancia/consultar${params}`);
return data;
}
// ──────────────────────────────────────────────────────────────────
// Documentos Extras — PDFs libres (acuses, contratos, poderes, etc.)
// ──────────────────────────────────────────────────────────────────
export interface DocumentoExtra {
id: number;
contribuyenteId: string | null;
nombre: string;
descripcion: string | null;
categoria: string | null;
pdfFilename: string;
subidoPor: string | null;
createdAt: string;
}
export interface CreateExtraInput {
nombre: string;
descripcion?: string;
categoria?: string;
pdfBase64: string;
pdfFilename: string;
}
export async function listarExtras(
contribuyenteId?: string | null,
categoria?: string | null,
): Promise<DocumentoExtra[]> {
const params = new URLSearchParams();
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
if (categoria) params.set('categoria', categoria);
const { data } = await apiClient.get(`/documentos/extras?${params}`);
return data;
}
export async function listarCategoriasExtras(
contribuyenteId?: string | null,
): Promise<string[]> {
const params = new URLSearchParams();
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const { data } = await apiClient.get(`/documentos/extras/categorias?${params}`);
return data;
}
export async function crearExtra(
input: CreateExtraInput,
contribuyenteId?: string | null,
): Promise<DocumentoExtra> {
const body = contribuyenteId ? { ...input, contribuyenteId } : input;
const { data } = await apiClient.post('/documentos/extras', body);
return data;
}
export async function descargarExtraPdf(id: number): Promise<Blob> {
const { data } = await apiClient.get(`/documentos/extras/${id}/pdf`, {
responseType: 'blob',
});
return data;
}
export async function eliminarExtra(id: number): Promise<void> {
await apiClient.delete(`/documentos/extras/${id}`);
}

View File

@@ -0,0 +1,172 @@
import { apiClient } from './client';
export interface OrgStatus {
configured: boolean;
orgId?: string;
legalName?: string;
hasCsd?: boolean;
}
export interface TimbreStatus {
configured: boolean;
// Backward compat (flat fields representan el pool mensual)
tipo?: string;
limite?: number;
usados?: number;
disponibles?: number;
periodoFin?: string;
// Shape extendido
mensual?: {
tipo: string;
limite: number;
usados: number;
disponibles: number;
periodoFin: string;
};
adicionales?: {
total: number;
usados: number;
disponibles: number;
paquetes: Array<{
id: number;
cantidad: number;
usados: number;
disponibles: number;
adquiridoEn: string;
expiraEn: string;
}>;
};
totalDisponibles?: number;
}
export interface InvoiceCustomer {
legalName: string;
taxId: string;
taxSystem: string;
email?: string;
zip: string;
}
export interface InvoiceLineItem {
description: string;
productKey: string;
unitKey?: string;
unitName?: string;
quantity: number;
price: number;
taxIncluded?: boolean;
taxes?: Array<{ type: string; rate: number }>;
}
export interface InvoiceData {
customer: InvoiceCustomer;
items: InvoiceLineItem[];
use: string;
paymentForm: string;
paymentMethod?: string;
currency?: string;
exchangeRate?: number;
series?: string;
folioNumber?: number;
conditions?: string;
}
export interface InvoiceResult {
id: string;
uuid: string;
total: number;
status: string;
}
export const getOrgStatus = () => apiClient.get<OrgStatus>('/facturacion/org/status').then(r => r.data);
export const createOrg = () => apiClient.post('/facturacion/org').then(r => r.data);
export const uploadCsd = (data: { cerFile: string; keyFile: string; password: string }) =>
apiClient.post('/facturacion/csd', data).then(r => r.data);
export const getTimbres = () => apiClient.get<TimbreStatus>('/facturacion/timbres').then(r => r.data);
export interface PaqueteCatalogo {
id: number;
cantidad: number;
precio: number;
}
export const getPaquetesCatalogo = () =>
apiClient.get<PaqueteCatalogo[]>('/facturacion/timbres/paquetes-catalogo').then(r => r.data);
export const comprarPaquete = (catalogoId: number) =>
apiClient.post<{ paymentId: string; checkoutUrl: string }>('/facturacion/timbres/paquetes/comprar', { catalogoId })
.then(r => r.data);
export interface PaqueteCatalogoAdmin {
id: number;
cantidad: number;
precio: number;
active: boolean;
updatedAt: string;
}
export const getPaquetesCatalogoAdmin = () =>
apiClient.get<PaqueteCatalogoAdmin[]>('/facturacion/timbres/paquetes-catalogo/admin').then(r => r.data);
export const updatePaqueteCatalogo = (id: number, data: { precio?: number; active?: boolean }) =>
apiClient.put<PaqueteCatalogoAdmin>(`/facturacion/timbres/paquetes-catalogo/${id}`, data).then(r => r.data);
export const emitirFactura = (data: InvoiceData) =>
apiClient.post<InvoiceResult>('/facturacion/emitir', data).then(r => r.data);
export const cancelarFactura = (uuid: string, motive?: string, substitution?: string) =>
apiClient.post(`/facturacion/cancelar/${uuid}`, { motive, substitution }).then(r => r.data);
export const downloadPdf = (id: string) =>
apiClient.get(`/facturacion/pdf/${id}`, { responseType: 'blob' }).then(r => r.data);
export const downloadXml = (id: string) =>
apiClient.get(`/facturacion/xml/${id}`, { responseType: 'blob' }).then(r => r.data);
export interface RfcSearchResult {
id: number;
rfc: string;
razonSocial: string | null;
regimenFiscal: string | null;
codigoPostal: string | null;
}
export const searchRfcs = (q: string) =>
apiClient.get<RfcSearchResult[]>(`/facturacion/rfcs/search?q=${encodeURIComponent(q)}`).then(r => r.data);
export interface CfdiPpdPendiente {
uuid: string;
serie: string | null;
folio: string | null;
totalMxn: number;
fechaEmision: string;
rfcReceptor: string;
nombreReceptor: string;
ivaTrasladoMxn: number;
saldoPendiente: number;
}
export const getCfdisPpd = (rfc: string) =>
apiClient.get<CfdiPpdPendiente[]>(`/facturacion/cfdis-ppd?rfc=${encodeURIComponent(rfc)}`).then(r => r.data);
export interface ConceptoPrevio {
claveProdServ: string;
descripcion: string;
claveUnidad: string | null;
unidad: string | null;
valorUnitario: number;
importe: number;
ivaTraslado: number;
isrRetencion: number;
ivaRetencion: number;
tipoCfdi: string;
rfcEmisor: string;
nombreEmisor: string;
rfcReceptor: string;
nombreReceptor: string;
fechaEmision: string;
}
export const searchConceptos = (q: string, tipo?: string, contribuyenteId?: string | null) => {
const params = new URLSearchParams();
if (q) params.set('q', q);
if (tipo) params.set('tipo', tipo);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
return apiClient.get<ConceptoPrevio[]>(`/facturacion/conceptos/search?${params}`).then(r => r.data);
};

41
apps/web/lib/api/fiel.ts Normal file
View File

@@ -0,0 +1,41 @@
import { apiClient } from './client';
import type { FielStatus, FielUploadRequest } from '@horux/shared';
/**
* FIEL API — contribuyente-aware for despachos.
* If contribuyenteId is provided, uses per-contribuyente endpoints (tenant BD).
* Otherwise, uses legacy tenant-level endpoints (central BD).
*/
export async function uploadFiel(data: FielUploadRequest, contribuyenteId?: string | null): Promise<{ message: string; status: FielStatus }> {
if (contribuyenteId) {
const response = await apiClient.post(`/contribuyentes/${contribuyenteId}/fiel`, data);
return response.data;
}
const response = await apiClient.post('/fiel/upload', data);
return response.data;
}
export async function getFielStatus(contribuyenteId?: string | null): Promise<FielStatus> {
if (contribuyenteId) {
const response = await apiClient.get<FielStatus>(`/contribuyentes/${contribuyenteId}/fiel/status`);
return response.data;
}
const response = await apiClient.get<FielStatus>('/fiel/status');
return response.data;
}
export async function deleteFiel(contribuyenteId?: string | null): Promise<void> {
if (contribuyenteId) {
// Try per-contribuyente delete first, then legacy as fallback
try {
await apiClient.delete(`/contribuyentes/${contribuyenteId}/fiel`);
return;
} catch {
// Fallback to legacy if per-contribuyente endpoint doesn't exist
try { await apiClient.delete('/fiel'); } catch { /* ignore */ }
return;
}
}
await apiClient.delete('/fiel');
}

View File

@@ -0,0 +1,76 @@
import { apiClient } from './client';
import type { IvaMensual, IsrMensual, ResumenIva, ResumenIsr, ResumenIsrDesglosado } from '@horux/shared';
export async function getIsrMensual(año?: number, conciliacion?: boolean, contribuyenteId?: string | null, regimenClave?: string | null, considerarActivos?: boolean, considerarNCs?: boolean): Promise<IsrMensual[]> {
const params = new URLSearchParams();
if (año) params.set('año', año.toString());
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
if (regimenClave) params.set('regimenClave', regimenClave);
if (considerarActivos !== undefined) params.set('considerarActivos', String(considerarActivos));
if (considerarNCs !== undefined) params.set('considerarNCs', String(considerarNCs));
const response = await apiClient.get<IsrMensual[]>(`/impuestos/isr/mensual?${params}`);
return response.data;
}
export async function getIvaMensual(año?: number, conciliacion?: boolean, contribuyenteId?: string | null, considerarActivos?: boolean, considerarNCs?: boolean): Promise<IvaMensual[]> {
const params = new URLSearchParams();
if (año) params.set('año', año.toString());
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
if (considerarActivos !== undefined) params.set('considerarActivos', String(considerarActivos));
if (considerarNCs !== undefined) params.set('considerarNCs', String(considerarNCs));
const response = await apiClient.get<IvaMensual[]>(`/impuestos/iva/mensual?${params}`);
return response.data;
}
export async function getResumenIva(fechaInicio: string, fechaFin: string, conciliacion?: boolean, contribuyenteId?: string | null, considerarActivos?: boolean, considerarNCs?: boolean): Promise<ResumenIva> {
const params = new URLSearchParams();
params.set('fechaInicio', fechaInicio);
params.set('fechaFin', fechaFin);
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
if (considerarActivos !== undefined) params.set('considerarActivos', String(considerarActivos));
if (considerarNCs !== undefined) params.set('considerarNCs', String(considerarNCs));
const response = await apiClient.get<ResumenIva>(`/impuestos/iva/resumen?${params}`);
return response.data;
}
export async function getCoeficiente(anio: number): Promise<{ anio: number; coeficiente: number | null }> {
const response = await apiClient.get<{ anio: number; coeficiente: number | null }>(`/impuestos/isr/coeficiente?anio=${anio}`);
return response.data;
}
export async function setCoeficiente(anio: number, coeficiente: number): Promise<{ anio: number; coeficiente: number }> {
const response = await apiClient.put<{ anio: number; coeficiente: number }>('/impuestos/isr/coeficiente', { anio, coeficiente });
return response.data;
}
export async function getResumenIsr(fechaInicio: string, fechaFin: string, conciliacion?: boolean, contribuyenteId?: string | null, considerarActivos?: boolean, considerarNCs?: boolean): Promise<ResumenIsr> {
const params = new URLSearchParams();
params.set('fechaInicio', fechaInicio);
params.set('fechaFin', fechaFin);
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
if (considerarActivos !== undefined) params.set('considerarActivos', String(considerarActivos));
if (considerarNCs !== undefined) params.set('considerarNCs', String(considerarNCs));
const response = await apiClient.get<ResumenIsr>(`/impuestos/isr/resumen?${params}`);
return response.data;
}
export async function getResumenIsrDesglosado(
fechaFin: string,
conciliacion?: boolean,
contribuyenteId?: string | null,
considerarActivos?: boolean,
considerarNCs?: boolean,
): Promise<ResumenIsrDesglosado> {
const params = new URLSearchParams();
params.set('fechaFin', fechaFin);
if (conciliacion) params.set('conciliacion', 'true');
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
if (considerarActivos !== undefined) params.set('considerarActivos', String(considerarActivos));
if (considerarNCs !== undefined) params.set('considerarNCs', String(considerarNCs));
const response = await apiClient.get<ResumenIsrDesglosado>(`/impuestos/isr/resumen-desglosado?${params}`);
return response.data;
}

View File

@@ -0,0 +1,37 @@
import { apiClient } from './client';
import type { PlatformRole } from '@horux/shared';
export interface PlatformStaffUser {
id: string;
email: string;
nombre: string;
active: boolean;
tenant: { id: string; nombre: string; rfc: string } | null;
roles: PlatformRole[];
}
export interface CandidateUser {
id: string;
email: string;
nombre: string;
active: boolean;
tenant: { id: string; nombre: string; rfc: string } | null;
}
export async function listStaff(): Promise<PlatformStaffUser[]> {
const response = await apiClient.get<PlatformStaffUser[]>('/platform-staff');
return response.data;
}
export async function searchUsers(q: string): Promise<CandidateUser[]> {
const response = await apiClient.get<CandidateUser[]>('/platform-staff/search', { params: { q } });
return response.data;
}
export async function grantRole(userId: string, role: PlatformRole): Promise<void> {
await apiClient.post('/platform-staff/grant', { userId, role });
}
export async function revokeRole(userId: string, role: PlatformRole): Promise<void> {
await apiClient.post('/platform-staff/revoke', { userId, role });
}

View File

@@ -0,0 +1,69 @@
import { apiClient } from './client';
import type { EstadoResultados, FlujoEfectivo, ComparativoPeriodos, ConcentradoRfc } from '@horux/shared';
export async function getEstadoResultados(fechaInicio?: string, fechaFin?: string, contribuyenteId?: string): Promise<EstadoResultados> {
const params = new URLSearchParams();
if (fechaInicio) params.set('fechaInicio', fechaInicio);
if (fechaFin) params.set('fechaFin', fechaFin);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<EstadoResultados>(`/reportes/estado-resultados?${params}`);
return response.data;
}
export async function getFlujoEfectivo(fechaInicio?: string, fechaFin?: string, contribuyenteId?: string): Promise<FlujoEfectivo> {
const params = new URLSearchParams();
if (fechaInicio) params.set('fechaInicio', fechaInicio);
if (fechaFin) params.set('fechaFin', fechaFin);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<FlujoEfectivo>(`/reportes/flujo-efectivo?${params}`);
return response.data;
}
export async function getComparativo(año?: number, contribuyenteId?: string): Promise<ComparativoPeriodos> {
const params = new URLSearchParams();
if (año) params.set('año', String(año));
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const qs = params.toString();
const response = await apiClient.get<ComparativoPeriodos>(`/reportes/comparativo${qs ? `?${qs}` : ''}`);
return response.data;
}
export async function getConcentradoRfc(
tipo: 'cliente' | 'proveedor',
fechaInicio?: string,
fechaFin?: string,
contribuyenteId?: string,
): Promise<ConcentradoRfc[]> {
const params = new URLSearchParams({ tipo });
if (fechaInicio) params.set('fechaInicio', fechaInicio);
if (fechaFin) params.set('fechaFin', fechaFin);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<ConcentradoRfc[]>(`/reportes/concentrado-rfc?${params}`);
return response.data;
}
export interface CuentasPendientes {
cantidadCfdis: number;
saldoTotal: number;
detalle: { rfc: string; nombre: string; cantidad: number; saldo: number }[];
}
export async function getCuentasXPagar(fechaInicio: string, fechaFin: string, regimen?: string, contribuyenteId?: string): Promise<CuentasPendientes> {
const params = new URLSearchParams();
params.set('fechaInicio', fechaInicio);
params.set('fechaFin', fechaFin);
if (regimen) params.set('regimen', regimen);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<CuentasPendientes>(`/reportes/cuentas-x-pagar?${params}`);
return response.data;
}
export async function getCuentasXCobrar(fechaInicio: string, fechaFin: string, regimen?: string, contribuyenteId?: string): Promise<CuentasPendientes> {
const params = new URLSearchParams();
params.set('fechaInicio', fechaInicio);
params.set('fechaFin', fechaFin);
if (regimen) params.set('regimen', regimen);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<CuentasPendientes>(`/reportes/cuentas-x-cobrar?${params}`);
return response.data;
}

50
apps/web/lib/api/sat.ts Normal file
View File

@@ -0,0 +1,50 @@
import { apiClient } from './client';
import type {
SatSyncJob,
SatSyncStatusResponse,
SatSyncHistoryResponse,
StartSyncRequest,
StartSyncResponse,
} from '@horux/shared';
export async function startSync(data?: StartSyncRequest, contribuyenteId?: string | null): Promise<StartSyncResponse> {
const response = await apiClient.post<StartSyncResponse>('/sat/sync', {
...data,
contribuyenteId: contribuyenteId || undefined,
});
return response.data;
}
export async function getSyncStatus(contribuyenteId?: string | null): Promise<SatSyncStatusResponse> {
const response = await apiClient.get<SatSyncStatusResponse>('/sat/sync/status', {
params: { contribuyenteId: contribuyenteId || undefined },
});
return response.data;
}
export async function getSyncHistory(page: number = 1, limit: number = 10, contribuyenteId?: string | null): Promise<SatSyncHistoryResponse> {
const response = await apiClient.get<SatSyncHistoryResponse>('/sat/sync/history', {
params: { page, limit, contribuyenteId: contribuyenteId || undefined },
});
return response.data;
}
export async function getSyncJob(id: string): Promise<SatSyncJob> {
const response = await apiClient.get<SatSyncJob>(`/sat/sync/${id}`);
return response.data;
}
export async function retrySync(id: string): Promise<StartSyncResponse> {
const response = await apiClient.post<StartSyncResponse>(`/sat/sync/${id}/retry`);
return response.data;
}
export async function getCronInfo(): Promise<{ scheduled: boolean; expression: string; timezone: string }> {
const response = await apiClient.get('/sat/cron');
return response.data;
}
export async function runCron(): Promise<{ message: string }> {
const response = await apiClient.post('/sat/cron/run');
return response.data;
}

View File

@@ -0,0 +1,114 @@
import { apiClient } from './client';
export interface Subscription {
id: string;
tenantId: string;
plan: string;
status: string;
amount: string;
frequency: string;
mpPreapprovalId: string | null;
currentPeriodStart: string | null;
currentPeriodEnd: string | null;
pendingPlan: string | null;
pendingFrequency: string | null;
pendingEffectiveAt: string | null;
upgradePreferenceId: string | null;
upgradeTargetPlan: string | null;
upgradeTargetAmount: string | null;
createdAt: string;
updatedAt: string;
}
export interface PlanPrice {
id: number;
plan: string;
frequency: 'monthly' | 'annual';
amount: string;
updatedAt: string;
}
export interface Payment {
id: string;
tenantId: string;
subscriptionId: string | null;
mpPaymentId: string | null;
amount: string;
status: string;
paymentMethod: string | null;
paidAt: string | null;
createdAt: string;
}
export async function getSubscription(tenantId: string): Promise<Subscription | null> {
try {
const response = await apiClient.get<Subscription>(`/subscriptions/${tenantId}`);
return response.data;
} catch (error: any) {
// 404 significa "no hay suscripción registrada" — no es error, es estado inicial
if (error?.response?.status === 404) return null;
throw error;
}
}
export async function getPlans(): Promise<PlanPrice[]> {
const response = await apiClient.get<PlanPrice[]>('/subscriptions/plans');
return response.data;
}
type PlanFreqPayload = { plan: string; frequency: 'monthly' | 'annual' };
export async function startTrial(data: PlanFreqPayload): Promise<{ subscription: Subscription; trialEndsAt: string }> {
const response = await apiClient.post('/subscriptions/me/trial', data);
return response.data;
}
export async function subscribeMe(data: PlanFreqPayload): Promise<{ subscription: Subscription; paymentUrl: string }> {
const response = await apiClient.post('/subscriptions/me/subscribe', data);
return response.data;
}
export async function changeMyPlan(data: PlanFreqPayload): Promise<{ subscription: Subscription; effectiveAt: string }> {
const response = await apiClient.post('/subscriptions/me/change', data);
return response.data;
}
export async function cancelMySubscription(): Promise<{ subscription: Subscription }> {
const response = await apiClient.post('/subscriptions/me/cancel');
return response.data;
}
export async function updatePlanPrice(id: number, amount: number): Promise<PlanPrice> {
const response = await apiClient.put<PlanPrice>(`/subscriptions/plans/${id}`, { amount });
return response.data;
}
export async function upgradeMe(plan: string): Promise<{ subscription: Subscription; checkoutUrl: string; proratedAmount: number }> {
const response = await apiClient.post('/subscriptions/me/upgrade', { plan });
return response.data;
}
export async function cancelPendingUpgrade(): Promise<{ ok: boolean }> {
const response = await apiClient.post('/subscriptions/me/upgrade/cancel');
return response.data;
}
export async function reactivateMe(): Promise<{ subscription: Subscription; paymentUrl: string }> {
const response = await apiClient.post('/subscriptions/me/reactivate');
return response.data;
}
export async function generatePaymentLink(tenantId: string): Promise<{ paymentUrl: string }> {
const response = await apiClient.post<{ paymentUrl: string }>(`/subscriptions/${tenantId}/generate-link`);
return response.data;
}
export async function markAsPaid(tenantId: string, amount: number): Promise<Payment> {
const response = await apiClient.post<Payment>(`/subscriptions/${tenantId}/mark-paid`, { amount });
return response.data;
}
export async function getPaymentHistory(tenantId: string): Promise<Payment[]> {
const response = await apiClient.get<Payment[]>(`/subscriptions/${tenantId}/payments`);
return response.data;
}

View File

@@ -0,0 +1,97 @@
import { apiClient } from './client';
export interface Tenant {
id: string;
nombre: string;
rfc: string;
plan: string;
databaseName: string;
createdAt: string;
_count?: {
/** Memberships activos (matches el `_count.memberships` que retorna `getAllTenants`). */
memberships: number;
};
}
export interface CreateTenantData {
nombre: string;
rfc: string;
plan?: 'starter' | 'business' | 'business_ia' | 'enterprise' | 'custom';
cfdiLimit?: number;
usersLimit?: number;
adminEmail: string;
adminNombre: string;
amount?: number;
}
export async function getTenants(): Promise<Tenant[]> {
const response = await apiClient.get<Tenant[]>('/tenants');
return response.data;
}
export async function getTenant(id: string): Promise<Tenant> {
const response = await apiClient.get<Tenant>(`/tenants/${id}`);
return response.data;
}
export async function createTenant(data: CreateTenantData): Promise<Tenant> {
const response = await apiClient.post<Tenant>('/tenants', data);
return response.data;
}
export interface UpdateTenantData {
nombre?: string;
rfc?: string;
plan?: 'starter' | 'business' | 'business_ia' | 'enterprise' | 'custom';
cfdiLimit?: number;
usersLimit?: number;
active?: boolean;
}
export async function updateTenant(id: string, data: UpdateTenantData): Promise<Tenant> {
const response = await apiClient.put<Tenant>(`/tenants/${id}`, data);
return response.data;
}
export async function deleteTenant(id: string): Promise<void> {
await apiClient.delete(`/tenants/${id}`);
}
// ============================================================================
// Self-serve multi-tenant (memberships del caller)
// ============================================================================
export interface MyTenantDetailed {
tenantId: string;
nombre: string;
rfc: string;
plan: string;
role: string;
isOwner: boolean;
trialEndsAt: string | null;
subscription: {
status: string;
plan: string;
amount: number;
frequency: string;
currentPeriodEnd: string | null;
pendingPlan: string | null;
pendingEffectiveAt: string | null;
} | null;
}
export async function getMyTenants(): Promise<MyTenantDetailed[]> {
const response = await apiClient.get<MyTenantDetailed[]>('/tenants/mine');
return response.data;
}
export interface AddMyTenantData {
nombre: string;
rfc: string;
plan?: 'starter' | 'business' | 'business_ia' | 'enterprise';
}
export async function addMyTenant(data: AddMyTenantData): Promise<{ tenant: Tenant }> {
const response = await apiClient.post<{ tenant: Tenant }>('/tenants/mine', data);
return response.data;
}

View File

@@ -0,0 +1,36 @@
import { apiClient } from './client';
import type { UserListItem, UserInvite, UserUpdate } from '@horux/shared';
export async function getUsuarios(): Promise<UserListItem[]> {
const response = await apiClient.get<UserListItem[]>('/usuarios');
return response.data;
}
export async function inviteUsuario(data: UserInvite): Promise<UserListItem> {
const response = await apiClient.post<UserListItem>('/usuarios/invite', data);
return response.data;
}
export async function updateUsuario(id: string, data: UserUpdate): Promise<UserListItem> {
const response = await apiClient.patch<UserListItem>(`/usuarios/${id}`, data);
return response.data;
}
export async function deleteUsuario(id: string): Promise<void> {
await apiClient.delete(`/usuarios/${id}`);
}
// Funciones globales (admin global)
export async function getAllUsuarios(): Promise<UserListItem[]> {
const response = await apiClient.get<UserListItem[]>('/usuarios/global/all');
return response.data;
}
export async function updateUsuarioGlobal(id: string, data: UserUpdate): Promise<UserListItem> {
const response = await apiClient.patch<UserListItem>(`/usuarios/global/${id}`, data);
return response.data;
}
export async function deleteUsuarioGlobal(id: string): Promise<void> {
await apiClient.delete(`/usuarios/global/${id}`);
}

View File

@@ -0,0 +1,30 @@
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
interface ExcelColumn {
header: string;
key: string;
width?: number;
}
export function exportToExcel(
data: Record<string, any>[],
columns: ExcelColumn[],
filename: string,
) {
const rows = data.map((row) =>
Object.fromEntries(columns.map((col) => [col.header, row[col.key] ?? '']))
);
const ws = XLSX.utils.json_to_sheet(rows);
// Aplicar anchos de columna
ws['!cols'] = columns.map((col) => ({ wch: col.width || 15 }));
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Datos');
const buf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
saveAs(blob, `${filename}.xlsx`);
}

View File

@@ -0,0 +1,29 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import * as api from '../api/addons';
export function useMyAddons(contribuyenteId?: string) {
return useQuery({
queryKey: ['my-addons', contribuyenteId ?? 'all'],
queryFn: () => api.listMyAddons(contribuyenteId),
});
}
export function useSubscribeAddon() {
const qc = useQueryClient();
return useMutation({
mutationFn: api.subscribeAddon,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['my-addons'] });
},
});
}
export function useCancelAddon() {
const qc = useQueryClient();
return useMutation({
mutationFn: api.cancelAddon,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['my-addons'] });
},
});
}

View File

@@ -0,0 +1,73 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as alertasApi from '../api/alertas';
import type { AlertaCreate, AlertaUpdate } from '@horux/shared';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
export function useAlertas(filters?: { leida?: boolean; resuelta?: boolean }) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['alertas', filters, selectedContribuyenteId],
queryFn: () => alertasApi.getAlertas({ ...filters, contribuyenteId: selectedContribuyenteId || undefined }),
});
}
export function useAlertasAutomaticas() {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['alertas-automaticas', selectedContribuyenteId],
queryFn: () => alertasApi.getAlertasAutomaticas(selectedContribuyenteId || undefined),
});
}
export function useAlertasStats() {
return useQuery({
queryKey: ['alertas-stats'],
queryFn: alertasApi.getStats,
});
}
export function useCreateAlerta() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: AlertaCreate) => alertasApi.createAlerta(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alertas'] });
queryClient.invalidateQueries({ queryKey: ['alertas-stats'] });
},
});
}
export function useUpdateAlerta() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: AlertaUpdate }) => alertasApi.updateAlerta(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alertas'] });
queryClient.invalidateQueries({ queryKey: ['alertas-stats'] });
},
});
}
export function useDeleteAlerta() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => alertasApi.deleteAlerta(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alertas'] });
queryClient.invalidateQueries({ queryKey: ['alertas-stats'] });
},
});
}
export function useMarkAllAsRead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: alertasApi.markAllAsRead,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alertas'] });
queryClient.invalidateQueries({ queryKey: ['alertas-stats'] });
},
});
}

View File

@@ -0,0 +1,12 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { listAuditLog, type AuditLogFilters } from '../api/audit-log';
export function useAuditLog(filters: AuditLogFilters) {
return useQuery({
queryKey: ['audit-log', filters],
queryFn: () => listAuditLog(filters),
staleTime: 30 * 1000,
});
}

View File

@@ -0,0 +1,39 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as bancosApi from '@/lib/api/bancos';
import { useTenantViewStore } from '@/stores/tenant-view-store';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
function useBancosKey() {
const { viewingTenantId } = useTenantViewStore();
const { selectedContribuyenteId } = useContribuyenteStore();
const tenantKey = viewingTenantId || 'own';
return ['bancos', tenantKey, selectedContribuyenteId] as const;
}
export function useBancos() {
const key = useBancosKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: key,
queryFn: () => bancosApi.getBancos(selectedContribuyenteId),
});
}
export function useCreateBanco() {
const qc = useQueryClient();
const { selectedContribuyenteId } = useContribuyenteStore();
return useMutation({
mutationFn: (data: { banco: string; terminacionCuenta: string }) =>
bancosApi.createBanco({ ...data, contribuyenteId: selectedContribuyenteId || undefined }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['bancos'] }),
});
}
export function useDeleteBanco() {
const qc = useQueryClient();
const key = useBancosKey();
return useMutation({
mutationFn: bancosApi.deleteBanco,
onSuccess: () => qc.invalidateQueries({ queryKey: key }),
});
}

View File

@@ -0,0 +1,57 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as calendarioApi from '../api/calendario';
import { useTenantViewStore } from '@/stores/tenant-view-store';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
import type { EventoCreate, EventoUpdate } from '@horux/shared';
function useTenantKey() {
const { viewingTenantId } = useTenantViewStore();
return viewingTenantId || 'own';
}
export function useEventos(año: number) {
const tenantKey = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['calendario', tenantKey, año, selectedContribuyenteId],
queryFn: () => calendarioApi.getEventos(año, selectedContribuyenteId),
});
}
export function useProximosEventos(dias = 30) {
const tenantKey = useTenantKey();
return useQuery({
queryKey: ['calendario-proximos', tenantKey, dias],
queryFn: () => calendarioApi.getProximos(dias),
});
}
export function useCreateEvento() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: EventoCreate) => calendarioApi.createEvento(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['calendario'] });
},
});
}
export function useUpdateEvento() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: EventoUpdate }) => calendarioApi.updateEvento(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['calendario'] });
},
});
}
export function useDeleteEvento() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => calendarioApi.deleteEvento(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['calendario'] });
},
});
}

View File

@@ -0,0 +1,62 @@
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as api from '@/lib/api/carteras';
export function useCarteras() {
return useQuery({
queryKey: ['carteras'],
queryFn: () => api.getCarteras().then(r => r.data),
});
}
export function useSupervisores() {
return useQuery({
queryKey: ['cartera-supervisores'],
queryFn: () => api.getSupervisores().then(r => r.data),
});
}
export function useCreateCartera() {
const qc = useQueryClient();
return useMutation({
mutationFn: api.createCartera,
onSuccess: () => qc.invalidateQueries({ queryKey: ['carteras'] }),
});
}
export function useDeleteCartera() {
const qc = useQueryClient();
return useMutation({
mutationFn: api.deleteCartera,
onSuccess: () => qc.invalidateQueries({ queryKey: ['carteras'] }),
});
}
export function useCarteraEntidades(carteraId: string | null) {
return useQuery({
queryKey: ['cartera-entidades', carteraId],
queryFn: () => api.getCarteraEntidades(carteraId!).then(r => r.data),
enabled: !!carteraId,
});
}
export function useSubcarteras(carteraId: string | null) {
return useQuery({
queryKey: ['subcarteras', carteraId],
queryFn: () => api.getSubcarteras(carteraId!).then(r => r.data),
enabled: !!carteraId,
});
}
export function useCreateSubcartera() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ carteraId, ...payload }: { carteraId: string; nombre: string; descripcion?: string; auxiliarUserId: string }) =>
api.createSubcartera(carteraId, payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['subcarteras'] });
qc.invalidateQueries({ queryKey: ['carteras'] });
},
});
}

View File

@@ -0,0 +1,72 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as cfdiApi from '@/lib/api/cfdi';
import type { CfdiFilters } from '@horux/shared';
import type { CreateCfdiData } from '@/lib/api/cfdi';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
export function useCfdis(filters: CfdiFilters) {
const { selectedContribuyenteId } = useContribuyenteStore();
const filtersWithContribuyente: CfdiFilters = {
...filters,
contribuyenteId: selectedContribuyenteId || undefined,
};
return useQuery({
queryKey: ['cfdis', filters, selectedContribuyenteId],
queryFn: () => cfdiApi.getCfdis(filtersWithContribuyente),
});
}
export function useCfdi(id: string) {
return useQuery({
queryKey: ['cfdi', id],
queryFn: () => cfdiApi.getCfdiById(id),
enabled: !!id,
});
}
export function useResumenCfdi(año?: number, mes?: number) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['cfdi-resumen', año, mes, selectedContribuyenteId],
queryFn: () => cfdiApi.getResumenCfdi(año, mes, selectedContribuyenteId || undefined),
});
}
export function useCreateCfdi() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateCfdiData) => cfdiApi.createCfdi(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cfdis'] });
queryClient.invalidateQueries({ queryKey: ['cfdi-resumen'] });
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
},
});
}
export function useCreateManyCfdis() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (cfdis: CreateCfdiData[]) => cfdiApi.createManyCfdis(cfdis),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cfdis'] });
queryClient.invalidateQueries({ queryKey: ['cfdi-resumen'] });
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
},
});
}
export function useDeleteCfdi() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => cfdiApi.deleteCfdi(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cfdis'] });
queryClient.invalidateQueries({ queryKey: ['cfdi-resumen'] });
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
},
});
}

View File

@@ -0,0 +1,37 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as conciliacionApi from '@/lib/api/conciliacion';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
export function useCfdisConConciliacion(params: {
tipo: string;
fechaInicio?: string;
fechaFin?: string;
regimen?: string;
}) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['conciliacion', params, selectedContribuyenteId],
queryFn: () => conciliacionApi.getCfdisConConciliacion({
...params,
contribuyenteId: selectedContribuyenteId || undefined,
}),
enabled: !!params.tipo,
});
}
export function useConciliar() {
const qc = useQueryClient();
return useMutation({
mutationFn: conciliacionApi.conciliar,
onSuccess: () => qc.invalidateQueries({ queryKey: ['conciliacion'] }),
});
}
export function useDesconciliar() {
const qc = useQueryClient();
return useMutation({
mutationFn: conciliacionApi.desconciliar,
onSuccess: () => qc.invalidateQueries({ queryKey: ['conciliacion'] }),
});
}

View File

@@ -0,0 +1,42 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { listConstancias, consultarConstancia, descargarConstanciaPdf } from '../api/constancias';
import { useTenantViewStore } from '../../stores/tenant-view-store';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
export function useConstancias() {
const viewingTenantId = useTenantViewStore((s) => s.viewingTenantId);
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['constancias', viewingTenantId, selectedContribuyenteId],
queryFn: () => listConstancias(selectedContribuyenteId || undefined),
});
}
export function useConsultarConstancia() {
const qc = useQueryClient();
const viewingTenantId = useTenantViewStore((s) => s.viewingTenantId);
return useMutation({
mutationFn: consultarConstancia,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['constancias', viewingTenantId] });
qc.invalidateQueries({ queryKey: ['tenant-info'] });
qc.invalidateQueries({ queryKey: ['regimenes-activos'] });
},
});
}
export function useDescargarConstanciaPdf() {
return useMutation({
mutationFn: async (id: number) => {
const blob = await descargarConstanciaPdf(id);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `constancia_${id}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
},
});
}

View File

@@ -0,0 +1,48 @@
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAuthStore } from '@/stores/auth-store';
import * as api from '@/lib/api/contribuyentes';
export function useContribuyentes() {
const user = useAuthStore((s) => s.user);
return useQuery({
queryKey: ['contribuyentes', user?.tenantId],
queryFn: () => api.getContribuyentes().then((r) => r.data),
enabled: !!user,
});
}
export function useContribuyente(id: string | null) {
const user = useAuthStore((s) => s.user);
return useQuery({
queryKey: ['contribuyente', id, user?.tenantId],
queryFn: () => api.getContribuyente(id!),
enabled: !!user && !!id,
});
}
export function useCreateContribuyente() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createContribuyente,
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contribuyentes'] }); },
});
}
export function useUpdateContribuyente() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<api.CreateContribuyenteData> }) =>
api.updateContribuyente(id, data),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contribuyentes'] }); },
});
}
export function useDeactivateContribuyente() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deactivateContribuyente,
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['contribuyentes'] }); },
});
}

View File

@@ -0,0 +1,47 @@
import { useQuery } from '@tanstack/react-query';
import * as dashboardApi from '../api/dashboard';
import { useTenantViewStore } from '@/stores/tenant-view-store';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
function useTenantKey() {
const { viewingTenantId } = useTenantViewStore();
return viewingTenantId || 'own';
}
export function useKpis(fechaInicio: string, fechaFin: string, conciliacion?: boolean) {
const tenantKey = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['kpis', tenantKey, fechaInicio, fechaFin, conciliacion, selectedContribuyenteId],
queryFn: () => dashboardApi.getKpis(fechaInicio, fechaFin, conciliacion, selectedContribuyenteId),
enabled: !!fechaInicio && !!fechaFin,
});
}
export function useIngresosEgresos(año?: number, conciliacion?: boolean) {
const tenantKey = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['ingresos-egresos', tenantKey, año, conciliacion, selectedContribuyenteId],
queryFn: () => dashboardApi.getIngresosEgresos(año, conciliacion, selectedContribuyenteId),
});
}
export function useRegimenesDelPeriodo(fechaInicio: string, fechaFin: string, conciliacion?: boolean) {
const tenantKey = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['regimenes-periodo', tenantKey, fechaInicio, fechaFin, conciliacion, selectedContribuyenteId],
queryFn: () => dashboardApi.getRegimenesDelPeriodo(fechaInicio, fechaFin, conciliacion, selectedContribuyenteId),
enabled: !!fechaInicio && !!fechaFin,
});
}
export function useAlertas(limit = 5) {
const tenantKey = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['alertas', tenantKey, limit, selectedContribuyenteId],
queryFn: () => dashboardApi.getAlertas(limit, selectedContribuyenteId),
});
}

View File

@@ -0,0 +1,67 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
listDeclaraciones,
createDeclaracion,
uploadComprobantePago,
deleteDeclaracion,
downloadDeclaracionPdf,
downloadBlob,
type CreateDeclaracionData,
} from '../api/declaraciones';
import { useTenantViewStore } from '../../stores/tenant-view-store';
export function useDeclaraciones(fechaDesde?: string, fechaHasta?: string, contribuyenteId?: string | null) {
const viewingTenantId = useTenantViewStore((s) => s.viewingTenantId);
return useQuery({
queryKey: ['declaraciones', fechaDesde, fechaHasta, contribuyenteId ?? 'all', viewingTenantId],
queryFn: () => listDeclaraciones(fechaDesde, fechaHasta, contribuyenteId),
});
}
export function useCreateDeclaracion() {
const qc = useQueryClient();
const viewingTenantId = useTenantViewStore((s) => s.viewingTenantId);
return useMutation({
mutationFn: (data: CreateDeclaracionData) => createDeclaracion(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['declaraciones'] });
qc.invalidateQueries({ queryKey: ['alertas'] });
qc.invalidateQueries({ queryKey: ['alertas-manuales'] });
qc.invalidateQueries({ queryKey: ['alertas-automaticas'] });
qc.invalidateQueries({ queryKey: ['eventos'] });
},
});
}
export function useUploadComprobantePago() {
const qc = useQueryClient();
const viewingTenantId = useTenantViewStore((s) => s.viewingTenantId);
return useMutation({
mutationFn: ({ id, pdfBase64, pdfFilename }: { id: number; pdfBase64: string; pdfFilename: string }) =>
uploadComprobantePago(id, pdfBase64, pdfFilename),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['declaraciones'] });
qc.invalidateQueries({ queryKey: ['alertas'] });
qc.invalidateQueries({ queryKey: ['alertas-manuales'] });
qc.invalidateQueries({ queryKey: ['alertas-automaticas'] });
qc.invalidateQueries({ queryKey: ['eventos'] });
},
});
}
export function useDeleteDeclaracion() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteDeclaracion(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ['declaraciones'] }),
});
}
export function useDownloadDeclaracionPdf() {
return useMutation({
mutationFn: async ({ id, variant, filename }: { id: number; variant: 'declaracion' | 'liga' | 'pago'; filename: string }) => {
const blob = await downloadDeclaracionPdf(id, variant);
downloadBlob(blob, filename);
},
});
}

View File

@@ -0,0 +1,41 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getOpiniones, descargarOpinionPdf, consultarOpinion } from '../api/documentos';
import { useTenantViewStore } from '../../stores/tenant-view-store';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
export function useOpiniones() {
const viewingTenantId = useTenantViewStore((s) => s.viewingTenantId);
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['opiniones', viewingTenantId, selectedContribuyenteId],
queryFn: () => getOpiniones(selectedContribuyenteId || undefined),
});
}
export function useConsultarOpinion() {
const queryClient = useQueryClient();
const viewingTenantId = useTenantViewStore((s) => s.viewingTenantId);
const { selectedContribuyenteId } = useContribuyenteStore();
return useMutation({
mutationFn: () => consultarOpinion(selectedContribuyenteId || undefined),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['opiniones', viewingTenantId, selectedContribuyenteId] });
},
});
}
export function useDescargarPdf() {
return useMutation({
mutationFn: async (id: number) => {
const blob = await descargarOpinionPdf(id);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `opinion_cumplimiento_${id}.pdf`;
a.click();
URL.revokeObjectURL(url);
},
});
}

View File

@@ -0,0 +1,53 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as facturacionApi from '../api/facturacion';
import * as catalogosApi from '../api/catalogos';
import { useTenantViewStore } from '@/stores/tenant-view-store';
function useTenantKey() {
const { viewingTenantId } = useTenantViewStore();
return viewingTenantId || 'own';
}
// Facturación
export function useOrgStatus() {
const tk = useTenantKey();
return useQuery({ queryKey: ['facturapi-org', tk], queryFn: facturacionApi.getOrgStatus });
}
export function useTimbres() {
const tk = useTenantKey();
return useQuery({ queryKey: ['facturapi-timbres', tk], queryFn: facturacionApi.getTimbres });
}
export function useEmitirFactura() {
const qc = useQueryClient();
return useMutation({
mutationFn: facturacionApi.emitirFactura,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['facturapi-timbres'] });
qc.invalidateQueries({ queryKey: ['cfdi'] });
qc.invalidateQueries({ queryKey: ['kpis'] });
},
});
}
// Catálogos (se cachean globalmente, no dependen del tenant)
export function useFormasPago() {
return useQuery({ queryKey: ['cat-forma-pago'], queryFn: catalogosApi.getFormasPago, staleTime: Infinity });
}
export function useMetodosPago() {
return useQuery({ queryKey: ['cat-metodo-pago'], queryFn: catalogosApi.getMetodosPago, staleTime: Infinity });
}
export function useUsosCfdi() {
return useQuery({ queryKey: ['cat-uso-cfdi'], queryFn: catalogosApi.getUsosCfdi, staleTime: Infinity });
}
export function useMonedas() {
return useQuery({ queryKey: ['cat-moneda'], queryFn: catalogosApi.getMonedas, staleTime: Infinity });
}
export function useClavesUnidad() {
return useQuery({ queryKey: ['cat-clave-unidad'], queryFn: catalogosApi.getClavesUnidad, staleTime: Infinity });
}

View File

@@ -0,0 +1,65 @@
import { useQuery } from '@tanstack/react-query';
import * as impuestosApi from '@/lib/api/impuestos';
import { useTenantViewStore } from '@/stores/tenant-view-store';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
function useTenantKey() {
const { viewingTenantId } = useTenantViewStore();
return viewingTenantId || 'own';
}
export function useIvaMensual(año?: number, conciliacion?: boolean, considerarActivos?: boolean, considerarNCs?: boolean) {
const tk = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['iva-mensual', tk, año, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs],
queryFn: () => impuestosApi.getIvaMensual(año, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs),
});
}
export function useResumenIva(fechaInicio: string, fechaFin: string, conciliacion?: boolean, considerarActivos?: boolean, considerarNCs?: boolean) {
const tk = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['iva-resumen', tk, fechaInicio, fechaFin, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs],
queryFn: () => impuestosApi.getResumenIva(fechaInicio, fechaFin, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs),
enabled: !!fechaInicio && !!fechaFin,
});
}
export function useCoeficiente(anio: number) {
const tk = useTenantKey();
return useQuery({
queryKey: ['coeficiente', tk, anio],
queryFn: () => impuestosApi.getCoeficiente(anio),
});
}
export function useIsrMensual(año?: number, conciliacion?: boolean, regimenClave?: string | null, considerarActivos?: boolean, considerarNCs?: boolean) {
const tk = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['isr-mensual', tk, año, conciliacion, selectedContribuyenteId, regimenClave, considerarActivos, considerarNCs],
queryFn: () => impuestosApi.getIsrMensual(año, conciliacion, selectedContribuyenteId, regimenClave, considerarActivos, considerarNCs),
});
}
export function useResumenIsr(fechaInicio: string, fechaFin: string, conciliacion?: boolean, considerarActivos?: boolean, considerarNCs?: boolean) {
const tk = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['isr-resumen', tk, fechaInicio, fechaFin, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs],
queryFn: () => impuestosApi.getResumenIsr(fechaInicio, fechaFin, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs),
enabled: !!fechaInicio && !!fechaFin,
});
}
export function useResumenIsrDesglosado(fechaFin: string, conciliacion?: boolean, considerarActivos?: boolean, considerarNCs?: boolean) {
const tk = useTenantKey();
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['isr-resumen-desglosado', tk, fechaFin, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs],
queryFn: () => impuestosApi.getResumenIsrDesglosado(fechaFin, conciliacion, selectedContribuyenteId, considerarActivos, considerarNCs),
enabled: !!fechaFin,
});
}

View File

@@ -0,0 +1,40 @@
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as api from '../api/platform-staff';
import type { PlatformRole } from '@horux/shared';
export function useStaff() {
return useQuery({
queryKey: ['platform-staff'],
queryFn: api.listStaff,
staleTime: 60 * 1000,
});
}
export function useSearchUsers(q: string) {
return useQuery({
queryKey: ['platform-staff-search', q],
queryFn: () => api.searchUsers(q),
enabled: q.length >= 2,
staleTime: 30 * 1000,
});
}
export function useGrantRole() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ userId, role }: { userId: string; role: PlatformRole }) =>
api.grantRole(userId, role),
onSuccess: () => qc.invalidateQueries({ queryKey: ['platform-staff'] }),
});
}
export function useRevokeRole() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ userId, role }: { userId: string; role: PlatformRole }) =>
api.revokeRole(userId, role),
onSuccess: () => qc.invalidateQueries({ queryKey: ['platform-staff'] }),
});
}

View File

@@ -0,0 +1,59 @@
import { useQuery } from '@tanstack/react-query';
import * as reportesApi from '../api/reportes';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
export function useEstadoResultados(fechaInicio?: string, fechaFin?: string) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['estado-resultados', fechaInicio, fechaFin, selectedContribuyenteId],
queryFn: () => reportesApi.getEstadoResultados(fechaInicio, fechaFin, selectedContribuyenteId || undefined),
});
}
export function useFlujoEfectivo(fechaInicio?: string, fechaFin?: string) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['flujo-efectivo', fechaInicio, fechaFin, selectedContribuyenteId],
queryFn: () => reportesApi.getFlujoEfectivo(fechaInicio, fechaFin, selectedContribuyenteId || undefined),
});
}
export function useComparativo(año?: number) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['comparativo', año, selectedContribuyenteId],
queryFn: () => reportesApi.getComparativo(año, selectedContribuyenteId || undefined),
});
}
export function useConcentradoRfc(tipo: 'cliente' | 'proveedor', fechaInicio?: string, fechaFin?: string) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['concentrado-rfc', tipo, fechaInicio, fechaFin, selectedContribuyenteId],
queryFn: () => reportesApi.getConcentradoRfc(tipo, fechaInicio, fechaFin, selectedContribuyenteId || undefined),
});
}
export function useCuentasXPagar(fechaInicio: string, fechaFin: string, regimen?: string) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['cuentas-x-pagar', fechaInicio, fechaFin, regimen, selectedContribuyenteId],
queryFn: () => reportesApi.getCuentasXPagar(fechaInicio, fechaFin, regimen || undefined, selectedContribuyenteId || undefined),
enabled: !!fechaInicio && !!fechaFin,
});
}
export function useCuentasXCobrar(fechaInicio: string, fechaFin: string, regimen?: string) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['cuentas-x-cobrar', fechaInicio, fechaFin, regimen, selectedContribuyenteId],
queryFn: () => reportesApi.getCuentasXCobrar(fechaInicio, fechaFin, regimen || undefined, selectedContribuyenteId || undefined),
enabled: !!fechaInicio && !!fechaFin,
});
}

View File

@@ -0,0 +1,133 @@
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as subscriptionApi from '../api/subscription';
export function useSubscription(tenantId: string | undefined) {
return useQuery({
queryKey: ['subscription', tenantId],
queryFn: () => subscriptionApi.getSubscription(tenantId!),
enabled: !!tenantId,
staleTime: 5 * 60 * 1000,
});
}
export function usePaymentHistory(tenantId: string | undefined) {
return useQuery({
queryKey: ['payments', tenantId],
queryFn: () => subscriptionApi.getPaymentHistory(tenantId!),
enabled: !!tenantId,
staleTime: 60 * 1000,
});
}
export function useGeneratePaymentLink() {
return useMutation({
mutationFn: (tenantId: string) => subscriptionApi.generatePaymentLink(tenantId),
});
}
export function useMarkAsPaid() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ tenantId, amount }: { tenantId: string; amount: number }) =>
subscriptionApi.markAsPaid(tenantId, amount),
onSuccess: (_, { tenantId }) => {
queryClient.invalidateQueries({ queryKey: ['subscription', tenantId] });
queryClient.invalidateQueries({ queryKey: ['payments', tenantId] });
},
});
}
// ============================================================================
// Self-serve hooks (actúan sobre el tenant del usuario autenticado)
// ============================================================================
export function usePlans() {
return useQuery({
queryKey: ['subscription-plans'],
queryFn: subscriptionApi.getPlans,
staleTime: 10 * 60 * 1000, // 10 min — los precios cambian poco
});
}
export function useStartTrial() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: subscriptionApi.startTrial,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}
export function useSubscribeMe() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: subscriptionApi.subscribeMe,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}
export function useChangeMyPlan() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: subscriptionApi.changeMyPlan,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}
export function useCancelMySubscription() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: subscriptionApi.cancelMySubscription,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}
export function useUpdatePlanPrice() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, amount }: { id: number; amount: number }) =>
subscriptionApi.updatePlanPrice(id, amount),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription-plans'] });
},
});
}
export function useUpgradeMe() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (plan: string) => subscriptionApi.upgradeMe(plan),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}
export function useCancelPendingUpgrade() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: subscriptionApi.cancelPendingUpgrade,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}
export function useReactivateMe() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: subscriptionApi.reactivateMe,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}

View File

@@ -0,0 +1,42 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getTenants, createTenant, updateTenant, deleteTenant, type CreateTenantData, type UpdateTenantData } from '@/lib/api/tenants';
export function useTenants() {
return useQuery({
queryKey: ['tenants'],
queryFn: getTenants,
});
}
export function useCreateTenant() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateTenantData) => createTenant(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tenants'] });
},
});
}
export function useUpdateTenant() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateTenantData }) => updateTenant(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tenants'] });
},
});
}
export function useDeleteTenant() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteTenant(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tenants'] });
},
});
}

View File

@@ -0,0 +1,68 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as usuariosApi from '../api/usuarios';
import type { UserInvite, UserUpdate } from '@horux/shared';
export function useUsuarios() {
return useQuery({
queryKey: ['usuarios'],
queryFn: usuariosApi.getUsuarios,
});
}
export function useInviteUsuario() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UserInvite) => usuariosApi.inviteUsuario(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['usuarios'] });
},
});
}
export function useUpdateUsuario() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UserUpdate }) => usuariosApi.updateUsuario(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['usuarios'] });
},
});
}
export function useDeleteUsuario() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => usuariosApi.deleteUsuario(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['usuarios'] });
},
});
}
// Hooks globales (admin global)
export function useAllUsuarios() {
return useQuery({
queryKey: ['usuarios', 'global'],
queryFn: usuariosApi.getAllUsuarios,
});
}
export function useUpdateUsuarioGlobal() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UserUpdate }) => usuariosApi.updateUsuarioGlobal(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['usuarios'] });
},
});
}
export function useDeleteUsuarioGlobal() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => usuariosApi.deleteUsuarioGlobal(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['usuarios'] });
},
});
}

8
apps/web/lib/utils.ts Normal file
View File

@@ -0,0 +1,8 @@
export function formatCurrency(value: number): string {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
}