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
CONTACT_US_MAIL=support@bigcapital.ly
# App proxy
PUBLIC_PROXY_PORT=80
PUBLIC_PROXY_SSL_PORT=443
# Agendash
AGENDASH_AUTH_USER=agendash
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.
## [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
`@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.
- [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.

View File

@@ -15,14 +15,14 @@ services:
- ./data/logs/nginx/:/var/log/nginx
- ./docker/certbot/certs/:/var/certs
ports:
- "80:80"
- "443:443"
- "${PUBLIC_PROXY_PORT:-80}:80"
- "${PUBLIC_PROXY_SSL_PORT:-443}:443"
tty: true
depends_on:
- server
- webapp
webapp:
webapp:
container_name: bigcapital-webapp
image: ghcr.io/bigcapitalhq/webapp:latest
@@ -72,6 +72,11 @@ services:
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
- 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:
container_name: bigcapital-database-migration
build:

View File

@@ -34,7 +34,11 @@ ARG MAIL_HOST= \
BASE_URL= \
# 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 \
MAIL_USERNAME=$MAIL_USERNAME \
@@ -68,7 +72,11 @@ ENV MAIL_HOST=$MAIL_HOST \
# MongoDB
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
# 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.
WORKDIR /app

View File

@@ -3,7 +3,12 @@ import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
import {
AbilitySubject,
AccountAction,
IAccountDTO,
IAccountsStructureType,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes';
@@ -172,6 +177,11 @@ export default class AccountsController extends BaseController {
query('inactive_mode').optional().isBoolean().toBoolean(),
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',
columnSortBy: 'created_at',
inactiveMode: false,
structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req),
};

View File

@@ -49,6 +49,7 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.resetPassword.bind(this)),
this.handlerErrors
);
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
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.
*/
@@ -247,6 +265,30 @@ export default class AuthenticationController extends BaseController {
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);
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import dotenv from 'dotenv';
import path from 'path';
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
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.
*/

View File

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

View File

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

View File

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

View File

@@ -74,4 +74,8 @@ export interface IAuthSendingResetPassword {
export interface IAuthSendedResetPassword {
user: ISystemUser,
token: string;
}
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
}

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ export class LedgerRevert {
private tenancy: HasTenancyService;
@Inject()
ledgerStorage: LedgerStorageService;
private ledgerStorage: LedgerStorageService;
/**
* 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 { formatNumber } from 'utils';
import {
assocDepthLevelToObjectTree,
flatToNestedArray,
formatNumber,
nestedArrayToFlatten,
} from 'utils';
export class AccountTransformer extends Transformer {
/**
@@ -8,7 +13,23 @@ export class AccountTransformer extends Transformer {
* @returns {Array}
*/
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}
*/
protected formattedAmount = (account: IAccount): string => {
return formatNumber(account.amount, {
currencyCode: account.currencyCode,
return formatNumber(account.amount, { 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) => {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Find the given account or throw not found error.
const account = await Account.query().findById(accountId).throwIfNotFound();
const accountsGraph = await accountRepository.getDependencyGraph();
// Transformes the account model to POJO.
const transformed = await this.transformer.transform(
tenantId,
account,
new AccountTransformer()
new AccountTransformer(),
{ accountsGraph }
);
return this.i18nService.i18nApply(
[['accountTypeLabel'], ['accountNormalFormatted']],

View File

@@ -1,6 +1,11 @@
import { Inject, Service } from 'typedi';
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 DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { AccountTransformer } from './AccountTransform';
@@ -38,6 +43,7 @@ export class GetAccounts {
filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
@@ -53,17 +59,16 @@ export class GetAccounts {
dynamicList.buildQuery()(builder);
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,
accounts,
new AccountTransformer()
new AccountTransformer(),
{ accountsGraph, structure: filterDTO.structure }
);
// Transform accounts to nested array.
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
return {
accounts: transformedAccounts,

View File

@@ -1,8 +1,14 @@
import { Service, Inject, Container } from 'typedi';
import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces';
import {
IRegisterDTO,
ISystemUser,
IPasswordReset,
IAuthGetMetaPOJO,
} from '@/interfaces';
import { AuthSigninService } from './AuthSignin';
import { AuthSignupService } from './AuthSignup';
import { AuthSendResetPassword } from './AuthSendResetPassword';
import { GetAuthMeta } from './GetAuthMeta';
@Service()
export default class AuthenticationApplication {
@@ -15,6 +21,9 @@ export default class AuthenticationApplication {
@Inject()
private authResetPasswordService: AuthSendResetPassword;
@Inject()
private authGetMeta: GetAuthMeta;
/**
* Signin and generates JWT token.
* @throws {ServiceError}
@@ -53,4 +62,12 @@ export default class AuthenticationApplication {
public async resetPassword(token: string, password: string): Promise<void> {
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 { ServiceError } from '@/exceptions';
import {
@@ -13,6 +13,7 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import TenantsManagerService from '../Tenancy/TenantsManager';
import events from '@/subscribers/events';
import { hashPassword } from '@/utils';
import config from '@/config';
export class AuthSignupService {
@Inject()
@@ -33,6 +34,9 @@ export class AuthSignupService {
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
// Validates the signup disable restrictions.
await this.validateSignupRestrictions(signupDTO.email);
// Validates the given email uniqiness.
await this.validateEmailUniqiness(signupDTO.email);
@@ -74,4 +78,34 @@ export class AuthSignupService {
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',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_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,
ICreditNoteEditedPayload,
ICreditNoteOpenedPayload,
IRefundCreditNoteOpenedPayload,
} from '@/interfaces';
import CreditNoteGLEntries from './CreditNoteGLEntries';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class CreditNoteGLEntriesSubscriber {
@Inject()
creditNoteGLEntries: CreditNoteGLEntries;
@Inject()
tenancy: HasTenancyService;
private creditNoteGLEntries: CreditNoteGLEntries;
/**
* Attaches events with handlers.

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
formatMoney: 'total',
precision: 2,
},
basis: 'accural',
basis: 'accrual',
noneZero: false,
noneTransactions: true,
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 { ServiceError } from '@/exceptions';
import {
IManualJournalDTO,
IManualJournalEntry,
IManualJournal,
IManualJournalEntryDTO,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
@@ -286,7 +285,7 @@ export class CommandManualJournalValidators {
public validateJournalCurrencyWithAccountsCurrency = async (
tenantId: number,
manualJournalDTO: IManualJournalDTO,
baseCurrency: string,
baseCurrency: string
) => {
const { Account } = this.tenancy.models(tenantId);

View File

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

View File

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

View File

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

View File

@@ -21,13 +21,10 @@ export class ProjectBillableExpensesSubscriber {
events.saleInvoice.onCreated,
this.handleIncreaseBillableExpenses
);
bus.subscribe(
events.saleInvoice.onEdited,
this.handleDecreaseBillableExpenses
);
bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableExpenses);
bus.subscribe(
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') : '';
};
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 {
templateRender,
accumSum,
@@ -449,4 +501,7 @@ export {
dateRangeFromToCollection,
transformToMapKeyValue,
mergeObjectsBykey,
nestedArrayToFlatten,
assocDepthLevelToObjectTree,
castCommaListEnvVarToArray
};

View File

@@ -5,10 +5,10 @@ USER root
WORKDIR /app
# Install dependencies
COPY package.json ./
COPY package*.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 run bootstrap

View File

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

View File

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

View File

@@ -20,24 +20,6 @@
-->
<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.
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"
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>
</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
import React from 'react';
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.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MultiSelect
itemRenderer={(
item,
{ active, selected, handleClick, modifiers, query },
) => {
return (
<MenuItem
active={active}
icon={selected ? 'tick' : 'blank'}
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
}}
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
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 }}
fill={true}
tagRenderer={(item) => item.name}
resetOnSelect={true}
{...multiSelectProps}
itemPredicate={accountPredicate}
itemRenderer={accountRenderer}
createNewItemRenderer={maybeCreateNewItemRenderer}
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
import React, { useCallback } from 'react';
import classNames from 'classnames';
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);
};
import React from 'react';
import { FSelect } from '@/components/Forms';
export function AccountsTypesSelect({ ...props }) {
return (
<ListSelect
items={accountsTypes}
selectedItemProp={'key'}
selectedItem={selectedTypeId}
textProp={'label'}
defaultText={defaultSelectText}
onItemSelect={handleItemSelected}
itemPredicate={filterAccountTypeItems}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...restProps}
<FSelect
valueAccessor={'key'}
labelAccessor={'label'}
textAccessor={'label'}
placeholder={'Select an account...'}
{...props}
/>
);
}

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 './AccountsSelectList';
export * from './AccountsSuggestField';
export * from './AccountsTypesSelect';

View File

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

View File

@@ -2,7 +2,6 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
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
@@ -60,18 +35,13 @@ const currencySelectProps = {
export function CurrencySelect({ currencies, ...rest }) {
return (
<FSelect
{...currencySelectProps}
itemPredicate={currencyItemPredicate}
valueAccessor={'currency_code'}
textAccessor={'currency_name'}
labelAccessor={'currency_code'}
{...rest}
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 EstimatePdfPreviewDialog from '@/containers/Dialogs/EstimatePdfPreviewDialog';
import ReceiptPdfPreviewDialog from '@/containers/Dialogs/ReceiptPdfPreviewDialog';
import MoneyInDialog from '@/containers/Dialogs/MoneyInDialog';
import MoneyOutDialog from '@/containers/Dialogs/MoneyOutDialog';
import MoneyInDialog from '@/containers/CashFlow/MoneyInDialog';
import MoneyOutDialog from '@/containers/CashFlow/MoneyOutDialog';
import BadDebtDialog from '@/containers/Dialogs/BadDebtDialog';
import NotifyInvoiceViaSMSDialog from '@/containers/Dialogs/NotifyInvoiceViaSMSDialog';
import NotifyReceiptViaSMSDialog from '@/containers/Dialogs/NotifyReceiptViaSMSDialog';

View File

@@ -1,4 +1,5 @@
// @ts-nocheck
import React from 'react';
import {
FormGroup,
InputGroup,
@@ -9,8 +10,9 @@ import {
EditableText,
TextArea,
} from '@blueprintjs-formik/core';
import { Select, MultiSelect } from '@blueprintjs-formik/select';
import { MultiSelect } from '@blueprintjs-formik/select';
import { DateInput } from '@blueprintjs-formik/datetime';
import { FSelect } from './Select';
export {
FormGroup as FFormGroup,
@@ -19,7 +21,7 @@ export {
Checkbox as FCheckbox,
RadioGroup as FRadioGroup,
Switch as FSwitch,
Select as FSelect,
FSelect,
MultiSelect as FMultiSelect,
EditableText as FEditableText,
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',
ProjectInvoicingForm = 'project-invoicing-form',
ProjectBillableEntriesForm = 'project-billable-entries',
InvoiceNumberSettings = 'InvoiceNumberSettings'
}

View File

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

View File

@@ -8,6 +8,19 @@ import { PageFormBigNumber, FormattedMessage as T } from '@/components';
import MakeJournalEntriesHeaderFields from './MakeJournalEntriesHeaderFields';
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 {
values: { entries, currency_code },
} = useFormikContext();
@@ -17,14 +30,10 @@ export default function MakeJournalEntriesHeader() {
const total = Math.max(totalCredit, totalDebit);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<MakeJournalEntriesHeaderFields />
<PageFormBigNumber
label={<T id={'amount'} />}
amount={total}
currencyCode={currency_code}
/>
</div>
<PageFormBigNumber
label={<T id={'amount'} />}
amount={total}
currencyCode={currency_code}
/>
);
}

View File

@@ -6,14 +6,14 @@ import {
Position,
ControlGroup,
} from '@blueprintjs/core';
import { FastField, ErrorMessage } from 'formik';
import { FastField, ErrorMessage, useFormikContext } from 'formik';
import { DateInput } from '@blueprintjs/datetime';
import * as R from 'ramda';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import {
momentFormatter,
compose,
inputIntent,
handleDateChange,
tansformDateValue,
@@ -25,54 +25,101 @@ import {
Icon,
InputPrependButton,
CurrencySelectList,
FormattedMessage as T
FormattedMessage as T,
FInputGroup,
FFormGroup,
} from '@/components';
import { useMakeJournalFormContext } from './MakeJournalProvider';
import { JournalExchangeRateInputField } from './components';
import { currenciesFieldShouldUpdate } from './utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import {
currenciesFieldShouldUpdate,
useObserveJournalNoSettings,
} from './utils';
/**
* Journal number field of make journal form.
*/
const MakeJournalTransactionNoField = R.compose(
withDialogActions,
withSettings(({ manualJournalsSettings }) => ({
journalAutoIncrement: manualJournalsSettings?.autoIncrement,
})),
)(
({
// #withDialog
openDialog,
// #withSettings
journalAutoIncrement,
}) => {
const { setFieldValue, values } = useFormikContext();
const handleJournalNumberChange = () => {
openDialog('journal-number-form');
};
const handleJournalNoBlur = (event) => {
const newValue = event.target.value;
if (values.journal_number !== newValue && journalAutoIncrement) {
openDialog('journal-number-form', {
initialFormValues: {
onceManualNumber: newValue,
incrementMode: 'manual-transaction',
},
});
}
if (!journalAutoIncrement) {
setFieldValue('journal_number', newValue);
setFieldValue('journal_number_manually', newValue);
}
};
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.
*/
function MakeJournalEntriesHeader({
// #ownProps
onJournalNumberChanged,
// #withDialog
openDialog,
// #withSettings
journalAutoIncrement,
journalNextNumber,
journalNumberPrefix,
}) {
export default function MakeJournalEntriesHeader({}) {
const { currencies } = useMakeJournalFormContext();
// Handle journal number change.
const handleJournalNumberChange = () => {
openDialog('journal-number-form');
};
// Handle journal number blur.
const handleJournalNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && journalAutoIncrement) {
openDialog('journal-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
useObserveJournalNoSettings(journalNumberPrefix, journalNextNumber);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/*------------ Posting date -----------*/}
@@ -106,46 +153,7 @@ function MakeJournalEntriesHeader({
</FastField>
{/*------------ Journal number -----------*/}
<FastField name={'journal_number'}>
{({ 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>
<MakeJournalTransactionNoField />
{/*------------ Reference -----------*/}
<FastField name={'reference'}>
@@ -219,12 +227,3 @@ function MakeJournalEntriesHeader({
</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();
// Update the form once the journal number form submit confirm.
const handleConfirm = ({ manually, incrementNumber }) => {
setFieldValue('journal_number', incrementNumber || '');
setFieldValue('journal_number_manually', manually);
const handleConfirm = (settings) => {
// Set the invoice transaction no. that cames from dialog to the form.
// 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 (
<>
<JournalNumberDialog
dialogName={'journal-number-form'}
onConfirm={handleConfirm}
/>
</>
<JournalNumberDialog
dialogName={'journal-number-form'}
onConfirm={handleConfirm}
/>
);
}

View File

@@ -1,9 +1,10 @@
// @ts-nocheck
import React from 'react';
import React, { useEffect } from 'react';
import intl from 'react-intl-universal';
import { Menu, MenuItem, Position, Button } from '@blueprintjs/core';
import { Popover2 } from '@blueprintjs/popover2';
import { useFormikContext } from 'formik';
import * as R from 'ramda';
import {
ExchangeRateInputGroup,
@@ -24,6 +25,9 @@ import { CellType, Features, Align } from '@/constants';
import { useCurrentOrganization, useFeatureCan } from '@/hooks/state';
import { useJournalIsForeign } from './utils';
import withSettings from '@/containers/Settings/withSettings';
import { transactionNumber } from '@/utils';
import { useUpdateEffect } from '@/hooks';
/**
* 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 { sumBy, setWith, toSafeInteger, get, first } from 'lodash';
import {
transactionNumber,
updateTableCell,
repeatValue,
transformToForm,
@@ -46,7 +45,7 @@ export const defaultEntry = {
export const defaultManualJournal = {
journal_number: '',
journal_number_manually: false,
journal_number_manually: '',
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
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.
*/

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 '@/style/pages/Authentication/Auth.scss';
import { AuthMetaBootProvider } from './AuthMetaBoot';
export function Authentication() {
const to = { pathname: '/' };
const location = useLocation();
const isAuthenticated = useIsAuthenticated();
const locationKey = location.pathname;
if (isAuthenticated) {
return <Redirect to={to} />;
@@ -28,30 +27,41 @@ export function Authentication() {
<Icon icon="bigcapital" height={37} width={214} />
</AuthLogo>
<TransitionGroup>
<CSSTransition
timeout={500}
key={locationKey}
classNames="authTransition"
>
<Switch>
{authenticationRoutes.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</CSSTransition>
</TransitionGroup>
<AuthMetaBootProvider>
<AuthenticationRoutes />
</AuthMetaBootProvider>
</AuthInsider>
</AuthPage>
</BodyClassName>
);
}
function AuthenticationRoutes() {
const location = useLocation();
const locationKey = location.pathname;
return (
<TransitionGroup>
<CSSTransition
timeout={500}
key={locationKey}
classNames="authTransition"
>
<Switch>
{authenticationRoutes.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</CSSTransition>
</TransitionGroup>
);
}
const AuthPage = styled.div``;
const AuthInsider = styled.div`
width: 384px;

View File

@@ -14,11 +14,12 @@ import {
AuthFooterLink,
AuthInsiderCard,
} from './_components';
import { useAuthMetaBoot } from './AuthMetaBoot';
const initialValues = {
crediential: '',
password: '',
keepLoggedIn: false
keepLoggedIn: false,
};
/**
@@ -64,12 +65,15 @@ export default function Login() {
}
function LoginFooterLinks() {
const { signupDisabled } = useAuthMetaBoot();
return (
<AuthFooterLinks>
<AuthFooterLink>
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
</AuthFooterLink>
{!signupDisabled && (
<AuthFooterLink>
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
</AuthFooterLink>
)}
<AuthFooterLink>
<Link to={'/auth/send_reset_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 RegisterForm from './RegisterForm';
import { RegisterSchema, transformRegisterErrorsToForm } from './utils';
import { RegisterSchema, transformRegisterErrorsToForm, transformRegisterToastMessages } from './utils';
import {
AuthFooterLinks,
AuthFooterLink,
@@ -57,7 +57,11 @@ export default function RegisterUserForm() {
},
}) => {
const formErrors = transformRegisterErrorsToForm(errors);
const toastMessages = transformRegisterToastMessages(errors);
toastMessages.forEach((toastMessage) => {
AppToaster.show(toastMessage);
});
setErrors(formErrors);
setSubmitting(false);
},

View File

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

View File

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

View File

@@ -94,3 +94,21 @@ export const transformRegisterErrorsToForm = (errors) => {
}
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
import React from 'react';
import OwnerContributionFormFields from './OwnerContribution/OwnerContributionFormFields';
import OtherIncomeFormFields from './OtherIncome/OtherIncomeFormFields';
import TransferFromAccountFormFields from './TransferFromAccount/TransferFromAccountFormFields';
/**
*
* @param param0
* @returns
*/
export default function MoneyInContentFields({ accountType }) {
const handleTransactionType = () => {
switch (accountType) {
@@ -19,6 +25,5 @@ export default function MoneyInContentFields({ accountType }) {
break;
}
};
return <React.Fragment>{handleTransactionType()}</React.Fragment>;
}

View File

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

View File

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

View File

@@ -16,14 +16,12 @@ import {
InputPrependText,
MoneyInputGroup,
FieldRequiredHint,
Icon,
Col,
Row,
If,
FeatureCan,
BranchSelect,
BranchSelectButton,
InputPrependButton,
ExchangeRateMutedField,
} from '@/components';
import { DateInput } from '@blueprintjs/datetime';
@@ -35,65 +33,26 @@ import {
momentFormatter,
tansformDateValue,
handleDateChange,
compose,
} from '@/utils';
import { useMoneyInDailogContext } from '../MoneyInDialogProvider';
import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm,
useForeignAccount,
BranchRowDivider,
} from '../utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MoneyInOutTransactionNoField } from '../../_components';
/**
* Other income form fields.
*/
function OtherIncomeFormFields({
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
export default function OtherIncomeFormFields() {
// Money in dialog context.
const { accounts, account, branches } = useMoneyInDailogContext();
const { values } = useFormikContext();
const amountFieldRef = useAutofocus();
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.
useSetPrimaryBranchToForm();
@@ -149,42 +108,7 @@ function OtherIncomeFormFields({
</Col>
<Col xs={5}>
{/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}>
{({ 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>
<MoneyInOutTransactionNoField />
</Col>
</Row>
{/*------------ amount -----------*/}
@@ -298,12 +222,3 @@ function OtherIncomeFormFields({
</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,
MoneyInputGroup,
FieldRequiredHint,
Icon,
Col,
Row,
If,
InputPrependButton,
ExchangeRateMutedField,
BranchSelect,
BranchSelectButton,
@@ -35,31 +33,20 @@ import {
momentFormatter,
tansformDateValue,
handleDateChange,
compose,
} from '@/utils';
import { useMoneyInDailogContext } from '../MoneyInDialogProvider';
import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm,
useForeignAccount,
BranchRowDivider,
} from '../../MoneyInDialog/utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MoneyInOutTransactionNoField } from '../../_components';
/**
/**
* Owner contribution form fields.
*/
function OwnerContributionFormFields({
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
export default function OwnerContributionFormFields() {
// Money in dialog context.
const { accounts, account, branches } = useMoneyInDailogContext();
@@ -69,31 +56,6 @@ function OwnerContributionFormFields({
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.
useSetPrimaryBranchToForm();
@@ -148,42 +110,7 @@ function OwnerContributionFormFields({
</Col>
<Col xs={5}>
{/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}>
{({ 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>
<MoneyInOutTransactionNoField />
</Col>
</Row>
{/*------------ amount -----------*/}
@@ -294,12 +221,3 @@ function OwnerContributionFormFields({
</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,
MoneyInputGroup,
FieldRequiredHint,
Icon,
Col,
Row,
If,
InputPrependButton,
ExchangeRateMutedField,
FeatureCan,
BranchSelect,
@@ -35,30 +33,20 @@ import {
momentFormatter,
tansformDateValue,
handleDateChange,
compose,
} from '@/utils';
import { useMoneyInDailogContext } from '../MoneyInDialogProvider';
import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm,
useForeignAccount,
BranchRowDivider,
} from '../../MoneyInDialog/utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MoneyInOutTransactionNoField } from '../../_components';
/**
* Transfer from account form fields.
*/
function TransferFromAccountFormFields({
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
export default function TransferFromAccountFormFields() {
// Money in dialog context.
const { accounts, account, branches } = useMoneyInDailogContext();
@@ -67,33 +55,9 @@ function TransferFromAccountFormFields({
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.
useSetPrimaryBranchToForm();
// Syncs transaction number settings with form.
useObserveTransactionNoSettings(
transactionNumberPrefix,
transactionNextNumber,
);
return (
<React.Fragment>
<FeatureCan feature={Features.Branches}>
@@ -145,42 +109,7 @@ function TransferFromAccountFormFields({
</Col>
<Col xs={5}>
{/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}>
{({ 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>
<MoneyInOutTransactionNoField />
</Col>
</Row>
{/*------------ amount -----------*/}
@@ -296,12 +225,3 @@ function TransferFromAccountFormFields({
</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;
}
};
return <React.Fragment>{handleTransactionType()}</React.Fragment>;
}

View File

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

View File

@@ -16,11 +16,9 @@ import {
InputPrependText,
MoneyInputGroup,
FieldRequiredHint,
Icon,
Col,
Row,
If,
InputPrependButton,
FeatureCan,
BranchSelect,
BranchSelectButton,
@@ -35,31 +33,21 @@ import {
momentFormatter,
tansformDateValue,
handleDateChange,
compose,
} from '@/utils';
import { CLASSES } from '@/constants/classes';
import { useMoneyOutDialogContext } from '../MoneyOutDialogProvider';
import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm,
useForeignAccount,
BranchRowDivider,
} from '../utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MoneyInOutTransactionNoField } from '../../_components';
/**
* Other expense form fields.
*/
function OtherExpnseFormFields({
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
export default function OtherExpnseFormFields() {
// Money in dialog context.
const { accounts, account, branches } = useMoneyOutDialogContext();
@@ -68,31 +56,6 @@ function OtherExpnseFormFields({
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.
useSetPrimaryBranchToForm();
@@ -147,42 +110,7 @@ function OtherExpnseFormFields({
</Col>
<Col xs={5}>
{/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}>
{({ 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>
<MoneyInOutTransactionNoField />
</Col>
</Row>
{/*------------ amount -----------*/}
@@ -296,12 +224,3 @@ function OtherExpnseFormFields({
</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,
MoneyInputGroup,
FieldRequiredHint,
InputPrependButton,
Icon,
If,
Col,
Row,
@@ -34,30 +32,19 @@ import {
momentFormatter,
tansformDateValue,
handleDateChange,
compose,
} from '@/utils';
import { useMoneyOutDialogContext } from '../MoneyOutDialogProvider';
import {
useObserveTransactionNoSettings,
useSetPrimaryBranchToForm,
useForeignAccount,
BranchRowDivider,
} from '../../MoneyOutDialog/utils';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MoneyInOutTransactionNoField } from '../../_components';
/**
* Owner drawings form fields.
*/
function OwnerDrawingsFormFields({
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
export default function OwnerDrawingsFormFields() {
// Money out dialog context.
const { accounts, account, branches } = useMoneyOutDialogContext();
const { values } = useFormikContext();
@@ -65,31 +52,6 @@ function OwnerDrawingsFormFields({
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.
useSetPrimaryBranchToForm();
@@ -144,42 +106,7 @@ function OwnerDrawingsFormFields({
</Col>
<Col xs={5}>
{/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}>
{({ 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>
<MoneyInOutTransactionNoField />
</Col>
</Row>
{/*------------ amount -----------*/}
@@ -291,12 +218,3 @@ function OwnerDrawingsFormFields({
</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';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MoneyInOutTransactionNoField } from '../../_components';
/**
* Transfer to account form fields.
*/
function TransferToAccountFormFields({
// #withDialogActions
openDialog,
// #withSettings
transactionAutoIncrement,
transactionNumberPrefix,
transactionNextNumber,
}) {
export default function TransferToAccountFormFields() {
// Money in dialog context.
const { accounts, account, branches } = useMoneyOutDialogContext();
const { values } = useFormikContext();
@@ -68,31 +61,6 @@ function TransferToAccountFormFields({
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.
useSetPrimaryBranchToForm();
@@ -147,42 +115,7 @@ function TransferToAccountFormFields({
</Col>
<Col xs={5}>
{/*------------ Transaction number -----------*/}
<Field name={'transaction_number'}>
{({ 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>
<MoneyInOutTransactionNoField />
</Col>
</Row>
{/*------------ amount -----------*/}
@@ -298,11 +231,3 @@ function TransferToAccountFormFields({
</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) => ({
openDialog: (name, payload) => dispatch({ type: t.OPEN_DIALOG, name, payload }),
closeDialog: (name, payload) => dispatch({ type: t.CLOSE_DIALOG, name, payload }),
openDialog: (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
import React from 'react';
import classNames from 'classnames';
import { Form, FastField, Field, ErrorMessage, useFormikContext } from 'formik';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
Checkbox,
} from '@blueprintjs/core';
import { Form, useFormikContext } from 'formik';
import { Button, Classes, FormGroup, Intent } from '@blueprintjs/core';
import {
If,
FieldRequiredHint,
Hint,
AccountsSelectList,
AccountsSelect,
AccountsTypesSelect,
CurrencySelect,
FormattedMessage as T,
FFormGroup,
FInputGroup,
FCheckbox,
FTextArea,
} from '@/components';
import withAccounts from '@/containers/Accounts/withAccounts';
import { inputIntent, compose } from '@/utils';
import { useAutofocus } from '@/hooks';
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
import { useAutofocus } from '@/hooks';
import { useAccountDialogContext } from './AccountDialogProvider';
import { parentAccountShouldUpdate } from './utils';
import { compose } from '@/utils';
/**
* Account form dialogs fields.
@@ -36,7 +33,7 @@ function AccountFormDialogFields({
onClose,
action,
}) {
const { values, isSubmitting } = useFormikContext();
const { values, isSubmitting, setFieldValue } = useFormikContext();
const accountNameFieldRef = useAutofocus();
// Account form context.
@@ -46,146 +43,120 @@ function AccountFormDialogFields({
return (
<Form>
<div className={Classes.DIALOG_BODY}>
<Field name={'account_type'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account_type'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--account-type', Classes.FILL)}
inline={true}
helperText={<ErrorMessage name="account_type" />}
intent={inputIntent({ error, touched })}
>
<AccountsTypesSelect
accountsTypes={accountsTypes}
selectedTypeId={value}
defaultSelectText={<T id={'select_account_type'} />}
onTypeSelected={(accountType) => {
form.setFieldValue('account_type', accountType.key);
form.setFieldValue('currency_code', '');
}}
disabled={fieldsDisabled.accountType}
popoverProps={{ minimal: true }}
popoverFill={true}
/>
</FormGroup>
)}
</Field>
<FastField name={'name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--account-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />}
inline={true}
>
<InputGroup
medium={true}
inputRef={(ref) => (accountNameFieldRef.current = ref)}
{...field}
/>
</FormGroup>
)}
</FastField>
<FastField name={'code'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account_code'} />}
className={'form-group--account-code'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="code" />}
inline={true}
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}
>
<Checkbox
inline={true}
label={<T id={'sub_account'} />}
name={'subaccount'}
{...field}
/>
</FormGroup>
)}
</Field>
<FastField
name={'parent_account_id'}
shouldUpdate={parentAccountShouldUpdate}
<FFormGroup
inline={true}
label={<T id={'account_type'} />}
labelInfo={<FieldRequiredHint />}
name={'account_type'}
fastField={true}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={<T id={'parent_account'} />}
className={classNames('form-group--parent-account', Classes.FILL)}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="parent_account_id" />}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
setFieldValue('parent_account_id', account.id);
}}
defaultSelectText={<T id={'select_parent_account'} />}
selectedAccountId={value}
popoverFill={true}
filterByTypes={values.account_type}
disabled={!values.subaccount}
/>
</FormGroup>
)}
</FastField>
<AccountsTypesSelect
name={'account_type'}
items={accountsTypes}
onItemSelect={(accountType) => {
setFieldValue('account_type', accountType.key);
setFieldValue('currency_code', '');
}}
disabled={fieldsDisabled.accountType}
popoverProps={{ minimal: true }}
fastField={true}
fill={true}
/>
</FFormGroup>
<FFormGroup
name={'name'}
label={<T id={'account_name'} />}
labelInfo={<FieldRequiredHint />}
inline={true}
fastField={true}
>
<FInputGroup
medium={true}
inputRef={(ref) => (accountNameFieldRef.current = ref)}
name={'name'}
fastField={true}
/>
</FFormGroup>
<FFormGroup
label={<T id={'account_code'} />}
name={'code'}
labelInfo={<Hint content={<T id="account_code_hint" />} />}
inline={true}
fastField={true}
>
<FInputGroup medium={true} name={'code'} fastField={true} />
</FFormGroup>
<FFormGroup
label={' '}
name={'subaccount'}
inline={true}
fastField={true}
>
<FCheckbox
inline={true}
label={<T id={'sub_account'} />}
name={'subaccount'}
fastField={true}
/>
</FFormGroup>
{values.subaccount && (
<FFormGroup
name={'parent_account_id'}
shouldUpdate={parentAccountShouldUpdate}
label={<T id={'parent_account'} />}
inline={true}
fastField={true}
>
<AccountsSelect
name={'parent_account_id'}
items={accounts}
shouldUpdate={parentAccountShouldUpdate}
placeholder={<T id={'select_parent_account'} />}
filterByTypes={values.account_type}
buttonProps={{ disabled: !values.subaccount }}
fastField={true}
fill={true}
allowCreate={true}
/>
</FFormGroup>
)}
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
{/*------------ Currency -----------*/}
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames('form-group--select-list', Classes.FILL)}
inline={true}
>
<CurrencySelect
name={'currency_code'}
currencies={currencies}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
label={<T id={'currency'} />}
name={'currency_code'}
inline={true}
fastField={true}
>
<CurrencySelect
name={'currency_code'}
currencies={currencies}
popoverProps={{ minimal: true }}
fastField={true}
fill={true}
/>
</FFormGroup>
</If>
<FastField name={'description'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'description'} />}
inline={true}
>
<TextArea growVertically={true} height={280} {...field} />
</FormGroup>
)}
</FastField>
<FFormGroup
label={<T id={'description'} />}
name={'description'}
inline={true}
fastField={true}
>
<FTextArea
name={'description'}
growVertically={true}
height={280}
fill={true}
fastField={true}
/>
</FFormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>

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