main
Some checks failed
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-03-30 00:21:32 +03:00
parent 85c35c73e8
commit acb103657b
29 changed files with 11473 additions and 13081 deletions

View File

@@ -0,0 +1,50 @@
import {
Controller,
Get,
Logger,
Req,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { DashboardService } from './dashboard.service';
@ApiTags('dashboard')
@ApiBearerAuth()
@Controller('dashboard')
export class DashboardController {
private readonly logger = new Logger(DashboardController.name);
constructor(private readonly dashboardService: DashboardService) {}
/**
* Dashboard ana istatistikleri — kullanıcı bazlı.
*/
@Get('stats')
@ApiOperation({ summary: 'Dashboard istatistiklerini getir' })
@ApiResponse({ status: 200, description: 'Dashboard istatistikleri' })
async getStats(@Req() req: any) {
const userId = req.user?.id || req.user?.sub;
this.logger.debug(`Dashboard stats istendi: ${userId}`);
return this.dashboardService.getStats(userId);
}
/**
* Video üretim kuyruğu durumu.
*/
@Get('queue-status')
@ApiOperation({ summary: 'Kuyruk durum bilgisi' })
@ApiResponse({ status: 200, description: 'Kuyruk ve WebSocket durumu' })
async getQueueStatus() {
return this.dashboardService.getQueueStatus();
}
/**
* Bu ayki video üretim chart verisi (gün bazlı).
*/
@Get('chart')
@ApiOperation({ summary: 'Aylık video üretim chart verisi' })
@ApiResponse({ status: 200, description: 'Gün bazlı üretim verileri' })
async getMonthlyChart(@Req() req: any) {
const userId = req.user?.id || req.user?.sub;
return this.dashboardService.getMonthlyChart(userId);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { DashboardController } from './dashboard.controller';
import { DashboardService } from './dashboard.service';
import { VideoQueueModule } from '../video-queue/video-queue.module';
import { EventsModule } from '../events/events.module';
@Module({
imports: [VideoQueueModule, EventsModule],
controllers: [DashboardController],
providers: [DashboardService],
exports: [DashboardService],
})
export class DashboardModule {}

View File

@@ -0,0 +1,190 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { VideoGenerationProducer } from '../video-queue/video-generation.producer';
import { EventsGateway } from '../events/events.gateway';
@Injectable()
export class DashboardService {
private readonly logger = new Logger(DashboardService.name);
constructor(
private readonly db: PrismaService,
private readonly videoGenerationProducer: VideoGenerationProducer,
private readonly eventsGateway: EventsGateway,
) {}
/**
* Dashboard istatistiklerini hesaplar.
* UserID bazlı filtreleme — her kullanıcı kendi verilerini görür.
* Gerçek plan bilgisini Subscription → Plan tablosundan çeker.
*/
async getStats(userId: string) {
const [
totalProjects,
completedVideos,
activeRenderJobs,
failedProjects,
draftProjects,
totalCreditsUsed,
recentProjects,
activeSubscription,
creditBalance,
] = await Promise.all([
// Toplam proje sayısı
this.db.project.count({
where: { userId, deletedAt: null },
}),
// Tamamlanan video sayısı
this.db.project.count({
where: { userId, status: 'COMPLETED', deletedAt: null },
}),
// Aktif render job sayısı
this.db.project.count({
where: {
userId,
deletedAt: null,
status: { in: ['PENDING', 'GENERATING_MEDIA', 'RENDERING', 'GENERATING_SCRIPT'] },
},
}),
// Başarısız projeler
this.db.project.count({
where: { userId, status: 'FAILED', deletedAt: null },
}),
// Draft projeler
this.db.project.count({
where: { userId, status: 'DRAFT', deletedAt: null },
}),
// Toplam harcanan kredi
this.db.project.aggregate({
where: { userId, deletedAt: null },
_sum: { creditsUsed: true },
}),
// Son 5 proje
this.db.project.findMany({
where: { userId, deletedAt: null },
orderBy: { createdAt: 'desc' },
take: 5,
select: {
id: true,
title: true,
status: true,
progress: true,
thumbnailUrl: true,
finalVideoUrl: true,
videoStyle: true,
aspectRatio: true,
language: true,
sourceType: true,
createdAt: true,
updatedAt: true,
completedAt: true,
},
}),
// Kullanıcının aktif aboneliği (plan dahil)
this.db.subscription.findFirst({
where: {
userId,
status: { in: ['active', 'trialing'] },
},
include: { plan: true },
orderBy: { createdAt: 'desc' },
}),
// Bu ayki net kredi bakiyesi (CreditTransaction tablosundan)
this.db.creditTransaction.aggregate({
where: {
userId,
createdAt: {
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
},
},
_sum: { amount: true },
}),
]);
// Plan bilgisini aktif abonelikten çek, yoksa free plan varsayılanları
const plan = activeSubscription?.plan;
const currentPlan = plan?.name || 'free';
const monthlyLimit = plan?.monthlyCredits ?? 3;
const maxDuration = plan?.maxDuration ?? 30;
const maxResolution = plan?.maxResolution ?? '720p';
// Kredi hesaplama: CreditTransaction tablosundan kalan bakiye
const creditsUsed = totalCreditsUsed._sum.creditsUsed || 0;
const netCreditBalance = creditBalance._sum.amount || 0;
const creditsRemaining = Math.max(0, netCreditBalance);
return {
totalProjects,
completedVideos,
activeRenderJobs,
failedProjects,
draftProjects,
totalCreditsUsed: creditsUsed,
creditsRemaining,
monthlyLimit,
currentPlan,
maxDuration,
maxResolution,
recentProjects,
};
}
/**
* Kuyruk durumunu BullMQ ve Worker kuyruğundan alır.
*/
async getQueueStatus() {
const queueStats = await this.videoGenerationProducer.getQueueStats();
const wsClients = this.eventsGateway.getConnectedClientsCount();
return {
queue: queueStats,
websocket: {
connectedClients: wsClients,
},
};
}
/**
* Bu ay üretilen videoların gün bazlı dağılımı (chart verisi).
*/
async getMonthlyChart(userId: string) {
const startOfMonth = new Date();
startOfMonth.setDate(1);
startOfMonth.setHours(0, 0, 0, 0);
const projects = await this.db.project.findMany({
where: {
userId,
deletedAt: null,
createdAt: { gte: startOfMonth },
},
select: {
createdAt: true,
status: true,
},
orderBy: { createdAt: 'asc' },
});
// Gün bazlı gruplama
const dailyMap: Record<string, { created: number; completed: number }> = {};
projects.forEach((p) => {
const day = p.createdAt.toISOString().split('T')[0];
if (!dailyMap[day]) dailyMap[day] = { created: 0, completed: 0 };
dailyMap[day].created++;
if (p.status === 'COMPLETED') dailyMap[day].completed++;
});
return Object.entries(dailyMap).map(([date, counts]) => ({
date,
...counts,
}));
}
}