Compare commits

...

46 Commits

Author SHA1 Message Date
a.bouhuolia
e1ea5c402c feat: add env variable to customize the proxy public ports 2023-05-31 20:29:37 +02:00
a.bouhuolia
34b2c2c8b4 fix: copy package-lock.json inside the container 2023-05-31 13:07:30 +02:00
a.bouhuolia
5d96fe6aa0 chore(webapp): remove Sentry from webapp 2023-05-31 09:59:35 +02:00
Ameir Abdeldayem
d2b5084b42 chore: fix typo in README file (#124) 2023-05-30 10:46:51 +02:00
a.bouhuolia
81fb0734d5 update CHANGELOG 2023-05-28 15:04:31 +02:00
a.bouhuolia
3639ce44e5 chore: bump CHANGELOG v0.9.1 2023-05-28 15:02:14 +02:00
Ahmed Bouhuolia
a7c00d60d5 Merge pull request #121 from bigcapitalhq/BIG-429-clean-up-the-auto-increment-of-transactions
fix: the auto-increment of transactions.
2023-05-28 14:50:43 +02:00
a.bouhuolia
932750b62d fix(webapp): fix credit note and receipt auto-increment 2023-05-28 14:45:34 +02:00
a.bouhuolia
c90ffed67f fix(webapp): payment receive auto-increment 2023-05-26 00:02:47 +02:00
a.bouhuolia
e92c4486aa fix(webapp): auto-increment estimate transactions 2023-05-25 22:04:21 +02:00
a.bouhuolia
aaceea5338 fix(webapp): warehouse and branch reset on invoice form 2023-05-24 23:52:05 +02:00
a.bouhuolia
4d54d180bc fix(webapp): invoice transactions increment 2023-05-24 23:28:09 +02:00
Ahmed Bouhuolia
8fdd98e34d Merge pull request #122 from bigcapitalhq/BIG-434-delete-invoice-transaction-issue
fix(server): delete invoice transaction
2023-05-23 15:12:43 +02:00
a.bouhuolia
d53c5ee5e6 fix(server): delete invoice transaction 2023-05-23 15:11:56 +02:00
a.bouhuolia
4082e4e2b8 fix: auto-increment transaction field 2023-05-23 14:39:57 +02:00
a.bouhuolia
0c689459cb fix: auto-increment cashflow transactions 2023-05-23 13:56:35 +02:00
a.bouhuolia
40ef02f215 fix: auto-increment settings 2023-05-22 21:57:43 +02:00
a.bouhuolia
d369f0bb17 fix: the auto-increment of transactions. 2023-05-19 00:29:35 +02:00
Ahmed Bouhuolia
425d0293cc Merge pull request #120 from bigcapitalhq/BIG-433-fix-base-currency-should-be-enabled-with-account-model
BIG-433-fix-base-currency-should-be-enabled-with-account-model
2023-05-12 15:58:48 +02:00
a.bouhuolia
b621650975 fix(server): base currency should be enabled with account model. 2023-05-12 15:58:01 +02:00
a.bouhuolia
40948160fe fix(webapp): localization 2023-05-12 12:46:59 +02:00
Ahmed Bouhuolia
aa6b9dd295 Merge pull request #118 from bigcapitalhq/BIG-428-clean-up-the-preferences-pages
fix(webapp): general, accoutant and items preferences
2023-05-12 12:36:19 +02:00
a.bouhuolia
05c2232b97 chore(webapp): refactor the setup organization form to use Formik binded component 2023-05-12 12:31:55 +02:00
Ahmed Bouhuolia
8f6325d529 Merge pull request #119 from bigcapitalhq/fix-delete-journals-manual-journal
fix(server): deleting ledger entries of manual journal
2023-05-12 00:14:36 +02:00
a.bouhuolia
0aa681043d fix(server): deleting ledger entries of manual journal 2023-05-11 22:46:34 +02:00
a.bouhuolia
40bddfdfeb fix(webapp): accrual typo 2023-05-11 21:07:49 +02:00
a.bouhuolia
d6e2f01d70 fix(server): accrual typo 2023-05-11 21:07:01 +02:00
a.bouhuolia
2344d3d34d fix(webapp): general, accoutant and items preferences 2023-05-11 01:47:09 +02:00
a.bouhuolia
883c5dcb41 Merge branch 'signup-restrictions' into develop 2023-05-08 00:36:50 +02:00
a.bouhuolia
be10b8934d fix(webapp): change the error code handler 2023-05-08 00:35:44 +02:00
a.bouhuolia
ce38c71fa7 fix(server): should allowed email addresses and domain be irrespective. 2023-05-08 00:35:28 +02:00
Ahmed Bouhuolia
1162fbc7c3 Merge pull request #117 from bigcapitalhq/signup-restrictions
Sign-up restrictions for self-hosted
2023-05-08 00:18:56 +02:00
a.bouhuolia
18b9e25f2b chore: update .env.example 2023-05-07 23:59:41 +02:00
a.bouhuolia
dd26bdc482 feat(webapp): sign-up restrictions 2023-05-07 23:54:42 +02:00
a.bouhuolia
ad3c9ebfe9 feat(server): sign-up restrictions for self-hosted 2023-05-07 17:22:18 +02:00
a.bouhuolia
36611652da fix(webapp): resource meta of vendors list 2023-05-05 15:41:32 +02:00
a.bouhuolia
06c7ee71b4 fix(webapp): display transactions count in cashflow account 2023-05-05 13:54:45 +02:00
Ahmed Bouhuolia
54d3188666 Merge pull request #116 from bigcapitalhq/BIG-427-fix-sending-invite-email
fix(server): sending invite email
2023-05-05 00:30:24 +02:00
Ahmed Bouhuolia
1249415054 Merge pull request #115 from bigcapitalhq/BIG-409-some-flag-icons-are-missing
fix(webapp): some flag icons are missing
2023-05-04 21:32:10 +02:00
Ahmed Bouhuolia
6c96c371c5 Merge pull request #114 from bigcapitalhq/BIG-279-select-specific-accounts-in-general-ledger-does-not-working
`BIG-279` Select specific accounts in general ledger does not working.
2023-05-04 14:29:35 +02:00
a.bouhuolia
6c61a69f10 feat(webapp): handle create item on Accounts select components 2023-05-04 14:24:45 +02:00
a.bouhuolia
981b65349d feat(webapp): allow to create a new account item in accounts list component. 2023-05-03 22:41:54 +02:00
a.bouhuolia
a7d29a31c8 refactor(webapp): all services with new AccountSelect and AccountMultiSelect components. 2023-05-01 00:13:23 +02:00
a.bouhuolia
c1d92b74f0 chore(Select):style the Select button. 2023-04-30 21:13:33 +02:00
a.bouhuolia
6f0f47f38a refactor(webapp): Accounts Select and MultiSelect components 2023-04-30 17:33:15 +02:00
a.bouhuolia
83510cfa70 feat(server): add structure query flat or tree to accounts chart endpoint 2023-04-30 17:24:49 +02:00
181 changed files with 3141 additions and 3056 deletions

View File

@@ -29,6 +29,15 @@ JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
BASE_URL=https://bigcapital.ly BASE_URL=https://bigcapital.ly
CONTACT_US_MAIL=support@bigcapital.ly CONTACT_US_MAIL=support@bigcapital.ly
# App proxy
PUBLIC_PROXY_PORT=80
PUBLIC_PROXY_SSL_PORT=443
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123 AGENDASH_AUTH_PASSWORD=123123
# Sign-up restrictions
SIGNUP_DISABLED=true
SIGNUP_ALLOWED_DOMAINS=
SIGNUP_ALLOWED_EMAILS=

View File

@@ -2,6 +2,24 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
## [0.9.1] - 28-05-2023
`@bigcapital/server`
- fix: deleting ledger entries of manual journal.
- fix: base currency should be enabled.
- fix: delete invoice transaction issue.
`@bigcapital/webapp`
- fix: general, accoutant and items preferences.
- fix: auto-increment sale invoices, estiamtes, credit notes, payments and manual journals.
- refactor: the setup organization form to use binded Formik components.
## [0.9.0] - 06-05-2023
`@bigcapital/server`
- [Sign-up restrictions](https://docs.bigcapital.ly/docs/deployment/signup_restriction) for self-hosting instances to disable signup or control the allowed email addresses and domains that can sign-up.
## [0.8.3] - 06-04-2023 ## [0.8.3] - 06-04-2023
`@bigcaptial/monorepo` `@bigcaptial/monorepo`

View File

@@ -26,6 +26,6 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs. - [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
- [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo. - [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo.
# Changlog # Changelog
Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently. Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently.

View File

@@ -15,8 +15,8 @@ services:
- ./data/logs/nginx/:/var/log/nginx - ./data/logs/nginx/:/var/log/nginx
- ./docker/certbot/certs/:/var/certs - ./docker/certbot/certs/:/var/certs
ports: ports:
- "80:80" - "${PUBLIC_PROXY_PORT:-80}:80"
- "443:443" - "${PUBLIC_PROXY_SSL_PORT:-443}:443"
tty: true tty: true
depends_on: depends_on:
- server - server
@@ -72,6 +72,11 @@ services:
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER} - AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD} - AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
# Sign-up restrictions
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration
build: build:

View File

@@ -34,7 +34,11 @@ ARG MAIL_HOST= \
BASE_URL= \ BASE_URL= \
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash \ AGENDASH_AUTH_USER=agendash \
AGENDASH_AUTH_PASSWORD=123123 AGENDASH_AUTH_PASSWORD=123123 \
# Sign-up restriction
SIGNUP_DISABLED= \
SIGNUP_ALLOWED_DOMAINS= \
SIGNUP_ALLOWED_EMAILS=
ENV MAIL_HOST=$MAIL_HOST \ ENV MAIL_HOST=$MAIL_HOST \
MAIL_USERNAME=$MAIL_USERNAME \ MAIL_USERNAME=$MAIL_USERNAME \
@@ -68,7 +72,11 @@ ENV MAIL_HOST=$MAIL_HOST \
# MongoDB # MongoDB
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \ MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
# Application # Application
BASE_URL=$BASE_URL BASE_URL=$BASE_URL \
# Sign-up restriction
SIGNUP_DISABLED=$SIGNUP_DISABLED \
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
# Create app directory. # Create app directory.
WORKDIR /app WORKDIR /app

View File

@@ -3,7 +3,12 @@ import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces'; import {
AbilitySubject,
AccountAction,
IAccountDTO,
IAccountsStructureType,
} from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes'; import { DATATYPES_LENGTH } from '@/data/DataTypes';
@@ -172,6 +177,11 @@ export default class AccountsController extends BaseController {
query('inactive_mode').optional().isBoolean().toBoolean(), query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(), query('search_keyword').optional({ nullable: true }).isString().trim(),
query('structure')
.optional()
.isString()
.isIn([IAccountsStructureType.Tree, IAccountsStructureType.Flat]),
]; ];
} }
@@ -341,6 +351,7 @@ export default class AccountsController extends BaseController {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
inactiveMode: false, inactiveMode: false,
structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };

View File

@@ -49,6 +49,7 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.resetPassword.bind(this)), asyncMiddleware(this.resetPassword.bind(this)),
this.handlerErrors this.handlerErrors
); );
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
return router; return router;
} }
@@ -207,6 +208,23 @@ export default class AuthenticationController extends BaseController {
} }
} }
/**
* Retrieves the authentication meta for SPA.
* @param {Request} req
* @param {Response} res
* @param {Function} next
* @returns {Response|void}
*/
private async getAuthMeta(req: Request, res: Response, next: Function) {
try {
const meta = await this.authApplication.getAuthMeta();
return res.status(200).send({ meta });
} catch (error) {
next(error);
}
}
/** /**
* Handles the service errors. * Handles the service errors.
*/ */
@@ -247,6 +265,30 @@ export default class AuthenticationController extends BaseController {
errors: [{ type: 'EMAIL.EXISTS', code: 600 }], errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
}); });
} }
if (error.errorType === 'SIGNUP_RESTRICTED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED',
message:
'Sign-up is restricted no one can sign-up to the system.',
code: 700,
},
],
});
}
if (error.errorType === 'SIGNUP_RESTRICTED_NOT_ALLOWED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
message:
'Sign-up is restricted the given email address is not allowed to sign-up.',
code: 710,
},
],
});
}
} }
next(error); next(error);
} }

View File

@@ -41,7 +41,7 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
get balanceSheetValidationSchema(): ValidationChain[] { get balanceSheetValidationSchema(): ValidationChain[] {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('accounting_method').optional().isIn(['cash', 'accural']), query('accounting_method').optional().isIn(['cash', 'accrual']),
query('from_date').optional(), query('from_date').optional(),
query('to_date').optional(), query('to_date').optional(),

View File

@@ -67,6 +67,7 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
try { try {
const { data, query, meta } = const { data, query, meta } =
await this.generalLedgetService.generalLedger(tenantId, filter); await this.generalLedgetService.generalLedger(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
meta: this.transfromToResponse(meta), meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data), data: this.transfromToResponse(data),

View File

@@ -58,7 +58,7 @@ export default class OrganizationController extends BaseController {
private get organizationValidationSchema(): ValidationChain[] { private get organizationValidationSchema(): ValidationChain[] {
return [ return [
check('name').exists().trim(), check('name').exists().trim(),
check('industry').optional().isString(), check('industry').optional({ nullable: true }).isString().trim().escape(),
check('location').exists().isString().isISO31661Alpha2(), check('location').exists().isString().isISO31661Alpha2(),
check('base_currency').exists().isISO4217(), check('base_currency').exists().isISO4217(),
check('timezone').exists().isIn(moment.tz.names()), check('timezone').exists().isIn(moment.tz.names()),

View File

@@ -1,5 +1,6 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
dotenv.config(); dotenv.config();
@@ -146,6 +147,19 @@ module.exports = {
}, },
}, },
/**
* Sign-up restrictions
*/
signupRestrictions: {
disabled: parseBoolean<boolean>(process.env.SIGNUP_DISABLED, false),
allowedDomains: castCommaListEnvVarToArray(
process.env.SIGNUP_ALLOWED_DOMAINS
),
allowedEmails: castCommaListEnvVarToArray(
process.env.SIGNUP_ALLOWED_EMAILS
),
},
/** /**
* Puppeteer remote browserless connection. * Puppeteer remote browserless connection.
*/ */

View File

@@ -6,14 +6,14 @@ export default class SeedAccounts extends TenantSeeder {
* Seeds initial accounts to the organization. * Seeds initial accounts to the organization.
*/ */
up(knex) { up(knex) {
const data = AccountsData.map((account) => { const data = AccountsData.map((account) => ({
return {
...account, ...account,
name: this.i18n.__(account.name), name: this.i18n.__(account.name),
description: this.i18n.__(account.description), description: this.i18n.__(account.description),
currencyCode: this.tenant.metadata.baseCurrency, currencyCode: this.tenant.metadata.baseCurrency,
}; seededAt: new Date(),
}); })
);
return knex('accounts').then(async () => { return knex('accounts').then(async () => {
// Inserts seed entries. // Inserts seed entries.
return knex('accounts').insert(data); return knex('accounts').insert(data);

View File

@@ -8,7 +8,7 @@ export default class SeedSettings extends TenantSeeder {
up() { up() {
const settings = [ const settings = [
// Orgnization settings. // Orgnization settings.
{ group: 'organization', key: 'accounting_basis', value: 'accural' }, { group: 'organization', key: 'accounting_basis', value: 'accrual' },
// Accounts settings. // Accounts settings.
{ group: 'accounts', key: 'account_code_unique', value: true }, { group: 'accounts', key: 'account_code_unique', value: true },

View File

@@ -79,9 +79,15 @@ export interface IAccountTransaction {
} }
export interface IAccountResponse extends IAccount {} export interface IAccountResponse extends IAccount {}
export enum IAccountsStructureType {
Tree = 'tree',
Flat = 'flat',
}
export interface IAccountsFilter extends IDynamicListFilterDTO { export interface IAccountsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
onlyInactive: boolean; onlyInactive: boolean;
structure?: IAccountsStructureType;
} }
export interface IAccountType { export interface IAccountType {

View File

@@ -75,3 +75,7 @@ export interface IAuthSendedResetPassword {
user: ISystemUser, user: ISystemUser,
token: string; token: string;
} }
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
}

View File

@@ -44,7 +44,7 @@ export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery {
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
noneTransactions: boolean; noneTransactions: boolean;
noneZero: boolean; noneZero: boolean;
basis: 'cash' | 'accural'; basis: 'cash' | 'accrual';
accountIds: number[]; accountIds: number[];
percentageOfColumn: boolean; percentageOfColumn: boolean;

View File

@@ -4,7 +4,7 @@ export interface ITrialBalanceSheetQuery {
fromDate: Date | string; fromDate: Date | string;
toDate: Date | string; toDate: Date | string;
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
basis: 'cash' | 'accural'; basis: 'cash' | 'accrual';
noneZero: boolean; noneZero: boolean;
noneTransactions: boolean; noneTransactions: boolean;
onlyActive: boolean; onlyActive: boolean;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash'; import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
import { isArrayLikeObject } from 'lodash/fp';
export class Transformer { export class Transformer {
public context: any; public context: any;
@@ -39,12 +40,33 @@ export class Transformer {
return object; return object;
}; };
/**
*
* @param object
* @returns
*/
protected preCollectionTransform = (object: any) => {
return object;
};
/**
*
* @param object
* @returns
*/
protected postCollectionTransform = (object: any) => {
return object;
};
/** /**
* *
*/ */
public work = (object: any) => { public work = (object: any) => {
if (Array.isArray(object)) { if (Array.isArray(object)) {
return object.map(this.getTransformation); const preTransformed = this.preCollectionTransform(object);
const transformed = preTransformed.map(this.getTransformation);
return this.postCollectionTransform(transformed);
} else if (isObject(object)) { } else if (isObject(object)) {
return this.getTransformation(object); return this.getTransformation(object);
} }

View File

@@ -10,7 +10,7 @@ export class LedgerRevert {
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject() @Inject()
ledgerStorage: LedgerStorageService; private ledgerStorage: LedgerStorageService;
/** /**
* Reverts the jouranl entries. * Reverts the jouranl entries.

View File

@@ -1,6 +1,11 @@
import { IAccount } from '@/interfaces'; import { IAccount, IAccountsStructureType } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils'; import {
assocDepthLevelToObjectTree,
flatToNestedArray,
formatNumber,
nestedArrayToFlatten,
} from 'utils';
export class AccountTransformer extends Transformer { export class AccountTransformer extends Transformer {
/** /**
@@ -8,7 +13,23 @@ export class AccountTransformer extends Transformer {
* @returns {Array} * @returns {Array}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return ['formattedAmount']; return ['formattedAmount', 'flattenName'];
};
/**
* Retrieves the flatten name with all dependants accounts names.
* @param {IAccount} account -
* @returns {string}
*/
public flattenName = (account: IAccount): string => {
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
account.id
);
const prefixAccounts = parentDependantsIds.map((dependId) => {
const node = this.options.accountsGraph.getNodeData(dependId);
return `${node.name}: `;
});
return `${prefixAccounts}${account.name}`;
}; };
/** /**
@@ -17,8 +38,28 @@ export class AccountTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected formattedAmount = (account: IAccount): string => { protected formattedAmount = (account: IAccount): string => {
return formatNumber(account.amount, { return formatNumber(account.amount, { currencyCode: account.currencyCode });
currencyCode: account.currencyCode, };
/**
* Transformes the accounts collection to flat or nested array.
* @param {IAccount[]}
* @returns {IAccount[]}
*/
protected postCollectionTransform = (accounts: IAccount[]) => {
// Transfom the flatten to accounts tree.
const transformed = flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
}); });
// Associate `accountLevel` attr to indicate object depth.
const transformed2 = assocDepthLevelToObjectTree(
transformed,
1,
'accountLevel'
);
return this.options.structure === IAccountsStructureType.Flat
? nestedArrayToFlatten(transformed2)
: transformed2;
}; };
} }

View File

@@ -22,15 +22,19 @@ export class GetAccount {
*/ */
public getAccount = async (tenantId: number, accountId: number) => { public getAccount = async (tenantId: number, accountId: number) => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Find the given account or throw not found error. // Find the given account or throw not found error.
const account = await Account.query().findById(accountId).throwIfNotFound(); const account = await Account.query().findById(accountId).throwIfNotFound();
const accountsGraph = await accountRepository.getDependencyGraph();
// Transformes the account model to POJO. // Transformes the account model to POJO.
const transformed = await this.transformer.transform( const transformed = await this.transformer.transform(
tenantId, tenantId,
account, account,
new AccountTransformer() new AccountTransformer(),
{ accountsGraph }
); );
return this.i18nService.i18nApply( return this.i18nService.i18nApply(
[['accountTypeLabel'], ['accountNormalFormatted']], [['accountTypeLabel'], ['accountNormalFormatted']],

View File

@@ -1,6 +1,11 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import { IAccountsFilter, IAccountResponse, IFilterMeta } from '@/interfaces'; import {
IAccountsFilter,
IAccountResponse,
IFilterMeta,
IAccountsStructureType,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { AccountTransformer } from './AccountTransform'; import { AccountTransformer } from './AccountTransform';
@@ -38,6 +43,7 @@ export class GetAccounts {
filterDTO: IAccountsFilter filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => { ): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Parses the stringified filter roles. // Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO); const filter = this.parseListFilterDTO(filterDTO);
@@ -53,17 +59,16 @@ export class GetAccounts {
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode); builder.modify('inactiveMode', filter.inactiveMode);
}); });
// Retrievs the formatted accounts collection.
const preTransformedAccounts = await this.transformer.transform( const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieves the transformed accounts collection.
const transformedAccounts = await this.transformer.transform(
tenantId, tenantId,
accounts, accounts,
new AccountTransformer() new AccountTransformer(),
{ accountsGraph, structure: filterDTO.structure }
); );
// Transform accounts to nested array.
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
return { return {
accounts: transformedAccounts, accounts: transformedAccounts,

View File

@@ -1,8 +1,14 @@
import { Service, Inject, Container } from 'typedi'; import { Service, Inject, Container } from 'typedi';
import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces'; import {
IRegisterDTO,
ISystemUser,
IPasswordReset,
IAuthGetMetaPOJO,
} from '@/interfaces';
import { AuthSigninService } from './AuthSignin'; import { AuthSigninService } from './AuthSignin';
import { AuthSignupService } from './AuthSignup'; import { AuthSignupService } from './AuthSignup';
import { AuthSendResetPassword } from './AuthSendResetPassword'; import { AuthSendResetPassword } from './AuthSendResetPassword';
import { GetAuthMeta } from './GetAuthMeta';
@Service() @Service()
export default class AuthenticationApplication { export default class AuthenticationApplication {
@@ -15,6 +21,9 @@ export default class AuthenticationApplication {
@Inject() @Inject()
private authResetPasswordService: AuthSendResetPassword; private authResetPasswordService: AuthSendResetPassword;
@Inject()
private authGetMeta: GetAuthMeta;
/** /**
* Signin and generates JWT token. * Signin and generates JWT token.
* @throws {ServiceError} * @throws {ServiceError}
@@ -53,4 +62,12 @@ export default class AuthenticationApplication {
public async resetPassword(token: string, password: string): Promise<void> { public async resetPassword(token: string, password: string): Promise<void> {
return this.authResetPasswordService.resetPassword(token, password); return this.authResetPasswordService.resetPassword(token, password);
} }
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return this.authGetMeta.getAuthMeta();
}
} }

View File

@@ -1,4 +1,4 @@
import { omit } from 'lodash'; import { isEmpty, omit } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
@@ -13,6 +13,7 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import TenantsManagerService from '../Tenancy/TenantsManager'; import TenantsManagerService from '../Tenancy/TenantsManager';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { hashPassword } from '@/utils'; import { hashPassword } from '@/utils';
import config from '@/config';
export class AuthSignupService { export class AuthSignupService {
@Inject() @Inject()
@@ -33,6 +34,9 @@ export class AuthSignupService {
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> { public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
// Validates the signup disable restrictions.
await this.validateSignupRestrictions(signupDTO.email);
// Validates the given email uniqiness. // Validates the given email uniqiness.
await this.validateEmailUniqiness(signupDTO.email); await this.validateEmailUniqiness(signupDTO.email);
@@ -74,4 +78,34 @@ export class AuthSignupService {
throw new ServiceError(ERRORS.EMAIL_EXISTS); throw new ServiceError(ERRORS.EMAIL_EXISTS);
} }
} }
/**
* Validate sign-up disable restrictions.
* @param {string} email
*/
private async validateSignupRestrictions(email: string) {
// Can't continue if the signup is not disabled.
if (!config.signupRestrictions.disabled) return;
// Validate the allowed email addresses and domains.
if (
!isEmpty(config.signupRestrictions.allowedEmails) ||
!isEmpty(config.signupRestrictions.allowedDomains)
) {
const emailDomain = email.split('@').pop();
const isAllowedEmail =
config.signupRestrictions.allowedEmails.indexOf(email) !== -1;
const isAllowedDomain = config.signupRestrictions.allowedDomains.some(
(domain) => emailDomain === domain
);
if (!isAllowedEmail && !isAllowedDomain) {
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED);
}
// Throw error if the signup is disabled with no exceptions.
} else {
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED);
}
}
} }

View File

@@ -0,0 +1,16 @@
import { Service } from 'typedi';
import { IAuthGetMetaPOJO } from '@/interfaces';
import config from '@/config';
@Service()
export class GetAuthMeta {
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: config.signupRestrictions.disabled,
};
}
}

View File

@@ -7,4 +7,6 @@ export const ERRORS = {
TOKEN_EXPIRED: 'TOKEN_EXPIRED', TOKEN_EXPIRED: 'TOKEN_EXPIRED',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS', EMAIL_EXISTS: 'EMAIL_EXISTS',
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
}; };

View File

@@ -5,18 +5,13 @@ import {
ICreditNoteDeletedPayload, ICreditNoteDeletedPayload,
ICreditNoteEditedPayload, ICreditNoteEditedPayload,
ICreditNoteOpenedPayload, ICreditNoteOpenedPayload,
IRefundCreditNoteOpenedPayload,
} from '@/interfaces'; } from '@/interfaces';
import CreditNoteGLEntries from './CreditNoteGLEntries'; import CreditNoteGLEntries from './CreditNoteGLEntries';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export default class CreditNoteGLEntriesSubscriber { export default class CreditNoteGLEntriesSubscriber {
@Inject() @Inject()
creditNoteGLEntries: CreditNoteGLEntries; private creditNoteGLEntries: CreditNoteGLEntries;
@Inject()
tenancy: HasTenancyService;
/** /**
* Attaches events with handlers. * Attaches events with handlers.

View File

@@ -5,7 +5,7 @@ import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces
@Service() @Service()
export default class CashflowAccountTransactionsRepo { export default class CashflowAccountTransactionsRepo {
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
/** /**
* Retrieve the cashflow account transactions. * Retrieve the cashflow account transactions.

View File

@@ -17,7 +17,7 @@ export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
formatMoney: 'total', formatMoney: 'total',
precision: 2, precision: 2,
}, },
basis: 'accural', basis: 'accrual',
noneZero: false, noneZero: false,
noneTransactions: false, noneTransactions: false,

View File

@@ -35,7 +35,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
formatMoney: 'total', formatMoney: 'total',
precision: 2, precision: 2,
}, },
basis: 'accural', basis: 'accrual',
noneZero: false, noneZero: false,
noneTransactions: true, noneTransactions: true,
onlyActive: false, onlyActive: false,

View File

@@ -1,11 +1,10 @@
import { difference, sumBy, omit, map } from 'lodash'; import { difference } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
IManualJournalDTO, IManualJournalDTO,
IManualJournalEntry, IManualJournalEntry,
IManualJournal, IManualJournal,
IManualJournalEntryDTO,
} from '@/interfaces'; } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
@@ -286,7 +285,7 @@ export class CommandManualJournalValidators {
public validateJournalCurrencyWithAccountsCurrency = async ( public validateJournalCurrencyWithAccountsCurrency = async (
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO,
baseCurrency: string, baseCurrency: string
) => { ) => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);

View File

@@ -3,25 +3,20 @@ import * as R from 'ramda';
import { import {
IManualJournal, IManualJournal,
IManualJournalEntry, IManualJournalEntry,
IAccount,
ILedgerEntry, ILedgerEntry,
} from '@/interfaces'; } from '@/interfaces';
import { Knex } from 'knex'; import { Knex } from 'knex';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { LedgerRevert } from '@/services/Accounting/LedgerStorageRevert';
@Service() @Service()
export class ManualJournalGLEntries { export class ManualJournalGLEntries {
@Inject() @Inject()
ledgerStorage: LedgerStorageService; private ledgerStorage: LedgerStorageService;
@Inject() @Inject()
ledgerRevert: LedgerRevert; private tenancy: HasTenancyService;
@Inject()
tenancy: HasTenancyService;
/** /**
* Create manual journal GL entries. * Create manual journal GL entries.
@@ -77,7 +72,7 @@ export class ManualJournalGLEntries {
manualJournalId: number, manualJournalId: number,
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<void> => { ): Promise<void> => {
return this.ledgerRevert.revertGLEntries( return this.ledgerStorage.deleteByReference(
tenantId, tenantId,
manualJournalId, manualJournalId,
'Journal', 'Journal',
@@ -86,7 +81,7 @@ export class ManualJournalGLEntries {
}; };
/** /**
* * Retrieves the ledger of the given manual journal.
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {Ledger} * @returns {Ledger}
*/ */
@@ -97,11 +92,13 @@ export class ManualJournalGLEntries {
}; };
/** /**
* * Retrieves the common entry details of the manual journal
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {} * @returns {Partial<ILedgerEntry>}
*/ */
private getManualJournalCommonEntry = (manualJournal: IManualJournal) => { private getManualJournalCommonEntry = (
manualJournal: IManualJournal
): Partial<ILedgerEntry> => {
return { return {
transactionNumber: manualJournal.journalNumber, transactionNumber: manualJournal.journalNumber,
referenceNumber: manualJournal.reference, referenceNumber: manualJournal.reference,
@@ -118,7 +115,8 @@ export class ManualJournalGLEntries {
}; };
/** /**
* * Retrieves the ledger entry of the given manual journal and
* its associated entry.
* @param {IManualJournal} manualJournal - * @param {IManualJournal} manualJournal -
* @param {IManualJournalEntry} entry - * @param {IManualJournalEntry} entry -
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
@@ -149,7 +147,7 @@ export class ManualJournalGLEntries {
); );
/** /**
* * Retrieves the ledger of the given manual journal.
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {ILedgerEntry[]} * @returns {ILedgerEntry[]}
*/ */

View File

@@ -23,8 +23,11 @@ export class ProjectBillableBillSubscriber {
events.saleInvoice.onCreated, events.saleInvoice.onCreated,
this.handleIncreaseBillableBill this.handleIncreaseBillableBill
); );
bus.subscribe(events.saleInvoice.onEdited, this.handleDecreaseBillableBill); bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableBill);
bus.subscribe(events.saleInvoice.onDeleted, this.handleEditBillableBill); bus.subscribe(
events.saleInvoice.onDeleted,
this.handleDecreaseBillableBill
);
} }
/** /**

View File

@@ -1,7 +1,11 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import async from 'async'; import async from 'async';
import { ISaleInvoice, ISaleInvoiceDTO, ProjectLinkRefType } from '@/interfaces'; import {
ISaleInvoice,
ISaleInvoiceDTO,
ProjectLinkRefType,
} from '@/interfaces';
import { ProjectBillableExpense } from './ProjectBillableExpense'; import { ProjectBillableExpense } from './ProjectBillableExpense';
import { filterEntriesByRefType } from './_utils'; import { filterEntriesByRefType } from './_utils';

View File

@@ -21,13 +21,10 @@ export class ProjectBillableExpensesSubscriber {
events.saleInvoice.onCreated, events.saleInvoice.onCreated,
this.handleIncreaseBillableExpenses this.handleIncreaseBillableExpenses
); );
bus.subscribe( bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableExpenses);
events.saleInvoice.onEdited,
this.handleDecreaseBillableExpenses
);
bus.subscribe( bus.subscribe(
events.saleInvoice.onDeleted, events.saleInvoice.onDeleted,
this.handleEditBillableExpenses this.handleDecreaseBillableExpenses
); );
} }

View File

@@ -419,6 +419,58 @@ export const parseDate = (date: string) => {
return date ? moment(date).utcOffset(0).format('YYYY-MM-DD') : ''; return date ? moment(date).utcOffset(0).format('YYYY-MM-DD') : '';
}; };
const nestedArrayToFlatten = (
collection,
property = 'children',
parseItem = (a, level) => a,
level = 1
) => {
const parseObject = (obj) =>
parseItem(
{
..._.omit(obj, [property]),
},
level
);
return collection.reduce((items, currentValue, index) => {
let localItems = [...items];
const parsedItem = parseObject(currentValue, level);
localItems.push(parsedItem);
if (Array.isArray(currentValue[property])) {
const flattenArray = nestedArrayToFlatten(
currentValue[property],
property,
parseItem,
level + 1
);
localItems = _.concat(localItems, flattenArray);
}
return localItems;
}, []);
};
const assocDepthLevelToObjectTree = (
objects,
level = 1,
propertyName = 'level'
) => {
for (let i = 0; i < objects.length; i++) {
const object = objects[i];
object[propertyName] = level;
if (object.children) {
assocDepthLevelToObjectTree(object.children, level + 1, propertyName);
}
}
return objects;
};
const castCommaListEnvVarToArray = (envVar: string): Array<string> => {
return envVar ? envVar?.split(',')?.map(_.trim) : [];
};
export { export {
templateRender, templateRender,
accumSum, accumSum,
@@ -449,4 +501,7 @@ export {
dateRangeFromToCollection, dateRangeFromToCollection,
transformToMapKeyValue, transformToMapKeyValue,
mergeObjectsBykey, mergeObjectsBykey,
nestedArrayToFlatten,
assocDepthLevelToObjectTree,
castCommaListEnvVarToArray
}; };

View File

@@ -5,10 +5,10 @@ USER root
WORKDIR /app WORKDIR /app
# Install dependencies # Install dependencies
COPY package.json ./ COPY package*.json ./
COPY lerna.json ./ COPY lerna.json ./
COPY ./packages/webapp/package.json /app/packages/webapp/package.json COPY ./packages/webapp/package*.json /app/packages/webapp/
RUN npm install RUN npm install
RUN npm run bootstrap RUN npm run bootstrap

View File

@@ -1205,9 +1205,9 @@
} }
}, },
"@blueprintjs-formik/core": { "@blueprintjs-formik/core": {
"version": "0.2.1", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.3.3.tgz",
"integrity": "sha512-YGJe+QorDGbkWDSUg6x69LYGN62Kgvb92Iz/voqmszVRKj4KcoPvd/7coF8Jmu+ZQE6LcwM/9ccB2i63L99ITA==", "integrity": "sha512-ko7g54YSEcSq2K/GEpmiTG0foGLqe7DwgXGhkGxYEiHhLAUv8WvQmrFsm8e/KOW7n8mLGq0uaZVe2l8m3JTGGQ==",
"requires": { "requires": {
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.keyby": "^4.6.0", "lodash.keyby": "^4.6.0",
@@ -1227,9 +1227,9 @@
} }
}, },
"@blueprintjs-formik/select": { "@blueprintjs-formik/select": {
"version": "0.1.5", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.1.5.tgz", "resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.2.3.tgz",
"integrity": "sha512-EqGbuoiS1VrWpzjd39uVhBAmfVobdpgqalGcpODyGA+XAYoft1UU12yzTzrEOwBZpQKiC12UQwekUPspYBsVKA==", "integrity": "sha512-j/zkX0B9wgtoHgK6Z/rlowB7F7zemrAajBU+d3caCoEYMMqwAI0XA++GytqrIhv5fEGjkZ1hkxS9j8eqX8vtjA==",
"requires": { "requires": {
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.keyby": "^4.6.0", "lodash.keyby": "^4.6.0",
@@ -7298,6 +7298,11 @@
"locate-path": "^3.0.0" "locate-path": "^3.0.0"
} }
}, },
"flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="
},
"flat-cache": { "flat-cache": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",

View File

@@ -3,9 +3,9 @@
"version": "1.7.1", "version": "1.7.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@blueprintjs-formik/core": "^0.2.1", "@blueprintjs-formik/core": "^0.3.3",
"@blueprintjs-formik/datetime": "^0.3.4", "@blueprintjs-formik/datetime": "^0.3.4",
"@blueprintjs-formik/select": "^0.1.4", "@blueprintjs-formik/select": "^0.2.3",
"@blueprintjs/core": "^3.50.2", "@blueprintjs/core": "^3.50.2",
"@blueprintjs/datetime": "^3.23.12", "@blueprintjs/datetime": "^3.23.12",
"@blueprintjs/popover2": "^0.11.1", "@blueprintjs/popover2": "^0.11.1",
@@ -16,8 +16,6 @@
"@casl/react": "^2.3.0", "@casl/react": "^2.3.0",
"@craco/craco": "^5.9.0", "@craco/craco": "^5.9.0",
"@reduxjs/toolkit": "^1.2.5", "@reduxjs/toolkit": "^1.2.5",
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0", "@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
@@ -45,6 +43,7 @@
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dependency-graph": "^0.11.0", "dependency-graph": "^0.11.0",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"flat": "^5.0.2",
"formik": "^2.2.5", "formik": "^2.2.5",
"http-proxy-middleware": "^1.0.0", "http-proxy-middleware": "^1.0.0",
"jest": "24.9.0", "jest": "24.9.0",

View File

@@ -20,24 +20,6 @@
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<% if (process.env.NODE_ENV === 'production') { %>
<!-- Hotjar Tracking Code for https://app.bigcapital.ly/ -->
<script>
(function (h, o, t, j, a, r) {
h.hj =
h.hj ||
function () {
(h.hj.q = h.hj.q || []).push(arguments);
};
h._hjSettings = { hjid: 2774528, hjsv: 6 };
a = o.getElementsByTagName('head')[0];
r = o.createElement('script');
r.async = 1;
r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv;
a.appendChild(r);
})(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv=');
</script>
<% } %>
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
@@ -69,7 +51,5 @@
href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"
type="text/css" type="text/css"
/> />
<!-- <link href="https://cdn.syncfusion.com/ej2/material.css" rel="stylesheet"> -->
<!-- <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" /> -->
</body> </body>
</html> </html>

View File

@@ -1,74 +0,0 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { MenuItem } from '@blueprintjs/core';
import { FMultiSelect } from '../Forms';
import classNames from 'classnames';
import { Classes } from '@blueprintjs/popover2';
/**
*
* @param {*} query
* @param {*} account
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const accountItemPredicate = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} account
* @param {*} param1
* @returns
*/
const accountItemRenderer = (
account,
{ handleClick, modifiers, query },
{ isSelected },
) => {
return (
<MenuItem
icon={isSelected ? 'tick' : 'blank'}
text={account.name}
label={account.code}
key={account.id}
onClick={handleClick}
/>
);
};
const accountSelectProps = {
itemPredicate: accountItemPredicate,
itemRenderer: accountItemRenderer,
valueAccessor: (item) => item.id,
labelAccessor: (item) => item.code,
tagRenderer: (item) => item.name,
};
/**
* branches mulit select.
* @param {*} param0
* @returns {JSX.Element}
*/
export function AccountMultiSelect({ accounts, ...rest }) {
return (
<FMultiSelect
items={accounts}
popoverProps={{
minimal: true,
}}
{...accountSelectProps}
{...rest}
/>
);
}

View File

@@ -1,31 +1,97 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from '../MultiSelectTaggable'; import { FMultiSelect } from '../Forms';
import { accountPredicate } from './_components';
import { MenuItemNestedText } from '../Menu';
import { usePreprocessingAccounts } from './_hooks';
export function AccountsMultiSelect({ ...multiSelectProps }) { // Create new account renderer.
return ( const createNewItemRenderer = (query, active, handleClick) => {
<MultiSelect
itemRenderer={(
item,
{ active, selected, handleClick, modifiers, query },
) => {
return ( return (
<MenuItem <MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active} active={active}
icon={selected ? 'tick' : 'blank'}
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick} onClick={handleClick}
/> />
); );
}} };
/**
* Default account item renderer of the list.
* @returns {JSX.Element}
*/
const accountRenderer = (
item,
{ handleClick, modifiers, query },
{ isSelected },
) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={<MenuItemNestedText level={item.account_level} text={item.name} />}
key={item.id}
onClick={handleClick}
icon={isSelected ? 'tick' : 'blank'}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => ({ name });
/**
* Accounts multi-select field binded with Formik form.
* @returns {JSX.Element}
*/
export function AccountsMultiSelect({
items,
allowCreate,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
...rest
}) {
// Filters accounts based on filter props.
const filteredAccounts = usePreprocessingAccounts(items, {
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
});
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
// Handles the create item click.
const handleCreateItemClick = () => {
openDialog(DialogsName.AccountForm);
};
return (
<FMultiSelect
items={filteredAccounts}
valueAccessor={'id'}
textAccessor={'name'}
labelAccessor={'code'}
tagAccessor={'name'}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
fill={true} itemPredicate={accountPredicate}
tagRenderer={(item) => item.name} itemRenderer={accountRenderer}
resetOnSelect={true} createNewItemRenderer={maybeCreateNewItemRenderer}
{...multiSelectProps} createNewItemFromQuery={maybeCreateNewItemFromQuery}
onCreateItemSelect={handleCreateItemClick}
{...rest}
/> />
); );
} }

View File

@@ -0,0 +1,101 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { MenuItemNestedText, FSelect } from '@/components';
import { accountPredicate } from './_components';
import { DialogsName } from '@/constants/dialogs';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { usePreprocessingAccounts } from './_hooks';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => ({ name });
/**
* Default account item renderer.
* @returns {JSX.Element}
*/
const accountRenderer = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
label={item.code}
key={item.id}
text={<MenuItemNestedText level={item.account_level} text={item.name} />}
onClick={handleClick}
/>
);
};
/**
* Accounts select field binded with Formik form.
* @returns {JSX.Element}
*/
function AccountsSelectRoot({
// #withDialogActions
openDialog,
// #ownProps
items,
allowCreate,
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
...restProps
}) {
// Filters accounts based on filter props.
const filteredAccounts = usePreprocessingAccounts(items, {
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
});
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
// Handles the create item click.
const handleCreateItemClick = () => {
openDialog(DialogsName.AccountForm);
};
return (
<FSelect
items={filteredAccounts}
textAccessor={'name'}
labelAccessor={'code'}
valueAccessor={'id'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
itemPredicate={accountPredicate}
itemRenderer={accountRenderer}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
onCreateItemSelect={handleCreateItemClick}
{...restProps}
/>
);
}
export const AccountsSelect = R.compose(withDialogActions)(AccountsSelectRoot);

View File

@@ -1,177 +0,0 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import * as R from 'ramda';
import { MenuItemNestedText, FormattedMessage as T } from '@/components';
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
import { CLASSES } from '@/constants/classes';
import { DialogsName } from '@/constants/dialogs';
import withDialogActions from '@/containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts select list.
*/
function AccountsSelectListRoot({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = 'Select account',
onAccountSelected,
disabled = false,
popoverFill = false,
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
allowCreate,
buttonProps = {},
}) {
const flattenAccounts = useMemo(
() => nestedArrayToflatten(accounts),
[accounts],
);
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
});
return filteredAccounts;
}, [
flattenAccounts,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
]);
// Find initial account object to set it as default account in initial render.
const initialAccount = useMemo(
() => filteredAccounts.find((a) => a.id === initialAccountId),
[initialAccountId, filteredAccounts],
);
// Select account item.
const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null,
);
useEffect(() => {
if (typeof selectedAccountId !== 'undefined') {
const account = selectedAccountId
? filteredAccounts.find((a) => a.id === selectedAccountId)
: null;
setSelectedAccount(account);
}
}, [selectedAccountId, filteredAccounts, setSelectedAccount]);
// Account item of select accounts field.
const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={<MenuItemNestedText level={item.level} text={item.name} />}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
}, []);
// Handle the account item select.
const handleAccountSelect = useCallback(
(account) => {
if (account.id) {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
openDialog(DialogsName.AccountForm);
}
},
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
filterable={true}
onItemSelect={handleAccountSelect}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
>
<Button
disabled={disabled}
text={selectedAccount ? selectedAccount.name : defaultSelectText}
{...buttonProps}
/>
</Select>
);
}
export const AccountsSelectList = R.compose(withDialogActions)(
AccountsSelectListRoot,
);

View File

@@ -1,49 +1,15 @@
// @ts-nocheck // @ts-nocheck
import React, { useCallback } from 'react'; import React from 'react';
import classNames from 'classnames'; import { FSelect } from '@/components/Forms';
import { ListSelect } from '@/components';
import { CLASSES } from '@/constants/classes';
export function AccountsTypesSelect({
accountsTypes,
selectedTypeId,
defaultSelectText = 'Select account type',
onTypeSelected,
disabled = false,
popoverFill = false,
...restProps
}) {
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
const normalizedTitle = accountType.label.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Handle item selected.
const handleItemSelected = (accountType) => {
onTypeSelected && onTypeSelected(accountType);
};
export function AccountsTypesSelect({ ...props }) {
return ( return (
<ListSelect <FSelect
items={accountsTypes} valueAccessor={'key'}
selectedItemProp={'key'} labelAccessor={'label'}
selectedItem={selectedTypeId} textAccessor={'label'}
textProp={'label'} placeholder={'Select an account...'}
defaultText={defaultSelectText} {...props}
onItemSelect={handleItemSelected}
itemPredicate={filterAccountTypeItems}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...restProps}
/> />
); );
} }

View File

@@ -0,0 +1,14 @@
// @ts-nocheck
import React from 'react';
// Filters accounts items.
export const accountPredicate = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};

View File

@@ -0,0 +1,36 @@
import { useMemo } from 'react';
import { filterAccountsByQuery, nestedArrayToflatten } from '@/utils';
interface PreprocessingAccountsOptions {
filterByRootTypes: string[];
filterByParentTypes: string[];
filterByTypes: string[];
filterByNormal: string[];
}
export const usePreprocessingAccounts = (
items: any,
{
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
}: PreprocessingAccountsOptions,
) => {
return useMemo(() => {
const flattenAccounts = nestedArrayToflatten(items);
const filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
});
return filteredAccounts;
}, [
items,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
]);
};

View File

@@ -1,5 +1,4 @@
export * from './AccountMultiSelect'; export * from './AccountsSelect';
export * from './AccountsMultiSelect'; export * from './AccountsMultiSelect';
export * from './AccountsSelectList';
export * from './AccountsSuggestField'; export * from './AccountsSuggestField';
export * from './AccountsTypesSelect'; export * from './AccountsTypesSelect';

View File

@@ -77,11 +77,13 @@ export function BankAccount({
</BankAccountHeader> </BankAccountHeader>
<BankAccountMeta> <BankAccountMeta>
{false && (
<BankAccountMetaLine <BankAccountMetaLine
title={intl.get('cash_flow.label_account_transcations')} title={intl.get('cash_flow.transactions_for_review')}
value={2} value={'0'}
className={clsx({ [Classes.SKELETON]: loading })} className={clsx({ [Classes.SKELETON]: loading })}
/> />
)}
<BankAccountMetaLine <BankAccountMetaLine
title={updatedBeforeText} title={updatedBeforeText}
className={clsx({ [Classes.SKELETON]: loading })} className={clsx({ [Classes.SKELETON]: loading })}

View File

@@ -2,7 +2,6 @@
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from '../Forms'; import { FSelect } from '../Forms';
/** /**
@@ -28,30 +27,6 @@ const currencyItemPredicate = (query, currency, _index, exactMatch) => {
} }
}; };
/**
* @param {*} currency
* @returns
*/
const currencyItemRenderer = (currency, { handleClick, modifiers, query }) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={currency.currency_name}
label={currency.currency_code.toString()}
key={currency.id}
onClick={handleClick}
/>
);
};
const currencySelectProps = {
itemPredicate: currencyItemPredicate,
itemRenderer: currencyItemRenderer,
valueAccessor: 'currency_code',
labelAccessor: 'currency_code',
};
/** /**
* *
* @param {*} currencies * @param {*} currencies
@@ -60,18 +35,13 @@ const currencySelectProps = {
export function CurrencySelect({ currencies, ...rest }) { export function CurrencySelect({ currencies, ...rest }) {
return ( return (
<FSelect <FSelect
{...currencySelectProps} itemPredicate={currencyItemPredicate}
valueAccessor={'currency_code'}
textAccessor={'currency_name'}
labelAccessor={'currency_code'}
{...rest} {...rest}
items={currencies} items={currencies}
input={CurrnecySelectButton} placeholder={intl.get('select_currency_code')}
/> />
); );
} }
/**
* @param {*} label
* @returns
*/
function CurrnecySelectButton({ label }) {
return <Button text={label ? label : intl.get('select_currency_code')} />;
}

View File

@@ -17,8 +17,8 @@ import AllocateLandedCostDialog from '@/containers/Dialogs/AllocateLandedCostDia
import InvoicePdfPreviewDialog from '@/containers/Dialogs/InvoicePdfPreviewDialog'; import InvoicePdfPreviewDialog from '@/containers/Dialogs/InvoicePdfPreviewDialog';
import EstimatePdfPreviewDialog from '@/containers/Dialogs/EstimatePdfPreviewDialog'; import EstimatePdfPreviewDialog from '@/containers/Dialogs/EstimatePdfPreviewDialog';
import ReceiptPdfPreviewDialog from '@/containers/Dialogs/ReceiptPdfPreviewDialog'; import ReceiptPdfPreviewDialog from '@/containers/Dialogs/ReceiptPdfPreviewDialog';
import MoneyInDialog from '@/containers/Dialogs/MoneyInDialog'; import MoneyInDialog from '@/containers/CashFlow/MoneyInDialog';
import MoneyOutDialog from '@/containers/Dialogs/MoneyOutDialog'; import MoneyOutDialog from '@/containers/CashFlow/MoneyOutDialog';
import BadDebtDialog from '@/containers/Dialogs/BadDebtDialog'; import BadDebtDialog from '@/containers/Dialogs/BadDebtDialog';
import NotifyInvoiceViaSMSDialog from '@/containers/Dialogs/NotifyInvoiceViaSMSDialog'; import NotifyInvoiceViaSMSDialog from '@/containers/Dialogs/NotifyInvoiceViaSMSDialog';
import NotifyReceiptViaSMSDialog from '@/containers/Dialogs/NotifyReceiptViaSMSDialog'; import NotifyReceiptViaSMSDialog from '@/containers/Dialogs/NotifyReceiptViaSMSDialog';

View File

@@ -1,4 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import { import {
FormGroup, FormGroup,
InputGroup, InputGroup,
@@ -9,8 +10,9 @@ import {
EditableText, EditableText,
TextArea, TextArea,
} from '@blueprintjs-formik/core'; } from '@blueprintjs-formik/core';
import { Select, MultiSelect } from '@blueprintjs-formik/select'; import { MultiSelect } from '@blueprintjs-formik/select';
import { DateInput } from '@blueprintjs-formik/datetime'; import { DateInput } from '@blueprintjs-formik/datetime';
import { FSelect } from './Select';
export { export {
FormGroup as FFormGroup, FormGroup as FFormGroup,
@@ -19,7 +21,7 @@ export {
Checkbox as FCheckbox, Checkbox as FCheckbox,
RadioGroup as FRadioGroup, RadioGroup as FRadioGroup,
Switch as FSwitch, Switch as FSwitch,
Select as FSelect, FSelect,
MultiSelect as FMultiSelect, MultiSelect as FMultiSelect,
EditableText as FEditableText, EditableText as FEditableText,
TextArea as FTextArea, TextArea as FTextArea,

View File

@@ -0,0 +1,58 @@
// @ts-nocheck
import React from 'react';
import { Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs-formik/select';
import styled from 'styled-components';
import clsx from 'classnames';
export function FSelect({ ...props }) {
const input = ({ activeItem, text, label, value }) => {
return (
<SelectButton
text={text || props.placeholder || 'Select an item ...'}
disabled={props.disabled || false}
{...props.buttonProps}
className={clsx({ 'is-selected': !!text }, props.className)}
/>
);
};
return <Select input={input} {...props} fill={true} />;
}
const SelectButton = styled(Button)`
outline: none;
box-shadow: 0 0 0 transparent;
border: 1px solid #ced4da;
position: relative;
padding-right: 30px;
&:not(.is-selected):not([class*='bp3-intent-']):not(.bp3-minimal) {
color: #5c7080;
}
&:after {
content: '';
display: inline-block;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #8d8d8d;
position: absolute;
right: 0;
top: 50%;
margin-top: -2px;
margin-right: 12px;
border-radius: 1px;
}
&:not([class*='bp3-intent-']):not(.bp3-disabled) {
&,
&:hover {
background: #fff;
}
}
.bp3-intent-danger & {
border-color: #db3737;
}
`;

View File

@@ -46,4 +46,5 @@ export enum DialogsName {
EstimateExpenseForm = 'estimate-expense-form', EstimateExpenseForm = 'estimate-expense-form',
ProjectInvoicingForm = 'project-invoicing-form', ProjectInvoicingForm = 'project-invoicing-form',
ProjectBillableEntriesForm = 'project-billable-entries', ProjectBillableEntriesForm = 'project-billable-entries',
InvoiceNumberSettings = 'InvoiceNumberSettings'
} }

View File

@@ -4,7 +4,7 @@ import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import * as R from 'ramda'; import * as R from 'ramda';
import { defaultTo, isEmpty, omit } from 'lodash'; import { isEmpty, omit } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@@ -31,6 +31,7 @@ import {
transformToEditForm, transformToEditForm,
defaultManualJournal, defaultManualJournal,
} from './utils'; } from './utils';
import { JournalSyncIncrementSettingsToForm } from './components';
/** /**
* Journal entries form. * Journal entries form.
@@ -40,6 +41,7 @@ function MakeJournalEntriesForm({
journalNextNumber, journalNextNumber,
journalNumberPrefix, journalNumberPrefix,
journalAutoIncrement, journalAutoIncrement,
// #withCurrentOrganization // #withCurrentOrganization
organization: { base_currency }, organization: { base_currency },
}) { }) {
@@ -69,6 +71,8 @@ function MakeJournalEntriesForm({
} }
: { : {
...defaultManualJournal, ...defaultManualJournal,
// If the auto-increment mode is enabled, take the next journal
// number from the settings.
...(journalAutoIncrement && { ...(journalAutoIncrement && {
journal_number: journalNumber, journal_number: journalNumber,
}), }),
@@ -116,7 +120,6 @@ function MakeJournalEntriesForm({
entries: R.compose(orderingLinesIndexes)(entries), entries: R.compose(orderingLinesIndexes)(entries),
publish: submitPayload.publish, publish: submitPayload.publish,
}; };
// Handle the request error. // Handle the request error.
const handleError = ({ const handleError = ({
response: { response: {
@@ -126,7 +129,6 @@ function MakeJournalEntriesForm({
transformErrors(errors, { setErrors }); transformErrors(errors, { setErrors });
setSubmitting(false); setSubmitting(false);
}; };
// Handle the request success. // Handle the request success.
const handleSuccess = (errors) => { const handleSuccess = (errors) => {
AppToaster.show({ AppToaster.show({
@@ -147,7 +149,6 @@ function MakeJournalEntriesForm({
resetForm(); resetForm();
} }
}; };
if (isNewMode) { if (isNewMode) {
createJournalMutate(form).then(handleSuccess).catch(handleError); createJournalMutate(form).then(handleSuccess).catch(handleError);
} else { } else {
@@ -179,6 +180,9 @@ function MakeJournalEntriesForm({
{/* --------- Dialogs --------- */} {/* --------- Dialogs --------- */}
<MakeJournalFormDialogs /> <MakeJournalFormDialogs />
{/* --------- Effects --------- */}
<JournalSyncIncrementSettingsToForm />
</Form> </Form>
</Formik> </Formik>
</div> </div>

View File

@@ -8,6 +8,19 @@ import { PageFormBigNumber, FormattedMessage as T } from '@/components';
import MakeJournalEntriesHeaderFields from './MakeJournalEntriesHeaderFields'; import MakeJournalEntriesHeaderFields from './MakeJournalEntriesHeaderFields';
export default function MakeJournalEntriesHeader() { export default function MakeJournalEntriesHeader() {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<MakeJournalEntriesHeaderFields />
<MakeJournalHeaderBigNumber />
</div>
);
}
/**
* Big total number of make journal header.
* @returns {React.ReactNode}
*/
function MakeJournalHeaderBigNumber() {
const { const {
values: { entries, currency_code }, values: { entries, currency_code },
} = useFormikContext(); } = useFormikContext();
@@ -17,14 +30,10 @@ export default function MakeJournalEntriesHeader() {
const total = Math.max(totalCredit, totalDebit); const total = Math.max(totalCredit, totalDebit);
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<MakeJournalEntriesHeaderFields />
<PageFormBigNumber <PageFormBigNumber
label={<T id={'amount'} />} label={<T id={'amount'} />}
amount={total} amount={total}
currencyCode={currency_code} currencyCode={currency_code}
/> />
</div>
); );
} }

View File

@@ -6,14 +6,14 @@ import {
Position, Position,
ControlGroup, ControlGroup,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FastField, ErrorMessage } from 'formik'; import { FastField, ErrorMessage, useFormikContext } from 'formik';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import * as R from 'ramda';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { import {
momentFormatter, momentFormatter,
compose,
inputIntent, inputIntent,
handleDateChange, handleDateChange,
tansformDateValue, tansformDateValue,
@@ -25,53 +25,100 @@ import {
Icon, Icon,
InputPrependButton, InputPrependButton,
CurrencySelectList, CurrencySelectList,
FormattedMessage as T FormattedMessage as T,
FInputGroup,
FFormGroup,
} from '@/components'; } from '@/components';
import { useMakeJournalFormContext } from './MakeJournalProvider'; import { useMakeJournalFormContext } from './MakeJournalProvider';
import { JournalExchangeRateInputField } from './components'; import { JournalExchangeRateInputField } from './components';
import { currenciesFieldShouldUpdate } from './utils';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import {
currenciesFieldShouldUpdate,
useObserveJournalNoSettings,
} from './utils';
/**
* Make journal entries header.
*/
function MakeJournalEntriesHeader({
// #ownProps
onJournalNumberChanged,
/**
* Journal number field of make journal form.
*/
const MakeJournalTransactionNoField = R.compose(
withDialogActions,
withSettings(({ manualJournalsSettings }) => ({
journalAutoIncrement: manualJournalsSettings?.autoIncrement,
})),
)(
({
// #withDialog // #withDialog
openDialog, openDialog,
// #withSettings // #withSettings
journalAutoIncrement, journalAutoIncrement,
journalNextNumber, }) => {
journalNumberPrefix, const { setFieldValue, values } = useFormikContext();
}) {
const { currencies } = useMakeJournalFormContext();
// Handle journal number change.
const handleJournalNumberChange = () => { const handleJournalNumberChange = () => {
openDialog('journal-number-form'); openDialog('journal-number-form');
}; };
const handleJournalNoBlur = (event) => {
// Handle journal number blur.
const handleJournalNoBlur = (form, field) => (event) => {
const newValue = event.target.value; const newValue = event.target.value;
if (field.value !== newValue && journalAutoIncrement) { if (values.journal_number !== newValue && journalAutoIncrement) {
openDialog('journal-number-form', { openDialog('journal-number-form', {
initialFormValues: { initialFormValues: {
manualTransactionNo: newValue, onceManualNumber: newValue,
incrementMode: 'manual-transaction', incrementMode: 'manual-transaction',
}, },
}); });
} }
if (!journalAutoIncrement) {
setFieldValue('journal_number', newValue);
setFieldValue('journal_number_manually', newValue);
}
}; };
useObserveJournalNoSettings(journalNumberPrefix, journalNextNumber); return (
<FFormGroup
name={'journal_number'}
label={<T id={'journal_no'} />}
labelInfo={
<>
<FieldRequiredHint />
<FieldHint />
</>
}
fill={true}
inline={true}
fastField={true}
>
<ControlGroup fill={true}>
<FInputGroup
name={'journal_number'}
fill={true}
asyncControl={true}
onBlur={handleJournalNoBlur}
fastField={true}
onChange={() => {}}
/>
<InputPrependButton
buttonProps={{
onClick: handleJournalNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: <T id={'setting_your_auto_generated_journal_number'} />,
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FFormGroup>
);
},
);
/**
* Make journal entries header.
*/
export default function MakeJournalEntriesHeader({}) {
const { currencies } = useMakeJournalFormContext();
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
@@ -106,46 +153,7 @@ function MakeJournalEntriesHeader({
</FastField> </FastField>
{/*------------ Journal number -----------*/} {/*------------ Journal number -----------*/}
<FastField name={'journal_number'}> <MakeJournalTransactionNoField />
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'journal_no'} />}
labelInfo={
<>
<FieldRequiredHint />
<FieldHint />
</>
}
className={'form-group--journal-number'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="journal_number" />}
fill={true}
inline={true}
>
<ControlGroup fill={true}>
<InputGroup
fill={true}
value={field.value}
asyncControl={true}
onBlur={handleJournalNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleJournalNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T id={'setting_your_auto_generated_journal_number'} />
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------ Reference -----------*/} {/*------------ Reference -----------*/}
<FastField name={'reference'}> <FastField name={'reference'}>
@@ -219,12 +227,3 @@ function MakeJournalEntriesHeader({
</div> </div>
); );
} }
export default compose(
withDialogActions,
withSettings(({ manualJournalsSettings }) => ({
journalAutoIncrement: manualJournalsSettings?.autoIncrement,
journalNextNumber: manualJournalsSettings?.nextNumber,
journalNumberPrefix: manualJournalsSettings?.numberPrefix,
})),
)(MakeJournalEntriesHeader);

View File

@@ -10,17 +10,21 @@ export default function MakeJournalFormDialogs() {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
// Update the form once the journal number form submit confirm. // Update the form once the journal number form submit confirm.
const handleConfirm = ({ manually, incrementNumber }) => { const handleConfirm = (settings) => {
setFieldValue('journal_number', incrementNumber || ''); // Set the invoice transaction no. that cames from dialog to the form.
setFieldValue('journal_number_manually', manually); // the `journal_number` will be empty except the increment mode is not auto.
setFieldValue('journal_number', settings.transactionNumber);
setFieldValue('journal_number_manually', '');
if (settings.incrementMode !== 'auto') {
setFieldValue('journal_number_manually', settings.transactionNumber);
}
}; };
return ( return (
<>
<JournalNumberDialog <JournalNumberDialog
dialogName={'journal-number-form'} dialogName={'journal-number-form'}
onConfirm={handleConfirm} onConfirm={handleConfirm}
/> />
</>
); );
} }

View File

@@ -1,9 +1,10 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useEffect } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Menu, MenuItem, Position, Button } from '@blueprintjs/core'; import { Menu, MenuItem, Position, Button } from '@blueprintjs/core';
import { Popover2 } from '@blueprintjs/popover2'; import { Popover2 } from '@blueprintjs/popover2';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import * as R from 'ramda';
import { import {
ExchangeRateInputGroup, ExchangeRateInputGroup,
@@ -24,6 +25,9 @@ import { CellType, Features, Align } from '@/constants';
import { useCurrentOrganization, useFeatureCan } from '@/hooks/state'; import { useCurrentOrganization, useFeatureCan } from '@/hooks/state';
import { useJournalIsForeign } from './utils'; import { useJournalIsForeign } from './utils';
import withSettings from '@/containers/Settings/withSettings';
import { transactionNumber } from '@/utils';
import { useUpdateEffect } from '@/hooks';
/** /**
* Contact header cell. * Contact header cell.
@@ -199,3 +203,37 @@ export function JournalExchangeRateInputField({ ...props }) {
/> />
); );
} }
/**
* Syncs journal auto-increment settings to form.
* @return {React.ReactNode}
*/
export const JournalSyncIncrementSettingsToForm = R.compose(
withSettings(({ manualJournalsSettings }) => ({
journalAutoIncrement: manualJournalsSettings?.autoIncrement,
journalNextNumber: manualJournalsSettings?.nextNumber,
journalNumberPrefix: manualJournalsSettings?.numberPrefix,
})),
)(({ journalAutoIncrement, journalNextNumber, journalNumberPrefix }) => {
const { setFieldValue } = useFormikContext();
useUpdateEffect(() => {
// Do not update if the journal auto-increment mode is disabled.
if (!journalAutoIncrement) return null;
setFieldValue(
'journal_number',
transactionNumber(journalNumberPrefix, journalNextNumber),
);
}, [
setFieldValue,
journalNumberPrefix,
journalNextNumber,
journalAutoIncrement,
]);
return null;
});
JournalSyncIncrementSettingsToForm.displayName =
'JournalSyncIncrementSettingsToForm';

View File

@@ -6,7 +6,6 @@ import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { sumBy, setWith, toSafeInteger, get, first } from 'lodash'; import { sumBy, setWith, toSafeInteger, get, first } from 'lodash';
import { import {
transactionNumber,
updateTableCell, updateTableCell,
repeatValue, repeatValue,
transformToForm, transformToForm,
@@ -46,7 +45,7 @@ export const defaultEntry = {
export const defaultManualJournal = { export const defaultManualJournal = {
journal_number: '', journal_number: '',
journal_number_manually: false, journal_number_manually: '',
journal_type: 'Journal', journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'), date: moment(new Date()).format('YYYY-MM-DD'),
description: '', description: '',
@@ -174,15 +173,6 @@ export const transformErrors = (resErrors, { setErrors, errors }) => {
} }
}; };
export const useObserveJournalNoSettings = (prefix, nextNumber) => {
const { setFieldValue } = useFormikContext();
React.useEffect(() => {
const journalNo = transactionNumber(prefix, nextNumber);
setFieldValue('journal_number', journalNo);
}, [setFieldValue, prefix, nextNumber]);
};
/** /**
* Detarmines entries fast field should update. * Detarmines entries fast field should update.
*/ */

View File

@@ -0,0 +1,36 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { useAuthMetadata } from '@/hooks/query';
import { Spinner } from '@blueprintjs/core';
import styled from 'styled-components';
const AuthMetaBootContext = createContext();
/**
* Boots the authentication page metadata.
*/
function AuthMetaBootProvider({ ...props }) {
const { isLoading: isAuthMetaLoading, data: authMeta } = useAuthMetadata();
const state = {
isAuthMetaLoading,
signupDisabled: authMeta?.meta?.signup_disabled,
};
if (isAuthMetaLoading) {
return (
<SpinnerRoot>
<Spinner size={30} value={null} />
</SpinnerRoot>
);
}
return <AuthMetaBootContext.Provider value={state} {...props} />;
}
const useAuthMetaBoot = () => React.useContext(AuthMetaBootContext);
export { AuthMetaBootContext, AuthMetaBootProvider, useAuthMetaBoot };
const SpinnerRoot = styled.div`
margin-top: 5rem;
`;

View File

@@ -10,12 +10,11 @@ import { Icon, FormattedMessage as T } from '@/components';
import { useIsAuthenticated } from '@/hooks/state'; import { useIsAuthenticated } from '@/hooks/state';
import '@/style/pages/Authentication/Auth.scss'; import '@/style/pages/Authentication/Auth.scss';
import { AuthMetaBootProvider } from './AuthMetaBoot';
export function Authentication() { export function Authentication() {
const to = { pathname: '/' }; const to = { pathname: '/' };
const location = useLocation();
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
const locationKey = location.pathname;
if (isAuthenticated) { if (isAuthenticated) {
return <Redirect to={to} />; return <Redirect to={to} />;
@@ -28,6 +27,20 @@ export function Authentication() {
<Icon icon="bigcapital" height={37} width={214} /> <Icon icon="bigcapital" height={37} width={214} />
</AuthLogo> </AuthLogo>
<AuthMetaBootProvider>
<AuthenticationRoutes />
</AuthMetaBootProvider>
</AuthInsider>
</AuthPage>
</BodyClassName>
);
}
function AuthenticationRoutes() {
const location = useLocation();
const locationKey = location.pathname;
return (
<TransitionGroup> <TransitionGroup>
<CSSTransition <CSSTransition
timeout={500} timeout={500}
@@ -46,9 +59,6 @@ export function Authentication() {
</Switch> </Switch>
</CSSTransition> </CSSTransition>
</TransitionGroup> </TransitionGroup>
</AuthInsider>
</AuthPage>
</BodyClassName>
); );
} }

View File

@@ -14,11 +14,12 @@ import {
AuthFooterLink, AuthFooterLink,
AuthInsiderCard, AuthInsiderCard,
} from './_components'; } from './_components';
import { useAuthMetaBoot } from './AuthMetaBoot';
const initialValues = { const initialValues = {
crediential: '', crediential: '',
password: '', password: '',
keepLoggedIn: false keepLoggedIn: false,
}; };
/** /**
@@ -64,12 +65,15 @@ export default function Login() {
} }
function LoginFooterLinks() { function LoginFooterLinks() {
const { signupDisabled } = useAuthMetaBoot();
return ( return (
<AuthFooterLinks> <AuthFooterLinks>
{!signupDisabled && (
<AuthFooterLink> <AuthFooterLink>
Don't have an account? <Link to={'/auth/register'}>Sign up</Link> Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
</AuthFooterLink> </AuthFooterLink>
)}
<AuthFooterLink> <AuthFooterLink>
<Link to={'/auth/send_reset_password'}> <Link to={'/auth/send_reset_password'}>
<T id={'forget_my_password'} /> <T id={'forget_my_password'} />

View File

@@ -10,7 +10,7 @@ import AuthInsider from '@/containers/Authentication/AuthInsider';
import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication'; import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication';
import RegisterForm from './RegisterForm'; import RegisterForm from './RegisterForm';
import { RegisterSchema, transformRegisterErrorsToForm } from './utils'; import { RegisterSchema, transformRegisterErrorsToForm, transformRegisterToastMessages } from './utils';
import { import {
AuthFooterLinks, AuthFooterLinks,
AuthFooterLink, AuthFooterLink,
@@ -57,7 +57,11 @@ export default function RegisterUserForm() {
}, },
}) => { }) => {
const formErrors = transformRegisterErrorsToForm(errors); const formErrors = transformRegisterErrorsToForm(errors);
const toastMessages = transformRegisterToastMessages(errors);
toastMessages.forEach((toastMessage) => {
AppToaster.show(toastMessage);
});
setErrors(formErrors); setErrors(formErrors);
setSubmitting(false); setSubmitting(false);
}, },

View File

@@ -16,6 +16,7 @@ import {
} from './_components'; } from './_components';
import ResetPasswordForm from './ResetPasswordForm'; import ResetPasswordForm from './ResetPasswordForm';
import { ResetPasswordSchema } from './utils'; import { ResetPasswordSchema } from './utils';
import { useAuthMetaBoot } from './AuthMetaBoot';
const initialValues = { const initialValues = {
password: '', password: '',
@@ -79,12 +80,15 @@ export default function ResetPassword() {
} }
function ResetPasswordFooterLinks() { function ResetPasswordFooterLinks() {
const { signupDisabled } = useAuthMetaBoot();
return ( return (
<AuthFooterLinks> <AuthFooterLinks>
{!signupDisabled && (
<AuthFooterLink> <AuthFooterLink>
Don't have an account? <Link to={'/auth/register'}>Sign up</Link> Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
</AuthFooterLink> </AuthFooterLink>
)}
<AuthFooterLink> <AuthFooterLink>
Return to <Link to={'/auth/login'}>Sign In</Link> Return to <Link to={'/auth/login'}>Sign In</Link>
</AuthFooterLink> </AuthFooterLink>

View File

@@ -19,6 +19,7 @@ import {
transformSendResetPassErrorsToToasts, transformSendResetPassErrorsToToasts,
} from './utils'; } from './utils';
import AuthInsider from '@/containers/Authentication/AuthInsider'; import AuthInsider from '@/containers/Authentication/AuthInsider';
import { useAuthMetaBoot } from './AuthMetaBoot';
const initialValues = { const initialValues = {
crediential: '', crediential: '',
@@ -27,7 +28,7 @@ const initialValues = {
/** /**
* Send reset password page. * Send reset password page.
*/ */
export default function SendResetPassword({ requestSendResetPassword }) { export default function SendResetPassword() {
const history = useHistory(); const history = useHistory();
const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword(); const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword();
@@ -75,12 +76,15 @@ export default function SendResetPassword({ requestSendResetPassword }) {
} }
function SendResetPasswordFooterLinks() { function SendResetPasswordFooterLinks() {
const { signupDisabled } = useAuthMetaBoot();
return ( return (
<AuthFooterLinks> <AuthFooterLinks>
{!signupDisabled && (
<AuthFooterLink> <AuthFooterLink>
Don't have an account? <Link to={'/auth/register'}>Sign up</Link> Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
</AuthFooterLink> </AuthFooterLink>
)}
<AuthFooterLink> <AuthFooterLink>
Return to <Link to={'/auth/login'}>Sign In</Link> Return to <Link to={'/auth/login'}>Sign In</Link>
</AuthFooterLink> </AuthFooterLink>

View File

@@ -94,3 +94,21 @@ export const transformRegisterErrorsToForm = (errors) => {
} }
return formErrors; return formErrors;
}; };
export const transformRegisterToastMessages = (errors) => {
const toastErrors = [];
if (errors.some((e) => e.type === 'SIGNUP_RESTRICTED_NOT_ALLOWED')) {
toastErrors.push({
message:
'The sign-up is restricted, the given email address is not allowed to sign-up.',
intent: Intent.DANGER,
});
} else if (errors.find((e) => e.type === 'SIGNUP_RESTRICTED')) {
toastErrors.push({
message: 'Sign-up is disabled, and no new accounts can be created.',
intent: Intent.DANGER,
});
}
return toastErrors;
};

View File

@@ -1,9 +1,15 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import OwnerContributionFormFields from './OwnerContribution/OwnerContributionFormFields'; import OwnerContributionFormFields from './OwnerContribution/OwnerContributionFormFields';
import OtherIncomeFormFields from './OtherIncome/OtherIncomeFormFields'; import OtherIncomeFormFields from './OtherIncome/OtherIncomeFormFields';
import TransferFromAccountFormFields from './TransferFromAccount/TransferFromAccountFormFields'; import TransferFromAccountFormFields from './TransferFromAccount/TransferFromAccountFormFields';
/**
*
* @param param0
* @returns
*/
export default function MoneyInContentFields({ accountType }) { export default function MoneyInContentFields({ accountType }) {
const handleTransactionType = () => { const handleTransactionType = () => {
switch (accountType) { switch (accountType) {
@@ -19,6 +25,5 @@ export default function MoneyInContentFields({ accountType }) {
break; break;
} }
}; };
return <React.Fragment>{handleTransactionType()}</React.Fragment>; return <React.Fragment>{handleTransactionType()}</React.Fragment>;
} }

View File

@@ -5,6 +5,7 @@ import { Form } from 'formik';
import MoneyInFormFields from './MoneyInFormFields'; import MoneyInFormFields from './MoneyInFormFields';
import MoneyInFormDialog from './MoneyInFormDialog'; import MoneyInFormDialog from './MoneyInFormDialog';
import MoneyInFloatingActions from './MoneyInFloatingActions'; import MoneyInFloatingActions from './MoneyInFloatingActions';
import { MoneyInOutSyncIncrementSettingsToForm } from '../_components';
/** /**
* Money In form content. * Money In form content.
@@ -15,6 +16,7 @@ export default function MoneyInFormContent() {
<MoneyInFormFields /> <MoneyInFormFields />
<MoneyInFormDialog /> <MoneyInFormDialog />
<MoneyInFloatingActions /> <MoneyInFloatingActions />
<MoneyInOutSyncIncrementSettingsToForm />
</Form> </Form>
); );
} }

View File

@@ -11,12 +11,9 @@ export default function MoneyInFormDialog() {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
// Update the form once the transaction number form submit confirm. // Update the form once the transaction number form submit confirm.
const handleTransactionNumberFormConfirm = ({ const handleTransactionNumberFormConfirm = (settings) => {
incrementNumber, setFieldValue('transaction_number', settings.transactionNumber);
manually, setFieldValue('transaction_number_manually', settings.transactionNumber);
}) => {
setFieldValue('transaction_number', incrementNumber || '');
setFieldValue('transaction_number_manually', manually);
}; };
return ( return (
<React.Fragment> <React.Fragment>

View File

@@ -16,14 +16,12 @@ import {
InputPrependText, InputPrependText,
MoneyInputGroup, MoneyInputGroup,
FieldRequiredHint, FieldRequiredHint,
Icon,
Col, Col,
Row, Row,
If, If,
FeatureCan, FeatureCan,
BranchSelect, BranchSelect,
BranchSelectButton, BranchSelectButton,
InputPrependButton,
ExchangeRateMutedField, ExchangeRateMutedField,
} from '@/components'; } from '@/components';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
@@ -35,65 +33,26 @@ import {
momentFormatter, momentFormatter,
tansformDateValue, tansformDateValue,
handleDateChange, handleDateChange,
compose,
} from '@/utils'; } from '@/utils';
import { useMoneyInDailogContext } from '../MoneyInDialogProvider'; import { useMoneyInDailogContext } from '../MoneyInDialogProvider';
import { import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm, useSetPrimaryBranchToForm,
useForeignAccount, useForeignAccount,
BranchRowDivider, BranchRowDivider,
} from '../utils'; } from '../utils';
import withSettings from '@/containers/Settings/withSettings'; import { MoneyInOutTransactionNoField } from '../../_components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
/** /**
* Other income form fields. * Other income form fields.
*/ */
function OtherIncomeFormFields({ export default function OtherIncomeFormFields() {
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
// Money in dialog context. // Money in dialog context.
const { accounts, account, branches } = useMoneyInDailogContext(); const { accounts, account, branches } = useMoneyInDailogContext();
const { values } = useFormikContext(); const { values } = useFormikContext();
const amountFieldRef = useAutofocus(); const amountFieldRef = useAutofocus();
const isForeigAccount = useForeignAccount(); const isForeigAccount = useForeignAccount();
// Handle tranaction number changing.
const handleTransactionNumberChange = () => {
openDialog('transaction-number-form');
};
// Handle transaction no. field blur.
const handleTransactionNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && transactionAutoIncrement) {
openDialog('transaction-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Syncs transaction number settings with form.
useObserveTransactionNoSettings(
transactionNumberPrefix,
transactionNextNumber,
);
// Sets the primary branch to form. // Sets the primary branch to form.
useSetPrimaryBranchToForm(); useSetPrimaryBranchToForm();
@@ -149,42 +108,7 @@ function OtherIncomeFormFields({
</Col> </Col>
<Col xs={5}> <Col xs={5}>
{/*------------ Transaction number -----------*/} {/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}> <MoneyInOutTransactionNoField />
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="transaction_number" />}
className={'form-group--transaction_number'}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleTransactionNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleTransactionNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={
'cash_flow.setting_your_auto_generated_transaction_number'
}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</Field>
</Col> </Col>
</Row> </Row>
{/*------------ amount -----------*/} {/*------------ amount -----------*/}
@@ -298,12 +222,3 @@ function OtherIncomeFormFields({
</React.Fragment> </React.Fragment>
); );
} }
export default compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(OtherIncomeFormFields);

View File

@@ -17,11 +17,9 @@ import {
InputPrependText, InputPrependText,
MoneyInputGroup, MoneyInputGroup,
FieldRequiredHint, FieldRequiredHint,
Icon,
Col, Col,
Row, Row,
If, If,
InputPrependButton,
ExchangeRateMutedField, ExchangeRateMutedField,
BranchSelect, BranchSelect,
BranchSelectButton, BranchSelectButton,
@@ -35,31 +33,20 @@ import {
momentFormatter, momentFormatter,
tansformDateValue, tansformDateValue,
handleDateChange, handleDateChange,
compose,
} from '@/utils'; } from '@/utils';
import { useMoneyInDailogContext } from '../MoneyInDialogProvider'; import { useMoneyInDailogContext } from '../MoneyInDialogProvider';
import { import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm, useSetPrimaryBranchToForm,
useForeignAccount, useForeignAccount,
BranchRowDivider, BranchRowDivider,
} from '../../MoneyInDialog/utils'; } from '../../MoneyInDialog/utils';
import withSettings from '@/containers/Settings/withSettings'; import { MoneyInOutTransactionNoField } from '../../_components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
/** /**
/** /**
* Owner contribution form fields. * Owner contribution form fields.
*/ */
function OwnerContributionFormFields({ export default function OwnerContributionFormFields() {
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
// Money in dialog context. // Money in dialog context.
const { accounts, account, branches } = useMoneyInDailogContext(); const { accounts, account, branches } = useMoneyInDailogContext();
@@ -69,31 +56,6 @@ function OwnerContributionFormFields({
const isForeigAccount = useForeignAccount(); const isForeigAccount = useForeignAccount();
// Handle tranaction number changing.
const handleTransactionNumberChange = () => {
openDialog('transaction-number-form');
};
// Handle transaction no. field blur.
const handleTransactionNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && transactionAutoIncrement) {
openDialog('transaction-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Syncs transaction number settings with form.
useObserveTransactionNoSettings(
transactionNumberPrefix,
transactionNextNumber,
);
// Sets the primary branch to form. // Sets the primary branch to form.
useSetPrimaryBranchToForm(); useSetPrimaryBranchToForm();
@@ -148,42 +110,7 @@ function OwnerContributionFormFields({
</Col> </Col>
<Col xs={5}> <Col xs={5}>
{/*------------ Transaction number -----------*/} {/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}> <MoneyInOutTransactionNoField />
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="transaction_number" />}
className={'form-group--transaction_number'}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleTransactionNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleTransactionNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={
'cash_flow.setting_your_auto_generated_transaction_number'
}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</Field>
</Col> </Col>
</Row> </Row>
{/*------------ amount -----------*/} {/*------------ amount -----------*/}
@@ -294,12 +221,3 @@ function OwnerContributionFormFields({
</React.Fragment> </React.Fragment>
); );
} }
export default compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(OwnerContributionFormFields);

View File

@@ -17,11 +17,9 @@ import {
InputPrependText, InputPrependText,
MoneyInputGroup, MoneyInputGroup,
FieldRequiredHint, FieldRequiredHint,
Icon,
Col, Col,
Row, Row,
If, If,
InputPrependButton,
ExchangeRateMutedField, ExchangeRateMutedField,
FeatureCan, FeatureCan,
BranchSelect, BranchSelect,
@@ -35,30 +33,20 @@ import {
momentFormatter, momentFormatter,
tansformDateValue, tansformDateValue,
handleDateChange, handleDateChange,
compose,
} from '@/utils'; } from '@/utils';
import { useMoneyInDailogContext } from '../MoneyInDialogProvider'; import { useMoneyInDailogContext } from '../MoneyInDialogProvider';
import { import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm, useSetPrimaryBranchToForm,
useForeignAccount, useForeignAccount,
BranchRowDivider, BranchRowDivider,
} from '../../MoneyInDialog/utils'; } from '../../MoneyInDialog/utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import { MoneyInOutTransactionNoField } from '../../_components';
/** /**
* Transfer from account form fields. * Transfer from account form fields.
*/ */
function TransferFromAccountFormFields({ export default function TransferFromAccountFormFields() {
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
// Money in dialog context. // Money in dialog context.
const { accounts, account, branches } = useMoneyInDailogContext(); const { accounts, account, branches } = useMoneyInDailogContext();
@@ -67,33 +55,9 @@ function TransferFromAccountFormFields({
const { values } = useFormikContext(); const { values } = useFormikContext();
// Handle tranaction number changing.
const handleTransactionNumberChange = () => {
openDialog('transaction-number-form');
};
// Handle transaction no. field blur.
const handleTransactionNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && transactionAutoIncrement) {
openDialog('transaction-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Sets the primary branch to form. // Sets the primary branch to form.
useSetPrimaryBranchToForm(); useSetPrimaryBranchToForm();
// Syncs transaction number settings with form.
useObserveTransactionNoSettings(
transactionNumberPrefix,
transactionNextNumber,
);
return ( return (
<React.Fragment> <React.Fragment>
<FeatureCan feature={Features.Branches}> <FeatureCan feature={Features.Branches}>
@@ -145,42 +109,7 @@ function TransferFromAccountFormFields({
</Col> </Col>
<Col xs={5}> <Col xs={5}>
{/*------------ Transaction number -----------*/} {/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}> <MoneyInOutTransactionNoField />
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="transaction_number" />}
className={'form-group--transaction_number'}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleTransactionNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleTransactionNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={
'cash_flow.setting_your_auto_generated_transaction_number'
}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</Field>
</Col> </Col>
</Row> </Row>
{/*------------ amount -----------*/} {/*------------ amount -----------*/}
@@ -296,12 +225,3 @@ function TransferFromAccountFormFields({
</React.Fragment> </React.Fragment>
); );
} }
export default compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(TransferFromAccountFormFields);

View File

@@ -20,7 +20,6 @@ function MoneyOutContentFields({ accountType }) {
break; break;
} }
}; };
return <React.Fragment>{handleTransactionType()}</React.Fragment>; return <React.Fragment>{handleTransactionType()}</React.Fragment>;
} }

View File

@@ -3,8 +3,10 @@ import React from 'react';
import { Form } from 'formik'; import { Form } from 'formik';
import MoneyOutFormFields from './MoneyOutFormFields'; import MoneyOutFormFields from './MoneyOutFormFields';
import MoneyOutFormDialog from './MoneyOutFormDialog' import MoneyOutFormDialog from './MoneyOutFormDialog';
import MoneyOutFloatingActions from './MoneyOutFloatingActions'; import MoneyOutFloatingActions from './MoneyOutFloatingActions';
import { MoneyInOutSyncIncrementSettingsToForm } from '../_components';
/** /**
* Money out form content. * Money out form content.
*/ */
@@ -14,6 +16,7 @@ export default function MoneyOutFormContent() {
<MoneyOutFormFields /> <MoneyOutFormFields />
<MoneyOutFormDialog /> <MoneyOutFormDialog />
<MoneyOutFloatingActions /> <MoneyOutFloatingActions />
<MoneyInOutSyncIncrementSettingsToForm />
</Form> </Form>
); );
} }

View File

@@ -16,11 +16,9 @@ import {
InputPrependText, InputPrependText,
MoneyInputGroup, MoneyInputGroup,
FieldRequiredHint, FieldRequiredHint,
Icon,
Col, Col,
Row, Row,
If, If,
InputPrependButton,
FeatureCan, FeatureCan,
BranchSelect, BranchSelect,
BranchSelectButton, BranchSelectButton,
@@ -35,31 +33,21 @@ import {
momentFormatter, momentFormatter,
tansformDateValue, tansformDateValue,
handleDateChange, handleDateChange,
compose,
} from '@/utils'; } from '@/utils';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useMoneyOutDialogContext } from '../MoneyOutDialogProvider'; import { useMoneyOutDialogContext } from '../MoneyOutDialogProvider';
import { import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm, useSetPrimaryBranchToForm,
useForeignAccount, useForeignAccount,
BranchRowDivider, BranchRowDivider,
} from '../utils'; } from '../utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import { MoneyInOutTransactionNoField } from '../../_components';
/** /**
* Other expense form fields. * Other expense form fields.
*/ */
function OtherExpnseFormFields({ export default function OtherExpnseFormFields() {
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
// Money in dialog context. // Money in dialog context.
const { accounts, account, branches } = useMoneyOutDialogContext(); const { accounts, account, branches } = useMoneyOutDialogContext();
@@ -68,31 +56,6 @@ function OtherExpnseFormFields({
const amountFieldRef = useAutofocus(); const amountFieldRef = useAutofocus();
// Handle tranaction number changing.
const handleTransactionNumberChange = () => {
openDialog('transaction-number-form');
};
// Handle transaction no. field blur.
const handleTransactionNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && transactionAutoIncrement) {
openDialog('transaction-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Syncs transaction number settings with form.
useObserveTransactionNoSettings(
transactionNumberPrefix,
transactionNextNumber,
);
// Sets the primary branch to form. // Sets the primary branch to form.
useSetPrimaryBranchToForm(); useSetPrimaryBranchToForm();
@@ -147,42 +110,7 @@ function OtherExpnseFormFields({
</Col> </Col>
<Col xs={5}> <Col xs={5}>
{/*------------ Transaction number -----------*/} {/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}> <MoneyInOutTransactionNoField />
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="transaction_number" />}
className={'form-group--transaction_number'}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleTransactionNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleTransactionNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={
'cash_flow.setting_your_auto_generated_transaction_number'
}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</Field>
</Col> </Col>
</Row> </Row>
{/*------------ amount -----------*/} {/*------------ amount -----------*/}
@@ -296,12 +224,3 @@ function OtherExpnseFormFields({
</React.Fragment> </React.Fragment>
); );
} }
export default compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(OtherExpnseFormFields);

View File

@@ -17,8 +17,6 @@ import {
InputPrependText, InputPrependText,
MoneyInputGroup, MoneyInputGroup,
FieldRequiredHint, FieldRequiredHint,
InputPrependButton,
Icon,
If, If,
Col, Col,
Row, Row,
@@ -34,30 +32,19 @@ import {
momentFormatter, momentFormatter,
tansformDateValue, tansformDateValue,
handleDateChange, handleDateChange,
compose,
} from '@/utils'; } from '@/utils';
import { useMoneyOutDialogContext } from '../MoneyOutDialogProvider'; import { useMoneyOutDialogContext } from '../MoneyOutDialogProvider';
import { import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm, useSetPrimaryBranchToForm,
useForeignAccount, useForeignAccount,
BranchRowDivider, BranchRowDivider,
} from '../../MoneyOutDialog/utils'; } from '../../MoneyOutDialog/utils';
import withSettings from '@/containers/Settings/withSettings'; import { MoneyInOutTransactionNoField } from '../../_components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
/** /**
* Owner drawings form fields. * Owner drawings form fields.
*/ */
function OwnerDrawingsFormFields({ export default function OwnerDrawingsFormFields() {
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
// Money out dialog context. // Money out dialog context.
const { accounts, account, branches } = useMoneyOutDialogContext(); const { accounts, account, branches } = useMoneyOutDialogContext();
const { values } = useFormikContext(); const { values } = useFormikContext();
@@ -65,31 +52,6 @@ function OwnerDrawingsFormFields({
const amountFieldRef = useAutofocus(); const amountFieldRef = useAutofocus();
// Handle tranaction number changing.
const handleTransactionNumberChange = () => {
openDialog('transaction-number-form');
};
// Handle transaction no. field blur.
const handleTransactionNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && transactionAutoIncrement) {
openDialog('transaction-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Syncs transaction number settings with form.
useObserveTransactionNoSettings(
transactionNumberPrefix,
transactionNextNumber,
);
// Sets the primary branch to form. // Sets the primary branch to form.
useSetPrimaryBranchToForm(); useSetPrimaryBranchToForm();
@@ -144,42 +106,7 @@ function OwnerDrawingsFormFields({
</Col> </Col>
<Col xs={5}> <Col xs={5}>
{/*------------ Transaction number -----------*/} {/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}> <MoneyInOutTransactionNoField />
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="transaction_number" />}
className={'form-group--transaction_number'}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleTransactionNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleTransactionNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={
'cash_flow.setting_your_auto_generated_transaction_number'
}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</Field>
</Col> </Col>
</Row> </Row>
{/*------------ amount -----------*/} {/*------------ amount -----------*/}
@@ -291,12 +218,3 @@ function OwnerDrawingsFormFields({
</React.Fragment> </React.Fragment>
); );
} }
export default compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(OwnerDrawingsFormFields);

View File

@@ -48,19 +48,12 @@ import {
} from '../utils'; } from '../utils';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MoneyInOutTransactionNoField } from '../../_components';
/** /**
* Transfer to account form fields. * Transfer to account form fields.
*/ */
function TransferToAccountFormFields({ export default function TransferToAccountFormFields() {
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
// Money in dialog context. // Money in dialog context.
const { accounts, account, branches } = useMoneyOutDialogContext(); const { accounts, account, branches } = useMoneyOutDialogContext();
const { values } = useFormikContext(); const { values } = useFormikContext();
@@ -68,31 +61,6 @@ function TransferToAccountFormFields({
const accountRef = useAutofocus(); const accountRef = useAutofocus();
// Handle tranaction number changing.
const handleTransactionNumberChange = () => {
openDialog('transaction-number-form');
};
// Handle transaction no. field blur.
const handleTransactionNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && transactionAutoIncrement) {
openDialog('transaction-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Syncs transaction number settings with form.
useObserveTransactionNoSettings(
transactionNumberPrefix,
transactionNextNumber,
);
// Sets the primary branch to form. // Sets the primary branch to form.
useSetPrimaryBranchToForm(); useSetPrimaryBranchToForm();
@@ -147,42 +115,7 @@ function TransferToAccountFormFields({
</Col> </Col>
<Col xs={5}> <Col xs={5}>
{/*------------ Transaction number -----------*/} {/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}> <MoneyInOutTransactionNoField />
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'transaction_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="transaction_number" />}
className={'form-group--transaction_number'}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleTransactionNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleTransactionNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={
'cash_flow.setting_your_auto_generated_transaction_number'
}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</Field>
</Col> </Col>
</Row> </Row>
{/*------------ amount -----------*/} {/*------------ amount -----------*/}
@@ -298,11 +231,3 @@ function TransferToAccountFormFields({
</React.Fragment> </React.Fragment>
); );
} }
export default compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(TransferToAccountFormFields);

View File

@@ -0,0 +1,127 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { InputGroup, Position, ControlGroup } from '@blueprintjs/core';
import * as R from 'ramda';
import {
FFormGroup,
Icon,
InputPrependButton,
FormattedMessage as T,
} from '@/components';
import { useUpdateEffect } from '@/hooks';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
/**
* Syncs cashflow auto-increment settings to the form once update.
*/
export const MoneyInOutSyncIncrementSettingsToForm = R.compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(
({
// #withSettings
transactionAutoIncrement,
transactionNextNumber,
transactionNumberPrefix,
}) => {
const { setFieldValue } = useFormikContext();
useUpdateEffect(() => {
// Do not update if the invoice auto-increment is disabled.
if (!transactionAutoIncrement) return null;
const transactionNumber = transactionNumber(
transactionNumberPrefix,
transactionNextNumber,
);
setFieldValue('transaction_number', transactionNumber);
}, [setFieldValue, transactionNumberPrefix, transactionNextNumber]);
return null;
},
);
/**
* Money In/Out transaction number field.
*/
export const MoneyInOutTransactionNoField = R.compose(
withDialogActions,
withSettings(({ cashflowSetting }) => ({
transactionAutoIncrement: cashflowSetting?.autoIncrement,
transactionNextNumber: cashflowSetting?.nextNumber,
transactionNumberPrefix: cashflowSetting?.numberPrefix,
})),
)(
({
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
}) => {
const { values, setFieldValue } = useFormikContext();
// Handle tranaction number changing.
const handleTransactionNumberChange = () => {
openDialog('transaction-number-form');
};
// Handle transaction no. field blur.
const handleTransactionNoBlur = (event) => {
const newValue = event.target.value;
if (values.transaction_number !== newValue && transactionAutoIncrement) {
openDialog('transaction-number-form', {
initialFormValues: {
onceManualNumber: newValue,
incrementMode: 'manual-transaction',
},
});
}
if (!transactionAutoIncrement) {
setFieldValue('transaction_number', values.transaction_number);
setFieldValue('transaction_number_manually', values.transaction_number);
}
};
return (
<FFormGroup
name={'transaction_number'}
label={<T id={'transaction_number'} />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={values.transaction_number}
asyncControl={true}
onBlur={handleTransactionNoBlur}
/>
<InputPrependButton
buttonProps={{
onClick: handleTransactionNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={
'cash_flow.setting_your_auto_generated_transaction_number'
}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FFormGroup>
);
},
);

View File

@@ -7,8 +7,10 @@ export const mapStateToProps = (state, props) => {
}; };
export const mapDispatchToProps = (dispatch) => ({ export const mapDispatchToProps = (dispatch) => ({
openDialog: (name, payload) => dispatch({ type: t.OPEN_DIALOG, name, payload }), openDialog: (name, payload) =>
closeDialog: (name, payload) => dispatch({ type: t.CLOSE_DIALOG, name, payload }), dispatch({ type: t.OPEN_DIALOG, name, payload }),
closeDialog: (name, payload) =>
dispatch({ type: t.CLOSE_DIALOG, name, payload }),
}); });
export default connect(null, mapDispatchToProps); export default connect(null, mapDispatchToProps);

View File

@@ -1,32 +1,29 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import { Form, useFormikContext } from 'formik';
import { Form, FastField, Field, ErrorMessage, useFormikContext } from 'formik'; import { Button, Classes, FormGroup, Intent } from '@blueprintjs/core';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
Checkbox,
} from '@blueprintjs/core';
import { import {
If, If,
FieldRequiredHint, FieldRequiredHint,
Hint, Hint,
AccountsSelectList, AccountsSelect,
AccountsTypesSelect, AccountsTypesSelect,
CurrencySelect, CurrencySelect,
FormattedMessage as T, FormattedMessage as T,
FFormGroup,
FInputGroup,
FCheckbox,
FTextArea,
} from '@/components'; } from '@/components';
import withAccounts from '@/containers/Accounts/withAccounts'; import withAccounts from '@/containers/Accounts/withAccounts';
import { inputIntent, compose } from '@/utils';
import { useAutofocus } from '@/hooks';
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes'; import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
import { useAutofocus } from '@/hooks';
import { useAccountDialogContext } from './AccountDialogProvider'; import { useAccountDialogContext } from './AccountDialogProvider';
import { parentAccountShouldUpdate } from './utils'; import { parentAccountShouldUpdate } from './utils';
import { compose } from '@/utils';
/** /**
* Account form dialogs fields. * Account form dialogs fields.
@@ -36,7 +33,7 @@ function AccountFormDialogFields({
onClose, onClose,
action, action,
}) { }) {
const { values, isSubmitting } = useFormikContext(); const { values, isSubmitting, setFieldValue } = useFormikContext();
const accountNameFieldRef = useAutofocus(); const accountNameFieldRef = useAutofocus();
// Account form context. // Account form context.
@@ -46,146 +43,120 @@ function AccountFormDialogFields({
return ( return (
<Form> <Form>
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
<Field name={'account_type'}> <FFormGroup
{({ form, field: { value }, meta: { error, touched } }) => ( inline={true}
<FormGroup
label={<T id={'account_type'} />} label={<T id={'account_type'} />}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
className={classNames('form-group--account-type', Classes.FILL)} name={'account_type'}
inline={true} fastField={true}
helperText={<ErrorMessage name="account_type" />}
intent={inputIntent({ error, touched })}
> >
<AccountsTypesSelect <AccountsTypesSelect
accountsTypes={accountsTypes} name={'account_type'}
selectedTypeId={value} items={accountsTypes}
defaultSelectText={<T id={'select_account_type'} />} onItemSelect={(accountType) => {
onTypeSelected={(accountType) => { setFieldValue('account_type', accountType.key);
form.setFieldValue('account_type', accountType.key); setFieldValue('currency_code', '');
form.setFieldValue('currency_code', '');
}} }}
disabled={fieldsDisabled.accountType} disabled={fieldsDisabled.accountType}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
popoverFill={true} fastField={true}
fill={true}
/> />
</FormGroup> </FFormGroup>
)}
</Field>
<FastField name={'name'}> <FFormGroup
{({ field, meta: { error, touched } }) => ( name={'name'}
<FormGroup
label={<T id={'account_name'} />} label={<T id={'account_name'} />}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
className={'form-group--account-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />}
inline={true} inline={true}
fastField={true}
> >
<InputGroup <FInputGroup
medium={true} medium={true}
inputRef={(ref) => (accountNameFieldRef.current = ref)} inputRef={(ref) => (accountNameFieldRef.current = ref)}
{...field} name={'name'}
fastField={true}
/> />
</FormGroup> </FFormGroup>
)}
</FastField>
<FastField name={'code'}> <FFormGroup
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account_code'} />} label={<T id={'account_code'} />}
className={'form-group--account-code'} name={'code'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="code" />}
inline={true}
labelInfo={<Hint content={<T id="account_code_hint" />} />} labelInfo={<Hint content={<T id="account_code_hint" />} />}
>
<InputGroup medium={true} {...field} />
</FormGroup>
)}
</FastField>
<Field name={'subaccount'} type={'checkbox'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={' '}
className={classNames('form-group--subaccount')}
intent={inputIntent({ error, touched })}
inline={true} inline={true}
fastField={true}
> >
<Checkbox <FInputGroup medium={true} name={'code'} fastField={true} />
</FFormGroup>
<FFormGroup
label={' '}
name={'subaccount'}
inline={true}
fastField={true}
>
<FCheckbox
inline={true} inline={true}
label={<T id={'sub_account'} />} label={<T id={'sub_account'} />}
name={'subaccount'} name={'subaccount'}
{...field} fastField={true}
/> />
</FormGroup> </FFormGroup>
)}
</Field>
<FastField {values.subaccount && (
<FFormGroup
name={'parent_account_id'} name={'parent_account_id'}
shouldUpdate={parentAccountShouldUpdate} shouldUpdate={parentAccountShouldUpdate}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={<T id={'parent_account'} />} label={<T id={'parent_account'} />}
className={classNames('form-group--parent-account', Classes.FILL)}
inline={true} inline={true}
intent={inputIntent({ error, touched })} fastField={true}
helperText={<ErrorMessage name="parent_account_id" />}
> >
<AccountsSelectList <AccountsSelect
accounts={accounts} name={'parent_account_id'}
onAccountSelected={(account) => { items={accounts}
setFieldValue('parent_account_id', account.id); shouldUpdate={parentAccountShouldUpdate}
}} placeholder={<T id={'select_parent_account'} />}
defaultSelectText={<T id={'select_parent_account'} />}
selectedAccountId={value}
popoverFill={true}
filterByTypes={values.account_type} filterByTypes={values.account_type}
disabled={!values.subaccount} buttonProps={{ disabled: !values.subaccount }}
fastField={true}
fill={true}
allowCreate={true}
/> />
</FormGroup> </FFormGroup>
)} )}
</FastField>
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}> <If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
{/*------------ Currency -----------*/} {/*------------ Currency -----------*/}
<FastField name={'currency_code'}> <FFormGroup
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />} label={<T id={'currency'} />}
className={classNames('form-group--select-list', Classes.FILL)} name={'currency_code'}
inline={true} inline={true}
fastField={true}
> >
<CurrencySelect <CurrencySelect
name={'currency_code'} name={'currency_code'}
currencies={currencies} currencies={currencies}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
fastField={true}
fill={true}
/> />
</FormGroup> </FFormGroup>
)}
</FastField>
</If> </If>
<FastField name={'description'}>
{({ field, meta: { error, touched } }) => ( <FFormGroup
<FormGroup
label={<T id={'description'} />} label={<T id={'description'} />}
className={'form-group--description'} name={'description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'description'} />}
inline={true} inline={true}
fastField={true}
> >
<TextArea growVertically={true} height={280} {...field} /> <FTextArea
</FormGroup> name={'description'}
)} growVertically={true}
</FastField> height={280}
fill={true}
fastField={true}
/>
</FFormGroup>
</div> </div>
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>

Some files were not shown because too many files have changed in this diff Show More