mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
enhance service filtering and dashboard functionality with company profile support
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user