feat(frontend): Add API services for invoices, inventory, sales, purchases
- Add invoices feature: types, API service for CRUD, validation, payments, PDF - Add inventory feature: types, API service for stock, movements, warehouses, locations - Add sales feature: types, API service for orders, quotations, PDF - Add purchases feature: types, API service for orders, receipts Part of FASE 2: P0 Frontend API Services
This commit is contained in:
parent
d3fdc0b9c0
commit
6cb8e99f50
204
src/features/inventory/api/inventory.api.ts
Normal file
204
src/features/inventory/api/inventory.api.ts
Normal file
@ -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<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}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
2
src/features/inventory/index.ts
Normal file
2
src/features/inventory/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './api/inventory.api';
|
||||||
|
export * from './types';
|
||||||
1
src/features/inventory/types/index.ts
Normal file
1
src/features/inventory/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './inventory.types';
|
||||||
223
src/features/inventory/types/inventory.types.ts
Normal file
223
src/features/inventory/types/inventory.types.ts
Normal file
@ -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<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;
|
||||||
|
};
|
||||||
|
}
|
||||||
138
src/features/invoices/api/invoices.api.ts
Normal file
138
src/features/invoices/api/invoices.api.ts
Normal file
@ -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<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' });
|
||||||
|
},
|
||||||
|
};
|
||||||
2
src/features/invoices/index.ts
Normal file
2
src/features/invoices/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './api/invoices.api';
|
||||||
|
export * from './types';
|
||||||
1
src/features/invoices/types/index.ts
Normal file
1
src/features/invoices/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './invoice.types';
|
||||||
189
src/features/invoices/types/invoice.types.ts
Normal file
189
src/features/invoices/types/invoice.types.ts
Normal file
@ -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<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;
|
||||||
|
}
|
||||||
157
src/features/purchases/api/purchases.api.ts
Normal file
157
src/features/purchases/api/purchases.api.ts
Normal file
@ -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<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);
|
||||||
|
},
|
||||||
|
};
|
||||||
2
src/features/purchases/index.ts
Normal file
2
src/features/purchases/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './api/purchases.api';
|
||||||
|
export * from './types';
|
||||||
1
src/features/purchases/types/index.ts
Normal file
1
src/features/purchases/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './purchase.types';
|
||||||
174
src/features/purchases/types/purchase.types.ts
Normal file
174
src/features/purchases/types/purchase.types.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
194
src/features/sales/api/sales.api.ts
Normal file
194
src/features/sales/api/sales.api.ts
Normal file
@ -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<SalesOrdersResponse> => {
|
||||||
|
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<SalesOrdersResponse>(`${ORDERS_URL}?${params.toString()}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get sales order by ID
|
||||||
|
getOrderById: async (id: string): Promise<SalesOrder> => {
|
||||||
|
const response = await api.get<SalesOrder>(`${ORDERS_URL}/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create sales order
|
||||||
|
createOrder: async (data: CreateSalesOrderDto): Promise<SalesOrder> => {
|
||||||
|
const response = await api.post<SalesOrder>(ORDERS_URL, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update sales order
|
||||||
|
updateOrder: async (id: string, data: UpdateSalesOrderDto): Promise<SalesOrder> => {
|
||||||
|
const response = await api.patch<SalesOrder>(`${ORDERS_URL}/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete sales order
|
||||||
|
deleteOrder: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`${ORDERS_URL}/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== Order Lines ====================
|
||||||
|
|
||||||
|
// Add line to order
|
||||||
|
addLine: async (orderId: string, data: CreateSalesOrderLineDto): Promise<SalesOrderLine> => {
|
||||||
|
const response = await api.post<SalesOrderLine>(`${ORDERS_URL}/${orderId}/lines`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update order line
|
||||||
|
updateLine: async (orderId: string, lineId: string, data: UpdateSalesOrderLineDto): Promise<SalesOrderLine> => {
|
||||||
|
const response = await api.patch<SalesOrderLine>(`${ORDERS_URL}/${orderId}/lines/${lineId}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove order line
|
||||||
|
removeLine: async (orderId: string, lineId: string): Promise<void> => {
|
||||||
|
await api.delete(`${ORDERS_URL}/${orderId}/lines/${lineId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== Order Actions ====================
|
||||||
|
|
||||||
|
// Confirm order (send to customer)
|
||||||
|
confirmOrder: async (id: string): Promise<SalesOrder> => {
|
||||||
|
const response = await api.post<SalesOrder>(`${ORDERS_URL}/${id}/confirm`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cancel order
|
||||||
|
cancelOrder: async (id: string): Promise<SalesOrder> => {
|
||||||
|
const response = await api.post<SalesOrder>(`${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<SalesOrderFilters, 'status'>): Promise<SalesOrdersResponse> => {
|
||||||
|
return salesApi.getOrders({ ...filters, status: 'draft' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get confirmed orders
|
||||||
|
getConfirmed: async (filters?: Omit<SalesOrderFilters, 'status'>): Promise<SalesOrdersResponse> => {
|
||||||
|
return salesApi.getOrders({ ...filters, status: 'sale' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get orders by customer
|
||||||
|
getByCustomer: async (partnerId: string, filters?: Omit<SalesOrderFilters, 'partnerId'>): Promise<SalesOrdersResponse> => {
|
||||||
|
return salesApi.getOrders({ ...filters, partnerId });
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== Quotations ====================
|
||||||
|
|
||||||
|
// Get all quotations
|
||||||
|
getQuotations: async (filters?: QuotationFilters): Promise<QuotationsResponse> => {
|
||||||
|
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<QuotationsResponse>(`${QUOTATIONS_URL}?${params.toString()}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get quotation by ID
|
||||||
|
getQuotationById: async (id: string): Promise<Quotation> => {
|
||||||
|
const response = await api.get<Quotation>(`${QUOTATIONS_URL}/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create quotation
|
||||||
|
createQuotation: async (data: CreateQuotationDto): Promise<Quotation> => {
|
||||||
|
const response = await api.post<Quotation>(QUOTATIONS_URL, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Send quotation
|
||||||
|
sendQuotation: async (id: string): Promise<Quotation> => {
|
||||||
|
const response = await api.post<Quotation>(`${QUOTATIONS_URL}/${id}/send`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Accept quotation
|
||||||
|
acceptQuotation: async (id: string): Promise<Quotation> => {
|
||||||
|
const response = await api.post<Quotation>(`${QUOTATIONS_URL}/${id}/accept`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reject quotation
|
||||||
|
rejectQuotation: async (id: string, reason?: string): Promise<Quotation> => {
|
||||||
|
const response = await api.post<Quotation>(`${QUOTATIONS_URL}/${id}/reject`, { reason });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Convert quotation to sales order
|
||||||
|
convertToOrder: async (id: string): Promise<SalesOrder> => {
|
||||||
|
const response = await api.post<SalesOrder>(`${QUOTATIONS_URL}/${id}/convert`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Generate quotation PDF
|
||||||
|
generateQuotationPdf: async (id: string): Promise<Blob> => {
|
||||||
|
const response = await api.get(`${QUOTATIONS_URL}/${id}/pdf`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Download quotation PDF
|
||||||
|
downloadQuotationPdf: async (id: string, fileName?: string): Promise<void> => {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
2
src/features/sales/index.ts
Normal file
2
src/features/sales/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './api/sales.api';
|
||||||
|
export * from './types';
|
||||||
1
src/features/sales/types/index.ts
Normal file
1
src/features/sales/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './sales.types';
|
||||||
207
src/features/sales/types/sales.types.ts
Normal file
207
src/features/sales/types/sales.types.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user