initial: standalone repo from monorepo split
This commit is contained in:
185
src/dashboard/dashboard.service.ts
Normal file
185
src/dashboard/dashboard.service.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user