mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 06:40:57 +00:00
add contracts overview tab with KPIs, recent contracts, and charts
- Introduced OverviewContractKpisDto and OverviewRecentContractDto for contract metrics. - Implemented OverviewContractsTabDto to structure the contracts tab response. - Added contract-related constants for status groupings and pipeline stages. - Created OverviewContractsTabPanel and OverviewRecentContractsTable components for UI representation. - Updated OverviewService and OverviewRepository to fetch contract data. - Integrated contracts tab into OverviewController and OverviewTabContent. - Added hooks for fetching contracts data in useOverview. - Updated types in the overview module to include contracts. - Enhanced the contract detail page with a "View contract" button for generated PDFs.
This commit is contained in:
@@ -8,6 +8,14 @@ export class OverviewBookingKpisDto {
|
||||
@ApiProperty() submittedToday!: number;
|
||||
}
|
||||
|
||||
export class OverviewContractKpisDto {
|
||||
@ApiProperty() totalActive!: number;
|
||||
@ApiProperty() needsAction!: number;
|
||||
@ApiProperty() inApproval!: number;
|
||||
@ApiProperty() inClearance!: number;
|
||||
@ApiProperty() createdToday!: number;
|
||||
}
|
||||
|
||||
export class OverviewOperationsKpisDto {
|
||||
@ApiProperty() trainsActive!: number;
|
||||
@ApiProperty() wagonsAvailable!: number;
|
||||
@@ -36,6 +44,9 @@ export class OverviewKpisDto {
|
||||
@ApiProperty({ type: OverviewBookingKpisDto })
|
||||
bookings!: OverviewBookingKpisDto;
|
||||
|
||||
@ApiProperty({ type: OverviewContractKpisDto })
|
||||
contracts!: OverviewContractKpisDto;
|
||||
|
||||
@ApiProperty({ type: OverviewOperationsKpisDto })
|
||||
operations!: OverviewOperationsKpisDto;
|
||||
|
||||
@@ -81,6 +92,18 @@ export class OverviewRecentBookingDto {
|
||||
@ApiProperty() createdAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewRecentContractDto {
|
||||
@ApiProperty() id!: string;
|
||||
@ApiProperty() reference!: string;
|
||||
@ApiProperty() customerLabel!: string;
|
||||
@ApiProperty() status!: string;
|
||||
@ApiProperty() contractKind!: string;
|
||||
@ApiProperty() freightType!: string;
|
||||
@ApiProperty({ nullable: true }) paymentCurrency!: string | null;
|
||||
@ApiProperty({ nullable: true }) validUntil!: string | null;
|
||||
@ApiProperty() createdAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewResponseDto {
|
||||
@ApiProperty({ type: OverviewKpisDto })
|
||||
kpis!: OverviewKpisDto;
|
||||
|
||||
@@ -3,11 +3,13 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
OverviewBillingKpisDto,
|
||||
OverviewBookingKpisDto,
|
||||
OverviewContractKpisDto,
|
||||
OverviewCustomerKpisDto,
|
||||
OverviewOperationsKpisDto,
|
||||
OverviewPaymentTrendPointDto,
|
||||
OverviewPipelineCountDto,
|
||||
OverviewRecentBookingDto,
|
||||
OverviewRecentContractDto,
|
||||
OverviewStaffKpisDto,
|
||||
OverviewStatusCountDto,
|
||||
OverviewTrendPointDto,
|
||||
@@ -56,6 +58,32 @@ export class OverviewBookingsTabDto {
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewContractsTabDto {
|
||||
@ApiProperty({ type: OverviewContractKpisDto })
|
||||
kpis!: OverviewContractKpisDto;
|
||||
|
||||
@ApiProperty({ type: [OverviewTrendPointDto] })
|
||||
contractTrend!: OverviewTrendPointDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewStatusCountDto] })
|
||||
contractsByStatus!: OverviewStatusCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewPipelineCountDto] })
|
||||
contractsByPipeline!: OverviewPipelineCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
contractsByKind!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewLabelCountDto] })
|
||||
contractsByFreightType!: OverviewLabelCountDto[];
|
||||
|
||||
@ApiProperty({ type: [OverviewRecentContractDto] })
|
||||
recentContracts!: OverviewRecentContractDto[];
|
||||
|
||||
@ApiProperty()
|
||||
generatedAt!: string;
|
||||
}
|
||||
|
||||
export class OverviewBillingTabDto {
|
||||
@ApiProperty({ type: OverviewBillingKpisDto })
|
||||
kpis!: OverviewBillingKpisDto;
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
export const OVERVIEW_URGENT_PRIORITY_THRESHOLD = 70;
|
||||
|
||||
// ── Booking status groupings ─────────────────────────────────────────────────
|
||||
// Derived from the current BookingStatus enum (@edr/types). "needs action" =
|
||||
// anything sitting on a staff queue; "closed" = terminal states.
|
||||
|
||||
export const OVERVIEW_NEEDS_ACTION_STATUSES = [
|
||||
'SUBMITTED',
|
||||
'PENDING_APPROVAL',
|
||||
'APPROVED_PENDING_SIGNATURE',
|
||||
'AWAITING_DOCUMENTS',
|
||||
'DOCUMENTS_UNDER_REVIEW',
|
||||
'OPERATION_REQUEST_PENDING',
|
||||
'OPERATION_CHANGES_REQUESTED',
|
||||
'OPERATION_PRICE_PENDING_CONFIRM',
|
||||
'PENDING_CONSOLIDATION',
|
||||
] as const;
|
||||
|
||||
export const OVERVIEW_IN_APPROVAL_STATUSES = [
|
||||
@@ -15,8 +25,82 @@ export const OVERVIEW_CLOSED_STATUSES = [
|
||||
'REJECTED',
|
||||
'CANCELLED',
|
||||
'COMPLETED',
|
||||
'DELIVERED',
|
||||
'EXPIRED',
|
||||
'CONTRACT_CLOSED',
|
||||
] as const;
|
||||
|
||||
// ── Contract status groupings ────────────────────────────────────────────────
|
||||
// Derived from CONTRACT_STATUSES (@edr/types). Separate machine from bookings.
|
||||
|
||||
export const OVERVIEW_CONTRACT_NEEDS_ACTION_STATUSES = [
|
||||
'SUBMITTED',
|
||||
'PENDING_APPROVAL',
|
||||
'APPROVED_PENDING_SIGNATURE',
|
||||
'PRICE_CHANGED_PENDING_CONFIRM',
|
||||
'CHANGES_REQUESTED',
|
||||
'RENEWAL_SUBMITTED',
|
||||
'RENEWAL_PENDING_APPROVAL',
|
||||
'AMENDMENTS_PROPOSED',
|
||||
] as const;
|
||||
|
||||
export const OVERVIEW_CONTRACT_IN_APPROVAL_STATUSES = [
|
||||
'PENDING_APPROVAL',
|
||||
'APPROVED_PENDING_SIGNATURE',
|
||||
'RENEWAL_PENDING_APPROVAL',
|
||||
] as const;
|
||||
|
||||
export const OVERVIEW_CONTRACT_IN_CLEARANCE_STATUSES = [
|
||||
'AWAITING_CLEARANCE_DOCUMENTS',
|
||||
'CLEARANCE_UNDER_REVIEW',
|
||||
'CLEARANCE_READY_FOR_BOOKING',
|
||||
] as const;
|
||||
|
||||
export const OVERVIEW_CONTRACT_CLOSED_STATUSES = [
|
||||
'CONTRACT_CLOSED',
|
||||
'EXPIRED',
|
||||
'REJECTED',
|
||||
'CANCELLED',
|
||||
'ARCHIVED',
|
||||
] as const;
|
||||
|
||||
/** Contract pipeline stages for the status chart, in workflow order. */
|
||||
export const OVERVIEW_CONTRACT_PIPELINE: ReadonlyArray<{
|
||||
stage: string;
|
||||
statuses: readonly string[];
|
||||
}> = [
|
||||
{ stage: 'draft', statuses: ['DRAFT', 'RENEWAL_DRAFT'] },
|
||||
{
|
||||
stage: 'intake',
|
||||
statuses: ['SUBMITTED', 'RENEWAL_SUBMITTED', 'PRICE_CHANGED_PENDING_CONFIRM', 'CHANGES_REQUESTED', 'AMENDMENTS_PROPOSED'],
|
||||
},
|
||||
{
|
||||
stage: 'in_approval',
|
||||
statuses: ['PENDING_APPROVAL', 'APPROVED', 'APPROVED_PENDING_SIGNATURE', 'RENEWAL_PENDING_APPROVAL'],
|
||||
},
|
||||
{ stage: 'signing', statuses: ['CONTRACT_READY', 'SIGNED_CUSTOMER'] },
|
||||
{
|
||||
stage: 'clearance',
|
||||
statuses: ['AWAITING_CLEARANCE_DOCUMENTS', 'CLEARANCE_UNDER_REVIEW', 'CLEARANCE_READY_FOR_BOOKING'],
|
||||
},
|
||||
{
|
||||
stage: 'active',
|
||||
statuses: ['FULLY_EXECUTED', 'CONTRACT_ACTIVE', 'ACTIVE_SHIPMENT_IN_PROGRESS'],
|
||||
},
|
||||
{ stage: 'closed', statuses: ['CONTRACT_CLOSED', 'EXPIRED', 'ARCHIVED'] },
|
||||
{ stage: 'cancelled', statuses: ['REJECTED', 'CANCELLED'] },
|
||||
];
|
||||
|
||||
/** Map a raw status→count record onto the contract pipeline stages. */
|
||||
export function mapContractStatusCountsToPipeline(
|
||||
statusCounts: Record<string, number>,
|
||||
): Array<{ stage: string; count: number }> {
|
||||
return OVERVIEW_CONTRACT_PIPELINE.map(({ stage, statuses }) => ({
|
||||
stage,
|
||||
count: statuses.reduce((sum, s) => sum + (statusCounts[s] ?? 0), 0),
|
||||
}));
|
||||
}
|
||||
|
||||
export const OVERVIEW_RANGE_DAYS = {
|
||||
'7d': 7,
|
||||
'30d': 30,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { OverviewResponseDto } from './dto/overview-response.dto';
|
||||
import {
|
||||
OverviewBillingTabDto,
|
||||
OverviewBookingsTabDto,
|
||||
OverviewContractsTabDto,
|
||||
OverviewCustomersTabDto,
|
||||
OverviewOperationsTabDto,
|
||||
OverviewStaffTabDto,
|
||||
@@ -40,6 +41,14 @@ export class OverviewController {
|
||||
return this.overviewService.getBookingsTab(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('contracts')
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Contracts tab metrics and charts' })
|
||||
@ApiOkResponse({ type: OverviewContractsTabDto })
|
||||
getContractsTab(@Query() query: OverviewQueryDto): Promise<OverviewContractsTabDto> {
|
||||
return this.overviewService.getContractsTab(query.range ?? '30d');
|
||||
}
|
||||
|
||||
@Get('billing')
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: 'Billing tab metrics and charts' })
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Booking } from "../bookings/entities/booking.entity";
|
||||
import { Cargo } from "../cargoes/entities/cargoes.entity";
|
||||
import { Container } from "../container-management/entities/container.entity";
|
||||
import { Company } from "../companies/entities/company.entity";
|
||||
import { Contract } from "../contracts/entities/contract.entity";
|
||||
import { PaymentEntity } from "../payment/entities/payment.entity";
|
||||
import { Train } from "../trains/entities/train.entity";
|
||||
import { Wagon } from "../wagons/entities/wagon.entity";
|
||||
@@ -24,6 +25,7 @@ import { OverviewService } from "./overview.service";
|
||||
Wagon,
|
||||
Container,
|
||||
Cargo,
|
||||
Contract,
|
||||
Employee,
|
||||
User,
|
||||
]),
|
||||
|
||||
@@ -9,17 +9,26 @@ 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 { Contract } from "../contracts/entities/contract.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_CONTRACT_CLOSED_STATUSES,
|
||||
OVERVIEW_CONTRACT_IN_APPROVAL_STATUSES,
|
||||
OVERVIEW_CONTRACT_IN_CLEARANCE_STATUSES,
|
||||
OVERVIEW_CONTRACT_NEEDS_ACTION_STATUSES,
|
||||
OVERVIEW_IN_APPROVAL_STATUSES,
|
||||
OVERVIEW_NEEDS_ACTION_STATUSES,
|
||||
OVERVIEW_URGENT_PRIORITY_THRESHOLD,
|
||||
} from "./overview.constants";
|
||||
import { Company } from "../companies/entities/company.entity";
|
||||
|
||||
/** Bookings carry a contract_kind column; GENERAL = umbrella contract row, not a shipment. */
|
||||
const EXCLUDE_GENERAL_CONTRACT_BOOKINGS =
|
||||
"(booking.contract_kind IS NULL OR booking.contract_kind <> 'GENERAL')";
|
||||
|
||||
export type OverviewBookingKpisRow = {
|
||||
totalActive: number;
|
||||
needsAction: number;
|
||||
@@ -39,6 +48,26 @@ export type OverviewRecentBookingRow = {
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
export type OverviewContractKpisRow = {
|
||||
totalActive: number;
|
||||
needsAction: number;
|
||||
inApproval: number;
|
||||
inClearance: number;
|
||||
createdToday: number;
|
||||
};
|
||||
|
||||
export type OverviewRecentContractRow = {
|
||||
id: string;
|
||||
reference: string;
|
||||
customerLabel: string;
|
||||
status: string;
|
||||
contractKind: string;
|
||||
freightType: string;
|
||||
paymentCurrency: string | null;
|
||||
validUntil: Date | null;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class OverviewRepository {
|
||||
constructor(
|
||||
@@ -56,6 +85,8 @@ export class OverviewRepository {
|
||||
private readonly containerRepository: Repository<Container>,
|
||||
@InjectRepository(Cargo)
|
||||
private readonly cargoRepository: Repository<Cargo>,
|
||||
@InjectRepository(Contract)
|
||||
private readonly contractRepository: Repository<Contract>,
|
||||
@InjectRepository(Employee)
|
||||
private readonly employeeRepository: Repository<Employee>,
|
||||
@InjectRepository(User)
|
||||
@@ -86,6 +117,7 @@ export class OverviewRepository {
|
||||
"submittedToday",
|
||||
)
|
||||
.where("booking.deleted_at IS NULL")
|
||||
.andWhere(EXCLUDE_GENERAL_CONTRACT_BOOKINGS)
|
||||
.setParameters({
|
||||
closedStatuses: [...OVERVIEW_CLOSED_STATUSES],
|
||||
needsActionStatuses: [...OVERVIEW_NEEDS_ACTION_STATUSES],
|
||||
@@ -235,6 +267,7 @@ export class OverviewRepository {
|
||||
.select(`to_char(booking.created_at::date, 'YYYY-MM-DD')`, "date")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("booking.deleted_at IS NULL")
|
||||
.andWhere(EXCLUDE_GENERAL_CONTRACT_BOOKINGS)
|
||||
.andWhere(`booking.created_at >= CURRENT_DATE - :days::int + 1`, { days })
|
||||
.groupBy("booking.created_at::date")
|
||||
.orderBy("booking.created_at::date", "ASC")
|
||||
@@ -252,6 +285,7 @@ export class OverviewRepository {
|
||||
.select("booking.status", "status")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("booking.deleted_at IS NULL")
|
||||
.andWhere(EXCLUDE_GENERAL_CONTRACT_BOOKINGS)
|
||||
.groupBy("booking.status")
|
||||
.getRawMany<{ status: string; count: string }>();
|
||||
|
||||
@@ -306,6 +340,7 @@ export class OverviewRepository {
|
||||
.addSelect("booking.payment_currency", "paymentCurrency")
|
||||
.addSelect("booking.created_at", "createdAt")
|
||||
.where("booking.deleted_at IS NULL")
|
||||
.andWhere(EXCLUDE_GENERAL_CONTRACT_BOOKINGS)
|
||||
.orderBy("booking.created_at", "DESC")
|
||||
.limit(limit)
|
||||
.getRawMany<{
|
||||
@@ -339,6 +374,7 @@ export class OverviewRepository {
|
||||
.select("booking.freight_type", "label")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("booking.deleted_at IS NULL")
|
||||
.andWhere(EXCLUDE_GENERAL_CONTRACT_BOOKINGS)
|
||||
.andWhere("booking.status != 'DRAFT'")
|
||||
.groupBy("booking.freight_type")
|
||||
.orderBy("count", "DESC")
|
||||
@@ -356,6 +392,7 @@ export class OverviewRepository {
|
||||
.select("booking.payment_currency", "label")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("booking.deleted_at IS NULL")
|
||||
.andWhere(EXCLUDE_GENERAL_CONTRACT_BOOKINGS)
|
||||
.andWhere("booking.status != 'DRAFT'")
|
||||
.groupBy("booking.payment_currency")
|
||||
.orderBy("count", "DESC")
|
||||
@@ -592,4 +629,142 @@ export class OverviewRepository {
|
||||
{ label: "Inactive", count: inactive },
|
||||
];
|
||||
}
|
||||
|
||||
// ── Contracts (overview Contract tab) ──────────────────────────────────────
|
||||
|
||||
async getContractKpis(): Promise<OverviewContractKpisRow> {
|
||||
const row = await this.contractRepository
|
||||
.createQueryBuilder("contract")
|
||||
.select(
|
||||
`COUNT(*) FILTER (WHERE contract.status NOT IN (:...closedStatuses) AND contract.status != 'DRAFT')::int`,
|
||||
"totalActive",
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE contract.status IN (:...needsActionStatuses))::int`,
|
||||
"needsAction",
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE contract.status IN (:...inApprovalStatuses))::int`,
|
||||
"inApproval",
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE contract.status IN (:...inClearanceStatuses))::int`,
|
||||
"inClearance",
|
||||
)
|
||||
.addSelect(
|
||||
`COUNT(*) FILTER (WHERE contract.created_at >= CURRENT_DATE AND contract.status != 'DRAFT')::int`,
|
||||
"createdToday",
|
||||
)
|
||||
.where("contract.deleted_at IS NULL")
|
||||
.setParameters({
|
||||
closedStatuses: [...OVERVIEW_CONTRACT_CLOSED_STATUSES],
|
||||
needsActionStatuses: [...OVERVIEW_CONTRACT_NEEDS_ACTION_STATUSES],
|
||||
inApprovalStatuses: [...OVERVIEW_CONTRACT_IN_APPROVAL_STATUSES],
|
||||
inClearanceStatuses: [...OVERVIEW_CONTRACT_IN_CLEARANCE_STATUSES],
|
||||
})
|
||||
.getRawOne<Record<string, string>>();
|
||||
|
||||
return {
|
||||
totalActive: Number(row?.totalActive ?? 0),
|
||||
needsAction: Number(row?.needsAction ?? 0),
|
||||
inApproval: Number(row?.inApproval ?? 0),
|
||||
inClearance: Number(row?.inClearance ?? 0),
|
||||
createdToday: Number(row?.createdToday ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async getContractStatusCounts(): Promise<Record<string, number>> {
|
||||
const rows = await this.contractRepository
|
||||
.createQueryBuilder("contract")
|
||||
.select("contract.status", "status")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("contract.deleted_at IS NULL")
|
||||
.groupBy("contract.status")
|
||||
.getRawMany<{ status: string; count: string }>();
|
||||
|
||||
return Object.fromEntries(rows.map((row) => [row.status, Number(row.count)]));
|
||||
}
|
||||
|
||||
async getContractTrend(days: number): Promise<{ date: string; count: number }[]> {
|
||||
const rows = await this.contractRepository
|
||||
.createQueryBuilder("contract")
|
||||
.select(`to_char(contract.created_at::date, 'YYYY-MM-DD')`, "date")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("contract.deleted_at IS NULL")
|
||||
.andWhere(`contract.created_at >= CURRENT_DATE - :days::int + 1`, { days })
|
||||
.groupBy("contract.created_at::date")
|
||||
.orderBy("contract.created_at::date", "ASC")
|
||||
.getRawMany<{ date: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({ date: row.date, count: Number(row.count) }));
|
||||
}
|
||||
|
||||
async getContractsByKind(): Promise<{ label: string; count: number }[]> {
|
||||
const rows = await this.contractRepository
|
||||
.createQueryBuilder("contract")
|
||||
.select("contract.contract_kind", "label")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("contract.deleted_at IS NULL")
|
||||
.andWhere("contract.status != 'DRAFT'")
|
||||
.groupBy("contract.contract_kind")
|
||||
.orderBy("count", "DESC")
|
||||
.getRawMany<{ label: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({ label: row.label, count: Number(row.count) }));
|
||||
}
|
||||
|
||||
async getContractsByFreightType(): Promise<{ label: string; count: number }[]> {
|
||||
const rows = await this.contractRepository
|
||||
.createQueryBuilder("contract")
|
||||
.select("contract.freight_type", "label")
|
||||
.addSelect("COUNT(*)::int", "count")
|
||||
.where("contract.deleted_at IS NULL")
|
||||
.andWhere("contract.status != 'DRAFT'")
|
||||
.groupBy("contract.freight_type")
|
||||
.orderBy("count", "DESC")
|
||||
.getRawMany<{ label: string; count: string }>();
|
||||
|
||||
return rows.map((row) => ({ label: row.label, count: Number(row.count) }));
|
||||
}
|
||||
|
||||
async getRecentContracts(limit: number): Promise<OverviewRecentContractRow[]> {
|
||||
const rows = await this.contractRepository
|
||||
.createQueryBuilder("contract")
|
||||
.leftJoin("contract.company", "company")
|
||||
.select("contract.id", "id")
|
||||
.addSelect("contract.reference", "reference")
|
||||
.addSelect("COALESCE(company.name, '—')", "customerLabel")
|
||||
.addSelect("contract.status", "status")
|
||||
.addSelect("contract.contract_kind", "contractKind")
|
||||
.addSelect("contract.freight_type", "freightType")
|
||||
.addSelect("contract.payment_currency", "paymentCurrency")
|
||||
.addSelect("contract.contract_valid_until", "validUntil")
|
||||
.addSelect("contract.created_at", "createdAt")
|
||||
.where("contract.deleted_at IS NULL")
|
||||
.orderBy("contract.created_at", "DESC")
|
||||
.limit(limit)
|
||||
.getRawMany<{
|
||||
id: string;
|
||||
reference: string;
|
||||
customerLabel: string;
|
||||
status: string;
|
||||
contractKind: string;
|
||||
freightType: string;
|
||||
paymentCurrency: string | null;
|
||||
validUntil: Date | null;
|
||||
createdAt: Date;
|
||||
}>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
reference: row.reference,
|
||||
customerLabel: row.customerLabel,
|
||||
status: row.status,
|
||||
contractKind: row.contractKind,
|
||||
freightType: row.freightType,
|
||||
paymentCurrency: row.paymentCurrency,
|
||||
validUntil: row.validUntil,
|
||||
createdAt: row.createdAt,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,15 @@ import type { OverviewResponseDto } from './dto/overview-response.dto';
|
||||
import type {
|
||||
OverviewBillingTabDto,
|
||||
OverviewBookingsTabDto,
|
||||
OverviewContractsTabDto,
|
||||
OverviewCustomersTabDto,
|
||||
OverviewOperationsTabDto,
|
||||
OverviewStaffTabDto,
|
||||
} from './dto/overview-tab-response.dto';
|
||||
import { OVERVIEW_RANGE_DAYS } from './overview.constants';
|
||||
import {
|
||||
OVERVIEW_RANGE_DAYS,
|
||||
mapContractStatusCountsToPipeline,
|
||||
} from './overview.constants';
|
||||
import { OverviewRepository } from './overview.repository';
|
||||
|
||||
@Injectable()
|
||||
@@ -41,6 +45,7 @@ export class OverviewService {
|
||||
|
||||
const [
|
||||
bookingKpis,
|
||||
contractKpis,
|
||||
operationsKpis,
|
||||
customerKpis,
|
||||
billingKpis,
|
||||
@@ -51,6 +56,7 @@ export class OverviewService {
|
||||
recentBookings,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getBookingKpis(),
|
||||
this.overviewRepository.getContractKpis(),
|
||||
this.overviewRepository.getOperationsKpis(),
|
||||
this.overviewRepository.getCustomerKpis(),
|
||||
this.overviewRepository.getBillingKpis(),
|
||||
@@ -67,6 +73,7 @@ export class OverviewService {
|
||||
return {
|
||||
kpis: {
|
||||
bookings: bookingKpis,
|
||||
contracts: contractKpis,
|
||||
operations: operationsKpis,
|
||||
customers: customerKpis,
|
||||
billing: billingKpis,
|
||||
@@ -121,6 +128,48 @@ export class OverviewService {
|
||||
};
|
||||
}
|
||||
|
||||
async getContractsTab(
|
||||
range: OverviewRangeQuery = '30d',
|
||||
): Promise<OverviewContractsTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
const [
|
||||
kpis,
|
||||
contractTrend,
|
||||
statusCounts,
|
||||
contractsByKind,
|
||||
contractsByFreightType,
|
||||
recentContracts,
|
||||
] = await Promise.all([
|
||||
this.overviewRepository.getContractKpis(),
|
||||
this.overviewRepository.getContractTrend(days),
|
||||
this.overviewRepository.getContractStatusCounts(),
|
||||
this.overviewRepository.getContractsByKind(),
|
||||
this.overviewRepository.getContractsByFreightType(),
|
||||
this.overviewRepository.getRecentContracts(8),
|
||||
]);
|
||||
|
||||
const contractsByStatus = Object.entries(statusCounts)
|
||||
.map(([status, count]) => ({ status, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
const contractsByPipeline = mapContractStatusCountsToPipeline(statusCounts);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
contractTrend,
|
||||
contractsByStatus,
|
||||
contractsByPipeline,
|
||||
contractsByKind,
|
||||
contractsByFreightType,
|
||||
recentContracts: recentContracts.map((row) => ({
|
||||
...row,
|
||||
validUntil: row.validUntil ? row.validUntil.toISOString() : null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
})),
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getBillingTab(range: OverviewRangeQuery = '30d'): Promise<OverviewBillingTabDto> {
|
||||
const days = OVERVIEW_RANGE_DAYS[range];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user