diff --git a/packages/server/src/modules/App/App.module.ts b/packages/server/src/modules/App/App.module.ts index 43b7da758..b04b92c28 100644 --- a/packages/server/src/modules/App/App.module.ts +++ b/packages/server/src/modules/App/App.module.ts @@ -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: [ diff --git a/packages/server/src/modules/Contacts/Contacts.constants.ts b/packages/server/src/modules/Contacts/Contacts.constants.ts new file mode 100644 index 000000000..177196b2c --- /dev/null +++ b/packages/server/src/modules/Contacts/Contacts.constants.ts @@ -0,0 +1,6 @@ + + +export const ERRORS = { + CONTACT_ALREADY_ACTIVE: 'CONTACT_ALREADY_ACTIVE', + CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE' +} \ No newline at end of file diff --git a/packages/server/src/modules/Contacts/Contacts.controller.ts b/packages/server/src/modules/Contacts/Contacts.controller.ts new file mode 100644 index 000000000..6ae1ef37a --- /dev/null +++ b/packages/server/src/modules/Contacts/Contacts.controller.ts @@ -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 }; + } +} diff --git a/packages/server/src/modules/Contacts/Contacts.module.ts b/packages/server/src/modules/Contacts/Contacts.module.ts new file mode 100644 index 000000000..ce7690319 --- /dev/null +++ b/packages/server/src/modules/Contacts/Contacts.module.ts @@ -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 {} diff --git a/packages/server/src/modules/Contacts/Contacts.types.ts b/packages/server/src/modules/Contacts/Contacts.types.ts new file mode 100644 index 000000000..9b1cd32c4 --- /dev/null +++ b/packages/server/src/modules/Contacts/Contacts.types.ts @@ -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; +} diff --git a/packages/server/src/modules/Contacts/commands/ActivateContact.service.ts b/packages/server/src/modules/Contacts/commands/ActivateContact.service.ts new file mode 100644 index 000000000..f777faa29 --- /dev/null +++ b/packages/server/src/modules/Contacts/commands/ActivateContact.service.ts @@ -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, + ) {} + + 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 }); + } +} diff --git a/packages/server/src/modules/Contacts/commands/InactivateContact.service.ts b/packages/server/src/modules/Contacts/commands/InactivateContact.service.ts new file mode 100644 index 000000000..775c8898f --- /dev/null +++ b/packages/server/src/modules/Contacts/commands/InactivateContact.service.ts @@ -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, + ) {} + + 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 }); + } +} diff --git a/packages/server/src/modules/Contacts/dtos/GetContactsAutoCompleteQuery.dto.ts b/packages/server/src/modules/Contacts/dtos/GetContactsAutoCompleteQuery.dto.ts new file mode 100644 index 000000000..5838c9f41 --- /dev/null +++ b/packages/server/src/modules/Contacts/dtos/GetContactsAutoCompleteQuery.dto.ts @@ -0,0 +1,11 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; + +export class GetContactsAutoCompleteQuery { + @IsNumber() + @IsOptional() + limit: number; + + @IsString() + @IsOptional() + keyword: string; +} diff --git a/packages/server/src/modules/Contacts/queries/GetAutoCompleteContacts.service.ts b/packages/server/src/modules/Contacts/queries/GetAutoCompleteContacts.service.ts new file mode 100644 index 000000000..3b87684ea --- /dev/null +++ b/packages/server/src/modules/Contacts/queries/GetAutoCompleteContacts.service.ts @@ -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, + ) {} + + /** + * 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; + } +} diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialogContent.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialogContent.tsx index 0da3dd486..0ec767dc2 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialogContent.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialogContent.tsx @@ -36,7 +36,6 @@ function DisconnectBankAccountDialogContent({ values: DisconnectFormValues, { setErrors, setSubmitting }: FormikHelpers, ) => { - debugger; setSubmitting(true); if (values.label !== 'DISCONNECT ACCOUNT') { diff --git a/packages/webapp/src/hooks/query/contacts.tsx b/packages/webapp/src/hooks/query/contacts.tsx index 5a554034a..3de3e3a77 100644 --- a/packages/webapp/src/hooks/query/contacts.tsx +++ b/packages/webapp/src/hooks/query/contacts.tsx @@ -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, }, diff --git a/packages/webapp/src/hooks/query/currencies.tsx b/packages/webapp/src/hooks/query/currencies.tsx index 187b78ad7..09e755ca9 100644 --- a/packages/webapp/src/hooks/query/currencies.tsx +++ b/packages/webapp/src/hooks/query/currencies.tsx @@ -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 }, diff --git a/packages/webapp/src/hooks/query/invite.tsx b/packages/webapp/src/hooks/query/invite.tsx index 7cc3c7e36..9d7de9ed4 100644 --- a/packages/webapp/src/hooks/query/invite.tsx +++ b/packages/webapp/src/hooks/query/invite.tsx @@ -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 ) } \ No newline at end of file diff --git a/packages/webapp/src/hooks/query/organization.tsx b/packages/webapp/src/hooks/query/organization.tsx index 14a40cda3..6f0baac72 100644 --- a/packages/webapp/src/hooks/query/organization.tsx +++ b/packages/webapp/src/hooks/query/organization.tsx @@ -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: [], diff --git a/packages/webapp/src/hooks/query/users.tsx b/packages/webapp/src/hooks/query/users.tsx index c96a8953f..58cc093c1 100644 --- a/packages/webapp/src/hooks/query/users.tsx +++ b/packages/webapp/src/hooks/query/users.tsx @@ -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,