Compare commits

..

16 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
3bf2803360 feat(trpc): implement tRPC integration for accounts module
- Add tRPC server setup with NestJS (nestjs-trpc)
- Create AccountsTrpcRouter with CRUD operations
- Add tRPC client configuration in webapp
- Create tRPC React hooks for accounts module
- Replace existing REST hooks with tRPC hooks across 35+ files
- Maintain backward compatibility with existing REST API
- Add proper cache invalidation for mutations

New files:
- packages/server/src/modules/Trpc/*
- packages/webapp/src/trpc.ts
- packages/webapp/src/hooks/trpc/*
- shared/bigcapital-utils/src/trpc.ts

Dependencies added:
- @trpc/server, @trpc/client, @trpc/react-query
- nestjs-trpc, superjson
- @tanstack/react-query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 04:34:04 +02:00
Ahmed Bouhuolia
688b1bfb56 fix(server): add invoices Map for validateInvoicesRemainingAmount 2026-02-24 02:52:28 +02:00
Ahmed Bouhuolia
0f8147daff Merge branch 'develop' into fix/credit-note-apply-invoice-validation 2026-02-24 02:42:23 +02:00
Ahmed Bouhuolia
96b24d4fb9 Merge pull request #982 from bigcapitalhq/fix/credit-notes-applied-invoice-delete
fix: Add DELETE endpoint for credit notes applied invoices
2026-02-24 02:17:37 +02:00
Ahmed Bouhuolia
2a87103bc8 fix: Add DELETE endpoint for credit notes applied invoices
- Add missing DELETE /credit-notes/applied-invoices/:id endpoint
- Fix CreditNotesApplyInvoice controller to use correct service methods
- Add missing GetCreditNoteAssociatedInvoicesToApply endpoint
- Add proper DTO for ApplyCreditNoteToInvoices
- Update frontend creditNote hook to use correct API paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 02:15:32 +02:00
Ahmed Bouhuolia
238b60144f Merge pull request #981 from bigcapitalhq/fix/register-verify-dark-mode
fix: add dark mode support to email confirmation UI
2026-02-23 01:10:50 +02:00
Ahmed Bouhuolia
fcee85e358 fix: add dark mode support to email confirmation UI
Refactored RegisterVerify component to use xstyled for styling
with proper dark mode color values instead of hardcoded light theme colors.
2026-02-23 00:48:32 +02:00
Ahmed Bouhuolia
64a10053e3 Merge pull request #980 from bigcapitalhq/fix-signup-verification
fix: signup confirmation
2026-02-23 00:39:45 +02:00
Ahmed Bouhuolia
ce9f2a238f fix: signup confirmation 2026-02-23 00:37:56 +02:00
Yong ke Weng
75b98c39d8 fix: validate credit note per-entry amount against each invoice due amount
The `validateInvoicesRemainingAmount` method was incorrectly comparing the
total credit amount (sum of all entries) against each individual invoice's
due amount. This caused valid credit note applications to be rejected when
applying to multiple invoices where the total exceeded any single invoice's
due amount.

Changed the validation to compare each invoice's due amount against only the
specific entry amount being applied to that invoice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 09:50:39 -05:00
Ahmed Bouhuolia
80e545072d Merge pull request #975 from bigcapitalhq/fix/localize-financial-reports
fix: localize hardcoded strings in financial reports
2026-02-18 22:09:05 +02:00
Ahmed Bouhuolia
de3d4698ea fix: localize hardcoded strings in financial reports
- Fix cash_flow_statement.net_cash_investing not being localized
- Add translation keys for Account name, Total, sheet name, From/To dates
- Create contact_summary_balance.json for Customer/Vendor Balance Summary
- Create trial_balance_sheet.json for Trial Balance Sheet columns
- Create inventory_item_details.json for Inventory Item Details
- Create transactions_by_contact.json for Transactions by Contact
- Fix hardcoded strings in TrialBalanceSheetTable column labels
- Fix hardcoded 'Total' in CustomerBalanceSummary and VendorBalanceSummary
- Fix hardcoded column headers in InventoryItemDetailsTable
- Fix hardcoded Opening/Closing balance strings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:07:00 +02:00
Ahmed Bouhuolia
171091e0e0 Merge pull request #972 from bigcapitalhq/fix/ahmedbouhuolia/invite-user-service
fix: invite user service issues
2026-02-18 12:33:57 +02:00
Ahmed Bouhuolia
78032d7bfc fix: invite user service issues 2026-02-18 12:32:04 +02:00
Ahmed Bouhuolia
06b8a836c5 Merge pull request #967 from bigcapitalhq/fix/ahmedbouhuolia/baseurl-config-key
fix: correct config key for base URL in email services
2026-02-18 01:31:08 +02:00
Ahmed Bouhuolia
37fa9f9bc6 fix: correct config key for base URL in email services 2026-02-18 01:28:48 +02:00
91 changed files with 956 additions and 241 deletions

View File

@@ -127,7 +127,10 @@
"uuid": "^10.0.0", "uuid": "^10.0.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"yup": "^0.28.1", "yup": "^0.28.1",
"zod": "^3.23.8" "zod": "^3.23.8",
"@trpc/server": "^11.0.0-rc.648",
"nestjs-trpc": "^1.6.1",
"superjson": "^2.2.2"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",

View File

@@ -9,5 +9,10 @@
"net_cash_financing": "Net cash provided by financing activities", "net_cash_financing": "Net cash provided by financing activities",
"cash_beginning_period": "Cash at beginning of period", "cash_beginning_period": "Cash at beginning of period",
"net_cash_increase": "NET CASH INCREASE FOR PERIOD", "net_cash_increase": "NET CASH INCREASE FOR PERIOD",
"cash_end_period": "CASH AT END OF PERIOD" "cash_end_period": "CASH AT END OF PERIOD",
"account_name": "Account name",
"total": "Total",
"sheet_name": "Statement of Cash Flow",
"from_date": "From",
"to_date": "To"
} }

View File

@@ -0,0 +1,5 @@
{
"account_name": "Account name",
"total": "Total",
"percentage_column": "% of Column"
}

View File

@@ -0,0 +1,14 @@
{
"opening_balance": "Opening balance",
"closing_balance": "Closing balance",
"date": "Date",
"transaction_type": "Transaction type",
"transaction_number": "Transaction #",
"quantity": "Quantity",
"rate": "Rate",
"total": "Total",
"value": "Value",
"profit_margin": "Profit Margin",
"running_quantity": "Running quantity",
"running_value": "Running Value"
}

View File

@@ -0,0 +1,4 @@
{
"opening_balance": "Opening balance",
"closing_balance": "Closing balance"
}

View File

@@ -0,0 +1,6 @@
{
"account": "Account",
"debit": "Debit",
"credit": "Credit",
"total": "Total"
}

View File

@@ -104,6 +104,7 @@ import { BillLandedCostsModule } from '../BillLandedCosts/BillLandedCosts.module
import { SocketModule } from '../Socket/Socket.module'; import { SocketModule } from '../Socket/Socket.module';
import { ThrottlerGuard } from '@nestjs/throttler'; import { ThrottlerGuard } from '@nestjs/throttler';
import { AppThrottleModule } from './AppThrottle.module'; import { AppThrottleModule } from './AppThrottle.module';
import { AppTrpcModule } from '../Trpc/Trpc.module';
@Module({ @Module({
imports: [ imports: [
@@ -256,6 +257,7 @@ import { AppThrottleModule } from './AppThrottle.module';
UsersModule, UsersModule,
ContactsModule, ContactsModule,
SocketModule, SocketModule,
AppTrpcModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [

View File

@@ -1,5 +1,5 @@
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import bluebird from 'bluebird'; import * as bluebird from 'bluebird';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
validateLinkModelEntryExists, validateLinkModelEntryExists,
@@ -53,7 +53,8 @@ export class LinkAttachment {
const foundLinkModel = await LinkModel().query(trx).findById(modelId); const foundLinkModel = await LinkModel().query(trx).findById(modelId);
validateLinkModelEntryExists(foundLinkModel); validateLinkModelEntryExists(foundLinkModel);
const foundLinks = await this.documentLinkModel().query(trx) const foundLinks = await this.documentLinkModel()
.query(trx)
.where('modelRef', modelRef) .where('modelRef', modelRef)
.where('modelId', modelId) .where('modelId', modelId)
.where('documentId', foundFile.id); .where('documentId', foundFile.id);

View File

@@ -65,7 +65,7 @@ export class AuthController {
return this.authApp.signUp(signupDto); return this.authApp.signUp(signupDto);
} }
@Post('/signup/confirm') @Post('/signup/verify')
@ApiOperation({ summary: 'Confirm user signup' }) @ApiOperation({ summary: 'Confirm user signup' })
@ApiBody({ @ApiBody({
schema: { schema: {

View File

@@ -20,7 +20,7 @@ export class AuthenticationMailMesssages {
* @returns {Mail} * @returns {Mail}
*/ */
resetPasswordMessage(user: ModelObject<SystemUser>, token: string) { resetPasswordMessage(user: ModelObject<SystemUser>, token: string) {
const baseURL = this.configService.get('baseURL'); const baseURL = this.configService.get('app.baseUrl');
return new Mail() return new Mail()
.setSubject('Bigcapital - Password Reset') .setSubject('Bigcapital - Password Reset')
@@ -54,7 +54,7 @@ export class AuthenticationMailMesssages {
* @returns {Mail} * @returns {Mail}
*/ */
signupVerificationMail(email: string, fullName: string, token: string) { signupVerificationMail(email: string, fullName: string, token: string) {
const baseURL = this.configService.get('baseURL'); const baseURL = this.configService.get('app.baseUrl');
const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`; const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
return new Mail() return new Mail()

View File

@@ -7,17 +7,13 @@ import {
import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service'; import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service';
import { Controller, Get, Post } from '@nestjs/common'; import { Controller, Get, Post } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler'; import { Throttle } from '@nestjs/throttler';
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards'; import { TenantAgnosticRoute } from '../Tenancy/TenancyGlobal.guard';
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
import { AuthenticationApplication } from './AuthApplication.sevice'; import { AuthenticationApplication } from './AuthApplication.sevice';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard'; import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard';
@Controller('/auth') @Controller('/auth')
@ApiTags('Auth') @ApiTags('Auth')
@ApiExcludeController() @TenantAgnosticRoute()
@IgnoreTenantSeededRoute()
@IgnoreTenantInitializedRoute()
@IgnoreUserVerifiedRoute() @IgnoreUserVerifiedRoute()
@Throttle({ auth: {} }) @Throttle({ auth: {} })
export class AuthedController { export class AuthedController {

View File

@@ -13,7 +13,6 @@ import {
IAuthSignedUpEventPayload, IAuthSignedUpEventPayload,
IAuthSigningUpEventPayload, IAuthSigningUpEventPayload,
} from '../Auth.interfaces'; } from '../Auth.interfaces';
import { defaultTo } from 'ramda';
import { ERRORS } from '../Auth.constants'; import { ERRORS } from '../Auth.constants';
import { hashPassword } from '../Auth.utils'; import { hashPassword } from '../Auth.utils';
import { ClsService } from 'nestjs-cls'; import { ClsService } from 'nestjs-cls';
@@ -51,7 +50,7 @@ export class AuthSignupService {
const signupConfirmation = this.configService.get('signupConfirmation'); const signupConfirmation = this.configService.get('signupConfirmation');
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex'); const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
const verifiedEnabed = defaultTo(signupConfirmation.enabled, false); const verifiedEnabed = signupConfirmation.enabled ?? false;
const verifyToken = verifiedEnabed ? verifyTokenCrypto : ''; const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
const verified = !verifiedEnabed; const verified = !verifiedEnabed;

View File

@@ -4,7 +4,6 @@ import { SystemUser } from '@/modules/System/models/SystemUser';
import { ServiceError } from '@/modules/Items/ServiceError'; import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../Auth.constants'; import { ERRORS } from '../Auth.constants';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { ModelObject } from 'objection';
import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces'; import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';

View File

@@ -1,6 +1,7 @@
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
Param, Param,
Post, Post,
@@ -14,6 +15,10 @@ import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard'; import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types'; import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types'; import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNoteApplyToInvoices } from './commands/CreditNoteApplyToInvoices.service';
import { DeleteCreditNoteApplyToInvoices } from './commands/DeleteCreditNoteApplyToInvoices.service';
import { ApplyCreditNoteToInvoicesDto } from './dtos/ApplyCreditNoteToInvoices.dto';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Notes Apply Invoice') @ApiTags('Credit Notes Apply Invoice')
@@ -22,6 +27,9 @@ import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
export class CreditNotesApplyInvoiceController { export class CreditNotesApplyInvoiceController {
constructor( constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices, private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
private readonly getCreditNoteAssociatedInvoicesToApplyService: GetCreditNoteAssociatedInvoicesToApply,
private readonly creditNoteApplyToInvoicesService: CreditNoteApplyToInvoices,
private readonly deleteCreditNoteApplyToInvoicesService: DeleteCreditNoteApplyToInvoices,
) {} ) {}
@Get(':creditNoteId/applied-invoices') @Get(':creditNoteId/applied-invoices')
@@ -39,6 +47,23 @@ export class CreditNotesApplyInvoiceController {
); );
} }
@Get(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get credit note associated invoices to apply' })
@ApiResponse({
status: 200,
description: 'Credit note associated invoices to apply',
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
getCreditNoteAssociatedInvoicesToApply(
@Param('creditNoteId') creditNoteId: number,
) {
return this.getCreditNoteAssociatedInvoicesToApplyService.getCreditAssociatedInvoicesToApply(
creditNoteId,
);
}
@Post(':creditNoteId/apply-invoices') @Post(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote) @RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Apply credit note to invoices' }) @ApiOperation({ summary: 'Apply credit note to invoices' })
@@ -48,9 +73,32 @@ export class CreditNotesApplyInvoiceController {
}) })
@ApiResponse({ status: 404, description: 'Credit note not found' }) @ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' }) @ApiResponse({ status: 400, description: 'Invalid input data' })
applyCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) { applyCreditNoteToInvoices(
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices( @Param('creditNoteId') creditNoteId: number,
@Body() applyDto: ApplyCreditNoteToInvoicesDto,
) {
return this.creditNoteApplyToInvoicesService.applyCreditNoteToInvoices(
creditNoteId, creditNoteId,
applyDto,
);
}
@Delete('applied-invoices/:applyCreditToInvoicesId')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete applied credit note to invoice' })
@ApiResponse({
status: 200,
description: 'Credit note application successfully deleted',
})
@ApiResponse({
status: 404,
description: 'Credit note application not found',
})
deleteApplyCreditNoteToInvoices(
@Param('applyCreditToInvoicesId') applyCreditToInvoicesId: number,
) {
return this.deleteCreditNoteApplyToInvoicesService.deleteApplyCreditNoteToInvoices(
applyCreditToInvoicesId,
); );
} }
} }

View File

@@ -9,6 +9,8 @@ import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service'; import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service'; import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller'; import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
import { CreditNoteApplySyncCreditSubscriber } from './subscribers/CreditNoteApplySyncCreditSubscriber';
import { CreditNoteApplySyncInvoicesCreditedAmountSubscriber } from './subscribers/CreditNoteApplySyncInvoicesSubscriber';
@Module({ @Module({
providers: [ providers: [
@@ -19,6 +21,8 @@ import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.con
CreditNoteApplySyncCredit, CreditNoteApplySyncCredit,
GetCreditNoteAssociatedAppliedInvoices, GetCreditNoteAssociatedAppliedInvoices,
GetCreditNoteAssociatedInvoicesToApply, GetCreditNoteAssociatedInvoicesToApply,
CreditNoteApplySyncCreditSubscriber,
CreditNoteApplySyncInvoicesCreditedAmountSubscriber,
], ],
exports: [DeleteCustomerLinkedCreditNoteService], exports: [DeleteCustomerLinkedCreditNoteService],
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)], imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],

View File

@@ -1,6 +1,6 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types'; import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice'; import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';

View File

@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { sumBy } from 'lodash'; import { sumBy } from 'lodash';
import { import {
ICreditNoteAppliedToInvoice,
ICreditNoteAppliedToInvoiceModel, ICreditNoteAppliedToInvoiceModel,
IApplyCreditToInvoicesDTO, IApplyCreditToInvoicesDTO,
IApplyCreditToInvoicesCreatedPayload, IApplyCreditToInvoicesCreatedPayload,
@@ -17,6 +18,7 @@ import { CreditNote } from '@/modules/CreditNotes/models/CreditNote';
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice'; import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';
import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service'; import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ApplyCreditNoteToInvoicesDto } from '../dtos/ApplyCreditNoteToInvoices.dto';
@Injectable() @Injectable()
export class CreditNoteApplyToInvoices { export class CreditNoteApplyToInvoices {
@@ -48,7 +50,7 @@ export class CreditNoteApplyToInvoices {
*/ */
public async applyCreditNoteToInvoices( public async applyCreditNoteToInvoices(
creditNoteId: number, creditNoteId: number,
applyCreditToInvoicesDTO: IApplyCreditToInvoicesDTO, applyCreditToInvoicesDTO: ApplyCreditNoteToInvoicesDto,
): Promise<CreditNoteAppliedInvoice[]> { ): Promise<CreditNoteAppliedInvoice[]> {
// Saves the credit note or throw not found service error. // Saves the credit note or throw not found service error.
const creditNote = await this.creditNoteModel() const creditNote = await this.creditNoteModel()
@@ -71,7 +73,7 @@ export class CreditNoteApplyToInvoices {
// Validate invoices has remaining amount to apply. // Validate invoices has remaining amount to apply.
this.validateInvoicesRemainingAmount( this.validateInvoicesRemainingAmount(
appliedInvoicesEntries, appliedInvoicesEntries,
creditNoteAppliedModel.amount, creditNoteAppliedModel.entries,
); );
// Validate the credit note remaining amount. // Validate the credit note remaining amount.
this.creditNoteDTOTransform.validateCreditRemainingAmount( this.creditNoteDTOTransform.validateCreditRemainingAmount(
@@ -122,18 +124,20 @@ export class CreditNoteApplyToInvoices {
}; };
/** /**
* Validate the invoice remaining amount. * Validate each invoice has sufficient remaining amount for the applied credit.
* @param {ISaleInvoice[]} invoices * @param {ISaleInvoice[]} invoices
* @param {number} amount * @param {ICreditNoteAppliedToInvoice[]} entries
*/ */
private validateInvoicesRemainingAmount = ( private validateInvoicesRemainingAmount = (
invoices: SaleInvoice[], invoices: SaleInvoice[],
amount: number, entries: ICreditNoteAppliedToInvoice[],
) => { ) => {
const invalidInvoices = invoices.filter( const invoiceMap = new Map(invoices.map((inv) => [inv.id, inv]));
(invoice) => invoice.dueAmount < amount, const invalidEntries = entries.filter((entry) => {
); const invoice = invoiceMap.get(entry.invoiceId);
if (invalidInvoices.length > 0) { return invoice != null && invoice.dueAmount < entry.amount;
});
if (invalidEntries.length > 0) {
throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT); throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT);
} }
}; };

View File

@@ -0,0 +1,38 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsInt,
IsNotEmpty,
IsNumber,
ValidateNested,
} from 'class-validator';
export class ApplyCreditNoteInvoiceEntryDto {
@IsNotEmpty()
@IsInt()
@ApiProperty({ description: 'Invoice ID to apply credit to', example: 1 })
invoiceId: number;
@IsNotEmpty()
@IsNumber()
@ApiProperty({ description: 'Amount to apply', example: 100.5 })
amount: number;
}
export class ApplyCreditNoteToInvoicesDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => ApplyCreditNoteInvoiceEntryDto)
@ApiProperty({
description: 'Entries of invoice ID and amount to apply',
type: [ApplyCreditNoteInvoiceEntryDto],
example: [
{ invoice_id: 1, amount: 100.5 },
{ invoice_id: 2, amount: 50 },
],
})
entries: ApplyCreditNoteInvoiceEntryDto[];
}

View File

@@ -8,7 +8,7 @@ import { CreditNoteApplySyncInvoicesCreditedAmount } from '../commands/CreditNot
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
@Injectable() @Injectable()
export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber { export class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
constructor( constructor(
private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount, private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount,
) {} ) {}

View File

@@ -29,6 +29,7 @@ export interface IApplyCreditToInvoicesDeletedPayload {
export interface ICreditNoteAppliedToInvoice { export interface ICreditNoteAppliedToInvoice {
amount: number; amount: number;
creditNoteId: number; creditNoteId: number;
invoiceId: number;
} }
export interface ICreditNoteAppliedToInvoiceModel { export interface ICreditNoteAppliedToInvoiceModel {
amount: number; amount: number;

View File

@@ -239,7 +239,7 @@ export class CashFlowTable {
section: ICashFlowStatementSection, section: ICashFlowStatementSection,
): ICashFlowStatementSection => { ): ICashFlowStatementSection => {
const label = section.footerLabel const label = section.footerLabel
? section.footerLabel ? this.i18n.t(section.footerLabel)
: this.i18n.t('financial_sheet.total_row', { : this.i18n.t('financial_sheet.total_row', {
args: { value: section.label }, args: { value: section.label },
}); });
@@ -302,7 +302,7 @@ export class CashFlowTable {
* @returns {ITableColumn} * @returns {ITableColumn}
*/ */
private totalColumns = (): ITableColumn[] => { private totalColumns = (): ITableColumn[] => {
return [{ key: 'total', label: this.i18n.t('Total') }]; return [{ key: 'total', label: this.i18n.t('cash_flow_statement.total') }];
}; };
/** /**
@@ -366,7 +366,7 @@ export class CashFlowTable {
*/ */
public tableColumns = (): ITableColumn[] => { public tableColumns = (): ITableColumn[] => {
return R.compose( return R.compose(
R.concat([{ key: 'name', label: this.i18n.t('Account name') }]), R.concat([{ key: 'name', label: this.i18n.t('cash_flow_statement.account_name') }]),
R.when( R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumns()), R.concat(this.datePeriodsColumns()),

View File

@@ -1,5 +1,6 @@
import * as moment from 'moment'; import * as moment from 'moment';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta'; import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import { import {
ICashFlowStatementMeta, ICashFlowStatementMeta,
@@ -8,7 +9,10 @@ import {
@Injectable() @Injectable()
export class CashflowSheetMeta { export class CashflowSheetMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {} constructor(
private readonly financialSheetMeta: FinancialSheetMeta,
private readonly i18n: I18nService,
) {}
/** /**
* Cashflow sheet meta. * Cashflow sheet meta.
@@ -21,9 +25,11 @@ export class CashflowSheetMeta {
const meta = await this.financialSheetMeta.meta(); const meta = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD'); const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD'); const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`; const fromLabel = this.i18n.t('cash_flow_statement.from_date');
const toLabel = this.i18n.t('cash_flow_statement.to_date');
const formattedDateRange = `${fromLabel} ${formattedFromDate} | ${toLabel} ${formattedToDate}`;
const sheetName = 'Statement of Cash Flow'; const sheetName = this.i18n.t('cash_flow_statement.sheet_name');
return { return {
...meta, ...meta,

View File

@@ -91,7 +91,7 @@ export class CustomerBalanceSummaryTable {
*/ */
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => { private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [ const columns = [
{ key: 'name', value: this.i18n.t('Total') }, { key: 'name', value: this.i18n.t('contact_summary_balance.total') },
{ key: 'total', accessor: 'total.formattedAmount' }, { key: 'total', accessor: 'total.formattedAmount' },
]; ];
// @ts-ignore // @ts-ignore

View File

@@ -93,7 +93,7 @@ export class InventoryItemDetailsTable {
): ITableRow => { ): ITableRow => {
const columns: Array<IColumnMapperMeta> = [ const columns: Array<IColumnMapperMeta> = [
{ key: 'date', accessor: 'date.formattedDate' }, { key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: this.i18n.t('Opening balance') }, { key: 'closing', value: this.i18n.t('inventory_item_details.opening_balance') },
{ key: 'empty', value: '' }, { key: 'empty', value: '' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' }, { key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty', value: '' }, { key: 'empty', value: '' },
@@ -115,7 +115,7 @@ export class InventoryItemDetailsTable {
): ITableRow => { ): ITableRow => {
const columns: Array<IColumnMapperMeta> = [ const columns: Array<IColumnMapperMeta> = [
{ key: 'date', accessor: 'date.formattedDate' }, { key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: this.i18n.t('Closing balance') }, { key: 'closing', value: this.i18n.t('inventory_item_details.closing_balance') },
{ key: 'empty', value: '' }, { key: 'empty', value: '' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' }, { key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty', value: '' }, { key: 'empty', value: '' },
@@ -193,16 +193,16 @@ export class InventoryItemDetailsTable {
*/ */
public tableColumns = (): ITableColumn[] => { public tableColumns = (): ITableColumn[] => {
return [ return [
{ key: 'date', label: this.i18n.t('Date') }, { key: 'date', label: this.i18n.t('inventory_item_details.date') },
{ key: 'transaction_type', label: this.i18n.t('Transaction type') }, { key: 'transaction_type', label: this.i18n.t('inventory_item_details.transaction_type') },
{ key: 'transaction_id', label: this.i18n.t('Transaction #') }, { key: 'transaction_id', label: this.i18n.t('inventory_item_details.transaction_number') },
{ key: 'quantity', label: this.i18n.t('Quantity') }, { key: 'quantity', label: this.i18n.t('inventory_item_details.quantity') },
{ key: 'rate', label: this.i18n.t('Rate') }, { key: 'rate', label: this.i18n.t('inventory_item_details.rate') },
{ key: 'total', label: this.i18n.t('Total') }, { key: 'total', label: this.i18n.t('inventory_item_details.total') },
{ key: 'value', label: this.i18n.t('Value') }, { key: 'value', label: this.i18n.t('inventory_item_details.value') },
{ key: 'profit_margin', label: this.i18n.t('Profit Margin') }, { key: 'profit_margin', label: this.i18n.t('inventory_item_details.profit_margin') },
{ key: 'running_quantity', label: this.i18n.t('Running quantity') }, { key: 'running_quantity', label: this.i18n.t('inventory_item_details.running_quantity') },
{ key: 'running_value', label: this.i18n.t('Running Value') }, { key: 'running_value', label: this.i18n.t('inventory_item_details.running_value') },
]; ];
}; };
} }

View File

@@ -52,7 +52,7 @@ export class TransactionsByContactsTableRows {
const columns = [ const columns = [
{ {
key: 'openingBalanceLabel', key: 'openingBalanceLabel',
value: this.i18n.t('Opening balance') as string, value: this.i18n.t('transactions_by_contact.opening_balance') as string,
}, },
...R.repeat({ key: 'empty', value: '' }, 5), ...R.repeat({ key: 'empty', value: '' }, 5),
{ {
@@ -76,7 +76,7 @@ export class TransactionsByContactsTableRows {
const columns = [ const columns = [
{ {
key: 'closingBalanceLabel', key: 'closingBalanceLabel',
value: this.i18n.t('Closing balance') as string, value: this.i18n.t('transactions_by_contact.closing_balance') as string,
}, },
...R.repeat({ key: 'empty', value: '' }, 5), ...R.repeat({ key: 'empty', value: '' }, 5),
{ {

View File

@@ -141,10 +141,10 @@ export class TrialBalanceSheetTable extends R.compose(
return R.compose( return R.compose(
this.tableColumnsCellIndexing, this.tableColumnsCellIndexing,
R.concat([ R.concat([
{ key: 'account', label: 'Account' }, { key: 'account', label: this.i18n.t('trial_balance_sheet.account') },
{ key: 'debit', label: 'Debit' }, { key: 'debit', label: this.i18n.t('trial_balance_sheet.debit') },
{ key: 'credit', label: 'Credit' }, { key: 'credit', label: this.i18n.t('trial_balance_sheet.credit') },
{ key: 'total', label: 'Total' }, { key: 'total', label: this.i18n.t('trial_balance_sheet.total') },
]), ]),
)([]); )([]);
}; };

View File

@@ -91,7 +91,7 @@ export class VendorBalanceSummaryTable {
*/ */
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => { private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [ const columns = [
{ key: 'name', value: this.i18n.t('Total') }, { key: 'name', value: this.i18n.t('contact_summary_balance.total') },
{ key: 'total', accessor: 'total.formattedAmount' }, { key: 'total', accessor: 'total.formattedAmount' },
]; ];
return R.compose( return R.compose(

View File

@@ -28,6 +28,7 @@ import { UpdateOrganizationService } from './commands/UpdateOrganization.service
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard'; import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards'; import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
import { IgnoreTenantModelsInitialize } from '../Tenancy/TenancyInitializeModels.guard'; import { IgnoreTenantModelsInitialize } from '../Tenancy/TenancyInitializeModels.guard';
import { IgnoreUserVerifiedRoute } from '../Auth/guards/EnsureUserVerified.guard';
import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service'; import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service';
import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service'; import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service';
import { import {
@@ -93,6 +94,7 @@ export class OrganizationController {
@Get('current') @Get('current')
@HttpCode(200) @HttpCode(200)
@IgnoreUserVerifiedRoute()
@ApiOperation({ summary: 'Get current organization' }) @ApiOperation({ summary: 'Get current organization' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -8,6 +8,7 @@ import {
import { TenancyContext } from './TenancyContext.service'; import { TenancyContext } from './TenancyContext.service';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants'; import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
import { IS_TENANT_AGNOSTIC } from './TenancyGlobal.guard';
export const IS_IGNORE_TENANT_INITIALIZED = 'IS_IGNORE_TENANT_INITIALIZED'; export const IS_IGNORE_TENANT_INITIALIZED = 'IS_IGNORE_TENANT_INITIALIZED';
export const IgnoreTenantInitializedRoute = () => export const IgnoreTenantInitializedRoute = () =>
@@ -35,8 +36,12 @@ export class EnsureTenantIsInitializedGuard implements CanActivate {
IS_PUBLIC_ROUTE, IS_PUBLIC_ROUTE,
[context.getHandler(), context.getClass()], [context.getHandler(), context.getClass()],
); );
const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
IS_TENANT_AGNOSTIC,
[context.getHandler(), context.getClass()],
);
// Skip the guard early if the route marked as public or ignored. // Skip the guard early if the route marked as public or ignored.
if (isPublic || isIgnoreEnsureTenantInitialized) { if (isPublic || isIgnoreEnsureTenantInitialized || isTenantAgnostic) {
return true; return true;
} }
const tenant = await this.tenancyContext.getTenant(); const tenant = await this.tenancyContext.getTenant();

View File

@@ -9,6 +9,7 @@ import {
import { TenancyContext } from './TenancyContext.service'; import { TenancyContext } from './TenancyContext.service';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants'; import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
import { IS_TENANT_AGNOSTIC } from './TenancyGlobal.guard';
export const IS_IGNORE_TENANT_SEEDED = 'IS_IGNORE_TENANT_SEEDED'; export const IS_IGNORE_TENANT_SEEDED = 'IS_IGNORE_TENANT_SEEDED';
export const IgnoreTenantSeededRoute = () => export const IgnoreTenantSeededRoute = () =>
@@ -36,7 +37,12 @@ export class EnsureTenantIsSeededGuard implements CanActivate {
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]); ]);
if (isPublic || isIgnoreEnsureTenantSeeded) { const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
IS_TENANT_AGNOSTIC,
[context.getHandler(), context.getClass()],
);
// Skip the guard early if the route marked as public, tenant agnostic or ignored.
if (isPublic || isIgnoreEnsureTenantSeeded || isTenantAgnostic) {
return true; return true;
} }
const tenant = await this.tenancyContext.getTenant(); const tenant = await this.tenancyContext.getTenant();

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { TRPCContext, ContextOptions } from 'nestjs-trpc';
@Injectable()
export class TrpcContext implements TRPCContext {
async create(opts: ContextOptions): Promise<Record<string, unknown>> {
const { req } = opts;
// Extract auth token and organization from headers
const token = req.headers['x-access-token'];
const organizationId = req.headers['organization-id'];
return {
token,
organizationId: organizationId ? parseInt(organizationId as string, 10) : null,
req,
};
}
}

View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TRPCModule } from 'nestjs-trpc';
import { TrpcService } from './Trpc.service';
import { TrpcContext } from './Trpc.context';
import { AccountsTrpcRouter } from './routers/Accounts.router';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
@Module({
imports: [
TRPCModule.forRoot({
basePath: '/api/trpc',
context: TrpcContext,
}),
AccountsModule,
],
providers: [TrpcService, TrpcContext, AccountsTrpcRouter],
exports: [TrpcService],
})
export class AppTrpcModule {}

View File

@@ -0,0 +1,13 @@
import { Injectable } from '@nestjs/common';
import { Request, Response } from 'express';
export interface TrpcContext {
req: Request;
res: Response;
user: any;
organizationId: number | null;
}
@Injectable()
export class TrpcService {
}

View File

@@ -0,0 +1,192 @@
import { Injectable } from '@nestjs/common';
import { Router, Query, Mutation } from 'nestjs-trpc';
import { z } from 'zod';
import { AccountsApplication } from '@/modules/Accounts/AccountsApplication.service';
import { CreateAccountDTO } from '@/modules/Accounts/CreateAccount.dto';
import { EditAccountDTO } from '@/modules/Accounts/EditAccount.dto';
import { IAccountsStructureType } from '@/modules/Accounts/Accounts.types';
const accountResponseSchema = z.object({
id: z.number(),
name: z.string(),
slug: z.string(),
code: z.string(),
index: z.number(),
accountType: z.string(),
accountTypeLabel: z.string(),
parentAccountId: z.number().nullable(),
predefined: z.boolean(),
currencyCode: z.string(),
active: z.boolean(),
bankBalance: z.number(),
bankBalanceFormatted: z.string(),
lastFeedsUpdatedAt: z.union([z.string(), z.date(), z.null()]),
lastFeedsUpdatedAtFormatted: z.string(),
amount: z.number(),
formattedAmount: z.string(),
plaidItemId: z.string(),
plaidAccountId: z.string().nullable(),
isFeedsActive: z.boolean(),
isSyncingOwner: z.boolean(),
isFeedsPaused: z.boolean(),
accountNormal: z.string(),
accountNormalFormatted: z.string(),
flattenName: z.string(),
accountLevel: z.number().optional(),
createdAt: z.date(),
updatedAt: z.date(),
});
const accountTypeSchema = z.object({
label: z.string(),
key: z.string(),
normal: z.string(),
parentType: z.string(),
rootType: z.string(),
multiCurrency: z.boolean(),
balanceSheet: z.boolean(),
incomeSheet: z.boolean(),
});
const getAccountsQuerySchema = z.object({
onlyInactive: z.boolean().optional(),
structure: z.nativeEnum(IAccountsStructureType).optional(),
page: z.number().optional(),
pageSize: z.number().optional(),
searchKeyword: z.string().optional(),
});
const getAccountsResponseSchema = z.object({
accounts: z.array(z.any()),
filterMeta: z.object({
count: z.number(),
total: z.number(),
page: z.number(),
pageSize: z.number(),
}),
});
const getAccountTransactionsQuerySchema = z.object({
accountId: z.number(),
});
const createAccountInputSchema = z.object({
name: z.string().min(3).max(255),
code: z.string().min(3).max(6).optional(),
currencyCode: z.string().optional(),
accountType: z.string().min(3).max(255),
description: z.string().max(65535).optional(),
parentAccountId: z.number().optional(),
active: z.boolean().optional(),
plaidAccountId: z.string().optional(),
plaidItemId: z.string().optional(),
});
const editAccountInputSchema = createAccountInputSchema.partial();
const bulkDeleteInputSchema = z.object({
ids: z.array(z.number()),
skipUndeletable: z.boolean().optional(),
});
const validateBulkDeleteResponseSchema = z.object({
deletableIds: z.array(z.number()),
nonDeletableIds: z.array(z.number()),
deletableCount: z.number(),
nonDeletableCount: z.number(),
});
@Injectable()
@Router({ alias: 'accounts' })
export class AccountsTrpcRouter {
constructor(private readonly accountsApplication: AccountsApplication) {}
@Query({
input: getAccountsQuerySchema,
output: getAccountsResponseSchema,
})
async getAccounts(input: z.infer<typeof getAccountsQuerySchema>) {
return this.accountsApplication.getAccounts(input);
}
@Query({
input: z.object({ id: z.number() }),
output: accountResponseSchema,
})
async getAccount(input: { id: number }) {
return this.accountsApplication.getAccount(input.id);
}
@Query({
output: z.array(accountTypeSchema),
})
async getAccountTypes() {
return this.accountsApplication.getAccountTypes();
}
@Query({
input: getAccountTransactionsQuerySchema,
output: z.array(z.any()),
})
async getAccountTransactions(input: z.infer<typeof getAccountTransactionsQuerySchema>) {
return this.accountsApplication.getAccountsTransactions({
accountId: input.accountId,
limit: undefined,
});
}
@Mutation({
input: createAccountInputSchema,
})
async createAccount(input: z.infer<typeof createAccountInputSchema>) {
return this.accountsApplication.createAccount(input as CreateAccountDTO);
}
@Mutation({
input: z.object({
id: z.number(),
data: editAccountInputSchema,
}),
})
async editAccount(input: { id: number; data: any }) {
return this.accountsApplication.editAccount(input.id, input.data as EditAccountDTO);
}
@Mutation({
input: z.object({ id: z.number() }),
})
async deleteAccount(input: { id: number }) {
return this.accountsApplication.deleteAccount(input.id);
}
@Mutation({
input: z.object({ id: z.number() }),
})
async activateAccount(input: { id: number }) {
return this.accountsApplication.activateAccount(input.id);
}
@Mutation({
input: z.object({ id: z.number() }),
})
async inactivateAccount(input: { id: number }) {
return this.accountsApplication.inactivateAccount(input.id);
}
@Mutation({
input: bulkDeleteInputSchema,
})
async bulkDeleteAccounts(input: z.infer<typeof bulkDeleteInputSchema>) {
return this.accountsApplication.bulkDeleteAccounts(input.ids, {
skipUndeletable: input.skipUndeletable ?? false,
});
}
@Mutation({
input: z.object({ ids: z.array(z.number()) }),
output: validateBulkDeleteResponseSchema,
})
async validateBulkDeleteAccounts(input: { ids: number[] }) {
return this.accountsApplication.validateBulkDeleteAccounts(input.ids);
}
}

View File

@@ -20,6 +20,7 @@ import { GetUsersService } from './queries/GetUsers.service';
import { AcceptInviteUserService } from './commands/AcceptInviteUser.service'; import { AcceptInviteUserService } from './commands/AcceptInviteUser.service';
import { InviteTenantUserService } from './commands/InviteUser.service'; import { InviteTenantUserService } from './commands/InviteUser.service';
import { UsersInviteController } from './UsersInvite.controller'; import { UsersInviteController } from './UsersInvite.controller';
import { UsersInvitePublicController } from './UsersInvitePublic.controller';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module'; import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { SendInviteUserMailQueue } from './Users.constants'; import { SendInviteUserMailQueue } from './Users.constants';
import InviteSendMainNotificationSubscribe from './subscribers/InviteSendMailNotification.subscriber'; import InviteSendMainNotificationSubscribe from './subscribers/InviteSendMailNotification.subscriber';
@@ -60,6 +61,6 @@ const models = [InjectSystemModel(UserInvite)];
SendInviteUsersMailMessage, SendInviteUsersMailMessage,
UsersApplication UsersApplication
], ],
controllers: [UsersController, UsersInviteController], controllers: [UsersController, UsersInviteController, UsersInvitePublicController],
}) })
export class UsersModule {} export class UsersModule {}

View File

@@ -1,40 +1,13 @@
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; import { Body, Controller, Param, Patch, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { UsersApplication } from './Users.application'; import { UsersApplication } from './Users.application';
import { InviteUserDto, SendInviteUserDto } from './dtos/InviteUser.dto'; import { SendInviteUserDto } from './dtos/InviteUser.dto';
@Controller('invite') @Controller('invite')
@ApiTags('Users') @ApiTags('Users')
export class UsersInviteController { export class UsersInviteController {
constructor(private readonly usersApplication: UsersApplication) {} constructor(private readonly usersApplication: UsersApplication) {}
/**
* Accept a user invitation.
*/
@Post('accept/:token')
@ApiOperation({ summary: 'Accept a user invitation.' })
async acceptInvite(
@Param('token') token: string,
@Body() inviteUserDTO: InviteUserDto,
) {
await this.usersApplication.acceptInvite(token, inviteUserDTO);
return {
message: 'The invitation has been accepted successfully.',
};
}
/**
* Check if an invitation token is valid.
*/
@Get('check/:token')
@ApiOperation({ summary: 'Check if an invitation token is valid.' })
async checkInvite(@Param('token') token: string) {
const inviteDetails = await this.usersApplication.checkInvite(token);
return inviteDetails;
}
/** /**
* Send an invitation to a new user. * Send an invitation to a new user.
*/ */

View File

@@ -0,0 +1,39 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { PublicRoute } from '@/modules/Auth/guards/jwt.guard';
import { UsersApplication } from './Users.application';
import { InviteUserDto } from './dtos/InviteUser.dto';
@Controller('invite')
@ApiTags('Users')
@PublicRoute()
export class UsersInvitePublicController {
constructor(private readonly usersApplication: UsersApplication) {}
/**
* Accept a user invitation.
*/
@Post('accept/:token')
@ApiOperation({ summary: 'Accept a user invitation.' })
async acceptInvite(
@Param('token') token: string,
@Body() inviteUserDTO: InviteUserDto,
) {
await this.usersApplication.acceptInvite(token, inviteUserDTO);
return {
message: 'The invitation has been accepted successfully.',
};
}
/**
* Check if an invitation token is valid.
*/
@Get('check/:token')
@ApiOperation({ summary: 'Check if an invitation token is valid.' })
async checkInvite(@Param('token') token: string) {
const inviteDetails = await this.usersApplication.checkInvite(token);
return inviteDetails;
}
}

View File

@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as moment from 'moment'; import * as moment from 'moment';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { ClsService } from 'nestjs-cls';
import { import {
IAcceptInviteEventPayload, IAcceptInviteEventPayload,
ICheckInviteEventPayload, ICheckInviteEventPayload,
@@ -15,6 +16,11 @@ import { UserInvite } from '../models/InviteUser.model';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { InviteUserDto } from '../dtos/InviteUser.dto'; import { InviteUserDto } from '../dtos/InviteUser.dto';
interface InviteAcceptResponseDto {
inviteToken: { email: string, token: string, createdAt: Date };
orgName: string
}
@Injectable() @Injectable()
export class AcceptInviteUserService { export class AcceptInviteUserService {
constructor( constructor(
@@ -27,6 +33,7 @@ export class AcceptInviteUserService {
@Inject(UserInvite.name) @Inject(UserInvite.name)
private readonly userInviteModel: typeof UserInvite, private readonly userInviteModel: typeof UserInvite,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly cls: ClsService,
) {} ) {}
/** /**
@@ -62,6 +69,16 @@ export class AcceptInviteUserService {
// Clear invite token by the given user id. // Clear invite token by the given user id.
await this.clearInviteTokensByUserId(inviteToken.userId); await this.clearInviteTokensByUserId(inviteToken.userId);
// Retrieve the tenant to get the organizationId for CLS.
const tenant = await this.tenantModel
.query()
.findById(inviteToken.tenantId);
// Set CLS values for tenant context before triggering sync events.
this.cls.set('tenantId', inviteToken.tenantId);
this.cls.set('userId', systemUser.id);
this.cls.set('organizationId', tenant.organizationId);
// Triggers `onUserAcceptInvite` event. // Triggers `onUserAcceptInvite` event.
await this.eventEmitter.emitAsync(events.inviteUser.acceptInvite, { await this.eventEmitter.emitAsync(events.inviteUser.acceptInvite, {
inviteToken, inviteToken,
@@ -77,7 +94,7 @@ export class AcceptInviteUserService {
*/ */
public async checkInvite( public async checkInvite(
token: string, token: string,
): Promise<{ inviteToken: ModelObject<UserInvite>; orgName: string }> { ): Promise<InviteAcceptResponseDto> {
const inviteToken = await this.getInviteTokenOrThrowError(token); const inviteToken = await this.getInviteTokenOrThrowError(token);
// Find the tenant that associated to the given token. // Find the tenant that associated to the given token.
@@ -92,7 +109,16 @@ export class AcceptInviteUserService {
tenant, tenant,
} as ICheckInviteEventPayload); } as ICheckInviteEventPayload);
return { inviteToken, orgName: tenant.metadata.name }; // Explicitly convert to plain object to ensure all fields are serialized
const result = {
inviteToken: {
email: inviteToken.email,
token: inviteToken.token,
createdAt: inviteToken.createdAt,
},
orgName: tenant.metadata.name,
};
return result;
} }
/** /**

View File

@@ -28,7 +28,7 @@ export class SendInviteUsersMailMessage {
) { ) {
const tenant = await this.tenancyContext.getTenant(true); const tenant = await this.tenancyContext.getTenant(true);
const root = path.join(global.__images_dirname, '/bigcapital.png'); const root = path.join(global.__images_dirname, '/bigcapital.png');
const baseURL = this.configService.get('baseURL'); const baseURL = this.configService.get('app.baseUrl');
const mail = new Mail() const mail = new Mail()
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`) .setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)

View File

@@ -6,6 +6,7 @@ export class UserInvite extends BaseModel {
userId!: number; userId!: number;
tenantId!: number; tenantId!: number;
email!: string; email!: string;
createdAt!: Date;
/** /**
* Table name. * Table name.
@@ -32,4 +33,11 @@ export class UserInvite extends BaseModel {
}, },
}; };
} }
/**
* Called before inserting a new record.
*/
$beforeInsert() {
this.createdAt = new Date();
}
} }

View File

@@ -1,4 +1,4 @@
import { omit } from 'lodash'; import { pick } from 'lodash';
import * as moment from 'moment'; import * as moment from 'moment';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
@@ -22,13 +22,12 @@ export class SyncTenantAcceptInviteSubscriber {
async syncTenantAcceptInvite({ async syncTenantAcceptInvite({
inviteToken, inviteToken,
user, user,
inviteUserDTO,
}: IAcceptInviteEventPayload) { }: IAcceptInviteEventPayload) {
await this.tenantUserModel() await this.tenantUserModel()
.query() .query()
.where('systemUserId', inviteToken.userId) .where('systemUserId', inviteToken.userId)
.update({ .update({
...omit(inviteUserDTO, ['password']), ...pick(user, ['firstName', 'lastName', 'email', 'active']),
inviteAcceptedAt: moment().format('YYYY-MM-DD'), inviteAcceptedAt: moment().format('YYYY-MM-DD'),
}); });
} }

View File

@@ -1,4 +1,4 @@
import Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IVendorCreditAppliedBill } from '../types/VendorCreditApplyBills.types'; import { IVendorCreditAppliedBill } from '../types/VendorCreditApplyBills.types';

View File

@@ -95,6 +95,10 @@
"react-plaid-link": "^3.2.1", "react-plaid-link": "^3.2.1",
"react-query": "^3.6.0", "react-query": "^3.6.0",
"react-query-devtools": "^2.1.1", "react-query-devtools": "^2.1.1",
"@trpc/client": "^11.0.0-rc.648",
"@trpc/react-query": "^11.0.0-rc.648",
"@tanstack/react-query": "^5.62.0",
"superjson": "^2.2.2",
"react-redux": "^7.2.9", "react-redux": "^7.2.9",
"react-router": "5.3.4", "react-router": "5.3.4",
"react-router-breadcrumbs-hoc": "^3.2.10", "react-router-breadcrumbs-hoc": "^3.2.10",

View File

@@ -4,6 +4,7 @@ import { Router, Switch, Route } from 'react-router';
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history';
import { QueryClientProvider, QueryClient } from 'react-query'; import { QueryClientProvider, QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools'; import { ReactQueryDevtools } from 'react-query/devtools';
import { trpc, trpcClient, queryClient } from '@/trpc';
import '@/style/App.scss'; import '@/style/App.scss';
import 'moment/locale/ar-ly'; import 'moment/locale/ar-ly';
@@ -86,6 +87,7 @@ export default function App() {
const queryClient = new QueryClient(queryConfig); const queryClient = new QueryClient(queryConfig);
return ( return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<SplashScreen /> <SplashScreen />
@@ -95,5 +97,6 @@ export default function App() {
<ReactQueryDevtools initialIsOpen /> <ReactQueryDevtools initialIsOpen />
</QueryClientProvider> </QueryClientProvider>
</trpc.Provider>
); );
} }

View File

@@ -4,7 +4,6 @@ import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { DashboardInsider } from '@/components'; import { DashboardInsider } from '@/components';
import { import {
useAccounts,
useAutoCompleteContacts, useAutoCompleteContacts,
useCurrencies, useCurrencies,
useJournal, useJournal,
@@ -13,6 +12,7 @@ import {
useBranches, useBranches,
useSettingsManualJournals, useSettingsManualJournals,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useProjects } from '@/containers/Projects/hooks'; import { useProjects } from '@/containers/Projects/hooks';
const MakeJournalFormContext = createContext(); const MakeJournalFormContext = createContext();
@@ -27,7 +27,7 @@ function MakeJournalProvider({ journalId, query, ...props }) {
const isProjectFeatureCan = featureCan(Features.Projects); const isProjectFeatureCan = featureCan(Features.Projects);
// Load the accounts list. // Load the accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Load the customers list. // Load the customers list.
const { data: contacts, isLoading: isContactsLoading } = const { data: contacts, isLoading: isContactsLoading } =

View File

@@ -1,7 +1,8 @@
// @ts-nocheck // @ts-nocheck
import React, { createContext } from 'react'; import React, { createContext } from 'react';
import { DashboardInsider } from '@/components'; import { DashboardInsider } from '@/components';
import { useResourceViews, useResourceMeta, useAccounts } from '@/hooks/query'; import { useResourceViews, useResourceMeta } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { getFieldsFromResourceMeta } from '@/utils'; import { getFieldsFromResourceMeta } from '@/utils';
const AccountsChartContext = createContext(); const AccountsChartContext = createContext();
@@ -26,7 +27,7 @@ function AccountsChartProvider({ query, tableStateChanged, ...props }) {
data: accounts, data: accounts,
isFetching: isAccountsFetching, isFetching: isAccountsFetching,
isLoading: isAccountsLoading, isLoading: isAccountsLoading,
} = useAccounts(query, { keepPreviousData: true }); } = useAccountsTrpc(query, { placeholderData: (previousData) => previousData });
// Provider payload. // Provider payload.
const provider = { const provider = {

View File

@@ -1,10 +1,10 @@
// @ts-nocheck // @ts-nocheck
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { useValidateBulkDeleteAccounts } from '@/hooks/query/accounts'; import { useValidateBulkDeleteAccountsTrpc } from '@/hooks/trpc';
import { useBulkDeleteDialog } from '@/hooks/dialogs/useBulkDeleteDialog'; import { useBulkDeleteDialog } from '@/hooks/dialogs/useBulkDeleteDialog';
export const useBulkDeleteAccountsDialog = () => { export const useBulkDeleteAccountsDialog = () => {
const validateBulkDeleteMutation = useValidateBulkDeleteAccounts(); const validateBulkDeleteMutation = useValidateBulkDeleteAccountsTrpc();
const { const {
openBulkDeleteDialog, openBulkDeleteDialog,
closeBulkDeleteDialog, closeBulkDeleteDialog,

View File

@@ -7,7 +7,7 @@ import { AppToaster, FormattedMessage as T } from '@/components';
import { withAlertStoreConnect } from '@/containers/Alert/withAlertStoreConnect'; import { withAlertStoreConnect } from '@/containers/Alert/withAlertStoreConnect';
import { withAlertActions } from '@/containers/Alert/withAlertActions'; import { withAlertActions } from '@/containers/Alert/withAlertActions';
import { useActivateAccount } from '@/hooks/query'; import { useActivateAccountTrpc } from '@/hooks/trpc';
import { compose } from '@/utils'; import { compose } from '@/utils';
/** /**
@@ -25,7 +25,7 @@ function AccountActivateAlert({
const { const {
mutateAsync: activateAccount, mutateAsync: activateAccount,
isLoading isLoading
} = useActivateAccount(); } = useActivateAccountTrpc();
// Handle alert cancel. // Handle alert cancel.
const handleCancel = () => { const handleCancel = () => {

View File

@@ -14,7 +14,7 @@ import { withAlertStoreConnect } from '@/containers/Alert/withAlertStoreConnect'
import { withAlertActions } from '@/containers/Alert/withAlertActions'; import { withAlertActions } from '@/containers/Alert/withAlertActions';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { useDeleteAccount } from '@/hooks/query'; import { useDeleteAccountTrpc } from '@/hooks/trpc';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
@@ -34,7 +34,7 @@ function AccountDeleteAlert({
// #withDrawerActions // #withDrawerActions
closeDrawer, closeDrawer,
}) { }) {
const { isLoading, mutateAsync: deleteAccount } = useDeleteAccount(); const { isLoading, mutateAsync: deleteAccount } = useDeleteAccountTrpc();
// handle cancel delete account alert. // handle cancel delete account alert.
const handleCancelAccountDelete = () => { const handleCancelAccountDelete = () => {

View File

@@ -8,7 +8,7 @@ import { withAlertStoreConnect } from '@/containers/Alert/withAlertStoreConnect'
import { withAlertActions } from '@/containers/Alert/withAlertActions'; import { withAlertActions } from '@/containers/Alert/withAlertActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { useInactivateAccount } from '@/hooks/query'; import { useInactivateAccountTrpc } from '@/hooks/trpc';
/** /**
* Account inactivate alert. * Account inactivate alert.
@@ -23,7 +23,7 @@ function AccountInactivateAlert({
// #withAlertActions // #withAlertActions
closeAlert, closeAlert,
}) { }) {
const { mutateAsync: inactivateAccount, isLoading } = useInactivateAccount(); const { mutateAsync: inactivateAccount, isLoading } = useInactivateAccountTrpc();
const handleCancelInactiveAccount = () => { const handleCancelInactiveAccount = () => {
closeAlert('account-inactivate'); closeAlert('account-inactivate');

View File

@@ -58,7 +58,7 @@ export default function InviteAcceptForm() {
data: { errors }, data: { errors },
}, },
}) => { }) => {
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) { if (errors.find((e) => e.type === 'INVITE_TOKEN_INVALID')) {
AppToaster.show({ AppToaster.show({
message: intl.get('an_unexpected_error_occurred'), message: intl.get('an_unexpected_error_occurred'),
intent: Intent.DANGER, intent: Intent.DANGER,
@@ -71,14 +71,6 @@ export default function InviteAcceptForm() {
phone_number: 'This phone number is used in another account.', phone_number: 'This phone number is used in another account.',
}); });
} }
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
AppToaster.show({
message: intl.get('an_unexpected_error_occurred'),
intent: Intent.DANGER,
position: Position.BOTTOM,
});
history.push('/auth/login');
}
setSubmitting(false); setSubmitting(false);
}, },
); );

View File

@@ -29,14 +29,22 @@ function InviteAcceptProvider({ token, ...props }) {
if (inviteMetaError) { history.push('/auth/login'); } if (inviteMetaError) { history.push('/auth/login'); }
}, [history, inviteMetaError]); }, [history, inviteMetaError]);
// Transform the backend response to match frontend expectations.
const transformedInviteMeta = inviteMeta
? {
email: inviteMeta.inviteToken?.email,
organizationName: inviteMeta.orgName,
}
: null;
// Provider payload. // Provider payload.
const provider = { const provider = {
token, token,
inviteMeta, inviteMeta: transformedInviteMeta,
inviteMetaError, inviteMetaError,
isInviteMetaError, isInviteMetaError,
isInviteMetaLoading, isInviteMetaLoading,
inviteAcceptMutate inviteAcceptMutate,
}; };
if (inviteMetaError) { if (inviteMetaError) {
@@ -45,7 +53,6 @@ function InviteAcceptProvider({ token, ...props }) {
return ( return (
<InviteAcceptLoading isLoading={isInviteMetaLoading}> <InviteAcceptLoading isLoading={isInviteMetaLoading}>
{ isInviteMetaError }
<InviteAcceptContext.Provider value={provider} {...props} /> <InviteAcceptContext.Provider value={provider} {...props} />
</InviteAcceptLoading> </InviteAcceptLoading>
); );

View File

@@ -1,18 +0,0 @@
.root {
text-align: center;
}
.title{
font-size: 18px;
font-weight: 600;
margin-bottom: 0.5rem;
color: #252A31;
}
.description{
margin-bottom: 1rem;
font-size: 15px;
line-height: 1.45;
color: #404854;
}

View File

@@ -1,12 +1,13 @@
// @ts-nocheck // @ts-nocheck
import { Button, Intent } from '@blueprintjs/core'; import { Button, Intent } from '@blueprintjs/core';
import { x } from '@xstyled/emotion';
import AuthInsider from './AuthInsider'; import AuthInsider from './AuthInsider';
import { AuthInsiderCard } from './_components'; import { AuthInsiderCard } from './_components';
import styles from './RegisterVerify.module.scss';
import { AppToaster, Stack } from '@/components'; import { AppToaster, Stack } from '@/components';
import { useAuthActions, useAuthUserVerifyEmail } from '@/hooks/state'; import { useAuthActions, useAuthUserVerifyEmail } from '@/hooks/state';
import { useAuthSignUpVerifyResendMail } from '@/hooks/query'; import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
import { AuthContainer } from './AuthContainer'; import { AuthContainer } from './AuthContainer';
import { useIsDarkMode } from '@/hooks/useDarkMode';
export default function RegisterVerify() { export default function RegisterVerify() {
const { setLogout } = useAuthActions(); const { setLogout } = useAuthActions();
@@ -14,6 +15,7 @@ export default function RegisterVerify() {
useAuthSignUpVerifyResendMail(); useAuthSignUpVerifyResendMail();
const emailAddress = useAuthUserVerifyEmail(); const emailAddress = useAuthUserVerifyEmail();
const isDarkMode = useIsDarkMode();
const handleResendMailBtnClick = () => { const handleResendMailBtnClick = () => {
resendSignUpVerifyMail() resendSignUpVerifyMail()
@@ -37,12 +39,24 @@ export default function RegisterVerify() {
return ( return (
<AuthContainer> <AuthContainer>
<AuthInsider> <AuthInsider>
<AuthInsiderCard className={styles.root}> <AuthInsiderCard textAlign="center">
<h2 className={styles.title}>Please verify your email</h2> <x.h2
<p className={styles.description}> fontSize="18px"
fontWeight={600}
mb="0.5rem"
color={isDarkMode ? 'rgba(255, 255, 255, 0.85)' : '#252A31'}
>
Please verify your email
</x.h2>
<x.p
mb="1rem"
fontSize="15px"
lineHeight="1.45"
color={isDarkMode ? 'rgba(255, 255, 255, 0.7)' : '#404854'}
>
We sent an email to <strong>{emailAddress}</strong> Click the link We sent an email to <strong>{emailAddress}</strong> Click the link
inside to get started. inside to get started.
</p> </x.p>
<Stack spacing={4}> <Stack spacing={4}>
<Button <Button

View File

@@ -1,7 +1,7 @@
import React, { createContext } from 'react'; import React, { createContext } from 'react';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
import { useBankRule } from '@/hooks/query/bank-rules'; import { useBankRule } from '@/hooks/query/bank-rules';
import { useAccounts } from '@/hooks/query'; import { useAccountsTrpc } from '@/hooks/trpc';
interface RuleFormBootValues { interface RuleFormBootValues {
bankRule?: null; bankRule?: null;
@@ -27,7 +27,7 @@ function RuleFormBoot({ bankRuleId, ...props }: RuleFormBootProps) {
enabled: !!bankRuleId, enabled: !!bankRuleId,
}, },
); );
const { data: accounts, isLoading: isAccountsLoading } = useAccounts({}, {}); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
const isNewMode = !bankRuleId; const isNewMode = !bankRuleId;
const isEditMode = !isNewMode; const isEditMode = !isNewMode;

View File

@@ -2,7 +2,8 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DashboardInsider } from '@/components'; import { DashboardInsider } from '@/components';
import { useCashflowAccounts, useAccount } from '@/hooks/query'; import { useCashflowAccounts } from '@/hooks/query';
import { useAccountTrpc } from '@/hooks/trpc';
import { useAppQueryString } from '@/hooks'; import { useAppQueryString } from '@/hooks';
import { useGetBankAccountSummaryMeta } from '@/hooks/query/bank-rules'; import { useGetBankAccountSummaryMeta } from '@/hooks/query/bank-rules';
@@ -33,7 +34,7 @@ function AccountTransactionsProvider({ query, ...props }) {
data: currentAccount, data: currentAccount,
isFetching: isCurrentAccountFetching, isFetching: isCurrentAccountFetching,
isLoading: isCurrentAccountLoading, isLoading: isCurrentAccountLoading,
} = useAccount(accountId, { keepPreviousData: true }); } = useAccountTrpc(accountId, { placeholderData: (previousData) => previousData });
// Retrieves the bank account meta summary. // Retrieves the bank account meta summary.
const { const {

View File

@@ -2,7 +2,8 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { first } from 'lodash'; import { first } from 'lodash';
import { DrawerLoading } from '@/components'; import { DrawerLoading } from '@/components';
import { useAccounts, useBranches } from '@/hooks/query'; import { useBranches } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { Spinner } from '@blueprintjs/core'; import { Spinner } from '@blueprintjs/core';
@@ -43,7 +44,7 @@ function CategorizeTransactionBoot({
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Fetches accounts list. // Fetches accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches the branches list. // Fetches the branches list.
const { data: branches, isLoading: isBranchesLoading } = useBranches( const { data: branches, isLoading: isBranchesLoading } = useBranches(

View File

@@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { Spinner } from '@blueprintjs/core'; import { Spinner } from '@blueprintjs/core';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useAccounts, useBranches } from '@/hooks/query'; import { useBranches } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
interface MatchingReconcileTransactionBootProps { interface MatchingReconcileTransactionBootProps {
@@ -21,7 +22,7 @@ export function MatchingReconcileTransactionBoot({
const { featureCan } = useFeatureCan(); const { featureCan } = useFeatureCan();
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
const { data: accounts, isLoading: isAccountsLoading } = useAccounts({}, {}); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
const { data: branches, isLoading: isBranchesLoading } = useBranches( const { data: branches, isLoading: isBranchesLoading } = useBranches(
{}, {},
{ {

View File

@@ -5,11 +5,11 @@ import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
useCreateCashflowTransaction, useCreateCashflowTransaction,
useAccounts,
useBranches, useBranches,
useCashflowAccounts, useCashflowAccounts,
useSettingCashFlow, useSettingCashFlow,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
const MoneyInDialogContent = React.createContext(); const MoneyInDialogContent = React.createContext();
@@ -30,7 +30,7 @@ function MoneyInDialogProvider({
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Fetches accounts list. // Fetches accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches the branches list. // Fetches the branches list.
const { const {

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
import { useAccount } from '@/hooks/query'; import { useAccountTrpc } from '@/hooks/trpc';
import { useMoneyInDailogContext } from './MoneyInDialogProvider'; import { useMoneyInDailogContext } from './MoneyInDialogProvider';
const MoneyInFieldsContext = React.createContext(); const MoneyInFieldsContext = React.createContext();
@@ -13,7 +13,7 @@ function MoneyInFieldsProvider({ ...props }) {
const { accountId } = useMoneyInDailogContext(); const { accountId } = useMoneyInDailogContext();
// Fetches the specific account details. // Fetches the specific account details.
const { data: account, isLoading: isAccountLoading } = useAccount(accountId, { const { data: account, isLoading: isAccountLoading } = useAccountTrpc(accountId, {
enabled: !!accountId, enabled: !!accountId,
}); });
// Provider data. // Provider data.

View File

@@ -4,12 +4,12 @@ import { DialogContent } from '@/components';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
useAccounts,
useBranches, useBranches,
useCreateCashflowTransaction, useCreateCashflowTransaction,
useCashflowAccounts, useCashflowAccounts,
useSettingCashFlow, useSettingCashFlow,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
const MoneyInDialogContent = React.createContext(); const MoneyInDialogContent = React.createContext();
@@ -30,7 +30,7 @@ function MoneyOutProvider({
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Fetches accounts list. // Fetches accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches the branches list. // Fetches the branches list.
const { const {

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
import { useAccount } from '@/hooks/query'; import { useAccountTrpc } from '@/hooks/trpc';
import { useMoneyOutDialogContext } from './MoneyOutDialogProvider'; import { useMoneyOutDialogContext } from './MoneyOutDialogProvider';
const MoneyOutFieldsContext = React.createContext(); const MoneyOutFieldsContext = React.createContext();
@@ -13,7 +13,7 @@ function MoneyOutFieldsProvider({ ...props }) {
const { accountId } = useMoneyOutDialogContext(); const { accountId } = useMoneyOutDialogContext();
// Fetches the specific account details. // Fetches the specific account details.
const { data: account, isLoading: isAccountLoading } = useAccount(accountId, { const { data: account, isLoading: isAccountLoading } = useAccountTrpc(accountId, {
enabled: !!accountId, enabled: !!accountId,
}); });
// Provider data. // Provider data.

View File

@@ -2,13 +2,15 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
import { import {
useCreateAccount,
useAccountsTypes,
useCurrencies, useCurrencies,
useAccount,
useAccounts,
useEditAccount,
} from '@/hooks/query'; } from '@/hooks/query';
import {
useCreateAccountTrpc,
useAccountsTypesTrpc,
useAccountTrpc,
useAccountsTrpc,
useEditAccountTrpc,
} from '@/hooks/trpc';
import { AccountDialogAction, getDisabledFormFields } from './utils'; import { AccountDialogAction, getDisabledFormFields } from './utils';
const AccountDialogContext = createContext(); const AccountDialogContext = createContext();
@@ -18,18 +20,18 @@ const AccountDialogContext = createContext();
*/ */
function AccountDialogProvider({ dialogName, payload, ...props }) { function AccountDialogProvider({ dialogName, payload, ...props }) {
// Create and edit account mutations. // Create and edit account mutations.
const { mutateAsync: createAccountMutate } = useCreateAccount(); const { mutateAsync: createAccountMutate } = useCreateAccountTrpc();
const { mutateAsync: editAccountMutate } = useEditAccount(); const { mutateAsync: editAccountMutate } = useEditAccountTrpc();
// Fetches accounts list. // Fetches accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Fetches accounts types. // Fetches accounts types.
const { data: accountsTypes, isLoading: isAccountsTypesLoading } = const { data: accountsTypes, isLoading: isAccountsTypesLoading } =
useAccountsTypes(); useAccountsTypesTrpc();
// Fetches the specific account details. // Fetches the specific account details.
const { data: account, isLoading: isAccountLoading } = useAccount( const { data: account, isLoading: isAccountLoading } = useAccountTrpc(
payload.accountId, payload.accountId,
{ {
enabled: enabled:

View File

@@ -5,7 +5,7 @@ import { FormattedMessage as T, AppToaster } from '@/components';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent'; import BulkDeleteDialogContent from '@/containers/Dialogs/components/BulkDeleteDialogContent';
import { useBulkDeleteAccounts } from '@/hooks/query/accounts'; import { useBulkDeleteAccountsTrpc } from '@/hooks/trpc';
import withDialogRedux from '@/components/DialogReduxConnect'; import withDialogRedux from '@/components/DialogReduxConnect';
import { withDialogActions } from '@/containers/Dialog/withDialogActions'; import { withDialogActions } from '@/containers/Dialog/withDialogActions';
import { withAccountsTableActions } from '@/containers/Accounts/withAccountsTableActions'; import { withAccountsTableActions } from '@/containers/Accounts/withAccountsTableActions';
@@ -28,7 +28,7 @@ function AccountBulkDeleteDialog({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
const { mutateAsync: bulkDeleteAccounts, isLoading } = useBulkDeleteAccounts(); const { mutateAsync: bulkDeleteAccounts, isLoading } = useBulkDeleteAccountsTrpc();
const handleCancel = () => { const handleCancel = () => {
closeDialog(dialogName); closeDialog(dialogName);

View File

@@ -2,7 +2,8 @@
import React from 'react'; import React from 'react';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
import { useAccounts, useInvoice, useCreateBadDebt } from '@/hooks/query'; import { useInvoice, useCreateBadDebt } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
const BadDebtContext = React.createContext(); const BadDebtContext = React.createContext();
@@ -11,7 +12,7 @@ const BadDebtContext = React.createContext();
*/ */
function BadDebtFormProvider({ invoiceId, dialogName, ...props }) { function BadDebtFormProvider({ invoiceId, dialogName, ...props }) {
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Handle fetch invoice data. // Handle fetch invoice data.
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, { const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {

View File

@@ -5,11 +5,11 @@ import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
useItem, useItem,
useAccounts,
useBranches, useBranches,
useWarehouses, useWarehouses,
useCreateInventoryAdjustment, useCreateInventoryAdjustment,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
const InventoryAdjustmentContext = createContext(); const InventoryAdjustmentContext = createContext();
@@ -23,7 +23,7 @@ function InventoryAdjustmentFormProvider({ itemId, dialogName, ...props }) {
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Fetches accounts list. // Fetches accounts list.
const { isFetching: isAccountsLoading, data: accounts } = useAccounts(); const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches the item details. // Fetches the item details.
const { isFetching: isItemLoading, data: item } = useItem(itemId); const { isFetching: isItemLoading, data: item } = useItem(itemId);

View File

@@ -3,10 +3,10 @@ import React, { useMemo } from 'react';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
import { import {
useBill, useBill,
useAccounts,
useBranches, useBranches,
useCreatePaymentMade, useCreatePaymentMade,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { pick } from 'lodash'; import { pick } from 'lodash';
@@ -27,7 +27,7 @@ function QuickPaymentMadeFormProvider({ query, billId, dialogName, ...props }) {
}); });
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Create payment made mutations. // Create payment made mutations.
const { mutateAsync: createPaymentMadeMutate } = useCreatePaymentMade(); const { mutateAsync: createPaymentMadeMutate } = useCreatePaymentMade();

View File

@@ -5,12 +5,12 @@ import { DialogContent } from '@/components';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
useAccounts,
useInvoice, useInvoice,
useBranches, useBranches,
useSettingsPaymentReceives, useSettingsPaymentReceives,
useCreatePaymentReceive, useCreatePaymentReceive,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
const QuickPaymentReceiveContext = createContext(); const QuickPaymentReceiveContext = createContext();
@@ -28,7 +28,7 @@ function QuickPaymentReceiveFormProvider({
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Handle fetch invoice data. // Handle fetch invoice data.
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, { const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {

View File

@@ -5,11 +5,11 @@ import { pick } from 'lodash';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
useAccounts,
useCreditNote, useCreditNote,
useBranches, useBranches,
useCreateRefundCreditNote, useCreateRefundCreditNote,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
const RefundCreditNoteContext = React.createContext(); const RefundCreditNoteContext = React.createContext();
@@ -27,7 +27,7 @@ function RefundCreditNoteFormProvider({
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Handle fetch credit note data. // Handle fetch credit note data.
const { data: creditNote, isLoading: isCreditNoteLoading } = useCreditNote( const { data: creditNote, isLoading: isCreditNoteLoading } = useCreditNote(

View File

@@ -5,11 +5,11 @@ import { pick } from 'lodash';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
useAccounts,
useVendorCredit, useVendorCredit,
useBranches, useBranches,
useCreateRefundVendorCredit, useCreateRefundVendorCredit,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
const RefundVendorCreditContext = React.createContext(); const RefundVendorCreditContext = React.createContext();
@@ -24,7 +24,7 @@ function RefundVendorCreditFormProvider({
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Fetches the branches list. // Fetches the branches list.
const { const {

View File

@@ -1,6 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { useAccount, useAccountTransactions } from '@/hooks/query'; import { useAccountTrpc, useAccountTransactionsTrpc } from '@/hooks/trpc';
import { DrawerHeaderContent, DrawerLoading } from '@/components'; import { DrawerHeaderContent, DrawerLoading } from '@/components';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
@@ -11,13 +11,13 @@ const AccountDrawerContext = React.createContext();
*/ */
function AccountDrawerProvider({ accountId, name, ...props }) { function AccountDrawerProvider({ accountId, name, ...props }) {
// Fetches the specific account details. // Fetches the specific account details.
const { data: account, isLoading: isAccountLoading } = useAccount(accountId, { const { data: account, isLoading: isAccountLoading } = useAccountTrpc(accountId, {
enabled: !!accountId, enabled: !!accountId,
}); });
// Load the specific account transactions. // Load the specific account transactions.
const { data: accounts, isLoading: isAccountsLoading } = const { data: accounts, isLoading: isAccountsLoading } =
useAccountTransactions(accountId, { useAccountTransactionsTrpc(accountId, {}, {
enabled: !!accountId, enabled: !!accountId,
}); });

View File

@@ -8,11 +8,11 @@ import {
useCurrencies, useCurrencies,
useCustomers, useCustomers,
useExpense, useExpense,
useAccounts,
useBranches, useBranches,
useCreateExpense, useCreateExpense,
useEditExpense, useEditExpense,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useProjects } from '@/containers/Projects/hooks'; import { useProjects } from '@/containers/Projects/hooks';
const ExpenseFormPageContext = createContext(); const ExpenseFormPageContext = createContext();
@@ -47,7 +47,7 @@ function ExpenseFormPageProvider({ query, expenseId, ...props }) {
} = useBranches(query, { enabled: isBranchFeatureCan }); } = useBranches(query, { enabled: isBranchFeatureCan });
// Fetch accounts list. // Fetch accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Fetch the projects list. // Fetch the projects list.
const { const {

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { useAccounts } from '@/hooks/query'; import { useAccountsTrpc } from '@/hooks/trpc';
import { FinancialHeaderLoadingSkeleton } from '../FinancialHeaderLoadingSkeleton'; import { FinancialHeaderLoadingSkeleton } from '../FinancialHeaderLoadingSkeleton';
const GLHeaderGeneralPanelContext = createContext(); const GLHeaderGeneralPanelContext = createContext();
@@ -11,7 +11,7 @@ const GLHeaderGeneralPanelContext = createContext();
*/ */
function GLHeaderGeneralPanelProvider({ ...props }) { function GLHeaderGeneralPanelProvider({ ...props }) {
// Accounts list. // Accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Provider // Provider
const provider = { const provider = {

View File

@@ -7,8 +7,8 @@ import {
useItemsCategories, useItemsCategories,
useCreateItem, useCreateItem,
useEditItem, useEditItem,
useAccounts,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useWatchItemError } from './utils'; import { useWatchItemError } from './utils';
import { useTaxRates } from '@/hooks/query/taxRates'; import { useTaxRates } from '@/hooks/query/taxRates';
@@ -23,7 +23,7 @@ function ItemFormProvider({ itemId, ...props }) {
const duplicateId = state?.action; const duplicateId = state?.action;
// Fetches the accounts list. // Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches the items categories list. // Fetches the items categories list.
const { const {

View File

@@ -5,7 +5,8 @@ import styled from 'styled-components';
import { Card } from '@/components'; import { Card } from '@/components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useAccounts, useSaveSettings, useSettings } from '@/hooks/query'; import { useSaveSettings, useSettings } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import PreferencesPageLoader from '../PreferencesPageLoader'; import PreferencesPageLoader from '../PreferencesPageLoader';
const AccountantFormContext = React.createContext(); const AccountantFormContext = React.createContext();
@@ -15,7 +16,7 @@ const AccountantFormContext = React.createContext();
*/ */
function AccountantFormProvider({ ...props }) { function AccountantFormProvider({ ...props }) {
// Fetches the accounts list. // Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches Organization Settings. // Fetches Organization Settings.
const { isLoading: isSettingsLoading } = useSettings(); const { isLoading: isSettingsLoading } = useSettings();

View File

@@ -5,7 +5,8 @@ import styled from 'styled-components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { Card } from '@/components'; import { Card } from '@/components';
import { useSettingsItems, useAccounts, useSaveSettings } from '@/hooks/query'; import { useSettingsItems, useSaveSettings } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import PreferencesPageLoader from '../PreferencesPageLoader'; import PreferencesPageLoader from '../PreferencesPageLoader';
const ItemFormContext = createContext(); const ItemFormContext = createContext();
@@ -16,7 +17,7 @@ const ItemFormContext = createContext();
function ItemPreferencesFormProvider({ ...props }) { function ItemPreferencesFormProvider({ ...props }) {
// Fetches the accounts list. // Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
const { const {
isLoading: isItemsSettingsLoading, isLoading: isItemsSettingsLoading,

View File

@@ -1,6 +1,6 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { Spinner } from '@blueprintjs/core'; import { Spinner } from '@blueprintjs/core';
import { useAccounts } from '@/hooks/query'; import { useAccountsTrpc } from '@/hooks/trpc';
import { useGetPaymentMethod } from '@/hooks/query/payment-services'; import { useGetPaymentMethod } from '@/hooks/query/payment-services';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
@@ -40,7 +40,7 @@ export const StripeIntegrationEditBoot: React.FC<
payload: { stripePaymentMethodId }, payload: { stripePaymentMethodId },
} = useDrawerContext(); } = useDrawerContext();
const { data: accounts, isLoading: isAccountsLoading } = useAccounts({}, {}); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
const { data: paymentMethod, isLoading: isPaymentMethodLoading } = const { data: paymentMethod, isLoading: isPaymentMethodLoading } =
useGetPaymentMethod(stripePaymentMethodId, { useGetPaymentMethod(stripePaymentMethodId, {
enabled: !!stripePaymentMethodId, enabled: !!stripePaymentMethodId,

View File

@@ -5,7 +5,6 @@ import { useFeatureCan } from '@/hooks/state';
import { DashboardInsider } from '@/components/Dashboard'; import { DashboardInsider } from '@/components/Dashboard';
import { useProjects } from '@/containers/Projects/hooks'; import { useProjects } from '@/containers/Projects/hooks';
import { import {
useAccounts,
useVendors, useVendors,
useItems, useItems,
useBill, useBill,
@@ -15,6 +14,7 @@ import {
useCreateBill, useCreateBill,
useEditBill, useEditBill,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useTaxRates } from '@/hooks/query/taxRates'; import { useTaxRates } from '@/hooks/query/taxRates';
const BillFormContext = createContext(); const BillFormContext = createContext();
@@ -48,7 +48,7 @@ function BillFormProvider({ billId, ...props }) {
const isProjectsFeatureCan = featureCan(Features.Projects); const isProjectsFeatureCan = featureCan(Features.Projects);
// Handle fetch accounts. // Handle fetch accounts.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Handle fetch vendors data table // Handle fetch vendors data table
const { const {

View File

@@ -3,7 +3,6 @@ import React, { createContext, useContext, useState } from 'react';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
useAccounts,
useVendors, useVendors,
useItems, useItems,
useBranches, useBranches,
@@ -12,6 +11,7 @@ import {
useCreatePaymentMade, useCreatePaymentMade,
useEditPaymentMade, useEditPaymentMade,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { DashboardInsider } from '@/components'; import { DashboardInsider } from '@/components';
// Payment made form context. // Payment made form context.
@@ -29,7 +29,7 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) {
const isBranchFeatureCan = featureCan(Features.Branches); const isBranchFeatureCan = featureCan(Features.Branches);
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Handle fetch Items data table or list. // Handle fetch Items data table or list.
const { const {

View File

@@ -7,7 +7,6 @@ import { useProjects } from '@/containers/Projects/hooks';
import { import {
useSettingsPaymentReceives, useSettingsPaymentReceives,
usePaymentReceiveEditPage, usePaymentReceiveEditPage,
useAccounts,
useCustomers, useCustomers,
useBranches, useBranches,
useCreatePaymentReceive, useCreatePaymentReceive,
@@ -15,6 +14,7 @@ import {
usePaymentReceivedState, usePaymentReceivedState,
PaymentReceivedStateResponse, PaymentReceivedStateResponse,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates'; import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
interface PaymentReceivedFormContextValue { interface PaymentReceivedFormContextValue {
@@ -52,7 +52,7 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
const paymentEntriesEditPage = paymentReceivedEditData?.entries const paymentEntriesEditPage = paymentReceivedEditData?.entries
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Fetch payment made settings. // Fetch payment made settings.
const fetchSettings = useSettingsPaymentReceives(); const fetchSettings = useSettingsPaymentReceives();

View File

@@ -5,7 +5,6 @@ import { useFeatureCan } from '@/hooks/state';
import { DashboardInsider } from '@/components/Dashboard'; import { DashboardInsider } from '@/components/Dashboard';
import { import {
useReceipt, useReceipt,
useAccounts,
useSettingsReceipts, useSettingsReceipts,
useCustomers, useCustomers,
useWarehouses, useWarehouses,
@@ -16,6 +15,7 @@ import {
useGetReceiptState, useGetReceiptState,
IGetReceiptStateResponse, IGetReceiptStateResponse,
} from '@/hooks/query'; } from '@/hooks/query';
import { useAccountsTrpc } from '@/hooks/trpc';
import { useProjects } from '@/containers/Projects/hooks'; import { useProjects } from '@/containers/Projects/hooks';
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates'; import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
@@ -43,7 +43,7 @@ function ReceiptFormProvider({ receiptId, ...props }) {
enabled: !!receiptId, enabled: !!receiptId,
}); });
// Fetch accounts list. // Fetch accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Fetch customers list. // Fetch customers list.
const { const {

View File

@@ -128,7 +128,7 @@ export const useAuthMetadata = (props = {}) => {
* Resend the mail of signup verification. * Resend the mail of signup verification.
*/ */
export const useAuthSignUpVerifyResendMail = (props) => { export const useAuthSignUpVerifyResendMail = (props) => {
const apiRequest = useAuthApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation(
() => apiRequest.post(AuthRoute.SignupVerifyResend), () => apiRequest.post(AuthRoute.SignupVerifyResend),

View File

@@ -1,4 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { QueryClient } from '@tanstack/react-query';
// Query client config. // Query client config.
export const queryConfig = { export const queryConfig = {
defaultOptions: { defaultOptions: {
@@ -8,3 +10,6 @@ export const queryConfig = {
}, },
}, },
}; };
// Create a new QueryClient instance for tRPC
export const tanstackQueryClient = new QueryClient(queryConfig);

View File

@@ -58,16 +58,13 @@ export function useCreateCreditNote(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation((values) => apiRequest.post('credit-notes', values), {
(values) => apiRequest.post('credit-notes', values),
{
onSuccess: (res, values) => { onSuccess: (res, values) => {
// Common invalidate queries. // Common invalidate queries.
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
}, },
...props, ...props,
}, });
);
} }
/** /**
@@ -218,8 +215,7 @@ export function useCreateRefundCreditNote(props) {
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation(
([id, values]) => ([id, values]) => apiRequest.post(`credit-notes/${id}/refunds`, values),
apiRequest.post(`credit-notes/${id}/refunds`, values),
{ {
onSuccess: (res, [id, values]) => { onSuccess: (res, [id, values]) => {
// Common invalidate queries. // Common invalidate queries.
@@ -240,9 +236,7 @@ export function useDeleteRefundCreditNote(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation((id) => apiRequest.delete(`credit-notes/refunds/${id}`), {
(id) => apiRequest.delete(`credit-notes/refunds/${id}`),
{
onSuccess: (res, id) => { onSuccess: (res, id) => {
// Common invalidate queries. // Common invalidate queries.
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
@@ -251,8 +245,7 @@ export function useDeleteRefundCreditNote(props) {
queryClient.invalidateQueries([t.CREDIT_NOTE, id]); queryClient.invalidateQueries([t.CREDIT_NOTE, id]);
}, },
...props, ...props,
}, });
);
} }
/** /**
@@ -301,7 +294,7 @@ export function useReconcileCreditNote(id, props, requestProps) {
[t.RECONCILE_CREDIT_NOTE, id], [t.RECONCILE_CREDIT_NOTE, id],
{ {
method: 'get', method: 'get',
url: `credit-notes/${id}/applied-invoices`, url: `credit-notes/${id}/apply-invoices`,
...requestProps, ...requestProps,
}, },
{ {

View File

@@ -2,6 +2,7 @@
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useRequestQuery } from '../useQueryRequest'; import { useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest'; import useApiRequest from '../useRequest';
import { transformToCamelCase } from '@/utils';
/** /**
* Authentication invite accept. * Authentication invite accept.
@@ -22,9 +23,9 @@ export const useAuthInviteAccept = (props) => {
export const useInviteMetaByToken = (token, props) => { export const useInviteMetaByToken = (token, props) => {
return useRequestQuery( return useRequestQuery(
['INVITE_META', token], ['INVITE_META', token],
{ method: 'get', url: `invite/invited/${token}` }, { method: 'get', url: `invite/check/${token}` },
{ {
select: (res) => res.data, select: (res) => transformToCamelCase(res.data),
...props ...props
} }
); );

View File

@@ -0,0 +1,2 @@
// tRPC hooks for accounts module
export * from './useAccounts';

View File

@@ -0,0 +1,188 @@
import { trpc } from '@/trpc';
import { useQueryClient } from '@tanstack/react-query';
// Query keys for cache invalidation
const accountQueryKeys = {
accounts: ['accounts'],
account: (id: number) => ['account', id],
accountTypes: ['accountTypes'],
accountTransactions: (id: number) => ['accountTransactions', id],
cashFlowAccounts: ['cashFlowAccounts'],
financialReport: ['financialReport'],
};
/**
* Retrieve accounts list using tRPC.
*/
export function useAccountsTrpc(query?: Record<string, any>, options = {}) {
return trpc.accounts.getAccounts.useQuery(query || {}, {
select: (res) => res.accounts,
...options,
});
}
/**
* Retrieve the given account details using tRPC.
*/
export function useAccountTrpc(id: number, options = {}) {
return trpc.accounts.getAccount.useQuery(
{ id },
{
enabled: !!id,
...options,
}
);
}
/**
* Retrieve accounts types list using tRPC.
*/
export function useAccountsTypesTrpc(options = {}) {
return trpc.accounts.getAccountTypes.useQuery(undefined, options);
}
/**
* Retrieve account transactions using tRPC.
*/
export function useAccountTransactionsTrpc(
accountId: number,
filters?: { fromDate?: string; toDate?: string },
options = {}
) {
return trpc.accounts.getAccountTransactions.useQuery(
{
accountId,
fromDate: filters?.fromDate,
toDate: filters?.toDate,
},
{
enabled: !!accountId,
...options,
}
);
}
/**
* Creates account using tRPC.
*/
export function useCreateAccountTrpc(options = {}) {
const queryClient = useQueryClient();
return trpc.accounts.createAccount.useMutation({
onSuccess: () => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: accountQueryKeys.accounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.cashFlowAccounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.financialReport });
},
...options,
});
}
/**
* Edits the given account using tRPC.
*/
export function useEditAccountTrpc(options = {}) {
const queryClient = useQueryClient();
return trpc.accounts.editAccount.useMutation({
onSuccess: (_, variables) => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: accountQueryKeys.accounts });
queryClient.invalidateQueries({
queryKey: accountQueryKeys.account(variables.id),
});
queryClient.invalidateQueries({ queryKey: accountQueryKeys.cashFlowAccounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.financialReport });
},
...options,
});
}
/**
* Deletes the given account using tRPC.
*/
export function useDeleteAccountTrpc(options = {}) {
const queryClient = useQueryClient();
return trpc.accounts.deleteAccount.useMutation({
onSuccess: () => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: accountQueryKeys.accounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.cashFlowAccounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.financialReport });
},
...options,
});
}
/**
* Activates the given account using tRPC.
*/
export function useActivateAccountTrpc(options = {}) {
const queryClient = useQueryClient();
return trpc.accounts.activateAccount.useMutation({
onSuccess: () => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: accountQueryKeys.accounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.cashFlowAccounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.financialReport });
},
...options,
});
}
/**
* Inactivates the given account using tRPC.
*/
export function useInactivateAccountTrpc(options = {}) {
const queryClient = useQueryClient();
return trpc.accounts.inactivateAccount.useMutation({
onSuccess: () => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: accountQueryKeys.accounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.cashFlowAccounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.financialReport });
},
...options,
});
}
/**
* Validates which accounts can be deleted in bulk using tRPC.
*/
export function useValidateBulkDeleteAccountsTrpc(options = {}) {
return trpc.accounts.validateBulkDeleteAccounts.useMutation(options);
}
/**
* Deletes multiple accounts in bulk using tRPC.
*/
export function useBulkDeleteAccountsTrpc(options = {}) {
const queryClient = useQueryClient();
return trpc.accounts.bulkDeleteAccounts.useMutation({
onSuccess: () => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: accountQueryKeys.accounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.cashFlowAccounts });
queryClient.invalidateQueries({ queryKey: accountQueryKeys.financialReport });
},
...options,
});
}
/**
* Hook to refresh accounts list.
*/
export function useRefreshAccountsTrpc() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries({ queryKey: accountQueryKeys.accounts });
},
};
}

View File

@@ -0,0 +1,42 @@
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import { store } from '@/store/createStore';
import { tanstackQueryClient } from '@/hooks/query/base';
// Define the AppRouter type - this will be imported from the server package
// For now, we use any until the server exports the proper types
export type AppRouter = any;
export const trpc = createTRPCReact<AppRouter>();
export function getAuthHeaders() {
const state = store.getState();
const { token, organizationId } = state.authentication;
const headers: Record<string, string> = {};
if (token) {
headers['x-access-token'] = token;
}
if (organizationId) {
headers['organization-id'] = organizationId.toString();
}
headers['Accept-Language'] = 'en';
return headers;
}
export const trpcClient = trpc.createClient({
transformer: superjson,
links: [
httpBatchLink({
url: '/api/trpc',
headers() {
return getAuthHeaders();
},
}),
],
});
// Export the QueryClient for use in the TRPCProvider
export const queryClient = tanstackQueryClient;

View File

@@ -19,5 +19,8 @@
"dev": "npm run build -- --watch" "dev": "npm run build -- --watch"
}, },
"author": "", "author": "",
"license": "ISC" "license": "ISC",
"dependencies": {
"@trpc/server": "^11.10.0"
}
} }

View File

@@ -1,3 +1,4 @@
export * from './countries'; export * from './countries';
export * from './trpc';
export const test = () => {}; export const test = () => {};

View File

@@ -0,0 +1,32 @@
/**
* tRPC Router Types
* This file exports the types for the tRPC router that are shared between
* the server and the webapp (frontend).
*
* Note: This file only contains TYPE definitions. It does not import any
* runtime code from the server to avoid bundle bloat in the webapp.
*/
// We define the router type structure here to avoid importing the actual router
// from the server package, which would cause issues with bundling.
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
// This is a placeholder type that will be replaced by the actual router type
// when the server builds and exports it.
// The webapp will import the actual type from the server package during development
// but will use the built type declaration during production.
export type AppRouter = any;
/**
* Inference helpers for input types
* @example type MyInput = RouterInputs['accounts']['getAccount']
*/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helpers for output types
* @example type MyOutput = RouterOutputs['accounts']['getAccount']
*/
export type RouterOutputs = inferRouterOutputs<AppRouter>;