Compare commits

...

16 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
889b0cec4b fix(server): sale receipt cost gl entries 2026-01-25 22:20:28 +02:00
Ahmed Bouhuolia
17651e0768 Merge pull request #871 from bigcapitalhq/fix-payment-link-base-url
fix: generated payment link base url
2025-12-14 13:29:09 +02:00
Ahmed Bouhuolia
151b623771 fix: generated payment link base url 2025-12-14 13:26:34 +02:00
Ahmed Bouhuolia
2d4459c2f9 fix: payment portal page 2025-12-14 13:06:44 +02:00
Ahmed Bouhuolia
3cbdc3ec96 Merge pull request #870 from bigcapitalhq/report-pdf-template
fix: reports pdf template
2025-12-12 23:42:02 +02:00
Ahmed Bouhuolia
3cfb5cdde8 fix: reports pdf template 2025-12-12 23:38:48 +02:00
Ahmed Bouhuolia
736f2c4109 Merge pull request #869 from bigcapitalhq/fix-passing-number-format-to-reports
fix: passing number format to reports
2025-12-11 00:25:57 +02:00
Ahmed Bouhuolia
2e21437056 fix: update pnpm-lock.yaml 2025-12-11 00:23:50 +02:00
Ahmed Bouhuolia
340b78d968 fix: passing number format to reports 2025-12-11 00:19:55 +02:00
Ahmed Bouhuolia
d006362be2 fix: transaction locking handling 2025-12-05 23:47:29 +02:00
Ahmed Bouhuolia
bc21dcb37e fix(webapp): add api key button 2025-12-05 15:14:31 +02:00
Ahmed Bouhuolia
578b0deb3e fix: sending mail jobs (#868) 2025-12-05 00:09:11 +02:00
Ahmed Bouhuolia
c3dc26a1e4 fix: sending mail jobs 2025-12-05 00:07:26 +02:00
Ahmed Bouhuolia
32d74b0413 feat: onboarding pages darkmode (#867) 2025-12-03 16:04:46 +02:00
allcontributors[bot]
71b1206f8a docs: add Daniel15 as a contributor for bug, and code (#865)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-12-02 01:42:54 +02:00
Ahmed Bouhuolia
cb1bcaae77 Merge pull request #864 from Daniel15/patch-3
fix: Stripe integration
2025-12-02 01:41:04 +02:00
91 changed files with 1243 additions and 812 deletions

View File

@@ -168,6 +168,16 @@
"contributions": [
"bug"
]
},
{
"login": "Daniel15",
"name": "Daniel Lo Nigro",
"avatar_url": "https://avatars.githubusercontent.com/u/91933?v=4",
"profile": "https://d.sb/",
"contributions": [
"bug",
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -135,6 +135,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.camilooviedo.com/"><img src="https://avatars.githubusercontent.com/u/64604272?v=4?s=100" width="100px;" alt="Camilo Oviedo"/><br /><sub><b>Camilo Oviedo</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Champetaman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nklmantey.com/"><img src="https://avatars.githubusercontent.com/u/90279429?v=4?s=100" width="100px;" alt="Mantey"/><br /><sub><b>Mantey</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Anklmantey" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://d.sb/"><img src="https://avatars.githubusercontent.com/u/91933?v=4?s=100" width="100px;" alt="Daniel Lo Nigro"/><br /><sub><b>Daniel Lo Nigro</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3ADaniel15" title="Bug reports">🐛</a> <a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Daniel15" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,5 @@
import { registerAs } from '@nestjs/config';
export default registerAs('app', () => ({
baseUrl: process.env.BASE_URL,
}));

View File

@@ -1,3 +1,4 @@
import app from './app';
import systemDatabase from './system-database';
import tenantDatabase from './tenant-database';
import signup from './signup';
@@ -18,6 +19,7 @@ import throttle from './throttle';
import cloud from './cloud';
export const config = [
app,
systemDatabase,
cloud,
tenantDatabase,

View File

@@ -7,53 +7,46 @@ import {
} from '@nestjs/common';
import { type Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { mapKeysDeep } from '@/utils/deepdash';
export function camelToSnake<T = any>(value: T) {
export function camelToSnake<T = any>(value: T): T {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map(camelToSnake);
}
if (typeof value === 'object' && !(value instanceof Date)) {
return Object.fromEntries(
Object.entries(value).map(([key, value]) => [
key
.split(/(?=[A-Z])/)
.join('_')
.toLowerCase(),
camelToSnake(value),
]),
);
}
return value;
return mapKeysDeep(
value,
(_value: string, key: any, parent: any, context: any) => {
if (Array.isArray(parent)) {
// tell mapKeysDeep to skip mapping inside this branch
context.skipChildren = true;
return key;
}
return key
.split(/(?=[A-Z])/)
.join('_')
.toLowerCase();
},
) as T;
}
export function snakeToCamel<T = any>(value: T) {
export function snakeToCamel<T = any>(value: T): T {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map(snakeToCamel);
}
const impl = (str: string) => {
const converted = str.replace(/([-_]\w)/g, (group) =>
group[1].toUpperCase(),
);
return converted[0].toLowerCase() + converted.slice(1);
};
if (typeof value === 'object' && !(value instanceof Date)) {
return Object.fromEntries(
Object.entries(value).map(([key, value]) => [
impl(key),
snakeToCamel(value),
]),
);
}
return value;
return mapKeysDeep(
value,
(_value: string, key: any, parent: any, context: any) => {
if (Array.isArray(parent)) {
// tell mapKeysDeep to skip mapping inside this branch
context.skipChildren = true;
return key;
}
const converted = key.replace(/([-_]\w)/g, (group) =>
group[1].toUpperCase(),
);
return converted[0].toLowerCase() + converted.slice(1);
},
) as T;
}
export const DEFAULT_STRATEGY = {
@@ -63,7 +56,7 @@ export const DEFAULT_STRATEGY = {
@Injectable()
export class SerializeInterceptor implements NestInterceptor<any, any> {
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) {}
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) { }
intercept(
context: ExecutionContext,

View File

@@ -8,6 +8,7 @@ import { ServiceErrorFilter } from './common/filters/service-error.filter';
import { ModelHasRelationsFilter } from './common/filters/model-has-relations.filter';
import { ValidationPipe } from './common/pipes/ClassValidation.pipe';
import { ToJsonInterceptor } from './common/interceptors/to-json.interceptor';
import { NestExpressApplication } from '@nestjs/platform-express';
global.__public_dirname = path.join(__dirname, '..', 'public');
global.__static_dirname = path.join(__dirname, '../static');
@@ -15,7 +16,10 @@ global.__views_dirname = path.join(global.__static_dirname, '/views');
global.__images_dirname = path.join(global.__static_dirname, '/images');
async function bootstrap() {
const app = await NestFactory.create(AppModule, { rawBody: true });
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
rawBody: true,
});
app.set('query parser', 'extended');
app.setGlobalPrefix('/api');
// create and mount the middleware manually here

View File

@@ -1,4 +1,4 @@
import { Type } from 'class-transformer';
import { Transform, Type } from 'class-transformer';
import {
IsBoolean,
IsEnum,
@@ -7,6 +7,7 @@ import {
IsPositive,
} from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { parseBoolean } from '@/utils/parse-boolean';
export class NumberFormatQueryDto {
@ApiPropertyOptional({
@@ -24,6 +25,7 @@ export class NumberFormatQueryDto {
example: false,
})
@IsBoolean()
@Transform(({ value }) => parseBoolean(value, false))
@IsOptional()
readonly divideOn1000: boolean;
@@ -32,6 +34,7 @@ export class NumberFormatQueryDto {
example: true,
})
@IsBoolean()
@Transform(({ value }) => parseBoolean(value, false))
@IsOptional()
readonly showZero: boolean;

View File

@@ -3,29 +3,30 @@ import { ITableColumn, ITableData, ITableRow } from '../types/Table.types';
import { FinancialTableStructure } from './FinancialTableStructure';
import { tableClassNames } from '../utils';
import { Injectable } from '@nestjs/common';
import { TemplateInjectable } from '../../TemplateInjectable/TemplateInjectable.service';
import { ChromiumlyTenancy } from '../../ChromiumlyTenancy/ChromiumlyTenancy.service';
import { renderFinancialSheetTemplateHtml } from '@bigcapital/pdf-templates';
@Injectable()
export class TableSheetPdf {
/**
* @param {TemplateInjectable} templateInjectable - The template injectable service.
* @param {ChromiumlyTenancy} chromiumlyTenancy - The chromiumly tenancy service.
*/
constructor(
private readonly templateInjectable: TemplateInjectable,
private readonly chromiumlyTenancy: ChromiumlyTenancy,
) {}
) { }
/**
* Converts the table data into a PDF format.
* @param {ITableData} table - The table data to be converted.
* @param {string} organizationName - The organization name.
* @param {string} sheetName - The name of the sheet.
* @param {string} sheetDate - The date of the sheet.
* @param {string} customCSS - Optional custom CSS to inject.
* @returns A promise that resolves with the PDF conversion result.
*/
public async convertToPdf(
table: ITableData,
organizationName: string,
sheetName: string,
sheetDate: string,
customCSS?: string,
@@ -33,19 +34,26 @@ export class TableSheetPdf {
// Prepare columns and rows for PDF conversion
const columns = this.tablePdfColumns(table.columns);
const rows = this.tablePdfRows(table.rows);
const landscape = columns.length > 4;
// Generate HTML content from the template
const htmlContent = await this.templateInjectable.render(
'financial-sheet',
{
table: { rows, columns },
sheetName,
sheetDate,
customCSS,
// Generate HTML content from the React template
const htmlContent = renderFinancialSheetTemplateHtml({
organizationName,
sheetName,
sheetDate,
table: {
columns: columns.map((col) => ({
key: col.key,
label: col.label,
style: (col as any).style, // style may be added during transformation
})),
rows: rows.map((row) => ({
cells: row.cells,
classNames: (row as any).classNames,
})),
},
);
customCSS,
});
// Convert the HTML content to PDF
return this.chromiumlyTenancy.convertHtmlContent(htmlContent, {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
@@ -74,7 +82,6 @@ export class TableSheetPdf {
const flatNestedTree = curriedFlatNestedTree(R.__, {
nestedPrefix: '<span style="padding-left: 15px;"></span>',
});
// @ts-ignore
return R.compose(tableClassNames, flatNestedTree)(rows);
};

View File

@@ -21,6 +21,7 @@ export class APAgingSummaryPdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedAsDate,
HtmlTableCss,

View File

@@ -21,6 +21,7 @@ export class ARAgingSummaryPdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCss,

View File

@@ -9,7 +9,7 @@ export class BalanceSheetPdfInjectable {
constructor(
private readonly balanceSheetTable: BalanceSheetTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Converts the given balance sheet table to pdf.
@@ -21,6 +21,7 @@ export class BalanceSheetPdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -81,6 +81,7 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
})
@ValidateNested()
@Type(() => NumberFormatQueryDto)
@IsOptional()
numberFormat: NumberFormatQueryDto;
@ApiProperty({

View File

@@ -22,6 +22,7 @@ export class CashflowTablePdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -21,6 +21,7 @@ export class CustomerBalanceSummaryPdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -21,6 +21,7 @@ export class GeneralLedgerPdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -32,7 +32,7 @@ export class InventoryItemDetailsQueryDto {
@ApiPropertyOptional({
description: 'Number format for the inventory item details',
})
numberFormat: INumberFormatQuery;
numberFormat: NumberFormatQueryDto;
@Transform(({ value }) => parseBoolean(value, false))
@IsBoolean()

View File

@@ -26,6 +26,7 @@ export class InventoryDetailsTablePdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -22,6 +22,7 @@ export class InventoryValuationSheetPdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -9,7 +9,7 @@ export class JournalSheetPdfInjectable {
constructor(
private readonly journalSheetTable: JournalSheetTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Converts the given journal sheet table to pdf.
@@ -22,6 +22,7 @@ export class JournalSheetPdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -7,11 +7,13 @@ import {
IsEnum,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { ToNumber } from '@/common/decorators/Validators';
import { parseBoolean } from '@/utils/parse-boolean';
import { NumberFormatQueryDto } from '@/modules/BankingTransactions/dtos/NumberFormatQuery.dto';
export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
@IsString()
@@ -30,8 +32,10 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
toDate: moment.MomentInput;
@ApiProperty({ description: 'Number format configuration' })
@Type(() => Object)
numberFormat: INumberFormatQuery;
@ValidateNested()
@Type(() => NumberFormatQueryDto)
@IsOptional()
numberFormat: NumberFormatQueryDto;
@IsBoolean()
@Transform(({ value }) => parseBoolean(value, false))

View File

@@ -21,6 +21,7 @@ export class ProfitLossTablePdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -23,6 +23,7 @@ export class PurchasesByItemsPdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -9,7 +9,7 @@ export class SalesByItemsPdfInjectable {
constructor(
private readonly salesByItemsTable: SalesByItemsTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Retrieves the sales by items sheet in pdf format.
@@ -23,6 +23,7 @@ export class SalesByItemsPdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -8,7 +8,7 @@ export class SalesTaxLiabiltiySummaryPdf {
constructor(
private readonly salesTaxLiabiltiySummaryTable: SalesTaxLiabilitySummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Converts the given sales tax liability summary table to pdf.
@@ -21,6 +21,7 @@ export class SalesTaxLiabiltiySummaryPdf {
);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
);

View File

@@ -6,7 +6,7 @@ export class TransactionsByCustomersPdf {
constructor(
private readonly transactionsByCustomersTable: TransactionsByCustomersTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Retrieves the transactions by customers in PDF format.
@@ -18,6 +18,7 @@ export class TransactionsByCustomersPdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
);

View File

@@ -9,7 +9,7 @@ export class TransactionsByVendorsPdf {
constructor(
private readonly transactionsByVendorTable: TransactionsByVendorTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Converts the given balance sheet table to pdf.
@@ -21,6 +21,7 @@ export class TransactionsByVendorsPdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -9,7 +9,7 @@ export class TrialBalanceSheetPdfInjectable {
constructor(
private readonly trialBalanceSheetTable: TrialBalanceSheetTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Converts the given trial balance sheet table to pdf.
@@ -21,6 +21,7 @@ export class TrialBalanceSheetPdfInjectable {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,

View File

@@ -9,7 +9,7 @@ export class VendorBalanceSummaryPdf {
constructor(
private readonly vendorBalanceSummaryTable: VendorBalanceSummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
) { }
/**
* Retrieves the sales by items sheet in pdf format.
@@ -23,6 +23,7 @@ export class VendorBalanceSummaryPdf {
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.organizationName,
table.meta.sheetName,
table.meta.formattedAsDate,
HtmlTableCustomCss,

View File

@@ -4,16 +4,18 @@ export class ServiceError extends Error {
errorType: string;
message: string;
payload: any;
httpStatus: HttpStatus;
constructor(errorType: string, message?: string, payload?: any) {
constructor(errorType: string, message?: string, payload?: any, httpStatus?: HttpStatus) {
super(message);
this.errorType = errorType;
this.message = message || null;
this.payload = payload;
this.httpStatus = httpStatus || HttpStatus.BAD_REQUEST;
}
getStatus(): HttpStatus {
return HttpStatus.INTERNAL_SERVER_ERROR;
return this.httpStatus;
}
}

View File

@@ -1,12 +1,11 @@
import { JOB_REF, Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { Inject, Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import {
SEND_PAYMENT_RECEIVED_MAIL_JOB,
SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
} from '../constants';
import { Inject, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { ClsService } from 'nestjs-cls';
import { SendPaymentReceiveMailNotification } from '../commands/PaymentReceivedMailNotification';
import { SendPaymentReceivedMailPayload } from '../types/PaymentReceived.types';
@@ -21,9 +20,10 @@ export class SendPaymentReceivedMailProcessor {
@Inject(JOB_REF)
private readonly jobRef: Job<SendPaymentReceivedMailPayload>,
) {}
) { }
@Process(SEND_PAYMENT_RECEIVED_MAIL_JOB)
@UseCls()
async handleSendMail() {
const { messageOptions, paymentReceivedId, organizationId, userId } =
this.jobRef.data;
@@ -37,7 +37,8 @@ export class SendPaymentReceivedMailProcessor {
messageOptions,
);
} catch (error) {
console.log(error);
console.error('Failed to process payment received mail job:', error);
throw error;
}
}
}

View File

@@ -42,6 +42,7 @@ import { GetSaleEstimateMailTemplateService } from './queries/GetSaleEstimateMai
import { SaleEstimateAutoIncrementSubscriber } from './subscribers/SaleEstimateAutoIncrementSubscriber';
import { BulkDeleteSaleEstimatesService } from './BulkDeleteSaleEstimates.service';
import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSaleEstimates.service';
import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.process';
@Module({
imports: [
@@ -89,6 +90,7 @@ import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSale
SaleEstimateAutoIncrementSubscriber,
BulkDeleteSaleEstimatesService,
ValidateBulkDeleteSaleEstimatesService,
SendSaleEstimateMailProcess,
],
exports: [
SaleEstimatesExportable,

View File

@@ -24,6 +24,7 @@ import { Mail } from '@/modules/Mail/Mail';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { GetSaleEstimateMailTemplateService } from '../queries/GetSaleEstimateMailTemplate.service';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class SendSaleEstimateMail {
@@ -42,13 +43,14 @@ export class SendSaleEstimateMail {
private readonly getEstimateMailTemplate: GetSaleEstimateMailTemplateService,
private readonly eventPublisher: EventEmitter2,
private readonly mailTransporter: MailTransporter,
private readonly tenancyContext: TenancyContext,
@Inject(SaleEstimate.name)
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
@InjectQueue(SendSaleEstimateMailQueue)
private readonly sendEstimateMailQueue: Queue,
) {}
) { }
/**
* Triggers the reminder mail of the given sale estimate.
@@ -60,10 +62,19 @@ export class SendSaleEstimateMail {
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO,
): Promise<void> {
const tenant = await this.tenancyContext.getTenant();
const user = await this.tenancyContext.getSystemUser();
const organizationId = tenant.organizationId;
const userId = user.id;
const payload = {
saleEstimateId,
messageOptions,
userId,
organizationId,
};
await this.sendEstimateMailQueue.add(SendSaleEstimateMailJob, payload);
// Triggers `onSaleEstimatePreMailSend` event.

View File

@@ -1,19 +1,39 @@
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { Inject, Scope } from '@nestjs/common';
import { JOB_REF } from '@nestjs/bull';
import {
SendSaleEstimateMailJob,
SendSaleEstimateMailQueue,
} from '../types/SaleEstimates.types';
import { SendSaleEstimateMail } from '../commands/SendSaleEstimateMail';
import { ClsService, UseCls } from 'nestjs-cls';
@Processor(SendSaleEstimateMailQueue)
@Processor({
name: SendSaleEstimateMailQueue,
scope: Scope.REQUEST,
})
export class SendSaleEstimateMailProcess {
constructor(private readonly sendEstimateMailService: SendSaleEstimateMail) {}
constructor(
private readonly sendEstimateMailService: SendSaleEstimateMail,
private readonly clsService: ClsService,
@Inject(JOB_REF)
private readonly jobRef: Job,
) { }
@Process(SendSaleEstimateMailJob)
async handleSendMail(job: Job) {
const { saleEstimateId, messageOptions } = job.data;
@UseCls()
async handleSendMail() {
const { saleEstimateId, messageOptions, organizationId, userId } = this.jobRef.data;
await this.sendEstimateMailService.sendMail(saleEstimateId, messageOptions);
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);
try {
await this.sendEstimateMailService.sendMail(saleEstimateId, messageOptions);
} catch (error) {
console.error('Failed to process estimate mail job:', error);
throw error;
}
}
}

View File

@@ -99,7 +99,8 @@ export class SaleInvoicesController {
return this.saleInvoiceApplication.createSaleInvoice(saleInvoiceDTO);
}
@Put(':id/mail')
@Post(':id/mail')
@HttpCode(200)
@ApiOperation({ summary: 'Send the sale invoice mail.' })
@ApiResponse({
status: 200,

View File

@@ -10,6 +10,7 @@ import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
import { SaleInvoice } from '../models/SaleInvoice';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class GenerateShareLink {
@@ -18,13 +19,14 @@ export class GenerateShareLink {
private eventPublisher: EventEmitter2,
private transformer: TransformerInjectable,
private tenancyContext: TenancyContext,
private configService: ConfigService,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(PaymentLink.name)
private paymentLinkModel: typeof PaymentLink,
) {}
) { }
/**
* Generates private or public payment link for the given sale invoice.
@@ -75,6 +77,9 @@ export class GenerateShareLink {
return this.transformer.transform(
paymentLink,
new GeneratePaymentLinkTransformer(),
{
baseUrl: this.configService.get('app.baseUrl'),
}
);
});
}

View File

@@ -1,7 +1,10 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { PUBLIC_PAYMENT_LINK } from '../constants';
export class GeneratePaymentLinkTransformer extends Transformer {
interface GeneratePaymentLinkTransformerOptions {
baseUrl: string;
}
export class GeneratePaymentLinkTransformer extends Transformer<GeneratePaymentLinkTransformerOptions> {
/**
* Exclude these attributes from payment link object.
* @returns {Array}
@@ -23,6 +26,9 @@ export class GeneratePaymentLinkTransformer extends Transformer {
* @returns {string}
*/
public link(link) {
return PUBLIC_PAYMENT_LINK?.replace('{PAYMENT_LINK_ID}', link.linkId);
return PUBLIC_PAYMENT_LINK?.replace(
'{BASE_URL}',
this.options.baseUrl,
).replace('{PAYMENT_LINK_ID}', link.linkId);
}
}

View File

@@ -33,7 +33,7 @@ export class SendSaleInvoiceMail {
private readonly tenancyContect: TenancyContext,
@InjectQueue(SendSaleInvoiceQueue) private readonly sendInvoiceQueue: Queue,
) {}
) { }
/**
* Sends the invoice mail of the given sale invoice.
@@ -132,7 +132,13 @@ export class SendSaleInvoiceMail {
events.saleInvoice.onMailSend,
eventPayload,
);
await this.mailTransporter.send(mail);
try {
await this.mailTransporter.send(mail);
} catch (error) {
console.error('Failed to send invoice mail:', error);
throw error;
}
// Triggers the event `onSaleInvoiceSend`.
await this.eventEmitter.emitAsync(

View File

@@ -3,10 +3,9 @@
export const SendSaleInvoiceQueue = 'SendSaleInvoiceQueue';
export const SendSaleInvoiceMailJob = 'SendSaleInvoiceMailJob';
const BASE_URL = 'http://localhost:3000';
export const DEFAULT_INVOICE_MAIL_SUBJECT =
'Invoice {Invoice Number} from {Company Name} for {Customer Name}';
'Invoice {Invoice Number} from {Company Name} for {Customer Name}';
export const DEFAULT_INVOICE_MAIL_CONTENT = `Hi {Customer Name},
Here's invoice # {Invoice Number} for {Invoice Amount}
@@ -23,8 +22,8 @@ Thanks,
export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT =
'Invoice {InvoiceNumber} reminder from {CompanyName}';
export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `
<p>Dear {CustomerName}</p>
export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `
<p>Dear {CustomerName}</p>
<p>You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.</p>
<p>Invoice <strong>#{InvoiceNumber}</strong><br />
Due Date : <strong>{InvoiceDueDate}</strong><br />
@@ -36,7 +35,7 @@ Amount : <strong>{InvoiceAmount}</strong></p>
</p>
`;
export const PUBLIC_PAYMENT_LINK = `${BASE_URL}/payment/{PAYMENT_LINK_ID}`;
export const PUBLIC_PAYMENT_LINK = "{BASE_URL}/payment/{PAYMENT_LINK_ID}";
export const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',

View File

@@ -18,9 +18,10 @@ export class SendSaleInvoiceMailProcessor {
@Inject(JOB_REF)
private readonly jobRef: Job<SendSaleInvoiceMailJobPayload>,
private readonly clsService: ClsService,
) {}
) { }
@Process(SendSaleInvoiceMailJob)
@UseCls()
async handleSendInvoice() {
const { messageOptions, saleInvoiceId, organizationId, userId } =
this.jobRef.data;
@@ -31,7 +32,8 @@ export class SendSaleInvoiceMailProcessor {
try {
await this.sendSaleInvoiceMail.sendMail(saleInvoiceId, messageOptions);
} catch (error) {
console.log(error);
console.error('Failed to process invoice mail job:', error);
throw error;
}
}
}

View File

@@ -209,7 +209,7 @@ class GetInvoicePaymentLinkTaxEntryTransformer extends SaleInvoiceTaxEntryTransf
class GetInvoicePaymentLinkBrandingTemplate extends GetPdfTemplateTransformer {
public includeAttributes = (): string[] => {
return ['companyLogoUri', 'primaryColor'];
return ['companyLogoUri', 'primaryColor', 'secondaryColor'];
};
public excludeAttributes = (): string[] => {
@@ -219,4 +219,8 @@ class GetInvoicePaymentLinkBrandingTemplate extends GetPdfTemplateTransformer {
primaryColor = (template) => {
return template.attributes?.primaryColor;
};
secondaryColor = (template) => {
return template.attributes?.secondaryColor;
};
}

View File

@@ -0,0 +1,143 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { InventoryCostLotTracker } from '../InventoryCost/models/InventoryCostLotTracker';
import { LedgerStorageService } from '../Ledger/LedgerStorage.service';
import { groupInventoryTransactionsByTypeId } from '../InventoryCost/utils';
import { Ledger } from '../Ledger/Ledger';
import { AccountNormal } from '@/interfaces/Account';
import { ILedgerEntry } from '../Ledger/types/Ledger.types';
import { increment } from '@/utils/increment';
@Injectable()
export class SaleReceiptCostGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: TenantModelProxy<
typeof InventoryCostLotTracker
>,
) {}
/**
* Writes journal entries from sales receipts.
* @param {Date} startingDate - Starting date.
* @param {Knex.Transaction} trx - Transaction.
*/
public writeInventoryCostJournalEntries = async (
startingDate: Date,
trx?: Knex.Transaction,
): Promise<void> => {
const inventoryCostLotTrans = await this.inventoryCostLotTracker()
.query()
.where('direction', 'OUT')
.where('transaction_type', 'SaleReceipt')
.where('cost', '>', 0)
.modify('filterDateRange', startingDate)
.orderBy('date', 'ASC')
.withGraphFetched('receipt')
.withGraphFetched('item')
.withGraphFetched('itemEntry');
const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Retrieves the inventory cost lots ledger.
*/
private getInventoryCostLotsLedger = (
inventoryCostLots: InventoryCostLotTracker[],
) => {
const inventoryTransactions =
groupInventoryTransactionsByTypeId(inventoryCostLots);
const entries = inventoryTransactions
.map(this.getSaleReceiptCostGLEntries)
.flat();
return new Ledger(entries);
};
/**
* Builds the common GL entry fields for a sale receipt cost.
*/
private getReceiptCostGLCommonEntry = (
inventoryCostLot: InventoryCostLotTracker,
) => {
return {
currencyCode: inventoryCostLot.receipt.currencyCode,
exchangeRate: inventoryCostLot.receipt.exchangeRate,
transactionType: inventoryCostLot.transactionType,
transactionId: inventoryCostLot.transactionId,
transactionNumber: inventoryCostLot.receipt.receiptNumber,
referenceNumber: inventoryCostLot.receipt.referenceNo,
date: inventoryCostLot.date,
indexGroup: 20,
costable: true,
createdAt: inventoryCostLot.createdAt,
debit: 0,
credit: 0,
branchId: inventoryCostLot.receipt.branchId,
};
};
/**
* Retrieves the inventory cost GL entry for a single lot.
*/
private getInventoryCostGLEntry = R.curry(
(
getIndexIncrement: () => number,
inventoryCostLot: InventoryCostLotTracker,
): ILedgerEntry[] => {
const commonEntry = this.getReceiptCostGLCommonEntry(inventoryCostLot);
const costAccountId =
inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
const description = inventoryCostLot.itemEntry?.description || null;
const costEntry = {
...commonEntry,
debit: inventoryCostLot.cost,
accountId: costAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
note: description,
index: getIndexIncrement(),
};
const inventoryEntry = {
...commonEntry,
credit: inventoryCostLot.cost,
accountId: inventoryCostLot.item.inventoryAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
note: description,
index: getIndexIncrement(),
};
return [costEntry, inventoryEntry];
},
);
/**
* Builds GL entries for a group of sale receipt cost lots.
* - Cost of goods sold -> Debit
* - Inventory assets -> Credit
*/
public getSaleReceiptCostGLEntries = (
inventoryCostLots: InventoryCostLotTracker[],
): ILedgerEntry[] => {
const getIndexIncrement = increment(0);
const getInventoryLotEntry =
this.getInventoryCostGLEntry(getIndexIncrement);
return inventoryCostLots.map((t) => getInventoryLotEntry(t)).flat();
};
}

View File

@@ -25,7 +25,10 @@ import {
CreateSaleReceiptDto,
EditSaleReceiptDto,
} from './dtos/SaleReceipt.dto';
import { ISalesReceiptsFilter } from './types/SaleReceipts.types';
import {
ISalesReceiptsFilter,
SaleReceiptMailOptsDTO,
} from './types/SaleReceipts.types';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
import { SaleReceiptResponseDto } from './dtos/SaleReceiptResponse.dto';
@@ -87,7 +90,7 @@ export class SaleReceiptsController {
return this.saleReceiptApplication.createSaleReceipt(saleReceiptDTO);
}
@Put(':id/mail')
@Post(':id/mail')
@HttpCode(200)
@ApiOperation({ summary: 'Send the sale receipt mail.' })
@ApiParam({
@@ -96,8 +99,11 @@ export class SaleReceiptsController {
type: Number,
description: 'The sale receipt id',
})
sendSaleReceiptMail(@Param('id', ParseIntPipe) id: number) {
return this.saleReceiptApplication.getSaleReceiptMail(id);
sendSaleReceiptMail(
@Param('id', ParseIntPipe) id: number,
@Body() messageOpts: SaleReceiptMailOptsDTO,
) {
return this.saleReceiptApplication.sendSaleReceiptMail(id, messageOpts);
}
@Get('state')

View File

@@ -40,6 +40,8 @@ import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable';
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service';
import { SaleReceiptAutoIncrementSubscriber } from './subscribers/SaleReceiptAutoIncrementSubscriber';
import { SaleReceiptCostGLEntriesSubscriber } from './subscribers/SaleReceiptCostGLEntriesSubscriber';
import { SaleReceiptCostGLEntries } from './SaleReceiptCostGLEntries';
import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service';
import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service';
@@ -87,6 +89,8 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
GetSaleReceiptMailStateService,
GetSaleReceiptMailTemplateService,
SaleReceiptAutoIncrementSubscriber,
SaleReceiptCostGLEntries,
SaleReceiptCostGLEntriesSubscriber,
BulkDeleteSaleReceiptsService,
ValidateBulkDeleteSaleReceiptsService,
],

View File

@@ -1,148 +0,0 @@
// import { Service, Inject } from 'typedi';
// import * as R from 'ramda';
// import { Knex } from 'knex';
// import { AccountNormal, IInventoryLotCost, ILedgerEntry } from '@/interfaces';
// import { increment } from 'utils';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import Ledger from '@/services/Accounting/Ledger';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import { groupInventoryTransactionsByTypeId } from '../../Inventory/utils';
// @Service()
// export class SaleReceiptCostGLEntries {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private ledgerStorage: LedgerStorageService;
// /**
// * Writes journal entries from sales invoices.
// * @param {number} tenantId - The tenant id.
// * @param {Date} startingDate - Starting date.
// * @param {boolean} override
// */
// public writeInventoryCostJournalEntries = async (
// tenantId: number,
// startingDate: Date,
// trx?: Knex.Transaction
// ): Promise<void> => {
// const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
// const inventoryCostLotTrans = await InventoryCostLotTracker.query()
// .where('direction', 'OUT')
// .where('transaction_type', 'SaleReceipt')
// .where('cost', '>', 0)
// .modify('filterDateRange', startingDate)
// .orderBy('date', 'ASC')
// .withGraphFetched('receipt')
// .withGraphFetched('item');
// const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
// // Commit the ledger to the storage.
// await this.ledgerStorage.commit(tenantId, ledger, trx);
// };
// /**
// * Retrieves the inventory cost lots ledger.
// * @param {} inventoryCostLots
// * @returns {Ledger}
// */
// private getInventoryCostLotsLedger = (
// inventoryCostLots: IInventoryLotCost[]
// ) => {
// // Groups the inventory cost lots transactions.
// const inventoryTransactions =
// groupInventoryTransactionsByTypeId(inventoryCostLots);
// //
// const entries = inventoryTransactions
// .map(this.getSaleInvoiceCostGLEntries)
// .flat();
// return new Ledger(entries);
// };
// /**
// *
// * @param {IInventoryLotCost} inventoryCostLot
// * @returns {}
// */
// private getInvoiceCostGLCommonEntry = (
// inventoryCostLot: IInventoryLotCost
// ) => {
// return {
// currencyCode: inventoryCostLot.receipt.currencyCode,
// exchangeRate: inventoryCostLot.receipt.exchangeRate,
// transactionType: inventoryCostLot.transactionType,
// transactionId: inventoryCostLot.transactionId,
// date: inventoryCostLot.date,
// indexGroup: 20,
// costable: true,
// createdAt: inventoryCostLot.createdAt,
// debit: 0,
// credit: 0,
// branchId: inventoryCostLot.receipt.branchId,
// };
// };
// /**
// * Retrieves the inventory cost GL entry.
// * @param {IInventoryLotCost} inventoryLotCost
// * @returns {ILedgerEntry[]}
// */
// private getInventoryCostGLEntry = R.curry(
// (
// getIndexIncrement,
// inventoryCostLot: IInventoryLotCost
// ): ILedgerEntry[] => {
// const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot);
// const costAccountId =
// inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
// // XXX Debit - Cost account.
// const costEntry = {
// ...commonEntry,
// debit: inventoryCostLot.cost,
// accountId: costAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// // XXX Credit - Inventory account.
// const inventoryEntry = {
// ...commonEntry,
// credit: inventoryCostLot.cost,
// accountId: inventoryCostLot.item.inventoryAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// return [costEntry, inventoryEntry];
// }
// );
// /**
// * Writes journal entries for given sale invoice.
// * -------
// * - Cost of goods sold -> Debit -> YYYY
// * - Inventory assets -> Credit -> YYYY
// * --------
// * @param {ISaleInvoice} saleInvoice
// * @param {JournalPoster} journal
// */
// public getSaleInvoiceCostGLEntries = (
// inventoryCostLots: IInventoryLotCost[]
// ): ILedgerEntry[] => {
// const getIndexIncrement = increment(0);
// const getInventoryLotEntry =
// this.getInventoryCostGLEntry(getIndexIncrement);
// return inventoryCostLots.map(getInventoryLotEntry).flat();
// };
// }

View File

@@ -1,24 +1,42 @@
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { SendSaleReceiptMailQueue } from '../constants';
import { Inject, Scope } from '@nestjs/common';
import { JOB_REF } from '@nestjs/bull';
import { SendSaleReceiptMailQueue, SendSaleReceiptMailJob } from '../constants';
import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification';
import { SaleReceiptSendMailPayload } from '../types/SaleReceipts.types';
import { ClsService } from 'nestjs-cls';
import { ClsService, UseCls } from 'nestjs-cls';
@Processor(SendSaleReceiptMailQueue)
@Processor({
name: SendSaleReceiptMailQueue,
scope: Scope.REQUEST,
})
export class SendSaleReceiptMailProcess {
constructor(
private readonly saleReceiptMailNotification: SaleReceiptMailNotification,
private readonly clsService: ClsService,
) {}
@Process(SendSaleReceiptMailQueue)
async handleSendMailJob(job: Job<SaleReceiptSendMailPayload>) {
const { messageOpts, saleReceiptId, organizationId, userId } = job.data;
@Inject(JOB_REF)
private readonly jobRef: Job<SaleReceiptSendMailPayload>,
) { }
@Process(SendSaleReceiptMailJob)
@UseCls()
async handleSendMailJob() {
const { messageOpts, saleReceiptId, organizationId, userId } =
this.jobRef.data;
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);
await this.saleReceiptMailNotification.sendMail(saleReceiptId, messageOpts);
try {
await this.saleReceiptMailNotification.sendMail(
saleReceiptId,
messageOpts,
);
} catch (error) {
console.error('Failed to process receipt mail job:', error);
throw error;
}
}
}

View File

@@ -1,36 +1,26 @@
// import { Inject, Service } from 'typedi';
// import events from '@/subscribers/events';
// import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces';
// import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { IInventoryCostLotsGLEntriesWriteEvent } from '@/modules/InventoryCost/types/InventoryCost.types';
import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries';
// @Service()
// export class SaleReceiptCostGLEntriesSubscriber {
// @Inject()
// private saleReceiptCostEntries: SaleReceiptCostGLEntries;
@Injectable()
export class SaleReceiptCostGLEntriesSubscriber {
constructor(
private readonly saleReceiptCostEntries: SaleReceiptCostGLEntries,
) {}
// /**
// * Attaches events.
// */
// public attach(bus) {
// bus.subscribe(
// events.inventory.onCostLotsGLEntriesWrite,
// this.writeJournalEntriesOnceWriteoffCreate
// );
// }
// /**
// * Writes the receipts cost GL entries once the inventory cost lots be written.
// * @param {IInventoryCostLotsGLEntriesWriteEvent}
// */
// private writeJournalEntriesOnceWriteoffCreate = async ({
// trx,
// startingDate,
// tenantId,
// }: IInventoryCostLotsGLEntriesWriteEvent) => {
// await this.saleReceiptCostEntries.writeInventoryCostJournalEntries(
// tenantId,
// startingDate,
// trx
// );
// };
// }
/**
* Writes the receipts cost GL entries once the inventory cost lots are written.
*/
@OnEvent(events.inventory.onCostLotsGLEntriesWrite)
async writeReceiptsCostEntriesOnCostLotsWritten({
trx,
startingDate,
}: IInventoryCostLotsGLEntriesWriteEvent) {
await this.saleReceiptCostEntries.writeInventoryCostJournalEntries(
startingDate,
trx,
);
}
}

View File

@@ -10,7 +10,7 @@ import { ServiceError } from '@/modules/Items/ServiceError';
export class TransactionsLockingGuard {
constructor(
private readonly transactionsLockingRepo: TransactionsLockingRepository,
) {}
) { }
/**
* Detarmines whether the transaction date between the locking date period.
@@ -31,7 +31,7 @@ export class TransactionsLockingGuard {
const inUnlockDate =
unlockFromDate && unlockToDate
? moment(transactionDate).isSameOrAfter(unlockFromDate) &&
moment(transactionDate).isSameOrBefore(unlockFromDate)
moment(transactionDate).isSameOrBefore(unlockFromDate)
: false;
// Retruns true in case the transaction date between locking date
@@ -57,7 +57,7 @@ export class TransactionsLockingGuard {
);
if (isLocked) {
this.throwTransactionsLockError(lockingGroup);
await this.throwTransactionsLockError(lockingGroup);
}
};
@@ -90,11 +90,12 @@ export class TransactionsLockingGuard {
await this.transactionsLockingRepo.getTransactionsLockingType();
if (lockingType === TransactionsLockingGroup.All) {
return this.validateTransactionsLocking(
await this.validateTransactionsLocking(
transactionDate,
TransactionsLockingGroup.All,
);
return;
}
return this.validateTransactionsLocking(transactionDate, moduleType);
await this.validateTransactionsLocking(transactionDate, moduleType);
};
}

View File

@@ -22,7 +22,7 @@ import { events } from '@/common/events/events';
export class FinancialTransactionLockingGuardSubscriber {
constructor(
public readonly financialTransactionsLocking: FinancialTransactionLocking,
) {}
) { }
/**
* ---------------------------------------------
@@ -33,7 +33,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transaction locking guard on manual journal creating.
* @param {IManualJournalCreatingPayload} payload
*/
@OnEvent(events.manualJournals.onCreating)
@OnEvent(events.manualJournals.onCreating, { suppressErrors: false })
public async transactionsLockingGuardOnManualJournalCreating({
manualJournalDTO,
}: IManualJournalCreatingPayload) {
@@ -49,7 +49,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on manual journal deleting.
* @param {IManualJournalEditingPayload} payload
*/
@OnEvent(events.manualJournals.onDeleting)
@OnEvent(events.manualJournals.onDeleting, { suppressErrors: false })
public async transactionsLockingGuardOnManualJournalDeleting({
oldManualJournal,
}: IManualJournalEditingPayload) {
@@ -65,7 +65,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on manual journal editing.
* @param {IManualJournalDeletingPayload} payload
*/
@OnEvent(events.manualJournals.onEditing)
@OnEvent(events.manualJournals.onEditing, { suppressErrors: false })
public async transactionsLockingGuardOnManualJournalEditing({
oldManualJournal,
manualJournalDTO,
@@ -87,7 +87,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on manual journal publishing.
* @param {IManualJournalPublishingPayload}
*/
@OnEvent(events.manualJournals.onPublishing)
@OnEvent(events.manualJournals.onPublishing, { suppressErrors: false })
public async transactionsLockingGuardOnManualJournalPublishing({
oldManualJournal,
}: IManualJournalPublishingPayload) {
@@ -106,7 +106,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on expense creating.
* @param {IExpenseCreatingPayload} payload
*/
@OnEvent(events.expenses.onCreating)
@OnEvent(events.expenses.onCreating, { suppressErrors: false })
public async transactionsLockingGuardOnExpenseCreating({
expenseDTO,
}: IExpenseCreatingPayload) {
@@ -122,7 +122,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on expense deleting.
* @param {IExpenseDeletingPayload} payload
*/
@OnEvent(events.expenses.onDeleting)
@OnEvent(events.expenses.onDeleting, { suppressErrors: false })
public async transactionsLockingGuardOnExpenseDeleting({
oldExpense,
}: IExpenseDeletingPayload) {
@@ -138,7 +138,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on expense editing.
* @param {IExpenseEventEditingPayload}
*/
@OnEvent(events.expenses.onEditing)
@OnEvent(events.expenses.onEditing, { suppressErrors: false })
public async transactionsLockingGuardOnExpenseEditing({
oldExpense,
expenseDTO,
@@ -160,7 +160,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on expense publishing.
* @param {IExpensePublishingPayload} payload -
*/
@OnEvent(events.expenses.onPublishing)
@OnEvent(events.expenses.onPublishing, { suppressErrors: false })
public async transactionsLockingGuardOnExpensePublishing({
oldExpense,
}: IExpensePublishingPayload) {
@@ -179,7 +179,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on cashflow transaction creating.
* @param {ICommandCashflowCreatingPayload}
*/
@OnEvent(events.cashflow.onTransactionCreating)
@OnEvent(events.cashflow.onTransactionCreating, { suppressErrors: false })
public async transactionsLockingGuardOnCashflowTransactionCreating({
newTransactionDTO,
}: ICommandCashflowCreatingPayload) {
@@ -194,7 +194,7 @@ export class FinancialTransactionLockingGuardSubscriber {
* Transactions locking guard on cashflow transaction deleting.
* @param {ICommandCashflowDeletingPayload}
*/
@OnEvent(events.cashflow.onTransactionDeleting)
@OnEvent(events.cashflow.onTransactionDeleting, { suppressErrors: false })
public async transactionsLockingGuardOnCashflowTransactionDeleting({
oldCashflowTransaction,
}: ICommandCashflowDeletingPayload) {

View File

@@ -26,7 +26,7 @@ import { OnEvent } from '@nestjs/event-emitter';
export class PurchasesTransactionLockingGuardSubscriber {
constructor(
public readonly purchasesTransactionsLocking: PurchasesTransactionLockingGuard,
) {}
) { }
/**
* ---------------------------------------------
@@ -37,7 +37,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on payment editing.
* @param {IBillPaymentEditingPayload}
*/
@OnEvent(events.billPayment.onEditing)
@OnEvent(events.billPayment.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnPaymentEditing({
oldBillPayment,
billPaymentDTO,
@@ -56,7 +56,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on payment creating.
* @param {IBillPaymentCreatingPayload}
*/
@OnEvent(events.billPayment.onCreating)
@OnEvent(events.billPayment.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnPaymentCreating({
billPaymentDTO,
}: IBillPaymentCreatingPayload) {
@@ -69,7 +69,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on payment deleting.
* @param {IBillPaymentDeletingPayload} payload -
*/
@OnEvent(events.billPayment.onDeleting)
@OnEvent(events.billPayment.onDeleting, { suppressErrors: false })
public async transactionLockingGuardOnPaymentDeleting({
oldBillPayment,
}: IBillPaymentDeletingPayload) {
@@ -88,7 +88,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on bill creating.
* @param {IBillCreatingPayload} payload
*/
@OnEvent(events.bill.onCreating)
@OnEvent(events.bill.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnBillCreating({
billDTO,
}: IBillCreatingPayload) {
@@ -104,7 +104,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on bill editing.
* @param {IBillEditingPayload} payload
*/
@OnEvent(events.bill.onEditing)
@OnEvent(events.bill.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnBillEditing({
oldBill,
billDTO,
@@ -126,7 +126,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on bill deleting.
* @param {IBillEventDeletingPayload} payload
*/
@OnEvent(events.bill.onDeleting)
@OnEvent(events.bill.onDeleting, { suppressErrors: false })
public async transactionLockingGuardOnBillDeleting({
oldBill,
}: IBillEventDeletingPayload) {
@@ -148,7 +148,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on vendor credit creating.
* @param {IVendorCreditCreatingPayload} payload
*/
@OnEvent(events.vendorCredit.onCreating)
@OnEvent(events.vendorCredit.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnVendorCreditCreating({
vendorCreditCreateDTO,
}: IVendorCreditCreatingPayload) {
@@ -164,7 +164,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on vendor credit deleting.
* @param {IVendorCreditDeletingPayload} payload
*/
@OnEvent(events.vendorCredit.onDeleting)
@OnEvent(events.vendorCredit.onDeleting, { suppressErrors: false })
public async transactionLockingGuardOnVendorCreditDeleting({
oldVendorCredit,
}: IVendorCreditDeletingPayload) {
@@ -180,7 +180,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on vendor credit editing.
* @param {IVendorCreditEditingPayload} payload
*/
@OnEvent(events.vendorCredit.onEditing)
@OnEvent(events.vendorCredit.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnVendorCreditEditing({
oldVendorCredit,
vendorCreditDTO,
@@ -202,7 +202,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on refund vendor credit creating.
* @param {IRefundVendorCreditCreatingPayload} payload -
*/
@OnEvent(events.vendorCredit.onRefundCreating)
@OnEvent(events.vendorCredit.onRefundCreating, { suppressErrors: false })
public async transactionLockingGuardOnRefundVendorCredit({
refundVendorCreditDTO,
}: IRefundVendorCreditCreatingPayload) {
@@ -215,7 +215,7 @@ export class PurchasesTransactionLockingGuardSubscriber {
* Transaction locking guard on refund vendor credit deleting.
* @param {IRefundVendorCreditDeletingPayload} payload
*/
@OnEvent(events.vendorCredit.onRefundDeleting)
@OnEvent(events.vendorCredit.onRefundDeleting, { suppressErrors: false })
public async transactionLockingGuardOnRefundCreditDeleting({
oldRefundCredit,
}: IRefundVendorCreditDeletingPayload) {

View File

@@ -37,7 +37,7 @@ import { ISaleReceiptEventClosingPayload } from '@/modules/SaleReceipts/types/Sa
export class SalesTransactionLockingGuardSubscriber {
constructor(
public readonly salesLockingGuard: SalesTransactionLockingGuard,
) {}
) { }
/**
* ---------------------------------------------
@@ -48,7 +48,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on invoice creating.
* @param {ISaleInvoiceCreatingPaylaod} payload
*/
@OnEvent(events.saleInvoice.onCreating)
@OnEvent(events.saleInvoice.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnInvoiceCreating({
saleInvoiceDTO,
}: ISaleInvoiceCreatingPaylaod) {
@@ -64,7 +64,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on invoice editing.
* @param {ISaleInvoiceEditingPayload} payload
*/
@OnEvent(events.saleInvoice.onEditing)
@OnEvent(events.saleInvoice.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnInvoiceEditing({
oldSaleInvoice,
saleInvoiceDTO,
@@ -86,7 +86,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on invoice deleting.
* @param {ISaleInvoiceDeletePayload} payload
*/
@OnEvent(events.saleInvoice.onDelete)
@OnEvent(events.saleInvoice.onDelete, { suppressErrors: false })
public async transactionLockingGuardOnInvoiceDeleting({
oldSaleInvoice,
}: ISaleInvoiceDeletePayload) {
@@ -102,7 +102,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on invoice writingoff.
* @param {ISaleInvoiceWriteoffCreatePayload} payload
*/
@OnEvent(events.saleInvoice.onWriteoff)
@OnEvent(events.saleInvoice.onWriteoff, { suppressErrors: false })
public async transactionLockinGuardOnInvoiceWritingoff({
saleInvoice,
}: ISaleInvoiceWriteoffCreatePayload) {
@@ -115,7 +115,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaciton locking guard on canceling written-off invoice.
* @param {ISaleInvoiceWrittenOffCancelPayload} payload
*/
@OnEvent(events.saleInvoice.onWrittenoffCancel)
@OnEvent(events.saleInvoice.onWrittenoffCancel, { suppressErrors: false })
public async transactionLockinGuardOnInvoiceWritingoffCanceling({
saleInvoice,
}: ISaleInvoiceWrittenOffCancelPayload) {
@@ -134,7 +134,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on receipt creating.
* @param {ISaleReceiptCreatingPayload}
*/
@OnEvent(events.saleReceipt.onCreating)
@OnEvent(events.saleReceipt.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnReceiptCreating({
saleReceiptDTO,
}: ISaleReceiptCreatingPayload) {
@@ -150,7 +150,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on receipt creating.
* @param {ISaleReceiptDeletingPayload}
*/
@OnEvent(events.saleReceipt.onDeleting)
@OnEvent(events.saleReceipt.onDeleting, { suppressErrors: false })
public async transactionLockingGuardOnReceiptDeleting({
oldSaleReceipt,
}: ISaleReceiptDeletingPayload) {
@@ -165,7 +165,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on sale receipt editing.
* @param {ISaleReceiptEditingPayload} payload
*/
@OnEvent(events.saleReceipt.onEditing)
@OnEvent(events.saleReceipt.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnReceiptEditing({
oldSaleReceipt,
saleReceiptDTO,
@@ -184,7 +184,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on sale receipt closing.
* @param {ISaleReceiptEventClosingPayload} payload
*/
@OnEvent(events.saleReceipt.onClosing)
@OnEvent(events.saleReceipt.onClosing, { suppressErrors: false })
public async transactionLockingGuardOnReceiptClosing({
oldSaleReceipt,
}: ISaleReceiptEventClosingPayload) {
@@ -203,7 +203,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on credit note deleting.
* @param {ICreditNoteDeletingPayload} payload -
*/
@OnEvent(events.creditNote.onDeleting)
@OnEvent(events.creditNote.onDeleting, { suppressErrors: false })
public async transactionLockingGuardOnCreditDeleting({
oldCreditNote,
}: ICreditNoteDeletingPayload) {
@@ -219,7 +219,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on credit note creating.
* @param {ICreditNoteCreatingPayload} payload
*/
@OnEvent(events.creditNote.onCreating)
@OnEvent(events.creditNote.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnCreditCreating({
creditNoteDTO,
}: ICreditNoteCreatingPayload) {
@@ -235,7 +235,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on credit note editing.
* @param {ICreditNoteEditingPayload} payload -
*/
@OnEvent(events.creditNote.onEditing)
@OnEvent(events.creditNote.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnCreditEditing({
creditNoteEditDTO,
oldCreditNote,
@@ -257,7 +257,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on payment deleting.
* @param {IRefundCreditNoteDeletingPayload} paylaod -
*/
@OnEvent(events.creditNote.onRefundDeleting)
@OnEvent(events.creditNote.onRefundDeleting, { suppressErrors: false })
public async transactionLockingGuardOnCreditRefundDeleteing({
oldRefundCredit,
}: IRefundCreditNoteDeletingPayload) {
@@ -268,7 +268,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on refund credit note creating.
* @param {IRefundCreditNoteCreatingPayload} payload -
*/
@OnEvent(events.creditNote.onRefundCreating)
@OnEvent(events.creditNote.onRefundCreating, { suppressErrors: false })
public async transactionLockingGuardOnCreditRefundCreating({
newCreditNoteDTO,
}: IRefundCreditNoteCreatingPayload) {
@@ -284,7 +284,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on estimate creating.
* @param {ISaleEstimateCreatingPayload} payload -
*/
@OnEvent(events.saleEstimate.onCreating)
@OnEvent(events.saleEstimate.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnEstimateCreating({
estimateDTO,
}: ISaleEstimateCreatingPayload) {
@@ -300,7 +300,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on estimate deleting.
* @param {ISaleEstimateDeletingPayload} payload
*/
@OnEvent(events.saleEstimate.onDeleting)
@OnEvent(events.saleEstimate.onDeleting, { suppressErrors: false })
public async transactionLockingGuardOnEstimateDeleting({
oldSaleEstimate,
}: ISaleEstimateDeletingPayload) {
@@ -316,7 +316,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
@OnEvent(events.saleEstimate.onEditing)
@OnEvent(events.saleEstimate.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnEstimateEditing({
oldSaleEstimate,
estimateDTO,
@@ -344,7 +344,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on payment receive editing.
* @param {IPaymentReceivedEditingPayload}
*/
@OnEvent(events.paymentReceive.onEditing)
@OnEvent(events.paymentReceive.onEditing, { suppressErrors: false })
public async transactionLockingGuardOnPaymentEditing({
oldPaymentReceive,
paymentReceiveDTO,
@@ -363,7 +363,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on payment creating.
* @param {IPaymentReceivedCreatingPayload}
*/
@OnEvent(events.paymentReceive.onCreating)
@OnEvent(events.paymentReceive.onCreating, { suppressErrors: false })
public async transactionLockingGuardOnPaymentCreating({
paymentReceiveDTO,
}: IPaymentReceivedCreatingPayload) {
@@ -376,7 +376,7 @@ export class SalesTransactionLockingGuardSubscriber {
* Transaction locking guard on payment deleting.
* @param {IPaymentReceivedDeletingPayload} payload -
*/
@OnEvent(events.paymentReceive.onDeleting)
@OnEvent(events.paymentReceive.onDeleting, { suppressErrors: false })
public async transactionLockingGuardPaymentDeleting({
oldPaymentReceive,
}: IPaymentReceivedDeletingPayload) {

View File

@@ -21,9 +21,10 @@ export class SendInviteUserMailProcessor {
@Inject(JOB_REF)
private readonly jobRef: Job<SendInviteUserMailJobPayload>,
private readonly clsService: ClsService,
) {}
) { }
@Process(SendInviteUserMailJob)
@UseCls()
async handleSendInviteMail() {
const { fromUser, invite, organizationId, userId } = this.jobRef.data;
@@ -33,7 +34,8 @@ export class SendInviteUserMailProcessor {
try {
await this.sendInviteUsersMailService.sendInviteMail(fromUser, invite);
} catch (error) {
console.log(error);
console.error('Failed to process invite user mail job:', error);
throw error;
}
}
}

View File

@@ -229,7 +229,7 @@ describe('Sale Invoices (e2e)', () => {
.send(requestSaleInvoiceBody());
return request(app.getHttpServer())
.put(`/sale-invoices/${response.body.id}/mail`)
.post(`/sale-invoices/${response.body.id}/mail`)
.set('organization-id', orgainzationId)
.set('Authorization', AuthorizationHeader)
.send({

View File

@@ -7,11 +7,12 @@
"@bigcapital/pdf-templates": "*",
"@bigcapital/utils": "*",
"@blueprintjs-formik/core": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.4.0",
"@blueprintjs-formik/select": "^0.3.5",
"@blueprintjs/colors": "4.1.19",
"@blueprintjs/core": "^4.20.2",
"@blueprintjs/datetime": "^4.4.37",
"@blueprintjs/datetime2": "^3.0.10",
"@blueprintjs/popover2": "^1.14.11",
"@blueprintjs/select": "^4.9.24",
"@blueprintjs/table": "^4.10.12",
@@ -77,7 +78,7 @@
"plaid-threads": "^11.4.3",
"polished": "^4.3.1",
"prop-types": "15.8.1",
"query-string": "^7.1.1",
"qs": "^6.14.0",
"ramda": "^0.27.1",
"react": "^18.2.0",
"react-body-classname": "^1.3.1",
@@ -108,11 +109,11 @@
"react-use": "^13.26.1",
"react-use-context-menu": "^0.1.4",
"react-virtualized": "^9.22.3",
"regenerator-runtime": "^0.14.1",
"redux": "^4.2.1",
"redux-devtools": "^3.5.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.4.2",
"regenerator-runtime": "^0.14.1",
"reselect": "4.1.7",
"rtl-detect": "^1.0.3",
"sass": "^1.68.0",
@@ -126,8 +127,8 @@
"yup": "^0.28.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-legacy": "^5.4.2",
"@vitejs/plugin-react": "^4.3.4",
"eslint-config-react-app": "^7.0.1",
"vite": "^5.1.6"
},

View File

@@ -1,4 +1,3 @@
// @ts-nocheck
import React from 'react';
import {
FormGroup,
@@ -12,7 +11,7 @@ import {
HTMLSelect,
} from '@blueprintjs-formik/core';
import { MultiSelect, SuggestField } from '@blueprintjs-formik/select';
import { DateInput } from '@blueprintjs-formik/datetime';
import { DateInput, TimezoneSelect } from '@blueprintjs-formik/datetime';
import { FSelect } from './Select';
export {
@@ -29,4 +28,5 @@ export {
TextArea as FTextArea,
DateInput as FDateInput,
HTMLSelect as FHTMLSelect,
TimezoneSelect as FTimezoneSelect,
};

View File

@@ -9,6 +9,7 @@ import UsersActions from '@/containers/Preferences/Users/UsersActions';
import CurrenciesActions from '@/containers/Preferences/Currencies/CurrenciesActions';
import WarehousesActions from '@/containers/Preferences/Warehouses/WarehousesActions';
import BranchesActions from '@/containers/Preferences/Branches/BranchesActions';
import ApiKeysActions from '@/containers/Preferences/ApiKeys/ApiKeysActions';
import withDashboard from '@/containers/Dashboard/withDashboard';
import { compose } from '@/utils';
@@ -48,6 +49,11 @@ function PreferencesTopbar({ preferencesPageTitle }) {
path={'/preferences/branches'}
component={BranchesActions}
/>
<Route
exact
path={'/preferences/api-keys'}
component={ApiKeysActions}
/>
</Switch>
</Route>
</div>

View File

@@ -28,10 +28,11 @@ export function StepperStep({
isCompleted={state === StepperStepState.Completed}
isActive={state === StepperStepState.Progress}
>
{state === StepperStepState.Completed && (
{state === StepperStepState.Completed ? (
<Icon icon={'done'} iconSize={24} />
) : (
<StepIconText>{step}</StepIconText>
)}
<StepIconText>{step}</StepIconText>
</StepIcon>
</StepIconWrap>

View File

@@ -22,6 +22,7 @@ export const getDefaultAPAgingSummaryQuery = () => {
filterByOption: 'without-zero-balance',
vendorsIds: [],
branchesIds: [],
numberFormat: {},
};
};

View File

@@ -21,6 +21,7 @@ export const getDefaultARAgingSummaryQuery = () => {
filterByOption: 'without-zero-balance',
customersIds: [],
branchesIds: [],
numberFormat: {},
};
};

View File

@@ -10,7 +10,7 @@ import { useBalanceSheetContext } from '../../BalanceSheetProvider';
export default function BalanceSheetPdfDialogContent() {
const { httpQuery } = useBalanceSheetContext();
const { isLoading, pdfUrl } = useBalanceSheetPdf({ ...httpQuery });
const { isLoading, isLoaded, pdfUrl } = useBalanceSheetPdf({ ...httpQuery });
return (
<DialogContent>
@@ -18,8 +18,10 @@ export default function BalanceSheetPdfDialogContent() {
<AnchorButton
href={pdfUrl}
target={'__blank'}
minimal={true}
outlined={true}
disabled={!isLoaded}
small
minimal
outlined
>
<T id={'pdf_preview.preview.button'} />
</AnchorButton>
@@ -27,8 +29,11 @@ export default function BalanceSheetPdfDialogContent() {
<AnchorButton
href={pdfUrl}
download={'invoice.pdf'}
minimal={true}
outlined={true}
disabled={!isLoaded}
small
minimal
outlined
>
<T id={'pdf_preview.download.button'} />
</AnchorButton>

View File

@@ -33,6 +33,7 @@ export const getDefaultBalanceSheetQuery = () => ({
percentageOfRow: false,
branchesIds: [],
numberFormat: {},
});
/**

View File

@@ -17,6 +17,7 @@ export const getDefaultCashFlowSheetQuery = () => {
displayColumnsType: 'total',
filterByOption: 'with-transactions',
branchesIds: [],
numberFormat: {},
};
};

View File

@@ -18,6 +18,7 @@ export const getInventoryItemDetailsDefaultQuery = () => ({
itemsIds: [],
warehousesIds: [],
branchesIds: [],
numberFormat: {},
});
/**

View File

@@ -35,6 +35,7 @@ export const getDefaultProfitLossQuery = () => ({
percentageExpense: false,
branchesIds: [],
numberFormat: {},
});
/**
@@ -50,7 +51,6 @@ const parseProfitLossQuery = (locationQuery) => {
return {
...transformed,
// Ensures the branches ids is always array.
branchesIds: castArray(transformed.branchesIds),
};

View File

@@ -17,6 +17,7 @@ export function getDefaultTrialBalanceQuery() {
basis: 'accrual',
filterByOption: 'with-transactions',
branchesIds: [],
numberFormat: {},
};
}

View File

@@ -42,7 +42,6 @@ export const transformAccountsFilter = (form) => {
*/
export const transformFilterFormToQuery = (form) => {
return R.compose(
R.curry(flatten)({ safe: true }),
transfromToSnakeCase,
transformAccountsFilter,
transformDisplayColumnsType,

View File

@@ -69,7 +69,7 @@ function GlobalErrors({
if (globalErrors.transactionsLocked) {
AppToaster.show({
message: intl.get('global_error.transactions_locked', {
lockedToDate: globalErrors.transactionsLocked.formatted_locked_to_date,
lockedToDate: globalErrors.transactionsLocked.formattedLockedToDate,
}),
intent: Intent.DANGER,
onDismiss: () => {

View File

@@ -23,6 +23,8 @@ export function PaymentInvoicePreviewContent() {
termsConditions={sharableLinkMeta?.termsConditions}
statement={sharableLinkMeta?.invoiceMessage}
companyName={sharableLinkMeta?.companyName}
primaryColor={sharableLinkMeta?.brandingTemplate?.primaryColor}
secondaryColor={sharableLinkMeta?.brandingTemplate?.secondaryColor}
lines={sharableLinkMeta?.entries?.map((entry) => ({
item: entry.itemName,
description: entry.description,

View File

@@ -1,6 +1,9 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { x } from '@xstyled/emotion';
import { css } from '@emotion/css';
import { useIsDarkMode } from '@/hooks/useDarkMode';
import WorkflowIcon from './WorkflowIcon';
import { FormattedMessage as T } from '@/components';
@@ -8,13 +11,12 @@ import { FormattedMessage as T } from '@/components';
import withOrganizationActions from '@/containers/Organization/withOrganizationActions';
import { compose } from '@/utils';
import '@/style/pages/Setup/Congrats.scss';
/**
* Setup congrats page.
*/
function SetupCongratsPage({ setOrganizationSetupCompleted }) {
const [isReloading, setIsReloading] = React.useState(false);
const isDarkMode = useIsDarkMode();
const handleBtnClick = () => {
setIsReloading(true);
@@ -22,30 +24,55 @@ function SetupCongratsPage({ setOrganizationSetupCompleted }) {
};
return (
<div class="setup-congrats">
<div class="setup-congrats__workflow-pic">
<x.div
w={'500px'}
mx="auto"
textAlign="center"
pt={'80px'}
>
<x.div>
<WorkflowIcon width="280" height="330" />
</div>
</x.div>
<div class="setup-congrats__text">
<h1>
<T id={'setup.congrats.title'} />
</h1>
<p class="paragraph">
<T id={'setup.congrats.description'} />
</p>
<Button
intent={Intent.PRIMARY}
type="submit"
loading={isReloading}
onClick={handleBtnClick}
<x.div mt={30}>
<x.h2
color={isDarkMode ? 'rgba(255, 255, 255, 0.85)' : '#2d2b43'}
mb={'12px'}
>
<T id={'setup.congrats.go_to_dashboard'} />
</Button>
</div>
</div>
<T id={'setup.congrats.title'} />
</x.h2>
<x.p
fontSize={'16px'}
opacity={0.85}
mb={'14px'}
color={isDarkMode ? 'rgba(255, 255, 255, 0.7)' : undefined}
>
<T id={'setup.congrats.description'} />
</x.p>
<x.div
className={css`
.bp4-button {
height: 38px;
padding-left: 25px;
padding-right: 25px;
font-size: 15px;
margin-top: 12px;
}
`}
>
<Button
intent={Intent.PRIMARY}
type="submit"
loading={isReloading}
onClick={handleBtnClick}
>
<T id={'setup.congrats.go_to_dashboard'} />
</Button>
</x.div>
</x.div>
</x.div>
);
}

View File

@@ -2,6 +2,9 @@
import React from 'react';
import { ProgressBar, Intent } from '@blueprintjs/core';
import * as R from 'ramda';
import { x } from '@xstyled/emotion';
import { css } from '@emotion/css';
import { useIsDarkMode } from '@/hooks/useDarkMode';
import { useJob, useCurrentOrganization } from '@/hooks/query';
import { FormattedMessage as T } from '@/components';
@@ -10,8 +13,6 @@ import withOrganizationActions from '@/containers/Organization/withOrganizationA
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import withOrganization from '../Organization/withOrganization';
import '@/style/pages/Setup/Initializing.scss';
/**
* Setup initializing step form.
*/
@@ -47,7 +48,7 @@ function SetupInitializingForm({
}, [setOrganizationSetupCompleted, isJobDone, isSuccess]);
return (
<div class="setup-initializing-form">
<x.div w="95%" mx="auto" pt="16%">
{isFailed ? (
<SetupInitializingFailed />
) : isRunning || isWaiting || isJobFetching ? (
@@ -57,7 +58,7 @@ function SetupInitializingForm({
) : (
<SetupInitializingFailed />
)}
</div>
</x.div>
);
}
@@ -73,17 +74,29 @@ export default R.compose(
* State initializing failed state.
*/
function SetupInitializingFailed() {
const isDarkMode = useIsDarkMode();
return (
<div class="setup-initializing__content">
<div className={'setup-initializing-form__title'}>
<h1>
<x.div>
<x.div textAlign="center" mt={35}>
<x.h1
fontSize={'22px'}
fontWeight={500}
color={isDarkMode ? 'rgba(255, 255, 255, 0.75)' : '#454c59'}
mt={0}
mb={'14px'}
>
<T id={'setup.initializing.something_went_wrong'} />
</h1>
<p class="paragraph">
</x.h1>
<x.p
w="70%"
mx="auto"
color={isDarkMode ? 'rgba(255, 255, 255, 0.7)' : '#2e4266'}
>
<T id={'setup.initializing.please_refresh_the_page'} />
</p>
</div>
</div>
</x.p>
</x.div>
</x.div>
);
}
@@ -91,19 +104,49 @@ function SetupInitializingFailed() {
* Setup initializing running state.
*/
function SetupInitializingRunning() {
return (
<div class="setup-initializing__content">
<ProgressBar intent={Intent.PRIMARY} value={null} />
const isDarkMode = useIsDarkMode();
<div className={'setup-initializing-form__title'}>
<h1>
const progressBarStyles = css`
.bp4-progress-bar {
border-radius: 40px;
display: block;
height: 6px;
overflow: hidden;
position: relative;
width: 80%;
margin: 0 auto;
.bp4-progress-meter {
background-color: #809cb3;
}
}
`;
return (
<x.div>
<x.div className={progressBarStyles}>
<ProgressBar intent={Intent.NONE} value={null} />
</x.div>
<x.div textAlign="center" mt={35}>
<x.h1
fontSize={'22px'}
fontWeight={500}
color={isDarkMode ? 'rgba(255, 255, 255, 0.85)' : '#454c59'}
mt={0}
mb={'14px'}
>
<T id={'setup.initializing.title'} />
</h1>
<p className={'paragraph'}>
</x.h1>
<x.p
w="70%"
mx="auto"
color={isDarkMode ? 'rgba(255, 255, 255, 0.7)' : '#2e4266'}
>
<T id={'setup.initializing.description'} />
</p>
</div>
</div>
</x.p>
</x.div>
</x.div>
);
}
@@ -111,18 +154,30 @@ function SetupInitializingRunning() {
* Setup initializing completed state.
*/
function SetupInitializingCompleted() {
const isDarkMode = useIsDarkMode();
return (
<div class="setup-initializing__content">
<div className={'setup-initializing-form__title'}>
<h1>
<x.div>
<x.div textAlign="center" mt={35}>
<x.h1
fontSize={'22px'}
fontWeight={600}
color={isDarkMode ? 'rgba(255, 255, 255, 0.85)' : '#454c59'}
mt={0}
mb={'14px'}
>
<T id={'setup.initializing.waiting_to_redirect'} />
</h1>
<p class="paragraph">
</x.h1>
<x.p
w="70%"
mx="auto"
color={isDarkMode ? 'rgba(255, 255, 255, 0.7)' : '#2e4266'}
>
<T
id={'setup.initializing.refresh_the_page_if_redirect_not_worked'}
/>
</p>
</div>
</div>
</x.p>
</x.div>
</x.div>
);
}

View File

@@ -5,15 +5,18 @@ import { Button, Intent, FormGroup, Classes } from '@blueprintjs/core';
import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { getAllCountries } from '@bigcapital/utils';
import { x } from '@xstyled/emotion';
import {
FFormGroup,
FInputGroup,
FSelect,
FTimezoneSelect,
FormattedMessage as T,
} from '@/components';
import { Col, Row } from '@/components';
import { inputIntent } from '@/utils';
import { useIsDarkMode } from '@/hooks/useDarkMode';
import { getFiscalYear } from '@/constants/fiscalYearOptions';
import { getLanguages } from '@/constants/languagesOptions';
@@ -28,19 +31,24 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
const FiscalYear = getFiscalYear();
const Languages = getLanguages();
const currencies = getAllCurrenciesOptions();
const isDarkMode = useIsDarkMode();
return (
<Form>
<h3>
<x.h3
color={isDarkMode ? 'rgba(255, 255, 255, 0.5)' : '#868f9f'}
mb="2rem"
fontWeight={600}
>
<T id={'organization_details'} />
</h3>
</x.h3>
{/* ---------- Organization name ---------- */}
<FFormGroup
name={'name'}
label={<T id={'legal_organization_name'} />}
fastField={true}
fastField
>
<FInputGroup name={'name'} fastField={true} />
<FInputGroup name={'name'} large fastField />
</FFormGroup>
{/* ---------- Location ---------- */}
@@ -56,7 +64,8 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
textAccessor={'name'}
placeholder={<T id={'select_business_location'} />}
popoverProps={{ minimal: true }}
fastField={true}
buttonProps={{ large: true }}
fastField
/>
</FFormGroup>
@@ -75,18 +84,15 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
valueAccessor={'key'}
textAccessor={'name'}
placeholder={<T id={'select_base_currency'} />}
fastField={true}
buttonProps={{ large: true }}
fastField
/>
</FFormGroup>
</Col>
{/* ---------- Language ---------- */}
<Col xs={6}>
<FFormGroup
name={'language'}
label={<T id={'language'} />}
fastField={true}
>
<FFormGroup name={'language'} label={<T id={'language'} />} fastField>
<FSelect
name={'language'}
items={Languages}
@@ -94,7 +100,8 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
textAccessor={'name'}
placeholder={<T id={'select_language'} />}
popoverProps={{ minimal: true }}
fastField={true}
buttonProps={{ large: true }}
fastField
/>
</FFormGroup>
</Col>
@@ -104,7 +111,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FFormGroup
name={'fiscalYear'}
label={<T id={'fiscal_year'} />}
fastField={true}
fastField
>
<FSelect
name={'fiscalYear'}
@@ -113,50 +120,48 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
textAccessor={'name'}
placeholder={<T id={'select_fiscal_year'} />}
popoverProps={{ minimal: true }}
fastField={true}
buttonProps={{ large: true }}
fastField
/>
</FFormGroup>
{/* ---------- Time zone ---------- */}
<FastField name={'timezone'}>
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={<T id={'time_zone'} />}
className={classNames(
'form-group--time-zone',
'form-group--select-list',
Classes.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'timezone'} />}
>
<TimezonePicker
value={value}
onChange={(item) => {
setFieldValue('timezone', item);
}}
valueDisplayFormat="composite"
showLocalTimezone={true}
placeholder={<T id={'select_time_zone'} />}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</FastField>
<FFormGroup name={'timezone'} label={<T id={'time_zone'} />}>
<FTimezoneSelect
name={'timezone'}
valueDisplayFormat="composite"
showLocalTimezone={true}
placeholder={<T id={'select_time_zone'} />}
popoverProps={{ minimal: true }}
buttonProps={{
alignText: 'left',
fill: true,
large: true,
}}
/>
</FFormGroup>
<p className={'register-org-note'}>
<x.p
fontSize={14}
lineHeight="2.7rem"
mb={6}
borderBottom={`1px solid ${isDarkMode ? 'rgba(255, 255, 255, 0.1)' : '#f5f5f5'}`}
className={Classes.TEXT_MUTED}
>
<T id={'setup.organization.note_you_can_change_your_preferences'} />
</p>
</x.p>
<div className={'register-org-button'}>
<Button intent={Intent.PRIMARY} loading={isSubmitting} type="submit">
<x.div>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
fill
large
type="submit"
>
<T id={'save_continue'} />
</Button>
</div>
</x.div>
</Form>
);
}

View File

@@ -2,16 +2,15 @@
import React from 'react';
import { Formik } from 'formik';
import { FormattedMessage as T } from '@/components';
import '@/style/pages/Setup/Organization.scss';
import { x } from '@xstyled/emotion';
import SetupOrganizationForm from './SetupOrganizationForm';
import { useOrganizationSetup } from '@/hooks/query';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { setCookie, compose, transfromToSnakeCase } from '@/utils';
import { getSetupOrganizationValidation } from './SetupOrganization.schema';
import { setCookie, compose, transfromToSnakeCase } from '@/utils';
// Initial values.
const defaultValues = {
@@ -53,17 +52,22 @@ function SetupOrganizationPage({ wizard }) {
};
return (
<div className={'setup-organization'}>
<x.div
maxWidth={'600px'}
w="100%"
mx="auto"
pt={'45px'}
pb={'20px'}
px={'25px'}
>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
component={SetupOrganizationForm}
onSubmit={handleSubmit}
/>
</div>
</x.div>
);
}
export default compose(
withSettingsActions,
)(SetupOrganizationPage);
export default compose(withSettingsActions)(SetupOrganizationPage);

View File

@@ -1,5 +1,6 @@
// @ts-nocheck
import React from 'react';
import { x } from '@xstyled/emotion';
import SetupWizardContent from './SetupWizardContent';
@@ -27,9 +28,9 @@ function SetupRightSection({
isSubscriptionActive,
}) {
return (
<section className={'setup-page__right-section'}>
<x.section w="100%" overflow="auto">
<SetupWizardContent stepId={setupStepId} stepIndex={setupStepIndex} />
</section>
</x.section>
);
}

View File

@@ -1,3 +0,0 @@
.items {
padding: 40px 40px 20px;
}

View File

@@ -1,18 +1,23 @@
// @ts-nocheck
import React from 'react';
import { x } from '@xstyled/emotion';
import { css } from '@emotion/css';
import SetupSubscription from './SetupSubscription/SetupSubscription';
import SetupOrganizationPage from './SetupOrganizationPage';
import SetupInitializingForm from './SetupInitializingForm';
import SetupCongratsPage from './SetupCongratsPage';
import { Stepper } from '@/components/Stepper';
import styles from './SetupWizardContent.module.scss';
interface SetupWizardContentProps {
stepIndex: number;
stepId: string;
}
const itemsClassName = css`
padding: 40px 40px 20px;
`;
/**
* Setup wizard content.
*/
@@ -21,12 +26,11 @@ export default function SetupWizardContent({
stepId,
}: SetupWizardContentProps) {
return (
<div class="setup-page__content">
<x.div w="100%" overflow="auto">
<Stepper
active={stepIndex}
classNames={{
content: styles.content,
items: styles.items,
items: itemsClassName,
}}
>
<Stepper.Step label={'Subscription'}>
@@ -45,6 +49,6 @@ export default function SetupWizardContent({
<SetupCongratsPage id="congrats" />
</Stepper.Step>
</Stepper>
</div>
</x.div>
);
}

View File

@@ -99,7 +99,7 @@ export const useEditPdfTemplate = (
>(
({ templateId, values }) =>
apiRequest
.post(`/pdf-templates/${templateId}`, transfromToSnakeCase(values))
.put(`/pdf-templates/${templateId}`, transfromToSnakeCase(values))
.then((res) => res.data),
{
onSuccess: () => {

View File

@@ -1,16 +1,10 @@
// @ts-nocheck
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
import {
ParseOptions,
ParsedQuery,
StringifyOptions,
parse,
stringify,
} from 'query-string';
import * as qs from 'qs';
import { useHistory } from 'react-router';
export interface QueryStringResult {
[0]: ParsedQuery;
[0]: Record<string, any>;
[1]: Dispatch<SetStateAction<Record<string, any>>>;
}
@@ -20,6 +14,61 @@ type NavigateCallback = (
stringifedParams: string,
) => void;
type ParseOptions = {
parseNumbers?: boolean;
parseBooleans?: boolean;
[key: string]: any;
};
type StringifyOptions = qs.IStringifyOptions;
/**
* Checks if a string represents a number (including negatives, decimals, scientific notation)
*/
const isNumber = (val: string): boolean => {
return !isNaN(parseFloat(val)) && isFinite(Number(val)) && val !== '';
};
/**
* Checks if a string represents a boolean
*/
const isBoolean = (val: string): boolean => {
return val === 'false' || val === 'true';
};
/**
* Custom decoder for qs to parse numbers and booleans
* Based on query-types library approach: https://github.com/xpepermint/query-types
*/
const createDecoder = (parseNumbers: boolean, parseBooleans: boolean) => {
return (str: string, defaultDecoder?: any, charset?: string, type?: 'key' | 'value') => {
// Only decode values, not keys
if (type === 'key') {
return defaultDecoder ? defaultDecoder(str, defaultDecoder, charset) : str;
}
// First decode using default decoder
const decoded = defaultDecoder ? defaultDecoder(str, defaultDecoder, charset) : decodeURIComponent(str);
// Handle empty strings and undefined
if (typeof decoded === 'undefined' || decoded === '') {
return null;
}
// Parse booleans first (before numbers, as 'true'/'false' are strings)
if (parseBooleans && isBoolean(decoded)) {
return decoded === 'true';
}
// Parse numbers if enabled (handles integers, decimals, negatives, scientific notation)
if (parseNumbers && isNumber(decoded)) {
return Number(decoded);
}
return decoded;
};
};
/**
* Query string.
* @param {Location} location
@@ -35,14 +84,28 @@ export function useQueryString(
stringifyOptions?: StringifyOptions,
): QueryStringResult {
const isFirst = useRef(true);
const [state, setState] = useState(parse(location.search, parseOptions));
// Extract parseNumbers and parseBooleans from parseOptions
const { parseNumbers = false, parseBooleans = false, ...qsParseOptions } = parseOptions || {};
// Create decoder if needed
const parseConfig = {
...qsParseOptions,
...(parseNumbers || parseBooleans ? {
decoder: createDecoder(parseNumbers, parseBooleans),
} : {}),
};
const [state, setState] = useState(
qs.parse(location.search.substring(1), parseConfig)
);
useEffect((): void => {
if (isFirst.current) {
isFirst.current = false;
} else {
const pathname = location.pathname;
const stringifedParams = stringify(state, stringifyOptions);
const stringifedParams = qs.stringify(state, stringifyOptions);
const pathnameWithParams = pathname + '?' + stringifedParams;
navigate(pathnameWithParams, pathname, stringifedParams);
@@ -52,7 +115,7 @@ export function useQueryString(
const setQuery: typeof setState = (values): void => {
const nextState = typeof values === 'function' ? values(state) : values;
setState(
(state): ParsedQuery => ({
(state): Record<string, any> => ({
...state,
...nextState,
}),
@@ -78,7 +141,6 @@ export const useAppQueryString = (
window.location,
(pathnameWithParams, pathname, stringifiedParams) => {
history.push({ pathname, search: stringifiedParams });
navigate && navigate(pathnameWithParams, pathname, stringifiedParams);
},
{

View File

@@ -64,12 +64,11 @@ export default function useApiRequest() {
setGlobalErrors({ too_many_requests: true });
}
if (status === 400) {
if (
data.errors.find(
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
)
) {
setGlobalErrors({ transactionsLocked: { ...lockedError.data } });
const lockedError = data.errors.find(
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
);
if (lockedError) {
setGlobalErrors({ transactionsLocked: { ...lockedError.payload } });
}
if (
data.errors.find(

View File

@@ -52,7 +52,6 @@ body {
.App {
min-width: 1100px;
min-height: 100vh;
background-color: var(--color-app-background);
}
// =======
@@ -234,22 +233,12 @@ html[lang^='ar'] {
.dialog__header-actions {
position: absolute;
right: 50px;
right: 44px;
top: 0;
z-index: 9999999;
margin: 6px;
.bp4-button {
border-color: rgba(0, 0, 0, 0.25);
color: rgb(25, 32, 37);
min-height: 30px;
padding-left: 14px;
padding-right: 14px;
&+.bp4-button {
margin-left: 8px;
}
}
margin: 9px 6px 6px;
display: flex;
gap: 10px;
}
.bp4-dialog {

View File

@@ -61,7 +61,6 @@ $ns: bp4;
--color-ui-menu-select-item-active-background: var(--color-primary);
--color-app-body: #fff;
--color-app-background: #fff;
// Splash screen.
--color-splash-screen-background: #fff;
@@ -359,7 +358,6 @@ body.bp4-dark {
// App
--color-app-body: var(--color-dark-gray1);
--color-app-background: var(--color-dark-gray1);
// Splash screen.
--color-splash-screen-background: #fff;

View File

@@ -1,9 +1,9 @@
.bp4-button {
--color-button-color: #555;
--color-button-color: var(--color-light-gray3);
.bp4-dark & {
--color-button-color: var(--color-light-gray3);
}
min-width: 32px;
min-height: 32px;
padding-left: 12px;

View File

@@ -1,30 +0,0 @@
.setup-congrats {
width: 500px;
margin: 0 auto;
text-align: center;
padding-top: 80px;
&__page {
}
&__text {
margin-top: 30px;
h1 {
color: #2d2b43;
margin-bottom: 12px;
}
.paragraph {
font-size: 16px;
opacity: 0.85;
margin-bottom: 14px;
}
.bp4-button {
height: 38px;
padding-left: 25px;
padding-right: 25px;
font-size: 15px;
margin-top: 12px;
}
}
}

View File

@@ -1,41 +0,0 @@
// Setup initializing form
.setup-initializing-form {
width: 95%;
margin: 0 auto;
padding: 16% 0 0;
.bp4-progress-bar {
background: rgba(92, 112, 128, 0.2);
border-radius: 40px;
display: block;
height: 6px;
overflow: hidden;
position: relative;
width: 80%;
margin: 0 auto;
.bp4-progress-meter {
background-color: #809cb3;
}
}
&__title {
text-align: center;
margin-top: 35px;
h1 {
font-size: 22px;
font-weight: 600;
color: #454c59;
margin-top: 0;
margin-bottom: 14px;
}
.paragraph {
width: 70%;
margin: 0 auto;
color: #2e4266;
}
}
}

View File

@@ -1,65 +1 @@
.setup-organization {
max-width: 600px;
width: 100%;
margin: 0 auto;
padding: 45px 25px 20px;
form {
h3 {
color: #868f9f;
margin-bottom: 2rem;
font-weight: 600;
}
}
.bp4-form-group {
margin-bottom: 24px;
.bp4-input-group {
.bp4-input {
height: 38px;
}
}
.bp4-input,
.form-group--select-list .bp4-button{
font-size: 15px;
}
}
label.bp4-label{
color: #20242e;
}
.bp4-button:not([class*='bp4-intent-']):not(.bp4-minimal) {
width: 100%;
height: 38px;
padding: 8px;
}
.bp4-text-muted {
color: #000000;
}
.register-org-note {
font-size: 13px;
padding-bottom: 10px;
border-bottom: 1px solid #e1e1e1;
margin-bottom: 1.75rem;
color: #666;
}
.register-org-button {
.bp4-button {
background-color: #1c2448;
height: 40px;
font-size: 15px;
width: 100%;
&:disabled,
&.bp4-loading{
background-color: rgba(28, 36, 72, 0.5);
}
}
}
}

View File

@@ -13,30 +13,13 @@
grid-template-columns: 26% 74%;
}
&__right-section {
width: 100%;
overflow: auto;
h1 {
font-size: 22px;
}
h1,
h3 {
font-weight: 500;
color: #6b7382;
}
}
&__content {
width: 100%;
padding-bottom: 80px;
}
&__left-section {
background-color: #2f3d6f;
z-index: 1;
width: 100%;
.bp4-dark & {
background-color: #2f343c;
}
.content {
display: flex;
flex-direction: column;
@@ -173,9 +156,7 @@
&::before {
background-color: #75859c;
}
~li {
&:before,
&:after {
background: #e0e0e0;

469
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
import { x } from '@xstyled/emotion';
import { Box } from '../lib/layout/Box';
export interface TableColumn {
key: string;
label: string;
style?: string;
}
export interface TableCell {
key: string;
value: string;
}
export interface TableRow {
cells: TableCell[];
classNames?: string;
}
export interface FinancialSheetTemplateProps {
organizationName: string;
sheetName?: string;
sheetDate?: string;
table: {
columns: TableColumn[];
rows: TableRow[];
};
customCSS?: string;
}
export function FinancialSheetTemplate({
organizationName,
sheetName,
sheetDate,
table,
customCSS,
}: FinancialSheetTemplateProps) {
return (
<Box fontSize="14px">
<Box p="20px">
<Box textAlign="center" mb="1rem">
<Box m={0} fontSize="1.4rem">
{organizationName}
</Box>
{sheetName && <Box m={0}>{sheetName}</Box>}
{sheetDate && <Box mt="0.35rem">{sheetDate}</Box>}
</Box>
<x.table
borderTop="1px solid #000"
textAlign="left"
fontSize="inherit"
w="100%"
tableLayout="auto"
borderCollapse="collapse"
>
<x.thead>
<x.tr>
{table.columns.map((column) => (
<x.th
key={column.key}
color="#000"
borderBottom="1px solid #000000"
p="0.5rem"
className={`column--${column.key}`}
>
{column.label}
</x.th>
))}
</x.tr>
</x.thead>
<x.tbody>
{table.rows.map((row, rowIndex) => (
<x.tr key={rowIndex} className={row.classNames}>
{row.cells.map((cell) => (
<x.td
key={cell.key}
pt="0.28rem"
pb="0.28rem"
pl="0.5rem"
pr="0.5rem"
color="#252A31"
borderBottom="1px solid transparent"
className={`cell--${cell.key}`}
>
<span dangerouslySetInnerHTML={{ __html: cell.value }} />
</x.td>
))}
</x.tr>
))}
</x.tbody>
</x.table>
</Box>
</Box>
);
}

View File

@@ -3,8 +3,10 @@ export * from './components/InvoicePaperTemplate';
export * from './components/EstimatePaperTemplate';
export * from './components/ReceiptPaperTemplate';
export * from './components/PaymentReceivedPaperTemplate';
export * from './components/FinancialSheetTemplate';
export * from './renders/render-invoice-paper-template';
export * from './renders/render-estimate-paper-template';
export * from './renders/render-receipt-paper-template';
export * from './renders/render-payment-received-paper-template';
export * from './renders/render-financial-sheet-template';

View File

@@ -0,0 +1,14 @@
import {
FinancialSheetTemplate,
FinancialSheetTemplateProps,
} from '../components/FinancialSheetTemplate';
import { renderSSR } from './render-ssr';
export const renderFinancialSheetTemplateHtml = (
props: FinancialSheetTemplateProps
) => {
return renderSSR(
<FinancialSheetTemplate {...props} />
);
};

View File

@@ -1 +0,0 @@

View File

@@ -1 +0,0 @@

View File

@@ -1 +0,0 @@