mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 22:25:42 +00:00
- Updated CompanyInfoResponseDto to include company profile data in response. - Enhanced ResponseExternalProfileDto to include active profile type and onboarding details. - Added new fields to ExternalProfile entity for active profile type and onboarding status. - Implemented onboarding wizard dialog in the frontend to guide users through the onboarding process. - Introduced API endpoints for managing company profiles and onboarding steps. - Created migrations to add new columns for active mode and onboarding status in the database. - Added functionality to switch between operational modes (importer/exporter) and create profiles as needed. - Improved user experience by ensuring onboarding is enforced for new users and those without completed profiles.
150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository, SelectQueryBuilder } from 'typeorm';
|
|
|
|
import { Booking } from '../bookings/entities/booking.entity';
|
|
|
|
/** Booking statuses that represent a delivered/finished shipment. */
|
|
const DELIVERED_STATUSES = ['DELIVERED', 'COMPLETED'] as const;
|
|
|
|
/**
|
|
* Statuses that represent real, committed freight (excludes drafts and dead
|
|
* bookings) — used for tonnage so cancelled/expired drafts don't inflate volume.
|
|
*/
|
|
const COMMITTED_STATUSES = [
|
|
'APPROVED',
|
|
'CONTRACT_READY',
|
|
'SIGNED_CUSTOMER',
|
|
'FULLY_EXECUTED',
|
|
'SELECTED_FOR_BATCH',
|
|
'PNR_GENERATED',
|
|
'PAYMENT_VERIFICATION_IN_PROGRESS',
|
|
'PAID',
|
|
'IN_TRANSIT',
|
|
'COMPLETED',
|
|
'DELIVERED',
|
|
'CONSOLIDATED',
|
|
] as const;
|
|
|
|
export interface CurrencyTotal {
|
|
currency: string;
|
|
total: number;
|
|
}
|
|
|
|
/**
|
|
* What the dashboard is scoped to: a single operational profile (the active
|
|
* importer/exporter mode) when one resolves, otherwise the whole company
|
|
* (legacy / not-yet-onboarded fallback).
|
|
*/
|
|
export type DashboardScope =
|
|
| { companyProfileId: string }
|
|
| { companyId: string };
|
|
|
|
/** Apply the scope as a WHERE clause on a bookings query builder. */
|
|
function applyScope(
|
|
qb: SelectQueryBuilder<Booking>,
|
|
scope: DashboardScope,
|
|
): SelectQueryBuilder<Booking> {
|
|
return 'companyProfileId' in scope
|
|
? qb.where('b.company_profile_id = :companyProfileId', {
|
|
companyProfileId: scope.companyProfileId,
|
|
})
|
|
: qb.where('b.company_id = :companyId', { companyId: scope.companyId });
|
|
}
|
|
|
|
export interface MonthlyTonnage {
|
|
year: number;
|
|
month: number; // 1-12
|
|
tonnes: number;
|
|
}
|
|
|
|
/**
|
|
* Read-only aggregation queries against the bookings table, scoped to a
|
|
* company, that back the portal dashboard. Lives in the companies module so it
|
|
* can be exposed via `companies.controller` without a circular dependency on
|
|
* BookingsModule (which already imports CompaniesModule).
|
|
*/
|
|
@Injectable()
|
|
export class CompanyDashboardRepository {
|
|
constructor(
|
|
@InjectRepository(Booking)
|
|
private readonly bookings: Repository<Booking>,
|
|
) {}
|
|
|
|
/** Count of delivered/completed bookings within [from, to) for the scope. */
|
|
async countDelivered(scope: DashboardScope, from: Date, to: Date): Promise<number> {
|
|
return applyScope(this.bookings.createQueryBuilder('b'), scope)
|
|
.andWhere('b.deleted_at IS NULL')
|
|
.andWhere('b.status IN (:...statuses)', { statuses: [...DELIVERED_STATUSES] })
|
|
.andWhere('b.created_at >= :from AND b.created_at < :to', { from, to })
|
|
.getCount();
|
|
}
|
|
|
|
/** Count of committed (non-draft, non-dead) bookings within [from, to) for the scope. */
|
|
async countCommitted(scope: DashboardScope, from: Date, to: Date): Promise<number> {
|
|
return applyScope(this.bookings.createQueryBuilder('b'), scope)
|
|
.andWhere('b.deleted_at IS NULL')
|
|
.andWhere('b.status IN (:...statuses)', { statuses: [...COMMITTED_STATUSES] })
|
|
.andWhere('b.created_at >= :from AND b.created_at < :to', { from, to })
|
|
.getCount();
|
|
}
|
|
|
|
/** Sum of paid booking totals, grouped by currency, within [from, to) for the scope. */
|
|
async sumPaidSpendByCurrency(scope: DashboardScope, from: Date, to: Date): Promise<CurrencyTotal[]> {
|
|
const rows = await applyScope(
|
|
this.bookings
|
|
.createQueryBuilder('b')
|
|
.select('b.payment_currency', 'currency')
|
|
.addSelect('COALESCE(SUM(b.total_amount), 0)', 'total'),
|
|
scope,
|
|
)
|
|
.andWhere('b.deleted_at IS NULL')
|
|
.andWhere("b.payment_status = 'PAID'")
|
|
.andWhere('b.created_at >= :from AND b.created_at < :to', { from, to })
|
|
.groupBy('b.payment_currency')
|
|
.getRawMany<{ currency: string; total: string }>();
|
|
|
|
return rows.map((r) => ({ currency: r.currency ?? 'ETB', total: Number(r.total) }));
|
|
}
|
|
|
|
/** Total committed tonnage (cargo VGM) within [from, to) for the scope. */
|
|
async sumCommittedTonnage(scope: DashboardScope, from: Date, to: Date): Promise<number> {
|
|
const row = await applyScope(
|
|
this.bookings
|
|
.createQueryBuilder('b')
|
|
.select('COALESCE(SUM(b.cargo_total_weight_vgm), 0)', 'total'),
|
|
scope,
|
|
)
|
|
.andWhere('b.deleted_at IS NULL')
|
|
.andWhere('b.status IN (:...statuses)', { statuses: [...COMMITTED_STATUSES] })
|
|
.andWhere('b.created_at >= :from AND b.created_at < :to', { from, to })
|
|
.getRawOne<{ total: string }>();
|
|
|
|
return Number(row?.total ?? 0);
|
|
}
|
|
|
|
/** Committed tonnage grouped by calendar month within [from, to) for the scope. */
|
|
async monthlyCommittedTonnage(scope: DashboardScope, from: Date, to: Date): Promise<MonthlyTonnage[]> {
|
|
const rows = await applyScope(
|
|
this.bookings
|
|
.createQueryBuilder('b')
|
|
.select('EXTRACT(YEAR FROM b.created_at)', 'year')
|
|
.addSelect('EXTRACT(MONTH FROM b.created_at)', 'month')
|
|
.addSelect('COALESCE(SUM(b.cargo_total_weight_vgm), 0)', 'total'),
|
|
scope,
|
|
)
|
|
.andWhere('b.deleted_at IS NULL')
|
|
.andWhere('b.status IN (:...statuses)', { statuses: [...COMMITTED_STATUSES] })
|
|
.andWhere('b.created_at >= :from AND b.created_at < :to', { from, to })
|
|
.groupBy('year')
|
|
.addGroupBy('month')
|
|
.getRawMany<{ year: string; month: string; total: string }>();
|
|
|
|
return rows.map((r) => ({
|
|
year: Number(r.year),
|
|
month: Number(r.month),
|
|
tonnes: Number(r.total),
|
|
}));
|
|
}
|
|
}
|