miinventario-v2/apps/backend/src/modules/admin/services/admin-dashboard.service.ts
rckrdmrd 1a53b5c4d3 [MIINVENTARIO] feat: Initial commit - Sistema de inventario con análisis de video IA
- Backend NestJS con módulos de autenticación, inventario, créditos
- Frontend React con dashboard y componentes UI
- Base de datos PostgreSQL con migraciones
- Tests E2E configurados
- Configuración de Docker y deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 02:25:48 -06:00

207 lines
6.8 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between, MoreThanOrEqual } from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Store } from '../../stores/entities/store.entity';
import { Video } from '../../videos/entities/video.entity';
import { Payment, PaymentStatus } from '../../payments/entities/payment.entity';
import { CreditTransaction, TransactionType } from '../../credits/entities/credit-transaction.entity';
import { DashboardMetrics, RevenueSeriesPoint, DashboardPeriod } from '../dto/dashboard.dto';
@Injectable()
export class AdminDashboardService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Store)
private storeRepository: Repository<Store>,
@InjectRepository(Video)
private videoRepository: Repository<Video>,
@InjectRepository(Payment)
private paymentRepository: Repository<Payment>,
@InjectRepository(CreditTransaction)
private creditTransactionRepository: Repository<CreditTransaction>,
) {}
async getDashboardMetrics(
startDate?: Date,
endDate?: Date,
): Promise<DashboardMetrics> {
const now = new Date();
const start = startDate || new Date(now.getFullYear(), now.getMonth(), 1);
const end = endDate || now;
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const [
totalUsers,
newUsers,
totalStores,
activeStores,
totalVideos,
processedVideos,
payments,
creditTransactions,
mauCount,
dauCount,
] = await Promise.all([
this.userRepository.count(),
this.userRepository.count({
where: { createdAt: Between(start, end) },
}),
this.storeRepository.count(),
this.storeRepository
.createQueryBuilder('store')
.innerJoin('videos', 'video', 'video.storeId = store.id')
.where('video.createdAt >= :start', { start })
.getCount(),
this.videoRepository.count(),
this.videoRepository.count({
where: { createdAt: Between(start, end) },
}),
this.paymentRepository.find({
where: {
status: PaymentStatus.COMPLETED,
createdAt: Between(start, end),
},
relations: ['package'],
}),
this.creditTransactionRepository.find({
where: { createdAt: Between(start, end) },
}),
this.getActiveUsersCount(thirtyDaysAgo),
this.getActiveUsersCount(oneDayAgo),
]);
const revenue = payments.reduce((sum, p) => sum + Number(p.amountMXN), 0);
const revenueByPackage = this.groupRevenueByPackage(payments);
const purchased = creditTransactions
.filter(t => t.type === TransactionType.PURCHASE)
.reduce((sum, t) => sum + t.amount, 0);
const used = creditTransactions
.filter(t => t.type === TransactionType.CONSUMPTION)
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
const gifted = creditTransactions
.filter(t => [TransactionType.REFERRAL_BONUS, TransactionType.PROMO].includes(t.type))
.reduce((sum, t) => sum + t.amount, 0);
// Estimate COGS (placeholder - would integrate with IA provider usage)
const estimatedCogs = revenue * 0.15; // 15% estimate
return {
users: {
total: totalUsers,
mau: mauCount,
dau: dauCount,
newThisPeriod: newUsers,
},
stores: {
total: totalStores,
activeThisPeriod: activeStores,
},
videos: {
total: totalVideos,
processedThisPeriod: processedVideos,
averageProcessingTime: 0, // Would calculate from video metadata
},
revenue: {
total: revenue,
thisPeriod: revenue,
byPackage: revenueByPackage,
},
cogs: {
total: estimatedCogs,
thisPeriod: estimatedCogs,
byProvider: [], // Would integrate with IA provider usage tracking
},
margin: {
gross: revenue - estimatedCogs,
percentage: revenue > 0 ? ((revenue - estimatedCogs) / revenue) * 100 : 0,
},
credits: {
purchased,
used,
gifted,
},
};
}
async getRevenueSeries(
startDate: Date,
endDate: Date,
period: DashboardPeriod = DashboardPeriod.DAY,
): Promise<RevenueSeriesPoint[]> {
const payments = await this.paymentRepository.find({
where: {
status: PaymentStatus.COMPLETED,
createdAt: Between(startDate, endDate),
},
order: { createdAt: 'ASC' },
});
const groupedData = new Map<string, { revenue: number; cogs: number }>();
for (const payment of payments) {
const dateKey = this.getDateKey(payment.createdAt, period);
const existing = groupedData.get(dateKey) || { revenue: 0, cogs: 0 };
existing.revenue += Number(payment.amountMXN);
existing.cogs += Number(payment.amountMXN) * 0.15; // Estimated COGS
groupedData.set(dateKey, existing);
}
return Array.from(groupedData.entries()).map(([date, data]) => ({
date,
revenue: data.revenue,
cogs: data.cogs,
margin: data.revenue - data.cogs,
}));
}
private async getActiveUsersCount(since: Date): Promise<number> {
const result = await this.videoRepository
.createQueryBuilder('video')
.select('COUNT(DISTINCT video.uploadedById)', 'count')
.where('video.createdAt >= :since', { since })
.getRawOne();
return parseInt(result?.count || '0', 10);
}
private groupRevenueByPackage(payments: Payment[]): { packageId: string; name: string; amount: number }[] {
const grouped = new Map<string, { name: string; amount: number }>();
for (const payment of payments) {
if (payment.packageId && payment.package) {
const existing = grouped.get(payment.packageId) || { name: payment.package.name, amount: 0 };
existing.amount += Number(payment.amountMXN);
grouped.set(payment.packageId, existing);
}
}
return Array.from(grouped.entries()).map(([packageId, data]) => ({
packageId,
name: data.name,
amount: data.amount,
}));
}
private getDateKey(date: Date, period: DashboardPeriod): string {
switch (period) {
case DashboardPeriod.DAY:
return date.toISOString().split('T')[0];
case DashboardPeriod.WEEK:
const weekStart = new Date(date);
weekStart.setDate(date.getDate() - date.getDay());
return weekStart.toISOString().split('T')[0];
case DashboardPeriod.MONTH:
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
case DashboardPeriod.YEAR:
return String(date.getFullYear());
default:
return date.toISOString().split('T')[0];
}
}
}