Backoffice portal updates: dashboard, seat management, pricing, audit logging, reporting

This commit is contained in:
Stephanos A
2026-06-14 10:26:22 +03:00
parent ceb7f17e88
commit 20d483ed00
20 changed files with 1995 additions and 405 deletions

View File

@@ -0,0 +1,41 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { AuditService } from '../../common/audit.service';
import { IamGuard } from '../../common/iam-adapter';
@ApiTags('Audit')
@Controller('audit')
@UseGuards(IamGuard)
@ApiBearerAuth('IAM-auth')
export class AuditController {
constructor(private auditService: AuditService) {}
@Get('logs')
@ApiOperation({
summary: 'Get audit logs',
description: 'Retrieve system audit logs with optional filtering',
})
@ApiQuery({ name: 'search', required: false, description: 'Search by user email or entity ID' })
@ApiQuery({ name: 'action', required: false, description: 'Filter by action (CREATE, UPDATE, DELETE, etc.)' })
@ApiQuery({ name: 'entityType', required: false, description: 'Filter by entity type (Booking, Station, etc.)' })
async getLogs(
@Query('search') search?: string,
@Query('action') action?: string,
@Query('entityType') entityType?: string,
) {
const filters = {
search: search || undefined,
action: action || undefined,
entityType: entityType || undefined,
};
const items = await this.auditService.getLogs(filters);
return { items };
}
@Get('logs/:id')
@ApiOperation({ summary: 'Get audit log by ID' })
async getLog(@Param('id') id: string) {
return this.auditService.getLog(id);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { AuditModule } from '../../common/audit.module';
import { AuditController } from './audit.controller';
@Module({
imports: [AuditModule, HttpModule],
controllers: [AuditController],
})
export class AuditModuleFeature {}

View File

@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { AuditModule } from '../../common/audit.module';
import { BookingsController } from './bookings.controller';
import { BookingsService } from './bookings.service';
import { GuestBookingService } from './guest-booking.service';
@@ -8,7 +9,7 @@ import { VerifaydaModule } from '../verifayda/verifayda.module';
import { CurrencyModule } from '../currency/currency.module';
@Module({
imports: [SeatsModule, VerifaydaModule, CurrencyModule, HttpModule],
imports: [AuditModule, SeatsModule, VerifaydaModule, CurrencyModule, HttpModule],
controllers: [BookingsController],
providers: [BookingsService, GuestBookingService],
exports: [BookingsService, GuestBookingService]

View File

@@ -8,7 +8,10 @@ export class ReportsService {
async generateReport(dto: GenerateReportDto) {
const dateFrom = new Date(dto.dateFrom);
dateFrom.setHours(0, 0, 0, 0);
const dateTo = new Date(dto.dateTo);
dateTo.setHours(23, 59, 59, 999);
let data: any;
switch (dto.reportType) {
@@ -44,14 +47,16 @@ export class ReportsService {
}
private async generateRevenueReport(dateFrom: Date, dateTo: Date) {
// Fetch all bookings in date range, regardless of status
const bookings = await this.prisma.booking.findMany({
where: {
createdAt: { gte: dateFrom, lte: dateTo },
status: { in: ['CONFIRMED', 'COMPLETED'] }
createdAt: { gte: dateFrom, lte: dateTo }
},
include: { paymentIntent: true }
});
console.log(`[Reports] Revenue Report: Found ${bookings.length} bookings between ${dateFrom} and ${dateTo}`);
const totalRevenue = bookings.reduce((sum, b) => sum + b.totalMinor, 0);
const byPaymentMethod = bookings.reduce((acc, b) => {
const method = b.paymentIntent?.method ?? 'UNKNOWN';
@@ -59,12 +64,25 @@ export class ReportsService {
return acc;
}, {} as Record<string, number>);
// Group by date for charts
const byDate = bookings.reduce((acc, b) => {
const date = b.createdAt.toISOString().split('T')[0];
if (!acc[date]) {
acc[date] = { totalMinor: 0, count: 0 };
}
acc[date].totalMinor += b.totalMinor;
acc[date].count += 1;
return acc;
}, {} as Record<string, any>);
return {
totalBookings: bookings.length,
totalRevenueMinor: totalRevenue,
totalRevenue: totalRevenue / 100,
currency: 'ETB',
byPaymentMethod
byPaymentMethod,
byDate,
cancellationRate: 0
};
}
@@ -73,7 +91,7 @@ export class ReportsService {
where: { departureAt: { gte: dateFrom, lte: dateTo } },
include: {
coachAssignments: { include: { coach: { include: { seats: true } } } },
bookings: { where: { status: { in: ['CONFIRMED', 'COMPLETED'] } }, include: { seats: true } },
bookings: { include: { seats: true } },
},
});

View File

@@ -1,6 +1,12 @@
import { Module } from '@nestjs/common';
import { AuditModule } from '../../common/audit.module';
import { StationsController } from './stations.controller';
import { StationsService } from './stations.service';
@Module({ controllers: [StationsController], providers: [StationsService], exports: [StationsService] })
@Module({
imports: [AuditModule],
controllers: [StationsController],
providers: [StationsService],
exports: [StationsService],
})
export class StationsModule {}

View File

@@ -1,5 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException, Inject, Optional } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { PrismaService } from '../../common/prisma.service';
import { AuditService } from '../../common/audit.service';
import { CreateStationDto } from './stations.dto';
interface StationFilters {
@@ -10,7 +12,11 @@ interface StationFilters {
@Injectable()
export class StationsService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private auditService: AuditService,
@Optional() @Inject(REQUEST) private request?: any,
) {}
findAll(filters: StationFilters = {}) {
const where: any = {};
@@ -43,20 +49,51 @@ export class StationsService {
return s;
}
create(dto: CreateStationDto) {
return this.prisma.station.create({ data: dto });
async create(dto: CreateStationDto) {
const station = await this.prisma.station.create({ data: dto });
await this.auditService.log({
userId: this.request?.user?.id,
action: 'CREATE',
entityType: 'Station',
entityId: station.id,
newData: station,
});
return station;
}
async update(id: string, dto: Partial<CreateStationDto>) {
await this.findOne(id); // Check if exists
return this.prisma.station.update({
where: { id },
data: dto
const oldStation = await this.findOne(id);
const updatedStation = await this.prisma.station.update({
where: { id },
data: dto,
});
await this.auditService.log({
userId: this.request?.user?.id,
action: 'UPDATE',
entityType: 'Station',
entityId: id,
oldData: oldStation,
newData: updatedStation,
});
return updatedStation;
}
async remove(id: string) {
await this.findOne(id); // Check if exists
return this.prisma.station.delete({ where: { id } });
const station = await this.findOne(id);
const deleted = await this.prisma.station.delete({ where: { id } });
await this.auditService.log({
userId: this.request?.user?.id,
action: 'DELETE',
entityType: 'Station',
entityId: id,
oldData: station,
});
return deleted;
}
}