feat(custom-fields): add custom fields support
This commit is contained in:
@@ -705,6 +705,20 @@ export const events = {
|
|||||||
onDisconnected: 'onBankAccountDisconnected',
|
onDisconnected: 'onBankAccountDisconnected',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom fields service.
|
||||||
|
*/
|
||||||
|
customField: {
|
||||||
|
onCreating: 'onCustomFieldCreating',
|
||||||
|
onCreated: 'onCustomFieldCreated',
|
||||||
|
|
||||||
|
onEditing: 'onCustomFieldEditing',
|
||||||
|
onEdited: 'onCustomFieldEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onCustomFieldDeleting',
|
||||||
|
onDeleted: 'onCustomFieldDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
// Import files.
|
// Import files.
|
||||||
import: {
|
import: {
|
||||||
onImportCommitted: 'onImportFileCommitted',
|
onImportCommitted: 'onImportFileCommitted',
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('custom_fields', table => {
|
||||||
|
table.increments();
|
||||||
|
|
||||||
|
table.string('resource_name').notNullable();
|
||||||
|
table.string('field_name').notNullable();
|
||||||
|
table.string('label').notNullable();
|
||||||
|
table.string('field_type').notNullable();
|
||||||
|
table.json('options').nullable();
|
||||||
|
table.boolean('required').defaultTo(false);
|
||||||
|
table.integer('order').defaultTo(0);
|
||||||
|
table.boolean('active').defaultTo(true);
|
||||||
|
table.string('default_value').nullable();
|
||||||
|
|
||||||
|
table.timestamps();
|
||||||
|
|
||||||
|
table.index(['resource_name']);
|
||||||
|
table.index(['resource_name', 'active']);
|
||||||
|
table.unique(['resource_name', 'field_name']);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTableIfExists('custom_fields');
|
||||||
|
};
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('custom_field_values', table => {
|
||||||
|
table.increments();
|
||||||
|
|
||||||
|
table.integer('custom_field_id').unsigned().notNullable();
|
||||||
|
table.string('resource_type').notNullable();
|
||||||
|
table.integer('resource_id').unsigned().notNullable();
|
||||||
|
table.text('value').nullable();
|
||||||
|
|
||||||
|
table.timestamps();
|
||||||
|
|
||||||
|
table.index(['resource_type', 'resource_id'], 'cfv_resource_idx');
|
||||||
|
table.index(['custom_field_id'], 'cfv_field_idx');
|
||||||
|
table.unique(['custom_field_id', 'resource_type', 'resource_id'], 'cfv_unique_value');
|
||||||
|
|
||||||
|
table.foreign('custom_field_id')
|
||||||
|
.references('id')
|
||||||
|
.inTable('custom_fields')
|
||||||
|
.onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTableIfExists('custom_field_values');
|
||||||
|
};
|
||||||
@@ -132,6 +132,7 @@ export interface IItemEventCreatedPayload {
|
|||||||
// tenantId: number;
|
// tenantId: number;
|
||||||
item: Item;
|
item: Item;
|
||||||
itemId: number;
|
itemId: number;
|
||||||
|
itemDTO: any;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ export interface IItemEventEditedPayload {
|
|||||||
item: Item;
|
item: Item;
|
||||||
oldItem: Item;
|
oldItem: Item;
|
||||||
itemId: number;
|
itemId: number;
|
||||||
|
itemDTO: any;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module';
|
|||||||
import { UsersModule } from '../UsersModule/Users.module';
|
import { UsersModule } from '../UsersModule/Users.module';
|
||||||
import { ContactsModule } from '../Contacts/Contacts.module';
|
import { ContactsModule } from '../Contacts/Contacts.module';
|
||||||
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
|
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
|
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
|
||||||
import { ExchangeRatesModule } from '../ExchangeRates/ExchangeRates.module';
|
import { ExchangeRatesModule } from '../ExchangeRates/ExchangeRates.module';
|
||||||
import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module';
|
import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module';
|
||||||
@@ -256,6 +257,7 @@ import { AppThrottleModule } from './AppThrottle.module';
|
|||||||
MiscellaneousModule,
|
MiscellaneousModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
ContactsModule,
|
ContactsModule,
|
||||||
|
CustomFieldsModule,
|
||||||
SocketModule,
|
SocketModule,
|
||||||
ExchangeRatesModule,
|
ExchangeRatesModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.
|
|||||||
import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module';
|
import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module';
|
||||||
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
|
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
|
||||||
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
|
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -50,6 +51,7 @@ import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCredit
|
|||||||
AccountsModule,
|
AccountsModule,
|
||||||
DynamicListModule,
|
DynamicListModule,
|
||||||
InventoryCostModule,
|
InventoryCostModule,
|
||||||
|
CustomFieldsModule,
|
||||||
forwardRef(() => CreditNoteRefundsModule),
|
forwardRef(() => CreditNoteRefundsModule),
|
||||||
forwardRef(() => CreditNotesApplyInvoiceModule)
|
forwardRef(() => CreditNotesApplyInvoiceModule)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
|||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { CreateCreditNoteDto } from '../dtos/CreditNote.dto';
|
import { CreateCreditNoteDto } from '../dtos/CreditNote.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateCreditNoteService {
|
export class CreateCreditNoteService {
|
||||||
@@ -29,6 +30,7 @@ export class CreateCreditNoteService {
|
|||||||
private readonly itemsEntriesService: ItemsEntriesService,
|
private readonly itemsEntriesService: ItemsEntriesService,
|
||||||
private readonly eventPublisher: EventEmitter2,
|
private readonly eventPublisher: EventEmitter2,
|
||||||
private readonly commandCreditNoteDTOTransform: CommandCreditNoteDTOTransform,
|
private readonly commandCreditNoteDTOTransform: CommandCreditNoteDTOTransform,
|
||||||
|
private readonly saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(CreditNote.name)
|
@Inject(CreditNote.name)
|
||||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||||
@@ -84,6 +86,17 @@ export class CreateCreditNoteService {
|
|||||||
.upsertGraph({
|
.upsertGraph({
|
||||||
...creditNoteModel,
|
...creditNoteModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (creditNoteDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'CreditNote',
|
||||||
|
creditNote.id,
|
||||||
|
creditNoteDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onCreditNoteCreated` event.
|
// Triggers `onCreditNoteCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
|
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
|
||||||
creditNoteDTO,
|
creditNoteDTO,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { events } from '@/common/events/events';
|
|||||||
import { CommandCreditNoteDTOTransform } from './CommandCreditNoteDTOTransform.service';
|
import { CommandCreditNoteDTOTransform } from './CommandCreditNoteDTOTransform.service';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { EditCreditNoteDto } from '../dtos/CreditNote.dto';
|
import { EditCreditNoteDto } from '../dtos/CreditNote.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EditCreditNoteService {
|
export class EditCreditNoteService {
|
||||||
@@ -35,6 +36,7 @@ export class EditCreditNoteService {
|
|||||||
private itemsEntriesService: ItemsEntriesService,
|
private itemsEntriesService: ItemsEntriesService,
|
||||||
private eventPublisher: EventEmitter2,
|
private eventPublisher: EventEmitter2,
|
||||||
private uow: UnitOfWork,
|
private uow: UnitOfWork,
|
||||||
|
private saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,6 +95,17 @@ export class EditCreditNoteService {
|
|||||||
id: creditNoteId,
|
id: creditNoteId,
|
||||||
...creditNoteModel,
|
...creditNoteModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (creditNoteEditDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'CreditNote',
|
||||||
|
creditNoteId,
|
||||||
|
creditNoteEditDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onCreditNoteEdited` event.
|
// Triggers `onCreditNoteEdited` event.
|
||||||
await this.eventPublisher.emitAsync(events.creditNote.onEdited, {
|
await this.eventPublisher.emitAsync(events.creditNote.onEdited, {
|
||||||
trx,
|
trx,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
IsInt,
|
IsInt,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -126,6 +127,15 @@ export class CommandCreditNoteDto {
|
|||||||
@ToNumber()
|
@ToNumber()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
adjustment?: number;
|
adjustment?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateCreditNoteDto extends CommandCreditNoteDto {}
|
export class CreateCreditNoteDto extends CommandCreditNoteDto {}
|
||||||
|
|||||||
@@ -263,4 +263,11 @@ export class CreditNoteResponseDto {
|
|||||||
example: '$1,000.00',
|
example: '$1,000.00',
|
||||||
})
|
})
|
||||||
totalLocalFormatted: string;
|
totalLocalFormatted: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,18 @@ export class CreditNoteTransformer extends Transformer {
|
|||||||
|
|
||||||
'entries',
|
'entries',
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve custom fields from options.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public customFields(): Record<string, any> {
|
||||||
|
return this.options?.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted credit note date.
|
* Retrieve formatted credit note date.
|
||||||
* @param {ICreditNote} credit
|
* @param {ICreditNote} credit
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { CreditNote } from '../models/CreditNote';
|
import { CreditNote } from '../models/CreditNote';
|
||||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetCreditNoteService {
|
export class GetCreditNoteService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly transformer: TransformerInjectable,
|
private readonly transformer: TransformerInjectable,
|
||||||
|
private readonly getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
|
|
||||||
@Inject(CreditNote.name)
|
@Inject(CreditNote.name)
|
||||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||||
@@ -32,7 +34,19 @@ export class GetCreditNoteService {
|
|||||||
if (!creditNote) {
|
if (!creditNote) {
|
||||||
throw new ServiceError(ERRORS.CREDIT_NOTE_NOT_FOUND);
|
throw new ServiceError(ERRORS.CREDIT_NOTE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
// Load custom field values.
|
||||||
|
const customFields = await this.getResourceCustomFieldsService.getResourceCustomFields(
|
||||||
|
'CreditNote',
|
||||||
|
creditNoteId,
|
||||||
|
);
|
||||||
|
|
||||||
// Transforms the credit note model to POJO.
|
// Transforms the credit note model to POJO.
|
||||||
return this.transformer.transform(creditNote, new CreditNoteTransformer());
|
const transformed = await this.transformer.transform(
|
||||||
|
creditNote,
|
||||||
|
new CreditNoteTransformer(),
|
||||||
|
{ customFields },
|
||||||
|
);
|
||||||
|
|
||||||
|
return transformed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CreateCustomFieldService } from './commands/CreateCustomField.service';
|
||||||
|
import { EditCustomFieldService } from './commands/EditCustomField.service';
|
||||||
|
import { DeleteCustomFieldService } from './commands/DeleteCustomField.service';
|
||||||
|
import { ReorderCustomFieldsService } from './commands/ReorderCustomFields.service';
|
||||||
|
import { UpdateFieldStatusService } from './commands/UpdateFieldStatus.service';
|
||||||
|
import { GetCustomFieldService } from './queries/GetCustomField.service';
|
||||||
|
import { GetCustomFieldsService } from './queries/GetCustomFields.service';
|
||||||
|
import { CreateCustomFieldDto, EditCustomFieldDto } from './dtos/CustomField.dto';
|
||||||
|
import { ReorderCustomFieldsDto } from './dtos/ReorderCustomFields.dto';
|
||||||
|
import { UpdateFieldStatusDto } from './dtos/UpdateFieldStatus.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CustomFieldsApplication {
|
||||||
|
constructor(
|
||||||
|
private createCustomFieldService: CreateCustomFieldService,
|
||||||
|
private editCustomFieldService: EditCustomFieldService,
|
||||||
|
private deleteCustomFieldService: DeleteCustomFieldService,
|
||||||
|
private reorderCustomFieldsService: ReorderCustomFieldsService,
|
||||||
|
private updateFieldStatusService: UpdateFieldStatusService,
|
||||||
|
private getCustomFieldService: GetCustomFieldService,
|
||||||
|
private getCustomFieldsService: GetCustomFieldsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createCustomField(dto: CreateCustomFieldDto) {
|
||||||
|
return this.createCustomFieldService.createCustomField(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async editCustomField(fieldId: number, dto: EditCustomFieldDto) {
|
||||||
|
return this.editCustomFieldService.editCustomField(fieldId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomField(fieldId: number) {
|
||||||
|
return this.deleteCustomFieldService.deleteCustomField(fieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomField(fieldId: number) {
|
||||||
|
return this.getCustomFieldService.getCustomField(fieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomFields(resourceName?: string) {
|
||||||
|
return this.getCustomFieldsService.getCustomFields(resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reorderCustomFields(dto: ReorderCustomFieldsDto) {
|
||||||
|
return this.reorderCustomFieldsService.reorderCustomFields(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFieldStatus(fieldId: number, dto: UpdateFieldStatusDto) {
|
||||||
|
return this.updateFieldStatusService.updateFieldStatus(fieldId, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CustomFieldsApplication } from './CustomFields.application';
|
||||||
|
import {
|
||||||
|
ApiExtraModels,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
getSchemaPath,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { CreateCustomFieldDto, EditCustomFieldDto } from './dtos/CustomField.dto';
|
||||||
|
import { CustomFieldResponseDto } from './dtos/CustomFieldResponse.dto';
|
||||||
|
import { ReorderCustomFieldsDto } from './dtos/ReorderCustomFields.dto';
|
||||||
|
import { UpdateFieldStatusDto } from './dtos/UpdateFieldStatus.dto';
|
||||||
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { CustomFieldAction } from './types/CustomFields.types';
|
||||||
|
|
||||||
|
@Controller('custom-fields')
|
||||||
|
@ApiTags('Custom Fields')
|
||||||
|
@ApiExtraModels(CustomFieldResponseDto)
|
||||||
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
|
export class CustomFieldsController {
|
||||||
|
constructor(private readonly customFieldsApplication: CustomFieldsApplication) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@RequirePermission(CustomFieldAction.CREATE, AbilitySubject.CustomField)
|
||||||
|
@ApiOperation({ summary: 'Create a new custom field.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'The custom field has been successfully created.',
|
||||||
|
schema: { $ref: getSchemaPath(CustomFieldResponseDto) },
|
||||||
|
})
|
||||||
|
public createCustomField(@Body() createCustomFieldDTO: CreateCustomFieldDto) {
|
||||||
|
return this.customFieldsApplication.createCustomField(createCustomFieldDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@RequirePermission(CustomFieldAction.EDIT, AbilitySubject.CustomField)
|
||||||
|
@ApiOperation({ summary: 'Edit the given custom field.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The custom field has been successfully updated.',
|
||||||
|
schema: { $ref: getSchemaPath(CustomFieldResponseDto) },
|
||||||
|
})
|
||||||
|
public editCustomField(
|
||||||
|
@Param('id') fieldId: number,
|
||||||
|
@Body() editCustomFieldDTO: EditCustomFieldDto,
|
||||||
|
) {
|
||||||
|
return this.customFieldsApplication.editCustomField(fieldId, editCustomFieldDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@RequirePermission(CustomFieldAction.DELETE, AbilitySubject.CustomField)
|
||||||
|
@ApiOperation({ summary: 'Delete the given custom field.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The custom field has been successfully deleted.',
|
||||||
|
schema: { $ref: getSchemaPath(CustomFieldResponseDto) },
|
||||||
|
})
|
||||||
|
public deleteCustomField(@Param('id') fieldId: number) {
|
||||||
|
return this.customFieldsApplication.deleteCustomField(fieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@RequirePermission(CustomFieldAction.VIEW, AbilitySubject.CustomField)
|
||||||
|
@ApiOperation({ summary: 'Retrieves the custom field details.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The custom field details have been successfully retrieved.',
|
||||||
|
schema: { $ref: getSchemaPath(CustomFieldResponseDto) },
|
||||||
|
})
|
||||||
|
public getCustomField(@Param('id') fieldId: number) {
|
||||||
|
return this.customFieldsApplication.getCustomField(fieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@RequirePermission(CustomFieldAction.VIEW, AbilitySubject.CustomField)
|
||||||
|
@ApiOperation({ summary: 'Retrieves the custom fields.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The custom fields have been successfully retrieved.',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: { $ref: getSchemaPath(CustomFieldResponseDto) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
public getCustomFields(@Query('resource') resourceName?: string) {
|
||||||
|
return this.customFieldsApplication.getCustomFields(resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('reorder')
|
||||||
|
@RequirePermission(CustomFieldAction.EDIT, AbilitySubject.CustomField)
|
||||||
|
@ApiOperation({ summary: 'Reorder custom fields.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The custom fields have been successfully reordered.',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: { $ref: getSchemaPath(CustomFieldResponseDto) },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
public reorderCustomFields(@Body() reorderDTO: ReorderCustomFieldsDto) {
|
||||||
|
return this.customFieldsApplication.reorderCustomFields(reorderDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id/status')
|
||||||
|
@RequirePermission(CustomFieldAction.EDIT, AbilitySubject.CustomField)
|
||||||
|
@ApiOperation({ summary: 'Update custom field status.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The custom field status has been successfully updated.',
|
||||||
|
schema: { $ref: getSchemaPath(CustomFieldResponseDto) },
|
||||||
|
})
|
||||||
|
public updateFieldStatus(
|
||||||
|
@Param('id') fieldId: number,
|
||||||
|
@Body() statusDTO: UpdateFieldStatusDto,
|
||||||
|
) {
|
||||||
|
return this.customFieldsApplication.updateFieldStatus(fieldId, statusDTO);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CustomFieldsController } from './CustomFields.controller';
|
||||||
|
import { CustomFieldsApplication } from './CustomFields.application';
|
||||||
|
import { CreateCustomFieldService } from './commands/CreateCustomField.service';
|
||||||
|
import { EditCustomFieldService } from './commands/EditCustomField.service';
|
||||||
|
import { DeleteCustomFieldService } from './commands/DeleteCustomField.service';
|
||||||
|
import { ReorderCustomFieldsService } from './commands/ReorderCustomFields.service';
|
||||||
|
import { UpdateFieldStatusService } from './commands/UpdateFieldStatus.service';
|
||||||
|
import { GetCustomFieldService } from './queries/GetCustomField.service';
|
||||||
|
import { GetCustomFieldsService } from './queries/GetCustomFields.service';
|
||||||
|
import { GetResourceCustomFieldsService } from './queries/GetResourceCustomFields.service';
|
||||||
|
import { SaveCustomFieldValuesService } from './queries/SaveCustomFieldValues.service';
|
||||||
|
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||||
|
import { CustomField } from './models/CustomField';
|
||||||
|
import { CustomFieldValue } from './models/CustomFieldValue';
|
||||||
|
import { CustomFieldsEntityEventsSubscriber } from './subscribers/CustomFieldsEntityEventsSubscriber';
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
RegisterTenancyModel(CustomField),
|
||||||
|
RegisterTenancyModel(CustomFieldValue),
|
||||||
|
];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [...models],
|
||||||
|
controllers: [CustomFieldsController],
|
||||||
|
providers: [
|
||||||
|
CustomFieldsApplication,
|
||||||
|
CreateCustomFieldService,
|
||||||
|
EditCustomFieldService,
|
||||||
|
DeleteCustomFieldService,
|
||||||
|
ReorderCustomFieldsService,
|
||||||
|
UpdateFieldStatusService,
|
||||||
|
GetCustomFieldService,
|
||||||
|
GetCustomFieldsService,
|
||||||
|
GetResourceCustomFieldsService,
|
||||||
|
SaveCustomFieldValuesService,
|
||||||
|
CustomFieldsEntityEventsSubscriber,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
GetResourceCustomFieldsService,
|
||||||
|
SaveCustomFieldValuesService,
|
||||||
|
GetCustomFieldsService,
|
||||||
|
...models,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CustomFieldsModule {}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { CreateCustomFieldDto } from '../dtos/CustomField.dto';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateCustomFieldService {
|
||||||
|
constructor(
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createCustomField(dto: CreateCustomFieldDto) {
|
||||||
|
return this.uow.withTransaction(async (trx) => {
|
||||||
|
const customField = await this.customFieldModel().query(trx).insert({
|
||||||
|
resourceName: dto.resourceName,
|
||||||
|
fieldName: dto.fieldName,
|
||||||
|
label: dto.label,
|
||||||
|
fieldType: dto.fieldType,
|
||||||
|
options: dto.options,
|
||||||
|
required: dto.required,
|
||||||
|
order: dto.order,
|
||||||
|
active: dto.active,
|
||||||
|
defaultValue: dto.defaultValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(events.customField.onCreated, { customField });
|
||||||
|
|
||||||
|
return customField;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
import { CustomFieldValue } from '../models/CustomFieldValue';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeleteCustomFieldService {
|
||||||
|
constructor(
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
@Inject(CustomFieldValue.name)
|
||||||
|
private readonly customFieldValueModel: TenantModelProxy<typeof CustomFieldValue>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async deleteCustomField(fieldId: number) {
|
||||||
|
return this.uow.withTransaction(async (trx) => {
|
||||||
|
const customField = await this.customFieldModel().query(trx).findById(fieldId);
|
||||||
|
if (!customField) {
|
||||||
|
throw new NotFoundException('Custom field not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.customFieldValueModel().query(trx).where('custom_field_id', fieldId).delete();
|
||||||
|
await this.customFieldModel().query(trx).findById(fieldId).delete();
|
||||||
|
|
||||||
|
this.eventEmitter.emit(events.customField.onDeleted, { customField });
|
||||||
|
|
||||||
|
return customField;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { EditCustomFieldDto } from '../dtos/CustomField.dto';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EditCustomFieldService {
|
||||||
|
constructor(
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async editCustomField(fieldId: number, dto: EditCustomFieldDto) {
|
||||||
|
return this.uow.withTransaction(async (trx) => {
|
||||||
|
const oldCustomField = await this.customFieldModel().query(trx).findById(fieldId);
|
||||||
|
if (!oldCustomField) {
|
||||||
|
throw new NotFoundException('Custom field not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customField = await this.customFieldModel()
|
||||||
|
.query(trx)
|
||||||
|
.patchAndFetchById(fieldId, {
|
||||||
|
resourceName: dto.resourceName,
|
||||||
|
fieldName: dto.fieldName,
|
||||||
|
label: dto.label,
|
||||||
|
fieldType: dto.fieldType,
|
||||||
|
options: dto.options,
|
||||||
|
required: dto.required,
|
||||||
|
order: dto.order,
|
||||||
|
active: dto.active,
|
||||||
|
defaultValue: dto.defaultValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(events.customField.onEdited, { customField, oldCustomField });
|
||||||
|
|
||||||
|
return customField;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { ReorderCustomFieldsDto } from '../dtos/ReorderCustomFields.dto';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReorderCustomFieldsService {
|
||||||
|
constructor(
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async reorderCustomFields(dto: ReorderCustomFieldsDto) {
|
||||||
|
return this.uow.withTransaction(async (trx) => {
|
||||||
|
for (let i = 0; i < dto.fieldIds.length; i++) {
|
||||||
|
await this.customFieldModel()
|
||||||
|
.query(trx)
|
||||||
|
.findById(dto.fieldIds[i])
|
||||||
|
.patch({ order: i + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.customFieldModel()
|
||||||
|
.query(trx)
|
||||||
|
.where('resource_name', dto.resourceName)
|
||||||
|
.orderBy('order', 'ASC');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { UpdateFieldStatusDto } from '../dtos/UpdateFieldStatus.dto';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UpdateFieldStatusService {
|
||||||
|
constructor(
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async updateFieldStatus(fieldId: number, dto: UpdateFieldStatusDto) {
|
||||||
|
return this.uow.withTransaction(async (trx) => {
|
||||||
|
const customField = await this.customFieldModel().query(trx).findById(fieldId);
|
||||||
|
if (!customField) {
|
||||||
|
throw new NotFoundException('Custom field not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.customFieldModel()
|
||||||
|
.query(trx)
|
||||||
|
.patchAndFetchById(fieldId, {
|
||||||
|
active: dto.active,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CommandCustomFieldDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ description: 'Resource name the custom field belongs to.', example: 'SaleInvoice' })
|
||||||
|
resourceName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ description: 'Internal field name.', example: 'cf_priority' })
|
||||||
|
fieldName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ description: 'Display label.', example: 'Priority' })
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ description: 'Field type.', example: 'dropdown' })
|
||||||
|
fieldType: string;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: 'Field options (e.g., dropdown choices).', example: { choices: ['Low', 'Medium', 'High'] } })
|
||||||
|
options?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value ?? false)
|
||||||
|
@ApiProperty({ description: 'Whether the field is required.', example: false })
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value ?? 0)
|
||||||
|
@ApiProperty({ description: 'Display order.', example: 1 })
|
||||||
|
order?: number;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value ?? true)
|
||||||
|
@ApiProperty({ description: 'Whether the field is active.', example: true })
|
||||||
|
active?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: 'Default value.', example: 'Medium' })
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateCustomFieldDto extends CommandCustomFieldDto {}
|
||||||
|
export class EditCustomFieldDto extends CommandCustomFieldDto {}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CustomFieldResponseDto {
|
||||||
|
@ApiProperty({ description: 'Custom field ID.', example: 1 })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Resource name.', example: 'SaleInvoice' })
|
||||||
|
resourceName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Field name.', example: 'cf_priority' })
|
||||||
|
fieldName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Display label.', example: 'Priority' })
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Field type.', example: 'dropdown' })
|
||||||
|
fieldType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Field options.', example: { choices: ['Low', 'Medium', 'High'] } })
|
||||||
|
options?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Whether the field is required.', example: false })
|
||||||
|
required: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Display order.', example: 1 })
|
||||||
|
order: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Whether the field is active.', example: true })
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Default value.', example: 'Medium' })
|
||||||
|
defaultValue?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Created at timestamp.' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Updated at timestamp.' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class ReorderCustomFieldsDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ description: 'Resource name.', example: 'SaleInvoice' })
|
||||||
|
resourceName: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ description: 'Ordered array of custom field IDs.', example: [3, 1, 2] })
|
||||||
|
fieldIds: number[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateFieldStatusDto {
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ description: 'New active status.', example: true })
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
|
||||||
|
export class CustomField extends TenantBaseModel {
|
||||||
|
public resourceName!: string;
|
||||||
|
public fieldName!: string;
|
||||||
|
public label!: string;
|
||||||
|
public fieldType!: string;
|
||||||
|
public options?: Record<string, any>;
|
||||||
|
public required!: boolean;
|
||||||
|
public order!: number;
|
||||||
|
public active!: boolean;
|
||||||
|
public defaultValue?: string;
|
||||||
|
|
||||||
|
static get tableName() {
|
||||||
|
return 'custom_fields';
|
||||||
|
}
|
||||||
|
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
active(query) {
|
||||||
|
query.where('active', true);
|
||||||
|
},
|
||||||
|
resource(query, resourceName: string) {
|
||||||
|
query.where('resource_name', resourceName);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get relationMappings() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
|
||||||
|
export class CustomFieldValue extends TenantBaseModel {
|
||||||
|
public customFieldId!: number;
|
||||||
|
public resourceType!: string;
|
||||||
|
public resourceId!: number;
|
||||||
|
public value?: string;
|
||||||
|
|
||||||
|
static get tableName() {
|
||||||
|
return 'custom_field_values';
|
||||||
|
}
|
||||||
|
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
resource(query, resourceType: string, resourceId: number) {
|
||||||
|
query.where('resource_type', resourceType).where('resource_id', resourceId);
|
||||||
|
},
|
||||||
|
customField(query, customFieldId: number) {
|
||||||
|
query.where('custom_field_id', customFieldId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get relationMappings() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetCustomFieldService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getCustomField(fieldId: number) {
|
||||||
|
const customField = await this.customFieldModel().query().findById(fieldId);
|
||||||
|
if (!customField) {
|
||||||
|
throw new NotFoundException('Custom field not found.');
|
||||||
|
}
|
||||||
|
return customField;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetCustomFieldsService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getCustomFields(resourceName?: string) {
|
||||||
|
let query = this.customFieldModel()
|
||||||
|
.query()
|
||||||
|
.orderBy('order', 'ASC');
|
||||||
|
|
||||||
|
if (resourceName) {
|
||||||
|
query = query.where('resource_name', resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
import { CustomFieldValue } from '../models/CustomFieldValue';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetResourceCustomFieldsService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
@Inject(CustomFieldValue.name)
|
||||||
|
private readonly customFieldValueModel: TenantModelProxy<typeof CustomFieldValue>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getResourceCustomFields(resourceType: string, resourceId: number) {
|
||||||
|
const customFields = await this.customFieldModel()
|
||||||
|
.query()
|
||||||
|
.where('resource_name', resourceType)
|
||||||
|
.where('active', true)
|
||||||
|
.orderBy('order', 'ASC');
|
||||||
|
|
||||||
|
const values = await this.customFieldValueModel()
|
||||||
|
.query()
|
||||||
|
.where('resource_type', resourceType)
|
||||||
|
.where('resource_id', resourceId);
|
||||||
|
|
||||||
|
const valuesMap = new Map(values.map((v) => [v.customFieldId, v.value]));
|
||||||
|
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (const field of customFields) {
|
||||||
|
const value = valuesMap.get(field.id);
|
||||||
|
result[field.fieldName] = value !== undefined ? value : field.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResourceCustomFieldsBulk(resourceType: string, resourceIds: number[]) {
|
||||||
|
const customFields = await this.customFieldModel()
|
||||||
|
.query()
|
||||||
|
.where('resource_name', resourceType)
|
||||||
|
.where('active', true)
|
||||||
|
.orderBy('order', 'ASC');
|
||||||
|
|
||||||
|
const values = await this.customFieldValueModel()
|
||||||
|
.query()
|
||||||
|
.where('resource_type', resourceType)
|
||||||
|
.whereIn('resource_id', resourceIds);
|
||||||
|
|
||||||
|
const valuesMap = new Map(
|
||||||
|
values.map((v) => [`${v.customFieldId}:${v.resourceId}`, v.value]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result: Record<number, Record<string, any>> = {};
|
||||||
|
for (const resourceId of resourceIds) {
|
||||||
|
const resourceValues: Record<string, any> = {};
|
||||||
|
for (const field of customFields) {
|
||||||
|
const value = valuesMap.get(`${field.id}:${resourceId}`);
|
||||||
|
resourceValues[field.fieldName] = value !== undefined ? value : field.defaultValue;
|
||||||
|
}
|
||||||
|
result[resourceId] = resourceValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { CustomField } from '../models/CustomField';
|
||||||
|
import { CustomFieldValue } from '../models/CustomFieldValue';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SaveCustomFieldValuesService {
|
||||||
|
constructor(
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
@Inject(CustomField.name)
|
||||||
|
private readonly customFieldModel: TenantModelProxy<typeof CustomField>,
|
||||||
|
@Inject(CustomFieldValue.name)
|
||||||
|
private readonly customFieldValueModel: TenantModelProxy<typeof CustomFieldValue>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async saveValues(
|
||||||
|
resourceType: string,
|
||||||
|
resourceId: number,
|
||||||
|
customFields: Record<string, any>,
|
||||||
|
trx?: any,
|
||||||
|
) {
|
||||||
|
const execute = async (transaction) => {
|
||||||
|
const fieldDefinitions = await this.customFieldModel()
|
||||||
|
.query(transaction)
|
||||||
|
.where('resource_name', resourceType)
|
||||||
|
.where('active', true);
|
||||||
|
|
||||||
|
const fieldMap = new Map(fieldDefinitions.map((f) => [f.fieldName, f]));
|
||||||
|
|
||||||
|
for (const [fieldName, value] of Object.entries(customFields)) {
|
||||||
|
const fieldDef = fieldMap.get(fieldName);
|
||||||
|
if (!fieldDef) continue;
|
||||||
|
|
||||||
|
const existing = await this.customFieldValueModel()
|
||||||
|
.query(transaction)
|
||||||
|
.where('custom_field_id', fieldDef.id)
|
||||||
|
.where('resource_type', resourceType)
|
||||||
|
.where('resource_id', resourceId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await this.customFieldValueModel()
|
||||||
|
.query(transaction)
|
||||||
|
.findById(existing.id)
|
||||||
|
.patch({ value: value != null ? String(value) : null });
|
||||||
|
} else {
|
||||||
|
await this.customFieldValueModel()
|
||||||
|
.query(transaction)
|
||||||
|
.insert({
|
||||||
|
customFieldId: fieldDef.id,
|
||||||
|
resourceType,
|
||||||
|
resourceId,
|
||||||
|
value: value != null ? String(value) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trx) {
|
||||||
|
return execute(trx);
|
||||||
|
}
|
||||||
|
return this.uow.withTransaction(execute);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteValues(resourceType: string, resourceId: number, trx?: any) {
|
||||||
|
const execute = async (transaction) => {
|
||||||
|
await this.customFieldValueModel()
|
||||||
|
.query(transaction)
|
||||||
|
.where('resource_type', resourceType)
|
||||||
|
.where('resource_id', resourceId)
|
||||||
|
.delete();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trx) {
|
||||||
|
return execute(trx);
|
||||||
|
}
|
||||||
|
return this.uow.withTransaction(execute);
|
||||||
|
}
|
||||||
|
}
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { SaveCustomFieldValuesService } from '../queries/SaveCustomFieldValues.service';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CustomFieldsEntityEventsSubscriber {
|
||||||
|
constructor(
|
||||||
|
private readonly saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// --- Sale Invoices ---
|
||||||
|
@OnEvent(events.saleInvoice.onCreated)
|
||||||
|
async handleSaleInvoiceCreated(payload: any) {
|
||||||
|
if (payload.saleInvoiceDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'SaleInvoice',
|
||||||
|
payload.saleInvoiceId,
|
||||||
|
payload.saleInvoiceDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(events.saleInvoice.onEdited)
|
||||||
|
async handleSaleInvoiceEdited(payload: any) {
|
||||||
|
if (payload.saleInvoiceDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'SaleInvoice',
|
||||||
|
payload.saleInvoiceId,
|
||||||
|
payload.saleInvoiceDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sale Estimates ---
|
||||||
|
@OnEvent(events.saleEstimate.onCreated)
|
||||||
|
async handleSaleEstimateCreated(payload: any) {
|
||||||
|
if (payload.saleEstimateDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'SaleEstimate',
|
||||||
|
payload.saleEstimateId,
|
||||||
|
payload.saleEstimateDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(events.saleEstimate.onEdited)
|
||||||
|
async handleSaleEstimateEdited(payload: any) {
|
||||||
|
if (payload.estimateDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'SaleEstimate',
|
||||||
|
payload.estimateId,
|
||||||
|
payload.estimateDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sale Receipts ---
|
||||||
|
@OnEvent(events.saleReceipt.onCreated)
|
||||||
|
async handleSaleReceiptCreated(payload: any) {
|
||||||
|
if (payload.saleReceiptDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'SaleReceipt',
|
||||||
|
payload.saleReceiptId,
|
||||||
|
payload.saleReceiptDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(events.saleReceipt.onEdited)
|
||||||
|
async handleSaleReceiptEdited(payload: any) {
|
||||||
|
if (payload.saleReceiptDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'SaleReceipt',
|
||||||
|
payload.saleReceipt.id,
|
||||||
|
payload.saleReceiptDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Customers ---
|
||||||
|
@OnEvent(events.customers.onCreated)
|
||||||
|
async handleCustomerCreated(payload: any) {
|
||||||
|
if (payload.customerDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Customer',
|
||||||
|
payload.customerId,
|
||||||
|
payload.customerDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(events.customers.onEdited)
|
||||||
|
async handleCustomerEdited(payload: any) {
|
||||||
|
if (payload.customerDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Customer',
|
||||||
|
payload.customerId,
|
||||||
|
payload.customerDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Items ---
|
||||||
|
@OnEvent(events.item.onCreated)
|
||||||
|
async handleItemCreated(payload: any) {
|
||||||
|
if (payload.itemDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Item',
|
||||||
|
payload.itemId,
|
||||||
|
payload.itemDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(events.item.onEdited)
|
||||||
|
async handleItemEdited(payload: any) {
|
||||||
|
if (payload.itemDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Item',
|
||||||
|
payload.itemId,
|
||||||
|
payload.itemDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Credit Notes ---
|
||||||
|
@OnEvent(events.creditNote.onCreated)
|
||||||
|
async handleCreditNoteCreated(payload: any) {
|
||||||
|
if (payload.creditNoteDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'CreditNote',
|
||||||
|
payload.creditNote.id,
|
||||||
|
payload.creditNoteDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(events.creditNote.onEdited)
|
||||||
|
async handleCreditNoteEdited(payload: any) {
|
||||||
|
if (payload.creditNoteEditDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'CreditNote',
|
||||||
|
payload.creditNoteId,
|
||||||
|
payload.creditNoteEditDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Payment Received ---
|
||||||
|
@OnEvent(events.paymentReceive.onCreated)
|
||||||
|
async handlePaymentReceiveCreated(payload: any) {
|
||||||
|
if (payload.paymentReceiveDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'PaymentReceive',
|
||||||
|
payload.paymentReceiveId,
|
||||||
|
payload.paymentReceiveDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(events.paymentReceive.onEdited)
|
||||||
|
async handlePaymentReceiveEdited(payload: any) {
|
||||||
|
if (payload.paymentReceiveDTO?.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'PaymentReceive',
|
||||||
|
payload.paymentReceiveId,
|
||||||
|
payload.paymentReceiveDTO.customFields,
|
||||||
|
payload.trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum CustomFieldAction {
|
||||||
|
CREATE = 'Create',
|
||||||
|
EDIT = 'Edit',
|
||||||
|
DELETE = 'Delete',
|
||||||
|
VIEW = 'View',
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service';
|
|||||||
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
|
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
|
||||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
import { CustomerGLEntries } from './CustomerGLEntries';
|
import { CustomerGLEntries } from './CustomerGLEntries';
|
||||||
import { CustomerGLEntriesStorage } from './CustomerGLEntriesStorage';
|
import { CustomerGLEntriesStorage } from './CustomerGLEntriesStorage';
|
||||||
import { CustomerWriteGLOpeningBalanceSubscriber } from './subscribers/CustomerGLEntriesSubscriber';
|
import { CustomerWriteGLOpeningBalanceSubscriber } from './subscribers/CustomerGLEntriesSubscriber';
|
||||||
@@ -30,6 +31,7 @@ import { CustomerWriteGLOpeningBalanceSubscriber } from './subscribers/CustomerG
|
|||||||
DynamicListModule,
|
DynamicListModule,
|
||||||
LedgerModule,
|
LedgerModule,
|
||||||
AccountsModule,
|
AccountsModule,
|
||||||
|
CustomFieldsModule,
|
||||||
],
|
],
|
||||||
controllers: [CustomersController],
|
controllers: [CustomersController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../types/Customers.types';
|
} from '../types/Customers.types';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { CreateCustomerDto } from '../dtos/CreateCustomer.dto';
|
import { CreateCustomerDto } from '../dtos/CreateCustomer.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateCustomer {
|
export class CreateCustomer {
|
||||||
@@ -24,6 +25,7 @@ export class CreateCustomer {
|
|||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly eventPublisher: EventEmitter2,
|
private readonly eventPublisher: EventEmitter2,
|
||||||
private readonly customerDTO: CreateEditCustomerDTO,
|
private readonly customerDTO: CreateEditCustomerDTO,
|
||||||
|
private readonly saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(Customer.name)
|
@Inject(Customer.name)
|
||||||
private readonly customerModel: TenantModelProxy<typeof Customer>,
|
private readonly customerModel: TenantModelProxy<typeof Customer>,
|
||||||
@@ -55,6 +57,17 @@ export class CreateCustomer {
|
|||||||
.insertAndFetch({
|
.insertAndFetch({
|
||||||
...customerObj,
|
...customerObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (customerDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Customer',
|
||||||
|
customer.id,
|
||||||
|
customerDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onCustomerCreated` event.
|
// Triggers `onCustomerCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.customers.onCreated, {
|
await this.eventPublisher.emitAsync(events.customers.onCreated, {
|
||||||
customer,
|
customer,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
|||||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { EditCustomerDto } from '../dtos/EditCustomer.dto';
|
import { EditCustomerDto } from '../dtos/EditCustomer.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EditCustomer {
|
export class EditCustomer {
|
||||||
@@ -25,6 +26,7 @@ export class EditCustomer {
|
|||||||
private uow: UnitOfWork,
|
private uow: UnitOfWork,
|
||||||
private eventPublisher: EventEmitter2,
|
private eventPublisher: EventEmitter2,
|
||||||
private customerDTO: CreateEditCustomerDTO,
|
private customerDTO: CreateEditCustomerDTO,
|
||||||
|
private saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(Customer.name)
|
@Inject(Customer.name)
|
||||||
private customerModel: TenantModelProxy<typeof Customer>,
|
private customerModel: TenantModelProxy<typeof Customer>,
|
||||||
@@ -64,6 +66,17 @@ export class EditCustomer {
|
|||||||
.updateAndFetchById(customerId, {
|
.updateAndFetchById(customerId, {
|
||||||
...customerObj,
|
...customerObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (customerDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Customer',
|
||||||
|
customerId,
|
||||||
|
customerDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onCustomerEdited` event.
|
// Triggers `onCustomerEdited` event.
|
||||||
await this.eventPublisher.emitAsync(events.customers.onEdited, {
|
await this.eventPublisher.emitAsync(events.customers.onEdited, {
|
||||||
customerId,
|
customerId,
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import {
|
|||||||
IsEmail,
|
IsEmail,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
|
import { ToNumber } from '@/common/decorators/Validators';
|
||||||
import { ContactAddressDto } from './ContactAddress.dto';
|
import { ContactAddressDto } from './ContactAddress.dto';
|
||||||
|
|
||||||
export class CreateCustomerDto extends ContactAddressDto {
|
export class CreateCustomerDto extends ContactAddressDto {
|
||||||
@@ -164,4 +166,13 @@ export class CreateCustomerDto extends ContactAddressDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
description: 'Custom fields values',
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,4 +115,11 @@ export class CustomerResponseDto {
|
|||||||
|
|
||||||
@ApiProperty({ example: 1500.0 })
|
@ApiProperty({ example: 1500.0 })
|
||||||
closingBalance: number;
|
closingBalance: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
description: 'Custom fields values',
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,4 +68,12 @@ export class EditCustomerDto extends ContactAddressDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
description: 'Custom fields values',
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,18 @@ export class CustomerTransfromer extends ContactTransfromer {
|
|||||||
'formattedOpeningBalanceAt',
|
'formattedOpeningBalanceAt',
|
||||||
'customerType',
|
'customerType',
|
||||||
'formattedCustomerType',
|
'formattedCustomerType',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve custom fields from options.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public customFields(): Record<string, any> {
|
||||||
|
return this.options?.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve customer type.
|
* Retrieve customer type.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { CustomerTransfromer } from './CustomerTransformer';
|
|||||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||||
import { Customer } from '../models/Customer';
|
import { Customer } from '../models/Customer';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetCustomerService {
|
export class GetCustomerService {
|
||||||
constructor(
|
constructor(
|
||||||
private transformer: TransformerInjectable,
|
private transformer: TransformerInjectable,
|
||||||
|
private getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
|
|
||||||
@Inject(Customer.name)
|
@Inject(Customer.name)
|
||||||
private customerModel: TenantModelProxy<typeof Customer>,
|
private customerModel: TenantModelProxy<typeof Customer>,
|
||||||
@@ -24,7 +26,19 @@ export class GetCustomerService {
|
|||||||
.findById(customerId)
|
.findById(customerId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Load custom field values.
|
||||||
|
const customFields = await this.getResourceCustomFieldsService.getResourceCustomFields(
|
||||||
|
'Customer',
|
||||||
|
customerId,
|
||||||
|
);
|
||||||
|
|
||||||
// Retrieves the transformered customers.
|
// Retrieves the transformered customers.
|
||||||
return this.transformer.transform(customer, new CustomerTransfromer());
|
const transformed = await this.transformer.transform(
|
||||||
|
customer,
|
||||||
|
new CustomerTransfromer(),
|
||||||
|
{ customFields },
|
||||||
|
);
|
||||||
|
|
||||||
|
return transformed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class ImportFileCommon {
|
|||||||
parsedData: Record<string, any>[],
|
parsedData: Record<string, any>[],
|
||||||
trx?: Knex.Transaction,
|
trx?: Knex.Transaction,
|
||||||
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
||||||
const resourceFields = this.resource.getResourceFields2(
|
const resourceFields = await this.resource.getResourceFields2(
|
||||||
importFile.resource,
|
importFile.resource,
|
||||||
);
|
);
|
||||||
const importable = await this.importableRegistry.getImportable(
|
const importable = await this.importableRegistry.getImportable(
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class ImportFileMapping {
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Invalidate the from/to map attributes.
|
// Invalidate the from/to map attributes.
|
||||||
this.validateMapsAttrs(importFile, maps);
|
await this.validateMapsAttrs(importFile, maps);
|
||||||
|
|
||||||
// @todo validate the required fields.
|
// @todo validate the required fields.
|
||||||
|
|
||||||
@@ -63,8 +63,8 @@ export class ImportFileMapping {
|
|||||||
* @param {ImportMappingAttr[]} maps
|
* @param {ImportMappingAttr[]} maps
|
||||||
* @throws {ServiceError(ERRORS.INVALID_MAP_ATTRS)}
|
* @throws {ServiceError(ERRORS.INVALID_MAP_ATTRS)}
|
||||||
*/
|
*/
|
||||||
private validateMapsAttrs(importFile: any, maps: ImportMappingAttr[]) {
|
private async validateMapsAttrs(importFile: any, maps: ImportMappingAttr[]) {
|
||||||
const fields = this.resource.getResourceFields2(importFile.resource);
|
const fields = await this.resource.getResourceFields2(importFile.resource);
|
||||||
const columnsMap = fromPairs(
|
const columnsMap = fromPairs(
|
||||||
importFile.columnsParsed.map((field) => [field, '']),
|
importFile.columnsParsed.map((field) => [field, '']),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class ImportFileProcess {
|
|||||||
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||||
|
|
||||||
const resource = importFile.resource;
|
const resource = importFile.resource;
|
||||||
const resourceFields = this.resource.getResourceFields2(resource);
|
const resourceFields = await this.resource.getResourceFields2(resource);
|
||||||
|
|
||||||
// Runs the importing operation with ability to return errors that will happen.
|
// Runs the importing operation with ability to return errors that will happen.
|
||||||
const [successedImport, failedImport, allData] =
|
const [successedImport, failedImport, allData] =
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export class ImportFileUploadService {
|
|||||||
params: paramsStringified,
|
params: paramsStringified,
|
||||||
});
|
});
|
||||||
const resourceColumnsMap =
|
const resourceColumnsMap =
|
||||||
this.resourceService.getResourceFields2(resource);
|
await this.resourceService.getResourceFields2(resource);
|
||||||
const resourceColumns = getResourceColumns(resourceColumnsMap);
|
const resourceColumns = getResourceColumns(resourceColumnsMap);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Item } from './models/Item';
|
|||||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||||
import { CreateItemDto } from './dtos/Item.dto';
|
import { CreateItemDto } from './dtos/Item.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable({ scope: Scope.REQUEST })
|
@Injectable({ scope: Scope.REQUEST })
|
||||||
export class CreateItemService {
|
export class CreateItemService {
|
||||||
@@ -23,6 +24,7 @@ export class CreateItemService {
|
|||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly validators: ItemsValidators,
|
private readonly validators: ItemsValidators,
|
||||||
|
private readonly saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(Item.name)
|
@Inject(Item.name)
|
||||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||||
@@ -110,10 +112,22 @@ export class CreateItemService {
|
|||||||
.insertAndFetch({
|
.insertAndFetch({
|
||||||
...itemInsert,
|
...itemInsert,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (itemDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Item',
|
||||||
|
item.id,
|
||||||
|
itemDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onItemCreated` event.
|
// Triggers `onItemCreated` event.
|
||||||
await this.eventEmitter.emitAsync(events.item.onCreated, {
|
await this.eventEmitter.emitAsync(events.item.onCreated, {
|
||||||
item,
|
item,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
|
itemDTO,
|
||||||
trx,
|
trx,
|
||||||
} as IItemEventCreatedPayload);
|
} as IItemEventCreatedPayload);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Item } from './models/Item';
|
|||||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||||
import { EditItemDto } from './dtos/Item.dto';
|
import { EditItemDto } from './dtos/Item.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EditItemService {
|
export class EditItemService {
|
||||||
@@ -22,6 +23,7 @@ export class EditItemService {
|
|||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly validators: ItemsValidators,
|
private readonly validators: ItemsValidators,
|
||||||
|
private readonly saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(Item.name)
|
@Inject(Item.name)
|
||||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||||
@@ -130,11 +132,22 @@ export class EditItemService {
|
|||||||
.query(trx)
|
.query(trx)
|
||||||
.patchAndFetchById(itemId, itemModel);
|
.patchAndFetchById(itemId, itemModel);
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (itemDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'Item',
|
||||||
|
itemId,
|
||||||
|
itemDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Edit event payload.
|
// Edit event payload.
|
||||||
const eventPayload: IItemEventEditedPayload = {
|
const eventPayload: IItemEventEditedPayload = {
|
||||||
item: newItem,
|
item: newItem,
|
||||||
oldItem,
|
oldItem,
|
||||||
itemId: newItem.id,
|
itemId: newItem.id,
|
||||||
|
itemDTO,
|
||||||
trx,
|
trx,
|
||||||
};
|
};
|
||||||
// Triggers `onItemEdited` event.
|
// Triggers `onItemEdited` event.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { TransformerInjectable } from '../Transformer/TransformerInjectable.serv
|
|||||||
import { ItemTransformer } from './Item.transformer';
|
import { ItemTransformer } from './Item.transformer';
|
||||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetItemService {
|
export class GetItemService {
|
||||||
@@ -15,6 +16,7 @@ export class GetItemService {
|
|||||||
private readonly eventEmitter2: EventEmitter2,
|
private readonly eventEmitter2: EventEmitter2,
|
||||||
private readonly transformerInjectable: TransformerInjectable,
|
private readonly transformerInjectable: TransformerInjectable,
|
||||||
private readonly clsService: ClsService,
|
private readonly clsService: ClsService,
|
||||||
|
private readonly getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,10 +37,18 @@ export class GetItemService {
|
|||||||
.withGraphFetched('purchaseTaxRate')
|
.withGraphFetched('purchaseTaxRate')
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Load custom field values.
|
||||||
|
const customFields = await this.getResourceCustomFieldsService.getResourceCustomFields(
|
||||||
|
'Item',
|
||||||
|
itemId,
|
||||||
|
);
|
||||||
|
|
||||||
const transformed = await this.transformerInjectable.transform(
|
const transformed = await this.transformerInjectable.transform(
|
||||||
item,
|
item,
|
||||||
new ItemTransformer(),
|
new ItemTransformer(),
|
||||||
|
{ customFields },
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventPayload = { itemId };
|
const eventPayload = { itemId };
|
||||||
|
|
||||||
// Triggers the `onItemViewed` event.
|
// Triggers the `onItemViewed` event.
|
||||||
|
|||||||
@@ -13,9 +13,18 @@ export class ItemTransformer extends Transformer {
|
|||||||
'sellPriceFormatted',
|
'sellPriceFormatted',
|
||||||
'costPriceFormatted',
|
'costPriceFormatted',
|
||||||
'itemWarehouses',
|
'itemWarehouses',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve custom fields from options.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public customFields(): Record<string, any> {
|
||||||
|
return this.options?.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatted item type.
|
* Formatted item type.
|
||||||
* @param {IItem} item
|
* @param {IItem} item
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ import { ItemsExportable } from './ItemsExportable.service';
|
|||||||
import { ItemsImportable } from './ItemsImportable.service';
|
import { ItemsImportable } from './ItemsImportable.service';
|
||||||
import { BulkDeleteItemsService } from './BulkDeleteItems.service';
|
import { BulkDeleteItemsService } from './BulkDeleteItems.service';
|
||||||
import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service';
|
import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TenancyDatabaseModule,
|
TenancyDatabaseModule,
|
||||||
DynamicListModule,
|
DynamicListModule,
|
||||||
InventoryAdjustmentsModule,
|
InventoryAdjustmentsModule,
|
||||||
|
CustomFieldsModule,
|
||||||
],
|
],
|
||||||
controllers: [ItemsController],
|
controllers: [ItemsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import {
|
|||||||
MaxLength,
|
MaxLength,
|
||||||
Min,
|
Min,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
|
import { ToNumber } from '@/common/decorators/Validators';
|
||||||
|
|
||||||
export class CommandItemDto {
|
export class CommandItemDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -209,6 +211,15 @@ export class CommandItemDto {
|
|||||||
example: [1, 2, 3],
|
example: [1, 2, 3],
|
||||||
})
|
})
|
||||||
mediaIds?: number[];
|
mediaIds?: number[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateItemDto extends CommandItemDto {}
|
export class CreateItemDto extends CommandItemDto {}
|
||||||
|
|||||||
@@ -240,4 +240,11 @@ export class ItemResponseDto {
|
|||||||
example: '2024-03-20T10:00:00Z',
|
example: '2024-03-20T10:00:00Z',
|
||||||
})
|
})
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { GetPaymentReceivedMailTemplate } from './queries/GetPaymentReceivedMail
|
|||||||
import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailState.service';
|
import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailState.service';
|
||||||
import { BulkDeletePaymentReceivedService } from './BulkDeletePaymentReceived.service';
|
import { BulkDeletePaymentReceivedService } from './BulkDeletePaymentReceived.service';
|
||||||
import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePaymentReceived.service';
|
import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePaymentReceived.service';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [PaymentReceivesController],
|
controllers: [PaymentReceivesController],
|
||||||
@@ -95,6 +96,7 @@ import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePa
|
|||||||
AccountsModule,
|
AccountsModule,
|
||||||
MailNotificationModule,
|
MailNotificationModule,
|
||||||
DynamicListModule,
|
DynamicListModule,
|
||||||
|
CustomFieldsModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }),
|
BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }),
|
||||||
BullBoardModule.forFeature({
|
BullBoardModule.forFeature({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { CreatePaymentReceivedDto } from '../dtos/PaymentReceived.dto';
|
import { CreatePaymentReceivedDto } from '../dtos/PaymentReceived.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreatePaymentReceivedService {
|
export class CreatePaymentReceivedService {
|
||||||
@@ -24,6 +25,7 @@ export class CreatePaymentReceivedService {
|
|||||||
private uow: UnitOfWork,
|
private uow: UnitOfWork,
|
||||||
private transformer: PaymentReceiveDTOTransformer,
|
private transformer: PaymentReceiveDTOTransformer,
|
||||||
private tenancyContext: TenancyContext,
|
private tenancyContext: TenancyContext,
|
||||||
|
private saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(PaymentReceived.name)
|
@Inject(PaymentReceived.name)
|
||||||
private paymentReceived: TenantModelProxy<typeof PaymentReceived>,
|
private paymentReceived: TenantModelProxy<typeof PaymentReceived>,
|
||||||
@@ -92,6 +94,17 @@ export class CreatePaymentReceivedService {
|
|||||||
.insertGraphAndFetch({
|
.insertGraphAndFetch({
|
||||||
...paymentReceiveObj,
|
...paymentReceiveObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (paymentReceiveDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'PaymentReceive',
|
||||||
|
paymentReceive.id,
|
||||||
|
paymentReceiveDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onPaymentReceiveCreated` event.
|
// Triggers `onPaymentReceiveCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, {
|
await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, {
|
||||||
paymentReceive,
|
paymentReceive,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Customer } from '@/modules/Customers/models/Customer';
|
|||||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { EditPaymentReceivedDto } from '../dtos/PaymentReceived.dto';
|
import { EditPaymentReceivedDto } from '../dtos/PaymentReceived.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EditPaymentReceivedService {
|
export class EditPaymentReceivedService {
|
||||||
@@ -24,6 +25,7 @@ export class EditPaymentReceivedService {
|
|||||||
private readonly eventPublisher: EventEmitter2,
|
private readonly eventPublisher: EventEmitter2,
|
||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly tenancyContext: TenancyContext,
|
private readonly tenancyContext: TenancyContext,
|
||||||
|
private readonly saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(PaymentReceived.name)
|
@Inject(PaymentReceived.name)
|
||||||
private readonly paymentReceiveModel: TenantModelProxy<
|
private readonly paymentReceiveModel: TenantModelProxy<
|
||||||
@@ -130,6 +132,17 @@ export class EditPaymentReceivedService {
|
|||||||
id: paymentReceiveId,
|
id: paymentReceiveId,
|
||||||
...paymentReceiveObj,
|
...paymentReceiveObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (paymentReceiveDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'PaymentReceive',
|
||||||
|
paymentReceiveId,
|
||||||
|
paymentReceiveDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onPaymentReceiveEdited` event.
|
// Triggers `onPaymentReceiveEdited` event.
|
||||||
await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, {
|
await this.eventPublisher.emitAsync(events.paymentReceive.onEdited, {
|
||||||
paymentReceiveId,
|
paymentReceiveId,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
IsArray,
|
IsArray,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsInt,
|
IsInt,
|
||||||
|
IsObject,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { ToNumber } from '@/common/decorators/Validators';
|
import { ToNumber } from '@/common/decorators/Validators';
|
||||||
@@ -138,6 +139,15 @@ export class CommandPaymentReceivedDto {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
attachments?: AttachmentLinkDto[];
|
attachments?: AttachmentLinkDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreatePaymentReceivedDto extends CommandPaymentReceivedDto {}
|
export class CreatePaymentReceivedDto extends CommandPaymentReceivedDto {}
|
||||||
|
|||||||
@@ -199,4 +199,11 @@ export class PaymentReceivedResponseDto {
|
|||||||
})
|
})
|
||||||
@Type(() => AttachmentLinkDto)
|
@Type(() => AttachmentLinkDto)
|
||||||
attachments?: AttachmentLinkDto[];
|
attachments?: AttachmentLinkDto[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { PaymentReceived } from '../models/PaymentReceived';
|
|||||||
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
|
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
|
||||||
import { ServiceError } from '../../Items/ServiceError';
|
import { ServiceError } from '../../Items/ServiceError';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetPaymentReceivedService {
|
export class GetPaymentReceivedService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly transformer: TransformerInjectable,
|
private readonly transformer: TransformerInjectable,
|
||||||
|
private readonly getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
|
|
||||||
@Inject(PaymentReceived.name)
|
@Inject(PaymentReceived.name)
|
||||||
private readonly paymentReceiveModel: TenantModelProxy<
|
private readonly paymentReceiveModel: TenantModelProxy<
|
||||||
@@ -38,9 +40,18 @@ export class GetPaymentReceivedService {
|
|||||||
if (!paymentReceive) {
|
if (!paymentReceive) {
|
||||||
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
|
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
|
||||||
}
|
}
|
||||||
return this.transformer.transform(
|
// Load custom field values.
|
||||||
|
const customFields = await this.getResourceCustomFieldsService.getResourceCustomFields(
|
||||||
|
'PaymentReceive',
|
||||||
|
paymentReceiveId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transformed = await this.transformer.transform(
|
||||||
paymentReceive,
|
paymentReceive,
|
||||||
new PaymentReceiveTransfromer(),
|
new PaymentReceiveTransfromer(),
|
||||||
|
{ customFields },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return transformed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,18 @@ export class PaymentReceiveTransfromer extends Transformer {
|
|||||||
'formattedExchangeRate',
|
'formattedExchangeRate',
|
||||||
'entries',
|
'entries',
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve custom fields from options.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public customFields(): Record<string, any> {
|
||||||
|
return this.options?.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted payment receive date.
|
* Retrieve formatted payment receive date.
|
||||||
* @param {PaymentReceived} invoice
|
* @param {PaymentReceived} invoice
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
|||||||
import { AccountsExportable } from '../Accounts/AccountsExportable.service';
|
import { AccountsExportable } from '../Accounts/AccountsExportable.service';
|
||||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||||
import { ResourceController } from './Resource.controller';
|
import { ResourceController } from './Resource.controller';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BranchesModule, WarehousesModule, AccountsModule],
|
imports: [BranchesModule, WarehousesModule, AccountsModule, CustomFieldsModule],
|
||||||
providers: [ResourceService],
|
providers: [ResourceService],
|
||||||
exports: [ResourceService],
|
exports: [ResourceService],
|
||||||
controllers: [ResourceController]
|
controllers: [ResourceController]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { IModelMeta } from '@/interfaces/Model';
|
|||||||
import { IModelMetaField } from '@/interfaces/Model';
|
import { IModelMetaField } from '@/interfaces/Model';
|
||||||
import { Features } from '@/common/types/Features';
|
import { Features } from '@/common/types/Features';
|
||||||
import { resourceToModelName } from './_utils';
|
import { resourceToModelName } from './_utils';
|
||||||
|
import { GetCustomFieldsService } from '../CustomFields/queries/GetCustomFields.service';
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND',
|
RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND',
|
||||||
@@ -22,6 +23,7 @@ export class ResourceService {
|
|||||||
private readonly warehousesSettings: WarehousesSettings,
|
private readonly warehousesSettings: WarehousesSettings,
|
||||||
private readonly moduleRef: ModuleRef,
|
private readonly moduleRef: ModuleRef,
|
||||||
private readonly i18nService: I18nService,
|
private readonly i18nService: I18nService,
|
||||||
|
private readonly getCustomFieldsService: GetCustomFieldsService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,20 +137,69 @@ export class ResourceService {
|
|||||||
return mapValues(fields, (field) => this.localizeField(field));
|
return mapValues(fields, (field) => this.localizeField(field));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps model name to custom field resource name.
|
||||||
|
* @param {string} modelName - Model name.
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
|
private getCustomFieldResourceName(modelName: string): string | null {
|
||||||
|
const resourceMap: Record<string, string> = {
|
||||||
|
'SaleInvoice': 'SaleInvoice',
|
||||||
|
'SaleEstimate': 'SaleEstimate',
|
||||||
|
'SaleReceipt': 'SaleReceipt',
|
||||||
|
'Customer': 'Customer',
|
||||||
|
'Item': 'Item',
|
||||||
|
'CreditNote': 'CreditNote',
|
||||||
|
'PaymentReceive': 'PaymentReceive',
|
||||||
|
};
|
||||||
|
return resourceMap[modelName] || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the resource fields with localized names and hints.
|
* Retrieve the resource fields with localized names and hints.
|
||||||
* @param {string} modelName
|
* @param {string} modelName
|
||||||
* @returns {IModelMetaField2}
|
* @returns {IModelMetaField2}
|
||||||
*/
|
*/
|
||||||
public getResourceFields2(modelName: string): {
|
public async getResourceFields2(modelName: string): Promise<{
|
||||||
[key: string]: IModelMetaField2;
|
[key: string]: IModelMetaField2;
|
||||||
} {
|
}> {
|
||||||
const meta = this.getResourceMeta(modelName);
|
const meta = this.getResourceMeta(modelName);
|
||||||
const filteredFields = this.filterSupportFeatures(meta.fields2);
|
const filteredFields = this.filterSupportFeatures(meta.fields2);
|
||||||
|
|
||||||
return this.localizeFields(
|
const localizedFields = this.localizeFields(
|
||||||
filteredFields as Record<string, IModelMetaField2>,
|
filteredFields as Record<string, IModelMetaField2>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Inject custom fields from the database.
|
||||||
|
const customFieldResource = this.getCustomFieldResourceName(modelName);
|
||||||
|
if (customFieldResource) {
|
||||||
|
const customFields = await this.getCustomFieldsService.getCustomFields(customFieldResource);
|
||||||
|
for (const customField of customFields) {
|
||||||
|
const fieldTypeMap: Record<string, string> = {
|
||||||
|
'text': 'text',
|
||||||
|
'number': 'number',
|
||||||
|
'date': 'date',
|
||||||
|
'checkbox': 'boolean',
|
||||||
|
'dropdown': 'enumeration',
|
||||||
|
'url': 'url',
|
||||||
|
'textarea': 'text',
|
||||||
|
'autonumber': 'text',
|
||||||
|
'lookup': 'relation',
|
||||||
|
'formula': 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
localizedFields[customField.fieldName] = {
|
||||||
|
name: customField.label,
|
||||||
|
fieldType: fieldTypeMap[customField.fieldType] || 'text',
|
||||||
|
required: customField.required,
|
||||||
|
...(customField.fieldType === 'dropdown' && customField.options?.choices
|
||||||
|
? { options: customField.options.choices.map((choice: string) => ({ label: choice, key: choice })) }
|
||||||
|
: {}),
|
||||||
|
} as IModelMetaField2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localizedFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ export enum AbilitySubject {
|
|||||||
CreditNote = 'CreditNode',
|
CreditNote = 'CreditNode',
|
||||||
VendorCredit = 'VendorCredit',
|
VendorCredit = 'VendorCredit',
|
||||||
Project = 'Project',
|
Project = 'Project',
|
||||||
TaxRate = 'TaxRate'
|
TaxRate = 'TaxRate',
|
||||||
|
CustomField = 'CustomField',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRoleCreatedPayload {
|
export interface IRoleCreatedPayload {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { SaleEstimateAutoIncrementSubscriber } from './subscribers/SaleEstimateA
|
|||||||
import { BulkDeleteSaleEstimatesService } from './BulkDeleteSaleEstimates.service';
|
import { BulkDeleteSaleEstimatesService } from './BulkDeleteSaleEstimates.service';
|
||||||
import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSaleEstimates.service';
|
import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSaleEstimates.service';
|
||||||
import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.process';
|
import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.process';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -55,6 +56,7 @@ import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.pr
|
|||||||
ChromiumlyTenancyModule,
|
ChromiumlyTenancyModule,
|
||||||
TemplateInjectableModule,
|
TemplateInjectableModule,
|
||||||
PdfTemplatesModule,
|
PdfTemplatesModule,
|
||||||
|
CustomFieldsModule,
|
||||||
BullModule.registerQueue({ name: SendSaleEstimateMailQueue }),
|
BullModule.registerQueue({ name: SendSaleEstimateMailQueue }),
|
||||||
BullBoardModule.forFeature({
|
BullBoardModule.forFeature({
|
||||||
name: SendSaleEstimateMailQueue,
|
name: SendSaleEstimateMailQueue,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export class CreateSaleEstimate {
|
|||||||
.upsertGraphAndFetch({
|
.upsertGraphAndFetch({
|
||||||
...estimateObj,
|
...estimateObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Triggers `onSaleEstimateCreated` event.
|
// Triggers `onSaleEstimateCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
|
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
|
||||||
saleEstimate,
|
saleEstimate,
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import {
|
|||||||
IsEnum,
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Min,
|
Min,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
|
import { ToNumber } from '@/common/decorators/Validators';
|
||||||
|
|
||||||
enum DiscountType {
|
enum DiscountType {
|
||||||
Percentage = 'percentage',
|
Percentage = 'percentage',
|
||||||
@@ -176,6 +178,15 @@ export class CommandSaleEstimateDto {
|
|||||||
example: 1,
|
example: 1,
|
||||||
})
|
})
|
||||||
adjustment?: number;
|
adjustment?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateSaleEstimateDto extends CommandSaleEstimateDto { }
|
export class CreateSaleEstimateDto extends CommandSaleEstimateDto { }
|
||||||
|
|||||||
@@ -203,4 +203,11 @@ export class SaleEstimateResponseDto {
|
|||||||
})
|
})
|
||||||
@Type(() => AttachmentLinkDto)
|
@Type(() => AttachmentLinkDto)
|
||||||
attachments: AttachmentLinkDto[];
|
attachments: AttachmentLinkDto[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectab
|
|||||||
import { SaleEstimate } from '../models/SaleEstimate';
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetSaleEstimate {
|
export class GetSaleEstimate {
|
||||||
@@ -16,6 +17,7 @@ export class GetSaleEstimate {
|
|||||||
private readonly transformer: TransformerInjectable,
|
private readonly transformer: TransformerInjectable,
|
||||||
private readonly validators: SaleEstimateValidators,
|
private readonly validators: SaleEstimateValidators,
|
||||||
private readonly eventPublisher: EventEmitter2,
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,11 +37,19 @@ export class GetSaleEstimate {
|
|||||||
// Validates the estimate existance.
|
// Validates the estimate existance.
|
||||||
this.validators.validateEstimateExistance(estimate);
|
this.validators.validateEstimateExistance(estimate);
|
||||||
|
|
||||||
|
// Load custom field values.
|
||||||
|
const customFields = await this.getResourceCustomFieldsService.getResourceCustomFields(
|
||||||
|
'SaleEstimate',
|
||||||
|
estimateId,
|
||||||
|
);
|
||||||
|
|
||||||
// Transformes sale estimate model to POJO.
|
// Transformes sale estimate model to POJO.
|
||||||
const transformed = await this.transformer.transform(
|
const transformed = await this.transformer.transform(
|
||||||
estimate,
|
estimate,
|
||||||
new SaleEstimateTransfromer(),
|
new SaleEstimateTransfromer(),
|
||||||
|
{ customFields },
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventPayload = { saleEstimateId: estimateId };
|
const eventPayload = { saleEstimateId: estimateId };
|
||||||
|
|
||||||
// Triggers `onSaleEstimateViewed` event.
|
// Triggers `onSaleEstimateViewed` event.
|
||||||
|
|||||||
@@ -27,9 +27,18 @@ export class SaleEstimateTransfromer extends Transformer {
|
|||||||
'formattedCreatedAt',
|
'formattedCreatedAt',
|
||||||
'entries',
|
'entries',
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve custom fields from options.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public customFields(): Record<string, any> {
|
||||||
|
return this.options?.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted estimate date.
|
* Retrieve formatted estimate date.
|
||||||
* @param {ISaleEstimate} invoice
|
* @param {ISaleEstimate} invoice
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import { SaleInvoicesCost } from './SalesInvoicesCost';
|
|||||||
import { SaleInvoicesExportable } from './commands/SaleInvoicesExportable';
|
import { SaleInvoicesExportable } from './commands/SaleInvoicesExportable';
|
||||||
import { SaleInvoicesImportable } from './commands/SaleInvoicesImportable';
|
import { SaleInvoicesImportable } from './commands/SaleInvoicesImportable';
|
||||||
import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
import { BulkDeleteSaleInvoicesService } from './BulkDeleteSaleInvoices.service';
|
import { BulkDeleteSaleInvoicesService } from './BulkDeleteSaleInvoices.service';
|
||||||
import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleInvoices.service';
|
import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleInvoices.service';
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleI
|
|||||||
forwardRef(() => InventoryCostModule),
|
forwardRef(() => InventoryCostModule),
|
||||||
forwardRef(() => PaymentLinksModule),
|
forwardRef(() => PaymentLinksModule),
|
||||||
DynamicListModule,
|
DynamicListModule,
|
||||||
|
CustomFieldsModule,
|
||||||
BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
|
BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
|
||||||
BullBoardModule.forFeature({
|
BullBoardModule.forFeature({
|
||||||
name: SendSaleInvoiceQueue,
|
name: SendSaleInvoiceQueue,
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export class EditSaleInvoice {
|
|||||||
id: saleInvoiceId,
|
id: saleInvoiceId,
|
||||||
...saleInvoiceObj,
|
...saleInvoiceObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Edit event payload.
|
// Edit event payload.
|
||||||
const editEventPayload: ISaleInvoiceEditedPayload = {
|
const editEventPayload: ISaleInvoiceEditedPayload = {
|
||||||
saleInvoiceId,
|
saleInvoiceId,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
|
import { ToNumber } from '@/common/decorators/Validators';
|
||||||
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
IsInt,
|
IsInt,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Min,
|
Min,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
@@ -216,6 +218,15 @@ class CommandSaleInvoiceDto {
|
|||||||
example: [{ key: '123456' }],
|
example: [{ key: '123456' }],
|
||||||
})
|
})
|
||||||
attachments?: AttachmentDto[];
|
attachments?: AttachmentDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateSaleInvoiceDto extends CommandSaleInvoiceDto {}
|
export class CreateSaleInvoiceDto extends CommandSaleInvoiceDto {}
|
||||||
|
|||||||
@@ -234,4 +234,11 @@ export class SaleInvoiceResponseDto {
|
|||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { CommandSaleInvoiceValidators } from '../commands/CommandSaleInvoiceVali
|
|||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { SaleInvoiceResponseDto } from '../dtos/SaleInvoiceResponse.dto';
|
import { SaleInvoiceResponseDto } from '../dtos/SaleInvoiceResponse.dto';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetSaleInvoice {
|
export class GetSaleInvoice {
|
||||||
@@ -15,6 +16,7 @@ export class GetSaleInvoice {
|
|||||||
private transformer: TransformerInjectable,
|
private transformer: TransformerInjectable,
|
||||||
private validators: CommandSaleInvoiceValidators,
|
private validators: CommandSaleInvoiceValidators,
|
||||||
private eventPublisher: EventEmitter2,
|
private eventPublisher: EventEmitter2,
|
||||||
|
private getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
|
|
||||||
@Inject(SaleInvoice.name)
|
@Inject(SaleInvoice.name)
|
||||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||||
@@ -44,10 +46,18 @@ export class GetSaleInvoice {
|
|||||||
// Validates the given sale invoice existance.
|
// Validates the given sale invoice existance.
|
||||||
this.validators.validateInvoiceExistance(saleInvoice);
|
this.validators.validateInvoiceExistance(saleInvoice);
|
||||||
|
|
||||||
|
// Load custom field values.
|
||||||
|
const customFields = await this.getResourceCustomFieldsService.getResourceCustomFields(
|
||||||
|
'SaleInvoice',
|
||||||
|
saleInvoiceId,
|
||||||
|
);
|
||||||
|
|
||||||
const transformed = await this.transformer.transform(
|
const transformed = await this.transformer.transform(
|
||||||
saleInvoice,
|
saleInvoice,
|
||||||
new SaleInvoiceTransformer(),
|
new SaleInvoiceTransformer(),
|
||||||
|
{ customFields },
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventPayload = {
|
const eventPayload = {
|
||||||
saleInvoiceId,
|
saleInvoiceId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
|
|||||||
import { SaleInvoice } from '../models/SaleInvoice';
|
import { SaleInvoice } from '../models/SaleInvoice';
|
||||||
import { GetSaleInvoicesQueryDto } from '../dtos/GetSaleInvoicesQuery.dto';
|
import { GetSaleInvoicesQueryDto } from '../dtos/GetSaleInvoicesQuery.dto';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetSaleInvoicesService {
|
export class GetSaleInvoicesService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly dynamicListService: DynamicListService,
|
private readonly dynamicListService: DynamicListService,
|
||||||
private readonly transformer: TransformerInjectable,
|
private readonly transformer: TransformerInjectable,
|
||||||
|
private readonly getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
|
|
||||||
@Inject(SaleInvoice.name)
|
@Inject(SaleInvoice.name)
|
||||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||||
@@ -63,6 +65,18 @@ export class GetSaleInvoicesService {
|
|||||||
new SaleInvoiceTransformer(),
|
new SaleInvoiceTransformer(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Load custom field values for all invoices.
|
||||||
|
const invoiceIds = salesInvoices.map((invoice) => invoice.id);
|
||||||
|
if (invoiceIds.length > 0) {
|
||||||
|
const customFieldsMap = await this.getResourceCustomFieldsService.getResourceCustomFieldsBulk(
|
||||||
|
'SaleInvoice',
|
||||||
|
invoiceIds,
|
||||||
|
);
|
||||||
|
salesInvoices.forEach((invoice) => {
|
||||||
|
invoice.customFields = customFieldsMap[invoice.id] || {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
salesInvoices,
|
salesInvoices,
|
||||||
pagination,
|
pagination,
|
||||||
|
|||||||
@@ -32,9 +32,18 @@ export class SaleInvoiceTransformer extends Transformer {
|
|||||||
'taxes',
|
'taxes',
|
||||||
'entries',
|
'entries',
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve custom fields from options.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public customFields(): Record<string, any> {
|
||||||
|
return this.options?.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted invoice date.
|
* Retrieve formatted invoice date.
|
||||||
* @param {ISaleInvoice} invoice
|
* @param {ISaleInvoice} invoice
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { SaleReceiptCostGLEntriesSubscriber } from './subscribers/SaleReceiptCos
|
|||||||
import { SaleReceiptCostGLEntries } from './SaleReceiptCostGLEntries';
|
import { SaleReceiptCostGLEntries } from './SaleReceiptCostGLEntries';
|
||||||
import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service';
|
import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service';
|
||||||
import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service';
|
import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service';
|
||||||
|
import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [SaleReceiptsController],
|
controllers: [SaleReceiptsController],
|
||||||
@@ -61,6 +62,7 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
|
|||||||
AccountsModule,
|
AccountsModule,
|
||||||
InventoryCostModule,
|
InventoryCostModule,
|
||||||
DynamicListModule,
|
DynamicListModule,
|
||||||
|
CustomFieldsModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
MailNotificationModule,
|
MailNotificationModule,
|
||||||
BullModule.registerQueue({ name: SendSaleReceiptMailQueue }),
|
BullModule.registerQueue({ name: SendSaleReceiptMailQueue }),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { events } from '@/common/events/events';
|
|||||||
import { Customer } from '@/modules/Customers/models/Customer';
|
import { Customer } from '@/modules/Customers/models/Customer';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { EditSaleReceiptDto } from '../dtos/SaleReceipt.dto';
|
import { EditSaleReceiptDto } from '../dtos/SaleReceipt.dto';
|
||||||
|
import { SaveCustomFieldValuesService } from '@/modules/CustomFields/queries/SaveCustomFieldValues.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EditSaleReceipt {
|
export class EditSaleReceipt {
|
||||||
@@ -24,6 +25,7 @@ export class EditSaleReceipt {
|
|||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly validators: SaleReceiptValidators,
|
private readonly validators: SaleReceiptValidators,
|
||||||
private readonly dtoTransformer: SaleReceiptDTOTransformer,
|
private readonly dtoTransformer: SaleReceiptDTOTransformer,
|
||||||
|
private readonly saveCustomFieldValuesService: SaveCustomFieldValuesService,
|
||||||
|
|
||||||
@Inject(SaleReceipt.name)
|
@Inject(SaleReceipt.name)
|
||||||
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
|
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
|
||||||
@@ -96,6 +98,17 @@ export class EditSaleReceipt {
|
|||||||
id: saleReceiptId,
|
id: saleReceiptId,
|
||||||
...saleReceiptObj,
|
...saleReceiptObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save custom field values.
|
||||||
|
if (saleReceiptDTO.customFields) {
|
||||||
|
await this.saveCustomFieldValuesService.saveValues(
|
||||||
|
'SaleReceipt',
|
||||||
|
saleReceiptId,
|
||||||
|
saleReceiptDTO.customFields,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Triggers `onSaleReceiptEdited` event.
|
// Triggers `onSaleReceiptEdited` event.
|
||||||
await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, {
|
await this.eventPublisher.emitAsync(events.saleReceipt.onEdited, {
|
||||||
oldSaleReceipt,
|
oldSaleReceipt,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
IsEnum,
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -175,6 +176,15 @@ export class CommandSaleReceiptDto {
|
|||||||
example: 1,
|
example: 1,
|
||||||
})
|
})
|
||||||
adjustment?: number;
|
adjustment?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateSaleReceiptDto extends CommandSaleReceiptDto {}
|
export class CreateSaleReceiptDto extends CommandSaleReceiptDto {}
|
||||||
|
|||||||
@@ -243,4 +243,11 @@ export class SaleReceiptResponseDto {
|
|||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Custom fields values',
|
||||||
|
required: false,
|
||||||
|
example: { cf_priority: 'High' },
|
||||||
|
})
|
||||||
|
customFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SaleReceiptValidators } from '../commands/SaleReceiptValidators.service
|
|||||||
import { SaleReceipt } from '../models/SaleReceipt';
|
import { SaleReceipt } from '../models/SaleReceipt';
|
||||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { GetResourceCustomFieldsService } from '@/modules/CustomFields/queries/GetResourceCustomFields.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetSaleReceipt {
|
export class GetSaleReceipt {
|
||||||
@@ -11,6 +12,7 @@ export class GetSaleReceipt {
|
|||||||
@Inject(SaleReceipt.name)
|
@Inject(SaleReceipt.name)
|
||||||
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
|
private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
|
||||||
private readonly transformer: TransformerInjectable,
|
private readonly transformer: TransformerInjectable,
|
||||||
|
private readonly getResourceCustomFieldsService: GetResourceCustomFieldsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,9 +31,18 @@ export class GetSaleReceipt {
|
|||||||
.withGraphFetched('attachments')
|
.withGraphFetched('attachments')
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
return this.transformer.transform(
|
// Load custom field values.
|
||||||
|
const customFields = await this.getResourceCustomFieldsService.getResourceCustomFields(
|
||||||
|
'SaleReceipt',
|
||||||
|
saleReceiptId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transformed = await this.transformer.transform(
|
||||||
saleReceipt,
|
saleReceipt,
|
||||||
new SaleReceiptTransformer(),
|
new SaleReceiptTransformer(),
|
||||||
|
{ customFields },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return transformed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,18 @@ export class SaleReceiptTransformer extends Transformer {
|
|||||||
|
|
||||||
'entries',
|
'entries',
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'customFields',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve custom fields from options.
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public customFields(): Record<string, any> {
|
||||||
|
return this.options?.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted receipt date.
|
* Retrieve formatted receipt date.
|
||||||
* @param {ISaleReceipt} invoice
|
* @param {ISaleReceipt} invoice
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import * as request from 'supertest';
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import {
|
||||||
|
app,
|
||||||
|
AuthorizationHeader,
|
||||||
|
orgainzationId,
|
||||||
|
} from './init-app-test';
|
||||||
|
|
||||||
|
const makeCustomFieldRequest = (overrides = {}) => ({
|
||||||
|
resourceName: 'Item',
|
||||||
|
fieldName: `cf_${faker.string.alphanumeric({ length: 8 }).toLowerCase()}`,
|
||||||
|
label: faker.commerce.productAdjective(),
|
||||||
|
fieldType: 'text',
|
||||||
|
required: false,
|
||||||
|
order: 1,
|
||||||
|
active: true,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeItemRequest = () => ({
|
||||||
|
name: faker.commerce.productName(),
|
||||||
|
type: 'service',
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Custom Fields (e2e)', () => {
|
||||||
|
it('/custom-fields (POST) - should create a custom field', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest())
|
||||||
|
.expect(201)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.id).toBeDefined();
|
||||||
|
expect(res.body.fieldName).toBeDefined();
|
||||||
|
expect(res.body.label).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/custom-fields (GET) - should retrieve custom fields', async () => {
|
||||||
|
const field = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest());
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.data).toBeDefined();
|
||||||
|
expect(Array.isArray(res.body.data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/custom-fields?resource=Item (GET) - should retrieve custom fields by resource', async () => {
|
||||||
|
const field = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest({ resourceName: 'Item' }));
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/custom-fields')
|
||||||
|
.query({ resource: 'Item' })
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.data).toBeDefined();
|
||||||
|
expect(Array.isArray(res.body.data)).toBe(true);
|
||||||
|
expect(res.body.data.some((f: any) => f.id === field.body.id)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/custom-fields/:id (GET) - should retrieve a single custom field', async () => {
|
||||||
|
const field = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest());
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/custom-fields/${field.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.id).toBe(field.body.id);
|
||||||
|
expect(res.body.fieldName).toBe(field.body.fieldName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/custom-fields/:id (PUT) - should edit a custom field', async () => {
|
||||||
|
const field = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest());
|
||||||
|
|
||||||
|
const newLabel = faker.commerce.productAdjective();
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.put(`/custom-fields/${field.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send({
|
||||||
|
resourceName: field.body.resourceName,
|
||||||
|
fieldName: field.body.fieldName,
|
||||||
|
label: newLabel,
|
||||||
|
fieldType: field.body.fieldType,
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.id).toBe(field.body.id);
|
||||||
|
expect(res.body.label).toBe(newLabel);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/custom-fields/:id/status (PUT) - should update field status', async () => {
|
||||||
|
const field = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest({ active: true }));
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.put(`/custom-fields/${field.body.id}/status`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send({ active: false })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const updated = await request(app.getHttpServer())
|
||||||
|
.get(`/custom-fields/${field.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(updated.body.active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/custom-fields/reorder (POST) - should reorder custom fields', async () => {
|
||||||
|
const field1 = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest({ order: 1 }));
|
||||||
|
|
||||||
|
const field2 = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest({ order: 2 }));
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/custom-fields/reorder')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send({
|
||||||
|
orders: [
|
||||||
|
{ id: field1.body.id, order: 2 },
|
||||||
|
{ id: field2.body.id, order: 1 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(Array.isArray(res.body)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/custom-fields/:id (DELETE) - should delete a custom field', async () => {
|
||||||
|
const field = await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest());
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.delete(`/custom-fields/${field.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/custom-fields/${field.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save custom field values when creating an item', async () => {
|
||||||
|
const fieldName = `cf_${faker.string.alphanumeric({ length: 8 }).toLowerCase()}`;
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest({
|
||||||
|
resourceName: 'Item',
|
||||||
|
fieldName,
|
||||||
|
label: 'Test Field',
|
||||||
|
fieldType: 'text',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const customFieldValue = faker.commerce.productMaterial();
|
||||||
|
|
||||||
|
const item = await request(app.getHttpServer())
|
||||||
|
.post('/items')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send({
|
||||||
|
...makeItemRequest(),
|
||||||
|
customFields: {
|
||||||
|
[fieldName]: customFieldValue,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const fetchedItem = await request(app.getHttpServer())
|
||||||
|
.get(`/items/${item.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(fetchedItem.body.customFields).toBeDefined();
|
||||||
|
expect(fetchedItem.body.customFields[fieldName]).toBe(customFieldValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update custom field values when editing an item', async () => {
|
||||||
|
const fieldName = `cf_${faker.string.alphanumeric({ length: 8 }).toLowerCase()}`;
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest({
|
||||||
|
resourceName: 'Item',
|
||||||
|
fieldName,
|
||||||
|
label: 'Test Field',
|
||||||
|
fieldType: 'text',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const initialValue = faker.commerce.productMaterial();
|
||||||
|
const updatedValue = faker.commerce.productMaterial();
|
||||||
|
|
||||||
|
const item = await request(app.getHttpServer())
|
||||||
|
.post('/items')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send({
|
||||||
|
...makeItemRequest(),
|
||||||
|
customFields: {
|
||||||
|
[fieldName]: initialValue,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.put(`/items/${item.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send({
|
||||||
|
...makeItemRequest(),
|
||||||
|
customFields: {
|
||||||
|
[fieldName]: updatedValue,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const fetchedItem = await request(app.getHttpServer())
|
||||||
|
.get(`/items/${item.body.id}`)
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(fetchedItem.body.customFields).toBeDefined();
|
||||||
|
expect(fetchedItem.body.customFields[fieldName]).toBe(updatedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should inject custom fields into resource metadata', async () => {
|
||||||
|
const fieldName = `cf_${faker.string.alphanumeric({ length: 8 }).toLowerCase()}`;
|
||||||
|
const label = faker.commerce.productAdjective();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/custom-fields')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.send(makeCustomFieldRequest({
|
||||||
|
resourceName: 'Item',
|
||||||
|
fieldName,
|
||||||
|
label,
|
||||||
|
fieldType: 'text',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const meta = await request(app.getHttpServer())
|
||||||
|
.get('/resources/Item/meta')
|
||||||
|
.set('organization-id', orgainzationId)
|
||||||
|
.set('Authorization', AuthorizationHeader)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(meta.body.fields2).toBeDefined();
|
||||||
|
expect(meta.body.fields2[fieldName]).toBeDefined();
|
||||||
|
expect(meta.body.fields2[fieldName].name).toBe(label);
|
||||||
|
expect(meta.body.fields2[fieldName].fieldType).toBe('text');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"testRegex": "auth.e2e-spec.ts$",
|
"testRegex": "(auth|custom-fields)\\.e2e-spec\\.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ export const PreferencesMenu = [
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
href: '/preferences/items',
|
href: '/preferences/items',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Custom Fields',
|
||||||
|
disabled: false,
|
||||||
|
href: '/preferences/custom-fields',
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// text: 'Integrations',
|
// text: 'Integrations',
|
||||||
// disabled: false,
|
// disabled: false,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import {
|
||||||
|
AppToaster,
|
||||||
|
FormattedMessage as T,
|
||||||
|
FormattedHTMLMessage,
|
||||||
|
} from '@/components';
|
||||||
|
import { Intent, Alert } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import { useDeleteCustomField } from '@/hooks/query';
|
||||||
|
|
||||||
|
import { withAlertStoreConnect } from '@/containers/Alert/withAlertStoreConnect';
|
||||||
|
import { withAlertActions } from '@/containers/Alert/withAlertActions';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom field delete alert.
|
||||||
|
*/
|
||||||
|
function CustomFieldDeleteAlert({
|
||||||
|
name,
|
||||||
|
|
||||||
|
// #withAlertStoreConnect
|
||||||
|
isOpen,
|
||||||
|
payload: { customFieldId },
|
||||||
|
|
||||||
|
// #withAlertActions
|
||||||
|
closeAlert,
|
||||||
|
}) {
|
||||||
|
const { mutateAsync: deleteCustomField, isLoading } = useDeleteCustomField();
|
||||||
|
|
||||||
|
// Handle cancel delete alert.
|
||||||
|
const handleCancelDelete = () => {
|
||||||
|
closeAlert(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle confirm delete custom field.
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
deleteCustomField(customFieldId)
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: intl.get('custom_fields.delete_success_message'),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: intl.get('custom_fields.delete_error_message'),
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
closeAlert(name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
cancelButtonText={<T id={'cancel'} />}
|
||||||
|
confirmButtonText={<T id={'delete'} />}
|
||||||
|
icon="trash"
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onCancel={handleCancelDelete}
|
||||||
|
onConfirm={handleConfirmDelete}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<FormattedHTMLMessage
|
||||||
|
id={'custom_fields.delete_confirmation'}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withAlertStoreConnect(),
|
||||||
|
withAlertActions,
|
||||||
|
)(CustomFieldDeleteAlert);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const CustomFieldDeleteAlert = React.lazy(
|
||||||
|
() => import('@/containers/Alerts/CustomFields/CustomFieldDeleteAlert'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom fields alerts
|
||||||
|
*/
|
||||||
|
export default [{ name: 'custom-field-delete', component: CustomFieldDeleteAlert }];
|
||||||
@@ -31,6 +31,7 @@ import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
|
|||||||
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
|
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
|
||||||
import { BrandingTemplatesAlerts } from '../BrandingTemplates/alerts/BrandingTemplatesAlerts';
|
import { BrandingTemplatesAlerts } from '../BrandingTemplates/alerts/BrandingTemplatesAlerts';
|
||||||
import { PaymentMethodsAlerts } from '../Preferences/PaymentMethods/alerts/PaymentMethodsAlerts';
|
import { PaymentMethodsAlerts } from '../Preferences/PaymentMethods/alerts/PaymentMethodsAlerts';
|
||||||
|
import CustomFieldsAlerts from '@/containers/Alerts/CustomFields/CustomFieldsAlerts';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...AccountsAlerts,
|
...AccountsAlerts,
|
||||||
@@ -65,4 +66,5 @@ export default [
|
|||||||
...BankAccountAlerts,
|
...BankAccountAlerts,
|
||||||
...BrandingTemplatesAlerts,
|
...BrandingTemplatesAlerts,
|
||||||
...PaymentMethodsAlerts,
|
...PaymentMethodsAlerts,
|
||||||
|
...CustomFieldsAlerts,
|
||||||
];
|
];
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
|
||||||
|
|
||||||
|
export const CustomFieldsFormSchema = Yup.object().shape({
|
||||||
|
resourceName: Yup.string().required().label(intl.get('custom_fields.label.resource')),
|
||||||
|
fieldName: Yup.string().required().max(DATATYPES_LENGTH.STRING).label(intl.get('custom_fields.label.field_name')),
|
||||||
|
label: Yup.string().required().max(DATATYPES_LENGTH.STRING).label(intl.get('custom_fields.label.label')),
|
||||||
|
fieldType: Yup.string().required().label(intl.get('custom_fields.label.type')),
|
||||||
|
required: Yup.boolean(),
|
||||||
|
order: Yup.number().integer().min(0),
|
||||||
|
active: Yup.boolean(),
|
||||||
|
defaultValue: Yup.string().nullable().max(DATATYPES_LENGTH.STRING),
|
||||||
|
});
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||||
|
import { CustomFieldsFormSchema } from './CustomFieldsForm.schema';
|
||||||
|
import { useCustomFieldsFormContext } from './CustomFieldsFormProvider';
|
||||||
|
import { withDashboardActions } from '@/containers/Dashboard/withDashboardActions';
|
||||||
|
import CustomFieldsFormContent from './CustomFieldsFormContent';
|
||||||
|
import { compose, transformToForm } from '@/utils';
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
resourceName: 'Item',
|
||||||
|
fieldName: '',
|
||||||
|
label: '',
|
||||||
|
fieldType: 'text',
|
||||||
|
options: {},
|
||||||
|
required: false,
|
||||||
|
order: 0,
|
||||||
|
active: true,
|
||||||
|
defaultValue: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences - Custom Fields Form.
|
||||||
|
*/
|
||||||
|
function CustomFieldsForm({
|
||||||
|
// #withDashboardActions
|
||||||
|
changePreferencesPageTitle,
|
||||||
|
}) {
|
||||||
|
// History context.
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
// Custom fields form context.
|
||||||
|
const {
|
||||||
|
isNewMode,
|
||||||
|
createCustomFieldMutate,
|
||||||
|
editCustomFieldMutate,
|
||||||
|
customField,
|
||||||
|
customFieldId,
|
||||||
|
} = useCustomFieldsFormContext();
|
||||||
|
|
||||||
|
// Initial values.
|
||||||
|
const initialValues = {
|
||||||
|
...defaultValues,
|
||||||
|
...(!isEmpty(customField)
|
||||||
|
? transformToForm(customField, defaultValues)
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
changePreferencesPageTitle(
|
||||||
|
isNewMode ? (
|
||||||
|
<T id={'custom_fields.new_custom_field'} />
|
||||||
|
) : (
|
||||||
|
<T id={'custom_fields.edit_custom_field'} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [changePreferencesPageTitle, isNewMode]);
|
||||||
|
|
||||||
|
// Handle the form submit.
|
||||||
|
const handleFormSubmit = (values, { setSubmitting }) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
const onSuccess = () => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: intl.get(
|
||||||
|
isNewMode
|
||||||
|
? 'custom_fields.create_success_message'
|
||||||
|
: 'custom_fields.edit_success_message',
|
||||||
|
),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
history.push('/preferences/custom-fields');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = (error) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
AppToaster.show({
|
||||||
|
message: intl.get('custom_fields.error_message'),
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNewMode) {
|
||||||
|
createCustomFieldMutate(values).then(onSuccess).catch(onError);
|
||||||
|
} else {
|
||||||
|
editCustomFieldMutate([customFieldId, values]).then(onSuccess).catch(onError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={CustomFieldsFormSchema}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
>
|
||||||
|
<CustomFieldsFormContent />
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDashboardActions)(CustomFieldsForm);
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { Form } from 'formik';
|
||||||
|
|
||||||
|
import { CustomFieldsFormHeader } from './CustomFieldsFormHeader';
|
||||||
|
import { CustomFieldsFormFloatingActions } from './CustomFieldsFormFloatingActions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences - Custom Fields Form content.
|
||||||
|
* @returns {React.JSX}
|
||||||
|
*/
|
||||||
|
export default function CustomFieldsFormContent() {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<CustomFieldsFormHeader />
|
||||||
|
<CustomFieldsFormFloatingActions />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
import { FormattedMessage as T } from '@/components';
|
||||||
|
import { Button, Intent, Classes } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import { useCustomFieldsFormContext } from './CustomFieldsFormProvider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Fields form floating actions.
|
||||||
|
*/
|
||||||
|
export function CustomFieldsFormFloatingActions() {
|
||||||
|
// History context.
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
// Formik context.
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
// Custom fields form context.
|
||||||
|
const { isNewMode } = useCustomFieldsFormContext();
|
||||||
|
|
||||||
|
// Handle cancel button click.
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
history.go(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isSubmitting}
|
||||||
|
style={{ minWidth: 85 }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isNewMode ? (
|
||||||
|
<T id={'save'} />
|
||||||
|
) : (
|
||||||
|
<T id={'update'} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCancelBtnClick}
|
||||||
|
style={{ minWidth: 75 }}>
|
||||||
|
<T id={'cancel'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormattedMessage as T,
|
||||||
|
FieldRequiredHint,
|
||||||
|
Card,
|
||||||
|
FFormGroup,
|
||||||
|
FInputGroup,
|
||||||
|
FSelect,
|
||||||
|
FCheckbox,
|
||||||
|
FSwitch,
|
||||||
|
FNumericInput,
|
||||||
|
} from '@/components';
|
||||||
|
import { useAutofocus } from '@/hooks';
|
||||||
|
import { useCustomFieldsFormContext } from './CustomFieldsFormProvider';
|
||||||
|
|
||||||
|
const RESOURCE_OPTIONS = [
|
||||||
|
{ label: 'Sale Invoice', value: 'SaleInvoice' },
|
||||||
|
{ label: 'Sale Estimate', value: 'SaleEstimate' },
|
||||||
|
{ label: 'Sale Receipt', value: 'SaleReceipt' },
|
||||||
|
{ label: 'Customer', value: 'Customer' },
|
||||||
|
{ label: 'Item', value: 'Item' },
|
||||||
|
{ label: 'Credit Note', value: 'CreditNote' },
|
||||||
|
{ label: 'Payment Receive', value: 'PaymentReceive' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FIELD_TYPE_OPTIONS = [
|
||||||
|
{ label: 'Text', value: 'text' },
|
||||||
|
{ label: 'Number', value: 'number' },
|
||||||
|
{ label: 'Date', value: 'date' },
|
||||||
|
{ label: 'Checkbox', value: 'checkbox' },
|
||||||
|
{ label: 'Dropdown', value: 'dropdown' },
|
||||||
|
{ label: 'URL', value: 'url' },
|
||||||
|
{ label: 'Textarea', value: 'textarea' },
|
||||||
|
{ label: 'Auto Number', value: 'autonumber' },
|
||||||
|
{ label: 'Lookup', value: 'lookup' },
|
||||||
|
{ label: 'Formula', value: 'formula' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Fields form header.
|
||||||
|
* @returns {React.JSX}
|
||||||
|
*/
|
||||||
|
export function CustomFieldsFormHeader() {
|
||||||
|
const labelFieldRef = useAutofocus();
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
const { isNewMode } = useCustomFieldsFormContext();
|
||||||
|
const isDropdown = values.fieldType === 'dropdown';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
{/* ---------- Resource ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'resourceName'}
|
||||||
|
label={
|
||||||
|
<strong>
|
||||||
|
<T id={'custom_fields.label.resource'} />
|
||||||
|
</strong>
|
||||||
|
}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FSelect
|
||||||
|
name={'resourceName'}
|
||||||
|
items={RESOURCE_OPTIONS}
|
||||||
|
valueAccessor={'value'}
|
||||||
|
textAccessor={'label'}
|
||||||
|
placeholder={intl.get('select_an_item')}
|
||||||
|
disabled={!isNewMode}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/* ---------- Field Name ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'fieldName'}
|
||||||
|
label={
|
||||||
|
<strong>
|
||||||
|
<T id={'custom_fields.label.field_name'} />
|
||||||
|
</strong>
|
||||||
|
}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FInputGroup
|
||||||
|
name={'fieldName'}
|
||||||
|
medium={true}
|
||||||
|
inputRef={(ref) => (labelFieldRef.current = ref)}
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/* ---------- Label ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'label'}
|
||||||
|
label={
|
||||||
|
<strong>
|
||||||
|
<T id={'custom_fields.label.label'} />
|
||||||
|
</strong>
|
||||||
|
}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FInputGroup
|
||||||
|
name={'label'}
|
||||||
|
medium={true}
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/* ---------- Field Type ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'fieldType'}
|
||||||
|
label={
|
||||||
|
<strong>
|
||||||
|
<T id={'custom_fields.label.type'} />
|
||||||
|
</strong>
|
||||||
|
}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FSelect
|
||||||
|
name={'fieldType'}
|
||||||
|
items={FIELD_TYPE_OPTIONS}
|
||||||
|
valueAccessor={'value'}
|
||||||
|
textAccessor={'label'}
|
||||||
|
placeholder={intl.get('select_an_item')}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/* ---------- Dropdown Options ---------- */}
|
||||||
|
{isDropdown && (
|
||||||
|
<DropdownOptionsField />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---------- Default Value ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'defaultValue'}
|
||||||
|
label={<T id={'custom_fields.label.default_value'} />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FInputGroup
|
||||||
|
name={'defaultValue'}
|
||||||
|
medium={true}
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/* ---------- Required ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'required'}
|
||||||
|
label={<T id={'custom_fields.label.required'} />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FCheckbox
|
||||||
|
name={'required'}
|
||||||
|
label={intl.get('custom_fields.required_description')}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/* ---------- Order ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'order'}
|
||||||
|
label={<T id={'custom_fields.label.order'} />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FNumericInput
|
||||||
|
name={'order'}
|
||||||
|
min={0}
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/* ---------- Active ---------- */}
|
||||||
|
<FFormGroup
|
||||||
|
name={'active'}
|
||||||
|
label={<T id={'custom_fields.label.active'} />}
|
||||||
|
inline
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FSwitch
|
||||||
|
name={'active'}
|
||||||
|
label={intl.get('custom_fields.active_description')}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropdown options field.
|
||||||
|
*/
|
||||||
|
function DropdownOptionsField() {
|
||||||
|
const { values, setFieldValue } = useFormikContext();
|
||||||
|
const choices = values.options?.choices || [];
|
||||||
|
|
||||||
|
const handleAddChoice = () => {
|
||||||
|
const newChoices = [...choices, ''];
|
||||||
|
setFieldValue('options', { ...values.options, choices: newChoices });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveChoice = (index) => {
|
||||||
|
const newChoices = choices.filter((_, i) => i !== index);
|
||||||
|
setFieldValue('options', { ...values.options, choices: newChoices });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeChoice = (index, value) => {
|
||||||
|
const newChoices = [...choices];
|
||||||
|
newChoices[index] = value;
|
||||||
|
setFieldValue('options', { ...values.options, choices: newChoices });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FFormGroup
|
||||||
|
label={<strong><T id={'custom_fields.label.choices'} /></strong>}
|
||||||
|
inline
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}>
|
||||||
|
{choices.map((choice, index) => (
|
||||||
|
<div key={index} style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={choice}
|
||||||
|
onChange={(e) => handleChangeChoice(index, e.target.value)}
|
||||||
|
className="bp4-input bp4-fill"
|
||||||
|
placeholder={intl.get('custom_fields.choice_placeholder')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bp4-button bp4-intent-danger"
|
||||||
|
onClick={() => handleRemoveChoice(index)}
|
||||||
|
>
|
||||||
|
{intl.get('remove')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bp4-button bp4-intent-primary"
|
||||||
|
onClick={handleAddChoice}
|
||||||
|
style={{ alignSelf: 'flex-start' }}
|
||||||
|
>
|
||||||
|
{intl.get('custom_fields.add_choice')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { CustomFieldsFormProvider } from './CustomFieldsFormProvider';
|
||||||
|
import CustomFieldsForm from './CustomFieldsForm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Fields Form page.
|
||||||
|
*/
|
||||||
|
export default function CustomFieldsFormPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const idInteger = id ? parseInt(id, 10) : null;
|
||||||
|
const isNewMode = !idInteger;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomFieldsFormProvider customFieldId={idInteger} isNewMode={isNewMode}>
|
||||||
|
<CustomFieldsForm />
|
||||||
|
</CustomFieldsFormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { CLASSES } from '@/constants/classes';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useCreateCustomField,
|
||||||
|
useEditCustomField,
|
||||||
|
useCustomField,
|
||||||
|
} from '@/hooks/query';
|
||||||
|
import PreferencesPageLoader from '@/containers/Preferences/PreferencesPageLoader';
|
||||||
|
|
||||||
|
const CustomFieldsFormContext = React.createContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Fields Form page provider.
|
||||||
|
*/
|
||||||
|
function CustomFieldsFormProvider({ customFieldId, isNewMode, ...props }) {
|
||||||
|
// Create and edit custom fields mutations.
|
||||||
|
const { mutateAsync: createCustomFieldMutate } = useCreateCustomField();
|
||||||
|
const { mutateAsync: editCustomFieldMutate } = useEditCustomField();
|
||||||
|
|
||||||
|
// Retrieve custom field.
|
||||||
|
const {
|
||||||
|
data: customField,
|
||||||
|
isLoading: isCustomFieldLoading,
|
||||||
|
} = useCustomField(customFieldId, {
|
||||||
|
enabled: !!customFieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provider state.
|
||||||
|
const provider = {
|
||||||
|
isNewMode,
|
||||||
|
customFieldId,
|
||||||
|
customField,
|
||||||
|
createCustomFieldMutate,
|
||||||
|
editCustomFieldMutate,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isCustomFieldLoading ? (
|
||||||
|
<PreferencesPageLoader />
|
||||||
|
) : (
|
||||||
|
<CustomFieldsFormContext.Provider value={provider} {...props} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCustomFieldsFormContext = () => React.useContext(CustomFieldsFormContext);
|
||||||
|
|
||||||
|
export { CustomFieldsFormProvider, useCustomFieldsFormContext };
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Intent, Button, Position } from '@blueprintjs/core';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DataTable, AppToaster, TableSkeletonRows } from '@/components';
|
||||||
|
|
||||||
|
import { useCustomFieldsTableColumns, ActionsMenu } from './components';
|
||||||
|
import { withAlertActions } from '@/containers/Alert/withAlertActions';
|
||||||
|
import { useCustomFieldsContext } from './CustomFieldsListProvider';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom fields data table.
|
||||||
|
*/
|
||||||
|
function CustomFieldsDataTable({
|
||||||
|
// #withAlertActions
|
||||||
|
openAlert,
|
||||||
|
}) {
|
||||||
|
// History context.
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
// Retrieve custom fields table columns
|
||||||
|
const columns = useCustomFieldsTableColumns();
|
||||||
|
|
||||||
|
// Custom fields table context.
|
||||||
|
const { customFields, isCustomFieldsFetching, isCustomFieldsLoading } =
|
||||||
|
useCustomFieldsContext();
|
||||||
|
|
||||||
|
// Handles delete the given custom field.
|
||||||
|
const handleDeleteCustomField = ({ id }) => {
|
||||||
|
openAlert('custom-field-delete', { customFieldId: id });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the edit of the given custom field.
|
||||||
|
const handleEditCustomField = ({ id }) => {
|
||||||
|
history.push(`/preferences/custom-fields/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles navigate to the new custom field page.
|
||||||
|
const handleNewCustomField = () => {
|
||||||
|
history.push('/preferences/custom-fields/new');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
onClick={handleNewCustomField}
|
||||||
|
text={intl.get('custom_fields.new_custom_field')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CustomFieldsTable
|
||||||
|
columns={columns}
|
||||||
|
data={customFields}
|
||||||
|
loading={isCustomFieldsLoading}
|
||||||
|
headerLoading={isCustomFieldsFetching}
|
||||||
|
progressBarLoading={isCustomFieldsFetching}
|
||||||
|
TableLoadingRenderer={TableSkeletonRows}
|
||||||
|
ContextMenu={ActionsMenu}
|
||||||
|
payload={{
|
||||||
|
onDeleteCustomField: handleDeleteCustomField,
|
||||||
|
onEditCustomField: handleEditCustomField,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomFieldsTable = styled(DataTable)`
|
||||||
|
.table .tr {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default compose(withAlertActions)(CustomFieldsDataTable);
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { CustomFieldsListProvider } from './CustomFieldsListProvider';
|
||||||
|
import CustomFieldsDataTable from './CustomFieldsDataTable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom fields list.
|
||||||
|
*/
|
||||||
|
function CustomFieldsList() {
|
||||||
|
return (
|
||||||
|
<CustomFieldsListProvider>
|
||||||
|
<CustomFieldsDataTable />
|
||||||
|
</CustomFieldsListProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomFieldsList;
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { CLASSES } from '@/constants/classes';
|
||||||
|
import { useCustomFields } from '@/hooks/query';
|
||||||
|
|
||||||
|
const CustomFieldsListContext = React.createContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom fields list provider.
|
||||||
|
*/
|
||||||
|
function CustomFieldsListProvider({ ...props }) {
|
||||||
|
// Fetch custom fields list.
|
||||||
|
const {
|
||||||
|
data: customFields,
|
||||||
|
isFetching: isCustomFieldsFetching,
|
||||||
|
isLoading: isCustomFieldsLoading,
|
||||||
|
} = useCustomFields();
|
||||||
|
|
||||||
|
// Provider state.
|
||||||
|
const provider = {
|
||||||
|
customFields,
|
||||||
|
isCustomFieldsFetching,
|
||||||
|
isCustomFieldsLoading,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CustomFieldsListContext.Provider value={provider} {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCustomFieldsContext = () => React.useContext(CustomFieldsListContext);
|
||||||
|
|
||||||
|
export { CustomFieldsListProvider, useCustomFieldsContext };
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
import { Intent, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
|
||||||
|
import { safeCallback } from '@/utils';
|
||||||
|
import { Icon } from '@/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context menu of custom fields.
|
||||||
|
*/
|
||||||
|
export function ActionsMenu({
|
||||||
|
payload: { onDeleteCustomField, onEditCustomField },
|
||||||
|
row: { original },
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
icon={<Icon icon="pen-18" />}
|
||||||
|
text={intl.get('custom_fields.edit')}
|
||||||
|
onClick={safeCallback(onEditCustomField, original)}
|
||||||
|
/>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||||
|
text={intl.get('custom_fields.delete')}
|
||||||
|
onClick={safeCallback(onDeleteCustomField, original)}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve Custom Fields table columns.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useCustomFieldsTableColumns() {
|
||||||
|
return React.useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'label',
|
||||||
|
Header: intl.get('custom_fields.column.label'),
|
||||||
|
accessor: 'label',
|
||||||
|
className: 'label',
|
||||||
|
width: '120',
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fieldName',
|
||||||
|
Header: intl.get('custom_fields.column.field_name'),
|
||||||
|
accessor: 'fieldName',
|
||||||
|
className: 'field-name',
|
||||||
|
width: '120',
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resourceName',
|
||||||
|
Header: intl.get('custom_fields.column.resource'),
|
||||||
|
accessor: 'resourceName',
|
||||||
|
className: 'resource-name',
|
||||||
|
width: '100',
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fieldType',
|
||||||
|
Header: intl.get('custom_fields.column.type'),
|
||||||
|
accessor: 'fieldType',
|
||||||
|
className: 'field-type',
|
||||||
|
width: '80',
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'required',
|
||||||
|
Header: intl.get('custom_fields.column.required'),
|
||||||
|
accessor: 'required',
|
||||||
|
className: 'required',
|
||||||
|
width: '60',
|
||||||
|
Cell: ({ value }) => (value ? intl.get('yes') : intl.get('no')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'active',
|
||||||
|
Header: intl.get('custom_fields.column.active'),
|
||||||
|
accessor: 'active',
|
||||||
|
className: 'active',
|
||||||
|
width: '60',
|
||||||
|
Cell: ({ value }) => (value ? intl.get('yes') : intl.get('no')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order',
|
||||||
|
Header: intl.get('custom_fields.column.order'),
|
||||||
|
accessor: 'order',
|
||||||
|
className: 'order',
|
||||||
|
width: '50',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useMutation, useQueryClient } from 'react-query';
|
||||||
|
import { useRequestQuery } from '../useQueryRequest';
|
||||||
|
import useApiRequest from '../useRequest';
|
||||||
|
import t from './types';
|
||||||
|
|
||||||
|
// Common invalidate queries.
|
||||||
|
const commonInvalidateQueries = (queryClient) => {
|
||||||
|
queryClient.invalidateQueries(t.CUSTOM_FIELDS);
|
||||||
|
queryClient.invalidateQueries(t.CUSTOM_FIELD);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the custom fields.
|
||||||
|
*/
|
||||||
|
export function useCustomFields(props, query) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[t.CUSTOM_FIELDS, query],
|
||||||
|
{ method: 'get', url: 'custom-fields', params: query },
|
||||||
|
{
|
||||||
|
select: (res) => res.data?.data || [],
|
||||||
|
defaultData: [],
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the custom field.
|
||||||
|
*/
|
||||||
|
export function useCustomField(fieldId, props, requestProps) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[t.CUSTOM_FIELD, fieldId],
|
||||||
|
{ method: 'get', url: `custom-fields/${fieldId}`, ...requestProps },
|
||||||
|
{
|
||||||
|
select: (res) => res.data,
|
||||||
|
defaultData: {},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new custom field.
|
||||||
|
*/
|
||||||
|
export function useCreateCustomField(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation((values) => apiRequest.post('custom-fields', values), {
|
||||||
|
onSuccess: () => {
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit the given custom field.
|
||||||
|
*/
|
||||||
|
export function useEditCustomField(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(([id, values]) => apiRequest.put(`custom-fields/${id}`, values), {
|
||||||
|
onSuccess: () => {
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the given custom field.
|
||||||
|
*/
|
||||||
|
export function useDeleteCustomField(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation((id) => apiRequest.delete(`custom-fields/${id}`), {
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
queryClient.invalidateQueries(t.CUSTOM_FIELD, id);
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorder custom fields.
|
||||||
|
*/
|
||||||
|
export function useReorderCustomFields(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation((values) => apiRequest.post('custom-fields/reorder', values), {
|
||||||
|
onSuccess: () => {
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update custom field status.
|
||||||
|
*/
|
||||||
|
export function useUpdateCustomFieldStatus(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(([id, values]) => apiRequest.put(`custom-fields/${id}/status`, values), {
|
||||||
|
onSuccess: () => {
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -39,3 +39,4 @@ export * from './warehousesTransfers';
|
|||||||
export * from './plaid';
|
export * from './plaid';
|
||||||
export * from './FinancialReports';
|
export * from './FinancialReports';
|
||||||
export * from './apiKeys';
|
export * from './apiKeys';
|
||||||
|
export * from './customFields';
|
||||||
|
|||||||
@@ -245,6 +245,11 @@ export const API_KEYS = {
|
|||||||
API_KEYS: 'API_KEYS',
|
API_KEYS: 'API_KEYS',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CUSTOM_FIELDS = {
|
||||||
|
CUSTOM_FIELDS: 'CUSTOM_FIELDS',
|
||||||
|
CUSTOM_FIELD: 'CUSTOM_FIELD',
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...Authentication,
|
...Authentication,
|
||||||
...ACCOUNTS,
|
...ACCOUNTS,
|
||||||
@@ -281,4 +286,5 @@ export default {
|
|||||||
...TAX_RATES,
|
...TAX_RATES,
|
||||||
...EXCHANGE_RATE,
|
...EXCHANGE_RATE,
|
||||||
...API_KEYS,
|
...API_KEYS,
|
||||||
|
...CUSTOM_FIELDS,
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user