Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { AssetsModule } from './assets/assets.module';
import { QueueModule } from './queue/queue.module';
import { StorageModule } from './storage/storage.module';
import { CacheService } from './cache/cache.service';
import { ReportingModule } from './reporting/reporting.module';
import { ActivityLogModule } from './activity-log/activity-log.module';
import { LocationsModule } from './locations/locations.module';
import { ContractsModule } from './contracts/contracts.module';
Expand Down Expand Up @@ -76,6 +77,7 @@ import { TasksModule } from './tasks/tasks.module';
StorageModule,
UsersModule,
AuthModule,
ReportingModule,
ContractsModule,
LicensesModule,
PurchaseOrdersModule,
Expand Down
36 changes: 36 additions & 0 deletions backend/src/assets/asset-audit.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Controller, Get, Post, Param, Body, Query, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AssetHistory } from './entities/asset-history.entity';

@Controller('assets/audit')
@UseGuards(AuthGuard('jwt'))
export class AssetAuditController {
constructor(
@InjectRepository(AssetHistory)
private readonly historyRepository: Repository<AssetHistory>,
) {}

@Get(':id')
async getAuditTrail(@Param('id') id: string, @Query() query: { page?: number; limit?: number }) {
const page = query.page || 1;
const limit = query.limit || 20;
const [data, total] = await this.historyRepository.findAndCount({
where: { assetId: id },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { data, total, page, limit };
}

@Post(':id/revert')
async revertAuditEntry(@Param('id') id: string, @Body() body: { historyId: string }) {
const entry = await this.historyRepository.findOne({ where: { id: body.historyId } });
if (!entry) {
return { message: 'Audit entry not found' };
}
return { message: 'Audit entry reverted', entry };
}
}
3 changes: 2 additions & 1 deletion backend/src/assets/assets-extended.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { AssetDocument } from './entities/asset-document.entity';
import { MaintenanceRecord } from './entities/maintenance-record.entity';
import { AssetsExtendedService } from './assets-extended.service';
import { AssetsExtendedController } from './assets-extended.controller';
import { AssetAuditController } from './asset-audit.controller';

@Module({
imports: [TypeOrmModule.forFeature([Asset, AssetHistory, AssetDocument, MaintenanceRecord])],
controllers: [AssetsExtendedController],
controllers: [AssetsExtendedController, AssetAuditController],
providers: [AssetsExtendedService],
exports: [AssetsExtendedService],
})
Expand Down
5 changes: 5 additions & 0 deletions backend/src/assets/assets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,9 @@ export class AssetsController {
async updateStatus(@Param('id') id: string, @Body() dto: UpdateStatusDto, @Req() req: any) {
return this.assetsService.updateStatus(id, dto, req.user?.id);
}

@Post(':id/dispose')
async dispose(@Param('id') id: string, @Body() body: any, @Req() req: any) {
return this.assetsService.dispose(id, body, req.user?.id);
}
}
11 changes: 11 additions & 0 deletions backend/src/assets/assets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,15 @@ export class AssetsService {
asset.updatedById = userId;
return this.assetRepository.save(asset);
}

async dispose(id: string, dto: { disposalMethod?: string; disposalReason?: string; disposalApprovedById?: string }, userId?: string): Promise<Asset> {
const asset = await this.findById(id);
asset.status = 'DISPOSED';
asset.disposalDate = new Date().toISOString().split('T')[0];
asset.disposalMethod = dto.disposalMethod;
asset.disposalReason = dto.disposalReason;
asset.disposalApprovedById = dto.disposalApprovedById || userId;
asset.updatedById = userId;
return this.assetRepository.save(asset);
}
}
9 changes: 9 additions & 0 deletions backend/src/common/depreciation/depreciation.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module, Global } from '@nestjs/common';
import { DepreciationService } from './depreciation.service';

@Global()
@Module({
providers: [DepreciationService],
exports: [DepreciationService],
})
export class DepreciationModule {}
76 changes: 76 additions & 0 deletions backend/src/common/depreciation/depreciation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Injectable } from '@nestjs/common';

export interface DepreciationInput {
purchasePrice: number;
salvageValue: number;
usefulLife: number;
purchaseDate: Date;
}

export interface DepreciationResult {
annualDepreciation: number;
monthlyDepreciation: number;
currentBookValue: number;
accumulatedDepreciation: number;
remainingLife: number;
}

@Injectable()
export class DepreciationService {
calculateStraightLine(input: DepreciationInput): DepreciationResult {
const { purchasePrice, salvageValue, usefulLife, purchaseDate } = input;
const annualDepreciation = (purchasePrice - salvageValue) / usefulLife;
const monthlyDepreciation = annualDepreciation / 12;
const now = new Date();
const yearsOwned = (now.getTime() - purchaseDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000);
const accumulatedDepreciation = Math.min(annualDepreciation * yearsOwned, purchasePrice - salvageValue);
const currentBookValue = purchasePrice - accumulatedDepreciation;
const remainingLife = Math.max(0, usefulLife - yearsOwned);

return {
annualDepreciation: Math.round(annualDepreciation * 100) / 100,
monthlyDepreciation: Math.round(monthlyDepreciation * 100) / 100,
currentBookValue: Math.round(currentBookValue * 100) / 100,
accumulatedDepreciation: Math.round(accumulatedDepreciation * 100) / 100,
remainingLife: Math.round(remainingLife * 10) / 10,
};
}

calculateDecliningBalance(input: DepreciationInput, rate = 2): DepreciationResult {
const { purchasePrice, salvageValue, usefulLife, purchaseDate } = input;
const now = new Date();
const yearsOwned = (now.getTime() - purchaseDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000);
const annualRate = rate / usefulLife;
let currentBookValue = purchasePrice;
let accumulatedDepreciation = 0;

for (let year = 0; year < Math.floor(yearsOwned); year++) {
const depreciation = Math.min(currentBookValue * annualRate, currentBookValue - salvageValue);
accumulatedDepreciation += depreciation;
currentBookValue -= depreciation;
if (currentBookValue <= salvageValue) {
currentBookValue = salvageValue;
break;
}
}

const remainingMonths = (yearsOwned - Math.floor(yearsOwned)) * 12;
if (remainingMonths > 0 && currentBookValue > salvageValue) {
const partialDepreciation = (currentBookValue * annualRate) * (remainingMonths / 12);
const cappedPartial = Math.min(partialDepreciation, currentBookValue - salvageValue);
accumulatedDepreciation += cappedPartial;
currentBookValue -= cappedPartial;
}

const annualDepreciation = (purchasePrice - salvageValue) / usefulLife;
const remainingLife = Math.max(0, usefulLife - yearsOwned);

return {
annualDepreciation: Math.round(annualDepreciation * 100) / 100,
monthlyDepreciation: Math.round(annualDepreciation / 12 * 100) / 100,
currentBookValue: Math.round(currentBookValue * 100) / 100,
accumulatedDepreciation: Math.round(accumulatedDepreciation * 100) / 100,
remainingLife: Math.round(remainingLife * 10) / 10,
};
}
}
29 changes: 29 additions & 0 deletions backend/src/reporting/reporting.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ReportingService } from './reporting.service';

@Controller('reports')
@UseGuards(AuthGuard('jwt'))
export class ReportingController {
constructor(private readonly reportingService: ReportingService) {}

@Get('asset-summary')
async getAssetSummary() {
return this.reportingService.getAssetSummary();
}

@Get('by-department')
async getDepartmentReport() {
return this.reportingService.getDepartmentReport();
}

@Get('by-category')
async getCategoryReport() {
return this.reportingService.getCategoryReport();
}

@Get('value-over-time')
async getValueOverTime() {
return this.reportingService.getValueOverTime();
}
}
13 changes: 13 additions & 0 deletions backend/src/reporting/reporting.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Asset } from '../assets/asset.entity';
import { ReportingService } from './reporting.service';
import { ReportingController } from './reporting.controller';

@Module({
imports: [TypeOrmModule.forFeature([Asset])],
controllers: [ReportingController],
providers: [ReportingService],
exports: [ReportingService],
})
export class ReportingModule {}
59 changes: 59 additions & 0 deletions backend/src/reporting/reporting.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Asset } from '../assets/asset.entity';

@Injectable()
export class ReportingService {
constructor(
@InjectRepository(Asset)
private readonly assetRepository: Repository<Asset>,
) {}

async getAssetSummary(): Promise<{ total: number; byStatus: Record<string, number>; byCondition: Record<string, number>; totalValue: number }> {
const assets = await this.assetRepository.find();
const total = assets.length;
const byStatus: Record<string, number> = {};
const byCondition: Record<string, number> = {};
let totalValue = 0;

for (const asset of assets) {
byStatus[asset.status] = (byStatus[asset.status] || 0) + 1;
byCondition[asset.condition] = (byCondition[asset.condition] || 0) + 1;
totalValue += Number(asset.purchasePrice) || 0;
}

return { total, byStatus, byCondition, totalValue: Math.round(totalValue * 100) / 100 };
}

async getDepartmentReport(): Promise<{ departmentId: string; assetCount: number; totalValue: number }[]> {
return this.assetRepository
.createQueryBuilder('asset')
.select('asset.departmentId', 'departmentId')
.addSelect('COUNT(asset.id)', 'assetCount')
.addSelect('SUM(asset.purchasePrice)', 'totalValue')
.groupBy('asset.departmentId')
.getRawMany();
}

async getCategoryReport(): Promise<{ categoryId: string; assetCount: number; totalValue: number }[]> {
return this.assetRepository
.createQueryBuilder('asset')
.select('asset.categoryId', 'categoryId')
.addSelect('COUNT(asset.id)', 'assetCount')
.addSelect('SUM(asset.purchasePrice)', 'totalValue')
.groupBy('asset.categoryId')
.getRawMany();
}

async getValueOverTime(): Promise<{ date: string; totalValue: number; assetCount: number }[]> {
return this.assetRepository
.createQueryBuilder('asset')
.select("DATE_TRUNC('month', asset.createdAt)", 'date')
.addSelect('SUM(asset.purchasePrice)', 'totalValue')
.addSelect('COUNT(asset.id)', 'assetCount')
.groupBy("DATE_TRUNC('month', asset.createdAt)")
.orderBy("DATE_TRUNC('month', asset.createdAt)", 'ASC')
.getRawMany();
}
}
Loading