1
0

feat(server): implement tracking tags for GL entries and reporting

- Add tracking tags infrastructure with 5 database tables
- Create TrackingTags module with CRUD API endpoints
- Associate tags with invoice/bill line items and manual journal entries
- Propagate tags to GL entries (accounts_transactions) via ledger storage
- Add tracking tag filtering to all 12 GL-dependent financial reports:
  - General Ledger, Balance Sheet, Profit & Loss
  - Trial Balance, Journal Sheet, Cash Flow Statement
  - Customer/Vendor Balance Summary
  - Transactions by Customer/Vendor/Reference
  - Sales Tax Liability Summary
- Add filterByTrackingTags query modifier to AccountTransaction model
- Add sdk-ts types and fetch functions for tracking tags
- Add React hooks (useTrackingTags, useCreateTrackingTag, etc.)
- TypeScript typecheck passes across all packages
This commit is contained in:
Ahmed Bouhuolia
2026-04-19 00:29:47 +02:00
parent 2ec3ca8d33
commit 0d687018f8
66 changed files with 2104 additions and 11 deletions
@@ -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');
};
@@ -230,6 +230,23 @@ export class AccountTransaction extends BaseModel {
query.where('reference_id', referenceId); query.where('reference_id', referenceId);
query.where('reference_type', referenceType); 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),
);
},
}; };
} }
@@ -99,6 +99,7 @@ import { UsersModule } from '../UsersModule/Users.module';
import { ContactsModule } from '../Contacts/Contacts.module'; import { ContactsModule } from '../Contacts/Contacts.module';
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module'; import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
import { CustomFieldsModule } from '../CustomFields/CustomFields.module'; import { CustomFieldsModule } from '../CustomFields/CustomFields.module';
import { TrackingTagsModule } from '../TrackingTags/TrackingTags.module';
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module'; import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
import { ExchangeRatesModule } from '../ExchangeRates/ExchangeRates.module'; import { ExchangeRatesModule } from '../ExchangeRates/ExchangeRates.module';
import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module'; import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module';
@@ -258,6 +259,7 @@ import { AppThrottleModule } from './AppThrottle.module';
UsersModule, UsersModule,
ContactsModule, ContactsModule,
CustomFieldsModule, CustomFieldsModule,
TrackingTagsModule,
SocketModule, SocketModule,
ExchangeRatesModule, ExchangeRatesModule,
], ],
@@ -11,6 +11,7 @@ import {
} from 'class-validator'; } from 'class-validator';
import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto'; import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto { export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
@ApiProperty({ @ApiProperty({
@@ -173,4 +174,12 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
@Transform(({ value }) => parseBoolean(value, false)) @Transform(({ value }) => parseBoolean(value, false))
@IsOptional() @IsOptional()
previousYearPercentageChange: boolean; previousYearPercentageChange: boolean;
@ApiProperty({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
required: false,
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -5,6 +5,7 @@ import {
INumberFormatQuery, INumberFormatQuery,
} from '../../types/Report.types'; } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types'; import { IFinancialTable } from '../../types/Table.types';
import { ITrackingTagFilter } from '../../types/TrackingTagFilter.types';
// Balance sheet schema nodes types. // Balance sheet schema nodes types.
export enum BALANCE_SHEET_SCHEMA_NODE_TYPE { export enum BALANCE_SHEET_SCHEMA_NODE_TYPE {
@@ -63,6 +64,8 @@ export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery {
previousYear: boolean; previousYear: boolean;
previousYearAmountChange: boolean; previousYearAmountChange: boolean;
previousYearPercentageChange: boolean; previousYearPercentageChange: boolean;
trackingTags?: ITrackingTagFilter[];
} }
// Balance sheet meta. // Balance sheet meta.
@@ -401,5 +401,8 @@ export class BalanceSheetRepository extends R.compose(
if (!isEmpty(this.query.branchesIds)) { if (!isEmpty(this.query.branchesIds)) {
query.modify('filterByBranches', this.query.branchesIds); query.modify('filterByBranches', this.query.branchesIds);
} }
if (!isEmpty(this.query.trackingTags)) {
query.modify('filterByTrackingTags', this.query.trackingTags);
}
}; };
} }
@@ -173,5 +173,8 @@ export class CashFlowRepository {
if (!isEmpty(query.branchesIds)) { if (!isEmpty(query.branchesIds)) {
knexQuery.modify('filterByBranches', query.branchesIds); knexQuery.modify('filterByBranches', query.branchesIds);
} }
if (!isEmpty(query.trackingTags)) {
knexQuery.modify('filterByTrackingTags', query.trackingTags);
}
}; };
} }
@@ -11,6 +11,7 @@ import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberF
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { parseBoolean } from '@/utils/parse-boolean'; import { parseBoolean } from '@/utils/parse-boolean';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto { export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
@ApiProperty({ @ApiProperty({
@@ -92,4 +93,12 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
basis: string; basis: string;
@ApiProperty({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
required: false,
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -16,6 +16,7 @@ export interface ICashFlowStatementQuery {
basis: string; basis: string;
branchesIds?: number[]; branchesIds?: number[];
trackingTags?: Array<{ tagId: number; optionId?: number }>;
} }
export interface ICashFlowStatementTotal { export interface ICashFlowStatementTotal {
@@ -1,6 +1,7 @@
import { IsArray, IsOptional } from 'class-validator'; import { IsArray, IsOptional } from 'class-validator';
import { ContactBalanceSummaryQueryDto } from '../ContactBalanceSummary/ContactBalanceSummaryQuery.dto'; import { ContactBalanceSummaryQueryDto } from '../ContactBalanceSummary/ContactBalanceSummaryQuery.dto';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class CustomerBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto { export class CustomerBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
@@ -11,4 +12,11 @@ export class CustomerBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDt
@IsArray() @IsArray()
@IsOptional() @IsOptional()
customersIds: number[]; customersIds: number[];
@ApiPropertyOptional({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -56,6 +56,7 @@ export class CustomerBalanceSummaryRepository {
*/ */
public async getCustomersTransactions( public async getCustomersTransactions(
asDate: any, asDate: any,
trackingTags?: Array<{ tagId: number; optionId?: number }>,
): Promise<ModelObject<AccountTransaction>[]> { ): Promise<ModelObject<AccountTransaction>[]> {
// Retrieve the receivable accounts A/R. // Retrieve the receivable accounts A/R.
const receivableAccounts = await this.getReceivableAccounts(); const receivableAccounts = await this.getReceivableAccounts();
@@ -67,6 +68,9 @@ export class CustomerBalanceSummaryRepository {
.onBuild((query) => { .onBuild((query) => {
query.whereIn('accountId', receivableAccountsIds); query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate); query.modify('filterDateRange', null, asDate);
if (!isEmpty(trackingTags)) {
query.modify('filterByTrackingTags', trackingTags);
}
query.groupBy('contactId'); query.groupBy('contactId');
query.sum('credit as credit'); query.sum('credit as credit');
query.sum('debit as debit'); query.sum('debit as debit');
@@ -1,5 +1,6 @@
import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types"; import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types";
import { IFinancialTable } from "../../types/Table.types"; import { IFinancialTable } from "../../types/Table.types";
import { ITrackingTagFilter } from "../../types/TrackingTagFilter.types";
export interface IGeneralLedgerSheetQuery { export interface IGeneralLedgerSheetQuery {
fromDate: Date | string; fromDate: Date | string;
@@ -10,6 +11,19 @@ export interface IGeneralLedgerSheetQuery {
noneTransactions: boolean; noneTransactions: boolean;
accountsIds: number[]; accountsIds: number[];
branchesIds?: 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{ export interface IGeneralLedgerNumberFormat extends INumberFormatQuery{
@@ -119,6 +119,9 @@ export class GeneralLedgerRepository {
if (!isEmpty(this.filter.branchesIds)) { if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', this.filter.branchesIds); query.modify('filterByBranches', this.filter.branchesIds);
} }
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
query.orderBy('date', 'ASC'); query.orderBy('date', 'ASC');
if (this.filter.accountsIds?.length > 0) { if (this.filter.accountsIds?.length > 0) {
@@ -146,6 +149,9 @@ export class GeneralLedgerRepository {
if (!isEmpty(this.filter.branchesIds)) { if (!isEmpty(this.filter.branchesIds)) {
query.modify('filterByBranches', this.filter.branchesIds); query.modify('filterByBranches', this.filter.branchesIds);
} }
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
query.withGraphFetched('account'); query.withGraphFetched('account');
}); });
// Accounts opening transactions. // Accounts opening transactions.
@@ -10,6 +10,7 @@ import {
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto'; import { FinancialSheetBranchesQueryDto } from '../../dtos/FinancialSheetBranchesQuery.dto';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
class JournalSheetNumberFormatQueryDto { class JournalSheetNumberFormatQueryDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
@@ -93,4 +94,11 @@ export class JournalSheetQueryDto extends FinancialSheetBranchesQueryDto {
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
toRange: number; toRange: number;
@ApiPropertyOptional({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -6,6 +6,7 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { transformToMap } from '@/utils/transform-to-key'; import { transformToMap } from '@/utils/transform-to-key';
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { isEmpty } from 'lodash';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
export class JournalSheetRepository { export class JournalSheetRepository {
@@ -112,6 +113,9 @@ export class JournalSheetRepository {
if (this.filter.transactionType && this.filter.transactionId) { if (this.filter.transactionType && this.filter.transactionId) {
query.where('reference_id', this.filter.transactionId); query.where('reference_id', this.filter.transactionId);
} }
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
query.withGraphFetched('account'); query.withGraphFetched('account');
}); });
this.accountTransactions = transactions; this.accountTransactions = transactions;
@@ -5,6 +5,7 @@ import {
INumberFormatQuery, INumberFormatQuery,
} from '../../types/Report.types'; } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types'; import { IFinancialTable } from '../../types/Table.types';
import { ITrackingTagFilter } from '../../types/TrackingTagFilter.types';
export enum ProfitLossAggregateNodeId { export enum ProfitLossAggregateNodeId {
INCOME = 'INCOME', INCOME = 'INCOME',
@@ -86,6 +87,8 @@ export interface IProfitLossSheetQuery extends IFinancialSheetBranchesQuery {
previousYear: boolean; previousYear: boolean;
previousYearAmountChange: boolean; previousYearAmountChange: boolean;
previousYearPercentageChange: boolean; previousYearPercentageChange: boolean;
trackingTags?: ITrackingTagFilter[];
} }
export interface IProfitLossSheetTotal { export interface IProfitLossSheetTotal {
@@ -14,6 +14,7 @@ import { Transform, Type } from 'class-transformer';
import { ToNumber } from '@/common/decorators/Validators'; import { ToNumber } from '@/common/decorators/Validators';
import { parseBoolean } from '@/utils/parse-boolean'; import { parseBoolean } from '@/utils/parse-boolean';
import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto'; import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto { export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
@IsString() @IsString()
@@ -136,4 +137,11 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
description: 'Whether to show previous year percentage change', description: 'Whether to show previous year percentage change',
}) })
previousYearPercentageChange: boolean; previousYearPercentageChange: boolean;
@ApiPropertyOptional({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -363,6 +363,9 @@ export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
if (!isEmpty(this.query.query.branchesIds)) { if (!isEmpty(this.query.query.branchesIds)) {
query.modify('filterByBranches', this.query.query.branchesIds); query.modify('filterByBranches', this.query.query.branchesIds);
} }
if (!isEmpty(this.query.query.trackingTags)) {
query.modify('filterByTrackingTags', this.query.query.trackingTags);
}
}; };
/** /**
@@ -31,4 +31,5 @@ export interface ITransactionsByContactsFilter {
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
noneTransactions: boolean; noneTransactions: boolean;
noneZero: boolean; noneZero: boolean;
trackingTags?: Array<{ tagId: number; optionId?: number }>;
} }
@@ -1,8 +1,17 @@
import { IsArray, IsOptional } from 'class-validator'; import { IsArray, IsOptional } from 'class-validator';
import { TransactionsByContactQueryDto } from '../TransactionsByContact/TransactionsByContactQuery.dto'; import { TransactionsByContactQueryDto } from '../TransactionsByContact/TransactionsByContactQuery.dto';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class TransactionsByCustomerQueryDto extends TransactionsByContactQueryDto { export class TransactionsByCustomerQueryDto extends TransactionsByContactQueryDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
customersIds: number[]; customersIds: number[];
@ApiPropertyOptional({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -236,7 +236,12 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
openingDate, openingDate,
receivableAccountsIds, receivableAccountsIds,
customersIds, customersIds,
); )
.onBuild((query) => {
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
});
return openingTransactions; return openingTransactions;
} }
@@ -264,6 +269,10 @@ export class TransactionsByCustomersRepository extends TransactionsByContactRepo
// Filter by accounts. // Filter by accounts.
query.whereIn('accountId', receivableAccountsIds); query.whereIn('accountId', receivableAccountsIds);
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
}); });
return transactions; return transactions;
} }
@@ -1,5 +1,6 @@
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class TransactionsByReferenceQueryDto { export class TransactionsByReferenceQueryDto {
@IsString() @IsString()
@@ -19,4 +20,12 @@ export class TransactionsByReferenceQueryDto {
required: true, required: true,
}) })
referenceId: number; referenceId: number;
@ApiProperty({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
required: false,
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -2,6 +2,7 @@ import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { isEmpty } from 'lodash';
@Injectable() @Injectable()
export class TransactionsByReferenceRepository { export class TransactionsByReferenceRepository {
@@ -21,12 +22,18 @@ export class TransactionsByReferenceRepository {
public async getTransactions( public async getTransactions(
referenceId: number, referenceId: number,
referenceType: string, referenceType: string,
trackingTags?: Array<{ tagId: number; optionId?: number }>,
): Promise<Array<ModelObject<AccountTransaction>>> { ): Promise<Array<ModelObject<AccountTransaction>>> {
return this.accountTransactionModel() return this.accountTransactionModel()
.query() .query()
.skipUndefined() .skipUndefined()
.where('reference_id', referenceId) .where('reference_id', referenceId)
.where('reference_type', referenceType) .where('reference_type', referenceType)
.onBuild((query) => {
if (!isEmpty(trackingTags)) {
query.modify('filterByTrackingTags', trackingTags);
}
})
.withGraphFetched('account'); .withGraphFetched('account');
} }
} }
@@ -1,6 +1,7 @@
import { IsArray, IsOptional } from 'class-validator'; import { IsArray, IsOptional } from 'class-validator';
import { TransactionsByContactQueryDto } from '../TransactionsByContact/TransactionsByContactQuery.dto'; import { TransactionsByContactQueryDto } from '../TransactionsByContact/TransactionsByContactQuery.dto';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class TransactionsByVendorQueryDto extends TransactionsByContactQueryDto { export class TransactionsByVendorQueryDto extends TransactionsByContactQueryDto {
@IsArray() @IsArray()
@@ -10,4 +11,11 @@ export class TransactionsByVendorQueryDto extends TransactionsByContactQueryDto
example: [1, 2, 3], example: [1, 2, 3],
}) })
vendorsIds: number[]; vendorsIds: number[];
@ApiPropertyOptional({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -242,7 +242,12 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
openingDate, openingDate,
payableAccountsIds, payableAccountsIds,
customersIds, customersIds,
); )
.onBuild((query) => {
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
});
return openingTransactions; return openingTransactions;
} }
@@ -270,6 +275,10 @@ export class TransactionsByVendorRepository extends TransactionsByContactReposit
// Filter by accounts. // Filter by accounts.
query.whereIn('accountId', receivableAccountsIds); query.whereIn('accountId', receivableAccountsIds);
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
}); });
return transactions; return transactions;
} }
@@ -1,5 +1,6 @@
import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types"; import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types";
import { IFinancialTable } from "../../types/Table.types"; import { IFinancialTable } from "../../types/Table.types";
import { ITrackingTagFilter } from "../../types/TrackingTagFilter.types";
export interface ITrialBalanceSheetQuery { export interface ITrialBalanceSheetQuery {
fromDate: Date | string; fromDate: Date | string;
@@ -11,6 +12,7 @@ export interface ITrialBalanceSheetQuery {
onlyActive: boolean; onlyActive: boolean;
accountIds: number[]; accountIds: number[];
branchesIds?: number[]; branchesIds?: number[];
trackingTags?: ITrackingTagFilter[];
} }
export interface ITrialBalanceTotal { export interface ITrialBalanceTotal {
@@ -12,6 +12,7 @@ import {
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { parseBoolean } from '@/utils/parse-boolean'; import { parseBoolean } from '@/utils/parse-boolean';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class TrialBalanceSheetQueryDto extends FinancialSheetBranchesQueryDto { export class TrialBalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
@ApiProperty({ @ApiProperty({
@@ -92,4 +93,12 @@ export class TrialBalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
accountIds: number[]; accountIds: number[];
@ApiProperty({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
required: false,
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -111,5 +111,9 @@ export class TrialBalanceSheetRepository {
// @ts-ignore // @ts-ignore
query.modify('filterByBranches', this.query.branchesIds); query.modify('filterByBranches', this.query.branchesIds);
} }
if (!isEmpty(this.query.trackingTags)) {
// @ts-ignore
query.modify('filterByTrackingTags', this.query.trackingTags);
}
}; };
} }
@@ -11,6 +11,7 @@ export interface IVendorBalanceSummaryQuery {
percentageColumn: boolean; percentageColumn: boolean;
noneTransactions: boolean; noneTransactions: boolean;
noneZero: boolean; noneZero: boolean;
trackingTags?: Array<{ tagId: number; optionId?: number }>;
} }
export interface IVendorBalanceSummaryAmount { export interface IVendorBalanceSummaryAmount {
@@ -1,6 +1,7 @@
import { IsArray, IsOptional } from 'class-validator'; import { IsArray, IsOptional } from 'class-validator';
import { ContactBalanceSummaryQueryDto } from '../ContactBalanceSummary/ContactBalanceSummaryQuery.dto'; import { ContactBalanceSummaryQueryDto } from '../ContactBalanceSummary/ContactBalanceSummaryQuery.dto';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class VendorBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto { export class VendorBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto {
@IsArray() @IsArray()
@@ -11,4 +12,11 @@ export class VendorBalanceSummaryQueryDto extends ContactBalanceSummaryQueryDto
example: [1, 2, 3], example: [1, 2, 3],
}) })
vendorsIds: number[]; vendorsIds: number[];
@ApiPropertyOptional({
description: 'Tracking tags to filter the report',
type: [TrackingTagAssignmentDto],
})
@IsOptional()
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -138,6 +138,9 @@ export class VendorBalanceSummaryRepository {
.onBuild((query) => { .onBuild((query) => {
query.whereIn('accountId', payableAccountsIds); query.whereIn('accountId', payableAccountsIds);
query.modify('filterDateRange', null, asDate); query.modify('filterDateRange', null, asDate);
if (!isEmpty(this.filter.trackingTags)) {
query.modify('filterByTrackingTags', this.filter.trackingTags);
}
query.groupBy('contactId'); query.groupBy('contactId');
query.sum('credit as credit'); query.sum('credit as credit');
query.sum('debit as debit'); query.sum('debit as debit');
@@ -0,0 +1,4 @@
export interface ITrackingTagFilter {
tagId: number;
optionId?: number;
}
@@ -8,6 +8,7 @@ import {
} from './types/Ledger.types'; } from './types/Ledger.types';
import { ILedger } from './types/Ledger.types'; import { ILedger } from './types/Ledger.types';
import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; import { AccountTransaction } from '../Accounts/models/AccountTransaction.model';
import { AccountTransactionTrackingTag } from '../TrackingTags/models/AccountTransactionTrackingTag';
import { TenantModelProxy } from '../System/models/TenantBaseModel'; import { TenantModelProxy } from '../System/models/TenantBaseModel';
// Filter the blank entries. // Filter the blank entries.
@@ -24,6 +25,11 @@ export class LedgerEntriesStorageService {
private readonly accountTransactionModel: TenantModelProxy< private readonly accountTransactionModel: TenantModelProxy<
typeof AccountTransaction typeof AccountTransaction
>, >,
@Inject(AccountTransactionTrackingTag.name)
private readonly accountTransactionTrackingTagModel: TenantModelProxy<
typeof AccountTransactionTrackingTag
>,
) {} ) {}
/** /**
@@ -71,7 +77,22 @@ export class LedgerEntriesStorageService {
): Promise<void> => { ): Promise<void> => {
const transaction = transformLedgerEntryToTransaction(entry); 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);
}
}; };
/** /**
@@ -69,6 +69,8 @@ export interface ILedgerEntry {
createdAt?: Date | string; createdAt?: Date | string;
costable?: boolean; costable?: boolean;
trackingTags?: Array<{ tagId: number; optionId: number }>;
} }
export interface ISaveLedgerEntryQueuePayload { export interface ISaveLedgerEntryQueuePayload {
@@ -56,6 +56,18 @@ export class CreateManualJournalService {
const authorizedUser = await this.tenancyContext.getSystemUser(); const authorizedUser = await this.tenancyContext.getSystemUser();
const entries = R.compose( 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. // Associate the default index to each item entry.
assocItemEntriesDefaultIndex, assocItemEntriesDefaultIndex,
)(manualJournalDTO.entries); )(manualJournalDTO.entries);
@@ -68,6 +68,18 @@ export class EditManualJournal {
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); 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 { return {
id: oldManualJournal.id, id: oldManualJournal.id,
...omit(manualJournalDTO, ['publish', 'attachments']), ...omit(manualJournalDTO, ['publish', 'attachments']),
@@ -76,6 +88,7 @@ export class EditManualJournal {
: {}), : {}),
amount, amount,
date, date,
entries,
}; };
}; };
@@ -52,6 +52,11 @@ export class ManualJournalGL {
public getManualJournalEntry(entry: ManualJournalEntry): ILedgerEntry { public getManualJournalEntry(entry: ManualJournalEntry): ILedgerEntry {
const commonEntry = this.manualJournalCommonEntry; const commonEntry = this.manualJournalCommonEntry;
const trackingTags = entry.trackingTagAssociations?.map((assoc) => ({
tagId: assoc.tagId,
optionId: assoc.optionId,
}));
return { return {
...commonEntry, ...commonEntry,
debit: entry.debit, debit: entry.debit,
@@ -66,6 +71,7 @@ export class ManualJournalGL {
branchId: entry.branchId, branchId: entry.branchId,
projectId: entry.projectId, projectId: entry.projectId,
trackingTags,
}; };
} }
@@ -16,6 +16,7 @@ import {
Min, Min,
ValidateNested, ValidateNested,
} from 'class-validator'; } from 'class-validator';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class ManualJournalEntryDto { export class ManualJournalEntryDto {
@ApiProperty({ description: 'Entry index' }) @ApiProperty({ description: 'Entry index' })
@@ -63,6 +64,17 @@ export class ManualJournalEntryDto {
@IsOptional() @IsOptional()
@IsInt() @IsInt()
projectId?: number; 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 { class AttachmentDto {
@@ -18,6 +18,7 @@ export class ManualJournalEntry extends BaseModel {
contact?: Contact; contact?: Contact;
account?: Account; account?: Account;
branch?: Branch; branch?: Branch;
trackingTagAssociations?: any[];
/** /**
* Table name. * Table name.
@@ -40,6 +41,7 @@ export class ManualJournalEntry extends BaseModel {
const { Account } = require('../../Accounts/models/Account.model'); const { Account } = require('../../Accounts/models/Account.model');
const { Contact } = require('../../Contacts/models/Contact'); const { Contact } = require('../../Contacts/models/Contact');
const { Branch } = require('../../Branches/models/Branch.model'); const { Branch } = require('../../Branches/models/Branch.model');
const { ManualJournalEntryTrackingTag } = require('../../TrackingTags/models/ManualJournalEntryTrackingTag');
return { return {
account: { account: {
@@ -66,6 +68,15 @@ export class ManualJournalEntry extends BaseModel {
to: 'branches.id', to: 'branches.id',
}, },
}, },
trackingTagAssociations: {
relation: Model.HasManyRelation,
modelClass: ManualJournalEntryTrackingTag,
join: {
from: 'manual_journals_entries.id',
to: 'manual_journal_entry_tracking_tags.manualJournalEntryId',
},
},
}; };
} }
} }
@@ -62,6 +62,7 @@ export enum AbilitySubject {
Project = 'Project', Project = 'Project',
TaxRate = 'TaxRate', TaxRate = 'TaxRate',
CustomField = 'CustomField', CustomField = 'CustomField',
TrackingTag = 'TrackingTag',
} }
export interface IRoleCreatedPayload { export interface IRoleCreatedPayload {
@@ -91,6 +91,19 @@ export class CommandSaleInvoiceDTOTransformer {
// Remove tax code from entries. // Remove tax code from entries.
R.map(R.omit(['taxCode'])), 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. // Associate the default index for each item entry lin.
assocItemEntriesDefaultIndex, assocItemEntriesDefaultIndex,
)(asyncEntries); )(asyncEntries);
@@ -109,6 +109,11 @@ export class InvoiceGL {
const localAmount = const localAmount =
entry.totalExcludingTax * this.saleInvoice.exchangeRate; entry.totalExcludingTax * this.saleInvoice.exchangeRate;
const trackingTags = entry.trackingTagAssociations?.map((assoc) => ({
tagId: assoc.tagId,
optionId: assoc.optionId,
}));
return { return {
...commonEntry, ...commonEntry,
credit: localAmount, credit: localAmount,
@@ -119,6 +124,7 @@ export class InvoiceGL {
accountNormal: AccountNormal.CREDIT, accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId, taxRateId: entry.taxRateId,
taxRate: entry.taxRate, taxRate: entry.taxRate,
trackingTags,
}; };
}, },
); );
@@ -40,6 +40,11 @@ import { PaymentReceived } from '@/modules/PaymentReceived/models/PaymentReceive
import { Model } from 'objection'; import { Model } from 'objection';
import { ClsModule } from 'nestjs-cls'; import { ClsModule } from 'nestjs-cls';
import { TenantUser } from './models/TenantUser.model'; 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 = [ const models = [
Item, Item,
@@ -80,6 +85,11 @@ const models = [
PaymentReceived, PaymentReceived,
PaymentReceivedEntry, PaymentReceivedEntry,
TenantUser, TenantUser,
TrackingTag,
TrackingTagOption,
ItemEntryTrackingTag,
ManualJournalEntryTrackingTag,
AccountTransactionTrackingTag,
]; ];
/** /**
@@ -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<TrackingTag>}
*/
public createTrackingTag = (dto: CreateTrackingTagDto): Promise<TrackingTag> => {
return this.createService.createTrackingTag(dto);
};
/**
* Edits a tracking tag.
* @param {number} tagId
* @param {EditTrackingTagDto} dto
* @returns {Promise<TrackingTag>}
*/
public editTrackingTag = (tagId: number, dto: EditTrackingTagDto): Promise<TrackingTag> => {
return this.editService.editTrackingTag(tagId, dto);
};
/**
* Deletes a tracking tag.
* @param {number} tagId
* @returns {Promise<void>}
*/
public deleteTrackingTag = (tagId: number): Promise<void> => {
return this.deleteService.deleteTrackingTag(tagId);
};
/**
* Retrieves all tracking tags.
* @returns {Promise<TrackingTag[]>}
*/
public getTrackingTags = (): Promise<TrackingTag[]> => {
return this.getTagsService.getTrackingTags();
};
/**
* Retrieves a tracking tag by ID.
* @param {number} tagId
* @returns {Promise<TrackingTag>}
*/
public getTrackingTag = (tagId: number): Promise<TrackingTag> => {
return this.getTagService.getTrackingTag(tagId);
};
}
@@ -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);
}
}
@@ -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 {}
@@ -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<typeof TrackingTag>,
) {}
/**
* Creates a new tracking tag with options.
* @param {CreateTrackingTagDto} dto
* @returns {Promise<TrackingTag>}
*/
public createTrackingTag = async (dto: CreateTrackingTagDto): Promise<TrackingTag> => {
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;
};
}
@@ -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<typeof TrackingTag>,
) {}
/**
* Deletes a tracking tag.
* @param {number} tagId
* @returns {Promise<void>}
*/
public deleteTrackingTag = async (tagId: number): Promise<void> => {
const tag = await this.trackingTagModel()
.query()
.findById(tagId)
.throwIfNotFound();
await this.trackingTagModel().query().deleteById(tagId);
};
}
@@ -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<typeof TrackingTag>,
) {}
/**
* Edits a tracking tag.
* @param {number} tagId
* @param {EditTrackingTagDto} dto
* @returns {Promise<TrackingTag>}
*/
public editTrackingTag = async (
tagId: number,
dto: EditTrackingTagDto,
): Promise<TrackingTag> => {
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;
};
}
@@ -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[];
}
@@ -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;
}
@@ -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',
},
},
};
}
}
@@ -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',
},
},
};
}
}
@@ -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',
},
},
};
}
}
@@ -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);
},
},
};
}
}
@@ -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',
},
},
};
}
}
@@ -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<typeof TrackingTag>,
) {}
/**
* Retrieves a tracking tag by ID with its options.
* @param {number} tagId
* @returns {Promise<TrackingTag>}
*/
public getTrackingTag = async (tagId: number): Promise<TrackingTag> => {
const tag = await this.trackingTagModel()
.query()
.findById(tagId)
.withGraphFetched('options');
if (!tag) {
throw new NotFoundException('Tracking tag not found.');
}
return tag;
};
}
@@ -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<typeof TrackingTag>,
) {}
/**
* Retrieves all tracking tags with their options.
* @returns {Promise<TrackingTag[]>}
*/
public getTrackingTags = async (): Promise<TrackingTag[]> => {
return this.trackingTagModel()
.query()
.withGraphFetched('options');
};
}
@@ -2,6 +2,7 @@ import { ToNumber } from '@/common/decorators/Validators';
import { DiscountType } from '@/common/types/Discount'; import { DiscountType } from '@/common/types/Discount';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { import {
IsArray,
IsEnum, IsEnum,
IsIn, IsIn,
IsInt, IsInt,
@@ -9,7 +10,10 @@ import {
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
ValidateNested,
} from 'class-validator'; } from 'class-validator';
import { Type } from 'class-transformer';
import { TrackingTagAssignmentDto } from '@/modules/TrackingTags/dtos/AssignTrackingTags.dto';
export class ItemEntryDto { export class ItemEntryDto {
@IsInt() @IsInt()
@@ -153,4 +157,15 @@ export class ItemEntryDto {
example: 1021, example: 1021,
}) })
costAccountId?: number; costAccountId?: number;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => TrackingTagAssignmentDto)
@ApiProperty({
description: 'The tracking tags of the item entry',
type: [TrackingTagAssignmentDto],
required: false,
})
trackingTags?: TrackingTagAssignmentDto[];
} }
@@ -39,6 +39,7 @@ export class ItemEntry extends BaseModel {
item: Item; item: Item;
allocatedCostEntries: BillLandedCostEntry[]; allocatedCostEntries: BillLandedCostEntry[];
trackingTagAssociations: any[];
/** /**
* Table name. * Table name.
@@ -190,6 +191,7 @@ export class ItemEntry extends BaseModel {
const { SaleReceipt } = require('../../SaleReceipts/models/SaleReceipt'); const { SaleReceipt } = require('../../SaleReceipts/models/SaleReceipt');
const { SaleEstimate } = require('../../SaleEstimates/models/SaleEstimate'); const { SaleEstimate } = require('../../SaleEstimates/models/SaleEstimate');
const { TaxRateModel } = require('../../TaxRates/models/TaxRate.model'); const { TaxRateModel } = require('../../TaxRates/models/TaxRate.model');
const { ItemEntryTrackingTag } = require('../../TrackingTags/models/ItemEntryTrackingTag');
// const { Expense } = require('../../Expenses/models/Expense.model'); // const { Expense } = require('../../Expenses/models/Expense.model');
// const ProjectTask = require('models/Task'); // const ProjectTask = require('models/Task');
@@ -297,6 +299,18 @@ export class ItemEntry extends BaseModel {
to: 'tax_rates.id', to: 'tax_rates.id',
}, },
}, },
/**
* Tracking tag associations.
*/
trackingTagAssociations: {
relation: Model.HasManyRelation,
modelClass: ItemEntryTrackingTag,
join: {
from: 'items_entries.id',
to: 'item_entry_tracking_tags.itemEntryId',
},
},
}; };
} }
} }
@@ -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,
});
}
@@ -250,6 +250,11 @@ const CUSTOM_FIELDS = {
CUSTOM_FIELD: 'CUSTOM_FIELD', CUSTOM_FIELD: 'CUSTOM_FIELD',
}; };
const TRACKING_TAGS = {
TRACKING_TAGS: 'TRACKING_TAGS',
TRACKING_TAG: 'TRACKING_TAG',
};
export default { export default {
...Authentication, ...Authentication,
...ACCOUNTS, ...ACCOUNTS,
@@ -287,4 +292,5 @@ export default {
...EXCHANGE_RATE, ...EXCHANGE_RATE,
...API_KEYS, ...API_KEYS,
...CUSTOM_FIELDS, ...CUSTOM_FIELDS,
...TRACKING_TAGS,
}; };
+533
View File
@@ -15341,6 +15341,26 @@
"type": "boolean" "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", "name": "accept",
"required": true, "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", "name": "accept",
"required": true, "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", "name": "accept",
"required": true, "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", "name": "accept",
"required": true, "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", "name": "accept",
"required": true, "required": true,
@@ -19741,6 +19841,26 @@
"type": "boolean" "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", "name": "accept",
"required": true, "required": true,
@@ -19796,6 +19916,26 @@
"example": "1", "example": "1",
"type": "number" "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": { "responses": {
@@ -21099,6 +21239,26 @@
"type": "number" "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", "name": "accept",
"required": true, "required": true,
@@ -21644,6 +21804,26 @@
"type": "boolean" "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", "name": "accept",
"required": true, "required": true,
@@ -22077,6 +22257,26 @@
"type": "string" "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", "name": "accept",
"required": true, "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 <token>' where <token> 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 <token>' where <token> 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 <token>' where <token> 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 <token>' where <token> 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 <token>' where <token> 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": { "/api/exchange-rates/latest": {
"get": { "get": {
"operationId": "ExchangeRatesController_getLatestExchangeRate", "operationId": "ExchangeRatesController_getLatestExchangeRate",
@@ -26480,6 +26893,25 @@
"defaultTemplateId" "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": { "ItemEntryDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -26562,6 +26994,13 @@
"type": "number", "type": "number",
"description": "The cost account id of the item entry", "description": "The cost account id of the item entry",
"example": 1021 "example": 1021
},
"trackingTags": {
"description": "The tracking tags of the item entry",
"type": "array",
"items": {
"$ref": "#/components/schemas/TrackingTagAssignmentDto"
}
} }
}, },
"required": [ "required": [
@@ -31659,6 +32098,13 @@
"description": "The cost account id of the item entry", "description": "The cost account id of the item entry",
"example": 1021 "example": 1021
}, },
"trackingTags": {
"description": "The tracking tags of the item entry",
"type": "array",
"items": {
"$ref": "#/components/schemas/TrackingTagAssignmentDto"
}
},
"landedCost": { "landedCost": {
"type": "boolean", "type": "boolean",
"description": "Flag indicating whether the entry contributes to landed cost", "description": "Flag indicating whether the entry contributes to landed cost",
@@ -32131,6 +32577,13 @@
"projectId": { "projectId": {
"type": "number", "type": "number",
"description": "Project ID" "description": "Project ID"
},
"trackingTags": {
"description": "The tracking tags of the manual journal entry",
"type": "array",
"items": {
"$ref": "#/components/schemas/TrackingTagAssignmentDto"
}
} }
}, },
"required": [ "required": [
@@ -40639,6 +41092,86 @@
"password" "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": { "ExchangeRateLatestResponseDto": {
"type": "object", "type": "object",
"properties": { "properties": {
+1
View File
@@ -20,6 +20,7 @@ export * from './import';
export * from './manual-journals'; export * from './manual-journals';
export * from './roles'; export * from './roles';
export * from './custom-fields'; export * from './custom-fields';
export * from './tracking-tags';
export * from './users'; export * from './users';
export * from './dashboard'; export * from './dashboard';
export * from './settings'; export * from './settings';
+283 -7
View File
@@ -4714,6 +4714,43 @@ export interface paths {
patch: operations["ContactsController_inactivateContact"]; patch: operations["ContactsController_inactivateContact"];
trace?: never; 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": { "/api/exchange-rates/latest": {
parameters: { parameters: {
query?: never; query?: never;
@@ -6349,6 +6386,18 @@ export interface components {
*/ */
defaultTemplateId: number | null; defaultTemplateId: number | null;
}; };
TrackingTagAssignmentDto: {
/**
* @description Tag ID
* @example 1
*/
tagId: number;
/**
* @description Option ID
* @example 5
*/
optionId: number;
};
ItemEntryDto: { ItemEntryDto: {
/** /**
* @description The index of the item entry * @description The index of the item entry
@@ -6430,6 +6479,8 @@ export interface components {
* @example 1021 * @example 1021
*/ */
costAccountId: number; costAccountId: number;
/** @description The tracking tags of the item entry */
trackingTags?: components["schemas"]["TrackingTagAssignmentDto"][];
}; };
AttachmentLinkDto: Record<string, never>; AttachmentLinkDto: Record<string, never>;
PaymentMethodDto: { PaymentMethodDto: {
@@ -10050,6 +10101,8 @@ export interface components {
* @example 1021 * @example 1021
*/ */
costAccountId: number; costAccountId: number;
/** @description The tracking tags of the item entry */
trackingTags?: components["schemas"]["TrackingTagAssignmentDto"][];
/** /**
* @description Flag indicating whether the entry contributes to landed cost * @description Flag indicating whether the entry contributes to landed cost
* @example true * @example true
@@ -10397,6 +10450,8 @@ export interface components {
branchId?: number; branchId?: number;
/** @description Project ID */ /** @description Project ID */
projectId?: number; projectId?: number;
/** @description The tracking tags of the manual journal entry */
trackingTags?: components["schemas"]["TrackingTagAssignmentDto"][];
}; };
CreateManualJournalDto: { CreateManualJournalDto: {
/** /**
@@ -14601,6 +14656,58 @@ export interface components {
*/ */
password: string; 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: { ExchangeRateLatestResponseDto: {
/** /**
* @description The base currency code * @description The base currency code
@@ -23238,6 +23345,10 @@ export interface operations {
previousYearAmountChange?: boolean; previousYearAmountChange?: boolean;
/** @description Whether to show percentage change from previous year */ /** @description Whether to show percentage change from previous year */
previousYearPercentageChange?: boolean; previousYearPercentageChange?: boolean;
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -24645,7 +24756,7 @@ export interface operations {
}; };
CustomerBalanceSummaryController_customerBalanceSummary: { CustomerBalanceSummaryController_customerBalanceSummary: {
parameters: { parameters: {
query?: { query: {
/** @description The date as of which the balance summary is calculated */ /** @description The date as of which the balance summary is calculated */
asDate?: string; asDate?: string;
/** @description Number of decimal places to display */ /** @description Number of decimal places to display */
@@ -24666,6 +24777,10 @@ export interface operations {
noneZero?: boolean; noneZero?: boolean;
/** @description Array of customer IDs to filter the summary */ /** @description Array of customer IDs to filter the summary */
customersIds?: number[]; customersIds?: number[];
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -24693,7 +24808,7 @@ export interface operations {
}; };
VendorBalanceSummaryController_vendorBalanceSummary: { VendorBalanceSummaryController_vendorBalanceSummary: {
parameters: { parameters: {
query?: { query: {
/** @description The date as of which the balance summary is calculated */ /** @description The date as of which the balance summary is calculated */
asDate?: string; asDate?: string;
/** @description Number of decimal places to display */ /** @description Number of decimal places to display */
@@ -24714,6 +24829,10 @@ export interface operations {
noneZero?: boolean; noneZero?: boolean;
/** @description Array of vendor IDs to filter the summary */ /** @description Array of vendor IDs to filter the summary */
vendorsIds?: number[]; vendorsIds?: number[];
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -26000,7 +26119,7 @@ export interface operations {
}; };
TrialBalanceSheetController_getTrialBalanceSheet: { TrialBalanceSheetController_getTrialBalanceSheet: {
parameters: { parameters: {
query?: { query: {
/** @description Start date for the trial balance sheet */ /** @description Start date for the trial balance sheet */
fromDate?: string; fromDate?: string;
/** @description End date for the trial balance sheet */ /** @description End date for the trial balance sheet */
@@ -26025,6 +26144,10 @@ export interface operations {
onlyActive?: boolean; onlyActive?: boolean;
/** @description Filter by specific account IDs */ /** @description Filter by specific account IDs */
accountIds?: number[]; accountIds?: number[];
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -26641,7 +26764,7 @@ export interface operations {
}; };
TransactionsByVendorController_transactionsByVendor: { TransactionsByVendorController_transactionsByVendor: {
parameters: { parameters: {
query?: { query: {
/** @description Number of decimal places to display */ /** @description Number of decimal places to display */
precision?: number; precision?: number;
/** @description Whether to divide the number by 1000 */ /** @description Whether to divide the number by 1000 */
@@ -26658,6 +26781,10 @@ export interface operations {
noneZero?: boolean; noneZero?: boolean;
/** @description Array of vendor IDs to include */ /** @description Array of vendor IDs to include */
vendorsIds?: string[]; vendorsIds?: string[];
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -26685,7 +26812,7 @@ export interface operations {
}; };
TransactionsByCustomerController_transactionsByCustomer: { TransactionsByCustomerController_transactionsByCustomer: {
parameters: { parameters: {
query?: { query: {
/** @description Number of decimal places to display */ /** @description Number of decimal places to display */
precision?: number; precision?: number;
/** @description Whether to divide the number by 1000 */ /** @description Whether to divide the number by 1000 */
@@ -26700,6 +26827,10 @@ export interface operations {
noneTransactions?: boolean; noneTransactions?: boolean;
/** @description Whether to exclude zero values */ /** @description Whether to exclude zero values */
noneZero?: boolean; noneZero?: boolean;
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -26732,6 +26863,10 @@ export interface operations {
referenceType: string; referenceType: string;
/** @description The ID of the reference */ /** @description The ID of the reference */
referenceId: number; referenceId: number;
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header?: never; header?: never;
path?: never; path?: never;
@@ -27420,7 +27555,7 @@ export interface operations {
}; };
JournalSheetController_journalSheet: { JournalSheetController_journalSheet: {
parameters: { parameters: {
query?: { query: {
/** @description Whether to hide cents in the number format */ /** @description Whether to hide cents in the number format */
noCents?: boolean; noCents?: boolean;
/** @description Whether to divide numbers by 1000 */ /** @description Whether to divide numbers by 1000 */
@@ -27433,6 +27568,10 @@ export interface operations {
fromRange?: number; fromRange?: number;
/** @description End range for filtering */ /** @description End range for filtering */
toRange?: number; toRange?: number;
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -27785,6 +27924,10 @@ export interface operations {
previousYearAmountChange?: boolean; previousYearAmountChange?: boolean;
/** @description Whether to show previous year percentage change */ /** @description Whether to show previous year percentage change */
previousYearPercentageChange?: boolean; previousYearPercentageChange?: boolean;
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */
@@ -28054,7 +28197,7 @@ export interface operations {
}; };
CashflowController_getCashflow: { CashflowController_getCashflow: {
parameters: { parameters: {
query?: { query: {
/** @description Start date for the cash flow statement period */ /** @description Start date for the cash flow statement period */
fromDate?: string; fromDate?: string;
/** @description End date for the cash flow statement period */ /** @description End date for the cash flow statement period */
@@ -28079,6 +28222,10 @@ export interface operations {
negativeFormat?: "parentheses" | "mines"; negativeFormat?: "parentheses" | "mines";
/** @description Basis for the cash flow statement */ /** @description Basis for the cash flow statement */
basis?: string; basis?: string;
/** @description Tag ID */
tagId: number;
/** @description Option ID */
optionId: number;
}; };
header: { header: {
/** @description Value must be 'Bearer <token>' where <token> is an API key prefixed with 'bc_' or a JWT token. */ /** @description Value must be 'Bearer <token>' where <token> 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 <token>' where <token> 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 <token>' where <token> 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 <token>' where <token> 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 <token>' where <token> 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 <token>' where <token> 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: { ExchangeRatesController_getLatestExchangeRate: {
parameters: { parameters: {
query?: { query?: {
+57
View File
@@ -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<string, keyof paths>;
export type TrackingTagsList = OpResponseBody<OpForPath<typeof TRACKING_TAGS_ROUTES.LIST, 'get'>>;
export type TrackingTag = OpResponseBody<OpForPath<typeof TRACKING_TAGS_ROUTES.BY_ID, 'get'>>;
export type CreateTrackingTagBody = OpRequestBody<OpForPath<typeof TRACKING_TAGS_ROUTES.LIST, 'post'>>;
export type EditTrackingTagBody = OpRequestBody<OpForPath<typeof TRACKING_TAGS_ROUTES.BY_ID, 'put'>>;
export async function fetchTrackingTags(
fetcher: ApiFetcher,
): Promise<TrackingTagsList> {
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<TrackingTag> {
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<TrackingTag> {
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<TrackingTag> {
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<void> {
const del = fetcher.path(TRACKING_TAGS_ROUTES.BY_ID).method('delete').create();
await del({ id });
}