diff --git a/src/features/inventory/api/inventory.api.ts b/src/features/inventory/api/inventory.api.ts new file mode 100644 index 0000000..a8aa8c2 --- /dev/null +++ b/src/features/inventory/api/inventory.api.ts @@ -0,0 +1,204 @@ +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, +} 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'; + +export const inventoryApi = { + // ==================== Stock Levels ==================== + + // Get stock levels with filters + getStockLevels: async (params?: StockSearchParams): Promise => { + 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(`${STOCK_URL}?${searchParams.toString()}`); + return response.data; + }, + + // Get stock by product + getStockByProduct: async (productId: string): Promise => { + const response = await api.get(`${STOCK_URL}/product/${productId}`); + return response.data; + }, + + // Get stock by warehouse + getStockByWarehouse: async (warehouseId: string): Promise => { + const response = await api.get(`${STOCK_URL}/warehouse/${warehouseId}`); + return response.data; + }, + + // Get available stock for a product in a warehouse + getAvailableStock: async (productId: string, warehouseId: string): Promise => { + 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 => { + 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(`${MOVEMENTS_URL}?${searchParams.toString()}`); + return response.data; + }, + + // Get movement by ID + getMovementById: async (id: string): Promise => { + const response = await api.get(`${MOVEMENTS_URL}/${id}`); + return response.data; + }, + + // Create movement + createMovement: async (data: CreateStockMovementDto): Promise => { + const response = await api.post(MOVEMENTS_URL, data); + return response.data; + }, + + // Confirm movement + confirmMovement: async (id: string): Promise => { + const response = await api.post(`${MOVEMENTS_URL}/${id}/confirm`); + return response.data; + }, + + // Cancel movement + cancelMovement: async (id: string): Promise => { + const response = await api.post(`${MOVEMENTS_URL}/${id}/cancel`); + return response.data; + }, + + // ==================== Stock Operations ==================== + + // Adjust stock + adjustStock: async (data: AdjustStockDto): Promise => { + const response = await api.post(`${STOCK_URL}/adjust`, data); + return response.data; + }, + + // Transfer stock between warehouses + transferStock: async (data: TransferStockDto): Promise => { + const response = await api.post(`${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 => { + const params = new URLSearchParams(); + if (page) params.append('page', String(page)); + if (limit) params.append('limit', String(limit)); + + const response = await api.get(`${WAREHOUSES_URL}?${params.toString()}`); + return response.data; + }, + + // Get warehouse by ID + getWarehouseById: async (id: string): Promise => { + const response = await api.get(`${WAREHOUSES_URL}/${id}`); + return response.data; + }, + + // Create warehouse + createWarehouse: async (data: CreateWarehouseDto): Promise => { + const response = await api.post(WAREHOUSES_URL, data); + return response.data; + }, + + // Update warehouse + updateWarehouse: async (id: string, data: UpdateWarehouseDto): Promise => { + const response = await api.patch(`${WAREHOUSES_URL}/${id}`, data); + return response.data; + }, + + // Delete warehouse + deleteWarehouse: async (id: string): Promise => { + await api.delete(`${WAREHOUSES_URL}/${id}`); + }, + + // ==================== Locations ==================== + + // Get locations by warehouse + getLocationsByWarehouse: async (warehouseId: string): Promise => { + const response = await api.get(`${LOCATIONS_URL}?warehouseId=${warehouseId}`); + return response.data; + }, + + // Get location by ID + getLocationById: async (id: string): Promise => { + const response = await api.get(`${LOCATIONS_URL}/${id}`); + return response.data; + }, + + // Create location + createLocation: async (data: CreateLocationDto): Promise => { + const response = await api.post(LOCATIONS_URL, data); + return response.data; + }, + + // Update location + updateLocation: async (id: string, data: UpdateLocationDto): Promise => { + const response = await api.patch(`${LOCATIONS_URL}/${id}`, data); + return response.data; + }, + + // Delete location + deleteLocation: async (id: string): Promise => { + await api.delete(`${LOCATIONS_URL}/${id}`); + }, +}; diff --git a/src/features/inventory/index.ts b/src/features/inventory/index.ts new file mode 100644 index 0000000..e7b74bc --- /dev/null +++ b/src/features/inventory/index.ts @@ -0,0 +1,2 @@ +export * from './api/inventory.api'; +export * from './types'; diff --git a/src/features/inventory/types/index.ts b/src/features/inventory/types/index.ts new file mode 100644 index 0000000..128dded --- /dev/null +++ b/src/features/inventory/types/index.ts @@ -0,0 +1 @@ +export * from './inventory.types'; diff --git a/src/features/inventory/types/inventory.types.ts b/src/features/inventory/types/inventory.types.ts new file mode 100644 index 0000000..d8b19e2 --- /dev/null +++ b/src/features/inventory/types/inventory.types.ts @@ -0,0 +1,223 @@ +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 | null; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export interface CreateWarehouseDto { + code: string; + name: string; + warehouseType?: 'main' | 'transit' | 'customer' | 'supplier' | 'virtual'; + address?: Record; +} + +export interface UpdateWarehouseDto { + code?: string; + name?: string; + warehouseType?: 'main' | 'transit' | 'customer' | 'supplier' | 'virtual'; + address?: Record | 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; + }; +} diff --git a/src/features/invoices/api/invoices.api.ts b/src/features/invoices/api/invoices.api.ts new file mode 100644 index 0000000..34ce07b --- /dev/null +++ b/src/features/invoices/api/invoices.api.ts @@ -0,0 +1,138 @@ +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 => { + 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(`${BASE_URL}?${params.toString()}`); + return response.data; + }, + + // Get invoice by ID + getById: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/${id}`); + return response.data; + }, + + // Create invoice + create: async (data: CreateInvoiceDto): Promise => { + const response = await api.post(BASE_URL, data); + return response.data; + }, + + // Update invoice + update: async (id: string, data: UpdateInvoiceDto): Promise => { + const response = await api.patch(`${BASE_URL}/${id}`, data); + return response.data; + }, + + // Delete invoice + delete: async (id: string): Promise => { + await api.delete(`${BASE_URL}/${id}`); + }, + + // Validate invoice (change status from draft to validated) + validate: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/validate`); + return response.data; + }, + + // Send invoice + send: async (id: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/send`); + return response.data; + }, + + // Record payment + recordPayment: async (id: string, data: { + amount: number; + paymentMethod: string; + paymentReference?: string; + paymentDate?: string; + }): Promise => { + const response = await api.post(`${BASE_URL}/${id}/payment`, data); + return response.data; + }, + + // Cancel invoice + cancel: async (id: string, reason?: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/cancel`, { reason }); + return response.data; + }, + + // Void invoice + void: async (id: string, reason?: string): Promise => { + const response = await api.post(`${BASE_URL}/${id}/void`, { reason }); + return response.data; + }, + + // Get invoice stats + getStats: async (tenantId?: string): Promise => { + const params = tenantId ? `?tenantId=${tenantId}` : ''; + const response = await api.get(`${BASE_URL}/stats${params}`); + return response.data; + }, + + // Generate PDF + generatePdf: async (id: string): Promise => { + const response = await api.get(`${BASE_URL}/${id}/pdf`, { + responseType: 'blob', + }); + return response.data; + }, + + // Download PDF + downloadPdf: async (id: string, fileName?: string): Promise => { + 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): Promise => { + return invoicesApi.getAll({ ...filters, invoiceType: 'sale' }); + }, + + // Get purchase invoices + getPurchases: async (filters?: Omit): Promise => { + return invoicesApi.getAll({ ...filters, invoiceType: 'purchase' }); + }, + + // Get draft invoices + getDrafts: async (filters?: Omit): Promise => { + return invoicesApi.getAll({ ...filters, status: 'draft' }); + }, + + // Get overdue invoices + getOverdue: async (filters?: Omit): Promise => { + return invoicesApi.getAll({ ...filters, status: 'overdue' }); + }, +}; diff --git a/src/features/invoices/index.ts b/src/features/invoices/index.ts new file mode 100644 index 0000000..e0ce1a6 --- /dev/null +++ b/src/features/invoices/index.ts @@ -0,0 +1,2 @@ +export * from './api/invoices.api'; +export * from './types'; diff --git a/src/features/invoices/types/index.ts b/src/features/invoices/types/index.ts new file mode 100644 index 0000000..77401dd --- /dev/null +++ b/src/features/invoices/types/index.ts @@ -0,0 +1 @@ +export * from './invoice.types'; diff --git a/src/features/invoices/types/invoice.types.ts b/src/features/invoices/types/invoice.types.ts new file mode 100644 index 0000000..d626a0f --- /dev/null +++ b/src/features/invoices/types/invoice.types.ts @@ -0,0 +1,189 @@ +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 | 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; + 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; + 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; +} + +export interface UpdateInvoiceDto { + partnerId?: string; + partnerName?: string; + partnerTaxId?: string; + billingName?: string; + billingEmail?: string; + billingAddress?: Record; + 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; + totalRevenue: number; + pendingAmount: number; + overdueAmount: number; +} diff --git a/src/features/purchases/api/purchases.api.ts b/src/features/purchases/api/purchases.api.ts new file mode 100644 index 0000000..64054d2 --- /dev/null +++ b/src/features/purchases/api/purchases.api.ts @@ -0,0 +1,157 @@ +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 => { + 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(`${ORDERS_URL}?${params.toString()}`); + return response.data; + }, + + // Get purchase order by ID + getOrderById: async (id: string): Promise => { + const response = await api.get(`${ORDERS_URL}/${id}`); + return response.data; + }, + + // Create purchase order + createOrder: async (data: CreatePurchaseOrderDto): Promise => { + const response = await api.post(ORDERS_URL, data); + return response.data; + }, + + // Update purchase order + updateOrder: async (id: string, data: UpdatePurchaseOrderDto): Promise => { + const response = await api.patch(`${ORDERS_URL}/${id}`, data); + return response.data; + }, + + // Delete purchase order + deleteOrder: async (id: string): Promise => { + await api.delete(`${ORDERS_URL}/${id}`); + }, + + // ==================== Order Actions ==================== + + // Confirm purchase order + confirmOrder: async (id: string): Promise => { + const response = await api.post(`${ORDERS_URL}/${id}/confirm`); + return response.data; + }, + + // Cancel purchase order + cancelOrder: async (id: string): Promise => { + const response = await api.post(`${ORDERS_URL}/${id}/cancel`); + return response.data; + }, + + // Get draft orders + getDrafts: async (filters?: Omit): Promise => { + return purchasesApi.getOrders({ ...filters, status: 'draft' }); + }, + + // Get confirmed orders + getConfirmed: async (filters?: Omit): Promise => { + return purchasesApi.getOrders({ ...filters, status: 'confirmed' }); + }, + + // Get orders by supplier + getBySupplier: async (partnerId: string, filters?: Omit): Promise => { + return purchasesApi.getOrders({ ...filters, partnerId }); + }, + + // ==================== Purchase Receipts ==================== + + // Get all receipts + getReceipts: async (filters?: PurchaseReceiptFilters): Promise => { + 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(`${RECEIPTS_URL}?${params.toString()}`); + return response.data; + }, + + // Get receipt by ID + getReceiptById: async (id: string): Promise => { + const response = await api.get(`${RECEIPTS_URL}/${id}`); + return response.data; + }, + + // Create receipt (receive goods) + createReceipt: async (data: CreatePurchaseReceiptDto): Promise => { + const response = await api.post(RECEIPTS_URL, data); + return response.data; + }, + + // Confirm receipt + confirmReceipt: async (id: string): Promise => { + const response = await api.post(`${RECEIPTS_URL}/${id}/confirm`); + return response.data; + }, + + // Cancel receipt + cancelReceipt: async (id: string): Promise => { + const response = await api.post(`${RECEIPTS_URL}/${id}/cancel`); + return response.data; + }, + + // Get receipts by order + getReceiptsByOrder: async (purchaseOrderId: string): Promise => { + return purchasesApi.getReceipts({ purchaseOrderId }); + }, + + // Generate purchase order PDF + generateOrderPdf: async (id: string): Promise => { + 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 => { + 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); + }, +}; diff --git a/src/features/purchases/index.ts b/src/features/purchases/index.ts new file mode 100644 index 0000000..a4b7077 --- /dev/null +++ b/src/features/purchases/index.ts @@ -0,0 +1,2 @@ +export * from './api/purchases.api'; +export * from './types'; diff --git a/src/features/purchases/types/index.ts b/src/features/purchases/types/index.ts new file mode 100644 index 0000000..f8fbad1 --- /dev/null +++ b/src/features/purchases/types/index.ts @@ -0,0 +1 @@ +export * from './purchase.types'; diff --git a/src/features/purchases/types/purchase.types.ts b/src/features/purchases/types/purchase.types.ts new file mode 100644 index 0000000..4c8774a --- /dev/null +++ b/src/features/purchases/types/purchase.types.ts @@ -0,0 +1,174 @@ +export type PurchaseOrderStatus = 'draft' | 'sent' | 'confirmed' | 'done' | 'cancelled'; + +export interface PurchaseOrderLine { + id: string; + orderId: string; + productId: string; + productName?: string; + productCode?: string; + description: string; + quantity: number; + qtyReceived: number; + qtyInvoiced: number; + uomId: string; + uomName?: string; + priceUnit: number; + discount: number; + amountUntaxed: number; + amountTax: number; + amountTotal: number; + expectedDate?: string | null; +} + +export interface PurchaseOrder { + id: string; + tenantId: string; + companyId: string; + companyName?: string; + name: string; + ref?: string | null; + partnerId: string; + partnerName?: string; + orderDate: string; + expectedDate?: string | null; + effectiveDate?: string | null; + currencyId: string; + currencyCode?: string; + paymentTermId?: string | null; + amountUntaxed: number; + amountTax: number; + amountTotal: number; + status: PurchaseOrderStatus; + receiptStatus?: string | null; + invoiceStatus?: string | null; + notes?: string | null; + lines?: PurchaseOrderLine[]; + createdAt: string; + confirmedAt?: string | null; +} + +export interface CreatePurchaseOrderLineDto { + productId: string; + description: string; + quantity: number; + uomId: string; + priceUnit: number; + discount?: number; + expectedDate?: string; +} + +export interface CreatePurchaseOrderDto { + companyId: string; + name: string; + ref?: string; + partnerId: string; + orderDate: string; + expectedDate?: string; + currencyId: string; + paymentTermId?: string; + notes?: string; + lines: CreatePurchaseOrderLineDto[]; +} + +export interface UpdatePurchaseOrderDto { + ref?: string | null; + partnerId?: string; + orderDate?: string; + expectedDate?: string | null; + currencyId?: string; + paymentTermId?: string | null; + notes?: string | null; + lines?: CreatePurchaseOrderLineDto[]; +} + +export interface PurchaseOrderFilters { + companyId?: string; + partnerId?: string; + status?: PurchaseOrderStatus; + dateFrom?: string; + dateTo?: string; + search?: string; + page?: number; + limit?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} + +export interface PurchaseOrdersResponse { + data: PurchaseOrder[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +} + +// Purchase Receipt types +export interface PurchaseReceiptLine { + id: string; + receiptId: string; + orderLineId?: string | null; + productId: string; + productName?: string; + description: string; + quantityOrdered: number; + quantityReceived: number; + uomId: string; + uomName?: string; + warehouseId: string; + locationId?: string | null; + lotNumber?: string | null; + serialNumber?: string | null; +} + +export interface PurchaseReceipt { + id: string; + tenantId: string; + companyId: string; + receiptNumber: string; + purchaseOrderId: string; + purchaseOrderName?: string; + partnerId: string; + partnerName?: string; + receiptDate: string; + status: 'draft' | 'done' | 'cancelled'; + notes?: string | null; + lines?: PurchaseReceiptLine[]; + createdAt: string; +} + +export interface CreatePurchaseReceiptDto { + purchaseOrderId: string; + receiptDate?: string; + warehouseId: string; + notes?: string; + lines: { + orderLineId: string; + quantityReceived: number; + locationId?: string; + lotNumber?: string; + serialNumber?: string; + }[]; +} + +export interface PurchaseReceiptFilters { + purchaseOrderId?: string; + partnerId?: string; + status?: string; + dateFrom?: string; + dateTo?: string; + search?: string; + page?: number; + limit?: number; +} + +export interface PurchaseReceiptsResponse { + data: PurchaseReceipt[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +} diff --git a/src/features/sales/api/sales.api.ts b/src/features/sales/api/sales.api.ts new file mode 100644 index 0000000..f5173b5 --- /dev/null +++ b/src/features/sales/api/sales.api.ts @@ -0,0 +1,194 @@ +import { api } from '@services/api/axios-instance'; +import type { + SalesOrder, + SalesOrderLine, + CreateSalesOrderDto, + UpdateSalesOrderDto, + CreateSalesOrderLineDto, + UpdateSalesOrderLineDto, + SalesOrderFilters, + SalesOrdersResponse, + Quotation, + CreateQuotationDto, + QuotationFilters, + QuotationsResponse, +} from '../types'; + +const ORDERS_URL = '/api/v1/sales/orders'; +const QUOTATIONS_URL = '/api/v1/sales/quotations'; + +export const salesApi = { + // ==================== Sales Orders ==================== + + // Get all sales orders with filters + getOrders: async (filters?: SalesOrderFilters): Promise => { + 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?.invoiceStatus) params.append('invoiceStatus', filters.invoiceStatus); + if (filters?.deliveryStatus) params.append('deliveryStatus', filters.deliveryStatus); + 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(`${ORDERS_URL}?${params.toString()}`); + return response.data; + }, + + // Get sales order by ID + getOrderById: async (id: string): Promise => { + const response = await api.get(`${ORDERS_URL}/${id}`); + return response.data; + }, + + // Create sales order + createOrder: async (data: CreateSalesOrderDto): Promise => { + const response = await api.post(ORDERS_URL, data); + return response.data; + }, + + // Update sales order + updateOrder: async (id: string, data: UpdateSalesOrderDto): Promise => { + const response = await api.patch(`${ORDERS_URL}/${id}`, data); + return response.data; + }, + + // Delete sales order + deleteOrder: async (id: string): Promise => { + await api.delete(`${ORDERS_URL}/${id}`); + }, + + // ==================== Order Lines ==================== + + // Add line to order + addLine: async (orderId: string, data: CreateSalesOrderLineDto): Promise => { + const response = await api.post(`${ORDERS_URL}/${orderId}/lines`, data); + return response.data; + }, + + // Update order line + updateLine: async (orderId: string, lineId: string, data: UpdateSalesOrderLineDto): Promise => { + const response = await api.patch(`${ORDERS_URL}/${orderId}/lines/${lineId}`, data); + return response.data; + }, + + // Remove order line + removeLine: async (orderId: string, lineId: string): Promise => { + await api.delete(`${ORDERS_URL}/${orderId}/lines/${lineId}`); + }, + + // ==================== Order Actions ==================== + + // Confirm order (send to customer) + confirmOrder: async (id: string): Promise => { + const response = await api.post(`${ORDERS_URL}/${id}/confirm`); + return response.data; + }, + + // Cancel order + cancelOrder: async (id: string): Promise => { + const response = await api.post(`${ORDERS_URL}/${id}/cancel`); + return response.data; + }, + + // Create invoice from order + createInvoice: async (id: string): Promise<{ orderId: string; invoiceId: string }> => { + const response = await api.post<{ orderId: string; invoiceId: string }>(`${ORDERS_URL}/${id}/invoice`); + return response.data; + }, + + // Get draft orders + getDrafts: async (filters?: Omit): Promise => { + return salesApi.getOrders({ ...filters, status: 'draft' }); + }, + + // Get confirmed orders + getConfirmed: async (filters?: Omit): Promise => { + return salesApi.getOrders({ ...filters, status: 'sale' }); + }, + + // Get orders by customer + getByCustomer: async (partnerId: string, filters?: Omit): Promise => { + return salesApi.getOrders({ ...filters, partnerId }); + }, + + // ==================== Quotations ==================== + + // Get all quotations + getQuotations: async (filters?: QuotationFilters): Promise => { + 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)); + + const response = await api.get(`${QUOTATIONS_URL}?${params.toString()}`); + return response.data; + }, + + // Get quotation by ID + getQuotationById: async (id: string): Promise => { + const response = await api.get(`${QUOTATIONS_URL}/${id}`); + return response.data; + }, + + // Create quotation + createQuotation: async (data: CreateQuotationDto): Promise => { + const response = await api.post(QUOTATIONS_URL, data); + return response.data; + }, + + // Send quotation + sendQuotation: async (id: string): Promise => { + const response = await api.post(`${QUOTATIONS_URL}/${id}/send`); + return response.data; + }, + + // Accept quotation + acceptQuotation: async (id: string): Promise => { + const response = await api.post(`${QUOTATIONS_URL}/${id}/accept`); + return response.data; + }, + + // Reject quotation + rejectQuotation: async (id: string, reason?: string): Promise => { + const response = await api.post(`${QUOTATIONS_URL}/${id}/reject`, { reason }); + return response.data; + }, + + // Convert quotation to sales order + convertToOrder: async (id: string): Promise => { + const response = await api.post(`${QUOTATIONS_URL}/${id}/convert`); + return response.data; + }, + + // Generate quotation PDF + generateQuotationPdf: async (id: string): Promise => { + const response = await api.get(`${QUOTATIONS_URL}/${id}/pdf`, { + responseType: 'blob', + }); + return response.data; + }, + + // Download quotation PDF + downloadQuotationPdf: async (id: string, fileName?: string): Promise => { + const blob = await salesApi.generateQuotationPdf(id); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName || `quotation-${id}.pdf`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }, +}; diff --git a/src/features/sales/index.ts b/src/features/sales/index.ts new file mode 100644 index 0000000..197fa01 --- /dev/null +++ b/src/features/sales/index.ts @@ -0,0 +1,2 @@ +export * from './api/sales.api'; +export * from './types'; diff --git a/src/features/sales/types/index.ts b/src/features/sales/types/index.ts new file mode 100644 index 0000000..7b322b6 --- /dev/null +++ b/src/features/sales/types/index.ts @@ -0,0 +1 @@ +export * from './sales.types'; diff --git a/src/features/sales/types/sales.types.ts b/src/features/sales/types/sales.types.ts new file mode 100644 index 0000000..ed555b9 --- /dev/null +++ b/src/features/sales/types/sales.types.ts @@ -0,0 +1,207 @@ +export type SalesOrderStatus = 'draft' | 'sent' | 'sale' | 'done' | 'cancelled'; +export type InvoiceStatus = 'pending' | 'partial' | 'invoiced'; +export type DeliveryStatus = 'pending' | 'partial' | 'delivered'; +export type InvoicePolicy = 'order' | 'delivery'; + +export interface SalesOrderLine { + id: string; + orderId: string; + productId: string; + productName?: string; + description: string; + quantity: number; + qtyDelivered: number; + qtyInvoiced: number; + uomId: string; + uomName?: string; + priceUnit: number; + discount: number; + taxIds: string[]; + amountUntaxed: number; + amountTax: number; + amountTotal: number; + analyticAccountId?: string | null; +} + +export interface SalesOrder { + id: string; + tenantId: string; + companyId: string; + companyName?: string; + name: string; + clientOrderRef?: string | null; + partnerId: string; + partnerName?: string; + orderDate: string; + validityDate?: string | null; + commitmentDate?: string | null; + currencyId: string; + currencyCode?: string; + pricelistId?: string | null; + pricelistName?: string; + paymentTermId?: string | null; + userId?: string | null; + userName?: string; + salesTeamId?: string | null; + salesTeamName?: string; + amountUntaxed: number; + amountTax: number; + amountTotal: number; + status: SalesOrderStatus; + invoiceStatus: InvoiceStatus; + deliveryStatus: DeliveryStatus; + invoicePolicy: InvoicePolicy; + pickingId?: string | null; + notes?: string | null; + termsConditions?: string | null; + lines?: SalesOrderLine[]; + createdAt: string; + confirmedAt?: string | null; +} + +export interface CreateSalesOrderDto { + companyId: string; + partnerId: string; + clientOrderRef?: string; + orderDate?: string; + validityDate?: string; + commitmentDate?: string; + currencyId: string; + pricelistId?: string; + paymentTermId?: string; + salesTeamId?: string; + invoicePolicy?: InvoicePolicy; + notes?: string; + termsConditions?: string; +} + +export interface UpdateSalesOrderDto { + partnerId?: string; + clientOrderRef?: string | null; + orderDate?: string; + validityDate?: string | null; + commitmentDate?: string | null; + currencyId?: string; + pricelistId?: string | null; + paymentTermId?: string | null; + salesTeamId?: string | null; + invoicePolicy?: InvoicePolicy; + notes?: string | null; + termsConditions?: string | null; +} + +export interface CreateSalesOrderLineDto { + productId: string; + description: string; + quantity: number; + uomId: string; + priceUnit: number; + discount?: number; + taxIds?: string[]; + analyticAccountId?: string; +} + +export interface UpdateSalesOrderLineDto { + description?: string; + quantity?: number; + uomId?: string; + priceUnit?: number; + discount?: number; + taxIds?: string[]; + analyticAccountId?: string | null; +} + +export interface SalesOrderFilters { + companyId?: string; + partnerId?: string; + status?: SalesOrderStatus; + invoiceStatus?: InvoiceStatus; + deliveryStatus?: DeliveryStatus; + dateFrom?: string; + dateTo?: string; + search?: string; + page?: number; + limit?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} + +export interface SalesOrdersResponse { + data: SalesOrder[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +} + +// Quotation types +export interface Quotation { + id: string; + tenantId: string; + companyId: string; + companyName?: string; + quotationNumber: string; + partnerId: string; + partnerName?: string; + quotationDate: string; + validityDate?: string | null; + currencyId: string; + currencyCode?: string; + amountUntaxed: number; + amountTax: number; + amountTotal: number; + status: 'draft' | 'sent' | 'accepted' | 'rejected' | 'expired' | 'converted'; + notes?: string | null; + termsConditions?: string | null; + lines?: QuotationLine[]; + createdAt: string; +} + +export interface QuotationLine { + id: string; + quotationId: string; + productId: string; + productName?: string; + description: string; + quantity: number; + uomId: string; + uomName?: string; + priceUnit: number; + discount: number; + amountUntaxed: number; + amountTax: number; + amountTotal: number; +} + +export interface CreateQuotationDto { + companyId: string; + partnerId: string; + quotationDate?: string; + validityDate?: string; + currencyId: string; + notes?: string; + termsConditions?: string; +} + +export interface QuotationFilters { + companyId?: string; + partnerId?: string; + status?: string; + dateFrom?: string; + dateTo?: string; + search?: string; + page?: number; + limit?: number; +} + +export interface QuotationsResponse { + data: Quotation[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +}