Update: nueva version Horux Despachos
This commit is contained in:
37
apps/web/lib/api/addons.ts
Normal file
37
apps/web/lib/api/addons.ts
Normal 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}`);
|
||||
}
|
||||
44
apps/web/lib/api/admin-clientes.ts
Normal file
44
apps/web/lib/api/admin-clientes.ts
Normal 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;
|
||||
}
|
||||
40
apps/web/lib/api/alertas.ts
Normal file
40
apps/web/lib/api/alertas.ts
Normal 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');
|
||||
}
|
||||
37
apps/web/lib/api/audit-log.ts
Normal file
37
apps/web/lib/api/audit-log.ts
Normal 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
48
apps/web/lib/api/auth.ts
Normal 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;
|
||||
}
|
||||
28
apps/web/lib/api/bancos.ts
Normal file
28
apps/web/lib/api/bancos.ts
Normal 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}`);
|
||||
}
|
||||
28
apps/web/lib/api/calendario.ts
Normal file
28
apps/web/lib/api/calendario.ts
Normal 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}`);
|
||||
}
|
||||
73
apps/web/lib/api/carteras.ts
Normal file
73
apps/web/lib/api/carteras.ts
Normal 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;
|
||||
}
|
||||
24
apps/web/lib/api/catalogos.ts
Normal file
24
apps/web/lib/api/catalogos.ts
Normal 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
135
apps/web/lib/api/cfdi.ts
Normal 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;
|
||||
}
|
||||
79
apps/web/lib/api/client.ts
Normal file
79
apps/web/lib/api/client.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
58
apps/web/lib/api/conciliacion.ts
Normal file
58
apps/web/lib/api/conciliacion.ts
Normal 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}`);
|
||||
}
|
||||
78
apps/web/lib/api/constancias.ts
Normal file
78
apps/web/lib/api/constancias.ts
Normal 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);
|
||||
66
apps/web/lib/api/contribuyentes.ts
Normal file
66
apps/web/lib/api/contribuyentes.ts
Normal 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 });
|
||||
}
|
||||
41
apps/web/lib/api/dashboard.ts
Normal file
41
apps/web/lib/api/dashboard.ts
Normal 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;
|
||||
}
|
||||
87
apps/web/lib/api/declaraciones.ts
Normal file
87
apps/web/lib/api/declaraciones.ts
Normal 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);
|
||||
}
|
||||
90
apps/web/lib/api/documentos.ts
Normal file
90
apps/web/lib/api/documentos.ts
Normal 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}`);
|
||||
}
|
||||
172
apps/web/lib/api/facturacion.ts
Normal file
172
apps/web/lib/api/facturacion.ts
Normal 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
41
apps/web/lib/api/fiel.ts
Normal 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');
|
||||
}
|
||||
76
apps/web/lib/api/impuestos.ts
Normal file
76
apps/web/lib/api/impuestos.ts
Normal 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;
|
||||
}
|
||||
37
apps/web/lib/api/platform-staff.ts
Normal file
37
apps/web/lib/api/platform-staff.ts
Normal 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 });
|
||||
}
|
||||
69
apps/web/lib/api/reportes.ts
Normal file
69
apps/web/lib/api/reportes.ts
Normal 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
50
apps/web/lib/api/sat.ts
Normal 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;
|
||||
}
|
||||
114
apps/web/lib/api/subscription.ts
Normal file
114
apps/web/lib/api/subscription.ts
Normal 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;
|
||||
}
|
||||
97
apps/web/lib/api/tenants.ts
Normal file
97
apps/web/lib/api/tenants.ts
Normal 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;
|
||||
}
|
||||
36
apps/web/lib/api/usuarios.ts
Normal file
36
apps/web/lib/api/usuarios.ts
Normal 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}`);
|
||||
}
|
||||
30
apps/web/lib/export-excel.ts
Normal file
30
apps/web/lib/export-excel.ts
Normal 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`);
|
||||
}
|
||||
29
apps/web/lib/hooks/use-addons.ts
Normal file
29
apps/web/lib/hooks/use-addons.ts
Normal 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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
73
apps/web/lib/hooks/use-alertas.ts
Normal file
73
apps/web/lib/hooks/use-alertas.ts
Normal 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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
12
apps/web/lib/hooks/use-audit-log.ts
Normal file
12
apps/web/lib/hooks/use-audit-log.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
39
apps/web/lib/hooks/use-bancos.ts
Normal file
39
apps/web/lib/hooks/use-bancos.ts
Normal 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 }),
|
||||
});
|
||||
}
|
||||
57
apps/web/lib/hooks/use-calendario.ts
Normal file
57
apps/web/lib/hooks/use-calendario.ts
Normal 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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
62
apps/web/lib/hooks/use-carteras.ts
Normal file
62
apps/web/lib/hooks/use-carteras.ts
Normal 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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
72
apps/web/lib/hooks/use-cfdi.ts
Normal file
72
apps/web/lib/hooks/use-cfdi.ts
Normal 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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
37
apps/web/lib/hooks/use-conciliacion.ts
Normal file
37
apps/web/lib/hooks/use-conciliacion.ts
Normal 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'] }),
|
||||
});
|
||||
}
|
||||
42
apps/web/lib/hooks/use-constancias.ts
Normal file
42
apps/web/lib/hooks/use-constancias.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
48
apps/web/lib/hooks/use-contribuyentes.ts
Normal file
48
apps/web/lib/hooks/use-contribuyentes.ts
Normal 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'] }); },
|
||||
});
|
||||
}
|
||||
47
apps/web/lib/hooks/use-dashboard.ts
Normal file
47
apps/web/lib/hooks/use-dashboard.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
67
apps/web/lib/hooks/use-declaraciones.ts
Normal file
67
apps/web/lib/hooks/use-declaraciones.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
41
apps/web/lib/hooks/use-documentos.ts
Normal file
41
apps/web/lib/hooks/use-documentos.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
53
apps/web/lib/hooks/use-facturacion.ts
Normal file
53
apps/web/lib/hooks/use-facturacion.ts
Normal 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 });
|
||||
}
|
||||
65
apps/web/lib/hooks/use-impuestos.ts
Normal file
65
apps/web/lib/hooks/use-impuestos.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
40
apps/web/lib/hooks/use-platform-staff.ts
Normal file
40
apps/web/lib/hooks/use-platform-staff.ts
Normal 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'] }),
|
||||
});
|
||||
}
|
||||
59
apps/web/lib/hooks/use-reportes.ts
Normal file
59
apps/web/lib/hooks/use-reportes.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
133
apps/web/lib/hooks/use-subscription.ts
Normal file
133
apps/web/lib/hooks/use-subscription.ts
Normal 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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
42
apps/web/lib/hooks/use-tenants.ts
Normal file
42
apps/web/lib/hooks/use-tenants.ts
Normal 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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
68
apps/web/lib/hooks/use-usuarios.ts
Normal file
68
apps/web/lib/hooks/use-usuarios.ts
Normal 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
8
apps/web/lib/utils.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user