UNPKG

prodobit

Version:

Open-core business application development platform

395 lines (343 loc) 10.2 kB
import type { Database } from "@prodobit/database"; import { assets, assetTypes, locations } from "@prodobit/database"; import type { AssetQuery, CreateAssetRequest, CreateAssetTypeRequest, } from "@prodobit/types"; import { and, eq, isNull, like, or, SQL } from "drizzle-orm"; export class AssetService { constructor(private db: Database) {} // Create asset async createAsset(data: CreateAssetRequest, tenantId: string): Promise<any> { // Verify location exists const [location] = await this.db .select() .from(locations) .where( and( eq(locations.id, data.locationId), eq(locations.tenantId, tenantId), isNull(locations.deletedAt) ) ) .limit(1); if (!location) { throw new Error("Location not found"); } // Generate code if not provided const code = data.code || this.generateAssetCode(tenantId, data.assetType); const [asset] = await this.db .insert(assets) .values({ tenantId: tenantId, locationId: data.locationId, name: data.name, code, assetType: data.assetType, status: data.status || "active", parentAssetId: data.parentAssetId, }) .returning(); return asset; } // Get all assets with optional filtering async getAssets(params: { tenantId: string } & AssetQuery): Promise<any[]> { const { tenantId, ...filters } = params; const conditions = [ eq(assets.tenantId, tenantId), isNull(assets.deletedAt), ]; if (filters?.locationId) { conditions.push(eq(assets.locationId, filters.locationId)); } if (filters?.assetType) { conditions.push(eq(assets.assetType, filters.assetType)); } if (filters?.status) { conditions.push(eq(assets.status, filters.status)); } if (filters?.parentAssetId) { conditions.push(eq(assets.parentAssetId, filters.parentAssetId)); } else if (filters?.parentAssetId === null) { conditions.push(isNull(assets.parentAssetId)); } if (filters?.search) { conditions.push( or( like(assets.name, `%${filters.search}%`), like(assets.code, `%${filters.search}%`) ) as SQL<unknown> ); } const result = await this.db .select({ asset: assets, location: { id: locations.id, name: locations.name, code: locations.code, locationType: locations.locationType, }, }) .from(assets) .leftJoin(locations, eq(assets.locationId, locations.id)) .where(and(...conditions)) .orderBy(assets.name); return result.map((row) => ({ ...row.asset, location: row.location, })); } // Get asset by ID async getAssetById(assetId: string, tenantId: string): Promise<any | null> { const [result] = await this.db .select({ asset: assets, location: { id: locations.id, name: locations.name, code: locations.code, locationType: locations.locationType, }, }) .from(assets) .leftJoin(locations, eq(assets.locationId, locations.id)) .where( and( eq(assets.id, assetId), eq(assets.tenantId, tenantId), isNull(assets.deletedAt) ) ) .limit(1); if (!result) return null; return { ...result.asset, location: result.location, }; } // Get assets by location async getAssetsByLocation( locationId: string, tenantId: string ): Promise<any[]> { return this.getAssets({ tenantId, locationId }); } // Get child assets async getChildAssets( parentAssetId: string, tenantId: string ): Promise<any[]> { return this.getAssets({ tenantId, parentAssetId }); } // Get asset hierarchy (all parents up to root) async getAssetHierarchy(assetId: string, tenantId: string): Promise<any[]> { const hierarchy: any[] = []; let currentAssetId: string | null = assetId; while (currentAssetId) { const asset = await this.getAssetById(currentAssetId, tenantId); if (!asset) break; hierarchy.unshift(asset); currentAssetId = asset.parentAssetId; } return hierarchy; } // Update asset async updateAsset( assetId: string, tenantId: string, data: Partial<CreateAssetRequest> ): Promise<any | null> { // If location is being updated, verify it exists if (data.locationId) { const [location] = await this.db .select() .from(locations) .where( and( eq(locations.id, data.locationId), eq(locations.tenantId, tenantId), isNull(locations.deletedAt) ) ) .limit(1); if (!location) { throw new Error("Location not found"); } } const [asset] = await this.db .update(assets) .set({ ...data, updatedAt: new Date(), }) .where( and( eq(assets.id, assetId), eq(assets.tenantId, tenantId), isNull(assets.deletedAt) ) ) .returning(); return asset || null; } // Delete asset (soft delete) async deleteAsset(assetId: string, tenantId: string): Promise<boolean> { // Check if asset has child assets const childAssets = await this.getChildAssets(assetId, tenantId); if (childAssets.length > 0) { throw new Error("Cannot delete asset with child assets"); } const [deleted] = await this.db .update(assets) .set({ deletedAt: new Date(), }) .where( and( eq(assets.id, assetId), eq(assets.tenantId, tenantId), isNull(assets.deletedAt) ) ) .returning(); return !!deleted; } // Move asset to different location async moveAsset( assetId: string, tenantId: string, newLocationId: string ): Promise<any | null> { return this.updateAsset(assetId, tenantId, { locationId: newLocationId }); } // Asset Types methods async createAssetType(data: CreateAssetTypeRequest, tenantId: string): Promise<any> { const [assetType] = await this.db .insert(assetTypes) .values({ tenantId: tenantId, name: data.name, code: data.code || this.generateAssetTypeCode(tenantId, data.name), description: data.description, category: data.category, isActive: data.isActive ?? true, }) .returning(); return assetType; } async getAssetTypes(tenantId: string, category?: string): Promise<any[]> { const conditions = [eq(assetTypes.tenantId, tenantId)]; if (category) { conditions.push(eq(assetTypes.category, category)); } const result = await this.db .select() .from(assetTypes) .where(and(...conditions)) .orderBy(assetTypes.name); return result; } async getAssetTypeById( assetTypeId: string, tenantId: string ): Promise<any | null> { const [assetType] = await this.db .select() .from(assetTypes) .where( and(eq(assetTypes.id, assetTypeId), eq(assetTypes.tenantId, tenantId)) ) .limit(1); return assetType || null; } // Helper methods private generateAssetCode(tenantId: string, assetType: string): string { const prefix = this.getAssetPrefix(assetType); const timestamp = Date.now().toString().slice(-6); const random = Math.random().toString(36).substring(2, 4).toUpperCase(); return `${prefix}${timestamp}${random}`; } private getAssetPrefix(assetType: string): string { switch (assetType.toLowerCase()) { case "work_station": return "WS"; case "work_center": return "WC"; case "machine": return "MCH"; case "equipment": return "EQP"; default: return "AST"; } } private generateAssetTypeCode(tenantId: string, name: string): string { const prefix = name.substring(0, 3).toUpperCase(); const timestamp = Date.now().toString().slice(-4); return `${prefix}${timestamp}`; } // Get asset statistics async getAssetStats(tenantId: string): Promise<{ totalAssets: number; assetsByType: Record<string, number>; assetsByStatus: Record<string, number>; assetsByLocation: Record<string, number>; }> { const allAssets = await this.getAssets({ tenantId }); const assetsByType: Record<string, number> = {}; const assetsByStatus: Record<string, number> = {}; const assetsByLocation: Record<string, number> = {}; allAssets.forEach((asset) => { // Count by type const type = asset.assetType || "unspecified"; assetsByType[type] = (assetsByType[type] || 0) + 1; // Count by status const status = asset.status || "unknown"; assetsByStatus[status] = (assetsByStatus[status] || 0) + 1; // Count by location const locationName = asset.location?.name || "unknown location"; assetsByLocation[locationName] = (assetsByLocation[locationName] || 0) + 1; }); return { totalAssets: allAssets.length, assetsByType, assetsByStatus, assetsByLocation, }; } // Search assets across multiple criteria async searchAssets(tenantId: string, query: string): Promise<any[]> { const conditions = [ eq(assets.tenantId, tenantId), isNull(assets.deletedAt), or( like(assets.name, `%${query}%`), like(assets.code, `%${query}%`), like(assets.assetType, `%${query}%`) ), ]; const result = await this.db .select({ asset: assets, location: { id: locations.id, name: locations.name, code: locations.code, locationType: locations.locationType, }, }) .from(assets) .leftJoin(locations, eq(assets.locationId, locations.id)) .where(and(...conditions)) .orderBy(assets.name) .limit(50); return result.map((row) => ({ ...row.asset, location: row.location, })); } }