mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
loading={isPending}
|
||||
onClick={() => act("active")}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="red"
|
||||
radius="md"
|
||||
loading={isPending}
|
||||
onClick={() => act("blacklisted")}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "active") {
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
radius="md"
|
||||
loading={isPending}
|
||||
onClick={() => act("suspended")}
|
||||
>
|
||||
Suspend
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "suspended") {
|
||||
return (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
loading={isPending}
|
||||
onClick={() => act("active")}
|
||||
>
|
||||
Reactivate
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="red"
|
||||
radius="md"
|
||||
loading={isPending}
|
||||
onClick={() => act("blacklisted")}
|
||||
>
|
||||
Blacklist
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "blacklisted") {
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="gray"
|
||||
radius="md"
|
||||
loading={isPending}
|
||||
onClick={() => act("pending")}
|
||||
>
|
||||
Reinstate
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
CompanyStatusBadge,
|
||||
CompanyTypeBadge,
|
||||
PaymentStatusBadge,
|
||||
ProfileApprovalActions,
|
||||
ProfileChips,
|
||||
ProfileStatusBadge,
|
||||
ProfileTypeBadge,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
@@ -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 }) => (
|
||||
<Text size="sm" c="dimmed">
|
||||
{formatDate(row.original.createdAt)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
meta: { headerClassName: "text-right", cellClassName: "text-right" },
|
||||
cell: ({ row }) => (
|
||||
<ProfileApprovalActions
|
||||
profileId={row.original.id}
|
||||
status={row.original.status}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
@@ -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() {
|
||||
<ProfileChips profiles={company.companyProfiles} />
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }} w="100%">
|
||||
<Box miw={720}>
|
||||
<Box miw={860}>
|
||||
<DataTable
|
||||
columns={profileColumns}
|
||||
data={company.companyProfiles}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
@@ -30,8 +31,7 @@ import {
|
||||
formatDate,
|
||||
} from "@/components/customers";
|
||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||
import { useCustomerList } from "@/hooks/customers/useCustomers";
|
||||
import { MOCK_COMPANIES } from "@/pages/customers/customers.mock";
|
||||
import { api } from "@/services/api";
|
||||
import type { Company } from "@/types/customer";
|
||||
import {
|
||||
DataTable,
|
||||
@@ -40,14 +40,6 @@ import {
|
||||
type ColumnDef,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
/** KPI counts are derived from the full fixture set (mock-only). */
|
||||
const KPIS = {
|
||||
total: MOCK_COMPANIES.length,
|
||||
active: MOCK_COMPANIES.filter((c) => 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() {
|
||||
|
||||
<KpiStrip
|
||||
items={[
|
||||
{ label: "Companies", value: KPIS.total, icon: Users, color: "edr-green" },
|
||||
{ label: "Active", value: KPIS.active, icon: CheckCircle2, color: "edr-green" },
|
||||
{ label: "Pending", value: KPIS.pending, icon: Clock, color: "yellow" },
|
||||
{ label: "Companies", value: stats?.total ?? "—", icon: Users, color: "edr-green" },
|
||||
{ label: "Active", value: stats?.active ?? "—", icon: CheckCircle2, color: "edr-green" },
|
||||
{ label: "Pending", value: stats?.pending ?? "—", icon: Clock, color: "yellow" },
|
||||
{
|
||||
label: "Blacklisted",
|
||||
value: KPIS.blacklisted,
|
||||
value: stats?.blacklisted ?? "—",
|
||||
icon: ShieldOff,
|
||||
color: "red",
|
||||
},
|
||||
|
||||
@@ -1,722 +0,0 @@
|
||||
/**
|
||||
* Mock fixtures for the Customer Management dashboard.
|
||||
*
|
||||
* Shapes match the backend `Company` / `CompanyProfile` entities and the
|
||||
* lightweight related-data types in `@/types/customer`. Swap the service layer
|
||||
* to live endpoints later — these fixtures (and the helpers below) are the only
|
||||
* thing that has to change.
|
||||
*/
|
||||
import type {
|
||||
Company,
|
||||
CustomerBooking,
|
||||
CustomerDocument,
|
||||
CustomerPayment,
|
||||
} from "@/types/customer";
|
||||
|
||||
const iso = (date: string) => 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<string, CustomerBooking[]> = {
|
||||
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<string, CustomerDocument[]> = {
|
||||
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<string, CustomerPayment[]> = {
|
||||
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] ?? [];
|
||||
}
|
||||
@@ -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<readonly unknown[]> = [
|
||||
@@ -1879,6 +1891,64 @@ export const api = {
|
||||
),
|
||||
},
|
||||
|
||||
customers: {
|
||||
stats: endpoint<Record<string, never>, 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),
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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 = <T>(value: T, ms = 350): Promise<T> =>
|
||||
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<string, unknown>): Company {
|
||||
const attrs = (dto.attributes as Record<string, unknown> | 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<CompanyStats> {
|
||||
return apiClient
|
||||
.get<CompanyStats>(URL_CONSTANTS.COMPANIES.STATS)
|
||||
.then((r) => r.data);
|
||||
},
|
||||
|
||||
list(filter: CompanyListFilter): Promise<PaginatedCompanies> {
|
||||
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<string, unknown>[]; 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<Company | undefined> {
|
||||
return delay(getCompanyById(id));
|
||||
return apiClient
|
||||
.get<Record<string, unknown>>(URL_CONSTANTS.COMPANIES.BY_ID(id))
|
||||
.then((r) => mapCompany(r.data));
|
||||
},
|
||||
|
||||
bookingsFor(companyId: string): Promise<CustomerBooking[]> {
|
||||
return delay(getBookingsFor(companyId));
|
||||
return apiClient
|
||||
.get<CustomerBooking[]>(
|
||||
URL_CONSTANTS.COMPANIES.BOOKINGS_CUSTOMER_VIEW(companyId),
|
||||
)
|
||||
.then((r) => r.data);
|
||||
},
|
||||
|
||||
documentsFor(companyId: string): Promise<CustomerDocument[]> {
|
||||
return delay(getDocumentsFor(companyId));
|
||||
return apiClient
|
||||
.get<CustomerDocument[]>(URL_CONSTANTS.COMPANIES.DOCUMENTS(companyId))
|
||||
.then((r) => r.data);
|
||||
},
|
||||
|
||||
paymentsFor(companyId: string): Promise<CustomerPayment[]> {
|
||||
return delay(getPaymentsFor(companyId));
|
||||
return apiClient
|
||||
.get<CustomerPayment[]>(
|
||||
URL_CONSTANTS.COMPANIES.PAYMENTS_CUSTOMER_VIEW(companyId),
|
||||
)
|
||||
.then((r) => r.data);
|
||||
},
|
||||
|
||||
setProfileStatus(profileId: string, status: ProfileStatus): Promise<CompanyProfile> {
|
||||
return apiClient
|
||||
.patch<CompanyProfile>(
|
||||
URL_CONSTANTS.COMPANIES.PROFILE_STATUS(profileId),
|
||||
{ status },
|
||||
)
|
||||
.then((r) => r.data);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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). *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
Reference in New Issue
Block a user