From 9afa23cc47604426f50f70808b687ee146315d05 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 16 Jun 2026 13:06:14 +0000 Subject: [PATCH] feat: add dashboard summary endpoint and related DTOs for company KPIs --- .../modules/companies/companies.controller.ts | 7 + .../src/modules/companies/companies.module.ts | 6 +- .../modules/companies/companies.service.ts | 119 +++++++++++++++++ .../companies/company-dashboard.repository.ts | 126 ++++++++++++++++++ .../dto/dashboard-summary-response.dto.ts | 59 ++++++++ .../portal/src/constants/URLS.ts | 1 + .../portal/src/lib/currentCustomer.ts | 14 +- .../portal/src/pages/MyPortalPage.tsx | 125 ++++++++++------- .../portal/src/services/api.ts | 7 + .../portal/src/services/companies.service.ts | 27 ++++ 10 files changed, 431 insertions(+), 60 deletions(-) create mode 100644 apps/edr-freight-api/src/modules/companies/company-dashboard.repository.ts create mode 100644 apps/edr-freight-api/src/modules/companies/dto/dashboard-summary-response.dto.ts diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index b1481d0d4..5b38c4d65 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -15,6 +15,7 @@ import { ResponseFFClientDto } from './dto/response-ff-client.dto'; import { CompanyInfoResponseDto } from './dto/company-info-response.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { ProfileResponseDto } from './dto/profile-response.dto'; +import { DashboardSummaryResponseDto } from './dto/dashboard-summary-response.dto'; interface CurrentIamUser { id: string; @@ -45,6 +46,12 @@ export class CompaniesController { return new ProfileResponseDto(profile, company); } + @Get('dashboard') + @ApiOperation({ summary: 'Get portal dashboard KPIs (delivered, spend, freight volume) for the current user' }) + async getDashboard(@CurrentUser() user: CurrentIamUser): Promise { + return this.companiesService.getDashboardSummary(user.id); + } + @Patch('profile') @ApiOperation({ summary: 'Update profile (flattened settings page)' }) async updateProfile( diff --git a/apps/edr-freight-api/src/modules/companies/companies.module.ts b/apps/edr-freight-api/src/modules/companies/companies.module.ts index d18573460..406c4f509 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.module.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.module.ts @@ -6,14 +6,16 @@ import { CompaniesService } from './companies.service'; import { CompaniesRepository } from './companies.repository'; import { ExternalProfileRepository } from './external-profile.repository'; import { FFClientRepository } from './ff-client.repository'; +import { CompanyDashboardRepository } from './company-dashboard.repository'; import { Company } from './entities/company.entity'; import { ExternalProfile } from './entities/external-profile.entity'; import { FFClient } from './entities/ff-client.entity'; +import { Booking } from '../bookings/entities/booking.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Company, ExternalProfile, FFClient]), FilesModule], + imports: [TypeOrmModule.forFeature([Company, ExternalProfile, FFClient, Booking]), FilesModule], controllers: [CompaniesController], - providers: [CompaniesService, CompaniesRepository, ExternalProfileRepository, FFClientRepository], + providers: [CompaniesService, CompaniesRepository, ExternalProfileRepository, FFClientRepository, CompanyDashboardRepository], exports: [CompaniesService], }) export class CompaniesModule {} diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index f383b9e55..086e8d767 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -2,6 +2,7 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common import { CompaniesRepository } from './companies.repository'; import { ExternalProfileRepository } from './external-profile.repository'; import { FFClientRepository } from './ff-client.repository'; +import { CompanyDashboardRepository } from './company-dashboard.repository'; import { CreateCompanyDto } from './dto/create-company.dto'; import { UpdateCompanyDto } from './dto/update-company.dto'; import { CreateExternalProfileDto } from './dto/create-external-profile.dto'; @@ -9,6 +10,7 @@ import { CreateFFClientDto } from './dto/create-ff-client.dto'; import { CreateCompanyWithProfileDto } from './dto/create-company-with-profile.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { ProfileResponseDto } from './dto/profile-response.dto'; +import { DashboardSummaryResponseDto } from './dto/dashboard-summary-response.dto'; import { Company } from './entities/company.entity'; import { ExternalProfile } from './entities/external-profile.entity'; import { FFClient } from './entities/ff-client.entity'; @@ -27,6 +29,7 @@ export class CompaniesService { private readonly companiesRepo: CompaniesRepository, private readonly profilesRepo: ExternalProfileRepository, private readonly ffClientsRepo: FFClientRepository, + private readonly dashboardRepo: CompanyDashboardRepository, ) {} async createCompany(dto: CreateCompanyDto): Promise { @@ -98,6 +101,122 @@ export class CompaniesService { return { profile, company }; } + /** + * Dashboard KPIs for the portal home (MyPortalPage), aggregated from the + * current user's company bookings. All figures are scoped to that company. + * + * Note: delivered/spend/volume all derive from the bookings table — there is + * no separate data source for them. On-time delivery rate is replaced by + * completion rate (delivered ÷ committed): the schema has no ETA / + * promised-delivery date, so on-time cannot be computed. + * + * Period attribution uses booking.created_at: there is no delivery-date + * column, so "delivered YTD" counts bookings created this year that reached a + * delivered/completed status. + */ + async getDashboardSummary(userId: string): Promise { + // A user without a company profile has no bookings — return an empty summary + // rather than 404, so the portal home still renders. + const profile = await this.profilesRepo.findByUserId(userId); + const companyId = profile?.company?.id ?? profile?.companyId ?? null; + if (!companyId) return this.emptyDashboardSummary(); + + const now = new Date(); + const yearStart = new Date(now.getFullYear(), 0, 1); + const prevYearStart = new Date(now.getFullYear() - 1, 0, 1); + // Same point in the previous year, so YoY compares like-for-like windows. + const prevYearToDate = new Date(prevYearStart.getTime() + (now.getTime() - yearStart.getTime())); + + const [ + deliveredThis, + committedThis, + spendThisByCcy, + spendPrevByCcy, + tonnageThis, + tonnagePrev, + monthlyRows, + ] = await Promise.all([ + this.dashboardRepo.countDelivered(companyId, yearStart, now), + this.dashboardRepo.countCommitted(companyId, yearStart, now), + this.dashboardRepo.sumPaidSpendByCurrency(companyId, yearStart, now), + this.dashboardRepo.sumPaidSpendByCurrency(companyId, prevYearStart, prevYearToDate), + this.dashboardRepo.sumCommittedTonnage(companyId, yearStart, now), + this.dashboardRepo.sumCommittedTonnage(companyId, prevYearStart, prevYearToDate), + this.dashboardRepo.monthlyCommittedTonnage(companyId, this.monthsAgo(now, 5), now), + ]); + + // Spend can span currencies; report the dominant one (prefer ETB on ties). + const spend = this.pickCurrencyTotal(spendThisByCcy); + const spendPrev = spendPrevByCcy.find((c) => c.currency === spend.currency)?.total ?? 0; + + return { + deliveredCount: deliveredThis, + // Share of committed bookings that reached delivered/completed. + completionRate: committedThis > 0 ? Math.round((deliveredThis / committedThis) * 100) : 0, + spendYtd: spend.total, + spendCurrency: spend.currency, + spendYtdChangePct: this.changePct(spend.total, spendPrev), + freightVolume: { + totalTonnes: Math.round(tonnageThis), + totalValue: spend.total, + currency: spend.currency, + ytdChangePct: this.changePct(tonnageThis, tonnagePrev), + monthly: this.buildMonthlySeries(now, monthlyRows), + }, + }; + } + + private emptyDashboardSummary(): DashboardSummaryResponseDto { + const now = new Date(); + return { + deliveredCount: 0, + completionRate: 0, + spendYtd: 0, + spendCurrency: 'ETB', + spendYtdChangePct: 0, + freightVolume: { + totalTonnes: 0, + totalValue: 0, + currency: 'ETB', + ytdChangePct: 0, + monthly: this.buildMonthlySeries(now, []), + }, + }; + } + + /** First day of the month `n` months before `from`. */ + private monthsAgo(from: Date, n: number): Date { + return new Date(from.getFullYear(), from.getMonth() - n, 1); + } + + /** Pick the currency with the largest total, preferring ETB on ties / when empty. */ + private pickCurrencyTotal(totals: { currency: string; total: number }[]): { currency: string; total: number } { + if (totals.length === 0) return { currency: 'ETB', total: 0 }; + return totals.reduce((best, cur) => (cur.total > best.total ? cur : best)); + } + + /** Percentage change vs a prior value, rounded; 0 when there is no prior base. */ + private changePct(current: number, previous: number): number { + if (previous <= 0) return 0; + return Math.round(((current - previous) / previous) * 100); + } + + /** Build a fixed 6-month tonnage series ending on `now`, zero-filling gaps. */ + private buildMonthlySeries( + now: Date, + rows: { year: number; month: number; tonnes: number }[], + ): { month: string; tonnes: number }[] { + const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const byKey = new Map(rows.map((r) => [`${r.year}-${r.month}`, r.tonnes])); + const series: { month: string; tonnes: number }[] = []; + for (let i = 5; i >= 0; i--) { + const d = new Date(now.getFullYear(), now.getMonth() - i, 1); + const key = `${d.getFullYear()}-${d.getMonth() + 1}`; + series.push({ month: labels[d.getMonth()], tonnes: Math.round(byKey.get(key) ?? 0) }); + } + return series; + } + async updateCompany(id: string, dto: UpdateCompanyDto): Promise { await this.findCompanyById(id); const updated = await this.companiesRepo.update(id, dto); diff --git a/apps/edr-freight-api/src/modules/companies/company-dashboard.repository.ts b/apps/edr-freight-api/src/modules/companies/company-dashboard.repository.ts new file mode 100644 index 000000000..365cf1daa --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/company-dashboard.repository.ts @@ -0,0 +1,126 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } 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; +} + +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, + ) {} + + /** Count of delivered/completed bookings for a company within [from, to). */ + async countDelivered(companyId: string, from: Date, to: Date): Promise { + return this.bookings + .createQueryBuilder('b') + .where('b.company_id = :companyId', { companyId }) + .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 for a company within [from, to). */ + async countCommitted(companyId: string, from: Date, to: Date): Promise { + return this.bookings + .createQueryBuilder('b') + .where('b.company_id = :companyId', { companyId }) + .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). */ + async sumPaidSpendByCurrency(companyId: string, from: Date, to: Date): Promise { + const rows = await this.bookings + .createQueryBuilder('b') + .select('b.payment_currency', 'currency') + .addSelect('COALESCE(SUM(b.total_amount), 0)', 'total') + .where('b.company_id = :companyId', { companyId }) + .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) for a company within [from, to). */ + async sumCommittedTonnage(companyId: string, from: Date, to: Date): Promise { + const row = await this.bookings + .createQueryBuilder('b') + .select('COALESCE(SUM(b.cargo_total_weight_vgm), 0)', 'total') + .where('b.company_id = :companyId', { companyId }) + .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). */ + async monthlyCommittedTonnage(companyId: string, from: Date, to: Date): Promise { + const rows = await 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') + .where('b.company_id = :companyId', { companyId }) + .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), + })); + } +} diff --git a/apps/edr-freight-api/src/modules/companies/dto/dashboard-summary-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/dashboard-summary-response.dto.ts new file mode 100644 index 000000000..c0d99643c --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/dashboard-summary-response.dto.ts @@ -0,0 +1,59 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FreightVolumePointDto { + @ApiProperty({ example: 'May', description: 'Short month label' }) + month!: string; + + @ApiProperty({ example: 940, description: 'Tonnage shipped in the month' }) + tonnes!: number; +} + +export class FreightVolumeDto { + @ApiProperty({ example: 4180, description: 'Total tonnage shipped year-to-date' }) + totalTonnes!: number; + + @ApiProperty({ example: 1240000, description: 'Total committed freight value year-to-date' }) + totalValue!: number; + + @ApiProperty({ example: 'ETB' }) + currency!: string; + + @ApiProperty({ example: 16, description: 'Tonnage change vs same period last year, in percent' }) + ytdChangePct!: number; + + @ApiProperty({ type: [FreightVolumePointDto], description: 'Monthly tonnage series (oldest first, last 6 months)' }) + monthly!: FreightVolumePointDto[]; +} + +/** + * KPIs for the portal dashboard (MyPortalPage), aggregated from the current + * user's company bookings. All figures are scoped to that company. + * + * Note: every metric here derives from the bookings table — there is no + * separate "non-booking" data source for delivered/spend/volume. On-time + * delivery rate is replaced by completion rate: no ETA / promised-delivery + * column exists in the schema, so on-time cannot be computed, whereas + * completion rate (delivered ÷ committed) can. + */ +export class DashboardSummaryResponseDto { + @ApiProperty({ example: 12, description: 'Bookings delivered/completed year-to-date' }) + deliveredCount!: number; + + @ApiProperty({ + example: 92, + description: 'Share of committed bookings that have been delivered/completed (YTD), in percent', + }) + completionRate!: number; + + @ApiProperty({ example: 1240000, description: 'Total paid spend year-to-date' }) + spendYtd!: number; + + @ApiProperty({ example: 'ETB' }) + spendCurrency!: string; + + @ApiProperty({ example: 16, description: 'Spend change vs same period last year, in percent' }) + spendYtdChangePct!: number; + + @ApiProperty({ type: FreightVolumeDto }) + freightVolume!: FreightVolumeDto; +} diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index 540cedce8..a2a72655e 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -83,6 +83,7 @@ export const URL_CONSTANTS = { GET_INFO: "/api/companies/getInfo", CREATE: "/api/companies/create", PROFILE: "/api/companies/profile", + DASHBOARD: "/api/companies/dashboard", DOCUMENTS: (id: string) => `/api/companies/${id}/documents`, }, diff --git a/apps/edr-freight-web/portal/src/lib/currentCustomer.ts b/apps/edr-freight-web/portal/src/lib/currentCustomer.ts index 8fee820e6..47fc1efa6 100644 --- a/apps/edr-freight-web/portal/src/lib/currentCustomer.ts +++ b/apps/edr-freight-web/portal/src/lib/currentCustomer.ts @@ -1,7 +1,5 @@ -import { customers, type Customer } from "@/pages/customers/customers.mock"; -import { bookings, type Booking } from "@/pages/bookings/bookings.mock"; -import { shipments, type Shipment } from "@/pages/tracking/shipments.mock"; import { invoices, type Invoice } from "@/pages/billing/invoices.mock"; +import { customers, type Customer } from "@/pages/customers/customers.mock"; /** * Mock "logged-in customer". When auth integrates, replace this with the value @@ -16,16 +14,6 @@ export function getCurrentCustomer(): Customer { ); } -export function getMyBookings(): Booking[] { - const me = getCurrentCustomer(); - return bookings.filter((b) => b.customerId === me.id); -} - -export function getMyShipments(): Shipment[] { - const myBookingIds = new Set(getMyBookings().map((b) => b.id)); - return shipments.filter((s) => myBookingIds.has(s.bookingId)); -} - export function getMyInvoices(): Invoice[] { const me = getCurrentCustomer(); return invoices.filter((inv) => inv.customerId === me.id); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx index e05e3a503..1bd7f12df 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx @@ -26,7 +26,7 @@ import { useMemo } from "react"; import { Link, useNavigate } from "react-router-dom"; import useAuth from "@/hooks/useAuth"; -import { getMyInvoices, getMyShipments } from "@/lib/currentCustomer"; +import { getMyInvoices } from "@/lib/currentCustomer"; import type { InvoiceStatus } from "@/pages/billing/invoices.mock"; import { formatCurrency } from "@/pages/billing/invoices.mock"; import { api } from "@/services/api"; @@ -38,6 +38,9 @@ const cv = (token: string) => { return `var(--mantine-color-${name}-${shade ?? "6"})`; }; +/** Format a signed percentage for KPI deltas, e.g. 16 → "+16%", -4 → "-4%". */ +const formatPct = (n: number) => `${n >= 0 ? "+" : ""}${n}%`; + const ACTIVE_STATUSES = [ "DRAFT", "SUBMITTED", @@ -356,12 +359,8 @@ const INVOICE_BADGE: Record< Cancelled: { label: "Cancelled", bg: "edr-slate-soft", text: "edr-slate" }, }; -const MONTHS = ["Dec", "Jan", "Feb", "Mar", "Apr", "May"]; -const VOLUME_DATA = [420, 680, 510, 820, 750, 940]; - export default function MyPortalPage() { const { user, customer } = useAuth(); - const myShipments = useMemo(() => getMyShipments(), []); const myInvoices = useMemo(() => getMyInvoices(), []); const navigate = useNavigate(); @@ -371,10 +370,17 @@ export default function MyPortalPage() { }), ); + const dashboardQuery = useQuery(api.companies.getDashboard.queryOptions()); + const dashboard = dashboardQuery.data; + const allBookings = bookingsQuery.data?.items ?? []; const activeBookings = allBookings.filter((b) => ACTIVE_STATUSES.includes(b.status), ); + const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + const newActiveThisWeek = activeBookings.filter( + (b) => new Date(b.createdAt).getTime() >= weekAgo, + ).length; const visibleBookings = allBookings; const outstandingInvoices = myInvoices.filter( @@ -384,9 +390,6 @@ export default function MyPortalPage() { (sum, inv) => sum + inv.amount, 0, ); - const deliveredCount = - myShipments.filter((s) => s.status === "Delivered").length || 12; - const displayName = user?.name?.en ?? user?.username ?? user?.email ?? "—"; const companyName = (customer as any)?.companyName ?? displayName; @@ -398,7 +401,9 @@ export default function MyPortalPage() { ? "Good afternoon," : "Good evening,"; const recentInvoices = myInvoices.slice(0, 3); - const maxVolume = Math.max(...VOLUME_DATA); + + const volumePoints = dashboard?.freightVolume.monthly ?? []; + const maxVolume = Math.max(1, ...volumePoints.map((p) => p.tonnes)); return ( @@ -471,8 +476,12 @@ export default function MyPortalPage() { @@ -670,37 +685,57 @@ export default function MyPortalPage() { Freight Volume - - 4,180 t - - - ETB 1.24M - - - +16% YTD - + {dashboardQuery.isPending ? ( + + ) : ( + <> + + {(dashboard?.freightVolume.totalTonnes ?? 0).toLocaleString()}{" "} + t + + + {formatCurrency( + dashboard?.freightVolume.totalValue ?? 0, + dashboard?.freightVolume.currency ?? "ETB", + )} + + + {formatPct(dashboard?.freightVolume.ytdChangePct ?? 0)} YTD + + + )} - - {VOLUME_DATA.map((val, i) => { - const isLast = i === VOLUME_DATA.length - 1; - return ( - + {dashboardQuery.isPending ? ( + + ) : volumePoints.length === 0 ? ( + + + No freight volume yet. + + + ) : ( + + {volumePoints.map((point, i) => { + const isLast = i === volumePoints.length - 1; + return ( - - {MONTHS[i]} - - - ); - })} - + key={point.month} + className="flex flex-1 flex-col items-center gap-2" + > + + + {point.month} + + + ); + })} + + )} diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts index c2a8fb02d..32ef5bbc0 100644 --- a/apps/edr-freight-web/portal/src/services/api.ts +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -37,6 +37,7 @@ import { import type { CompanyInfoResponse, CreateCompanyPayload, + DashboardSummary, } from "./companies.service"; import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; import type { @@ -119,6 +120,12 @@ export const api = { "updateProfile", companiesService.updateProfile, ), + + getDashboard: endpoint( + "companies", + "getDashboard", + companiesService.getDashboard, + ), }, bookings: { diff --git a/apps/edr-freight-web/portal/src/services/companies.service.ts b/apps/edr-freight-web/portal/src/services/companies.service.ts index 359881a9b..409d9045b 100644 --- a/apps/edr-freight-web/portal/src/services/companies.service.ts +++ b/apps/edr-freight-web/portal/src/services/companies.service.ts @@ -59,6 +59,26 @@ export interface CreateCompanyPayload { attributes?: Record; } +export interface FreightVolumePoint { + month: string; + tonnes: number; +} + +export interface DashboardSummary { + deliveredCount: number; + completionRate: number; + spendYtd: number; + spendCurrency: string; + spendYtdChangePct: number; + freightVolume: { + totalTonnes: number; + totalValue: number; + currency: string; + ytdChangePct: number; + monthly: FreightVolumePoint[]; + }; +} + export const companiesService = { getInfo: async (): Promise => { try { @@ -97,6 +117,13 @@ export const companiesService = { return unwrap(response.data); }, + getDashboard: async (): Promise => { + const response = await client.get>( + URL_CONSTANTS.COMPANIES_API.DASHBOARD, + ); + return unwrap(response.data); + }, + uploadDocuments: async ( companyId: string, files: Record,