diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index ae0f1765a..edb91ef6b 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -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 }) diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index 4c8ef2cab..7db2870fc 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -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, + })); + } } 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 ac2868ec3..34fe46505 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -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 { + return this.companiesService.getCompanyStats(); + } + @Get() - @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 { - 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 { - 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 { + 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" }) diff --git a/apps/edr-freight-api/src/modules/companies/companies.repository.ts b/apps/edr-freight-api/src/modules/companies/companies.repository.ts index 1156823f7..b31f2939d 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.repository.ts @@ -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 { @@ -32,4 +34,68 @@ export class CompaniesRepository extends BaseRepository { 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 { + 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, + }; + } } 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 fe1bc5598..40b82e3f0 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -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 { + return this.companiesRepo.getStats(); + } + async findAllCompanies(): Promise { return this.companiesRepo.findAll({ order: { name: "ASC" } }); } @@ -405,6 +417,16 @@ export class CompaniesService { } } + async setCompanyProfileStatus( + profileId: string, + status: ProfileStatus, + ): Promise { + 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, diff --git a/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts b/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts index db7427112..24210dc4f 100644 --- a/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts @@ -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.exporter]: "seq_company_profile_ex", @@ -58,4 +58,16 @@ export class CompanyProfileRepository extends BaseRepository { async findByReference(reference: string): Promise { return this.repository.findOne({ where: { reference } }); } + + async findById(id: string): Promise { + return this.repository.findOne({ where: { id } }); + } + + async updateStatus( + id: string, + status: ProfileStatus, + ): Promise { + await this.repository.update({ id }, { status }); + return this.repository.findOne({ where: { id } }); + } } diff --git a/apps/edr-freight-api/src/modules/companies/dto/company-stats-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/company-stats-response.dto.ts new file mode 100644 index 000000000..a6b8b3b6e --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/company-stats-response.dto.ts @@ -0,0 +1,7 @@ +export class CompanyStatsResponseDto { + total!: number; + active!: number; + pending!: number; + suspended!: number; + blacklisted!: number; +} diff --git a/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts new file mode 100644 index 000000000..c92592286 --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/list-companies-query.dto.ts @@ -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; +} diff --git a/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts index cb7777e8b..28097ad71 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts @@ -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; diff --git a/apps/edr-freight-api/src/modules/companies/dto/update-company-profile-status.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/update-company-profile-status.dto.ts new file mode 100644 index 000000000..96c02d846 --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/dto/update-company-profile-status.dto.ts @@ -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; +} diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index 14308883d..f1f34c3b1 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -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" }) diff --git a/apps/edr-freight-api/src/modules/payment/payment.repository.ts b/apps/edr-freight-api/src/modules/payment/payment.repository.ts index 8c830a20f..25c3bdd6b 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.repository.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.repository.ts @@ -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, + })); + } + } \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index 177b7475b..efeaf3428 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -428,4 +428,8 @@ export class PaymentService { default: return "action-required"; } } + + async findByCompanyId(companyId: string) { + return this.paymentRepo.findByCompanyId(companyId); + } } diff --git a/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx b/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx index 4ebdc675d..e108d1b1f 100644 --- a/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx +++ b/apps/edr-freight-web/backoffice/src/components/customers/badges.tsx @@ -1,4 +1,6 @@ -import { Badge, Group, Tooltip } from "@mantine/core"; +import { Badge, Button, Group, Tooltip } from "@mantine/core"; +import { useMutation } from "@tanstack/react-query"; +import { api } from "@/services/api"; import type { CompanyProfile, @@ -207,3 +209,108 @@ export function PaymentStatusBadge({ status }: { status: CustomerPaymentStatus } ); } + +/** + * Inline approval action buttons for a profile row. + * Transitions: pending → approve/reject | active → suspend | suspended → reactivate/blacklist | blacklisted → reinstate + */ +export function ProfileApprovalActions({ + profileId, + status, +}: { + profileId: string; + status: ProfileStatus; +}) { + const { mutate, isPending } = useMutation( + api.customers.setProfileStatus.mutationOptions(), + ); + + const act = (next: ProfileStatus) => + mutate({ profileId, status: next }); + + if (status === "pending") { + return ( + + + + + ); + } + + if (status === "active") { + return ( + + ); + } + + if (status === "suspended") { + return ( + + + + + ); + } + + if (status === "blacklisted") { + return ( + + ); + } + + return null; +} diff --git a/apps/edr-freight-web/backoffice/src/components/customers/index.ts b/apps/edr-freight-web/backoffice/src/components/customers/index.ts index f94ca5a73..61b75767a 100644 --- a/apps/edr-freight-web/backoffice/src/components/customers/index.ts +++ b/apps/edr-freight-web/backoffice/src/components/customers/index.ts @@ -3,6 +3,7 @@ export { CompanyStatusBadge, CompanyTypeBadge, PaymentStatusBadge, + ProfileApprovalActions, ProfileChips, ProfileStatusBadge, ProfileTypeBadge, 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 8d91b6f0d..aced8cda4 100644 --- a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts @@ -27,6 +27,7 @@ export const QUERY_KEYS = { CUSTOMERS: { ROOT: ["customers"] as const, + stats: ["customers", "stats"] as const, list: (filter?: CompanyListFilter) => ["customers", "list", filter ?? {}] as const, byId: (id: string) => ["customers", "detail", id] as const, diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index b1412b9a1..e0d57fbf4 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -71,7 +71,12 @@ export const URL_CONSTANTS = { COMPANIES: { BASE: "/companies", + STATS: "/companies/stats", BY_ID: (id: string | number) => `/companies/${id}`, + DOCUMENTS: (id: string) => `/companies/${id}/documents`, + PROFILE_STATUS: (profileId: string) => `/companies/company-profiles/${profileId}/status`, + BOOKINGS_CUSTOMER_VIEW: (id: string) => `/bookings/by-company/${id}/customer-view`, + PAYMENTS_CUSTOMER_VIEW: (id: string) => `/payments/by-company/${id}/customer-view`, }, CUSTOMERS_API: { diff --git a/apps/edr-freight-web/backoffice/src/hooks/customers/useCustomers.ts b/apps/edr-freight-web/backoffice/src/hooks/customers/useCustomers.ts deleted file mode 100644 index 11af924ca..000000000 --- a/apps/edr-freight-web/backoffice/src/hooks/customers/useCustomers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; -import { customersService } from "@/services/customers.service"; -import type { CompanyListFilter } from "@/types/customer"; - -export function useCustomerList(filter: CompanyListFilter) { - return useQuery({ - queryKey: QUERY_KEYS.CUSTOMERS.list(filter), - queryFn: () => customersService.list(filter), - }); -} - -export function useCustomerDetail(id: string | undefined) { - return useQuery({ - queryKey: QUERY_KEYS.CUSTOMERS.byId(id ?? ""), - queryFn: () => customersService.getById(id!), - enabled: Boolean(id), - }); -} - -export function useCustomerBookings(id: string | undefined) { - return useQuery({ - queryKey: QUERY_KEYS.CUSTOMERS.bookings(id ?? ""), - queryFn: () => customersService.bookingsFor(id!), - enabled: Boolean(id), - }); -} - -export function useCustomerDocuments(id: string | undefined) { - return useQuery({ - queryKey: QUERY_KEYS.CUSTOMERS.documents(id ?? ""), - queryFn: () => customersService.documentsFor(id!), - enabled: Boolean(id), - }); -} - -export function useCustomerPayments(id: string | undefined) { - return useQuery({ - queryKey: QUERY_KEYS.CUSTOMERS.payments(id ?? ""), - queryFn: () => customersService.paymentsFor(id!), - enabled: Boolean(id), - }); -} diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx index dd492281b..f38e8c38b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx @@ -22,6 +22,7 @@ import { LayoutGrid, Package, } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; import { useNavigate, useParams } from "react-router-dom"; @@ -30,6 +31,7 @@ import { CompanyStatusBadge, CompanyTypeBadge, PaymentStatusBadge, + ProfileApprovalActions, ProfileChips, ProfileStatusBadge, ProfileTypeBadge, @@ -40,12 +42,7 @@ import { humanize, } from "@/components/customers"; import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; -import { - useCustomerBookings, - useCustomerDetail, - useCustomerDocuments, - useCustomerPayments, -} from "@/hooks/customers/useCustomers"; +import { api } from "@/services/api"; import type { CompanyProfile, CustomerBooking, @@ -81,10 +78,30 @@ export default function CustomerDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { data: company, isLoading } = useCustomerDetail(id); - const bookingsQuery = useCustomerBookings(id); - const documentsQuery = useCustomerDocuments(id); - const paymentsQuery = useCustomerPayments(id); + const { data: company, isLoading } = useQuery( + api.customers.getById.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + const bookingsQuery = useQuery( + api.customers.bookings.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + const documentsQuery = useQuery( + api.customers.documents.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + const paymentsQuery = useQuery( + api.customers.payments.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); const bookings = bookingsQuery.data ?? []; const documents = documentsQuery.data ?? []; @@ -132,13 +149,23 @@ export default function CustomerDetailPage() { { id: "createdAt", header: "Registered", - meta: { headerClassName: "text-right", cellClassName: "text-right" }, cell: ({ row }) => ( {formatDate(row.original.createdAt)} ), }, + { + id: "actions", + header: "", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + ), + }, ], [], ); @@ -402,6 +429,14 @@ export default function CustomerDetailPage() { icon: IdCard, color: "edr-green", }, + { + label: "Pending approval", + value: company.companyProfiles.filter( + (p) => p.status === "pending", + ).length, + icon: IdCard, + color: "yellow", + }, { label: "Bookings", value: bookings.length, @@ -414,12 +449,6 @@ export default function CustomerDetailPage() { icon: Banknote, color: "edr-green", }, - { - label: "Documents", - value: documents.length, - icon: FileText, - color: "grape", - }, ]} /> @@ -472,7 +501,7 @@ export default function CustomerDetailPage() { - + c.status === "active").length, - pending: MOCK_COMPANIES.filter((c) => c.status === "pending").length, - blacklisted: MOCK_COMPANIES.filter((c) => c.status === "blacklisted").length, -}; - export default function CustomersPage() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); @@ -63,8 +55,11 @@ export default function CustomersPage() { [pagination.pageIndex, pagination.pageSize, debouncedQuery], ); - const { data, isLoading, isError, refetch, isFetching } = - useCustomerList(filter); + const { data: stats } = useQuery(api.customers.stats.queryOptions({ input: {} })); + + const { data, isLoading, isError, refetch, isFetching } = useQuery( + api.customers.list.queryOptions({ input: { filter } }), + ); const rows = data?.items ?? []; const total = data?.total ?? 0; @@ -184,12 +179,12 @@ export default function CustomersPage() { new Date(date).toISOString(); - -export const MOCK_COMPANIES: Company[] = [ - { - id: "c1", - name: "Abyssinia Import & Export PLC", - type: "customer", - status: "active", - tin: "0012345678", - vatNumber: "VAT-100245", - fanNumber: "FAN-0099", - country: "Ethiopia", - address: "Bole Road, Addis Ababa", - phone: "+251 911 234 567", - email: "ops@abyssinia-ie.com", - contactPersonName: "Selam Bekele", - contactPersonPhone: "+251 911 234 567", - generalManagerName: "Dawit Tadesse", - generalManagerEmail: "dawit@abyssinia-ie.com", - generalManagerPhone: "+251 911 000 111", - website: "https://abyssinia-ie.com", - attributes: null, - companyProfiles: [ - { - id: "p1", - companyId: "c1", - type: "importer", - reference: "IMP-00123", - status: "active", - businessLicense: "BL-IMP-4521", - createdAt: iso("2024-02-10"), - updatedAt: iso("2024-02-10"), - }, - { - id: "p2", - companyId: "c1", - type: "exporter", - reference: "EXP-00456", - status: "active", - businessLicense: "BL-EXP-7782", - createdAt: iso("2024-03-01"), - updatedAt: iso("2024-03-01"), - }, - ], - createdAt: iso("2024-02-10"), - updatedAt: iso("2025-01-12"), - }, - { - id: "c2", - name: "Horn Logistics Freight Forwarding", - type: "freight_forwarder", - status: "active", - tin: "0023456789", - vatNumber: "VAT-200112", - fanNumber: null, - country: "Ethiopia", - address: "Kality Industrial Zone, Addis Ababa", - phone: "+251 911 765 432", - email: "dispatch@hornlogistics.et", - contactPersonName: "Yonas Girma", - contactPersonPhone: "+251 911 765 432", - generalManagerName: "Hanna Mekonnen", - generalManagerEmail: "hanna@hornlogistics.et", - generalManagerPhone: "+251 911 222 333", - website: "https://hornlogistics.et", - attributes: null, - companyProfiles: [ - { - id: "p3", - companyId: "c2", - type: "freight_forwarder", - reference: "FFW-01001", - status: "active", - businessLicense: "BL-FFW-1190", - createdAt: iso("2023-11-05"), - updatedAt: iso("2023-11-05"), - }, - { - id: "p4", - companyId: "c2", - type: "importer", - reference: "IMP-00890", - status: "pending", - businessLicense: null, - createdAt: iso("2024-06-18"), - updatedAt: iso("2024-06-18"), - }, - { - id: "p5", - companyId: "c2", - type: "exporter", - reference: "EXP-00777", - status: "active", - businessLicense: "BL-EXP-3310", - createdAt: iso("2024-07-01"), - updatedAt: iso("2024-07-01"), - }, - ], - createdAt: iso("2023-11-05"), - updatedAt: iso("2025-02-20"), - }, - { - id: "c3", - name: "Djibouti Gateway Forwarders", - type: "dj_freight_forwarder", - status: "active", - tin: "0034567890", - vatNumber: null, - fanNumber: "FAN-0451", - country: "Djibouti", - address: "Port de Djibouti, Djibouti City", - phone: "+253 21 35 00 00", - email: "ops@djgateway.dj", - contactPersonName: "Amina Idriss", - contactPersonPhone: "+253 77 12 34 56", - generalManagerName: "Omar Farah", - generalManagerEmail: "omar@djgateway.dj", - generalManagerPhone: "+253 77 99 88 77", - website: null, - attributes: null, - companyProfiles: [ - { - id: "p6", - companyId: "c3", - type: "dj_freight_forwarder", - reference: "DJF-02050", - status: "active", - businessLicense: "BL-DJF-0088", - createdAt: iso("2023-09-12"), - updatedAt: iso("2023-09-12"), - }, - ], - createdAt: iso("2023-09-12"), - updatedAt: iso("2024-12-30"), - }, - { - id: "c4", - name: "Rift Valley Transporters", - type: "transporter", - status: "suspended", - tin: "0045678901", - vatNumber: "VAT-300988", - fanNumber: null, - country: "Ethiopia", - address: "Adama Ring Road, Adama", - phone: "+251 912 345 678", - email: "fleet@riftvalley-tr.com", - contactPersonName: "Bereket Alemu", - contactPersonPhone: "+251 912 345 678", - generalManagerName: "Meron Haile", - generalManagerEmail: "meron@riftvalley-tr.com", - generalManagerPhone: "+251 912 111 222", - website: null, - attributes: null, - companyProfiles: [ - { - id: "p7", - companyId: "c4", - type: "transporter", - reference: "TRP-03012", - status: "suspended", - businessLicense: "BL-TRP-2204", - createdAt: iso("2024-01-22"), - updatedAt: iso("2024-10-04"), - }, - ], - createdAt: iso("2024-01-22"), - updatedAt: iso("2024-10-04"), - }, - { - id: "c5", - name: "Nile Trading & General Import", - type: "customer", - status: "pending", - tin: "0056789012", - vatNumber: null, - fanNumber: null, - country: "Ethiopia", - address: "Merkato, Addis Ababa", - phone: "+251 913 456 789", - email: "info@niletrading.et", - contactPersonName: "Sara Tesfaye", - contactPersonPhone: "+251 913 456 789", - generalManagerName: "Kebede Worku", - generalManagerEmail: "kebede@niletrading.et", - generalManagerPhone: "+251 913 000 999", - website: null, - attributes: null, - companyProfiles: [ - { - id: "p8", - companyId: "c5", - type: "importer", - reference: "IMP-01456", - status: "pending", - businessLicense: null, - createdAt: iso("2025-03-14"), - updatedAt: iso("2025-03-14"), - }, - ], - createdAt: iso("2025-03-14"), - updatedAt: iso("2025-03-14"), - }, - { - id: "c6", - name: "Sheba Steel & Cement Importers", - type: "customer", - status: "active", - tin: "0067890123", - vatNumber: "VAT-400777", - fanNumber: "FAN-0512", - country: "Ethiopia", - address: "Lebu, Addis Ababa", - phone: "+251 914 567 890", - email: "procure@shebasteel.com", - contactPersonName: "Tigist Assefa", - contactPersonPhone: "+251 914 567 890", - generalManagerName: "Robel Negash", - generalManagerEmail: "robel@shebasteel.com", - generalManagerPhone: "+251 914 222 444", - website: "https://shebasteel.com", - attributes: null, - companyProfiles: [ - { - id: "p9", - companyId: "c6", - type: "importer", - reference: "IMP-01987", - status: "active", - businessLicense: "BL-IMP-9001", - createdAt: iso("2024-05-09"), - updatedAt: iso("2024-05-09"), - }, - ], - createdAt: iso("2024-05-09"), - updatedAt: iso("2025-04-02"), - }, - { - id: "c7", - name: "Awash Agro Export Union", - type: "customer", - status: "active", - tin: "0078901234", - vatNumber: "VAT-500321", - fanNumber: null, - country: "Ethiopia", - address: "Awash, Afar", - phone: "+251 915 678 901", - email: "export@awashagro.coop", - contactPersonName: "Lensa Diriba", - contactPersonPhone: "+251 915 678 901", - generalManagerName: "Gemechu Bayisa", - generalManagerEmail: "gemechu@awashagro.coop", - generalManagerPhone: "+251 915 333 555", - website: null, - attributes: null, - companyProfiles: [ - { - id: "p10", - companyId: "c7", - type: "exporter", - reference: "EXP-02233", - status: "active", - businessLicense: "BL-EXP-6650", - createdAt: iso("2024-08-19"), - updatedAt: iso("2024-08-19"), - }, - { - id: "p11", - companyId: "c7", - type: "importer", - reference: "IMP-02234", - status: "active", - businessLicense: "BL-IMP-6651", - createdAt: iso("2024-08-19"), - updatedAt: iso("2024-08-19"), - }, - ], - createdAt: iso("2024-08-19"), - updatedAt: iso("2025-05-10"), - }, - { - id: "c8", - name: "Blacklisted Holdings Trading", - type: "customer", - status: "blacklisted", - tin: "0089012345", - vatNumber: null, - fanNumber: null, - country: "Ethiopia", - address: "Unknown", - phone: "+251 916 789 012", - email: "contact@blacklistedholdings.com", - contactPersonName: "N/A", - contactPersonPhone: "+251 916 789 012", - generalManagerName: "N/A", - generalManagerEmail: null, - generalManagerPhone: null, - website: null, - attributes: null, - companyProfiles: [ - { - id: "p12", - companyId: "c8", - type: "importer", - reference: "IMP-02999", - status: "blacklisted", - businessLicense: null, - createdAt: iso("2023-12-01"), - updatedAt: iso("2024-02-15"), - }, - ], - createdAt: iso("2023-12-01"), - updatedAt: iso("2024-02-15"), - }, - { - id: "c9", - name: "Lalibela Coffee Exporters", - type: "customer", - status: "active", - tin: "0090123456", - vatNumber: "VAT-600145", - fanNumber: "FAN-0623", - country: "Ethiopia", - address: "Sidama, Hawassa", - phone: "+251 917 890 123", - email: "trade@lalibelacoffee.com", - contactPersonName: "Eyob Tariku", - contactPersonPhone: "+251 917 890 123", - generalManagerName: "Frehiwot Solomon", - generalManagerEmail: "frehiwot@lalibelacoffee.com", - generalManagerPhone: "+251 917 444 666", - website: "https://lalibelacoffee.com", - attributes: null, - companyProfiles: [ - { - id: "p13", - companyId: "c9", - type: "exporter", - reference: "EXP-03456", - status: "active", - businessLicense: "BL-EXP-8842", - createdAt: iso("2024-04-03"), - updatedAt: iso("2024-04-03"), - }, - ], - createdAt: iso("2024-04-03"), - updatedAt: iso("2025-03-22"), - }, - { - id: "c10", - name: "Unity Multimodal Forwarders", - type: "freight_forwarder", - status: "active", - tin: "0101234567", - vatNumber: "VAT-700998", - fanNumber: null, - country: "Ethiopia", - address: "Sululta, Oromia", - phone: "+251 918 901 234", - email: "ops@unitymultimodal.com", - contactPersonName: "Helen Tsegaye", - contactPersonPhone: "+251 918 901 234", - generalManagerName: "Nahom Berhanu", - generalManagerEmail: "nahom@unitymultimodal.com", - generalManagerPhone: "+251 918 555 777", - website: null, - attributes: null, - companyProfiles: [ - { - id: "p14", - companyId: "c10", - type: "freight_forwarder", - reference: "FFW-04088", - status: "active", - businessLicense: "BL-FFW-5521", - createdAt: iso("2024-09-28"), - updatedAt: iso("2024-09-28"), - }, - { - id: "p15", - companyId: "c10", - type: "transporter", - reference: "TRP-04089", - status: "active", - businessLicense: "BL-TRP-5522", - createdAt: iso("2024-09-28"), - updatedAt: iso("2024-09-28"), - }, - { - id: "p16", - companyId: "c10", - type: "exporter", - reference: "EXP-04090", - status: "pending", - businessLicense: null, - createdAt: iso("2025-01-15"), - updatedAt: iso("2025-01-15"), - }, - ], - createdAt: iso("2024-09-28"), - updatedAt: iso("2025-01-15"), - }, -]; - -/** Bookings keyed by companyId. */ -const BOOKINGS_BY_COMPANY: Record = { - c1: [ - { - id: "b1", - reference: "BK-2025-0481", - status: "PAID", - tradeDirection: "IMPORT", - freightType: "CONTAINER", - originLabel: "Djibouti Port", - destinationLabel: "Mojo Dry Port", - totalAmount: 482000, - currency: "ETB", - scheduledDate: iso("2025-05-20"), - createdAt: iso("2025-05-02"), - }, - { - id: "b2", - reference: "BK-2025-0512", - status: "IN_TRANSIT", - tradeDirection: "IMPORT", - freightType: "CONTAINER", - originLabel: "Djibouti Port", - destinationLabel: "Indode Terminal", - totalAmount: 356500, - currency: "ETB", - scheduledDate: iso("2025-06-01"), - createdAt: iso("2025-05-18"), - }, - { - id: "b3", - reference: "BK-2025-0298", - status: "COMPLETED", - tradeDirection: "EXPORT", - freightType: "BULK", - originLabel: "Mojo Dry Port", - destinationLabel: "Djibouti Port", - totalAmount: 198000, - currency: "ETB", - scheduledDate: iso("2025-03-30"), - createdAt: iso("2025-03-12"), - }, - ], - c2: [ - { - id: "b4", - reference: "BK-2025-0633", - status: "PENDING_APPROVAL", - tradeDirection: "IMPORT", - freightType: "CONTAINER", - originLabel: "Djibouti Port", - destinationLabel: "Mojo Dry Port", - totalAmount: 720000, - currency: "ETB", - scheduledDate: null, - createdAt: iso("2025-06-10"), - }, - { - id: "b5", - reference: "BK-2025-0588", - status: "PAID", - tradeDirection: "EXPORT", - freightType: "CONTAINER", - originLabel: "Indode Terminal", - destinationLabel: "Djibouti Port", - totalAmount: 540000, - currency: "ETB", - scheduledDate: iso("2025-06-15"), - createdAt: iso("2025-05-29"), - }, - ], - c6: [ - { - id: "b6", - reference: "BK-2025-0701", - status: "SUBMITTED", - tradeDirection: "IMPORT", - freightType: "BULK", - originLabel: "Djibouti Port", - destinationLabel: "Mojo Dry Port", - totalAmount: 1250000, - currency: "ETB", - scheduledDate: null, - createdAt: iso("2025-06-18"), - }, - ], - c7: [ - { - id: "b7", - reference: "BK-2025-0344", - status: "COMPLETED", - tradeDirection: "EXPORT", - freightType: "BULK", - originLabel: "Mojo Dry Port", - destinationLabel: "Djibouti Port", - totalAmount: 2100, - currency: "USD", - scheduledDate: iso("2025-04-05"), - createdAt: iso("2025-03-20"), - }, - { - id: "b8", - reference: "BK-2025-0410", - status: "CANCELLED", - tradeDirection: "EXPORT", - freightType: "CONTAINER", - originLabel: "Indode Terminal", - destinationLabel: "Djibouti Port", - totalAmount: 3400, - currency: "USD", - scheduledDate: null, - createdAt: iso("2025-04-22"), - }, - ], -}; - -/** Documents keyed by companyId. */ -const DOCUMENTS_BY_COMPANY: Record = { - c1: [ - { - id: "d1", - name: "Business License 2025.pdf", - code: "business_license", - mimeType: "application/pdf", - size: 482_113, - uploadedAt: iso("2025-01-12"), - url: "#", - }, - { - id: "d2", - name: "VAT Registration.pdf", - code: "vat_certificate", - mimeType: "application/pdf", - size: 211_544, - uploadedAt: iso("2024-02-11"), - url: "#", - }, - { - id: "d3", - name: "Trade Agreement - Signed.pdf", - code: "contract", - mimeType: "application/pdf", - size: 1_204_882, - uploadedAt: iso("2025-05-03"), - url: "#", - }, - ], - c2: [ - { - id: "d4", - name: "Forwarding License.pdf", - code: "business_license", - mimeType: "application/pdf", - size: 365_002, - uploadedAt: iso("2023-11-06"), - url: "#", - }, - { - id: "d5", - name: "Company Profile.docx", - code: "company_profile", - mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - size: 92_440, - uploadedAt: iso("2024-07-02"), - url: "#", - }, - ], - c6: [ - { - id: "d6", - name: "Import License.pdf", - code: "business_license", - mimeType: "application/pdf", - size: 410_223, - uploadedAt: iso("2024-05-10"), - url: "#", - }, - ], - c7: [ - { - id: "d7", - name: "Export Permit.pdf", - code: "export_permit", - mimeType: "application/pdf", - size: 298_770, - uploadedAt: iso("2024-08-20"), - url: "#", - }, - { - id: "d8", - name: "Quality Certificate.jpg", - code: "quality_certificate", - mimeType: "image/jpeg", - size: 1_882_001, - uploadedAt: iso("2025-03-21"), - url: "#", - }, - ], -}; - -/** Payments keyed by companyId. */ -const PAYMENTS_BY_COMPANY: Record = { - c1: [ - { - id: "pay1", - reference: "PMT-77120", - bookingReference: "BK-2025-0481", - amount: 482000, - currency: "ETB", - method: "telebirr", - status: "success", - paidAt: iso("2025-05-04"), - createdAt: iso("2025-05-03"), - }, - { - id: "pay2", - reference: "PMT-77450", - bookingReference: "BK-2025-0512", - amount: 356500, - currency: "ETB", - method: "cbe-birr", - status: "processing", - paidAt: null, - createdAt: iso("2025-05-19"), - }, - { - id: "pay3", - reference: "PMT-71002", - bookingReference: "BK-2025-0298", - amount: 198000, - currency: "ETB", - method: "telebirr", - status: "success", - paidAt: iso("2025-03-14"), - createdAt: iso("2025-03-13"), - }, - ], - c2: [ - { - id: "pay4", - reference: "PMT-78900", - bookingReference: "BK-2025-0588", - amount: 540000, - currency: "ETB", - method: "cbe-birr", - status: "success", - paidAt: iso("2025-05-30"), - createdAt: iso("2025-05-29"), - }, - { - id: "pay5", - reference: "PMT-79120", - bookingReference: "BK-2025-0633", - amount: 720000, - currency: "ETB", - method: "card", - status: "action-required", - paidAt: null, - createdAt: iso("2025-06-10"), - }, - ], - c7: [ - { - id: "pay6", - reference: "PMT-70044", - bookingReference: "BK-2025-0344", - amount: 2100, - currency: "USD", - method: "card", - status: "success", - paidAt: iso("2025-03-22"), - createdAt: iso("2025-03-21"), - }, - { - id: "pay7", - reference: "PMT-70410", - bookingReference: "BK-2025-0410", - amount: 3400, - currency: "USD", - method: "card", - status: "refunded", - paidAt: iso("2025-04-23"), - createdAt: iso("2025-04-22"), - }, - ], -}; - -export function getCompanyById(id: string): Company | undefined { - return MOCK_COMPANIES.find((c) => c.id === id); -} - -export function getBookingsFor(companyId: string): CustomerBooking[] { - return BOOKINGS_BY_COMPANY[companyId] ?? []; -} - -export function getDocumentsFor(companyId: string): CustomerDocument[] { - return DOCUMENTS_BY_COMPANY[companyId] ?? []; -} - -export function getPaymentsFor(companyId: string): CustomerPayment[] { - return PAYMENTS_BY_COMPANY[companyId] ?? []; -} diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index f5239110d..86d14ef7d 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -1,13 +1,17 @@ import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; -import { endpoint } from "@/utils/endpoint"; +import type { FleetResourceSlug } from "@/pages/fleet/config/resources"; +import type { BookingDetail } from "@/types/booking"; import type { - CreateFileUploadFieldDto, - CreateFileUploadSettingDto, - FileUploadField, - FileUploadSetting, - UpdateFileUploadFieldDto, - UpdateFileUploadSettingDto, -} from "@/types/fileUploadSettings"; + Company, + CompanyListFilter, + CompanyProfile, + CompanyStats, + CustomerBooking, + CustomerDocument, + CustomerPayment, + PaginatedCompanies, + ProfileStatus, +} from "@/types/customer"; import { CreateDropdownOptionDto, CreateDropdownSettingDto, @@ -16,72 +20,20 @@ import { UpdateDropdownOptionDto, UpdateDropdownSettingDto, } from "@/types/dropdownSettings"; +import type { + CreateFileUploadFieldDto, + CreateFileUploadSettingDto, + FileUploadField, + FileUploadSetting, + UpdateFileUploadFieldDto, + UpdateFileUploadSettingDto, +} from "@/types/fileUploadSettings"; +import type { IOverviewDashboard, OverviewRange } from "@/types/overview"; import { RuleEngineListResult, RuleEngineRecord, RuleEngineResourceSlug, } from "@/types/rule-engine"; -import { fileUploadSettingsService } from "./fileUploadSettings.service"; -import { dropdownSettingsService } from "./dropdownSettings.service"; -import { - ruleEngineService, - RuleEngineListParams, -} from "./ruleEngine/ruleEngine.service"; -import { - bookingsService, - BookingListFilter, - type ApproveStepPayload, - type PaginatedBookings, - type RejectStepPayload, -} from "./bookings.service"; -import type { BookingDetail } from "@/types/booking"; -import type { IOverviewDashboard, OverviewRange } from "@/types/overview"; -import { overviewService } from "./overview.service"; -import { - cargoService, - type Cargo, - type DeliverCargoPayload, -} from "./cargoService"; -import { containerService, type Container } from "./containerService"; -import { containerTypesService } from "./container-types.service"; -import { - wagonService, - type Wagon, - type WagonListFilters, -} from "./wagon.service"; -import { wagonTypesService, type WagonType } from "./wagon-types.service"; -import { trainService, type Train } from "./trains.service"; -import { - locomotivesService, - type Locomotive, - type SaveLocomotivePayload, -} from "./locomotives.service"; -import { cargoTypesService } from "./cargo-types.service"; -import { - fleetService, - type FleetListFilters, - type FleetRecord, -} from "./fleet/fleet.service"; -import type { FleetResourceSlug } from "@/pages/fleet/config/resources"; -import { - paymentsService, - type PaginatedPayments, - type PaymentListFilter, - type PaymentSummary, -} from "./payments.service"; -import { - signaturesService, - type SavedSignature, - type SaveSignaturePayload, -} from "./signatures.service"; -import { warehouseService } from "./warehouse.service"; -import { trainSchedulingService } from "./trainScheduling.service"; -import { - routesService, - type RouteRecord, - type SaveRoutePayload, - type YardRef, -} from "./routes.service"; import type { AssignBookingsPayload, BatchBoardSchedule, @@ -157,6 +109,66 @@ import type { WarehouseYard, WarehouseZone, } from "@/types/warehouse"; +import { endpoint } from "@/utils/endpoint"; +import { + BookingListFilter, + bookingsService, + type ApproveStepPayload, + type PaginatedBookings, + type RejectStepPayload, +} from "./bookings.service"; +import { cargoTypesService } from "./cargo-types.service"; +import { + cargoService, + type Cargo, + type DeliverCargoPayload, +} from "./cargoService"; +import { containerTypesService } from "./container-types.service"; +import { containerService, type Container } from "./containerService"; +import { customersService } from "./customers.service"; +import { dropdownSettingsService } from "./dropdownSettings.service"; +import { fileUploadSettingsService } from "./fileUploadSettings.service"; +import { + fleetService, + type FleetListFilters, + type FleetRecord, +} from "./fleet/fleet.service"; +import { + locomotivesService, + type Locomotive, + type SaveLocomotivePayload, +} from "./locomotives.service"; +import { overviewService } from "./overview.service"; +import { + paymentsService, + type PaginatedPayments, + type PaymentListFilter, + type PaymentSummary, +} from "./payments.service"; +import { + routesService, + type RouteRecord, + type SaveRoutePayload, + type YardRef, +} from "./routes.service"; +import { + RuleEngineListParams, + ruleEngineService, +} from "./ruleEngine/ruleEngine.service"; +import { + signaturesService, + type SavedSignature, + type SaveSignaturePayload, +} from "./signatures.service"; +import { trainService, type Train } from "./trains.service"; +import { trainSchedulingService } from "./trainScheduling.service"; +import { wagonTypesService, type WagonType } from "./wagon-types.service"; +import { + wagonService, + type Wagon, + type WagonListFilters, +} from "./wagon.service"; +import { warehouseService } from "./warehouse.service"; /** Query keys for inventory-lifecycle mutations that ripple across views. */ const INVENTORY_INVALIDATIONS: ReadonlyArray = [ @@ -1879,6 +1891,64 @@ export const api = { ), }, + customers: { + stats: endpoint, CompanyStats>( + "customers", + "stats", + () => customersService.stats(), + () => QUERY_KEYS.CUSTOMERS.stats, + ), + + list: endpoint<{ filter: CompanyListFilter }, PaginatedCompanies>( + "customers", + "list", + ({ filter }) => customersService.list(filter), + ({ filter }) => QUERY_KEYS.CUSTOMERS.list(filter), + ), + + getById: endpoint<{ id: string }, Company | undefined>( + "customers", + "getById", + ({ id }) => customersService.getById(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.byId(id), + ), + + bookings: endpoint<{ id: string }, CustomerBooking[]>( + "customers", + "bookings", + ({ id }) => customersService.bookingsFor(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.bookings(id), + ), + + documents: endpoint<{ id: string }, CustomerDocument[]>( + "customers", + "documents", + ({ id }) => customersService.documentsFor(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.documents(id), + ), + + payments: endpoint<{ id: string }, CustomerPayment[]>( + "customers", + "payments", + ({ id }) => customersService.paymentsFor(id), + ({ id }) => QUERY_KEYS.CUSTOMERS.payments(id), + ), + + setProfileStatus: endpoint< + { profileId: string; status: ProfileStatus }, + CompanyProfile + >( + "customers", + "setProfileStatus", + ({ profileId, status }) => customersService.setProfileStatus(profileId, status), + undefined, + (_input, data) => [ + QUERY_KEYS.CUSTOMERS.byId(data.companyId), + QUERY_KEYS.CUSTOMERS.ROOT, + ], + ), + }, + overview: { get: endpoint<{ range?: OverviewRange }, IOverviewDashboard>( "overview", @@ -1886,4 +1956,4 @@ export const api = { ({ range }) => overviewService.getDashboard(range), ), }, -}; +}; \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/services/customers.service.ts b/apps/edr-freight-web/backoffice/src/services/customers.service.ts index cc9f47f0b..d375ad7a7 100644 --- a/apps/edr-freight-web/backoffice/src/services/customers.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/customers.service.ts @@ -1,72 +1,90 @@ -/** - * Customers service. - * - * Currently backed by in-memory mock fixtures (`customers.mock.ts`); the public - * surface mirrors the other services (e.g. `bookings.service.ts`) — async - * methods returning `{ items, total }` / detail objects — so it can be pointed - * at the live `/companies` API later without touching the hooks or pages. - */ -import { - getBookingsFor, - getCompanyById, - getDocumentsFor, - getPaymentsFor, - MOCK_COMPANIES, -} from "@/pages/customers/customers.mock"; +import { api as apiClient } from "@/auth/http"; +import { URL_CONSTANTS } from "@/constants/URLS"; import type { Company, CompanyListFilter, + CompanyProfile, + CompanyStats, CustomerBooking, CustomerDocument, CustomerPayment, PaginatedCompanies, + ProfileStatus, } from "@/types/customer"; -/** Simulate network latency so loading states are visible during UI work. */ -const delay = (value: T, ms = 350): Promise => - new Promise((resolve) => setTimeout(() => resolve(value), ms)); - -function matchesSearch(company: Company, search: string): boolean { - const q = search.trim().toLowerCase(); - if (!q) return true; - return ( - company.name.toLowerCase().includes(q) || - company.tin.toLowerCase().includes(q) || - company.email?.toLowerCase().includes(q) === true || - company.companyProfiles.some((p) => p.reference.toLowerCase().includes(q)) +const cleanParams = (params: object) => + Object.fromEntries( + Object.entries(params).filter( + ([, value]) => value !== undefined && value !== "" && value !== null, + ), ); + +/** Lift attributes JSONB into the flat contact/manager fields the UI reads. */ +function mapCompany(dto: Record): Company { + const attrs = (dto.attributes as Record | null) ?? {}; + return { + ...(dto as unknown as Company), + contactPersonName: (attrs.contactPersonName as string | null) ?? null, + contactPersonPhone: (attrs.contactPersonPhone as string | null) ?? null, + generalManagerName: (attrs.generalManagerName as string | null) ?? null, + generalManagerEmail: (attrs.generalManagerEmail as string | null) ?? null, + generalManagerPhone: (attrs.generalManagerPhone as string | null) ?? null, + }; } export const customersService = { + stats(): Promise { + return apiClient + .get(URL_CONSTANTS.COMPANIES.STATS) + .then((r) => r.data); + }, + list(filter: CompanyListFilter): Promise { - const { page, pageSize, search = "", type, status } = filter; - - const filtered = MOCK_COMPANIES.filter( - (c) => - matchesSearch(c, search) && - (!type || c.type === type) && - (!status || c.status === status), - ); - - const start = (page - 1) * pageSize; - const items = filtered.slice(start, start + pageSize); - - return delay({ items, total: filtered.length }); + return apiClient + .get<{ items: Record[]; total: number }>( + URL_CONSTANTS.COMPANIES.BASE, + { params: cleanParams(filter) }, + ) + .then((r) => ({ + items: r.data.items.map(mapCompany), + total: r.data.total, + })); }, getById(id: string): Promise { - return delay(getCompanyById(id)); + return apiClient + .get>(URL_CONSTANTS.COMPANIES.BY_ID(id)) + .then((r) => mapCompany(r.data)); }, bookingsFor(companyId: string): Promise { - return delay(getBookingsFor(companyId)); + return apiClient + .get( + URL_CONSTANTS.COMPANIES.BOOKINGS_CUSTOMER_VIEW(companyId), + ) + .then((r) => r.data); }, documentsFor(companyId: string): Promise { - return delay(getDocumentsFor(companyId)); + return apiClient + .get(URL_CONSTANTS.COMPANIES.DOCUMENTS(companyId)) + .then((r) => r.data); }, paymentsFor(companyId: string): Promise { - return delay(getPaymentsFor(companyId)); + return apiClient + .get( + URL_CONSTANTS.COMPANIES.PAYMENTS_CUSTOMER_VIEW(companyId), + ) + .then((r) => r.data); + }, + + setProfileStatus(profileId: string, status: ProfileStatus): Promise { + return apiClient + .patch( + URL_CONSTANTS.COMPANIES.PROFILE_STATUS(profileId), + { status }, + ) + .then((r) => r.data); }, }; diff --git a/apps/edr-freight-web/backoffice/src/types/customer.ts b/apps/edr-freight-web/backoffice/src/types/customer.ts index 49b2dc850..0cdccc10b 100644 --- a/apps/edr-freight-web/backoffice/src/types/customer.ts +++ b/apps/edr-freight-web/backoffice/src/types/customer.ts @@ -82,6 +82,15 @@ export interface PaginatedCompanies { total: number; } +/** KPI counts returned by GET /companies/stats. */ +export interface CompanyStats { + total: number; + active: number; + pending: number; + suspended: number; + blacklisted: number; +} + /* ------------------------------------------------------------------ * * Related data shown on the customer detail page (mocked for now). * * ------------------------------------------------------------------ */