diff --git a/packages/server/src/database/tenant/migrations/20250418000001_create_tracking_tags_tables.ts b/packages/server/src/database/tenant/migrations/20250418000001_create_tracking_tags_tables.ts new file mode 100644 index 000000000..4a7fe2129 --- /dev/null +++ b/packages/server/src/database/tenant/migrations/20250418000001_create_tracking_tags_tables.ts @@ -0,0 +1,67 @@ +exports.up = function(knex) { + return knex.schema + .createTable('tracking_tags', table => { + table.increments(); + table.string('name').notNullable(); + table.string('description').nullable(); + table.boolean('active').defaultTo(true); + table.timestamps(); + table.index(['active']); + table.unique(['name']); + }) + .createTable('tracking_tag_options', table => { + table.increments(); + table.integer('tag_id').unsigned().notNullable(); + table.string('name').notNullable(); + table.boolean('active').defaultTo(true); + table.timestamps(); + table.foreign('tag_id').references('tracking_tags.id').onDelete('CASCADE'); + table.unique(['tag_id', 'name']); + }) + .createTable('item_entry_tracking_tags', table => { + table.integer('item_entry_id').unsigned().notNullable(); + table.integer('tag_id').unsigned().notNullable(); + table.integer('option_id').unsigned().notNullable(); + table.timestamps(); + table.primary(['item_entry_id', 'tag_id']); + table.foreign('item_entry_id').references('items_entries.id').onDelete('CASCADE'); + table.foreign('tag_id').references('tracking_tags.id').onDelete('CASCADE'); + table.foreign('option_id').references('tracking_tag_options.id').onDelete('CASCADE'); + table.index(['tag_id']); + table.index(['option_id']); + }) + .createTable('manual_journal_entry_tracking_tags', table => { + table.integer('manual_journal_entry_id').unsigned().notNullable(); + table.integer('tag_id').unsigned().notNullable(); + table.integer('option_id').unsigned().notNullable(); + table.timestamps(); + table.primary(['manual_journal_entry_id', 'tag_id']); + table.foreign('manual_journal_entry_id').references('manual_journals_entries.id').onDelete('CASCADE'); + table.foreign('tag_id').references('tracking_tags.id').onDelete('CASCADE'); + table.foreign('option_id').references('tracking_tag_options.id').onDelete('CASCADE'); + table.index(['tag_id']); + table.index(['option_id']); + }) + .createTable('account_transaction_tracking_tags', table => { + table.integer('account_transaction_id').unsigned().notNullable(); + table.integer('tag_id').unsigned().notNullable(); + table.integer('option_id').unsigned().notNullable(); + table.timestamps(); + table.primary(['account_transaction_id', 'tag_id']); + table.foreign('account_transaction_id').references('accounts_transactions.id').onDelete('CASCADE'); + table.foreign('tag_id').references('tracking_tags.id').onDelete('CASCADE'); + table.foreign('option_id').references('tracking_tag_options.id').onDelete('CASCADE'); + table.index(['tag_id']); + table.index(['option_id']); + table.index(['account_transaction_id']); + }); +}; + +exports.down = function(knex) { + return knex.schema + .dropTableIfExists('account_transaction_tracking_tags') + .dropTableIfExists('manual_journal_entry_tracking_tags') + .dropTableIfExists('item_entry_tracking_tags') + .dropTableIfExists('tracking_tag_options') + .dropTableIfExists('tracking_tags'); +}; diff --git a/packages/server/src/modules/Accounts/models/AccountTransaction.model.ts b/packages/server/src/modules/Accounts/models/AccountTransaction.model.ts index 408902e70..92f3e7703 100644 --- a/packages/server/src/modules/Accounts/models/AccountTransaction.model.ts +++ b/packages/server/src/modules/Accounts/models/AccountTransaction.model.ts @@ -230,6 +230,23 @@ export class AccountTransaction extends BaseModel { query.where('reference_id', referenceId); query.where('reference_type', referenceType); }, + + filterByTrackingTags(query, trackingTags: Array<{ tagId: number; optionId?: number }>) { + if (isEmpty(trackingTags)) { + return; + } + const tagIds = trackingTags.map((t) => t.tagId); + query.whereExists( + query + .knex() + .select(1) + .from('account_transaction_tracking_tags') + .whereRaw( + 'account_transaction_tracking_tags.account_transaction_id = accounts_transactions.id', + ) + .whereIn('account_transaction_tracking_tags.tag_id', tagIds), + ); + }, }; } diff --git a/packages/server/src/modules/App/App.module.ts b/packages/server/src/modules/App/App.module.ts index f53a942e6..0ba23f5f4 100644 --- a/packages/server/src/modules/App/App.module.ts +++ b/packages/server/src/modules/App/App.module.ts @@ -99,6 +99,7 @@ import { UsersModule } from '../UsersModule/Users.module'; import { ContactsModule } from '../Contacts/Contacts.module'; import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module'; import { CustomFieldsModule } from '../CustomFields/CustomFields.module'; +import { TrackingTagsModule } from '../TrackingTags/TrackingTags.module'; import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module'; import { ExchangeRatesModule } from '../ExchangeRates/ExchangeRates.module'; import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module'; @@ -258,6 +259,7 @@ import { AppThrottleModule } from './AppThrottle.module'; UsersModule, ContactsModule, CustomFieldsModule, + TrackingTagsModule, SocketModule, ExchangeRatesModule, ], diff --git a/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.dto.ts b/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.dto.ts index b63d65768..d5a024058 100644 --- a/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.dto.ts @@ -11,6 +11,7 @@ import { } from 'class-validator'; import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto'; import { ApiProperty } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto { @ApiProperty({ @@ -173,4 +174,12 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto { @Transform(({ value }) => parseBoolean(value, false)) @IsOptional() previousYearPercentageChange: boolean; + + @ApiProperty({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + required: false, + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.types.ts b/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.types.ts index f0a1f094c..a3c5febb5 100644 --- a/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.types.ts +++ b/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.types.ts @@ -5,6 +5,7 @@ import { INumberFormatQuery, } from '../../types/Report.types'; import { IFinancialTable } from '../../types/Table.types'; +import { ITrackingTagFilter } from '../../types/TrackingTagFilter.types'; // Balance sheet schema nodes types. export enum BALANCE_SHEET_SCHEMA_NODE_TYPE { @@ -63,6 +64,8 @@ export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery { previousYear: boolean; previousYearAmountChange: boolean; previousYearPercentageChange: boolean; + + trackingTags?: ITrackingTagFilter[]; } // Balance sheet meta. diff --git a/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepository.ts b/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepository.ts index a7a9b2a8b..f40191ea0 100644 --- a/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepository.ts @@ -401,5 +401,8 @@ export class BalanceSheetRepository extends R.compose( if (!isEmpty(this.query.branchesIds)) { query.modify('filterByBranches', this.query.branchesIds); } + if (!isEmpty(this.query.trackingTags)) { + query.modify('filterByTrackingTags', this.query.trackingTags); + } }; } diff --git a/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowRepository.ts b/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowRepository.ts index bb99b7651..30d576b2c 100644 --- a/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowRepository.ts @@ -173,5 +173,8 @@ export class CashFlowRepository { if (!isEmpty(query.branchesIds)) { knexQuery.modify('filterByBranches', query.branchesIds); } + if (!isEmpty(query.trackingTags)) { + knexQuery.modify('filterByTrackingTags', query.trackingTags); + } }; } diff --git a/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowStatementQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowStatementQuery.dto.ts index 51d4af8b4..394911c98 100644 --- a/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowStatementQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/CashFlowStatementQuery.dto.ts @@ -11,6 +11,7 @@ import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberF import { Transform, Type } from 'class-transformer'; import { parseBoolean } from '@/utils/parse-boolean'; import { ApiProperty } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto { @ApiProperty({ @@ -92,4 +93,12 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto { @IsString() @IsOptional() basis: string; + + @ApiProperty({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + required: false, + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/Cashflow.types.ts b/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/Cashflow.types.ts index 6d21ca118..ab9a904b3 100644 --- a/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/Cashflow.types.ts +++ b/packages/server/src/modules/FinancialStatements/modules/CashFlowStatement/Cashflow.types.ts @@ -16,6 +16,7 @@ export interface ICashFlowStatementQuery { basis: string; branchesIds?: number[]; + trackingTags?: Array<{ tagId: number; optionId?: number }>; } export interface ICashFlowStatementTotal { diff --git a/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryQuery.dto.ts index e8358cedd..7f2d6d2a2 100644 --- a/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryQuery.dto.ts @@ -1,6 +1,7 @@ import { IsArray, IsOptional } from 'class-validator'; import { ContactBalanceSummaryQueryDto } from '../ContactBalanceSummary/ContactBalanceSummaryQuery.dto'; import { ApiPropertyOptional } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class CustomerBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto { @ApiPropertyOptional({ @@ -11,4 +12,11 @@ export class CustomerBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDt @IsArray() @IsOptional() customersIds: number[]; + + @ApiPropertyOptional({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryRepository.ts b/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryRepository.ts index 913ea657b..94accbe23 100644 --- a/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/CustomerBalanceSummary/CustomerBalanceSummaryRepository.ts @@ -56,6 +56,7 @@ export class CustomerBalanceSummaryRepository { */ public async getCustomersTransactions( asDate: any, + trackingTags?: Array<{ tagId: number; optionId?: number }>, ): Promise[]> { // Retrieve the receivable accounts A/R. const receivableAccounts = await this.getReceivableAccounts(); @@ -67,6 +68,9 @@ export class CustomerBalanceSummaryRepository { .onBuild((query) => { query.whereIn('accountId', receivableAccountsIds); query.modify('filterDateRange', null, asDate); + if (!isEmpty(trackingTags)) { + query.modify('filterByTrackingTags', trackingTags); + } query.groupBy('contactId'); query.sum('credit as credit'); query.sum('debit as debit'); diff --git a/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedger.types.ts b/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedger.types.ts index 05881cdf2..ea2b3efb0 100644 --- a/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedger.types.ts +++ b/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedger.types.ts @@ -1,5 +1,6 @@ import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types"; import { IFinancialTable } from "../../types/Table.types"; +import { ITrackingTagFilter } from "../../types/TrackingTagFilter.types"; export interface IGeneralLedgerSheetQuery { fromDate: Date | string; @@ -10,6 +11,19 @@ export interface IGeneralLedgerSheetQuery { noneTransactions: boolean; accountsIds: number[]; branchesIds?: number[]; + trackingTags?: ITrackingTagFilter[]; +} + +export interface IGeneralLedgerSheetQuery { + fromDate: Date | string; + toDate: Date | string; + basis: string; + numberFormat: IGeneralLedgerNumberFormat; + dateFormat?: string; + noneTransactions: boolean; + accountsIds: number[]; + branchesIds?: number[]; + trackingTags?: ITrackingTagFilter[]; } export interface IGeneralLedgerNumberFormat extends INumberFormatQuery{ diff --git a/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedgerRepository.ts b/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedgerRepository.ts index fc51284c2..aed86162d 100644 --- a/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedgerRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/GeneralLedger/GeneralLedgerRepository.ts @@ -119,6 +119,9 @@ export class GeneralLedgerRepository { if (!isEmpty(this.filter.branchesIds)) { query.modify('filterByBranches', this.filter.branchesIds); } + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } query.orderBy('date', 'ASC'); if (this.filter.accountsIds?.length > 0) { @@ -146,6 +149,9 @@ export class GeneralLedgerRepository { if (!isEmpty(this.filter.branchesIds)) { query.modify('filterByBranches', this.filter.branchesIds); } + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } query.withGraphFetched('account'); }); // Accounts opening transactions. diff --git a/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetQuery.dto.ts index 30e62afd4..43429779b 100644 --- a/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetQuery.dto.ts @@ -10,6 +10,7 @@ import { import { Type } from 'class-transformer'; import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto'; import { ApiPropertyOptional } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; class JournalSheetNumberFormatQueryDto { @ApiPropertyOptional({ @@ -93,4 +94,11 @@ export class JournalSheetQueryDto extends FinancialSheetBranchesQueryDto { @IsNumber() @IsOptional() toRange: number; + + @ApiPropertyOptional({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetRepository.ts b/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetRepository.ts index 3b0d35f51..eda65502c 100644 --- a/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/JournalSheet/JournalSheetRepository.ts @@ -6,6 +6,7 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { transformToMap } from '@/utils/transform-to-key'; import { Inject } from '@nestjs/common'; +import { isEmpty } from 'lodash'; import { ModelObject } from 'objection'; export class JournalSheetRepository { @@ -112,6 +113,9 @@ export class JournalSheetRepository { if (this.filter.transactionType && this.filter.transactionId) { query.where('reference_id', this.filter.transactionId); } + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } query.withGraphFetched('account'); }); this.accountTransactions = transactions; diff --git a/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheet.types.ts b/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheet.types.ts index 9eada0d4b..700bb9d33 100644 --- a/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheet.types.ts +++ b/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheet.types.ts @@ -5,6 +5,7 @@ import { INumberFormatQuery, } from '../../types/Report.types'; import { IFinancialTable } from '../../types/Table.types'; +import { ITrackingTagFilter } from '../../types/TrackingTagFilter.types'; export enum ProfitLossAggregateNodeId { INCOME = 'INCOME', @@ -86,6 +87,8 @@ export interface IProfitLossSheetQuery extends IFinancialSheetBranchesQuery { previousYear: boolean; previousYearAmountChange: boolean; previousYearPercentageChange: boolean; + + trackingTags?: ITrackingTagFilter[]; } export interface IProfitLossSheetTotal { diff --git a/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetQuery.dto.ts index 60039912a..be98f7934 100644 --- a/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetQuery.dto.ts @@ -14,6 +14,7 @@ import { Transform, Type } from 'class-transformer'; import { ToNumber } from '@/common/decorators/Validators'; import { parseBoolean } from '@/utils/parse-boolean'; import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto { @IsString() @@ -136,4 +137,11 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto { description: 'Whether to show previous year percentage change', }) previousYearPercentageChange: boolean; + + @ApiPropertyOptional({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetRepository.ts b/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetRepository.ts index 33c4902f8..ac2089d9d 100644 --- a/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/ProfitLossSheet/ProfitLossSheetRepository.ts @@ -363,6 +363,9 @@ export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)( if (!isEmpty(this.query.query.branchesIds)) { query.modify('filterByBranches', this.query.query.branchesIds); } + if (!isEmpty(this.query.query.trackingTags)) { + query.modify('filterByTrackingTags', this.query.query.trackingTags); + } }; /** diff --git a/packages/server/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.types.ts b/packages/server/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.types.ts index 957bc2828..9ffea6815 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.types.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.types.ts @@ -31,4 +31,5 @@ export interface ITransactionsByContactsFilter { numberFormat: INumberFormatQuery; noneTransactions: boolean; noneZero: boolean; + trackingTags?: Array<{ tagId: number; optionId?: number }>; } diff --git a/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomerQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomerQuery.dto.ts index 17defa904..60ba39dd7 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomerQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomerQuery.dto.ts @@ -1,8 +1,17 @@ import { IsArray, IsOptional } from 'class-validator'; import { TransactionsByContactQueryDto } from '../TransactionsByContact/TransactionsByContactQuery.dto'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class TransactionsByCustomerQueryDto extends TransactionsByContactQueryDto { @IsArray() @IsOptional() customersIds: number[]; + + @ApiPropertyOptional({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts b/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts index dbe50adc4..2103101b1 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts @@ -236,7 +236,12 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo openingDate, receivableAccountsIds, customersIds, - ); + ) + .onBuild((query) => { + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } + }); return openingTransactions; } @@ -264,6 +269,10 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo // Filter by accounts. query.whereIn('accountId', receivableAccountsIds); + + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } }); return transactions; } diff --git a/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceQuery.dto.ts index e18aa9562..160fd4247 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceQuery.dto.ts @@ -1,5 +1,6 @@ -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class TransactionsByReferenceQueryDto { @IsString() @@ -19,4 +20,12 @@ export class TransactionsByReferenceQueryDto { required: true, }) referenceId: number; + + @ApiProperty({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + required: false, + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceRepository.ts b/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceRepository.ts index 82c4b58a1..32065424a 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceRepository.ts @@ -2,6 +2,7 @@ import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { Inject, Injectable } from '@nestjs/common'; import { ModelObject } from 'objection'; +import { isEmpty } from 'lodash'; @Injectable() export class TransactionsByReferenceRepository { @@ -21,12 +22,18 @@ export class TransactionsByReferenceRepository { public async getTransactions( referenceId: number, referenceType: string, + trackingTags?: Array<{ tagId: number; optionId?: number }>, ): Promise>> { return this.accountTransactionModel() .query() .skipUndefined() .where('reference_id', referenceId) .where('reference_type', referenceType) + .onBuild((query) => { + if (!isEmpty(trackingTags)) { + query.modify('filterByTrackingTags', trackingTags); + } + }) .withGraphFetched('account'); } } diff --git a/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorQuery.dto.ts index f8bb8d231..6b125cf24 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorQuery.dto.ts @@ -1,6 +1,7 @@ import { IsArray, IsOptional } from 'class-validator'; import { TransactionsByContactQueryDto } from '../TransactionsByContact/TransactionsByContactQuery.dto'; import { ApiPropertyOptional } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class TransactionsByVendorQueryDto extends TransactionsByContactQueryDto { @IsArray() @@ -10,4 +11,11 @@ export class TransactionsByVendorQueryDto extends TransactionsByContactQueryDto example: [1, 2, 3], }) vendorsIds: number[]; + + @ApiPropertyOptional({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts b/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts index ad45136cb..b034e1bbc 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts @@ -242,7 +242,12 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit openingDate, payableAccountsIds, customersIds, - ); + ) + .onBuild((query) => { + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } + }); return openingTransactions; } @@ -270,6 +275,10 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit // Filter by accounts. query.whereIn('accountId', receivableAccountsIds); + + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } }); return transactions; } diff --git a/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.types.ts b/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.types.ts index 5c663e1d9..f8da574fa 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.types.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.types.ts @@ -1,5 +1,6 @@ import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types"; import { IFinancialTable } from "../../types/Table.types"; +import { ITrackingTagFilter } from "../../types/TrackingTagFilter.types"; export interface ITrialBalanceSheetQuery { fromDate: Date | string; @@ -11,6 +12,7 @@ export interface ITrialBalanceSheetQuery { onlyActive: boolean; accountIds: number[]; branchesIds?: number[]; + trackingTags?: ITrackingTagFilter[]; } export interface ITrialBalanceTotal { diff --git a/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetQuery.dto.ts index e9a280eae..36a821c1b 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetQuery.dto.ts @@ -12,6 +12,7 @@ import { import { Transform, Type } from 'class-transformer'; import { parseBoolean } from '@/utils/parse-boolean'; import { ApiProperty } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class TrialBalanceSheetQueryDto extends FinancialSheetBranchesQueryDto { @ApiProperty({ @@ -92,4 +93,12 @@ export class TrialBalanceSheetQueryDto extends FinancialSheetBranchesQueryDto { @IsArray() @IsOptional() accountIds: number[]; + + @ApiProperty({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + required: false, + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetRepository.ts b/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetRepository.ts index 533e8c8b5..cf4925e82 100644 --- a/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetRepository.ts @@ -111,5 +111,9 @@ export class TrialBalanceSheetRepository { // @ts-ignore query.modify('filterByBranches', this.query.branchesIds); } + if (!isEmpty(this.query.trackingTags)) { + // @ts-ignore + query.modify('filterByTrackingTags', this.query.trackingTags); + } }; } diff --git a/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.types.ts b/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.types.ts index 192da060b..4bce251c6 100644 --- a/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.types.ts +++ b/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummary.types.ts @@ -11,6 +11,7 @@ export interface IVendorBalanceSummaryQuery { percentageColumn: boolean; noneTransactions: boolean; noneZero: boolean; + trackingTags?: Array<{ tagId: number; optionId?: number }>; } export interface IVendorBalanceSummaryAmount { diff --git a/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryQuery.dto.ts b/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryQuery.dto.ts index 177dc6e09..8453e9a1d 100644 --- a/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryQuery.dto.ts +++ b/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryQuery.dto.ts @@ -1,6 +1,7 @@ import { IsArray, IsOptional } from 'class-validator'; import { ContactBalanceSummaryQueryDto } from '../ContactBalanceSummary/ContactBalanceSummaryQuery.dto'; import { ApiPropertyOptional } from '@nestjs/swagger'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class VendorBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto { @IsArray() @@ -11,4 +12,11 @@ export class VendorBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto example: [1, 2, 3], }) vendorsIds: number[]; + + @ApiPropertyOptional({ + description: 'Tracking tags to filter the report', + type: [TrackingTagAssignmentDto], + }) + @IsOptional() + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryRepository.ts b/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryRepository.ts index 1b544cac2..ebf40d42b 100644 --- a/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryRepository.ts +++ b/packages/server/src/modules/FinancialStatements/modules/VendorBalanceSummary/VendorBalanceSummaryRepository.ts @@ -138,6 +138,9 @@ export class VendorBalanceSummaryRepository { .onBuild((query) => { query.whereIn('accountId', payableAccountsIds); query.modify('filterDateRange', null, asDate); + if (!isEmpty(this.filter.trackingTags)) { + query.modify('filterByTrackingTags', this.filter.trackingTags); + } query.groupBy('contactId'); query.sum('credit as credit'); query.sum('debit as debit'); diff --git a/packages/server/src/modules/FinancialStatements/types/TrackingTagFilter.types.ts b/packages/server/src/modules/FinancialStatements/types/TrackingTagFilter.types.ts new file mode 100644 index 000000000..4afd7b0a8 --- /dev/null +++ b/packages/server/src/modules/FinancialStatements/types/TrackingTagFilter.types.ts @@ -0,0 +1,4 @@ +export interface ITrackingTagFilter { + tagId: number; + optionId?: number; +} diff --git a/packages/server/src/modules/Ledger/LedgerEntriesStorage.service.ts b/packages/server/src/modules/Ledger/LedgerEntriesStorage.service.ts index 7cb7ea37a..4a752f7f2 100644 --- a/packages/server/src/modules/Ledger/LedgerEntriesStorage.service.ts +++ b/packages/server/src/modules/Ledger/LedgerEntriesStorage.service.ts @@ -8,6 +8,7 @@ import { } from './types/Ledger.types'; import { ILedger } from './types/Ledger.types'; import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; +import { AccountTransactionTrackingTag } from '../TrackingTags/models/AccountTransactionTrackingTag'; import { TenantModelProxy } from '../System/models/TenantBaseModel'; // Filter the blank entries. @@ -24,6 +25,11 @@ export class LedgerEntriesStorageService { private readonly accountTransactionModel: TenantModelProxy< typeof AccountTransaction >, + + @Inject(AccountTransactionTrackingTag.name) + private readonly accountTransactionTrackingTagModel: TenantModelProxy< + typeof AccountTransactionTrackingTag + >, ) {} /** @@ -71,7 +77,22 @@ export class LedgerEntriesStorageService { ): Promise => { const transaction = transformLedgerEntryToTransaction(entry); - await this.accountTransactionModel().query(trx).insert(transaction); + const insertedTransaction = await this.accountTransactionModel() + .query(trx) + .insert(transaction); + + // Save tracking tag associations if present. + if (entry.trackingTags?.length > 0) { + const tagAssociations = entry.trackingTags.map((tag) => ({ + accountTransactionId: insertedTransaction.id, + tagId: tag.tagId, + optionId: tag.optionId, + })); + + await this.accountTransactionTrackingTagModel() + .query(trx) + .insert(tagAssociations); + } }; /** diff --git a/packages/server/src/modules/Ledger/types/Ledger.types.ts b/packages/server/src/modules/Ledger/types/Ledger.types.ts index 8a400349d..78d7126ff 100644 --- a/packages/server/src/modules/Ledger/types/Ledger.types.ts +++ b/packages/server/src/modules/Ledger/types/Ledger.types.ts @@ -69,6 +69,8 @@ export interface ILedgerEntry { createdAt?: Date | string; costable?: boolean; + + trackingTags?: Array<{ tagId: number; optionId: number }>; } export interface ISaveLedgerEntryQueuePayload { diff --git a/packages/server/src/modules/ManualJournals/commands/CreateManualJournal.service.ts b/packages/server/src/modules/ManualJournals/commands/CreateManualJournal.service.ts index 7141b7a1f..b7efb63e0 100644 --- a/packages/server/src/modules/ManualJournals/commands/CreateManualJournal.service.ts +++ b/packages/server/src/modules/ManualJournals/commands/CreateManualJournal.service.ts @@ -56,6 +56,18 @@ export class CreateManualJournalService { const authorizedUser = await this.tenancyContext.getSystemUser(); const entries = R.compose( + // Map trackingTags to trackingTagAssociations for upsertGraph. + R.map((entry: any) => ({ + ...entry, + ...(entry.trackingTags + ? { + trackingTagAssociations: entry.trackingTags.map((tag) => ({ + tagId: tag.tagId, + optionId: tag.optionId, + })), + } + : {}), + })), // Associate the default index to each item entry. assocItemEntriesDefaultIndex, )(manualJournalDTO.entries); diff --git a/packages/server/src/modules/ManualJournals/commands/EditManualJournal.service.ts b/packages/server/src/modules/ManualJournals/commands/EditManualJournal.service.ts index 11df0fde4..11659013d 100644 --- a/packages/server/src/modules/ManualJournals/commands/EditManualJournal.service.ts +++ b/packages/server/src/modules/ManualJournals/commands/EditManualJournal.service.ts @@ -68,6 +68,18 @@ export class EditManualJournal { const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); + const entries = manualJournalDTO.entries.map((entry: any) => ({ + ...entry, + ...(entry.trackingTags + ? { + trackingTagAssociations: entry.trackingTags.map((tag) => ({ + tagId: tag.tagId, + optionId: tag.optionId, + })), + } + : {}), + })); + return { id: oldManualJournal.id, ...omit(manualJournalDTO, ['publish', 'attachments']), @@ -76,6 +88,7 @@ export class EditManualJournal { : {}), amount, date, + entries, }; }; diff --git a/packages/server/src/modules/ManualJournals/commands/ManualJournalGL.ts b/packages/server/src/modules/ManualJournals/commands/ManualJournalGL.ts index 21ea2e9d1..df0a9b942 100644 --- a/packages/server/src/modules/ManualJournals/commands/ManualJournalGL.ts +++ b/packages/server/src/modules/ManualJournals/commands/ManualJournalGL.ts @@ -52,6 +52,11 @@ export class ManualJournalGL { public getManualJournalEntry(entry: ManualJournalEntry): ILedgerEntry { const commonEntry = this.manualJournalCommonEntry; + const trackingTags = entry.trackingTagAssociations?.map((assoc) => ({ + tagId: assoc.tagId, + optionId: assoc.optionId, + })); + return { ...commonEntry, debit: entry.debit, @@ -66,6 +71,7 @@ export class ManualJournalGL { branchId: entry.branchId, projectId: entry.projectId, + trackingTags, }; } diff --git a/packages/server/src/modules/ManualJournals/dtos/ManualJournal.dto.ts b/packages/server/src/modules/ManualJournals/dtos/ManualJournal.dto.ts index 957ad6510..cd51d7e47 100644 --- a/packages/server/src/modules/ManualJournals/dtos/ManualJournal.dto.ts +++ b/packages/server/src/modules/ManualJournals/dtos/ManualJournal.dto.ts @@ -16,6 +16,7 @@ import { Min, ValidateNested, } from 'class-validator'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class ManualJournalEntryDto { @ApiProperty({ description: 'Entry index' }) @@ -63,6 +64,17 @@ export class ManualJournalEntryDto { @IsOptional() @IsInt() projectId?: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TrackingTagAssignmentDto) + @ApiProperty({ + description: 'The tracking tags of the manual journal entry', + type: [TrackingTagAssignmentDto], + required: false, + }) + trackingTags?: TrackingTagAssignmentDto[]; } class AttachmentDto { diff --git a/packages/server/src/modules/ManualJournals/models/ManualJournalEntry.ts b/packages/server/src/modules/ManualJournals/models/ManualJournalEntry.ts index 5898dd4b4..d17e41a92 100644 --- a/packages/server/src/modules/ManualJournals/models/ManualJournalEntry.ts +++ b/packages/server/src/modules/ManualJournals/models/ManualJournalEntry.ts @@ -18,6 +18,7 @@ export class ManualJournalEntry extends BaseModel { contact?: Contact; account?: Account; branch?: Branch; + trackingTagAssociations?: any[]; /** * Table name. @@ -40,6 +41,7 @@ export class ManualJournalEntry extends BaseModel { const { Account } = require('../../Accounts/models/Account.model'); const { Contact } = require('../../Contacts/models/Contact'); const { Branch } = require('../../Branches/models/Branch.model'); + const { ManualJournalEntryTrackingTag } = require('../../TrackingTags/models/ManualJournalEntryTrackingTag'); return { account: { @@ -66,6 +68,15 @@ export class ManualJournalEntry extends BaseModel { to: 'branches.id', }, }, + + trackingTagAssociations: { + relation: Model.HasManyRelation, + modelClass: ManualJournalEntryTrackingTag, + join: { + from: 'manual_journals_entries.id', + to: 'manual_journal_entry_tracking_tags.manualJournalEntryId', + }, + }, }; } } diff --git a/packages/server/src/modules/Roles/Roles.types.ts b/packages/server/src/modules/Roles/Roles.types.ts index 29984802e..30d4a194e 100644 --- a/packages/server/src/modules/Roles/Roles.types.ts +++ b/packages/server/src/modules/Roles/Roles.types.ts @@ -62,6 +62,7 @@ export enum AbilitySubject { Project = 'Project', TaxRate = 'TaxRate', CustomField = 'CustomField', + TrackingTag = 'TrackingTag', } export interface IRoleCreatedPayload { diff --git a/packages/server/src/modules/SaleInvoices/commands/CommandSaleInvoiceDTOTransformer.service.ts b/packages/server/src/modules/SaleInvoices/commands/CommandSaleInvoiceDTOTransformer.service.ts index 29a777766..610794fd0 100644 --- a/packages/server/src/modules/SaleInvoices/commands/CommandSaleInvoiceDTOTransformer.service.ts +++ b/packages/server/src/modules/SaleInvoices/commands/CommandSaleInvoiceDTOTransformer.service.ts @@ -91,6 +91,19 @@ export class CommandSaleInvoiceDTOTransformer { // Remove tax code from entries. R.map(R.omit(['taxCode'])), + // Map trackingTags to trackingTagAssociations for upsertGraph. + R.map((entry: any) => ({ + ...entry, + ...(entry.trackingTags + ? { + trackingTagAssociations: entry.trackingTags.map((tag) => ({ + tagId: tag.tagId, + optionId: tag.optionId, + })), + } + : {}), + })), + // Associate the default index for each item entry lin. assocItemEntriesDefaultIndex, )(asyncEntries); diff --git a/packages/server/src/modules/SaleInvoices/ledger/InvoiceGL.ts b/packages/server/src/modules/SaleInvoices/ledger/InvoiceGL.ts index 597c58067..a8f1b1dc0 100644 --- a/packages/server/src/modules/SaleInvoices/ledger/InvoiceGL.ts +++ b/packages/server/src/modules/SaleInvoices/ledger/InvoiceGL.ts @@ -109,6 +109,11 @@ export class InvoiceGL { const localAmount = entry.totalExcludingTax * this.saleInvoice.exchangeRate; + const trackingTags = entry.trackingTagAssociations?.map((assoc) => ({ + tagId: assoc.tagId, + optionId: assoc.optionId, + })); + return { ...commonEntry, credit: localAmount, @@ -119,6 +124,7 @@ export class InvoiceGL { accountNormal: AccountNormal.CREDIT, taxRateId: entry.taxRateId, taxRate: entry.taxRate, + trackingTags, }; }, ); diff --git a/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts b/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts index 0bc1d639c..4f6f9525d 100644 --- a/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts +++ b/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts @@ -40,6 +40,11 @@ import { PaymentReceived } from '@/modules/PaymentReceived/models/PaymentReceive import { Model } from 'objection'; import { ClsModule } from 'nestjs-cls'; import { TenantUser } from './models/TenantUser.model'; +import { TrackingTag } from '@/modules/TrackingTags/models/TrackingTag'; +import { TrackingTagOption } from '@/modules/TrackingTags/models/TrackingTagOption'; +import { ItemEntryTrackingTag } from '@/modules/TrackingTags/models/ItemEntryTrackingTag'; +import { ManualJournalEntryTrackingTag } from '@/modules/TrackingTags/models/ManualJournalEntryTrackingTag'; +import { AccountTransactionTrackingTag } from '@/modules/TrackingTags/models/AccountTransactionTrackingTag'; const models = [ Item, @@ -80,6 +85,11 @@ const models = [ PaymentReceived, PaymentReceivedEntry, TenantUser, + TrackingTag, + TrackingTagOption, + ItemEntryTrackingTag, + ManualJournalEntryTrackingTag, + AccountTransactionTrackingTag, ]; /** diff --git a/packages/server/src/modules/TrackingTags/TrackingTags.application.ts b/packages/server/src/modules/TrackingTags/TrackingTags.application.ts new file mode 100644 index 000000000..25bb0b231 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/TrackingTags.application.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { CreateTrackingTagService } from './commands/CreateTrackingTag.service'; +import { EditTrackingTagService } from './commands/EditTrackingTag.service'; +import { DeleteTrackingTagService } from './commands/DeleteTrackingTag.service'; +import { GetTrackingTagsService } from './queries/GetTrackingTags.service'; +import { GetTrackingTagService } from './queries/GetTrackingTag.service'; +import { CreateTrackingTagDto, EditTrackingTagDto } from './dtos/TrackingTag.dto'; +import { TrackingTag } from './models/TrackingTag'; + +@Injectable() +export class TrackingTagsApplication { + constructor( + private createService: CreateTrackingTagService, + private editService: EditTrackingTagService, + private deleteService: DeleteTrackingTagService, + private getTagsService: GetTrackingTagsService, + private getTagService: GetTrackingTagService, + ) {} + + /** + * Creates a new tracking tag. + * @param {CreateTrackingTagDto} dto + * @returns {Promise} + */ + public createTrackingTag = (dto: CreateTrackingTagDto): Promise => { + return this.createService.createTrackingTag(dto); + }; + + /** + * Edits a tracking tag. + * @param {number} tagId + * @param {EditTrackingTagDto} dto + * @returns {Promise} + */ + public editTrackingTag = (tagId: number, dto: EditTrackingTagDto): Promise => { + return this.editService.editTrackingTag(tagId, dto); + }; + + /** + * Deletes a tracking tag. + * @param {number} tagId + * @returns {Promise} + */ + public deleteTrackingTag = (tagId: number): Promise => { + return this.deleteService.deleteTrackingTag(tagId); + }; + + /** + * Retrieves all tracking tags. + * @returns {Promise} + */ + public getTrackingTags = (): Promise => { + return this.getTagsService.getTrackingTags(); + }; + + /** + * Retrieves a tracking tag by ID. + * @param {number} tagId + * @returns {Promise} + */ + public getTrackingTag = (tagId: number): Promise => { + return this.getTagService.getTrackingTag(tagId); + }; +} diff --git a/packages/server/src/modules/TrackingTags/TrackingTags.controller.ts b/packages/server/src/modules/TrackingTags/TrackingTags.controller.ts new file mode 100644 index 000000000..db4df77ac --- /dev/null +++ b/packages/server/src/modules/TrackingTags/TrackingTags.controller.ts @@ -0,0 +1,87 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { TrackingTagsApplication } from './TrackingTags.application'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + CreateTrackingTagDto, + EditTrackingTagDto, +} from './dtos/TrackingTag.dto'; +import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; +import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator'; +import { PermissionGuard } from '@/modules/Roles/Permission.guard'; +import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard'; +import { AbilitySubject } from '@/modules/Roles/Roles.types'; + +@Controller('tracking-tags') +@ApiTags('Tracking Tags') +@ApiCommonHeaders() +@UseGuards(AuthorizationGuard, PermissionGuard) +export class TrackingTagsController { + constructor(private readonly trackingTagsApplication: TrackingTagsApplication) {} + + @Post() + @RequirePermission('Create', AbilitySubject.TrackingTag) + @ApiOperation({ summary: 'Create a new tracking tag.' }) + @ApiResponse({ + status: 201, + description: 'The tracking tag has been successfully created.', + }) + public createTrackingTag(@Body() dto: CreateTrackingTagDto) { + return this.trackingTagsApplication.createTrackingTag(dto); + } + + @Put(':id') + @RequirePermission('Edit', AbilitySubject.TrackingTag) + @ApiOperation({ summary: 'Edit the given tracking tag.' }) + @ApiResponse({ + status: 200, + description: 'The tracking tag has been successfully updated.', + }) + public editTrackingTag( + @Param('id') tagId: number, + @Body() dto: EditTrackingTagDto, + ) { + return this.trackingTagsApplication.editTrackingTag(tagId, dto); + } + + @Delete(':id') + @RequirePermission('Delete', AbilitySubject.TrackingTag) + @ApiOperation({ summary: 'Delete the given tracking tag.' }) + @ApiResponse({ + status: 200, + description: 'The tracking tag has been successfully deleted.', + }) + public deleteTrackingTag(@Param('id') tagId: number) { + return this.trackingTagsApplication.deleteTrackingTag(tagId); + } + + @Get() + @RequirePermission('View', AbilitySubject.TrackingTag) + @ApiOperation({ summary: 'Retrieves all tracking tags.' }) + @ApiResponse({ + status: 200, + description: 'The tracking tags have been successfully retrieved.', + }) + public getTrackingTags() { + return this.trackingTagsApplication.getTrackingTags(); + } + + @Get(':id') + @RequirePermission('View', AbilitySubject.TrackingTag) + @ApiOperation({ summary: 'Retrieves the tracking tag details.' }) + @ApiResponse({ + status: 200, + description: 'The tracking tag details have been successfully retrieved.', + }) + public getTrackingTag(@Param('id') tagId: number) { + return this.trackingTagsApplication.getTrackingTag(tagId); + } +} diff --git a/packages/server/src/modules/TrackingTags/TrackingTags.module.ts b/packages/server/src/modules/TrackingTags/TrackingTags.module.ts new file mode 100644 index 000000000..4699a3c90 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/TrackingTags.module.ts @@ -0,0 +1,41 @@ +import { Module } from '@nestjs/common'; +import { TrackingTagsController } from './TrackingTags.controller'; +import { TrackingTagsApplication } from './TrackingTags.application'; +import { CreateTrackingTagService } from './commands/CreateTrackingTag.service'; +import { EditTrackingTagService } from './commands/EditTrackingTag.service'; +import { DeleteTrackingTagService } from './commands/DeleteTrackingTag.service'; +import { GetTrackingTagsService } from './queries/GetTrackingTags.service'; +import { GetTrackingTagService } from './queries/GetTrackingTag.service'; +import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module'; +import { TrackingTag } from './models/TrackingTag'; +import { TrackingTagOption } from './models/TrackingTagOption'; +import { ItemEntryTrackingTag } from './models/ItemEntryTrackingTag'; +import { ManualJournalEntryTrackingTag } from './models/ManualJournalEntryTrackingTag'; +import { AccountTransactionTrackingTag } from './models/AccountTransactionTrackingTag'; + +const models = [ + RegisterTenancyModel(TrackingTag), + RegisterTenancyModel(TrackingTagOption), + RegisterTenancyModel(ItemEntryTrackingTag), + RegisterTenancyModel(ManualJournalEntryTrackingTag), + RegisterTenancyModel(AccountTransactionTrackingTag), +]; + +@Module({ + imports: [...models], + controllers: [TrackingTagsController], + providers: [ + TrackingTagsApplication, + CreateTrackingTagService, + EditTrackingTagService, + DeleteTrackingTagService, + GetTrackingTagsService, + GetTrackingTagService, + ], + exports: [ + GetTrackingTagsService, + GetTrackingTagService, + ...models, + ], +}) +export class TrackingTagsModule {} diff --git a/packages/server/src/modules/TrackingTags/commands/CreateTrackingTag.service.ts b/packages/server/src/modules/TrackingTags/commands/CreateTrackingTag.service.ts new file mode 100644 index 000000000..e556d3737 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/commands/CreateTrackingTag.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; +import { TrackingTag } from '../models/TrackingTag'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { CreateTrackingTagDto } from '../dtos/TrackingTag.dto'; + +@Injectable() +export class CreateTrackingTagService { + constructor( + @Inject(TrackingTag.name) + private trackingTagModel: TenantModelProxy, + ) {} + + /** + * Creates a new tracking tag with options. + * @param {CreateTrackingTagDto} dto + * @returns {Promise} + */ + public createTrackingTag = async (dto: CreateTrackingTagDto): Promise => { + const tag = await this.trackingTagModel() + .query() + .insertGraphAndFetch({ + name: dto.name, + description: dto.description, + active: dto.active, + options: dto.options.map((opt) => ({ + name: opt.name, + active: opt.active, + })), + }); + + return tag; + }; +} diff --git a/packages/server/src/modules/TrackingTags/commands/DeleteTrackingTag.service.ts b/packages/server/src/modules/TrackingTags/commands/DeleteTrackingTag.service.ts new file mode 100644 index 000000000..065ac129f --- /dev/null +++ b/packages/server/src/modules/TrackingTags/commands/DeleteTrackingTag.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; +import { TrackingTag } from '../models/TrackingTag'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; + +@Injectable() +export class DeleteTrackingTagService { + constructor( + @Inject(TrackingTag.name) + private trackingTagModel: TenantModelProxy, + ) {} + + /** + * Deletes a tracking tag. + * @param {number} tagId + * @returns {Promise} + */ + public deleteTrackingTag = async (tagId: number): Promise => { + const tag = await this.trackingTagModel() + .query() + .findById(tagId) + .throwIfNotFound(); + + await this.trackingTagModel().query().deleteById(tagId); + }; +} diff --git a/packages/server/src/modules/TrackingTags/commands/EditTrackingTag.service.ts b/packages/server/src/modules/TrackingTags/commands/EditTrackingTag.service.ts new file mode 100644 index 000000000..35528ae19 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/commands/EditTrackingTag.service.ts @@ -0,0 +1,58 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; +import { TrackingTag } from '../models/TrackingTag'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { EditTrackingTagDto } from '../dtos/TrackingTag.dto'; + +@Injectable() +export class EditTrackingTagService { + constructor( + @Inject(TrackingTag.name) + private trackingTagModel: TenantModelProxy, + ) {} + + /** + * Edits a tracking tag. + * @param {number} tagId + * @param {EditTrackingTagDto} dto + * @returns {Promise} + */ + public editTrackingTag = async ( + tagId: number, + dto: EditTrackingTagDto, + ): Promise => { + const tag = await this.trackingTagModel() + .query() + .findById(tagId) + .throwIfNotFound(); + + const updatePayload: any = {}; + + if (dto.name !== undefined) updatePayload.name = dto.name; + if (dto.description !== undefined) updatePayload.description = dto.description; + if (dto.active !== undefined) updatePayload.active = dto.active; + + if (dto.options) { + updatePayload.options = dto.options.map((opt) => ({ + ...(opt.id ? { id: opt.id } : {}), + name: opt.name, + active: opt.active, + })); + } + + await this.trackingTagModel() + .query() + .upsertGraphAndFetch({ + id: tagId, + ...updatePayload, + }); + + const updatedTag = await this.trackingTagModel() + .query() + .findById(tagId) + .withGraphFetched('options') + .throwIfNotFound(); + + return updatedTag; + }; +} diff --git a/packages/server/src/modules/TrackingTags/dtos/AssignTrackingTags.dto.ts b/packages/server/src/modules/TrackingTags/dtos/AssignTrackingTags.dto.ts new file mode 100644 index 000000000..3f1d16e08 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/dtos/AssignTrackingTags.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsNotEmpty, IsOptional, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class TrackingTagAssignmentDto { + @IsInt() + @IsNotEmpty() + @ApiProperty({ description: 'Tag ID', example: 1 }) + tagId: number; + + @IsInt() + @IsOptional() + @ApiProperty({ description: 'Option ID', example: 5 }) + optionId?: number; +} + +export class AssignTrackingTagsDto { + @ValidateNested({ each: true }) + @Type(() => TrackingTagAssignmentDto) + @ApiProperty({ description: 'Tracking tag assignments', type: [TrackingTagAssignmentDto] }) + assignments: TrackingTagAssignmentDto[]; +} diff --git a/packages/server/src/modules/TrackingTags/dtos/TrackingTag.dto.ts b/packages/server/src/modules/TrackingTags/dtos/TrackingTag.dto.ts new file mode 100644 index 000000000..247fb9a99 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/dtos/TrackingTag.dto.ts @@ -0,0 +1,99 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsArray, + IsBoolean, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class TrackingTagOptionDto { + @IsInt() + @IsOptional() + @ApiProperty({ description: 'Option ID (for updates)', required: false }) + id?: number; + + @IsString() + @IsNotEmpty() + @ApiProperty({ description: 'Option name', example: 'New York' }) + name: string; + + @IsBoolean() + @IsOptional() + @ApiProperty({ description: 'Whether the option is active', required: false, example: true }) + active?: boolean = true; +} + +export class CreateTrackingTagDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ description: 'Tag name', example: 'Location' }) + name: string; + + @IsString() + @IsOptional() + @ApiProperty({ description: 'Tag description', required: false, example: 'Business location tracking' }) + description?: string; + + @IsBoolean() + @IsOptional() + @ApiProperty({ description: 'Whether the tag is active', required: false, example: true }) + active?: boolean = true; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TrackingTagOptionDto) + @ApiProperty({ description: 'Tag options', type: [TrackingTagOptionDto] }) + options: TrackingTagOptionDto[]; +} + +export class EditTrackingTagDto { + @IsString() + @IsOptional() + @ApiProperty({ description: 'Tag name', required: false, example: 'Location' }) + name?: string; + + @IsString() + @IsOptional() + @ApiProperty({ description: 'Tag description', required: false, example: 'Business location tracking' }) + description?: string; + + @IsBoolean() + @IsOptional() + @ApiProperty({ description: 'Whether the tag is active', required: false, example: true }) + active?: boolean; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TrackingTagOptionDto) + @IsOptional() + @ApiProperty({ description: 'Tag options', type: [TrackingTagOptionDto], required: false }) + options?: TrackingTagOptionDto[]; +} + +export class CreateTrackingTagOptionDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ description: 'Option name', example: 'New York' }) + name: string; + + @IsBoolean() + @IsOptional() + @ApiProperty({ description: 'Whether the option is active', required: false, example: true }) + active?: boolean = true; +} + +export class EditTrackingTagOptionDto { + @IsString() + @IsOptional() + @ApiProperty({ description: 'Option name', required: false, example: 'New York' }) + name?: string; + + @IsBoolean() + @IsOptional() + @ApiProperty({ description: 'Whether the option is active', required: false, example: true }) + active?: boolean; +} diff --git a/packages/server/src/modules/TrackingTags/models/AccountTransactionTrackingTag.ts b/packages/server/src/modules/TrackingTags/models/AccountTransactionTrackingTag.ts new file mode 100644 index 000000000..71f637bb7 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/models/AccountTransactionTrackingTag.ts @@ -0,0 +1,60 @@ +import { Model } from 'objection'; +import { BaseModel } from '@/models/Model'; +import { TrackingTag } from './TrackingTag'; +import { TrackingTagOption } from './TrackingTagOption'; +import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model'; + +export class AccountTransactionTrackingTag extends BaseModel { + public accountTransactionId!: number; + public tagId!: number; + public optionId!: number; + + public tag!: TrackingTag; + public option!: TrackingTagOption; + public accountTransaction!: AccountTransaction; + + static get tableName() { + return 'account_transaction_tracking_tags'; + } + + static get idColumn() { + return ['accountTransactionId', 'tagId']; + } + + static get timestamps() { + return ['createdAt', 'updatedAt']; + } + + static get relationMappings() { + const { TrackingTag } = require('./TrackingTag'); + const { TrackingTagOption } = require('./TrackingTagOption'); + const { AccountTransaction } = require('../../Accounts/models/AccountTransaction.model'); + + return { + tag: { + relation: Model.BelongsToOneRelation, + modelClass: TrackingTag, + join: { + from: 'account_transaction_tracking_tags.tagId', + to: 'tracking_tags.id', + }, + }, + option: { + relation: Model.BelongsToOneRelation, + modelClass: TrackingTagOption, + join: { + from: 'account_transaction_tracking_tags.optionId', + to: 'tracking_tag_options.id', + }, + }, + accountTransaction: { + relation: Model.BelongsToOneRelation, + modelClass: AccountTransaction, + join: { + from: 'account_transaction_tracking_tags.accountTransactionId', + to: 'accounts_transactions.id', + }, + }, + }; + } +} diff --git a/packages/server/src/modules/TrackingTags/models/ItemEntryTrackingTag.ts b/packages/server/src/modules/TrackingTags/models/ItemEntryTrackingTag.ts new file mode 100644 index 000000000..7cf3c3bc9 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/models/ItemEntryTrackingTag.ts @@ -0,0 +1,60 @@ +import { Model } from 'objection'; +import { BaseModel } from '@/models/Model'; +import { TrackingTag } from './TrackingTag'; +import { TrackingTagOption } from './TrackingTagOption'; +import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry'; + +export class ItemEntryTrackingTag extends BaseModel { + public itemEntryId!: number; + public tagId!: number; + public optionId!: number; + + public tag!: TrackingTag; + public option!: TrackingTagOption; + public itemEntry!: ItemEntry; + + static get tableName() { + return 'item_entry_tracking_tags'; + } + + static get idColumn() { + return ['itemEntryId', 'tagId']; + } + + static get timestamps() { + return ['createdAt', 'updatedAt']; + } + + static get relationMappings() { + const { TrackingTag } = require('./TrackingTag'); + const { TrackingTagOption } = require('./TrackingTagOption'); + const { ItemEntry } = require('../../TransactionItemEntry/models/ItemEntry'); + + return { + tag: { + relation: Model.BelongsToOneRelation, + modelClass: TrackingTag, + join: { + from: 'item_entry_tracking_tags.tagId', + to: 'tracking_tags.id', + }, + }, + option: { + relation: Model.BelongsToOneRelation, + modelClass: TrackingTagOption, + join: { + from: 'item_entry_tracking_tags.optionId', + to: 'tracking_tag_options.id', + }, + }, + itemEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ItemEntry, + join: { + from: 'item_entry_tracking_tags.itemEntryId', + to: 'items_entries.id', + }, + }, + }; + } +} diff --git a/packages/server/src/modules/TrackingTags/models/ManualJournalEntryTrackingTag.ts b/packages/server/src/modules/TrackingTags/models/ManualJournalEntryTrackingTag.ts new file mode 100644 index 000000000..5ad44991a --- /dev/null +++ b/packages/server/src/modules/TrackingTags/models/ManualJournalEntryTrackingTag.ts @@ -0,0 +1,60 @@ +import { Model } from 'objection'; +import { BaseModel } from '@/models/Model'; +import { TrackingTag } from './TrackingTag'; +import { TrackingTagOption } from './TrackingTagOption'; +import { ManualJournalEntry } from '@/modules/ManualJournals/models/ManualJournalEntry'; + +export class ManualJournalEntryTrackingTag extends BaseModel { + public manualJournalEntryId!: number; + public tagId!: number; + public optionId!: number; + + public tag!: TrackingTag; + public option!: TrackingTagOption; + public manualJournalEntry!: ManualJournalEntry; + + static get tableName() { + return 'manual_journal_entry_tracking_tags'; + } + + static get idColumn() { + return ['manualJournalEntryId', 'tagId']; + } + + static get timestamps() { + return ['createdAt', 'updatedAt']; + } + + static get relationMappings() { + const { TrackingTag } = require('./TrackingTag'); + const { TrackingTagOption } = require('./TrackingTagOption'); + const { ManualJournalEntry } = require('../../ManualJournals/models/ManualJournalEntry'); + + return { + tag: { + relation: Model.BelongsToOneRelation, + modelClass: TrackingTag, + join: { + from: 'manual_journal_entry_tracking_tags.tagId', + to: 'tracking_tags.id', + }, + }, + option: { + relation: Model.BelongsToOneRelation, + modelClass: TrackingTagOption, + join: { + from: 'manual_journal_entry_tracking_tags.optionId', + to: 'tracking_tag_options.id', + }, + }, + manualJournalEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ManualJournalEntry, + join: { + from: 'manual_journal_entry_tracking_tags.manualJournalEntryId', + to: 'manual_journals_entries.id', + }, + }, + }; + } +} diff --git a/packages/server/src/modules/TrackingTags/models/TrackingTag.ts b/packages/server/src/modules/TrackingTags/models/TrackingTag.ts new file mode 100644 index 000000000..bfb3ae545 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/models/TrackingTag.ts @@ -0,0 +1,37 @@ +import { Model } from 'objection'; +import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; +import { TrackingTagOption } from './TrackingTagOption'; + +export class TrackingTag extends TenantBaseModel { + public name!: string; + public description!: string | null; + public active!: boolean; + + public options!: TrackingTagOption[]; + + static get tableName() { + return 'tracking_tags'; + } + + static get timestamps() { + return ['createdAt', 'updatedAt']; + } + + static get relationMappings() { + const { TrackingTagOption } = require('./TrackingTagOption'); + + return { + options: { + relation: Model.HasManyRelation, + modelClass: TrackingTagOption, + join: { + from: 'tracking_tags.id', + to: 'tracking_tag_options.tagId', + }, + filter(query) { + query.where('active', true); + }, + }, + }; + } +} diff --git a/packages/server/src/modules/TrackingTags/models/TrackingTagOption.ts b/packages/server/src/modules/TrackingTags/models/TrackingTagOption.ts new file mode 100644 index 000000000..905333060 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/models/TrackingTagOption.ts @@ -0,0 +1,34 @@ +import { Model } from 'objection'; +import { BaseModel } from '@/models/Model'; +import { TrackingTag } from './TrackingTag'; + +export class TrackingTagOption extends BaseModel { + public tagId!: number; + public name!: string; + public active!: boolean; + + public tag!: TrackingTag; + + static get tableName() { + return 'tracking_tag_options'; + } + + static get timestamps() { + return ['createdAt', 'updatedAt']; + } + + static get relationMappings() { + const { TrackingTag } = require('./TrackingTag'); + + return { + tag: { + relation: Model.BelongsToOneRelation, + modelClass: TrackingTag, + join: { + from: 'tracking_tag_options.tagId', + to: 'tracking_tags.id', + }, + }, + }; + } +} diff --git a/packages/server/src/modules/TrackingTags/queries/GetTrackingTag.service.ts b/packages/server/src/modules/TrackingTags/queries/GetTrackingTag.service.ts new file mode 100644 index 000000000..420dbeedc --- /dev/null +++ b/packages/server/src/modules/TrackingTags/queries/GetTrackingTag.service.ts @@ -0,0 +1,30 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; +import { TrackingTag } from '../models/TrackingTag'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; + +@Injectable() +export class GetTrackingTagService { + constructor( + @Inject(TrackingTag.name) + private trackingTagModel: TenantModelProxy, + ) {} + + /** + * Retrieves a tracking tag by ID with its options. + * @param {number} tagId + * @returns {Promise} + */ + public getTrackingTag = async (tagId: number): Promise => { + const tag = await this.trackingTagModel() + .query() + .findById(tagId) + .withGraphFetched('options'); + + if (!tag) { + throw new NotFoundException('Tracking tag not found.'); + } + + return tag; + }; +} diff --git a/packages/server/src/modules/TrackingTags/queries/GetTrackingTags.service.ts b/packages/server/src/modules/TrackingTags/queries/GetTrackingTags.service.ts new file mode 100644 index 000000000..45bcc9332 --- /dev/null +++ b/packages/server/src/modules/TrackingTags/queries/GetTrackingTags.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; +import { TrackingTag } from '../models/TrackingTag'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; + +@Injectable() +export class GetTrackingTagsService { + constructor( + @Inject(TrackingTag.name) + private trackingTagModel: TenantModelProxy, + ) {} + + /** + * Retrieves all tracking tags with their options. + * @returns {Promise} + */ + public getTrackingTags = async (): Promise => { + return this.trackingTagModel() + .query() + .withGraphFetched('options'); + }; +} diff --git a/packages/server/src/modules/TransactionItemEntry/dto/ItemEntry.dto.ts b/packages/server/src/modules/TransactionItemEntry/dto/ItemEntry.dto.ts index fb745fb98..46a64d137 100644 --- a/packages/server/src/modules/TransactionItemEntry/dto/ItemEntry.dto.ts +++ b/packages/server/src/modules/TransactionItemEntry/dto/ItemEntry.dto.ts @@ -2,6 +2,7 @@ import { ToNumber } from '@/common/decorators/Validators'; import { DiscountType } from '@/common/types/Discount'; import { ApiProperty } from '@nestjs/swagger'; import { + IsArray, IsEnum, IsIn, IsInt, @@ -9,7 +10,10 @@ import { IsNumber, IsOptional, IsString, + ValidateNested, } from 'class-validator'; +import { Type } from 'class-transformer'; +import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto'; export class ItemEntryDto { @IsInt() @@ -153,4 +157,15 @@ export class ItemEntryDto { example: 1021, }) costAccountId?: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TrackingTagAssignmentDto) + @ApiProperty({ + description: 'The tracking tags of the item entry', + type: [TrackingTagAssignmentDto], + required: false, + }) + trackingTags?: TrackingTagAssignmentDto[]; } diff --git a/packages/server/src/modules/TransactionItemEntry/models/ItemEntry.ts b/packages/server/src/modules/TransactionItemEntry/models/ItemEntry.ts index 5ce0268d1..8435c5746 100644 --- a/packages/server/src/modules/TransactionItemEntry/models/ItemEntry.ts +++ b/packages/server/src/modules/TransactionItemEntry/models/ItemEntry.ts @@ -39,6 +39,7 @@ export class ItemEntry extends BaseModel { item: Item; allocatedCostEntries: BillLandedCostEntry[]; + trackingTagAssociations: any[]; /** * Table name. @@ -190,6 +191,7 @@ export class ItemEntry extends BaseModel { const { SaleReceipt } = require('../../SaleReceipts/models/SaleReceipt'); const { SaleEstimate } = require('../../SaleEstimates/models/SaleEstimate'); const { TaxRateModel } = require('../../TaxRates/models/TaxRate.model'); + const { ItemEntryTrackingTag } = require('../../TrackingTags/models/ItemEntryTrackingTag'); // const { Expense } = require('../../Expenses/models/Expense.model'); // const ProjectTask = require('models/Task'); @@ -297,6 +299,18 @@ export class ItemEntry extends BaseModel { to: 'tax_rates.id', }, }, + + /** + * Tracking tag associations. + */ + trackingTagAssociations: { + relation: Model.HasManyRelation, + modelClass: ItemEntryTrackingTag, + join: { + from: 'items_entries.id', + to: 'item_entry_tracking_tags.itemEntryId', + }, + }, }; } } diff --git a/packages/webapp/src/hooks/query/tracking-tags.ts b/packages/webapp/src/hooks/query/tracking-tags.ts new file mode 100644 index 000000000..3695e41b8 --- /dev/null +++ b/packages/webapp/src/hooks/query/tracking-tags.ts @@ -0,0 +1,95 @@ +// @ts-nocheck +import { useMutation, useQueryClient } from 'react-query'; +import { useRequestQuery } from '../useQueryRequest'; +import QUERY_TYPES from './types'; +import useApiRequest from '../useRequest'; + +// Common invalidate queries. +const commonInvalidateQueries = (queryClient) => { + queryClient.invalidateQueries(QUERY_TYPES.TRACKING_TAGS); +}; + +/** + * Retrieves tracking tags. + */ +export function useTrackingTags(props) { + return useRequestQuery( + [QUERY_TYPES.TRACKING_TAGS], + { + method: 'get', + url: `tracking-tags`, + }, + { + select: (res) => res.data, + defaultData: [], + ...props, + }, + ); +} + +/** + * Retrieves tracking tag. + * @param {number} tagId - Tracking tag id. + */ +export function useTrackingTag(tagId: string, props) { + return useRequestQuery( + [QUERY_TYPES.TRACKING_TAGS, tagId], + { + method: 'get', + url: `tracking-tags/${tagId}`, + }, + { + select: (res) => res.data, + ...props, + }, + ); +} + +/** + * Creates a new tracking tag. + */ +export function useCreateTrackingTag(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation((values) => apiRequest.post('tracking-tags', values), { + onSuccess: () => { + commonInvalidateQueries(queryClient); + }, + ...props, + }); +} + +/** + * Edit the given tracking tag. + */ +export function useEditTrackingTag(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation( + ([id, values]) => apiRequest.put(`tracking-tags/${id}`, values), + { + onSuccess: (res, id) => { + commonInvalidateQueries(queryClient); + queryClient.invalidateQueries([QUERY_TYPES.TRACKING_TAGS, id]); + }, + ...props, + }, + ); +} + +/** + * Delete the given tracking tag. + */ +export function useDeleteTrackingTag(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation((id) => apiRequest.delete(`tracking-tags/${id}`), { + onSuccess: () => { + commonInvalidateQueries(queryClient); + }, + ...props, + }); +} diff --git a/packages/webapp/src/hooks/query/types.tsx b/packages/webapp/src/hooks/query/types.tsx index 4917058e2..5b242cb6e 100644 --- a/packages/webapp/src/hooks/query/types.tsx +++ b/packages/webapp/src/hooks/query/types.tsx @@ -250,6 +250,11 @@ const CUSTOM_FIELDS = { CUSTOM_FIELD: 'CUSTOM_FIELD', }; +const TRACKING_TAGS = { + TRACKING_TAGS: 'TRACKING_TAGS', + TRACKING_TAG: 'TRACKING_TAG', +}; + export default { ...Authentication, ...ACCOUNTS, @@ -287,4 +292,5 @@ export default { ...EXCHANGE_RATE, ...API_KEYS, ...CUSTOM_FIELDS, + ...TRACKING_TAGS, }; diff --git a/shared/sdk-ts/openapi.json b/shared/sdk-ts/openapi.json index 2bf2be0e9..c9821f965 100644 --- a/shared/sdk-ts/openapi.json +++ b/shared/sdk-ts/openapi.json @@ -15341,6 +15341,26 @@ "type": "boolean" } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -17016,6 +17036,26 @@ } } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -17190,6 +17230,26 @@ } } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -18870,6 +18930,26 @@ } } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -19605,6 +19685,26 @@ } } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -19741,6 +19841,26 @@ "type": "boolean" } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -19796,6 +19916,26 @@ "example": "1", "type": "number" } + }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } } ], "responses": { @@ -21099,6 +21239,26 @@ "type": "number" } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -21644,6 +21804,26 @@ "type": "boolean" } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -22077,6 +22257,26 @@ "type": "string" } }, + { + "description": "Tag ID", + "name": "tagId", + "in": "query", + "required": true, + "schema": { + "example": 1, + "type": "number" + } + }, + { + "description": "Option ID", + "name": "optionId", + "in": "query", + "required": true, + "schema": { + "example": 5, + "type": "number" + } + }, { "name": "accept", "required": true, @@ -24127,6 +24327,219 @@ ] } }, + "/api/tracking-tags": { + "post": { + "operationId": "TrackingTagsController_createTrackingTag", + "summary": "Create a new tracking tag.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token.", + "required": true, + "schema": { + "type": "string", + "example": "Bearer bc_1234567890abcdef" + } + }, + { + "name": "organization-id", + "in": "header", + "description": "Required if Authorization is a JWT token. The organization ID to operate within.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTrackingTagDto" + } + } + } + }, + "responses": { + "201": { + "description": "The tracking tag has been successfully created." + } + }, + "tags": [ + "Tracking Tags" + ] + }, + "get": { + "operationId": "TrackingTagsController_getTrackingTags", + "summary": "Retrieves all tracking tags.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token.", + "required": true, + "schema": { + "type": "string", + "example": "Bearer bc_1234567890abcdef" + } + }, + { + "name": "organization-id", + "in": "header", + "description": "Required if Authorization is a JWT token. The organization ID to operate within.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The tracking tags have been successfully retrieved." + } + }, + "tags": [ + "Tracking Tags" + ] + } + }, + "/api/tracking-tags/{id}": { + "put": { + "operationId": "TrackingTagsController_editTrackingTag", + "summary": "Edit the given tracking tag.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token.", + "required": true, + "schema": { + "type": "string", + "example": "Bearer bc_1234567890abcdef" + } + }, + { + "name": "organization-id", + "in": "header", + "description": "Required if Authorization is a JWT token. The organization ID to operate within.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditTrackingTagDto" + } + } + } + }, + "responses": { + "200": { + "description": "The tracking tag has been successfully updated." + } + }, + "tags": [ + "Tracking Tags" + ] + }, + "delete": { + "operationId": "TrackingTagsController_deleteTrackingTag", + "summary": "Delete the given tracking tag.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token.", + "required": true, + "schema": { + "type": "string", + "example": "Bearer bc_1234567890abcdef" + } + }, + { + "name": "organization-id", + "in": "header", + "description": "Required if Authorization is a JWT token. The organization ID to operate within.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "The tracking tag has been successfully deleted." + } + }, + "tags": [ + "Tracking Tags" + ] + }, + "get": { + "operationId": "TrackingTagsController_getTrackingTag", + "summary": "Retrieves the tracking tag details.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token.", + "required": true, + "schema": { + "type": "string", + "example": "Bearer bc_1234567890abcdef" + } + }, + { + "name": "organization-id", + "in": "header", + "description": "Required if Authorization is a JWT token. The organization ID to operate within.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "The tracking tag details have been successfully retrieved." + } + }, + "tags": [ + "Tracking Tags" + ] + } + }, "/api/exchange-rates/latest": { "get": { "operationId": "ExchangeRatesController_getLatestExchangeRate", @@ -26480,6 +26893,25 @@ "defaultTemplateId" ] }, + "TrackingTagAssignmentDto": { + "type": "object", + "properties": { + "tagId": { + "type": "number", + "description": "Tag ID", + "example": 1 + }, + "optionId": { + "type": "number", + "description": "Option ID", + "example": 5 + } + }, + "required": [ + "tagId", + "optionId" + ] + }, "ItemEntryDto": { "type": "object", "properties": { @@ -26562,6 +26994,13 @@ "type": "number", "description": "The cost account id of the item entry", "example": 1021 + }, + "trackingTags": { + "description": "The tracking tags of the item entry", + "type": "array", + "items": { + "$ref": "#/components/schemas/TrackingTagAssignmentDto" + } } }, "required": [ @@ -31659,6 +32098,13 @@ "description": "The cost account id of the item entry", "example": 1021 }, + "trackingTags": { + "description": "The tracking tags of the item entry", + "type": "array", + "items": { + "$ref": "#/components/schemas/TrackingTagAssignmentDto" + } + }, "landedCost": { "type": "boolean", "description": "Flag indicating whether the entry contributes to landed cost", @@ -32131,6 +32577,13 @@ "projectId": { "type": "number", "description": "Project ID" + }, + "trackingTags": { + "description": "The tracking tags of the manual journal entry", + "type": "array", + "items": { + "$ref": "#/components/schemas/TrackingTagAssignmentDto" + } } }, "required": [ @@ -40639,6 +41092,86 @@ "password" ] }, + "TrackingTagOptionDto": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Option ID (for updates)" + }, + "name": { + "type": "string", + "description": "Option name", + "example": "New York" + }, + "active": { + "type": "boolean", + "description": "Whether the option is active", + "example": true + } + }, + "required": [ + "name" + ] + }, + "CreateTrackingTagDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Tag name", + "example": "Location" + }, + "description": { + "type": "string", + "description": "Tag description", + "example": "Business location tracking" + }, + "active": { + "type": "boolean", + "description": "Whether the tag is active", + "example": true + }, + "options": { + "description": "Tag options", + "type": "array", + "items": { + "$ref": "#/components/schemas/TrackingTagOptionDto" + } + } + }, + "required": [ + "name", + "options" + ] + }, + "EditTrackingTagDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Tag name", + "example": "Location" + }, + "description": { + "type": "string", + "description": "Tag description", + "example": "Business location tracking" + }, + "active": { + "type": "boolean", + "description": "Whether the tag is active", + "example": true + }, + "options": { + "description": "Tag options", + "type": "array", + "items": { + "$ref": "#/components/schemas/TrackingTagOptionDto" + } + } + } + }, "ExchangeRateLatestResponseDto": { "type": "object", "properties": { diff --git a/shared/sdk-ts/src/index.ts b/shared/sdk-ts/src/index.ts index 021da3ca5..40f05c871 100644 --- a/shared/sdk-ts/src/index.ts +++ b/shared/sdk-ts/src/index.ts @@ -20,6 +20,7 @@ export * from './import'; export * from './manual-journals'; export * from './roles'; export * from './custom-fields'; +export * from './tracking-tags'; export * from './users'; export * from './dashboard'; export * from './settings'; diff --git a/shared/sdk-ts/src/schema.ts b/shared/sdk-ts/src/schema.ts index f146f4906..d7b0d97e9 100644 --- a/shared/sdk-ts/src/schema.ts +++ b/shared/sdk-ts/src/schema.ts @@ -4714,6 +4714,43 @@ export interface paths { patch: operations["ContactsController_inactivateContact"]; trace?: never; }; + "/api/tracking-tags": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Retrieves all tracking tags. */ + get: operations["TrackingTagsController_getTrackingTags"]; + put?: never; + /** Create a new tracking tag. */ + post: operations["TrackingTagsController_createTrackingTag"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/tracking-tags/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Retrieves the tracking tag details. */ + get: operations["TrackingTagsController_getTrackingTag"]; + /** Edit the given tracking tag. */ + put: operations["TrackingTagsController_editTrackingTag"]; + post?: never; + /** Delete the given tracking tag. */ + delete: operations["TrackingTagsController_deleteTrackingTag"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/exchange-rates/latest": { parameters: { query?: never; @@ -6349,6 +6386,18 @@ export interface components { */ defaultTemplateId: number | null; }; + TrackingTagAssignmentDto: { + /** + * @description Tag ID + * @example 1 + */ + tagId: number; + /** + * @description Option ID + * @example 5 + */ + optionId: number; + }; ItemEntryDto: { /** * @description The index of the item entry @@ -6430,6 +6479,8 @@ export interface components { * @example 1021 */ costAccountId: number; + /** @description The tracking tags of the item entry */ + trackingTags?: components["schemas"]["TrackingTagAssignmentDto"][]; }; AttachmentLinkDto: Record; PaymentMethodDto: { @@ -10050,6 +10101,8 @@ export interface components { * @example 1021 */ costAccountId: number; + /** @description The tracking tags of the item entry */ + trackingTags?: components["schemas"]["TrackingTagAssignmentDto"][]; /** * @description Flag indicating whether the entry contributes to landed cost * @example true @@ -10397,6 +10450,8 @@ export interface components { branchId?: number; /** @description Project ID */ projectId?: number; + /** @description The tracking tags of the manual journal entry */ + trackingTags?: components["schemas"]["TrackingTagAssignmentDto"][]; }; CreateManualJournalDto: { /** @@ -14601,6 +14656,58 @@ export interface components { */ password: string; }; + TrackingTagOptionDto: { + /** @description Option ID (for updates) */ + id?: number; + /** + * @description Option name + * @example New York + */ + name: string; + /** + * @description Whether the option is active + * @example true + */ + active?: boolean; + }; + CreateTrackingTagDto: { + /** + * @description Tag name + * @example Location + */ + name: string; + /** + * @description Tag description + * @example Business location tracking + */ + description?: string; + /** + * @description Whether the tag is active + * @example true + */ + active?: boolean; + /** @description Tag options */ + options: components["schemas"]["TrackingTagOptionDto"][]; + }; + EditTrackingTagDto: { + /** + * @description Tag name + * @example Location + */ + name?: string; + /** + * @description Tag description + * @example Business location tracking + */ + description?: string; + /** + * @description Whether the tag is active + * @example true + */ + active?: boolean; + /** @description Tag options */ + options?: components["schemas"]["TrackingTagOptionDto"][]; + }; ExchangeRateLatestResponseDto: { /** * @description The base currency code @@ -23238,6 +23345,10 @@ export interface operations { previousYearAmountChange?: boolean; /** @description Whether to show percentage change from previous year */ previousYearPercentageChange?: boolean; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -24645,7 +24756,7 @@ export interface operations { }; CustomerBalanceSummaryController_customerBalanceSummary: { parameters: { - query?: { + query: { /** @description The date as of which the balance summary is calculated */ asDate?: string; /** @description Number of decimal places to display */ @@ -24666,6 +24777,10 @@ export interface operations { noneZero?: boolean; /** @description Array of customer IDs to filter the summary */ customersIds?: number[]; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -24693,7 +24808,7 @@ export interface operations { }; VendorBalanceSummaryController_vendorBalanceSummary: { parameters: { - query?: { + query: { /** @description The date as of which the balance summary is calculated */ asDate?: string; /** @description Number of decimal places to display */ @@ -24714,6 +24829,10 @@ export interface operations { noneZero?: boolean; /** @description Array of vendor IDs to filter the summary */ vendorsIds?: number[]; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -26000,7 +26119,7 @@ export interface operations { }; TrialBalanceSheetController_getTrialBalanceSheet: { parameters: { - query?: { + query: { /** @description Start date for the trial balance sheet */ fromDate?: string; /** @description End date for the trial balance sheet */ @@ -26025,6 +26144,10 @@ export interface operations { onlyActive?: boolean; /** @description Filter by specific account IDs */ accountIds?: number[]; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -26641,7 +26764,7 @@ export interface operations { }; TransactionsByVendorController_transactionsByVendor: { parameters: { - query?: { + query: { /** @description Number of decimal places to display */ precision?: number; /** @description Whether to divide the number by 1000 */ @@ -26658,6 +26781,10 @@ export interface operations { noneZero?: boolean; /** @description Array of vendor IDs to include */ vendorsIds?: string[]; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -26685,7 +26812,7 @@ export interface operations { }; TransactionsByCustomerController_transactionsByCustomer: { parameters: { - query?: { + query: { /** @description Number of decimal places to display */ precision?: number; /** @description Whether to divide the number by 1000 */ @@ -26700,6 +26827,10 @@ export interface operations { noneTransactions?: boolean; /** @description Whether to exclude zero values */ noneZero?: boolean; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -26732,6 +26863,10 @@ export interface operations { referenceType: string; /** @description The ID of the reference */ referenceId: number; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header?: never; path?: never; @@ -27420,7 +27555,7 @@ export interface operations { }; JournalSheetController_journalSheet: { parameters: { - query?: { + query: { /** @description Whether to hide cents in the number format */ noCents?: boolean; /** @description Whether to divide numbers by 1000 */ @@ -27433,6 +27568,10 @@ export interface operations { fromRange?: number; /** @description End range for filtering */ toRange?: number; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -27785,6 +27924,10 @@ export interface operations { previousYearAmountChange?: boolean; /** @description Whether to show previous year percentage change */ previousYearPercentageChange?: boolean; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -28054,7 +28197,7 @@ export interface operations { }; CashflowController_getCashflow: { parameters: { - query?: { + query: { /** @description Start date for the cash flow statement period */ fromDate?: string; /** @description End date for the cash flow statement period */ @@ -28079,6 +28222,10 @@ export interface operations { negativeFormat?: "parentheses" | "mines"; /** @description Basis for the cash flow statement */ basis?: string; + /** @description Tag ID */ + tagId: number; + /** @description Option ID */ + optionId: number; }; header: { /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ @@ -29617,6 +29764,135 @@ export interface operations { }; }; }; + TrackingTagsController_getTrackingTags: { + parameters: { + query?: never; + header: { + /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ + Authorization: string; + /** @description Required if Authorization is a JWT token. The organization ID to operate within. */ + "organization-id": string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The tracking tags have been successfully retrieved. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TrackingTagsController_createTrackingTag: { + parameters: { + query?: never; + header: { + /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ + Authorization: string; + /** @description Required if Authorization is a JWT token. The organization ID to operate within. */ + "organization-id": string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTrackingTagDto"]; + }; + }; + responses: { + /** @description The tracking tag has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TrackingTagsController_getTrackingTag: { + parameters: { + query?: never; + header: { + /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ + Authorization: string; + /** @description Required if Authorization is a JWT token. The organization ID to operate within. */ + "organization-id": string; + }; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The tracking tag details have been successfully retrieved. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TrackingTagsController_editTrackingTag: { + parameters: { + query?: never; + header: { + /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ + Authorization: string; + /** @description Required if Authorization is a JWT token. The organization ID to operate within. */ + "organization-id": string; + }; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EditTrackingTagDto"]; + }; + }; + responses: { + /** @description The tracking tag has been successfully updated. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TrackingTagsController_deleteTrackingTag: { + parameters: { + query?: never; + header: { + /** @description Value must be 'Bearer ' where is an API key prefixed with 'bc_' or a JWT token. */ + Authorization: string; + /** @description Required if Authorization is a JWT token. The organization ID to operate within. */ + "organization-id": string; + }; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The tracking tag has been successfully deleted. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; ExchangeRatesController_getLatestExchangeRate: { parameters: { query?: { diff --git a/shared/sdk-ts/src/tracking-tags.ts b/shared/sdk-ts/src/tracking-tags.ts new file mode 100644 index 000000000..f9d79f042 --- /dev/null +++ b/shared/sdk-ts/src/tracking-tags.ts @@ -0,0 +1,57 @@ +import type { ApiFetcher } from './fetch-utils'; +import { paths } from './schema'; +import { OpForPath, OpQueryParams, OpRequestBody, OpResponseBody } from './utils'; + +export const TRACKING_TAGS_ROUTES = { + LIST: '/api/tracking-tags', + BY_ID: '/api/tracking-tags/{id}', +} as const satisfies Record; + +export type TrackingTagsList = OpResponseBody>; +export type TrackingTag = OpResponseBody>; +export type CreateTrackingTagBody = OpRequestBody>; +export type EditTrackingTagBody = OpRequestBody>; + +export async function fetchTrackingTags( + fetcher: ApiFetcher, +): Promise { + const get = fetcher.path(TRACKING_TAGS_ROUTES.LIST).method('get').create(); + const { data } = await get({}); + return data; +} + +export async function fetchTrackingTag( + fetcher: ApiFetcher, + id: number, +): Promise { + const get = fetcher.path(TRACKING_TAGS_ROUTES.BY_ID).method('get').create(); + const { data } = await get({ id }); + return data; +} + +export async function createTrackingTag( + fetcher: ApiFetcher, + values: CreateTrackingTagBody, +): Promise { + const post = fetcher.path(TRACKING_TAGS_ROUTES.LIST).method('post').create(); + const { data } = await post(values); + return data; +} + +export async function editTrackingTag( + fetcher: ApiFetcher, + id: number, + values: EditTrackingTagBody, +): Promise { + const put = fetcher.path(TRACKING_TAGS_ROUTES.BY_ID).method('put').create(); + const { data } = await put({ id, ...values }); + return data; +} + +export async function deleteTrackingTag( + fetcher: ApiFetcher, + id: number, +): Promise { + const del = fetcher.path(TRACKING_TAGS_ROUTES.BY_ID).method('delete').create(); + await del({ id }); +}