Compare commits
No commits in common. "3a461cb184d9f37fb804a315a74609ca4a9ca5b9" and "aba79054558a86b21c3c19d641ea8f291e23c9df" have entirely different histories.
3a461cb184
...
aba7905455
38
.gitignore
vendored
38
.gitignore
vendored
@ -1,41 +1,47 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Build output
|
||||
# Build
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
|
||||
# TypeScript build info
|
||||
*.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.node.tsbuildinfo
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.node.tsbuildinfo
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Misc
|
||||
*.local
|
||||
.eslintcache
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# erp-core-frontend-v2
|
||||
|
||||
Frontend de erp-core - Workspace V2
|
||||
@ -29,26 +29,6 @@ const PartnerDetailPage = lazy(() => import('@pages/partners/PartnerDetailPage')
|
||||
const PartnerCreatePage = lazy(() => import('@pages/partners/PartnerCreatePage'));
|
||||
const PartnerEditPage = lazy(() => import('@pages/partners/PartnerEditPage'));
|
||||
|
||||
// Billing pages
|
||||
const BillingPage = lazy(() => import('@pages/billing/BillingPage').then(m => ({ default: m.BillingPage })));
|
||||
const PlansPage = lazy(() => import('@pages/billing/PlansPage').then(m => ({ default: m.PlansPage })));
|
||||
const InvoicesPage = lazy(() => import('@pages/billing/InvoicesPage').then(m => ({ default: m.InvoicesPage })));
|
||||
const UsagePage = lazy(() => import('@pages/billing/UsagePage').then(m => ({ default: m.UsagePage })));
|
||||
|
||||
// Settings pages
|
||||
const SettingsPage = lazy(() => import('@pages/settings/SettingsPage').then(m => ({ default: m.SettingsPage })));
|
||||
const CompanySettingsPage = lazy(() => import('@pages/settings/CompanySettingsPage').then(m => ({ default: m.CompanySettingsPage })));
|
||||
const UsersSettingsPage = lazy(() => import('@pages/settings/UsersSettingsPage').then(m => ({ default: m.UsersSettingsPage })));
|
||||
const ProfileSettingsPage = lazy(() => import('@pages/settings/ProfileSettingsPage').then(m => ({ default: m.ProfileSettingsPage })));
|
||||
const SecuritySettingsPage = lazy(() => import('@pages/settings/SecuritySettingsPage').then(m => ({ default: m.SecuritySettingsPage })));
|
||||
const SystemSettingsPage = lazy(() => import('@pages/settings/SystemSettingsPage').then(m => ({ default: m.SystemSettingsPage })));
|
||||
const AuditLogsPage = lazy(() => import('@pages/settings/AuditLogsPage').then(m => ({ default: m.AuditLogsPage })));
|
||||
|
||||
// CRM pages
|
||||
const PipelineKanbanPage = lazy(() => import('@pages/crm/PipelineKanbanPage').then(m => ({ default: m.PipelineKanbanPage })));
|
||||
const LeadsPage = lazy(() => import('@pages/crm/LeadsPage').then(m => ({ default: m.LeadsPage })));
|
||||
const OpportunitiesPage = lazy(() => import('@pages/crm/OpportunitiesPage').then(m => ({ default: m.OpportunitiesPage })));
|
||||
|
||||
function LazyWrapper({ children }: { children: React.ReactNode }) {
|
||||
return <Suspense fallback={<FullPageSpinner />}>{children}</Suspense>;
|
||||
}
|
||||
@ -244,40 +224,11 @@ export const router = createBrowserRouter([
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
// CRM routes
|
||||
{
|
||||
path: '/crm',
|
||||
element: <Navigate to="/crm/pipeline" replace />,
|
||||
},
|
||||
{
|
||||
path: '/crm/pipeline',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<PipelineKanbanPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/crm/leads',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<LeadsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/crm/opportunities',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<OpportunitiesPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/crm/*',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<div className="text-center text-gray-500">Seccion CRM - En desarrollo</div>
|
||||
<div className="text-center text-gray-500">Módulo CRM - En desarrollo</div>
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
@ -289,110 +240,11 @@ export const router = createBrowserRouter([
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
// Settings routes
|
||||
{
|
||||
path: '/settings',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<SettingsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings/company',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<CompanySettingsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings/users',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<UsersSettingsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings/profile',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<ProfileSettingsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings/security',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<SecuritySettingsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings/security/audit-logs',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<AuditLogsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings/system',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<SystemSettingsPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/settings/*',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<div className="text-center text-gray-500">Seccion de Configuracion - En desarrollo</div>
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
|
||||
// Billing routes
|
||||
{
|
||||
path: '/billing',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<BillingPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/billing/plans',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<PlansPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/billing/invoices',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<InvoicesPage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/billing/usage',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<UsagePage />
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/billing/*',
|
||||
element: (
|
||||
<DashboardWrapper>
|
||||
<div className="text-center text-gray-500">Sección de facturación - En desarrollo</div>
|
||||
<div className="text-center text-gray-500">Configuración - En desarrollo</div>
|
||||
</DashboardWrapper>
|
||||
),
|
||||
},
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
Coupon,
|
||||
CreateCouponDto,
|
||||
UpdateCouponDto,
|
||||
CouponValidation,
|
||||
CouponFilters,
|
||||
CouponsResponse,
|
||||
CouponStats,
|
||||
CouponRedemption,
|
||||
} from '../types';
|
||||
|
||||
const BASE_URL = '/api/v1/billing/coupons';
|
||||
|
||||
export const couponsApi = {
|
||||
getAll: async (filters?: CouponFilters): Promise<CouponsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.discountType) params.append('discountType', filters.discountType);
|
||||
if (filters?.isActive !== undefined) params.append('isActive', String(filters.isActive));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<CouponsResponse>(`${BASE_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Coupon> => {
|
||||
const response = await api.get<Coupon>(`${BASE_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getByCode: async (code: string): Promise<Coupon> => {
|
||||
const response = await api.get<Coupon>(`${BASE_URL}/code/${code}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreateCouponDto): Promise<Coupon> => {
|
||||
const response = await api.post<Coupon>(BASE_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateCouponDto): Promise<Coupon> => {
|
||||
const response = await api.patch<Coupon>(`${BASE_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${BASE_URL}/${id}`);
|
||||
},
|
||||
|
||||
validate: async (code: string, planId?: string, amount?: number): Promise<CouponValidation> => {
|
||||
const response = await api.post<CouponValidation>(`${BASE_URL}/validate`, {
|
||||
code,
|
||||
planId,
|
||||
amount,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
activate: async (id: string): Promise<Coupon> => {
|
||||
const response = await api.post<Coupon>(`${BASE_URL}/${id}/activate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deactivate: async (id: string): Promise<Coupon> => {
|
||||
const response = await api.post<Coupon>(`${BASE_URL}/${id}/deactivate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getStats: async (): Promise<CouponStats> => {
|
||||
const response = await api.get<CouponStats>(`${BASE_URL}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getRedemptions: async (
|
||||
couponId: string
|
||||
): Promise<{ data: CouponRedemption[]; total: number }> => {
|
||||
const response = await api.get<{ data: CouponRedemption[]; total: number }>(
|
||||
`${BASE_URL}/${couponId}/redemptions`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
export { plansApi } from './plans.api';
|
||||
export { subscriptionsApi } from './subscriptions.api';
|
||||
export { invoicesApi } from './invoices.api';
|
||||
export { usageApi } from './usage.api';
|
||||
export { couponsApi } from './coupons.api';
|
||||
@ -1,91 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type { Invoice, CreateInvoiceDto, InvoiceFilters, InvoicesResponse } from '../types';
|
||||
|
||||
const BASE_URL = '/api/v1/billing/invoices';
|
||||
|
||||
export const invoicesApi = {
|
||||
getAll: async (filters?: InvoiceFilters): Promise<InvoicesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.tenantId) params.append('tenantId', filters.tenantId);
|
||||
if (filters?.companyId) params.append('companyId', filters.companyId);
|
||||
if (filters?.partnerId) params.append('partnerId', filters.partnerId);
|
||||
if (filters?.subscriptionId) params.append('subscriptionId', filters.subscriptionId);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.invoiceType) params.append('invoiceType', filters.invoiceType);
|
||||
if (filters?.invoiceContext) params.append('invoiceContext', filters.invoiceContext);
|
||||
if (filters?.startDate) params.append('startDate', filters.startDate);
|
||||
if (filters?.endDate) params.append('endDate', filters.endDate);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<InvoicesResponse>(`${BASE_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Invoice> => {
|
||||
const response = await api.get<Invoice>(`${BASE_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getByNumber: async (invoiceNumber: string): Promise<Invoice> => {
|
||||
const response = await api.get<Invoice>(`${BASE_URL}/number/${invoiceNumber}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getSaasInvoices: async (filters?: InvoiceFilters): Promise<InvoicesResponse> => {
|
||||
return invoicesApi.getAll({ ...filters, invoiceContext: 'saas' });
|
||||
},
|
||||
|
||||
create: async (data: CreateInvoiceDto): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(BASE_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
issue: async (id: string): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/issue`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markAsPaid: async (id: string, paymentDate?: string): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/mark-paid`, { paymentDate });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
cancel: async (id: string, reason?: string): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/cancel`, { reason });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
download: async (id: string): Promise<Blob> => {
|
||||
const response = await api.get<Blob>(`${BASE_URL}/${id}/download`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
sendByEmail: async (id: string, email?: string): Promise<void> => {
|
||||
await api.post(`${BASE_URL}/${id}/send-email`, { email });
|
||||
},
|
||||
|
||||
getUpcoming: async (): Promise<Invoice | null> => {
|
||||
try {
|
||||
const response = await api.get<Invoice>(`${BASE_URL}/upcoming`);
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getSummary: async (
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): Promise<{ total: number; paid: number; pending: number; overdue: number }> => {
|
||||
const response = await api.get<{ total: number; paid: number; pending: number; overdue: number }>(
|
||||
`${BASE_URL}/summary`,
|
||||
{ params: { startDate, endDate } }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@ -1,71 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
SubscriptionPlan,
|
||||
CreatePlanDto,
|
||||
UpdatePlanDto,
|
||||
PlanFilters,
|
||||
PlansResponse,
|
||||
} from '../types';
|
||||
|
||||
const BASE_URL = '/api/v1/billing/plans';
|
||||
|
||||
export const plansApi = {
|
||||
getAll: async (filters?: PlanFilters): Promise<PlansResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.planType) params.append('planType', filters.planType);
|
||||
if (filters?.isActive !== undefined) params.append('isActive', String(filters.isActive));
|
||||
if (filters?.isPublic !== undefined) params.append('isPublic', String(filters.isPublic));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<PlansResponse>(`${BASE_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPublic: async (): Promise<SubscriptionPlan[]> => {
|
||||
const response = await api.get<SubscriptionPlan[]>(`${BASE_URL}/public`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<SubscriptionPlan> => {
|
||||
const response = await api.get<SubscriptionPlan>(`${BASE_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getByCode: async (code: string): Promise<SubscriptionPlan> => {
|
||||
const response = await api.get<SubscriptionPlan>(`${BASE_URL}/code/${code}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreatePlanDto): Promise<SubscriptionPlan> => {
|
||||
const response = await api.post<SubscriptionPlan>(BASE_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdatePlanDto): Promise<SubscriptionPlan> => {
|
||||
const response = await api.patch<SubscriptionPlan>(`${BASE_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${BASE_URL}/${id}`);
|
||||
},
|
||||
|
||||
activate: async (id: string): Promise<SubscriptionPlan> => {
|
||||
const response = await api.post<SubscriptionPlan>(`${BASE_URL}/${id}/activate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deactivate: async (id: string): Promise<SubscriptionPlan> => {
|
||||
const response = await api.post<SubscriptionPlan>(`${BASE_URL}/${id}/deactivate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
comparePlans: async (planIds: string[]): Promise<SubscriptionPlan[]> => {
|
||||
const response = await api.post<SubscriptionPlan[]>(`${BASE_URL}/compare`, { planIds });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@ -1,102 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
TenantSubscription,
|
||||
CreateSubscriptionDto,
|
||||
UpdateSubscriptionDto,
|
||||
CancelSubscriptionDto,
|
||||
SubscriptionFilters,
|
||||
SubscriptionsResponse,
|
||||
} from '../types';
|
||||
|
||||
const BASE_URL = '/api/v1/billing/subscriptions';
|
||||
|
||||
export const subscriptionsApi = {
|
||||
getAll: async (filters?: SubscriptionFilters): Promise<SubscriptionsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.tenantId) params.append('tenantId', filters.tenantId);
|
||||
if (filters?.planId) params.append('planId', filters.planId);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.billingCycle) params.append('billingCycle', filters.billingCycle);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<SubscriptionsResponse>(`${BASE_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<TenantSubscription> => {
|
||||
const response = await api.get<TenantSubscription>(`${BASE_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getCurrent: async (): Promise<TenantSubscription> => {
|
||||
const response = await api.get<TenantSubscription>(`${BASE_URL}/current`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getByTenant: async (tenantId: string): Promise<TenantSubscription> => {
|
||||
const response = await api.get<TenantSubscription>(`${BASE_URL}/tenant/${tenantId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreateSubscriptionDto): Promise<TenantSubscription> => {
|
||||
const response = await api.post<TenantSubscription>(BASE_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateSubscriptionDto): Promise<TenantSubscription> => {
|
||||
const response = await api.patch<TenantSubscription>(`${BASE_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
cancel: async (id: string, data?: CancelSubscriptionDto): Promise<TenantSubscription> => {
|
||||
const response = await api.post<TenantSubscription>(`${BASE_URL}/${id}/cancel`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
reactivate: async (id: string): Promise<TenantSubscription> => {
|
||||
const response = await api.post<TenantSubscription>(`${BASE_URL}/${id}/reactivate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
changePlan: async (
|
||||
id: string,
|
||||
planId: string,
|
||||
billingCycle?: 'monthly' | 'annual'
|
||||
): Promise<TenantSubscription> => {
|
||||
const response = await api.post<TenantSubscription>(`${BASE_URL}/${id}/change-plan`, {
|
||||
planId,
|
||||
billingCycle,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
applyCoupon: async (id: string, couponCode: string): Promise<TenantSubscription> => {
|
||||
const response = await api.post<TenantSubscription>(`${BASE_URL}/${id}/apply-coupon`, {
|
||||
couponCode,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateBillingInfo: async (
|
||||
id: string,
|
||||
data: Pick<UpdateSubscriptionDto, 'billingEmail' | 'billingName' | 'billingAddress' | 'taxId'>
|
||||
): Promise<TenantSubscription> => {
|
||||
const response = await api.patch<TenantSubscription>(`${BASE_URL}/${id}/billing-info`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updatePaymentMethod: async (
|
||||
id: string,
|
||||
paymentMethodId: string,
|
||||
paymentProvider: string
|
||||
): Promise<TenantSubscription> => {
|
||||
const response = await api.patch<TenantSubscription>(`${BASE_URL}/${id}/payment-method`, {
|
||||
paymentMethodId,
|
||||
paymentProvider,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@ -1,86 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type { UsageTracking, UsageSummary, UsageFilters, UsageResponse } from '../types';
|
||||
|
||||
const BASE_URL = '/api/v1/billing/usage';
|
||||
|
||||
export const usageApi = {
|
||||
getAll: async (filters?: UsageFilters): Promise<UsageResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.tenantId) params.append('tenantId', filters.tenantId);
|
||||
if (filters?.startDate) params.append('startDate', filters.startDate);
|
||||
if (filters?.endDate) params.append('endDate', filters.endDate);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<UsageResponse>(`${BASE_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getCurrent: async (): Promise<UsageTracking | null> => {
|
||||
try {
|
||||
const response = await api.get<UsageTracking>(`${BASE_URL}/current`);
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getByPeriod: async (periodStart: string): Promise<UsageTracking | null> => {
|
||||
try {
|
||||
const response = await api.get<UsageTracking>(`${BASE_URL}/period/${periodStart}`);
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getSummary: async (): Promise<UsageSummary> => {
|
||||
const response = await api.get<UsageSummary>(`${BASE_URL}/summary`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getHistory: async (months: number = 12): Promise<UsageTracking[]> => {
|
||||
const response = await api.get<UsageTracking[]>(`${BASE_URL}/history`, {
|
||||
params: { months },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTrend: async (
|
||||
metric: 'users' | 'storage' | 'apiCalls' | 'sales',
|
||||
months: number = 6
|
||||
): Promise<{ period: string; value: number }[]> => {
|
||||
const response = await api.get<{ period: string; value: number }[]>(`${BASE_URL}/trend/${metric}`, {
|
||||
params: { months },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
checkLimit: async (
|
||||
limitKey: string
|
||||
): Promise<{ allowed: boolean; current: number; limit: number; percent: number }> => {
|
||||
const response = await api.get<{ allowed: boolean; current: number; limit: number; percent: number }>(
|
||||
`${BASE_URL}/check-limit/${limitKey}`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getOverages: async (): Promise<{
|
||||
users: number;
|
||||
branches: number;
|
||||
storage: number;
|
||||
apiCalls: number;
|
||||
totalAmount: number;
|
||||
}> => {
|
||||
const response = await api.get<{
|
||||
users: number;
|
||||
branches: number;
|
||||
storage: number;
|
||||
apiCalls: number;
|
||||
totalAmount: number;
|
||||
}>(`${BASE_URL}/overages`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@ -1,198 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { CouponValidation } from '../types';
|
||||
|
||||
interface CouponInputProps {
|
||||
onValidate: (code: string) => Promise<CouponValidation | null>;
|
||||
onApply?: (code: string) => Promise<void>;
|
||||
disabled?: boolean;
|
||||
planId?: string;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
export const CouponInput: React.FC<CouponInputProps> = ({
|
||||
onValidate,
|
||||
onApply,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [code, setCode] = useState('');
|
||||
const [validation, setValidation] = useState<CouponValidation | null>(null);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [applied, setApplied] = useState(false);
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!code.trim()) return;
|
||||
|
||||
setIsValidating(true);
|
||||
setValidation(null);
|
||||
|
||||
try {
|
||||
const result = await onValidate(code.trim().toUpperCase());
|
||||
setValidation(result);
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!onApply || !validation?.isValid) return;
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
await onApply(code.trim().toUpperCase());
|
||||
setApplied(true);
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setCode('');
|
||||
setValidation(null);
|
||||
setApplied(false);
|
||||
};
|
||||
|
||||
const formatDiscount = () => {
|
||||
if (!validation?.coupon) return '';
|
||||
const { discountType, discountValue } = validation.coupon;
|
||||
if (discountType === 'percentage') {
|
||||
return `${discountValue}% de descuento`;
|
||||
}
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: validation.coupon.currency || 'MXN',
|
||||
}).format(discountValue) + ' de descuento';
|
||||
};
|
||||
|
||||
if (applied && validation?.isValid) {
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-lg border border-green-200 bg-green-50 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className="h-5 w-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-medium text-green-800">
|
||||
Cupón {validation.coupon?.code} aplicado
|
||||
</p>
|
||||
<p className="text-sm text-green-600">{formatDiscount()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-green-600 hover:text-green-800"
|
||||
onClick={handleClear}
|
||||
>
|
||||
Cambiar
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||
placeholder="Código de cupón"
|
||||
disabled={disabled || isValidating || isApplying}
|
||||
className={`
|
||||
w-full rounded-lg border px-4 py-2 text-sm uppercase
|
||||
${validation?.isValid === false
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: validation?.isValid
|
||||
? 'border-green-300 focus:border-green-500 focus:ring-green-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}
|
||||
disabled:bg-gray-100 disabled:text-gray-500
|
||||
`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleValidate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{code && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{validation?.isValid && onApply ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || isApplying}
|
||||
className="rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 disabled:bg-green-300"
|
||||
onClick={handleApply}
|
||||
>
|
||||
{isApplying ? 'Aplicando...' : 'Aplicar'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || isValidating || !code.trim()}
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:bg-blue-300"
|
||||
onClick={handleValidate}
|
||||
>
|
||||
{isValidating ? 'Validando...' : 'Validar'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{validation && (
|
||||
<div
|
||||
className={`rounded-lg p-3 text-sm ${
|
||||
validation.isValid
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'bg-red-50 text-red-700'
|
||||
}`}
|
||||
>
|
||||
{validation.isValid ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-4 w-4 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
<strong>{validation.coupon?.name}</strong>: {formatDiscount()}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-4 w-4 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>{validation.message || 'Cupón no válido'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,161 +0,0 @@
|
||||
import React from 'react';
|
||||
import { InvoiceStatusBadge } from './InvoiceStatusBadge';
|
||||
import type { Invoice } from '../types';
|
||||
|
||||
interface InvoiceListProps {
|
||||
invoices: Invoice[];
|
||||
isLoading?: boolean;
|
||||
onDownload?: (invoice: Invoice) => void;
|
||||
onView?: (invoice: Invoice) => void;
|
||||
}
|
||||
|
||||
export const InvoiceList: React.FC<InvoiceListProps> = ({
|
||||
invoices,
|
||||
isLoading = false,
|
||||
onDownload,
|
||||
onView,
|
||||
}) => {
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('es-MX', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number, currency: string = 'MXN') => {
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (invoices.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg bg-gray-50 p-8 text-center">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 text-gray-500">No hay facturas</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
|
||||
Factura
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
|
||||
Fecha
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
|
||||
Período
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
|
||||
Estado
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">
|
||||
Total
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{invoices.map((invoice) => (
|
||||
<tr key={invoice.id} className="hover:bg-gray-50">
|
||||
<td className="whitespace-nowrap px-4 py-3">
|
||||
<span className="font-medium text-gray-900">{invoice.invoiceNumber}</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
|
||||
{formatDate(invoice.issueDate)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
|
||||
{invoice.periodStart && invoice.periodEnd ? (
|
||||
<>
|
||||
{formatDate(invoice.periodStart)} - {formatDate(invoice.periodEnd)}
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3">
|
||||
<InvoiceStatusBadge status={invoice.status} size="sm" />
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3 text-right text-sm font-medium text-gray-900">
|
||||
{formatCurrency(invoice.total, invoice.currency)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{onView && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
onClick={() => onView(invoice)}
|
||||
title="Ver detalles"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{onDownload && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-600 hover:text-gray-800"
|
||||
onClick={() => onDownload(invoice)}
|
||||
title="Descargar PDF"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { InvoiceStatus } from '../types';
|
||||
|
||||
interface InvoiceStatusBadgeProps {
|
||||
status: InvoiceStatus;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const statusConfig: Record<InvoiceStatus, { label: string; className: string }> = {
|
||||
draft: {
|
||||
label: 'Borrador',
|
||||
className: 'bg-gray-100 text-gray-700',
|
||||
},
|
||||
issued: {
|
||||
label: 'Emitida',
|
||||
className: 'bg-blue-100 text-blue-700',
|
||||
},
|
||||
paid: {
|
||||
label: 'Pagada',
|
||||
className: 'bg-green-100 text-green-700',
|
||||
},
|
||||
cancelled: {
|
||||
label: 'Cancelada',
|
||||
className: 'bg-red-100 text-red-700',
|
||||
},
|
||||
overdue: {
|
||||
label: 'Vencida',
|
||||
className: 'bg-orange-100 text-orange-700',
|
||||
},
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-0.5 text-xs',
|
||||
md: 'px-2.5 py-1 text-sm',
|
||||
lg: 'px-3 py-1.5 text-base',
|
||||
};
|
||||
|
||||
export const InvoiceStatusBadge: React.FC<InvoiceStatusBadgeProps> = ({
|
||||
status,
|
||||
size = 'md',
|
||||
}) => {
|
||||
const config = statusConfig[status];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center rounded-full font-medium
|
||||
${config.className}
|
||||
${sizeClasses[size]}
|
||||
`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@ -1,167 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { SubscriptionPlan } from '../types';
|
||||
|
||||
interface PlanCardProps {
|
||||
plan: SubscriptionPlan;
|
||||
isSelected?: boolean;
|
||||
isCurrent?: boolean;
|
||||
billingCycle?: 'monthly' | 'annual';
|
||||
onSelect?: (plan: SubscriptionPlan) => void;
|
||||
showFeatures?: boolean;
|
||||
}
|
||||
|
||||
export const PlanCard: React.FC<PlanCardProps> = ({
|
||||
plan,
|
||||
isSelected = false,
|
||||
isCurrent = false,
|
||||
billingCycle = 'monthly',
|
||||
onSelect,
|
||||
showFeatures = true,
|
||||
}) => {
|
||||
const price = billingCycle === 'annual' && plan.baseAnnualPrice
|
||||
? plan.baseAnnualPrice / 12
|
||||
: plan.baseMonthlyPrice;
|
||||
|
||||
const annualSavings = plan.baseAnnualPrice
|
||||
? ((plan.baseMonthlyPrice * 12 - plan.baseAnnualPrice) / (plan.baseMonthlyPrice * 12)) * 100
|
||||
: 0;
|
||||
|
||||
const formatPrice = (amount: number) => {
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: 'MXN',
|
||||
minimumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const enabledFeatures = Object.entries(plan.features)
|
||||
.filter(([, enabled]) => enabled)
|
||||
.map(([name]) => name);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative rounded-lg border-2 p-6 transition-all
|
||||
${isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white'}
|
||||
${isCurrent ? 'ring-2 ring-green-500' : ''}
|
||||
${onSelect ? 'cursor-pointer hover:border-blue-300' : ''}
|
||||
`}
|
||||
onClick={() => onSelect?.(plan)}
|
||||
>
|
||||
{isCurrent && (
|
||||
<span className="absolute -top-3 left-4 rounded-full bg-green-500 px-3 py-1 text-xs font-medium text-white">
|
||||
Plan actual
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-bold text-gray-900">{plan.name}</h3>
|
||||
{plan.description && (
|
||||
<p className="mt-1 text-sm text-gray-500">{plan.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-4xl font-bold text-gray-900">{formatPrice(price)}</span>
|
||||
<span className="ml-1 text-gray-500">/mes</span>
|
||||
</div>
|
||||
{billingCycle === 'annual' && annualSavings > 0 && (
|
||||
<p className="mt-1 text-sm text-green-600">
|
||||
Ahorra {annualSavings.toFixed(0)}% con facturación anual
|
||||
</p>
|
||||
)}
|
||||
{plan.setupFee > 0 && (
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
+ {formatPrice(plan.setupFee)} setup único
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6 space-y-3 border-t border-gray-200 pt-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Usuarios</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{plan.maxUsers === -1 ? 'Ilimitados' : `Hasta ${plan.maxUsers}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Sucursales</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{plan.maxBranches === -1 ? 'Ilimitadas' : `Hasta ${plan.maxBranches}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Almacenamiento</span>
|
||||
<span className="font-medium text-gray-900">{plan.storageGb} GB</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">API calls/mes</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{plan.apiCallsMonthly === -1 ? 'Ilimitadas' : plan.apiCallsMonthly.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showFeatures && enabledFeatures.length > 0 && (
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<p className="mb-2 text-sm font-medium text-gray-900">Incluye:</p>
|
||||
<ul className="space-y-2">
|
||||
{enabledFeatures.slice(0, 5).map((feature) => (
|
||||
<li key={feature} className="flex items-center text-sm text-gray-600">
|
||||
<svg className="mr-2 h-4 w-4 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{feature.replace(/_/g, ' ')}
|
||||
</li>
|
||||
))}
|
||||
{enabledFeatures.length > 5 && (
|
||||
<li className="text-sm text-gray-500">
|
||||
+{enabledFeatures.length - 5} más
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plan.includedModules.length > 0 && (
|
||||
<div className="mt-4 border-t border-gray-200 pt-4">
|
||||
<p className="mb-2 text-sm font-medium text-gray-900">Módulos:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plan.includedModules.map((module) => (
|
||||
<span
|
||||
key={module}
|
||||
className="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600"
|
||||
>
|
||||
{module}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onSelect && (
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
mt-6 w-full rounded-lg py-2 text-sm font-medium transition-colors
|
||||
${isSelected
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect(plan);
|
||||
}}
|
||||
>
|
||||
{isSelected ? 'Seleccionado' : 'Seleccionar'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,90 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PlanCard } from './PlanCard';
|
||||
import type { SubscriptionPlan, BillingCycle } from '../types';
|
||||
|
||||
interface PlanSelectorProps {
|
||||
plans: SubscriptionPlan[];
|
||||
selectedPlanId?: string;
|
||||
currentPlanId?: string;
|
||||
onSelect: (plan: SubscriptionPlan, billingCycle: BillingCycle) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const PlanSelector: React.FC<PlanSelectorProps> = ({
|
||||
plans,
|
||||
selectedPlanId,
|
||||
currentPlanId,
|
||||
onSelect,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const [billingCycle, setBillingCycle] = useState<BillingCycle>('monthly');
|
||||
|
||||
const activePlans = plans.filter((plan) => plan.isActive && plan.isPublic);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activePlans.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg bg-gray-50 p-8 text-center">
|
||||
<p className="text-gray-500">No hay planes disponibles</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8 flex justify-center">
|
||||
<div className="inline-flex rounded-lg bg-gray-100 p-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
rounded-md px-4 py-2 text-sm font-medium transition-colors
|
||||
${billingCycle === 'monthly'
|
||||
? 'bg-white text-gray-900 shadow'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}
|
||||
`}
|
||||
onClick={() => setBillingCycle('monthly')}
|
||||
>
|
||||
Mensual
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
rounded-md px-4 py-2 text-sm font-medium transition-colors
|
||||
${billingCycle === 'annual'
|
||||
? 'bg-white text-gray-900 shadow'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}
|
||||
`}
|
||||
onClick={() => setBillingCycle('annual')}
|
||||
>
|
||||
Anual
|
||||
<span className="ml-1 rounded bg-green-100 px-1.5 py-0.5 text-xs text-green-700">
|
||||
Ahorra
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{activePlans.map((plan) => (
|
||||
<PlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
isSelected={plan.id === selectedPlanId}
|
||||
isCurrent={plan.id === currentPlanId}
|
||||
billingCycle={billingCycle}
|
||||
onSelect={(p) => onSelect(p, billingCycle)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { SubscriptionStatus } from '../types';
|
||||
|
||||
interface SubscriptionStatusBadgeProps {
|
||||
status: SubscriptionStatus;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const statusConfig: Record<SubscriptionStatus, { label: string; className: string }> = {
|
||||
trial: {
|
||||
label: 'Prueba',
|
||||
className: 'bg-purple-100 text-purple-700',
|
||||
},
|
||||
active: {
|
||||
label: 'Activa',
|
||||
className: 'bg-green-100 text-green-700',
|
||||
},
|
||||
past_due: {
|
||||
label: 'Pago pendiente',
|
||||
className: 'bg-yellow-100 text-yellow-700',
|
||||
},
|
||||
cancelled: {
|
||||
label: 'Cancelada',
|
||||
className: 'bg-red-100 text-red-700',
|
||||
},
|
||||
suspended: {
|
||||
label: 'Suspendida',
|
||||
className: 'bg-gray-100 text-gray-700',
|
||||
},
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-0.5 text-xs',
|
||||
md: 'px-2.5 py-1 text-sm',
|
||||
lg: 'px-3 py-1.5 text-base',
|
||||
};
|
||||
|
||||
export const SubscriptionStatusBadge: React.FC<SubscriptionStatusBadgeProps> = ({
|
||||
status,
|
||||
size = 'md',
|
||||
}) => {
|
||||
const config = statusConfig[status];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center rounded-full font-medium
|
||||
${config.className}
|
||||
${sizeClasses[size]}
|
||||
`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface UsageProgressBarProps {
|
||||
label: string;
|
||||
current: number;
|
||||
limit: number;
|
||||
unit?: string;
|
||||
showOverage?: boolean;
|
||||
}
|
||||
|
||||
export const UsageProgressBar: React.FC<UsageProgressBarProps> = ({
|
||||
label,
|
||||
current,
|
||||
limit,
|
||||
unit = '',
|
||||
showOverage = true,
|
||||
}) => {
|
||||
const isUnlimited = limit === -1;
|
||||
const percent = isUnlimited ? 0 : Math.min((current / limit) * 100, 100);
|
||||
const overage = !isUnlimited && current > limit ? current - limit : 0;
|
||||
|
||||
const getColorClass = () => {
|
||||
if (isUnlimited) return 'bg-gray-400';
|
||||
if (percent >= 100) return 'bg-red-500';
|
||||
if (percent >= 80) return 'bg-yellow-500';
|
||||
return 'bg-blue-500';
|
||||
};
|
||||
|
||||
const formatValue = (value: number) => {
|
||||
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
|
||||
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
|
||||
return value.toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium text-gray-700">{label}</span>
|
||||
<span className="text-gray-500">
|
||||
{formatValue(current)}
|
||||
{unit && ` ${unit}`}
|
||||
{!isUnlimited && (
|
||||
<>
|
||||
{' / '}
|
||||
{formatValue(limit)}
|
||||
{unit && ` ${unit}`}
|
||||
</>
|
||||
)}
|
||||
{isUnlimited && ' (ilimitado)'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-2 overflow-hidden rounded-full bg-gray-200">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${getColorClass()}`}
|
||||
style={{ width: isUnlimited ? '0%' : `${Math.min(percent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-400">
|
||||
{isUnlimited ? 'Sin límite' : `${percent.toFixed(1)}% usado`}
|
||||
</span>
|
||||
{showOverage && overage > 0 && (
|
||||
<span className="font-medium text-red-600">
|
||||
+{formatValue(overage)} excedente
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,102 +0,0 @@
|
||||
import React from 'react';
|
||||
import { UsageProgressBar } from './UsageProgressBar';
|
||||
import type { UsageSummary } from '../types';
|
||||
|
||||
interface UsageSummaryCardProps {
|
||||
summary: UsageSummary | null;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const UsageSummaryCard: React.FC<UsageSummaryCardProps> = ({
|
||||
summary,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<p className="text-center text-gray-500">No hay datos de uso disponibles</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasOverages =
|
||||
summary.overages.users > 0 ||
|
||||
summary.overages.branches > 0 ||
|
||||
summary.overages.storage > 0 ||
|
||||
summary.overages.apiCalls > 0;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white">
|
||||
<div className="border-b border-gray-200 px-6 py-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Uso del Plan</h3>
|
||||
{summary.currentPeriod && (
|
||||
<p className="text-sm text-gray-500">
|
||||
Período: {new Date(summary.currentPeriod.periodStart).toLocaleDateString('es-MX')} -{' '}
|
||||
{new Date(summary.currentPeriod.periodEnd).toLocaleDateString('es-MX')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 p-6">
|
||||
<UsageProgressBar
|
||||
label="Usuarios activos"
|
||||
current={summary.currentPeriod?.activeUsers || 0}
|
||||
limit={summary.limits.maxUsers}
|
||||
/>
|
||||
|
||||
<UsageProgressBar
|
||||
label="Sucursales"
|
||||
current={summary.currentPeriod?.activeBranches || 0}
|
||||
limit={summary.limits.maxBranches}
|
||||
/>
|
||||
|
||||
<UsageProgressBar
|
||||
label="Almacenamiento"
|
||||
current={summary.currentPeriod?.storageUsedGb || 0}
|
||||
limit={summary.limits.storageGb}
|
||||
unit="GB"
|
||||
/>
|
||||
|
||||
<UsageProgressBar
|
||||
label="API calls"
|
||||
current={summary.currentPeriod?.apiCalls || 0}
|
||||
limit={summary.limits.apiCallsMonthly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasOverages && (
|
||||
<div className="border-t border-gray-200 bg-red-50 px-6 py-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg
|
||||
className="mt-0.5 h-5 w-5 flex-shrink-0 text-red-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-medium text-red-800">Has excedido los límites de tu plan</p>
|
||||
<p className="mt-1 text-sm text-red-700">
|
||||
Los excedentes se facturarán en tu próximo período de facturación.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,8 +0,0 @@
|
||||
export { PlanCard } from './PlanCard';
|
||||
export { PlanSelector } from './PlanSelector';
|
||||
export { SubscriptionStatusBadge } from './SubscriptionStatusBadge';
|
||||
export { InvoiceStatusBadge } from './InvoiceStatusBadge';
|
||||
export { InvoiceList } from './InvoiceList';
|
||||
export { UsageProgressBar } from './UsageProgressBar';
|
||||
export { UsageSummaryCard } from './UsageSummaryCard';
|
||||
export { CouponInput } from './CouponInput';
|
||||
@ -1,12 +0,0 @@
|
||||
export { usePlans, usePlan, usePublicPlans } from './usePlans';
|
||||
export { useSubscription, useSubscriptionById } from './useSubscription';
|
||||
export { useInvoices, useInvoice, useUpcomingInvoice } from './useInvoices';
|
||||
export {
|
||||
useUsageHistory,
|
||||
useCurrentUsage,
|
||||
useUsageSummary,
|
||||
useUsageTrend,
|
||||
useLimitCheck,
|
||||
useOverages,
|
||||
} from './useUsage';
|
||||
export { useCoupons, useCoupon, useCouponValidation, useCouponStats } from './useCoupons';
|
||||
@ -1,192 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { couponsApi } from '../api';
|
||||
import type {
|
||||
Coupon,
|
||||
CouponFilters,
|
||||
CreateCouponDto,
|
||||
UpdateCouponDto,
|
||||
CouponValidation,
|
||||
CouponStats,
|
||||
} from '../types';
|
||||
|
||||
interface UseCouponsState {
|
||||
coupons: Coupon[];
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseCouponsReturn extends UseCouponsState {
|
||||
filters: CouponFilters;
|
||||
setFilters: (filters: CouponFilters) => void;
|
||||
refresh: () => Promise<void>;
|
||||
createCoupon: (data: CreateCouponDto) => Promise<Coupon>;
|
||||
updateCoupon: (id: string, data: UpdateCouponDto) => Promise<Coupon>;
|
||||
deleteCoupon: (id: string) => Promise<void>;
|
||||
activateCoupon: (id: string) => Promise<void>;
|
||||
deactivateCoupon: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useCoupons(initialFilters?: CouponFilters): UseCouponsReturn {
|
||||
const [state, setState] = useState<UseCouponsState>({
|
||||
coupons: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<CouponFilters>(initialFilters || { page: 1, limit: 10 });
|
||||
|
||||
const fetchCoupons = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
try {
|
||||
const response = await couponsApi.getAll(filters);
|
||||
setState({
|
||||
coupons: response.data,
|
||||
total: response.meta.total,
|
||||
page: response.meta.page,
|
||||
totalPages: response.meta.totalPages,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : 'Error al cargar cupones',
|
||||
}));
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCoupons();
|
||||
}, [fetchCoupons]);
|
||||
|
||||
const createCoupon = async (data: CreateCouponDto): Promise<Coupon> => {
|
||||
const coupon = await couponsApi.create(data);
|
||||
await fetchCoupons();
|
||||
return coupon;
|
||||
};
|
||||
|
||||
const updateCoupon = async (id: string, data: UpdateCouponDto): Promise<Coupon> => {
|
||||
const coupon = await couponsApi.update(id, data);
|
||||
await fetchCoupons();
|
||||
return coupon;
|
||||
};
|
||||
|
||||
const deleteCoupon = async (id: string): Promise<void> => {
|
||||
await couponsApi.delete(id);
|
||||
await fetchCoupons();
|
||||
};
|
||||
|
||||
const activateCoupon = async (id: string): Promise<void> => {
|
||||
await couponsApi.activate(id);
|
||||
await fetchCoupons();
|
||||
};
|
||||
|
||||
const deactivateCoupon = async (id: string): Promise<void> => {
|
||||
await couponsApi.deactivate(id);
|
||||
await fetchCoupons();
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
filters,
|
||||
setFilters,
|
||||
refresh: fetchCoupons,
|
||||
createCoupon,
|
||||
updateCoupon,
|
||||
deleteCoupon,
|
||||
activateCoupon,
|
||||
deactivateCoupon,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCoupon(id: string | undefined) {
|
||||
const [coupon, setCoupon] = useState<Coupon | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchCoupon = useCallback(async () => {
|
||||
if (!id) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await couponsApi.getById(id);
|
||||
setCoupon(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar cupón');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCoupon();
|
||||
}, [fetchCoupon]);
|
||||
|
||||
return { coupon, isLoading, error, refresh: fetchCoupon };
|
||||
}
|
||||
|
||||
export function useCouponValidation() {
|
||||
const [validation, setValidation] = useState<CouponValidation | null>(null);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validateCoupon = useCallback(async (code: string, planId?: string, amount?: number) => {
|
||||
setIsValidating(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await couponsApi.validate(code, planId, amount);
|
||||
setValidation(result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Error al validar cupón';
|
||||
setError(message);
|
||||
setValidation({ isValid: false, message, errors: [message] });
|
||||
return null;
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearValidation = useCallback(() => {
|
||||
setValidation(null);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return { validation, isValidating, error, validateCoupon, clearValidation };
|
||||
}
|
||||
|
||||
export function useCouponStats() {
|
||||
const [stats, setStats] = useState<CouponStats | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await couponsApi.getStats();
|
||||
setStats(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar estadísticas');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [fetchStats]);
|
||||
|
||||
return { stats, isLoading, error, refresh: fetchStats };
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invoicesApi } from '../api';
|
||||
import type { Invoice, InvoiceFilters } from '../types';
|
||||
|
||||
interface UseInvoicesState {
|
||||
invoices: Invoice[];
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseInvoicesReturn extends UseInvoicesState {
|
||||
filters: InvoiceFilters;
|
||||
setFilters: (filters: InvoiceFilters) => void;
|
||||
refresh: () => Promise<void>;
|
||||
downloadInvoice: (id: string) => Promise<void>;
|
||||
sendInvoiceByEmail: (id: string, email?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useInvoices(initialFilters?: InvoiceFilters): UseInvoicesReturn {
|
||||
const [state, setState] = useState<UseInvoicesState>({
|
||||
invoices: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<InvoiceFilters>(
|
||||
initialFilters || { page: 1, limit: 10, invoiceContext: 'saas' }
|
||||
);
|
||||
|
||||
const fetchInvoices = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
try {
|
||||
const response = await invoicesApi.getAll(filters);
|
||||
setState({
|
||||
invoices: response.data,
|
||||
total: response.meta.total,
|
||||
page: response.meta.page,
|
||||
totalPages: response.meta.totalPages,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : 'Error al cargar facturas',
|
||||
}));
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInvoices();
|
||||
}, [fetchInvoices]);
|
||||
|
||||
const downloadInvoice = async (id: string): Promise<void> => {
|
||||
const blob = await invoicesApi.download(id);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `invoice-${id}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
const sendInvoiceByEmail = async (id: string, email?: string): Promise<void> => {
|
||||
await invoicesApi.sendByEmail(id, email);
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
filters,
|
||||
setFilters,
|
||||
refresh: fetchInvoices,
|
||||
downloadInvoice,
|
||||
sendInvoiceByEmail,
|
||||
};
|
||||
}
|
||||
|
||||
export function useInvoice(id: string | undefined) {
|
||||
const [invoice, setInvoice] = useState<Invoice | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchInvoice = useCallback(async () => {
|
||||
if (!id) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await invoicesApi.getById(id);
|
||||
setInvoice(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar factura');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInvoice();
|
||||
}, [fetchInvoice]);
|
||||
|
||||
return { invoice, isLoading, error, refresh: fetchInvoice };
|
||||
}
|
||||
|
||||
export function useUpcomingInvoice() {
|
||||
const [invoice, setInvoice] = useState<Invoice | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchInvoice = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await invoicesApi.getUpcoming();
|
||||
setInvoice(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar próxima factura');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInvoice();
|
||||
}, [fetchInvoice]);
|
||||
|
||||
return { invoice, isLoading, error, refresh: fetchInvoice };
|
||||
}
|
||||
@ -1,155 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { plansApi } from '../api';
|
||||
import type { SubscriptionPlan, PlanFilters, CreatePlanDto, UpdatePlanDto } from '../types';
|
||||
|
||||
interface UsePlansState {
|
||||
plans: SubscriptionPlan[];
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UsePlansReturn extends UsePlansState {
|
||||
filters: PlanFilters;
|
||||
setFilters: (filters: PlanFilters) => void;
|
||||
refresh: () => Promise<void>;
|
||||
createPlan: (data: CreatePlanDto) => Promise<SubscriptionPlan>;
|
||||
updatePlan: (id: string, data: UpdatePlanDto) => Promise<SubscriptionPlan>;
|
||||
deletePlan: (id: string) => Promise<void>;
|
||||
activatePlan: (id: string) => Promise<void>;
|
||||
deactivatePlan: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function usePlans(initialFilters?: PlanFilters): UsePlansReturn {
|
||||
const [state, setState] = useState<UsePlansState>({
|
||||
plans: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<PlanFilters>(initialFilters || { page: 1, limit: 10 });
|
||||
|
||||
const fetchPlans = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
try {
|
||||
const response = await plansApi.getAll(filters);
|
||||
setState({
|
||||
plans: response.data,
|
||||
total: response.meta.total,
|
||||
page: response.meta.page,
|
||||
totalPages: response.meta.totalPages,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : 'Error al cargar planes',
|
||||
}));
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlans();
|
||||
}, [fetchPlans]);
|
||||
|
||||
const createPlan = async (data: CreatePlanDto): Promise<SubscriptionPlan> => {
|
||||
const plan = await plansApi.create(data);
|
||||
await fetchPlans();
|
||||
return plan;
|
||||
};
|
||||
|
||||
const updatePlan = async (id: string, data: UpdatePlanDto): Promise<SubscriptionPlan> => {
|
||||
const plan = await plansApi.update(id, data);
|
||||
await fetchPlans();
|
||||
return plan;
|
||||
};
|
||||
|
||||
const deletePlan = async (id: string): Promise<void> => {
|
||||
await plansApi.delete(id);
|
||||
await fetchPlans();
|
||||
};
|
||||
|
||||
const activatePlan = async (id: string): Promise<void> => {
|
||||
await plansApi.activate(id);
|
||||
await fetchPlans();
|
||||
};
|
||||
|
||||
const deactivatePlan = async (id: string): Promise<void> => {
|
||||
await plansApi.deactivate(id);
|
||||
await fetchPlans();
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
filters,
|
||||
setFilters,
|
||||
refresh: fetchPlans,
|
||||
createPlan,
|
||||
updatePlan,
|
||||
deletePlan,
|
||||
activatePlan,
|
||||
deactivatePlan,
|
||||
};
|
||||
}
|
||||
|
||||
export function usePlan(id: string | undefined) {
|
||||
const [plan, setPlan] = useState<SubscriptionPlan | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchPlan = useCallback(async () => {
|
||||
if (!id) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await plansApi.getById(id);
|
||||
setPlan(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar plan');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlan();
|
||||
}, [fetchPlan]);
|
||||
|
||||
return { plan, isLoading, error, refresh: fetchPlan };
|
||||
}
|
||||
|
||||
export function usePublicPlans() {
|
||||
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchPlans = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await plansApi.getPublic();
|
||||
setPlans(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar planes');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlans();
|
||||
}, [fetchPlans]);
|
||||
|
||||
return { plans, isLoading, error, refresh: fetchPlans };
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { subscriptionsApi } from '../api';
|
||||
import type {
|
||||
TenantSubscription,
|
||||
UpdateSubscriptionDto,
|
||||
CancelSubscriptionDto,
|
||||
BillingCycle,
|
||||
} from '../types';
|
||||
|
||||
interface UseSubscriptionState {
|
||||
subscription: TenantSubscription | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseSubscriptionReturn extends UseSubscriptionState {
|
||||
refresh: () => Promise<void>;
|
||||
updateSubscription: (data: UpdateSubscriptionDto) => Promise<TenantSubscription>;
|
||||
cancelSubscription: (data?: CancelSubscriptionDto) => Promise<void>;
|
||||
reactivateSubscription: () => Promise<void>;
|
||||
changePlan: (planId: string, billingCycle?: BillingCycle) => Promise<void>;
|
||||
applyCoupon: (couponCode: string) => Promise<void>;
|
||||
updateBillingInfo: (
|
||||
data: Pick<UpdateSubscriptionDto, 'billingEmail' | 'billingName' | 'billingAddress' | 'taxId'>
|
||||
) => Promise<void>;
|
||||
updatePaymentMethod: (paymentMethodId: string, paymentProvider: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useSubscription(): UseSubscriptionReturn {
|
||||
const [state, setState] = useState<UseSubscriptionState>({
|
||||
subscription: null,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const fetchSubscription = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
try {
|
||||
const data = await subscriptionsApi.getCurrent();
|
||||
setState({
|
||||
subscription: data,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
setState({
|
||||
subscription: null,
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : 'Error al cargar suscripción',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSubscription();
|
||||
}, [fetchSubscription]);
|
||||
|
||||
const updateSubscription = async (data: UpdateSubscriptionDto): Promise<TenantSubscription> => {
|
||||
if (!state.subscription) throw new Error('No hay suscripción activa');
|
||||
const updated = await subscriptionsApi.update(state.subscription.id, data);
|
||||
setState((prev) => ({ ...prev, subscription: updated }));
|
||||
return updated;
|
||||
};
|
||||
|
||||
const cancelSubscription = async (data?: CancelSubscriptionDto): Promise<void> => {
|
||||
if (!state.subscription) throw new Error('No hay suscripción activa');
|
||||
const updated = await subscriptionsApi.cancel(state.subscription.id, data);
|
||||
setState((prev) => ({ ...prev, subscription: updated }));
|
||||
};
|
||||
|
||||
const reactivateSubscription = async (): Promise<void> => {
|
||||
if (!state.subscription) throw new Error('No hay suscripción activa');
|
||||
const updated = await subscriptionsApi.reactivate(state.subscription.id);
|
||||
setState((prev) => ({ ...prev, subscription: updated }));
|
||||
};
|
||||
|
||||
const changePlan = async (planId: string, billingCycle?: BillingCycle): Promise<void> => {
|
||||
if (!state.subscription) throw new Error('No hay suscripción activa');
|
||||
const updated = await subscriptionsApi.changePlan(state.subscription.id, planId, billingCycle);
|
||||
setState((prev) => ({ ...prev, subscription: updated }));
|
||||
};
|
||||
|
||||
const applyCoupon = async (couponCode: string): Promise<void> => {
|
||||
if (!state.subscription) throw new Error('No hay suscripción activa');
|
||||
const updated = await subscriptionsApi.applyCoupon(state.subscription.id, couponCode);
|
||||
setState((prev) => ({ ...prev, subscription: updated }));
|
||||
};
|
||||
|
||||
const updateBillingInfo = async (
|
||||
data: Pick<UpdateSubscriptionDto, 'billingEmail' | 'billingName' | 'billingAddress' | 'taxId'>
|
||||
): Promise<void> => {
|
||||
if (!state.subscription) throw new Error('No hay suscripción activa');
|
||||
const updated = await subscriptionsApi.updateBillingInfo(state.subscription.id, data);
|
||||
setState((prev) => ({ ...prev, subscription: updated }));
|
||||
};
|
||||
|
||||
const updatePaymentMethod = async (
|
||||
paymentMethodId: string,
|
||||
paymentProvider: string
|
||||
): Promise<void> => {
|
||||
if (!state.subscription) throw new Error('No hay suscripción activa');
|
||||
const updated = await subscriptionsApi.updatePaymentMethod(
|
||||
state.subscription.id,
|
||||
paymentMethodId,
|
||||
paymentProvider
|
||||
);
|
||||
setState((prev) => ({ ...prev, subscription: updated }));
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
refresh: fetchSubscription,
|
||||
updateSubscription,
|
||||
cancelSubscription,
|
||||
reactivateSubscription,
|
||||
changePlan,
|
||||
applyCoupon,
|
||||
updateBillingInfo,
|
||||
updatePaymentMethod,
|
||||
};
|
||||
}
|
||||
|
||||
export function useSubscriptionById(id: string | undefined) {
|
||||
const [subscription, setSubscription] = useState<TenantSubscription | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchSubscription = useCallback(async () => {
|
||||
if (!id) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await subscriptionsApi.getById(id);
|
||||
setSubscription(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar suscripción');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSubscription();
|
||||
}, [fetchSubscription]);
|
||||
|
||||
return { subscription, isLoading, error, refresh: fetchSubscription };
|
||||
}
|
||||
@ -1,199 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { usageApi } from '../api';
|
||||
import type { UsageTracking, UsageSummary, UsageFilters } from '../types';
|
||||
|
||||
interface UseUsageState {
|
||||
usage: UsageTracking[];
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseUsageReturn extends UseUsageState {
|
||||
filters: UsageFilters;
|
||||
setFilters: (filters: UsageFilters) => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useUsageHistory(initialFilters?: UsageFilters): UseUsageReturn {
|
||||
const [state, setState] = useState<UseUsageState>({
|
||||
usage: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<UsageFilters>(initialFilters || { page: 1, limit: 12 });
|
||||
|
||||
const fetchUsage = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
try {
|
||||
const response = await usageApi.getAll(filters);
|
||||
setState({
|
||||
usage: response.data,
|
||||
total: response.meta.total,
|
||||
page: response.meta.page,
|
||||
totalPages: response.meta.totalPages,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : 'Error al cargar historial de uso',
|
||||
}));
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsage();
|
||||
}, [fetchUsage]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
filters,
|
||||
setFilters,
|
||||
refresh: fetchUsage,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCurrentUsage() {
|
||||
const [usage, setUsage] = useState<UsageTracking | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchUsage = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await usageApi.getCurrent();
|
||||
setUsage(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar uso actual');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsage();
|
||||
}, [fetchUsage]);
|
||||
|
||||
return { usage, isLoading, error, refresh: fetchUsage };
|
||||
}
|
||||
|
||||
export function useUsageSummary() {
|
||||
const [summary, setSummary] = useState<UsageSummary | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchSummary = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await usageApi.getSummary();
|
||||
setSummary(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar resumen de uso');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSummary();
|
||||
}, [fetchSummary]);
|
||||
|
||||
return { summary, isLoading, error, refresh: fetchSummary };
|
||||
}
|
||||
|
||||
export function useUsageTrend(metric: 'users' | 'storage' | 'apiCalls' | 'sales', months: number = 6) {
|
||||
const [trend, setTrend] = useState<{ period: string; value: number }[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchTrend = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await usageApi.getTrend(metric, months);
|
||||
setTrend(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar tendencia');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [metric, months]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTrend();
|
||||
}, [fetchTrend]);
|
||||
|
||||
return { trend, isLoading, error, refresh: fetchTrend };
|
||||
}
|
||||
|
||||
export function useLimitCheck(limitKey: string) {
|
||||
const [limitStatus, setLimitStatus] = useState<{
|
||||
allowed: boolean;
|
||||
current: number;
|
||||
limit: number;
|
||||
percent: number;
|
||||
} | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const checkLimit = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await usageApi.checkLimit(limitKey);
|
||||
setLimitStatus(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al verificar límite');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [limitKey]);
|
||||
|
||||
useEffect(() => {
|
||||
checkLimit();
|
||||
}, [checkLimit]);
|
||||
|
||||
return { limitStatus, isLoading, error, refresh: checkLimit };
|
||||
}
|
||||
|
||||
export function useOverages() {
|
||||
const [overages, setOverages] = useState<{
|
||||
users: number;
|
||||
branches: number;
|
||||
storage: number;
|
||||
apiCalls: number;
|
||||
totalAmount: number;
|
||||
} | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchOverages = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await usageApi.getOverages();
|
||||
setOverages(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar excedentes');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOverages();
|
||||
}, [fetchOverages]);
|
||||
|
||||
return { overages, isLoading, error, refresh: fetchOverages };
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
export type DiscountType = 'percentage' | 'fixed';
|
||||
export type DurationPeriod = 'once' | 'forever' | 'months';
|
||||
|
||||
export interface Coupon {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
discountType: DiscountType;
|
||||
discountValue: number;
|
||||
currency: string;
|
||||
applicablePlans: string[];
|
||||
minAmount: number;
|
||||
durationPeriod: DurationPeriod;
|
||||
durationMonths?: number | null;
|
||||
maxRedemptions?: number | null;
|
||||
currentRedemptions: number;
|
||||
validFrom?: string | null;
|
||||
validUntil?: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CouponRedemption {
|
||||
id: string;
|
||||
couponId: string;
|
||||
subscriptionId: string;
|
||||
tenantId: string;
|
||||
discountApplied: number;
|
||||
appliedAt: string;
|
||||
expiresAt?: string | null;
|
||||
createdAt: string;
|
||||
coupon?: Coupon;
|
||||
}
|
||||
|
||||
export interface CreateCouponDto {
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
discountType: DiscountType;
|
||||
discountValue: number;
|
||||
currency?: string;
|
||||
applicablePlans?: string[];
|
||||
minAmount?: number;
|
||||
durationPeriod?: DurationPeriod;
|
||||
durationMonths?: number;
|
||||
maxRedemptions?: number;
|
||||
validFrom?: string;
|
||||
validUntil?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateCouponDto {
|
||||
name?: string;
|
||||
description?: string;
|
||||
applicablePlans?: string[];
|
||||
minAmount?: number;
|
||||
maxRedemptions?: number;
|
||||
validFrom?: string;
|
||||
validUntil?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface CouponValidation {
|
||||
isValid: boolean;
|
||||
coupon?: Coupon;
|
||||
discountAmount?: number;
|
||||
message?: string;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface CouponFilters {
|
||||
search?: string;
|
||||
discountType?: DiscountType;
|
||||
isActive?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface CouponsResponse {
|
||||
data: Coupon[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CouponStats {
|
||||
totalCoupons: number;
|
||||
activeCoupons: number;
|
||||
totalRedemptions: number;
|
||||
totalDiscountGiven: number;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export * from './plan.types';
|
||||
export * from './subscription.types';
|
||||
export * from './invoice.types';
|
||||
export * from './usage.types';
|
||||
export * from './coupon.types';
|
||||
@ -1,113 +0,0 @@
|
||||
export type InvoiceStatus = 'draft' | 'issued' | 'paid' | 'cancelled' | 'overdue';
|
||||
export type InvoiceType = 'standard' | 'credit_note' | 'debit_note';
|
||||
export type InvoiceContext = 'commercial' | 'saas';
|
||||
|
||||
export interface InvoiceItem {
|
||||
id: string;
|
||||
invoiceId: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
discount: number;
|
||||
taxRate: number;
|
||||
taxAmount: number;
|
||||
subtotal: number;
|
||||
total: number;
|
||||
productId?: string | null;
|
||||
productCode?: string | null;
|
||||
productName?: string | null;
|
||||
unitOfMeasure?: string | null;
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
invoiceNumber: string;
|
||||
invoiceType: InvoiceType;
|
||||
invoiceContext: InvoiceContext;
|
||||
status: InvoiceStatus;
|
||||
partnerId?: string | null;
|
||||
partnerName?: string | null;
|
||||
subscriptionId?: string | null;
|
||||
periodStart?: string | null;
|
||||
periodEnd?: string | null;
|
||||
issueDate: string;
|
||||
dueDate: string;
|
||||
paymentDate?: string | null;
|
||||
currency: string;
|
||||
exchangeRate: number;
|
||||
subtotal: number;
|
||||
discountTotal: number;
|
||||
taxTotal: number;
|
||||
total: number;
|
||||
amountPaid: number;
|
||||
amountDue: number;
|
||||
notes?: string | null;
|
||||
internalNotes?: string | null;
|
||||
paymentTerms?: string | null;
|
||||
paymentMethodId?: string | null;
|
||||
billingAddress: Record<string, string>;
|
||||
cfdiUse?: string | null;
|
||||
cfdiUuid?: string | null;
|
||||
satStatus?: string | null;
|
||||
cancelledAt?: string | null;
|
||||
cancellationReason?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
items?: InvoiceItem[];
|
||||
}
|
||||
|
||||
export interface CreateInvoiceDto {
|
||||
companyId: string;
|
||||
invoiceType?: InvoiceType;
|
||||
invoiceContext?: InvoiceContext;
|
||||
partnerId?: string;
|
||||
subscriptionId?: string;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
dueDate: string;
|
||||
currency?: string;
|
||||
notes?: string;
|
||||
internalNotes?: string;
|
||||
paymentTerms?: string;
|
||||
billingAddress?: Record<string, string>;
|
||||
cfdiUse?: string;
|
||||
items: CreateInvoiceItemDto[];
|
||||
}
|
||||
|
||||
export interface CreateInvoiceItemDto {
|
||||
description: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
discount?: number;
|
||||
taxRate?: number;
|
||||
productId?: string;
|
||||
unitOfMeasure?: string;
|
||||
}
|
||||
|
||||
export interface InvoiceFilters {
|
||||
tenantId?: string;
|
||||
companyId?: string;
|
||||
partnerId?: string;
|
||||
subscriptionId?: string;
|
||||
status?: InvoiceStatus;
|
||||
invoiceType?: InvoiceType;
|
||||
invoiceContext?: InvoiceContext;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface InvoicesResponse {
|
||||
data: Invoice[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
export type PlanType = 'saas' | 'on_premise' | 'hybrid';
|
||||
|
||||
export interface SubscriptionPlan {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
planType: PlanType;
|
||||
baseMonthlyPrice: number;
|
||||
baseAnnualPrice?: number | null;
|
||||
setupFee: number;
|
||||
maxUsers: number;
|
||||
maxBranches: number;
|
||||
storageGb: number;
|
||||
apiCallsMonthly: number;
|
||||
includedModules: string[];
|
||||
includedPlatforms: string[];
|
||||
features: Record<string, boolean>;
|
||||
isActive: boolean;
|
||||
isPublic: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreatePlanDto {
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
planType?: PlanType;
|
||||
baseMonthlyPrice: number;
|
||||
baseAnnualPrice?: number;
|
||||
setupFee?: number;
|
||||
maxUsers?: number;
|
||||
maxBranches?: number;
|
||||
storageGb?: number;
|
||||
apiCallsMonthly?: number;
|
||||
includedModules?: string[];
|
||||
includedPlatforms?: string[];
|
||||
features?: Record<string, boolean>;
|
||||
isActive?: boolean;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdatePlanDto {
|
||||
name?: string;
|
||||
description?: string;
|
||||
planType?: PlanType;
|
||||
baseMonthlyPrice?: number;
|
||||
baseAnnualPrice?: number;
|
||||
setupFee?: number;
|
||||
maxUsers?: number;
|
||||
maxBranches?: number;
|
||||
storageGb?: number;
|
||||
apiCallsMonthly?: number;
|
||||
includedModules?: string[];
|
||||
includedPlatforms?: string[];
|
||||
features?: Record<string, boolean>;
|
||||
isActive?: boolean;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
export interface PlanFilters {
|
||||
search?: string;
|
||||
planType?: PlanType;
|
||||
isActive?: boolean;
|
||||
isPublic?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface PlansResponse {
|
||||
data: SubscriptionPlan[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
import type { SubscriptionPlan } from './plan.types';
|
||||
|
||||
export type BillingCycle = 'monthly' | 'annual';
|
||||
export type SubscriptionStatus = 'trial' | 'active' | 'past_due' | 'cancelled' | 'suspended';
|
||||
|
||||
export interface BillingAddress {
|
||||
street?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
postalCode?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface TenantSubscription {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
planId: string;
|
||||
billingCycle: BillingCycle;
|
||||
currentPeriodStart: string;
|
||||
currentPeriodEnd: string;
|
||||
status: SubscriptionStatus;
|
||||
trialStart?: string | null;
|
||||
trialEnd?: string | null;
|
||||
billingEmail?: string | null;
|
||||
billingName?: string | null;
|
||||
billingAddress: BillingAddress;
|
||||
taxId?: string | null;
|
||||
paymentMethodId?: string | null;
|
||||
paymentProvider?: string | null;
|
||||
stripeCustomerId?: string | null;
|
||||
stripeSubscriptionId?: string | null;
|
||||
lastPaymentAt?: string | null;
|
||||
lastPaymentAmount?: number | null;
|
||||
currentPrice: number;
|
||||
discountPercent: number;
|
||||
discountReason?: string | null;
|
||||
contractedUsers?: number | null;
|
||||
contractedBranches?: number | null;
|
||||
autoRenew: boolean;
|
||||
nextInvoiceDate?: string | null;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
cancelledAt?: string | null;
|
||||
cancellationReason?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
plan?: SubscriptionPlan;
|
||||
}
|
||||
|
||||
export interface CreateSubscriptionDto {
|
||||
tenantId: string;
|
||||
planId: string;
|
||||
billingCycle?: BillingCycle;
|
||||
billingEmail?: string;
|
||||
billingName?: string;
|
||||
billingAddress?: BillingAddress;
|
||||
taxId?: string;
|
||||
paymentMethodId?: string;
|
||||
paymentProvider?: string;
|
||||
currentPrice?: number;
|
||||
contractedUsers?: number;
|
||||
contractedBranches?: number;
|
||||
couponCode?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSubscriptionDto {
|
||||
planId?: string;
|
||||
billingCycle?: BillingCycle;
|
||||
billingEmail?: string;
|
||||
billingName?: string;
|
||||
billingAddress?: BillingAddress;
|
||||
taxId?: string;
|
||||
paymentMethodId?: string;
|
||||
paymentProvider?: string;
|
||||
autoRenew?: boolean;
|
||||
contractedUsers?: number;
|
||||
contractedBranches?: number;
|
||||
}
|
||||
|
||||
export interface CancelSubscriptionDto {
|
||||
cancelImmediately?: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptionFilters {
|
||||
tenantId?: string;
|
||||
planId?: string;
|
||||
status?: SubscriptionStatus;
|
||||
billingCycle?: BillingCycle;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface SubscriptionsResponse {
|
||||
data: TenantSubscription[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
export interface UsageTracking {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
activeUsers: number;
|
||||
peakConcurrentUsers: number;
|
||||
usersByProfile: Record<string, number>;
|
||||
usersByPlatform: Record<string, number>;
|
||||
activeBranches: number;
|
||||
storageUsedGb: number;
|
||||
documentsCount: number;
|
||||
apiCalls: number;
|
||||
apiErrors: number;
|
||||
salesCount: number;
|
||||
salesAmount: number;
|
||||
invoicesGenerated: number;
|
||||
mobileSessions: number;
|
||||
offlineSyncs: number;
|
||||
paymentTransactions: number;
|
||||
totalBillableAmount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UsageSummary {
|
||||
currentPeriod: UsageTracking | null;
|
||||
limits: {
|
||||
maxUsers: number;
|
||||
maxBranches: number;
|
||||
storageGb: number;
|
||||
apiCallsMonthly: number;
|
||||
};
|
||||
usage: {
|
||||
usersPercent: number;
|
||||
branchesPercent: number;
|
||||
storagePercent: number;
|
||||
apiCallsPercent: number;
|
||||
};
|
||||
overages: {
|
||||
users: number;
|
||||
branches: number;
|
||||
storage: number;
|
||||
apiCalls: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UsageFilters {
|
||||
tenantId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface UsageResponse {
|
||||
data: UsageTracking[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import type { Country, State, CreateStateDto, UpdateStateDto } from '../types';
|
||||
|
||||
const API_BASE = '/api/core';
|
||||
|
||||
export const countriesApi = {
|
||||
// Countries
|
||||
getAll: async (): Promise<Country[]> => {
|
||||
const response = await axios.get(`${API_BASE}/countries`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Country> => {
|
||||
const response = await axios.get(`${API_BASE}/countries/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// States
|
||||
getStates: async (params?: { countryId?: string; countryCode?: string; active?: boolean }): Promise<State[]> => {
|
||||
const response = await axios.get(`${API_BASE}/states`, { params });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getStatesByCountry: async (countryId: string): Promise<State[]> => {
|
||||
const response = await axios.get(`${API_BASE}/countries/${countryId}/states`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getStatesByCountryCode: async (countryCode: string): Promise<State[]> => {
|
||||
const response = await axios.get(`${API_BASE}/countries/code/${countryCode}/states`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getStateById: async (id: string): Promise<State> => {
|
||||
const response = await axios.get(`${API_BASE}/states/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
createState: async (data: CreateStateDto): Promise<State> => {
|
||||
const response = await axios.post(`${API_BASE}/states`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
updateState: async (id: string, data: UpdateStateDto): Promise<State> => {
|
||||
const response = await axios.put(`${API_BASE}/states/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
deleteState: async (id: string): Promise<void> => {
|
||||
await axios.delete(`${API_BASE}/states/${id}`);
|
||||
},
|
||||
};
|
||||
@ -1,93 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import type {
|
||||
Currency,
|
||||
CreateCurrencyDto,
|
||||
UpdateCurrencyDto,
|
||||
CurrencyRate,
|
||||
CreateCurrencyRateDto,
|
||||
CurrencyConversion,
|
||||
LatestRates,
|
||||
} from '../types';
|
||||
|
||||
const API_BASE = '/api/core';
|
||||
|
||||
export const currenciesApi = {
|
||||
// Currencies
|
||||
getAll: async (activeOnly?: boolean): Promise<Currency[]> => {
|
||||
const params = activeOnly ? { active: 'true' } : {};
|
||||
const response = await axios.get(`${API_BASE}/currencies`, { params });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Currency> => {
|
||||
const response = await axios.get(`${API_BASE}/currencies/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
create: async (data: CreateCurrencyDto): Promise<Currency> => {
|
||||
const response = await axios.post(`${API_BASE}/currencies`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateCurrencyDto): Promise<Currency> => {
|
||||
const response = await axios.put(`${API_BASE}/currencies/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// Currency Rates
|
||||
getRates: async (params?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
}): Promise<CurrencyRate[]> => {
|
||||
const response = await axios.get(`${API_BASE}/currency-rates`, { params });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getRate: async (from: string, to: string, date?: string): Promise<{
|
||||
from: string;
|
||||
to: string;
|
||||
rate: number;
|
||||
date: string;
|
||||
}> => {
|
||||
const params = date ? { date } : {};
|
||||
const response = await axios.get(`${API_BASE}/currency-rates/rate/${from}/${to}`, { params });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getLatestRates: async (baseCurrency?: string): Promise<LatestRates> => {
|
||||
const params = baseCurrency ? { base: baseCurrency } : {};
|
||||
const response = await axios.get(`${API_BASE}/currency-rates/latest`, { params });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getRateHistory: async (from: string, to: string, days?: number): Promise<CurrencyRate[]> => {
|
||||
const params = days ? { days } : {};
|
||||
const response = await axios.get(`${API_BASE}/currency-rates/history/${from}/${to}`, { params });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
createRate: async (data: CreateCurrencyRateDto): Promise<CurrencyRate> => {
|
||||
const response = await axios.post(`${API_BASE}/currency-rates`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
convert: async (
|
||||
amount: number,
|
||||
fromCurrencyCode: string,
|
||||
toCurrencyCode: string,
|
||||
date?: string
|
||||
): Promise<CurrencyConversion> => {
|
||||
const response = await axios.post(`${API_BASE}/currency-rates/convert`, {
|
||||
amount,
|
||||
fromCurrencyCode,
|
||||
toCurrencyCode,
|
||||
date,
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
deleteRate: async (id: string): Promise<void> => {
|
||||
await axios.delete(`${API_BASE}/currency-rates/${id}`);
|
||||
},
|
||||
};
|
||||
@ -1,255 +0,0 @@
|
||||
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,19 +0,0 @@
|
||||
export { countriesApi } from './countries.api';
|
||||
export { currenciesApi } from './currencies.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';
|
||||
@ -1,81 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import type {
|
||||
UomCategory,
|
||||
Uom,
|
||||
CreateUomDto,
|
||||
UpdateUomDto,
|
||||
UomConversion,
|
||||
ConversionTable,
|
||||
} from '../types';
|
||||
|
||||
const API_BASE = '/api/core';
|
||||
|
||||
export const uomApi = {
|
||||
// UoM Categories
|
||||
getCategories: async (activeOnly?: boolean): Promise<UomCategory[]> => {
|
||||
const params = activeOnly ? { active: 'true' } : {};
|
||||
const response = await axios.get(`${API_BASE}/uom-categories`, { params });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getCategoryById: async (id: string): Promise<UomCategory> => {
|
||||
const response = await axios.get(`${API_BASE}/uom-categories/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// UoM
|
||||
getAll: async (params?: { categoryId?: string; active?: boolean }): Promise<Uom[]> => {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (params?.categoryId) queryParams.category_id = params.categoryId;
|
||||
if (params?.active) queryParams.active = 'true';
|
||||
|
||||
const response = await axios.get(`${API_BASE}/uom`, { params: queryParams });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Uom> => {
|
||||
const response = await axios.get(`${API_BASE}/uom/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getByCode: async (code: string): Promise<Uom | null> => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/uom/by-code/${code}`);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
create: async (data: CreateUomDto): Promise<Uom> => {
|
||||
const response = await axios.post(`${API_BASE}/uom`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateUomDto): Promise<Uom> => {
|
||||
const response = await axios.put(`${API_BASE}/uom/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// Conversions
|
||||
convert: async (
|
||||
quantity: number,
|
||||
fromUomId: string,
|
||||
toUomId: string
|
||||
): Promise<UomConversion> => {
|
||||
const response = await axios.post(`${API_BASE}/uom/convert`, {
|
||||
quantity,
|
||||
fromUomId,
|
||||
toUomId,
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getConversionTable: async (categoryId: string): Promise<ConversionTable> => {
|
||||
const response = await axios.get(`${API_BASE}/uom-categories/${categoryId}/conversions`);
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
@ -1,301 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { CountrySelect } from './CountrySelect';
|
||||
import { StateSelect } from './StateSelect';
|
||||
import type { Country, State } from '../types';
|
||||
|
||||
export interface AddressData {
|
||||
street?: string;
|
||||
exteriorNumber?: string;
|
||||
interiorNumber?: string;
|
||||
neighborhood?: string;
|
||||
city?: string;
|
||||
postalCode?: string;
|
||||
countryId?: string;
|
||||
countryCode?: string;
|
||||
stateId?: string;
|
||||
stateCode?: string;
|
||||
}
|
||||
|
||||
interface AddressInputProps {
|
||||
value?: AddressData;
|
||||
onChange: (address: AddressData) => void;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
errors?: Partial<Record<keyof AddressData, string>>;
|
||||
showLabels?: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const AddressInput: React.FC<AddressInputProps> = ({
|
||||
value = {},
|
||||
onChange,
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
errors = {},
|
||||
showLabels = true,
|
||||
compact = false,
|
||||
}) => {
|
||||
const [selectedCountry, setSelectedCountry] = useState<Country | null>(null);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(field: keyof AddressData, fieldValue: string | undefined) => {
|
||||
onChange({ ...value, [field]: fieldValue });
|
||||
},
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
const handleCountryChange = useCallback(
|
||||
(country: Country | null) => {
|
||||
setSelectedCountry(country);
|
||||
onChange({
|
||||
...value,
|
||||
countryId: country?.id,
|
||||
countryCode: country?.code,
|
||||
stateId: undefined,
|
||||
stateCode: undefined,
|
||||
});
|
||||
},
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
const handleStateChange = useCallback(
|
||||
(state: State | null) => {
|
||||
onChange({
|
||||
...value,
|
||||
stateId: state?.id,
|
||||
stateCode: state?.code,
|
||||
});
|
||||
},
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
const inputClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const getInputClass = (field: keyof AddressData) => {
|
||||
return `${inputClasses} ${errors[field] ? 'border-red-300' : 'border-gray-300'}`;
|
||||
};
|
||||
|
||||
const renderLabel = (text: string, isRequired: boolean = false) => {
|
||||
if (!showLabels) return null;
|
||||
return (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{text}
|
||||
{isRequired && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const renderError = (field: keyof AddressData) => {
|
||||
if (!errors[field]) return null;
|
||||
return <p className="mt-1 text-sm text-red-600">{errors[field]}</p>;
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
{renderLabel('Calle', required)}
|
||||
<input
|
||||
type="text"
|
||||
value={value.street || ''}
|
||||
onChange={(e) => handleFieldChange('street', e.target.value)}
|
||||
placeholder="Calle"
|
||||
disabled={disabled}
|
||||
className={getInputClass('street')}
|
||||
/>
|
||||
{renderError('street')}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
{renderLabel('No. Ext')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.exteriorNumber || ''}
|
||||
onChange={(e) => handleFieldChange('exteriorNumber', e.target.value)}
|
||||
placeholder="Ext"
|
||||
disabled={disabled}
|
||||
className={getInputClass('exteriorNumber')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{renderLabel('No. Int')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.interiorNumber || ''}
|
||||
onChange={(e) => handleFieldChange('interiorNumber', e.target.value)}
|
||||
placeholder="Int"
|
||||
disabled={disabled}
|
||||
className={getInputClass('interiorNumber')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
{renderLabel('Colonia')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.neighborhood || ''}
|
||||
onChange={(e) => handleFieldChange('neighborhood', e.target.value)}
|
||||
placeholder="Colonia"
|
||||
disabled={disabled}
|
||||
className={getInputClass('neighborhood')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{renderLabel('Ciudad', required)}
|
||||
<input
|
||||
type="text"
|
||||
value={value.city || ''}
|
||||
onChange={(e) => handleFieldChange('city', e.target.value)}
|
||||
placeholder="Ciudad"
|
||||
disabled={disabled}
|
||||
className={getInputClass('city')}
|
||||
/>
|
||||
{renderError('city')}
|
||||
</div>
|
||||
<div>
|
||||
{renderLabel('C.P.')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.postalCode || ''}
|
||||
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
|
||||
placeholder="C.P."
|
||||
disabled={disabled}
|
||||
className={getInputClass('postalCode')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<CountrySelect
|
||||
value={value.countryId}
|
||||
onChange={handleCountryChange}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
error={errors.countryId}
|
||||
label={showLabels ? 'País' : undefined}
|
||||
/>
|
||||
<StateSelect
|
||||
value={value.stateId}
|
||||
onChange={handleStateChange}
|
||||
countryId={selectedCountry?.id || value.countryId}
|
||||
disabled={disabled}
|
||||
error={errors.stateId}
|
||||
label={showLabels ? 'Estado' : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
<div>
|
||||
{renderLabel('Calle', required)}
|
||||
<input
|
||||
type="text"
|
||||
value={value.street || ''}
|
||||
onChange={(e) => handleFieldChange('street', e.target.value)}
|
||||
placeholder="Nombre de la calle"
|
||||
disabled={disabled}
|
||||
className={getInputClass('street')}
|
||||
/>
|
||||
{renderError('street')}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
{renderLabel('Número Exterior')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.exteriorNumber || ''}
|
||||
onChange={(e) => handleFieldChange('exteriorNumber', e.target.value)}
|
||||
placeholder="No. Ext"
|
||||
disabled={disabled}
|
||||
className={getInputClass('exteriorNumber')}
|
||||
/>
|
||||
{renderError('exteriorNumber')}
|
||||
</div>
|
||||
<div>
|
||||
{renderLabel('Número Interior')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.interiorNumber || ''}
|
||||
onChange={(e) => handleFieldChange('interiorNumber', e.target.value)}
|
||||
placeholder="No. Int (opcional)"
|
||||
disabled={disabled}
|
||||
className={getInputClass('interiorNumber')}
|
||||
/>
|
||||
{renderError('interiorNumber')}
|
||||
</div>
|
||||
<div>
|
||||
{renderLabel('Código Postal')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.postalCode || ''}
|
||||
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
|
||||
placeholder="C.P."
|
||||
disabled={disabled}
|
||||
className={getInputClass('postalCode')}
|
||||
/>
|
||||
{renderError('postalCode')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
{renderLabel('Colonia')}
|
||||
<input
|
||||
type="text"
|
||||
value={value.neighborhood || ''}
|
||||
onChange={(e) => handleFieldChange('neighborhood', e.target.value)}
|
||||
placeholder="Colonia o fraccionamiento"
|
||||
disabled={disabled}
|
||||
className={getInputClass('neighborhood')}
|
||||
/>
|
||||
{renderError('neighborhood')}
|
||||
</div>
|
||||
<div>
|
||||
{renderLabel('Ciudad', required)}
|
||||
<input
|
||||
type="text"
|
||||
value={value.city || ''}
|
||||
onChange={(e) => handleFieldChange('city', e.target.value)}
|
||||
placeholder="Ciudad o municipio"
|
||||
disabled={disabled}
|
||||
className={getInputClass('city')}
|
||||
/>
|
||||
{renderError('city')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<CountrySelect
|
||||
value={value.countryId}
|
||||
onChange={handleCountryChange}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
error={errors.countryId}
|
||||
label={showLabels ? 'País' : undefined}
|
||||
/>
|
||||
<StateSelect
|
||||
value={value.stateId}
|
||||
onChange={handleStateChange}
|
||||
countryId={selectedCountry?.id || value.countryId}
|
||||
disabled={disabled}
|
||||
error={errors.stateId}
|
||||
label={showLabels ? 'Estado / Provincia' : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,68 +0,0 @@
|
||||
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;
|
||||
@ -1,144 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useConversionTable } from '../hooks';
|
||||
|
||||
interface ConversionTableDisplayProps {
|
||||
categoryId: string | null;
|
||||
className?: string;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const ConversionTableDisplay: React.FC<ConversionTableDisplayProps> = ({
|
||||
categoryId,
|
||||
className = '',
|
||||
compact = false,
|
||||
}) => {
|
||||
const { table, isLoading, error } = useConversionTable(categoryId);
|
||||
|
||||
if (!categoryId) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-gray-50 p-4 text-center ${className}`}>
|
||||
<p className="text-sm text-gray-500">Seleccione una categoría para ver las conversiones</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-gray-50 p-4 ${className}`}>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<span className="ml-2 text-sm text-gray-500">Cargando conversiones...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-red-50 p-4 ${className}`}>
|
||||
<p className="text-sm text-red-600">{error.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!table || table.units.length === 0) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-gray-50 p-4 text-center ${className}`}>
|
||||
<p className="text-sm text-gray-500">No hay unidades en esta categoría</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { categoryName, referenceUnit, units, conversions } = table;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`rounded-lg border border-gray-200 p-3 ${className}`}>
|
||||
<p className="mb-2 text-xs font-medium text-gray-600">{categoryName}</p>
|
||||
<div className="space-y-1">
|
||||
{conversions.slice(0, 5).map((conv, idx) => (
|
||||
<div key={idx} className="flex justify-between text-xs">
|
||||
<span className="text-gray-600">
|
||||
1 {conv.fromCode} →
|
||||
</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{conv.factor.toFixed(4)} {conv.toCode}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{conversions.length > 5 && (
|
||||
<p className="text-xs text-gray-400">
|
||||
+{conversions.length - 5} más...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-gray-200 ${className}`}>
|
||||
<div className="border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<h3 className="text-sm font-medium text-gray-900">{categoryName}</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Unidad de referencia: <span className="font-medium">{referenceUnit}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{units.map((unit) => (
|
||||
<span
|
||||
key={unit.id}
|
||||
className={`
|
||||
inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium
|
||||
${unit.isReference
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{unit.code}
|
||||
{unit.isReference && (
|
||||
<span className="ml-1 text-blue-600">*</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500">
|
||||
De
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500">
|
||||
A
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium uppercase text-gray-500">
|
||||
Factor
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{conversions.map((conv, idx) => (
|
||||
<tr key={idx} className="hover:bg-gray-50">
|
||||
<td className="whitespace-nowrap px-3 py-2 font-medium text-gray-900">
|
||||
1 {conv.fromCode}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-2 text-gray-600">
|
||||
{conv.toCode}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-2 text-right font-mono text-gray-900">
|
||||
{conv.factor.toFixed(6)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,79 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useCountries } from '../hooks';
|
||||
import type { Country } from '../types';
|
||||
|
||||
interface CountrySelectProps {
|
||||
value?: string;
|
||||
onChange: (country: Country | null) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const CountrySelect: React.FC<CountrySelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Seleccionar país',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
error,
|
||||
label,
|
||||
}) => {
|
||||
const { countries, isLoading, error: loadError } = useCountries();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedId = e.target.value;
|
||||
if (!selectedId) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
const country = countries.find((c) => c.id === selectedId);
|
||||
onChange(country || null);
|
||||
};
|
||||
|
||||
const baseClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const errorClasses = error || loadError
|
||||
? 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
disabled={disabled || isLoading}
|
||||
required={required}
|
||||
className={`${baseClasses} ${errorClasses}`}
|
||||
>
|
||||
<option value="">
|
||||
{isLoading ? 'Cargando...' : placeholder}
|
||||
</option>
|
||||
{countries.map((country) => (
|
||||
<option key={country.id} value={country.id}>
|
||||
{country.name} ({country.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{(error || loadError) && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{error || loadError?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,139 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useCurrencies, useCurrencyConversion } from '../hooks';
|
||||
import type { Currency, CurrencyConversion } from '../types';
|
||||
|
||||
interface CurrencyInputProps {
|
||||
value?: number;
|
||||
onChange: (amount: number) => void;
|
||||
currencyId?: string;
|
||||
baseCurrencyId?: string;
|
||||
showConversion?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
label?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export const CurrencyInput: React.FC<CurrencyInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
currencyId,
|
||||
baseCurrencyId,
|
||||
showConversion = false,
|
||||
placeholder = '0.00',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
error,
|
||||
label,
|
||||
min = 0,
|
||||
max,
|
||||
step = 0.01,
|
||||
}) => {
|
||||
const { currencies } = useCurrencies();
|
||||
const { convert, isLoading: isConverting } = useCurrencyConversion();
|
||||
const [conversion, setConversion] = useState<CurrencyConversion | null>(null);
|
||||
|
||||
const selectedCurrency = currencies.find((c) => c.id === currencyId);
|
||||
const baseCurrency = currencies.find((c) => c.id === baseCurrencyId);
|
||||
|
||||
const fetchConversion = useCallback(async () => {
|
||||
if (!showConversion || !value || !currencyId || !baseCurrencyId || currencyId === baseCurrencyId) {
|
||||
setConversion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromCurrency = currencies.find((c) => c.id === currencyId);
|
||||
const toCurrency = currencies.find((c) => c.id === baseCurrencyId);
|
||||
if (!fromCurrency || !toCurrency) return;
|
||||
|
||||
const result = await convert(value, fromCurrency.code, toCurrency.code);
|
||||
setConversion(result);
|
||||
}, [showConversion, value, currencyId, baseCurrencyId, currencies, convert]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(fetchConversion, 500);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [fetchConversion]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = parseFloat(e.target.value) || 0;
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number, currency?: Currency) => {
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: currency?.code || 'MXN',
|
||||
minimumFractionDigits: currency?.decimals ?? 2,
|
||||
maximumFractionDigits: currency?.decimals ?? 2,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const baseClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm text-right
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const errorClasses = error
|
||||
? 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
{selectedCurrency?.symbol && (
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 text-sm">
|
||||
{selectedCurrency.symbol}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="number"
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
className={`${baseClasses} ${errorClasses} ${selectedCurrency?.symbol ? 'pl-8' : ''}`}
|
||||
/>
|
||||
{selectedCurrency && (
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs">
|
||||
{selectedCurrency.code}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{showConversion && conversion && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{isConverting ? (
|
||||
'Calculando...'
|
||||
) : (
|
||||
<>
|
||||
≈ {formatAmount(conversion.convertedAmount, baseCurrency)}
|
||||
<span className="ml-1 text-gray-400">
|
||||
(TC: {conversion.rate.toFixed(4)})
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p className="mt-1 text-sm text-red-600">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,127 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useLatestRates } from '../hooks';
|
||||
|
||||
interface CurrencyRatesDisplayProps {
|
||||
baseCurrency?: string;
|
||||
className?: string;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const CurrencyRatesDisplay: React.FC<CurrencyRatesDisplayProps> = ({
|
||||
baseCurrency = 'MXN',
|
||||
className = '',
|
||||
compact = false,
|
||||
}) => {
|
||||
const { data, isLoading, error, refresh } = useLatestRates(baseCurrency);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-gray-50 p-4 ${className}`}>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<span className="ml-2 text-sm text-gray-500">Cargando tipos de cambio...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-red-50 p-4 ${className}`}>
|
||||
<p className="text-sm text-red-600">{error.message}</p>
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="mt-2 text-sm text-red-700 underline hover:no-underline"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data.rates).length === 0) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-gray-50 p-4 text-center ${className}`}>
|
||||
<p className="text-sm text-gray-500">No hay tipos de cambio disponibles</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-MX', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const rates = Object.entries(data.rates);
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`rounded-lg border border-gray-200 p-3 ${className}`}>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-gray-600">
|
||||
Tipos de Cambio ({baseCurrency})
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">{formatDate(data.date)}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{rates.slice(0, 4).map(([currency, rate]) => (
|
||||
<div key={currency} className="flex justify-between text-xs">
|
||||
<span className="text-gray-600">{currency}</span>
|
||||
<span className="font-medium text-gray-900">{rate.toFixed(4)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{rates.length > 4 && (
|
||||
<p className="mt-1 text-xs text-gray-400">+{rates.length - 4} más</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-gray-200 ${className}`}>
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900">Tipos de Cambio</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Base: <span className="font-medium">{baseCurrency}</span> |
|
||||
Fecha: <span className="font-medium">{formatDate(data.date)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
||||
>
|
||||
Actualizar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{rates.map(([currency, rate]) => (
|
||||
<div
|
||||
key={currency}
|
||||
className="flex items-center justify-between rounded-lg bg-gray-50 px-3 py-2"
|
||||
>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-900">{currency}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="font-mono text-sm font-semibold text-gray-900">
|
||||
{rate.toFixed(4)}
|
||||
</span>
|
||||
<p className="text-xs text-gray-500">
|
||||
1 {currency} = {(1 / rate).toFixed(4)} {baseCurrency}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useCurrencies } from '../hooks';
|
||||
import type { Currency } from '../types';
|
||||
|
||||
interface CurrencySelectProps {
|
||||
value?: string;
|
||||
onChange: (currency: Currency | null) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
label?: string;
|
||||
showSymbol?: boolean;
|
||||
activeOnly?: boolean;
|
||||
}
|
||||
|
||||
export const CurrencySelect: React.FC<CurrencySelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Seleccionar moneda',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
error,
|
||||
label,
|
||||
showSymbol = true,
|
||||
activeOnly = true,
|
||||
}) => {
|
||||
const { currencies, isLoading, error: loadError } = useCurrencies(activeOnly);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedId = e.target.value;
|
||||
if (!selectedId) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
const currency = currencies.find((c) => c.id === selectedId);
|
||||
onChange(currency || null);
|
||||
};
|
||||
|
||||
const baseClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const errorClasses = error || loadError
|
||||
? 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
disabled={disabled || isLoading}
|
||||
required={required}
|
||||
className={`${baseClasses} ${errorClasses}`}
|
||||
>
|
||||
<option value="">
|
||||
{isLoading ? 'Cargando...' : placeholder}
|
||||
</option>
|
||||
{currencies.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{currency.code} - {currency.name}
|
||||
{showSymbol && currency.symbol && ` (${currency.symbol})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{(error || loadError) && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{error || loadError?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,130 +0,0 @@
|
||||
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;
|
||||
@ -1,59 +0,0 @@
|
||||
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;
|
||||
@ -1,62 +0,0 @@
|
||||
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;
|
||||
@ -1,66 +0,0 @@
|
||||
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;
|
||||
@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useStates } from '../hooks';
|
||||
import type { State } from '../types';
|
||||
|
||||
interface StateSelectProps {
|
||||
value?: string;
|
||||
onChange: (state: State | null) => void;
|
||||
countryId?: string;
|
||||
countryCode?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const StateSelect: React.FC<StateSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
countryId,
|
||||
countryCode,
|
||||
placeholder = 'Seleccionar estado',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
error,
|
||||
label,
|
||||
}) => {
|
||||
const { states, isLoading, error: loadError } = useStates(countryId, countryCode);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedId = e.target.value;
|
||||
if (!selectedId) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
const state = states.find((s) => s.id === selectedId);
|
||||
onChange(state || null);
|
||||
};
|
||||
|
||||
const hasCountryFilter = countryId || countryCode;
|
||||
|
||||
const baseClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const errorClasses = error || loadError
|
||||
? 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
disabled={disabled || isLoading || !hasCountryFilter}
|
||||
required={required}
|
||||
className={`${baseClasses} ${errorClasses}`}
|
||||
>
|
||||
<option value="">
|
||||
{!hasCountryFilter
|
||||
? 'Seleccione un país primero'
|
||||
: isLoading
|
||||
? 'Cargando...'
|
||||
: placeholder}
|
||||
</option>
|
||||
{states.map((state) => (
|
||||
<option key={state.id} value={state.id}>
|
||||
{state.name} ({state.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{(error || loadError) && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{error || loadError?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,65 +0,0 @@
|
||||
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;
|
||||
@ -1,79 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useUomCategories } from '../hooks';
|
||||
import type { UomCategory } from '../types';
|
||||
|
||||
interface UomCategorySelectProps {
|
||||
value?: string;
|
||||
onChange: (category: UomCategory | null) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const UomCategorySelect: React.FC<UomCategorySelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Seleccionar categoría',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
error,
|
||||
label,
|
||||
}) => {
|
||||
const { categories, isLoading, error: loadError } = useUomCategories();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedId = e.target.value;
|
||||
if (!selectedId) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
const category = categories.find((c) => c.id === selectedId);
|
||||
onChange(category || null);
|
||||
};
|
||||
|
||||
const baseClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const errorClasses = error || loadError
|
||||
? 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
disabled={disabled || isLoading}
|
||||
required={required}
|
||||
className={`${baseClasses} ${errorClasses}`}
|
||||
>
|
||||
<option value="">
|
||||
{isLoading ? 'Cargando...' : placeholder}
|
||||
</option>
|
||||
{categories.map((category) => (
|
||||
<option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{(error || loadError) && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{error || loadError?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,128 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useUom, useUomConversion } from '../hooks';
|
||||
import type { Uom, UomConversion } from '../types';
|
||||
|
||||
interface UomQuantityInputProps {
|
||||
value?: number;
|
||||
onChange: (quantity: number) => void;
|
||||
uomId?: string;
|
||||
targetUomId?: string;
|
||||
categoryId?: string;
|
||||
showConversion?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
label?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export const UomQuantityInput: React.FC<UomQuantityInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
uomId,
|
||||
targetUomId,
|
||||
categoryId,
|
||||
showConversion = false,
|
||||
placeholder = '0',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
error,
|
||||
label,
|
||||
min = 0,
|
||||
max,
|
||||
step = 1,
|
||||
}) => {
|
||||
const { uoms } = useUom(categoryId);
|
||||
const { convert, isLoading: isConverting } = useUomConversion();
|
||||
const [conversion, setConversion] = useState<UomConversion | null>(null);
|
||||
|
||||
const selectedUom = uoms.find((u) => u.id === uomId);
|
||||
const targetUom = uoms.find((u) => u.id === targetUomId);
|
||||
|
||||
const fetchConversion = useCallback(async () => {
|
||||
if (!showConversion || !value || !uomId || !targetUomId || uomId === targetUomId) {
|
||||
setConversion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await convert(value, uomId, targetUomId);
|
||||
setConversion(result);
|
||||
}, [showConversion, value, uomId, targetUomId, convert]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(fetchConversion, 300);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [fetchConversion]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = parseFloat(e.target.value) || 0;
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const formatQuantity = (qty: number, uom?: Uom) => {
|
||||
const decimals = uom?.code?.includes('kg') || uom?.code?.includes('l') ? 3 : 2;
|
||||
return `${qty.toFixed(decimals)} ${uom?.code || ''}`;
|
||||
};
|
||||
|
||||
const baseClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm text-right
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const errorClasses = error
|
||||
? 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
className={`${baseClasses} ${errorClasses} pr-16`}
|
||||
/>
|
||||
{selectedUom && (
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs">
|
||||
{selectedUom.code}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{showConversion && conversion && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{isConverting ? (
|
||||
'Calculando...'
|
||||
) : (
|
||||
<>
|
||||
= {formatQuantity(conversion.convertedQuantity, targetUom)}
|
||||
<span className="ml-1 text-gray-400">
|
||||
(factor: {conversion.factor.toFixed(6)})
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p className="mt-1 text-sm text-red-600">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useUom } from '../hooks';
|
||||
import type { Uom } from '../types';
|
||||
|
||||
interface UomSelectProps {
|
||||
value?: string;
|
||||
onChange: (uom: Uom | null) => void;
|
||||
categoryId?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
error?: string;
|
||||
label?: string;
|
||||
activeOnly?: boolean;
|
||||
}
|
||||
|
||||
export const UomSelect: React.FC<UomSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
categoryId,
|
||||
placeholder = 'Seleccionar unidad',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
error,
|
||||
label,
|
||||
activeOnly = true,
|
||||
}) => {
|
||||
const { uoms, isLoading, error: loadError } = useUom(categoryId, activeOnly);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedId = e.target.value;
|
||||
if (!selectedId) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
const uom = uoms.find((u) => u.id === selectedId);
|
||||
onChange(uom || null);
|
||||
};
|
||||
|
||||
const baseClasses = `
|
||||
w-full rounded-md border px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const errorClasses = error || loadError
|
||||
? 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && (
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
disabled={disabled || isLoading}
|
||||
required={required}
|
||||
className={`${baseClasses} ${errorClasses}`}
|
||||
>
|
||||
<option value="">
|
||||
{isLoading ? 'Cargando...' : placeholder}
|
||||
</option>
|
||||
{uoms.map((uom) => (
|
||||
<option key={uom.id} value={uom.id}>
|
||||
{uom.name} ({uom.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{(error || loadError) && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{error || loadError?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,67 +0,0 @@
|
||||
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;
|
||||
@ -1,26 +0,0 @@
|
||||
// Select components
|
||||
export { CountrySelect } from './CountrySelect';
|
||||
export { StateSelect } from './StateSelect';
|
||||
export { CurrencySelect } from './CurrencySelect';
|
||||
export { UomCategorySelect } from './UomCategorySelect';
|
||||
export { UomSelect } from './UomSelect';
|
||||
|
||||
// Input components
|
||||
export { CurrencyInput } from './CurrencyInput';
|
||||
export { UomQuantityInput } from './UomQuantityInput';
|
||||
export { AddressInput } from './AddressInput';
|
||||
export type { AddressData } from './AddressInput';
|
||||
|
||||
// Display components
|
||||
export { ConversionTableDisplay } from './ConversionTableDisplay';
|
||||
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,14 +0,0 @@
|
||||
export { useCountries, useCountry, useStates } from './useCountries';
|
||||
export { useCurrencies, useCurrencyRates, useLatestRates, useCurrencyConversion } from './useCurrencies';
|
||||
export { useUomCategories, useUom, useUomConversion, useConversionTable } from './useUom';
|
||||
export {
|
||||
useTaxCategories,
|
||||
useFiscalRegimes,
|
||||
useFiscalRegimesByPersonType,
|
||||
useCfdiUses,
|
||||
useCfdiUsesForRegime,
|
||||
useFiscalPaymentMethods,
|
||||
useFiscalPaymentTypes,
|
||||
useWithholdingTypes,
|
||||
useWithholdingTypesByCategory,
|
||||
} from './useFiscalCatalogs';
|
||||
@ -1,120 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { countriesApi } from '../api';
|
||||
import type { Country, State, CreateStateDto, UpdateStateDto } from '../types';
|
||||
|
||||
export function useCountries() {
|
||||
const [countries, setCountries] = useState<Country[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchCountries = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await countriesApi.getAll();
|
||||
setCountries(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar países'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCountries();
|
||||
}, [fetchCountries]);
|
||||
|
||||
return {
|
||||
countries,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchCountries,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCountry(id: string | null) {
|
||||
const [country, setCountry] = useState<Country | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
setCountry(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetch = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await countriesApi.getById(id);
|
||||
setCountry(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar país'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetch();
|
||||
}, [id]);
|
||||
|
||||
return { country, isLoading, error };
|
||||
}
|
||||
|
||||
export function useStates(countryId?: string, countryCode?: string) {
|
||||
const [states, setStates] = useState<State[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchStates = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
let data: State[];
|
||||
if (countryId) {
|
||||
data = await countriesApi.getStatesByCountry(countryId);
|
||||
} else if (countryCode) {
|
||||
data = await countriesApi.getStatesByCountryCode(countryCode);
|
||||
} else {
|
||||
data = await countriesApi.getStates({ active: true });
|
||||
}
|
||||
setStates(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar estados'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [countryId, countryCode]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStates();
|
||||
}, [fetchStates]);
|
||||
|
||||
const createState = useCallback(async (data: CreateStateDto): Promise<State> => {
|
||||
const newState = await countriesApi.createState(data);
|
||||
setStates(prev => [...prev, newState]);
|
||||
return newState;
|
||||
}, []);
|
||||
|
||||
const updateState = useCallback(async (id: string, data: UpdateStateDto): Promise<State> => {
|
||||
const updated = await countriesApi.updateState(id, data);
|
||||
setStates(prev => prev.map(s => s.id === id ? updated : s));
|
||||
return updated;
|
||||
}, []);
|
||||
|
||||
const deleteState = useCallback(async (id: string): Promise<void> => {
|
||||
await countriesApi.deleteState(id);
|
||||
setStates(prev => prev.filter(s => s.id !== id));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
states,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchStates,
|
||||
createState,
|
||||
updateState,
|
||||
deleteState,
|
||||
};
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { currenciesApi } from '../api';
|
||||
import type {
|
||||
Currency,
|
||||
CreateCurrencyDto,
|
||||
UpdateCurrencyDto,
|
||||
CurrencyRate,
|
||||
CreateCurrencyRateDto,
|
||||
CurrencyConversion,
|
||||
LatestRates,
|
||||
} from '../types';
|
||||
|
||||
export function useCurrencies(activeOnly = true) {
|
||||
const [currencies, setCurrencies] = useState<Currency[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchCurrencies = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await currenciesApi.getAll(activeOnly);
|
||||
setCurrencies(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar monedas'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [activeOnly]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrencies();
|
||||
}, [fetchCurrencies]);
|
||||
|
||||
const createCurrency = useCallback(async (data: CreateCurrencyDto): Promise<Currency> => {
|
||||
const newCurrency = await currenciesApi.create(data);
|
||||
setCurrencies(prev => [...prev, newCurrency]);
|
||||
return newCurrency;
|
||||
}, []);
|
||||
|
||||
const updateCurrency = useCallback(async (id: string, data: UpdateCurrencyDto): Promise<Currency> => {
|
||||
const updated = await currenciesApi.update(id, data);
|
||||
setCurrencies(prev => prev.map(c => c.id === id ? updated : c));
|
||||
return updated;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
currencies,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchCurrencies,
|
||||
createCurrency,
|
||||
updateCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCurrencyRates(from?: string, to?: string) {
|
||||
const [rates, setRates] = useState<CurrencyRate[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchRates = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await currenciesApi.getRates({ from, to });
|
||||
setRates(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar tipos de cambio'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [from, to]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRates();
|
||||
}, [fetchRates]);
|
||||
|
||||
const createRate = useCallback(async (data: CreateCurrencyRateDto): Promise<CurrencyRate> => {
|
||||
const newRate = await currenciesApi.createRate(data);
|
||||
setRates(prev => [newRate, ...prev]);
|
||||
return newRate;
|
||||
}, []);
|
||||
|
||||
const deleteRate = useCallback(async (id: string): Promise<void> => {
|
||||
await currenciesApi.deleteRate(id);
|
||||
setRates(prev => prev.filter(r => r.id !== id));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
rates,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchRates,
|
||||
createRate,
|
||||
deleteRate,
|
||||
};
|
||||
}
|
||||
|
||||
export function useLatestRates(baseCurrency = 'MXN') {
|
||||
const [data, setData] = useState<LatestRates | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchRates = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await currenciesApi.getLatestRates(baseCurrency);
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar tipos de cambio'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [baseCurrency]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRates();
|
||||
}, [fetchRates]);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchRates,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCurrencyConversion() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const convert = useCallback(async (
|
||||
amount: number,
|
||||
from: string,
|
||||
to: string,
|
||||
date?: string
|
||||
): Promise<CurrencyConversion | null> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await currenciesApi.convert(amount, from, to, date);
|
||||
return result;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error en conversión'));
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
convert,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@ -1,276 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
@ -1,150 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { uomApi } from '../api';
|
||||
import type {
|
||||
UomCategory,
|
||||
Uom,
|
||||
CreateUomDto,
|
||||
UpdateUomDto,
|
||||
UomConversion,
|
||||
ConversionTable,
|
||||
} from '../types';
|
||||
|
||||
export function useUomCategories() {
|
||||
const [categories, setCategories] = useState<UomCategory[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchCategories = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await uomApi.getCategories();
|
||||
setCategories(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar categorías de UdM'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
}, [fetchCategories]);
|
||||
|
||||
return {
|
||||
categories,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchCategories,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUom(categoryId?: string, activeOnly = true) {
|
||||
const [uoms, setUoms] = useState<Uom[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchUoms = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await uomApi.getAll({
|
||||
categoryId,
|
||||
active: activeOnly,
|
||||
});
|
||||
setUoms(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar unidades de medida'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [categoryId, activeOnly]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUoms();
|
||||
}, [fetchUoms]);
|
||||
|
||||
const createUom = useCallback(async (data: CreateUomDto): Promise<Uom> => {
|
||||
const newUom = await uomApi.create(data);
|
||||
setUoms(prev => [...prev, newUom]);
|
||||
return newUom;
|
||||
}, []);
|
||||
|
||||
const updateUom = useCallback(async (id: string, data: UpdateUomDto): Promise<Uom> => {
|
||||
const updated = await uomApi.update(id, data);
|
||||
setUoms(prev => prev.map(u => u.id === id ? updated : u));
|
||||
return updated;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
uoms,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchUoms,
|
||||
createUom,
|
||||
updateUom,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUomConversion() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const convert = useCallback(async (
|
||||
quantity: number,
|
||||
fromUomId: string,
|
||||
toUomId: string
|
||||
): Promise<UomConversion | null> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await uomApi.convert(quantity, fromUomId, toUomId);
|
||||
return result;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error en conversión de UdM'));
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
convert,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useConversionTable(categoryId: string | null) {
|
||||
const [table, setTable] = useState<ConversionTable | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!categoryId) {
|
||||
setTable(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetch = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await uomApi.getConversionTable(categoryId);
|
||||
setTable(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error al cargar tabla de conversiones'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetch();
|
||||
}, [categoryId]);
|
||||
|
||||
return {
|
||||
table,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// API
|
||||
export { countriesApi, currenciesApi, uomApi } from './api';
|
||||
|
||||
// Hooks
|
||||
export {
|
||||
useCountries,
|
||||
useCountry,
|
||||
useStates,
|
||||
useCurrencies,
|
||||
useCurrencyRates,
|
||||
useLatestRates,
|
||||
useCurrencyConversion,
|
||||
useUomCategories,
|
||||
useUom,
|
||||
useUomConversion,
|
||||
useConversionTable,
|
||||
} from './hooks';
|
||||
|
||||
// Components
|
||||
export {
|
||||
CountrySelect,
|
||||
StateSelect,
|
||||
CurrencySelect,
|
||||
UomCategorySelect,
|
||||
UomSelect,
|
||||
CurrencyInput,
|
||||
UomQuantityInput,
|
||||
AddressInput,
|
||||
ConversionTableDisplay,
|
||||
CurrencyRatesDisplay,
|
||||
} from './components';
|
||||
|
||||
export type { AddressData } from './components';
|
||||
@ -1,358 +0,0 @@
|
||||
// Country types
|
||||
export interface Country {
|
||||
id: string;
|
||||
code: string;
|
||||
codeAlpha3?: string;
|
||||
name: string;
|
||||
phoneCode?: string;
|
||||
currencyCode?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// State types
|
||||
export interface State {
|
||||
id: string;
|
||||
countryId: string;
|
||||
country?: Country;
|
||||
code: string;
|
||||
name: string;
|
||||
timezone?: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateStateDto {
|
||||
countryId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
timezone?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateStateDto {
|
||||
name?: string;
|
||||
timezone?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// Currency types
|
||||
export interface Currency {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
rounding: number;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateCurrencyDto {
|
||||
code: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals?: number;
|
||||
}
|
||||
|
||||
export interface UpdateCurrencyDto {
|
||||
name?: string;
|
||||
symbol?: string;
|
||||
decimals?: number;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
// Currency Rate types
|
||||
export interface CurrencyRate {
|
||||
id: string;
|
||||
tenantId?: string;
|
||||
fromCurrencyId: string;
|
||||
fromCurrency?: Currency;
|
||||
toCurrencyId: string;
|
||||
toCurrency?: Currency;
|
||||
rate: number;
|
||||
rateDate: string;
|
||||
source: 'manual' | 'banxico' | 'xe' | 'openexchange';
|
||||
createdBy?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateCurrencyRateDto {
|
||||
fromCurrencyCode: string;
|
||||
toCurrencyCode: string;
|
||||
rate: number;
|
||||
rateDate?: string;
|
||||
source?: 'manual' | 'banxico' | 'xe' | 'openexchange';
|
||||
}
|
||||
|
||||
export interface CurrencyConversion {
|
||||
originalAmount: number;
|
||||
convertedAmount: number;
|
||||
rate: number;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface LatestRates {
|
||||
base: string;
|
||||
rates: Record<string, number>;
|
||||
date: string;
|
||||
}
|
||||
|
||||
// UoM Category types
|
||||
export interface UomCategory {
|
||||
id: string;
|
||||
tenantId?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// UoM types
|
||||
export type UomType = 'reference' | 'bigger' | 'smaller';
|
||||
|
||||
export interface Uom {
|
||||
id: string;
|
||||
tenantId?: string;
|
||||
categoryId: string;
|
||||
category?: UomCategory;
|
||||
code: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
uomType: UomType;
|
||||
factor: number;
|
||||
rounding: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateUomDto {
|
||||
categoryId: string;
|
||||
name: string;
|
||||
code: string;
|
||||
uomType?: UomType;
|
||||
ratio?: number;
|
||||
}
|
||||
|
||||
export interface UpdateUomDto {
|
||||
name?: string;
|
||||
ratio?: number;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface UomConversion {
|
||||
originalQuantity: number;
|
||||
originalUom: string;
|
||||
convertedQuantity: number;
|
||||
targetUom: string;
|
||||
factor: number;
|
||||
}
|
||||
|
||||
export interface ConversionTableEntry {
|
||||
uom: Uom;
|
||||
toReference: number;
|
||||
fromReference: number;
|
||||
}
|
||||
|
||||
export interface ConversionTableConversion {
|
||||
fromCode: string;
|
||||
toCode: string;
|
||||
factor: number;
|
||||
}
|
||||
|
||||
export interface ConversionTableUnit {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
isReference: boolean;
|
||||
}
|
||||
|
||||
export interface ConversionTable {
|
||||
categoryName: string;
|
||||
referenceUnit: string;
|
||||
referenceUom: Uom;
|
||||
units: ConversionTableUnit[];
|
||||
conversions: ConversionTableConversion[];
|
||||
}
|
||||
|
||||
// Product Category types
|
||||
export interface ProductCategory {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
parentId?: string;
|
||||
parent?: ProductCategory;
|
||||
code?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
hierarchyPath?: string;
|
||||
hierarchyLevel: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateProductCategoryDto {
|
||||
code?: string;
|
||||
name: string;
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProductCategoryDto {
|
||||
name?: string;
|
||||
parentId?: string | null;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
// Payment Term types
|
||||
export interface PaymentTermLine {
|
||||
id: string;
|
||||
paymentTermId: string;
|
||||
sequence: number;
|
||||
lineType: 'balance' | 'percent' | 'fixed';
|
||||
valuePercent?: number;
|
||||
valueAmount?: number;
|
||||
days: number;
|
||||
dayOfMonth?: number;
|
||||
endOfMonth?: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentTerm {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
dueDays?: number;
|
||||
discountPercent?: number;
|
||||
discountDays?: number;
|
||||
isImmediate: boolean;
|
||||
isActive: boolean;
|
||||
lines: PaymentTermLine[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreatePaymentTermDto {
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
dueDays?: number;
|
||||
discountPercent?: number;
|
||||
discountDays?: number;
|
||||
isImmediate?: boolean;
|
||||
lines?: Partial<PaymentTermLine>[];
|
||||
}
|
||||
|
||||
// Discount Rule types
|
||||
export type DiscountType = 'percentage' | 'fixed' | 'price_override';
|
||||
export type DiscountAppliesTo = 'all' | 'category' | 'product' | 'customer' | 'customer_group';
|
||||
export type DiscountCondition = 'none' | 'min_quantity' | 'min_amount' | 'date_range' | 'first_purchase';
|
||||
|
||||
export interface DiscountRule {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
discountType: DiscountType;
|
||||
discountValue: number;
|
||||
maxDiscountAmount?: number;
|
||||
appliesTo: DiscountAppliesTo;
|
||||
appliesToId?: string;
|
||||
conditionType: DiscountCondition;
|
||||
conditionValue?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
priority: number;
|
||||
combinable: boolean;
|
||||
usageLimit?: number;
|
||||
usageCount: number;
|
||||
isActive: boolean;
|
||||
createdAt: 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;
|
||||
}
|
||||
@ -1,228 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
Lead,
|
||||
LeadCreateInput,
|
||||
LeadUpdateInput,
|
||||
LeadFilters,
|
||||
LeadConvertInput,
|
||||
LeadsResponse,
|
||||
Opportunity,
|
||||
OpportunityCreateInput,
|
||||
OpportunityUpdateInput,
|
||||
OpportunityFilters,
|
||||
OpportunitiesResponse,
|
||||
Pipeline,
|
||||
Stage,
|
||||
StageCreateInput,
|
||||
StageUpdateInput,
|
||||
StageType,
|
||||
StagesResponse,
|
||||
Activity,
|
||||
ActivityCreateInput,
|
||||
ActivityUpdateInput,
|
||||
ActivityFilters,
|
||||
ActivitiesResponse,
|
||||
} from '../types';
|
||||
|
||||
const CRM_BASE = '/api/crm';
|
||||
|
||||
// ============================================================================
|
||||
// Leads API
|
||||
// ============================================================================
|
||||
|
||||
export const leadsApi = {
|
||||
getAll: async (filters: LeadFilters = {}): Promise<LeadsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.companyId) params.append('company_id', filters.companyId);
|
||||
if (filters.status) params.append('status', filters.status);
|
||||
if (filters.stageId) params.append('stage_id', filters.stageId);
|
||||
if (filters.userId) params.append('user_id', filters.userId);
|
||||
if (filters.source) params.append('source', filters.source);
|
||||
if (filters.priority !== undefined) params.append('priority', String(filters.priority));
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<LeadsResponse>(`${CRM_BASE}/leads?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Lead> => {
|
||||
const response = await api.get<Lead>(`${CRM_BASE}/leads/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: LeadCreateInput): Promise<Lead> => {
|
||||
const response = await api.post<Lead>(`${CRM_BASE}/leads`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: LeadUpdateInput): Promise<Lead> => {
|
||||
const response = await api.patch<Lead>(`${CRM_BASE}/leads/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${CRM_BASE}/leads/${id}`);
|
||||
},
|
||||
|
||||
convert: async (id: string, data: LeadConvertInput): Promise<{ opportunityId?: string; partnerId?: string }> => {
|
||||
const response = await api.post<{ opportunityId?: string; partnerId?: string }>(
|
||||
`${CRM_BASE}/leads/${id}/convert`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markLost: async (id: string, reason?: string): Promise<Lead> => {
|
||||
const response = await api.post<Lead>(`${CRM_BASE}/leads/${id}/lost`, { reason });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Opportunities API
|
||||
// ============================================================================
|
||||
|
||||
export const opportunitiesApi = {
|
||||
getAll: async (filters: OpportunityFilters = {}): Promise<OpportunitiesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.companyId) params.append('company_id', filters.companyId);
|
||||
if (filters.status) params.append('status', filters.status);
|
||||
if (filters.stageId) params.append('stage_id', filters.stageId);
|
||||
if (filters.userId) params.append('user_id', filters.userId);
|
||||
if (filters.partnerId) params.append('partner_id', filters.partnerId);
|
||||
if (filters.priority !== undefined) params.append('priority', String(filters.priority));
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<OpportunitiesResponse>(`${CRM_BASE}/opportunities?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Opportunity> => {
|
||||
const response = await api.get<Opportunity>(`${CRM_BASE}/opportunities/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: OpportunityCreateInput): Promise<Opportunity> => {
|
||||
const response = await api.post<Opportunity>(`${CRM_BASE}/opportunities`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: OpportunityUpdateInput): Promise<Opportunity> => {
|
||||
const response = await api.patch<Opportunity>(`${CRM_BASE}/opportunities/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${CRM_BASE}/opportunities/${id}`);
|
||||
},
|
||||
|
||||
markWon: async (id: string): Promise<Opportunity> => {
|
||||
const response = await api.post<Opportunity>(`${CRM_BASE}/opportunities/${id}/won`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markLost: async (id: string, reason?: string): Promise<Opportunity> => {
|
||||
const response = await api.post<Opportunity>(`${CRM_BASE}/opportunities/${id}/lost`, { reason });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPipeline: async (companyId?: string): Promise<Pipeline> => {
|
||||
const params = companyId ? `?company_id=${companyId}` : '';
|
||||
const response = await api.get<Pipeline>(`${CRM_BASE}/opportunities/pipeline${params}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Stages API
|
||||
// ============================================================================
|
||||
|
||||
export const stagesApi = {
|
||||
getAll: async (type?: StageType, companyId?: string): Promise<StagesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (type) params.append('type', type);
|
||||
if (companyId) params.append('company_id', companyId);
|
||||
|
||||
const response = await api.get<StagesResponse>(`${CRM_BASE}/stages?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Stage> => {
|
||||
const response = await api.get<Stage>(`${CRM_BASE}/stages/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: StageCreateInput): Promise<Stage> => {
|
||||
const response = await api.post<Stage>(`${CRM_BASE}/stages`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: StageUpdateInput): Promise<Stage> => {
|
||||
const response = await api.patch<Stage>(`${CRM_BASE}/stages/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${CRM_BASE}/stages/${id}`);
|
||||
},
|
||||
|
||||
reorder: async (stageIds: string[]): Promise<void> => {
|
||||
await api.post(`${CRM_BASE}/stages/reorder`, { stageIds });
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Activities API
|
||||
// ============================================================================
|
||||
|
||||
export const activitiesApi = {
|
||||
getAll: async (filters: ActivityFilters = {}): Promise<ActivitiesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.companyId) params.append('company_id', filters.companyId);
|
||||
if (filters.type) params.append('type', filters.type);
|
||||
if (filters.state) params.append('state', filters.state);
|
||||
if (filters.userId) params.append('user_id', filters.userId);
|
||||
if (filters.leadId) params.append('lead_id', filters.leadId);
|
||||
if (filters.opportunityId) params.append('opportunity_id', filters.opportunityId);
|
||||
if (filters.partnerId) params.append('partner_id', filters.partnerId);
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<ActivitiesResponse>(`${CRM_BASE}/activities?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Activity> => {
|
||||
const response = await api.get<Activity>(`${CRM_BASE}/activities/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: ActivityCreateInput): Promise<Activity> => {
|
||||
const response = await api.post<Activity>(`${CRM_BASE}/activities`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: ActivityUpdateInput): Promise<Activity> => {
|
||||
const response = await api.patch<Activity>(`${CRM_BASE}/activities/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${CRM_BASE}/activities/${id}`);
|
||||
},
|
||||
|
||||
markDone: async (id: string): Promise<Activity> => {
|
||||
const response = await api.post<Activity>(`${CRM_BASE}/activities/${id}/done`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
cancel: async (id: string): Promise<Activity> => {
|
||||
const response = await api.post<Activity>(`${CRM_BASE}/activities/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from './crm.api';
|
||||
@ -1 +0,0 @@
|
||||
export * from './useCrm';
|
||||
@ -1,428 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { leadsApi, opportunitiesApi, stagesApi, activitiesApi } from '../api';
|
||||
import type {
|
||||
Lead,
|
||||
LeadCreateInput,
|
||||
LeadUpdateInput,
|
||||
LeadFilters,
|
||||
LeadStatus,
|
||||
LeadSource,
|
||||
LeadConvertInput,
|
||||
Opportunity,
|
||||
OpportunityCreateInput,
|
||||
OpportunityUpdateInput,
|
||||
OpportunityFilters,
|
||||
OpportunityStatus,
|
||||
Pipeline,
|
||||
Stage,
|
||||
StageCreateInput,
|
||||
StageUpdateInput,
|
||||
StageType,
|
||||
Activity,
|
||||
ActivityCreateInput,
|
||||
ActivityUpdateInput,
|
||||
ActivityFilters,
|
||||
ActivityType,
|
||||
ActivityState,
|
||||
} from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// useLeads Hook
|
||||
// ============================================================================
|
||||
|
||||
export interface UseLeadsOptions {
|
||||
status?: LeadStatus;
|
||||
stageId?: string;
|
||||
userId?: string;
|
||||
source?: LeadSource;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useLeads(options: UseLeadsOptions = {}) {
|
||||
const [leads, setLeads] = useState<Lead[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const { status, stageId, userId, source, search, limit = 20, autoFetch = true } = options;
|
||||
|
||||
const fetchLeads = useCallback(async (pageNum = 1) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const filters: LeadFilters = {
|
||||
status,
|
||||
stageId,
|
||||
userId,
|
||||
source,
|
||||
search,
|
||||
page: pageNum,
|
||||
limit,
|
||||
};
|
||||
const response = await leadsApi.getAll(filters);
|
||||
setLeads(response.data);
|
||||
setTotal(response.total);
|
||||
setPage(response.page);
|
||||
setTotalPages(response.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching leads'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [status, stageId, userId, source, search, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchLeads(1);
|
||||
}
|
||||
}, [fetchLeads, autoFetch]);
|
||||
|
||||
const createLead = useCallback(async (data: LeadCreateInput): Promise<Lead> => {
|
||||
const lead = await leadsApi.create(data);
|
||||
await fetchLeads(page);
|
||||
return lead;
|
||||
}, [fetchLeads, page]);
|
||||
|
||||
const updateLead = useCallback(async (id: string, data: LeadUpdateInput): Promise<Lead> => {
|
||||
const lead = await leadsApi.update(id, data);
|
||||
await fetchLeads(page);
|
||||
return lead;
|
||||
}, [fetchLeads, page]);
|
||||
|
||||
const deleteLead = useCallback(async (id: string): Promise<void> => {
|
||||
await leadsApi.delete(id);
|
||||
await fetchLeads(page);
|
||||
}, [fetchLeads, page]);
|
||||
|
||||
const convertLead = useCallback(async (id: string, data: LeadConvertInput): Promise<{ opportunityId?: string; partnerId?: string }> => {
|
||||
const result = await leadsApi.convert(id, data);
|
||||
await fetchLeads(page);
|
||||
return result;
|
||||
}, [fetchLeads, page]);
|
||||
|
||||
const markLeadLost = useCallback(async (id: string, reason?: string): Promise<Lead> => {
|
||||
const lead = await leadsApi.markLost(id, reason);
|
||||
await fetchLeads(page);
|
||||
return lead;
|
||||
}, [fetchLeads, page]);
|
||||
|
||||
return {
|
||||
leads,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage: (p: number) => fetchLeads(p),
|
||||
refresh: () => fetchLeads(page),
|
||||
createLead,
|
||||
updateLead,
|
||||
deleteLead,
|
||||
convertLead,
|
||||
markLeadLost,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useOpportunities Hook
|
||||
// ============================================================================
|
||||
|
||||
export interface UseOpportunitiesOptions {
|
||||
status?: OpportunityStatus;
|
||||
stageId?: string;
|
||||
userId?: string;
|
||||
partnerId?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useOpportunities(options: UseOpportunitiesOptions = {}) {
|
||||
const [opportunities, setOpportunities] = useState<Opportunity[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const { status, stageId, userId, partnerId, search, limit = 20, autoFetch = true } = options;
|
||||
|
||||
const fetchOpportunities = useCallback(async (pageNum = 1) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const filters: OpportunityFilters = {
|
||||
status,
|
||||
stageId,
|
||||
userId,
|
||||
partnerId,
|
||||
search,
|
||||
page: pageNum,
|
||||
limit,
|
||||
};
|
||||
const response = await opportunitiesApi.getAll(filters);
|
||||
setOpportunities(response.data);
|
||||
setTotal(response.total);
|
||||
setPage(response.page);
|
||||
setTotalPages(response.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching opportunities'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [status, stageId, userId, partnerId, search, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchOpportunities(1);
|
||||
}
|
||||
}, [fetchOpportunities, autoFetch]);
|
||||
|
||||
const createOpportunity = useCallback(async (data: OpportunityCreateInput): Promise<Opportunity> => {
|
||||
const opp = await opportunitiesApi.create(data);
|
||||
await fetchOpportunities(page);
|
||||
return opp;
|
||||
}, [fetchOpportunities, page]);
|
||||
|
||||
const updateOpportunity = useCallback(async (id: string, data: OpportunityUpdateInput): Promise<Opportunity> => {
|
||||
const opp = await opportunitiesApi.update(id, data);
|
||||
await fetchOpportunities(page);
|
||||
return opp;
|
||||
}, [fetchOpportunities, page]);
|
||||
|
||||
const deleteOpportunity = useCallback(async (id: string): Promise<void> => {
|
||||
await opportunitiesApi.delete(id);
|
||||
await fetchOpportunities(page);
|
||||
}, [fetchOpportunities, page]);
|
||||
|
||||
const markWon = useCallback(async (id: string): Promise<Opportunity> => {
|
||||
const opp = await opportunitiesApi.markWon(id);
|
||||
await fetchOpportunities(page);
|
||||
return opp;
|
||||
}, [fetchOpportunities, page]);
|
||||
|
||||
const markLost = useCallback(async (id: string, reason?: string): Promise<Opportunity> => {
|
||||
const opp = await opportunitiesApi.markLost(id, reason);
|
||||
await fetchOpportunities(page);
|
||||
return opp;
|
||||
}, [fetchOpportunities, page]);
|
||||
|
||||
return {
|
||||
opportunities,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage: (p: number) => fetchOpportunities(p),
|
||||
refresh: () => fetchOpportunities(page),
|
||||
createOpportunity,
|
||||
updateOpportunity,
|
||||
deleteOpportunity,
|
||||
markWon,
|
||||
markLost,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// usePipeline Hook
|
||||
// ============================================================================
|
||||
|
||||
export function usePipeline(companyId?: string) {
|
||||
const [pipeline, setPipeline] = useState<Pipeline | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchPipeline = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await opportunitiesApi.getPipeline(companyId);
|
||||
setPipeline(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching pipeline'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [companyId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPipeline();
|
||||
}, [fetchPipeline]);
|
||||
|
||||
return {
|
||||
pipeline,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchPipeline,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useStages Hook
|
||||
// ============================================================================
|
||||
|
||||
export function useStages(type?: StageType, companyId?: string) {
|
||||
const [stages, setStages] = useState<Stage[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchStages = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await stagesApi.getAll(type, companyId);
|
||||
setStages(response.data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching stages'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [type, companyId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStages();
|
||||
}, [fetchStages]);
|
||||
|
||||
const createStage = useCallback(async (data: StageCreateInput): Promise<Stage> => {
|
||||
const stage = await stagesApi.create(data);
|
||||
await fetchStages();
|
||||
return stage;
|
||||
}, [fetchStages]);
|
||||
|
||||
const updateStage = useCallback(async (id: string, data: StageUpdateInput): Promise<Stage> => {
|
||||
const stage = await stagesApi.update(id, data);
|
||||
await fetchStages();
|
||||
return stage;
|
||||
}, [fetchStages]);
|
||||
|
||||
const deleteStage = useCallback(async (id: string): Promise<void> => {
|
||||
await stagesApi.delete(id);
|
||||
await fetchStages();
|
||||
}, [fetchStages]);
|
||||
|
||||
const reorderStages = useCallback(async (stageIds: string[]): Promise<void> => {
|
||||
await stagesApi.reorder(stageIds);
|
||||
await fetchStages();
|
||||
}, [fetchStages]);
|
||||
|
||||
return {
|
||||
stages,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchStages,
|
||||
createStage,
|
||||
updateStage,
|
||||
deleteStage,
|
||||
reorderStages,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useActivities Hook
|
||||
// ============================================================================
|
||||
|
||||
export interface UseActivitiesOptions {
|
||||
type?: ActivityType;
|
||||
state?: ActivityState;
|
||||
userId?: string;
|
||||
leadId?: string;
|
||||
opportunityId?: string;
|
||||
partnerId?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useActivities(options: UseActivitiesOptions = {}) {
|
||||
const [activities, setActivities] = useState<Activity[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const { type, state, userId, leadId, opportunityId, partnerId, search, limit = 20, autoFetch = true } = options;
|
||||
|
||||
const fetchActivities = useCallback(async (pageNum = 1) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const filters: ActivityFilters = {
|
||||
type,
|
||||
state,
|
||||
userId,
|
||||
leadId,
|
||||
opportunityId,
|
||||
partnerId,
|
||||
search,
|
||||
page: pageNum,
|
||||
limit,
|
||||
};
|
||||
const response = await activitiesApi.getAll(filters);
|
||||
setActivities(response.data);
|
||||
setTotal(response.total);
|
||||
setPage(response.page);
|
||||
setTotalPages(response.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching activities'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [type, state, userId, leadId, opportunityId, partnerId, search, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchActivities(1);
|
||||
}
|
||||
}, [fetchActivities, autoFetch]);
|
||||
|
||||
const createActivity = useCallback(async (data: ActivityCreateInput): Promise<Activity> => {
|
||||
const activity = await activitiesApi.create(data);
|
||||
await fetchActivities(page);
|
||||
return activity;
|
||||
}, [fetchActivities, page]);
|
||||
|
||||
const updateActivity = useCallback(async (id: string, data: ActivityUpdateInput): Promise<Activity> => {
|
||||
const activity = await activitiesApi.update(id, data);
|
||||
await fetchActivities(page);
|
||||
return activity;
|
||||
}, [fetchActivities, page]);
|
||||
|
||||
const deleteActivity = useCallback(async (id: string): Promise<void> => {
|
||||
await activitiesApi.delete(id);
|
||||
await fetchActivities(page);
|
||||
}, [fetchActivities, page]);
|
||||
|
||||
const markActivityDone = useCallback(async (id: string): Promise<Activity> => {
|
||||
const activity = await activitiesApi.markDone(id);
|
||||
await fetchActivities(page);
|
||||
return activity;
|
||||
}, [fetchActivities, page]);
|
||||
|
||||
const cancelActivity = useCallback(async (id: string): Promise<Activity> => {
|
||||
const activity = await activitiesApi.cancel(id);
|
||||
await fetchActivities(page);
|
||||
return activity;
|
||||
}, [fetchActivities, page]);
|
||||
|
||||
return {
|
||||
activities,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage: (p: number) => fetchActivities(p),
|
||||
refresh: () => fetchActivities(page),
|
||||
createActivity,
|
||||
updateActivity,
|
||||
deleteActivity,
|
||||
markActivityDone,
|
||||
cancelActivity,
|
||||
};
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export * from './api/crm.api';
|
||||
export * from './types';
|
||||
export * from './hooks';
|
||||
@ -1,300 +0,0 @@
|
||||
// CRM Types - Leads, Opportunities, Activities, Stages
|
||||
|
||||
// ============================================================================
|
||||
// Lead Types
|
||||
// ============================================================================
|
||||
|
||||
export type LeadStatus = 'new' | 'contacted' | 'qualified' | 'converted' | 'lost';
|
||||
export type LeadSource = 'website' | 'phone' | 'email' | 'referral' | 'social_media' | 'advertising' | 'event' | 'other';
|
||||
|
||||
export interface Lead {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
contactName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
website?: string;
|
||||
street?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
stageId?: string;
|
||||
stageName?: string;
|
||||
status: LeadStatus;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
teamId?: string;
|
||||
source?: LeadSource;
|
||||
medium?: string;
|
||||
campaign?: string;
|
||||
priority: number;
|
||||
probability: number;
|
||||
expectedRevenue?: number;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
convertedOpportunityId?: string;
|
||||
lostReason?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LeadCreateInput {
|
||||
name: string;
|
||||
contactName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
website?: string;
|
||||
street?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
stageId?: string;
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
source?: LeadSource;
|
||||
medium?: string;
|
||||
campaign?: string;
|
||||
priority?: number;
|
||||
probability?: number;
|
||||
expectedRevenue?: number;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface LeadUpdateInput extends Partial<LeadCreateInput> {
|
||||
status?: LeadStatus;
|
||||
lostReason?: string;
|
||||
}
|
||||
|
||||
export interface LeadFilters {
|
||||
companyId?: string;
|
||||
status?: LeadStatus;
|
||||
stageId?: string;
|
||||
userId?: string;
|
||||
source?: LeadSource;
|
||||
priority?: number;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface LeadConvertInput {
|
||||
createOpportunity?: boolean;
|
||||
opportunityName?: string;
|
||||
partnerId?: string;
|
||||
createPartner?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Opportunity Types
|
||||
// ============================================================================
|
||||
|
||||
export type OpportunityStatus = 'open' | 'won' | 'lost';
|
||||
|
||||
export interface Opportunity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
partnerId: string;
|
||||
partnerName?: string;
|
||||
stageId?: string;
|
||||
stageName?: string;
|
||||
status: OpportunityStatus;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
teamId?: string;
|
||||
priority: number;
|
||||
probability: number;
|
||||
expectedRevenue?: number;
|
||||
expectedCloseDate?: string;
|
||||
quotationId?: string;
|
||||
orderId?: string;
|
||||
leadId?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
lostReason?: string;
|
||||
wonDate?: string;
|
||||
lostDate?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface OpportunityCreateInput {
|
||||
name: string;
|
||||
partnerId: string;
|
||||
stageId?: string;
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
priority?: number;
|
||||
probability?: number;
|
||||
expectedRevenue?: number;
|
||||
expectedCloseDate?: string;
|
||||
leadId?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface OpportunityUpdateInput extends Partial<OpportunityCreateInput> {
|
||||
status?: OpportunityStatus;
|
||||
lostReason?: string;
|
||||
}
|
||||
|
||||
export interface OpportunityFilters {
|
||||
companyId?: string;
|
||||
status?: OpportunityStatus;
|
||||
stageId?: string;
|
||||
userId?: string;
|
||||
partnerId?: string;
|
||||
priority?: number;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stage Types
|
||||
// ============================================================================
|
||||
|
||||
export type StageType = 'lead' | 'opportunity';
|
||||
|
||||
export interface Stage {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId?: string;
|
||||
name: string;
|
||||
type: StageType;
|
||||
sequence: number;
|
||||
probability?: number;
|
||||
isFolded?: boolean;
|
||||
isWon?: boolean;
|
||||
requirements?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface StageCreateInput {
|
||||
name: string;
|
||||
type: StageType;
|
||||
sequence?: number;
|
||||
probability?: number;
|
||||
isFolded?: boolean;
|
||||
isWon?: boolean;
|
||||
requirements?: string;
|
||||
}
|
||||
|
||||
export interface StageUpdateInput extends Partial<StageCreateInput> {}
|
||||
|
||||
// ============================================================================
|
||||
// Activity Types
|
||||
// ============================================================================
|
||||
|
||||
export type ActivityType = 'call' | 'meeting' | 'email' | 'task' | 'note';
|
||||
export type ActivityState = 'planned' | 'done' | 'cancelled';
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
type: ActivityType;
|
||||
summary: string;
|
||||
description?: string;
|
||||
dateDeadline?: string;
|
||||
dateDone?: string;
|
||||
state: ActivityState;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
leadId?: string;
|
||||
opportunityId?: string;
|
||||
partnerId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ActivityCreateInput {
|
||||
type: ActivityType;
|
||||
summary: string;
|
||||
description?: string;
|
||||
dateDeadline?: string;
|
||||
leadId?: string;
|
||||
opportunityId?: string;
|
||||
partnerId?: string;
|
||||
}
|
||||
|
||||
export interface ActivityUpdateInput extends Partial<ActivityCreateInput> {
|
||||
state?: ActivityState;
|
||||
dateDone?: string;
|
||||
}
|
||||
|
||||
export interface ActivityFilters {
|
||||
companyId?: string;
|
||||
type?: ActivityType;
|
||||
state?: ActivityState;
|
||||
userId?: string;
|
||||
leadId?: string;
|
||||
opportunityId?: string;
|
||||
partnerId?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pipeline Types
|
||||
// ============================================================================
|
||||
|
||||
export interface PipelineStage {
|
||||
id: string;
|
||||
name: string;
|
||||
sequence: number;
|
||||
probability: number;
|
||||
count: number;
|
||||
totalRevenue: number;
|
||||
opportunities: Opportunity[];
|
||||
}
|
||||
|
||||
export interface Pipeline {
|
||||
stages: PipelineStage[];
|
||||
totals: {
|
||||
totalOpportunities: number;
|
||||
totalRevenue: number;
|
||||
weightedRevenue: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Response Types
|
||||
// ============================================================================
|
||||
|
||||
export interface LeadsResponse {
|
||||
data: Lead[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface OpportunitiesResponse {
|
||||
data: Opportunity[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface StagesResponse {
|
||||
data: Stage[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ActivitiesResponse {
|
||||
data: Activity[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './crm.types';
|
||||
@ -1,356 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
Account,
|
||||
CreateAccountDto,
|
||||
UpdateAccountDto,
|
||||
AccountFilters,
|
||||
AccountsResponse,
|
||||
AccountType,
|
||||
Journal,
|
||||
CreateJournalDto,
|
||||
UpdateJournalDto,
|
||||
JournalFilters,
|
||||
JournalsResponse,
|
||||
JournalEntry,
|
||||
CreateJournalEntryDto,
|
||||
UpdateJournalEntryDto,
|
||||
JournalEntryFilters,
|
||||
JournalEntriesResponse,
|
||||
FinancialInvoice,
|
||||
CreateInvoiceDto,
|
||||
UpdateInvoiceDto,
|
||||
InvoiceFilters,
|
||||
InvoicesResponse,
|
||||
Payment,
|
||||
CreatePaymentDto,
|
||||
UpdatePaymentDto,
|
||||
PaymentFilters,
|
||||
PaymentsResponse,
|
||||
} from '../types';
|
||||
|
||||
const ACCOUNTS_URL = '/api/v1/financial/accounts';
|
||||
const ACCOUNT_TYPES_URL = '/api/v1/financial/account-types';
|
||||
const JOURNALS_URL = '/api/v1/financial/journals';
|
||||
const ENTRIES_URL = '/api/v1/financial/journal-entries';
|
||||
const INVOICES_URL = '/api/v1/financial/invoices';
|
||||
const PAYMENTS_URL = '/api/v1/financial/payments';
|
||||
|
||||
export const financialApi = {
|
||||
// ==================== Account Types ====================
|
||||
|
||||
// Get all account types
|
||||
getAccountTypes: async (): Promise<AccountType[]> => {
|
||||
const response = await api.get<AccountType[]>(ACCOUNT_TYPES_URL);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ==================== Accounts ====================
|
||||
|
||||
// Get all accounts with filters
|
||||
getAccounts: async (filters?: AccountFilters): Promise<AccountsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.companyId) params.append('companyId', filters.companyId);
|
||||
if (filters?.accountTypeId) params.append('accountTypeId', filters.accountTypeId);
|
||||
if (filters?.parentId) params.append('parentId', filters.parentId);
|
||||
if (filters?.isReconcilable !== undefined) params.append('isReconcilable', String(filters.isReconcilable));
|
||||
if (filters?.isDeprecated !== undefined) params.append('isDeprecated', String(filters.isDeprecated));
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<AccountsResponse>(`${ACCOUNTS_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get account by ID
|
||||
getAccountById: async (id: string): Promise<Account> => {
|
||||
const response = await api.get<Account>(`${ACCOUNTS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create account
|
||||
createAccount: async (data: CreateAccountDto): Promise<Account> => {
|
||||
const response = await api.post<Account>(ACCOUNTS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update account
|
||||
updateAccount: async (id: string, data: UpdateAccountDto): Promise<Account> => {
|
||||
const response = await api.patch<Account>(`${ACCOUNTS_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete account
|
||||
deleteAccount: async (id: string): Promise<void> => {
|
||||
await api.delete(`${ACCOUNTS_URL}/${id}`);
|
||||
},
|
||||
|
||||
// Get accounts by type
|
||||
getAccountsByType: async (accountTypeId: string, filters?: Omit<AccountFilters, 'accountTypeId'>): Promise<AccountsResponse> => {
|
||||
return financialApi.getAccounts({ ...filters, accountTypeId });
|
||||
},
|
||||
|
||||
// Get child accounts
|
||||
getChildAccounts: async (parentId: string, filters?: Omit<AccountFilters, 'parentId'>): Promise<AccountsResponse> => {
|
||||
return financialApi.getAccounts({ ...filters, parentId });
|
||||
},
|
||||
|
||||
// ==================== Journals ====================
|
||||
|
||||
// Get all journals with filters
|
||||
getJournals: async (filters?: JournalFilters): Promise<JournalsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.companyId) params.append('companyId', filters.companyId);
|
||||
if (filters?.journalType) params.append('journalType', filters.journalType);
|
||||
if (filters?.active !== undefined) params.append('active', String(filters.active));
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<JournalsResponse>(`${JOURNALS_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get journal by ID
|
||||
getJournalById: async (id: string): Promise<Journal> => {
|
||||
const response = await api.get<Journal>(`${JOURNALS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create journal
|
||||
createJournal: async (data: CreateJournalDto): Promise<Journal> => {
|
||||
const response = await api.post<Journal>(JOURNALS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update journal
|
||||
updateJournal: async (id: string, data: UpdateJournalDto): Promise<Journal> => {
|
||||
const response = await api.patch<Journal>(`${JOURNALS_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete journal
|
||||
deleteJournal: async (id: string): Promise<void> => {
|
||||
await api.delete(`${JOURNALS_URL}/${id}`);
|
||||
},
|
||||
|
||||
// Get journals by type
|
||||
getJournalsByType: async (journalType: string, filters?: Omit<JournalFilters, 'journalType'>): Promise<JournalsResponse> => {
|
||||
return financialApi.getJournals({ ...filters, journalType: journalType as JournalFilters['journalType'] });
|
||||
},
|
||||
|
||||
// ==================== Journal Entries ====================
|
||||
|
||||
// Get all journal entries with filters
|
||||
getEntries: async (filters?: JournalEntryFilters): Promise<JournalEntriesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.companyId) params.append('companyId', filters.companyId);
|
||||
if (filters?.journalId) params.append('journalId', filters.journalId);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.dateFrom) params.append('dateFrom', filters.dateFrom);
|
||||
if (filters?.dateTo) params.append('dateTo', filters.dateTo);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<JournalEntriesResponse>(`${ENTRIES_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get journal entry by ID
|
||||
getEntryById: async (id: string): Promise<JournalEntry> => {
|
||||
const response = await api.get<JournalEntry>(`${ENTRIES_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create journal entry
|
||||
createEntry: async (data: CreateJournalEntryDto): Promise<JournalEntry> => {
|
||||
const response = await api.post<JournalEntry>(ENTRIES_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update journal entry
|
||||
updateEntry: async (id: string, data: UpdateJournalEntryDto): Promise<JournalEntry> => {
|
||||
const response = await api.patch<JournalEntry>(`${ENTRIES_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete journal entry
|
||||
deleteEntry: async (id: string): Promise<void> => {
|
||||
await api.delete(`${ENTRIES_URL}/${id}`);
|
||||
},
|
||||
|
||||
// Post journal entry
|
||||
postEntry: async (id: string): Promise<JournalEntry> => {
|
||||
const response = await api.post<JournalEntry>(`${ENTRIES_URL}/${id}/post`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel journal entry
|
||||
cancelEntry: async (id: string): Promise<JournalEntry> => {
|
||||
const response = await api.post<JournalEntry>(`${ENTRIES_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get draft entries
|
||||
getDraftEntries: async (filters?: Omit<JournalEntryFilters, 'status'>): Promise<JournalEntriesResponse> => {
|
||||
return financialApi.getEntries({ ...filters, status: 'draft' });
|
||||
},
|
||||
|
||||
// Get posted entries
|
||||
getPostedEntries: async (filters?: Omit<JournalEntryFilters, 'status'>): Promise<JournalEntriesResponse> => {
|
||||
return financialApi.getEntries({ ...filters, status: 'posted' });
|
||||
},
|
||||
|
||||
// ==================== Invoices ====================
|
||||
|
||||
// Get all invoices with filters
|
||||
getInvoices: async (filters?: InvoiceFilters): Promise<InvoicesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.companyId) params.append('companyId', filters.companyId);
|
||||
if (filters?.partnerId) params.append('partnerId', filters.partnerId);
|
||||
if (filters?.invoiceType) params.append('invoiceType', filters.invoiceType);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.dateFrom) params.append('dateFrom', filters.dateFrom);
|
||||
if (filters?.dateTo) params.append('dateTo', filters.dateTo);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<InvoicesResponse>(`${INVOICES_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get invoice by ID
|
||||
getInvoiceById: async (id: string): Promise<FinancialInvoice> => {
|
||||
const response = await api.get<FinancialInvoice>(`${INVOICES_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create invoice
|
||||
createInvoice: async (data: CreateInvoiceDto): Promise<FinancialInvoice> => {
|
||||
const response = await api.post<FinancialInvoice>(INVOICES_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update invoice
|
||||
updateInvoice: async (id: string, data: UpdateInvoiceDto): Promise<FinancialInvoice> => {
|
||||
const response = await api.patch<FinancialInvoice>(`${INVOICES_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete invoice
|
||||
deleteInvoice: async (id: string): Promise<void> => {
|
||||
await api.delete(`${INVOICES_URL}/${id}`);
|
||||
},
|
||||
|
||||
// Validate invoice (draft -> open)
|
||||
validateInvoice: async (id: string): Promise<FinancialInvoice> => {
|
||||
const response = await api.post<FinancialInvoice>(`${INVOICES_URL}/${id}/validate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel invoice
|
||||
cancelInvoice: async (id: string): Promise<FinancialInvoice> => {
|
||||
const response = await api.post<FinancialInvoice>(`${INVOICES_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get customer invoices
|
||||
getCustomerInvoices: async (filters?: Omit<InvoiceFilters, 'invoiceType'>): Promise<InvoicesResponse> => {
|
||||
return financialApi.getInvoices({ ...filters, invoiceType: 'customer' });
|
||||
},
|
||||
|
||||
// Get supplier invoices
|
||||
getSupplierInvoices: async (filters?: Omit<InvoiceFilters, 'invoiceType'>): Promise<InvoicesResponse> => {
|
||||
return financialApi.getInvoices({ ...filters, invoiceType: 'supplier' });
|
||||
},
|
||||
|
||||
// Get open invoices
|
||||
getOpenInvoices: async (filters?: Omit<InvoiceFilters, 'status'>): Promise<InvoicesResponse> => {
|
||||
return financialApi.getInvoices({ ...filters, status: 'open' });
|
||||
},
|
||||
|
||||
// Get invoices by partner
|
||||
getInvoicesByPartner: async (partnerId: string, filters?: Omit<InvoiceFilters, 'partnerId'>): Promise<InvoicesResponse> => {
|
||||
return financialApi.getInvoices({ ...filters, partnerId });
|
||||
},
|
||||
|
||||
// ==================== Payments ====================
|
||||
|
||||
// Get all payments with filters
|
||||
getPayments: async (filters?: PaymentFilters): Promise<PaymentsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.companyId) params.append('companyId', filters.companyId);
|
||||
if (filters?.partnerId) params.append('partnerId', filters.partnerId);
|
||||
if (filters?.paymentType) params.append('paymentType', filters.paymentType);
|
||||
if (filters?.paymentMethod) params.append('paymentMethod', filters.paymentMethod);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.dateFrom) params.append('dateFrom', filters.dateFrom);
|
||||
if (filters?.dateTo) params.append('dateTo', filters.dateTo);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<PaymentsResponse>(`${PAYMENTS_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get payment by ID
|
||||
getPaymentById: async (id: string): Promise<Payment> => {
|
||||
const response = await api.get<Payment>(`${PAYMENTS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create payment
|
||||
createPayment: async (data: CreatePaymentDto): Promise<Payment> => {
|
||||
const response = await api.post<Payment>(PAYMENTS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update payment
|
||||
updatePayment: async (id: string, data: UpdatePaymentDto): Promise<Payment> => {
|
||||
const response = await api.patch<Payment>(`${PAYMENTS_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete payment
|
||||
deletePayment: async (id: string): Promise<void> => {
|
||||
await api.delete(`${PAYMENTS_URL}/${id}`);
|
||||
},
|
||||
|
||||
// Post payment
|
||||
postPayment: async (id: string): Promise<Payment> => {
|
||||
const response = await api.post<Payment>(`${PAYMENTS_URL}/${id}/post`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel payment
|
||||
cancelPayment: async (id: string): Promise<Payment> => {
|
||||
const response = await api.post<Payment>(`${PAYMENTS_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get inbound payments (customer payments)
|
||||
getInboundPayments: async (filters?: Omit<PaymentFilters, 'paymentType'>): Promise<PaymentsResponse> => {
|
||||
return financialApi.getPayments({ ...filters, paymentType: 'inbound' });
|
||||
},
|
||||
|
||||
// Get outbound payments (supplier payments)
|
||||
getOutboundPayments: async (filters?: Omit<PaymentFilters, 'paymentType'>): Promise<PaymentsResponse> => {
|
||||
return financialApi.getPayments({ ...filters, paymentType: 'outbound' });
|
||||
},
|
||||
|
||||
// Get payments by partner
|
||||
getPaymentsByPartner: async (partnerId: string, filters?: Omit<PaymentFilters, 'partnerId'>): Promise<PaymentsResponse> => {
|
||||
return financialApi.getPayments({ ...filters, partnerId });
|
||||
},
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export { financialApi } from './financial.api';
|
||||
@ -1,20 +0,0 @@
|
||||
export {
|
||||
useAccountTypes,
|
||||
useAccounts,
|
||||
useAccount,
|
||||
useJournals,
|
||||
useJournalEntries,
|
||||
useJournalEntry,
|
||||
useInvoices,
|
||||
useInvoice,
|
||||
usePayments,
|
||||
usePayment,
|
||||
} from './useFinancial';
|
||||
|
||||
export type {
|
||||
UseAccountsOptions,
|
||||
UseJournalsOptions,
|
||||
UseJournalEntriesOptions,
|
||||
UseInvoicesOptions,
|
||||
UsePaymentsOptions,
|
||||
} from './useFinancial';
|
||||
@ -1,749 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export * from './api/financial.api';
|
||||
export * from './types';
|
||||
export * from './hooks';
|
||||
@ -1,400 +0,0 @@
|
||||
// Account Types
|
||||
export type AccountTypeEnum = 'asset' | 'liability' | 'equity' | 'income' | 'expense';
|
||||
|
||||
export interface AccountType {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
accountType: AccountTypeEnum;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
companyName?: string;
|
||||
code: string;
|
||||
name: string;
|
||||
accountTypeId: string;
|
||||
accountTypeName?: string;
|
||||
accountTypeCode?: string;
|
||||
parentId: string | null;
|
||||
parentName?: string;
|
||||
currencyId: string | null;
|
||||
currencyCode?: string;
|
||||
isReconcilable: boolean;
|
||||
isDeprecated: boolean;
|
||||
notes: string | null;
|
||||
balance?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateAccountDto {
|
||||
companyId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
accountTypeId: string;
|
||||
parentId?: string;
|
||||
currencyId?: string;
|
||||
isReconcilable?: boolean;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateAccountDto {
|
||||
name?: string;
|
||||
accountTypeId?: string;
|
||||
parentId?: string | null;
|
||||
currencyId?: string | null;
|
||||
isReconcilable?: boolean;
|
||||
isDeprecated?: boolean;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface AccountFilters {
|
||||
companyId?: string;
|
||||
accountTypeId?: string;
|
||||
parentId?: string;
|
||||
isReconcilable?: boolean;
|
||||
isDeprecated?: boolean;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface AccountsResponse {
|
||||
data: Account[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Journal Types
|
||||
export type JournalType = 'sale' | 'purchase' | 'cash' | 'bank' | 'general';
|
||||
|
||||
export interface Journal {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
companyName?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
journalType: JournalType;
|
||||
defaultAccountId: string | null;
|
||||
defaultAccountName?: string;
|
||||
sequenceId: string | null;
|
||||
currencyId: string | null;
|
||||
currencyCode?: string;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateJournalDto {
|
||||
companyId: string;
|
||||
name: string;
|
||||
code: string;
|
||||
journalType: JournalType;
|
||||
defaultAccountId?: string;
|
||||
currencyId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateJournalDto {
|
||||
name?: string;
|
||||
defaultAccountId?: string | null;
|
||||
currencyId?: string | null;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface JournalFilters {
|
||||
companyId?: string;
|
||||
journalType?: JournalType;
|
||||
active?: boolean;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface JournalsResponse {
|
||||
data: Journal[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Journal Entry Types
|
||||
export type EntryStatus = 'draft' | 'posted' | 'cancelled';
|
||||
|
||||
export interface JournalEntryLine {
|
||||
id: string;
|
||||
entryId: string;
|
||||
tenantId: string;
|
||||
accountId: string;
|
||||
accountCode?: string;
|
||||
accountName?: string;
|
||||
partnerId: string | null;
|
||||
partnerName?: string;
|
||||
debit: number;
|
||||
credit: number;
|
||||
description: string | null;
|
||||
ref: string | null;
|
||||
}
|
||||
|
||||
export interface JournalEntry {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
companyName?: string;
|
||||
journalId: string;
|
||||
journalName?: string;
|
||||
journalCode?: string;
|
||||
name: string;
|
||||
ref: string | null;
|
||||
date: string;
|
||||
status: EntryStatus;
|
||||
notes: string | null;
|
||||
fiscalPeriodId: string | null;
|
||||
lines?: JournalEntryLine[];
|
||||
totalDebit?: number;
|
||||
totalCredit?: number;
|
||||
createdAt: string;
|
||||
postedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CreateJournalEntryDto {
|
||||
companyId: string;
|
||||
journalId: string;
|
||||
ref?: string;
|
||||
date: string;
|
||||
notes?: string;
|
||||
lines: CreateJournalEntryLineDto[];
|
||||
}
|
||||
|
||||
export interface CreateJournalEntryLineDto {
|
||||
accountId: string;
|
||||
partnerId?: string;
|
||||
debit: number;
|
||||
credit: number;
|
||||
description?: string;
|
||||
ref?: string;
|
||||
}
|
||||
|
||||
export interface UpdateJournalEntryDto {
|
||||
ref?: string | null;
|
||||
date?: string;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface JournalEntryFilters {
|
||||
companyId?: string;
|
||||
journalId?: string;
|
||||
status?: EntryStatus;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface JournalEntriesResponse {
|
||||
data: JournalEntry[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Invoice Types
|
||||
export type InvoiceType = 'customer' | 'supplier';
|
||||
export type FinancialInvoiceStatus = 'draft' | 'open' | 'paid' | 'cancelled';
|
||||
|
||||
export interface InvoiceLine {
|
||||
id: string;
|
||||
invoiceId: string;
|
||||
productId: string | null;
|
||||
productName?: string;
|
||||
accountId: string;
|
||||
accountName?: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
priceUnit: number;
|
||||
discount: number;
|
||||
amountUntaxed: number;
|
||||
amountTax: number;
|
||||
amountTotal: number;
|
||||
}
|
||||
|
||||
export interface FinancialInvoice {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
companyName?: string;
|
||||
partnerId: string;
|
||||
partnerName?: string;
|
||||
invoiceType: InvoiceType;
|
||||
number: string | null;
|
||||
ref: string | null;
|
||||
invoiceDate: string;
|
||||
dueDate: string | null;
|
||||
currencyId: string;
|
||||
currencyCode?: string;
|
||||
amountUntaxed: number;
|
||||
amountTax: number;
|
||||
amountTotal: number;
|
||||
amountPaid: number;
|
||||
amountResidual: number;
|
||||
status: FinancialInvoiceStatus;
|
||||
paymentTermId: string | null;
|
||||
journalId: string | null;
|
||||
journalEntryId: string | null;
|
||||
notes: string | null;
|
||||
lines?: InvoiceLine[];
|
||||
createdAt: string;
|
||||
validatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CreateInvoiceDto {
|
||||
companyId: string;
|
||||
partnerId: string;
|
||||
invoiceType: InvoiceType;
|
||||
ref?: string;
|
||||
invoiceDate: string;
|
||||
dueDate?: string;
|
||||
currencyId: string;
|
||||
paymentTermId?: string;
|
||||
journalId?: string;
|
||||
notes?: string;
|
||||
lines?: CreateInvoiceLineDto[];
|
||||
}
|
||||
|
||||
export interface CreateInvoiceLineDto {
|
||||
productId?: string;
|
||||
accountId: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
priceUnit: number;
|
||||
discount?: number;
|
||||
}
|
||||
|
||||
export interface UpdateInvoiceDto {
|
||||
partnerId?: string;
|
||||
ref?: string | null;
|
||||
invoiceDate?: string;
|
||||
dueDate?: string | null;
|
||||
currencyId?: string;
|
||||
paymentTermId?: string | null;
|
||||
journalId?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface InvoiceFilters {
|
||||
companyId?: string;
|
||||
partnerId?: string;
|
||||
invoiceType?: InvoiceType;
|
||||
status?: FinancialInvoiceStatus;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface InvoicesResponse {
|
||||
data: FinancialInvoice[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Payment Types
|
||||
export type PaymentType = 'inbound' | 'outbound';
|
||||
export type PaymentMethod = 'cash' | 'bank_transfer' | 'check' | 'card' | 'other';
|
||||
export type PaymentStatus = 'draft' | 'posted' | 'reconciled' | 'cancelled';
|
||||
|
||||
export interface Payment {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
companyName?: string;
|
||||
partnerId: string;
|
||||
partnerName?: string;
|
||||
paymentType: PaymentType;
|
||||
paymentMethod: PaymentMethod;
|
||||
amount: number;
|
||||
currencyId: string;
|
||||
currencyCode?: string;
|
||||
paymentDate: string;
|
||||
ref: string | null;
|
||||
status: PaymentStatus;
|
||||
journalId: string;
|
||||
journalName?: string;
|
||||
journalEntryId: string | null;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
postedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CreatePaymentDto {
|
||||
companyId: string;
|
||||
partnerId: string;
|
||||
paymentType: PaymentType;
|
||||
paymentMethod: PaymentMethod;
|
||||
amount: number;
|
||||
currencyId: string;
|
||||
paymentDate: string;
|
||||
ref?: string;
|
||||
journalId: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdatePaymentDto {
|
||||
partnerId?: string;
|
||||
paymentMethod?: PaymentMethod;
|
||||
amount?: number;
|
||||
currencyId?: string;
|
||||
paymentDate?: string;
|
||||
ref?: string | null;
|
||||
journalId?: string;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface PaymentFilters {
|
||||
companyId?: string;
|
||||
partnerId?: string;
|
||||
paymentType?: PaymentType;
|
||||
paymentMethod?: PaymentMethod;
|
||||
status?: PaymentStatus;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface PaymentsResponse {
|
||||
data: Payment[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './financial.types';
|
||||
@ -1,394 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
StockLevel,
|
||||
StockMovement,
|
||||
StockSearchParams,
|
||||
MovementSearchParams,
|
||||
CreateStockMovementDto,
|
||||
AdjustStockDto,
|
||||
TransferStockDto,
|
||||
ReserveStockDto,
|
||||
StockLevelsResponse,
|
||||
MovementsResponse,
|
||||
Warehouse,
|
||||
CreateWarehouseDto,
|
||||
UpdateWarehouseDto,
|
||||
WarehousesResponse,
|
||||
Location,
|
||||
CreateLocationDto,
|
||||
UpdateLocationDto,
|
||||
LocationsResponse,
|
||||
InventoryCount,
|
||||
InventoryCountLine,
|
||||
CreateInventoryCountDto,
|
||||
InventoryCountsResponse,
|
||||
InventoryAdjustment,
|
||||
InventoryAdjustmentLine,
|
||||
CreateInventoryAdjustmentDto,
|
||||
InventoryAdjustmentsResponse,
|
||||
StockValuationLayer,
|
||||
StockValuationsResponse,
|
||||
ValuationSummaryResponse,
|
||||
} from '../types';
|
||||
|
||||
const STOCK_URL = '/api/v1/inventory/stock';
|
||||
const MOVEMENTS_URL = '/api/v1/inventory/movements';
|
||||
const WAREHOUSES_URL = '/api/v1/warehouses';
|
||||
const LOCATIONS_URL = '/api/v1/inventory/locations';
|
||||
const COUNTS_URL = '/api/v1/inventory/counts';
|
||||
const ADJUSTMENTS_URL = '/api/v1/inventory/adjustments';
|
||||
const VALUATIONS_URL = '/api/v1/inventory/valuations';
|
||||
|
||||
export const inventoryApi = {
|
||||
// ==================== Stock Levels ====================
|
||||
|
||||
// Get stock levels with filters
|
||||
getStockLevels: async (params?: StockSearchParams): Promise<StockLevelsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.productId) searchParams.append('productId', params.productId);
|
||||
if (params?.warehouseId) searchParams.append('warehouseId', params.warehouseId);
|
||||
if (params?.locationId) searchParams.append('locationId', params.locationId);
|
||||
if (params?.lotNumber) searchParams.append('lotNumber', params.lotNumber);
|
||||
if (params?.hasStock !== undefined) searchParams.append('hasStock', String(params.hasStock));
|
||||
if (params?.lowStock !== undefined) searchParams.append('lowStock', String(params.lowStock));
|
||||
if (params?.page) searchParams.append('page', String(params.page));
|
||||
if (params?.limit) searchParams.append('limit', String(params.limit));
|
||||
|
||||
const response = await api.get<StockLevelsResponse>(`${STOCK_URL}?${searchParams.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get stock by product
|
||||
getStockByProduct: async (productId: string): Promise<StockLevel[]> => {
|
||||
const response = await api.get<StockLevel[]>(`${STOCK_URL}/product/${productId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get stock by warehouse
|
||||
getStockByWarehouse: async (warehouseId: string): Promise<StockLevel[]> => {
|
||||
const response = await api.get<StockLevel[]>(`${STOCK_URL}/warehouse/${warehouseId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available stock for a product in a warehouse
|
||||
getAvailableStock: async (productId: string, warehouseId: string): Promise<number> => {
|
||||
const response = await api.get<{ available: number }>(`${STOCK_URL}/available/${productId}/${warehouseId}`);
|
||||
return response.data.available;
|
||||
},
|
||||
|
||||
// ==================== Stock Movements ====================
|
||||
|
||||
// Get movements with filters
|
||||
getMovements: async (params?: MovementSearchParams): Promise<MovementsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.movementType) searchParams.append('movementType', params.movementType);
|
||||
if (params?.productId) searchParams.append('productId', params.productId);
|
||||
if (params?.warehouseId) searchParams.append('warehouseId', params.warehouseId);
|
||||
if (params?.status) searchParams.append('status', params.status);
|
||||
if (params?.referenceType) searchParams.append('referenceType', params.referenceType);
|
||||
if (params?.referenceId) searchParams.append('referenceId', params.referenceId);
|
||||
if (params?.fromDate) searchParams.append('fromDate', params.fromDate);
|
||||
if (params?.toDate) searchParams.append('toDate', params.toDate);
|
||||
if (params?.page) searchParams.append('page', String(params.page));
|
||||
if (params?.limit) searchParams.append('limit', String(params.limit));
|
||||
|
||||
const response = await api.get<MovementsResponse>(`${MOVEMENTS_URL}?${searchParams.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get movement by ID
|
||||
getMovementById: async (id: string): Promise<StockMovement> => {
|
||||
const response = await api.get<StockMovement>(`${MOVEMENTS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create movement
|
||||
createMovement: async (data: CreateStockMovementDto): Promise<StockMovement> => {
|
||||
const response = await api.post<StockMovement>(MOVEMENTS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Confirm movement
|
||||
confirmMovement: async (id: string): Promise<StockMovement> => {
|
||||
const response = await api.post<StockMovement>(`${MOVEMENTS_URL}/${id}/confirm`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel movement
|
||||
cancelMovement: async (id: string): Promise<StockMovement> => {
|
||||
const response = await api.post<StockMovement>(`${MOVEMENTS_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ==================== Stock Operations ====================
|
||||
|
||||
// Adjust stock
|
||||
adjustStock: async (data: AdjustStockDto): Promise<StockMovement> => {
|
||||
const response = await api.post<StockMovement>(`${STOCK_URL}/adjust`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Transfer stock between warehouses
|
||||
transferStock: async (data: TransferStockDto): Promise<StockMovement> => {
|
||||
const response = await api.post<StockMovement>(`${STOCK_URL}/transfer`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reserve stock
|
||||
reserveStock: async (data: ReserveStockDto): Promise<{ success: boolean }> => {
|
||||
const response = await api.post<{ success: boolean }>(`${STOCK_URL}/reserve`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Release reservation
|
||||
releaseReservation: async (productId: string, warehouseId: string, quantity: number): Promise<{ success: boolean }> => {
|
||||
const response = await api.post<{ success: boolean }>(`${STOCK_URL}/release`, {
|
||||
productId,
|
||||
warehouseId,
|
||||
quantity,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ==================== Warehouses ====================
|
||||
|
||||
// Get all warehouses
|
||||
getWarehouses: async (page?: number, limit?: number): Promise<WarehousesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (page) params.append('page', String(page));
|
||||
if (limit) params.append('limit', String(limit));
|
||||
|
||||
const response = await api.get<WarehousesResponse>(`${WAREHOUSES_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get warehouse by ID
|
||||
getWarehouseById: async (id: string): Promise<Warehouse> => {
|
||||
const response = await api.get<Warehouse>(`${WAREHOUSES_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create warehouse
|
||||
createWarehouse: async (data: CreateWarehouseDto): Promise<Warehouse> => {
|
||||
const response = await api.post<Warehouse>(WAREHOUSES_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update warehouse
|
||||
updateWarehouse: async (id: string, data: UpdateWarehouseDto): Promise<Warehouse> => {
|
||||
const response = await api.patch<Warehouse>(`${WAREHOUSES_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete warehouse
|
||||
deleteWarehouse: async (id: string): Promise<void> => {
|
||||
await api.delete(`${WAREHOUSES_URL}/${id}`);
|
||||
},
|
||||
|
||||
// ==================== Locations ====================
|
||||
|
||||
// Get locations by warehouse
|
||||
getLocationsByWarehouse: async (warehouseId: string): Promise<LocationsResponse> => {
|
||||
const response = await api.get<LocationsResponse>(`${LOCATIONS_URL}?warehouseId=${warehouseId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get location by ID
|
||||
getLocationById: async (id: string): Promise<Location> => {
|
||||
const response = await api.get<Location>(`${LOCATIONS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create location
|
||||
createLocation: async (data: CreateLocationDto): Promise<Location> => {
|
||||
const response = await api.post<Location>(LOCATIONS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update location
|
||||
updateLocation: async (id: string, data: UpdateLocationDto): Promise<Location> => {
|
||||
const response = await api.patch<Location>(`${LOCATIONS_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete location
|
||||
deleteLocation: async (id: string): Promise<void> => {
|
||||
await api.delete(`${LOCATIONS_URL}/${id}`);
|
||||
},
|
||||
|
||||
// ==================== Inventory Counts ====================
|
||||
|
||||
// Get inventory counts
|
||||
getInventoryCounts: async (params?: {
|
||||
warehouseId?: string;
|
||||
status?: string;
|
||||
countType?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<InventoryCountsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.warehouseId) searchParams.append('warehouseId', params.warehouseId);
|
||||
if (params?.status) searchParams.append('status', params.status);
|
||||
if (params?.countType) searchParams.append('countType', params.countType);
|
||||
if (params?.page) searchParams.append('page', String(params.page));
|
||||
if (params?.limit) searchParams.append('limit', String(params.limit));
|
||||
|
||||
const response = await api.get<InventoryCountsResponse>(`${COUNTS_URL}?${searchParams.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get inventory count by ID
|
||||
getInventoryCountById: async (id: string): Promise<InventoryCount> => {
|
||||
const response = await api.get<InventoryCount>(`${COUNTS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get count lines
|
||||
getInventoryCountLines: async (countId: string): Promise<InventoryCountLine[]> => {
|
||||
const response = await api.get<InventoryCountLine[]>(`${COUNTS_URL}/${countId}/lines`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create inventory count
|
||||
createInventoryCount: async (data: CreateInventoryCountDto): Promise<InventoryCount> => {
|
||||
const response = await api.post<InventoryCount>(COUNTS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Start inventory count
|
||||
startInventoryCount: async (id: string): Promise<InventoryCount> => {
|
||||
const response = await api.post<InventoryCount>(`${COUNTS_URL}/${id}/start`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update count line
|
||||
updateCountLine: async (countId: string, lineId: string, data: { countedQuantity: number; notes?: string }): Promise<InventoryCountLine> => {
|
||||
const response = await api.patch<InventoryCountLine>(`${COUNTS_URL}/${countId}/lines/${lineId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Complete inventory count
|
||||
completeInventoryCount: async (id: string): Promise<InventoryCount> => {
|
||||
const response = await api.post<InventoryCount>(`${COUNTS_URL}/${id}/complete`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel inventory count
|
||||
cancelInventoryCount: async (id: string): Promise<InventoryCount> => {
|
||||
const response = await api.post<InventoryCount>(`${COUNTS_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ==================== Inventory Adjustments ====================
|
||||
|
||||
// Get inventory adjustments
|
||||
getInventoryAdjustments: async (params?: {
|
||||
locationId?: string;
|
||||
status?: string;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<InventoryAdjustmentsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.locationId) searchParams.append('locationId', params.locationId);
|
||||
if (params?.status) searchParams.append('status', params.status);
|
||||
if (params?.fromDate) searchParams.append('fromDate', params.fromDate);
|
||||
if (params?.toDate) searchParams.append('toDate', params.toDate);
|
||||
if (params?.page) searchParams.append('page', String(params.page));
|
||||
if (params?.limit) searchParams.append('limit', String(params.limit));
|
||||
|
||||
const response = await api.get<InventoryAdjustmentsResponse>(`${ADJUSTMENTS_URL}?${searchParams.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get adjustment by ID
|
||||
getInventoryAdjustmentById: async (id: string): Promise<InventoryAdjustment> => {
|
||||
const response = await api.get<InventoryAdjustment>(`${ADJUSTMENTS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get adjustment lines
|
||||
getInventoryAdjustmentLines: async (adjustmentId: string): Promise<InventoryAdjustmentLine[]> => {
|
||||
const response = await api.get<InventoryAdjustmentLine[]>(`${ADJUSTMENTS_URL}/${adjustmentId}/lines`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create inventory adjustment
|
||||
createInventoryAdjustment: async (data: CreateInventoryAdjustmentDto): Promise<InventoryAdjustment> => {
|
||||
const response = await api.post<InventoryAdjustment>(ADJUSTMENTS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Confirm adjustment
|
||||
confirmInventoryAdjustment: async (id: string): Promise<InventoryAdjustment> => {
|
||||
const response = await api.post<InventoryAdjustment>(`${ADJUSTMENTS_URL}/${id}/confirm`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Apply adjustment (done)
|
||||
applyInventoryAdjustment: async (id: string): Promise<InventoryAdjustment> => {
|
||||
const response = await api.post<InventoryAdjustment>(`${ADJUSTMENTS_URL}/${id}/apply`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel adjustment
|
||||
cancelInventoryAdjustment: async (id: string): Promise<InventoryAdjustment> => {
|
||||
const response = await api.post<InventoryAdjustment>(`${ADJUSTMENTS_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ==================== Stock Valuations ====================
|
||||
|
||||
// Get valuation layers
|
||||
getValuationLayers: async (params?: {
|
||||
productId?: string;
|
||||
warehouseId?: string;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
hasRemaining?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<StockValuationsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.productId) searchParams.append('productId', params.productId);
|
||||
if (params?.warehouseId) searchParams.append('warehouseId', params.warehouseId);
|
||||
if (params?.fromDate) searchParams.append('fromDate', params.fromDate);
|
||||
if (params?.toDate) searchParams.append('toDate', params.toDate);
|
||||
if (params?.hasRemaining !== undefined) searchParams.append('hasRemaining', String(params.hasRemaining));
|
||||
if (params?.page) searchParams.append('page', String(params.page));
|
||||
if (params?.limit) searchParams.append('limit', String(params.limit));
|
||||
|
||||
const response = await api.get<StockValuationsResponse>(`${VALUATIONS_URL}?${searchParams.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get valuation summary by product
|
||||
getValuationSummary: async (params?: {
|
||||
warehouseId?: string;
|
||||
asOfDate?: string;
|
||||
}): Promise<ValuationSummaryResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.warehouseId) searchParams.append('warehouseId', params.warehouseId);
|
||||
if (params?.asOfDate) searchParams.append('asOfDate', params.asOfDate);
|
||||
|
||||
const response = await api.get<ValuationSummaryResponse>(`${VALUATIONS_URL}/summary?${searchParams.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get valuation for specific product
|
||||
getProductValuation: async (productId: string): Promise<{
|
||||
totalQuantity: number;
|
||||
totalValue: number;
|
||||
averageCost: number;
|
||||
layers: StockValuationLayer[];
|
||||
}> => {
|
||||
const response = await api.get<{
|
||||
totalQuantity: number;
|
||||
totalValue: number;
|
||||
averageCost: number;
|
||||
layers: StockValuationLayer[];
|
||||
}>(`${VALUATIONS_URL}/product/${productId}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
export {
|
||||
useStockLevels,
|
||||
useMovements,
|
||||
useWarehouses,
|
||||
useLocations,
|
||||
useStockOperations,
|
||||
useInventoryCounts,
|
||||
useInventoryAdjustments,
|
||||
} from './useInventory';
|
||||
|
||||
export type {
|
||||
UseStockLevelsOptions,
|
||||
UseMovementsOptions,
|
||||
UseInventoryCountsOptions,
|
||||
UseInventoryAdjustmentsOptions,
|
||||
} from './useInventory';
|
||||
@ -1,632 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { inventoryApi } from '../api/inventory.api';
|
||||
import type {
|
||||
StockLevel,
|
||||
StockMovement,
|
||||
StockSearchParams,
|
||||
MovementSearchParams,
|
||||
CreateStockMovementDto,
|
||||
AdjustStockDto,
|
||||
TransferStockDto,
|
||||
Warehouse,
|
||||
CreateWarehouseDto,
|
||||
UpdateWarehouseDto,
|
||||
Location,
|
||||
CreateLocationDto,
|
||||
UpdateLocationDto,
|
||||
InventoryCount,
|
||||
CreateInventoryCountDto,
|
||||
CountType,
|
||||
CountStatus,
|
||||
InventoryAdjustment,
|
||||
CreateInventoryAdjustmentDto,
|
||||
AdjustmentStatus,
|
||||
} from '../types';
|
||||
|
||||
// ==================== Stock Levels Hook ====================
|
||||
|
||||
export interface UseStockLevelsOptions extends StockSearchParams {
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useStockLevels(options: UseStockLevelsOptions = {}) {
|
||||
const { autoFetch = true, ...params } = options;
|
||||
const [stockLevels, setStockLevels] = useState<StockLevel[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(params.page || 1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchStockLevels = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await inventoryApi.getStockLevels({ ...params, page });
|
||||
setStockLevels(response.data);
|
||||
setTotal(response.meta.total);
|
||||
setTotalPages(response.meta.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar niveles de stock');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [params.productId, params.warehouseId, params.locationId, params.lotNumber, params.hasStock, params.lowStock, params.limit, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchStockLevels();
|
||||
}
|
||||
}, [fetchStockLevels, autoFetch]);
|
||||
|
||||
return {
|
||||
stockLevels,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage,
|
||||
refresh: fetchStockLevels,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Stock Movements Hook ====================
|
||||
|
||||
export interface UseMovementsOptions extends MovementSearchParams {
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useMovements(options: UseMovementsOptions = {}) {
|
||||
const { autoFetch = true, ...params } = options;
|
||||
const [movements, setMovements] = useState<StockMovement[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(params.page || 1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchMovements = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await inventoryApi.getMovements({ ...params, page });
|
||||
setMovements(response.data);
|
||||
setTotal(response.meta.total);
|
||||
setTotalPages(response.meta.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar movimientos');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [params.movementType, params.productId, params.warehouseId, params.status, params.fromDate, params.toDate, params.limit, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchMovements();
|
||||
}
|
||||
}, [fetchMovements, autoFetch]);
|
||||
|
||||
const createMovement = async (data: CreateStockMovementDto) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newMovement = await inventoryApi.createMovement(data);
|
||||
await fetchMovements();
|
||||
return newMovement;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al crear movimiento');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmMovement = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.confirmMovement(id);
|
||||
await fetchMovements();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al confirmar movimiento');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelMovement = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.cancelMovement(id);
|
||||
await fetchMovements();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cancelar movimiento');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
movements,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage,
|
||||
refresh: fetchMovements,
|
||||
createMovement,
|
||||
confirmMovement,
|
||||
cancelMovement,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Warehouses Hook ====================
|
||||
|
||||
export function useWarehouses() {
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchWarehouses = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await inventoryApi.getWarehouses(page, 20);
|
||||
setWarehouses(response.data);
|
||||
setTotal(response.meta.total);
|
||||
setTotalPages(response.meta.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar almacenes');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWarehouses();
|
||||
}, [fetchWarehouses]);
|
||||
|
||||
const createWarehouse = async (data: CreateWarehouseDto) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newWarehouse = await inventoryApi.createWarehouse(data);
|
||||
await fetchWarehouses();
|
||||
return newWarehouse;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al crear almacén');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateWarehouse = async (id: string, data: UpdateWarehouseDto) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updated = await inventoryApi.updateWarehouse(id, data);
|
||||
await fetchWarehouses();
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al actualizar almacén');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteWarehouse = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.deleteWarehouse(id);
|
||||
await fetchWarehouses();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al eliminar almacén');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
warehouses,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage,
|
||||
refresh: fetchWarehouses,
|
||||
createWarehouse,
|
||||
updateWarehouse,
|
||||
deleteWarehouse,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Locations Hook ====================
|
||||
|
||||
export function useLocations(warehouseId?: string) {
|
||||
const [locations, setLocations] = useState<Location[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchLocations = useCallback(async () => {
|
||||
if (!warehouseId) {
|
||||
setLocations([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await inventoryApi.getLocationsByWarehouse(warehouseId);
|
||||
setLocations(response.data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar ubicaciones');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [warehouseId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLocations();
|
||||
}, [fetchLocations]);
|
||||
|
||||
const createLocation = async (data: CreateLocationDto) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newLocation = await inventoryApi.createLocation(data);
|
||||
await fetchLocations();
|
||||
return newLocation;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al crear ubicación');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateLocation = async (id: string, data: UpdateLocationDto) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updated = await inventoryApi.updateLocation(id, data);
|
||||
await fetchLocations();
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al actualizar ubicación');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteLocation = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.deleteLocation(id);
|
||||
await fetchLocations();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al eliminar ubicación');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
locations,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchLocations,
|
||||
createLocation,
|
||||
updateLocation,
|
||||
deleteLocation,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Stock Operations Hook ====================
|
||||
|
||||
export function useStockOperations() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const adjustStock = async (data: AdjustStockDto) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await inventoryApi.adjustStock(data);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Error al ajustar stock';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const transferStock = async (data: TransferStockDto) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await inventoryApi.transferStock(data);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Error al transferir stock';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reserveStock = async (productId: string, warehouseId: string, quantity: number, referenceType?: string, referenceId?: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await inventoryApi.reserveStock({
|
||||
productId,
|
||||
warehouseId,
|
||||
quantity,
|
||||
referenceType,
|
||||
referenceId,
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Error al reservar stock';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const releaseReservation = async (productId: string, warehouseId: string, quantity: number) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await inventoryApi.releaseReservation(productId, warehouseId, quantity);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Error al liberar reserva';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
adjustStock,
|
||||
transferStock,
|
||||
reserveStock,
|
||||
releaseReservation,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Inventory Counts Hook ====================
|
||||
|
||||
export interface UseInventoryCountsOptions {
|
||||
warehouseId?: string;
|
||||
status?: CountStatus;
|
||||
countType?: CountType;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useInventoryCounts(options: UseInventoryCountsOptions = {}) {
|
||||
const { autoFetch = true, ...params } = options;
|
||||
const [counts, setCounts] = useState<InventoryCount[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(params.page || 1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchCounts = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await inventoryApi.getInventoryCounts({ ...params, page });
|
||||
setCounts(response.data);
|
||||
setTotal(response.meta.total);
|
||||
setTotalPages(response.meta.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar conteos de inventario');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [params.warehouseId, params.status, params.countType, params.limit, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchCounts();
|
||||
}
|
||||
}, [fetchCounts, autoFetch]);
|
||||
|
||||
const createCount = async (data: CreateInventoryCountDto) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newCount = await inventoryApi.createInventoryCount(data);
|
||||
await fetchCounts();
|
||||
return newCount;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al crear conteo');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startCount = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.startInventoryCount(id);
|
||||
await fetchCounts();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al iniciar conteo');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const completeCount = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.completeInventoryCount(id);
|
||||
await fetchCounts();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al completar conteo');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelCount = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.cancelInventoryCount(id);
|
||||
await fetchCounts();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cancelar conteo');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
counts,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage,
|
||||
refresh: fetchCounts,
|
||||
createCount,
|
||||
startCount,
|
||||
completeCount,
|
||||
cancelCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Inventory Adjustments Hook ====================
|
||||
|
||||
export interface UseInventoryAdjustmentsOptions {
|
||||
locationId?: string;
|
||||
status?: AdjustmentStatus;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useInventoryAdjustments(options: UseInventoryAdjustmentsOptions = {}) {
|
||||
const { autoFetch = true, ...params } = options;
|
||||
const [adjustments, setAdjustments] = useState<InventoryAdjustment[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(params.page || 1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchAdjustments = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await inventoryApi.getInventoryAdjustments({ ...params, page });
|
||||
setAdjustments(response.data);
|
||||
setTotal(response.meta.total);
|
||||
setTotalPages(response.meta.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar ajustes de inventario');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [params.locationId, params.status, params.fromDate, params.toDate, params.limit, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchAdjustments();
|
||||
}
|
||||
}, [fetchAdjustments, autoFetch]);
|
||||
|
||||
const createAdjustment = async (data: CreateInventoryAdjustmentDto) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newAdjustment = await inventoryApi.createInventoryAdjustment(data);
|
||||
await fetchAdjustments();
|
||||
return newAdjustment;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al crear ajuste');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAdjustment = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.confirmInventoryAdjustment(id);
|
||||
await fetchAdjustments();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al confirmar ajuste');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyAdjustment = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.applyInventoryAdjustment(id);
|
||||
await fetchAdjustments();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al aplicar ajuste');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelAdjustment = async (id: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await inventoryApi.cancelInventoryAdjustment(id);
|
||||
await fetchAdjustments();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cancelar ajuste');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
adjustments,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage,
|
||||
refresh: fetchAdjustments,
|
||||
createAdjustment,
|
||||
confirmAdjustment,
|
||||
applyAdjustment,
|
||||
cancelAdjustment,
|
||||
};
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './api/inventory.api';
|
||||
export * from './types';
|
||||
@ -1 +0,0 @@
|
||||
export * from './inventory.types';
|
||||
@ -1,377 +0,0 @@
|
||||
export type MovementType = 'receipt' | 'shipment' | 'transfer' | 'adjustment' | 'return' | 'production' | 'consumption';
|
||||
export type MovementStatus = 'draft' | 'confirmed' | 'cancelled';
|
||||
|
||||
export interface StockLevel {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
productId: string;
|
||||
warehouseId: string;
|
||||
locationId?: string | null;
|
||||
lotNumber?: string | null;
|
||||
serialNumber?: string | null;
|
||||
quantityOnHand: number;
|
||||
quantityReserved: number;
|
||||
quantityAvailable: number;
|
||||
quantityIncoming: number;
|
||||
quantityOutgoing: number;
|
||||
unitCost?: number | null;
|
||||
totalCost?: number | null;
|
||||
lastMovementAt?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface StockMovement {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
movementNumber: string;
|
||||
movementType: MovementType;
|
||||
productId: string;
|
||||
sourceWarehouseId?: string | null;
|
||||
sourceLocationId?: string | null;
|
||||
destWarehouseId?: string | null;
|
||||
destLocationId?: string | null;
|
||||
quantity: number;
|
||||
unitCost?: number | null;
|
||||
totalCost?: number | null;
|
||||
lotNumber?: string | null;
|
||||
serialNumber?: string | null;
|
||||
expiryDate?: string | null;
|
||||
reason?: string | null;
|
||||
notes?: string | null;
|
||||
referenceType?: string | null;
|
||||
referenceId?: string | null;
|
||||
status: MovementStatus;
|
||||
confirmedAt?: string | null;
|
||||
confirmedBy?: string | null;
|
||||
createdBy?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface StockSearchParams {
|
||||
productId?: string;
|
||||
warehouseId?: string;
|
||||
locationId?: string;
|
||||
lotNumber?: string;
|
||||
hasStock?: boolean;
|
||||
lowStock?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface MovementSearchParams {
|
||||
movementType?: MovementType;
|
||||
productId?: string;
|
||||
warehouseId?: string;
|
||||
status?: MovementStatus;
|
||||
referenceType?: string;
|
||||
referenceId?: string;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface CreateStockMovementDto {
|
||||
movementType: MovementType;
|
||||
productId: string;
|
||||
sourceWarehouseId?: string;
|
||||
sourceLocationId?: string;
|
||||
destWarehouseId?: string;
|
||||
destLocationId?: string;
|
||||
quantity: number;
|
||||
unitCost?: number;
|
||||
lotNumber?: string;
|
||||
serialNumber?: string;
|
||||
expiryDate?: string;
|
||||
reason?: string;
|
||||
notes?: string;
|
||||
referenceType?: string;
|
||||
referenceId?: string;
|
||||
}
|
||||
|
||||
export interface AdjustStockDto {
|
||||
productId: string;
|
||||
warehouseId: string;
|
||||
locationId?: string;
|
||||
lotNumber?: string;
|
||||
serialNumber?: string;
|
||||
newQuantity: number;
|
||||
reason?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface TransferStockDto {
|
||||
productId: string;
|
||||
sourceWarehouseId: string;
|
||||
sourceLocationId?: string;
|
||||
destWarehouseId: string;
|
||||
destLocationId?: string;
|
||||
quantity: number;
|
||||
lotNumber?: string;
|
||||
serialNumber?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ReserveStockDto {
|
||||
productId: string;
|
||||
warehouseId: string;
|
||||
locationId?: string;
|
||||
lotNumber?: string;
|
||||
quantity: number;
|
||||
referenceType?: string;
|
||||
referenceId?: string;
|
||||
}
|
||||
|
||||
export interface StockLevelsResponse {
|
||||
data: StockLevel[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MovementsResponse {
|
||||
data: StockMovement[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Warehouse types
|
||||
export interface Warehouse {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
warehouseType: 'main' | 'transit' | 'customer' | 'supplier' | 'virtual';
|
||||
address?: Record<string, any> | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateWarehouseDto {
|
||||
code: string;
|
||||
name: string;
|
||||
warehouseType?: 'main' | 'transit' | 'customer' | 'supplier' | 'virtual';
|
||||
address?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface UpdateWarehouseDto {
|
||||
code?: string;
|
||||
name?: string;
|
||||
warehouseType?: 'main' | 'transit' | 'customer' | 'supplier' | 'virtual';
|
||||
address?: Record<string, any> | null;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface WarehousesResponse {
|
||||
data: Warehouse[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Location types
|
||||
export interface Location {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
warehouseId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
locationType: 'internal' | 'view' | 'supplier' | 'customer' | 'inventory' | 'production' | 'transit';
|
||||
parentId?: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateLocationDto {
|
||||
warehouseId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
locationType?: 'internal' | 'view' | 'supplier' | 'customer' | 'inventory' | 'production' | 'transit';
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateLocationDto {
|
||||
code?: string;
|
||||
name?: string;
|
||||
locationType?: 'internal' | 'view' | 'supplier' | 'customer' | 'inventory' | 'production' | 'transit';
|
||||
parentId?: string | null;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface LocationsResponse {
|
||||
data: Location[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Inventory Count types
|
||||
export type CountType = 'full' | 'partial' | 'cycle' | 'spot';
|
||||
export type CountStatus = 'draft' | 'in_progress' | 'completed' | 'cancelled';
|
||||
|
||||
export interface InventoryCount {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
warehouseId: string;
|
||||
countNumber: string;
|
||||
name?: string | null;
|
||||
countType: CountType;
|
||||
scheduledDate?: string | null;
|
||||
startedAt?: string | null;
|
||||
completedAt?: string | null;
|
||||
status: CountStatus;
|
||||
assignedTo?: string | null;
|
||||
notes?: string | null;
|
||||
createdAt: string;
|
||||
createdBy?: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface InventoryCountLine {
|
||||
id: string;
|
||||
countId: string;
|
||||
productId: string;
|
||||
locationId?: string | null;
|
||||
systemQuantity?: number | null;
|
||||
countedQuantity?: number | null;
|
||||
lotNumber?: string | null;
|
||||
serialNumber?: string | null;
|
||||
isCounted: boolean;
|
||||
countedAt?: string | null;
|
||||
countedBy?: string | null;
|
||||
notes?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateInventoryCountDto {
|
||||
warehouseId: string;
|
||||
name?: string;
|
||||
countType?: CountType;
|
||||
scheduledDate?: string;
|
||||
assignedTo?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface InventoryCountsResponse {
|
||||
data: InventoryCount[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Inventory Adjustment types
|
||||
export type AdjustmentStatus = 'draft' | 'confirmed' | 'done' | 'cancelled';
|
||||
|
||||
export interface InventoryAdjustment {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
locationId: string;
|
||||
date: string;
|
||||
status: AdjustmentStatus;
|
||||
notes?: string | null;
|
||||
createdAt: string;
|
||||
createdBy?: string | null;
|
||||
updatedAt?: string | null;
|
||||
updatedBy?: string | null;
|
||||
}
|
||||
|
||||
export interface InventoryAdjustmentLine {
|
||||
id: string;
|
||||
adjustmentId: string;
|
||||
tenantId: string;
|
||||
productId: string;
|
||||
locationId: string;
|
||||
lotId?: string | null;
|
||||
theoreticalQty: number;
|
||||
countedQty: number;
|
||||
differenceQty: number;
|
||||
uomId?: string | null;
|
||||
notes?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateInventoryAdjustmentDto {
|
||||
name: string;
|
||||
locationId: string;
|
||||
date?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface InventoryAdjustmentsResponse {
|
||||
data: InventoryAdjustment[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Stock Valuation types
|
||||
export interface StockValuationLayer {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
productId: string;
|
||||
companyId: string;
|
||||
quantity: number;
|
||||
unitCost: number;
|
||||
value: number;
|
||||
remainingQty: number;
|
||||
remainingValue: number;
|
||||
stockMoveId?: string | null;
|
||||
description?: string | null;
|
||||
accountMoveId?: string | null;
|
||||
journalEntryId?: string | null;
|
||||
createdAt: string;
|
||||
createdBy?: string | null;
|
||||
updatedAt?: string | null;
|
||||
updatedBy?: string | null;
|
||||
}
|
||||
|
||||
export interface ValuationSummary {
|
||||
productId: string;
|
||||
productName?: string;
|
||||
totalQuantity: number;
|
||||
totalValue: number;
|
||||
averageCost: number;
|
||||
layerCount: number;
|
||||
}
|
||||
|
||||
export interface StockValuationsResponse {
|
||||
data: StockValuationLayer[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ValuationSummaryResponse {
|
||||
data: ValuationSummary[];
|
||||
totalValue: number;
|
||||
totalProducts: number;
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
Invoice,
|
||||
CreateInvoiceDto,
|
||||
UpdateInvoiceDto,
|
||||
InvoiceFilters,
|
||||
InvoicesResponse,
|
||||
InvoiceStats,
|
||||
} from '../types';
|
||||
|
||||
const BASE_URL = '/api/v1/invoices';
|
||||
|
||||
export const invoicesApi = {
|
||||
// Get all invoices with filters
|
||||
getAll: async (filters?: InvoiceFilters): Promise<InvoicesResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.invoiceType) params.append('invoiceType', filters.invoiceType);
|
||||
if (filters?.invoiceContext) params.append('invoiceContext', filters.invoiceContext);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.partnerId) params.append('partnerId', filters.partnerId);
|
||||
if (filters?.dateFrom) params.append('dateFrom', filters.dateFrom);
|
||||
if (filters?.dateTo) params.append('dateTo', filters.dateTo);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<InvoicesResponse>(`${BASE_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get invoice by ID
|
||||
getById: async (id: string): Promise<Invoice> => {
|
||||
const response = await api.get<Invoice>(`${BASE_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create invoice
|
||||
create: async (data: CreateInvoiceDto): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(BASE_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update invoice
|
||||
update: async (id: string, data: UpdateInvoiceDto): Promise<Invoice> => {
|
||||
const response = await api.patch<Invoice>(`${BASE_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete invoice
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${BASE_URL}/${id}`);
|
||||
},
|
||||
|
||||
// Validate invoice (change status from draft to validated)
|
||||
validate: async (id: string): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/validate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Send invoice
|
||||
send: async (id: string): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/send`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Record payment
|
||||
recordPayment: async (id: string, data: {
|
||||
amount: number;
|
||||
paymentMethod: string;
|
||||
paymentReference?: string;
|
||||
paymentDate?: string;
|
||||
}): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/payment`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel invoice
|
||||
cancel: async (id: string, reason?: string): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/cancel`, { reason });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Void invoice
|
||||
void: async (id: string, reason?: string): Promise<Invoice> => {
|
||||
const response = await api.post<Invoice>(`${BASE_URL}/${id}/void`, { reason });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get invoice stats
|
||||
getStats: async (tenantId?: string): Promise<InvoiceStats> => {
|
||||
const params = tenantId ? `?tenantId=${tenantId}` : '';
|
||||
const response = await api.get<InvoiceStats>(`${BASE_URL}/stats${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Generate PDF
|
||||
generatePdf: async (id: string): Promise<Blob> => {
|
||||
const response = await api.get(`${BASE_URL}/${id}/pdf`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Download PDF
|
||||
downloadPdf: async (id: string, fileName?: string): Promise<void> => {
|
||||
const blob = await invoicesApi.generatePdf(id);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName || `invoice-${id}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
// Get sales invoices
|
||||
getSales: async (filters?: Omit<InvoiceFilters, 'invoiceType'>): Promise<InvoicesResponse> => {
|
||||
return invoicesApi.getAll({ ...filters, invoiceType: 'sale' });
|
||||
},
|
||||
|
||||
// Get purchase invoices
|
||||
getPurchases: async (filters?: Omit<InvoiceFilters, 'invoiceType'>): Promise<InvoicesResponse> => {
|
||||
return invoicesApi.getAll({ ...filters, invoiceType: 'purchase' });
|
||||
},
|
||||
|
||||
// Get draft invoices
|
||||
getDrafts: async (filters?: Omit<InvoiceFilters, 'status'>): Promise<InvoicesResponse> => {
|
||||
return invoicesApi.getAll({ ...filters, status: 'draft' });
|
||||
},
|
||||
|
||||
// Get overdue invoices
|
||||
getOverdue: async (filters?: Omit<InvoiceFilters, 'status'>): Promise<InvoicesResponse> => {
|
||||
return invoicesApi.getAll({ ...filters, status: 'overdue' });
|
||||
},
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './api/invoices.api';
|
||||
export * from './types';
|
||||
@ -1 +0,0 @@
|
||||
export * from './invoice.types';
|
||||
@ -1,189 +0,0 @@
|
||||
export type InvoiceType = 'sale' | 'purchase' | 'credit_note' | 'debit_note';
|
||||
export type InvoiceStatus = 'draft' | 'validated' | 'sent' | 'partial' | 'paid' | 'overdue' | 'void' | 'refunded' | 'cancelled' | 'voided';
|
||||
export type InvoiceContext = 'commercial' | 'saas';
|
||||
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
invoiceNumber: string;
|
||||
invoiceType: InvoiceType;
|
||||
invoiceContext: InvoiceContext;
|
||||
|
||||
// Commercial fields
|
||||
salesOrderId?: string | null;
|
||||
purchaseOrderId?: string | null;
|
||||
partnerId?: string | null;
|
||||
partnerName?: string | null;
|
||||
partnerTaxId?: string | null;
|
||||
|
||||
// SaaS fields
|
||||
subscriptionId?: string | null;
|
||||
periodStart?: string | null;
|
||||
periodEnd?: string | null;
|
||||
|
||||
// Billing info
|
||||
billingName?: string | null;
|
||||
billingEmail?: string | null;
|
||||
billingAddress?: Record<string, any> | null;
|
||||
taxId?: string | null;
|
||||
|
||||
// Dates
|
||||
invoiceDate: string;
|
||||
dueDate?: string | null;
|
||||
paymentDate?: string | null;
|
||||
paidAt?: string | null;
|
||||
|
||||
// Amounts
|
||||
currency: string;
|
||||
exchangeRate: number;
|
||||
subtotal: number;
|
||||
taxAmount: number;
|
||||
withholdingTax: number;
|
||||
discountAmount: number;
|
||||
total: number;
|
||||
amountPaid: number;
|
||||
amountDue?: number | null;
|
||||
paidAmount: number;
|
||||
|
||||
// Payment
|
||||
paymentTermDays: number;
|
||||
paymentMethod?: string | null;
|
||||
paymentReference?: string | null;
|
||||
|
||||
// Status
|
||||
status: InvoiceStatus;
|
||||
|
||||
// CFDI
|
||||
cfdiUuid?: string | null;
|
||||
cfdiStatus?: string | null;
|
||||
cfdiXml?: string | null;
|
||||
cfdiPdfUrl?: string | null;
|
||||
|
||||
// Notes
|
||||
notes?: string | null;
|
||||
internalNotes?: string | null;
|
||||
|
||||
// Audit
|
||||
createdAt: string;
|
||||
createdBy?: string | null;
|
||||
updatedAt: string;
|
||||
updatedBy?: string | null;
|
||||
deletedAt?: string | null;
|
||||
|
||||
// Relations
|
||||
items?: InvoiceItem[];
|
||||
}
|
||||
|
||||
export interface InvoiceItem {
|
||||
id: string;
|
||||
invoiceId: string;
|
||||
description: string;
|
||||
itemType: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
subtotal: number;
|
||||
profileCode?: string | null;
|
||||
platform?: string | null;
|
||||
periodStart?: string | null;
|
||||
periodEnd?: string | null;
|
||||
metadata?: Record<string, any>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateInvoiceDto {
|
||||
invoiceType?: InvoiceType;
|
||||
invoiceContext?: InvoiceContext;
|
||||
|
||||
// Commercial
|
||||
partnerId?: string;
|
||||
partnerName?: string;
|
||||
partnerTaxId?: string;
|
||||
salesOrderId?: string;
|
||||
purchaseOrderId?: string;
|
||||
|
||||
// SaaS
|
||||
subscriptionId?: string;
|
||||
periodStart?: string;
|
||||
periodEnd?: string;
|
||||
|
||||
// Billing
|
||||
billingName?: string;
|
||||
billingEmail?: string;
|
||||
billingAddress?: Record<string, any>;
|
||||
taxId?: string;
|
||||
|
||||
// Dates
|
||||
invoiceDate?: string;
|
||||
dueDate?: string;
|
||||
|
||||
// Amounts
|
||||
currency?: string;
|
||||
exchangeRate?: number;
|
||||
paymentTermDays?: number;
|
||||
|
||||
// Notes
|
||||
notes?: string;
|
||||
internalNotes?: string;
|
||||
|
||||
// Items
|
||||
items?: CreateInvoiceItemDto[];
|
||||
}
|
||||
|
||||
export interface CreateInvoiceItemDto {
|
||||
description: string;
|
||||
itemType?: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface UpdateInvoiceDto {
|
||||
partnerId?: string;
|
||||
partnerName?: string;
|
||||
partnerTaxId?: string;
|
||||
billingName?: string;
|
||||
billingEmail?: string;
|
||||
billingAddress?: Record<string, any>;
|
||||
taxId?: string;
|
||||
invoiceDate?: string;
|
||||
dueDate?: string | null;
|
||||
currency?: string;
|
||||
exchangeRate?: number;
|
||||
paymentTermDays?: number;
|
||||
paymentMethod?: string;
|
||||
paymentReference?: string;
|
||||
notes?: string | null;
|
||||
internalNotes?: string | null;
|
||||
}
|
||||
|
||||
export interface InvoiceFilters {
|
||||
search?: string;
|
||||
invoiceType?: InvoiceType;
|
||||
invoiceContext?: InvoiceContext;
|
||||
status?: InvoiceStatus;
|
||||
partnerId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface InvoicesResponse {
|
||||
data: Invoice[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InvoiceStats {
|
||||
total: number;
|
||||
byStatus: Record<InvoiceStatus, number>;
|
||||
totalRevenue: number;
|
||||
pendingAmount: number;
|
||||
overdueAmount: number;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './notifications.api';
|
||||
@ -1,148 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
NotificationChannel,
|
||||
NotificationTemplate,
|
||||
NotificationTemplateCreateInput,
|
||||
NotificationPreference,
|
||||
NotificationPreferenceUpdateInput,
|
||||
Notification,
|
||||
NotificationCreateInput,
|
||||
NotificationStatus,
|
||||
InAppNotification,
|
||||
InAppNotificationCreateInput,
|
||||
InAppNotificationsFilters,
|
||||
InAppNotificationsResponse,
|
||||
} from '../types';
|
||||
|
||||
const NOTIFICATIONS_BASE = '/api/v1/notifications';
|
||||
|
||||
// ============================================================================
|
||||
// Channels API
|
||||
// ============================================================================
|
||||
|
||||
export const channelsApi = {
|
||||
getAll: async (): Promise<NotificationChannel[]> => {
|
||||
const response = await api.get<NotificationChannel[]>(`${NOTIFICATIONS_BASE}/channels`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getByCode: async (code: string): Promise<NotificationChannel> => {
|
||||
const response = await api.get<NotificationChannel>(`${NOTIFICATIONS_BASE}/channels/${code}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Templates API
|
||||
// ============================================================================
|
||||
|
||||
export const templatesApi = {
|
||||
getAll: async (): Promise<NotificationTemplate[]> => {
|
||||
const response = await api.get<NotificationTemplate[]>(`${NOTIFICATIONS_BASE}/templates`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getByCode: async (code: string): Promise<NotificationTemplate> => {
|
||||
const response = await api.get<NotificationTemplate>(`${NOTIFICATIONS_BASE}/templates/${code}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: NotificationTemplateCreateInput): Promise<NotificationTemplate> => {
|
||||
const response = await api.post<NotificationTemplate>(`${NOTIFICATIONS_BASE}/templates`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: Partial<NotificationTemplateCreateInput>): Promise<NotificationTemplate> => {
|
||||
const response = await api.patch<NotificationTemplate>(`${NOTIFICATIONS_BASE}/templates/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${NOTIFICATIONS_BASE}/templates/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Preferences API
|
||||
// ============================================================================
|
||||
|
||||
export const preferencesApi = {
|
||||
get: async (): Promise<NotificationPreference> => {
|
||||
const response = await api.get<NotificationPreference>(`${NOTIFICATIONS_BASE}/preferences`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (data: NotificationPreferenceUpdateInput): Promise<NotificationPreference> => {
|
||||
const response = await api.patch<NotificationPreference>(`${NOTIFICATIONS_BASE}/preferences`, data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Notifications API
|
||||
// ============================================================================
|
||||
|
||||
export const notificationsApi = {
|
||||
create: async (data: NotificationCreateInput): Promise<Notification> => {
|
||||
const response = await api.post<Notification>(`${NOTIFICATIONS_BASE}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPending: async (limit = 50): Promise<Notification[]> => {
|
||||
const response = await api.get<Notification[]>(`${NOTIFICATIONS_BASE}/pending?limit=${limit}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateStatus: async (id: string, status: NotificationStatus, errorMessage?: string): Promise<Notification> => {
|
||||
const response = await api.patch<Notification>(`${NOTIFICATIONS_BASE}/${id}/status`, {
|
||||
status,
|
||||
errorMessage,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// In-App Notifications API
|
||||
// ============================================================================
|
||||
|
||||
export const inAppApi = {
|
||||
getAll: async (filters: InAppNotificationsFilters = {}): Promise<InAppNotificationsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.includeRead !== undefined) params.append('include_read', String(filters.includeRead));
|
||||
if (filters.category) params.append('category', filters.category);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<InAppNotificationsResponse>(`${NOTIFICATIONS_BASE}/in-app?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getUnreadCount: async (): Promise<number> => {
|
||||
const response = await api.get<{ count: number }>(`${NOTIFICATIONS_BASE}/in-app/unread-count`);
|
||||
return response.data.count;
|
||||
},
|
||||
|
||||
markAsRead: async (id: string): Promise<InAppNotification> => {
|
||||
const response = await api.post<InAppNotification>(`${NOTIFICATIONS_BASE}/in-app/${id}/read`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markAllAsRead: async (): Promise<void> => {
|
||||
await api.post(`${NOTIFICATIONS_BASE}/in-app/read-all`);
|
||||
},
|
||||
|
||||
create: async (data: InAppNotificationCreateInput): Promise<InAppNotification> => {
|
||||
const response = await api.post<InAppNotification>(`${NOTIFICATIONS_BASE}/in-app`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
archive: async (id: string): Promise<InAppNotification> => {
|
||||
const response = await api.post<InAppNotification>(`${NOTIFICATIONS_BASE}/in-app/${id}/archive`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${NOTIFICATIONS_BASE}/in-app/${id}`);
|
||||
},
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
export {
|
||||
useChannels,
|
||||
useTemplates,
|
||||
useNotificationPreferences,
|
||||
useInAppNotifications,
|
||||
useNotificationBell,
|
||||
} from './useNotifications';
|
||||
@ -1,356 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
channelsApi,
|
||||
templatesApi,
|
||||
preferencesApi,
|
||||
inAppApi,
|
||||
} from '../api';
|
||||
import type {
|
||||
NotificationChannel,
|
||||
NotificationTemplate,
|
||||
NotificationTemplateCreateInput,
|
||||
NotificationPreference,
|
||||
NotificationPreferenceUpdateInput,
|
||||
InAppNotification,
|
||||
InAppNotificationsFilters,
|
||||
} from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// Channels Hook
|
||||
// ============================================================================
|
||||
|
||||
interface UseChannelsReturn {
|
||||
channels: NotificationChannel[];
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refresh: () => Promise<void>;
|
||||
getByCode: (code: string) => Promise<NotificationChannel>;
|
||||
}
|
||||
|
||||
export function useChannels(): UseChannelsReturn {
|
||||
const [channels, setChannels] = useState<NotificationChannel[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchChannels = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await channelsApi.getAll();
|
||||
setChannels(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching channels'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchChannels();
|
||||
}, [fetchChannels]);
|
||||
|
||||
const getByCode = useCallback(async (code: string): Promise<NotificationChannel> => {
|
||||
return channelsApi.getByCode(code);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
channels,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchChannels,
|
||||
getByCode,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Templates Hook
|
||||
// ============================================================================
|
||||
|
||||
interface UseTemplatesReturn {
|
||||
templates: NotificationTemplate[];
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refresh: () => Promise<void>;
|
||||
getByCode: (code: string) => Promise<NotificationTemplate>;
|
||||
create: (data: NotificationTemplateCreateInput) => Promise<NotificationTemplate>;
|
||||
update: (id: string, data: Partial<NotificationTemplateCreateInput>) => Promise<NotificationTemplate>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useTemplates(): UseTemplatesReturn {
|
||||
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchTemplates = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await templatesApi.getAll();
|
||||
setTemplates(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching templates'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplates();
|
||||
}, [fetchTemplates]);
|
||||
|
||||
const getByCode = useCallback(async (code: string): Promise<NotificationTemplate> => {
|
||||
return templatesApi.getByCode(code);
|
||||
}, []);
|
||||
|
||||
const create = useCallback(async (data: NotificationTemplateCreateInput): Promise<NotificationTemplate> => {
|
||||
const result = await templatesApi.create(data);
|
||||
await fetchTemplates();
|
||||
return result;
|
||||
}, [fetchTemplates]);
|
||||
|
||||
const update = useCallback(async (id: string, data: Partial<NotificationTemplateCreateInput>): Promise<NotificationTemplate> => {
|
||||
const result = await templatesApi.update(id, data);
|
||||
await fetchTemplates();
|
||||
return result;
|
||||
}, [fetchTemplates]);
|
||||
|
||||
const remove = useCallback(async (id: string): Promise<void> => {
|
||||
await templatesApi.delete(id);
|
||||
await fetchTemplates();
|
||||
}, [fetchTemplates]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchTemplates,
|
||||
getByCode,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Preferences Hook
|
||||
// ============================================================================
|
||||
|
||||
interface UseNotificationPreferencesReturn {
|
||||
preferences: NotificationPreference | null;
|
||||
isLoading: boolean;
|
||||
isSaving: boolean;
|
||||
error: Error | null;
|
||||
refresh: () => Promise<void>;
|
||||
update: (data: NotificationPreferenceUpdateInput) => Promise<NotificationPreference>;
|
||||
}
|
||||
|
||||
export function useNotificationPreferences(): UseNotificationPreferencesReturn {
|
||||
const [preferences, setPreferences] = useState<NotificationPreference | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchPreferences = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await preferencesApi.get();
|
||||
setPreferences(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching preferences'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPreferences();
|
||||
}, [fetchPreferences]);
|
||||
|
||||
const update = useCallback(async (data: NotificationPreferenceUpdateInput): Promise<NotificationPreference> => {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await preferencesApi.update(data);
|
||||
setPreferences(result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Error updating preferences');
|
||||
setError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
preferences,
|
||||
isLoading,
|
||||
isSaving,
|
||||
error,
|
||||
refresh: fetchPreferences,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// In-App Notifications Hook
|
||||
// ============================================================================
|
||||
|
||||
interface UseInAppNotificationsOptions {
|
||||
initialFilters?: InAppNotificationsFilters;
|
||||
autoLoad?: boolean;
|
||||
pollInterval?: number;
|
||||
}
|
||||
|
||||
interface UseInAppNotificationsReturn {
|
||||
notifications: InAppNotification[];
|
||||
unreadCount: number;
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
filters: InAppNotificationsFilters;
|
||||
setFilters: (filters: InAppNotificationsFilters) => void;
|
||||
refresh: () => Promise<void>;
|
||||
markAsRead: (id: string) => Promise<void>;
|
||||
markAllAsRead: () => Promise<void>;
|
||||
archive: (id: string) => Promise<void>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useInAppNotifications(
|
||||
options: UseInAppNotificationsOptions = {}
|
||||
): UseInAppNotificationsReturn {
|
||||
const { initialFilters = {}, autoLoad = true, pollInterval } = options;
|
||||
|
||||
const [notifications, setNotifications] = useState<InAppNotification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [filters, setFilters] = useState<InAppNotificationsFilters>(initialFilters);
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await inAppApi.getAll(filters);
|
||||
setNotifications(response.data);
|
||||
setUnreadCount(response.unreadCount);
|
||||
setTotal(response.total);
|
||||
setPage(response.page);
|
||||
setTotalPages(response.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching notifications'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
const fetchUnreadCount = useCallback(async () => {
|
||||
try {
|
||||
const count = await inAppApi.getUnreadCount();
|
||||
setUnreadCount(count);
|
||||
} catch (err) {
|
||||
// Silently fail for unread count polling
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoLoad) {
|
||||
fetchNotifications();
|
||||
}
|
||||
}, [autoLoad, fetchNotifications]);
|
||||
|
||||
// Poll for unread count
|
||||
useEffect(() => {
|
||||
if (!pollInterval) return;
|
||||
|
||||
const interval = setInterval(fetchUnreadCount, pollInterval);
|
||||
return () => clearInterval(interval);
|
||||
}, [pollInterval, fetchUnreadCount]);
|
||||
|
||||
const markAsRead = useCallback(async (id: string): Promise<void> => {
|
||||
await inAppApi.markAsRead(id);
|
||||
setNotifications(prev =>
|
||||
prev.map(n => n.id === id ? { ...n, isRead: true, readAt: new Date().toISOString() } : n)
|
||||
);
|
||||
setUnreadCount(prev => Math.max(0, prev - 1));
|
||||
}, []);
|
||||
|
||||
const markAllAsRead = useCallback(async (): Promise<void> => {
|
||||
await inAppApi.markAllAsRead();
|
||||
setNotifications(prev =>
|
||||
prev.map(n => ({ ...n, isRead: true, readAt: new Date().toISOString() }))
|
||||
);
|
||||
setUnreadCount(0);
|
||||
}, []);
|
||||
|
||||
const archive = useCallback(async (id: string): Promise<void> => {
|
||||
await inAppApi.archive(id);
|
||||
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(async (id: string): Promise<void> => {
|
||||
await inAppApi.delete(id);
|
||||
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
unreadCount,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
filters,
|
||||
setFilters,
|
||||
refresh: fetchNotifications,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
archive,
|
||||
remove,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Notification Bell Hook (for header/navbar)
|
||||
// ============================================================================
|
||||
|
||||
interface UseNotificationBellReturn {
|
||||
unreadCount: number;
|
||||
recentNotifications: InAppNotification[];
|
||||
isLoading: boolean;
|
||||
refresh: () => Promise<void>;
|
||||
markAsRead: (id: string) => Promise<void>;
|
||||
markAllAsRead: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useNotificationBell(pollInterval = 30000): UseNotificationBellReturn {
|
||||
const {
|
||||
notifications: recentNotifications,
|
||||
unreadCount,
|
||||
isLoading,
|
||||
refresh,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
} = useInAppNotifications({
|
||||
initialFilters: { limit: 5, includeRead: false },
|
||||
pollInterval,
|
||||
});
|
||||
|
||||
return {
|
||||
unreadCount,
|
||||
recentNotifications,
|
||||
isLoading,
|
||||
refresh,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
};
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
// Notifications Feature - Barrel Export
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// API
|
||||
export * from './api';
|
||||
|
||||
// Hooks
|
||||
export * from './hooks';
|
||||
@ -1 +0,0 @@
|
||||
export * from './notifications.types';
|
||||
@ -1,293 +0,0 @@
|
||||
// Notifications Types - Channels, Templates, Preferences, In-App
|
||||
|
||||
// ============================================================================
|
||||
// Channel Types
|
||||
// ============================================================================
|
||||
|
||||
export type ChannelType = 'email' | 'sms' | 'push' | 'whatsapp' | 'in_app' | 'webhook';
|
||||
|
||||
export interface NotificationChannel {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
type: ChannelType;
|
||||
isActive: boolean;
|
||||
providerConfig?: Record<string, any>;
|
||||
rateLimitPerMinute?: number;
|
||||
rateLimitPerHour?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Template Types
|
||||
// ============================================================================
|
||||
|
||||
export interface NotificationTemplate {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
channelType: ChannelType;
|
||||
subject?: string;
|
||||
body: string;
|
||||
variables: string[];
|
||||
isActive: boolean;
|
||||
translations?: TemplateTranslation[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TemplateTranslation {
|
||||
id: string;
|
||||
templateId: string;
|
||||
locale: string;
|
||||
subject?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface NotificationTemplateCreateInput {
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
channelType: ChannelType;
|
||||
subject?: string;
|
||||
body: string;
|
||||
variables?: string[];
|
||||
}
|
||||
|
||||
export interface NotificationTemplateUpdateInput extends Partial<NotificationTemplateCreateInput> {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Preference Types
|
||||
// ============================================================================
|
||||
|
||||
export interface NotificationPreference {
|
||||
id: string;
|
||||
userId: string;
|
||||
tenantId: string;
|
||||
emailEnabled: boolean;
|
||||
smsEnabled: boolean;
|
||||
pushEnabled: boolean;
|
||||
whatsappEnabled: boolean;
|
||||
inAppEnabled: boolean;
|
||||
quietHoursStart?: string;
|
||||
quietHoursEnd?: string;
|
||||
digestEnabled: boolean;
|
||||
digestFrequency?: 'daily' | 'weekly';
|
||||
categoryPreferences?: Record<string, boolean>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface NotificationPreferenceUpdateInput {
|
||||
emailEnabled?: boolean;
|
||||
smsEnabled?: boolean;
|
||||
pushEnabled?: boolean;
|
||||
whatsappEnabled?: boolean;
|
||||
inAppEnabled?: boolean;
|
||||
quietHoursStart?: string;
|
||||
quietHoursEnd?: string;
|
||||
digestEnabled?: boolean;
|
||||
digestFrequency?: 'daily' | 'weekly';
|
||||
categoryPreferences?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Notification Types
|
||||
// ============================================================================
|
||||
|
||||
export type NotificationStatus =
|
||||
| 'pending'
|
||||
| 'queued'
|
||||
| 'sending'
|
||||
| 'sent'
|
||||
| 'delivered'
|
||||
| 'read'
|
||||
| 'failed'
|
||||
| 'cancelled';
|
||||
|
||||
export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
templateId?: string;
|
||||
channelType: ChannelType;
|
||||
recipient: string;
|
||||
subject?: string;
|
||||
body: string;
|
||||
status: NotificationStatus;
|
||||
priority: NotificationPriority;
|
||||
scheduledAt?: string;
|
||||
sentAt?: string;
|
||||
deliveredAt?: string;
|
||||
readAt?: string;
|
||||
failedAt?: string;
|
||||
retryCount: number;
|
||||
maxRetries: number;
|
||||
lastError?: string;
|
||||
metadata?: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface NotificationCreateInput {
|
||||
userId: string;
|
||||
templateCode?: string;
|
||||
channelType: ChannelType;
|
||||
recipient: string;
|
||||
subject?: string;
|
||||
body?: string;
|
||||
variables?: Record<string, any>;
|
||||
priority?: NotificationPriority;
|
||||
scheduledAt?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// In-App Notification Types
|
||||
// ============================================================================
|
||||
|
||||
export type InAppCategory =
|
||||
| 'system'
|
||||
| 'order'
|
||||
| 'payment'
|
||||
| 'inventory'
|
||||
| 'user'
|
||||
| 'alert'
|
||||
| 'reminder'
|
||||
| 'other';
|
||||
|
||||
export interface InAppNotification {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
message: string;
|
||||
category: InAppCategory;
|
||||
priority: NotificationPriority;
|
||||
isRead: boolean;
|
||||
isArchived: boolean;
|
||||
actionUrl?: string;
|
||||
actionLabel?: string;
|
||||
icon?: string;
|
||||
imageUrl?: string;
|
||||
metadata?: Record<string, any>;
|
||||
expiresAt?: string;
|
||||
readAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface InAppNotificationCreateInput {
|
||||
userId: string;
|
||||
title: string;
|
||||
message: string;
|
||||
category?: InAppCategory;
|
||||
priority?: NotificationPriority;
|
||||
actionUrl?: string;
|
||||
actionLabel?: string;
|
||||
icon?: string;
|
||||
imageUrl?: string;
|
||||
metadata?: Record<string, any>;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface InAppNotificationsFilters {
|
||||
includeRead?: boolean;
|
||||
category?: InAppCategory;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface InAppNotificationsResponse {
|
||||
data: InAppNotification[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Notification Types
|
||||
// ============================================================================
|
||||
|
||||
export type BatchStatus = 'draft' | 'scheduled' | 'sending' | 'completed' | 'failed' | 'cancelled';
|
||||
|
||||
export interface NotificationBatch {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
templateId: string;
|
||||
channelType: ChannelType;
|
||||
status: BatchStatus;
|
||||
audienceFilter?: Record<string, any>;
|
||||
scheduledAt?: string;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
totalCount: number;
|
||||
sentCount: number;
|
||||
deliveredCount: number;
|
||||
failedCount: number;
|
||||
readCount: number;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constants and Labels
|
||||
// ============================================================================
|
||||
|
||||
export const CHANNEL_TYPE_LABELS: Record<ChannelType, string> = {
|
||||
email: 'Correo electronico',
|
||||
sms: 'SMS',
|
||||
push: 'Notificacion push',
|
||||
whatsapp: 'WhatsApp',
|
||||
in_app: 'En la aplicacion',
|
||||
webhook: 'Webhook',
|
||||
};
|
||||
|
||||
export const NOTIFICATION_STATUS_LABELS: Record<NotificationStatus, string> = {
|
||||
pending: 'Pendiente',
|
||||
queued: 'En cola',
|
||||
sending: 'Enviando',
|
||||
sent: 'Enviado',
|
||||
delivered: 'Entregado',
|
||||
read: 'Leido',
|
||||
failed: 'Fallido',
|
||||
cancelled: 'Cancelado',
|
||||
};
|
||||
|
||||
export const NOTIFICATION_PRIORITY_LABELS: Record<NotificationPriority, string> = {
|
||||
low: 'Baja',
|
||||
normal: 'Normal',
|
||||
high: 'Alta',
|
||||
urgent: 'Urgente',
|
||||
};
|
||||
|
||||
export const IN_APP_CATEGORY_LABELS: Record<InAppCategory, string> = {
|
||||
system: 'Sistema',
|
||||
order: 'Pedidos',
|
||||
payment: 'Pagos',
|
||||
inventory: 'Inventario',
|
||||
user: 'Usuarios',
|
||||
alert: 'Alertas',
|
||||
reminder: 'Recordatorios',
|
||||
other: 'Otros',
|
||||
};
|
||||
|
||||
export const BATCH_STATUS_LABELS: Record<BatchStatus, string> = {
|
||||
draft: 'Borrador',
|
||||
scheduled: 'Programado',
|
||||
sending: 'Enviando',
|
||||
completed: 'Completado',
|
||||
failed: 'Fallido',
|
||||
cancelled: 'Cancelado',
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from './projects.api';
|
||||
@ -1,245 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
Project,
|
||||
ProjectCreateInput,
|
||||
ProjectUpdateInput,
|
||||
ProjectFilters,
|
||||
ProjectsResponse,
|
||||
ProjectStats,
|
||||
Task,
|
||||
TaskCreateInput,
|
||||
TaskUpdateInput,
|
||||
TaskFilters,
|
||||
TasksResponse,
|
||||
TaskBoard,
|
||||
TaskStage,
|
||||
TaskStageCreateInput,
|
||||
TaskStageUpdateInput,
|
||||
TaskStagesResponse,
|
||||
Timesheet,
|
||||
TimesheetCreateInput,
|
||||
TimesheetUpdateInput,
|
||||
TimesheetFilters,
|
||||
TimesheetsResponse,
|
||||
} from '../types';
|
||||
|
||||
const PROJECTS_BASE = '/api/projects';
|
||||
|
||||
// ============================================================================
|
||||
// Projects API
|
||||
// ============================================================================
|
||||
|
||||
export const projectsApi = {
|
||||
getAll: async (filters: ProjectFilters = {}): Promise<ProjectsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.companyId) params.append('company_id', filters.companyId);
|
||||
if (filters.managerId) params.append('manager_id', filters.managerId);
|
||||
if (filters.partnerId) params.append('partner_id', filters.partnerId);
|
||||
if (filters.status) params.append('status', filters.status);
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<ProjectsResponse>(`${PROJECTS_BASE}?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Project> => {
|
||||
const response = await api.get<Project>(`${PROJECTS_BASE}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: ProjectCreateInput): Promise<Project> => {
|
||||
const response = await api.post<Project>(PROJECTS_BASE, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: ProjectUpdateInput): Promise<Project> => {
|
||||
const response = await api.patch<Project>(`${PROJECTS_BASE}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${PROJECTS_BASE}/${id}`);
|
||||
},
|
||||
|
||||
getStats: async (id: string): Promise<ProjectStats> => {
|
||||
const response = await api.get<ProjectStats>(`${PROJECTS_BASE}/${id}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
activate: async (id: string): Promise<Project> => {
|
||||
const response = await api.post<Project>(`${PROJECTS_BASE}/${id}/activate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
complete: async (id: string): Promise<Project> => {
|
||||
const response = await api.post<Project>(`${PROJECTS_BASE}/${id}/complete`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
cancel: async (id: string): Promise<Project> => {
|
||||
const response = await api.post<Project>(`${PROJECTS_BASE}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
hold: async (id: string): Promise<Project> => {
|
||||
const response = await api.post<Project>(`${PROJECTS_BASE}/${id}/hold`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Tasks API
|
||||
// ============================================================================
|
||||
|
||||
export const tasksApi = {
|
||||
getAll: async (filters: TaskFilters = {}): Promise<TasksResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.projectId) params.append('project_id', filters.projectId);
|
||||
if (filters.assignedTo) params.append('assigned_to', filters.assignedTo);
|
||||
if (filters.status) params.append('status', filters.status);
|
||||
if (filters.priority) params.append('priority', filters.priority);
|
||||
if (filters.parentId) params.append('parent_id', filters.parentId);
|
||||
if (filters.stageId) params.append('stage_id', filters.stageId);
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<TasksResponse>(`${PROJECTS_BASE}/tasks?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Task> => {
|
||||
const response = await api.get<Task>(`${PROJECTS_BASE}/tasks/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: TaskCreateInput): Promise<Task> => {
|
||||
const response = await api.post<Task>(`${PROJECTS_BASE}/tasks`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: TaskUpdateInput): Promise<Task> => {
|
||||
const response = await api.patch<Task>(`${PROJECTS_BASE}/tasks/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${PROJECTS_BASE}/tasks/${id}`);
|
||||
},
|
||||
|
||||
getBoard: async (projectId: string): Promise<TaskBoard> => {
|
||||
const response = await api.get<TaskBoard>(`${PROJECTS_BASE}/${projectId}/board`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
moveToStage: async (taskId: string, stageId: string, sequence?: number): Promise<Task> => {
|
||||
const response = await api.post<Task>(`${PROJECTS_BASE}/tasks/${taskId}/move`, {
|
||||
stageId,
|
||||
sequence,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
start: async (id: string): Promise<Task> => {
|
||||
const response = await api.post<Task>(`${PROJECTS_BASE}/tasks/${id}/start`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
complete: async (id: string): Promise<Task> => {
|
||||
const response = await api.post<Task>(`${PROJECTS_BASE}/tasks/${id}/complete`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
cancel: async (id: string): Promise<Task> => {
|
||||
const response = await api.post<Task>(`${PROJECTS_BASE}/tasks/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Task Stages API
|
||||
// ============================================================================
|
||||
|
||||
export const taskStagesApi = {
|
||||
getAll: async (projectId?: string): Promise<TaskStagesResponse> => {
|
||||
const params = projectId ? `?project_id=${projectId}` : '';
|
||||
const response = await api.get<TaskStagesResponse>(`${PROJECTS_BASE}/stages${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<TaskStage> => {
|
||||
const response = await api.get<TaskStage>(`${PROJECTS_BASE}/stages/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: TaskStageCreateInput): Promise<TaskStage> => {
|
||||
const response = await api.post<TaskStage>(`${PROJECTS_BASE}/stages`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: TaskStageUpdateInput): Promise<TaskStage> => {
|
||||
const response = await api.patch<TaskStage>(`${PROJECTS_BASE}/stages/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${PROJECTS_BASE}/stages/${id}`);
|
||||
},
|
||||
|
||||
reorder: async (stageIds: string[]): Promise<void> => {
|
||||
await api.post(`${PROJECTS_BASE}/stages/reorder`, { stageIds });
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Timesheets API
|
||||
// ============================================================================
|
||||
|
||||
export const timesheetsApi = {
|
||||
getAll: async (filters: TimesheetFilters = {}): Promise<TimesheetsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.companyId) params.append('company_id', filters.companyId);
|
||||
if (filters.projectId) params.append('project_id', filters.projectId);
|
||||
if (filters.taskId) params.append('task_id', filters.taskId);
|
||||
if (filters.employeeId) params.append('employee_id', filters.employeeId);
|
||||
if (filters.dateFrom) params.append('date_from', filters.dateFrom);
|
||||
if (filters.dateTo) params.append('date_to', filters.dateTo);
|
||||
if (filters.isBillable !== undefined) params.append('is_billable', String(filters.isBillable));
|
||||
if (filters.isBilled !== undefined) params.append('is_billed', String(filters.isBilled));
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<TimesheetsResponse>(`${PROJECTS_BASE}/timesheets?${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Timesheet> => {
|
||||
const response = await api.get<Timesheet>(`${PROJECTS_BASE}/timesheets/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: TimesheetCreateInput): Promise<Timesheet> => {
|
||||
const response = await api.post<Timesheet>(`${PROJECTS_BASE}/timesheets`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: TimesheetUpdateInput): Promise<Timesheet> => {
|
||||
const response = await api.patch<Timesheet>(`${PROJECTS_BASE}/timesheets/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`${PROJECTS_BASE}/timesheets/${id}`);
|
||||
},
|
||||
|
||||
getByProject: async (projectId: string, filters: Omit<TimesheetFilters, 'projectId'> = {}): Promise<TimesheetsResponse> => {
|
||||
return timesheetsApi.getAll({ ...filters, projectId });
|
||||
},
|
||||
|
||||
getByEmployee: async (employeeId: string, filters: Omit<TimesheetFilters, 'employeeId'> = {}): Promise<TimesheetsResponse> => {
|
||||
return timesheetsApi.getAll({ ...filters, employeeId });
|
||||
},
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from './useProjects';
|
||||
@ -1,499 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { projectsApi, tasksApi, taskStagesApi, timesheetsApi } from '../api';
|
||||
import type {
|
||||
Project,
|
||||
ProjectCreateInput,
|
||||
ProjectUpdateInput,
|
||||
ProjectFilters,
|
||||
ProjectStatus,
|
||||
ProjectStats,
|
||||
Task,
|
||||
TaskCreateInput,
|
||||
TaskUpdateInput,
|
||||
TaskFilters,
|
||||
TaskStatus,
|
||||
TaskPriority,
|
||||
TaskBoard,
|
||||
TaskStage,
|
||||
TaskStageCreateInput,
|
||||
TaskStageUpdateInput,
|
||||
Timesheet,
|
||||
TimesheetCreateInput,
|
||||
TimesheetUpdateInput,
|
||||
TimesheetFilters,
|
||||
} from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// useProjects Hook
|
||||
// ============================================================================
|
||||
|
||||
export interface UseProjectsOptions {
|
||||
managerId?: string;
|
||||
partnerId?: string;
|
||||
status?: ProjectStatus;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useProjects(options: UseProjectsOptions = {}) {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const { managerId, partnerId, status, search, limit = 20, autoFetch = true } = options;
|
||||
|
||||
const fetchProjects = useCallback(async (pageNum = 1) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const filters: ProjectFilters = {
|
||||
managerId,
|
||||
partnerId,
|
||||
status,
|
||||
search,
|
||||
page: pageNum,
|
||||
limit,
|
||||
};
|
||||
const response = await projectsApi.getAll(filters);
|
||||
setProjects(response.data);
|
||||
setTotal(response.total);
|
||||
setPage(response.page);
|
||||
setTotalPages(response.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching projects'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [managerId, partnerId, status, search, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchProjects(1);
|
||||
}
|
||||
}, [fetchProjects, autoFetch]);
|
||||
|
||||
const createProject = useCallback(async (data: ProjectCreateInput): Promise<Project> => {
|
||||
const project = await projectsApi.create(data);
|
||||
await fetchProjects(page);
|
||||
return project;
|
||||
}, [fetchProjects, page]);
|
||||
|
||||
const updateProject = useCallback(async (id: string, data: ProjectUpdateInput): Promise<Project> => {
|
||||
const project = await projectsApi.update(id, data);
|
||||
await fetchProjects(page);
|
||||
return project;
|
||||
}, [fetchProjects, page]);
|
||||
|
||||
const deleteProject = useCallback(async (id: string): Promise<void> => {
|
||||
await projectsApi.delete(id);
|
||||
await fetchProjects(page);
|
||||
}, [fetchProjects, page]);
|
||||
|
||||
const activateProject = useCallback(async (id: string): Promise<Project> => {
|
||||
const project = await projectsApi.activate(id);
|
||||
await fetchProjects(page);
|
||||
return project;
|
||||
}, [fetchProjects, page]);
|
||||
|
||||
const completeProject = useCallback(async (id: string): Promise<Project> => {
|
||||
const project = await projectsApi.complete(id);
|
||||
await fetchProjects(page);
|
||||
return project;
|
||||
}, [fetchProjects, page]);
|
||||
|
||||
const cancelProject = useCallback(async (id: string): Promise<Project> => {
|
||||
const project = await projectsApi.cancel(id);
|
||||
await fetchProjects(page);
|
||||
return project;
|
||||
}, [fetchProjects, page]);
|
||||
|
||||
const holdProject = useCallback(async (id: string): Promise<Project> => {
|
||||
const project = await projectsApi.hold(id);
|
||||
await fetchProjects(page);
|
||||
return project;
|
||||
}, [fetchProjects, page]);
|
||||
|
||||
return {
|
||||
projects,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage: (p: number) => fetchProjects(p),
|
||||
refresh: () => fetchProjects(page),
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
activateProject,
|
||||
completeProject,
|
||||
cancelProject,
|
||||
holdProject,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useProject Hook (Single Project)
|
||||
// ============================================================================
|
||||
|
||||
export function useProject(projectId: string | null) {
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [stats, setStats] = useState<ProjectStats | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchProject = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [projectData, statsData] = await Promise.all([
|
||||
projectsApi.getById(projectId),
|
||||
projectsApi.getStats(projectId),
|
||||
]);
|
||||
setProject(projectData);
|
||||
setStats(statsData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching project'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProject();
|
||||
}, [fetchProject]);
|
||||
|
||||
return {
|
||||
project,
|
||||
stats,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchProject,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useTasks Hook
|
||||
// ============================================================================
|
||||
|
||||
export interface UseTasksOptions {
|
||||
projectId?: string;
|
||||
assignedTo?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: TaskPriority;
|
||||
parentId?: string;
|
||||
stageId?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useTasks(options: UseTasksOptions = {}) {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const { projectId, assignedTo, status, priority, parentId, stageId, search, limit = 20, autoFetch = true } = options;
|
||||
|
||||
const fetchTasks = useCallback(async (pageNum = 1) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const filters: TaskFilters = {
|
||||
projectId,
|
||||
assignedTo,
|
||||
status,
|
||||
priority,
|
||||
parentId,
|
||||
stageId,
|
||||
search,
|
||||
page: pageNum,
|
||||
limit,
|
||||
};
|
||||
const response = await tasksApi.getAll(filters);
|
||||
setTasks(response.data);
|
||||
setTotal(response.total);
|
||||
setPage(response.page);
|
||||
setTotalPages(response.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching tasks'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [projectId, assignedTo, status, priority, parentId, stageId, search, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchTasks(1);
|
||||
}
|
||||
}, [fetchTasks, autoFetch]);
|
||||
|
||||
const createTask = useCallback(async (data: TaskCreateInput): Promise<Task> => {
|
||||
const task = await tasksApi.create(data);
|
||||
await fetchTasks(page);
|
||||
return task;
|
||||
}, [fetchTasks, page]);
|
||||
|
||||
const updateTask = useCallback(async (id: string, data: TaskUpdateInput): Promise<Task> => {
|
||||
const task = await tasksApi.update(id, data);
|
||||
await fetchTasks(page);
|
||||
return task;
|
||||
}, [fetchTasks, page]);
|
||||
|
||||
const deleteTask = useCallback(async (id: string): Promise<void> => {
|
||||
await tasksApi.delete(id);
|
||||
await fetchTasks(page);
|
||||
}, [fetchTasks, page]);
|
||||
|
||||
const startTask = useCallback(async (id: string): Promise<Task> => {
|
||||
const task = await tasksApi.start(id);
|
||||
await fetchTasks(page);
|
||||
return task;
|
||||
}, [fetchTasks, page]);
|
||||
|
||||
const completeTask = useCallback(async (id: string): Promise<Task> => {
|
||||
const task = await tasksApi.complete(id);
|
||||
await fetchTasks(page);
|
||||
return task;
|
||||
}, [fetchTasks, page]);
|
||||
|
||||
const cancelTask = useCallback(async (id: string): Promise<Task> => {
|
||||
const task = await tasksApi.cancel(id);
|
||||
await fetchTasks(page);
|
||||
return task;
|
||||
}, [fetchTasks, page]);
|
||||
|
||||
const moveTask = useCallback(async (taskId: string, stageId: string, sequence?: number): Promise<Task> => {
|
||||
const task = await tasksApi.moveToStage(taskId, stageId, sequence);
|
||||
await fetchTasks(page);
|
||||
return task;
|
||||
}, [fetchTasks, page]);
|
||||
|
||||
return {
|
||||
tasks,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
setPage: (p: number) => fetchTasks(p),
|
||||
refresh: () => fetchTasks(page),
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
startTask,
|
||||
completeTask,
|
||||
cancelTask,
|
||||
moveTask,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useTaskBoard Hook
|
||||
// ============================================================================
|
||||
|
||||
export function useTaskBoard(projectId: string | null) {
|
||||
const [board, setBoard] = useState<TaskBoard | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchBoard = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await tasksApi.getBoard(projectId);
|
||||
setBoard(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching task board'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBoard();
|
||||
}, [fetchBoard]);
|
||||
|
||||
const moveTask = useCallback(async (taskId: string, stageId: string, sequence?: number): Promise<void> => {
|
||||
await tasksApi.moveToStage(taskId, stageId, sequence);
|
||||
await fetchBoard();
|
||||
}, [fetchBoard]);
|
||||
|
||||
return {
|
||||
board,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchBoard,
|
||||
moveTask,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useTaskStages Hook
|
||||
// ============================================================================
|
||||
|
||||
export function useTaskStages(projectId?: string) {
|
||||
const [stages, setStages] = useState<TaskStage[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchStages = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await taskStagesApi.getAll(projectId);
|
||||
setStages(response.data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching task stages'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStages();
|
||||
}, [fetchStages]);
|
||||
|
||||
const createStage = useCallback(async (data: TaskStageCreateInput): Promise<TaskStage> => {
|
||||
const stage = await taskStagesApi.create(data);
|
||||
await fetchStages();
|
||||
return stage;
|
||||
}, [fetchStages]);
|
||||
|
||||
const updateStage = useCallback(async (id: string, data: TaskStageUpdateInput): Promise<TaskStage> => {
|
||||
const stage = await taskStagesApi.update(id, data);
|
||||
await fetchStages();
|
||||
return stage;
|
||||
}, [fetchStages]);
|
||||
|
||||
const deleteStage = useCallback(async (id: string): Promise<void> => {
|
||||
await taskStagesApi.delete(id);
|
||||
await fetchStages();
|
||||
}, [fetchStages]);
|
||||
|
||||
const reorderStages = useCallback(async (stageIds: string[]): Promise<void> => {
|
||||
await taskStagesApi.reorder(stageIds);
|
||||
await fetchStages();
|
||||
}, [fetchStages]);
|
||||
|
||||
return {
|
||||
stages,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchStages,
|
||||
createStage,
|
||||
updateStage,
|
||||
deleteStage,
|
||||
reorderStages,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// useTimesheets Hook
|
||||
// ============================================================================
|
||||
|
||||
export interface UseTimesheetsOptions {
|
||||
projectId?: string;
|
||||
taskId?: string;
|
||||
employeeId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
isBillable?: boolean;
|
||||
isBilled?: boolean;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useTimesheets(options: UseTimesheetsOptions = {}) {
|
||||
const [timesheets, setTimesheets] = useState<Timesheet[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const { projectId, taskId, employeeId, dateFrom, dateTo, isBillable, isBilled, search, limit = 20, autoFetch = true } = options;
|
||||
|
||||
const fetchTimesheets = useCallback(async (pageNum = 1) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const filters: TimesheetFilters = {
|
||||
projectId,
|
||||
taskId,
|
||||
employeeId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
isBillable,
|
||||
isBilled,
|
||||
search,
|
||||
page: pageNum,
|
||||
limit,
|
||||
};
|
||||
const response = await timesheetsApi.getAll(filters);
|
||||
setTimesheets(response.data);
|
||||
setTotal(response.total);
|
||||
setPage(response.page);
|
||||
setTotalPages(response.totalPages);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Error fetching timesheets'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [projectId, taskId, employeeId, dateFrom, dateTo, isBillable, isBilled, search, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchTimesheets(1);
|
||||
}
|
||||
}, [fetchTimesheets, autoFetch]);
|
||||
|
||||
const createTimesheet = useCallback(async (data: TimesheetCreateInput): Promise<Timesheet> => {
|
||||
const timesheet = await timesheetsApi.create(data);
|
||||
await fetchTimesheets(page);
|
||||
return timesheet;
|
||||
}, [fetchTimesheets, page]);
|
||||
|
||||
const updateTimesheet = useCallback(async (id: string, data: TimesheetUpdateInput): Promise<Timesheet> => {
|
||||
const timesheet = await timesheetsApi.update(id, data);
|
||||
await fetchTimesheets(page);
|
||||
return timesheet;
|
||||
}, [fetchTimesheets, page]);
|
||||
|
||||
const deleteTimesheet = useCallback(async (id: string): Promise<void> => {
|
||||
await timesheetsApi.delete(id);
|
||||
await fetchTimesheets(page);
|
||||
}, [fetchTimesheets, page]);
|
||||
|
||||
// Calculate totals
|
||||
const totalHours = timesheets.reduce((sum, t) => sum + t.hours, 0);
|
||||
const billableHours = timesheets.filter(t => t.isBillable).reduce((sum, t) => sum + t.hours, 0);
|
||||
const billedHours = timesheets.filter(t => t.isBilled).reduce((sum, t) => sum + t.hours, 0);
|
||||
|
||||
return {
|
||||
timesheets,
|
||||
total,
|
||||
page,
|
||||
totalPages,
|
||||
isLoading,
|
||||
error,
|
||||
totalHours,
|
||||
billableHours,
|
||||
billedHours,
|
||||
setPage: (p: number) => fetchTimesheets(p),
|
||||
refresh: () => fetchTimesheets(page),
|
||||
createTimesheet,
|
||||
updateTimesheet,
|
||||
deleteTimesheet,
|
||||
};
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export * from './api/projects.api';
|
||||
export * from './types';
|
||||
export * from './hooks';
|
||||
@ -1 +0,0 @@
|
||||
export * from './projects.types';
|
||||
@ -1,262 +0,0 @@
|
||||
// Projects Types - Projects, Tasks, Timesheets
|
||||
|
||||
// ============================================================================
|
||||
// Project Types
|
||||
// ============================================================================
|
||||
|
||||
export type ProjectStatus = 'draft' | 'active' | 'completed' | 'cancelled' | 'on_hold';
|
||||
export type ProjectPrivacy = 'public' | 'private' | 'followers';
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
managerId?: string;
|
||||
managerName?: string;
|
||||
partnerId?: string;
|
||||
partnerName?: string;
|
||||
status: ProjectStatus;
|
||||
privacy: ProjectPrivacy;
|
||||
dateStart?: string;
|
||||
dateEnd?: string;
|
||||
plannedHours?: number;
|
||||
allowTimesheets: boolean;
|
||||
taskCount?: number;
|
||||
completedTaskCount?: number;
|
||||
totalHours?: number;
|
||||
tags?: string[];
|
||||
color?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectCreateInput {
|
||||
name: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
managerId?: string;
|
||||
partnerId?: string;
|
||||
status?: ProjectStatus;
|
||||
privacy?: ProjectPrivacy;
|
||||
dateStart?: string;
|
||||
dateEnd?: string;
|
||||
plannedHours?: number;
|
||||
allowTimesheets?: boolean;
|
||||
tags?: string[];
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface ProjectUpdateInput extends Partial<ProjectCreateInput> {}
|
||||
|
||||
export interface ProjectFilters {
|
||||
companyId?: string;
|
||||
managerId?: string;
|
||||
partnerId?: string;
|
||||
status?: ProjectStatus;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Task Types
|
||||
// ============================================================================
|
||||
|
||||
export type TaskStatus = 'todo' | 'in_progress' | 'review' | 'done' | 'cancelled';
|
||||
export type TaskPriority = 'low' | 'normal' | 'high' | 'urgent';
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
assignedTo?: string;
|
||||
assignedToName?: string;
|
||||
parentId?: string;
|
||||
parentName?: string;
|
||||
priority: TaskPriority;
|
||||
status: TaskStatus;
|
||||
dateDeadline?: string;
|
||||
dateStart?: string;
|
||||
dateEnd?: string;
|
||||
estimatedHours?: number;
|
||||
spentHours?: number;
|
||||
remainingHours?: number;
|
||||
sequence: number;
|
||||
stageId?: string;
|
||||
stageName?: string;
|
||||
tags?: string[];
|
||||
subtaskCount?: number;
|
||||
completedSubtaskCount?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TaskCreateInput {
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
assignedTo?: string;
|
||||
parentId?: string;
|
||||
priority?: TaskPriority;
|
||||
status?: TaskStatus;
|
||||
dateDeadline?: string;
|
||||
dateStart?: string;
|
||||
estimatedHours?: number;
|
||||
sequence?: number;
|
||||
stageId?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface TaskUpdateInput extends Partial<Omit<TaskCreateInput, 'projectId'>> {}
|
||||
|
||||
export interface TaskFilters {
|
||||
projectId?: string;
|
||||
assignedTo?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: TaskPriority;
|
||||
parentId?: string;
|
||||
stageId?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timesheet Types
|
||||
// ============================================================================
|
||||
|
||||
export interface Timesheet {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
taskId?: string;
|
||||
taskName?: string;
|
||||
employeeId: string;
|
||||
employeeName?: string;
|
||||
date: string;
|
||||
hours: number;
|
||||
description?: string;
|
||||
isBillable: boolean;
|
||||
isBilled?: boolean;
|
||||
invoiceLineId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TimesheetCreateInput {
|
||||
projectId: string;
|
||||
taskId?: string;
|
||||
employeeId: string;
|
||||
date: string;
|
||||
hours: number;
|
||||
description?: string;
|
||||
isBillable?: boolean;
|
||||
}
|
||||
|
||||
export interface TimesheetUpdateInput extends Partial<Omit<TimesheetCreateInput, 'projectId' | 'employeeId'>> {}
|
||||
|
||||
export interface TimesheetFilters {
|
||||
companyId?: string;
|
||||
projectId?: string;
|
||||
taskId?: string;
|
||||
employeeId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
isBillable?: boolean;
|
||||
isBilled?: boolean;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Task Stage Types
|
||||
// ============================================================================
|
||||
|
||||
export interface TaskStage {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
projectId?: string;
|
||||
name: string;
|
||||
sequence: number;
|
||||
isFolded?: boolean;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TaskStageCreateInput {
|
||||
projectId?: string;
|
||||
name: string;
|
||||
sequence?: number;
|
||||
isFolded?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TaskStageUpdateInput extends Partial<TaskStageCreateInput> {}
|
||||
|
||||
// ============================================================================
|
||||
// Project Stats Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ProjectStats {
|
||||
projectId: string;
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
inProgressTasks: number;
|
||||
overdueTasks: number;
|
||||
totalHours: number;
|
||||
billableHours: number;
|
||||
billedHours: number;
|
||||
completionPercentage: number;
|
||||
}
|
||||
|
||||
export interface TaskBoard {
|
||||
stages: {
|
||||
id: string;
|
||||
name: string;
|
||||
sequence: number;
|
||||
tasks: Task[];
|
||||
}[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Response Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ProjectsResponse {
|
||||
data: Project[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface TasksResponse {
|
||||
data: Task[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface TimesheetsResponse {
|
||||
data: Timesheet[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface TaskStagesResponse {
|
||||
data: TaskStage[];
|
||||
total: number;
|
||||
}
|
||||
@ -1,157 +0,0 @@
|
||||
import { api } from '@services/api/axios-instance';
|
||||
import type {
|
||||
PurchaseOrder,
|
||||
CreatePurchaseOrderDto,
|
||||
UpdatePurchaseOrderDto,
|
||||
PurchaseOrderFilters,
|
||||
PurchaseOrdersResponse,
|
||||
PurchaseReceipt,
|
||||
CreatePurchaseReceiptDto,
|
||||
PurchaseReceiptFilters,
|
||||
PurchaseReceiptsResponse,
|
||||
} from '../types';
|
||||
|
||||
const ORDERS_URL = '/api/v1/purchases/orders';
|
||||
const RECEIPTS_URL = '/api/v1/purchases/receipts';
|
||||
|
||||
export const purchasesApi = {
|
||||
// ==================== Purchase Orders ====================
|
||||
|
||||
// Get all purchase orders with filters
|
||||
getOrders: async (filters?: PurchaseOrderFilters): Promise<PurchaseOrdersResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.companyId) params.append('companyId', filters.companyId);
|
||||
if (filters?.partnerId) params.append('partnerId', filters.partnerId);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.dateFrom) params.append('dateFrom', filters.dateFrom);
|
||||
if (filters?.dateTo) params.append('dateTo', filters.dateTo);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
if (filters?.sortBy) params.append('sortBy', filters.sortBy);
|
||||
if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
|
||||
|
||||
const response = await api.get<PurchaseOrdersResponse>(`${ORDERS_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get purchase order by ID
|
||||
getOrderById: async (id: string): Promise<PurchaseOrder> => {
|
||||
const response = await api.get<PurchaseOrder>(`${ORDERS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create purchase order
|
||||
createOrder: async (data: CreatePurchaseOrderDto): Promise<PurchaseOrder> => {
|
||||
const response = await api.post<PurchaseOrder>(ORDERS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update purchase order
|
||||
updateOrder: async (id: string, data: UpdatePurchaseOrderDto): Promise<PurchaseOrder> => {
|
||||
const response = await api.patch<PurchaseOrder>(`${ORDERS_URL}/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete purchase order
|
||||
deleteOrder: async (id: string): Promise<void> => {
|
||||
await api.delete(`${ORDERS_URL}/${id}`);
|
||||
},
|
||||
|
||||
// ==================== Order Actions ====================
|
||||
|
||||
// Confirm purchase order
|
||||
confirmOrder: async (id: string): Promise<PurchaseOrder> => {
|
||||
const response = await api.post<PurchaseOrder>(`${ORDERS_URL}/${id}/confirm`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel purchase order
|
||||
cancelOrder: async (id: string): Promise<PurchaseOrder> => {
|
||||
const response = await api.post<PurchaseOrder>(`${ORDERS_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get draft orders
|
||||
getDrafts: async (filters?: Omit<PurchaseOrderFilters, 'status'>): Promise<PurchaseOrdersResponse> => {
|
||||
return purchasesApi.getOrders({ ...filters, status: 'draft' });
|
||||
},
|
||||
|
||||
// Get confirmed orders
|
||||
getConfirmed: async (filters?: Omit<PurchaseOrderFilters, 'status'>): Promise<PurchaseOrdersResponse> => {
|
||||
return purchasesApi.getOrders({ ...filters, status: 'confirmed' });
|
||||
},
|
||||
|
||||
// Get orders by supplier
|
||||
getBySupplier: async (partnerId: string, filters?: Omit<PurchaseOrderFilters, 'partnerId'>): Promise<PurchaseOrdersResponse> => {
|
||||
return purchasesApi.getOrders({ ...filters, partnerId });
|
||||
},
|
||||
|
||||
// ==================== Purchase Receipts ====================
|
||||
|
||||
// Get all receipts
|
||||
getReceipts: async (filters?: PurchaseReceiptFilters): Promise<PurchaseReceiptsResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.purchaseOrderId) params.append('purchaseOrderId', filters.purchaseOrderId);
|
||||
if (filters?.partnerId) params.append('partnerId', filters.partnerId);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.dateFrom) params.append('dateFrom', filters.dateFrom);
|
||||
if (filters?.dateTo) params.append('dateTo', filters.dateTo);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.limit) params.append('limit', String(filters.limit));
|
||||
|
||||
const response = await api.get<PurchaseReceiptsResponse>(`${RECEIPTS_URL}?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get receipt by ID
|
||||
getReceiptById: async (id: string): Promise<PurchaseReceipt> => {
|
||||
const response = await api.get<PurchaseReceipt>(`${RECEIPTS_URL}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create receipt (receive goods)
|
||||
createReceipt: async (data: CreatePurchaseReceiptDto): Promise<PurchaseReceipt> => {
|
||||
const response = await api.post<PurchaseReceipt>(RECEIPTS_URL, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Confirm receipt
|
||||
confirmReceipt: async (id: string): Promise<PurchaseReceipt> => {
|
||||
const response = await api.post<PurchaseReceipt>(`${RECEIPTS_URL}/${id}/confirm`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Cancel receipt
|
||||
cancelReceipt: async (id: string): Promise<PurchaseReceipt> => {
|
||||
const response = await api.post<PurchaseReceipt>(`${RECEIPTS_URL}/${id}/cancel`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get receipts by order
|
||||
getReceiptsByOrder: async (purchaseOrderId: string): Promise<PurchaseReceiptsResponse> => {
|
||||
return purchasesApi.getReceipts({ purchaseOrderId });
|
||||
},
|
||||
|
||||
// Generate purchase order PDF
|
||||
generateOrderPdf: async (id: string): Promise<Blob> => {
|
||||
const response = await api.get(`${ORDERS_URL}/${id}/pdf`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Download purchase order PDF
|
||||
downloadOrderPdf: async (id: string, fileName?: string): Promise<void> => {
|
||||
const blob = await purchasesApi.generateOrderPdf(id);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName || `purchase-order-${id}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
export {
|
||||
usePurchaseOrders,
|
||||
usePurchaseOrder,
|
||||
usePurchaseReceipts,
|
||||
usePurchaseReceipt,
|
||||
} from './usePurchases';
|
||||
|
||||
export type {
|
||||
UsePurchaseOrdersOptions,
|
||||
UsePurchaseReceiptsOptions,
|
||||
} from './usePurchases';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user