392 lines
11 KiB
TypeScript

import apiService from './apiService';
import API_CONFIG from '../config/api';
// Asset Interfaces
export interface Asset {
name: string;
asset_name: string;
company: string;
docstatus?: number; // 0 = Draft, 1 = Submitted, 2 = Cancelled
custom_serial_number?: string;
custom_technical_department?: string;
location?: string;
custom_building?: string;
custom_room_number?: string;
custom_manufacturer?: string;
department?: string;
custom_asset_type?: string;
custom_manufacturing_year?: string;
custom_model?: string;
custom_class?: string;
custom_device_status?: string;
custom_down_time?: number;
asset_owner_company?: string;
custom_up_time?: number;
custom_total_hours?: number;
custom_modality?: string;
custom_attach_image?: string;
custom_site_contractor?: string;
custom_site?: string;
custom_total_amount?: number;
creation?: string;
modified?: string;
owner?: string;
modified_by?: string;
status?: string;
workflow_state?: string;
custom_delete_status?: string;
custom_category?: string;
custom_recalled?: string;
calculate_depreciation?: boolean;
gross_purchase_amount?: number;
available_for_use_date?:string;
finance_books?: AssetFinanceBookRow[];
custom_spare_parts?: Array<{
item_code?: string;
item_name?: string;
qty?: number;
rate?: number;
amount?: number;
uom?: string;
work_order?: string;
}>;
custom_total_spare_parts_amount?: number;
// Checkbox fields
custom_warranty?: boolean;
custom_extended_warranty?: boolean;
custom__service_contract?: boolean;
custom_covering_spare_parts?: boolean;
custom_spare_parts_labour?: boolean;
custom_covering_labour?: boolean;
custom_ppm_only?: boolean;
custom_support_plan?: string;
// Service agreement fields
custom_service_agreement?: string;
custom_service_coverage?: string;
custom_start_date?: string;
custom_end_date?: string;
}
export interface AssetListResponse {
assets: Asset[];
total_count: number;
limit: number;
offset: number;
has_more: boolean;
}
export interface AssetFilters {
company?: string;
location?: string;
department?: string;
custom_asset_type?: string;
custom_manufacturer?: string;
custom_device_status?: string;
[key: string]: any;
}
export interface AssetFilterOptions {
companies: string[];
locations: string[];
departments: string[];
asset_types: string[];
manufacturers: string[];
device_statuses: string[];
}
export interface AssetStats {
total_assets: number;
by_status: Record<string, number>;
by_company: Record<string, number>;
by_type: Record<string, number>;
total_amount: number;
}
// Add child row type
export interface AssetFinanceBookRow {
finance_book?: string;
depreciation_method?: string;
total_number_of_depreciations?: number;
frequency_of_depreciation?: number;
depreciation_start_date?: string; // YYYY-MM-DD
}
export interface CreateAssetData {
asset_name: string;
company: string;
custom_serial_number?: string;
custom_technical_department?: string;
location?: string;
custom_building?: string;
custom_room_number?: string;
custom_manufacturer?: string;
department?: string;
custom_asset_type?: string;
custom_manufacturing_year?: string;
custom_model?: string;
custom_class?: string;
custom_device_status?: string;
custom_down_time?: number;
asset_owner_company?: string;
custom_up_time?: number;
custom_total_hours?: number;
custom_modality?: string;
custom_attach_image?: string;
custom_site_contractor?: string;
custom_site?: string;
custom_total_amount?: number;
calculate_depreciation?: boolean;
finance_books?: AssetFinanceBookRow[];
// Checkbox fields
custom_warranty?: boolean;
custom_extended_warranty?: boolean;
custom__service_contract?: boolean;
custom_covering_spare_parts?: boolean;
custom_spare_parts_labour?: boolean;
custom_covering_labour?: boolean;
custom_ppm_only?: boolean;
custom_support_plan?: string;
// Spare parts
custom_spare_parts?: Array<{
item_code?: string;
item_name?: string;
qty?: number;
rate?: number;
amount?: number;
uom?: string;
work_order?: string;
}>;
custom_total_spare_parts_amount?: number;
[key: string]: any;
}
class AssetService {
/**
* Get list of assets with optional filters and pagination
*/
// async getAssets(
// filters?: AssetFilters,
// fields?: string[],
// limit: number = 20,
// offset: number = 0,
// orderBy?: string
// ): Promise<AssetListResponse> {
// const params = new URLSearchParams();
// if (filters) {
// params.append('filters', JSON.stringify(filters));
// }
// if (fields && fields.length > 0) {
// params.append('fields', JSON.stringify(fields));
// }
// params.append('limit', limit.toString());
// params.append('offset', offset.toString());
// if (orderBy) {
// params.append('order_by', orderBy);
// }
// const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSETS}?${params.toString()}`;
// return apiService.apiCall<AssetListResponse>(endpoint);
// }
/**
* Get list of assets with optional filters and pagination
* Uses Frappe's built-in REST API which auto-applies User Permissions
*/
async getAssets(
filters?: AssetFilters,
fields?: string[],
limit: number = 20,
offset: number = 0,
orderBy?: string
): Promise<AssetListResponse> {
const params = new URLSearchParams();
if (filters && Object.keys(filters).length > 0) {
const { toFrappeFilterArray } = await import('../utils/listFilterUtils');
const filterArray = toFrappeFilterArray(filters);
if (filterArray.length > 0) params.append('filters', JSON.stringify(filterArray));
}
if (fields && fields.length > 0) {
params.append('fields', JSON.stringify(fields));
}
params.append('limit', limit.toString());
params.append('offset', offset.toString());
if (orderBy) {
params.append('order_by', orderBy);
}
const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSETS}?${params.toString()}`;
return apiService.apiCall<AssetListResponse>(endpoint);
}
/**
* Get detailed information about a specific asset
*/
async getAssetDetails(assetName: string): Promise<Asset> {
const endpoint = `${API_CONFIG.ENDPOINTS.GET_ASSET_DETAILS}?asset_name=${encodeURIComponent(assetName)}`;
return apiService.apiCall<Asset>(endpoint);
}
/**
* Create a new asset
*/
async createAsset(assetData: CreateAssetData): Promise<{ success: boolean; asset: Asset; message: string }> {
return apiService.apiCall(API_CONFIG.ENDPOINTS.CREATE_ASSET, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ asset_data: assetData })
});
}
/**
* Update an existing asset
*/
async updateAsset(
assetName: string,
assetData: Partial<CreateAssetData>
): Promise<{ success: boolean; asset: Asset; message: string }> {
return apiService.apiCall(API_CONFIG.ENDPOINTS.UPDATE_ASSET, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
asset_name: assetName,
asset_data: assetData
})
});
}
/**
* Delete an asset
*/
async deleteAsset(assetName: string): Promise<{ success: boolean; message: string }> {
return apiService.apiCall(API_CONFIG.ENDPOINTS.DELETE_ASSET, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ asset_name: assetName })
});
}
/**
* Get available filter options
*/
async getAssetFilters(): Promise<AssetFilterOptions> {
return apiService.apiCall<AssetFilterOptions>(API_CONFIG.ENDPOINTS.GET_ASSET_FILTERS);
}
/**
* Get asset statistics
*/
async getAssetStats(): Promise<AssetStats> {
return apiService.apiCall<AssetStats>(API_CONFIG.ENDPOINTS.GET_ASSET_STATS);
}
/**
* Search assets by keyword
*/
async searchAssets(searchTerm: string, limit: number = 10): Promise<Asset[]> {
const endpoint = `${API_CONFIG.ENDPOINTS.SEARCH_ASSETS}?search_term=${encodeURIComponent(searchTerm)}&limit=${limit}`;
return apiService.apiCall<Asset[]>(endpoint);
}
/**
* Return which of the given values exist as Asset.custom_serial_number (exact match).
*/
async getExistingSerialNumbers(values: string[]): Promise<Set<string>> {
const unique = Array.from(
new Set(values.map((value) => value.trim()).filter(Boolean))
);
if (unique.length === 0) {
return new Set();
}
const matched = new Set<string>();
const chunkSize = 500;
for (let offset = 0; offset < unique.length; offset += chunkSize) {
const chunk = unique.slice(offset, offset + chunkSize);
const queryParams = new URLSearchParams();
queryParams.append('fields', JSON.stringify(['custom_serial_number']));
queryParams.append('filters', JSON.stringify([['custom_serial_number', 'in', chunk]]));
queryParams.append('limit_page_length', '0');
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/resource/Asset?${queryParams.toString()}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(typeof window !== 'undefined' &&
(window as Window & { csrf_token?: string }).csrf_token
? { 'X-Frappe-CSRF-Token': (window as Window & { csrf_token?: string }).csrf_token! }
: {}),
},
credentials: 'include',
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
for (const row of result.data || []) {
const serial = (row.custom_serial_number || '').trim();
if (serial) {
matched.add(serial);
}
}
}
return matched;
}
/**
* Submit an asset document (changes docstatus from 0 to 1)
*/
// async submitAsset(assetName: string): Promise<{ message: string }> {
// return apiService.apiCall('/api/method/frappe.client.submit', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({
// doc: {
// doctype: 'Asset',
// name: assetName
// }
// })
// });
// }
async submitAsset(assetName: string): Promise<{ success: boolean; asset: Asset; message: string }> {
return apiService.apiCall(API_CONFIG.ENDPOINTS.SUBMIT_ASSET || '/api/method/asset_lite.api.asset_api.submit_asset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
asset_name: assetName
})
});
}
}
// Create and export singleton instance
const assetService = new AssetService();
export default assetService;