From d6c420e8b504f53b86729f808892f5bc19c6a07f Mon Sep 17 00:00:00 2001 From: mftee Date: Sat, 4 Oct 2025 10:00:03 +0100 Subject: [PATCH] dashboard implementation functionality --- backend/src/dashboard/dashboard.controller.ts | 20 +++++ backend/src/dashboard/dashboard.dto.ts | 40 ++++++++++ backend/src/dashboard/dashboard.entity.ts | 32 ++++++++ backend/src/dashboard/dashboard.module.ts | 14 ++++ backend/src/dashboard/dashboard.service.ts | 76 +++++++++++++++++++ 5 files changed, 182 insertions(+) create mode 100644 backend/src/dashboard/dashboard.controller.ts create mode 100644 backend/src/dashboard/dashboard.dto.ts create mode 100644 backend/src/dashboard/dashboard.entity.ts create mode 100644 backend/src/dashboard/dashboard.module.ts create mode 100644 backend/src/dashboard/dashboard.service.ts diff --git a/backend/src/dashboard/dashboard.controller.ts b/backend/src/dashboard/dashboard.controller.ts new file mode 100644 index 00000000..bb9fe0e4 --- /dev/null +++ b/backend/src/dashboard/dashboard.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { DashboardService } from './dashboard.service'; +import { DashboardMetricsDto } from './dto/dashboard-metrics.dto'; + + +@Controller('dashboard') +export class DashboardController { +constructor(private readonly service: DashboardService) {} + + +/** +* GET /dashboard/metrics +* Optional query: lowStockThreshold (number) — threshold to consider "low stock" +*/ +@Get('metrics') +async getMetrics(@Query('lowStockThreshold') lowStockThreshold?: string): Promise { +const threshold = lowStockThreshold ? Number(lowStockThreshold) : undefined; +return this.service.getMetrics({ lowStockThreshold: threshold }); +} +} \ No newline at end of file diff --git a/backend/src/dashboard/dashboard.dto.ts b/backend/src/dashboard/dashboard.dto.ts new file mode 100644 index 00000000..fca3cf4f --- /dev/null +++ b/backend/src/dashboard/dashboard.dto.ts @@ -0,0 +1,40 @@ +export interface StatusGroup { +status: string; +count: number; +} + + +export interface LowStockLocation { +location: string | null; +totalQuantity: number; +} + + +export class DashboardMetricsDto { +// total number of asset rows +totalCount: number; + + +// sum of `quantity` across all assets (useful for stock units) +totalQuantity: number; + + +// assets marked as disposed +disposedCount: number; + + +// counts per status +statusGroups: StatusGroup[]; + + +// number of assets under the low-stock threshold +lowStockCount: number; + + +// top few locations (or groups) with smallest aggregated quantity +lowStockLocations: LowStockLocation[]; + + +// the threshold used for low-stock calculation +lowStockThreshold?: number; +} \ No newline at end of file diff --git a/backend/src/dashboard/dashboard.entity.ts b/backend/src/dashboard/dashboard.entity.ts new file mode 100644 index 00000000..f0caef46 --- /dev/null +++ b/backend/src/dashboard/dashboard.entity.ts @@ -0,0 +1,32 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + + +@Entity('assets') +export class Asset { +@PrimaryGeneratedColumn('uuid') +id: string; + + +@Column({ length: 50, default: 'active' }) +status: string; // e.g. 'active' | 'disposed' | 'maintenance' + + +@Column({ type: 'int', nullable: true }) +quantity: number | null; + + +@Column({ type: 'timestamptz', nullable: true }) +disposedAt: Date | null; + + +@Column({ length: 100, nullable: true }) +location: string | null; + + +@CreateDateColumn() +createdAt: Date; + + +@UpdateDateColumn() +updatedAt: Date; +} \ No newline at end of file diff --git a/backend/src/dashboard/dashboard.module.ts b/backend/src/dashboard/dashboard.module.ts new file mode 100644 index 00000000..f34c7078 --- /dev/null +++ b/backend/src/dashboard/dashboard.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DashboardService } from './dashboard.service'; +import { DashboardController } from './dashboard.controller'; +import { Asset } from '../assets/entities/asset.entity'; + + +@Module({ +imports: [TypeOrmModule.forFeature([Asset])], +providers: [DashboardService], +controllers: [DashboardController], +exports: [DashboardService], +}) +export class DashboardModule {} \ No newline at end of file diff --git a/backend/src/dashboard/dashboard.service.ts b/backend/src/dashboard/dashboard.service.ts new file mode 100644 index 00000000..5d2b779a --- /dev/null +++ b/backend/src/dashboard/dashboard.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@nestjs/common'; +.select('COUNT(asset.id)', 'disposedCount') +.where('asset.status = :disposed', { disposed: 'disposed' }); + + +// 3) Grouped by status (counts) +const statusGroupQuery = this.assetRepo +.createQueryBuilder('asset') +.select('asset.status', 'status') +.addSelect('COUNT(asset.id)', 'count') +.groupBy('asset.status'); + + +// 4) Low stock items (count and list limited) +const lowStockCountQuery = this.assetRepo +.createQueryBuilder('asset') +.select('COUNT(asset.id)', 'lowStockCount') +.where('asset.quantity IS NOT NULL') +.andWhere('asset.quantity <= :threshold', { threshold: lowStockThreshold }); + + +// 5) Example: get top 5 locations with lowest total quantity +const lowStockLocationsQuery = this.assetRepo +.createQueryBuilder('asset') +.select('asset.location', 'location') +.addSelect('COALESCE(SUM(asset.quantity),0)', 'totalQuantity') +.groupBy('asset.location') +.orderBy('totalQuantity', 'ASC') +.limit(5); + + +// Execute queries in parallel +const [totalRaw, disposedRaw, statusGroupsRaw, lowStockCountRaw, lowStockLocationsRaw] = +await Promise.all([ +totalQuery.getRawOne(), +disposedQuery.getRawOne(), +statusGroupQuery.getRawMany(), +lowStockCountQuery.getRawOne(), +lowStockLocationsQuery.getRawMany(), +]); + + +const totalCount = Number(totalRaw?.totalCount ?? 0); +const totalQuantity = Number(totalRaw?.totalQuantity ?? 0); +const disposedCount = Number(disposedRaw?.disposedCount ?? 0); + + +const statusGroups: StatusGroup[] = (statusGroupsRaw ?? []).map((r: any) => ({ +status: r.status, +count: Number(r.count), +})); + + +const lowStockCount = Number(lowStockCountRaw?.lowStockCount ?? 0); + + +const lowStockLocations = (lowStockLocationsRaw ?? []).map((r: any) => ({ +location: r.location, +totalQuantity: Number(r.totalQuantity), +})); + + +const dto: DashboardMetricsDto = { +totalCount, +totalQuantity, +disposedCount, +statusGroups, +lowStockCount, +lowStockLocations, +lowStockThreshold, +}; + + +return dto; +} +} \ No newline at end of file