feat(catalogs): Add fiscal catalog components (MGN-005)
- Add types for fiscal entities (TaxCategory, FiscalRegime, CfdiUse, etc.) - Add API clients for fiscal endpoints - Add hooks for fiscal data fetching - Add components: - FiscalRegimeSelect - SAT fiscal regime selector - CfdiUseSelect - CFDI use code selector - FiscalPaymentMethodSelect - SAT payment method selector - FiscalPaymentTypeSelect - PUE/PPD selector - TaxCategorySelect - Tax category selector - WithholdingTypeSelect - Withholding type selector - FiscalDataInput - Compound component for fiscal data collection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
94ba9f6bcd
commit
c9e1c3fb06
255
src/features/catalogs/api/fiscal.api.ts
Normal file
255
src/features/catalogs/api/fiscal.api.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type {
|
||||||
|
TaxCategory,
|
||||||
|
FiscalRegime,
|
||||||
|
CfdiUse,
|
||||||
|
FiscalPaymentMethod,
|
||||||
|
FiscalPaymentType,
|
||||||
|
WithholdingType,
|
||||||
|
TaxNature,
|
||||||
|
PersonType,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const API_BASE = '/api/v1/fiscal';
|
||||||
|
|
||||||
|
// ========== TAX CATEGORIES API ==========
|
||||||
|
|
||||||
|
export interface TaxCategoryFilter {
|
||||||
|
taxNature?: TaxNature;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const taxCategoriesApi = {
|
||||||
|
getAll: async (filter?: TaxCategoryFilter): Promise<TaxCategory[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter?.taxNature) params.append('tax_nature', filter.taxNature);
|
||||||
|
if (filter?.active !== undefined) params.append('active', String(filter.active));
|
||||||
|
|
||||||
|
const response = await axios.get(`${API_BASE}/tax-categories`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: async (id: string): Promise<TaxCategory> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/tax-categories/${id}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getByCode: async (code: string): Promise<TaxCategory | null> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/tax-categories/by-code/${code}`);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getBySatCode: async (satCode: string): Promise<TaxCategory | null> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/tax-categories/by-sat-code/${satCode}`);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== FISCAL REGIMES API ==========
|
||||||
|
|
||||||
|
export interface FiscalRegimeFilter {
|
||||||
|
appliesTo?: PersonType;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fiscalRegimesApi = {
|
||||||
|
getAll: async (filter?: FiscalRegimeFilter): Promise<FiscalRegime[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter?.appliesTo) params.append('applies_to', filter.appliesTo);
|
||||||
|
if (filter?.active !== undefined) params.append('active', String(filter.active));
|
||||||
|
|
||||||
|
const response = await axios.get(`${API_BASE}/fiscal-regimes`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: async (id: string): Promise<FiscalRegime> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/fiscal-regimes/${id}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getByCode: async (code: string): Promise<FiscalRegime | null> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/fiscal-regimes/by-code/${code}`);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getForPersonType: async (personType: PersonType): Promise<FiscalRegime[]> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/fiscal-regimes/person-type/${personType}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== CFDI USES API ==========
|
||||||
|
|
||||||
|
export interface CfdiUseFilter {
|
||||||
|
appliesTo?: PersonType;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cfdiUsesApi = {
|
||||||
|
getAll: async (filter?: CfdiUseFilter): Promise<CfdiUse[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter?.appliesTo) params.append('applies_to', filter.appliesTo);
|
||||||
|
if (filter?.active !== undefined) params.append('active', String(filter.active));
|
||||||
|
|
||||||
|
const response = await axios.get(`${API_BASE}/cfdi-uses`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: async (id: string): Promise<CfdiUse> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/cfdi-uses/${id}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getByCode: async (code: string): Promise<CfdiUse | null> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/cfdi-uses/by-code/${code}`);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getForPersonType: async (personType: PersonType): Promise<CfdiUse[]> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/cfdi-uses/person-type/${personType}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getForRegime: async (regimeCode: string): Promise<CfdiUse[]> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/cfdi-uses/regime/${regimeCode}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== PAYMENT METHODS API (SAT c_FormaPago) ==========
|
||||||
|
|
||||||
|
export interface PaymentMethodFilter {
|
||||||
|
requiresBankInfo?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fiscalPaymentMethodsApi = {
|
||||||
|
getAll: async (filter?: PaymentMethodFilter): Promise<FiscalPaymentMethod[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter?.requiresBankInfo !== undefined) {
|
||||||
|
params.append('requires_bank_info', String(filter.requiresBankInfo));
|
||||||
|
}
|
||||||
|
if (filter?.active !== undefined) params.append('active', String(filter.active));
|
||||||
|
|
||||||
|
const response = await axios.get(`${API_BASE}/payment-methods`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: async (id: string): Promise<FiscalPaymentMethod> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/payment-methods/${id}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getByCode: async (code: string): Promise<FiscalPaymentMethod | null> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/payment-methods/by-code/${code}`);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== PAYMENT TYPES API (SAT c_MetodoPago) ==========
|
||||||
|
|
||||||
|
export interface PaymentTypeFilter {
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fiscalPaymentTypesApi = {
|
||||||
|
getAll: async (filter?: PaymentTypeFilter): Promise<FiscalPaymentType[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter?.active !== undefined) params.append('active', String(filter.active));
|
||||||
|
|
||||||
|
const response = await axios.get(`${API_BASE}/payment-types`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: async (id: string): Promise<FiscalPaymentType> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/payment-types/${id}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getByCode: async (code: string): Promise<FiscalPaymentType | null> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/payment-types/by-code/${code}`);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== WITHHOLDING TYPES API ==========
|
||||||
|
|
||||||
|
export interface WithholdingTypeFilter {
|
||||||
|
taxCategoryId?: string;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withholdingTypesApi = {
|
||||||
|
getAll: async (filter?: WithholdingTypeFilter): Promise<WithholdingType[]> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter?.taxCategoryId) params.append('tax_category_id', filter.taxCategoryId);
|
||||||
|
if (filter?.active !== undefined) params.append('active', String(filter.active));
|
||||||
|
|
||||||
|
const response = await axios.get(`${API_BASE}/withholding-types`, { params });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: async (id: string): Promise<WithholdingType> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/withholding-types/${id}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getByCode: async (code: string): Promise<WithholdingType | null> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE}/withholding-types/by-code/${code}`);
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getByCategory: async (categoryId: string): Promise<WithholdingType[]> => {
|
||||||
|
const response = await axios.get(`${API_BASE}/withholding-types/by-category/${categoryId}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,3 +1,19 @@
|
|||||||
export { countriesApi } from './countries.api';
|
export { countriesApi } from './countries.api';
|
||||||
export { currenciesApi } from './currencies.api';
|
export { currenciesApi } from './currencies.api';
|
||||||
export { uomApi } from './uom.api';
|
export { uomApi } from './uom.api';
|
||||||
|
export {
|
||||||
|
taxCategoriesApi,
|
||||||
|
fiscalRegimesApi,
|
||||||
|
cfdiUsesApi,
|
||||||
|
fiscalPaymentMethodsApi,
|
||||||
|
fiscalPaymentTypesApi,
|
||||||
|
withholdingTypesApi,
|
||||||
|
} from './fiscal.api';
|
||||||
|
export type {
|
||||||
|
TaxCategoryFilter,
|
||||||
|
FiscalRegimeFilter,
|
||||||
|
CfdiUseFilter,
|
||||||
|
PaymentMethodFilter,
|
||||||
|
PaymentTypeFilter,
|
||||||
|
WithholdingTypeFilter,
|
||||||
|
} from './fiscal.api';
|
||||||
|
|||||||
68
src/features/catalogs/components/CfdiUseSelect.tsx
Normal file
68
src/features/catalogs/components/CfdiUseSelect.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { useCfdiUses, useCfdiUsesForRegime } from '../hooks/useFiscalCatalogs';
|
||||||
|
import type { PersonType } from '../types';
|
||||||
|
|
||||||
|
interface CfdiUseSelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
personType?: PersonType;
|
||||||
|
regimeCode?: string;
|
||||||
|
activeOnly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CfdiUseSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
personType,
|
||||||
|
regimeCode,
|
||||||
|
activeOnly = true,
|
||||||
|
placeholder = 'Seleccionar uso de CFDI',
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
error,
|
||||||
|
required = false,
|
||||||
|
label,
|
||||||
|
}: CfdiUseSelectProps) {
|
||||||
|
// Use regime specific hook if regimeCode is provided, otherwise use general hook
|
||||||
|
const allUses = useCfdiUses({ appliesTo: personType, active: activeOnly });
|
||||||
|
const regimeUses = useCfdiUsesForRegime(regimeCode);
|
||||||
|
|
||||||
|
const { uses, loading, error: loadError } = regimeCode ? regimeUses : allUses;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-1 ${className}`}>
|
||||||
|
{label && (
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
|
||||||
|
error || loadError ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Cargando...' : placeholder}</option>
|
||||||
|
{uses.map((use) => (
|
||||||
|
<option key={use.id} value={use.id}>
|
||||||
|
{use.code} - {use.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(error || loadError) && (
|
||||||
|
<span className="text-sm text-red-500">{error || loadError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CfdiUseSelect;
|
||||||
130
src/features/catalogs/components/FiscalDataInput.tsx
Normal file
130
src/features/catalogs/components/FiscalDataInput.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { FiscalRegimeSelect } from './FiscalRegimeSelect';
|
||||||
|
import { CfdiUseSelect } from './CfdiUseSelect';
|
||||||
|
import { FiscalPaymentMethodSelect } from './FiscalPaymentMethodSelect';
|
||||||
|
import { FiscalPaymentTypeSelect } from './FiscalPaymentTypeSelect';
|
||||||
|
import type { PersonType } from '../types';
|
||||||
|
|
||||||
|
export interface FiscalData {
|
||||||
|
taxId?: string;
|
||||||
|
fiscalRegimeId?: string;
|
||||||
|
cfdiUseId?: string;
|
||||||
|
paymentMethodId?: string;
|
||||||
|
paymentTypeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FiscalDataInputProps {
|
||||||
|
value: FiscalData;
|
||||||
|
onChange: (value: FiscalData) => void;
|
||||||
|
personType?: PersonType;
|
||||||
|
showTaxId?: boolean;
|
||||||
|
showPaymentFields?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
errors?: {
|
||||||
|
taxId?: string;
|
||||||
|
fiscalRegimeId?: string;
|
||||||
|
cfdiUseId?: string;
|
||||||
|
paymentMethodId?: string;
|
||||||
|
paymentTypeId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FiscalDataInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
personType = 'both',
|
||||||
|
showTaxId = true,
|
||||||
|
showPaymentFields = false,
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
errors = {},
|
||||||
|
}: FiscalDataInputProps) {
|
||||||
|
const handleChange = (field: keyof FiscalData, fieldValue: string) => {
|
||||||
|
onChange({ ...value, [field]: fieldValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegimeChange = (regimeId: string) => {
|
||||||
|
handleChange('fiscalRegimeId', regimeId);
|
||||||
|
// Reset CFDI use when regime changes
|
||||||
|
handleChange('cfdiUseId', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-4 ${className}`}>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Datos Fiscales</h3>
|
||||||
|
|
||||||
|
{showTaxId && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
RFC <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value.taxId || ''}
|
||||||
|
onChange={(e) => handleChange('taxId', e.target.value.toUpperCase())}
|
||||||
|
placeholder="XAXX010101000"
|
||||||
|
disabled={disabled}
|
||||||
|
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed uppercase ${
|
||||||
|
errors.taxId ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
maxLength={13}
|
||||||
|
/>
|
||||||
|
{errors.taxId && (
|
||||||
|
<span className="text-sm text-red-500">{errors.taxId}</span>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{personType === 'natural'
|
||||||
|
? '13 caracteres para persona física'
|
||||||
|
: personType === 'legal'
|
||||||
|
? '12 caracteres para persona moral'
|
||||||
|
: '12-13 caracteres'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FiscalRegimeSelect
|
||||||
|
value={value.fiscalRegimeId || ''}
|
||||||
|
onChange={handleRegimeChange}
|
||||||
|
personType={personType}
|
||||||
|
disabled={disabled}
|
||||||
|
label="Régimen Fiscal"
|
||||||
|
required
|
||||||
|
error={errors.fiscalRegimeId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CfdiUseSelect
|
||||||
|
value={value.cfdiUseId || ''}
|
||||||
|
onChange={(v) => handleChange('cfdiUseId', v)}
|
||||||
|
personType={personType}
|
||||||
|
disabled={disabled}
|
||||||
|
label="Uso del CFDI"
|
||||||
|
required
|
||||||
|
error={errors.cfdiUseId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showPaymentFields && (
|
||||||
|
<>
|
||||||
|
<FiscalPaymentMethodSelect
|
||||||
|
value={value.paymentMethodId || ''}
|
||||||
|
onChange={(v) => handleChange('paymentMethodId', v)}
|
||||||
|
disabled={disabled}
|
||||||
|
label="Forma de Pago (SAT)"
|
||||||
|
required
|
||||||
|
error={errors.paymentMethodId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FiscalPaymentTypeSelect
|
||||||
|
value={value.paymentTypeId || ''}
|
||||||
|
onChange={(v) => handleChange('paymentTypeId', v)}
|
||||||
|
disabled={disabled}
|
||||||
|
label="Método de Pago (SAT)"
|
||||||
|
required
|
||||||
|
error={errors.paymentTypeId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiscalDataInput;
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { useFiscalPaymentMethods } from '../hooks/useFiscalCatalogs';
|
||||||
|
|
||||||
|
interface FiscalPaymentMethodSelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
activeOnly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FiscalPaymentMethodSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
activeOnly = true,
|
||||||
|
placeholder = 'Seleccionar forma de pago',
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
error,
|
||||||
|
required = false,
|
||||||
|
label,
|
||||||
|
}: FiscalPaymentMethodSelectProps) {
|
||||||
|
const { methods, loading, error: loadError } = useFiscalPaymentMethods({ active: activeOnly });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-1 ${className}`}>
|
||||||
|
{label && (
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
|
||||||
|
error || loadError ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Cargando...' : placeholder}</option>
|
||||||
|
{methods.map((method) => (
|
||||||
|
<option key={method.id} value={method.id}>
|
||||||
|
{method.code} - {method.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(error || loadError) && (
|
||||||
|
<span className="text-sm text-red-500">{error || loadError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiscalPaymentMethodSelect;
|
||||||
62
src/features/catalogs/components/FiscalPaymentTypeSelect.tsx
Normal file
62
src/features/catalogs/components/FiscalPaymentTypeSelect.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useFiscalPaymentTypes } from '../hooks/useFiscalCatalogs';
|
||||||
|
|
||||||
|
interface FiscalPaymentTypeSelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
activeOnly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FiscalPaymentTypeSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
activeOnly = true,
|
||||||
|
placeholder = 'Seleccionar método de pago',
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
error,
|
||||||
|
required = false,
|
||||||
|
label,
|
||||||
|
}: FiscalPaymentTypeSelectProps) {
|
||||||
|
const { types, loading, error: loadError } = useFiscalPaymentTypes({ active: activeOnly });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-1 ${className}`}>
|
||||||
|
{label && (
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
|
||||||
|
error || loadError ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Cargando...' : placeholder}</option>
|
||||||
|
{types.map((type) => (
|
||||||
|
<option key={type.id} value={type.id}>
|
||||||
|
{type.code} - {type.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(error || loadError) && (
|
||||||
|
<span className="text-sm text-red-500">{error || loadError}</span>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
PUE: Pago en una exhibición | PPD: Pago en parcialidades o diferido
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiscalPaymentTypeSelect;
|
||||||
66
src/features/catalogs/components/FiscalRegimeSelect.tsx
Normal file
66
src/features/catalogs/components/FiscalRegimeSelect.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { useFiscalRegimes, useFiscalRegimesByPersonType } from '../hooks/useFiscalCatalogs';
|
||||||
|
import type { PersonType } from '../types';
|
||||||
|
|
||||||
|
interface FiscalRegimeSelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
personType?: PersonType;
|
||||||
|
activeOnly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FiscalRegimeSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
personType,
|
||||||
|
activeOnly = true,
|
||||||
|
placeholder = 'Seleccionar régimen fiscal',
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
error,
|
||||||
|
required = false,
|
||||||
|
label,
|
||||||
|
}: FiscalRegimeSelectProps) {
|
||||||
|
// Use person type specific hook if personType is provided
|
||||||
|
const allRegimes = useFiscalRegimes({ active: activeOnly });
|
||||||
|
const filteredRegimes = useFiscalRegimesByPersonType(personType);
|
||||||
|
|
||||||
|
const { regimes, loading, error: loadError } = personType ? filteredRegimes : allRegimes;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-1 ${className}`}>
|
||||||
|
{label && (
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
|
||||||
|
error || loadError ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Cargando...' : placeholder}</option>
|
||||||
|
{regimes.map((regime) => (
|
||||||
|
<option key={regime.id} value={regime.id}>
|
||||||
|
{regime.code} - {regime.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(error || loadError) && (
|
||||||
|
<span className="text-sm text-red-500">{error || loadError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiscalRegimeSelect;
|
||||||
65
src/features/catalogs/components/TaxCategorySelect.tsx
Normal file
65
src/features/catalogs/components/TaxCategorySelect.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useTaxCategories } from '../hooks/useFiscalCatalogs';
|
||||||
|
import type { TaxNature } from '../types';
|
||||||
|
|
||||||
|
interface TaxCategorySelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
taxNature?: TaxNature;
|
||||||
|
activeOnly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaxCategorySelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
taxNature,
|
||||||
|
activeOnly = true,
|
||||||
|
placeholder = 'Seleccionar categoría de impuesto',
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
error,
|
||||||
|
required = false,
|
||||||
|
label,
|
||||||
|
}: TaxCategorySelectProps) {
|
||||||
|
const { categories, loading, error: loadError } = useTaxCategories({
|
||||||
|
taxNature,
|
||||||
|
active: activeOnly,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-1 ${className}`}>
|
||||||
|
{label && (
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
|
||||||
|
error || loadError ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Cargando...' : placeholder}</option>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<option key={cat.id} value={cat.id}>
|
||||||
|
{cat.code} - {cat.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(error || loadError) && (
|
||||||
|
<span className="text-sm text-red-500">{error || loadError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaxCategorySelect;
|
||||||
67
src/features/catalogs/components/WithholdingTypeSelect.tsx
Normal file
67
src/features/catalogs/components/WithholdingTypeSelect.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useWithholdingTypes, useWithholdingTypesByCategory } from '../hooks/useFiscalCatalogs';
|
||||||
|
|
||||||
|
interface WithholdingTypeSelectProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
taxCategoryId?: string;
|
||||||
|
activeOnly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
showRate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WithholdingTypeSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
taxCategoryId,
|
||||||
|
activeOnly = true,
|
||||||
|
placeholder = 'Seleccionar tipo de retención',
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
error,
|
||||||
|
required = false,
|
||||||
|
label,
|
||||||
|
showRate = true,
|
||||||
|
}: WithholdingTypeSelectProps) {
|
||||||
|
// Use category specific hook if taxCategoryId is provided
|
||||||
|
const allTypes = useWithholdingTypes({ active: activeOnly });
|
||||||
|
const categoryTypes = useWithholdingTypesByCategory(taxCategoryId);
|
||||||
|
|
||||||
|
const { types, loading, error: loadError } = taxCategoryId ? categoryTypes : allTypes;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-1 ${className}`}>
|
||||||
|
{label && (
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
|
||||||
|
error || loadError ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Cargando...' : placeholder}</option>
|
||||||
|
{types.map((type) => (
|
||||||
|
<option key={type.id} value={type.id}>
|
||||||
|
{type.code} - {type.name} {showRate && `(${type.defaultRate}%)`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(error || loadError) && (
|
||||||
|
<span className="text-sm text-red-500">{error || loadError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WithholdingTypeSelect;
|
||||||
@ -14,3 +14,13 @@ export type { AddressData } from './AddressInput';
|
|||||||
// Display components
|
// Display components
|
||||||
export { ConversionTableDisplay } from './ConversionTableDisplay';
|
export { ConversionTableDisplay } from './ConversionTableDisplay';
|
||||||
export { CurrencyRatesDisplay } from './CurrencyRatesDisplay';
|
export { CurrencyRatesDisplay } from './CurrencyRatesDisplay';
|
||||||
|
|
||||||
|
// Fiscal components
|
||||||
|
export { FiscalRegimeSelect } from './FiscalRegimeSelect';
|
||||||
|
export { CfdiUseSelect } from './CfdiUseSelect';
|
||||||
|
export { FiscalPaymentMethodSelect } from './FiscalPaymentMethodSelect';
|
||||||
|
export { FiscalPaymentTypeSelect } from './FiscalPaymentTypeSelect';
|
||||||
|
export { TaxCategorySelect } from './TaxCategorySelect';
|
||||||
|
export { WithholdingTypeSelect } from './WithholdingTypeSelect';
|
||||||
|
export { FiscalDataInput } from './FiscalDataInput';
|
||||||
|
export type { FiscalData } from './FiscalDataInput';
|
||||||
|
|||||||
@ -1,3 +1,14 @@
|
|||||||
export { useCountries, useCountry, useStates } from './useCountries';
|
export { useCountries, useCountry, useStates } from './useCountries';
|
||||||
export { useCurrencies, useCurrencyRates, useLatestRates, useCurrencyConversion } from './useCurrencies';
|
export { useCurrencies, useCurrencyRates, useLatestRates, useCurrencyConversion } from './useCurrencies';
|
||||||
export { useUomCategories, useUom, useUomConversion, useConversionTable } from './useUom';
|
export { useUomCategories, useUom, useUomConversion, useConversionTable } from './useUom';
|
||||||
|
export {
|
||||||
|
useTaxCategories,
|
||||||
|
useFiscalRegimes,
|
||||||
|
useFiscalRegimesByPersonType,
|
||||||
|
useCfdiUses,
|
||||||
|
useCfdiUsesForRegime,
|
||||||
|
useFiscalPaymentMethods,
|
||||||
|
useFiscalPaymentTypes,
|
||||||
|
useWithholdingTypes,
|
||||||
|
useWithholdingTypesByCategory,
|
||||||
|
} from './useFiscalCatalogs';
|
||||||
|
|||||||
276
src/features/catalogs/hooks/useFiscalCatalogs.ts
Normal file
276
src/features/catalogs/hooks/useFiscalCatalogs.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
taxCategoriesApi,
|
||||||
|
fiscalRegimesApi,
|
||||||
|
cfdiUsesApi,
|
||||||
|
fiscalPaymentMethodsApi,
|
||||||
|
fiscalPaymentTypesApi,
|
||||||
|
withholdingTypesApi,
|
||||||
|
TaxCategoryFilter,
|
||||||
|
FiscalRegimeFilter,
|
||||||
|
CfdiUseFilter,
|
||||||
|
PaymentMethodFilter,
|
||||||
|
PaymentTypeFilter,
|
||||||
|
WithholdingTypeFilter,
|
||||||
|
} from '../api/fiscal.api';
|
||||||
|
import type {
|
||||||
|
TaxCategory,
|
||||||
|
FiscalRegime,
|
||||||
|
CfdiUse,
|
||||||
|
FiscalPaymentMethod,
|
||||||
|
FiscalPaymentType,
|
||||||
|
WithholdingType,
|
||||||
|
PersonType,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
// ========== TAX CATEGORIES HOOK ==========
|
||||||
|
|
||||||
|
export function useTaxCategories(filter?: TaxCategoryFilter) {
|
||||||
|
const [categories, setCategories] = useState<TaxCategory[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchCategories = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await taxCategoriesApi.getAll(filter);
|
||||||
|
setCategories(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar categorías de impuestos');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter?.taxNature, filter?.active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCategories();
|
||||||
|
}, [fetchCategories]);
|
||||||
|
|
||||||
|
return { categories, loading, error, refetch: fetchCategories };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== FISCAL REGIMES HOOK ==========
|
||||||
|
|
||||||
|
export function useFiscalRegimes(filter?: FiscalRegimeFilter) {
|
||||||
|
const [regimes, setRegimes] = useState<FiscalRegime[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchRegimes = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await fiscalRegimesApi.getAll(filter);
|
||||||
|
setRegimes(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar regímenes fiscales');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter?.appliesTo, filter?.active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRegimes();
|
||||||
|
}, [fetchRegimes]);
|
||||||
|
|
||||||
|
return { regimes, loading, error, refetch: fetchRegimes };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFiscalRegimesByPersonType(personType: PersonType | undefined) {
|
||||||
|
const [regimes, setRegimes] = useState<FiscalRegime[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchRegimes = useCallback(async () => {
|
||||||
|
if (!personType) {
|
||||||
|
setRegimes([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await fiscalRegimesApi.getForPersonType(personType);
|
||||||
|
setRegimes(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar regímenes fiscales');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [personType]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRegimes();
|
||||||
|
}, [fetchRegimes]);
|
||||||
|
|
||||||
|
return { regimes, loading, error, refetch: fetchRegimes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== CFDI USES HOOK ==========
|
||||||
|
|
||||||
|
export function useCfdiUses(filter?: CfdiUseFilter) {
|
||||||
|
const [uses, setUses] = useState<CfdiUse[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchUses = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await cfdiUsesApi.getAll(filter);
|
||||||
|
setUses(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar usos de CFDI');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter?.appliesTo, filter?.active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUses();
|
||||||
|
}, [fetchUses]);
|
||||||
|
|
||||||
|
return { uses, loading, error, refetch: fetchUses };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCfdiUsesForRegime(regimeCode: string | undefined) {
|
||||||
|
const [uses, setUses] = useState<CfdiUse[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchUses = useCallback(async () => {
|
||||||
|
if (!regimeCode) {
|
||||||
|
setUses([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await cfdiUsesApi.getForRegime(regimeCode);
|
||||||
|
setUses(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar usos de CFDI');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [regimeCode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUses();
|
||||||
|
}, [fetchUses]);
|
||||||
|
|
||||||
|
return { uses, loading, error, refetch: fetchUses };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== PAYMENT METHODS HOOK ==========
|
||||||
|
|
||||||
|
export function useFiscalPaymentMethods(filter?: PaymentMethodFilter) {
|
||||||
|
const [methods, setMethods] = useState<FiscalPaymentMethod[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchMethods = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await fiscalPaymentMethodsApi.getAll(filter);
|
||||||
|
setMethods(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar formas de pago');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter?.requiresBankInfo, filter?.active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMethods();
|
||||||
|
}, [fetchMethods]);
|
||||||
|
|
||||||
|
return { methods, loading, error, refetch: fetchMethods };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== PAYMENT TYPES HOOK ==========
|
||||||
|
|
||||||
|
export function useFiscalPaymentTypes(filter?: PaymentTypeFilter) {
|
||||||
|
const [types, setTypes] = useState<FiscalPaymentType[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchTypes = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await fiscalPaymentTypesApi.getAll(filter);
|
||||||
|
setTypes(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar métodos de pago');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter?.active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTypes();
|
||||||
|
}, [fetchTypes]);
|
||||||
|
|
||||||
|
return { types, loading, error, refetch: fetchTypes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== WITHHOLDING TYPES HOOK ==========
|
||||||
|
|
||||||
|
export function useWithholdingTypes(filter?: WithholdingTypeFilter) {
|
||||||
|
const [types, setTypes] = useState<WithholdingType[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchTypes = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await withholdingTypesApi.getAll(filter);
|
||||||
|
setTypes(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar tipos de retención');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [filter?.taxCategoryId, filter?.active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTypes();
|
||||||
|
}, [fetchTypes]);
|
||||||
|
|
||||||
|
return { types, loading, error, refetch: fetchTypes };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWithholdingTypesByCategory(categoryId: string | undefined) {
|
||||||
|
const [types, setTypes] = useState<WithholdingType[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchTypes = useCallback(async () => {
|
||||||
|
if (!categoryId) {
|
||||||
|
setTypes([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await withholdingTypesApi.getByCategory(categoryId);
|
||||||
|
setTypes(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error al cargar tipos de retención');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [categoryId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTypes();
|
||||||
|
}, [fetchTypes]);
|
||||||
|
|
||||||
|
return { types, loading, error, refetch: fetchTypes };
|
||||||
|
}
|
||||||
@ -273,3 +273,86 @@ export interface DiscountRule {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== FISCAL TYPES ==========
|
||||||
|
|
||||||
|
// Tax Nature
|
||||||
|
export type TaxNature = 'tax' | 'withholding' | 'both';
|
||||||
|
|
||||||
|
// Person Type
|
||||||
|
export type PersonType = 'natural' | 'legal' | 'both';
|
||||||
|
|
||||||
|
// Tax Category (IVA, ISR, IEPS, etc.)
|
||||||
|
export interface TaxCategory {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
taxNature: TaxNature;
|
||||||
|
satCode?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fiscal Regime (SAT c_RegimenFiscal)
|
||||||
|
export interface FiscalRegime {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
appliesTo: PersonType;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CFDI Use (SAT c_UsoCFDI)
|
||||||
|
export interface CfdiUse {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
appliesTo: PersonType;
|
||||||
|
allowedRegimes?: string[];
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment Method - SAT Forms of Payment (c_FormaPago)
|
||||||
|
export interface FiscalPaymentMethod {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
requiresBankInfo: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment Type - SAT Payment Methods (c_MetodoPago: PUE, PPD)
|
||||||
|
export interface FiscalPaymentType {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Withholding Type
|
||||||
|
export interface WithholdingType {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
defaultRate: number;
|
||||||
|
taxCategoryId?: string;
|
||||||
|
taxCategory?: TaxCategory;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user