Compare commits
33 Commits
listen-pay
...
fix-gettin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f92acbcbe0 | ||
|
|
c986585cd9 | ||
|
|
250f0a30ef | ||
|
|
84b5e1adc1 | ||
|
|
2ab28370db | ||
|
|
a88a525326 | ||
|
|
fce8e2c5a4 | ||
|
|
095608266c | ||
|
|
0ec8aaa330 | ||
|
|
9fcb3ef77d | ||
|
|
dc61c57daf | ||
|
|
af284f3f6d | ||
|
|
c43123db76 | ||
|
|
ebbcab3926 | ||
|
|
a235f573c0 | ||
|
|
84a0b8f495 | ||
|
|
b87321c897 | ||
|
|
fc6ebfea5c | ||
|
|
c9fe6d9b37 | ||
|
|
161d60393a | ||
|
|
79413fa85e | ||
|
|
58552c6c94 | ||
|
|
2072e35cfa | ||
|
|
1eaac9d691 | ||
|
|
a916e8a0cb | ||
|
|
a56f560036 | ||
|
|
0fb886936c | ||
|
|
670136916f | ||
|
|
768297f137 | ||
|
|
ef505a0a62 | ||
|
|
42d40620ec | ||
|
|
959ef7a691 | ||
|
|
e44ebb700a |
@@ -37,6 +37,7 @@
|
||||
"agendash": "^3.1.0",
|
||||
"app-root-path": "^3.0.0",
|
||||
"async": "^3.2.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.6.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
||||
@@ -246,8 +246,11 @@ module.exports = {
|
||||
apiKey: process.env.LOOPS_API_KEY,
|
||||
},
|
||||
|
||||
oneClickDemoAccounts: parseBoolean(
|
||||
process.env.ONE_CLICK_DEMO_ACCOUNTS,
|
||||
false
|
||||
),
|
||||
/**
|
||||
* One-click demo accounts.
|
||||
*/
|
||||
oneClickDemoAccounts: {
|
||||
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
|
||||
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export default class SeedSettings extends TenantSeeder {
|
||||
|
||||
// Manual journals settings.
|
||||
{ group: 'manual_journals', key: 'next_number', value: '00001' },
|
||||
{ group: 'manual_journals', key: 'number_prefix', value: 'J-' },
|
||||
{ group: 'manual_journals', key: 'auto_increment', value: true },
|
||||
|
||||
// Sale invoices settings.
|
||||
|
||||
@@ -76,6 +76,10 @@ export interface IAuthSendedResetPassword {
|
||||
|
||||
export interface IAuthGetMetaPOJO {
|
||||
signupDisabled: boolean;
|
||||
oneClickDemo: {
|
||||
enable: boolean;
|
||||
demoUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAuthSignUpVerifingEventPayload {
|
||||
|
||||
@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
||||
|
||||
export interface IImportFileCommitedEventPayload {
|
||||
tenantId: number;
|
||||
importId: number;
|
||||
importId: string;
|
||||
meta: ImportFilePreviewPOJO;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
||||
import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
|
||||
import { EXPORT_DTE_FORMAT } from '@/services/Export/constants';
|
||||
|
||||
export class Transformer {
|
||||
public context: any;
|
||||
@@ -156,22 +157,34 @@ export class Transformer {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param date
|
||||
* @returns
|
||||
* Format date.
|
||||
* @param {} date
|
||||
* @param {string} format -
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatDate(date) {
|
||||
return date ? moment(date).format(this.dateFormat) : '';
|
||||
protected formatDate(date, format?: string) {
|
||||
// Use the export date format if the async operation is in exporting,
|
||||
// otherwise use the given or default format.
|
||||
const _format = this.context.exportAls.isExport
|
||||
? EXPORT_DTE_FORMAT
|
||||
: format || this.dateFormat;
|
||||
|
||||
return date ? moment(date).format(_format) : '';
|
||||
}
|
||||
|
||||
protected formatDateFromNow(date){
|
||||
/**
|
||||
*
|
||||
* @param date
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatDateFromNow(date) {
|
||||
return date ? moment(date).fromNow(true) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param number
|
||||
* @returns
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatNumber(number, props?) {
|
||||
return formatNumber(number, { money: false, ...props });
|
||||
@@ -181,7 +194,7 @@ export class Transformer {
|
||||
*
|
||||
* @param money
|
||||
* @param options
|
||||
* @returns
|
||||
* @returns {}
|
||||
*/
|
||||
protected formatMoney(money, options?) {
|
||||
return formatNumber(money, {
|
||||
|
||||
@@ -3,12 +3,17 @@ import { isNull } from 'lodash';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
import { Transformer } from './Transformer';
|
||||
import { ImportAls } from '@/services/Import/ImportALS';
|
||||
import { ExportAls } from '@/services/Export/ExportAls';
|
||||
|
||||
@Service()
|
||||
export class TransformerInjectable {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private exportAls: ExportAls;
|
||||
|
||||
/**
|
||||
* Retrieves the application context of all tenant transformers.
|
||||
* @param {number} tenantId
|
||||
@@ -17,10 +22,12 @@ export class TransformerInjectable {
|
||||
async getApplicationContext(tenantId: number) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const organization = await TenantMetadata.query().findOne({ tenantId });
|
||||
const exportAls = this.exportAls;
|
||||
|
||||
return {
|
||||
organization,
|
||||
i18n,
|
||||
exportAls,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,11 @@ export default {
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
customerType: {
|
||||
name: 'Customer Type',
|
||||
type: 'text',
|
||||
accessor: 'formattedCustomerType',
|
||||
},
|
||||
firstName: {
|
||||
name: 'vendor.field.first_name',
|
||||
type: 'text',
|
||||
@@ -135,116 +140,117 @@ export default {
|
||||
openingBalance: {
|
||||
name: 'vendor.field.opening_balance',
|
||||
type: 'number',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
openingBalanceAt: {
|
||||
name: 'vendor.field.opening_balance_at',
|
||||
type: 'date',
|
||||
printable: false
|
||||
printable: false,
|
||||
accessor: 'formattedOpeningBalanceAt'
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'vendor.field.currency',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
status: {
|
||||
name: 'vendor.field.status',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
note: {
|
||||
name: 'vendor.field.note',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
// Billing Address
|
||||
billingAddress1: {
|
||||
name: 'Billing Address 1',
|
||||
column: 'billing_address1',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddress2: {
|
||||
name: 'Billing Address 2',
|
||||
column: 'billing_address2',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressCity: {
|
||||
name: 'Billing Address City',
|
||||
column: 'billing_address_city',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressCountry: {
|
||||
name: 'Billing Address Country',
|
||||
column: 'billing_address_country',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressPostcode: {
|
||||
name: 'Billing Address Postcode',
|
||||
column: 'billing_address_postcode',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressState: {
|
||||
name: 'Billing Address State',
|
||||
column: 'billing_address_state',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
billingAddressPhone: {
|
||||
name: 'Billing Address Phone',
|
||||
column: 'billing_address_phone',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
// Shipping Address
|
||||
shippingAddress1: {
|
||||
name: 'Shipping Address 1',
|
||||
column: 'shipping_address1',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddress2: {
|
||||
name: 'Shipping Address 2',
|
||||
column: 'shipping_address2',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressCity: {
|
||||
name: 'Shipping Address City',
|
||||
column: 'shipping_address_city',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressCountry: {
|
||||
name: 'Shipping Address Country',
|
||||
column: 'shipping_address_country',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressPostcode: {
|
||||
name: 'Shipping Address Postcode',
|
||||
column: 'shipping_address_postcode',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressPhone: {
|
||||
name: 'Shipping Address Phone',
|
||||
column: 'shipping_address_phone',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
shippingAddressState: {
|
||||
name: 'Shipping Address State',
|
||||
column: 'shipping_address_state',
|
||||
type: 'text',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
createdAt: {
|
||||
name: 'vendor.field.created_at',
|
||||
type: 'date',
|
||||
printable: false
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { AccountsApplication } from './AccountsApplication';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class AccountsExportable extends Exportable {
|
||||
@@ -20,7 +21,7 @@ export class AccountsExportable extends Exportable {
|
||||
inactiveMode: false,
|
||||
...query,
|
||||
structure: IAccountsStructureType.Flat,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
page: 1,
|
||||
} as IAccountsFilter;
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ export class GetAuthMeta {
|
||||
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
|
||||
return {
|
||||
signupDisabled: config.signupRestrictions.disabled,
|
||||
oneClickDemo: {
|
||||
enable: config.oneClickDemoAccounts.enable,
|
||||
demoUrl: config.oneClickDemoAccounts.demoUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ export class TriggerRecognizedTransactions {
|
||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||
tenantId,
|
||||
importId,
|
||||
meta,
|
||||
}: IImportFileCommitedEventPayload) {
|
||||
const importFile = await Import.query().findOne({ importId });
|
||||
const batch = importFile.paramsParsed.batch;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IItemsFilter } from '@/interfaces';
|
||||
import { CustomersApplication } from './CustomersApplication';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class CustomersExportable extends Exportable {
|
||||
@@ -17,9 +18,9 @@ export class CustomersExportable extends Exportable {
|
||||
const parsedQuery = {
|
||||
sortOrder: 'DESC',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
...query,
|
||||
pageSize: 12,
|
||||
page: 1,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IItemsFilter;
|
||||
|
||||
return this.customersApplication
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IItemsFilter } from '@/interfaces';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { VendorsApplication } from './VendorsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class VendorsExportable extends Exportable {
|
||||
@@ -17,9 +18,9 @@ export class VendorsExportable extends Exportable {
|
||||
const parsedQuery = {
|
||||
sortOrder: 'DESC',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
...query,
|
||||
pageSize: 12,
|
||||
page: 1,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IItemsFilter;
|
||||
|
||||
return this.vendorsApplication
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { IExpensesFilter } from '@/interfaces';
|
||||
import { ExpensesApplication } from './ExpensesApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class ExpensesExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class ExpensesExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IExpensesFilter;
|
||||
|
||||
return this.expensesApplication
|
||||
|
||||
48
packages/server/src/services/Export/ExportAls.ts
Normal file
48
packages/server/src/services/Export/ExportAls.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Service } from 'typedi';
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
@Service()
|
||||
export class ExportAls {
|
||||
private als: AsyncLocalStorage<Map<string, any>>;
|
||||
|
||||
constructor() {
|
||||
this.als = new AsyncLocalStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function within the context of a new AsyncLocalStorage store.
|
||||
* @param callback The function to be executed within the AsyncLocalStorage context.
|
||||
* @returns The result of the callback function.
|
||||
*/
|
||||
public run<T>(callback: () => T): T {
|
||||
return this.als.run<T>(new Map(), () => {
|
||||
this.markAsExport();
|
||||
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current AsyncLocalStorage store.
|
||||
* @returns The current store or undefined if not in a valid context.
|
||||
*/
|
||||
public getStore(): Map<string, any> | undefined {
|
||||
return this.als.getStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an export operation.
|
||||
* @param flag Boolean flag to set or unset the export status. Defaults to true.
|
||||
*/
|
||||
public markAsExport(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isExport', flag);
|
||||
}
|
||||
/**
|
||||
* Checks if the current context is an export operation.
|
||||
* @returns {boolean} True if the context is an export operation, false otherwise.
|
||||
*/
|
||||
public get isExport(): boolean {
|
||||
return !!this.getStore()?.get('isExport');
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { Errors, ExportFormat } from './common';
|
||||
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
|
||||
import { flatDataCollections, getDataAccessor } from './utils';
|
||||
import { ExportPdf } from './ExportPdf';
|
||||
import { ExportAls } from './ExportAls';
|
||||
|
||||
@Service()
|
||||
export class ExportResourceService {
|
||||
@@ -22,13 +23,33 @@ export class ExportResourceService {
|
||||
@Inject()
|
||||
private exportPdf: ExportPdf;
|
||||
|
||||
@Inject()
|
||||
private exportAls: ExportAls;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {string} resourceName
|
||||
* @param {ExportFormat} format
|
||||
* @returns
|
||||
*/
|
||||
public async export(
|
||||
tenantId: number,
|
||||
resourceName: string,
|
||||
format: ExportFormat = ExportFormat.Csv
|
||||
) {
|
||||
return this.exportAls.run(() =>
|
||||
this.exportAlsRun(tenantId, resourceName, format)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the given resource data through csv, xlsx or pdf.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} resourceName - Resource name.
|
||||
* @param {ExportFormat} format - File format.
|
||||
*/
|
||||
public async export(
|
||||
public async exportAlsRun(
|
||||
tenantId: number,
|
||||
resourceName: string,
|
||||
format: ExportFormat = ExportFormat.Csv
|
||||
|
||||
2
packages/server/src/services/Export/constants.ts
Normal file
2
packages/server/src/services/Export/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const EXPORT_SIZE_LIMIT = 9999999;
|
||||
export const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';
|
||||
105
packages/server/src/services/Import/ImportALS.ts
Normal file
105
packages/server/src/services/Import/ImportALS.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Service } from 'typedi';
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
@Service()
|
||||
export class ImportAls {
|
||||
private als: AsyncLocalStorage<Map<string, any>>;
|
||||
|
||||
constructor() {
|
||||
this.als = new AsyncLocalStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function within the context of a new AsyncLocalStorage store.
|
||||
* @param callback The function to be executed within the AsyncLocalStorage context.
|
||||
* @returns The result of the callback function.
|
||||
*/
|
||||
public run<T>(callback: () => T): T {
|
||||
return this.als.run<T>(new Map(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function in preview mode within the AsyncLocalStorage context.
|
||||
* @param callback The function to be executed in preview mode.
|
||||
* @returns The result of the callback function.
|
||||
*/
|
||||
public runPreview<T>(callback: () => T): T {
|
||||
return this.run(() => {
|
||||
this.markAsImport();
|
||||
this.markAsImportPreview();
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback function in commit mode within the AsyncLocalStorage context.
|
||||
* @param {() => T} callback - The function to be executed in commit mode.
|
||||
* @returns {T} The result of the callback function.
|
||||
*/
|
||||
public runCommit<T>(callback: () => T): T {
|
||||
return this.run(() => {
|
||||
this.markAsImport();
|
||||
this.markAsImportCommit();
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current AsyncLocalStorage store.
|
||||
* @returns The current store or undefined if not in a valid context.
|
||||
*/
|
||||
public getStore(): Map<string, any> | undefined {
|
||||
return this.als.getStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an import operation.
|
||||
* @param flag Boolean flag to set or unset the import status. Defaults to true.
|
||||
*/
|
||||
public markAsImport(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isImport', flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an import commit operation.
|
||||
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
|
||||
*/
|
||||
public markAsImportCommit(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isImportCommit', flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current context as an import preview operation.
|
||||
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
|
||||
*/
|
||||
public markAsImportPreview(flag: boolean = true): void {
|
||||
const store = this.getStore();
|
||||
store?.set('isImportPreview', flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current context is an import operation.
|
||||
* @returns {boolean} True if the context is an import operation, false otherwise.
|
||||
*/
|
||||
public get isImport(): boolean {
|
||||
return !!this.getStore()?.get('isImport');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current context is an import commit operation.
|
||||
* @returns {boolean} True if the context is an import commit operation, false otherwise.
|
||||
*/
|
||||
public get isImportCommit(): boolean {
|
||||
return !!this.getStore()?.get('isImportCommit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current context is an import preview operation.
|
||||
* @returns {boolean} True if the context is an import preview operation, false otherwise.
|
||||
*/
|
||||
public get isImportPreview(): boolean {
|
||||
return !!this.getStore()?.get('isImportPreview');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import XLSX from 'xlsx';
|
||||
import bluebird from 'bluebird';
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Service } from 'typedi';
|
||||
@@ -27,23 +26,7 @@ export class ImportFileCommon {
|
||||
|
||||
@Inject()
|
||||
private resource: ResourceService;
|
||||
|
||||
/**
|
||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
||||
* @param {Record<string, any>[]} body - The array of data objects to map.
|
||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
||||
*/
|
||||
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
||||
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Imports the given parsed data to the resource storage through registered importable service.
|
||||
* @param {number} tenantId -
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { ImportAls } from './ImportALS';
|
||||
|
||||
@Service()
|
||||
export class ImportFilePreview {
|
||||
@@ -11,13 +12,31 @@ export class ImportFilePreview {
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private importAls: ImportAls;
|
||||
|
||||
/**
|
||||
* Preview the imported file results before commiting the transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} importId -
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async preview(
|
||||
tenantId: number,
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
|
||||
this.previewAlsRun(tenantId, importId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the imported file results before commiting the transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async preview(
|
||||
public async previewAlsRun(
|
||||
tenantId: number,
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
|
||||
@@ -2,18 +2,14 @@ import { Inject, Service } from 'typedi';
|
||||
import { chain } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ERRORS,
|
||||
getSheetColumns,
|
||||
getUnmappedSheetColumns,
|
||||
readImportFile,
|
||||
} from './_utils';
|
||||
import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
|
||||
import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
import { parseSheetData } from './sheet_utils';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcess {
|
||||
@@ -49,10 +45,10 @@ export class ImportFileProcess {
|
||||
if (!importFile.isMapped) {
|
||||
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
||||
}
|
||||
// Read the imported file.
|
||||
// Read the imported file and parse the given buffer to get columns
|
||||
// and sheet data in json format.
|
||||
const buffer = await readImportFile(importFile.filename);
|
||||
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
||||
const header = getSheetColumns(sheetData);
|
||||
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||
|
||||
const resource = importFile.resource;
|
||||
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
|
||||
@@ -87,7 +83,7 @@ export class ImportFileProcess {
|
||||
.flatten()
|
||||
.value();
|
||||
|
||||
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
||||
const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
|
||||
const totalCount = allData.length;
|
||||
|
||||
const createdCount = successedImport.length;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||
import { ImportAls } from './ImportALS';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcessCommit {
|
||||
@@ -14,16 +15,34 @@ export class ImportFileProcessCommit {
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private importAls: ImportAls;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Commits the imported file under ALS.
|
||||
* @param {number} tenantId
|
||||
* @param {string} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public commit(
|
||||
tenantId: number,
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
|
||||
this.commitAlsRun(tenantId, importId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the imported file.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async commit(
|
||||
public async commitAlsRun(
|
||||
tenantId: number,
|
||||
importId: string
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||
import { ImportFileUploadPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
import { parseSheetData } from './sheet_utils';
|
||||
|
||||
@Service()
|
||||
export class ImportFileUploadService {
|
||||
@@ -77,14 +78,12 @@ export class ImportFileUploadService {
|
||||
const buffer = await readImportFile(filename);
|
||||
|
||||
// Parse the buffer file to array data.
|
||||
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
||||
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||
|
||||
// Throws service error if the sheet data is empty.
|
||||
validateSheetEmpty(sheetData);
|
||||
|
||||
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||
|
||||
try {
|
||||
// Validates the params Yup schema.
|
||||
await this.importFileCommon.validateParamsSchema(resource, params);
|
||||
|
||||
56
packages/server/src/services/Import/sheet_utils.ts
Normal file
56
packages/server/src/services/Import/sheet_utils.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import XLSX from 'xlsx';
|
||||
import { first } from 'lodash';
|
||||
|
||||
/**
|
||||
* Parses the given sheet buffer to worksheet.
|
||||
* @param {Buffer} buffer
|
||||
* @returns {XLSX.WorkSheet}
|
||||
*/
|
||||
export function parseFirstSheet(buffer: Buffer): XLSX.WorkSheet {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
||||
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the given worksheet to columns.
|
||||
* @param {XLSX.WorkSheet} worksheet
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function extractSheetColumns(worksheet: XLSX.WorkSheet): Array<string> {
|
||||
// By default, sheet_to_json scans the first row and uses the values as headers.
|
||||
// With the header: 1 option, the function exports an array of arrays of values.
|
||||
const sheetCells = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||
const sheetCols = first(sheetCells) as Array<string>;
|
||||
|
||||
return sheetCols.filter((col) => col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given worksheet to json values. the keys are columns labels.
|
||||
* @param {XLSX.WorkSheet} worksheet
|
||||
* @returns {Array<Record<string, string>>}
|
||||
*/
|
||||
export function parseSheetToJson(
|
||||
worksheet: XLSX.WorkSheet
|
||||
): Array<Record<string, string>> {
|
||||
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given sheet buffer then retrieves the sheet data and columns.
|
||||
* @param {Buffer} buffer
|
||||
*/
|
||||
export function parseSheetData(
|
||||
buffer: Buffer
|
||||
): [Array<Record<string, string>>, string[]] {
|
||||
const worksheet = parseFirstSheet(buffer);
|
||||
|
||||
const columns = extractSheetColumns(worksheet);
|
||||
const data = parseSheetToJson(worksheet);
|
||||
|
||||
return [data, columns];
|
||||
}
|
||||
@@ -139,24 +139,25 @@ export default class InventoryService {
|
||||
) {
|
||||
const agenda = Container.get('agenda');
|
||||
|
||||
const commonJobsQuery = {
|
||||
name: 'compute-item-cost',
|
||||
lastRunAt: { $exists: false },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
};
|
||||
// Cancel any `compute-item-cost` in the queue has upper starting date
|
||||
// with the same given item.
|
||||
await agenda.cancel({
|
||||
name: 'compute-item-cost',
|
||||
nextRunAt: { $ne: null },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
'data.startingDate': { $gt: startingDate },
|
||||
...commonJobsQuery,
|
||||
'data.startingDate': { $lte: startingDate },
|
||||
});
|
||||
// Retrieve any `compute-item-cost` in the queue has lower starting date
|
||||
// with the same given item.
|
||||
const dependsJobs = await agenda.jobs({
|
||||
name: 'compute-item-cost',
|
||||
nextRunAt: { $ne: null },
|
||||
'data.tenantId': tenantId,
|
||||
'data.itemId': itemId,
|
||||
'data.startingDate': { $lte: startingDate },
|
||||
...commonJobsQuery,
|
||||
'data.startingDate': { $gte: startingDate },
|
||||
});
|
||||
// If the depends jobs cleared.
|
||||
if (dependsJobs.length === 0) {
|
||||
await agenda.schedule(
|
||||
config.scheduleComputeItemCost,
|
||||
@@ -172,6 +173,13 @@ export default class InventoryService {
|
||||
events.inventory.onComputeItemCostJobScheduled,
|
||||
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
|
||||
);
|
||||
} else {
|
||||
// Re-schedule the jobs that have higher date from current moment.
|
||||
await Promise.all(
|
||||
dependsJobs.map((job) =>
|
||||
job.schedule(config.scheduleComputeItemCost).save()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { IItemsFilter } from '@/interfaces';
|
||||
import { ItemsApplication } from './ItemsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class ItemsExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class ItemsExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
...query,
|
||||
pageSize: 12,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IItemsFilter;
|
||||
|
||||
return this.itemsApplication
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IManualJournalsFilter } from '@/interfaces';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { ManualJournalsApplication } from './ManualJournalsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
|
||||
@Service()
|
||||
export class ManualJournalsExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class ManualJournalsExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IManualJournalsFilter;
|
||||
|
||||
return this.manualJournalsApplication
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { BillPaymentsApplication } from './BillPaymentsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class BillPaymentExportable extends Exportable {
|
||||
@@ -14,11 +15,11 @@ export class BillPaymentExportable extends Exportable {
|
||||
*/
|
||||
public exportable(tenantId: number, query: any) {
|
||||
const parsedQuery = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...query,
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as any;
|
||||
|
||||
return this.billPaymentsApplication
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IBillsFilter } from '@/interfaces';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { BillsApplication } from './BillsApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class BillsExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class BillsExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IBillsFilter;
|
||||
|
||||
return this.billsApplication
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { ISalesInvoicesFilter } from '@/interfaces';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { SaleEstimatesApplication } from './SaleEstimatesApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class SaleEstimatesExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 12000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as ISalesInvoicesFilter;
|
||||
|
||||
return this.saleEstimatesApplication
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { ISalesInvoicesFilter } from '@/interfaces';
|
||||
import { SaleInvoiceApplication } from './SaleInvoicesApplication';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicesExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class SaleInvoicesExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 120000,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as ISalesInvoicesFilter;
|
||||
|
||||
return this.saleInvoicesApplication
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import { chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
@@ -34,17 +35,26 @@ export class SaleInvoicesCost {
|
||||
inventoryItemsIds: number[],
|
||||
startingDate: Date
|
||||
): Promise<void> {
|
||||
const asyncOpers: Promise<[]>[] = [];
|
||||
const mutex = new Mutex();
|
||||
|
||||
inventoryItemsIds.forEach((inventoryItemId: number) => {
|
||||
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
|
||||
tenantId,
|
||||
inventoryItemId,
|
||||
startingDate
|
||||
);
|
||||
asyncOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...asyncOpers]);
|
||||
const asyncOpers = inventoryItemsIds.map(
|
||||
async (inventoryItemId: number) => {
|
||||
// @todo refactor the lock acquire to be distrbuted using Redis
|
||||
// and run the cost schedule job after running invoice transaction.
|
||||
const release = await mutex.acquire();
|
||||
|
||||
try {
|
||||
await this.inventoryService.scheduleComputeItemCost(
|
||||
tenantId,
|
||||
inventoryItemId,
|
||||
startingDate
|
||||
);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
);
|
||||
await Promise.all(asyncOpers);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,17 +96,22 @@ export class SaleInvoicesCost {
|
||||
tenantId: number,
|
||||
inventoryTransactions: IInventoryTransaction[]
|
||||
) {
|
||||
const asyncOpers: Promise<[]>[] = [];
|
||||
const mutex = new Mutex();
|
||||
const reducedTransactions = this.getMaxDateInventoryTransactions(
|
||||
inventoryTransactions
|
||||
);
|
||||
reducedTransactions.forEach((transaction) => {
|
||||
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
|
||||
tenantId,
|
||||
transaction.itemId,
|
||||
transaction.date
|
||||
);
|
||||
asyncOpers.push(oper);
|
||||
const asyncOpers = reducedTransactions.map(async (transaction) => {
|
||||
const release = await mutex.acquire();
|
||||
|
||||
try {
|
||||
await this.inventoryService.scheduleComputeItemCost(
|
||||
tenantId,
|
||||
transaction.itemId,
|
||||
transaction.date
|
||||
);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
});
|
||||
await Promise.all([...asyncOpers]);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { PaymentReceivesApplication } from './PaymentReceivedApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class PaymentsReceivedExportable extends Exportable {
|
||||
@@ -21,6 +22,8 @@ export class PaymentsReceivedExportable extends Exportable {
|
||||
inactiveMode: false,
|
||||
...query,
|
||||
structure: IAccountsStructureType.Flat,
|
||||
page: 1,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as IPaymentsReceivedFilter;
|
||||
|
||||
return this.paymentReceivedApp
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { ISalesReceiptsFilter } from '@/interfaces';
|
||||
import { Exportable } from '@/services/Export/Exportable';
|
||||
import { SaleReceiptApplication } from './SaleReceiptApplication';
|
||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptsExportable extends Exportable {
|
||||
@@ -19,7 +20,7 @@ export class SaleReceiptsExportable extends Exportable {
|
||||
columnSortBy: 'created_at',
|
||||
...query,
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
} as ISalesReceiptsFilter;
|
||||
|
||||
return this.saleReceiptsApp
|
||||
|
||||
@@ -18,7 +18,10 @@ export class LemonResumeSubscription {
|
||||
* @param {string} subscriptionSlug - Subscription slug by default main subscription.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async resumeSubscription(tenantId: number, subscriptionSlug: string = 'main') {
|
||||
public async resumeSubscription(
|
||||
tenantId: number,
|
||||
subscriptionSlug: string = 'main'
|
||||
) {
|
||||
configureLemonSqueezy();
|
||||
|
||||
const subscription = await PlanSubscription.query().findOne({
|
||||
@@ -34,7 +37,7 @@ export class LemonResumeSubscription {
|
||||
cancelled: false,
|
||||
});
|
||||
if (returnedSub.error) {
|
||||
throw new ServiceError(ٌٌُERRORS.SOMETHING_WENT_WRONG_WITH_LS);
|
||||
throw new ServiceError(ERRORS.SOMETHING_WENT_WRONG_WITH_LS);
|
||||
}
|
||||
// Triggers `onSubscriptionResume` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@/interfaces';
|
||||
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
|
||||
import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost';
|
||||
import { ImportAls } from '@/services/Import/ImportALS';
|
||||
|
||||
@Service()
|
||||
export default class InventorySubscriber {
|
||||
@@ -25,6 +26,9 @@ export default class InventorySubscriber {
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
@Inject()
|
||||
private importAls: ImportAls;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
@@ -86,20 +90,17 @@ export default class InventorySubscriber {
|
||||
private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({
|
||||
tenantId,
|
||||
inventoryTransactions,
|
||||
trx
|
||||
trx,
|
||||
}: IInventoryTransactionsCreatedPayload) => {
|
||||
const inventoryItemsIds = map(inventoryTransactions, 'itemId');
|
||||
const inImportPreviewScope = this.importAls.isImportPreview;
|
||||
|
||||
runAfterTransaction(trx, async () => {
|
||||
try {
|
||||
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
|
||||
tenantId,
|
||||
inventoryTransactions
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
// Avoid running the cost items job if the async process is in import preview.
|
||||
if (inImportPreviewScope) return;
|
||||
|
||||
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
|
||||
tenantId,
|
||||
inventoryTransactions
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Router, Switch, Route } from 'react-router';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
@@ -11,26 +12,27 @@ import 'moment/locale/es-us';
|
||||
import AppIntlLoader from './AppIntlLoader';
|
||||
import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
|
||||
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
||||
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
||||
import { Authentication } from '@/containers/Authentication/Authentication';
|
||||
|
||||
import LazyLoader from '@/components/LazyLoader';
|
||||
import { SplashScreen, DashboardThemeProvider } from '../components';
|
||||
import { queryConfig } from '../hooks/query/base';
|
||||
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
|
||||
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
|
||||
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
|
||||
import { EnsureOneClickDemoAccountEnabled } from '@/containers/OneClickDemo/EnsureOneClickDemoAccountEnabled';
|
||||
|
||||
const EmailConfirmation = LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||
});
|
||||
const RegisterVerify = LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/RegisterVerify'),
|
||||
});
|
||||
const OneClickDemoPage = LazyLoader({
|
||||
loader: () => import('@/containers/OneClickDemo/OneClickDemoPage'),
|
||||
});
|
||||
const DashboardPrivatePages = lazy(
|
||||
() => import('@/components/Dashboard/PrivatePages'),
|
||||
);
|
||||
const AuthenticationPage = lazy(
|
||||
() => import('@/containers/Authentication/AuthenticationPage'),
|
||||
);
|
||||
const EmailConfirmation = lazy(
|
||||
() => import('@/containers/Authentication/EmailConfirmation'),
|
||||
);
|
||||
const RegisterVerify = lazy(
|
||||
() => import('@/containers/Authentication/RegisterVerify'),
|
||||
);
|
||||
const OneClickDemoPage = lazy(
|
||||
() => import('@/containers/OneClickDemo/OneClickDemoPage'),
|
||||
);
|
||||
|
||||
/**
|
||||
* App inner.
|
||||
*/
|
||||
@@ -38,42 +40,27 @@ function AppInsider({ history }) {
|
||||
return (
|
||||
<div className="App">
|
||||
<DashboardThemeProvider>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/one_click_demo'}>
|
||||
<EnsureOneClickDemoAccountEnabled>
|
||||
<EnsureAuthNotAuthenticated>
|
||||
<OneClickDemoPage />
|
||||
</EnsureAuthNotAuthenticated>
|
||||
</EnsureOneClickDemoAccountEnabled>
|
||||
</Route>
|
||||
<Route path={'/auth/register/verify'}>
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailNotVerified>
|
||||
<RegisterVerify />
|
||||
</EnsureUserEmailNotVerified>
|
||||
</EnsureAuthenticated>
|
||||
</Route>
|
||||
<Suspense fallback={'Loading...'}>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/one_click_demo'} children={<OneClickDemoPage />} />
|
||||
<Route path={'/auth/register/verify'}>
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailNotVerified>
|
||||
<RegisterVerify />
|
||||
</EnsureUserEmailNotVerified>
|
||||
</EnsureAuthenticated>
|
||||
</Route>
|
||||
|
||||
<Route path={'/auth/email_confirmation'}>
|
||||
<EmailConfirmation />
|
||||
</Route>
|
||||
|
||||
<Route path={'/auth'}>
|
||||
<EnsureAuthNotAuthenticated>
|
||||
<Authentication />
|
||||
</EnsureAuthNotAuthenticated>
|
||||
</Route>
|
||||
|
||||
<Route path={'/'}>
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailVerified>
|
||||
<DashboardPrivatePages />
|
||||
</EnsureUserEmailVerified>
|
||||
</EnsureAuthenticated>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
<Route
|
||||
path={'/auth/email_confirmation'}
|
||||
children={<EmailConfirmation />}
|
||||
/>
|
||||
<Route path={'/auth'} children={<AuthenticationPage />} />
|
||||
<Route path={'/'} children={<DashboardPrivatePages />} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</Suspense>
|
||||
|
||||
<GlobalErrors />
|
||||
</DashboardThemeProvider>
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { lazy } from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
|
||||
import Dashboard from '@/components/Dashboard/Dashboard';
|
||||
import SetupWizardPage from '@/containers/Setup/WizardSetupPage';
|
||||
|
||||
import EnsureOrganizationIsReady from '../Guards/EnsureOrganizationIsReady';
|
||||
import EnsureOrganizationIsNotReady from '../Guards/EnsureOrganizationIsNotReady';
|
||||
import { PrivatePagesProvider } from './PrivatePagesProvider';
|
||||
import EnsureOrganizationIsReady from '../Guards/EnsureOrganizationIsReady';
|
||||
import { EnsureAuthenticated } from '../Guards/EnsureAuthenticated';
|
||||
import { EnsureUserEmailVerified } from '../Guards/EnsureUserEmailVerified';
|
||||
|
||||
import '@/style/pages/Dashboard/Dashboard.scss';
|
||||
|
||||
const SetupWizardPage = lazy(
|
||||
() => import('@/containers/Setup/WizardSetupPage'),
|
||||
);
|
||||
/**
|
||||
* Dashboard inner private pages.
|
||||
*/
|
||||
export default function DashboardPrivatePages() {
|
||||
return (
|
||||
<PrivatePagesProvider>
|
||||
<Switch>
|
||||
<Route path={'/setup'}>
|
||||
<EnsureOrganizationIsNotReady>
|
||||
<SetupWizardPage />
|
||||
</EnsureOrganizationIsNotReady>
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<EnsureOrganizationIsReady>
|
||||
<Dashboard />
|
||||
</EnsureOrganizationIsReady>
|
||||
</Route>
|
||||
</Switch>
|
||||
</PrivatePagesProvider>
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailVerified>
|
||||
<PrivatePagesProvider>
|
||||
<Switch>
|
||||
<Route path={'/setup'} children={<SetupWizardPage />} />
|
||||
<Route path="/">
|
||||
<EnsureOrganizationIsReady>
|
||||
<Dashboard />
|
||||
</EnsureOrganizationIsReady>
|
||||
</Route>
|
||||
</Switch>
|
||||
</PrivatePagesProvider>
|
||||
</EnsureUserEmailVerified>
|
||||
</EnsureAuthenticated>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useApplicationBoot } from '@/components';
|
||||
import { useAuthMetadata } from '@/hooks/query/authentication';
|
||||
|
||||
/**
|
||||
* Private pages provider.
|
||||
@@ -9,7 +10,10 @@ export function PrivatePagesProvider({
|
||||
// #ownProps
|
||||
children,
|
||||
}) {
|
||||
const { isLoading } = useApplicationBoot();
|
||||
const { isLoading: isAppBootLoading } = useApplicationBoot();
|
||||
const { isLoading: isAuthMetaLoading } = useAuthMetadata();
|
||||
|
||||
const isLoading = isAppBootLoading || isAuthMetaLoading;
|
||||
|
||||
return <React.Fragment>{!isLoading ? children : null}</React.Fragment>;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import * as Loadable from 'react-loadable';
|
||||
|
||||
const Loader = (config) =>
|
||||
Loadable({
|
||||
loading: (props) => {
|
||||
if (props.error) {
|
||||
/* tslint:disable */
|
||||
console.error(`======= DefaultLoader Error =======`);
|
||||
console.error(props.error);
|
||||
console.error(`======= DefaultLoader Error =======`);
|
||||
/* tslint:enable */
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
delay: 250,
|
||||
...config
|
||||
});
|
||||
|
||||
export default Loader;
|
||||
@@ -1,21 +1,33 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import preferencesRoutes from '@/routes/preferences';
|
||||
import { getPreferenceRoutes } from '@/routes/preferences';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
import { Box } from '../Layout';
|
||||
|
||||
export default function DashboardContentRoute() {
|
||||
const preferencesRoutes = getPreferenceRoutes();
|
||||
|
||||
return (
|
||||
<Route pathname="/preferences">
|
||||
<Switch>
|
||||
{preferencesRoutes.map((route, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
path={`${route.path}`}
|
||||
exact={route.exact}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box style={{ padding: 20 }}>
|
||||
<Spinner size={20} />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
{preferencesRoutes.map((route, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
path={`${route.path}`}
|
||||
exact={route.exact}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export const Config = {
|
||||
oneClickDemo: {
|
||||
enable: process.env.REACT_APP_ONE_CLICK_DEMO_ENABLE === 'true',
|
||||
demoUrl: process.env.REACT_APP_DEMO_ACCOUNT_URL || '',
|
||||
},
|
||||
};
|
||||
@@ -54,9 +54,9 @@ export default [
|
||||
disabled: false,
|
||||
href: '/preferences/items',
|
||||
},
|
||||
{
|
||||
text: <T id={'sms_integration.label'} />,
|
||||
disabled: false,
|
||||
href: '/preferences/sms-message',
|
||||
},
|
||||
// {
|
||||
// text: <T id={'sms_integration.label'} />,
|
||||
// disabled: false,
|
||||
// href: '/preferences/sms-message',
|
||||
// },
|
||||
];
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { Route, Switch, useLocation } from 'react-router-dom';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import styled from 'styled-components';
|
||||
import { Suspense } from 'react';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
import authenticationRoutes from '@/routes/authentication';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
import { Box, Icon, FormattedMessage as T } from '@/components';
|
||||
import { AuthMetaBootProvider } from './AuthMetaBoot';
|
||||
|
||||
import '@/style/pages/Authentication/Auth.scss';
|
||||
@@ -20,7 +22,15 @@ export function Authentication() {
|
||||
</AuthLogo>
|
||||
|
||||
<AuthMetaBootProvider>
|
||||
<AuthenticationRoutes />
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box style={{ marginTop: '5rem' }}>
|
||||
<Spinner size={30} />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<AuthenticationRoutes />
|
||||
</Suspense>
|
||||
</AuthMetaBootProvider>
|
||||
</AuthInsider>
|
||||
</AuthPage>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { EnsureAuthNotAuthenticated } from "@/components/Guards/EnsureAuthNotAuthenticated";
|
||||
import { Authentication } from "./Authentication";
|
||||
|
||||
export default function AuthenticationPage() {
|
||||
return (
|
||||
<EnsureAuthNotAuthenticated>
|
||||
<Authentication />
|
||||
</EnsureAuthNotAuthenticated>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Config } from '@/config';
|
||||
import { useOneClickDemoBoot } from './OneClickDemoBoot';
|
||||
|
||||
interface EnsureOneClickDemoAccountEnabledProps {
|
||||
children: React.ReactNode;
|
||||
@@ -11,9 +11,10 @@ export const EnsureOneClickDemoAccountEnabled = ({
|
||||
children,
|
||||
redirectTo = '/',
|
||||
}: EnsureOneClickDemoAccountEnabledProps) => {
|
||||
const enabeld = Config.oneClickDemo.enable || false;
|
||||
const { authMeta } = useOneClickDemoBoot();
|
||||
const enabled = authMeta?.meta?.one_click_demo?.enable || false;
|
||||
|
||||
if (!enabeld) {
|
||||
if (!enabled) {
|
||||
return <Redirect to={{ pathname: redirectTo }} />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { useAuthMetadata } from '@/hooks/query/authentication';
|
||||
|
||||
interface OneClickDemoContextType {
|
||||
authMeta: any;
|
||||
}
|
||||
|
||||
const OneClickDemoContext = createContext<OneClickDemoContextType>(
|
||||
{} as OneClickDemoContextType,
|
||||
);
|
||||
|
||||
export const useOneClickDemoBoot = () => {
|
||||
const context = useContext(OneClickDemoContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useOneClickDemo must be used within a OneClickDemoProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface OneClickDemoBootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const OneClickDemoBoot: React.FC<OneClickDemoBootProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { isLoading: isAuthMetaLoading, data: authMeta } = useAuthMetadata();
|
||||
|
||||
const value = {
|
||||
isAuthMetaLoading,
|
||||
authMeta,
|
||||
};
|
||||
|
||||
if (isAuthMetaLoading) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<OneClickDemoContext.Provider value={value}>
|
||||
{children}
|
||||
</OneClickDemoContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +1,16 @@
|
||||
// @ts-nocheck
|
||||
import { Button, Intent, ProgressBar, Text } from '@blueprintjs/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
useCreateOneClickDemo,
|
||||
useOneClickDemoSignin,
|
||||
} from '@/hooks/query/oneclick-demo';
|
||||
import { Box, Icon, Stack } from '@/components';
|
||||
import { useJob } from '@/hooks/query';
|
||||
import style from './OneClickDemoPage.module.scss';
|
||||
import { EnsureAuthNotAuthenticated } from '@/components/Guards/EnsureAuthNotAuthenticated';
|
||||
import { EnsureOneClickDemoAccountEnabled } from './EnsureOneClickDemoAccountEnabled';
|
||||
import { OneClickDemoBoot } from './OneClickDemoBoot';
|
||||
import { OneClickDemoPageContent } from './OneClickDemoPageContent';
|
||||
|
||||
export default function OneClickDemoPage() {
|
||||
const {
|
||||
mutateAsync: createOneClickDemo,
|
||||
isLoading: isCreateOneClickLoading,
|
||||
} = useCreateOneClickDemo();
|
||||
const {
|
||||
mutateAsync: oneClickDemoSignIn,
|
||||
isLoading: isOneclickDemoSigningIn,
|
||||
} = useOneClickDemoSignin();
|
||||
|
||||
// Job states.
|
||||
const [demoId, setDemoId] = useState<string>('');
|
||||
const [buildJobId, setBuildJobId] = useState<string>('');
|
||||
const [isJobDone, setIsJobDone] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
data: { running, completed },
|
||||
} = useJob(buildJobId, {
|
||||
refetchInterval: 2000,
|
||||
enabled: !isJobDone && !!buildJobId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (completed) {
|
||||
setIsJobDone(true);
|
||||
}
|
||||
}, [completed, setIsJobDone]);
|
||||
|
||||
// One the job done request sign-in using the demo id.
|
||||
useEffect(() => {
|
||||
if (isJobDone) {
|
||||
oneClickDemoSignIn({ demoId }).then((res) => {
|
||||
debugger;
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isJobDone]);
|
||||
|
||||
const handleCreateAccountBtnClick = () => {
|
||||
createOneClickDemo({})
|
||||
.then(({ data: { data } }) => {
|
||||
setBuildJobId(data?.build_job?.job_id);
|
||||
setDemoId(data?.demo_id);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
const isLoading = running || isOneclickDemoSigningIn;
|
||||
|
||||
return (
|
||||
<Box className={style.root}>
|
||||
<Box className={style.inner}>
|
||||
<Stack align={'center'} spacing={40}>
|
||||
<Icon icon="bigcapital" height={37} width={228} />
|
||||
|
||||
{isLoading && (
|
||||
<Stack align={'center'} spacing={15}>
|
||||
<ProgressBar stripes value={null} className={style.progressBar} />
|
||||
{isOneclickDemoSigningIn && (
|
||||
<Text className={style.waitingText}>
|
||||
It's signin-in to your demo account, Just a second!
|
||||
</Text>
|
||||
)}
|
||||
{running && (
|
||||
<Text className={style.waitingText}>
|
||||
We're preparing temporary environment for trial, It typically
|
||||
take few seconds. Do not close or refresh the page.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{!isLoading && (
|
||||
<Button
|
||||
className={style.oneClickBtn}
|
||||
intent={Intent.NONE}
|
||||
onClick={handleCreateAccountBtnClick}
|
||||
loading={isCreateOneClickLoading}
|
||||
>
|
||||
Create Demo Account
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<EnsureAuthNotAuthenticated>
|
||||
<OneClickDemoBoot>
|
||||
<EnsureOneClickDemoAccountEnabled>
|
||||
<OneClickDemoPageContent />
|
||||
</EnsureOneClickDemoAccountEnabled>
|
||||
</OneClickDemoBoot>
|
||||
</EnsureAuthNotAuthenticated>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// @ts-nocheck
|
||||
import { Button, Intent, ProgressBar, Text } from '@blueprintjs/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
useCreateOneClickDemo,
|
||||
useOneClickDemoSignin,
|
||||
} from '@/hooks/query/oneclick-demo';
|
||||
import { Box, Icon, Stack } from '@/components';
|
||||
import { useJob } from '@/hooks/query';
|
||||
import style from './OneClickDemoPage.module.scss';
|
||||
|
||||
export function OneClickDemoPageContent() {
|
||||
const {
|
||||
mutateAsync: createOneClickDemo,
|
||||
isLoading: isCreateOneClickLoading,
|
||||
} = useCreateOneClickDemo();
|
||||
const {
|
||||
mutateAsync: oneClickDemoSignIn,
|
||||
isLoading: isOneclickDemoSigningIn,
|
||||
} = useOneClickDemoSignin();
|
||||
|
||||
// Job states.
|
||||
const [demoId, setDemoId] = useState<string>('');
|
||||
const [buildJobId, setBuildJobId] = useState<string>('');
|
||||
const [isJobDone, setIsJobDone] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
data: { running, completed },
|
||||
} = useJob(buildJobId, {
|
||||
refetchInterval: 2000,
|
||||
enabled: !isJobDone && !!buildJobId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (completed) {
|
||||
setIsJobDone(true);
|
||||
}
|
||||
}, [completed, setIsJobDone]);
|
||||
|
||||
// One the job done request sign-in using the demo id.
|
||||
useEffect(() => {
|
||||
if (isJobDone) {
|
||||
oneClickDemoSignIn({ demoId }).then((res) => {
|
||||
debugger;
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isJobDone]);
|
||||
|
||||
const handleCreateAccountBtnClick = () => {
|
||||
createOneClickDemo({})
|
||||
.then(({ data: { data } }) => {
|
||||
setBuildJobId(data?.build_job?.job_id);
|
||||
setDemoId(data?.demo_id);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
const isLoading = running || isOneclickDemoSigningIn;
|
||||
|
||||
return (
|
||||
<Box className={style.root}>
|
||||
<Box className={style.inner}>
|
||||
<Stack align={'center'} spacing={40}>
|
||||
<Icon icon="bigcapital" height={37} width={228} />
|
||||
|
||||
{isLoading && (
|
||||
<Stack align={'center'} spacing={15}>
|
||||
<ProgressBar stripes value={null} className={style.progressBar} />
|
||||
{isOneclickDemoSigningIn && (
|
||||
<Text className={style.waitingText}>
|
||||
It's signin-in to your demo account, Just a second!
|
||||
</Text>
|
||||
)}
|
||||
{running && (
|
||||
<Text className={style.waitingText}>
|
||||
We're preparing the temporary environment for trial. It
|
||||
typically takes a few seconds. Do not close or refresh the
|
||||
page.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{!isLoading && (
|
||||
<Button
|
||||
className={style.oneClickBtn}
|
||||
intent={Intent.NONE}
|
||||
onClick={handleCreateAccountBtnClick}
|
||||
loading={isCreateOneClickLoading}
|
||||
>
|
||||
Create Demo Account
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
border: 1px solid rgb(255, 255, 255, 0.3);
|
||||
padding: 10px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border: 1px solid rgb(255, 255, 255, 0.2);
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-radius: 5px;
|
||||
@@ -12,11 +12,11 @@
|
||||
transition: border 0.15s ease-in-out, color 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgb(255, 255, 255, 0.6);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgb(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
.demoButtonLabel{
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Icon, For, FormattedMessage as T, Stack } from '@/components';
|
||||
import { getFooterLinks } from '@/constants/footerLinks';
|
||||
import { useAuthActions } from '@/hooks/state';
|
||||
import style from './SetupLeftSection.module.scss';
|
||||
import { Config } from '@/config';
|
||||
import { useAuthMetadata } from '@/hooks/query';
|
||||
|
||||
/**
|
||||
* Footer item link.
|
||||
@@ -28,13 +28,16 @@ function SetupLeftSectionFooter() {
|
||||
// Retrieve the footer links.
|
||||
const footerLinks = getFooterLinks();
|
||||
|
||||
const { data: authMeta } = useAuthMetadata();
|
||||
const demoUrl = authMeta?.meta?.one_click_demo?.demo_url;
|
||||
|
||||
const handleDemoBtnClick = () => {
|
||||
window.open(Config.oneClickDemo.demoUrl);
|
||||
window.open(demoUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'content__footer'}>
|
||||
{Config.oneClickDemo.demoUrl && (
|
||||
{demoUrl && (
|
||||
<Stack spacing={16}>
|
||||
<Text className={style.demoButtonLabel}>Not Now?</Text>
|
||||
<button className={style.demoButton} onClick={handleDemoBtnClick}>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
margin-bottom: 1.15rem;
|
||||
}
|
||||
|
||||
.iconText {
|
||||
display: inline-flex;
|
||||
font-size: 14px;
|
||||
margin-right: 16px;
|
||||
color: #00824d;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0; /* Remove the margin on the last item */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import styles from './SubscriptionPlansOfferChecks.module.scss';
|
||||
|
||||
export function SubscriptionPlansOfferChecks() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<span className={styles.iconText}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="18px"
|
||||
width="18px"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="rgb(0, 130, 77)"
|
||||
className={styles.icon}
|
||||
>
|
||||
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
|
||||
</svg>
|
||||
14-day free trial
|
||||
</span>
|
||||
|
||||
<span className={styles.iconText}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="18px"
|
||||
width="18px"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="rgb(0, 130, 77)"
|
||||
className={styles.icon}
|
||||
>
|
||||
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
|
||||
</svg>
|
||||
24/7 online support
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ function SubscriptionPlansPeriodSwitcherRoot({
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}>
|
||||
<Group position={'center'} spacing={10} style={{ marginBottom: '1.6rem' }}>
|
||||
<Text>Pay Monthly</Text>
|
||||
<Switch
|
||||
large
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Callout } from '@blueprintjs/core';
|
||||
import { SubscriptionPlans } from './SubscriptionPlans';
|
||||
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
|
||||
import { SubscriptionPlansOfferChecks } from './SubscriptionPlansOfferChecks';
|
||||
|
||||
/**
|
||||
* Billing plans.
|
||||
@@ -14,6 +15,7 @@ export function SubscriptionPlansSection() {
|
||||
include applicable taxes.
|
||||
</Callout>
|
||||
|
||||
<SubscriptionPlansOfferChecks />
|
||||
<SubscriptionPlansPeriodSwitcher />
|
||||
<SubscriptionPlans />
|
||||
</section>
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
import React from 'react';
|
||||
import SetupRightSection from './SetupRightSection';
|
||||
import SetupLeftSection from './SetupLeftSection';
|
||||
import EnsureOrganizationIsNotReady from '@/components/Guards/EnsureOrganizationIsNotReady';
|
||||
|
||||
import '@/style/pages/Setup/SetupPage.scss';
|
||||
|
||||
export default function WizardSetupPage() {
|
||||
return (
|
||||
<div class="setup-page">
|
||||
<SetupLeftSection />
|
||||
<SetupRightSection />
|
||||
</div>
|
||||
<EnsureOrganizationIsNotReady>
|
||||
<div class="setup-page">
|
||||
<SetupLeftSection />
|
||||
<SetupRightSection />
|
||||
</div>
|
||||
</EnsureOrganizationIsNotReady>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export const useAuthResetPassword = (props) => {
|
||||
/**
|
||||
* Fetches the authentication page metadata.
|
||||
*/
|
||||
export const useAuthMetadata = (props) => {
|
||||
export const useAuthMetadata = (props = {}) => {
|
||||
return useRequestQuery(
|
||||
[t.AUTH_METADATA_PAGE],
|
||||
{
|
||||
|
||||
@@ -1,43 +1,35 @@
|
||||
// @ts-nocheck
|
||||
import LazyLoader from '@/components/LazyLoader';
|
||||
import { lazy } from 'react';
|
||||
|
||||
const BASE_URL = '/auth';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: `${BASE_URL}/login`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/Login'),
|
||||
}),
|
||||
component: lazy(() => import('@/containers/Authentication/Login')),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/send_reset_password`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/SendResetPassword'),
|
||||
}),
|
||||
component: lazy(
|
||||
() => import('@/containers/Authentication/SendResetPassword'),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/reset_password/:token`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/ResetPassword'),
|
||||
}),
|
||||
component: lazy(() => import('@/containers/Authentication/ResetPassword')),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/invite/:token/accept`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/InviteAccept'),
|
||||
}),
|
||||
component: lazy(() => import('@/containers/Authentication/InviteAccept')),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/register/email_confirmation`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||
}),
|
||||
component: lazy(
|
||||
() => import('@/containers/Authentication/EmailConfirmation'),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/register`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/Register'),
|
||||
}),
|
||||
component: lazy(() => import('@/containers/Authentication/Register')),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,103 +1,111 @@
|
||||
// @ts-nocheck
|
||||
import { lazy } from 'react';
|
||||
import General from '@/containers/Preferences/General/General';
|
||||
import Users from '../containers/Preferences/Users/Users';
|
||||
import Roles from '../containers/Preferences/Users/Roles/RolesForm/RolesFormPage';
|
||||
import Accountant from '@/containers/Preferences/Accountant/Accountant';
|
||||
import Currencies from '@/containers/Preferences/Currencies/Currencies';
|
||||
import Item from '@/containers/Preferences/Item';
|
||||
import SMSIntegration from '../containers/Preferences/SMSIntegration';
|
||||
import DefaultRoute from '../containers/Preferences/DefaultRoute';
|
||||
import Warehouses from '../containers/Preferences/Warehouses';
|
||||
import Branches from '../containers/Preferences/Branches';
|
||||
import Invoices from '../containers/Preferences/Invoices/PreferencesInvoices';
|
||||
import { PreferencesCreditNotes } from '../containers/Preferences/CreditNotes/PreferencesCreditNotes';
|
||||
import { PreferencesEstimates } from '@/containers/Preferences/Estimates/PreferencesEstimates';
|
||||
import { PreferencesReceipts } from '@/containers/Preferences/Receipts/PreferencesReceipts';
|
||||
|
||||
import BillingPage from '@/containers/Subscriptions/BillingPage';
|
||||
|
||||
const BASE_URL = '/preferences';
|
||||
|
||||
export default [
|
||||
export const getPreferenceRoutes = () => [
|
||||
{
|
||||
path: `${BASE_URL}/general`,
|
||||
component: General,
|
||||
component: lazy(() => import('@/containers/Preferences/General/General')),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/users`,
|
||||
component: Users,
|
||||
component: lazy(() => import('../containers/Preferences/Users/Users')),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/invoices`,
|
||||
component: Invoices,
|
||||
component: lazy(
|
||||
() => import('../containers/Preferences/Invoices/PreferencesInvoices'),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/credit-notes`,
|
||||
component: PreferencesCreditNotes,
|
||||
component: lazy(() =>
|
||||
import(
|
||||
'../containers/Preferences/CreditNotes/PreferencesCreditNotes'
|
||||
).then((module) => ({ default: module.PreferencesCreditNotes })),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/estimates`,
|
||||
component: PreferencesEstimates,
|
||||
component: lazy(() =>
|
||||
import('@/containers/Preferences/Estimates/PreferencesEstimates').then(
|
||||
(module) => ({ default: module.PreferencesEstimates }),
|
||||
),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/receipts`,
|
||||
component: PreferencesReceipts,
|
||||
component: lazy(() =>
|
||||
import('@/containers/Preferences/Receipts/PreferencesReceipts').then(
|
||||
(module) => ({ default: module.PreferencesReceipts }),
|
||||
),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/roles`,
|
||||
component: Roles,
|
||||
component: lazy(
|
||||
() =>
|
||||
import('../containers/Preferences/Users/Roles/RolesForm/RolesFormPage'),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/roles/:id`,
|
||||
component: Roles,
|
||||
component: lazy(
|
||||
() =>
|
||||
import('../containers/Preferences/Users/Roles/RolesForm/RolesFormPage'),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/currencies`,
|
||||
component: Currencies,
|
||||
component: lazy(
|
||||
() => import('@/containers/Preferences/Currencies/Currencies'),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/warehouses`,
|
||||
component: Warehouses,
|
||||
component: lazy(() => import('../containers/Preferences/Warehouses')),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/branches`,
|
||||
component: Branches,
|
||||
component: lazy(() => import('../containers/Preferences/Branches')),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/accountant`,
|
||||
component: Accountant,
|
||||
component: lazy(
|
||||
() => import('@/containers/Preferences/Accountant/Accountant'),
|
||||
),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/items`,
|
||||
component: Item,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/sms-message`,
|
||||
component: SMSIntegration,
|
||||
component: lazy(() => import('@/containers/Preferences/Item')),
|
||||
exact: true,
|
||||
},
|
||||
// {
|
||||
// path: `${BASE_URL}/sms-message`,
|
||||
// component: SMSIntegration,
|
||||
// exact: true,
|
||||
// },
|
||||
{
|
||||
path: `${BASE_URL}/billing`,
|
||||
component: BillingPage,
|
||||
component: lazy(() => import('@/containers/Subscriptions/BillingPage')),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/`,
|
||||
component: DefaultRoute,
|
||||
component: lazy(() => import('../containers/Preferences/DefaultRoute')),
|
||||
exact: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
padding-bottom: 40px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
&__left-section {
|
||||
@@ -72,10 +72,10 @@
|
||||
|
||||
&__text {
|
||||
font-size: 18px;
|
||||
opacity: 0.75;
|
||||
opacity: 0.55;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 200;
|
||||
}
|
||||
}
|
||||
|
||||
&__organization {
|
||||
font-size: 16px;
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -86,6 +86,9 @@ importers:
|
||||
async:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.5
|
||||
async-mutex:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
axios:
|
||||
specifier: ^1.6.0
|
||||
version: 1.7.2
|
||||
@@ -8009,6 +8012,12 @@ packages:
|
||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||
dev: false
|
||||
|
||||
/async-mutex@0.5.0:
|
||||
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/async-settle@1.0.0:
|
||||
resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
Reference in New Issue
Block a user