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
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
# Build output
|
# Build
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
|
||||||
# TypeScript build info
|
# Environment
|
||||||
*.tsbuildinfo
|
.env
|
||||||
tsconfig.tsbuildinfo
|
.env.local
|
||||||
tsconfig.node.tsbuildinfo
|
.env.*.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs/
|
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
tsconfig.node.tsbuildinfo
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
coverage/
|
coverage/
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
*.local
|
.cache/
|
||||||
.eslintcache
|
.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 PartnerCreatePage = lazy(() => import('@pages/partners/PartnerCreatePage'));
|
||||||
const PartnerEditPage = lazy(() => import('@pages/partners/PartnerEditPage'));
|
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 }) {
|
function LazyWrapper({ children }: { children: React.ReactNode }) {
|
||||||
return <Suspense fallback={<FullPageSpinner />}>{children}</Suspense>;
|
return <Suspense fallback={<FullPageSpinner />}>{children}</Suspense>;
|
||||||
}
|
}
|
||||||
@ -244,40 +224,11 @@ export const router = createBrowserRouter([
|
|||||||
</DashboardWrapper>
|
</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/*',
|
path: '/crm/*',
|
||||||
element: (
|
element: (
|
||||||
<DashboardWrapper>
|
<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>
|
</DashboardWrapper>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -289,110 +240,11 @@ export const router = createBrowserRouter([
|
|||||||
</DashboardWrapper>
|
</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/*',
|
path: '/settings/*',
|
||||||
element: (
|
element: (
|
||||||
<DashboardWrapper>
|
<DashboardWrapper>
|
||||||
<div className="text-center text-gray-500">Seccion de Configuracion - En desarrollo</div>
|
<div className="text-center text-gray-500">Configuración - 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>
|
|
||||||
</DashboardWrapper>
|
</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