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 81fba19fb..684e9e9bd 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -1,22 +1,34 @@ -import { Controller, Get, Post, Patch, Delete, Body, Param, Query, ParseUUIDPipe, HttpCode, HttpStatus, UseInterceptors, UploadedFiles } from '@nestjs/common'; -import { AnyFilesInterceptor } from '@nestjs/platform-express'; -import { ApiOperation, ApiTags, ApiConsumes } from '@nestjs/swagger'; -import { CurrentUser } from '@edr/api-common'; -import { FreightAdmin } from '../../common/booking-guards'; -import { FilesService } from '../files/files.service'; -import { CompaniesService } from './companies.service'; -import { CreateCompanyDto } from './dto/create-company.dto'; -import { UpdateCompanyDto } from './dto/update-company.dto'; -import { CreateExternalProfileDto } from './dto/create-external-profile.dto'; -import { CreateFFClientDto } from './dto/create-ff-client.dto'; -import { CreateCompanyWithProfileDto } from './dto/create-company-with-profile.dto'; -import { ResponseCompanyDto } from './dto/response-company.dto'; -import { ResponseExternalProfileDto } from './dto/response-external-profile.dto'; -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'; +import { + Controller, + Get, + Post, + Patch, + Delete, + Body, + Param, + Query, + ParseUUIDPipe, + HttpCode, + HttpStatus, + UseInterceptors, + UploadedFiles, +} from "@nestjs/common"; +import { AnyFilesInterceptor } from "@nestjs/platform-express"; +import { ApiOperation, ApiTags, ApiConsumes } from "@nestjs/swagger"; +import { CurrentUser } from "@edr/api-common"; +import { FreightAdmin } from "../../common/booking-guards"; +import { FilesService } from "../files/files.service"; +import { CompaniesService } from "./companies.service"; +import { CreateCompanyDto } from "./dto/create-company.dto"; +import { UpdateCompanyDto } from "./dto/update-company.dto"; +import { CreateExternalProfileDto } from "./dto/create-external-profile.dto"; +import { CreateCompanyWithProfileDto } from "./dto/create-company-with-profile.dto"; +import { ResponseCompanyDto } from "./dto/response-company.dto"; +import { ResponseExternalProfileDto } from "./dto/response-external-profile.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; @@ -25,36 +37,47 @@ interface CurrentIamUser { phoneNumber?: string; } -@ApiTags('Companies') -@Controller('companies') +@ApiTags("Companies") +@Controller("companies") export class CompaniesController { constructor( private readonly companiesService: CompaniesService, private readonly filesService: FilesService, - ) {} + ) { } - @Get('getInfo') - @ApiOperation({ summary: 'Get company info for the current user' }) - async getInfo(@CurrentUser() user: CurrentIamUser): Promise { - const { profile, company } = await this.companiesService.getCompanyInfoByUserId(user.id); + @Get("getInfo") + @ApiOperation({ summary: "Get company info for the current user" }) + async getInfo( + @CurrentUser() user: CurrentIamUser, + ): Promise { + const { profile, company } = + await this.companiesService.getCompanyInfoByUserId(user.id); return new CompanyInfoResponseDto(profile, company); } - @Get('profile') - @ApiOperation({ summary: 'Get flattened profile for the settings page' }) - async getProfile(@CurrentUser() user: CurrentIamUser): Promise { - const { profile, company } = await this.companiesService.getCompanyInfoByUserId(user.id); + @Get("profile") + @ApiOperation({ summary: "Get flattened profile for the settings page" }) + async getProfile( + @CurrentUser() user: CurrentIamUser, + ): Promise { + const { profile, company } = + await this.companiesService.getCompanyInfoByUserId(user.id); 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 { + @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)' }) + @Patch("profile") + @ApiOperation({ summary: "Update profile (flattened settings page)" }) async updateProfile( @CurrentUser() user: CurrentIamUser, @Body() dto: UpdateProfileDto, @@ -62,145 +85,134 @@ export class CompaniesController { return this.companiesService.updateProfile(user.id, dto); } - @Post('create') - @ApiOperation({ summary: 'Create a company with its associated external profile (onboarding)' }) + @Post("create") + @ApiOperation({ + summary: + "Create a company with its associated external profile (onboarding)", + }) async createWithProfile( @CurrentUser() user: CurrentIamUser, @Body() dto: CreateCompanyWithProfileDto, ): Promise { - const nameParts = (user.name?.en ?? '').split(' '); - const { profile, company } = await this.companiesService.createCompanyWithProfile( - { - userId: user.id, - firstName: nameParts[0] || '', - lastName: nameParts.slice(-1)[0] || '', - email: user.email ?? '', - phone: user.phoneNumber ?? '', - }, - dto, - ); + const nameParts = (user.name?.en ?? "").split(" "); + const { profile, company } = + await this.companiesService.createCompanyWithProfile( + { + userId: user.id, + firstName: nameParts[0] || "", + lastName: nameParts.slice(-1)[0] || "", + email: user.email ?? "", + phone: user.phoneNumber ?? "", + }, + dto, + ); return new CompanyInfoResponseDto(profile, company); } @Post() @FreightAdmin() - @ApiOperation({ summary: 'Create a new company (customer, forwarder, transporter, broker)' }) + @ApiOperation({ + summary: "Create a new company (customer, forwarder, transporter, broker)", + }) async create(@Body() dto: CreateCompanyDto): Promise { const company = await this.companiesService.createCompany(dto); return new ResponseCompanyDto(company); } @Get() - @ApiOperation({ summary: 'List all companies' }) + @ApiOperation({ summary: "List all companies" }) async findAll(): Promise { const companies = await this.companiesService.findAllCompanies(); return companies.map((c) => new ResponseCompanyDto(c)); } - @Get('type/:type') - @ApiOperation({ summary: 'Find companies by type' }) - async findByType(@Param('type') type: string): Promise { + @Get("type/:type") + @ApiOperation({ summary: "Find companies by type" }) + async findByType(@Param("type") type: string): Promise { const companies = await this.companiesService.findAllCompanies(); - return companies.filter((c) => c.type === type).map((c) => new ResponseCompanyDto(c)); + return companies + .filter((c) => c.type === type) + .map((c) => new ResponseCompanyDto(c)); } - @Get('search') - @ApiOperation({ summary: 'Search companies by name' }) - async search(@Query('name') name: string): Promise { + @Get("search") + @ApiOperation({ summary: "Search companies by name" }) + async search(@Query("name") name: string): Promise { const companies = await this.companiesService.findAllCompanies(); return companies .filter((c) => c.name.toLowerCase().includes(name.toLowerCase())) .map((c) => new ResponseCompanyDto(c)); } - @Get(':id') - @ApiOperation({ summary: 'Get company by ID' }) - async findById(@Param('id', ParseUUIDPipe) id: string): Promise { + @Get(":id") + @ApiOperation({ summary: "Get company by ID" }) + async findById( + @Param("id", ParseUUIDPipe) id: string, + ): Promise { const company = await this.companiesService.findCompanyById(id); return new ResponseCompanyDto(company); } - @Patch(':id') + @Patch(":id") @FreightAdmin() - @ApiOperation({ summary: 'Update a company' }) + @ApiOperation({ summary: "Update a company" }) async update( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateCompanyDto, ): Promise { const company = await this.companiesService.updateCompany(id, dto); return new ResponseCompanyDto(company); } - @Delete(':id') + @Delete(":id") @FreightAdmin() - @ApiOperation({ summary: 'Soft-delete a company' }) + @ApiOperation({ summary: "Soft-delete a company" }) @HttpCode(HttpStatus.NO_CONTENT) - async remove(@Param('id', ParseUUIDPipe) id: string): Promise { + async remove(@Param("id", ParseUUIDPipe) id: string): Promise { await this.companiesService.deleteCompany(id); } - @Post(':companyId/documents') + @Post(":companyId/documents") @UseInterceptors(AnyFilesInterceptor()) - @ApiConsumes('multipart/form-data') - @ApiOperation({ summary: 'Upload documents for a company (onboarding)' }) + @ApiConsumes("multipart/form-data") + @ApiOperation({ summary: "Upload documents for a company (onboarding)" }) async uploadDocuments( - @Param('companyId', ParseUUIDPipe) companyId: string, + @Param("companyId", ParseUUIDPipe) companyId: string, @UploadedFiles() files: Array, ) { - return this.filesService.uploadMany(companyId, 'companies', files); + return this.filesService.uploadMany(companyId, "companies", files); } - @Post(':companyId/profiles') + @Post(":companyId/profiles") @FreightAdmin() - @ApiOperation({ summary: 'Add a profile (employee) to a company' }) + @ApiOperation({ summary: "Add a profile (employee) to a company" }) async createProfile( - @Param('companyId', ParseUUIDPipe) companyId: string, + @Param("companyId", ParseUUIDPipe) companyId: string, @Body() dto: CreateExternalProfileDto, ): Promise { - const profile = await this.companiesService.createProfile({ ...dto, companyId }); + const profile = await this.companiesService.createProfile({ + ...dto, + companyId, + }); return new ResponseExternalProfileDto(profile); } - @Get(':companyId/profiles') - @ApiOperation({ summary: 'List profiles for a company' }) + @Get(":companyId/profiles") + @ApiOperation({ summary: "List profiles for a company" }) async listProfiles( - @Param('companyId', ParseUUIDPipe) companyId: string, + @Param("companyId", ParseUUIDPipe) companyId: string, ): Promise { - const profiles = await this.companiesService.findProfilesByCompany(companyId); + const profiles = + await this.companiesService.findProfilesByCompany(companyId); return profiles.map((p) => new ResponseExternalProfileDto(p)); } - @Get('profile/user/:userId') - @ApiOperation({ summary: 'Get profile by IAM user ID' }) + @Get("profile/user/:userId") + @ApiOperation({ summary: "Get profile by IAM user ID" }) async findProfileByUser( - @Param('userId', ParseUUIDPipe) userId: string, + @Param("userId", ParseUUIDPipe) userId: string, ): Promise { const profile = await this.companiesService.findProfileByUserId(userId); return new ResponseExternalProfileDto(profile); } - - @Post('ff-clients') - @FreightAdmin() - @ApiOperation({ summary: 'Link a forwarder to a client company' }) - async createFFClient(@Body() dto: CreateFFClientDto): Promise { - const client = await this.companiesService.createFFClient(dto); - return new ResponseFFClientDto(client); - } - - @Get(':forwarderCompanyId/clients') - @ApiOperation({ summary: 'List clients of a forwarder' }) - async listFFClients( - @Param('forwarderCompanyId', ParseUUIDPipe) forwarderCompanyId: string, - ): Promise { - const clients = await this.companiesService.findForwarderClients(forwarderCompanyId); - return clients.map((c) => new ResponseFFClientDto(c)); - } - - @Delete('ff-clients/:id') - @FreightAdmin() - @ApiOperation({ summary: 'Remove a forwarder-client relationship' }) - @HttpCode(HttpStatus.NO_CONTENT) - async removeFFClient(@Param('id', ParseUUIDPipe) id: string): Promise { - await this.companiesService.deleteFFClient(id); - } } 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 406c4f509..6c809e8b7 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.module.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.module.ts @@ -1,21 +1,27 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { FilesModule } from '../files/files.module'; -import { CompaniesController } from './companies.controller'; -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'; +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { FilesModule } from "../files/files.module"; +import { CompaniesController } from "./companies.controller"; +import { CompaniesService } from "./companies.service"; +import { CompaniesRepository } from "./companies.repository"; +import { ExternalProfileRepository } from "./external-profile.repository"; +import { CompanyDashboardRepository } from "./company-dashboard.repository"; +import { Company } from "./entities/company.entity"; +import { ExternalProfile } from "./entities/external-profile.entity"; +import { Booking } from "../bookings/entities/booking.entity"; @Module({ - imports: [TypeOrmModule.forFeature([Company, ExternalProfile, FFClient, Booking]), FilesModule], + imports: [ + TypeOrmModule.forFeature([Company, ExternalProfile, Booking]), + FilesModule, + ], controllers: [CompaniesController], - providers: [CompaniesService, CompaniesRepository, ExternalProfileRepository, FFClientRepository, CompanyDashboardRepository], + providers: [ + CompaniesService, + CompaniesRepository, + ExternalProfileRepository, + CompanyDashboardRepository, + ], exports: [CompaniesService], }) -export class CompaniesModule {} +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 086e8d767..702c25d4e 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -1,19 +1,20 @@ -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'; -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'; +import { + Injectable, + NotFoundException, + ConflictException, +} from "@nestjs/common"; +import { CompaniesRepository } from "./companies.repository"; +import { ExternalProfileRepository } from "./external-profile.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"; +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"; export interface UserIdentity { userId: string; @@ -28,9 +29,8 @@ export class CompaniesService { constructor( private readonly companiesRepo: CompaniesRepository, private readonly profilesRepo: ExternalProfileRepository, - private readonly ffClientsRepo: FFClientRepository, private readonly dashboardRepo: CompanyDashboardRepository, - ) {} + ) { } async createCompany(dto: CreateCompanyDto): Promise { const exists = await this.companiesRepo.existsByTin(dto.tin); @@ -40,27 +40,34 @@ export class CompaniesService { return this.companiesRepo.create(dto); } - async createCompanyWithProfile(identity: UserIdentity, dto: CreateCompanyWithProfileDto): Promise<{ company: Company; profile: ExternalProfile }> { + async createCompanyWithProfile( + identity: UserIdentity, + dto: CreateCompanyWithProfileDto, + ): Promise<{ company: Company; profile: ExternalProfile }> { if (dto.tin) { const exists = await this.companiesRepo.existsByTin(dto.tin); if (exists) { - throw new ConflictException(`Company with TIN ${dto.tin} already exists`); + throw new ConflictException( + `Company with TIN ${dto.tin} already exists`, + ); } } const existingProfile = await this.profilesRepo.findByEmail(identity.email); if (existingProfile) { - throw new ConflictException(`Profile with email ${identity.email} already exists`); + throw new ConflictException( + `Profile with email ${identity.email} already exists`, + ); } const company = await this.companiesRepo.create({ name: dto.companyName, type: dto.companyType, - tin: dto.tin ?? '', + tin: dto.tin ?? "", vatNumber: dto.vatNumber ?? null, businessLicense: dto.fanNumber ?? null, fanNumber: dto.fanNumber ?? null, - country: dto.companyLocation ?? 'Ethiopia', + country: dto.companyLocation ?? "Ethiopia", address: dto.companyAddress ?? null, phone: dto.companyPhone ?? null, email: dto.companyEmail ?? null, @@ -82,7 +89,7 @@ export class CompaniesService { } async findAllCompanies(): Promise { - return this.companiesRepo.findAll({ order: { name: 'ASC' as any } }); + return this.companiesRepo.findAll({ order: { name: "ASC" as any } }); } async findCompanyById(id: string): Promise { @@ -91,12 +98,18 @@ export class CompaniesService { return company; } - async getCompanyInfoByUserId(userId: string): Promise<{ profile: ExternalProfile; company: Company }> { + async getCompanyInfoByUserId( + userId: string, + ): Promise<{ profile: ExternalProfile; company: Company }> { const profile = await this.profilesRepo.findByUserId(userId); - if (!profile) throw new NotFoundException(`Profile for user ${userId} not found`); + if (!profile) + throw new NotFoundException(`Profile for user ${userId} not found`); const company = profile.company; - if (!company) throw new NotFoundException(`Company for profile ${profile.id} not found`); + if (!company) + throw new NotFoundException( + `Company for profile ${profile.id} not found`, + ); return { profile, company }; } @@ -114,7 +127,9 @@ export class CompaniesService { * column, so "delivered YTD" counts bookings created this year that reached a * delivered/completed status. */ - async getDashboardSummary(userId: string): Promise { + 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); @@ -125,7 +140,9 @@ export class CompaniesService { 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 prevYearToDate = new Date( + prevYearStart.getTime() + (now.getTime() - yearStart.getTime()), + ); const [ deliveredThis, @@ -139,20 +156,36 @@ export class CompaniesService { 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.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), + 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; + 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, + completionRate: + committedThis > 0 + ? Math.round((deliveredThis / committedThis) * 100) + : 0, spendYtd: spend.total, spendCurrency: spend.currency, spendYtdChangePct: this.changePct(spend.total, spendPrev), @@ -172,12 +205,12 @@ export class CompaniesService { deliveredCount: 0, completionRate: 0, spendYtd: 0, - spendCurrency: 'ETB', + spendCurrency: "ETB", spendYtdChangePct: 0, freightVolume: { totalTonnes: 0, totalValue: 0, - currency: 'ETB', + currency: "ETB", ytdChangePct: 0, monthly: this.buildMonthlySeries(now, []), }, @@ -190,8 +223,11 @@ export class CompaniesService { } /** 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 }; + 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)); } @@ -206,13 +242,29 @@ export class CompaniesService { 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 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) }); + series.push({ + month: labels[d.getMonth()], + tonnes: Math.round(byKey.get(key) ?? 0), + }); } return series; } @@ -224,7 +276,10 @@ export class CompaniesService { return updated; } - async updateProfile(userId: string, dto: UpdateProfileDto): Promise { + async updateProfile( + userId: string, + dto: UpdateProfileDto, + ): Promise { const { profile, company } = await this.getCompanyInfoByUserId(userId); const companyUpdates: Record = {}; @@ -233,8 +288,10 @@ export class CompaniesService { if (dto.companyName !== undefined) companyUpdates.name = dto.companyName; if (dto.companyEmail !== undefined) companyUpdates.email = dto.companyEmail; if (dto.companyPhone !== undefined) companyUpdates.phone = dto.companyPhone; - if (dto.companyLocation !== undefined) companyUpdates.country = dto.companyLocation; - if (dto.companyAddress !== undefined) companyUpdates.address = dto.companyAddress; + if (dto.companyLocation !== undefined) + companyUpdates.country = dto.companyLocation; + if (dto.companyAddress !== undefined) + companyUpdates.address = dto.companyAddress; if (dto.tin !== undefined) companyUpdates.tin = dto.tin; if (dto.vatNumber !== undefined) companyUpdates.vatNumber = dto.vatNumber; if (dto.fanNumber !== undefined) { @@ -242,21 +299,28 @@ export class CompaniesService { companyUpdates.fanNumber = dto.fanNumber; } - if (dto.contactPersonName !== undefined) attrUpdates.contactPersonName = dto.contactPersonName; - if (dto.contactPersonPhone !== undefined) attrUpdates.contactPersonPhone = dto.contactPersonPhone; - if (dto.generalManagerName !== undefined) attrUpdates.generalManagerName = dto.generalManagerName; - if (dto.generalManagerEmail !== undefined) attrUpdates.generalManagerEmail = dto.generalManagerEmail; - if (dto.generalManagerPhone !== undefined) attrUpdates.generalManagerPhone = dto.generalManagerPhone; + if (dto.contactPersonName !== undefined) + attrUpdates.contactPersonName = dto.contactPersonName; + if (dto.contactPersonPhone !== undefined) + attrUpdates.contactPersonPhone = dto.contactPersonPhone; + if (dto.generalManagerName !== undefined) + attrUpdates.generalManagerName = dto.generalManagerName; + if (dto.generalManagerEmail !== undefined) + attrUpdates.generalManagerEmail = dto.generalManagerEmail; + if (dto.generalManagerPhone !== undefined) + attrUpdates.generalManagerPhone = dto.generalManagerPhone; if (dto.poaName !== undefined) attrUpdates.poaName = dto.poaName; if (dto.poaPhone !== undefined) attrUpdates.poaPhone = dto.poaPhone; if (dto.poaEmail !== undefined) attrUpdates.poaEmail = dto.poaEmail; - if (dto.poaLocation !== undefined) attrUpdates.poaLocation = dto.poaLocation; + if (dto.poaLocation !== undefined) + attrUpdates.poaLocation = dto.poaLocation; if (dto.poaAddress !== undefined) attrUpdates.poaAddress = dto.poaAddress; companyUpdates.attributes = attrUpdates; const updated = await this.companiesRepo.update(company.id, companyUpdates); - if (!updated) throw new NotFoundException(`Company ${company.id} not found`); + if (!updated) + throw new NotFoundException(`Company ${company.id} not found`); return new ProfileResponseDto(profile, updated); } @@ -270,7 +334,9 @@ export class CompaniesService { const existing = await this.profilesRepo.findByEmail(dto.email); if (existing) { - throw new ConflictException(`Profile with email ${dto.email} already exists`); + throw new ConflictException( + `Profile with email ${dto.email} already exists`, + ); } return this.profilesRepo.create(dto); @@ -278,36 +344,12 @@ export class CompaniesService { async findProfileByUserId(userId: string): Promise { const profile = await this.profilesRepo.findByUserId(userId); - if (!profile) throw new NotFoundException(`Profile for user ${userId} not found`); + if (!profile) + throw new NotFoundException(`Profile for user ${userId} not found`); return profile; } async findProfilesByCompany(companyId: string): Promise { return this.profilesRepo.findByCompanyId(companyId); } - - async createFFClient(dto: CreateFFClientDto): Promise { - await this.findCompanyById(dto.forwarderCompanyId); - await this.findCompanyById(dto.clientCompanyId); - - const existing = await this.ffClientsRepo.findRelationship( - dto.forwarderCompanyId, - dto.clientCompanyId, - ); - if (existing) { - throw new ConflictException('This forwarder-client relationship already exists'); - } - - return this.ffClientsRepo.create(dto); - } - - async findForwarderClients(forwarderCompanyId: string): Promise { - return this.ffClientsRepo.findByForwarder(forwarderCompanyId); - } - - async deleteFFClient(id: string): Promise { - const client = await this.ffClientsRepo.findById(id); - if (!client) throw new NotFoundException(`FFClient ${id} not found`); - await this.ffClientsRepo.softDelete(id); - } } diff --git a/apps/edr-freight-api/src/modules/companies/dto/create-ff-client.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/create-ff-client.dto.ts deleted file mode 100644 index 46375d7ea..000000000 --- a/apps/edr-freight-api/src/modules/companies/dto/create-ff-client.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IsUUID, IsNotEmpty, IsOptional, IsBoolean, IsEnum } from 'class-validator'; -import { FFClientRelationship } from '../entities/ff-client.entity'; - -export class CreateFFClientDto { - @IsUUID() - @IsNotEmpty() - forwarderCompanyId!: string; - - @IsUUID() - @IsNotEmpty() - clientCompanyId!: string; - - @IsOptional() - @IsEnum(FFClientRelationship) - relationshipType?: FFClientRelationship; - - @IsOptional() - @IsBoolean() - canBookOnBehalf?: boolean; - - @IsOptional() - @IsBoolean() - canViewDocuments?: boolean; -} diff --git a/apps/edr-freight-api/src/modules/companies/dto/response-ff-client.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/response-ff-client.dto.ts deleted file mode 100644 index 44a48069b..000000000 --- a/apps/edr-freight-api/src/modules/companies/dto/response-ff-client.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { FFClient, FFClientRelationship } from '../entities/ff-client.entity'; - -export class ResponseFFClientDto { - id: string; - forwarderCompanyId: string; - clientCompanyId: string; - relationshipType: FFClientRelationship; - canBookOnBehalf: boolean; - canViewDocuments: boolean; - createdAt: Date; - updatedAt: Date; - - constructor(client: FFClient) { - this.id = client.id; - this.forwarderCompanyId = client.forwarderCompanyId; - this.clientCompanyId = client.clientCompanyId; - this.relationshipType = client.relationshipType; - this.canBookOnBehalf = client.canBookOnBehalf; - this.canViewDocuments = client.canViewDocuments; - this.createdAt = client.createdAt; - this.updatedAt = client.updatedAt; - } -} diff --git a/apps/edr-freight-api/src/modules/companies/dto/update-ff-client.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/update-ff-client.dto.ts deleted file mode 100644 index a7ace689d..000000000 --- a/apps/edr-freight-api/src/modules/companies/dto/update-ff-client.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreateFFClientDto } from './create-ff-client.dto'; - -export class UpdateFFClientDto extends PartialType(CreateFFClientDto) {} diff --git a/apps/edr-freight-api/src/modules/companies/entities/ff-client.entity.ts b/apps/edr-freight-api/src/modules/companies/entities/ff-client.entity.ts deleted file mode 100644 index 136dea277..000000000 --- a/apps/edr-freight-api/src/modules/companies/entities/ff-client.entity.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { BaseEntity } from '@edr/api-common'; -import { Column, Entity, Index, ManyToOne, JoinColumn, Unique } from 'typeorm'; -import { Company } from './company.entity'; - -export enum FFClientRelationship { - ManagedAccount = 'managed_account', - SubAgent = 'sub_agent', -} - -@Entity({ schema: 'freight', name: 'ff_clients' }) -@Unique(['forwarderCompanyId', 'clientCompanyId']) -@Index(['forwarderCompanyId']) -@Index(['clientCompanyId']) -export class FFClient extends BaseEntity { - @Column({ name: 'forwarder_company_id', type: 'uuid' }) - forwarderCompanyId!: string; - - @ManyToOne(() => Company) - @JoinColumn({ name: 'forwarder_company_id' }) - forwarderCompany!: Company; - - @Column({ name: 'client_company_id', type: 'uuid' }) - clientCompanyId!: string; - - @ManyToOne(() => Company) - @JoinColumn({ name: 'client_company_id' }) - clientCompany!: Company; - - @Column({ name: 'relationship_type', type: 'varchar', length: 32, default: FFClientRelationship.ManagedAccount }) - relationshipType!: FFClientRelationship; - - @Column({ name: 'can_book_on_behalf', type: 'boolean', default: true }) - canBookOnBehalf!: boolean; - - @Column({ name: 'can_view_documents', type: 'boolean', default: true }) - canViewDocuments!: boolean; -} diff --git a/apps/edr-freight-api/src/modules/companies/ff-client.repository.ts b/apps/edr-freight-api/src/modules/companies/ff-client.repository.ts deleted file mode 100644 index b94cedec6..000000000 --- a/apps/edr-freight-api/src/modules/companies/ff-client.repository.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { BaseRepository } from '@edr/api-common'; -import { FFClient } from './entities/ff-client.entity'; - -@Injectable() -export class FFClientRepository extends BaseRepository { - constructor( - @InjectRepository(FFClient) - repo: Repository, - ) { - super(repo); - } - - async findByForwarder(forwarderCompanyId: string): Promise { - return this.repository.find({ where: { forwarderCompanyId } as any }); - } - - async findByClient(clientCompanyId: string): Promise { - return this.repository.find({ where: { clientCompanyId } as any }); - } - - async findRelationship( - forwarderCompanyId: string, - clientCompanyId: string, - ): Promise { - return this.repository.findOne({ - where: { forwarderCompanyId, clientCompanyId } as any, - }); - } -}