186 lines
5.0 KiB
TypeScript
186 lines
5.0 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|