mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af80afcf59 | ||
|
|
66cb0521e5 | ||
|
|
9204b76346 | ||
|
|
36cbb1eef5 | ||
|
|
441e27581b | ||
|
|
e0d9a56a29 | ||
|
|
5a017104ce | ||
|
|
25ca620836 | ||
|
|
5a3655e093 | ||
|
|
49c2777587 | ||
|
|
a5680c08c2 | ||
|
|
d909dad1bf | ||
|
|
f32cc752ef | ||
|
|
a7f98201cc | ||
|
|
a1d0fc3f0a | ||
|
|
11575cfb96 | ||
|
|
a2160c0595 | ||
|
|
956a9b58dd | ||
|
|
acb701d618 | ||
|
|
09ff72d302 | ||
|
|
7375512fec | ||
|
|
77e65389a4 | ||
|
|
1972861c97 | ||
|
|
c47acdee03 | ||
|
|
8689962bf3 | ||
|
|
3258159474 | ||
|
|
36bfa573ad | ||
|
|
2c05785096 | ||
|
|
6af4be9c6c | ||
|
|
8def1d31d2 | ||
|
|
afab02a053 | ||
|
|
8e925c62f2 | ||
|
|
1b7d513adf | ||
|
|
7d764fb390 | ||
|
|
c571f50a74 | ||
|
|
6549026344 | ||
|
|
0963394b04 | ||
|
|
6cab0651fc | ||
|
|
4af537d6dd | ||
|
|
34db64612c | ||
|
|
10225bbfed | ||
|
|
c3a4fe6b37 | ||
|
|
02be959461 | ||
|
|
d5bf56e333 | ||
|
|
e3182c15b3 | ||
|
|
dfa63ece21 | ||
|
|
6e95bd7da1 | ||
|
|
f51fffa5c7 | ||
|
|
6193358cc3 |
@@ -35,4 +35,4 @@ WORKDIR /app/packages/server
|
||||
RUN git clone https://github.com/vishnubob/wait-for-it.git
|
||||
|
||||
# Once we listen the mysql port run the migration task.
|
||||
CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "pnpm run system:migrate:latest && pnpm run tenants:migrate:latest"
|
||||
CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "node dist/cli.js system:migrate:latest && node dist/cli.js tenants:migrate:latest"
|
||||
|
||||
@@ -75,6 +75,9 @@ COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/i18n ./packag
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/public ./packages/server/public
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/static ./packages/server/static
|
||||
|
||||
# Copy database migration files (needed for running migrations)
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/database ./packages/server/src/database
|
||||
|
||||
# Copy built shared packages (dist folders and package.json for module resolution)
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/bigcapital-utils/dist ./shared/bigcapital-utils/dist
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/pdf-templates/dist ./shared/pdf-templates/dist
|
||||
|
||||
@@ -2,10 +2,23 @@
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "main",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"assets": [
|
||||
{ "include": "i18n/**/*", "watchAssets": true }
|
||||
{ "include": "i18n/**/*", "watchAssets": true },
|
||||
{ "include": "database/**/*", "exclude": "**/*.ts", "watchAssets": true }
|
||||
]
|
||||
},
|
||||
"projects": {
|
||||
"cli": {
|
||||
"type": "application",
|
||||
"root": "src",
|
||||
"entryFile": "cli",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "tsconfig.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||
"@liaoliaots/nestjs-redis": "^10.0.0",
|
||||
"@nest-lab/throttler-storage-redis": "^1.1.0",
|
||||
"@bull-board/api": "^5.22.0",
|
||||
"@bull-board/express": "^5.22.0",
|
||||
"@bull-board/nestjs": "^5.22.0",
|
||||
"@nestjs/bull": "^10.2.1",
|
||||
"@nestjs/bullmq": "^10.2.2",
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
|
||||
8
packages/server/src/common/config/bull-board.ts
Normal file
8
packages/server/src/common/config/bull-board.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export default registerAs('bullBoard', () => ({
|
||||
enabled: parseBoolean<boolean>(process.env.BULL_BOARD_ENABLED, false),
|
||||
username: process.env.BULL_BOARD_USERNAME,
|
||||
password: process.env.BULL_BOARD_PASSWORD,
|
||||
}));
|
||||
@@ -19,6 +19,7 @@ import throttle from './throttle';
|
||||
import cloud from './cloud';
|
||||
import redis from './redis';
|
||||
import queue from './queue';
|
||||
import bullBoard from './bull-board';
|
||||
|
||||
export const config = [
|
||||
app,
|
||||
@@ -42,4 +43,5 @@ export const config = [
|
||||
throttle,
|
||||
redis,
|
||||
queue,
|
||||
bullBoard,
|
||||
];
|
||||
|
||||
@@ -6,4 +6,5 @@ export default registerAs('s3', () => ({
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
bucket: process.env.S3_BUCKET,
|
||||
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||
}));
|
||||
|
||||
59
packages/server/src/middleware/bull-board-auth.middleware.ts
Normal file
59
packages/server/src/middleware/bull-board-auth.middleware.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
/**
|
||||
* Creates Express middleware for the Bull Board UI:
|
||||
* - When disabled: responds with 404.
|
||||
* - When enabled and username/password are set: enforces HTTP Basic Auth (401 if invalid).
|
||||
* - When enabled and credentials are not set: allows access (no auth).
|
||||
*/
|
||||
export function createBullBoardAuthMiddleware(
|
||||
enabled: boolean,
|
||||
username: string | undefined,
|
||||
password: string | undefined,
|
||||
): (req: Request, res: Response, next: NextFunction) => void {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!enabled) {
|
||||
res.status(404).send('Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username || !password) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||
res.status(401).send('Authentication required');
|
||||
return;
|
||||
}
|
||||
|
||||
const base64Credentials = authHeader.slice(6);
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = Buffer.from(base64Credentials, 'base64').toString('utf8');
|
||||
} catch {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||
res.status(401).send('Invalid credentials');
|
||||
return;
|
||||
}
|
||||
|
||||
const colonIndex = decoded.indexOf(':');
|
||||
if (colonIndex === -1) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||
res.status(401).send('Invalid credentials');
|
||||
return;
|
||||
}
|
||||
|
||||
const reqUsername = decoded.slice(0, colonIndex);
|
||||
const reqPassword = decoded.slice(colonIndex + 1);
|
||||
|
||||
if (reqUsername !== username || reqPassword !== password) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||
res.status(401).send('Invalid credentials');
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
I18nModule,
|
||||
QueryResolver,
|
||||
} from 'nestjs-i18n';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { ExpressAdapter } from '@bull-board/express';
|
||||
import { createBullBoardAuthMiddleware } from '@/middleware/bull-board-auth.middleware';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
@@ -143,6 +146,24 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
BullBoardModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const enabled = configService.get<boolean>('bullBoard.enabled');
|
||||
const username = configService.get<string>('bullBoard.username');
|
||||
const password = configService.get<string>('bullBoard.password');
|
||||
return {
|
||||
route: '/queues',
|
||||
adapter: ExpressAdapter,
|
||||
middleware: createBullBoardAuthMiddleware(
|
||||
enabled,
|
||||
username,
|
||||
password,
|
||||
),
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
ClsModule.forRoot({
|
||||
global: true,
|
||||
middleware: {
|
||||
|
||||
@@ -17,6 +17,8 @@ import { PassportModule } from '@nestjs/passport';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||
import { AuthMailSubscriber } from './subscribers/AuthMail.subscriber';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import {
|
||||
SendResetPasswordMailQueue,
|
||||
@@ -63,6 +65,14 @@ const models = [
|
||||
TenancyModule,
|
||||
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
|
||||
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
|
||||
BullBoardModule.forFeature({
|
||||
name: SendResetPasswordMailQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
BullBoardModule.forFeature({
|
||||
name: SendSignupVerificationMailQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
],
|
||||
exports: [...models],
|
||||
providers: [
|
||||
@@ -98,4 +108,4 @@ const models = [
|
||||
AuthMailSubscriber,
|
||||
],
|
||||
})
|
||||
export class AuthModule { }
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import {
|
||||
SendResetPasswordMailJob,
|
||||
SendResetPasswordMailQueue,
|
||||
} from '../Auth.constants';
|
||||
import { Process } from '@nestjs/bull';
|
||||
import { SendResetPasswordMailQueue } from '../Auth.constants';
|
||||
import { Job } from 'bullmq';
|
||||
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||
@@ -23,7 +19,6 @@ export class SendResetPasswordMailProcessor extends WorkerHost {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(SendResetPasswordMailJob)
|
||||
async process(job: Job<SendResetPasswordMailJobPayload>) {
|
||||
try {
|
||||
await this.authMailMesssages.sendResetPasswordMail(
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { Process } from '@nestjs/bull';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import {
|
||||
SendSignupVerificationMailJob,
|
||||
SendSignupVerificationMailQueue,
|
||||
} from '../Auth.constants';
|
||||
import { SendSignupVerificationMailQueue } from '../Auth.constants';
|
||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
||||
|
||||
@@ -21,7 +17,6 @@ export class SendSignupVerificationMailProcessor extends WorkerHost {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(SendSignupVerificationMailJob)
|
||||
async process(job: Job<SendSignupVerificationMailJobPayload>) {
|
||||
try {
|
||||
await this.authMailMesssages.sendSignupVerificationMail(
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators';
|
||||
|
||||
class BankRuleConditionDto {
|
||||
@IsNotEmpty()
|
||||
@IsIn(['description', 'amount'])
|
||||
@IsIn(['description', 'amount', 'payee'])
|
||||
field: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SocketModule } from '../Socket/Socket.module';
|
||||
@@ -33,6 +35,10 @@ const models = [RegisterTenancyModel(PlaidItem)];
|
||||
BankingCategorizeModule,
|
||||
BankingTransactionsModule,
|
||||
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
|
||||
BullBoardModule.forFeature({
|
||||
name: UpdateBankingPlaidTransitionsQueueJob,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
...models,
|
||||
],
|
||||
providers: [
|
||||
@@ -51,4 +57,4 @@ const models = [RegisterTenancyModel(PlaidItem)];
|
||||
exports: [...models],
|
||||
controllers: [BankingPlaidController, BankingPlaidWebhooksController],
|
||||
})
|
||||
export class BankingPlaidModule { }
|
||||
export class BankingPlaidModule {}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Process } from '@nestjs/bull';
|
||||
import { UseCls } from 'nestjs-cls';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import {
|
||||
PlaidFetchTransitonsEventPayload,
|
||||
UpdateBankingPlaidTransitionsJob,
|
||||
UpdateBankingPlaidTransitionsQueueJob,
|
||||
} from '../types/BankingPlaid.types';
|
||||
import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions';
|
||||
@@ -28,7 +26,6 @@ export class PlaidFetchTransactionsProcessor extends WorkerHost {
|
||||
/**
|
||||
* Triggers the function.
|
||||
*/
|
||||
@Process(UpdateBankingPlaidTransitionsJob)
|
||||
@UseCls()
|
||||
async process(job: Job<PlaidFetchTransitonsEventPayload>) {
|
||||
const { plaidItemId } = job.data;
|
||||
|
||||
@@ -10,6 +10,8 @@ import { BankingRecognizedTransactionsController } from './BankingRecognizedTran
|
||||
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
|
||||
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
|
||||
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { RecognizeUncategorizedTransactionsQueue } from './_types';
|
||||
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
|
||||
@@ -25,6 +27,10 @@ const models = [RegisterTenancyModel(RecognizedBankTransaction)];
|
||||
BullModule.registerQueue({
|
||||
name: RecognizeUncategorizedTransactionsQueue,
|
||||
}),
|
||||
BullBoardModule.forFeature({
|
||||
name: RecognizeUncategorizedTransactionsQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
...models,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
|
||||
export const RecognizeUncategorizedTransactionsQueue =
|
||||
'recognize-uncategorized-transactions-queue';
|
||||
|
||||
|
||||
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
|
||||
ruleId: number,
|
||||
transactionsCriteria: any;
|
||||
transactionsCriteria?: RecognizeTransactionsCriteria;
|
||||
/**
|
||||
* When true, first reverts recognized transactions before recognizing again.
|
||||
* Used when a bank rule is edited to ensure transactions previously recognized
|
||||
* by lower-priority rules are re-evaluated against the updated rule.
|
||||
*/
|
||||
shouldRevert?: boolean;
|
||||
}
|
||||
@@ -93,6 +93,10 @@ export class RecognizeTranasctionsService {
|
||||
q.whereIn('id', rulesIds);
|
||||
}
|
||||
q.withGraphFetched('conditions');
|
||||
|
||||
// Order by the 'order' field to ensure higher priority rules (lower order values)
|
||||
// are matched first.
|
||||
q.orderBy('order', 'asc');
|
||||
});
|
||||
|
||||
const bankRulesByAccountId = transformToMapBy(
|
||||
|
||||
@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
|
||||
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
|
||||
const payload = {
|
||||
ruleId: bankRule.id,
|
||||
shouldRevert: true,
|
||||
...tenantPayload,
|
||||
} as RecognizeUncategorizedTransactionsJobPayload;
|
||||
|
||||
// Re-recognize the transactions based on the new rules.
|
||||
// Setting shouldRevert to true ensures that transactions previously recognized
|
||||
// by this or lower-priority rules are re-evaluated against the updated rule.
|
||||
await this.recognizeTransactionsQueue.add(
|
||||
RecognizeUncategorizedTransactionsJob,
|
||||
payload,
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
|
||||
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
|
||||
import {
|
||||
RecognizeUncategorizedTransactionsJobPayload,
|
||||
RecognizeUncategorizedTransactionsQueue,
|
||||
} from '../_types';
|
||||
import { Process } from '@nestjs/bull';
|
||||
|
||||
@Processor({
|
||||
name: RecognizeUncategorizedTransactionsQueue,
|
||||
@@ -16,10 +16,12 @@ import { Process } from '@nestjs/bull';
|
||||
export class RegonizeTransactionsPrcessor extends WorkerHost {
|
||||
/**
|
||||
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
|
||||
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
|
||||
* @param {ClsService} clsService -
|
||||
*/
|
||||
constructor(
|
||||
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
|
||||
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
|
||||
private readonly clsService: ClsService,
|
||||
) {
|
||||
super();
|
||||
@@ -28,15 +30,23 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
@Process(RecognizeUncategorizedTransactionsQueue)
|
||||
@UseCls()
|
||||
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
|
||||
const { ruleId, transactionsCriteria } = job.data;
|
||||
const { ruleId, transactionsCriteria, shouldRevert } = job.data;
|
||||
|
||||
this.clsService.set('organizationId', job.data.organizationId);
|
||||
this.clsService.set('userId', job.data.userId);
|
||||
|
||||
try {
|
||||
// If shouldRevert is true, first revert recognized transactions before re-recognizing.
|
||||
// This is used when a bank rule is edited to ensure transactions previously recognized
|
||||
// by lower-priority rules are re-evaluated against the updated rule.
|
||||
if (shouldRevert) {
|
||||
await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
|
||||
ruleId,
|
||||
transactionsCriteria,
|
||||
);
|
||||
}
|
||||
await this.recognizeTranasctionsService.recognizeTransactions(
|
||||
ruleId,
|
||||
transactionsCriteria,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
||||
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
|
||||
import { BranchResponseDto } from '@/modules/Branches/dtos/BranchResponse.dto';
|
||||
import { DiscountType } from '@/common/types/Discount';
|
||||
|
||||
export class BillResponseDto {
|
||||
@@ -89,6 +91,14 @@ export class BillResponseDto {
|
||||
})
|
||||
branchId?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Branch details',
|
||||
type: () => BranchResponseDto,
|
||||
required: false,
|
||||
})
|
||||
@Type(() => BranchResponseDto)
|
||||
branch?: BranchResponseDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The ID of the project',
|
||||
example: 301,
|
||||
|
||||
@@ -30,6 +30,7 @@ export class BillTransformer extends Transformer {
|
||||
'taxes',
|
||||
'entries',
|
||||
'attachments',
|
||||
'branch',
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance'
|
||||
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
|
||||
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
|
||||
import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches';
|
||||
import { BillActivateBranches } from './integrations/Purchases/BillBranchesActivate';
|
||||
import { VendorCreditActivateBranches } from './integrations/Purchases/VendorCreditBranchesActivate';
|
||||
import { BillPaymentsActivateBranches } from './integrations/Purchases/PaymentMadeBranchesActivate';
|
||||
import { BillBranchesActivateSubscriber } from './subscribers/Activate/BillBranchesActivateSubscriber';
|
||||
import { VendorCreditBranchesActivateSubscriber } from './subscribers/Activate/VendorCreditBranchesActivateSubscriber';
|
||||
import { PaymentMadeActivateBranchesSubscriber } from './subscribers/Activate/PaymentMadeBranchesActivateSubscriber';
|
||||
import { FeaturesModule } from '../Features/Features.module';
|
||||
|
||||
@Module({
|
||||
@@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module';
|
||||
ValidateBranchExistance,
|
||||
ManualJournalBranchesValidator,
|
||||
CashflowTransactionsActivateBranches,
|
||||
ExpensesActivateBranches
|
||||
ExpensesActivateBranches,
|
||||
BillActivateBranches,
|
||||
VendorCreditActivateBranches,
|
||||
BillPaymentsActivateBranches,
|
||||
BillBranchesActivateSubscriber,
|
||||
VendorCreditBranchesActivateSubscriber,
|
||||
PaymentMadeActivateBranchesSubscriber
|
||||
],
|
||||
exports: [
|
||||
BranchesSettingsService,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Bill } from '@/modules/Bills/models/Bill';
|
||||
|
||||
@Injectable()
|
||||
export class BillActivateBranches {
|
||||
constructor(private readonly billModel: TenantModelProxy<typeof Bill>) {}
|
||||
constructor(
|
||||
@Inject(Bill.name)
|
||||
private readonly billModel: TenantModelProxy<typeof Bill>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates all bills transactions with the primary branch.
|
||||
@@ -17,7 +20,7 @@ export class BillActivateBranches {
|
||||
primaryBranchId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
// Updates the sale invoice with primary branch.
|
||||
await Bill.query(trx).update({ branchId: primaryBranchId });
|
||||
// Updates the bills with primary branch.
|
||||
await this.billModel().query(trx).update({ branchId: primaryBranchId });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class BillPaymentsActivateBranches {
|
||||
constructor(
|
||||
@Inject(BillPayment.name)
|
||||
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
|
||||
|
||||
@Injectable()
|
||||
export class VendorCreditActivateBranches {
|
||||
constructor(
|
||||
@Inject(VendorCredit.name)
|
||||
private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IBranchesActivatedPayload } from '../../Branches.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BillActivateBranches } from '../../integrations/Purchases/BillBranchesActivate';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class BillBranchesActivateSubscriber {
|
||||
constructor(
|
||||
private readonly billActivateBranches: BillActivateBranches,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Updates bills transactions with the primary branch once
|
||||
* the multi-branches is activated.
|
||||
* @param {IBranchesActivatedPayload}
|
||||
*/
|
||||
@OnEvent(events.branch.onActivated)
|
||||
async updateBillsWithBranchOnActivated({
|
||||
primaryBranch,
|
||||
trx,
|
||||
}: IBranchesActivatedPayload) {
|
||||
await this.billActivateBranches.updateBillsWithBranch(
|
||||
primaryBranch.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IBranchesActivatedPayload } from '../../Branches.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { VendorCreditActivateBranches } from '../../integrations/Purchases/VendorCreditBranchesActivate';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class VendorCreditBranchesActivateSubscriber {
|
||||
constructor(
|
||||
private readonly vendorCreditActivateBranches: VendorCreditActivateBranches,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Updates vendor credits transactions with the primary branch once
|
||||
* the multi-branches is activated.
|
||||
* @param {IBranchesActivatedPayload}
|
||||
*/
|
||||
@OnEvent(events.branch.onActivated)
|
||||
async updateVendorCreditsWithBranchOnActivated({
|
||||
primaryBranch,
|
||||
trx,
|
||||
}: IBranchesActivatedPayload) {
|
||||
await this.vendorCreditActivateBranches.updateVendorCreditsWithBranch(
|
||||
primaryBranch.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export abstract class BaseCommand extends CommandRunner {
|
||||
},
|
||||
migrations: {
|
||||
directory: this.configService.get('systemDatabase.migrationDir'),
|
||||
loadExtensions: ['.js'],
|
||||
},
|
||||
seeds: {
|
||||
directory: this.configService.get('systemDatabase.seedsDir'),
|
||||
@@ -43,6 +44,7 @@ export abstract class BaseCommand extends CommandRunner {
|
||||
},
|
||||
migrations: {
|
||||
directory: this.configService.get('tenantDatabase.migrationsDir') || './src/database/migrations',
|
||||
loadExtensions: ['.js'],
|
||||
},
|
||||
seeds: {
|
||||
directory: this.configService.get('tenantDatabase.seedsDir') || './src/database/seeds/core',
|
||||
|
||||
@@ -24,14 +24,14 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
|
||||
displayColumnsType: 'total' | 'date_periods' = 'total';
|
||||
|
||||
@ApiProperty({
|
||||
enum: ['day', 'month', 'year'],
|
||||
enum: ['day', 'month', 'year', 'quarter'],
|
||||
default: 'year',
|
||||
description: 'Time period for column display',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsEnum(['day', 'month', 'year'])
|
||||
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
|
||||
@IsEnum(['day', 'month', 'year', 'quarter'])
|
||||
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Start date for the balance sheet period',
|
||||
|
||||
@@ -34,13 +34,13 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
|
||||
@ApiProperty({
|
||||
description: 'Display columns by time period',
|
||||
required: false,
|
||||
enum: ['day', 'month', 'year'],
|
||||
enum: ['day', 'month', 'year', 'quarter'],
|
||||
default: 'year',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsEnum(['day', 'month', 'year'])
|
||||
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
|
||||
@IsEnum(['day', 'month', 'year', 'quarter'])
|
||||
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Type of column display',
|
||||
|
||||
@@ -64,10 +64,10 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
|
||||
displayColumnsType: 'total' | 'date_periods';
|
||||
|
||||
@IsString()
|
||||
@IsEnum(['day', 'month', 'year'])
|
||||
@IsEnum(['day', 'month', 'year', 'quarter'])
|
||||
@IsOptional()
|
||||
@ApiProperty({ description: 'How to display columns' })
|
||||
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
|
||||
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
|
||||
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsBoolean()
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
ComputeItemCostQueue,
|
||||
WriteInventoryTransactionsGLEntriesQueue,
|
||||
} from './types/InventoryCost.types';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { InventoryAverageCostMethodService } from './commands/InventoryAverageCostMethod.service';
|
||||
import { InventoryItemCostService } from './commands/InventoryCosts.service';
|
||||
@@ -39,6 +41,14 @@ const models = [
|
||||
BullModule.registerQueue({
|
||||
name: WriteInventoryTransactionsGLEntriesQueue,
|
||||
}),
|
||||
BullBoardModule.forFeature({
|
||||
name: ComputeItemCostQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
BullBoardModule.forFeature({
|
||||
name: WriteInventoryTransactionsGLEntriesQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
forwardRef(() => SaleInvoicesModule),
|
||||
ImportModule,
|
||||
],
|
||||
@@ -56,7 +66,7 @@ const models = [
|
||||
InventoryItemCostService,
|
||||
InventoryItemOpeningAvgCostService,
|
||||
InventoryCostSubscriber,
|
||||
GetItemsInventoryValuationListService
|
||||
GetItemsInventoryValuationListService,
|
||||
],
|
||||
exports: [
|
||||
...models,
|
||||
@@ -64,6 +74,6 @@ const models = [
|
||||
InventoryItemCostService,
|
||||
InventoryComputeCostService,
|
||||
],
|
||||
controllers: [InventoryCostController]
|
||||
controllers: [InventoryCostController],
|
||||
})
|
||||
export class InventoryCostModule {}
|
||||
|
||||
@@ -7,11 +7,7 @@ import * as moment from 'moment';
|
||||
import { TenantJobPayload } from '@/interfaces/Tenant';
|
||||
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import {
|
||||
ComputeItemCostQueue,
|
||||
ComputeItemCostQueueJob,
|
||||
} from '../types/InventoryCost.types';
|
||||
import { Process } from '@nestjs/bull';
|
||||
import { ComputeItemCostQueue } from '../types/InventoryCost.types';
|
||||
|
||||
interface ComputeItemCostJobPayload extends TenantJobPayload {
|
||||
itemId: number;
|
||||
@@ -39,7 +35,6 @@ export class ComputeItemCostProcessor extends WorkerHost {
|
||||
* Process the compute item cost job.
|
||||
* @param {Job<ComputeItemCostJobPayload>} job - The job to process
|
||||
*/
|
||||
@Process(ComputeItemCostQueueJob)
|
||||
@UseCls()
|
||||
async process(job: Job<ComputeItemCostJobPayload>) {
|
||||
const { itemId, startingDate, organizationId, userId } = job.data;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Process } from '@nestjs/bull';
|
||||
import {
|
||||
WriteInventoryTransactionsGLEntriesQueue,
|
||||
WriteInventoryTransactionsGLEntriesQueueJob,
|
||||
} from '../types/InventoryCost.types';
|
||||
import { WriteInventoryTransactionsGLEntriesQueue } from '../types/InventoryCost.types';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
|
||||
@@ -15,6 +11,5 @@ export class WriteInventoryTransactionsGLEntriesProcessor extends WorkerHost {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(WriteInventoryTransactionsGLEntriesQueueJob)
|
||||
async process() {}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
BulkDeleteItemsDto,
|
||||
ValidateBulkDeleteItemsResponseDto,
|
||||
} from './dtos/BulkDeleteItems.dto';
|
||||
import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto';
|
||||
|
||||
@Controller('/items')
|
||||
@ApiTags('Items')
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
@ApiExtraModels(ItemEstimatesResponseDto)
|
||||
@ApiExtraModels(ItemReceiptsResponseDto)
|
||||
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
|
||||
@ApiExtraModels(ItemApiErrorResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class ItemsController extends TenantController {
|
||||
constructor(private readonly itemsApplication: ItemsApplicationService) {
|
||||
@@ -147,6 +149,13 @@ export class ItemsController extends TenantController {
|
||||
status: 200,
|
||||
description: 'The item has been successfully updated.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, INVENTORY_ACCOUNT_CANNOT_MODIFIED, TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS, etc.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ItemApiErrorResponseDto),
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
@@ -204,6 +213,13 @@ export class ItemsController extends TenantController {
|
||||
status: 200,
|
||||
description: 'The item has been successfully created.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, ITEM_CATEOGRY_NOT_FOUND, COST_ACCOUNT_NOT_COGS, SELL_ACCOUNT_NOT_INCOME, INVENTORY_ACCOUNT_NOT_INVENTORY, INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM, COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM, etc.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ItemApiErrorResponseDto),
|
||||
},
|
||||
})
|
||||
// @UsePipes(new ZodValidationPipe(createItemSchema))
|
||||
async createItem(
|
||||
@Body() createItemDto: CreateItemDto,
|
||||
@@ -219,6 +235,13 @@ export class ItemsController extends TenantController {
|
||||
status: 200,
|
||||
description: 'The item has been successfully deleted.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Cannot delete item. Possible error types: ITEM_HAS_ASSOCIATED_TRANSACTINS, ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT, etc.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ItemApiErrorResponseDto),
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
|
||||
112
packages/server/src/modules/Items/dtos/ItemErrorResponse.dto.ts
Normal file
112
packages/server/src/modules/Items/dtos/ItemErrorResponse.dto.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* Item API Error Types
|
||||
* These error types are returned when item operations fail validation
|
||||
*/
|
||||
export enum ItemErrorType {
|
||||
/** Item name already exists in the system */
|
||||
ItemNameExists = 'ITEM_NAME_EXISTS',
|
||||
|
||||
/** Item category was not found */
|
||||
ItemCategoryNotFound = 'ITEM_CATEOGRY_NOT_FOUND',
|
||||
|
||||
/** Cost account is not a Cost of Goods Sold account */
|
||||
CostAccountNotCogs = 'COST_ACCOUNT_NOT_COGS',
|
||||
|
||||
/** Cost account was not found */
|
||||
CostAccountNotFound = 'COST_ACCOUNT_NOT_FOUMD',
|
||||
|
||||
/** Sell account was not found */
|
||||
SellAccountNotFound = 'SELL_ACCOUNT_NOT_FOUND',
|
||||
|
||||
/** Sell account is not an income account */
|
||||
SellAccountNotIncome = 'SELL_ACCOUNT_NOT_INCOME',
|
||||
|
||||
/** Inventory account was not found */
|
||||
InventoryAccountNotFound = 'INVENTORY_ACCOUNT_NOT_FOUND',
|
||||
|
||||
/** Account is not an inventory type account */
|
||||
InventoryAccountNotInventory = 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||
|
||||
/** Multiple items have associated transactions */
|
||||
ItemsHaveAssociatedTransactions = 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
|
||||
|
||||
/** Item has associated transactions (singular) */
|
||||
ItemHasAssociatedTransactions = 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
|
||||
|
||||
/** Item has associated inventory adjustments */
|
||||
ItemHasAssociatedInventoryAdjustment = 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||
|
||||
/** Cannot change item type to inventory */
|
||||
ItemCannotChangeInventoryType = 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||
|
||||
/** Cannot change type when item has transactions */
|
||||
TypeCannotChangeWithItemHasTransactions = 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
|
||||
/** Inventory account cannot be modified */
|
||||
InventoryAccountCannotModified = 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
|
||||
/** Purchase tax rate was not found */
|
||||
PurchaseTaxRateNotFound = 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||
|
||||
/** Sell tax rate was not found */
|
||||
SellTaxRateNotFound = 'SELL_TAX_RATE_NOT_FOUND',
|
||||
|
||||
/** Income account is required for sellable items */
|
||||
IncomeAccountRequiredWithSellableItem = 'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
|
||||
|
||||
/** Cost account is required for purchasable items */
|
||||
CostAccountRequiredWithPurchasableItem = 'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
|
||||
|
||||
/** Item not found */
|
||||
NotFound = 'NOT_FOUND',
|
||||
|
||||
/** Items not found */
|
||||
ItemsNotFound = 'ITEMS_NOT_FOUND',
|
||||
}
|
||||
|
||||
/**
|
||||
* Item API Error Response
|
||||
* Returned when an item operation fails
|
||||
*/
|
||||
export class ItemErrorResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'HTTP status code',
|
||||
example: 400,
|
||||
})
|
||||
statusCode: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Error type identifier',
|
||||
enum: ItemErrorType,
|
||||
example: ItemErrorType.ItemNameExists,
|
||||
})
|
||||
type: ItemErrorType;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Human-readable error message',
|
||||
example: 'The item name is already exist.',
|
||||
required: false,
|
||||
nullable: true,
|
||||
})
|
||||
message: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Additional error payload data',
|
||||
required: false,
|
||||
nullable: true,
|
||||
})
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item API Error Response Wrapper
|
||||
*/
|
||||
export class ItemApiErrorResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Array of error details',
|
||||
type: [ItemErrorResponseDto],
|
||||
})
|
||||
errors: ItemErrorResponseDto[];
|
||||
}
|
||||
@@ -70,6 +70,16 @@ export class Item extends TenantBaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Model search roles.
|
||||
*/
|
||||
static get searchRoles() {
|
||||
return [
|
||||
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
|
||||
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
HttpCode,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { BuildOrganizationService } from './commands/BuildOrganization.service';
|
||||
import {
|
||||
BuildOrganizationDto,
|
||||
@@ -50,7 +51,7 @@ export class OrganizationController {
|
||||
private readonly updateOrganizationService: UpdateOrganizationService,
|
||||
private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob,
|
||||
private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
@Post('build')
|
||||
@HttpCode(200)
|
||||
@@ -77,6 +78,7 @@ export class OrganizationController {
|
||||
}
|
||||
|
||||
@Get('build/:buildJobId')
|
||||
@Throttle({ default: { limit: 300, ttl: 60000 } }) // 300 req/min
|
||||
@ApiParam({
|
||||
name: 'buildJobId',
|
||||
required: true,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { GetCurrentOrganizationService } from './queries/GetCurrentOrganization.
|
||||
import { BuildOrganizationService } from './commands/BuildOrganization.service';
|
||||
import { UpdateOrganizationService } from './commands/UpdateOrganization.service';
|
||||
import { OrganizationController } from './Organization.controller';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { OrganizationBuildQueue } from './Organization.types';
|
||||
import { OrganizationBuildProcessor } from './processors/OrganizationBuild.processor';
|
||||
@@ -25,10 +27,14 @@ import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob
|
||||
OrganizationBaseCurrencyLocking,
|
||||
SyncSystemUserToTenantService,
|
||||
SyncSystemUserToTenantSubscriber,
|
||||
GetBuildOrganizationBuildJob
|
||||
GetBuildOrganizationBuildJob,
|
||||
],
|
||||
imports: [
|
||||
BullModule.registerQueue({ name: OrganizationBuildQueue }),
|
||||
BullBoardModule.forFeature({
|
||||
name: OrganizationBuildQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
TenantDBManagerModule,
|
||||
],
|
||||
controllers: [OrganizationController],
|
||||
|
||||
@@ -2,10 +2,8 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import { Process } from '@nestjs/bull';
|
||||
import {
|
||||
OrganizationBuildQueue,
|
||||
OrganizationBuildQueueJob,
|
||||
OrganizationBuildQueueJobPayload,
|
||||
} from '../Organization.types';
|
||||
import { BuildOrganizationService } from '../commands/BuildOrganization.service';
|
||||
@@ -22,7 +20,6 @@ export class OrganizationBuildProcessor extends WorkerHost {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(OrganizationBuildQueueJob)
|
||||
@UseCls()
|
||||
async process(job: Job<OrganizationBuildQueueJobPayload>) {
|
||||
console.log('Processing organization build job:', job.id);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { PaymentReceivesController } from './PaymentsReceived.controller';
|
||||
import { PaymentReceivesApplication } from './PaymentReceived.application';
|
||||
import { CreatePaymentReceivedService } from './commands/CreatePaymentReceived.serivce';
|
||||
@@ -95,6 +97,10 @@ import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePa
|
||||
DynamicListModule,
|
||||
MailModule,
|
||||
BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }),
|
||||
BullBoardModule.forFeature({
|
||||
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class PaymentsReceivedModule {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { JOB_REF, Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { Inject, Scope } from '@nestjs/common';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import {
|
||||
SEND_PAYMENT_RECEIVED_MAIL_JOB,
|
||||
@@ -13,20 +13,18 @@ import { SendPaymentReceivedMailPayload } from '../types/PaymentReceived.types';
|
||||
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendPaymentReceivedMailProcessor {
|
||||
export class SendPaymentReceivedMailProcessor extends WorkerHost {
|
||||
constructor(
|
||||
private readonly sendPaymentReceivedMail: SendPaymentReceiveMailNotification,
|
||||
private readonly clsService: ClsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Inject(JOB_REF)
|
||||
private readonly jobRef: Job<SendPaymentReceivedMailPayload>,
|
||||
) { }
|
||||
|
||||
@Process(SEND_PAYMENT_RECEIVED_MAIL_JOB)
|
||||
@UseCls()
|
||||
async handleSendMail() {
|
||||
async process(job: Job<SendPaymentReceivedMailPayload>) {
|
||||
const { messageOptions, paymentReceivedId, organizationId, userId } =
|
||||
this.jobRef.data;
|
||||
job.data;
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
@@ -54,6 +56,10 @@ import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.pr
|
||||
TemplateInjectableModule,
|
||||
PdfTemplatesModule,
|
||||
BullModule.registerQueue({ name: SendSaleEstimateMailQueue }),
|
||||
BullBoardModule.forFeature({
|
||||
name: SendSaleEstimateMailQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
],
|
||||
controllers: [SaleEstimatesController],
|
||||
providers: [
|
||||
@@ -99,4 +105,4 @@ import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.pr
|
||||
GetSaleEstimateMailTemplateService,
|
||||
],
|
||||
})
|
||||
export class SaleEstimatesModule { }
|
||||
export class SaleEstimatesModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bull';
|
||||
import { Queue } from 'bullmq';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ContactMailNotification } from '@/modules/MailNotification/ContactMailNotification';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { Inject, Scope } from '@nestjs/common';
|
||||
import { JOB_REF } from '@nestjs/bull';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import {
|
||||
SendSaleEstimateMailJob,
|
||||
SendSaleEstimateMailQueue,
|
||||
@@ -13,18 +12,17 @@ import { ClsService, UseCls } from 'nestjs-cls';
|
||||
name: SendSaleEstimateMailQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendSaleEstimateMailProcess {
|
||||
export class SendSaleEstimateMailProcess extends WorkerHost {
|
||||
constructor(
|
||||
private readonly sendEstimateMailService: SendSaleEstimateMail,
|
||||
private readonly clsService: ClsService,
|
||||
@Inject(JOB_REF)
|
||||
private readonly jobRef: Job,
|
||||
) { }
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(SendSaleEstimateMailJob)
|
||||
@UseCls()
|
||||
async handleSendMail() {
|
||||
const { saleEstimateId, messageOptions, organizationId, userId } = this.jobRef.data;
|
||||
async process(job: Job) {
|
||||
const { saleEstimateId, messageOptions, organizationId, userId } = job.data;
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
@@ -45,7 +45,9 @@ import { SendSaleInvoiceMailCommon } from './commands/SendInvoiceInvoiceMailComm
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { MailNotificationModule } from '../MailNotification/MailNotification.module';
|
||||
import { SendSaleInvoiceMailProcessor } from './processors/SendSaleInvoiceMail.processor';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { SendSaleInvoiceQueue } from './constants';
|
||||
import { InvoicePaymentIntegrationSubscriber } from './subscribers/InvoicePaymentIntegrationSubscriber';
|
||||
import { InvoiceChangeStatusOnMailSentSubscriber } from './subscribers/InvoiceChangeStatusOnMailSentSubscriber';
|
||||
@@ -81,6 +83,10 @@ import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleI
|
||||
forwardRef(() => PaymentLinksModule),
|
||||
DynamicListModule,
|
||||
BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
|
||||
BullBoardModule.forFeature({
|
||||
name: SendSaleInvoiceQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
],
|
||||
controllers: [SaleInvoicesController],
|
||||
providers: [
|
||||
@@ -139,4 +145,4 @@ import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleI
|
||||
SaleInvoicesImportable,
|
||||
],
|
||||
})
|
||||
export class SaleInvoicesModule { }
|
||||
export class SaleInvoicesModule {}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { JOB_REF, Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import { SendSaleInvoiceMailJob, SendSaleInvoiceQueue } from '../constants';
|
||||
import { SendSaleInvoiceMail } from '../commands/SendSaleInvoiceMail';
|
||||
import { Inject, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import { SendSaleInvoiceMailJobPayload } from '../SaleInvoice.types';
|
||||
|
||||
@@ -11,20 +10,18 @@ import { SendSaleInvoiceMailJobPayload } from '../SaleInvoice.types';
|
||||
name: SendSaleInvoiceQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendSaleInvoiceMailProcessor {
|
||||
export class SendSaleInvoiceMailProcessor extends WorkerHost {
|
||||
constructor(
|
||||
private readonly sendSaleInvoiceMail: SendSaleInvoiceMail,
|
||||
@Inject(REQUEST) private readonly request: Request,
|
||||
@Inject(JOB_REF)
|
||||
private readonly jobRef: Job<SendSaleInvoiceMailJobPayload>,
|
||||
private readonly clsService: ClsService,
|
||||
) { }
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(SendSaleInvoiceMailJob)
|
||||
@UseCls()
|
||||
async handleSendInvoice() {
|
||||
async process(job: Job<SendSaleInvoiceMailJobPayload>) {
|
||||
const { messageOptions, saleInvoiceId, organizationId, userId } =
|
||||
this.jobRef.data;
|
||||
job.data;
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { SaleReceiptApplication } from './SaleReceiptApplication.service';
|
||||
import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service';
|
||||
import { EditSaleReceipt } from './commands/EditSaleReceipt.service';
|
||||
@@ -62,6 +64,10 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
|
||||
MailModule,
|
||||
MailNotificationModule,
|
||||
BullModule.registerQueue({ name: SendSaleReceiptMailQueue }),
|
||||
BullBoardModule.forFeature({
|
||||
name: SendSaleReceiptMailQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
TenancyContext,
|
||||
@@ -95,4 +101,4 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
|
||||
ValidateBulkDeleteSaleReceiptsService,
|
||||
],
|
||||
})
|
||||
export class SaleReceiptsModule { }
|
||||
export class SaleReceiptsModule {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bullmq';
|
||||
import {
|
||||
DEFAULT_RECEIPT_MAIL_CONTENT,
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { Inject, Scope } from '@nestjs/common';
|
||||
import { JOB_REF } from '@nestjs/bull';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { SendSaleReceiptMailQueue, SendSaleReceiptMailJob } from '../constants';
|
||||
import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification';
|
||||
import { SaleReceiptSendMailPayload } from '../types/SaleReceipts.types';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
|
||||
@Processor({
|
||||
name: SendSaleReceiptMailQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendSaleReceiptMailProcess {
|
||||
export class SendSaleReceiptMailProcess extends WorkerHost {
|
||||
constructor(
|
||||
private readonly saleReceiptMailNotification: SaleReceiptMailNotification,
|
||||
private readonly clsService: ClsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Inject(JOB_REF)
|
||||
private readonly jobRef: Job<SaleReceiptSendMailPayload>,
|
||||
) { }
|
||||
|
||||
@Process(SendSaleReceiptMailJob)
|
||||
@UseCls()
|
||||
async handleSendMailJob() {
|
||||
async process(job: Job) {
|
||||
const { messageOpts, saleReceiptId, organizationId, userId } =
|
||||
this.jobRef.data;
|
||||
job.data;
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Controller, Get, Post } from '@nestjs/common';
|
||||
import { Controller, Get, HttpCode } from '@nestjs/common';
|
||||
import { PublicRoute } from '@/modules/Auth/guards/jwt.guard';
|
||||
|
||||
@Controller('/system_db')
|
||||
@Controller('system_db')
|
||||
@PublicRoute()
|
||||
export class SystemDatabaseController {
|
||||
constructor() {}
|
||||
|
||||
@Post()
|
||||
@Get()
|
||||
ping(){
|
||||
|
||||
@HttpCode(200)
|
||||
ping() {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SystemKnexConnectionConfigure,
|
||||
} from './SystemDB.constants';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import { SystemDatabaseController } from './SystemDB.controller';
|
||||
|
||||
const providers = [
|
||||
{
|
||||
@@ -22,6 +23,7 @@ const providers = [
|
||||
},
|
||||
migrations: {
|
||||
directory: configService.get('systemDatabase.migrationDir'),
|
||||
loadExtensions: ['.js'],
|
||||
},
|
||||
seeds: {
|
||||
directory: configService.get('systemDatabase.seedsDir'),
|
||||
@@ -41,6 +43,7 @@ const providers = [
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
controllers: [SystemDatabaseController],
|
||||
providers: [...providers],
|
||||
exports: [...providers],
|
||||
})
|
||||
|
||||
@@ -62,10 +62,11 @@ export class TaxRatesApplication {
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates list.
|
||||
* @returns {Promise<ITaxRate[]>}
|
||||
* @returns {Promise<{ data: ITaxRate[] }>}
|
||||
*/
|
||||
public getTaxRates() {
|
||||
return this.getTaxRatesService.getTaxRates();
|
||||
public async getTaxRates() {
|
||||
const taxRates = await this.getTaxRatesService.getTaxRates();
|
||||
return { data: taxRates };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -85,9 +85,14 @@ export class TaxRatesController {
|
||||
status: 200,
|
||||
description: 'The tax rates have been successfully retrieved.',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: getSchemaPath(TaxRateResponseDto),
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: getSchemaPath(TaxRateResponseDto),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToNumber } from '@/common/decorators/Validators';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
@@ -30,6 +31,7 @@ export class CommandTaxRateDto {
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@ToNumber()
|
||||
@ApiProperty({
|
||||
description: 'The rate of the tax rate.',
|
||||
example: 10,
|
||||
|
||||
@@ -33,6 +33,7 @@ export const TenancyDatabaseProxyProvider = ClsModule.forFeatureAsync({
|
||||
},
|
||||
migrations: {
|
||||
directory: configService.get('tenantDatabase.migrationsDir'),
|
||||
loadExtensions: ['.js'],
|
||||
},
|
||||
seeds: {
|
||||
directory: configService.get('tenantDatabase.seedsDir'),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { ActivateUserService } from './commands/ActivateUser.service';
|
||||
import { DeleteUserService } from './commands/DeleteUser.service';
|
||||
import { EditUserService } from './commands/EditUser.service';
|
||||
@@ -18,11 +21,24 @@ import { AcceptInviteUserService } from './commands/AcceptInviteUser.service';
|
||||
import { InviteTenantUserService } from './commands/InviteUser.service';
|
||||
import { UsersInviteController } from './UsersInvite.controller';
|
||||
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||
import { SendInviteUserMailQueue } from './Users.constants';
|
||||
import InviteSendMainNotificationSubscribe from './subscribers/InviteSendMailNotification.subscriber';
|
||||
import { SendInviteUserMailProcessor } from './processors/SendInviteUserMail.processor';
|
||||
import { SendInviteUsersMailMessage } from './commands/SendInviteUsersMailMessage.service';
|
||||
import { MailModule } from '../Mail/Mail.module';
|
||||
|
||||
const models = [InjectSystemModel(UserInvite)];
|
||||
|
||||
@Module({
|
||||
imports: [TenancyModule],
|
||||
imports: [
|
||||
TenancyModule,
|
||||
MailModule,
|
||||
BullModule.registerQueue({ name: SendInviteUserMailQueue }),
|
||||
BullBoardModule.forFeature({
|
||||
name: SendInviteUserMailQueue,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
],
|
||||
exports: [...models],
|
||||
providers: [
|
||||
...models,
|
||||
@@ -39,6 +55,9 @@ const models = [InjectSystemModel(UserInvite)];
|
||||
SyncTenantUserMutateSubscriber,
|
||||
SyncSystemSendInviteSubscriber,
|
||||
SyncTenantAcceptInviteSubscriber,
|
||||
InviteSendMainNotificationSubscribe,
|
||||
SendInviteUserMailProcessor,
|
||||
SendInviteUsersMailMessage,
|
||||
UsersApplication
|
||||
],
|
||||
controllers: [UsersController, UsersInviteController],
|
||||
|
||||
@@ -32,10 +32,12 @@ export interface ITenantUserDeletedPayload {
|
||||
export interface IUserInvitedEventPayload {
|
||||
inviteToken: string;
|
||||
user: ModelObject<TenantUser>;
|
||||
invitingUser: ModelObject<TenantUser>;
|
||||
}
|
||||
export interface IUserInviteTenantSyncedEventPayload {
|
||||
invite: ModelObject<UserInvite>;
|
||||
user: ModelObject<TenantUser>;
|
||||
invitingUser: ModelObject<TenantUser>;
|
||||
}
|
||||
|
||||
export interface IUserInviteResendEventPayload {
|
||||
|
||||
@@ -15,11 +15,13 @@ import { events } from '@/common/events/events';
|
||||
import { Role } from '@/modules/Roles/models/Role.model';
|
||||
import { ModelObject } from 'objection';
|
||||
import { SendInviteUserDto } from '../dtos/InviteUser.dto';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Injectable()
|
||||
export class InviteTenantUserService {
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
|
||||
@Inject(TenantUser.name)
|
||||
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||
@@ -53,10 +55,18 @@ export class InviteTenantUserService {
|
||||
active: true,
|
||||
invitedAt: new Date(),
|
||||
});
|
||||
|
||||
// Retrieves the authorized user (inviting user).
|
||||
const authorizedUser = await this.tenancyContext.getSystemUser();
|
||||
const invitingUser = await this.tenantUserModel()
|
||||
.query()
|
||||
.findOne({ systemUserId: authorizedUser.id });
|
||||
|
||||
// Triggers `onUserSendInvite` event.
|
||||
await this.eventEmitter.emitAsync(events.inviteUser.sendInvite, {
|
||||
inviteToken,
|
||||
user,
|
||||
invitingUser,
|
||||
} as IUserInvitedEventPayload);
|
||||
|
||||
return { invitedUser: user };
|
||||
|
||||
@@ -27,7 +27,7 @@ export class SendInviteUsersMailMessage {
|
||||
invite: ModelObject<UserInvite>,
|
||||
) {
|
||||
const tenant = await this.tenancyContext.getTenant(true);
|
||||
const root = path.join(global.__views_dir, '/images/bigcapital.png');
|
||||
const root = path.join(global.__images_dirname, '/bigcapital.png');
|
||||
const baseURL = this.configService.get('baseURL');
|
||||
|
||||
const mail = new Mail()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { JOB_REF, Process, Processor } from '@nestjs/bull';
|
||||
import { Job } from 'bull';
|
||||
import { Inject, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import {
|
||||
SendInviteUserMailJob,
|
||||
@@ -14,19 +13,17 @@ import { SendInviteUsersMailMessage } from '../commands/SendInviteUsersMailMessa
|
||||
name: SendInviteUserMailQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendInviteUserMailProcessor {
|
||||
export class SendInviteUserMailProcessor extends WorkerHost {
|
||||
constructor(
|
||||
private readonly sendInviteUsersMailService: SendInviteUsersMailMessage,
|
||||
@Inject(REQUEST) private readonly request: Request,
|
||||
@Inject(JOB_REF)
|
||||
private readonly jobRef: Job<SendInviteUserMailJobPayload>,
|
||||
private readonly clsService: ClsService,
|
||||
) { }
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(SendInviteUserMailJob)
|
||||
@UseCls()
|
||||
async handleSendInviteMail() {
|
||||
const { fromUser, invite, organizationId, userId } = this.jobRef.data;
|
||||
async process(job: Job<SendInviteUserMailJobPayload>) {
|
||||
const { fromUser, invite, organizationId, userId } = job.data;
|
||||
|
||||
this.clsService.set('organizationId', organizationId);
|
||||
this.clsService.set('userId', userId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Queue } from 'bull';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bullmq';
|
||||
import { events } from '@/common/events/events';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ export default class InviteSendMainNotificationSubscribe {
|
||||
async sendMailNotification({
|
||||
invite,
|
||||
user,
|
||||
invitingUser,
|
||||
}: IUserInviteTenantSyncedEventPayload) {
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
const authedUser = await this.tenancyContext.getSystemUser();
|
||||
@@ -37,7 +38,7 @@ export default class InviteSendMainNotificationSubscribe {
|
||||
const userId = authedUser.id;
|
||||
|
||||
this.sendInviteMailQueue.add(SendInviteUserMailJob, {
|
||||
fromUser: user,
|
||||
fromUser: invitingUser,
|
||||
invite,
|
||||
userId,
|
||||
organizationId,
|
||||
|
||||
@@ -33,7 +33,7 @@ export class SyncSystemSendInviteSubscriber {
|
||||
* @param {IUserInvitedEventPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.inviteUser.sendInvite)
|
||||
async syncSendInviteSystem({ inviteToken, user }: IUserInvitedEventPayload) {
|
||||
async syncSendInviteSystem({ inviteToken, user, invitingUser }: IUserInvitedEventPayload) {
|
||||
const authorizedUser = await this.tenancyContext.getSystemUser();
|
||||
const tenantId = authorizedUser.tenantId;
|
||||
|
||||
@@ -63,6 +63,7 @@ export class SyncSystemSendInviteSubscriber {
|
||||
{
|
||||
invite,
|
||||
user,
|
||||
invitingUser,
|
||||
} as IUserInviteTenantSyncedEventPayload,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ import styled from 'styled-components';
|
||||
import { DataTable } from '../Datatable';
|
||||
|
||||
export const ReportDataTable = styled(DataTable)`
|
||||
--x-table-no-results-border-color: #ddd;
|
||||
|
||||
.bp4-dark & {
|
||||
--x-table-no-results-border-color: var(--color-dark-gray5);
|
||||
}
|
||||
.table .tbody .tr.no-results:last-of-type .td {
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-bottom: 1px solid var(--x-table-no-results-border-color);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import preferencesMenu from '@/constants/preferencesMenu';
|
||||
import { PreferencesMenu } from '@/constants/preferencesMenu';
|
||||
import PreferencesSidebarContainer from './PreferencesSidebarContainer';
|
||||
|
||||
import '@/style/pages/Preferences/Sidebar.scss';
|
||||
@@ -15,7 +15,7 @@ export default function PreferencesSidebar() {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const items = preferencesMenu.map((item) =>
|
||||
const items = PreferencesMenu.map((item) =>
|
||||
item.divider ? (
|
||||
<MenuDivider title={item.title} />
|
||||
) : (
|
||||
|
||||
@@ -10,12 +10,17 @@ const TextStatusRoot = styled.span`
|
||||
${(props) =>
|
||||
props.intent === 'warning' &&
|
||||
`
|
||||
color: #ec5b0a;`}
|
||||
color: #c87619;`}
|
||||
|
||||
${(props) =>
|
||||
props.intent === 'danger' &&
|
||||
`
|
||||
color: #f17377;`}
|
||||
|
||||
${(props) =>
|
||||
props.intent === 'success' &&
|
||||
`
|
||||
color: #2ba01d;`}
|
||||
color: #238551;`}
|
||||
|
||||
${(props) =>
|
||||
props.intent === 'none' &&
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { KeyboardEvent, ReactNode } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { isUndefined } from 'lodash';
|
||||
import {
|
||||
Overlay,
|
||||
@@ -10,11 +8,14 @@ import {
|
||||
MenuItem,
|
||||
Spinner,
|
||||
Intent,
|
||||
OverlayProps,
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
import { QueryList } from '@blueprintjs/select';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
import { Icon, If, ListSelect, FormattedMessage as T } from '@/components';
|
||||
import { QueryList, ItemRenderer } from '@blueprintjs/select';
|
||||
import { x } from '@xstyled/emotion';
|
||||
import { css } from '@emotion/css';
|
||||
import { Icon, If, FormattedMessage as T } from '@/components';
|
||||
import { Select } from '@blueprintjs-formik/select';
|
||||
import {
|
||||
UniversalSearchProvider,
|
||||
useUniversalSearchContext,
|
||||
@@ -22,59 +23,297 @@ import {
|
||||
import { filterItemsByResourceType } from './utils';
|
||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||
|
||||
// Resource type from RESOURCES_TYPES constant
|
||||
type ResourceType = string;
|
||||
|
||||
// Search type option item
|
||||
interface SearchTypeOption {
|
||||
key: ResourceType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Universal search item
|
||||
interface UniversalSearchItem {
|
||||
id: number | string;
|
||||
_type: ResourceType;
|
||||
text: string;
|
||||
subText?: string;
|
||||
label?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// CSS styles for complex selectors
|
||||
const overlayStyles = css`
|
||||
.bp4-overlay-appear,
|
||||
.bp4-overlay-enter {
|
||||
filter: blur(20px);
|
||||
opacity: 0.2;
|
||||
}
|
||||
.bp4-overlay-appear-active,
|
||||
.bp4-overlay-enter-active {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
transition:
|
||||
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
|
||||
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
|
||||
}
|
||||
.bp4-overlay-exit {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
}
|
||||
.bp4-overlay-exit-active {
|
||||
filter: blur(20px);
|
||||
opacity: 0.2;
|
||||
transition:
|
||||
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
|
||||
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
|
||||
}
|
||||
`;
|
||||
|
||||
const containerStyles = css`
|
||||
position: fixed;
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
background-color: var(--color-universal-search-background);
|
||||
border-radius: 3px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(16, 22, 26, 0.1),
|
||||
0 4px 8px rgba(16, 22, 26, 0.2),
|
||||
0 18px 46px 6px rgba(16, 22, 26, 0.2);
|
||||
left: calc(50% - 250px);
|
||||
top: 20vh;
|
||||
width: 500px;
|
||||
z-index: 20;
|
||||
|
||||
.bp4-input-group {
|
||||
.bp4-icon {
|
||||
margin: 16px;
|
||||
color: var(--color-universal-search-icon);
|
||||
|
||||
svg {
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
fill-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
--text-opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp4-input-group .bp4-input {
|
||||
border: 0;
|
||||
box-shadow: 0 0 0 0;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.bp4-input-group.bp4-large .bp4-input:not(:first-child) {
|
||||
padding-left: 50px !important;
|
||||
}
|
||||
.bp4-input-group.bp4-large .bp4-input:not(:last-child) {
|
||||
padding-right: 130px !important;
|
||||
}
|
||||
|
||||
.bp4-menu {
|
||||
border-top: 1px solid var(--color-universal-search-menu-border);
|
||||
max-height: calc(60vh - 20px);
|
||||
overflow: auto;
|
||||
|
||||
.bp4-menu-item {
|
||||
.bp4-text-muted {
|
||||
font-size: 12px;
|
||||
|
||||
.bp4-icon {
|
||||
color: var(--bp4-gray-600);
|
||||
}
|
||||
}
|
||||
&.bp4-intent-primary {
|
||||
&.bp4-active {
|
||||
background-color: var(--bp4-blue-100);
|
||||
color: var(--bp4-dark-gray-800);
|
||||
|
||||
.bp4-menu-item-label {
|
||||
color: var(--bp4-gray-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-label {
|
||||
flex-direction: row;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp4-input-action {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const inputRightElementsStyles = css`
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
|
||||
.bp4-spinner {
|
||||
margin-right: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
const footerStyles = css`
|
||||
padding: 12px 12px;
|
||||
border-top: 1px solid var(--color-universal-search-footer-divider);
|
||||
`;
|
||||
|
||||
const actionBaseStyles = css`
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.bp4-tag {
|
||||
background: var(--color-universal-search-tag-background);
|
||||
color: var(--color-universal-search-tag-text);
|
||||
}
|
||||
`;
|
||||
|
||||
const actionArrowsStyles = css`
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.bp4-tag {
|
||||
background: var(--color-universal-search-tag-background);
|
||||
color: var(--color-universal-search-tag-text);
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
margin-left: 4px;
|
||||
|
||||
svg {
|
||||
fill: var(--color-universal-search-tag-text);
|
||||
height: 100%;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// UniversalSearchInputRightElements props
|
||||
interface UniversalSearchInputRightElementsProps {
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (option: SearchTypeOption) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search input action.
|
||||
*/
|
||||
function UniversalSearchInputRightElements({ onSearchTypeChange }) {
|
||||
const { isLoading, searchType, defaultSearchResource, searchTypeOptions } =
|
||||
function UniversalSearchInputRightElements({
|
||||
onSearchTypeChange,
|
||||
}: UniversalSearchInputRightElementsProps) {
|
||||
const { isLoading, searchType, searchTypeOptions } =
|
||||
useUniversalSearchContext();
|
||||
|
||||
// Find the currently selected item object.
|
||||
const selectedItem = searchTypeOptions.find(
|
||||
(item) => item.key === searchType,
|
||||
);
|
||||
|
||||
// Handle search type option change.
|
||||
const handleSearchTypeChange = (option) => {
|
||||
onSearchTypeChange && onSearchTypeChange(option);
|
||||
const handleSearchTypeChange = (option: SearchTypeOption) => {
|
||||
onSearchTypeChange?.(option);
|
||||
};
|
||||
|
||||
// Item renderer for the select dropdown.
|
||||
const itemRenderer: ItemRenderer<SearchTypeOption> = (
|
||||
item,
|
||||
{ handleClick },
|
||||
) => {
|
||||
return <MenuItem text={item.label} key={item.key} onClick={handleClick} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={CLASSES.UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS}>
|
||||
<x.div display="flex" m="10px" className={inputRightElementsStyles}>
|
||||
<If condition={isLoading}>
|
||||
<Spinner tagName="div" intent={Intent.NONE} size={18} value={null} />
|
||||
<Spinner tagName="div" intent={Intent.NONE} size={18} />
|
||||
</If>
|
||||
|
||||
<ListSelect
|
||||
<Select<SearchTypeOption>
|
||||
items={searchTypeOptions}
|
||||
itemRenderer={itemRenderer}
|
||||
onItemSelect={handleSearchTypeChange}
|
||||
selectedValue={selectedItem?.key}
|
||||
valueAccessor={'key'}
|
||||
labelAccessor={'label'}
|
||||
filterable={false}
|
||||
initialSelectedItem={defaultSearchResource}
|
||||
selectedItem={searchType}
|
||||
selectedItemProp={'key'}
|
||||
textProp={'label'}
|
||||
// defaultText={intl.get('type')}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY,
|
||||
}}
|
||||
buttonProps={{
|
||||
minimal: true,
|
||||
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_BTN,
|
||||
}}
|
||||
input={({ activeItem }) => (
|
||||
<Button minimal={true} text={activeItem?.label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</x.div>
|
||||
);
|
||||
}
|
||||
|
||||
// QueryList renderer props
|
||||
interface QueryListRendererProps {
|
||||
/** Current query string */
|
||||
query: string;
|
||||
/** Callback when query changes */
|
||||
handleQueryChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
/** Item list element */
|
||||
itemList: ReactNode;
|
||||
/** Class name */
|
||||
className?: string;
|
||||
/** Handle key down */
|
||||
handleKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
||||
/** Handle key up */
|
||||
handleKeyUp?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
// UniversalSearchQueryList props
|
||||
interface UniversalSearchQueryListProps {
|
||||
/** Whether the search is open */
|
||||
isOpen: boolean;
|
||||
/** Whether the search is loading */
|
||||
isLoading: boolean;
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (option: SearchTypeOption) => void;
|
||||
/** Current search type */
|
||||
searchType: ResourceType;
|
||||
/** Items to display */
|
||||
items: UniversalSearchItem[];
|
||||
/** Renderer for items */
|
||||
itemRenderer?: ItemRenderer<UniversalSearchItem>;
|
||||
/** Callback when an item is selected */
|
||||
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
|
||||
/** Current query string */
|
||||
query: string;
|
||||
/** Callback when query changes */
|
||||
onQueryChange?: (query: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search query list.
|
||||
*/
|
||||
function UniversalSearchQueryList(props) {
|
||||
const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } =
|
||||
props;
|
||||
|
||||
function UniversalSearchQueryList({
|
||||
isOpen,
|
||||
isLoading,
|
||||
onSearchTypeChange,
|
||||
...restProps
|
||||
}: UniversalSearchQueryListProps) {
|
||||
return (
|
||||
<QueryList
|
||||
{...restProps}
|
||||
<QueryList<UniversalSearchItem>
|
||||
{...(restProps as any)}
|
||||
initialContent={null}
|
||||
renderer={(listProps) => (
|
||||
renderer={(listProps: QueryListRendererProps) => (
|
||||
<UniversalSearchBar
|
||||
isOpen={isOpen}
|
||||
onSearchTypeChange={onSearchTypeChange}
|
||||
@@ -100,47 +339,53 @@ function UniversalSearchQueryList(props) {
|
||||
*/
|
||||
function UniversalQuerySearchActions() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTIONS)}>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_SELECT)}>
|
||||
<x.div display="flex">
|
||||
<x.div className={actionBaseStyles}>
|
||||
<Tag>ENTER</Tag>
|
||||
<span class={'text'}>{intl.get('universal_search.enter_text')}</span>
|
||||
</div>
|
||||
<x.span ml="6px">{intl.get('universal_search.enter_text')}</x.span>
|
||||
</x.div>
|
||||
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_CLOSE)}>
|
||||
<x.div className={actionBaseStyles}>
|
||||
<Tag>ESC</Tag>{' '}
|
||||
<span class={'text'}>{intl.get('universal_search.close_text')}</span>
|
||||
</div>
|
||||
<x.span ml="6px">{intl.get('universal_search.close_text')}</x.span>
|
||||
</x.div>
|
||||
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_ARROWS)}>
|
||||
<x.div className={actionArrowsStyles}>
|
||||
<Tag>
|
||||
<Icon icon={'arrow-up-24'} iconSize={16} />
|
||||
</Tag>
|
||||
<Tag>
|
||||
<Icon icon={'arrow-down-24'} iconSize={16} />
|
||||
</Tag>
|
||||
<span class="text">{intl.get('universal_seach.navigate_text')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<x.span ml="6px">{intl.get('universal_seach.navigate_text')}</x.span>
|
||||
</x.div>
|
||||
</x.div>
|
||||
);
|
||||
}
|
||||
|
||||
// UniversalSearchBar props
|
||||
interface UniversalSearchBarProps extends QueryListRendererProps {
|
||||
/** Whether the search is open */
|
||||
isOpen: boolean;
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (option: SearchTypeOption) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search input bar with items list.
|
||||
*/
|
||||
function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
|
||||
function UniversalSearchBar({
|
||||
isOpen,
|
||||
onSearchTypeChange,
|
||||
...listProps
|
||||
}: UniversalSearchBarProps) {
|
||||
const { handleKeyDown, handleKeyUp } = listProps;
|
||||
const handlers = isOpen
|
||||
? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.UNIVERSAL_SEARCH_OMNIBAR,
|
||||
listProps.className,
|
||||
)}
|
||||
{...handlers}
|
||||
>
|
||||
<x.div {...handlers}>
|
||||
<InputGroup
|
||||
large={true}
|
||||
leftIcon={<Icon icon={'universal-search'} iconSize={20} />}
|
||||
@@ -155,17 +400,44 @@ function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
|
||||
autoFocus={true}
|
||||
/>
|
||||
{listProps.itemList}
|
||||
</div>
|
||||
</x.div>
|
||||
);
|
||||
}
|
||||
|
||||
// UniversalSearch props
|
||||
export interface UniversalSearchProps {
|
||||
/** Default search resource type */
|
||||
defaultSearchResource?: ResourceType;
|
||||
/** Controlled search resource type */
|
||||
searchResource?: ResourceType;
|
||||
/** Overlay props */
|
||||
overlayProps?: OverlayProps;
|
||||
/** Whether the search overlay is open */
|
||||
isOpen: boolean;
|
||||
/** Whether the search is loading */
|
||||
isLoading: boolean;
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (resource: SearchTypeOption) => void;
|
||||
/** Items to display */
|
||||
items: UniversalSearchItem[];
|
||||
/** Available search type options */
|
||||
searchTypeOptions: SearchTypeOption[];
|
||||
/** Renderer for items */
|
||||
itemRenderer?: ItemRenderer<UniversalSearchItem>;
|
||||
/** Callback when an item is selected */
|
||||
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
|
||||
/** Current query string */
|
||||
query: string;
|
||||
/** Callback when query changes */
|
||||
onQueryChange?: (query: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search.
|
||||
*/
|
||||
export function UniversalSearch({
|
||||
defaultSearchResource,
|
||||
searchResource,
|
||||
|
||||
overlayProps,
|
||||
isOpen,
|
||||
isLoading,
|
||||
@@ -173,9 +445,9 @@ export function UniversalSearch({
|
||||
items,
|
||||
searchTypeOptions,
|
||||
...queryListProps
|
||||
}) {
|
||||
}: UniversalSearchProps) {
|
||||
// Search type state.
|
||||
const [searchType, setSearchType] = React.useState(
|
||||
const [searchType, setSearchType] = React.useState<ResourceType>(
|
||||
defaultSearchResource || RESOURCES_TYPES.CUSTOMER,
|
||||
);
|
||||
// Handle search resource type controlled mode.
|
||||
@@ -189,9 +461,9 @@ export function UniversalSearch({
|
||||
}, [searchResource, defaultSearchResource]);
|
||||
|
||||
// Handle search type change.
|
||||
const handleSearchTypeChange = (searchTypeResource) => {
|
||||
const handleSearchTypeChange = (searchTypeResource: SearchTypeOption) => {
|
||||
setSearchType(searchTypeResource.key);
|
||||
onSearchTypeChange && onSearchTypeChange(searchTypeResource);
|
||||
onSearchTypeChange?.(searchTypeResource);
|
||||
};
|
||||
// Filters query list items based on the given search type.
|
||||
const filteredItems = filterItemsByResourceType(items, searchType);
|
||||
@@ -200,7 +472,7 @@ export function UniversalSearch({
|
||||
<Overlay
|
||||
hasBackdrop={true}
|
||||
isOpen={isOpen}
|
||||
className={classNames(CLASSES.UNIVERSAL_SEARCH_OVERLAY)}
|
||||
className={overlayStyles}
|
||||
{...overlayProps}
|
||||
>
|
||||
<UniversalSearchProvider
|
||||
@@ -209,7 +481,7 @@ export function UniversalSearch({
|
||||
defaultSearchResource={defaultSearchResource}
|
||||
searchTypeOptions={searchTypeOptions}
|
||||
>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH)}>
|
||||
<x.div className={containerStyles}>
|
||||
<UniversalSearchQueryList
|
||||
isOpen={isOpen}
|
||||
isLoading={isLoading}
|
||||
@@ -218,10 +490,10 @@ export function UniversalSearch({
|
||||
{...queryListProps}
|
||||
items={filteredItems}
|
||||
/>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_FOOTER)}>
|
||||
<x.div className={footerStyles}>
|
||||
<UniversalQuerySearchActions />
|
||||
</div>
|
||||
</div>
|
||||
</x.div>
|
||||
</x.div>
|
||||
</UniversalSearchProvider>
|
||||
</Overlay>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,82 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import React, { createContext, ReactNode, useContext } from 'react';
|
||||
|
||||
const UniversalSearchContext = createContext();
|
||||
// The resource type value from RESOURCES_TYPES constant
|
||||
type ResourceType = string;
|
||||
|
||||
// Search type option item
|
||||
interface SearchTypeOption {
|
||||
key: ResourceType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Context value type
|
||||
interface UniversalSearchContextValue {
|
||||
/** Whether the search is loading */
|
||||
isLoading: boolean;
|
||||
/** Current search type/resource type */
|
||||
searchType: ResourceType;
|
||||
/** Default search resource type */
|
||||
defaultSearchResource?: ResourceType;
|
||||
/** List of available search type options */
|
||||
searchTypeOptions: SearchTypeOption[];
|
||||
}
|
||||
|
||||
// Create the context with undefined as initial value
|
||||
const UniversalSearchContext = createContext<
|
||||
UniversalSearchContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
// Provider props interface
|
||||
interface UniversalSearchProviderProps {
|
||||
/** Whether the search is loading */
|
||||
isLoading: boolean;
|
||||
/** Default search resource type */
|
||||
defaultSearchResource?: ResourceType;
|
||||
/** Current search type/resource type */
|
||||
searchType: ResourceType;
|
||||
/** List of available search type options */
|
||||
searchTypeOptions: SearchTypeOption[];
|
||||
/** Child elements */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search data provider.
|
||||
*/
|
||||
function UniversalSearchProvider({
|
||||
export function UniversalSearchProvider({
|
||||
isLoading,
|
||||
defaultSearchResource,
|
||||
searchType,
|
||||
searchTypeOptions,
|
||||
...props
|
||||
}) {
|
||||
children,
|
||||
}: UniversalSearchProviderProps) {
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
const provider: UniversalSearchContextValue = {
|
||||
isLoading,
|
||||
searchType,
|
||||
defaultSearchResource,
|
||||
searchTypeOptions,
|
||||
};
|
||||
|
||||
return <UniversalSearchContext.Provider value={provider} {...props} />;
|
||||
return (
|
||||
<UniversalSearchContext.Provider value={provider}>
|
||||
{children}
|
||||
</UniversalSearchContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useUniversalSearchContext = () =>
|
||||
React.useContext(UniversalSearchContext);
|
||||
/**
|
||||
* Hook to access the universal search context.
|
||||
* @throws Error if used outside of UniversalSearchProvider
|
||||
*/
|
||||
export const useUniversalSearchContext = (): UniversalSearchContextValue => {
|
||||
const context = useContext(UniversalSearchContext);
|
||||
|
||||
export { UniversalSearchProvider, useUniversalSearchContext };
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useUniversalSearchContext must be used within a UniversalSearchProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
export const If = (props) =>
|
||||
props.condition ? (props.render ? props.render() : props.children) : null;
|
||||
interface IfProps {
|
||||
condition: boolean;
|
||||
children?: ReactNode;
|
||||
render?: () => ReactNode;
|
||||
}
|
||||
|
||||
If.propTypes = {
|
||||
// condition: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
render: PropTypes.func,
|
||||
};
|
||||
export const If = (props: IfProps): React.ReactElement | null =>
|
||||
props.condition ? (props.render ? <>{props.render()}</> : <>{props.children}</>) : null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export default [
|
||||
export const AllocateLandedCostType = [
|
||||
{ name: intl.get('bills'), value: 'Bill' },
|
||||
{ name: intl.get('expenses'), value: 'Expense' },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
export default {
|
||||
export const App = {
|
||||
"app_name": "BigCapital",
|
||||
"app_version": "0.0.1 (build 12344)",
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export default [
|
||||
export const ContactsOptions = [
|
||||
{ name: intl.get('customer'), path: 'customers' },
|
||||
{ name: intl.get('vendor'), path: 'vendors' },
|
||||
];
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
VendorAction,
|
||||
} from './abilityOption';
|
||||
|
||||
export default [
|
||||
export const KeyboardShortcutsOptions = [
|
||||
{
|
||||
shortcut_key: 'Shift + I',
|
||||
description: intl.get('jump_to_the_invoices'),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
|
||||
export default [
|
||||
export const PreferencesMenu = [
|
||||
{
|
||||
text: <T id={'general'} />,
|
||||
disabled: false,
|
||||
@@ -13,10 +13,10 @@ export default [
|
||||
disabled: false,
|
||||
href: '/preferences/branding',
|
||||
},
|
||||
{
|
||||
text: 'Billing',
|
||||
href: '/preferences/billing',
|
||||
},
|
||||
// {
|
||||
// text: 'Billing',
|
||||
// href: '/preferences/billing',
|
||||
// },
|
||||
{
|
||||
text: <T id={'users'} />,
|
||||
href: '/preferences/users',
|
||||
@@ -63,11 +63,11 @@ export default [
|
||||
disabled: false,
|
||||
href: '/preferences/items',
|
||||
},
|
||||
{
|
||||
text: 'Integrations',
|
||||
disabled: false,
|
||||
href: '/preferences/integrations'
|
||||
},
|
||||
// {
|
||||
// text: 'Integrations',
|
||||
// disabled: false,
|
||||
// href: '/preferences/integrations'
|
||||
// },
|
||||
{
|
||||
text: 'API Keys',
|
||||
disabled: false,
|
||||
|
||||
@@ -32,38 +32,38 @@ export default function MakeJournalFloatingAction() {
|
||||
|
||||
// Handle submit & publish button click.
|
||||
const handleSubmitPublishBtnClick = (event) => {
|
||||
submitForm();
|
||||
setSubmitPayload({ redirect: true, publish: true });
|
||||
submitForm();
|
||||
};
|
||||
|
||||
// Handle submit, publish & new button click.
|
||||
const handleSubmitPublishAndNewBtnClick = (event) => {
|
||||
submitForm();
|
||||
setSubmitPayload({ redirect: false, publish: true, resetForm: true });
|
||||
submitForm();
|
||||
};
|
||||
|
||||
// Handle submit, publish & edit button click.
|
||||
const handleSubmitPublishContinueEditingBtnClick = (event) => {
|
||||
submitForm();
|
||||
setSubmitPayload({ redirect: false, publish: true });
|
||||
submitForm();
|
||||
};
|
||||
|
||||
// Handle submit as draft button click.
|
||||
const handleSubmitDraftBtnClick = (event) => {
|
||||
submitForm();
|
||||
setSubmitPayload({ redirect: true, publish: false });
|
||||
submitForm();
|
||||
};
|
||||
|
||||
// Handle submit as draft & new button click.
|
||||
const handleSubmitDraftAndNewBtnClick = (event) => {
|
||||
submitForm();
|
||||
setSubmitPayload({ redirect: false, publish: false, resetForm: true });
|
||||
submitForm();
|
||||
};
|
||||
|
||||
// Handle submit as draft & continue editing button click.
|
||||
const handleSubmitDraftContinueEditingBtnClick = (event) => {
|
||||
submitForm();
|
||||
setSubmitPayload({ redirect: false, publish: false });
|
||||
submitForm();
|
||||
};
|
||||
|
||||
// Handle cancel button click.
|
||||
|
||||
@@ -8,6 +8,11 @@ import { If, AppToaster } from '@/components';
|
||||
import { NormalCell, BalanceCell, BankBalanceCell } from './components';
|
||||
import { transformTableStateToQuery, isBlank } from '@/utils';
|
||||
|
||||
export const DeleteAccountTypeError = {
|
||||
AccountPredefined: 'account_predefined',
|
||||
AccountHasAssociatedTransactions: 'account_has_associated_transactions',
|
||||
};
|
||||
|
||||
/**
|
||||
* Account name accessor.
|
||||
*/
|
||||
@@ -26,13 +31,13 @@ export const accountNameAccessor = (account) => {
|
||||
* Handle delete errors in bulk and singular.
|
||||
*/
|
||||
export const handleDeleteErrors = (errors) => {
|
||||
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
|
||||
if (errors.find((e) => e.type === DeleteAccountTypeError.AccountPredefined)) {
|
||||
AppToaster.show({
|
||||
message: intl.get('cannot_delete_predefined_accounts'),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
|
||||
if (errors.find((e) => e.type === DeleteAccountTypeError.AccountHasAssociatedTransactions)) {
|
||||
AppToaster.show({
|
||||
message: intl.get('cannot_delete_account_has_associated_transactions'),
|
||||
intent: Intent.DANGER,
|
||||
|
||||
@@ -17,8 +17,8 @@ const Schema = Yup.object().shape({
|
||||
.label(intl.get('display_name_')),
|
||||
|
||||
email: Yup.string().email().nullable(),
|
||||
work_phone: Yup.number(),
|
||||
personal_phone: Yup.number(),
|
||||
work_phone: Yup.string().nullable(),
|
||||
personal_phone: Yup.string().nullable(),
|
||||
website: Yup.string().url().nullable(),
|
||||
|
||||
active: Yup.boolean(),
|
||||
@@ -30,7 +30,7 @@ const Schema = Yup.object().shape({
|
||||
billing_address_city: Yup.string().trim(),
|
||||
billing_address_state: Yup.string().trim(),
|
||||
billing_address_postcode: Yup.string().nullable(),
|
||||
billing_address_phone: Yup.number(),
|
||||
billing_address_phone: Yup.string().nullable(),
|
||||
|
||||
shipping_address_country: Yup.string().trim(),
|
||||
shipping_address_1: Yup.string().trim(),
|
||||
@@ -38,7 +38,7 @@ const Schema = Yup.object().shape({
|
||||
shipping_address_city: Yup.string().trim(),
|
||||
shipping_address_state: Yup.string().trim(),
|
||||
shipping_address_postcode: Yup.string().nullable(),
|
||||
shipping_address_phone: Yup.number(),
|
||||
shipping_address_phone: Yup.string().nullable(),
|
||||
|
||||
opening_balance: Yup.number().nullable(),
|
||||
currency_code: Yup.string(),
|
||||
|
||||
@@ -16,7 +16,7 @@ import { FormattedMessage as T, If, FFormGroup, FSelect, FRadioGroup, FInputGrou
|
||||
import { handleStringChange } from '@/utils';
|
||||
import { FieldRequiredHint } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import allocateLandedCostType from '@/constants/allocateLandedCostType';
|
||||
import { AllocateLandedCostType } from '@/constants/allocateLandedCostType';
|
||||
|
||||
import AllocateLandedCostFormBody from './AllocateLandedCostFormBody';
|
||||
import {
|
||||
@@ -81,7 +81,7 @@ export default function AllocateLandedCostFormFields() {
|
||||
>
|
||||
<FSelect
|
||||
name={'transaction_type'}
|
||||
items={allocateLandedCostType}
|
||||
items={AllocateLandedCostType}
|
||||
onItemChange={handleTransactionTypeChange}
|
||||
filterable={false}
|
||||
valueAccessor={'value'}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { FormattedMessage as T } from '@/components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useContactDuplicateFromContext } from './ContactDuplicateProvider';
|
||||
|
||||
import Contacts from '@/constants/contactsOptions';
|
||||
import { ContactsOptions } from '@/constants/contactsOptions';
|
||||
|
||||
import { withDialogActions } from '@/containers/Dialog/withDialogActions';
|
||||
import { compose } from '@/utils';
|
||||
@@ -66,7 +66,7 @@ function ContactDuplicateForm({
|
||||
>
|
||||
<FSelect
|
||||
name={'contact_type'}
|
||||
items={Contacts}
|
||||
items={ContactsOptions}
|
||||
placeholder={<T id={'select_contact'} />}
|
||||
textAccessor={'name'}
|
||||
valueAccessor={'path'}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useContactDetailDrawerContext } from './ContactDetailDrawerProvider';
|
||||
import { withAlertActions } from '@/containers/Alert/withAlertActions';
|
||||
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
import { DashboardActionsBar, Icon, FormattedMessage as T } from '@/components';
|
||||
import { DrawerActionsBar, Icon, FormattedMessage as T } from '@/components';
|
||||
|
||||
import { safeCallback, compose } from '@/utils';
|
||||
|
||||
@@ -46,7 +46,7 @@ function ContactDetailActionsBar({
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
@@ -63,7 +63,7 @@ function ContactDetailActionsBar({
|
||||
onClick={safeCallback(onDeleteContact)}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
</DrawerActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import { withDialogActions } from '@/containers/Dialog/withDialogActions';
|
||||
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
import {
|
||||
DashboardActionsBar,
|
||||
Can,
|
||||
Icon,
|
||||
FormattedMessage as T,
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
If,
|
||||
Icon,
|
||||
FormattedMessage as T,
|
||||
DashboardActionsBar,
|
||||
DrawerActionsBar,
|
||||
Can,
|
||||
} from '@/components';
|
||||
|
||||
@@ -63,7 +63,7 @@ function VendorCreditDetailActionsBar({
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
<Can I={VendorCreditAction.Edit} a={AbilitySubject.VendorCredit}>
|
||||
<Button
|
||||
@@ -105,7 +105,7 @@ function VendorCreditDetailActionsBar({
|
||||
</If>
|
||||
</Can>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
</DrawerActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { getColumnWidth } from '@/utils';
|
||||
import * as R from 'ramda';
|
||||
import { useGeneralLedgerContext } from './GeneralLedgerProvider';
|
||||
import { Align } from '@/constants';
|
||||
import { Align, CLASSES } from '@/constants';
|
||||
|
||||
/**
|
||||
* Description cell – wraps value in a div with muted text class.
|
||||
*/
|
||||
function DescriptionCell({ cell: { value } }) {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: `cell ${CLASSES.TEXT_MUTED}` },
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
|
||||
|
||||
@@ -75,6 +87,16 @@ const transactionIdColumnAccessor = (column) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Description column accessor (muted text in wrapped cell).
|
||||
*/
|
||||
const descriptionColumnAccessor = (column) => {
|
||||
return {
|
||||
...column,
|
||||
Cell: DescriptionCell,
|
||||
};
|
||||
};
|
||||
|
||||
const dynamiColumnMapper = R.curry((data, column) => {
|
||||
const _numericColumnAccessor = numericColumnAccessor(data);
|
||||
|
||||
@@ -88,6 +110,7 @@ const dynamiColumnMapper = R.curry((data, column) => {
|
||||
R.pathEq(['key'], 'reference_number'),
|
||||
transactionIdColumnAccessor,
|
||||
),
|
||||
R.when(R.pathEq(['key'], 'description'), descriptionColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'amount'), _numericColumnAccessor),
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
// @ts-nocheck
|
||||
import { Align } from '@/constants';
|
||||
import React from 'react';
|
||||
import { Align, CLASSES } from '@/constants';
|
||||
import { getColumnWidth } from '@/utils';
|
||||
import * as R from 'ramda';
|
||||
import { useJournalSheetContext } from './JournalProvider';
|
||||
|
||||
/**
|
||||
* Description cell – wraps value in a div with muted text class.
|
||||
*/
|
||||
function DescriptionCell({ cell: { value } }) {
|
||||
return React.createElement(
|
||||
'span',
|
||||
{ className: `cell ${CLASSES.TEXT_MUTED}` },
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
|
||||
|
||||
const getReportColWidth = (data, accessor, headerText) => {
|
||||
@@ -86,6 +98,16 @@ const accountCodeColumnAccessor = (column) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Description column accessor (muted text in wrapped cell).
|
||||
*/
|
||||
const descriptionColumnAccessor = (column) => {
|
||||
return {
|
||||
...column,
|
||||
Cell: DescriptionCell,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic column mapper.
|
||||
* @param {} data -
|
||||
@@ -105,6 +127,7 @@ const dynamicColumnMapper = R.curry((data, column) => {
|
||||
R.pathEq(['key'], 'transaction_number'),
|
||||
transactionNumberColumnAccessor,
|
||||
),
|
||||
R.when(R.pathEq(['key'], 'description'), descriptionColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'account_code'), accountCodeColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor),
|
||||
R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor),
|
||||
@@ -113,7 +136,7 @@ const dynamicColumnMapper = R.curry((data, column) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Composes the fetched dynamic columns from the server to the columns to pass it
|
||||
* Composes the fetched dynamic columns from the server to the columns to pass it
|
||||
* to the table component.
|
||||
*/
|
||||
export const dynamicColumns = (columns, data) => {
|
||||
|
||||
@@ -14,6 +14,18 @@ import { useSettingsSelector } from '@/hooks/state';
|
||||
import { transformItemFormData } from './ItemForm.schema';
|
||||
import { useWatch } from '@/hooks/utils';
|
||||
|
||||
/**
|
||||
* Error types for item operations.
|
||||
*/
|
||||
export const ItemErrorType = {
|
||||
ItemNameExists: 'ITEM_NAME_EXISTS',
|
||||
InventoryAccountCannotModified: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
TypeCannotChangeWithItemHasTransactions: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
ItemHasAssociatedTransactions: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
|
||||
ItemHasAssociatedInventoryAdjustment: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||
ItemHasAssociatedTransactionsPlural: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||
} as const;
|
||||
|
||||
const defaultInitialValues = {
|
||||
active: 1,
|
||||
name: '',
|
||||
@@ -74,7 +86,7 @@ export const transitionItemTypeKeyToLabel = (itemTypeKey) => {
|
||||
// handle delete errors.
|
||||
export const handleDeleteErrors = (errors) => {
|
||||
if (
|
||||
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTINS')
|
||||
errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactions)
|
||||
) {
|
||||
AppToaster.show({
|
||||
message: intl.get('the_item_has_associated_transactions'),
|
||||
@@ -84,7 +96,7 @@ export const handleDeleteErrors = (errors) => {
|
||||
|
||||
if (
|
||||
errors.find(
|
||||
(error) => error.type === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||
(error) => error.type === ItemErrorType.ItemHasAssociatedInventoryAdjustment,
|
||||
)
|
||||
) {
|
||||
AppToaster.show({
|
||||
@@ -96,7 +108,7 @@ export const handleDeleteErrors = (errors) => {
|
||||
}
|
||||
if (
|
||||
errors.find(
|
||||
(error) => error.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
(error) => error.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions,
|
||||
)
|
||||
) {
|
||||
AppToaster.show({
|
||||
@@ -107,7 +119,7 @@ export const handleDeleteErrors = (errors) => {
|
||||
});
|
||||
}
|
||||
if (
|
||||
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS')
|
||||
errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactionsPlural)
|
||||
) {
|
||||
AppToaster.show({
|
||||
message: intl.get('item.error.you_could_not_delete_item_has_associated'),
|
||||
@@ -214,10 +226,10 @@ export const transformSubmitRequestErrors = (error) => {
|
||||
} = error;
|
||||
const fields = {};
|
||||
|
||||
if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) {
|
||||
if (errors.find((e) => e.type === ItemErrorType.ItemNameExists)) {
|
||||
fields.name = intl.get('the_name_used_before');
|
||||
}
|
||||
if (errors.find((e) => e.type === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED')) {
|
||||
if (errors.find((e) => e.type === ItemErrorType.InventoryAccountCannotModified)) {
|
||||
AppToaster.show({
|
||||
message: intl.get('cannot_change_item_inventory_account'),
|
||||
intent: Intent.DANGER,
|
||||
@@ -225,7 +237,7 @@ export const transformSubmitRequestErrors = (error) => {
|
||||
}
|
||||
if (
|
||||
errors.find(
|
||||
(e) => e.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
(e) => e.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions,
|
||||
)
|
||||
) {
|
||||
AppToaster.show({
|
||||
|
||||
@@ -87,7 +87,7 @@ export function WarehousesGridItemBox({
|
||||
<WarehouseBoxRoot>
|
||||
<WarehouseHeader>
|
||||
<WarehouseTitle>
|
||||
{title} {primary && <Icon icon={'star-18dp'} iconSize={16} />}
|
||||
{title} {primary ? <Icon icon={'star-18dp'} iconSize={16} /> : null}
|
||||
</WarehouseTitle>
|
||||
<WarehouseCode>{code}</WarehouseCode>
|
||||
<WarehouseIcon>
|
||||
@@ -118,12 +118,21 @@ export const WarehousesList = styled.div`
|
||||
`;
|
||||
|
||||
export const WarehouseBoxRoot = styled.div`
|
||||
--x-box-border-color: #c8cad0;
|
||||
--x-box-background-color: #fff;
|
||||
--x-box-hover-border-color: #0153cc;
|
||||
|
||||
.bp4-dark & {
|
||||
--x-box-border-color: rgba(255, 255, 255, 0.2);
|
||||
--x-box-background-color: var(--color-dark-gray3);
|
||||
--x-box-hover-border-color: #0153cc;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #c8cad0;
|
||||
background: #fff;
|
||||
border: 1px solid var(--x-box-border-color);
|
||||
background: var(--x-box-background-color);
|
||||
margin: 5px 5px 8px;
|
||||
width: 200px;
|
||||
height: 160px;
|
||||
@@ -132,7 +141,7 @@ export const WarehouseBoxRoot = styled.div`
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
border-color: #0153cc;
|
||||
border-color: var(--x-box-hover-border-color);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -143,9 +152,16 @@ export const WarehouseHeader = styled.div`
|
||||
`;
|
||||
|
||||
export const WarehouseTitle = styled.div`
|
||||
--x-title-color: #000;
|
||||
--x-title-icon-color: #e1b31d;
|
||||
|
||||
.bp4-dark & {
|
||||
--x-title-color: var(--color-light-gray5);
|
||||
--x-title-icon-color: #e1b31d;
|
||||
}
|
||||
font-size: 14px;
|
||||
font-style: inherit;
|
||||
color: #000;
|
||||
color: var(--x-title-color);
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
@@ -154,14 +170,19 @@ export const WarehouseTitle = styled.div`
|
||||
margin: 0;
|
||||
margin-left: 2px;
|
||||
vertical-align: top;
|
||||
color: #e1b31d;
|
||||
color: var(--x-title-icon-color);
|
||||
}
|
||||
`;
|
||||
|
||||
const WarehouseCode = styled.div`
|
||||
--x-code-color: #6b7176;
|
||||
|
||||
.bp4-dark & {
|
||||
--x-code-color: var(--color-muted-text);
|
||||
}
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #6b7176;
|
||||
color: var(--x-code-color);
|
||||
margin-top: 4px;
|
||||
`;
|
||||
|
||||
@@ -178,8 +199,13 @@ const WarehouseContent = styled.div`
|
||||
`;
|
||||
|
||||
const WarehouseItem = styled.div`
|
||||
--x-item-color: #000;
|
||||
|
||||
.bp4-dark & {
|
||||
--x-item-color: var(--color-light-gray1);
|
||||
}
|
||||
font-size: 11px;
|
||||
color: #000;
|
||||
color: var(--x-item-color);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MenuItem, Intent } from '@blueprintjs/core';
|
||||
|
||||
import { formattedAmount } from '@/utils';
|
||||
import { T, Icon, Choose, If } from '@/components';
|
||||
import { T, Icon, Choose, If, TextStatus } from '@/components';
|
||||
|
||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||
import { AbilitySubject, BillAction } from '@/constants/abilityOption';
|
||||
@@ -41,35 +41,35 @@ export function BillStatus({ bill }) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={bill.is_fully_paid && bill.is_open}>
|
||||
<span class="fully-paid-text">
|
||||
<TextStatus intent={Intent.SUCCESS}>
|
||||
<T id={'paid'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
<Choose.When condition={bill.is_open}>
|
||||
<Choose>
|
||||
<Choose.When condition={bill.is_overdue}>
|
||||
<span className={'overdue-status'}>
|
||||
<TextStatus intent={Intent.DANGER}>
|
||||
{intl.get('overdue_by', { overdue: bill.overdue_days })}
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span className={'due-status'}>
|
||||
<TextStatus intent={Intent.WARNING}>
|
||||
{intl.get('due_in', { due: bill.remaining_days })}
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
<If condition={bill.is_partially_paid}>
|
||||
<span className="partial-paid">
|
||||
<TextStatus intent={Intent.WARNING}>
|
||||
{intl.get('day_partially_paid', {
|
||||
due: formattedAmount(bill.due_amount, bill.currency_code),
|
||||
})}
|
||||
</span>
|
||||
</TextStatus>
|
||||
</If>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span class="draft">
|
||||
<TextStatus intent={Intent.NONE}>
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
import { Box } from '@/components';
|
||||
|
||||
export function CreditNoteCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
@@ -45,7 +46,9 @@ function CreditNoteCustomizeFormContent() {
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<CreditNotePaperTemplateFormConnected />
|
||||
<Box overflow="auto" flex="1 1" px={4} py={6}>
|
||||
<CreditNotePaperTemplateFormConnected />
|
||||
</Box>
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
import { Box } from '@/components';
|
||||
|
||||
export function EstimateCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
@@ -44,7 +45,9 @@ function EstimateCustomizeFormContent() {
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<EstimatePaperTemplateFormConnected />
|
||||
<Box overflow="auto" flex="1 1" px={4} py={6}>
|
||||
<EstimatePaperTemplateFormConnected />
|
||||
</Box>
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MenuItem, Intent } from '@blueprintjs/core';
|
||||
|
||||
import { Choose, T, Icon } from '@/components';
|
||||
import { Choose, T, Icon, TextStatus } from '@/components';
|
||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||
import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption';
|
||||
|
||||
@@ -37,28 +37,28 @@ export const EstimateUniversalSearchSelect = withDrawerActions(
|
||||
export const EstimateStatus = ({ estimate }) => (
|
||||
<Choose>
|
||||
<Choose.When condition={estimate.is_delivered && estimate.is_approved}>
|
||||
<span class="approved">
|
||||
<TextStatus intent={Intent.SUCCESS}>
|
||||
<T id={'approved'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
<Choose.When condition={estimate.is_delivered && estimate.is_rejected}>
|
||||
<span class="reject">
|
||||
<TextStatus intent={Intent.DANGER}>
|
||||
<T id={'rejected'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
<Choose.When
|
||||
condition={
|
||||
estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved
|
||||
}
|
||||
>
|
||||
<span class="delivered">
|
||||
<TextStatus intent={Intent.SUCCESS}>
|
||||
<T id={'delivered'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span class="draft">
|
||||
<TextStatus intent={Intent.NONE}>
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MenuItem, Intent } from '@blueprintjs/core';
|
||||
|
||||
import { T, Choose, Icon } from '@/components';
|
||||
import { T, Choose, Icon, TextStatus } from '@/components';
|
||||
import { highlightText } from '@/utils';
|
||||
|
||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||
@@ -39,29 +39,29 @@ function InvoiceStatus({ customer }) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={customer.is_fully_paid && customer.is_delivered}>
|
||||
<span class="status status-success">
|
||||
<TextStatus intent={Intent.SUCCESS}>
|
||||
<T id={'paid'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.When condition={customer.is_delivered}>
|
||||
<Choose>
|
||||
<Choose.When condition={customer.is_overdue}>
|
||||
<span className={'status status-warning'}>
|
||||
<TextStatus intent={Intent.DANGER}>
|
||||
{intl.get('overdue_by', { overdue: customer.overdue_days })}
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span className={'status status-warning'}>
|
||||
<TextStatus intent={Intent.WARNING}>
|
||||
{intl.get('due_in', { due: customer.remaining_days })}
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span class="status status--gray">
|
||||
<TextStatus intent={Intent.NONE}>
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
@@ -94,7 +94,6 @@ export function InvoiceUniversalSearchItem(
|
||||
</>
|
||||
}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--invoice'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useDrawerActions } from '@/hooks/state';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
import { Box } from '@/components';
|
||||
|
||||
export function PaymentReceivedCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
@@ -51,7 +52,9 @@ function PaymentReceivedCustomizeFormContent() {
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<PaymentReceivedPaperTemplateFormConnected />
|
||||
<Box overflow="auto" flex="1 1" px={4} py={6}>
|
||||
<PaymentReceivedPaperTemplateFormConnected />
|
||||
</Box>
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
import { Box } from '@/components';
|
||||
|
||||
export function ReceiptCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
@@ -44,7 +45,9 @@ function ReceiptCustomizeFormContent() {
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<ReceiptPaperTemplateFormConnected />
|
||||
<Box overflow="auto" flex="1 1" px={4} py={6}>
|
||||
<ReceiptPaperTemplateFormConnected />
|
||||
</Box>
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { Icon, Choose, T } from '@/components';
|
||||
import { MenuItem, Intent } from '@blueprintjs/core';
|
||||
import { Icon, Choose, T, TextStatus } from '@/components';
|
||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||
import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption';
|
||||
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
|
||||
@@ -39,15 +38,15 @@ function ReceiptStatus({ receipt }) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={receipt.is_closed}>
|
||||
<span class="closed">
|
||||
<TextStatus intent={Intent.SUCCESS}>
|
||||
<T id={'closed'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<span class="draft">
|
||||
<TextStatus intent={Intent.NONE}>
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</TextStatus>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Intent, Tag } from '@blueprintjs/core';
|
||||
import { Intent, Tag, Classes } from '@blueprintjs/core';
|
||||
import { Align } from '@/constants';
|
||||
import styled from 'styled-components';
|
||||
import clsx from 'classnames';
|
||||
|
||||
const codeAccessor = (taxRate) => {
|
||||
return (
|
||||
@@ -28,13 +28,17 @@ const nameAccessor = (taxRate) => {
|
||||
return (
|
||||
<>
|
||||
<span>{taxRate.name}</span>
|
||||
{!!taxRate.is_compound && <CompoundText>(Compound tax)</CompoundText>}
|
||||
{!!taxRate.is_compound && (
|
||||
<span className={clsx(Classes.TEXT_MUTED)}>(Compound tax)</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DescriptionAccessor = (taxRate) => {
|
||||
return <DescriptionText>{taxRate.description}</DescriptionText>;
|
||||
return (
|
||||
<span className={clsx(Classes.TEXT_MUTED)}>{taxRate.description}</span>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -72,11 +76,3 @@ export const useTaxRatesTableColumns = () => {
|
||||
];
|
||||
};
|
||||
|
||||
const CompoundText = styled('span')`
|
||||
color: #738091;
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const DescriptionText = styled('span')`
|
||||
color: #5f6b7c;
|
||||
`;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
import { AppToaster, Can, DashboardActionsBar, Icon } from '@/components';
|
||||
import { AppToaster, Can, DrawerActionsBar, Icon } from '@/components';
|
||||
import { AbilitySubject, TaxRateAction } from '@/constants/abilityOption';
|
||||
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
|
||||
import { withAlertActions } from '@/containers/Alert/withAlertActions';
|
||||
@@ -83,7 +83,7 @@ function TaxRateDetailsContentActionsBar({
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
<Can I={TaxRateAction.Edit} a={AbilitySubject.TaxRate}>
|
||||
<Button
|
||||
@@ -137,7 +137,7 @@ function TaxRateDetailsContentActionsBar({
|
||||
</Popover>
|
||||
</Can>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
</DrawerActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,9 +74,13 @@ const TaxRateHeader = styled(`div`)`
|
||||
const TaxRateAmount = styled('div')`
|
||||
line-height: 1;
|
||||
font-size: 30px;
|
||||
color: #565b71;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
color: var(--x-color-amount-text, #565b71);
|
||||
|
||||
.bp4-dark & {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
`;
|
||||
|
||||
const TaxRateActiveTag = styled(Tag)`
|
||||
|
||||
@@ -10,8 +10,8 @@ const Schema = Yup.object().shape({
|
||||
display_name: Yup.string().trim().required().label(intl.get('display_name_')),
|
||||
|
||||
email: Yup.string().email().nullable(),
|
||||
work_phone: Yup.number(),
|
||||
personal_phone: Yup.number(),
|
||||
work_phone: Yup.string().nullable(),
|
||||
personal_phone: Yup.string().nullable(),
|
||||
website: Yup.string().url().nullable(),
|
||||
|
||||
active: Yup.boolean(),
|
||||
@@ -23,7 +23,7 @@ const Schema = Yup.object().shape({
|
||||
billing_address_city: Yup.string().trim(),
|
||||
billing_address_state: Yup.string().trim(),
|
||||
billing_address_postcode: Yup.string().nullable(),
|
||||
billing_address_phone: Yup.number(),
|
||||
billing_address_phone: Yup.string().nullable(),
|
||||
|
||||
shipping_address_country: Yup.string().trim(),
|
||||
shipping_address_1: Yup.string().trim(),
|
||||
@@ -31,7 +31,7 @@ const Schema = Yup.object().shape({
|
||||
shipping_address_city: Yup.string().trim(),
|
||||
shipping_address_state: Yup.string().trim(),
|
||||
shipping_address_postcode: Yup.string().nullable(),
|
||||
shipping_address_phone: Yup.number(),
|
||||
shipping_address_phone: Yup.string().nullable(),
|
||||
|
||||
opening_balance: Yup.number().nullable(),
|
||||
currency_code: Yup.string(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import keyboardShortcuts from '@/constants/keyboardShortcutsOptions';
|
||||
import { KeyboardShortcutsOptions } from '@/constants/keyboardShortcutsOptions';
|
||||
import { useAbilitiesFilter } from '../utils/useAbilityContext';
|
||||
|
||||
/**
|
||||
@@ -10,7 +10,7 @@ export const useKeywordShortcuts = () => {
|
||||
const abilitiesFilter = useAbilitiesFilter();
|
||||
|
||||
return React.useMemo(
|
||||
() => abilitiesFilter(keyboardShortcuts),
|
||||
() => abilitiesFilter(KeyboardShortcutsOptions),
|
||||
[abilitiesFilter],
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user