mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
feat: finish company profile in the backoffice
This commit is contained in:
@@ -136,6 +136,14 @@ export class BookingsController {
|
||||
return this.bookingsService.findAll(filter, companyId);
|
||||
}
|
||||
|
||||
@Get('by-company/:companyId/customer-view')
|
||||
@ApiOperation({ summary: 'List bookings for a company (customer-view shape, backoffice)' })
|
||||
findByCompanyCustomerView(
|
||||
@Param('companyId', ParseUUIDPipe) companyId: string,
|
||||
) {
|
||||
return this.bookingsService.findCustomerBookings(companyId);
|
||||
}
|
||||
|
||||
@Get('list-summary')
|
||||
@ApiOperation({ summary: 'Booking list metrics and tab counts (backoffice)' })
|
||||
@ApiOkResponse({ type: BookingListSummaryDto })
|
||||
|
||||
@@ -1036,4 +1036,37 @@ export class BookingsService {
|
||||
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async findCustomerBookings(companyId: string): Promise<{
|
||||
id: string;
|
||||
reference: string;
|
||||
status: string;
|
||||
tradeDirection: string;
|
||||
freightType: string;
|
||||
originLabel: string;
|
||||
destinationLabel: string;
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
scheduledDate: Date | null;
|
||||
createdAt: Date;
|
||||
}[]> {
|
||||
const { items } = await this.bookingsRepository.findAllPaginated({
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
companyId,
|
||||
});
|
||||
return items.map((b) => ({
|
||||
id: b.id,
|
||||
reference: b.reference,
|
||||
status: b.status,
|
||||
tradeDirection: b.tradeDirection,
|
||||
freightType: b.freightType,
|
||||
originLabel: b.originYard?.label ?? '',
|
||||
destinationLabel: b.destinationYard?.label ?? '',
|
||||
totalAmount: Number(b.totalAmount),
|
||||
currency: b.paymentCurrency,
|
||||
scheduledDate: b.scheduledDate ?? null,
|
||||
createdAt: b.createdAt,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ 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 { ListCompaniesQueryDto } from "./dto/list-companies-query.dto";
|
||||
import { CompanyStatsResponseDto } from "./dto/company-stats-response.dto";
|
||||
import { UpdateCompanyProfileStatusDto } from "./dto/update-company-profile-status.dto";
|
||||
|
||||
interface CurrentIamUser {
|
||||
id: string;
|
||||
@@ -142,29 +145,19 @@ export class CompaniesController {
|
||||
return new ResponseCompanyDto(company);
|
||||
}
|
||||
|
||||
@Get("stats")
|
||||
@ApiOperation({ summary: "Company counts by status (KPI strip)" })
|
||||
async getStats(): Promise<CompanyStatsResponseDto> {
|
||||
return this.companiesService.getCompanyStats();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: "List all companies" })
|
||||
async findAll(): Promise<ResponseCompanyDto[]> {
|
||||
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<ResponseCompanyDto[]> {
|
||||
const companies = await this.companiesService.findAllCompanies();
|
||||
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<ResponseCompanyDto[]> {
|
||||
const companies = await this.companiesService.findAllCompanies();
|
||||
return companies
|
||||
.filter((c) => c.name.toLowerCase().includes(name.toLowerCase()))
|
||||
.map((c) => new ResponseCompanyDto(c));
|
||||
@ApiOperation({ summary: "List companies (paginated, filterable)" })
|
||||
async findAll(
|
||||
@Query() query: ListCompaniesQueryDto,
|
||||
): Promise<{ items: ResponseCompanyDto[]; total: number }> {
|
||||
const { items, total } = await this.companiesService.listCompanies(query);
|
||||
return { items: items.map((c) => new ResponseCompanyDto(c)), total };
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@@ -195,6 +188,23 @@ export class CompaniesController {
|
||||
await this.companiesService.deleteCompany(id);
|
||||
}
|
||||
|
||||
@Get(":companyId/documents")
|
||||
@ApiOperation({ summary: "List documents uploaded for a company" })
|
||||
async listDocuments(
|
||||
@Param("companyId", ParseUUIDPipe) companyId: string,
|
||||
) {
|
||||
const files = await this.filesService.findByResource(companyId, "companies");
|
||||
return files.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
code: f.code,
|
||||
mimeType: f.mimeType,
|
||||
size: f.size,
|
||||
uploadedAt: f.createdAt,
|
||||
url: f.url,
|
||||
}));
|
||||
}
|
||||
|
||||
@Post(":companyId/documents")
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@@ -206,6 +216,20 @@ export class CompaniesController {
|
||||
return this.filesService.uploadMany(companyId, "companies", files);
|
||||
}
|
||||
|
||||
@Patch("company-profiles/:profileId/status")
|
||||
@FreightAdmin()
|
||||
@ApiOperation({ summary: "Update a company profile's approval status" })
|
||||
async updateCompanyProfileStatus(
|
||||
@Param("profileId", ParseUUIDPipe) profileId: string,
|
||||
@Body() dto: UpdateCompanyProfileStatusDto,
|
||||
): Promise<ResponseCompanyProfileDto> {
|
||||
const profile = await this.companiesService.setCompanyProfileStatus(
|
||||
profileId,
|
||||
dto.status,
|
||||
);
|
||||
return new ResponseCompanyProfileDto(profile);
|
||||
}
|
||||
|
||||
@Post(":companyId/profiles")
|
||||
@FreightAdmin()
|
||||
@ApiOperation({ summary: "Add a profile (employee) to a company" })
|
||||
|
||||
@@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseRepository } from '@edr/api-common';
|
||||
import { Company } from './entities/company.entity';
|
||||
import { ListCompaniesQueryDto } from './dto/list-companies-query.dto';
|
||||
import { CompanyStatsResponseDto } from './dto/company-stats-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CompaniesRepository extends BaseRepository<Company> {
|
||||
@@ -32,4 +34,68 @@ export class CompaniesRepository extends BaseRepository<Company> {
|
||||
const count = await this.repository.count({ where: { tin } as any });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
async findPaginated(
|
||||
query: ListCompaniesQueryDto,
|
||||
): Promise<{ items: Company[]; total: number }> {
|
||||
const { page = 1, pageSize = 20, search, type, status } = query;
|
||||
|
||||
const qb = this.repository
|
||||
.createQueryBuilder('company')
|
||||
.leftJoinAndSelect('company.companyProfiles', 'companyProfiles')
|
||||
.where('company.deleted_at IS NULL');
|
||||
|
||||
if (type) {
|
||||
qb.andWhere('company.type = :type', { type });
|
||||
}
|
||||
|
||||
if (status) {
|
||||
qb.andWhere('company.status = :status', { status });
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const term = `%${search.trim()}%`;
|
||||
qb.andWhere(
|
||||
`(company.name ILIKE :term
|
||||
OR company.tin ILIKE :term
|
||||
OR company.email ILIKE :term
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM freight.company_profiles cp
|
||||
WHERE cp.company_id = company.id
|
||||
AND cp.reference ILIKE :term
|
||||
AND cp.deleted_at IS NULL
|
||||
))`,
|
||||
{ term },
|
||||
);
|
||||
}
|
||||
|
||||
const [items, total] = await qb
|
||||
.orderBy('company.name', 'ASC')
|
||||
.skip((page - 1) * pageSize)
|
||||
.take(pageSize)
|
||||
.getManyAndCount();
|
||||
|
||||
return { items, total };
|
||||
}
|
||||
|
||||
async getStats(): Promise<CompanyStatsResponseDto> {
|
||||
const rows: { status: string; count: string }[] = await this.repository
|
||||
.createQueryBuilder('company')
|
||||
.select('company.status', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('company.deleted_at IS NULL')
|
||||
.groupBy('company.status')
|
||||
.getRawMany();
|
||||
|
||||
const map = new Map(rows.map((r) => [r.status, parseInt(r.count, 10)]));
|
||||
const total = rows.reduce((sum, r) => sum + parseInt(r.count, 10), 0);
|
||||
|
||||
return {
|
||||
total,
|
||||
active: map.get('active') ?? 0,
|
||||
pending: map.get('pending') ?? 0,
|
||||
suspended: map.get('suspended') ?? 0,
|
||||
blacklisted: map.get('blacklisted') ?? 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import { CreateCompanyWithProfileDto } from "./dto/create-company-with-profile.d
|
||||
import { UpdateProfileDto } from "./dto/update-profile.dto";
|
||||
import { ProfileResponseDto } from "./dto/profile-response.dto";
|
||||
import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-response.dto";
|
||||
import { ListCompaniesQueryDto } from "./dto/list-companies-query.dto";
|
||||
import { CompanyStatsResponseDto } from "./dto/company-stats-response.dto";
|
||||
import { Company } from "./entities/company.entity";
|
||||
import { ExternalProfile } from "./entities/external-profile.entity";
|
||||
import {
|
||||
@@ -123,6 +125,16 @@ export class CompaniesService {
|
||||
return { company, profile };
|
||||
}
|
||||
|
||||
async listCompanies(
|
||||
query: ListCompaniesQueryDto,
|
||||
): Promise<{ items: Company[]; total: number }> {
|
||||
return this.companiesRepo.findPaginated(query);
|
||||
}
|
||||
|
||||
async getCompanyStats(): Promise<CompanyStatsResponseDto> {
|
||||
return this.companiesRepo.getStats();
|
||||
}
|
||||
|
||||
async findAllCompanies(): Promise<Company[]> {
|
||||
return this.companiesRepo.findAll({ order: { name: "ASC" } });
|
||||
}
|
||||
@@ -405,6 +417,16 @@ export class CompaniesService {
|
||||
}
|
||||
}
|
||||
|
||||
async setCompanyProfileStatus(
|
||||
profileId: string,
|
||||
status: ProfileStatus,
|
||||
): Promise<CompanyProfile> {
|
||||
const updated = await this.companyProfilesRepo.updateStatus(profileId, status);
|
||||
if (!updated)
|
||||
throw new NotFoundException(`Company profile ${profileId} not found`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async createCompanyProfile(
|
||||
companyId: string,
|
||||
profileType?: ProfileType,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { BaseRepository } from "@edr/api-common";
|
||||
import { CompanyProfile, ProfileType } from "./entities/company-profile.entity";
|
||||
import { CompanyProfile, ProfileStatus, ProfileType } from "./entities/company-profile.entity";
|
||||
|
||||
const SEQUENCE_MAP: Record<ProfileType, string> = {
|
||||
[ProfileType.exporter]: "seq_company_profile_ex",
|
||||
@@ -58,4 +58,16 @@ export class CompanyProfileRepository extends BaseRepository<CompanyProfile> {
|
||||
async findByReference(reference: string): Promise<CompanyProfile | null> {
|
||||
return this.repository.findOne({ where: { reference } });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<CompanyProfile | null> {
|
||||
return this.repository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async updateStatus(
|
||||
id: string,
|
||||
status: ProfileStatus,
|
||||
): Promise<CompanyProfile | null> {
|
||||
await this.repository.update({ id }, { status });
|
||||
return this.repository.findOne({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export class CompanyStatsResponseDto {
|
||||
total!: number;
|
||||
active!: number;
|
||||
pending!: number;
|
||||
suspended!: number;
|
||||
blacklisted!: number;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsIn, IsInt, IsOptional, IsString, Min } from "class-validator";
|
||||
import { Transform } from "class-transformer";
|
||||
import { CompanyStatus, CompanyType } from "../entities/company.entity";
|
||||
|
||||
export class ListCompaniesQueryDto {
|
||||
@ApiPropertyOptional({ default: 1 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }: { value: unknown }) => parseInt(String(value), 10))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ default: 20 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }: { value: unknown }) => parseInt(String(value), 10))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number = 20;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: CompanyType })
|
||||
@IsOptional()
|
||||
@IsIn(Object.values(CompanyType))
|
||||
type?: CompanyType;
|
||||
|
||||
@ApiPropertyOptional({ enum: CompanyStatus })
|
||||
@IsOptional()
|
||||
@IsIn(Object.values(CompanyStatus))
|
||||
status?: CompanyStatus;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ResponseExternalProfileDto } from './response-external-profile.dto';
|
||||
|
||||
export class ResponseCompanyProfileDto {
|
||||
id: string;
|
||||
companyId: string;
|
||||
type: string;
|
||||
reference: string;
|
||||
status: string;
|
||||
@@ -14,6 +15,7 @@ export class ResponseCompanyProfileDto {
|
||||
|
||||
constructor(profile: CompanyProfile) {
|
||||
this.id = profile.id;
|
||||
this.companyId = profile.companyId;
|
||||
this.type = profile.type;
|
||||
this.reference = profile.reference;
|
||||
this.status = profile.status;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsIn } from "class-validator";
|
||||
import { ProfileStatus } from "../entities/company-profile.entity";
|
||||
|
||||
export class UpdateCompanyProfileStatusDto {
|
||||
@ApiProperty({ enum: ProfileStatus })
|
||||
@IsIn(Object.values(ProfileStatus))
|
||||
status!: ProfileStatus;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Get,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
@@ -33,6 +34,14 @@ import {
|
||||
export class PaymentController {
|
||||
constructor(private readonly paymentService: PaymentService) { }
|
||||
|
||||
@Get("by-company/:companyId/customer-view")
|
||||
@ApiOperation({ summary: "List payments for a company (customer-view shape, backoffice)" })
|
||||
findByCompanyCustomerView(
|
||||
@Param("companyId", ParseUUIDPipe) companyId: string,
|
||||
) {
|
||||
return this.paymentService.findByCompanyId(companyId);
|
||||
}
|
||||
|
||||
@Get("summary")
|
||||
@BookingView()
|
||||
@ApiOperation({ summary: "Payment count/amount summary for dashboard cards" })
|
||||
|
||||
@@ -61,4 +61,56 @@ export class PaymentRepository {
|
||||
return this.paymentRepo.createQueryBuilder(alias);
|
||||
}
|
||||
|
||||
async findByCompanyId(companyId: string): Promise<{
|
||||
id: string;
|
||||
merchantOrderId: string;
|
||||
bookingReference: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
method: string;
|
||||
status: string;
|
||||
paidAt: Date | null;
|
||||
createdAt: Date;
|
||||
}[]> {
|
||||
const rows: {
|
||||
id: string;
|
||||
merchant_order_id: string;
|
||||
booking_reference: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
method: string;
|
||||
status: string;
|
||||
paid_at: Date | null;
|
||||
created_at: Date;
|
||||
}[] = await this.dataSource.query(
|
||||
`SELECT p.id,
|
||||
p.merchant_order_id,
|
||||
b.reference AS booking_reference,
|
||||
p.amount,
|
||||
p.currency,
|
||||
p.method,
|
||||
p.status,
|
||||
p.paid_at,
|
||||
p.created_at
|
||||
FROM freight.payments p
|
||||
JOIN freight.bookings b ON b.id = p.ref_id
|
||||
WHERE b.company_id = $1
|
||||
AND p.deleted_at IS NULL
|
||||
AND b.deleted_at IS NULL
|
||||
ORDER BY p.created_at DESC`,
|
||||
[companyId],
|
||||
);
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
merchantOrderId: r.merchant_order_id,
|
||||
bookingReference: r.booking_reference,
|
||||
amount: Number(r.amount),
|
||||
currency: r.currency,
|
||||
method: r.method,
|
||||
status: r.status,
|
||||
paidAt: r.paid_at,
|
||||
createdAt: r.created_at,
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -428,4 +428,8 @@ export class PaymentService {
|
||||
default: return "action-required";
|
||||
}
|
||||
}
|
||||
|
||||
async findByCompanyId(companyId: string) {
|
||||
return this.paymentRepo.findByCompanyId(companyId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user