From f90e5e0e1cbcdb9e4ca08908deceb3a5eaed74bf Mon Sep 17 00:00:00 2001 From: hagiye Date: Tue, 26 May 2026 01:06:06 +0300 Subject: [PATCH] company detail api and frontend --- .../modules/customers/customers.controller.ts | 50 +- .../modules/customers/customers.repository.ts | 111 ++- .../modules/customers/customers.service.ts | 83 +- .../customers/dto/create-customer.dto.ts | 117 ++- .../customers/dto/response-customer.dto.ts | 60 ++ .../customers/dto/update-customer.dto.ts | 12 +- .../customers/entities/customer.entity.ts | 116 ++- .../customers2/customers.controller.ts | 57 ++ .../modules/customers2/customers.module.ts | 15 + .../customers2/customers.repository.ts | 21 + .../modules/customers2/customers.service.ts | 61 ++ .../customers2/dto/create-customer.dto.ts | 73 ++ .../customers2/dto/update-customer.dto.ts | 5 + .../customers2/entities/customer.entity.ts | 54 ++ .../portal/src/constants/FILE_SETTINGS.ts | 3 +- .../pages/customers/NewCustomerPage copy.tsx | 223 +++++ .../src/pages/customers/NewCustomerPage.tsx | 869 +++++++++++------- .../pages/customers2/CustomerDetailPage.tsx | 273 ++++++ .../src/pages/customers2/CustomersPage.tsx | 386 ++++++++ .../pages/customers2/DeleteCustomerDialog.tsx | 72 ++ .../src/pages/customers2/NewCustomerPage.tsx | 384 ++++++++ .../src/pages/customers2/customers.mock.ts | 110 +++ 22 files changed, 2740 insertions(+), 415 deletions(-) create mode 100644 apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts create mode 100644 apps/edr-freight-api/src/modules/customers2/customers.controller.ts create mode 100644 apps/edr-freight-api/src/modules/customers2/customers.module.ts create mode 100644 apps/edr-freight-api/src/modules/customers2/customers.repository.ts create mode 100644 apps/edr-freight-api/src/modules/customers2/customers.service.ts create mode 100644 apps/edr-freight-api/src/modules/customers2/dto/create-customer.dto.ts create mode 100644 apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts create mode 100644 apps/edr-freight-api/src/modules/customers2/entities/customer.entity.ts create mode 100644 apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage copy.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/customers2/CustomerDetailPage.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/customers2/CustomersPage.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/customers2/DeleteCustomerDialog.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/customers2/NewCustomerPage.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/customers2/customers.mock.ts diff --git a/apps/edr-freight-api/src/modules/customers/customers.controller.ts b/apps/edr-freight-api/src/modules/customers/customers.controller.ts index 85b470453..49b9d9b0b 100644 --- a/apps/edr-freight-api/src/modules/customers/customers.controller.ts +++ b/apps/edr-freight-api/src/modules/customers/customers.controller.ts @@ -1,5 +1,6 @@ +// src/modules/customers/customers.controller.ts + import { - Body, Controller, Delete, Get, @@ -9,33 +10,54 @@ import { ParseUUIDPipe, Patch, Post, + Body, + Query, } from "@nestjs/common"; -import { ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { ApiOperation } from "@nestjs/swagger"; import { CustomersService } from "./customers.service"; import { CreateCustomerDto } from "./dto/create-customer.dto"; import { UpdateCustomerDto } from "./dto/update-customer.dto"; +import { Customer } from "./entities/customer.entity"; -@ApiTags("customers") @Controller("customers") export class CustomersController { constructor(private readonly customersService: CustomersService) {} @Post() - @ApiOperation({ summary: "Create a new customer" }) - create(@Body() dto: CreateCustomerDto) { - return this.customersService.create(dto); + create(@Body() createCustomerDto: CreateCustomerDto): Promise { + return this.customersService.create(createCustomerDto); } @Get() - @ApiOperation({ summary: "List all customers" }) - findAll() { + findAll(): Promise { return this.customersService.findAll(); } + @Get("stats") + @ApiOperation({ summary: "Get customer statistics" }) + getStats(): Promise<{ total: number; withVatNumber: number }> { + return this.customersService.getStats(); + } + + @Get("search") + searchByName(@Query("name") name: string): Promise { + return this.customersService.searchByName(name); + } + + @Get("email/:email") + findByEmail(@Param("email") email: string): Promise { + return this.customersService.findByEmail(email); + } + + @Get("vat/:vatNumber") + findByVatNumber(@Param("vatNumber") vatNumber: string): Promise { + return this.customersService.findByVatNumber(vatNumber); + } + @Get(":id") - @ApiOperation({ summary: "Get a customer by ID" }) - findOne(@Param("id", ParseUUIDPipe) id: string) { + findById(@Param("id", ParseUUIDPipe) id: string): Promise { return this.customersService.findById(id); } @@ -44,14 +66,14 @@ export class CustomersController { update( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateCustomerDto, - ) { + ): Promise { return this.customersService.update(id, dto); } @Delete(":id") @ApiOperation({ summary: "Soft-delete a customer" }) @HttpCode(HttpStatus.NO_CONTENT) - remove(@Param("id", ParseUUIDPipe) id: string) { - return this.customersService.remove(id); + remove(@Param("id", ParseUUIDPipe) id: string): Promise { + return this.customersService.delete(id); } -} +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/customers/customers.repository.ts b/apps/edr-freight-api/src/modules/customers/customers.repository.ts index c6cb72fcf..83caf61a4 100644 --- a/apps/edr-freight-api/src/modules/customers/customers.repository.ts +++ b/apps/edr-freight-api/src/modules/customers/customers.repository.ts @@ -1,21 +1,110 @@ -import { BaseRepository } from "@edr/api-common"; +// import { BaseRepository } from "@edr/api-common"; +// import { EntityRepository } from "typeorm"; + +// src/modules/customers/customers.repository.ts import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; - +import { Repository, FindManyOptions, FindOptionsWhere } from "typeorm"; import { Customer } from "./entities/customer.entity"; +import { CreateCustomerDto } from "./dto/create-customer.dto"; +// import { UpdateCustomerDto } from "./dto/update-customer.dto"; @Injectable() -export class CustomersRepository extends BaseRepository { +export class CustomersRepository { constructor( @InjectRepository(Customer) - repository: Repository, - ) { - super(repository); + private readonly repository: Repository, + ) {} + + async create(dto: CreateCustomerDto): Promise { + const customer = this.repository.create(dto); + return await this.repository.save(customer); } - /** Find a customer by their unique email. */ - findByEmail(email: string): Promise { - return this.repository.findOne({ where: { email } }); + async findAll(options?: FindManyOptions): Promise { + return await this.repository.find(options); } -} + + async findById(id: string): Promise { + return await this.repository.findOne({ where: { id } as FindOptionsWhere }); + } + + async findByEmail(email: string): Promise { + return await this.repository.findOne({ where: { email } as FindOptionsWhere }); + } + + async findByVatNumber(vatNumber: string): Promise { + return await this.repository.findOne({ where: { vatNumber } as FindOptionsWhere }); + } + + async findByName(name: string): Promise { + return await this.repository + .createQueryBuilder("customer") + .where("customer.name ILIKE :name", { name: `%${name}%` }) + .getMany(); + } + + async findOneByEmailOrVat(email?: string, vatNumber?: string): Promise { + if (!email && !vatNumber) return null; + + const queryBuilder = this.repository.createQueryBuilder('customer'); + + if (email && vatNumber) { + queryBuilder.where('customer.email = :email', { email }) + .orWhere('customer.vatNumber = :vatNumber', { vatNumber }); + } else if (email) { + queryBuilder.where('customer.email = :email', { email }); + } else if (vatNumber) { + queryBuilder.where('customer.vatNumber = :vatNumber', { vatNumber }); + } + + return await queryBuilder.getOne(); + } + + async update(id: string, updates: Partial): Promise { + await this.repository.update(id, updates); + return this.findById(id); + } + + async delete(id: string): Promise { + const result = await this.repository.delete(id); + return (result.affected ?? 0) > 0; + } + + async count(where?: any): Promise { + if (where?.createdAt) { + const result = await this.repository + .createQueryBuilder('customer') + .where('customer.createdAt >= :date', { date: where.createdAt }) + .getCount(); + return result; + } + return await this.repository.count(); + } + + async existsByUniqueFields(email: string, vatNumber?: string): Promise { + const queryBuilder = this.repository.createQueryBuilder('customer') + .where('customer.email = :email', { email }); + + if (vatNumber) { + queryBuilder.orWhere('customer.vatNumber = :vatNumber', { vatNumber }); + } + + const count = await queryBuilder.getCount(); + return count > 0; + } + + async countWithVatNumber(): Promise { + const count = await this.repository + .createQueryBuilder('customer') + .where('customer.vatNumber IS NOT NULL') + .andWhere("customer.vatNumber != ''") + .getCount(); + + return count; + } + + getRepository(): Repository { + return this.repository; + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/customers/customers.service.ts b/apps/edr-freight-api/src/modules/customers/customers.service.ts index 6394e1ad9..a55343e23 100644 --- a/apps/edr-freight-api/src/modules/customers/customers.service.ts +++ b/apps/edr-freight-api/src/modules/customers/customers.service.ts @@ -1,7 +1,8 @@ import { - ConflictException, Injectable, NotFoundException, + ConflictException, + BadRequestException, } from "@nestjs/common"; import { CustomersRepository } from "./customers.repository"; @@ -13,34 +14,87 @@ import { Customer } from "./entities/customer.entity"; export class CustomersService { constructor(private readonly customersRepository: CustomersRepository) {} + /** Create a new customer */ async create(dto: CreateCustomerDto): Promise { - const existing = await this.customersRepository.findByEmail(dto.email); - if (existing) { + const exists = await this.customersRepository.existsByUniqueFields( + dto.email, + dto.vatNumber, + ); + + if (exists) { throw new ConflictException( - `Customer with email "${dto.email}" already exists`, + "Customer with same email or VAT number already exists", ); } + + if (dto.vatNumber && dto.vatNumber.length !== 10) { + throw new BadRequestException("VAT number must be exactly 10 digits"); + } + return this.customersRepository.create(dto); } + /** Get all customers */ findAll(): Promise { - return this.customersRepository.findAll({ order: { name: "ASC" } }); + return this.customersRepository.findAll({ + order: { companyName: "ASC" }, + }); } + /** Get customer by ID */ async findById(id: string): Promise { const customer = await this.customersRepository.findById(id); + if (!customer) { - throw new NotFoundException(`Customer ${id} not found`); + throw new NotFoundException(`Customer with ID ${id} not found`); } + return customer; } + /** Get customer by email */ + async findByEmail(email: string): Promise { + const customer = await this.customersRepository.findByEmail(email); + + if (!customer) { + throw new NotFoundException(`Customer with email ${email} not found`); + } + + return customer; + } + + /** Get customer by VAT number */ + async findByVatNumber(vatNumber: string): Promise { + const customer = await this.customersRepository.findByVatNumber(vatNumber); + + if (!customer) { + throw new NotFoundException( + `Customer with VAT number ${vatNumber} not found`, + ); + } + + return customer; + } + + /** Search customers by name */ + searchByName(name: string): Promise { + return this.customersRepository.findByName(name); + } + + /** Update customer */ async update(id: string, dto: UpdateCustomerDto): Promise { await this.findById(id); + // Validate VAT number if provided + if (dto.vatNumber && dto.vatNumber.length !== 10) { + throw new BadRequestException("VAT number must be exactly 10 digits"); + } + + // Check email conflict if (dto.email) { - const conflict = await this.customersRepository.findByEmail(dto.email); - if (conflict && conflict.id !== id) { + const existing = await this.customersRepository.findByEmail(dto.email); + + if (existing && existing.userId !== id) { throw new ConflictException( `Customer with email "${dto.email}" already exists`, ); @@ -48,14 +102,25 @@ export class CustomersService { } const updated = await this.customersRepository.update(id, dto); + if (!updated) { throw new NotFoundException(`Customer ${id} not found`); } + return updated; } + /** Delete customer (soft delete) */ async remove(id: string): Promise { await this.findById(id); await this.customersRepository.softDelete(id); } -} + + /** Get customer statistics */ + async getStats(): Promise<{ total: number; withVatNumber: number }> { + const total = await this.customersRepository.count(); + const withVatNumber = await this.customersRepository.countWithVatNumber(); + + return { total, withVatNumber }; + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/customers/dto/create-customer.dto.ts b/apps/edr-freight-api/src/modules/customers/dto/create-customer.dto.ts index 854b3eaf1..8b6a61279 100644 --- a/apps/edr-freight-api/src/modules/customers/dto/create-customer.dto.ts +++ b/apps/edr-freight-api/src/modules/customers/dto/create-customer.dto.ts @@ -4,8 +4,12 @@ import { IsOptional, IsString, MaxLength, + IsNotEmpty, + Length, + Matches, } from "class-validator"; +// Enums export enum CustomerStatusDto { Active = "Active", Pending = "Pending", @@ -18,23 +22,53 @@ export enum CustomerTypeDto { Supplier = "Supplier", } +// DTO export class CreateCustomerDto { + // Basic identity @IsString() - @MaxLength(256) - name!: string; + @IsNotEmpty() + @MaxLength(100) + firstName!: string; + + @IsString() + @IsNotEmpty() + @MaxLength(100) + lastName!: string; @IsEmail() + @IsNotEmpty() email!: string; @IsString() - @MaxLength(32) + @IsNotEmpty() + @MaxLength(20) phone!: string; - @IsOptional() + // Company info @IsString() - @MaxLength(256) - company?: string; + @IsNotEmpty() + @MaxLength(200) + companyName!: string; + @IsEmail() + @IsNotEmpty() + companyEmail!: string; + + @IsString() + @IsNotEmpty() + @MaxLength(20) + companyPhone!: string; + + @IsString() + @IsNotEmpty() + @MaxLength(100) + companyLocation!: string; + + @IsString() + @IsNotEmpty() + companyAddress!: string; + + // Classification @IsOptional() @IsEnum(CustomerTypeDto) customerType?: CustomerTypeDto; @@ -43,31 +77,76 @@ export class CreateCustomerDto { @IsEnum(CustomerStatusDto) status?: CustomerStatusDto; + // Legal identifiers + @IsString() + @IsNotEmpty() + @Length(10, 10) + @Matches(/^\d+$/, { message: "TIN must contain only digits" }) + tinNumber!: string; + + @IsString() + @IsNotEmpty() + @Length(16, 16) + @Matches(/^\d+$/, { message: "FAN must contain only digits" }) + fanNumber!: string; + + @IsString() + @IsNotEmpty() + @MaxLength(50) + vatNumber!: string; + + // Contact person + @IsString() + @IsNotEmpty() + @MaxLength(100) + contactPersonName!: string; + + @IsString() + @IsNotEmpty() + @MaxLength(20) + contactPersonPhone!: string; + + // Management + @IsString() + @IsNotEmpty() + @MaxLength(100) + generalManagerName!: string; + + @IsEmail() + @IsNotEmpty() + generalManagerEmail!: string; + + @IsString() + @IsNotEmpty() + @MaxLength(20) + generalManagerPhone!: string; + + // POA (Power of Attorney) @IsOptional() @IsString() - @MaxLength(64) - tinNumber?: string; + @MaxLength(100) + poaName?: string; @IsOptional() @IsString() - @MaxLength(128) - city?: string; + @MaxLength(20) + poaPhone?: string; @IsOptional() @IsString() - @MaxLength(128) - country?: string; + poaAddress?: string; + + @IsOptional() + @IsEmail() + poaEmail?: string; @IsOptional() @IsString() - address?: string; - - @IsOptional() - @IsString() - @MaxLength(64) - taxId?: string; + @MaxLength(100) + poaLocation?: string; + // Extra @IsOptional() @IsString() notes?: string; -} +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts b/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts new file mode 100644 index 000000000..9aed70abc --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts @@ -0,0 +1,60 @@ +// src/modules/customers/dto/response-customer.dto.ts +import { Customer } from '../entities/customer.entity'; + +export class ResponseCustomerDto { + UserId: string; + firstName: string; + lastName: string; + email: string; + phone: string; + companyName: string; + companyEmail: string; + companyPhone: string; + companyLocation: string; + companyAddress: string; + contactPersonName: string; + contactPersonPhone: string; + tinNumber: string; + vatNumber: string; + fanNumber: string; + generalManagerName: string; + generalManagerEmail: string; + generalManagerPhone: string; + poaName?: string; + poaPhone?: string; + poaAddress?: string; + poaEmail?: string; + poaLocation?: string; + notes?: string; + createdAt: Date; + updatedAt: Date; + + constructor(customer: Customer) { + this.UserId = customer.userId; + this.firstName = customer.firstName; + this.lastName = customer.lastName; + this.email = customer.email; + this.phone = customer.phone; + this.companyName = customer.companyName; + this.companyEmail = customer.companyEmail; + this.companyPhone = customer.companyPhone; + this.companyLocation = customer.companyLocation; + this.companyAddress = customer.companyAddress; + this.contactPersonName = customer.contactPersonName; + this.contactPersonPhone = customer.contactPersonPhone; + this.tinNumber = customer.tinNumber; + this.vatNumber = customer.vatNumber; + this.fanNumber = customer.fanNumber; + this.generalManagerName = customer.generalManagerName; + this.generalManagerEmail = customer.generalManagerEmail; + this.generalManagerPhone = customer.generalManagerPhone; + this.poaName = customer.poaName ?? ''; + this.poaPhone = customer.poaPhone ?? ''; + this.poaAddress = customer.poaAddress ?? ''; + this.poaEmail = customer.poaEmail ?? ''; + this.poaLocation = customer.poaLocation ?? ''; + this.notes = customer.notes ?? ''; + this.createdAt = customer.createdAt; + this.updatedAt = customer.updatedAt; + } +} \ No newline at end of file diff --git a/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts b/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts index 94b49d0fd..3651f4b44 100644 --- a/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts +++ b/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts @@ -1,5 +1,9 @@ -import { PartialType } from "@nestjs/swagger"; +// src/modules/customers/dto/update-customer.dto.ts +import { PartialType } from '@nestjs/swagger'; +import { CreateCustomerDto } from './create-customer.dto'; -import { CreateCustomerDto } from "./create-customer.dto"; - -export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {} +export class UpdateCustomerDto extends PartialType(CreateCustomerDto) { + email?: string; + vatNumber?: string; + // Add any other properties you need to access directly +} diff --git a/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts b/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts index 98a248c97..3ad22e3f2 100644 --- a/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts +++ b/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts @@ -1,54 +1,94 @@ -import { BaseEntity } from "@edr/api-common"; -import { Column, Entity } from "typeorm"; -export type CustomerStatus = "Active" | "Pending" | "Inactive"; -export type CustomerType = "Importer" | "Exporter" | "Supplier"; +import { Column, Entity , + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + Index, +} from "typeorm"; -@Entity({schema:"freight", name: "customers" }) -export class Customer extends BaseEntity { - @Column({ name: "name", type: "varchar", length: 256 }) - name!: string; +@Entity("customers") +export class Customer { + @PrimaryGeneratedColumn("uuid") + userId!: string; - @Column({ name: "email", type: "varchar", length: 256, unique: true }) + @Column({ name: "firstName", length: 100 }) + @Index() + firstName!: string; + + @Column({ name: "lastName", length: 100 }) + @Index() + lastName!: string; + + @Column({ unique: true }) + @Index() email!: string; - @Column({ name: "phone", type: "varchar", length: 32 }) + @Column({ length: 20 }) phone!: string; - @Column({ name: "company", type: "varchar", length: 256, nullable: true }) - company?: string | null; + @Column({ name: "companyName", length: 200 }) + @Index() + companyName!: string; - @Column({ - name: "customer_type", - type: "varchar", - length: 32, - default: "Importer", - }) - customerType!: CustomerType; + @Column({ name: "companyEmail" }) + companyEmail!: string; - @Column({ - name: "status", - type: "varchar", - length: 32, - default: "Active", - }) - status!: CustomerStatus; + @Column({ name: "companyPhone", length: 20 }) + companyPhone!: string; - @Column({ name: "tin_number", type: "varchar", length: 64, nullable: true }) - tinNumber?: string | null; + @Column({ name: "companyLocation", length: 100 }) + companyLocation!: string; - @Column({ name: "city", type: "varchar", length: 128, nullable: true }) - city?: string | null; + @Column({ name: "companyAddress", type: "text" }) + companyAddress!: string; - @Column({ name: "country", type: "varchar", length: 128, nullable: true }) - country?: string | null; + @Column({ name: "contactPersonName", length: 100 }) + contactPersonName!: string; - @Column({ name: "address", type: "text", nullable: true }) - address?: string | null; + @Column({ name: "contactPersonPhone", length: 20 }) + contactPersonPhone!: string; - @Column({ name: "tax_id", type: "varchar", length: 64, nullable: true }) - taxId?: string | null; + @Column({ name: "tinNumber", length: 10, unique: true }) + @Index() + tinNumber!: string; - @Column({ name: "notes", type: "text", nullable: true }) - notes?: string | null; + @Column({ name: "vatNumber", length: 50 }) + vatNumber!: string; + + @Column({ name: "fanNumber", length: 16, unique: true }) + @Index() + fanNumber!: string; + + @Column({ name: "generalManagerName", length: 100 }) + generalManagerName!: string; + + @Column({ name: "generalManagerEmail" }) + generalManagerEmail!: string; + + @Column({ name: "generalManagerPhone", length: 20 }) + generalManagerPhone!: string; + + @Column({ name: "poaName", length: 100, nullable: true }) + poaName?: string; + + @Column({ name: "poaPhone", length: 20, nullable: true }) + poaPhone?: string; + + @Column({ name: "poaAddress", type: "text", nullable: true }) + poaAddress?: string; + + @Column({ name: "poaEmail", nullable: true }) + poaEmail?: string; + + @Column({ name: "poaLocation", length: 100, nullable: true }) + poaLocation?: string; + + @Column({ type: "text", nullable: true }) + notes?: string; + + @CreateDateColumn({ name: "createdAt" }) + createdAt!: Date; + + @UpdateDateColumn({ name: "updatedAt" }) + updatedAt!: Date; } diff --git a/apps/edr-freight-api/src/modules/customers2/customers.controller.ts b/apps/edr-freight-api/src/modules/customers2/customers.controller.ts new file mode 100644 index 000000000..85b470453 --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers2/customers.controller.ts @@ -0,0 +1,57 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, +} from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { CustomersService } from "./customers.service"; +import { CreateCustomerDto } from "./dto/create-customer.dto"; +import { UpdateCustomerDto } from "./dto/update-customer.dto"; + +@ApiTags("customers") +@Controller("customers") +export class CustomersController { + constructor(private readonly customersService: CustomersService) {} + + @Post() + @ApiOperation({ summary: "Create a new customer" }) + create(@Body() dto: CreateCustomerDto) { + return this.customersService.create(dto); + } + + @Get() + @ApiOperation({ summary: "List all customers" }) + findAll() { + return this.customersService.findAll(); + } + + @Get(":id") + @ApiOperation({ summary: "Get a customer by ID" }) + findOne(@Param("id", ParseUUIDPipe) id: string) { + return this.customersService.findById(id); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a customer" }) + update( + @Param("id", ParseUUIDPipe) id: string, + @Body() dto: UpdateCustomerDto, + ) { + return this.customersService.update(id, dto); + } + + @Delete(":id") + @ApiOperation({ summary: "Soft-delete a customer" }) + @HttpCode(HttpStatus.NO_CONTENT) + remove(@Param("id", ParseUUIDPipe) id: string) { + return this.customersService.remove(id); + } +} diff --git a/apps/edr-freight-api/src/modules/customers2/customers.module.ts b/apps/edr-freight-api/src/modules/customers2/customers.module.ts new file mode 100644 index 000000000..28c6b7c89 --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers2/customers.module.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { CustomersController } from "./customers.controller"; +import { CustomersRepository } from "./customers.repository"; +import { CustomersService } from "./customers.service"; +import { Customer } from "./entities/customer.entity"; + +@Module({ + imports: [TypeOrmModule.forFeature([Customer])], + controllers: [CustomersController], + providers: [CustomersService, CustomersRepository], + exports: [CustomersService], +}) +export class CustomersModule {} diff --git a/apps/edr-freight-api/src/modules/customers2/customers.repository.ts b/apps/edr-freight-api/src/modules/customers2/customers.repository.ts new file mode 100644 index 000000000..c6cb72fcf --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers2/customers.repository.ts @@ -0,0 +1,21 @@ +import { BaseRepository } from "@edr/api-common"; +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { Customer } from "./entities/customer.entity"; + +@Injectable() +export class CustomersRepository extends BaseRepository { + constructor( + @InjectRepository(Customer) + repository: Repository, + ) { + super(repository); + } + + /** Find a customer by their unique email. */ + findByEmail(email: string): Promise { + return this.repository.findOne({ where: { email } }); + } +} diff --git a/apps/edr-freight-api/src/modules/customers2/customers.service.ts b/apps/edr-freight-api/src/modules/customers2/customers.service.ts new file mode 100644 index 000000000..6394e1ad9 --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers2/customers.service.ts @@ -0,0 +1,61 @@ +import { + ConflictException, + Injectable, + NotFoundException, +} from "@nestjs/common"; + +import { CustomersRepository } from "./customers.repository"; +import { CreateCustomerDto } from "./dto/create-customer.dto"; +import { UpdateCustomerDto } from "./dto/update-customer.dto"; +import { Customer } from "./entities/customer.entity"; + +@Injectable() +export class CustomersService { + constructor(private readonly customersRepository: CustomersRepository) {} + + async create(dto: CreateCustomerDto): Promise { + const existing = await this.customersRepository.findByEmail(dto.email); + if (existing) { + throw new ConflictException( + `Customer with email "${dto.email}" already exists`, + ); + } + return this.customersRepository.create(dto); + } + + findAll(): Promise { + return this.customersRepository.findAll({ order: { name: "ASC" } }); + } + + async findById(id: string): Promise { + const customer = await this.customersRepository.findById(id); + if (!customer) { + throw new NotFoundException(`Customer ${id} not found`); + } + return customer; + } + + async update(id: string, dto: UpdateCustomerDto): Promise { + await this.findById(id); + + if (dto.email) { + const conflict = await this.customersRepository.findByEmail(dto.email); + if (conflict && conflict.id !== id) { + throw new ConflictException( + `Customer with email "${dto.email}" already exists`, + ); + } + } + + const updated = await this.customersRepository.update(id, dto); + if (!updated) { + throw new NotFoundException(`Customer ${id} not found`); + } + return updated; + } + + async remove(id: string): Promise { + await this.findById(id); + await this.customersRepository.softDelete(id); + } +} diff --git a/apps/edr-freight-api/src/modules/customers2/dto/create-customer.dto.ts b/apps/edr-freight-api/src/modules/customers2/dto/create-customer.dto.ts new file mode 100644 index 000000000..854b3eaf1 --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers2/dto/create-customer.dto.ts @@ -0,0 +1,73 @@ +import { + IsEmail, + IsEnum, + IsOptional, + IsString, + MaxLength, +} from "class-validator"; + +export enum CustomerStatusDto { + Active = "Active", + Pending = "Pending", + Inactive = "Inactive", +} + +export enum CustomerTypeDto { + Importer = "Importer", + Exporter = "Exporter", + Supplier = "Supplier", +} + +export class CreateCustomerDto { + @IsString() + @MaxLength(256) + name!: string; + + @IsEmail() + email!: string; + + @IsString() + @MaxLength(32) + phone!: string; + + @IsOptional() + @IsString() + @MaxLength(256) + company?: string; + + @IsOptional() + @IsEnum(CustomerTypeDto) + customerType?: CustomerTypeDto; + + @IsOptional() + @IsEnum(CustomerStatusDto) + status?: CustomerStatusDto; + + @IsOptional() + @IsString() + @MaxLength(64) + tinNumber?: string; + + @IsOptional() + @IsString() + @MaxLength(128) + city?: string; + + @IsOptional() + @IsString() + @MaxLength(128) + country?: string; + + @IsOptional() + @IsString() + address?: string; + + @IsOptional() + @IsString() + @MaxLength(64) + taxId?: string; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts b/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts new file mode 100644 index 000000000..94b49d0fd --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateCustomerDto } from "./create-customer.dto"; + +export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {} diff --git a/apps/edr-freight-api/src/modules/customers2/entities/customer.entity.ts b/apps/edr-freight-api/src/modules/customers2/entities/customer.entity.ts new file mode 100644 index 000000000..98a248c97 --- /dev/null +++ b/apps/edr-freight-api/src/modules/customers2/entities/customer.entity.ts @@ -0,0 +1,54 @@ +import { BaseEntity } from "@edr/api-common"; +import { Column, Entity } from "typeorm"; + +export type CustomerStatus = "Active" | "Pending" | "Inactive"; +export type CustomerType = "Importer" | "Exporter" | "Supplier"; + +@Entity({schema:"freight", name: "customers" }) +export class Customer extends BaseEntity { + @Column({ name: "name", type: "varchar", length: 256 }) + name!: string; + + @Column({ name: "email", type: "varchar", length: 256, unique: true }) + email!: string; + + @Column({ name: "phone", type: "varchar", length: 32 }) + phone!: string; + + @Column({ name: "company", type: "varchar", length: 256, nullable: true }) + company?: string | null; + + @Column({ + name: "customer_type", + type: "varchar", + length: 32, + default: "Importer", + }) + customerType!: CustomerType; + + @Column({ + name: "status", + type: "varchar", + length: 32, + default: "Active", + }) + status!: CustomerStatus; + + @Column({ name: "tin_number", type: "varchar", length: 64, nullable: true }) + tinNumber?: string | null; + + @Column({ name: "city", type: "varchar", length: 128, nullable: true }) + city?: string | null; + + @Column({ name: "country", type: "varchar", length: 128, nullable: true }) + country?: string | null; + + @Column({ name: "address", type: "text", nullable: true }) + address?: string | null; + + @Column({ name: "tax_id", type: "varchar", length: 64, nullable: true }) + taxId?: string | null; + + @Column({ name: "notes", type: "text", nullable: true }) + notes?: string | null; +} diff --git a/apps/edr-freight-web/portal/src/constants/FILE_SETTINGS.ts b/apps/edr-freight-web/portal/src/constants/FILE_SETTINGS.ts index 20a6c08ee..a07e2a2a5 100644 --- a/apps/edr-freight-web/portal/src/constants/FILE_SETTINGS.ts +++ b/apps/edr-freight-web/portal/src/constants/FILE_SETTINGS.ts @@ -1,3 +1,4 @@ export const FILE_SETTINGS = { - CUSTOMER_REGISTRATION: "customer_registration" + CUSTOMER_REGISTRATION: "customer_registration", + } \ No newline at end of file diff --git a/apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage copy.tsx b/apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage copy.tsx new file mode 100644 index 000000000..459732ad8 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/customers/NewCustomerPage copy.tsx @@ -0,0 +1,223 @@ +import type { ReactNode } from "react"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; + +import { + Building2, + Mail, + Phone, + User, + Globe, + MapPin, + FileText, +} from "lucide-react"; + +export interface CustomerFormData { + companyName?: string; + customerType?: string; + contactPerson?: string; + email?: string; + phone?: string; + tinNumber?: string; + city?: string; + country?: string; + address?: string; + notes?: string; +} + +export interface NewCustomerPageProps { + mode?: "create" | "edit"; + customer?: CustomerFormData; + children?: ReactNode; +} + +export default function NewCustomerPage({ + mode = "create", + customer, + children, +}: NewCustomerPageProps = {}) { + const isEdit = mode === "edit"; + const title = isEdit ? "Edit Customer" : "New Customer"; + const description = isEdit + ? "Update existing customer information." + : "Create and manage customer information."; + const submitLabel = isEdit ? "Save Changes" : "Create Customer"; + + return ( + + + {children ?? } + + + + + {title} + + {description} + + +
+ {/* Company Name */} +
+ + +
+ + + +
+
+ + {/* Customer Type */} +
+ + + +
+ + {/* Contact Person */} +
+ + +
+ + + +
+
+ + {/* Email */} +
+ + +
+ + + +
+
+ + {/* Phone */} +
+ + +
+ + + +
+
+ + {/* TIN */} +
+ + +
+ + + +
+
+ + {/* City */} +
+ + +
+ + + +
+
+ + {/* Country */} +
+ + +
+ + + +
+
+ + {/* Address */} +
+ + +