Compare commits

...

27 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a2160c0595 Merge pull request #939 from bigcapitalhq/fix/ahmedbouhuolia/docker-healthcheck-endpoint
fix(server): fix Docker healthcheck endpoint
2026-02-10 00:25:25 +02:00
Ahmed Bouhuolia
956a9b58dd 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 <noreply@anthropic.com>
2026-02-10 00:22:40 +02:00
Ahmed Bouhuolia
acb701d618 Merge pull request #938 from bigcapitalhq/fix/universal-search-api-endpoints
refactor: update UniversalSearch components with TypeScript and TextStatus
2026-02-09 19:55:37 +02:00
Ahmed Bouhuolia
09ff72d302 fix: add TypeScript types to If component 2026-02-09 19:52:17 +02:00
Ahmed Bouhuolia
7375512fec refactor: update UniversalSearch components with TypeScript and TextStatus 2026-02-09 19:26:26 +02:00
Ahmed Bouhuolia
77e65389a4 Merge pull request #937 from bigcapitalhq/fix/universal-search-api-endpoints
fix: universal search API endpoint errors
2026-02-09 13:42:53 +02:00
Ahmed Bouhuolia
1972861c97 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 <noreply@anthropic.com>
2026-02-09 13:40:34 +02:00
Ahmed Bouhuolia
c47acdee03 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 <noreply@anthropic.com>
2026-02-09 13:40:18 +02:00
Ahmed Bouhuolia
8689962bf3 Merge pull request #935 from bigcapitalhq/feat/abouolia/add-throttle-organization-build-job
feat: expand rate limiting of getting org build job endpoint
2026-02-09 13:23:49 +02:00
Ahmed Bouhuolia
3258159474 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 <noreply@anthropic.com>
2026-02-08 09:39:55 +02:00
Ahmed Bouhuolia
36bfa573ad 🐛 fix(manual-journal): fix race condition in form submission handlers
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 <noreply@anthropic.com>
2026-02-07 16:58:57 +02:00
Ahmed Bouhuolia
2c05785096 Merge pull request #934 from bigcapitalhq/fix/branches-activation-bills
fix(server): branches activation not marking bills and payments with primary branch
2026-02-05 16:06:39 +02:00
Ahmed Bouhuolia
6af4be9c6c 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
2026-02-05 16:03:57 +02:00
Ahmed Bouhuolia
8def1d31d2 Merge pull request #933 from bigcapitalhq/20260205-151219-7770
feat(webapp): add blurry background to sticky data table cells
2026-02-05 15:29:47 +02:00
Ahmed Bouhuolia
afab02a053 feat(webapp): add blurry background to sticky data table cells
Add backdrop-filter blur effect to sticky column cells in financial reports
to prevent content from showing through during horizontal scrolling.
The effect only applies when rows are not hovered to preserve hover
background interactions.
2026-02-05 15:27:45 +02:00
Ahmed Bouhuolia
8e925c62f2 Merge pull request #932 from bigcapitalhq/20260205-151219-7770
fix(server): balance sheet query validation schema
2026-02-05 15:14:45 +02:00
Ahmed Bouhuolia
1b7d513adf fix(server): balance sheet query validation schema 2026-02-05 15:12:54 +02:00
Ahmed Bouhuolia
7d764fb390 Merge pull request #931 from bigcapitalhq/fix/item-error-handling
fix(items): correct error type handling and add swagger documentation
2026-02-04 21:44:45 +02:00
Ahmed Bouhuolia
c571f50a74 fix(items): correct error type handling and add swagger documentation
- Fix error type mismatch: change 'ITEM.NAME.ALREADY.EXISTS' to 'ITEM_NAME_EXISTS'
- Add ItemErrorType constant with UpperCamelCase keys for better maintainability
- Update all error checks to use the new ItemErrorType constant
- Add ItemErrorResponse.dto.ts with documented error types for swagger
- Add @ApiResponse decorators to document 400 validation errors in swagger
2026-02-04 21:42:39 +02:00
Ahmed Bouhuolia
6549026344 Merge pull request #930 from bigcapitalhq/fix/account-delete-error-handling
fix(webapp): account delete error handling response types
2026-02-04 21:31:38 +02:00
Ahmed Bouhuolia
0963394b04 fix(webapp): account delete error handling response types 2026-02-04 21:27:25 +02:00
Ahmed Bouhuolia
6cab0651fc Merge pull request #927 from bigcapitalhq/feature/20260202223150
fix(webapp): darkmode warehouses list page
2026-02-02 22:36:42 +02:00
Ahmed Bouhuolia
4af537d6dd fix(webapp): darkmode warehouses list page 2026-02-02 22:31:53 +02:00
Ahmed Bouhuolia
34db64612c Merge pull request #926 from bigcapitalhq/20260202-185120-9c84
fix(webapp): constrant not found row color
2026-02-02 18:53:48 +02:00
Ahmed Bouhuolia
10225bbfed fix(webapp): constrant not found row color 2026-02-02 18:51:52 +02:00
Ahmed Bouhuolia
c3a4fe6b37 Merge pull request #924 from bigcapitalhq/20260201-180532-f578
fix(webapp): normalize api path
2026-02-01 18:06:51 +02:00
Ahmed Bouhuolia
02be959461 fix(webapp): normalize api path 2026-02-01 18:05:51 +02:00
50 changed files with 965 additions and 435 deletions

View File

@@ -1,6 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto'; import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto'; import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
import { BranchResponseDto } from '@/modules/Branches/dtos/BranchResponse.dto';
import { DiscountType } from '@/common/types/Discount'; import { DiscountType } from '@/common/types/Discount';
export class BillResponseDto { export class BillResponseDto {
@@ -89,6 +91,14 @@ export class BillResponseDto {
}) })
branchId?: number; branchId?: number;
@ApiProperty({
description: 'Branch details',
type: () => BranchResponseDto,
required: false,
})
@Type(() => BranchResponseDto)
branch?: BranchResponseDto;
@ApiProperty({ @ApiProperty({
description: 'The ID of the project', description: 'The ID of the project',
example: 301, example: 301,

View File

@@ -30,6 +30,7 @@ export class BillTransformer extends Transformer {
'taxes', 'taxes',
'entries', 'entries',
'attachments', 'attachments',
'branch',
]; ];
}; };

View File

@@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance'
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator'; import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches'; import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches'; 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'; import { FeaturesModule } from '../Features/Features.module';
@Module({ @Module({
@@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module';
ValidateBranchExistance, ValidateBranchExistance,
ManualJournalBranchesValidator, ManualJournalBranchesValidator,
CashflowTransactionsActivateBranches, CashflowTransactionsActivateBranches,
ExpensesActivateBranches ExpensesActivateBranches,
BillActivateBranches,
VendorCreditActivateBranches,
BillPaymentsActivateBranches,
BillBranchesActivateSubscriber,
VendorCreditBranchesActivateSubscriber,
PaymentMadeActivateBranchesSubscriber
], ],
exports: [ exports: [
BranchesSettingsService, BranchesSettingsService,

View File

@@ -1,11 +1,14 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill'; import { Bill } from '@/modules/Bills/models/Bill';
@Injectable() @Injectable()
export class BillActivateBranches { export class BillActivateBranches {
constructor(private readonly billModel: TenantModelProxy<typeof Bill>) {} constructor(
@Inject(Bill.name)
private readonly billModel: TenantModelProxy<typeof Bill>,
) {}
/** /**
* Updates all bills transactions with the primary branch. * Updates all bills transactions with the primary branch.
@@ -17,7 +20,7 @@ export class BillActivateBranches {
primaryBranchId: number, primaryBranchId: number,
trx?: Knex.Transaction, trx?: Knex.Transaction,
) => { ) => {
// Updates the sale invoice with primary branch. // Updates the bills with primary branch.
await Bill.query(trx).update({ branchId: primaryBranchId }); await this.billModel().query(trx).update({ branchId: primaryBranchId });
}; };
} }

View File

@@ -1,11 +1,12 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { BillPayment } from '@/modules/BillPayments/models/BillPayment'; import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class BillPaymentsActivateBranches { export class BillPaymentsActivateBranches {
constructor( constructor(
@Inject(BillPayment.name)
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>, private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
) {} ) {}

View File

@@ -1,11 +1,12 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit'; import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
@Injectable() @Injectable()
export class VendorCreditActivateBranches { export class VendorCreditActivateBranches {
constructor( constructor(
@Inject(VendorCredit.name)
private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>, private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>,
) {} ) {}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -24,14 +24,14 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
displayColumnsType: 'total' | 'date_periods' = 'total'; displayColumnsType: 'total' | 'date_periods' = 'total';
@ApiProperty({ @ApiProperty({
enum: ['day', 'month', 'year'], enum: ['day', 'month', 'year', 'quarter'],
default: 'year', default: 'year',
description: 'Time period for column display', description: 'Time period for column display',
}) })
@IsString() @IsString()
@IsOptional() @IsOptional()
@IsEnum(['day', 'month', 'year']) @IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year'; displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({ @ApiProperty({
description: 'Start date for the balance sheet period', description: 'Start date for the balance sheet period',

View File

@@ -34,13 +34,13 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
@ApiProperty({ @ApiProperty({
description: 'Display columns by time period', description: 'Display columns by time period',
required: false, required: false,
enum: ['day', 'month', 'year'], enum: ['day', 'month', 'year', 'quarter'],
default: 'year', default: 'year',
}) })
@IsString() @IsString()
@IsOptional() @IsOptional()
@IsEnum(['day', 'month', 'year']) @IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year'; displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({ @ApiProperty({
description: 'Type of column display', description: 'Type of column display',

View File

@@ -64,10 +64,10 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
displayColumnsType: 'total' | 'date_periods'; displayColumnsType: 'total' | 'date_periods';
@IsString() @IsString()
@IsEnum(['day', 'month', 'year']) @IsEnum(['day', 'month', 'year', 'quarter'])
@IsOptional() @IsOptional()
@ApiProperty({ description: 'How to display columns' }) @ApiProperty({ description: 'How to display columns' })
displayColumnsBy: 'day' | 'month' | 'year' = 'year'; displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@Transform(({ value }) => parseBoolean(value, false)) @Transform(({ value }) => parseBoolean(value, false))
@IsBoolean() @IsBoolean()

View File

@@ -34,6 +34,7 @@ import {
BulkDeleteItemsDto, BulkDeleteItemsDto,
ValidateBulkDeleteItemsResponseDto, ValidateBulkDeleteItemsResponseDto,
} from './dtos/BulkDeleteItems.dto'; } from './dtos/BulkDeleteItems.dto';
import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto';
@Controller('/items') @Controller('/items')
@ApiTags('Items') @ApiTags('Items')
@@ -45,6 +46,7 @@ import {
@ApiExtraModels(ItemEstimatesResponseDto) @ApiExtraModels(ItemEstimatesResponseDto)
@ApiExtraModels(ItemReceiptsResponseDto) @ApiExtraModels(ItemReceiptsResponseDto)
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto) @ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
@ApiExtraModels(ItemApiErrorResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
export class ItemsController extends TenantController { export class ItemsController extends TenantController {
constructor(private readonly itemsApplication: ItemsApplicationService) { constructor(private readonly itemsApplication: ItemsApplicationService) {
@@ -147,6 +149,13 @@ export class ItemsController extends TenantController {
status: 200, status: 200,
description: 'The item has been successfully updated.', description: 'The item has been successfully updated.',
}) })
@ApiResponse({
status: 400,
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, INVENTORY_ACCOUNT_CANNOT_MODIFIED, TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The item not found.' }) @ApiResponse({ status: 404, description: 'The item not found.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -204,6 +213,13 @@ export class ItemsController extends TenantController {
status: 200, status: 200,
description: 'The item has been successfully created.', description: 'The item has been successfully created.',
}) })
@ApiResponse({
status: 400,
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, ITEM_CATEOGRY_NOT_FOUND, COST_ACCOUNT_NOT_COGS, SELL_ACCOUNT_NOT_INCOME, INVENTORY_ACCOUNT_NOT_INVENTORY, INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM, COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
// @UsePipes(new ZodValidationPipe(createItemSchema)) // @UsePipes(new ZodValidationPipe(createItemSchema))
async createItem( async createItem(
@Body() createItemDto: CreateItemDto, @Body() createItemDto: CreateItemDto,
@@ -219,6 +235,13 @@ export class ItemsController extends TenantController {
status: 200, status: 200,
description: 'The item has been successfully deleted.', description: 'The item has been successfully deleted.',
}) })
@ApiResponse({
status: 400,
description: 'Cannot delete item. Possible error types: ITEM_HAS_ASSOCIATED_TRANSACTINS, ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The item not found.' }) @ApiResponse({ status: 404, description: 'The item not found.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',

View File

@@ -0,0 +1,112 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* Item API Error Types
* These error types are returned when item operations fail validation
*/
export enum ItemErrorType {
/** Item name already exists in the system */
ItemNameExists = 'ITEM_NAME_EXISTS',
/** Item category was not found */
ItemCategoryNotFound = 'ITEM_CATEOGRY_NOT_FOUND',
/** Cost account is not a Cost of Goods Sold account */
CostAccountNotCogs = 'COST_ACCOUNT_NOT_COGS',
/** Cost account was not found */
CostAccountNotFound = 'COST_ACCOUNT_NOT_FOUMD',
/** Sell account was not found */
SellAccountNotFound = 'SELL_ACCOUNT_NOT_FOUND',
/** Sell account is not an income account */
SellAccountNotIncome = 'SELL_ACCOUNT_NOT_INCOME',
/** Inventory account was not found */
InventoryAccountNotFound = 'INVENTORY_ACCOUNT_NOT_FOUND',
/** Account is not an inventory type account */
InventoryAccountNotInventory = 'INVENTORY_ACCOUNT_NOT_INVENTORY',
/** Multiple items have associated transactions */
ItemsHaveAssociatedTransactions = 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
/** Item has associated transactions (singular) */
ItemHasAssociatedTransactions = 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
/** Item has associated inventory adjustments */
ItemHasAssociatedInventoryAdjustment = 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
/** Cannot change item type to inventory */
ItemCannotChangeInventoryType = 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
/** Cannot change type when item has transactions */
TypeCannotChangeWithItemHasTransactions = 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
/** Inventory account cannot be modified */
InventoryAccountCannotModified = 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
/** Purchase tax rate was not found */
PurchaseTaxRateNotFound = 'PURCHASE_TAX_RATE_NOT_FOUND',
/** Sell tax rate was not found */
SellTaxRateNotFound = 'SELL_TAX_RATE_NOT_FOUND',
/** Income account is required for sellable items */
IncomeAccountRequiredWithSellableItem = 'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
/** Cost account is required for purchasable items */
CostAccountRequiredWithPurchasableItem = 'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
/** Item not found */
NotFound = 'NOT_FOUND',
/** Items not found */
ItemsNotFound = 'ITEMS_NOT_FOUND',
}
/**
* Item API Error Response
* Returned when an item operation fails
*/
export class ItemErrorResponseDto {
@ApiProperty({
description: 'HTTP status code',
example: 400,
})
statusCode: number;
@ApiProperty({
description: 'Error type identifier',
enum: ItemErrorType,
example: ItemErrorType.ItemNameExists,
})
type: ItemErrorType;
@ApiProperty({
description: 'Human-readable error message',
example: 'The item name is already exist.',
required: false,
nullable: true,
})
message: string | null;
@ApiProperty({
description: 'Additional error payload data',
required: false,
nullable: true,
})
payload: any;
}
/**
* Item API Error Response Wrapper
*/
export class ItemApiErrorResponseDto {
@ApiProperty({
description: 'Array of error details',
type: [ItemErrorResponseDto],
})
errors: ItemErrorResponseDto[];
}

View File

@@ -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. * Relationship mapping.
*/ */

View File

@@ -17,6 +17,7 @@ import {
HttpCode, HttpCode,
Param, Param,
} from '@nestjs/common'; } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { BuildOrganizationService } from './commands/BuildOrganization.service'; import { BuildOrganizationService } from './commands/BuildOrganization.service';
import { import {
BuildOrganizationDto, BuildOrganizationDto,
@@ -50,7 +51,7 @@ export class OrganizationController {
private readonly updateOrganizationService: UpdateOrganizationService, private readonly updateOrganizationService: UpdateOrganizationService,
private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob, private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob,
private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking, private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking,
) { } ) {}
@Post('build') @Post('build')
@HttpCode(200) @HttpCode(200)
@@ -77,6 +78,7 @@ export class OrganizationController {
} }
@Get('build/:buildJobId') @Get('build/:buildJobId')
@Throttle({ default: { limit: 300, ttl: 60000 } }) // 300 req/min
@ApiParam({ @ApiParam({
name: 'buildJobId', name: 'buildJobId',
required: true, required: true,

View File

@@ -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 { export class SystemDatabaseController {
constructor() {} constructor() {}
@Post()
@Get() @Get()
ping(){ @HttpCode(200)
ping() {
return { status: 'ok' };
} }
} }

View File

@@ -6,6 +6,7 @@ import {
SystemKnexConnectionConfigure, SystemKnexConnectionConfigure,
} from './SystemDB.constants'; } from './SystemDB.constants';
import { knexSnakeCaseMappers } from 'objection'; import { knexSnakeCaseMappers } from 'objection';
import { SystemDatabaseController } from './SystemDB.controller';
const providers = [ const providers = [
{ {
@@ -42,6 +43,7 @@ const providers = [
@Global() @Global()
@Module({ @Module({
controllers: [SystemDatabaseController],
providers: [...providers], providers: [...providers],
exports: [...providers], exports: [...providers],
}) })

View File

@@ -4,7 +4,12 @@ import styled from 'styled-components';
import { DataTable } from '../Datatable'; import { DataTable } from '../Datatable';
export const ReportDataTable = styled(DataTable)` export const ReportDataTable = styled(DataTable)`
--x-table-no-results-border-color: #ddd;
.bp4-dark & {
--x-table-no-results-border-color: var(--color-dark-gray5);
}
.table .tbody .tr.no-results:last-of-type .td { .table .tbody .tr.no-results:last-of-type .td {
border-bottom: 1px solid #ddd; border-bottom: 1px solid var(--x-table-no-results-border-color);
} }
`; `;

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core'; import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { FormattedMessage as T } from '@/components'; import { FormattedMessage as T } from '@/components';
import preferencesMenu from '@/constants/preferencesMenu'; import { PreferencesMenu } from '@/constants/preferencesMenu';
import PreferencesSidebarContainer from './PreferencesSidebarContainer'; import PreferencesSidebarContainer from './PreferencesSidebarContainer';
import '@/style/pages/Preferences/Sidebar.scss'; import '@/style/pages/Preferences/Sidebar.scss';
@@ -15,7 +15,7 @@ export default function PreferencesSidebar() {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const items = preferencesMenu.map((item) => const items = PreferencesMenu.map((item) =>
item.divider ? ( item.divider ? (
<MenuDivider title={item.title} /> <MenuDivider title={item.title} />
) : ( ) : (

View File

@@ -10,12 +10,17 @@ const TextStatusRoot = styled.span`
${(props) => ${(props) =>
props.intent === 'warning' && props.intent === 'warning' &&
` `
color: #ec5b0a;`} color: #c87619;`}
${(props) =>
props.intent === 'danger' &&
`
color: #f17377;`}
${(props) => ${(props) =>
props.intent === 'success' && props.intent === 'success' &&
` `
color: #2ba01d;`} color: #238551;`}
${(props) => ${(props) =>
props.intent === 'none' && props.intent === 'none' &&

View File

@@ -1,7 +1,5 @@
// @ts-nocheck import React, { KeyboardEvent, ReactNode } from 'react';
import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { import {
Overlay, Overlay,
@@ -10,11 +8,14 @@ import {
MenuItem, MenuItem,
Spinner, Spinner,
Intent, Intent,
OverlayProps,
Button,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { QueryList } from '@blueprintjs/select'; import { QueryList, ItemRenderer } from '@blueprintjs/select';
import { CLASSES } from '@/constants/classes'; import { x } from '@xstyled/emotion';
import { css } from '@emotion/css';
import { Icon, If, ListSelect, FormattedMessage as T } from '@/components'; import { Icon, If, FormattedMessage as T } from '@/components';
import { Select } from '@blueprintjs-formik/select';
import { import {
UniversalSearchProvider, UniversalSearchProvider,
useUniversalSearchContext, useUniversalSearchContext,
@@ -22,59 +23,297 @@ import {
import { filterItemsByResourceType } from './utils'; import { filterItemsByResourceType } from './utils';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; 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. * Universal search input action.
*/ */
function UniversalSearchInputRightElements({ onSearchTypeChange }) { function UniversalSearchInputRightElements({
const { isLoading, searchType, defaultSearchResource, searchTypeOptions } = onSearchTypeChange,
}: UniversalSearchInputRightElementsProps) {
const { isLoading, searchType, searchTypeOptions } =
useUniversalSearchContext(); useUniversalSearchContext();
// Find the currently selected item object.
const selectedItem = searchTypeOptions.find(
(item) => item.key === searchType,
);
// Handle search type option change. // Handle search type option change.
const handleSearchTypeChange = (option) => { const handleSearchTypeChange = (option: SearchTypeOption) => {
onSearchTypeChange && onSearchTypeChange(option); onSearchTypeChange?.(option);
};
// Item renderer for the select dropdown.
const itemRenderer: ItemRenderer<SearchTypeOption> = (
item,
{ handleClick },
) => {
return <MenuItem text={item.label} key={item.key} onClick={handleClick} />;
}; };
return ( return (
<div className={CLASSES.UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS}> <x.div display="flex" m="10px" className={inputRightElementsStyles}>
<If condition={isLoading}> <If condition={isLoading}>
<Spinner tagName="div" intent={Intent.NONE} size={18} value={null} /> <Spinner tagName="div" intent={Intent.NONE} size={18} />
</If> </If>
<ListSelect <Select<SearchTypeOption>
items={searchTypeOptions} items={searchTypeOptions}
itemRenderer={itemRenderer}
onItemSelect={handleSearchTypeChange} onItemSelect={handleSearchTypeChange}
selectedValue={selectedItem?.key}
valueAccessor={'key'}
labelAccessor={'label'}
filterable={false} filterable={false}
initialSelectedItem={defaultSearchResource}
selectedItem={searchType}
selectedItemProp={'key'}
textProp={'label'}
// defaultText={intl.get('type')}
popoverProps={{ popoverProps={{
minimal: true, minimal: true,
captureDismiss: true, captureDismiss: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY,
}}
buttonProps={{
minimal: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_BTN,
}} }}
input={({ activeItem }) => (
<Button minimal={true} text={activeItem?.label} />
)}
/> />
</div> </x.div>
); );
} }
// QueryList renderer props
interface QueryListRendererProps {
/** Current query string */
query: string;
/** Callback when query changes */
handleQueryChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/** Item list element */
itemList: ReactNode;
/** Class name */
className?: string;
/** Handle key down */
handleKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
/** Handle key up */
handleKeyUp?: (event: KeyboardEvent<HTMLDivElement>) => 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<UniversalSearchItem>;
/** 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. * Universal search query list.
*/ */
function UniversalSearchQueryList(props) { function UniversalSearchQueryList({
const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } = isOpen,
props; isLoading,
onSearchTypeChange,
...restProps
}: UniversalSearchQueryListProps) {
return ( return (
<QueryList <QueryList<UniversalSearchItem>
{...restProps} {...(restProps as any)}
initialContent={null} initialContent={null}
renderer={(listProps) => ( renderer={(listProps: QueryListRendererProps) => (
<UniversalSearchBar <UniversalSearchBar
isOpen={isOpen} isOpen={isOpen}
onSearchTypeChange={onSearchTypeChange} onSearchTypeChange={onSearchTypeChange}
@@ -100,47 +339,53 @@ function UniversalSearchQueryList(props) {
*/ */
function UniversalQuerySearchActions() { function UniversalQuerySearchActions() {
return ( return (
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTIONS)}> <x.div display="flex">
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_SELECT)}> <x.div className={actionBaseStyles}>
<Tag>ENTER</Tag> <Tag>ENTER</Tag>
<span class={'text'}>{intl.get('universal_search.enter_text')}</span> <x.span ml="6px">{intl.get('universal_search.enter_text')}</x.span>
</div> </x.div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_CLOSE)}> <x.div className={actionBaseStyles}>
<Tag>ESC</Tag>{' '} <Tag>ESC</Tag>{' '}
<span class={'text'}>{intl.get('universal_search.close_text')}</span> <x.span ml="6px">{intl.get('universal_search.close_text')}</x.span>
</div> </x.div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_ARROWS)}> <x.div className={actionArrowsStyles}>
<Tag> <Tag>
<Icon icon={'arrow-up-24'} iconSize={16} /> <Icon icon={'arrow-up-24'} iconSize={16} />
</Tag> </Tag>
<Tag> <Tag>
<Icon icon={'arrow-down-24'} iconSize={16} /> <Icon icon={'arrow-down-24'} iconSize={16} />
</Tag> </Tag>
<span class="text">{intl.get('universal_seach.navigate_text')}</span> <x.span ml="6px">{intl.get('universal_seach.navigate_text')}</x.span>
</div> </x.div>
</div> </x.div>
); );
} }
// 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. * Universal search input bar with items list.
*/ */
function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) { function UniversalSearchBar({
isOpen,
onSearchTypeChange,
...listProps
}: UniversalSearchBarProps) {
const { handleKeyDown, handleKeyUp } = listProps; const { handleKeyDown, handleKeyUp } = listProps;
const handlers = isOpen const handlers = isOpen
? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp } ? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }
: {}; : {};
return ( return (
<div <x.div {...handlers}>
className={classNames(
CLASSES.UNIVERSAL_SEARCH_OMNIBAR,
listProps.className,
)}
{...handlers}
>
<InputGroup <InputGroup
large={true} large={true}
leftIcon={<Icon icon={'universal-search'} iconSize={20} />} leftIcon={<Icon icon={'universal-search'} iconSize={20} />}
@@ -155,17 +400,44 @@ function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
autoFocus={true} autoFocus={true}
/> />
{listProps.itemList} {listProps.itemList}
</div> </x.div>
); );
} }
// 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<UniversalSearchItem>;
/** 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. * Universal search.
*/ */
export function UniversalSearch({ export function UniversalSearch({
defaultSearchResource, defaultSearchResource,
searchResource, searchResource,
overlayProps, overlayProps,
isOpen, isOpen,
isLoading, isLoading,
@@ -173,9 +445,9 @@ export function UniversalSearch({
items, items,
searchTypeOptions, searchTypeOptions,
...queryListProps ...queryListProps
}) { }: UniversalSearchProps) {
// Search type state. // Search type state.
const [searchType, setSearchType] = React.useState( const [searchType, setSearchType] = React.useState<ResourceType>(
defaultSearchResource || RESOURCES_TYPES.CUSTOMER, defaultSearchResource || RESOURCES_TYPES.CUSTOMER,
); );
// Handle search resource type controlled mode. // Handle search resource type controlled mode.
@@ -189,9 +461,9 @@ export function UniversalSearch({
}, [searchResource, defaultSearchResource]); }, [searchResource, defaultSearchResource]);
// Handle search type change. // Handle search type change.
const handleSearchTypeChange = (searchTypeResource) => { const handleSearchTypeChange = (searchTypeResource: SearchTypeOption) => {
setSearchType(searchTypeResource.key); setSearchType(searchTypeResource.key);
onSearchTypeChange && onSearchTypeChange(searchTypeResource); onSearchTypeChange?.(searchTypeResource);
}; };
// Filters query list items based on the given search type. // Filters query list items based on the given search type.
const filteredItems = filterItemsByResourceType(items, searchType); const filteredItems = filterItemsByResourceType(items, searchType);
@@ -200,7 +472,7 @@ export function UniversalSearch({
<Overlay <Overlay
hasBackdrop={true} hasBackdrop={true}
isOpen={isOpen} isOpen={isOpen}
className={classNames(CLASSES.UNIVERSAL_SEARCH_OVERLAY)} className={overlayStyles}
{...overlayProps} {...overlayProps}
> >
<UniversalSearchProvider <UniversalSearchProvider
@@ -209,7 +481,7 @@ export function UniversalSearch({
defaultSearchResource={defaultSearchResource} defaultSearchResource={defaultSearchResource}
searchTypeOptions={searchTypeOptions} searchTypeOptions={searchTypeOptions}
> >
<div className={classNames(CLASSES.UNIVERSAL_SEARCH)}> <x.div className={containerStyles}>
<UniversalSearchQueryList <UniversalSearchQueryList
isOpen={isOpen} isOpen={isOpen}
isLoading={isLoading} isLoading={isLoading}
@@ -218,10 +490,10 @@ export function UniversalSearch({
{...queryListProps} {...queryListProps}
items={filteredItems} items={filteredItems}
/> />
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_FOOTER)}> <x.div className={footerStyles}>
<UniversalQuerySearchActions /> <UniversalQuerySearchActions />
</div> </x.div>
</div> </x.div>
</UniversalSearchProvider> </UniversalSearchProvider>
</Overlay> </Overlay>
); );

View File

@@ -1,30 +1,82 @@
// @ts-nocheck import React, { createContext, ReactNode, useContext } from 'react';
import React, { createContext } 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. * Universal search data provider.
*/ */
function UniversalSearchProvider({ export function UniversalSearchProvider({
isLoading, isLoading,
defaultSearchResource, defaultSearchResource,
searchType, searchType,
searchTypeOptions, searchTypeOptions,
...props children,
}) { }: UniversalSearchProviderProps) {
// Provider payload. // Provider payload.
const provider = { const provider: UniversalSearchContextValue = {
isLoading, isLoading,
searchType, searchType,
defaultSearchResource, defaultSearchResource,
searchTypeOptions, searchTypeOptions,
}; };
return <UniversalSearchContext.Provider value={provider} {...props} />; return (
<UniversalSearchContext.Provider value={provider}>
{children}
</UniversalSearchContext.Provider>
);
} }
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;
};

View File

@@ -1,12 +1,10 @@
// @ts-nocheck import React, { ReactNode } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
export const If = (props) => interface IfProps {
props.condition ? (props.render ? props.render() : props.children) : null; condition: boolean;
children?: ReactNode;
render?: () => ReactNode;
}
If.propTypes = { export const If = (props: IfProps): React.ReactElement | null =>
// condition: PropTypes.bool.isRequired, props.condition ? (props.render ? <>{props.render()}</> : <>{props.children}</>) : null;
children: PropTypes.node,
render: PropTypes.func,
};

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
export default [ export const AllocateLandedCostType = [
{ name: intl.get('bills'), value: 'Bill' }, { name: intl.get('bills'), value: 'Bill' },
{ name: intl.get('expenses'), value: 'Expense' }, { name: intl.get('expenses'), value: 'Expense' },
]; ];

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
export default { export const App = {
"app_name": "BigCapital", "app_name": "BigCapital",
"app_version": "0.0.1 (build 12344)", "app_version": "0.0.1 (build 12344)",
} }

View File

@@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
export default [ export const ContactsOptions = [
{ name: intl.get('customer'), path: 'customers' }, { name: intl.get('customer'), path: 'customers' },
{ name: intl.get('vendor'), path: 'vendors' }, { name: intl.get('vendor'), path: 'vendors' },
]; ];

View File

@@ -16,7 +16,7 @@ import {
VendorAction, VendorAction,
} from './abilityOption'; } from './abilityOption';
export default [ export const KeyboardShortcutsOptions = [
{ {
shortcut_key: 'Shift + I', shortcut_key: 'Shift + I',
description: intl.get('jump_to_the_invoices'), description: intl.get('jump_to_the_invoices'),

View File

@@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage as T } from '@/components'; import { FormattedMessage as T } from '@/components';
export default [ export const PreferencesMenu = [
{ {
text: <T id={'general'} />, text: <T id={'general'} />,
disabled: false, disabled: false,
@@ -13,10 +13,10 @@ export default [
disabled: false, disabled: false,
href: '/preferences/branding', href: '/preferences/branding',
}, },
{ // {
text: 'Billing', // text: 'Billing',
href: '/preferences/billing', // href: '/preferences/billing',
}, // },
{ {
text: <T id={'users'} />, text: <T id={'users'} />,
href: '/preferences/users', href: '/preferences/users',
@@ -63,11 +63,11 @@ export default [
disabled: false, disabled: false,
href: '/preferences/items', href: '/preferences/items',
}, },
{ // {
text: 'Integrations', // text: 'Integrations',
disabled: false, // disabled: false,
href: '/preferences/integrations' // href: '/preferences/integrations'
}, // },
{ {
text: 'API Keys', text: 'API Keys',
disabled: false, disabled: false,

View File

@@ -32,38 +32,38 @@ export default function MakeJournalFloatingAction() {
// Handle submit & publish button click. // Handle submit & publish button click.
const handleSubmitPublishBtnClick = (event) => { const handleSubmitPublishBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: true, publish: true }); setSubmitPayload({ redirect: true, publish: true });
submitForm();
}; };
// Handle submit, publish & new button click. // Handle submit, publish & new button click.
const handleSubmitPublishAndNewBtnClick = (event) => { const handleSubmitPublishAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true, resetForm: true }); setSubmitPayload({ redirect: false, publish: true, resetForm: true });
submitForm();
}; };
// Handle submit, publish & edit button click. // Handle submit, publish & edit button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => { const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true }); setSubmitPayload({ redirect: false, publish: true });
submitForm();
}; };
// Handle submit as draft button click. // Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => { const handleSubmitDraftBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: true, publish: false }); setSubmitPayload({ redirect: true, publish: false });
submitForm();
}; };
// Handle submit as draft & new button click. // Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => { const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false, resetForm: true }); setSubmitPayload({ redirect: false, publish: false, resetForm: true });
submitForm();
}; };
// Handle submit as draft & continue editing button click. // Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => { const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false }); setSubmitPayload({ redirect: false, publish: false });
submitForm();
}; };
// Handle cancel button click. // Handle cancel button click.

View File

@@ -8,6 +8,11 @@ import { If, AppToaster } from '@/components';
import { NormalCell, BalanceCell, BankBalanceCell } from './components'; import { NormalCell, BalanceCell, BankBalanceCell } from './components';
import { transformTableStateToQuery, isBlank } from '@/utils'; import { transformTableStateToQuery, isBlank } from '@/utils';
export const DeleteAccountTypeError = {
AccountPredefined: 'account_predefined',
AccountHasAssociatedTransactions: 'account_has_associated_transactions',
};
/** /**
* Account name accessor. * Account name accessor.
*/ */
@@ -26,13 +31,13 @@ export const accountNameAccessor = (account) => {
* Handle delete errors in bulk and singular. * Handle delete errors in bulk and singular.
*/ */
export const handleDeleteErrors = (errors) => { export const handleDeleteErrors = (errors) => {
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) { if (errors.find((e) => e.type === DeleteAccountTypeError.AccountPredefined)) {
AppToaster.show({ AppToaster.show({
message: intl.get('cannot_delete_predefined_accounts'), message: intl.get('cannot_delete_predefined_accounts'),
intent: Intent.DANGER, intent: Intent.DANGER,
}); });
} }
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) { if (errors.find((e) => e.type === DeleteAccountTypeError.AccountHasAssociatedTransactions)) {
AppToaster.show({ AppToaster.show({
message: intl.get('cannot_delete_account_has_associated_transactions'), message: intl.get('cannot_delete_account_has_associated_transactions'),
intent: Intent.DANGER, intent: Intent.DANGER,

View File

@@ -16,7 +16,7 @@ import { FormattedMessage as T, If, FFormGroup, FSelect, FRadioGroup, FInputGrou
import { handleStringChange } from '@/utils'; import { handleStringChange } from '@/utils';
import { FieldRequiredHint } from '@/components'; import { FieldRequiredHint } from '@/components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import allocateLandedCostType from '@/constants/allocateLandedCostType'; import { AllocateLandedCostType } from '@/constants/allocateLandedCostType';
import AllocateLandedCostFormBody from './AllocateLandedCostFormBody'; import AllocateLandedCostFormBody from './AllocateLandedCostFormBody';
import { import {
@@ -81,7 +81,7 @@ export default function AllocateLandedCostFormFields() {
> >
<FSelect <FSelect
name={'transaction_type'} name={'transaction_type'}
items={allocateLandedCostType} items={AllocateLandedCostType}
onItemChange={handleTransactionTypeChange} onItemChange={handleTransactionTypeChange}
filterable={false} filterable={false}
valueAccessor={'value'} valueAccessor={'value'}

View File

@@ -9,7 +9,7 @@ import { FormattedMessage as T } from '@/components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useContactDuplicateFromContext } from './ContactDuplicateProvider'; import { useContactDuplicateFromContext } from './ContactDuplicateProvider';
import Contacts from '@/constants/contactsOptions'; import { ContactsOptions } from '@/constants/contactsOptions';
import { withDialogActions } from '@/containers/Dialog/withDialogActions'; import { withDialogActions } from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
@@ -66,7 +66,7 @@ function ContactDuplicateForm({
> >
<FSelect <FSelect
name={'contact_type'} name={'contact_type'}
items={Contacts} items={ContactsOptions}
placeholder={<T id={'select_contact'} />} placeholder={<T id={'select_contact'} />}
textAccessor={'name'} textAccessor={'name'}
valueAccessor={'path'} valueAccessor={'path'}

View File

@@ -1,8 +1,20 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import { getColumnWidth } from '@/utils'; import { getColumnWidth } from '@/utils';
import * as R from 'ramda'; import * as R from 'ramda';
import { useGeneralLedgerContext } from './GeneralLedgerProvider'; import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import { Align } from '@/constants'; import { Align, CLASSES } from '@/constants';
/**
* Description cell wraps value in a div with muted text class.
*/
function DescriptionCell({ cell: { value } }) {
return React.createElement(
'div',
{ className: `cell ${CLASSES.TEXT_MUTED}` },
value,
);
}
const getTableCellValueAccessor = (index) => `cells[${index}].value`; const getTableCellValueAccessor = (index) => `cells[${index}].value`;
@@ -75,6 +87,16 @@ const transactionIdColumnAccessor = (column) => {
}; };
}; };
/**
* Description column accessor (muted text in wrapped cell).
*/
const descriptionColumnAccessor = (column) => {
return {
...column,
Cell: DescriptionCell,
};
};
const dynamiColumnMapper = R.curry((data, column) => { const dynamiColumnMapper = R.curry((data, column) => {
const _numericColumnAccessor = numericColumnAccessor(data); const _numericColumnAccessor = numericColumnAccessor(data);
@@ -88,6 +110,7 @@ const dynamiColumnMapper = R.curry((data, column) => {
R.pathEq(['key'], 'reference_number'), R.pathEq(['key'], 'reference_number'),
transactionIdColumnAccessor, transactionIdColumnAccessor,
), ),
R.when(R.pathEq(['key'], 'description'), descriptionColumnAccessor),
R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor), R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor), R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'amount'), _numericColumnAccessor), R.when(R.pathEq(['key'], 'amount'), _numericColumnAccessor),

View File

@@ -1,9 +1,21 @@
// @ts-nocheck // @ts-nocheck
import { Align } from '@/constants'; import React from 'react';
import { Align, CLASSES } from '@/constants';
import { getColumnWidth } from '@/utils'; import { getColumnWidth } from '@/utils';
import * as R from 'ramda'; import * as R from 'ramda';
import { useJournalSheetContext } from './JournalProvider'; import { useJournalSheetContext } from './JournalProvider';
/**
* Description cell wraps value in a div with muted text class.
*/
function DescriptionCell({ cell: { value } }) {
return React.createElement(
'span',
{ className: `cell ${CLASSES.TEXT_MUTED}` },
value,
);
}
const getTableCellValueAccessor = (index) => `cells[${index}].value`; const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const getReportColWidth = (data, accessor, headerText) => { const getReportColWidth = (data, accessor, headerText) => {
@@ -86,6 +98,16 @@ const accountCodeColumnAccessor = (column) => {
}; };
}; };
/**
* Description column accessor (muted text in wrapped cell).
*/
const descriptionColumnAccessor = (column) => {
return {
...column,
Cell: DescriptionCell,
};
};
/** /**
* Dynamic column mapper. * Dynamic column mapper.
* @param {} data - * @param {} data -
@@ -105,6 +127,7 @@ const dynamicColumnMapper = R.curry((data, column) => {
R.pathEq(['key'], 'transaction_number'), R.pathEq(['key'], 'transaction_number'),
transactionNumberColumnAccessor, transactionNumberColumnAccessor,
), ),
R.when(R.pathEq(['key'], 'description'), descriptionColumnAccessor),
R.when(R.pathEq(['key'], 'account_code'), accountCodeColumnAccessor), R.when(R.pathEq(['key'], 'account_code'), accountCodeColumnAccessor),
R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor), R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor), R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor),
@@ -113,7 +136,7 @@ const dynamicColumnMapper = R.curry((data, column) => {
}); });
/** /**
* Composes the fetched dynamic columns from the server to the columns to pass it * Composes the fetched dynamic columns from the server to the columns to pass it
* to the table component. * to the table component.
*/ */
export const dynamicColumns = (columns, data) => { export const dynamicColumns = (columns, data) => {

View File

@@ -14,6 +14,18 @@ import { useSettingsSelector } from '@/hooks/state';
import { transformItemFormData } from './ItemForm.schema'; import { transformItemFormData } from './ItemForm.schema';
import { useWatch } from '@/hooks/utils'; import { useWatch } from '@/hooks/utils';
/**
* Error types for item operations.
*/
export const ItemErrorType = {
ItemNameExists: 'ITEM_NAME_EXISTS',
InventoryAccountCannotModified: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
TypeCannotChangeWithItemHasTransactions: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
ItemHasAssociatedTransactions: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
ItemHasAssociatedInventoryAdjustment: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
ItemHasAssociatedTransactionsPlural: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
} as const;
const defaultInitialValues = { const defaultInitialValues = {
active: 1, active: 1,
name: '', name: '',
@@ -74,7 +86,7 @@ export const transitionItemTypeKeyToLabel = (itemTypeKey) => {
// handle delete errors. // handle delete errors.
export const handleDeleteErrors = (errors) => { export const handleDeleteErrors = (errors) => {
if ( if (
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactions)
) { ) {
AppToaster.show({ AppToaster.show({
message: intl.get('the_item_has_associated_transactions'), message: intl.get('the_item_has_associated_transactions'),
@@ -84,7 +96,7 @@ export const handleDeleteErrors = (errors) => {
if ( if (
errors.find( errors.find(
(error) => error.type === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', (error) => error.type === ItemErrorType.ItemHasAssociatedInventoryAdjustment,
) )
) { ) {
AppToaster.show({ AppToaster.show({
@@ -96,7 +108,7 @@ export const handleDeleteErrors = (errors) => {
} }
if ( if (
errors.find( errors.find(
(error) => error.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', (error) => error.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions,
) )
) { ) {
AppToaster.show({ AppToaster.show({
@@ -107,7 +119,7 @@ export const handleDeleteErrors = (errors) => {
}); });
} }
if ( if (
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS') errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactionsPlural)
) { ) {
AppToaster.show({ AppToaster.show({
message: intl.get('item.error.you_could_not_delete_item_has_associated'), message: intl.get('item.error.you_could_not_delete_item_has_associated'),
@@ -214,10 +226,10 @@ export const transformSubmitRequestErrors = (error) => {
} = error; } = error;
const fields = {}; const fields = {};
if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) { if (errors.find((e) => e.type === ItemErrorType.ItemNameExists)) {
fields.name = intl.get('the_name_used_before'); fields.name = intl.get('the_name_used_before');
} }
if (errors.find((e) => e.type === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED')) { if (errors.find((e) => e.type === ItemErrorType.InventoryAccountCannotModified)) {
AppToaster.show({ AppToaster.show({
message: intl.get('cannot_change_item_inventory_account'), message: intl.get('cannot_change_item_inventory_account'),
intent: Intent.DANGER, intent: Intent.DANGER,
@@ -225,7 +237,7 @@ export const transformSubmitRequestErrors = (error) => {
} }
if ( if (
errors.find( errors.find(
(e) => e.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', (e) => e.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions,
) )
) { ) {
AppToaster.show({ AppToaster.show({

View File

@@ -87,7 +87,7 @@ export function WarehousesGridItemBox({
<WarehouseBoxRoot> <WarehouseBoxRoot>
<WarehouseHeader> <WarehouseHeader>
<WarehouseTitle> <WarehouseTitle>
{title} {primary && <Icon icon={'star-18dp'} iconSize={16} />} {title} {primary ? <Icon icon={'star-18dp'} iconSize={16} /> : null}
</WarehouseTitle> </WarehouseTitle>
<WarehouseCode>{code}</WarehouseCode> <WarehouseCode>{code}</WarehouseCode>
<WarehouseIcon> <WarehouseIcon>
@@ -118,12 +118,21 @@ export const WarehousesList = styled.div`
`; `;
export const WarehouseBoxRoot = styled.div` export const WarehouseBoxRoot = styled.div`
--x-box-border-color: #c8cad0;
--x-box-background-color: #fff;
--x-box-hover-border-color: #0153cc;
.bp4-dark & {
--x-box-border-color: rgba(255, 255, 255, 0.2);
--x-box-background-color: var(--color-dark-gray3);
--x-box-hover-border-color: #0153cc;
}
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0; flex-shrink: 0;
border-radius: 5px; border-radius: 5px;
border: 1px solid #c8cad0; border: 1px solid var(--x-box-border-color);
background: #fff; background: var(--x-box-background-color);
margin: 5px 5px 8px; margin: 5px 5px 8px;
width: 200px; width: 200px;
height: 160px; height: 160px;
@@ -132,7 +141,7 @@ export const WarehouseBoxRoot = styled.div`
position: relative; position: relative;
&:hover { &:hover {
border-color: #0153cc; border-color: var(--x-box-hover-border-color);
} }
`; `;
@@ -143,9 +152,16 @@ export const WarehouseHeader = styled.div`
`; `;
export const WarehouseTitle = styled.div` export const WarehouseTitle = styled.div`
--x-title-color: #000;
--x-title-icon-color: #e1b31d;
.bp4-dark & {
--x-title-color: var(--color-light-gray5);
--x-title-icon-color: #e1b31d;
}
font-size: 14px; font-size: 14px;
font-style: inherit; font-style: inherit;
color: #000; color: var(--x-title-color);
white-space: nowrap; white-space: nowrap;
font-weight: 500; font-weight: 500;
line-height: 1; line-height: 1;
@@ -154,14 +170,19 @@ export const WarehouseTitle = styled.div`
margin: 0; margin: 0;
margin-left: 2px; margin-left: 2px;
vertical-align: top; vertical-align: top;
color: #e1b31d; color: var(--x-title-icon-color);
} }
`; `;
const WarehouseCode = styled.div` const WarehouseCode = styled.div`
--x-code-color: #6b7176;
.bp4-dark & {
--x-code-color: var(--color-muted-text);
}
display: block; display: block;
font-size: 11px; font-size: 11px;
color: #6b7176; color: var(--x-code-color);
margin-top: 4px; margin-top: 4px;
`; `;
@@ -178,8 +199,13 @@ const WarehouseContent = styled.div`
`; `;
const WarehouseItem = styled.div` const WarehouseItem = styled.div`
--x-item-color: #000;
.bp4-dark & {
--x-item-color: var(--color-light-gray1);
}
font-size: 11px; font-size: 11px;
color: #000; color: var(--x-item-color);
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;

View File

@@ -1,10 +1,10 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem, Intent } from '@blueprintjs/core';
import { formattedAmount } from '@/utils'; 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 { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, BillAction } from '@/constants/abilityOption'; import { AbilitySubject, BillAction } from '@/constants/abilityOption';
@@ -41,35 +41,35 @@ export function BillStatus({ bill }) {
return ( return (
<Choose> <Choose>
<Choose.When condition={bill.is_fully_paid && bill.is_open}> <Choose.When condition={bill.is_fully_paid && bill.is_open}>
<span class="fully-paid-text"> <TextStatus intent={Intent.SUCCESS}>
<T id={'paid'} /> <T id={'paid'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When condition={bill.is_open}> <Choose.When condition={bill.is_open}>
<Choose> <Choose>
<Choose.When condition={bill.is_overdue}> <Choose.When condition={bill.is_overdue}>
<span className={'overdue-status'}> <TextStatus intent={Intent.DANGER}>
{intl.get('overdue_by', { overdue: bill.overdue_days })} {intl.get('overdue_by', { overdue: bill.overdue_days })}
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span className={'due-status'}> <TextStatus intent={Intent.WARNING}>
{intl.get('due_in', { due: bill.remaining_days })} {intl.get('due_in', { due: bill.remaining_days })}
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
<If condition={bill.is_partially_paid}> <If condition={bill.is_partially_paid}>
<span className="partial-paid"> <TextStatus intent={Intent.WARNING}>
{intl.get('day_partially_paid', { {intl.get('day_partially_paid', {
due: formattedAmount(bill.due_amount, bill.currency_code), due: formattedAmount(bill.due_amount, bill.currency_code),
})} })}
</span> </TextStatus>
</If> </If>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="draft"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );

View File

@@ -1,9 +1,9 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; 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 { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption'; import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption';
@@ -37,28 +37,28 @@ export const EstimateUniversalSearchSelect = withDrawerActions(
export const EstimateStatus = ({ estimate }) => ( export const EstimateStatus = ({ estimate }) => (
<Choose> <Choose>
<Choose.When condition={estimate.is_delivered && estimate.is_approved}> <Choose.When condition={estimate.is_delivered && estimate.is_approved}>
<span class="approved"> <TextStatus intent={Intent.SUCCESS}>
<T id={'approved'} /> <T id={'approved'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When condition={estimate.is_delivered && estimate.is_rejected}> <Choose.When condition={estimate.is_delivered && estimate.is_rejected}>
<span class="reject"> <TextStatus intent={Intent.DANGER}>
<T id={'rejected'} /> <T id={'rejected'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When <Choose.When
condition={ condition={
estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved
} }
> >
<span class="delivered"> <TextStatus intent={Intent.SUCCESS}>
<T id={'delivered'} /> <T id={'delivered'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="draft"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );

View File

@@ -1,9 +1,9 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; 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 { highlightText } from '@/utils';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
@@ -39,29 +39,29 @@ function InvoiceStatus({ customer }) {
return ( return (
<Choose> <Choose>
<Choose.When condition={customer.is_fully_paid && customer.is_delivered}> <Choose.When condition={customer.is_fully_paid && customer.is_delivered}>
<span class="status status-success"> <TextStatus intent={Intent.SUCCESS}>
<T id={'paid'} /> <T id={'paid'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When condition={customer.is_delivered}> <Choose.When condition={customer.is_delivered}>
<Choose> <Choose>
<Choose.When condition={customer.is_overdue}> <Choose.When condition={customer.is_overdue}>
<span className={'status status-warning'}> <TextStatus intent={Intent.DANGER}>
{intl.get('overdue_by', { overdue: customer.overdue_days })} {intl.get('overdue_by', { overdue: customer.overdue_days })}
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span className={'status status-warning'}> <TextStatus intent={Intent.WARNING}>
{intl.get('due_in', { due: customer.remaining_days })} {intl.get('due_in', { due: customer.remaining_days })}
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="status status--gray"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );
@@ -94,7 +94,6 @@ export function InvoiceUniversalSearchItem(
</> </>
} }
onClick={handleClick} onClick={handleClick}
className={'universal-search__item--invoice'}
/> />
); );
} }

View File

@@ -1,9 +1,8 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem, Intent } from '@blueprintjs/core';
import { Icon, Choose, T, TextStatus } from '@/components';
import { Icon, Choose, T } from '@/components';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption'; import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
@@ -39,15 +38,15 @@ function ReceiptStatus({ receipt }) {
return ( return (
<Choose> <Choose>
<Choose.When condition={receipt.is_closed}> <Choose.When condition={receipt.is_closed}>
<span class="closed"> <TextStatus intent={Intent.SUCCESS}>
<T id={'closed'} /> <T id={'closed'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="draft"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );

View File

@@ -1,6 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import keyboardShortcuts from '@/constants/keyboardShortcutsOptions'; import { KeyboardShortcutsOptions } from '@/constants/keyboardShortcutsOptions';
import { useAbilitiesFilter } from '../utils/useAbilityContext'; import { useAbilitiesFilter } from '../utils/useAbilityContext';
/** /**
@@ -10,7 +10,7 @@ export const useKeywordShortcuts = () => {
const abilitiesFilter = useAbilitiesFilter(); const abilitiesFilter = useAbilitiesFilter();
return React.useMemo( return React.useMemo(
() => abilitiesFilter(keyboardShortcuts), () => abilitiesFilter(KeyboardShortcutsOptions),
[abilitiesFilter], [abilitiesFilter],
); );
}; };

View File

@@ -32,19 +32,19 @@ export function useResourceData(type, query, props) {
*/ */
function getResourceUrlFromType(type) { function getResourceUrlFromType(type) {
const config = { const config = {
[RESOURCES_TYPES.INVOICE]: '/sales/invoices', [RESOURCES_TYPES.INVOICE]: '/sale-invoices',
[RESOURCES_TYPES.ESTIMATE]: '/sales/estimates', [RESOURCES_TYPES.ESTIMATE]: '/sale-estimates',
[RESOURCES_TYPES.ITEM]: '/items', [RESOURCES_TYPES.ITEM]: '/items',
[RESOURCES_TYPES.RECEIPT]: '/sales/receipts', [RESOURCES_TYPES.RECEIPT]: '/sale-receipts',
[RESOURCES_TYPES.BILL]: '/purchases/bills', [RESOURCES_TYPES.BILL]: '/bills',
[RESOURCES_TYPES.PAYMENT_RECEIVE]: '/sales/payment_receives', [RESOURCES_TYPES.PAYMENT_RECEIVE]: '/payments-received',
[RESOURCES_TYPES.PAYMENT_MADE]: '/purchases/bill_payments', [RESOURCES_TYPES.PAYMENT_MADE]: '/bill-payments',
[RESOURCES_TYPES.CUSTOMER]: '/customers', [RESOURCES_TYPES.CUSTOMER]: '/customers',
[RESOURCES_TYPES.VENDOR]: '/vendors', [RESOURCES_TYPES.VENDOR]: '/vendors',
[RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals', [RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals',
[RESOURCES_TYPES.ACCOUNT]: '/accounts', [RESOURCES_TYPES.ACCOUNT]: '/accounts',
[RESOURCES_TYPES.CREDIT_NOTE]: '/sales/credit_notes', [RESOURCES_TYPES.CREDIT_NOTE]: '/credit-notes',
[RESOURCES_TYPES.VENDOR_CREDIT]: '/purchases/vendor-credit', [RESOURCES_TYPES.VENDOR_CREDIT]: '/vendor-credits',
}; };
return config[type] || ''; return config[type] || '';
} }

View File

@@ -3,6 +3,7 @@ import { useQuery } from 'react-query';
import { castArray, defaultTo } from 'lodash'; import { castArray, defaultTo } from 'lodash';
import { useAuthOrganizationId } from './state'; import { useAuthOrganizationId } from './state';
import useApiRequest from './useRequest'; import useApiRequest from './useRequest';
import { normalizeApiPath } from '../utils';
import { useRef } from 'react'; import { useRef } from 'react';
/** /**
@@ -19,7 +20,11 @@ export function useRequestQuery(query, axios, props) {
const states = useQuery( const states = useQuery(
query, query,
() => apiRequest.http({ ...axios, url: `/api/${axios.url}` }), () =>
apiRequest.http({
...axios,
url: `/api/${normalizeApiPath(axios.url)}`,
}),
props, props,
); );
// Momerize the default data. // Momerize the default data.

View File

@@ -7,7 +7,7 @@ import {
useSetGlobalErrors, useSetGlobalErrors,
useAuthToken, useAuthToken,
} from './state'; } from './state';
import { getCookie } from '../utils'; import { getCookie, normalizeApiPath } from '../utils';
export default function useApiRequest() { export default function useApiRequest() {
const setGlobalErrors = useSetGlobalErrors(); const setGlobalErrors = useSetGlobalErrors();
@@ -93,27 +93,27 @@ export default function useApiRequest() {
http, http,
get(resource, params) { get(resource, params) {
return http.get(`/api/${resource}`, params); return http.get(`/api/${normalizeApiPath(resource)}`, params);
}, },
post(resource, params, config) { post(resource, params, config) {
return http.post(`/api/${resource}`, params, config); return http.post(`/api/${normalizeApiPath(resource)}`, params, config);
}, },
update(resource, slug, params) { update(resource, slug, params) {
return http.put(`/api/${resource}/${slug}`, params); return http.put(`/api/${normalizeApiPath(resource)}/${slug}`, params);
}, },
put(resource, params) { put(resource, params) {
return http.put(`/api/${resource}`, params); return http.put(`/api/${normalizeApiPath(resource)}`, params);
}, },
patch(resource, params, config) { patch(resource, params, config) {
return http.patch(`/api/${resource}`, params, config); return http.patch(`/api/${normalizeApiPath(resource)}`, params, config);
}, },
delete(resource, params) { delete(resource, params) {
return http.delete(`/api/${resource}`, params); return http.delete(`/api/${normalizeApiPath(resource)}`, params);
}, },
}), }),
[http], [http],
@@ -130,22 +130,22 @@ export function useAuthApiRequest() {
() => ({ () => ({
http, http,
get(resource, params) { get(resource, params) {
return http.get(`/api/${resource}`, params); return http.get(`/api/${normalizeApiPath(resource)}`, params);
}, },
post(resource, params, config) { post(resource, params, config) {
return http.post(`/api/${resource}`, params, config); return http.post(`/api/${normalizeApiPath(resource)}`, params, config);
}, },
update(resource, slug, params) { update(resource, slug, params) {
return http.put(`/api/${resource}/${slug}`, params); return http.put(`/api/${normalizeApiPath(resource)}/${slug}`, params);
}, },
put(resource, params) { put(resource, params) {
return http.put(`/api/${resource}`, params); return http.put(`/api/${normalizeApiPath(resource)}`, params);
}, },
patch(resource, params, config) { patch(resource, params, config) {
return http.patch(`/api/${resource}`, params, config); return http.patch(`/api/${normalizeApiPath(resource)}`, params, config);
}, },
delete(resource, params) { delete(resource, params) {
return http.delete(`/api/${resource}`, params); return http.delete(`/api/${normalizeApiPath(resource)}`, params);
}, },
}), }),
[http], [http],

View File

@@ -11,7 +11,10 @@ export const getPreferenceRoutes = () => [
}, },
{ {
path: `${BASE_URL}/branding`, path: `${BASE_URL}/branding`,
component: lazy(() => import('../containers/Preferences/Branding/PreferencesBrandingPage')), component: lazy(
() =>
import('../containers/Preferences/Branding/PreferencesBrandingPage'),
),
exact: true, exact: true,
}, },
{ {
@@ -29,14 +32,20 @@ export const getPreferenceRoutes = () => [
{ {
path: `${BASE_URL}/payment-methods`, path: `${BASE_URL}/payment-methods`,
component: lazy( component: lazy(
() => import('../containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage'), () =>
import(
'../containers/Preferences/PaymentMethods/PreferencesPaymentMethodsPage'
),
), ),
exact: true, exact: true,
}, },
{ {
path: `${BASE_URL}/payment-methods/stripe/callback`, path: `${BASE_URL}/payment-methods/stripe/callback`,
component: lazy( component: lazy(
() => import('../containers/Preferences/PaymentMethods/PreferencesStripeCallback'), () =>
import(
'../containers/Preferences/PaymentMethods/PreferencesStripeCallback'
),
), ),
exact: true, exact: true,
}, },
@@ -112,16 +121,6 @@ export const getPreferenceRoutes = () => [
component: lazy(() => import('@/containers/Preferences/Item')), component: lazy(() => import('@/containers/Preferences/Item')),
exact: true, exact: true,
}, },
// {
// path: `${BASE_URL}/sms-message`,
// component: SMSIntegration,
// exact: true,
// },
{
path: `${BASE_URL}/billing`,
component: lazy(() => import('@/containers/Subscriptions/BillingPage')),
exact: true,
},
{ {
path: `${BASE_URL}/api-keys`, path: `${BASE_URL}/api-keys`,
component: lazy(() => import('@/containers/Preferences/ApiKeys/ApiKeys')), component: lazy(() => import('@/containers/Preferences/ApiKeys/ApiKeys')),

View File

@@ -31,7 +31,6 @@
@import 'components/Overlay'; @import 'components/Overlay';
@import 'components/Menu'; @import 'components/Menu';
@import 'components/SidebarOverlay'; @import 'components/SidebarOverlay';
@import 'components/UniversalSearch';
// Pages // Pages
@import 'pages/view-form'; @import 'pages/view-form';

View File

@@ -7,6 +7,27 @@ $ns: bp4;
--color-primary: #8abbff; --color-primary: #8abbff;
--color-danger: red; --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-gray5: #404854;
--color-dark-gray4: #383e47; --color-dark-gray4: #383e47;
--color-dark-gray3: #2f343c; --color-dark-gray3: #2f343c;
@@ -196,7 +217,6 @@ $ns: bp4;
--color-preferences-sidebar-head-border: #bbcbd0; --color-preferences-sidebar-head-border: #bbcbd0;
--color-preferences-sidebar-head-text: #3b3b4c; --color-preferences-sidebar-head-text: #3b3b4c;
// Preferences - Topbar. // Preferences - Topbar.
--color-preferences-topbar-background: #fff; --color-preferences-topbar-background: #fff;
--color-preferences-topbar-border: #d2dde2; --color-preferences-topbar-border: #d2dde2;
@@ -209,7 +229,7 @@ $ns: bp4;
--color-financial-sheet-title-text: rgb(31, 50, 85); --color-financial-sheet-title-text: rgb(31, 50, 85);
--color-financial-sheet-type-text: rgb(31, 50, 85); --color-financial-sheet-type-text: rgb(31, 50, 85);
--color-financial-sheet-date-text: rgb(31, 50, 85); --color-financial-sheet-date-text: rgb(31, 50, 85);
--color-financial-sheet-footer-text: rgb(31, 50, 85); --color-financial-sheet-footer-text: var(--color-muted-text);
--color-financial-sheet-minimal-title-text: #333; --color-financial-sheet-minimal-title-text: #333;
// Transaction locking. // Transaction locking.
@@ -302,6 +322,27 @@ body.bp4-dark {
--color-primary: #8abbff; --color-primary: #8abbff;
--color-danger: rgb(213, 103, 103); --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-gray5: #404854;
--color-dark-gray4: #383e47; --color-dark-gray4: #383e47;
--color-dark-gray3: #2f343c; --color-dark-gray3: #2f343c;
@@ -514,7 +555,7 @@ body.bp4-dark {
--color-financial-sheet-title-text: var(--color-light-gray1); --color-financial-sheet-title-text: var(--color-light-gray1);
--color-financial-sheet-type-text: var(--color-light-gray1); --color-financial-sheet-type-text: var(--color-light-gray1);
--color-financial-sheet-date-text: var(--color-light-gray1); --color-financial-sheet-date-text: var(--color-light-gray1);
--color-financial-sheet-footer-text: var(--color-light-gray1); --color-financial-sheet-footer-text: var(--color-muted-text);
--color-financial-sheet-minimal-title-text: var(--color-white); --color-financial-sheet-minimal-title-text: var(--color-white);
// Transaction locking. // Transaction locking.

View File

@@ -365,7 +365,7 @@
border-bottom: 1px solid var(--color-datatable-constrant-head-border); border-bottom: 1px solid var(--color-datatable-constrant-head-border);
padding: 0.5rem; padding: 0.5rem;
} }
.tbody .tr .td { .tbody .tr .td {
background: transparent; background: transparent;
padding: 0.5rem 0.5rem; padding: 0.5rem 0.5rem;
@@ -375,3 +375,32 @@
} }
} }
} }
// Sticky header: blurred transparent background so body rows don't show through
.bigcapital-datatable.has-sticky,
.bigcapital-datatable.has-sticky-header {
&.table-constrant .table .thead .th,
&.table--constrant .table .thead .th {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
body.bp4-dark & {
background: rgba(28, 33, 39, 0.6);
}
}
}
// Sticky cells in table body: blurred transparent background so content doesn't show through
.bigcapital-datatable.has-sticky {
&.table-constrant .table .tbody .tr:not(:hover) .td[data-sticky-td],
&.table--constrant .table .tbody .tr:not(:hover) .td[data-sticky-td] {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
body.bp4-dark & {
background: rgba(28, 33, 39, 0.6);
}
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -13,6 +13,9 @@ import jsCookie from 'js-cookie';
import { deepMapKeys } from './map-key-deep'; import { deepMapKeys } from './map-key-deep';
export * from './deep'; export * from './deep';
/** Strips leading slash from a path segment to avoid double slashes when joining with a base (e.g. `/api/` + path). */
export const normalizeApiPath = (path) => (path || '').replace(/^\//, '');
export const getCookie = (name, defaultValue) => export const getCookie = (name, defaultValue) =>
_.defaultTo(jsCookie.get(name), defaultValue); _.defaultTo(jsCookie.get(name), defaultValue);