diff --git a/apps/edr-freight-api/src/migrations/1826000000000-AddCargoScopeQuantityCap.ts b/apps/edr-freight-api/src/migrations/1826000000000-AddCargoScopeQuantityCap.ts new file mode 100644 index 000000000..97428c11b --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1826000000000-AddCargoScopeQuantityCap.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * GENERAL contracts can be booked repeatedly until a total cargo quantity cap is + * reached (e.g. 100 containers across many shipments). `quantity_cap` on each + * cargo-scope line holds that ceiling (containers per size, or tons/items for + * bulk). NULL = uncapped; always NULL for ONE_TIME (single booking). + */ +export class AddCargoScopeQuantityCap1826000000000 implements MigrationInterface { + name = 'AddCargoScopeQuantityCap1826000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE freight.contract_cargo_scope ADD COLUMN IF NOT EXISTS quantity_cap NUMERIC(12,2);`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE freight.contract_cargo_scope DROP COLUMN IF EXISTS quantity_cap;`, + ); + } +} diff --git a/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts index 9811d03d5..320bc8b72 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts @@ -77,7 +77,9 @@ export class ContractBookingService { throw new BadRequestException('Contract validity has expired — no new bookings.'); } - // ONE_TIME: only one active booking at a time (also enforced by partial unique index). + // ONE_TIME: a single shipment at a time. The slot frees only if the prior + // booking reached a terminal state (e.g. payment expired without shipping), + // letting the customer re-book within contract validity (doc §10.4). if (contract.contractKind === 'ONE_TIME') { const active = await this.countActiveBookings(contractId); if (active > 0) { @@ -85,6 +87,9 @@ export class ContractBookingService { 'This one-time contract already has an active booking.', ); } + } else { + // GENERAL: draw down against the cargo quantity cap until it is full. + await this.assertWithinQuantityCap(contract, dto); } const route = await this.resolveRoute(contract, dto.contractRouteId); @@ -219,6 +224,115 @@ export class ContractBookingService { .getCount(); } + // ── GENERAL contract quantity cap (draw-down) ────────────────────────────── + + /** + * Reject a GENERAL booking whose cargo would exceed the contract's quantity + * cap. Container caps are per size; bulk is a single tons/items cap. Bookings + * that never shipped (CANCELLED / REJECTED / EXPIRED) release their hold. + */ + private async assertWithinQuantityCap( + contract: Contract, + dto: CreateBookingUnderContractDto, + ): Promise { + const capacity = await this.computeCapacity(contract); + if (capacity.length === 0) return; // uncapped contract + + if (contract.freightType === 'CONTAINER') { + for (const line of dto.containers ?? []) { + const cap = capacity.find((c) => c.containerSize === line.containerSize); + if (!cap || cap.remaining == null) continue; // size uncapped + if (line.quantity > cap.remaining) { + throw new BadRequestException( + `Only ${cap.remaining} of ${cap.cap} ${line.containerSize} containers remain on this contract.`, + ); + } + } + } else { + const requested = + (dto.bulkLines ?? []).reduce( + (sum, b) => sum + (b.cargoWeightTons ?? b.itemCount ?? 0), + 0, + ) || this.resolveBulkTons(dto) || 0; + const cap = capacity.find((c) => c.cap != null); + if (cap && cap.remaining != null && requested > cap.remaining) { + throw new BadRequestException( + `Only ${cap.remaining} of ${cap.cap} remain on this contract.`, + ); + } + } + } + + /** + * Remaining bookable quantity per cargo-scope line: cap minus what prior + * bookings already consumed. Returns [] when the contract has no caps. + */ + async computeCapacity( + contract: Contract, + ): Promise< + Array<{ + containerSize?: string | null; + cargoTypeId?: string | null; + cap: number | null; + booked: number; + remaining: number | null; + }> + > { + const scope = contract.cargoScope ?? []; + const capped = scope.filter((s) => s.quantityCap != null); + if (capped.length === 0) return []; + + const booked = await this.bookedQuantities(contract); + return capped.map((s) => { + const cap = Number(s.quantityCap); + const used = + contract.freightType === 'CONTAINER' + ? (booked.bySize.get(s.containerSize ?? '') ?? 0) + : booked.bulk; + return { + containerSize: s.containerSize, + cargoTypeId: s.cargoTypeId, + cap, + booked: used, + remaining: Math.max(0, cap - used), + }; + }); + } + + /** + * Quantities already booked under a contract that still hold capacity. Excludes + * bookings that never shipped (CANCELLED / REJECTED / EXPIRED). + */ + private async bookedQuantities( + contract: Contract, + ): Promise<{ bySize: Map; bulk: number }> { + const releasing = ['CANCELLED', 'REJECTED', 'EXPIRED']; + if (contract.freightType === 'CONTAINER') { + const rows = await this.dataSource + .getRepository(BookingContainer) + .createQueryBuilder('bc') + .innerJoin(Booking, 'b', 'b.id = bc.booking_id') + .select('bc.container_size', 'size') + .addSelect('COALESCE(SUM(bc.quantity), 0)', 'qty') + .where('b.contract_id = :contractId', { contractId: contract.id }) + .andWhere('b.status NOT IN (:...releasing)', { releasing }) + .groupBy('bc.container_size') + .getRawMany<{ size: string | null; qty: string }>(); + const bySize = new Map(); + for (const r of rows) bySize.set(r.size ?? '', Number(r.qty)); + return { bySize, bulk: 0 }; + } + + const row = await this.dataSource + .getRepository(Booking) + .createQueryBuilder('b') + .select('COALESCE(SUM(b.cargo_total_weight_vgm), 0)', 'tons') + .where('b.contract_id = :contractId', { contractId: contract.id }) + .andWhere('b.status NOT IN (:...releasing)', { releasing }) + .getRawOne<{ tons: string }>(); + return { bySize: new Map(), bulk: Number(row?.tons ?? 0) }; + } + private async resolveRoute( contract: Contract, contractRouteId?: string, diff --git a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts index cb047b572..b22b0e217 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts @@ -481,11 +481,7 @@ export class ContractClearanceService { /** * GL ET queue: customs (Path B) contracts awaiting pre-booking document review. */ - async queue( - filter: FilterContractDto, - region?: string, - ): Promise { - void region; // single ET pre-booking queue today; region reserved for split + async queue(filter: FilterContractDto): Promise { return this.contractsRepository.findAllPaginated({ page: filter.page ?? 1, pageSize: filter.pageSize ?? 100, diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts index e59afdc29..cf38f0fd9 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts @@ -166,11 +166,8 @@ export class ContractsController { @Get('clearance/queue') @BookingStaff(FREIGHT_PERMS.contracts.clearanceReview) @ApiOperation({ summary: 'GL ET queue: contracts awaiting pre-booking document review' }) - clearanceQueue( - @Query() filter: FilterContractDto, - @Query('region') region?: string, - ) { - return this.clearanceService.queue(filter, region); + clearanceQueue(@Query() filter: FilterContractDto) { + return this.clearanceService.queue(filter); } @Get(':id') @@ -485,6 +482,15 @@ export class ContractsController { ); } + @Get(':id/capacity') + @ApiOperation({ + summary: 'Remaining bookable quantity per cargo line (GENERAL draw-down cap)', + }) + async capacity(@Param('id', ParseUUIDPipe) id: string) { + const contract = await this.contractsService.findById(id); + return this.contractBookingService.computeCapacity(contract); + } + // ── Clearance milestones (doc §11.3, §12.2) ──────────────────────────────── @Get(':id/milestones') diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts b/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts index ecfea0b7d..354f02e8c 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts @@ -1,7 +1,7 @@ import { BaseRepository } from '@edr/api-common'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, IsNull, Repository, SelectQueryBuilder } from 'typeorm'; +import { DataSource, In, IsNull, Repository, SelectQueryBuilder } from 'typeorm'; import { FileRecord } from '../files/entities/file.entity'; import { Contract } from './entities/contract.entity'; @@ -132,6 +132,10 @@ export class ContractsRepository extends BaseRepository { .take(pageSize) .getManyAndCount(); + // Attach the generated contract PDF to each row so list/home can offer a + // direct download. Loaded separately to keep pagination counts correct. + await this.attachContractFiles(items); + const totalPages = pageSize > 0 ? Math.ceil(total / pageSize) : 0; return { items, @@ -147,6 +151,28 @@ export class ContractsRepository extends BaseRepository { }; } + /** + * Load contract-resource files for the given contracts and attach them to + * `contract.files`. Kept separate from the paginated query so the one-to-many + * join doesn't inflate the page count. + */ + private async attachContractFiles(contracts: Contract[]): Promise { + if (contracts.length === 0) return; + const ids = contracts.map((c) => c.id); + const files = await this.dataSource.getRepository(FileRecord).find({ + where: { resource: 'contracts', resourceId: In(ids), deletedAt: IsNull() }, + }); + const byContract = new Map(); + for (const file of files) { + const list = byContract.get(file.resourceId) ?? []; + list.push(file); + byContract.set(file.resourceId, list); + } + for (const contract of contracts) { + contract.files = byContract.get(contract.id) ?? []; + } + } + async getStatusCounts(): Promise> { const rows = await this.repository .createQueryBuilder('contract') diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts index c29baf4b4..cc788888c 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts @@ -210,7 +210,7 @@ export class ContractsService { } as never); await this.persistRoutes(contract.id, dto.routes); - await this.persistCargoScope(contract.id, dto.cargoScope); + await this.persistCargoScope(contract.id, dto.cargoScope, contract.contractKind); if (files.length > 0) { try { @@ -244,8 +244,12 @@ export class ContractsService { private async persistCargoScope( contractId: string, cargoScope: CreateContractDto['cargoScope'], + contractKind: string, ): Promise { const repo = this.dataSource.getRepository(ContractCargoScope); + // A quantity cap only governs GENERAL contracts (multi-shipment draw-down). + // ONE_TIME allows a single booking, so any cap on it is meaningless → null. + const isGeneral = contractKind === 'GENERAL'; await repo.save( cargoScope.map((c) => repo.create({ @@ -253,6 +257,7 @@ export class ContractsService { containerSize: c.containerSize ?? null, cargoTypeId: c.cargoTypeId ?? null, cargoFreeText: c.cargoFreeText ?? null, + quantityCap: isGeneral ? (c.quantityCap ?? null) : null, }), ), ); @@ -318,7 +323,7 @@ export class ContractsService { } if (dto.cargoScope) { await this.dataSource.getRepository(ContractCargoScope).delete({ contractId: id }); - await this.persistCargoScope(id, dto.cargoScope); + await this.persistCargoScope(id, dto.cargoScope, existing.contractKind); } if (files.length > 0) { diff --git a/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts b/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts index e70d436e6..70dba1e90 100644 --- a/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts +++ b/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts @@ -51,6 +51,17 @@ export class CreateContractCargoScopeDto { @IsString() @MaxLength(200) cargoFreeText?: string | null; + + @ApiPropertyOptional({ + description: + 'GENERAL only: total bookable quantity for this line (containers per size, or tons/items for bulk). Omit for uncapped.', + minimum: 1, + }) + @IsOptional() + @IsNumber() + @Min(1) + @Transform(({ value }) => (value == null || value === '' ? null : Number(value))) + quantityCap?: number | null; } /** A contracted lane (origin → destination). Routes carry NO quantity. */ diff --git a/apps/edr-freight-api/src/modules/contracts/entities/contract-cargo-scope.entity.ts b/apps/edr-freight-api/src/modules/contracts/entities/contract-cargo-scope.entity.ts index 51a975752..9d5778dde 100644 --- a/apps/edr-freight-api/src/modules/contracts/entities/contract-cargo-scope.entity.ts +++ b/apps/edr-freight-api/src/modules/contracts/entities/contract-cargo-scope.entity.ts @@ -31,4 +31,14 @@ export class ContractCargoScope extends BaseEntity { @Column({ name: 'cargo_free_text', type: 'varchar', length: 200, nullable: true }) cargoFreeText?: string | null; + + /** + * GENERAL contracts: total cargo quantity allowed across ALL shipments on this + * scope line over the validity window. Container → number of containers of + * this size; bulk → tons (or items for per-item commodities). Bookings draw + * down against it until the cap is reached. NULL = uncapped (always NULL for + * ONE_TIME, which allows a single booking). + */ + @Column({ name: 'quantity_cap', type: 'numeric', precision: 12, scale: 2, nullable: true }) + quantityCap?: number | null; } diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx index c55d5e0c1..ff2aaeca7 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx @@ -2,6 +2,8 @@ import { useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ActionIcon, + Alert, + Badge, Box, Button, Center, @@ -29,9 +31,11 @@ import { PageContainer } from "@/components/page"; import { PageHeader } from "@/components/page/PageHeader"; import { SectionCard } from "@/components/bookings/detail/SectionCard"; import { + useContractCapacity, useContractDetail, useContractMutations, } from "@/hooks/contracts/useContracts"; +import { Boxes } from "lucide-react"; interface UnitDraft { containerNumber: string; @@ -61,6 +65,7 @@ export default function GlCreateBookingForm() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { data: contract, isLoading } = useContractDetail(id); + const { data: capacity = [] } = useContractCapacity(id); const mutations = useContractMutations(id ?? ""); const [scheduledDate, setScheduledDate] = useState(""); @@ -221,6 +226,28 @@ export default function GlCreateBookingForm() { /> + {capacity.length > 0 && ( + c.remaining === 0) ? "red" : "blue"} + variant="light" + radius="md" + icon={} + title="Contract draw-down capacity" + > + + {capacity.map((c, i) => ( + + {c.containerSize ?? "Bulk"}: {c.remaining} of {c.cap} left + + ))} + + + )} diff --git a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts index ccd422a70..2a0422113 100644 --- a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts @@ -57,6 +57,7 @@ export const QUERY_KEYS = { clearanceQueue: (region?: string) => ["contracts", "clearance-queue", region ?? "ET"] as const, milestones: (id: string) => ["contracts", "milestones", id] as const, + capacity: (id: string) => ["contracts", "capacity", id] as const, bookingMilestones: (bookingId: string) => ["contracts", "booking-milestones", bookingId] as const, bookingIncidents: (bookingId: string) => diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 8d747782d..bcbe07dfa 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -150,6 +150,7 @@ export const URL_CONSTANTS = { OPS_CLEARANCE_FINALIZE: (id: string) => `/contracts/${id}/clearance/ops-finalize`, BOOKINGS: (id: string) => `/contracts/${id}/bookings`, + CAPACITY: (id: string) => `/contracts/${id}/capacity`, MILESTONES: (id: string) => `/contracts/${id}/milestones`, BOOKING_MILESTONES: (bookingId: string) => `/contracts/bookings/${bookingId}/milestones`, diff --git a/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts b/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts index e296dd6f9..43e3509b3 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/contracts/useContracts.ts @@ -44,10 +44,10 @@ export function useContractDetail(id: string | undefined) { }); } -export function useContractClearanceQueue(region = "ET", enabled = true) { +export function useContractClearanceQueue(enabled = true) { return useQuery({ - queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue(region), - queryFn: () => contractsService.getClearanceQueue(region), + queryKey: QUERY_KEYS.CONTRACTS.clearanceQueue("GL"), + queryFn: () => contractsService.getClearanceQueue(), enabled, }); } @@ -69,6 +69,14 @@ export function useContractMilestones(id: string | undefined) { }); } +export function useContractCapacity(id: string | undefined) { + return useQuery({ + queryKey: QUERY_KEYS.CONTRACTS.capacity(id ?? ""), + queryFn: () => contractsService.getCapacity(id!), + enabled: Boolean(id), + }); +} + export function useBookingMilestones(bookingId: string | undefined) { return useQuery({ queryKey: QUERY_KEYS.CONTRACTS.bookingMilestones(bookingId ?? ""), diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx index 1307b528c..2b34b0c4e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx @@ -46,7 +46,6 @@ import { } from "@/hooks/contracts/useContracts"; type ViewMode = "table" | "cards"; -type Region = "ET" | "DJ"; interface ClearanceRow { id: string; @@ -118,12 +117,11 @@ export default function ContractClearanceListPage({ opsMode?: boolean; } = {}) { const navigate = useNavigate(); - const [region, setRegion] = useState("ET"); const [query, setQuery] = useState(""); const [view, setView] = useState("table"); const { pagination, setPagination } = usePagination({ pageSize: 10 }); - const glQueue = useContractClearanceQueue(region, !opsMode); + const glQueue = useContractClearanceQueue(!opsMode); const opsQueue = useOpsClearanceQueue(opsMode); const { data, isLoading, isError, isFetching, refetch } = opsMode ? opsQueue @@ -340,24 +338,6 @@ export default function ContractClearanceListPage({ style={{ flex: 1, minWidth: 220 }} /> - {!opsMode && ( - { - setRegion(v as Region); - setPagination({ - pageIndex: 0, - pageSize: pagination.pageSize, - }); - }} - data={[ - { value: "ET", label: "Ethiopia" }, - { value: "DJ", label: "Djibouti" }, - ]} - /> - )} {total} record{total !== 1 ? "s" : ""} diff --git a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts index 2ede69340..3c814ce7f 100644 --- a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts @@ -163,12 +163,8 @@ export const contractsService = { postContract(C.CONTRACT_SIGN(id), payload), // ── Pre-booking clearance (Path B — GL ET) ── - getClearanceQueue: async ( - region = "ET", - ): Promise => { - const response = await client.get(C.CLEARANCE_QUEUE, { - params: { region }, - }); + getClearanceQueue: async (): Promise => { + const response = await client.get(C.CLEARANCE_QUEUE); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], @@ -230,6 +226,12 @@ export const contractsService = { payload: Freight.CreateBookingUnderContractDto, ) => postContract<{ id: string; reference: string }>(C.BOOKINGS(id), payload), + /** Remaining bookable quantity per cargo line (GENERAL draw-down cap). */ + getCapacity: async (id: string): Promise => { + const response = await client.get(C.CAPACITY(id)); + return (unwrap(response.data) ?? []) as Freight.ContractCapacityLine[]; + }, + // ── Clearance milestones ── listMilestonesForContract: async ( id: string, diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index bd939ad22..a5704d4bd 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -129,6 +129,7 @@ export const URL_CONSTANTS = { `/api/contracts/bookings/${bookingId}/milestones`, BOOKING_DUTY_SLIP: (bookingId: string) => `/api/contracts/bookings/${bookingId}/duty-slip`, + CAPACITY: (id: string) => `/api/contracts/${id}/capacity`, }, TRAIN_SCHEDULING: { diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx index 6374bd466..98ed091f9 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx @@ -5,7 +5,6 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { PROFILE_TYPE_LABELS } from "@/constants/profileMode"; import { - ActionNeededSection, FreightVolumeSection, HelloSection, InvoicesSection, @@ -14,7 +13,6 @@ import { ShipmentsSection, StatsSection, } from "./components"; -import { deriveActionItems } from "./actions"; import { useMyPortalData } from "./hooks"; export default function MyPortalPage() { @@ -27,7 +25,6 @@ export default function MyPortalPage() { bookingsQuery, dashboardQuery, contractsQuery, - allContracts, recentContracts, activeContractsCount, allBookings, @@ -48,8 +45,6 @@ export default function MyPortalPage() { label: `${PROFILE_TYPE_LABELS[p.type] ?? p.type} · ${p.reference}`, })); - const actionItems = deriveActionItems(allContracts, allBookings); - const handleBookingClick = (id: string) => { navigate(`/bookings/${id}`); }; @@ -58,8 +53,6 @@ export default function MyPortalPage() { - - {serviceOptions.length > 1 && (