erp-core-frontend-web/src/features/financial/hooks/useFinancial.ts
rckrdmrd 836ebaf638 feat(financial): Add complete Financial module frontend
- Create financial types (Account, Journal, JournalEntry, Invoice, Payment)
- Create financial API client with full CRUD operations
- Create comprehensive hooks (useAccounts, useJournals, useJournalEntries, etc.)
- Create AccountsPage with account type filtering and stats
- Create JournalEntriesPage with journal filtering and post/cancel actions
- Create InvoicesPage with customer/supplier filtering and validation

MGN-010 Financial frontend implementation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 10:12:43 -06:00

750 lines
20 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { financialApi } from '../api/financial.api';
import type {
Account,
AccountFilters,
CreateAccountDto,
UpdateAccountDto,
AccountType,
Journal,
JournalFilters,
CreateJournalDto,
UpdateJournalDto,
JournalEntry,
JournalEntryFilters,
CreateJournalEntryDto,
UpdateJournalEntryDto,
FinancialInvoice,
InvoiceFilters,
CreateInvoiceDto,
UpdateInvoiceDto,
Payment,
PaymentFilters,
CreatePaymentDto,
UpdatePaymentDto,
} from '../types';
// ==================== Account Types Hook ====================
export function useAccountTypes() {
const [accountTypes, setAccountTypes] = useState<AccountType[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchAccountTypes = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const data = await financialApi.getAccountTypes();
setAccountTypes(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar tipos de cuenta');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetchAccountTypes();
}, [fetchAccountTypes]);
return {
accountTypes,
isLoading,
error,
refresh: fetchAccountTypes,
};
}
// ==================== Accounts Hook ====================
export interface UseAccountsOptions extends AccountFilters {
autoFetch?: boolean;
}
export function useAccounts(options: UseAccountsOptions = {}) {
const { autoFetch = true, ...filters } = options;
const [accounts, setAccounts] = useState<Account[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(filters.page || 1);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchAccounts = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await financialApi.getAccounts({ ...filters, page });
setAccounts(response.data);
setTotal(response.meta.total);
setTotalPages(response.meta.totalPages);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar cuentas');
} finally {
setIsLoading(false);
}
}, [filters.companyId, filters.accountTypeId, filters.parentId, filters.isReconcilable, filters.isDeprecated, filters.search, filters.limit, filters.sortBy, filters.sortOrder, page]);
useEffect(() => {
if (autoFetch) {
fetchAccounts();
}
}, [fetchAccounts, autoFetch]);
const createAccount = async (data: CreateAccountDto) => {
setIsLoading(true);
try {
const newAccount = await financialApi.createAccount(data);
await fetchAccounts();
return newAccount;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al crear cuenta');
throw err;
} finally {
setIsLoading(false);
}
};
const updateAccount = async (id: string, data: UpdateAccountDto) => {
setIsLoading(true);
try {
const updated = await financialApi.updateAccount(id, data);
await fetchAccounts();
return updated;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al actualizar cuenta');
throw err;
} finally {
setIsLoading(false);
}
};
const deleteAccount = async (id: string) => {
setIsLoading(true);
try {
await financialApi.deleteAccount(id);
await fetchAccounts();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al eliminar cuenta');
throw err;
} finally {
setIsLoading(false);
}
};
return {
accounts,
total,
page,
totalPages,
isLoading,
error,
setPage,
refresh: fetchAccounts,
createAccount,
updateAccount,
deleteAccount,
};
}
// ==================== Single Account Hook ====================
export function useAccount(accountId: string | null) {
const [account, setAccount] = useState<Account | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchAccount = useCallback(async () => {
if (!accountId) {
setAccount(null);
return;
}
setIsLoading(true);
setError(null);
try {
const data = await financialApi.getAccountById(accountId);
setAccount(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar cuenta');
} finally {
setIsLoading(false);
}
}, [accountId]);
useEffect(() => {
fetchAccount();
}, [fetchAccount]);
return {
account,
isLoading,
error,
refresh: fetchAccount,
};
}
// ==================== Journals Hook ====================
export interface UseJournalsOptions extends JournalFilters {
autoFetch?: boolean;
}
export function useJournals(options: UseJournalsOptions = {}) {
const { autoFetch = true, ...filters } = options;
const [journals, setJournals] = useState<Journal[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(filters.page || 1);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchJournals = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await financialApi.getJournals({ ...filters, page });
setJournals(response.data);
setTotal(response.meta.total);
setTotalPages(response.meta.totalPages);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar diarios');
} finally {
setIsLoading(false);
}
}, [filters.companyId, filters.journalType, filters.active, filters.search, filters.limit, page]);
useEffect(() => {
if (autoFetch) {
fetchJournals();
}
}, [fetchJournals, autoFetch]);
const createJournal = async (data: CreateJournalDto) => {
setIsLoading(true);
try {
const newJournal = await financialApi.createJournal(data);
await fetchJournals();
return newJournal;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al crear diario');
throw err;
} finally {
setIsLoading(false);
}
};
const updateJournal = async (id: string, data: UpdateJournalDto) => {
setIsLoading(true);
try {
const updated = await financialApi.updateJournal(id, data);
await fetchJournals();
return updated;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al actualizar diario');
throw err;
} finally {
setIsLoading(false);
}
};
const deleteJournal = async (id: string) => {
setIsLoading(true);
try {
await financialApi.deleteJournal(id);
await fetchJournals();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al eliminar diario');
throw err;
} finally {
setIsLoading(false);
}
};
return {
journals,
total,
page,
totalPages,
isLoading,
error,
setPage,
refresh: fetchJournals,
createJournal,
updateJournal,
deleteJournal,
};
}
// ==================== Journal Entries Hook ====================
export interface UseJournalEntriesOptions extends JournalEntryFilters {
autoFetch?: boolean;
}
export function useJournalEntries(options: UseJournalEntriesOptions = {}) {
const { autoFetch = true, ...filters } = options;
const [entries, setEntries] = useState<JournalEntry[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(filters.page || 1);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchEntries = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await financialApi.getEntries({ ...filters, page });
setEntries(response.data);
setTotal(response.meta.total);
setTotalPages(response.meta.totalPages);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar asientos');
} finally {
setIsLoading(false);
}
}, [filters.companyId, filters.journalId, filters.status, filters.dateFrom, filters.dateTo, filters.search, filters.limit, filters.sortBy, filters.sortOrder, page]);
useEffect(() => {
if (autoFetch) {
fetchEntries();
}
}, [fetchEntries, autoFetch]);
const createEntry = async (data: CreateJournalEntryDto) => {
setIsLoading(true);
try {
const newEntry = await financialApi.createEntry(data);
await fetchEntries();
return newEntry;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al crear asiento');
throw err;
} finally {
setIsLoading(false);
}
};
const updateEntry = async (id: string, data: UpdateJournalEntryDto) => {
setIsLoading(true);
try {
const updated = await financialApi.updateEntry(id, data);
await fetchEntries();
return updated;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al actualizar asiento');
throw err;
} finally {
setIsLoading(false);
}
};
const deleteEntry = async (id: string) => {
setIsLoading(true);
try {
await financialApi.deleteEntry(id);
await fetchEntries();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al eliminar asiento');
throw err;
} finally {
setIsLoading(false);
}
};
const postEntry = async (id: string) => {
setIsLoading(true);
try {
await financialApi.postEntry(id);
await fetchEntries();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al publicar asiento');
throw err;
} finally {
setIsLoading(false);
}
};
const cancelEntry = async (id: string) => {
setIsLoading(true);
try {
await financialApi.cancelEntry(id);
await fetchEntries();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cancelar asiento');
throw err;
} finally {
setIsLoading(false);
}
};
return {
entries,
total,
page,
totalPages,
isLoading,
error,
setPage,
refresh: fetchEntries,
createEntry,
updateEntry,
deleteEntry,
postEntry,
cancelEntry,
};
}
// ==================== Single Journal Entry Hook ====================
export function useJournalEntry(entryId: string | null) {
const [entry, setEntry] = useState<JournalEntry | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchEntry = useCallback(async () => {
if (!entryId) {
setEntry(null);
return;
}
setIsLoading(true);
setError(null);
try {
const data = await financialApi.getEntryById(entryId);
setEntry(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar asiento');
} finally {
setIsLoading(false);
}
}, [entryId]);
useEffect(() => {
fetchEntry();
}, [fetchEntry]);
return {
entry,
isLoading,
error,
refresh: fetchEntry,
};
}
// ==================== Invoices Hook ====================
export interface UseInvoicesOptions extends InvoiceFilters {
autoFetch?: boolean;
}
export function useInvoices(options: UseInvoicesOptions = {}) {
const { autoFetch = true, ...filters } = options;
const [invoices, setInvoices] = useState<FinancialInvoice[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(filters.page || 1);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchInvoices = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await financialApi.getInvoices({ ...filters, page });
setInvoices(response.data);
setTotal(response.meta.total);
setTotalPages(response.meta.totalPages);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar facturas');
} finally {
setIsLoading(false);
}
}, [filters.companyId, filters.partnerId, filters.invoiceType, filters.status, filters.dateFrom, filters.dateTo, filters.search, filters.limit, filters.sortBy, filters.sortOrder, page]);
useEffect(() => {
if (autoFetch) {
fetchInvoices();
}
}, [fetchInvoices, autoFetch]);
const createInvoice = async (data: CreateInvoiceDto) => {
setIsLoading(true);
try {
const newInvoice = await financialApi.createInvoice(data);
await fetchInvoices();
return newInvoice;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al crear factura');
throw err;
} finally {
setIsLoading(false);
}
};
const updateInvoice = async (id: string, data: UpdateInvoiceDto) => {
setIsLoading(true);
try {
const updated = await financialApi.updateInvoice(id, data);
await fetchInvoices();
return updated;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al actualizar factura');
throw err;
} finally {
setIsLoading(false);
}
};
const deleteInvoice = async (id: string) => {
setIsLoading(true);
try {
await financialApi.deleteInvoice(id);
await fetchInvoices();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al eliminar factura');
throw err;
} finally {
setIsLoading(false);
}
};
const validateInvoice = async (id: string) => {
setIsLoading(true);
try {
await financialApi.validateInvoice(id);
await fetchInvoices();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al validar factura');
throw err;
} finally {
setIsLoading(false);
}
};
const cancelInvoice = async (id: string) => {
setIsLoading(true);
try {
await financialApi.cancelInvoice(id);
await fetchInvoices();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cancelar factura');
throw err;
} finally {
setIsLoading(false);
}
};
return {
invoices,
total,
page,
totalPages,
isLoading,
error,
setPage,
refresh: fetchInvoices,
createInvoice,
updateInvoice,
deleteInvoice,
validateInvoice,
cancelInvoice,
};
}
// ==================== Single Invoice Hook ====================
export function useInvoice(invoiceId: string | null) {
const [invoice, setInvoice] = useState<FinancialInvoice | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchInvoice = useCallback(async () => {
if (!invoiceId) {
setInvoice(null);
return;
}
setIsLoading(true);
setError(null);
try {
const data = await financialApi.getInvoiceById(invoiceId);
setInvoice(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar factura');
} finally {
setIsLoading(false);
}
}, [invoiceId]);
useEffect(() => {
fetchInvoice();
}, [fetchInvoice]);
return {
invoice,
isLoading,
error,
refresh: fetchInvoice,
};
}
// ==================== Payments Hook ====================
export interface UsePaymentsOptions extends PaymentFilters {
autoFetch?: boolean;
}
export function usePayments(options: UsePaymentsOptions = {}) {
const { autoFetch = true, ...filters } = options;
const [payments, setPayments] = useState<Payment[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(filters.page || 1);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchPayments = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await financialApi.getPayments({ ...filters, page });
setPayments(response.data);
setTotal(response.meta.total);
setTotalPages(response.meta.totalPages);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar pagos');
} finally {
setIsLoading(false);
}
}, [filters.companyId, filters.partnerId, filters.paymentType, filters.paymentMethod, filters.status, filters.dateFrom, filters.dateTo, filters.search, filters.limit, filters.sortBy, filters.sortOrder, page]);
useEffect(() => {
if (autoFetch) {
fetchPayments();
}
}, [fetchPayments, autoFetch]);
const createPayment = async (data: CreatePaymentDto) => {
setIsLoading(true);
try {
const newPayment = await financialApi.createPayment(data);
await fetchPayments();
return newPayment;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al crear pago');
throw err;
} finally {
setIsLoading(false);
}
};
const updatePayment = async (id: string, data: UpdatePaymentDto) => {
setIsLoading(true);
try {
const updated = await financialApi.updatePayment(id, data);
await fetchPayments();
return updated;
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al actualizar pago');
throw err;
} finally {
setIsLoading(false);
}
};
const deletePayment = async (id: string) => {
setIsLoading(true);
try {
await financialApi.deletePayment(id);
await fetchPayments();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al eliminar pago');
throw err;
} finally {
setIsLoading(false);
}
};
const postPayment = async (id: string) => {
setIsLoading(true);
try {
await financialApi.postPayment(id);
await fetchPayments();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al publicar pago');
throw err;
} finally {
setIsLoading(false);
}
};
const cancelPayment = async (id: string) => {
setIsLoading(true);
try {
await financialApi.cancelPayment(id);
await fetchPayments();
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cancelar pago');
throw err;
} finally {
setIsLoading(false);
}
};
return {
payments,
total,
page,
totalPages,
isLoading,
error,
setPage,
refresh: fetchPayments,
createPayment,
updatePayment,
deletePayment,
postPayment,
cancelPayment,
};
}
// ==================== Single Payment Hook ====================
export function usePayment(paymentId: string | null) {
const [payment, setPayment] = useState<Payment | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchPayment = useCallback(async () => {
if (!paymentId) {
setPayment(null);
return;
}
setIsLoading(true);
setError(null);
try {
const data = await financialApi.getPaymentById(paymentId);
setPayment(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar pago');
} finally {
setIsLoading(false);
}
}, [paymentId]);
useEffect(() => {
fetchPayment();
}, [fetchPayment]);
return {
payment,
isLoading,
error,
refresh: fetchPayment,
};
}