import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class DashboardService { constructor(private readonly prisma: PrismaService) {} async getKpis(tenantId: string) { const today = new Date(); today.setHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const [ activeSubscribers, totalClients, todayPayments, overdueInvoices, newSignups, pendingTickets, ] = await Promise.all([ this.prisma.subscription.count({ where: { tenantId, status: 'active' }, }), this.prisma.client.count({ where: { tenantId, status: 'active' }, }), this.prisma.payment.aggregate({ where: { tenantId, createdAt: { gte: today, lt: tomorrow }, }, _sum: { amount: true }, _count: true, }), this.prisma.invoice.count({ where: { tenantId, status: { in: ['sent', 'partial'] }, dueDate: { lt: today }, }, }), this.prisma.client.count({ where: { tenantId, createdAt: { gte: thirtyDaysAgo }, }, }), this.prisma.ticket.count({ where: { tenantId, status: { in: ['open', 'in_progress'] }, }, }), ]); return { activeSubscribers, totalClients, todayCollections: { amount: Number(todayPayments._sum.amount || 0), count: todayPayments._count, }, overdueAccounts: overdueInvoices, newSignups, pendingTickets, }; } async getRevenueChart(tenantId: string) { const months: { month: string; revenue: number; count: number }[] = []; for (let i = 5; i >= 0; i--) { const start = new Date(); start.setMonth(start.getMonth() - i, 1); start.setHours(0, 0, 0, 0); const end = new Date(start); end.setMonth(end.getMonth() + 1); const result = await this.prisma.payment.aggregate({ where: { tenantId, createdAt: { gte: start, lt: end }, }, _sum: { amount: true }, _count: true, }); months.push({ month: start.toLocaleString('en-US', { month: 'short', year: 'numeric' }), revenue: Number(result._sum.amount || 0), count: result._count, }); } return months; } async getRecentActivity(tenantId: string) { const [recentPayments, recentTickets, recentClients] = await Promise.all([ this.prisma.payment.findMany({ where: { tenantId }, orderBy: { createdAt: 'desc' }, take: 5, include: { client: { select: { firstName: true, lastName: true } }, }, }), this.prisma.ticket.findMany({ where: { tenantId }, orderBy: { createdAt: 'desc' }, take: 5, include: { client: { select: { firstName: true, lastName: true } }, }, }), this.prisma.client.findMany({ where: { tenantId }, orderBy: { createdAt: 'desc' }, take: 5, select: { id: true, firstName: true, lastName: true, accountNumber: true, createdAt: true }, }), ]); return { recentPayments, recentTickets, recentClients }; } async getFinancialSummary(tenantId: string) { const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); // Source of truth: compute all values from journal entries const assetAccounts = await this.prisma.chartOfAccount.findMany({ where: { tenantId, type: 'asset', code: { in: ['1010', '1020', '1030', '1040'] } }, include: { journalLines: { select: { debit: true, credit: true } }, }, }); const cashOnHand = assetAccounts.reduce( (sum, acc) => sum + acc.journalLines.reduce((s, l) => s + Number(l.debit) - Number(l.credit), 0), 0, ); const revenueAccounts = await this.prisma.chartOfAccount.findMany({ where: { tenantId, type: 'revenue' }, include: { journalLines: { where: { journalEntry: { entryDate: { gte: monthStart } } }, select: { credit: true }, }, }, }); const expenseAccounts = await this.prisma.chartOfAccount.findMany({ where: { tenantId, type: 'expense' }, include: { journalLines: { where: { journalEntry: { entryDate: { gte: monthStart } } }, select: { debit: true }, }, }, }); const monthlyIncome = revenueAccounts.reduce( (sum, acc) => sum + acc.journalLines.reduce((s, l) => s + Number(l.credit), 0), 0, ); const monthlyExpenses = expenseAccounts.reduce( (sum, acc) => sum + acc.journalLines.reduce((s, l) => s + Number(l.debit), 0), 0, ); return { cashOnHand, monthlyIncome, monthlyExpenses, netIncome: monthlyIncome - monthlyExpenses, }; } }