enhance service filtering and dashboard functionality with company profile support

This commit is contained in:
Marshal
2026-06-23 07:54:36 +00:00
parent fd1fc1ca69
commit 1d77f377f3
19 changed files with 206 additions and 235 deletions

View File

@@ -148,15 +148,10 @@ export class BookingsController {
},
};
}
// Scope to the active operational profile (importer/exporter) when one
// resolves; otherwise fall back to company-level scoping.
const companyProfileId =
await this.bookingsService.resolveActiveCompanyProfileId(userId);
return this.bookingsService.findAll(
filter,
companyId,
companyProfileId ?? undefined,
);
// Company-wide by default; the optional filter.companyProfileId (per-page
// service filter) narrows within the company. The company guard always
// applies, so a customer can only ever see their own company's bookings.
return this.bookingsService.findAll(filter, companyId);
}
@Get('by-company/:companyId/customer-view')

View File

@@ -656,10 +656,11 @@ export class BookingsService {
assignedToSchedule: filter.assignedToSchedule,
// A forced company scope (portal/customer) overrides any caller-provided
// companyId so a customer can only ever see their own company's bookings.
// When an active profile resolves, scope to it; otherwise fall back to the
// company so nothing breaks for not-yet-onboarded customers.
companyId: forceCompanyProfileId ? undefined : forceCompanyId ?? filter.companyId,
companyProfileId: forceCompanyProfileId,
// The company guard always applies; the optional companyProfileId filter
// (from the per-page service filter) narrows WITHIN the company — the repo
// ANDs both, so cross-company access is impossible.
companyId: forceCompanyId ?? filter.companyId,
companyProfileId: forceCompanyProfileId ?? filter.companyProfileId,
contractType: filter.contractType,
serviceTypeId: filter.serviceTypeId,
cargoTypeId: filter.cargoTypeId,
@@ -694,18 +695,15 @@ export class BookingsService {
filter: FilterBookingDto,
): Promise<PaginatedBookings> {
const { company } = await this.companiesService.getCompanyInfoByUserId(userId);
// Scope to the active operational profile when one resolves; fall back to
// company-level so not-yet-onboarded customers still see their payables.
const companyProfileId =
await this.companiesService.resolveActiveCompanyProfileId(userId);
return this.bookingsRepository.findAllPaginated({
page: filter.page ?? 1,
pageSize: filter.pageSize ?? 20,
statuses: BookingsService.PAYABLE_STATUSES,
excludePaymentStatus: 'PAID',
companyId: companyProfileId ? undefined : company.id,
companyProfileId: companyProfileId ?? undefined,
// Company-wide: payables span all of the customer's services.
companyId: company.id,
companyProfileId: filter.companyProfileId,
sortBy: filter.sortBy,
sortOrder: filter.sortOrder,
});

View File

@@ -38,6 +38,15 @@ export class FilterBookingDto {
@IsUUID()
companyId?: string;
@ApiPropertyOptional({
format: 'uuid',
description:
'Narrow to a single operational profile (importer/exporter/freight_forwarder) within the company.',
})
@IsOptional()
@IsUUID()
companyProfileId?: string;
@ApiPropertyOptional()
@IsOptional()
contractType?: string;

View File

@@ -28,6 +28,7 @@ import { CreateCompanyProfileDto } from "./dto/create-company-profile.dto";
import { SetActiveModeDto } from "./dto/set-active-mode.dto";
import { SetOnboardingStepDto } from "./dto/set-onboarding-step.dto";
import { StartOnboardingDto } from "./dto/start-onboarding.dto";
import { DashboardQueryDto } from "./dto/dashboard-query.dto";
import {
ResponseCompanyDto,
ResponseCompanyProfileDto,
@@ -86,8 +87,12 @@ export class CompaniesController {
})
async getDashboard(
@CurrentUser() user: CurrentIamUser,
@Query() query: DashboardQueryDto,
): Promise<DashboardSummaryResponseDto> {
return this.companiesService.getDashboardSummary(user.id);
return this.companiesService.getDashboardSummary(
user.id,
query.companyProfileId,
);
}
@Post("fetch-etrade-info")

View File

@@ -7,7 +7,10 @@ import {
import { CompaniesRepository } from "./companies.repository";
import { CompanyProfileRepository } from "./company-profile.repository";
import { ExternalProfileRepository } from "./external-profile.repository";
import { CompanyDashboardRepository } from "./company-dashboard.repository";
import {
CompanyDashboardRepository,
DashboardScope,
} from "./company-dashboard.repository";
import { MinioService } from "../minio/minio.service";
import { ETradeService } from "./services/etrade.service";
import { normalizeE164 } from "../../common/validators/is-phone-number.validator";
@@ -320,6 +323,7 @@ export class CompaniesService {
*/
async getDashboardSummary(
userId: string,
companyProfileId?: string,
): Promise<DashboardSummaryResponseDto> {
// A user without a company profile has no bookings — return an empty summary
// rather than 404, so the portal home still renders.
@@ -327,17 +331,17 @@ export class CompaniesService {
const companyId = profile?.company?.id ?? profile?.companyId ?? null;
if (!companyId) return this.emptyDashboardSummary();
// Scope KPIs to the active operational profile (importer/exporter mode) when
// one resolves; otherwise aggregate across the whole company.
const companyProfileId = profile?.activeProfileType
? ((await this.companyProfilesRepo.findByType(
companyId,
profile.activeProfileType,
)) ?? null)
: null;
const scope = companyProfileId
? { companyProfileId: companyProfileId.id }
: { companyId };
// Company-wide by default (all services' data). An optional companyProfileId
// (from the per-page service filter) narrows to one operational profile —
// but only after we confirm it belongs to this user's company, since the
// dashboard scope has no company guard at the repository layer.
let scope: DashboardScope = { companyId };
if (companyProfileId) {
const owned = await this.companyProfilesRepo.findByCompanyId(companyId);
if (owned.some((p) => p.id === companyProfileId)) {
scope = { companyProfileId };
}
}
const now = new Date();
const yearStart = new Date(now.getFullYear(), 0, 1);

View File

@@ -0,0 +1,13 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsOptional, IsUUID } from "class-validator";
export class DashboardQueryDto {
@ApiPropertyOptional({
format: "uuid",
description:
"Narrow dashboard KPIs to a single operational profile (importer/exporter/freight_forwarder) of the user's company. Omit for company-wide totals.",
})
@IsOptional()
@IsUUID()
companyProfileId?: string;
}