initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:36:55 +08:00
commit 4a8f1e9318
157 changed files with 9198 additions and 0 deletions

View 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,
};
}
}