generated from fahricansecer/boilerplate-be
191 lines
5.2 KiB
TypeScript
191 lines
5.2 KiB
TypeScript
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,
|
||
}));
|
||
}
|
||
}
|