mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
customer registration api integration
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
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")
|
||||
@@ -33,4 +38,20 @@ export class CustomersController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
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) {}
|
||||
|
||||
/** Create a new freight customer. */
|
||||
create(dto: CreateCustomerDto): Promise<Customer> {
|
||||
async create(dto: CreateCustomerDto): Promise<Customer> {
|
||||
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);
|
||||
}
|
||||
|
||||
/** List every customer (alphabetical). */
|
||||
findAll(): Promise<Customer[]> {
|
||||
return this.customersRepository.findAll({ order: { name: "ASC" } });
|
||||
}
|
||||
|
||||
/** Get a single customer by ID. */
|
||||
async findById(id: string): Promise<Customer> {
|
||||
const customer = await this.customersRepository.findById(id);
|
||||
if (!customer) {
|
||||
@@ -26,4 +34,28 @@ export class CustomersService {
|
||||
}
|
||||
return customer;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCustomerDto): Promise<Customer> {
|
||||
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<void> {
|
||||
await this.findById(id);
|
||||
await this.customersRepository.softDelete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,73 @@
|
||||
import { IsEmail, IsOptional, IsString } from "class-validator";
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateCustomerDto } from "./create-customer.dto";
|
||||
|
||||
export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import { Column, Entity } from "typeorm";
|
||||
|
||||
export type CustomerStatus = "Active" | "Pending" | "Inactive";
|
||||
export type CustomerType = "Importer" | "Exporter" | "Supplier";
|
||||
|
||||
@Entity({ name: "customers" })
|
||||
export class Customer extends BaseEntity {
|
||||
@Column({ name: "name", type: "varchar", length: 256 })
|
||||
@@ -12,9 +15,40 @@ export class Customer extends BaseEntity {
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto";
|
||||
import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto";
|
||||
import { UpdateDropdownOptionDto } from "./dto/update-dropdown-option.dto";
|
||||
import { UpdateDropdownSettingDto } from "./dto/update-dropdown-setting.dto";
|
||||
import { DropdownSettingsService } from "./dropdown-settings.service";
|
||||
|
||||
@ApiTags("dropdown-settings")
|
||||
@Controller("dropdown-settings")
|
||||
export class DropdownSettingsController {
|
||||
constructor(private readonly service: DropdownSettingsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: "List all dropdown settings" })
|
||||
list() {
|
||||
return this.service.list();
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@ApiOperation({ summary: "Get a dropdown setting by ID" })
|
||||
getById(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.service.getById(id);
|
||||
}
|
||||
|
||||
@Get("by-code/:code")
|
||||
@ApiOperation({ summary: "Get a dropdown setting by its stable code" })
|
||||
getByCode(@Param("code") code: string) {
|
||||
return this.service.getByCode(code);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a new dropdown setting" })
|
||||
create(@Body() dto: CreateDropdownSettingDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@ApiOperation({ summary: "Update a dropdown setting's metadata" })
|
||||
update(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateDropdownSettingDto,
|
||||
) {
|
||||
return this.service.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@ApiOperation({ summary: "Soft-delete a dropdown setting" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.service.remove(id);
|
||||
}
|
||||
|
||||
/* ------------------------- option routes ------------------------- */
|
||||
|
||||
@Put(":id/options")
|
||||
@ApiOperation({ summary: "Replace the full option list for a setting" })
|
||||
replaceOptions(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() options: CreateDropdownOptionDto[],
|
||||
) {
|
||||
return this.service.replaceOptions(id, options);
|
||||
}
|
||||
|
||||
@Post(":id/options")
|
||||
@ApiOperation({ summary: "Append a single option to a setting" })
|
||||
addOption(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: CreateDropdownOptionDto,
|
||||
) {
|
||||
return this.service.addOption(id, dto);
|
||||
}
|
||||
|
||||
@Patch("options/:optionId")
|
||||
@ApiOperation({ summary: "Update a single option" })
|
||||
updateOption(
|
||||
@Param("optionId", ParseUUIDPipe) optionId: string,
|
||||
@Body() dto: UpdateDropdownOptionDto,
|
||||
) {
|
||||
return this.service.updateOption(optionId, dto);
|
||||
}
|
||||
|
||||
@Delete("options/:optionId")
|
||||
@ApiOperation({ summary: "Soft-delete a single option" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeOption(@Param("optionId", ParseUUIDPipe) optionId: string) {
|
||||
return this.service.removeOption(optionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { DropdownOption } from "./entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "./entities/dropdown-setting.entity";
|
||||
import { DropdownSettingsController } from "./dropdown-settings.controller";
|
||||
import { DropdownSettingsRepository } from "./dropdown-settings.repository";
|
||||
import { DropdownSettingsService } from "./dropdown-settings.service";
|
||||
import { DROPDOWN_SETTINGS_REPOSITORY } from "./interfaces/dropdown-settings.repository.interface";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([DropdownSetting, DropdownOption])],
|
||||
controllers: [DropdownSettingsController],
|
||||
providers: [
|
||||
DropdownSettingsRepository,
|
||||
{
|
||||
provide: DROPDOWN_SETTINGS_REPOSITORY,
|
||||
useExisting: DropdownSettingsRepository,
|
||||
},
|
||||
DropdownSettingsService,
|
||||
],
|
||||
exports: [DropdownSettingsService],
|
||||
})
|
||||
export class DropdownSettingsModule {}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { BaseRepository } from "@edr/api-common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { DropdownOption } from "./entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "./entities/dropdown-setting.entity";
|
||||
import type { IDropdownSettingsRepository } from "./interfaces/dropdown-settings.repository.interface";
|
||||
|
||||
@Injectable()
|
||||
export class DropdownSettingsRepository
|
||||
extends BaseRepository<DropdownSetting>
|
||||
implements IDropdownSettingsRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(DropdownSetting)
|
||||
repository: Repository<DropdownSetting>,
|
||||
@InjectRepository(DropdownOption)
|
||||
private readonly optionsRepository: Repository<DropdownOption>,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
findByCode(code: string): Promise<DropdownSetting | null> {
|
||||
return this.repository.findOne({
|
||||
where: { code },
|
||||
relations: { children: true },
|
||||
order: { children: { order: "ASC" } },
|
||||
});
|
||||
}
|
||||
|
||||
override findById(id: string): Promise<DropdownSetting | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id },
|
||||
relations: { children: true },
|
||||
order: { children: { order: "ASC" } },
|
||||
});
|
||||
}
|
||||
|
||||
override findAll(): Promise<DropdownSetting[]> {
|
||||
return this.repository.find({
|
||||
order: { label: "ASC", children: { order: "ASC" } },
|
||||
relations: { children: true },
|
||||
});
|
||||
}
|
||||
|
||||
async replaceOptions(
|
||||
settingId: string,
|
||||
options: Array<Partial<DropdownOption>>,
|
||||
): Promise<DropdownOption[]> {
|
||||
await this.optionsRepository.delete({ settingId });
|
||||
if (options.length === 0) return [];
|
||||
const entities = options.map((o, idx) =>
|
||||
this.optionsRepository.create({
|
||||
...o,
|
||||
settingId,
|
||||
order: o.order ?? idx + 1,
|
||||
}),
|
||||
);
|
||||
return this.optionsRepository.save(entities);
|
||||
}
|
||||
|
||||
async addOption(
|
||||
settingId: string,
|
||||
option: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption> {
|
||||
const entity = this.optionsRepository.create({ ...option, settingId });
|
||||
return this.optionsRepository.save(entity);
|
||||
}
|
||||
|
||||
async updateOption(
|
||||
optionId: string,
|
||||
data: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption | null> {
|
||||
await this.optionsRepository.update(optionId, data as never);
|
||||
return this.optionsRepository.findOne({ where: { id: optionId } });
|
||||
}
|
||||
|
||||
async removeOption(optionId: string): Promise<void> {
|
||||
await this.optionsRepository.softDelete(optionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto";
|
||||
import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto";
|
||||
import { UpdateDropdownOptionDto } from "./dto/update-dropdown-option.dto";
|
||||
import { UpdateDropdownSettingDto } from "./dto/update-dropdown-setting.dto";
|
||||
import { DropdownOption } from "./entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "./entities/dropdown-setting.entity";
|
||||
import {
|
||||
DROPDOWN_SETTINGS_REPOSITORY,
|
||||
IDropdownSettingsRepository,
|
||||
} from "./interfaces/dropdown-settings.repository.interface";
|
||||
|
||||
@Injectable()
|
||||
export class DropdownSettingsService {
|
||||
constructor(
|
||||
@Inject(DROPDOWN_SETTINGS_REPOSITORY)
|
||||
private readonly repository: IDropdownSettingsRepository,
|
||||
) {}
|
||||
|
||||
list(): Promise<DropdownSetting[]> {
|
||||
return this.repository.findAll();
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<DropdownSetting> {
|
||||
const setting = await this.repository.findById(id);
|
||||
if (!setting) throw new NotFoundException(`Setting ${id} not found`);
|
||||
return setting;
|
||||
}
|
||||
|
||||
async getByCode(code: string): Promise<DropdownSetting> {
|
||||
const setting = await this.repository.findByCode(code);
|
||||
if (!setting) throw new NotFoundException(`Setting "${code}" not found`);
|
||||
return setting;
|
||||
}
|
||||
|
||||
async create(dto: CreateDropdownSettingDto): Promise<DropdownSetting> {
|
||||
const existing = await this.repository.findByCode(dto.code);
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Dropdown setting with code "${dto.code}" already exists`,
|
||||
);
|
||||
}
|
||||
|
||||
const setting = await this.repository.create({
|
||||
code: dto.code,
|
||||
label: dto.label,
|
||||
description: dto.description ?? null,
|
||||
multiple: dto.multiple ?? false,
|
||||
meta: dto.meta ?? null,
|
||||
});
|
||||
|
||||
if (dto.children && dto.children.length > 0) {
|
||||
await this.repository.replaceOptions(setting.id, dto.children);
|
||||
}
|
||||
|
||||
return this.getById(setting.id);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateDropdownSettingDto,
|
||||
): Promise<DropdownSetting> {
|
||||
await this.getById(id);
|
||||
const updated = await this.repository.update(id, dto);
|
||||
if (!updated) throw new NotFoundException(`Setting ${id} not found`);
|
||||
return this.getById(id);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.getById(id);
|
||||
await this.repository.softDelete(id);
|
||||
}
|
||||
|
||||
/* ------------------------ option operations ------------------------ */
|
||||
|
||||
async replaceOptions(
|
||||
settingId: string,
|
||||
options: CreateDropdownOptionDto[],
|
||||
): Promise<DropdownOption[]> {
|
||||
await this.getById(settingId);
|
||||
return this.repository.replaceOptions(settingId, options);
|
||||
}
|
||||
|
||||
async addOption(
|
||||
settingId: string,
|
||||
dto: CreateDropdownOptionDto,
|
||||
): Promise<DropdownOption> {
|
||||
await this.getById(settingId);
|
||||
return this.repository.addOption(settingId, dto);
|
||||
}
|
||||
|
||||
async updateOption(
|
||||
optionId: string,
|
||||
dto: UpdateDropdownOptionDto,
|
||||
): Promise<DropdownOption> {
|
||||
const updated = await this.repository.updateOption(optionId, dto);
|
||||
if (!updated) throw new NotFoundException(`Option ${optionId} not found`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async removeOption(optionId: string): Promise<void> {
|
||||
await this.repository.removeOption(optionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
import { DropdownOptionMetaDto } from "./dropdown-option-meta.dto";
|
||||
|
||||
export class CreateDropdownOptionDto {
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
value!: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
note?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
disabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
order?: number;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DropdownOptionMetaDto)
|
||||
meta?: DropdownOptionMetaDto;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./create-dropdown-option.dto";
|
||||
import { DropdownSettingMetaDto } from "./dropdown-setting-meta.dto";
|
||||
|
||||
export class CreateDropdownSettingDto {
|
||||
@IsString()
|
||||
@MaxLength(128)
|
||||
@Matches(/^[a-z][a-z0-9_]*$/i, {
|
||||
message: "code must be snake_case-friendly (letters, digits, underscores)",
|
||||
})
|
||||
code!: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
multiple?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DropdownSettingMetaDto)
|
||||
meta?: DropdownSettingMetaDto;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateDropdownOptionDto)
|
||||
children?: CreateDropdownOptionDto[];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IsOptional, IsString, MaxLength } from "class-validator";
|
||||
|
||||
export class DropdownOptionMetaDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
badge?: string;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Transform } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from "class-validator";
|
||||
|
||||
export class DropdownSettingMetaDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
searchable?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
clearable?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@Transform(({ value }) =>
|
||||
Array.isArray(value)
|
||||
? (value as string[]).map((s) => s.trim()).filter(Boolean)
|
||||
: value,
|
||||
)
|
||||
permissions?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
version?: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./create-dropdown-option.dto";
|
||||
|
||||
export class UpdateDropdownOptionDto extends PartialType(
|
||||
CreateDropdownOptionDto,
|
||||
) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { OmitType, PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateDropdownSettingDto } from "./create-dropdown-setting.dto";
|
||||
|
||||
export class UpdateDropdownSettingDto extends PartialType(
|
||||
OmitType(CreateDropdownSettingDto, ["children"] as const),
|
||||
) {}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
import { DropdownSetting } from "./dropdown-setting.entity";
|
||||
|
||||
export interface DropdownOptionMeta {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
@Entity({ name: "dropdown_options" })
|
||||
@Index(["settingId", "value"], { unique: true })
|
||||
export class DropdownOption extends BaseEntity {
|
||||
@ManyToOne(() => DropdownSetting, (setting) => setting.children, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn({ name: "setting_id" })
|
||||
setting!: DropdownSetting;
|
||||
|
||||
@Column({ name: "setting_id", type: "uuid" })
|
||||
settingId!: string;
|
||||
|
||||
@Column({ name: "value", type: "varchar", length: 256 })
|
||||
value!: string;
|
||||
|
||||
@Column({ name: "label", type: "varchar", length: 256 })
|
||||
label!: string;
|
||||
|
||||
@Column({ name: "note", type: "text", nullable: true })
|
||||
note?: string | null;
|
||||
|
||||
@Column({ name: "is_disabled", type: "boolean", default: false })
|
||||
disabled!: boolean;
|
||||
|
||||
@Column({ name: "display_order", type: "integer", default: 0 })
|
||||
order!: number;
|
||||
|
||||
@Column({ name: "meta", type: "jsonb", nullable: true })
|
||||
meta?: DropdownOptionMeta | null;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import { Column, Entity, Index, OneToMany } from "typeorm";
|
||||
|
||||
import { DropdownOption } from "./dropdown-option.entity";
|
||||
|
||||
export interface DropdownSettingMeta {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
searchable?: boolean;
|
||||
clearable?: boolean;
|
||||
permissions?: string[];
|
||||
version?: string;
|
||||
}
|
||||
|
||||
@Entity({ name: "dropdown_settings" })
|
||||
@Index(["code"], { unique: true })
|
||||
export class DropdownSetting extends BaseEntity {
|
||||
@Column({ name: "code", type: "varchar", length: 128, unique: true })
|
||||
code!: string;
|
||||
|
||||
@Column({ name: "label", type: "varchar", length: 256 })
|
||||
label!: string;
|
||||
|
||||
@Column({ name: "description", type: "text", nullable: true })
|
||||
description?: string | null;
|
||||
|
||||
@Column({ name: "multiple", type: "boolean", default: false })
|
||||
multiple!: boolean;
|
||||
|
||||
@Column({ name: "meta", type: "jsonb", nullable: true })
|
||||
meta?: DropdownSettingMeta | null;
|
||||
|
||||
@OneToMany(() => DropdownOption, (option) => option.setting, {
|
||||
cascade: true,
|
||||
})
|
||||
children!: DropdownOption[];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { DropdownOption } from "../entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "../entities/dropdown-setting.entity";
|
||||
|
||||
/**
|
||||
* Contract every DropdownSettings repository must satisfy. Lets services
|
||||
* depend on the abstraction and lets tests swap in an in-memory fake.
|
||||
*/
|
||||
export const DROPDOWN_SETTINGS_REPOSITORY = Symbol(
|
||||
"DROPDOWN_SETTINGS_REPOSITORY",
|
||||
);
|
||||
|
||||
export interface IDropdownSettingsRepository {
|
||||
findAll(): Promise<DropdownSetting[]>;
|
||||
findById(id: string): Promise<DropdownSetting | null>;
|
||||
findByCode(code: string): Promise<DropdownSetting | null>;
|
||||
|
||||
create(data: Partial<DropdownSetting>): Promise<DropdownSetting>;
|
||||
update(
|
||||
id: string,
|
||||
data: Partial<DropdownSetting>,
|
||||
): Promise<DropdownSetting | null>;
|
||||
softDelete(id: string): Promise<void>;
|
||||
|
||||
/* Option-level helpers */
|
||||
replaceOptions(
|
||||
settingId: string,
|
||||
options: Array<Partial<DropdownOption>>,
|
||||
): Promise<DropdownOption[]>;
|
||||
addOption(
|
||||
settingId: string,
|
||||
option: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption>;
|
||||
updateOption(
|
||||
optionId: string,
|
||||
data: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption | null>;
|
||||
removeOption(optionId: string): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user