mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
change price logic on the ,rule engine ui, auto generate the contrat
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsIn, IsOptional } from 'class-validator';
|
||||
|
||||
const OVERVIEW_RANGES = ['7d', '30d', '90d'] as const;
|
||||
|
||||
export type OverviewRangeQuery = (typeof OVERVIEW_RANGES)[number];
|
||||
|
||||
export class OverviewQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
enum: OVERVIEW_RANGES,
|
||||
default: '30d',
|
||||
description: 'Time range for trend charts',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(OVERVIEW_RANGES)
|
||||
range?: OverviewRangeQuery = '30d';
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class OverviewBookingKpisDto {
|
||||
@ApiProperty() totalActive!: number;
|
||||
@ApiProperty() needsAction!: number;
|
||||
@ApiProperty() urgent!: number;
|
||||
@ApiProperty() inApproval!: number;
|
||||
@ApiProperty() submittedToday!: number;
|
||||
}
|
||||
|
||||
export class OverviewOperationsKpisDto {
|
||||
@ApiProperty() trainsActive!: number;
|
||||
@ApiProperty() wagonsAvailable!: number;
|
||||
@ApiProperty() containersInTransit!: number;
|
||||
@ApiProperty() cargoesLoaded!: number;
|
||||
}
|
||||
|
||||
export class OverviewCustomerKpisDto {
|
||||
@ApiProperty() totalCustomers!: number;
|
||||
@ApiProperty() newCustomersThisMonth!: number;
|
||||
}
|
||||
|
||||
export class OverviewBillingKpisDto {
|
||||
@ApiProperty() revenueMtdEtb!: number;
|
||||
@ApiProperty() revenueMtdUsd!: number;
|
||||
@ApiProperty() pendingPayments!: number;
|
||||
@ApiProperty() successfulPaymentsMtd!: number;
|
||||
}
|
||||
|
||||
export class OverviewStaffKpisDto {
|
||||
@ApiProperty() activeEmployees!: number;
|
||||
@ApiProperty() activeUsers!: number;
|
||||
}
|
||||
|
||||
export class OverviewKpisDto {
|
||||
@ApiProperty({ type: OverviewBookingKpisDto })
|
||||
bookings!: OverviewBookingKpisDto;
|
||||
|
||||
@ApiProperty({ type: OverviewOperationsKpisDto })
|
||||
operations!: OverviewOperationsKpisDto;
|
||||
|
||||
@ApiProperty({ type: OverviewCustomerKpisDto })
|
||||
customers!: OverviewCustomerKpisDto;
|
||||
|
||||
@ApiProperty({ type: OverviewBillingKpisDto })
|
||||
billing!: OverviewBillingKpisDto;
|
||||
|
||||
@ApiProperty({ type: OverviewStaffKpisDto })
|
||||
staff!: OverviewStaffKpisDto;
|
||||
}
|
||||
|
||||
export class OverviewTrendPointDto {
|
||||
@ApiProperty({ example: '2026-06-01' }) date!: string;
|
||||
@ApiProperty() count!: number;
|
||||
}
|
||||
|
||||
export class OverviewStatusCountDto {
|
||||
@ApiProperty() status!: string;
|
||||
@ApiProperty() count!: number;
|
||||
}
|
||||
|
||||
export class OverviewPipelineCountDto {
|
||||
@ApiProperty() stage!: string;
|
||||
@ApiProperty() count!: number;
|
||||
}
|
||||
|
||||
export class OverviewPaymentTrendPointDto {
|
||||
@ApiProperty({ example: '2026-06-01' }) date!: string;
|
||||
@ApiProperty() amountEtb!: number;
|
||||
@ApiProperty() amountUsd!: number;
|
||||
}
|
||||
|
||||
export class OverviewRecentBookingDto {
|
||||
@ApiProperty() id!: string;
|
||||
@ApiProperty() reference!: string;
|
||||
@ApiProperty() customerLabel!: string;
|
||||
@ApiProperty() status!: string;
|
||||
@ApiProperty() priorityScore!: number;
|
||||
@ApiProperty({ nullable: true }) totalAmount!: number | null;
|
||||
@ApiProperty({ nullable: true }) paymentCurrency!: string | null;
|
||||
@ApiProperty() createdAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewResponseDto {
|
||||
@ApiProperty({ type: OverviewKpisDto })
|
||||
kpis!: OverviewKpisDto;
|
||||
|
||||
@ApiProperty({ type: [OverviewTrendPointDto] })
|
||||
bookingTrend!: OverviewTrendPointDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
bookingsByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewPipelineCountDto] })
|
||||
bookingsByPipeline!: OverviewPipelineCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewPaymentTrendPointDto] })
|
||||
paymentTrend!: OverviewPaymentTrendPointDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewRecentBookingDto] })
|
||||
recentBookings!: OverviewRecentBookingDto[];
|
||||
|
||||
@ApiProperty() generatedAt!: string;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
import {
|
||||
OverviewBillingKpisDto,
|
||||
OverviewBookingKpisDto,
|
||||
OverviewCustomerKpisDto,
|
||||
OverviewOperationsKpisDto,
|
||||
OverviewPaymentTrendPointDto,
|
||||
OverviewPipelineCountDto,
|
||||
OverviewRecentBookingDto,
|
||||
OverviewStaffKpisDto,
|
||||
OverviewStatusCountDto,
|
||||
OverviewTrendPointDto,
|
||||
} from './overview-response.dto';
|
||||
|
||||
export class OverviewLabelCountDto {
|
||||
@ApiProperty() label!: string;
|
||||
@ApiProperty() count!: number;
|
||||
}
|
||||
|
||||
export class OverviewPaymentMethodDto {
|
||||
@ApiProperty() method!: string;
|
||||
@ApiProperty() count!: number;
|
||||
@ApiProperty() amountEtb!: number;
|
||||
@ApiProperty() amountUsd!: number;
|
||||
}
|
||||
|
||||
export class OverviewCurrencyAmountDto {
|
||||
@ApiProperty() currency!: string;
|
||||
@ApiProperty() amount!: number;
|
||||
}
|
||||
|
||||
export class OverviewBookingsTabDto {
|
||||
@ApiProperty({ type: OverviewBookingKpisDto })
|
||||
kpis!: OverviewBookingKpisDto;
|
||||
|
||||
@ApiProperty({ type: [OverviewTrendPointDto] })
|
||||
bookingTrend!: OverviewTrendPointDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
bookingsByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewPipelineCountDto] })
|
||||
bookingsByPipeline!: OverviewPipelineCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
bookingsByFreightType!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
bookingsByCurrency!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewRecentBookingDto] })
|
||||
recentBookings!: OverviewRecentBookingDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewBillingTabDto {
|
||||
@ApiProperty({ type: OverviewBillingKpisDto })
|
||||
kpis!: OverviewBillingKpisDto;
|
||||
|
||||
@ApiProperty({ type: [OverviewPaymentTrendPointDto] })
|
||||
paymentTrend!: OverviewPaymentTrendPointDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
paymentsByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewPaymentMethodDto] })
|
||||
paymentsByMethod!: OverviewPaymentMethodDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewCurrencyAmountDto] })
|
||||
revenueByCurrency!: OverviewCurrencyAmountDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewOperationsTabDto {
|
||||
@ApiProperty({ type: OverviewOperationsKpisDto })
|
||||
kpis!: OverviewOperationsKpisDto;
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
trainStatusBreakdown!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
wagonStatusBreakdown!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
containerStatusBreakdown!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
cargoStatusBreakdown!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewCustomersTabDto {
|
||||
@ApiProperty({ type: OverviewCustomerKpisDto })
|
||||
kpis!: OverviewCustomerKpisDto;
|
||||
|
||||
@ApiProperty({ type: [OverviewTrendPointDto] })
|
||||
customerGrowthTrend!: OverviewTrendPointDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
customersByType!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
topCustomersByBookings!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewStaffTabDto {
|
||||
@ApiProperty({ type: OverviewStaffKpisDto })
|
||||
kpis!: OverviewStaffKpisDto;
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
usersByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewTrendPointDto] })
|
||||
employeeGrowthTrend!: OverviewTrendPointDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
activeUsersBreakdown!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export const OVERVIEW_URGENT_PRIORITY_THRESHOLD = 1000;
|
||||
|
||||
export const OVERVIEW_NEEDS_ACTION_STATUSES = [
|
||||
'SUBMITTED',
|
||||
'PENDING_APPROVAL',
|
||||
'APPROVED_PENDING_SIGNATURE',
|
||||
] as const;
|
||||
|
||||
export const OVERVIEW_IN_APPROVAL_STATUSES = [
|
||||
'PENDING_APPROVAL',
|
||||
'APPROVED_PENDING_SIGNATURE',
|
||||
] as const;
|
||||
|
||||
export const OVERVIEW_CLOSED_STATUSES = [
|
||||
'REJECTED',
|
||||
'CANCELLED',
|
||||
'COMPLETED',
|
||||
] as const;
|
||||
|
||||
export const OVERVIEW_RANGE_DAYS = {
|
||||
'7d': 7,
|
||||
'30d': 30,
|
||||
'90d': 90,
|
||||
} as const;
|
||||
|
||||
export type OverviewRange = keyof typeof OVERVIEW_RANGE_DAYS;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
import { BookingView } from '../../common/booking-guards';
|
||||
import { OverviewQueryDto } from './dto/overview-query.dto';
|
||||
import { OverviewResponseDto } from './dto/overview-response.dto';
|
||||
import {
|
||||
OverviewBillingTabDto,
|
||||
OverviewBookingsTabDto,
|
||||
OverviewCustomersTabDto,
|
||||
OverviewOperationsTabDto,
|
||||
OverviewStaffTabDto,
|
||||
} from './dto/overview-tab-response.dto';
|
||||
import { OverviewService } from './overview.service';
|
||||
|
||||
@ApiTags('Overview')
|
||||
@ApiBearerAuth()
|
||||
@Controller('overview')
|
||||
export class OverviewController {
|
||||
constructor(private readonly overviewService: OverviewService) {}
|
||||
|
||||
@Get()
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Aggregated dashboard summary for backoffice overview' })
|
||||
@ApiOkResponse({ type: OverviewResponseDto })
|
||||
getDashboard(@Query() query: OverviewQueryDto): Promise<OverviewResponseDto> {
|
||||
return this.overviewService.getDashboard(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('bookings')
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Bookings tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewBookingsTabDto })
|
||||
getBookingsTab(@Query() query: OverviewQueryDto): Promise<OverviewBookingsTabDto> {
|
||||
return this.overviewService.getBookingsTab(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('billing')
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Billing tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewBillingTabDto })
|
||||
getBillingTab(@Query() query: OverviewQueryDto): Promise<OverviewBillingTabDto> {
|
||||
return this.overviewService.getBillingTab(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('operations')
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Operations tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewOperationsTabDto })
|
||||
getOperationsTab(): Promise<OverviewOperationsTabDto> {
|
||||
return this.overviewService.getOperationsTab();
|
||||
}
|
||||
|
||||
@Get('customers')
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Customers tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewCustomersTabDto })
|
||||
getCustomersTab(@Query() query: OverviewQueryDto): Promise<OverviewCustomersTabDto> {
|
||||
return this.overviewService.getCustomersTab(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('staff')
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Staff tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewStaffTabDto })
|
||||
getStaffTab(@Query() query: OverviewQueryDto): Promise<OverviewStaffTabDto> {
|
||||
return this.overviewService.getStaffTab(query.range ?? '30d');
|
||||
}
|
||||
}
|
||||
34
apps/edr-freight-api/src/modules/overview/overview.module.ts
Normal file
34
apps/edr-freight-api/src/modules/overview/overview.module.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Employee } from '@tria-plc/iamapi-common';
|
||||
import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity';
|
||||
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { Cargo } from '../cargoes/entities/cargoes.entity';
|
||||
import { Container } from '../container-management/entities/container.entity';
|
||||
import { Customer } from '../customers/entities/customer.entity';
|
||||
import { PaymentEntity } from '../payment/entities/payment.entity';
|
||||
import { Train } from '../trains/entities/train.entity';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import { OverviewController } from './overview.controller';
|
||||
import { OverviewRepository } from './overview.repository';
|
||||
import { OverviewService } from './overview.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Booking,
|
||||
PaymentEntity,
|
||||
Customer,
|
||||
Train,
|
||||
Wagon,
|
||||
Container,
|
||||
Cargo,
|
||||
Employee,
|
||||
User,
|
||||
]),
|
||||
],
|
||||
controllers: [OverviewController],
|
||||
providers: [OverviewService, OverviewRepository],
|
||||
})
|
||||
export class OverviewModule {}
|
||||
553
apps/edr-freight-api/src/modules/overview/overview.repository.ts
Normal file
553
apps/edr-freight-api/src/modules/overview/overview.repository.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { EUserStatus } from '@tria-plc/api-common/utils/enums/user.enum';
|
||||
import { Employee } from '@tria-plc/iamapi-common';
|
||||
import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity';
|
||||
import { Freight } from '@edr/types';
|
||||
import { Repository, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { Cargo } from '../cargoes/entities/cargoes.entity';
|
||||
import { Container } from '../container-management/entities/container.entity';
|
||||
import { Customer } from '../customers/entities/customer.entity';
|
||||
import { PaymentEntity } from '../payment/entities/payment.entity';
|
||||
import { Train } from '../trains/entities/train.entity';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import {
|
||||
OVERVIEW_CLOSED_STATUSES,
|
||||
OVERVIEW_IN_APPROVAL_STATUSES,
|
||||
OVERVIEW_NEEDS_ACTION_STATUSES,
|
||||
OVERVIEW_URGENT_PRIORITY_THRESHOLD,
|
||||
} from './overview.constants';
|
||||
|
||||
export type OverviewBookingKpisRow = {
|
||||
totalActive: number;
|
||||
needsAction: number;
|
||||
urgent: number;
|
||||
inApproval: number;
|
||||
submittedToday: number;
|
||||
};
|
||||
|
||||
export type OverviewRecentBookingRow = {
|
||||
id: string;
|
||||
reference: string;
|
||||
customerLabel: string;
|
||||
status: string;
|
||||
priorityScore: number;
|
||||
totalAmount: number | null;
|
||||
paymentCurrency: string | null;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class OverviewRepository {
|
||||
constructor(
|
||||
@InjectRepository(Booking)
|
||||
private readonly bookingRepository: Repository<Booking>,
|
||||
@InjectRepository(PaymentEntity)
|
||||
private readonly paymentRepository: Repository<PaymentEntity>,
|
||||
@InjectRepository(Customer)
|
||||
private readonly customerRepository: Repository<Customer>,
|
||||
@InjectRepository(Train)
|
||||
private readonly trainRepository: Repository<Train>,
|
||||
@InjectRepository(Wagon)
|
||||
private readonly wagonRepository: Repository<Wagon>,
|
||||
@InjectRepository(Container)
|
||||
private readonly containerRepository: Repository<Container>,
|
||||
@InjectRepository(Cargo)
|
||||
private readonly cargoRepository: Repository<Cargo>,
|
||||
@InjectRepository(Employee)
|
||||
private readonly employeeRepository: Repository<Employee>,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async getBookingKpis(): Promise<OverviewBookingKpisRow> {
|
||||
const row = await this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.select(
|
||||
`COUNT(*) FILTER (WHERE booking.status NOT IN (:...closedStatuses) AND booking.status != 'DRAFT')::int`,
|
||||
'totalActive',
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE booking.status IN (:...needsActionStatuses))::int`,
|
||||
'needsAction',
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE booking.priority_score >= :urgentThreshold)::int`,
|
||||
'urgent',
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE booking.status IN (:...inApprovalStatuses))::int`,
|
||||
'inApproval',
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE booking.created_at >= CURRENT_DATE AND booking.status != 'DRAFT')::int`,
|
||||
'submittedToday',
|
||||
)
|
||||
.where('booking.deleted_at IS NULL')
|
||||
.setParameters({
|
||||
closedStatuses: [...OVERVIEW_CLOSED_STATUSES],
|
||||
needsActionStatuses: [...OVERVIEW_NEEDS_ACTION_STATUSES],
|
||||
inApprovalStatuses: [...OVERVIEW_IN_APPROVAL_STATUSES],
|
||||
urgentThreshold: OVERVIEW_URGENT_PRIORITY_THRESHOLD,
|
||||
})
|
||||
.getRawOne<Record<string, string>>();
|
||||
|
||||
return {
|
||||
totalActive: Number(row?.totalActive ?? 0),
|
||||
needsAction: Number(row?.needsAction ?? 0),
|
||||
urgent: Number(row?.urgent ?? 0),
|
||||
inApproval: Number(row?.inApproval ?? 0),
|
||||
submittedToday: Number(row?.submittedToday ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async getOperationsKpis(): Promise<{
|
||||
trainsActive: number;
|
||||
wagonsAvailable: number;
|
||||
containersInTransit: number;
|
||||
cargoesLoaded: number;
|
||||
}> {
|
||||
const [trainsActive, wagonsAvailable, containersInTransit, cargoesLoaded] =
|
||||
await Promise.all([
|
||||
this.trainRepository
|
||||
.createQueryBuilder('train')
|
||||
.where('train.deleted_at IS NULL')
|
||||
.andWhere('train.status IN (:...statuses)', {
|
||||
statuses: [
|
||||
Freight.TrainStatus.InService,
|
||||
Freight.TrainStatus.Scheduled,
|
||||
],
|
||||
})
|
||||
.getCount(),
|
||||
this.wagonRepository
|
||||
.createQueryBuilder('wagon')
|
||||
.where('wagon.deleted_at IS NULL')
|
||||
.andWhere('wagon.status = :status', { status: 'AVAILABLE' })
|
||||
.getCount(),
|
||||
this.containerRepository
|
||||
.createQueryBuilder('container')
|
||||
.where('container.deleted_at IS NULL')
|
||||
.andWhere('container.status = :status', { status: 'IN_TRANSIT' })
|
||||
.getCount(),
|
||||
this.cargoRepository
|
||||
.createQueryBuilder('cargo')
|
||||
.where('cargo.deleted_at IS NULL')
|
||||
.andWhere('cargo.status IN (:...statuses)', {
|
||||
statuses: ['LOADED', 'IN_TRANSIT'],
|
||||
})
|
||||
.getCount(),
|
||||
]);
|
||||
|
||||
return { trainsActive, wagonsAvailable, containersInTransit, cargoesLoaded };
|
||||
}
|
||||
|
||||
async getCustomerKpis(): Promise<{
|
||||
totalCustomers: number;
|
||||
newCustomersThisMonth: number;
|
||||
}> {
|
||||
const row = await this.customerRepository
|
||||
.createQueryBuilder('customer')
|
||||
.select('COUNT(*)::int', 'totalCustomers')
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE customer.created_at >= date_trunc('month', CURRENT_DATE))::int`,
|
||||
'newCustomersThisMonth',
|
||||
)
|
||||
.where('customer.deleted_at IS NULL')
|
||||
.getRawOne<Record<string, string>>();
|
||||
|
||||
return {
|
||||
totalCustomers: Number(row?.totalCustomers ?? 0),
|
||||
newCustomersThisMonth: Number(row?.newCustomersThisMonth ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async getBillingKpis(): Promise<{
|
||||
revenueMtdEtb: number;
|
||||
revenueMtdUsd: number;
|
||||
pendingPayments: number;
|
||||
successfulPaymentsMtd: number;
|
||||
}> {
|
||||
const revenueRow = await this.paymentRepository
|
||||
.createQueryBuilder('payment')
|
||||
.select(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'ETB'), 0)`,
|
||||
'revenueMtdEtb',
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
'revenueMtdUsd',
|
||||
)
|
||||
.addSelect(`COUNT(*)::int`, 'successfulPaymentsMtd')
|
||||
.where('payment.status = :status', { status: 'success' })
|
||||
.andWhere(
|
||||
`COALESCE(payment.paid_at, payment.created_at) >= date_trunc('month', CURRENT_DATE)`,
|
||||
)
|
||||
.getRawOne<Record<string, string>>();
|
||||
|
||||
const pendingPayments = await this.paymentRepository
|
||||
.createQueryBuilder('payment')
|
||||
.where('payment.status IN (:...statuses)', {
|
||||
statuses: ['action-required', 'processing'],
|
||||
})
|
||||
.getCount();
|
||||
|
||||
return {
|
||||
revenueMtdEtb: Number(revenueRow?.revenueMtdEtb ?? 0),
|
||||
revenueMtdUsd: Number(revenueRow?.revenueMtdUsd ?? 0),
|
||||
pendingPayments,
|
||||
successfulPaymentsMtd: Number(revenueRow?.successfulPaymentsMtd ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async getStaffKpis(): Promise<{ activeEmployees: number; activeUsers: number }> {
|
||||
const [activeEmployees, activeUsers] = await Promise.all([
|
||||
this.employeeRepository.count({
|
||||
where: { isCurrent: true },
|
||||
}),
|
||||
this.userRepository.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: EUserStatus.ACCEPTED,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return { activeEmployees, activeUsers };
|
||||
}
|
||||
|
||||
async getBookingTrend(days: number): Promise<{ date: string; count: number }[]> {
|
||||
const rows = await this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.select(`to_char(booking.created_at::date, 'YYYY-MM-DD')`, 'date')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('booking.deleted_at IS NULL')
|
||||
.andWhere(`booking.created_at >= CURRENT_DATE - :days::int + 1`, { days })
|
||||
.groupBy('booking.created_at::date')
|
||||
.orderBy('booking.created_at::date', 'ASC')
|
||||
.getRawMany<{ date: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
date: row.date,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getStatusCounts(): Promise<Record<string, number>> {
|
||||
const rows = await this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.select('booking.status', 'status')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('booking.deleted_at IS NULL')
|
||||
.groupBy('booking.status')
|
||||
.getRawMany<{ status: string; count: string }>();
|
||||
|
||||
return Object.fromEntries(
|
||||
rows.map((row) => [row.status, Number(row.count)]),
|
||||
);
|
||||
}
|
||||
|
||||
async getPaymentTrend(
|
||||
days: number,
|
||||
): Promise<{ date: string; amountEtb: number; amountUsd: number }[]> {
|
||||
const rows = await this.paymentRepository
|
||||
.createQueryBuilder('payment')
|
||||
.select(
|
||||
`to_char(COALESCE(payment.paid_at, payment.created_at)::date, 'YYYY-MM-DD')`,
|
||||
'date',
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'ETB'), 0)`,
|
||||
'amountEtb',
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
'amountUsd',
|
||||
)
|
||||
.where('payment.status = :status', { status: 'success' })
|
||||
.andWhere(
|
||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
||||
{ days },
|
||||
)
|
||||
.groupBy(`COALESCE(payment.paid_at, payment.created_at)::date`)
|
||||
.orderBy(`COALESCE(payment.paid_at, payment.created_at)::date`, 'ASC')
|
||||
.getRawMany<{ date: string; amountEtb: string; amountUsd: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
date: row.date,
|
||||
amountEtb: Number(row.amountEtb),
|
||||
amountUsd: Number(row.amountUsd),
|
||||
}));
|
||||
}
|
||||
|
||||
async getRecentBookings(limit: number): Promise<OverviewRecentBookingRow[]> {
|
||||
const rows = await this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.leftJoin('booking.company', 'company')
|
||||
.select('booking.id', 'id')
|
||||
.addSelect('booking.reference', 'reference')
|
||||
.addSelect('COALESCE(company.name, \'—\')', 'customerLabel')
|
||||
.addSelect('booking.status', 'status')
|
||||
.addSelect('booking.priority_score', 'priorityScore')
|
||||
.addSelect('booking.total_amount', 'totalAmount')
|
||||
.addSelect('booking.payment_currency', 'paymentCurrency')
|
||||
.addSelect('booking.created_at', 'createdAt')
|
||||
.where('booking.deleted_at IS NULL')
|
||||
.orderBy('booking.created_at', 'DESC')
|
||||
.limit(limit)
|
||||
.getRawMany<{
|
||||
id: string;
|
||||
reference: string;
|
||||
customerLabel: string;
|
||||
status: string;
|
||||
priorityScore: string;
|
||||
totalAmount: string | null;
|
||||
paymentCurrency: string | null;
|
||||
createdAt: Date;
|
||||
}>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
reference: row.reference,
|
||||
customerLabel: row.customerLabel,
|
||||
status: row.status,
|
||||
priorityScore: Number(row.priorityScore),
|
||||
totalAmount: row.totalAmount != null ? Number(row.totalAmount) : null,
|
||||
paymentCurrency: row.paymentCurrency,
|
||||
createdAt: row.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
async getBookingsByFreightType(): Promise<{ label: string; count: number }[]> {
|
||||
const rows = await this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.select('booking.freight_type', 'label')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('booking.deleted_at IS NULL')
|
||||
.andWhere("booking.status != 'DRAFT'")
|
||||
.groupBy('booking.freight_type')
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany<{ label: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
label: row.label,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getBookingsByCurrency(): Promise<{ label: string; count: number }[]> {
|
||||
const rows = await this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.select('booking.payment_currency', 'label')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('booking.deleted_at IS NULL')
|
||||
.andWhere("booking.status != 'DRAFT'")
|
||||
.groupBy('booking.payment_currency')
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany<{ label: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
label: row.label,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getPaymentsByStatus(): Promise<{ status: string; count: number }[]> {
|
||||
const rows = await this.paymentRepository
|
||||
.createQueryBuilder('payment')
|
||||
.select('payment.status', 'status')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.groupBy('payment.status')
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany<{ status: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
status: row.status,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getPaymentsByMethod(): Promise<
|
||||
{ method: string; count: number; amountEtb: number; amountUsd: number }[]
|
||||
> {
|
||||
const rows = await this.paymentRepository
|
||||
.createQueryBuilder('payment')
|
||||
.select('payment.method', 'method')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'ETB' AND payment.status = 'success'), 0)`,
|
||||
'amountEtb',
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD' AND payment.status = 'success'), 0)`,
|
||||
'amountUsd',
|
||||
)
|
||||
.groupBy('payment.method')
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany<{ method: string; count: string; amountEtb: string; amountUsd: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
method: row.method,
|
||||
count: Number(row.count),
|
||||
amountEtb: Number(row.amountEtb),
|
||||
amountUsd: Number(row.amountUsd),
|
||||
}));
|
||||
}
|
||||
|
||||
async getRevenueByCurrency(): Promise<{ currency: string; amount: number }[]> {
|
||||
const rows = await this.paymentRepository
|
||||
.createQueryBuilder('payment')
|
||||
.select('payment.currency', 'currency')
|
||||
.addSelect('COALESCE(SUM(payment.amount), 0)', 'amount')
|
||||
.where('payment.status = :status', { status: 'success' })
|
||||
.andWhere(
|
||||
`COALESCE(payment.paid_at, payment.created_at) >= date_trunc('month', CURRENT_DATE)`,
|
||||
)
|
||||
.groupBy('payment.currency')
|
||||
.getRawMany<{ currency: string; amount: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
currency: row.currency,
|
||||
amount: Number(row.amount),
|
||||
}));
|
||||
}
|
||||
|
||||
async getTrainStatusBreakdown(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusBreakdown(this.trainRepository, 'train');
|
||||
}
|
||||
|
||||
async getWagonStatusBreakdown(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusBreakdown(this.wagonRepository, 'wagon');
|
||||
}
|
||||
|
||||
async getContainerStatusBreakdown(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusBreakdown(this.containerRepository, 'container');
|
||||
}
|
||||
|
||||
async getCargoStatusBreakdown(): Promise<{ status: string; count: number }[]> {
|
||||
return this.statusBreakdown(this.cargoRepository, 'cargo');
|
||||
}
|
||||
|
||||
private async statusBreakdown(
|
||||
repository: Repository<ObjectLiteral>,
|
||||
alias: string,
|
||||
): Promise<{ status: string; count: number }[]> {
|
||||
const rows = await repository
|
||||
.createQueryBuilder(alias)
|
||||
.select(`${alias}.status`, 'status')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where(`${alias}.deleted_at IS NULL`)
|
||||
.groupBy(`${alias}.status`)
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany<{ status: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
status: row.status,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getCustomerGrowthTrend(days: number): Promise<{ date: string; count: number }[]> {
|
||||
const rows = await this.customerRepository
|
||||
.createQueryBuilder('customer')
|
||||
.select(`to_char(customer.created_at::date, 'YYYY-MM-DD')`, 'date')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('customer.deleted_at IS NULL')
|
||||
.andWhere(`customer.created_at >= CURRENT_DATE - :days::int + 1`, { days })
|
||||
.groupBy('customer.created_at::date')
|
||||
.orderBy('customer.created_at::date', 'ASC')
|
||||
.getRawMany<{ date: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
date: row.date,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getCustomersByType(): Promise<{ label: string; count: number }[]> {
|
||||
const rows = await this.customerRepository
|
||||
.createQueryBuilder('customer')
|
||||
.select(`COALESCE(NULLIF(customer.customer_type, ''), 'Unknown')`, 'label')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('customer.deleted_at IS NULL')
|
||||
.groupBy('customer.customer_type')
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany<{ label: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
label: row.label,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getTopCustomersByBookings(limit: number): Promise<{ label: string; count: number }[]> {
|
||||
const rows = await this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.leftJoin('booking.company', 'company')
|
||||
.select(`COALESCE(company.name, 'Unknown')`, 'label')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('booking.deleted_at IS NULL')
|
||||
.andWhere("booking.status != 'DRAFT'")
|
||||
.groupBy('company.name')
|
||||
.orderBy('count', 'DESC')
|
||||
.limit(limit)
|
||||
.getRawMany<{ label: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
label: row.label,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getUsersByStatus(): Promise<{ status: string; count: number }[]> {
|
||||
const rows = await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.select('user.status', 'status')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.groupBy('user.status')
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany<{ status: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
status: row.status,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getEmployeeGrowthTrend(days: number): Promise<{ date: string; count: number }[]> {
|
||||
const rows = await this.employeeRepository
|
||||
.createQueryBuilder('employee')
|
||||
.select(`to_char(employee.created_at::date, 'YYYY-MM-DD')`, 'date')
|
||||
.addSelect('COUNT(*)::int', 'count')
|
||||
.where('employee.is_current = true')
|
||||
.andWhere(`employee.created_at >= CURRENT_DATE - :days::int + 1`, { days })
|
||||
.groupBy('employee.created_at::date')
|
||||
.orderBy('employee.created_at::date', 'ASC')
|
||||
.getRawMany<{ date: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
date: row.date,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async getActiveUsersBreakdown(): Promise<{ label: string; count: number }[]> {
|
||||
const [active, inactive] = await Promise.all([
|
||||
this.userRepository.count({
|
||||
where: { isActive: true, status: EUserStatus.ACCEPTED },
|
||||
}),
|
||||
this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.is_active = false OR user.status != :status', {
|
||||
status: EUserStatus.ACCEPTED,
|
||||
})
|
||||
.getCount(),
|
||||
]);
|
||||
|
||||
return [
|
||||
{ label: 'Active', count: active },
|
||||
{ label: 'Inactive', count: inactive },
|
||||
];
|
||||
}
|
||||
}
|
||||
210
apps/edr-freight-api/src/modules/overview/overview.service.ts
Normal file
210
apps/edr-freight-api/src/modules/overview/overview.service.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
BOOKING_LIST_TABS,
|
||||
mapStatusCountsToTabs,
|
||||
} from '../bookings/booking-list-tabs.config';
|
||||
import type { OverviewRangeQuery } from './dto/overview-query.dto';
|
||||
import type { OverviewResponseDto } from './dto/overview-response.dto';
|
||||
import type {
|
||||
OverviewBillingTabDto,
|
||||
OverviewBookingsTabDto,
|
||||
OverviewCustomersTabDto,
|
||||
OverviewOperationsTabDto,
|
||||
OverviewStaffTabDto,
|
||||
} from './dto/overview-tab-response.dto';
|
||||
import { OVERVIEW_RANGE_DAYS } from './overview.constants';
|
||||
import { OverviewRepository } from './overview.repository';
|
||||
|
||||
@Injectable()
|
||||
export class OverviewService {
|
||||
constructor(private readonly overviewRepository: OverviewRepository) {}
|
||||
|
||||
private mapStatusCounts(statusCounts: Record<string, number>) {
|
||||
const pipelineTabs = mapStatusCountsToTabs(statusCounts);
|
||||
const bookingsByPipeline = BOOKING_LIST_TABS.filter(
|
||||
(tab) => tab.key !== 'all',
|
||||
).map((tab) => ({
|
||||
stage: tab.key,
|
||||
count: pipelineTabs[tab.key],
|
||||
}));
|
||||
|
||||
const bookingsByStatus = Object.entries(statusCounts)
|
||||
.map(([status, count]) => ({ status, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
return { bookingsByPipeline, bookingsByStatus };
|
||||
}
|
||||
|
||||
async getDashboard(range: OverviewRangeQuery = '30d'): Promise<OverviewResponseDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [
|
||||
bookingKpis,
|
||||
operationsKpis,
|
||||
customerKpis,
|
||||
billingKpis,
|
||||
staffKpis,
|
||||
bookingTrend,
|
||||
statusCounts,
|
||||
paymentTrend,
|
||||
recentBookings,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getBookingKpis(),
|
||||
this.overviewRepository.getOperationsKpis(),
|
||||
this.overviewRepository.getCustomerKpis(),
|
||||
this.overviewRepository.getBillingKpis(),
|
||||
this.overviewRepository.getStaffKpis(),
|
||||
this.overviewRepository.getBookingTrend(days),
|
||||
this.overviewRepository.getStatusCounts(),
|
||||
this.overviewRepository.getPaymentTrend(days),
|
||||
this.overviewRepository.getRecentBookings(8),
|
||||
]);
|
||||
|
||||
const { bookingsByPipeline, bookingsByStatus } =
|
||||
this.mapStatusCounts(statusCounts);
|
||||
|
||||
return {
|
||||
kpis: {
|
||||
bookings: bookingKpis,
|
||||
operations: operationsKpis,
|
||||
customers: customerKpis,
|
||||
billing: billingKpis,
|
||||
staff: staffKpis,
|
||||
},
|
||||
bookingTrend,
|
||||
bookingsByStatus,
|
||||
bookingsByPipeline,
|
||||
paymentTrend,
|
||||
recentBookings: recentBookings.map((row) => ({
|
||||
...row,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
})),
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getBookingsTab(range: OverviewRangeQuery = '30d'): Promise<OverviewBookingsTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [
|
||||
kpis,
|
||||
bookingTrend,
|
||||
statusCounts,
|
||||
bookingsByFreightType,
|
||||
bookingsByCurrency,
|
||||
recentBookings,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getBookingKpis(),
|
||||
this.overviewRepository.getBookingTrend(days),
|
||||
this.overviewRepository.getStatusCounts(),
|
||||
this.overviewRepository.getBookingsByFreightType(),
|
||||
this.overviewRepository.getBookingsByCurrency(),
|
||||
this.overviewRepository.getRecentBookings(8),
|
||||
]);
|
||||
|
||||
const { bookingsByPipeline, bookingsByStatus } =
|
||||
this.mapStatusCounts(statusCounts);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
bookingTrend,
|
||||
bookingsByStatus,
|
||||
bookingsByPipeline,
|
||||
bookingsByFreightType,
|
||||
bookingsByCurrency,
|
||||
recentBookings: recentBookings.map((row) => ({
|
||||
...row,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
})),
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getBillingTab(range: OverviewRangeQuery = '30d'): Promise<OverviewBillingTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [kpis, paymentTrend, paymentsByStatus, paymentsByMethod, revenueByCurrency] =
|
||||
await Promise.all([
|
||||
this.overviewRepository.getBillingKpis(),
|
||||
this.overviewRepository.getPaymentTrend(days),
|
||||
this.overviewRepository.getPaymentsByStatus(),
|
||||
this.overviewRepository.getPaymentsByMethod(),
|
||||
this.overviewRepository.getRevenueByCurrency(),
|
||||
]);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
paymentTrend,
|
||||
paymentsByStatus,
|
||||
paymentsByMethod,
|
||||
revenueByCurrency,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getOperationsTab(): Promise<OverviewOperationsTabDto> {
|
||||
const [
|
||||
kpis,
|
||||
trainStatusBreakdown,
|
||||
wagonStatusBreakdown,
|
||||
containerStatusBreakdown,
|
||||
cargoStatusBreakdown,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getOperationsKpis(),
|
||||
this.overviewRepository.getTrainStatusBreakdown(),
|
||||
this.overviewRepository.getWagonStatusBreakdown(),
|
||||
this.overviewRepository.getContainerStatusBreakdown(),
|
||||
this.overviewRepository.getCargoStatusBreakdown(),
|
||||
]);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
trainStatusBreakdown,
|
||||
wagonStatusBreakdown,
|
||||
containerStatusBreakdown,
|
||||
cargoStatusBreakdown,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getCustomersTab(range: OverviewRangeQuery = '30d'): Promise<OverviewCustomersTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [kpis, customerGrowthTrend, customersByType, topCustomersByBookings] =
|
||||
await Promise.all([
|
||||
this.overviewRepository.getCustomerKpis(),
|
||||
this.overviewRepository.getCustomerGrowthTrend(days),
|
||||
this.overviewRepository.getCustomersByType(),
|
||||
this.overviewRepository.getTopCustomersByBookings(8),
|
||||
]);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
customerGrowthTrend,
|
||||
customersByType,
|
||||
topCustomersByBookings,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getStaffTab(range: OverviewRangeQuery = '30d'): Promise<OverviewStaffTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [kpis, usersByStatus, employeeGrowthTrend, activeUsersBreakdown] =
|
||||
await Promise.all([
|
||||
this.overviewRepository.getStaffKpis(),
|
||||
this.overviewRepository.getUsersByStatus(),
|
||||
this.overviewRepository.getEmployeeGrowthTrend(days),
|
||||
this.overviewRepository.getActiveUsersBreakdown(),
|
||||
]);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
usersByStatus,
|
||||
employeeGrowthTrend,
|
||||
activeUsersBreakdown,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user