import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, Matches, Max, MaxLength, Min, ValidateNested, } from 'class-validator'; import { PaginationQueryDto, UserRelationDto, } from '../../../../common/http/response'; import { CORE_STATUSES } from '../../../../common/value-objects/status/status'; import { CONTACT_JOB_TITLE_MAX_LENGTH, CONTACT_NAME_MAX_LENGTH, CONTACT_NOTES_MAX_LENGTH, CUSTOMER_ADDRESS_MAX_LENGTH, CUSTOMER_CODE_MAX_LENGTH, CUSTOMER_CODE_PATTERN, CUSTOMER_NAME_MAX_LENGTH, CUSTOMER_NAME_PATTERN, CUSTOMER_NFC_ID_MAX_LENGTH, } from '../customer-fields'; export class CreateCustomerContactDto { @ApiProperty({ example: 'Jean Luc', maxLength: CONTACT_NAME_MAX_LENGTH }) @IsString() @IsNotEmpty() @MaxLength(CONTACT_NAME_MAX_LENGTH) name!: string; @ApiPropertyOptional({ example: 'Purchasing Manager' }) @IsOptional() @IsString() @MaxLength(CONTACT_JOB_TITLE_MAX_LENGTH) jobTitle?: string; @ApiPropertyOptional({ example: '+6281234567890' }) @IsOptional() @IsString() @IsNotEmpty() phone?: string; @ApiPropertyOptional({ example: '+6281234567891' }) @IsOptional() @IsString() @IsNotEmpty() mobilePhone?: string; @ApiPropertyOptional({ example: 'Call after 9am' }) @IsOptional() @IsString() @MaxLength(CONTACT_NOTES_MAX_LENGTH) notes?: string; } export class UpdateCustomerContactDto { @ApiPropertyOptional({ example: 'Jean Luc' }) @IsOptional() @IsString() @IsNotEmpty() @MaxLength(CONTACT_NAME_MAX_LENGTH) name?: string; @ApiPropertyOptional({ example: 'Purchasing Manager', nullable: true }) @IsOptional() @IsString() @MaxLength(CONTACT_JOB_TITLE_MAX_LENGTH) jobTitle?: string | null; @ApiPropertyOptional({ example: '+6281234567890', nullable: true }) @IsOptional() @IsString() phone?: string | null; @ApiPropertyOptional({ example: '+6281234567891', nullable: true }) @IsOptional() @IsString() mobilePhone?: string | null; @ApiPropertyOptional({ example: 'Call after 9am', nullable: true }) @IsOptional() @IsString() @MaxLength(CONTACT_NOTES_MAX_LENGTH) notes?: string | null; } export class CustomerContactDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ format: 'uuid' }) customerId!: string; @ApiProperty() name!: string; @ApiPropertyOptional({ nullable: true }) jobTitle!: string | null; @ApiPropertyOptional({ nullable: true }) phone!: string | null; @ApiPropertyOptional({ nullable: true }) mobilePhone!: string | null; @ApiPropertyOptional({ nullable: true }) notes!: string | null; } export class CreateCustomerDto { @ApiProperty({ example: 'CUST_01', maxLength: CUSTOMER_CODE_MAX_LENGTH }) @IsString() @IsNotEmpty() @MaxLength(CUSTOMER_CODE_MAX_LENGTH) @Matches(CUSTOMER_CODE_PATTERN, { message: 'code must contain only letters, numbers, and underscores', }) code!: string; @ApiProperty({ example: 'Acme Corp', maxLength: CUSTOMER_NAME_MAX_LENGTH }) @IsString() @IsNotEmpty() @MaxLength(CUSTOMER_NAME_MAX_LENGTH) @Matches(CUSTOMER_NAME_PATTERN, { message: 'name must contain only letters and spaces', }) name!: string; @ApiProperty({ example: '+6281234567890' }) @IsString() @IsNotEmpty() phone!: string; @ApiProperty({ example: 'Jl Sudirman No 1' }) @IsString() @IsNotEmpty() @MaxLength(CUSTOMER_ADDRESS_MAX_LENGTH) address!: string; @ApiPropertyOptional({ example: -6.2 }) @IsOptional() @Type(() => Number) @IsNumber() @Min(-90) @Max(90) latitude?: number; @ApiPropertyOptional({ example: 106.8 }) @IsOptional() @Type(() => Number) @IsNumber() @Min(-180) @Max(180) longitude?: number; @ApiPropertyOptional({ example: 'NFC-001' }) @IsOptional() @IsString() @IsNotEmpty() @MaxLength(CUSTOMER_NFC_ID_MAX_LENGTH) nfcId?: string; @ApiPropertyOptional({ enum: CORE_STATUSES }) @IsOptional() @IsIn([...CORE_STATUSES]) status?: string; @ApiPropertyOptional({ type: [CreateCustomerContactDto] }) @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => CreateCustomerContactDto) contacts?: CreateCustomerContactDto[]; } export class UpdateCustomerDto { @ApiPropertyOptional({ example: 'CUST_01' }) @IsOptional() @IsString() @IsNotEmpty() @MaxLength(CUSTOMER_CODE_MAX_LENGTH) @Matches(CUSTOMER_CODE_PATTERN, { message: 'code must contain only letters, numbers, and underscores', }) code?: string; @ApiPropertyOptional({ example: 'Acme Corp' }) @IsOptional() @IsString() @IsNotEmpty() @MaxLength(CUSTOMER_NAME_MAX_LENGTH) @Matches(CUSTOMER_NAME_PATTERN, { message: 'name must contain only letters and spaces', }) name?: string; @ApiPropertyOptional({ example: '+6281234567890' }) @IsOptional() @IsString() @IsNotEmpty() phone?: string; @ApiPropertyOptional({ example: 'Jl Sudirman No 1' }) @IsOptional() @IsString() @IsNotEmpty() @MaxLength(CUSTOMER_ADDRESS_MAX_LENGTH) address?: string; @ApiPropertyOptional({ example: -6.2, nullable: true }) @IsOptional() @Type(() => Number) @IsNumber() @Min(-90) @Max(90) latitude?: number | null; @ApiPropertyOptional({ example: 106.8, nullable: true }) @IsOptional() @Type(() => Number) @IsNumber() @Min(-180) @Max(180) longitude?: number | null; @ApiPropertyOptional({ example: 'NFC-001', nullable: true }) @IsOptional() @IsString() @MaxLength(CUSTOMER_NFC_ID_MAX_LENGTH) nfcId?: string | null; @ApiPropertyOptional({ type: [CreateCustomerContactDto] }) @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => CreateCustomerContactDto) contacts?: CreateCustomerContactDto[]; } export class UpdateCustomerStatusDto { @ApiProperty({ enum: CORE_STATUSES }) @IsIn([...CORE_STATUSES]) status!: string; } export class BulkIdsDto { @ApiProperty({ type: [String], format: 'uuid' }) @IsArray() @ArrayNotEmpty() @IsUUID('4', { each: true }) ids!: string[]; } export class BulkStatusDto { @ApiProperty({ type: [String], format: 'uuid' }) @IsArray() @ArrayNotEmpty() @IsUUID('4', { each: true }) ids!: string[]; @ApiProperty({ enum: CORE_STATUSES }) @IsIn([...CORE_STATUSES]) status!: string; } export class ListCustomersQueryDto extends PaginationQueryDto { @ApiPropertyOptional() @IsOptional() @IsString() code?: string; @ApiPropertyOptional() @IsOptional() @IsString() name?: string; @ApiPropertyOptional() @IsOptional() @IsString() phone?: string; @ApiPropertyOptional() @IsOptional() @IsString() address?: string; @ApiPropertyOptional() @IsOptional() @IsString() nfcId?: string; @ApiPropertyOptional({ enum: CORE_STATUSES }) @IsOptional() @IsIn([...CORE_STATUSES]) status?: string; @ApiPropertyOptional({ description: 'Case-insensitive match on code, name, or address', }) @IsOptional() @IsString() search?: string; } export class CustomerDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty() code!: string; @ApiProperty() name!: string; @ApiProperty({ example: '+6281234567890' }) phone!: string; @ApiProperty() address!: string; @ApiPropertyOptional({ nullable: true }) latitude!: number | null; @ApiPropertyOptional({ nullable: true }) longitude!: number | null; @ApiPropertyOptional({ nullable: true }) nfcId!: string | null; @ApiPropertyOptional({ type: [CustomerContactDto] }) contacts?: CustomerContactDto[]; @ApiProperty({ enum: CORE_STATUSES }) status!: string; @ApiProperty({ description: 'Unix ms' }) createdAt!: number; @ApiProperty({ description: 'Unix ms' }) updatedAt!: number; @ApiProperty({ type: UserRelationDto }) createdBy!: UserRelationDto; @ApiProperty({ type: UserRelationDto }) updatedBy!: UserRelationDto; }