From 6af4be9c6c74bf21c816cc7955b1304884ebc8bd Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 5 Feb 2026 16:03:57 +0200 Subject: [PATCH 001/106] fix(server): branches activation not marking bills and payments with primary branch When activating the multi-branches feature, existing bills, vendor credits, and bill payments were not being marked with the default primary branch. Changes: - Add missing @Inject decorators to BillActivateBranches, VendorCreditActivateBranches, and BillPaymentsActivateBranches services - Create BillBranchesActivateSubscriber to listen to onActivated event - Create VendorCreditBranchesActivateSubscriber to listen to onActivated event - Register BillPaymentsActivateBranches and PaymentMadeActivateBranchesSubscriber in BranchesModule - Add branch object to BillResponseDto for API responses - Add branch to BillTransformer includeAttributes Fixes: #935 --- .../modules/Bills/dtos/BillResponse.dto.ts | 10 +++++++ .../modules/Bills/queries/Bill.transformer.ts | 1 + .../src/modules/Branches/Branches.module.ts | 14 +++++++++- .../Purchases/BillBranchesActivate.ts | 11 +++++--- .../Purchases/PaymentMadeBranchesActivate.ts | 3 +- .../Purchases/VendorCreditBranchesActivate.ts | 3 +- .../BillBranchesActivateSubscriber.ts | 28 +++++++++++++++++++ .../VendorCreditBranchesActivateSubscriber.ts | 28 +++++++++++++++++++ 8 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 packages/server/src/modules/Branches/subscribers/Activate/BillBranchesActivateSubscriber.ts create mode 100644 packages/server/src/modules/Branches/subscribers/Activate/VendorCreditBranchesActivateSubscriber.ts diff --git a/packages/server/src/modules/Bills/dtos/BillResponse.dto.ts b/packages/server/src/modules/Bills/dtos/BillResponse.dto.ts index b082b1496..990070fc9 100644 --- a/packages/server/src/modules/Bills/dtos/BillResponse.dto.ts +++ b/packages/server/src/modules/Bills/dtos/BillResponse.dto.ts @@ -1,6 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto'; import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto'; +import { BranchResponseDto } from '@/modules/Branches/dtos/BranchResponse.dto'; import { DiscountType } from '@/common/types/Discount'; export class BillResponseDto { @@ -89,6 +91,14 @@ export class BillResponseDto { }) branchId?: number; + @ApiProperty({ + description: 'Branch details', + type: () => BranchResponseDto, + required: false, + }) + @Type(() => BranchResponseDto) + branch?: BranchResponseDto; + @ApiProperty({ description: 'The ID of the project', example: 301, diff --git a/packages/server/src/modules/Bills/queries/Bill.transformer.ts b/packages/server/src/modules/Bills/queries/Bill.transformer.ts index 851a284a4..b264d251a 100644 --- a/packages/server/src/modules/Bills/queries/Bill.transformer.ts +++ b/packages/server/src/modules/Bills/queries/Bill.transformer.ts @@ -30,6 +30,7 @@ export class BillTransformer extends Transformer { 'taxes', 'entries', 'attachments', + 'branch', ]; }; diff --git a/packages/server/src/modules/Branches/Branches.module.ts b/packages/server/src/modules/Branches/Branches.module.ts index d53e1d4c9..a6c661d45 100644 --- a/packages/server/src/modules/Branches/Branches.module.ts +++ b/packages/server/src/modules/Branches/Branches.module.ts @@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance' import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator'; import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches'; import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches'; +import { BillActivateBranches } from './integrations/Purchases/BillBranchesActivate'; +import { VendorCreditActivateBranches } from './integrations/Purchases/VendorCreditBranchesActivate'; +import { BillPaymentsActivateBranches } from './integrations/Purchases/PaymentMadeBranchesActivate'; +import { BillBranchesActivateSubscriber } from './subscribers/Activate/BillBranchesActivateSubscriber'; +import { VendorCreditBranchesActivateSubscriber } from './subscribers/Activate/VendorCreditBranchesActivateSubscriber'; +import { PaymentMadeActivateBranchesSubscriber } from './subscribers/Activate/PaymentMadeBranchesActivateSubscriber'; import { FeaturesModule } from '../Features/Features.module'; @Module({ @@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module'; ValidateBranchExistance, ManualJournalBranchesValidator, CashflowTransactionsActivateBranches, - ExpensesActivateBranches + ExpensesActivateBranches, + BillActivateBranches, + VendorCreditActivateBranches, + BillPaymentsActivateBranches, + BillBranchesActivateSubscriber, + VendorCreditBranchesActivateSubscriber, + PaymentMadeActivateBranchesSubscriber ], exports: [ BranchesSettingsService, diff --git a/packages/server/src/modules/Branches/integrations/Purchases/BillBranchesActivate.ts b/packages/server/src/modules/Branches/integrations/Purchases/BillBranchesActivate.ts index e05dd7619..e12af7615 100644 --- a/packages/server/src/modules/Branches/integrations/Purchases/BillBranchesActivate.ts +++ b/packages/server/src/modules/Branches/integrations/Purchases/BillBranchesActivate.ts @@ -1,11 +1,14 @@ import { Knex } from 'knex'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { Bill } from '@/modules/Bills/models/Bill'; @Injectable() export class BillActivateBranches { - constructor(private readonly billModel: TenantModelProxy) {} + constructor( + @Inject(Bill.name) + private readonly billModel: TenantModelProxy, + ) {} /** * Updates all bills transactions with the primary branch. @@ -17,7 +20,7 @@ export class BillActivateBranches { primaryBranchId: number, trx?: Knex.Transaction, ) => { - // Updates the sale invoice with primary branch. - await Bill.query(trx).update({ branchId: primaryBranchId }); + // Updates the bills with primary branch. + await this.billModel().query(trx).update({ branchId: primaryBranchId }); }; } diff --git a/packages/server/src/modules/Branches/integrations/Purchases/PaymentMadeBranchesActivate.ts b/packages/server/src/modules/Branches/integrations/Purchases/PaymentMadeBranchesActivate.ts index 32645f612..dcfc1c15d 100644 --- a/packages/server/src/modules/Branches/integrations/Purchases/PaymentMadeBranchesActivate.ts +++ b/packages/server/src/modules/Branches/integrations/Purchases/PaymentMadeBranchesActivate.ts @@ -1,11 +1,12 @@ import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { BillPayment } from '@/modules/BillPayments/models/BillPayment'; -import { Injectable } from '@nestjs/common'; @Injectable() export class BillPaymentsActivateBranches { constructor( + @Inject(BillPayment.name) private readonly billPaymentModel: TenantModelProxy, ) {} diff --git a/packages/server/src/modules/Branches/integrations/Purchases/VendorCreditBranchesActivate.ts b/packages/server/src/modules/Branches/integrations/Purchases/VendorCreditBranchesActivate.ts index 3dfb98174..0b33f2441 100644 --- a/packages/server/src/modules/Branches/integrations/Purchases/VendorCreditBranchesActivate.ts +++ b/packages/server/src/modules/Branches/integrations/Purchases/VendorCreditBranchesActivate.ts @@ -1,11 +1,12 @@ import { Knex } from 'knex'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit'; @Injectable() export class VendorCreditActivateBranches { constructor( + @Inject(VendorCredit.name) private readonly vendorCreditModel: TenantModelProxy, ) {} diff --git a/packages/server/src/modules/Branches/subscribers/Activate/BillBranchesActivateSubscriber.ts b/packages/server/src/modules/Branches/subscribers/Activate/BillBranchesActivateSubscriber.ts new file mode 100644 index 000000000..c819932f4 --- /dev/null +++ b/packages/server/src/modules/Branches/subscribers/Activate/BillBranchesActivateSubscriber.ts @@ -0,0 +1,28 @@ +import { IBranchesActivatedPayload } from '../../Branches.types'; +import { events } from '@/common/events/events'; +import { Injectable } from '@nestjs/common'; +import { BillActivateBranches } from '../../integrations/Purchases/BillBranchesActivate'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class BillBranchesActivateSubscriber { + constructor( + private readonly billActivateBranches: BillActivateBranches, + ) { } + + /** + * Updates bills transactions with the primary branch once + * the multi-branches is activated. + * @param {IBranchesActivatedPayload} + */ + @OnEvent(events.branch.onActivated) + async updateBillsWithBranchOnActivated({ + primaryBranch, + trx, + }: IBranchesActivatedPayload) { + await this.billActivateBranches.updateBillsWithBranch( + primaryBranch.id, + trx, + ); + } +} diff --git a/packages/server/src/modules/Branches/subscribers/Activate/VendorCreditBranchesActivateSubscriber.ts b/packages/server/src/modules/Branches/subscribers/Activate/VendorCreditBranchesActivateSubscriber.ts new file mode 100644 index 000000000..3d2991092 --- /dev/null +++ b/packages/server/src/modules/Branches/subscribers/Activate/VendorCreditBranchesActivateSubscriber.ts @@ -0,0 +1,28 @@ +import { IBranchesActivatedPayload } from '../../Branches.types'; +import { events } from '@/common/events/events'; +import { Injectable } from '@nestjs/common'; +import { VendorCreditActivateBranches } from '../../integrations/Purchases/VendorCreditBranchesActivate'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class VendorCreditBranchesActivateSubscriber { + constructor( + private readonly vendorCreditActivateBranches: VendorCreditActivateBranches, + ) { } + + /** + * Updates vendor credits transactions with the primary branch once + * the multi-branches is activated. + * @param {IBranchesActivatedPayload} + */ + @OnEvent(events.branch.onActivated) + async updateVendorCreditsWithBranchOnActivated({ + primaryBranch, + trx, + }: IBranchesActivatedPayload) { + await this.vendorCreditActivateBranches.updateVendorCreditsWithBranch( + primaryBranch.id, + trx, + ); + } +} From 36bfa573ada2907753192b0c61aaf9414fa96c87 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 7 Feb 2026 16:58:57 +0200 Subject: [PATCH 002/106] =?UTF-8?q?=F0=9F=90=9B=20fix(manual-journal):=20f?= =?UTF-8?q?ix=20race=20condition=20in=20form=20submission=20handlers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the order of setSubmitPayload and submitForm calls in all six button handlers to prevent race condition where submitForm reads stale state before setSubmitPayload updates it. Changes: - handleSubmitPublishBtnClick - handleSubmitPublishAndNewBtnClick - handleSubmitPublishContinueEditingBtnClick - handleSubmitDraftBtnClick - handleSubmitDraftAndNewBtnClick - handleSubmitDraftContinueEditingBtnClick Co-Authored-By: Claude Haiku 4.5 --- .../MakeJournal/MakeJournalFormFloatingActions.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/webapp/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.tsx b/packages/webapp/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.tsx index 1b0164e38..aca9a0c3a 100644 --- a/packages/webapp/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.tsx +++ b/packages/webapp/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.tsx @@ -32,38 +32,38 @@ export default function MakeJournalFloatingAction() { // Handle submit & publish button click. const handleSubmitPublishBtnClick = (event) => { - submitForm(); setSubmitPayload({ redirect: true, publish: true }); + submitForm(); }; // Handle submit, publish & new button click. const handleSubmitPublishAndNewBtnClick = (event) => { - submitForm(); setSubmitPayload({ redirect: false, publish: true, resetForm: true }); + submitForm(); }; // Handle submit, publish & edit button click. const handleSubmitPublishContinueEditingBtnClick = (event) => { - submitForm(); setSubmitPayload({ redirect: false, publish: true }); + submitForm(); }; // Handle submit as draft button click. const handleSubmitDraftBtnClick = (event) => { - submitForm(); setSubmitPayload({ redirect: true, publish: false }); + submitForm(); }; // Handle submit as draft & new button click. const handleSubmitDraftAndNewBtnClick = (event) => { - submitForm(); setSubmitPayload({ redirect: false, publish: false, resetForm: true }); + submitForm(); }; // Handle submit as draft & continue editing button click. const handleSubmitDraftContinueEditingBtnClick = (event) => { - submitForm(); setSubmitPayload({ redirect: false, publish: false }); + submitForm(); }; // Handle cancel button click. From 3258159474de05ade1f4f9e95e9230d9d6f89ba7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 8 Feb 2026 09:39:55 +0200 Subject: [PATCH 003/106] feat: add rate limiting to organization build job endpoint Add @Throttle decorator to GET /build/:buildJobId endpoint to limit to 300 requests per minute to prevent abuse. Co-Authored-By: Claude Sonnet 4.5 --- .../src/modules/Organization/Organization.controller.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/modules/Organization/Organization.controller.ts b/packages/server/src/modules/Organization/Organization.controller.ts index 529065e80..dc38fdfa9 100644 --- a/packages/server/src/modules/Organization/Organization.controller.ts +++ b/packages/server/src/modules/Organization/Organization.controller.ts @@ -17,6 +17,7 @@ import { HttpCode, Param, } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { BuildOrganizationService } from './commands/BuildOrganization.service'; import { BuildOrganizationDto, @@ -50,7 +51,7 @@ export class OrganizationController { private readonly updateOrganizationService: UpdateOrganizationService, private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob, private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking, - ) { } + ) {} @Post('build') @HttpCode(200) @@ -77,6 +78,7 @@ export class OrganizationController { } @Get('build/:buildJobId') + @Throttle({ default: { limit: 300, ttl: 60000 } }) // 300 req/min @ApiParam({ name: 'buildJobId', required: true, From c47acdee0373901ebee94a6a06d89629e21608f3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 9 Feb 2026 13:40:18 +0200 Subject: [PATCH 004/106] fix(webapp): correct API endpoint URLs for universal search Update resource URL mappings to match backend NestJS controller routes: - /sales/invoices -> /sale-invoices - /sales/estimates -> /sale-estimates - /sales/receipts -> /sale-receipts - /purchases/bills -> /bills - /sales/payment_receives -> /payments-received - /purchases/bill_payments -> /bill-payments - /sales/credit_notes -> /credit-notes - /purchases/vendor-credit -> /vendor-credits Co-Authored-By: Claude Sonnet 4.5 --- .../src/hooks/query/GenericResource/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/webapp/src/hooks/query/GenericResource/index.tsx b/packages/webapp/src/hooks/query/GenericResource/index.tsx index 70cfb2f2f..348202312 100644 --- a/packages/webapp/src/hooks/query/GenericResource/index.tsx +++ b/packages/webapp/src/hooks/query/GenericResource/index.tsx @@ -32,19 +32,19 @@ export function useResourceData(type, query, props) { */ function getResourceUrlFromType(type) { const config = { - [RESOURCES_TYPES.INVOICE]: '/sales/invoices', - [RESOURCES_TYPES.ESTIMATE]: '/sales/estimates', + [RESOURCES_TYPES.INVOICE]: '/sale-invoices', + [RESOURCES_TYPES.ESTIMATE]: '/sale-estimates', [RESOURCES_TYPES.ITEM]: '/items', - [RESOURCES_TYPES.RECEIPT]: '/sales/receipts', - [RESOURCES_TYPES.BILL]: '/purchases/bills', - [RESOURCES_TYPES.PAYMENT_RECEIVE]: '/sales/payment_receives', - [RESOURCES_TYPES.PAYMENT_MADE]: '/purchases/bill_payments', + [RESOURCES_TYPES.RECEIPT]: '/sale-receipts', + [RESOURCES_TYPES.BILL]: '/bills', + [RESOURCES_TYPES.PAYMENT_RECEIVE]: '/payments-received', + [RESOURCES_TYPES.PAYMENT_MADE]: '/bill-payments', [RESOURCES_TYPES.CUSTOMER]: '/customers', [RESOURCES_TYPES.VENDOR]: '/vendors', [RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals', [RESOURCES_TYPES.ACCOUNT]: '/accounts', - [RESOURCES_TYPES.CREDIT_NOTE]: '/sales/credit_notes', - [RESOURCES_TYPES.VENDOR_CREDIT]: '/purchases/vendor-credit', + [RESOURCES_TYPES.CREDIT_NOTE]: '/credit-notes', + [RESOURCES_TYPES.VENDOR_CREDIT]: '/vendor-credits', }; return config[type] || ''; } From 1972861c9775576b099794f2c9db7132cd258445 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 9 Feb 2026 13:40:34 +0200 Subject: [PATCH 005/106] fix(server): add missing searchRoles to Item model Add searchRoles static property to enable searching items by name and code. This fixes the 500 Internal Server Error when searching items via /api/items?search_keyword=... Co-Authored-By: Claude Sonnet 4.5 --- packages/server/src/modules/Items/models/Item.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/server/src/modules/Items/models/Item.ts b/packages/server/src/modules/Items/models/Item.ts index d3a5900f1..2d0e5fb9a 100644 --- a/packages/server/src/modules/Items/models/Item.ts +++ b/packages/server/src/modules/Items/models/Item.ts @@ -70,6 +70,16 @@ export class Item extends TenantBaseModel { }; } + /** + * Model search roles. + */ + static get searchRoles() { + return [ + { condition: 'or', fieldKey: 'name', comparator: 'contains' }, + { condition: 'or', fieldKey: 'code', comparator: 'like' }, + ]; + } + /** * Relationship mapping. */ From 7375512fecfc5260eb2160f9ff2557effa2d5b8e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 9 Feb 2026 19:26:26 +0200 Subject: [PATCH 006/106] refactor: update UniversalSearch components with TypeScript and TextStatus --- .../src/components/TextStatus/index.tsx | 9 +- .../UniversalSearch/UniversalSearch.tsx | 396 +++++++++++++++--- .../UniversalSearchProvider.tsx | 74 +++- .../Purchases/Bills/BillUniversalSearch.tsx | 24 +- .../EstimateUniversalSearch.tsx | 20 +- .../Sales/Invoices/InvoiceUniversalSearch.tsx | 21 +- .../Sales/Receipts/ReceiptUniversalSearch.tsx | 13 +- packages/webapp/src/style/App.scss | 1 - packages/webapp/src/style/_variables.scss | 42 ++ .../src/style/components/UniversalSearch.scss | 200 --------- 10 files changed, 484 insertions(+), 316 deletions(-) delete mode 100644 packages/webapp/src/style/components/UniversalSearch.scss diff --git a/packages/webapp/src/components/TextStatus/index.tsx b/packages/webapp/src/components/TextStatus/index.tsx index 1c3d5fe03..a46a0e81d 100644 --- a/packages/webapp/src/components/TextStatus/index.tsx +++ b/packages/webapp/src/components/TextStatus/index.tsx @@ -10,12 +10,17 @@ const TextStatusRoot = styled.span` ${(props) => props.intent === 'warning' && ` - color: #ec5b0a;`} + color: #c87619;`} + + ${(props) => + props.intent === 'danger' && + ` + color: #f17377;`} ${(props) => props.intent === 'success' && ` - color: #2ba01d;`} + color: #238551;`} ${(props) => props.intent === 'none' && diff --git a/packages/webapp/src/components/UniversalSearch/UniversalSearch.tsx b/packages/webapp/src/components/UniversalSearch/UniversalSearch.tsx index 9d65ef565..5874abba3 100644 --- a/packages/webapp/src/components/UniversalSearch/UniversalSearch.tsx +++ b/packages/webapp/src/components/UniversalSearch/UniversalSearch.tsx @@ -1,7 +1,5 @@ -// @ts-nocheck -import React from 'react'; +import React, { KeyboardEvent, ReactNode } from 'react'; import intl from 'react-intl-universal'; -import classNames from 'classnames'; import { isUndefined } from 'lodash'; import { Overlay, @@ -10,11 +8,14 @@ import { MenuItem, Spinner, Intent, + OverlayProps, + Button, } from '@blueprintjs/core'; -import { QueryList } from '@blueprintjs/select'; -import { CLASSES } from '@/constants/classes'; - -import { Icon, If, ListSelect, FormattedMessage as T } from '@/components'; +import { QueryList, ItemRenderer } from '@blueprintjs/select'; +import { x } from '@xstyled/emotion'; +import { css } from '@emotion/css'; +import { Icon, If, FormattedMessage as T } from '@/components'; +import { Select } from '@blueprintjs-formik/select'; import { UniversalSearchProvider, useUniversalSearchContext, @@ -22,59 +23,297 @@ import { import { filterItemsByResourceType } from './utils'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; +// Resource type from RESOURCES_TYPES constant +type ResourceType = string; + +// Search type option item +interface SearchTypeOption { + key: ResourceType; + label: string; +} + +// Universal search item +interface UniversalSearchItem { + id: number | string; + _type: ResourceType; + text: string; + subText?: string; + label?: string; + [key: string]: any; +} + +// CSS styles for complex selectors +const overlayStyles = css` + .bp4-overlay-appear, + .bp4-overlay-enter { + filter: blur(20px); + opacity: 0.2; + } + .bp4-overlay-appear-active, + .bp4-overlay-enter-active { + filter: blur(0); + opacity: 1; + transition: + filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9), + opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9); + } + .bp4-overlay-exit { + filter: blur(0); + opacity: 1; + } + .bp4-overlay-exit-active { + filter: blur(20px); + opacity: 0.2; + transition: + filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9), + opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9); + } +`; + +const containerStyles = css` + position: fixed; + filter: blur(0); + opacity: 1; + background-color: var(--color-universal-search-background); + border-radius: 3px; + box-shadow: + 0 0 0 1px rgba(16, 22, 26, 0.1), + 0 4px 8px rgba(16, 22, 26, 0.2), + 0 18px 46px 6px rgba(16, 22, 26, 0.2); + left: calc(50% - 250px); + top: 20vh; + width: 500px; + z-index: 20; + + .bp4-input-group { + .bp4-icon { + margin: 16px; + color: var(--color-universal-search-icon); + + svg { + stroke: currentColor; + fill: none; + fill-rule: evenodd; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; + --text-opacity: 1; + } + } + } + + .bp4-input-group .bp4-input { + border: 0; + box-shadow: 0 0 0 0; + height: 50px; + line-height: 50px; + font-size: 20px; + } + .bp4-input-group.bp4-large .bp4-input:not(:first-child) { + padding-left: 50px !important; + } + .bp4-input-group.bp4-large .bp4-input:not(:last-child) { + padding-right: 130px !important; + } + + .bp4-menu { + border-top: 1px solid var(--color-universal-search-menu-border); + max-height: calc(60vh - 20px); + overflow: auto; + + .bp4-menu-item { + .bp4-text-muted { + font-size: 12px; + + .bp4-icon { + color: var(--bp4-gray-600); + } + } + &.bp4-intent-primary { + &.bp4-active { + background-color: var(--bp4-blue-100); + color: var(--bp4-dark-gray-800); + + .bp4-menu-item-label { + color: var(--bp4-gray-600); + } + } + } + + &-label { + flex-direction: row; + text-align: right; + } + } + } + + .bp4-input-action { + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + } +`; + +const inputRightElementsStyles = css` + display: flex; + margin: 10px; + + .bp4-spinner { + margin-right: 6px; + } +`; + +const footerStyles = css` + padding: 12px 12px; + border-top: 1px solid var(--color-universal-search-footer-divider); +`; + +const actionBaseStyles = css` + &:not(:first-of-type) { + margin-left: 14px; + } + + .bp4-tag { + background: var(--color-universal-search-tag-background); + color: var(--color-universal-search-tag-text); + } +`; + +const actionArrowsStyles = css` + &:not(:first-of-type) { + margin-left: 14px; + } + + .bp4-tag { + background: var(--color-universal-search-tag-background); + color: var(--color-universal-search-tag-text); + padding: 0; + text-align: center; + line-height: 16px; + margin-left: 4px; + + svg { + fill: var(--color-universal-search-tag-text); + height: 100%; + display: block; + width: 100%; + padding: 2px; + } + } +`; + +// UniversalSearchInputRightElements props +interface UniversalSearchInputRightElementsProps { + /** Callback when search type changes */ + onSearchTypeChange?: (option: SearchTypeOption) => void; +} + /** * Universal search input action. */ -function UniversalSearchInputRightElements({ onSearchTypeChange }) { - const { isLoading, searchType, defaultSearchResource, searchTypeOptions } = +function UniversalSearchInputRightElements({ + onSearchTypeChange, +}: UniversalSearchInputRightElementsProps) { + const { isLoading, searchType, searchTypeOptions } = useUniversalSearchContext(); + // Find the currently selected item object. + const selectedItem = searchTypeOptions.find( + (item) => item.key === searchType, + ); + // Handle search type option change. - const handleSearchTypeChange = (option) => { - onSearchTypeChange && onSearchTypeChange(option); + const handleSearchTypeChange = (option: SearchTypeOption) => { + onSearchTypeChange?.(option); + }; + + // Item renderer for the select dropdown. + const itemRenderer: ItemRenderer = ( + item, + { handleClick }, + ) => { + return ; }; return ( -
+ - + - items={searchTypeOptions} + itemRenderer={itemRenderer} onItemSelect={handleSearchTypeChange} + selectedValue={selectedItem?.key} + valueAccessor={'key'} + labelAccessor={'label'} filterable={false} - initialSelectedItem={defaultSearchResource} - selectedItem={searchType} - selectedItemProp={'key'} - textProp={'label'} - // defaultText={intl.get('type')} popoverProps={{ minimal: true, captureDismiss: true, - className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY, - }} - buttonProps={{ - minimal: true, - className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_BTN, }} + input={({ activeItem }) => ( +
+ ); } +// QueryList renderer props +interface QueryListRendererProps { + /** Current query string */ + query: string; + /** Callback when query changes */ + handleQueryChange: (event: React.ChangeEvent) => void; + /** Item list element */ + itemList: ReactNode; + /** Class name */ + className?: string; + /** Handle key down */ + handleKeyDown?: (event: KeyboardEvent) => void; + /** Handle key up */ + handleKeyUp?: (event: KeyboardEvent) => void; +} + +// UniversalSearchQueryList props +interface UniversalSearchQueryListProps { + /** Whether the search is open */ + isOpen: boolean; + /** Whether the search is loading */ + isLoading: boolean; + /** Callback when search type changes */ + onSearchTypeChange?: (option: SearchTypeOption) => void; + /** Current search type */ + searchType: ResourceType; + /** Items to display */ + items: UniversalSearchItem[]; + /** Renderer for items */ + itemRenderer?: ItemRenderer; + /** Callback when an item is selected */ + onItemSelect?: (item: UniversalSearchItem, event?: any) => void; + /** Current query string */ + query: string; + /** Callback when query changes */ + onQueryChange?: (query: string) => void; +} + /** * Universal search query list. */ -function UniversalSearchQueryList(props) { - const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } = - props; - +function UniversalSearchQueryList({ + isOpen, + isLoading, + onSearchTypeChange, + ...restProps +}: UniversalSearchQueryListProps) { return ( - + {...(restProps as any)} initialContent={null} - renderer={(listProps) => ( + renderer={(listProps: QueryListRendererProps) => ( -
+ + ENTER - {intl.get('universal_search.enter_text')} -
+ {intl.get('universal_search.enter_text')} + -
+ ESC{' '} - {intl.get('universal_search.close_text')} -
+ {intl.get('universal_search.close_text')} + -
+ - {intl.get('universal_seach.navigate_text')} -
- + {intl.get('universal_seach.navigate_text')} + + ); } +// UniversalSearchBar props +interface UniversalSearchBarProps extends QueryListRendererProps { + /** Whether the search is open */ + isOpen: boolean; + /** Callback when search type changes */ + onSearchTypeChange?: (option: SearchTypeOption) => void; +} + /** * Universal search input bar with items list. */ -function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) { +function UniversalSearchBar({ + isOpen, + onSearchTypeChange, + ...listProps +}: UniversalSearchBarProps) { const { handleKeyDown, handleKeyUp } = listProps; const handlers = isOpen ? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp } : {}; return ( -
+ } @@ -155,17 +400,44 @@ function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) { autoFocus={true} /> {listProps.itemList} -
+ ); } +// UniversalSearch props +export interface UniversalSearchProps { + /** Default search resource type */ + defaultSearchResource?: ResourceType; + /** Controlled search resource type */ + searchResource?: ResourceType; + /** Overlay props */ + overlayProps?: OverlayProps; + /** Whether the search overlay is open */ + isOpen: boolean; + /** Whether the search is loading */ + isLoading: boolean; + /** Callback when search type changes */ + onSearchTypeChange?: (resource: SearchTypeOption) => void; + /** Items to display */ + items: UniversalSearchItem[]; + /** Available search type options */ + searchTypeOptions: SearchTypeOption[]; + /** Renderer for items */ + itemRenderer?: ItemRenderer; + /** Callback when an item is selected */ + onItemSelect?: (item: UniversalSearchItem, event?: any) => void; + /** Current query string */ + query: string; + /** Callback when query changes */ + onQueryChange?: (query: string) => void; +} + /** * Universal search. */ export function UniversalSearch({ defaultSearchResource, searchResource, - overlayProps, isOpen, isLoading, @@ -173,9 +445,9 @@ export function UniversalSearch({ items, searchTypeOptions, ...queryListProps -}) { +}: UniversalSearchProps) { // Search type state. - const [searchType, setSearchType] = React.useState( + const [searchType, setSearchType] = React.useState( defaultSearchResource || RESOURCES_TYPES.CUSTOMER, ); // Handle search resource type controlled mode. @@ -189,9 +461,9 @@ export function UniversalSearch({ }, [searchResource, defaultSearchResource]); // Handle search type change. - const handleSearchTypeChange = (searchTypeResource) => { + const handleSearchTypeChange = (searchTypeResource: SearchTypeOption) => { setSearchType(searchTypeResource.key); - onSearchTypeChange && onSearchTypeChange(searchTypeResource); + onSearchTypeChange?.(searchTypeResource); }; // Filters query list items based on the given search type. const filteredItems = filterItemsByResourceType(items, searchType); @@ -200,7 +472,7 @@ export function UniversalSearch({ -
+ -
+ -
-
+ +
); diff --git a/packages/webapp/src/components/UniversalSearch/UniversalSearchProvider.tsx b/packages/webapp/src/components/UniversalSearch/UniversalSearchProvider.tsx index f7f206593..6cb84b978 100644 --- a/packages/webapp/src/components/UniversalSearch/UniversalSearchProvider.tsx +++ b/packages/webapp/src/components/UniversalSearch/UniversalSearchProvider.tsx @@ -1,30 +1,82 @@ -// @ts-nocheck -import React, { createContext } from 'react'; +import React, { createContext, ReactNode, useContext } from 'react'; -const UniversalSearchContext = createContext(); +// The resource type value from RESOURCES_TYPES constant +type ResourceType = string; + +// Search type option item +interface SearchTypeOption { + key: ResourceType; + label: string; +} + +// Context value type +interface UniversalSearchContextValue { + /** Whether the search is loading */ + isLoading: boolean; + /** Current search type/resource type */ + searchType: ResourceType; + /** Default search resource type */ + defaultSearchResource?: ResourceType; + /** List of available search type options */ + searchTypeOptions: SearchTypeOption[]; +} + +// Create the context with undefined as initial value +const UniversalSearchContext = createContext< + UniversalSearchContextValue | undefined +>(undefined); + +// Provider props interface +interface UniversalSearchProviderProps { + /** Whether the search is loading */ + isLoading: boolean; + /** Default search resource type */ + defaultSearchResource?: ResourceType; + /** Current search type/resource type */ + searchType: ResourceType; + /** List of available search type options */ + searchTypeOptions: SearchTypeOption[]; + /** Child elements */ + children: ReactNode; +} /** * Universal search data provider. */ -function UniversalSearchProvider({ +export function UniversalSearchProvider({ isLoading, defaultSearchResource, searchType, searchTypeOptions, - ...props -}) { + children, +}: UniversalSearchProviderProps) { // Provider payload. - const provider = { + const provider: UniversalSearchContextValue = { isLoading, searchType, defaultSearchResource, searchTypeOptions, }; - return ; + return ( + + {children} + + ); } -const useUniversalSearchContext = () => - React.useContext(UniversalSearchContext); +/** + * Hook to access the universal search context. + * @throws Error if used outside of UniversalSearchProvider + */ +export const useUniversalSearchContext = (): UniversalSearchContextValue => { + const context = useContext(UniversalSearchContext); -export { UniversalSearchProvider, useUniversalSearchContext }; + if (context === undefined) { + throw new Error( + 'useUniversalSearchContext must be used within a UniversalSearchProvider', + ); + } + + return context; +}; diff --git a/packages/webapp/src/containers/Purchases/Bills/BillUniversalSearch.tsx b/packages/webapp/src/containers/Purchases/Bills/BillUniversalSearch.tsx index b54ced67d..cabc7b353 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillUniversalSearch.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillUniversalSearch.tsx @@ -1,10 +1,10 @@ // @ts-nocheck import React from 'react'; import intl from 'react-intl-universal'; -import { MenuItem } from '@blueprintjs/core'; +import { MenuItem, Intent } from '@blueprintjs/core'; import { formattedAmount } from '@/utils'; -import { T, Icon, Choose, If } from '@/components'; +import { T, Icon, Choose, If, TextStatus } from '@/components'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { AbilitySubject, BillAction } from '@/constants/abilityOption'; @@ -41,35 +41,35 @@ export function BillStatus({ bill }) { return ( - + - + - + {intl.get('overdue_by', { overdue: bill.overdue_days })} - + - + {intl.get('due_in', { due: bill.remaining_days })} - + - + {intl.get('day_partially_paid', { due: formattedAmount(bill.due_amount, bill.currency_code), })} - + - + - + ); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.tsx index fbdda4f4a..ef6e24bd3 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.tsx @@ -1,9 +1,9 @@ // @ts-nocheck import React from 'react'; import intl from 'react-intl-universal'; -import { MenuItem } from '@blueprintjs/core'; +import { MenuItem, Intent } from '@blueprintjs/core'; -import { Choose, T, Icon } from '@/components'; +import { Choose, T, Icon, TextStatus } from '@/components'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption'; @@ -37,28 +37,28 @@ export const EstimateUniversalSearchSelect = withDrawerActions( export const EstimateStatus = ({ estimate }) => ( - + - + - + - + - + - + - + - + ); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceUniversalSearch.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceUniversalSearch.tsx index aa76f2481..879ccf2d8 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceUniversalSearch.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceUniversalSearch.tsx @@ -1,9 +1,9 @@ // @ts-nocheck import React from 'react'; import intl from 'react-intl-universal'; -import { MenuItem } from '@blueprintjs/core'; +import { MenuItem, Intent } from '@blueprintjs/core'; -import { T, Choose, Icon } from '@/components'; +import { T, Choose, Icon, TextStatus } from '@/components'; import { highlightText } from '@/utils'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; @@ -39,29 +39,29 @@ function InvoiceStatus({ customer }) { return ( - + - + - + {intl.get('overdue_by', { overdue: customer.overdue_days })} - + - + {intl.get('due_in', { due: customer.remaining_days })} - + - + - + ); @@ -94,7 +94,6 @@ export function InvoiceUniversalSearchItem( } onClick={handleClick} - className={'universal-search__item--invoice'} /> ); } diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptUniversalSearch.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptUniversalSearch.tsx index 7b7c645d8..9c1aca8be 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptUniversalSearch.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptUniversalSearch.tsx @@ -1,9 +1,8 @@ // @ts-nocheck import React from 'react'; import intl from 'react-intl-universal'; -import { MenuItem } from '@blueprintjs/core'; - -import { Icon, Choose, T } from '@/components'; +import { MenuItem, Intent } from '@blueprintjs/core'; +import { Icon, Choose, T, TextStatus } from '@/components'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; @@ -39,15 +38,15 @@ function ReceiptStatus({ receipt }) { return ( - + - + - + - + ); diff --git a/packages/webapp/src/style/App.scss b/packages/webapp/src/style/App.scss index e6f046584..ecebc0c5c 100644 --- a/packages/webapp/src/style/App.scss +++ b/packages/webapp/src/style/App.scss @@ -31,7 +31,6 @@ @import 'components/Overlay'; @import 'components/Menu'; @import 'components/SidebarOverlay'; -@import 'components/UniversalSearch'; // Pages @import 'pages/view-form'; diff --git a/packages/webapp/src/style/_variables.scss b/packages/webapp/src/style/_variables.scss index 40df32901..9cf20f6a7 100644 --- a/packages/webapp/src/style/_variables.scss +++ b/packages/webapp/src/style/_variables.scss @@ -7,6 +7,27 @@ $ns: bp4; --color-primary: #8abbff; --color-danger: red; + // Green colors + --color-green-500: #165a36; + --color-green-400: #1c6e42; + --color-green-300: #238551; + --color-green-200: #32a467; + --color-green-100: #72ca9b; + + // Red colors + --color-red-500: #8e292c; + --color-red-400: #ac2f33; + --color-red-300: #cd4246; + --color-red-200: #e76a6e; + --color-red-100: #fa999c; + + // Orange colors + --color-orange-500: #77450d; + --color-orange-400: #935610; + --color-orange-300: #c87619; + --color-orange-200: #ec9a3c; + --color-orange-100: #fbb360; + --color-dark-gray5: #404854; --color-dark-gray4: #383e47; --color-dark-gray3: #2f343c; @@ -301,6 +322,27 @@ body.bp4-dark { --color-primary: #8abbff; --color-danger: rgb(213, 103, 103); + // Green colors (dark mode - lighter variants) + --color-green-500: #72ca9b; + --color-green-400: #32a467; + --color-green-300: #238551; + --color-green-200: #1c6e42; + --color-green-100: #165a36; + + // Red colors (dark mode - lighter variants) + --color-red-500: #fa999c; + --color-red-400: #e76a6e; + --color-red-300: #cd4246; + --color-red-200: #ac2f33; + --color-red-100: #8e292c; + + // Orange colors (dark mode - lighter variants) + --color-orange-500: #fbb360; + --color-orange-400: #ec9a3c; + --color-orange-300: #c87619; + --color-orange-200: #935610; + --color-orange-100: #77450d; + --color-dark-gray5: #404854; --color-dark-gray4: #383e47; --color-dark-gray3: #2f343c; diff --git a/packages/webapp/src/style/components/UniversalSearch.scss b/packages/webapp/src/style/components/UniversalSearch.scss deleted file mode 100644 index a9562e096..000000000 --- a/packages/webapp/src/style/components/UniversalSearch.scss +++ /dev/null @@ -1,200 +0,0 @@ -.universal-search { - position: fixed; - filter: blur(0); - opacity: 1; - background-color: var(--color-universal-search-background); - border-radius: 3px; - box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.1), - 0 4px 8px rgba(16, 22, 26, 0.2), - 0 18px 46px 6px rgba(16, 22, 26, 0.2); - left: calc(50% - 250px); - top: 20vh; - width: 500px; - z-index: 20; - - &.bp4-overlay-appear, - &.bp4-overlay-enter { - filter: blur(20px); - opacity: 0.2; - } - &.bp4-overlay-appear-active, - &.bp4-overlay-enter-active { - filter: blur(0); - opacity: 1; - transition-delay: 0; - transition-duration: 0.2s; - transition-property: filter, opacity; - transition-timing-function: cubic-bezier(0.4, 1, 0.75, 0.9); - } - &.bp4-overlay-exit { - filter: blur(0); - opacity: 1; - } - &.bp4-overlay-exit-active { - filter: blur(20px); - opacity: 0.2; - transition-delay: 0; - transition-duration: 0.2s; - transition-property: filter, opacity; - transition-timing-function: cubic-bezier(0.4, 1, 0.75, 0.9); - } - - &__omnibar { - .bp4-input-group { - .bp4-icon { - svg { - stroke: currentColor; - fill: none; - fill-rule: evenodd; - stroke-linecap: round; - stroke-linejoin: round; - } - } - } - - .bp4-input-group .bp4-input { - border: 0; - box-shadow: 0 0 0 0; - height: 50px; - line-height: 50px; - font-size: 20px; - } - .bp4-input-group.bp4-large .bp4-input:not(:first-child) { - padding-left: 50px !important; - } - .bp4-input-group.bp4-large .bp4-input:not(:last-child) { - padding-right: 130px !important; - } - - .bp4-input-group { - .bp4-icon { - margin: 16px; - color: var(--color-universal-search-icon); - - svg { - stroke-width: 2; - --text-opacity: 1; - } - } - } - - .bp4-menu { - border-top: 1px solid var(--color-universal-search-menu-border); - max-height: calc(60vh - 20px); - overflow: auto; - - .bp4-menu-item { - .bp4-text-muted { - font-size: 12px; - - .bp4-icon { - color: #8499a7; - } - } - &.bp4-intent-primary { - &.bp4-active { - background-color: rgb(235, 241, 246); - color: #252b30; - - .bp4-menu-item-label { - color: #5c7080; - } - } - } - - &-label { - flex-direction: row; - text-align: right; - } - } - } - - .bp4-input-action { - height: 100%; - display: flex; - flex-direction: row; - align-items: center; - } - } - - &__type-select-overlay { - .bp4-button { - margin: 0 !important; - } - } - - &__footer { - padding: 12px 12px; - border-top: 1px solid var(--color-universal-search-footer-divider); - } - - &__actions { - display: flex; - } - - &__action { - &:not(:first-of-type) { - margin-left: 14px; - } - - .bp4-tag { - background: var(--color-universal-search-tag-background); - color: var(--color-universal-search-tag-text); - } - &--arrows { - .bp4-tag { - padding: 0; - text-align: center; - line-height: 16px; - margin-left: 4px; - - svg { - fill: var(--color-universal-search-tag-text); - height: 100%; - display: block; - width: 100%; - padding: 2px; - } - } - } - - .text { - margin-left: 6px; - } - } - - &__footer { - } - - &-input-right-elements { - display: flex; - margin: 10px; - - .bp4-spinner { - margin-right: 6px; - } - } - - &__item { - &--invoice, - &--estimate, - &--bill, - &--receipt { - .amount { - color: #252b30; - } - - .status { - font-size: 13px; - - &.status-warning { - color: rgb(236, 91, 10); - } - - &.status-success { - color: #249017; - } - } - } - } -} From 09ff72d302a739ea1a18104d310ff5c3844e8af4 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 9 Feb 2026 19:52:17 +0200 Subject: [PATCH 007/106] fix: add TypeScript types to If component --- packages/webapp/src/components/Utils/If.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/webapp/src/components/Utils/If.tsx b/packages/webapp/src/components/Utils/If.tsx index baf80bdc8..85263ca34 100644 --- a/packages/webapp/src/components/Utils/If.tsx +++ b/packages/webapp/src/components/Utils/If.tsx @@ -1,12 +1,10 @@ -// @ts-nocheck -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ReactNode } from 'react'; -export const If = (props) => - props.condition ? (props.render ? props.render() : props.children) : null; +interface IfProps { + condition: boolean; + children?: ReactNode; + render?: () => ReactNode; +} -If.propTypes = { - // condition: PropTypes.bool.isRequired, - children: PropTypes.node, - render: PropTypes.func, -}; +export const If = (props: IfProps): React.ReactElement | null => + props.condition ? (props.render ? <>{props.render()} : <>{props.children}) : null; From 956a9b58dd00a65d65f1c49e5334c1ef9606bce2 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 10 Feb 2026 00:22:40 +0200 Subject: [PATCH 008/106] fix(server): register SystemDatabaseController and add PublicRoute decorator - Register SystemDatabaseController in SystemDatabaseModule to expose /api/system_db endpoint - Add PublicRoute decorator to bypass authentication for healthcheck endpoint - Update ping() method to return { status: 'ok' } with HTTP 200 This fixes the Docker healthcheck that was failing with 404 Not Found errors. Co-Authored-By: Claude Sonnet 4.5 --- .../modules/System/SystemDB/SystemDB.controller.ts | 12 +++++++----- .../src/modules/System/SystemDB/SystemDB.module.ts | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/server/src/modules/System/SystemDB/SystemDB.controller.ts b/packages/server/src/modules/System/SystemDB/SystemDB.controller.ts index ca010fd99..af4e98217 100644 --- a/packages/server/src/modules/System/SystemDB/SystemDB.controller.ts +++ b/packages/server/src/modules/System/SystemDB/SystemDB.controller.ts @@ -1,12 +1,14 @@ -import { Controller, Get, Post } from '@nestjs/common'; +import { Controller, Get, HttpCode } from '@nestjs/common'; +import { PublicRoute } from '@/modules/Auth/guards/jwt.guard'; -@Controller('/system_db') +@Controller('system_db') +@PublicRoute() export class SystemDatabaseController { constructor() {} - @Post() @Get() - ping(){ - + @HttpCode(200) + ping() { + return { status: 'ok' }; } } diff --git a/packages/server/src/modules/System/SystemDB/SystemDB.module.ts b/packages/server/src/modules/System/SystemDB/SystemDB.module.ts index d1387efa5..6c3610385 100644 --- a/packages/server/src/modules/System/SystemDB/SystemDB.module.ts +++ b/packages/server/src/modules/System/SystemDB/SystemDB.module.ts @@ -6,6 +6,7 @@ import { SystemKnexConnectionConfigure, } from './SystemDB.constants'; import { knexSnakeCaseMappers } from 'objection'; +import { SystemDatabaseController } from './SystemDB.controller'; const providers = [ { @@ -42,6 +43,7 @@ const providers = [ @Global() @Module({ + controllers: [SystemDatabaseController], providers: [...providers], exports: [...providers], }) From 11575cfb960ab0fe7ff0aa9a12d707e36ac563e3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Feb 2026 18:37:39 +0200 Subject: [PATCH 009/106] fix(webapp): allow formatted phone numbers in customer and vendor forms --- .../Customers/CustomerForm/CustomerForm.schema.tsx | 8 ++++---- .../containers/Vendors/VendorForm/VendorForm.schema.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/webapp/src/containers/Customers/CustomerForm/CustomerForm.schema.tsx b/packages/webapp/src/containers/Customers/CustomerForm/CustomerForm.schema.tsx index 8cdb5d308..c18783e4a 100644 --- a/packages/webapp/src/containers/Customers/CustomerForm/CustomerForm.schema.tsx +++ b/packages/webapp/src/containers/Customers/CustomerForm/CustomerForm.schema.tsx @@ -17,8 +17,8 @@ const Schema = Yup.object().shape({ .label(intl.get('display_name_')), email: Yup.string().email().nullable(), - work_phone: Yup.number(), - personal_phone: Yup.number(), + work_phone: Yup.string().nullable(), + personal_phone: Yup.string().nullable(), website: Yup.string().url().nullable(), active: Yup.boolean(), @@ -30,7 +30,7 @@ const Schema = Yup.object().shape({ billing_address_city: Yup.string().trim(), billing_address_state: Yup.string().trim(), billing_address_postcode: Yup.string().nullable(), - billing_address_phone: Yup.number(), + billing_address_phone: Yup.string().nullable(), shipping_address_country: Yup.string().trim(), shipping_address_1: Yup.string().trim(), @@ -38,7 +38,7 @@ const Schema = Yup.object().shape({ shipping_address_city: Yup.string().trim(), shipping_address_state: Yup.string().trim(), shipping_address_postcode: Yup.string().nullable(), - shipping_address_phone: Yup.number(), + shipping_address_phone: Yup.string().nullable(), opening_balance: Yup.number().nullable(), currency_code: Yup.string(), diff --git a/packages/webapp/src/containers/Vendors/VendorForm/VendorForm.schema.tsx b/packages/webapp/src/containers/Vendors/VendorForm/VendorForm.schema.tsx index 5e64c61f0..6393481c3 100644 --- a/packages/webapp/src/containers/Vendors/VendorForm/VendorForm.schema.tsx +++ b/packages/webapp/src/containers/Vendors/VendorForm/VendorForm.schema.tsx @@ -10,8 +10,8 @@ const Schema = Yup.object().shape({ display_name: Yup.string().trim().required().label(intl.get('display_name_')), email: Yup.string().email().nullable(), - work_phone: Yup.number(), - personal_phone: Yup.number(), + work_phone: Yup.string().nullable(), + personal_phone: Yup.string().nullable(), website: Yup.string().url().nullable(), active: Yup.boolean(), @@ -23,7 +23,7 @@ const Schema = Yup.object().shape({ billing_address_city: Yup.string().trim(), billing_address_state: Yup.string().trim(), billing_address_postcode: Yup.string().nullable(), - billing_address_phone: Yup.number(), + billing_address_phone: Yup.string().nullable(), shipping_address_country: Yup.string().trim(), shipping_address_1: Yup.string().trim(), @@ -31,7 +31,7 @@ const Schema = Yup.object().shape({ shipping_address_city: Yup.string().trim(), shipping_address_state: Yup.string().trim(), shipping_address_postcode: Yup.string().nullable(), - shipping_address_phone: Yup.number(), + shipping_address_phone: Yup.string().nullable(), opening_balance: Yup.number().nullable(), currency_code: Yup.string(), From a7f98201cc437cdee5f14f7cfe92735e755cd2f7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Feb 2026 19:11:58 +0200 Subject: [PATCH 010/106] fix: allow 'payee' field in bank rule conditions validation The BankRuleConditionDto validation only allowed 'description' and 'amount' fields, but the frontend also sends 'payee' as a valid condition field. This caused a 400 Bad Request error when creating rules with payee conditions. Co-Authored-By: Claude Sonnet 4.5 --- packages/server/src/modules/BankRules/dtos/BankRule.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/modules/BankRules/dtos/BankRule.dto.ts b/packages/server/src/modules/BankRules/dtos/BankRule.dto.ts index 455caa36d..0619a2dc9 100644 --- a/packages/server/src/modules/BankRules/dtos/BankRule.dto.ts +++ b/packages/server/src/modules/BankRules/dtos/BankRule.dto.ts @@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators'; class BankRuleConditionDto { @IsNotEmpty() - @IsIn(['description', 'amount']) + @IsIn(['description', 'amount', 'payee']) field: string; @IsNotEmpty() From d909dad1bffa9ad1c6a5abe5df13dd7c1c782d2c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Feb 2026 19:35:21 +0200 Subject: [PATCH 011/106] fix: add missing S3_FORCE_PATH_STYLE environment variable The S3 module was referencing config.forcePathStyle but the value was never being read from the environment. This adds the missing forcePathStyle configuration to the S3 config. Closes #940 Co-Authored-By: Claude Sonnet 4.5 --- packages/server/src/common/config/s3.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/common/config/s3.ts b/packages/server/src/common/config/s3.ts index dd6ac7ed6..b74ffd6ef 100644 --- a/packages/server/src/common/config/s3.ts +++ b/packages/server/src/common/config/s3.ts @@ -6,4 +6,5 @@ export default registerAs('s3', () => ({ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, endpoint: process.env.S3_ENDPOINT, bucket: process.env.S3_BUCKET, + forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true', })); From 49c2777587df5601514f8a9b8d436e7d0e1b1f32 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Feb 2026 23:15:20 +0200 Subject: [PATCH 012/106] fix: re-recognize transactions when bank rule is edited (closes #809) --- .../modules/BankingTranasctionsRegonize/_types.ts | 9 +++++++-- .../commands/RecognizeTranasctions.service.ts | 4 ++++ .../events/TriggerRecognizedTransactions.ts | 3 +++ .../jobs/RecognizeTransactionsJob.ts | 14 +++++++++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/server/src/modules/BankingTranasctionsRegonize/_types.ts b/packages/server/src/modules/BankingTranasctionsRegonize/_types.ts index 71a18e07e..5e5770bd3 100644 --- a/packages/server/src/modules/BankingTranasctionsRegonize/_types.ts +++ b/packages/server/src/modules/BankingTranasctionsRegonize/_types.ts @@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob = export const RecognizeUncategorizedTransactionsQueue = 'recognize-uncategorized-transactions-queue'; - export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload { ruleId: number, - transactionsCriteria: any; + transactionsCriteria?: RecognizeTransactionsCriteria; + /** + * When true, first reverts recognized transactions before recognizing again. + * Used when a bank rule is edited to ensure transactions previously recognized + * by lower-priority rules are re-evaluated against the updated rule. + */ + shouldRevert?: boolean; } \ No newline at end of file diff --git a/packages/server/src/modules/BankingTranasctionsRegonize/commands/RecognizeTranasctions.service.ts b/packages/server/src/modules/BankingTranasctionsRegonize/commands/RecognizeTranasctions.service.ts index 9d79678c0..9fe353530 100644 --- a/packages/server/src/modules/BankingTranasctionsRegonize/commands/RecognizeTranasctions.service.ts +++ b/packages/server/src/modules/BankingTranasctionsRegonize/commands/RecognizeTranasctions.service.ts @@ -93,6 +93,10 @@ export class RecognizeTranasctionsService { q.whereIn('id', rulesIds); } q.withGraphFetched('conditions'); + + // Order by the 'order' field to ensure higher priority rules (lower order values) + // are matched first. + q.orderBy('order', 'asc'); }); const bankRulesByAccountId = transformToMapBy( diff --git a/packages/server/src/modules/BankingTranasctionsRegonize/events/TriggerRecognizedTransactions.ts b/packages/server/src/modules/BankingTranasctionsRegonize/events/TriggerRecognizedTransactions.ts index 791d7d8cd..7a3ce7286 100644 --- a/packages/server/src/modules/BankingTranasctionsRegonize/events/TriggerRecognizedTransactions.ts +++ b/packages/server/src/modules/BankingTranasctionsRegonize/events/TriggerRecognizedTransactions.ts @@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber { const tenantPayload = await this.tenancyContect.getTenantJobPayload(); const payload = { ruleId: bankRule.id, + shouldRevert: true, ...tenantPayload, } as RecognizeUncategorizedTransactionsJobPayload; // Re-recognize the transactions based on the new rules. + // Setting shouldRevert to true ensures that transactions previously recognized + // by this or lower-priority rules are re-evaluated against the updated rule. await this.recognizeTransactionsQueue.add( RecognizeUncategorizedTransactionsJob, payload, diff --git a/packages/server/src/modules/BankingTranasctionsRegonize/jobs/RecognizeTransactionsJob.ts b/packages/server/src/modules/BankingTranasctionsRegonize/jobs/RecognizeTransactionsJob.ts index 3e4c81a85..eb72773f0 100644 --- a/packages/server/src/modules/BankingTranasctionsRegonize/jobs/RecognizeTransactionsJob.ts +++ b/packages/server/src/modules/BankingTranasctionsRegonize/jobs/RecognizeTransactionsJob.ts @@ -3,6 +3,7 @@ import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Scope } from '@nestjs/common'; import { ClsService, UseCls } from 'nestjs-cls'; import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service'; +import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service'; import { RecognizeUncategorizedTransactionsJobPayload, RecognizeUncategorizedTransactionsQueue, @@ -15,10 +16,12 @@ import { export class RegonizeTransactionsPrcessor extends WorkerHost { /** * @param {RecognizeTranasctionsService} recognizeTranasctionsService - + * @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService - * @param {ClsService} clsService - */ constructor( private readonly recognizeTranasctionsService: RecognizeTranasctionsService, + private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService, private readonly clsService: ClsService, ) { super(); @@ -29,12 +32,21 @@ export class RegonizeTransactionsPrcessor extends WorkerHost { */ @UseCls() async process(job: Job) { - const { ruleId, transactionsCriteria } = job.data; + const { ruleId, transactionsCriteria, shouldRevert } = job.data; this.clsService.set('organizationId', job.data.organizationId); this.clsService.set('userId', job.data.userId); try { + // If shouldRevert is true, first revert recognized transactions before re-recognizing. + // This is used when a bank rule is edited to ensure transactions previously recognized + // by lower-priority rules are re-evaluated against the updated rule. + if (shouldRevert) { + await this.revertRecognizedTransactionsService.revertRecognizedTransactions( + ruleId, + transactionsCriteria, + ); + } await this.recognizeTranasctionsService.recognizeTransactions( ruleId, transactionsCriteria, From 25ca62083665538baa0525bade363844f1f5c2a4 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 12 Feb 2026 14:59:55 +0200 Subject: [PATCH 013/106] fix: add consistent Box wrapper to paper template forms in customize components --- .../CreditNoteCustomize/CreditNoteCustomizeContent.tsx | 5 ++++- .../Estimates/EstimateCustomize/EstimateCustomizeContent.tsx | 5 ++++- .../PaymentReceivedCustomizeContent.tsx | 5 ++++- .../Receipts/ReceiptCustomize/ReceiptCustomizeContent.tsx | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeContent.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeContent.tsx index 08efc0897..03104ced0 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeContent.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeContent.tsx @@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; +import { Box } from '@/components'; export function CreditNoteCustomizeContent() { const { payload, name } = useDrawerContext(); @@ -45,7 +46,9 @@ function CreditNoteCustomizeFormContent() { return ( - + + + diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeContent.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeContent.tsx index 4a3424417..5c8c3425b 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeContent.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeContent.tsx @@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state'; import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; +import { Box } from '@/components'; export function EstimateCustomizeContent() { const { payload, name } = useDrawerContext(); @@ -44,7 +45,9 @@ function EstimateCustomizeFormContent() { return ( - + + + diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeContent.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeContent.tsx index 10eea4c87..f64dee669 100644 --- a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeContent.tsx +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeContent.tsx @@ -19,6 +19,7 @@ import { useDrawerActions } from '@/hooks/state'; import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; +import { Box } from '@/components'; export function PaymentReceivedCustomizeContent() { const { payload, name } = useDrawerContext(); @@ -51,7 +52,9 @@ function PaymentReceivedCustomizeFormContent() { return ( - + + + diff --git a/packages/webapp/src/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeContent.tsx b/packages/webapp/src/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeContent.tsx index baf816735..324c9881f 100644 --- a/packages/webapp/src/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeContent.tsx +++ b/packages/webapp/src/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeContent.tsx @@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; +import { Box } from '@/components'; export function ReceiptCustomizeContent() { const { payload, name } = useDrawerContext(); @@ -44,7 +45,9 @@ function ReceiptCustomizeFormContent() { return ( - + + + From e0d9a56a2993dafc8855fbeb25f6572a0d8cd735 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 12 Feb 2026 20:06:49 +0200 Subject: [PATCH 014/106] fix: tax rates API and UI improvements - Add @ToNumber() decorator to rate field for proper validation - Fix getTaxRates to return { data: taxRates } response - Fix useTaxRate URL typo and response handling - Fix activate/inactivate endpoint methods and paths - Apply TEXT_MUTED class to description and compound tax - Add dark mode support for rate number display --- .../modules/TaxRates/TaxRate.application.ts | 7 ++++--- .../modules/TaxRates/TaxRate.controller.ts | 11 +++++++--- .../src/modules/TaxRates/dtos/TaxRate.dto.ts | 2 ++ .../containers/TaxRates/containers/_utils.tsx | 20 ++++++++----------- .../TaxRateDetailsContentDetails.tsx | 6 +++++- packages/webapp/src/hooks/query/taxRates.ts | 8 ++++---- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/packages/server/src/modules/TaxRates/TaxRate.application.ts b/packages/server/src/modules/TaxRates/TaxRate.application.ts index 2159c7d33..8847dcbe7 100644 --- a/packages/server/src/modules/TaxRates/TaxRate.application.ts +++ b/packages/server/src/modules/TaxRates/TaxRate.application.ts @@ -62,10 +62,11 @@ export class TaxRatesApplication { /** * Retrieves the tax rates list. - * @returns {Promise} + * @returns {Promise<{ data: ITaxRate[] }>} */ - public getTaxRates() { - return this.getTaxRatesService.getTaxRates(); + public async getTaxRates() { + const taxRates = await this.getTaxRatesService.getTaxRates(); + return { data: taxRates }; } /** diff --git a/packages/server/src/modules/TaxRates/TaxRate.controller.ts b/packages/server/src/modules/TaxRates/TaxRate.controller.ts index 476ac4e32..43a2cf63a 100644 --- a/packages/server/src/modules/TaxRates/TaxRate.controller.ts +++ b/packages/server/src/modules/TaxRates/TaxRate.controller.ts @@ -85,9 +85,14 @@ export class TaxRatesController { status: 200, description: 'The tax rates have been successfully retrieved.', schema: { - type: 'array', - items: { - $ref: getSchemaPath(TaxRateResponseDto), + type: 'object', + properties: { + data: { + type: 'array', + items: { + $ref: getSchemaPath(TaxRateResponseDto), + }, + }, }, }, }) diff --git a/packages/server/src/modules/TaxRates/dtos/TaxRate.dto.ts b/packages/server/src/modules/TaxRates/dtos/TaxRate.dto.ts index 678e81c94..7bbca6105 100644 --- a/packages/server/src/modules/TaxRates/dtos/TaxRate.dto.ts +++ b/packages/server/src/modules/TaxRates/dtos/TaxRate.dto.ts @@ -1,3 +1,4 @@ +import { ToNumber } from '@/common/decorators/Validators'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { @@ -30,6 +31,7 @@ export class CommandTaxRateDto { */ @IsNumber() @IsNotEmpty() + @ToNumber() @ApiProperty({ description: 'The rate of the tax rate.', example: 10, diff --git a/packages/webapp/src/containers/TaxRates/containers/_utils.tsx b/packages/webapp/src/containers/TaxRates/containers/_utils.tsx index 29fe64649..fb752e5bf 100644 --- a/packages/webapp/src/containers/TaxRates/containers/_utils.tsx +++ b/packages/webapp/src/containers/TaxRates/containers/_utils.tsx @@ -1,8 +1,8 @@ // @ts-nocheck import React from 'react'; -import { Intent, Tag } from '@blueprintjs/core'; +import { Intent, Tag, Classes } from '@blueprintjs/core'; import { Align } from '@/constants'; -import styled from 'styled-components'; +import clsx from 'classnames'; const codeAccessor = (taxRate) => { return ( @@ -28,13 +28,17 @@ const nameAccessor = (taxRate) => { return ( <> {taxRate.name} - {!!taxRate.is_compound && (Compound tax)} + {!!taxRate.is_compound && ( + (Compound tax) + )} ); }; const DescriptionAccessor = (taxRate) => { - return {taxRate.description}; + return ( + {taxRate.description} + ); }; /** @@ -72,11 +76,3 @@ export const useTaxRatesTableColumns = () => { ]; }; -const CompoundText = styled('span')` - color: #738091; - margin-left: 5px; -`; - -const DescriptionText = styled('span')` - color: #5f6b7c; -`; diff --git a/packages/webapp/src/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsContentDetails.tsx b/packages/webapp/src/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsContentDetails.tsx index e145ddaef..ac6e9dd69 100644 --- a/packages/webapp/src/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsContentDetails.tsx +++ b/packages/webapp/src/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsContentDetails.tsx @@ -74,9 +74,13 @@ const TaxRateHeader = styled(`div`)` const TaxRateAmount = styled('div')` line-height: 1; font-size: 30px; - color: #565b71; font-weight: 600; display: inline-block; + color: var(--x-color-amount-text, #565b71); + + .bp4-dark & { + color: rgba(255, 255, 255, 0.9); + } `; const TaxRateActiveTag = styled(Tag)` diff --git a/packages/webapp/src/hooks/query/taxRates.ts b/packages/webapp/src/hooks/query/taxRates.ts index a026d4387..add6f7928 100644 --- a/packages/webapp/src/hooks/query/taxRates.ts +++ b/packages/webapp/src/hooks/query/taxRates.ts @@ -37,10 +37,10 @@ export function useTaxRate(taxRateId: string, props) { [QUERY_TYPES.TAX_RATES, taxRateId], { method: 'get', - url: `tax-rates/${taxRateId}}`, + url: `tax-rates/${taxRateId}`, }, { - select: (res) => res.data.data, + select: (res) => res.data, ...props, }, ); @@ -106,7 +106,7 @@ export function useActivateTaxRate(props) { const queryClient = useQueryClient(); const apiRequest = useApiRequest(); - return useMutation((id) => apiRequest.post(`tax-rates/${id}/active`), { + return useMutation((id) => apiRequest.put(`tax-rates/${id}/activate`), { onSuccess: (res, id) => { commonInvalidateQueries(queryClient); queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]); @@ -122,7 +122,7 @@ export function useInactivateTaxRate(props) { const queryClient = useQueryClient(); const apiRequest = useApiRequest(); - return useMutation((id) => apiRequest.post(`tax-rates/${id}/inactive`), { + return useMutation((id) => apiRequest.put(`tax-rates/${id}/inactivate`), { onSuccess: (res, id) => { commonInvalidateQueries(queryClient); queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]); From 36cbb1eef53399bb189231a0e7dc37b0408e36d4 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 12 Feb 2026 23:19:16 +0200 Subject: [PATCH 015/106] fix: use DrawerActionsBar instead of DashboardActionsBar in drawer components Replace DashboardActionsBar with DrawerActionsBar in all drawer action bars for consistency with the design system: - ContactDetailActionsBar - CustomerDetailsActionsBar - VendorCreditDetailActionsBar - TaxRateDetailsContentActionsBar Co-Authored-By: Claude Opus 4.6 --- .../Drawers/ContactDetailDrawer/ContactDetailActionsBar.tsx | 6 +++--- .../CustomerDetailsDrawer/CustomerDetailsActionsBar.tsx | 1 - .../VendorCreditDetailActionsBar.tsx | 6 +++--- .../TaxRateDetailsContentActionsBar.tsx | 6 +++--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/webapp/src/containers/Drawers/ContactDetailDrawer/ContactDetailActionsBar.tsx b/packages/webapp/src/containers/Drawers/ContactDetailDrawer/ContactDetailActionsBar.tsx index 27f767133..dfa09477b 100644 --- a/packages/webapp/src/containers/Drawers/ContactDetailDrawer/ContactDetailActionsBar.tsx +++ b/packages/webapp/src/containers/Drawers/ContactDetailDrawer/ContactDetailActionsBar.tsx @@ -15,7 +15,7 @@ import { useContactDetailDrawerContext } from './ContactDetailDrawerProvider'; import { withAlertActions } from '@/containers/Alert/withAlertActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; -import { DashboardActionsBar, Icon, FormattedMessage as T } from '@/components'; +import { DrawerActionsBar, Icon, FormattedMessage as T } from '@/components'; import { safeCallback, compose } from '@/utils'; @@ -46,7 +46,7 @@ function ContactDetailActionsBar({ }; return ( - +