Compare commits

..

11 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
68 changed files with 791 additions and 162 deletions

View File

@@ -127,7 +127,10 @@
"uuid": "^10.0.0",
"xlsx": "^0.18.5",
"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": {
"@nestjs/cli": "^10.0.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
@@ -14,6 +15,10 @@ import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.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')
@ApiTags('Credit Notes Apply Invoice')
@@ -22,6 +27,9 @@ import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
export class CreditNotesApplyInvoiceController {
constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
private readonly getCreditNoteAssociatedInvoicesToApplyService: GetCreditNoteAssociatedInvoicesToApply,
private readonly creditNoteApplyToInvoicesService: CreditNoteApplyToInvoices,
private readonly deleteCreditNoteApplyToInvoicesService: DeleteCreditNoteApplyToInvoices,
) {}
@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')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Apply credit note to invoices' })
@@ -48,9 +73,32 @@ export class CreditNotesApplyInvoiceController {
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
applyCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) {
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices(
applyCreditNoteToInvoices(
@Param('creditNoteId') creditNoteId: number,
@Body() applyDto: ApplyCreditNoteToInvoicesDto,
) {
return this.creditNoteApplyToInvoicesService.applyCreditNoteToInvoices(
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 { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
import { CreditNoteApplySyncCreditSubscriber } from './subscribers/CreditNoteApplySyncCreditSubscriber';
import { CreditNoteApplySyncInvoicesCreditedAmountSubscriber } from './subscribers/CreditNoteApplySyncInvoicesSubscriber';
@Module({
providers: [
@@ -19,6 +21,8 @@ import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.con
CreditNoteApplySyncCredit,
GetCreditNoteAssociatedAppliedInvoices,
GetCreditNoteAssociatedInvoicesToApply,
CreditNoteApplySyncCreditSubscriber,
CreditNoteApplySyncInvoicesCreditedAmountSubscriber,
],
exports: [DeleteCustomerLinkedCreditNoteService],
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import {
import { TenancyContext } from './TenancyContext.service';
import { Reflector } from '@nestjs/core';
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 IgnoreTenantInitializedRoute = () =>
@@ -35,8 +36,12 @@ export class EnsureTenantIsInitializedGuard implements CanActivate {
IS_PUBLIC_ROUTE,
[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.
if (isPublic || isIgnoreEnsureTenantInitialized) {
if (isPublic || isIgnoreEnsureTenantInitialized || isTenantAgnostic) {
return true;
}
const tenant = await this.tenancyContext.getTenant();

View File

@@ -9,6 +9,7 @@ import {
import { TenancyContext } from './TenancyContext.service';
import { Reflector } from '@nestjs/core';
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 IgnoreTenantSeededRoute = () =>
@@ -36,7 +37,12 @@ export class EnsureTenantIsSeededGuard implements CanActivate {
context.getHandler(),
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;
}
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

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

View File

@@ -95,6 +95,10 @@
"react-plaid-link": "^3.2.1",
"react-query": "^3.6.0",
"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-router": "5.3.4",
"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 { QueryClientProvider, QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import { trpc, trpcClient, queryClient } from '@/trpc';
import '@/style/App.scss';
import 'moment/locale/ar-ly';
@@ -86,6 +87,7 @@ export default function App() {
const queryClient = new QueryClient(queryConfig);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<SplashScreen />
@@ -95,5 +97,6 @@ export default function App() {
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
</trpc.Provider>
);
}

View File

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

View File

@@ -1,7 +1,8 @@
// @ts-nocheck
import React, { createContext } from 'react';
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';
const AccountsChartContext = createContext();
@@ -26,7 +27,7 @@ function AccountsChartProvider({ query, tableStateChanged, ...props }) {
data: accounts,
isFetching: isAccountsFetching,
isLoading: isAccountsLoading,
} = useAccounts(query, { keepPreviousData: true });
} = useAccountsTrpc(query, { placeholderData: (previousData) => previousData });
// Provider payload.
const provider = {

View File

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

View File

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

View File

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

View File

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

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
import { Button, Intent } from '@blueprintjs/core';
import { x } from '@xstyled/emotion';
import AuthInsider from './AuthInsider';
import { AuthInsiderCard } from './_components';
import styles from './RegisterVerify.module.scss';
import { AppToaster, Stack } from '@/components';
import { useAuthActions, useAuthUserVerifyEmail } from '@/hooks/state';
import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
import { AuthContainer } from './AuthContainer';
import { useIsDarkMode } from '@/hooks/useDarkMode';
export default function RegisterVerify() {
const { setLogout } = useAuthActions();
@@ -14,6 +15,7 @@ export default function RegisterVerify() {
useAuthSignUpVerifyResendMail();
const emailAddress = useAuthUserVerifyEmail();
const isDarkMode = useIsDarkMode();
const handleResendMailBtnClick = () => {
resendSignUpVerifyMail()
@@ -37,12 +39,24 @@ export default function RegisterVerify() {
return (
<AuthContainer>
<AuthInsider>
<AuthInsiderCard className={styles.root}>
<h2 className={styles.title}>Please verify your email</h2>
<p className={styles.description}>
<AuthInsiderCard textAlign="center">
<x.h2
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
inside to get started.
</p>
</x.p>
<Stack spacing={4}>
<Button

View File

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

View File

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

View File

@@ -2,7 +2,8 @@
import React, { useMemo } from 'react';
import { first } from 'lodash';
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 { Features } from '@/constants';
import { Spinner } from '@blueprintjs/core';
@@ -43,7 +44,7 @@ function CategorizeTransactionBoot({
const isBranchFeatureCan = featureCan(Features.Branches);
// Fetches accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts();
const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches the branches list.
const { data: branches, isLoading: isBranchesLoading } = useBranches(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,8 @@
import React from 'react';
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();
@@ -11,7 +12,7 @@ const BadDebtContext = React.createContext();
*/
function BadDebtFormProvider({ invoiceId, dialogName, ...props }) {
// Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts();
const { data: accounts, isLoading: isAccountsLoading } = useAccountsTrpc();
// Handle fetch invoice data.
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,8 @@ import styled from 'styled-components';
import { Card } from '@/components';
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';
const AccountantFormContext = React.createContext();
@@ -15,7 +16,7 @@ const AccountantFormContext = React.createContext();
*/
function AccountantFormProvider({ ...props }) {
// Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts();
const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
// Fetches Organization Settings.
const { isLoading: isSettingsLoading } = useSettings();

View File

@@ -5,7 +5,8 @@ import styled from 'styled-components';
import { CLASSES } from '@/constants/classes';
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';
const ItemFormContext = createContext();
@@ -16,7 +17,7 @@ const ItemFormContext = createContext();
function ItemPreferencesFormProvider({ ...props }) {
// Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts();
const { isLoading: isAccountsLoading, data: accounts } = useAccountsTrpc();
const {
isLoading: isItemsSettingsLoading,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
// @ts-nocheck
import { QueryClient } from '@tanstack/react-query';
// Query client config.
export const queryConfig = {
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 apiRequest = useApiRequest();
return useMutation(
(values) => apiRequest.post('credit-notes', values),
{
return useMutation((values) => apiRequest.post('credit-notes', values), {
onSuccess: (res, values) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
});
}
/**
@@ -218,8 +215,7 @@ export function useCreateRefundCreditNote(props) {
const apiRequest = useApiRequest();
return useMutation(
([id, values]) =>
apiRequest.post(`credit-notes/${id}/refunds`, values),
([id, values]) => apiRequest.post(`credit-notes/${id}/refunds`, values),
{
onSuccess: (res, [id, values]) => {
// Common invalidate queries.
@@ -240,9 +236,7 @@ export function useDeleteRefundCreditNote(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(id) => apiRequest.delete(`credit-notes/refunds/${id}`),
{
return useMutation((id) => apiRequest.delete(`credit-notes/refunds/${id}`), {
onSuccess: (res, id) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
@@ -251,8 +245,7 @@ export function useDeleteRefundCreditNote(props) {
queryClient.invalidateQueries([t.CREDIT_NOTE, id]);
},
...props,
},
);
});
}
/**
@@ -301,7 +294,7 @@ export function useReconcileCreditNote(id, props, requestProps) {
[t.RECONCILE_CREDIT_NOTE, id],
{
method: 'get',
url: `credit-notes/${id}/applied-invoices`,
url: `credit-notes/${id}/apply-invoices`,
...requestProps,
},
{

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"
},
"author": "",
"license": "ISC"
"license": "ISC",
"dependencies": {
"@trpc/server": "^11.10.0"
}
}

View File

@@ -1,3 +1,4 @@
export * from './countries';
export * from './trpc';
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>;