mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-12 19:00:31 +00:00
Compare commits
46 Commits
BIG-427-fi
...
docker-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ea5c402c | ||
|
|
34b2c2c8b4 | ||
|
|
5d96fe6aa0 | ||
|
|
d2b5084b42 | ||
|
|
81fb0734d5 | ||
|
|
3639ce44e5 | ||
|
|
a7c00d60d5 | ||
|
|
932750b62d | ||
|
|
c90ffed67f | ||
|
|
e92c4486aa | ||
|
|
aaceea5338 | ||
|
|
4d54d180bc | ||
|
|
8fdd98e34d | ||
|
|
d53c5ee5e6 | ||
|
|
4082e4e2b8 | ||
|
|
0c689459cb | ||
|
|
40ef02f215 | ||
|
|
d369f0bb17 | ||
|
|
425d0293cc | ||
|
|
b621650975 | ||
|
|
40948160fe | ||
|
|
aa6b9dd295 | ||
|
|
05c2232b97 | ||
|
|
8f6325d529 | ||
|
|
0aa681043d | ||
|
|
40bddfdfeb | ||
|
|
d6e2f01d70 | ||
|
|
2344d3d34d | ||
|
|
883c5dcb41 | ||
|
|
be10b8934d | ||
|
|
ce38c71fa7 | ||
|
|
1162fbc7c3 | ||
|
|
18b9e25f2b | ||
|
|
dd26bdc482 | ||
|
|
ad3c9ebfe9 | ||
|
|
36611652da | ||
|
|
06c7ee71b4 | ||
|
|
54d3188666 | ||
|
|
1249415054 | ||
|
|
6c96c371c5 | ||
|
|
6c61a69f10 | ||
|
|
981b65349d | ||
|
|
a7d29a31c8 | ||
|
|
c1d92b74f0 | ||
|
|
6f0f47f38a | ||
|
|
83510cfa70 |
@@ -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=
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -74,4 +74,8 @@ export interface IAuthSendingResetPassword {
|
||||
export interface IAuthSendedResetPassword {
|
||||
user: ISystemUser,
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IAuthGetMetaPOJO {
|
||||
signupDisabled: boolean;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export class LedgerRevert {
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
ledgerStorage: LedgerStorageService;
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Reverts the jouranl entries.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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']],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
packages/server/src/services/Authentication/GetAuthMeta.ts
Normal file
16
packages/server/src/services/Authentication/GetAuthMeta.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -17,7 +17,7 @@ export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
|
||||
formatMoney: 'total',
|
||||
precision: 2,
|
||||
},
|
||||
basis: 'accural',
|
||||
basis: 'accrual',
|
||||
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
|
||||
formatMoney: 'total',
|
||||
precision: 2,
|
||||
},
|
||||
basis: 'accural',
|
||||
basis: 'accrual',
|
||||
noneZero: false,
|
||||
noneTransactions: true,
|
||||
onlyActive: false,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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[]}
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
17
packages/webapp/package-lock.json
generated
17
packages/webapp/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
101
packages/webapp/src/components/Accounts/AccountsSelect.tsx
Normal file
101
packages/webapp/src/components/Accounts/AccountsSelect.tsx
Normal 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);
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
14
packages/webapp/src/components/Accounts/_components.tsx
Normal file
14
packages/webapp/src/components/Accounts/_components.tsx
Normal 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;
|
||||
}
|
||||
};
|
||||
36
packages/webapp/src/components/Accounts/_hooks.ts
Normal file
36
packages/webapp/src/components/Accounts/_hooks.ts
Normal 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,
|
||||
]);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './AccountMultiSelect';
|
||||
export * from './AccountsSelect';
|
||||
export * from './AccountsMultiSelect';
|
||||
export * from './AccountsSelectList';
|
||||
export * from './AccountsSuggestField';
|
||||
export * from './AccountsTypesSelect';
|
||||
|
||||
@@ -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 })}
|
||||
|
||||
@@ -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')} />;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
58
packages/webapp/src/components/Forms/Select.tsx
Normal file
58
packages/webapp/src/components/Forms/Select.tsx
Normal 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;
|
||||
}
|
||||
`;
|
||||
@@ -46,4 +46,5 @@ export enum DialogsName {
|
||||
EstimateExpenseForm = 'estimate-expense-form',
|
||||
ProjectInvoicingForm = 'project-invoicing-form',
|
||||
ProjectBillableEntriesForm = 'project-billable-entries',
|
||||
InvoiceNumberSettings = 'InvoiceNumberSettings'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -20,7 +20,6 @@ function MoneyOutContentFields({ accountType }) {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return <React.Fragment>{handleTransactionType()}</React.Fragment>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
127
packages/webapp/src/containers/CashFlow/_components.tsx
Normal file
127
packages/webapp/src/containers/CashFlow/_components.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user