fixing platform admin
This commit is contained in:
@@ -8,6 +8,29 @@ import { generateFinancialReport, toCsv } from '../services/financialReportServi
|
||||
const router = Router()
|
||||
router.use(requireCompanyAuth, requireTenant, requireSubscription)
|
||||
|
||||
function getRangeFromPeriod(period: string) {
|
||||
const now = new Date()
|
||||
|
||||
switch (period) {
|
||||
case 'WEEKLY': {
|
||||
const start = new Date(now)
|
||||
start.setDate(now.getDate() - 7)
|
||||
return { from: start, to: now }
|
||||
}
|
||||
case 'QUARTERLY': {
|
||||
const start = new Date(now.getFullYear(), now.getMonth() - 2, 1)
|
||||
return { from: start, to: now }
|
||||
}
|
||||
case 'ANNUAL': {
|
||||
const start = new Date(now.getFullYear(), 0, 1)
|
||||
return { from: start, to: now }
|
||||
}
|
||||
case 'MONTHLY':
|
||||
default:
|
||||
return { from: new Date(now.getFullYear(), now.getMonth(), 1), to: now }
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/summary', async (req, res, next) => {
|
||||
try {
|
||||
const { period = '30d' } = req.query as Record<string, string>
|
||||
@@ -25,6 +48,108 @@ router.get('/summary', async (req, res, next) => {
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/dashboard', async (req, res, next) => {
|
||||
try {
|
||||
const now = new Date()
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const prevMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||||
const prevMonthEnd = new Date(monthStart.getTime() - 1)
|
||||
|
||||
const [
|
||||
totalBookings,
|
||||
previousBookings,
|
||||
activeVehicles,
|
||||
previousActiveVehicles,
|
||||
totalCustomers,
|
||||
previousCustomers,
|
||||
monthlyRevenueAgg,
|
||||
previousRevenueAgg,
|
||||
recentReservations,
|
||||
sourceGroups,
|
||||
subscription,
|
||||
] = await Promise.all([
|
||||
prisma.reservation.count({ where: { companyId: req.companyId, createdAt: { gte: monthStart } } }),
|
||||
prisma.reservation.count({ where: { companyId: req.companyId, createdAt: { gte: prevMonthStart, lte: prevMonthEnd } } }),
|
||||
prisma.vehicle.count({ where: { companyId: req.companyId, status: { in: ['AVAILABLE', 'RENTED'] } } }),
|
||||
prisma.vehicle.count({ where: { companyId: req.companyId, createdAt: { lte: prevMonthEnd }, status: { in: ['AVAILABLE', 'RENTED'] } } }),
|
||||
prisma.customer.count({ where: { companyId: req.companyId } }),
|
||||
prisma.customer.count({ where: { companyId: req.companyId, createdAt: { lte: prevMonthEnd } } }),
|
||||
prisma.reservation.aggregate({ where: { companyId: req.companyId, status: { in: ['CONFIRMED', 'ACTIVE', 'COMPLETED'] }, createdAt: { gte: monthStart } }, _sum: { totalAmount: true } }),
|
||||
prisma.reservation.aggregate({ where: { companyId: req.companyId, status: { in: ['CONFIRMED', 'ACTIVE', 'COMPLETED'] }, createdAt: { gte: prevMonthStart, lte: prevMonthEnd } }, _sum: { totalAmount: true } }),
|
||||
prisma.reservation.findMany({
|
||||
where: { companyId: req.companyId },
|
||||
include: { customer: true, vehicle: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 8,
|
||||
}),
|
||||
prisma.reservation.groupBy({
|
||||
by: ['source'],
|
||||
where: { companyId: req.companyId },
|
||||
_count: { id: true },
|
||||
_sum: { totalAmount: true },
|
||||
}),
|
||||
prisma.subscription.findUnique({ where: { companyId: req.companyId } }),
|
||||
])
|
||||
|
||||
const pct = (current: number, previous: number) => {
|
||||
if (previous === 0) return current > 0 ? 100 : 0
|
||||
return Math.round(((current - previous) / previous) * 100)
|
||||
}
|
||||
|
||||
const typedRecentReservations = recentReservations as Array<{
|
||||
id: string
|
||||
contractNumber: string | null
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
status: string
|
||||
totalAmount: number
|
||||
customer: { firstName: string; lastName: string }
|
||||
vehicle: { make: string; model: string }
|
||||
}>
|
||||
|
||||
const typedSourceGroups = sourceGroups as Array<{
|
||||
source: string
|
||||
_count: { id: number }
|
||||
_sum: { totalAmount: number | null }
|
||||
}>
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
kpis: {
|
||||
totalBookings,
|
||||
activeVehicles,
|
||||
monthlyRevenue: monthlyRevenueAgg._sum.totalAmount ?? 0,
|
||||
totalCustomers,
|
||||
bookingsChange: pct(totalBookings, previousBookings),
|
||||
vehiclesChange: pct(activeVehicles, previousActiveVehicles),
|
||||
revenueChange: pct(monthlyRevenueAgg._sum.totalAmount ?? 0, previousRevenueAgg._sum.totalAmount ?? 0),
|
||||
customersChange: pct(totalCustomers, previousCustomers),
|
||||
},
|
||||
recentReservations: typedRecentReservations.map((reservation) => ({
|
||||
id: reservation.id,
|
||||
bookingRef: reservation.contractNumber ?? reservation.id.slice(-8).toUpperCase(),
|
||||
customerName: `${reservation.customer.firstName} ${reservation.customer.lastName}`,
|
||||
vehicleName: `${reservation.vehicle.make} ${reservation.vehicle.model}`,
|
||||
startDate: reservation.startDate,
|
||||
endDate: reservation.endDate,
|
||||
status: reservation.status,
|
||||
totalAmount: reservation.totalAmount,
|
||||
})),
|
||||
sourceBreakdown: typedSourceGroups.map((group) => ({
|
||||
source: group.source,
|
||||
count: group._count.id,
|
||||
revenue: group._sum.totalAmount ?? 0,
|
||||
})),
|
||||
subscription: subscription ? {
|
||||
status: subscription.status,
|
||||
planName: subscription.plan,
|
||||
trialEndsAt: subscription.trialEndAt,
|
||||
} : null,
|
||||
},
|
||||
})
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/sources', async (req, res, next) => {
|
||||
try {
|
||||
const sources = await prisma.reservation.groupBy({ by: ['source'], where: { companyId: req.companyId }, _count: { id: true } })
|
||||
@@ -34,14 +159,17 @@ router.get('/sources', async (req, res, next) => {
|
||||
|
||||
router.get('/report', async (req, res, next) => {
|
||||
try {
|
||||
const { from, to, format = 'JSON' } = req.query as Record<string, string>
|
||||
if (!from || !to) return res.status(400).json({ error: 'missing_params', message: 'from and to date params required', statusCode: 400 })
|
||||
const { from, to, format = 'JSON', period } = req.query as Record<string, string>
|
||||
const accountingSettings = await prisma.accountingSettings.findUnique({ where: { companyId: req.companyId } })
|
||||
const derivedRange = getRangeFromPeriod(period || accountingSettings?.reportingPeriod || 'MONTHLY')
|
||||
const startDate = from ? new Date(from) : derivedRange.from
|
||||
const endDate = to ? new Date(to) : derivedRange.to
|
||||
|
||||
const report = await generateFinancialReport(req.companyId, new Date(from), new Date(to))
|
||||
const report = await generateFinancialReport(req.companyId, startDate, endDate)
|
||||
|
||||
if (format === 'CSV') {
|
||||
res.setHeader('Content-Type', 'text/csv')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="report-${from}-${to}.csv"`)
|
||||
res.setHeader('Content-Disposition', `attachment; filename="report-${startDate.toISOString().slice(0, 10)}-${endDate.toISOString().slice(0, 10)}.csv"`)
|
||||
return res.send(toCsv(report.rows))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user