first commit
This commit is contained in:
104
apps/api/src/dashboard/dashboard.controller.ts
Normal file
104
apps/api/src/dashboard/dashboard.controller.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CatalogService } from '../catalog/catalog.service';
|
||||
import { AlertsService } from '../alerts/alerts.service';
|
||||
import { ClassificationTagEntity } from '../entities/classification-tag.entity';
|
||||
|
||||
@Controller('dashboard')
|
||||
export class DashboardController {
|
||||
constructor(
|
||||
private readonly catalog: CatalogService,
|
||||
private readonly alerts: AlertsService,
|
||||
@InjectRepository(ClassificationTagEntity)
|
||||
private readonly tags: Repository<ClassificationTagEntity>,
|
||||
) {}
|
||||
|
||||
@Get('summary')
|
||||
async summary(@Query('days') days?: string) {
|
||||
const daysCount = this.clampDays(days ? Number(days) : 14);
|
||||
const items = await this.catalog.list();
|
||||
const changes = await this.catalog.listChanges(20);
|
||||
const since = this.startOfDayOffset(daysCount - 1);
|
||||
const trendChanges = await this.catalog.listChangesSince(since);
|
||||
const profiles = await this.alerts.listProfiles(process.env.DEFAULT_ALERT_USER || '00000000-0000-0000-0000-000000000001');
|
||||
const tags = await this.tags.find();
|
||||
|
||||
const perPublisher: Record<string, number> = {};
|
||||
const perFormat: Record<string, number> = {};
|
||||
const perTopic: Record<string, number> = {};
|
||||
const perTerritory: Record<string, number> = {};
|
||||
const trend = this.buildTrend(daysCount);
|
||||
|
||||
for (const item of items) {
|
||||
if (item.publisher) perPublisher[item.publisher] = (perPublisher[item.publisher] || 0) + 1;
|
||||
if (item.format) {
|
||||
for (const format of String(item.format).split(',')) {
|
||||
const value = format.trim();
|
||||
if (!value) continue;
|
||||
perFormat[value] = (perFormat[value] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
if (tag.type === 'topic') perTopic[tag.value] = (perTopic[tag.value] || 0) + 1;
|
||||
if (tag.type === 'territory') perTerritory[tag.value] = (perTerritory[tag.value] || 0) + 1;
|
||||
}
|
||||
|
||||
for (const change of trendChanges) {
|
||||
const key = this.formatDateKey(change.createdAt);
|
||||
const bucket = trend[key];
|
||||
if (!bucket) continue;
|
||||
if (change.eventType === 'created') bucket.created += 1;
|
||||
else bucket.updated += 1;
|
||||
bucket.total += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totals: {
|
||||
items: items.length,
|
||||
changes: changes.length,
|
||||
alertProfiles: profiles.length,
|
||||
},
|
||||
latestChanges: changes.slice(0, 8),
|
||||
trend: Object.values(trend),
|
||||
topPublishers: this.toSorted(perPublisher).slice(0, 6),
|
||||
topFormats: this.toSorted(perFormat).slice(0, 6),
|
||||
topTopics: this.toSorted(perTopic).slice(0, 6),
|
||||
topTerritories: this.toSorted(perTerritory).slice(0, 6),
|
||||
};
|
||||
}
|
||||
|
||||
private toSorted(input: Record<string, number>) {
|
||||
return Object.entries(input)
|
||||
.map(([label, value]) => ({ label, value }))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
}
|
||||
|
||||
private clampDays(value: number) {
|
||||
if (!value || Number.isNaN(value)) return 14;
|
||||
return Math.min(Math.max(value, 7), 60);
|
||||
}
|
||||
|
||||
private startOfDayOffset(offsetDays: number) {
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
date.setDate(date.getDate() - offsetDays);
|
||||
return date;
|
||||
}
|
||||
|
||||
private formatDateKey(value: Date) {
|
||||
return new Date(value).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
private buildTrend(days: number) {
|
||||
const out: Record<string, { date: string; created: number; updated: number; total: number }> = {};
|
||||
for (let i = days - 1; i >= 0; i -= 1) {
|
||||
const d = this.startOfDayOffset(i);
|
||||
const key = this.formatDateKey(d);
|
||||
out[key] = { date: key, created: 0, updated: 0, total: 0 };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
12
apps/api/src/dashboard/dashboard.module.ts
Normal file
12
apps/api/src/dashboard/dashboard.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
import { CatalogModule } from '../catalog/catalog.module';
|
||||
import { AlertsModule } from '../alerts/alerts.module';
|
||||
import { ClassificationTagEntity } from '../entities/classification-tag.entity';
|
||||
|
||||
@Module({
|
||||
imports: [CatalogModule, AlertsModule, TypeOrmModule.forFeature([ClassificationTagEntity])],
|
||||
controllers: [DashboardController],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
Reference in New Issue
Block a user