refactor(nestjs): contacts module

This commit is contained in:
Ahmed Bouhuolia
2025-05-20 23:55:39 +02:00
parent 99fe5a6b0d
commit 0823bfc4e9
15 changed files with 195 additions and 9 deletions

View File

@@ -88,6 +88,7 @@ import { ViewsModule } from '../Views/Views.module';
import { CurrenciesModule } from '../Currencies/Currencies.module';
import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module';
import { UsersModule } from '../UsersModule/Users.module';
import { ContactsModule } from '../Contacts/Contacts.module';
@Module({
imports: [
@@ -210,7 +211,8 @@ import { UsersModule } from '../UsersModule/Users.module';
ViewsModule,
CurrenciesModule,
MiscellaneousModule,
UsersModule
UsersModule,
ContactsModule
],
controllers: [AppController],
providers: [

View File

@@ -0,0 +1,6 @@
export const ERRORS = {
CONTACT_ALREADY_ACTIVE: 'CONTACT_ALREADY_ACTIVE',
CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE'
}

View File

@@ -0,0 +1,45 @@
import {
Controller,
Get,
Query,
Param,
Post,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger';
import { GetContactsAutoCompleteQuery } from './dtos/GetContactsAutoCompleteQuery.dto';
import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service';
import { ActivateContactService } from './commands/ActivateContact.service';
import { InactivateContactService } from './commands/InactivateContact.service';
@Controller('contacts')
@ApiTags('contacts')
export class ContactsController {
constructor(
private readonly getAutoCompleteService: GetAutoCompleteContactsService,
private readonly activateContactService: ActivateContactService,
private readonly inactivateContactService: InactivateContactService,
) {}
@Get('auto-complete')
@ApiOperation({ summary: 'Get the auto-complete contacts' })
getAutoComplete(@Query() query: GetContactsAutoCompleteQuery) {
return this.getAutoCompleteService.autocompleteContacts(query);
}
@Post(':id/activate')
@ApiOperation({ summary: 'Activate a contact' })
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
async activateContact(@Param('id', ParseIntPipe) contactId: number) {
await this.activateContactService.activateContact(contactId);
return { id: contactId, activated: true };
}
@Post(':id/inactivate')
@ApiOperation({ summary: 'Inactivate a contact' })
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
async inactivateContact(@Param('id', ParseIntPipe) contactId: number) {
await this.inactivateContactService.inactivateContact(contactId);
return { id: contactId, inactivated: true };
}
}

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service';
import { ContactsController } from './Contacts.controller';
import { ActivateContactService } from './commands/ActivateContact.service';
import { InactivateContactService } from './commands/InactivateContact.service';
@Module({
providers: [
GetAutoCompleteContactsService,
ActivateContactService,
InactivateContactService,
],
controllers: [ContactsController],
})
export class ContactsModule {}

View File

@@ -0,0 +1,14 @@
import { IFilterRole } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
export interface IContactsAutoCompleteFilter {
limit: number;
keyword: string;
filterRoles?: IFilterRole[];
columnSortBy: string;
sortOrder: string;
}
export interface IContactAutoCompleteItem {
displayName: string;
contactService: string;
}

View File

@@ -0,0 +1,28 @@
import { ServiceError } from '@/modules/Items/ServiceError';
import { Contact } from '../models/Contact';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Inject, Injectable } from '@nestjs/common';
import { ERRORS } from '../Contacts.constants';
@Injectable()
export class ActivateContactService {
constructor(
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
async activateContact(contactId: number) {
const contact = await this.contactModel()
.query()
.findById(contactId)
.throwIfNotFound();
if (contact.active) {
throw new ServiceError(ERRORS.CONTACT_ALREADY_ACTIVE);
}
await this.contactModel()
.query()
.findById(contactId)
.update({ active: true });
}
}

View File

@@ -0,0 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { ServiceError } from '@/modules/Items/ServiceError';
import { Contact } from '../models/Contact';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ERRORS } from '../Contacts.constants';
@Injectable()
export class InactivateContactService {
constructor(
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
async inactivateContact(contactId: number) {
const contact = await this.contactModel()
.query()
.findById(contactId)
.throwIfNotFound();
if (!contact.active) {
throw new ServiceError(ERRORS.CONTACT_ALREADY_INACTIVE);
}
await this.contactModel()
.query()
.findById(contactId)
.update({ active: false });
}
}

View File

@@ -0,0 +1,11 @@
import { IsNumber, IsOptional, IsString } from 'class-validator';
export class GetContactsAutoCompleteQuery {
@IsNumber()
@IsOptional()
limit: number;
@IsString()
@IsOptional()
keyword: string;
}

View File

@@ -0,0 +1,39 @@
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Contact } from '../models/Contact';
import { Inject, Injectable } from '@nestjs/common';
import { IContactsAutoCompleteFilter } from '../Contacts.types';
import { GetContactsAutoCompleteQuery } from '../dtos/GetContactsAutoCompleteQuery.dto';
@Injectable()
export class GetAutoCompleteContactsService {
constructor(
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
/**
* Retrieve auto-complete contacts list.
* @param {number} tenantId -
* @param {IContactsAutoCompleteFilter} contactsFilter -
* @return {IContactAutoCompleteItem}
*/
async autocompleteContacts(queryDto: GetContactsAutoCompleteQuery) {
const _queryDto = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'display_name',
limit: 10,
...queryDto,
};
// Retrieve contacts list by the given query.
const contacts = await this.contactModel()
.query()
.onBuild((builder) => {
if (_queryDto.keyword) {
builder.where('display_name', 'LIKE', `%${_queryDto.keyword}%`);
}
builder.limit(_queryDto.limit);
});
return contacts;
}
}

View File

@@ -36,7 +36,6 @@ function DisconnectBankAccountDialogContent({
values: DisconnectFormValues,
{ setErrors, setSubmitting }: FormikHelpers<DisconnectFormValues>,
) => {
debugger;
setSubmitting(true);
if (values.label !== 'DISCONNECT ACCOUNT') {

View File

@@ -38,7 +38,7 @@ export function useAutoCompleteContacts(props) {
['CONTACTS', 'AUTO-COMPLETE'],
() => apiRequest.get('contacts/auto-complete'),
{
select: (res) => res.data.contacts,
select: (res) => res.data,
defaultData: [],
...props,
},

View File

@@ -67,7 +67,7 @@ export function useCurrencies(props) {
[t.CURRENCIES],
{ method: 'get', url: 'currencies' },
{
select: (res) => res.data.currencies,
select: (res) => res.data,
defaultData: [],
...props
},

View File

@@ -35,7 +35,7 @@ export const useResendInvitation = (props) => {
const apiRequest = useApiRequest();
return useMutation(
(userId) => apiRequest.post(`invite/resend/${userId}`),
(userId) => apiRequest.post(`invite/users/${userId}/resend`),
props
)
}

View File

@@ -101,7 +101,7 @@ export function useUpdateOrganization(props = {}) {
export function useOrgBaseCurrencyMutateAbilities(props) {
return useRequestQuery(
[t.ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES],
{ method: 'get', url: `organization/base_currency_mutate` },
{ method: 'get', url: `organization/base-currency-mutate` },
{
select: (res) => res.data.abilities,
defaultData: [],

View File

@@ -105,7 +105,7 @@ export function useUsers(props) {
url: 'users',
},
{
select: (res) => res.data.users,
select: (res) => res.data,
defaultData: [],
...props,
},
@@ -123,7 +123,7 @@ export function useUser(id, props) {
url: `users/${id}`,
},
{
select: (response) => response.data.user,
select: (response) => response.data,
defaultData: {},
...props,
},
@@ -143,7 +143,6 @@ export function useAuthenticatedAccount(props) {
select: (response) => response.data,
defaultData: {},
onSuccess: (data) => {
debugger;
setEmailConfirmed(data.verified, data.email);
},
...props,