diff --git a/packages/server/src/api/controllers/Purchases/BillsPayments.ts b/packages/server/src/api/controllers/Purchases/BillsPayments.ts index dfe46cf4d..56804b513 100644 --- a/packages/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/packages/server/src/api/controllers/Purchases/BillsPayments.ts @@ -111,6 +111,7 @@ export default class BillsPayments extends BaseController { check('vendor_id').exists().isNumeric().toInt(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), + check('amount').exists().isNumeric().toFloat(), check('payment_account_id').exists().isNumeric().toInt(), check('payment_number').optional({ nullable: true }).trim().escape(), check('payment_date').exists(), @@ -118,7 +119,7 @@ export default class BillsPayments extends BaseController { check('reference').optional().trim().escape(), check('branch_id').optional({ nullable: true }).isNumeric().toInt(), - check('entries').exists().isArray({ min: 1 }), + check('entries').exists().isArray(), check('entries.*.index').optional().isNumeric().toInt(), check('entries.*.bill_id').exists().isNumeric().toInt(), check('entries.*.payment_amount').exists().isNumeric().toFloat(), diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 5acd359e4..bdb71ce14 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -150,6 +150,7 @@ export default class PaymentReceivesController extends BaseController { check('customer_id').exists().isNumeric().toInt(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), + check('amount').exists().isNumeric().toFloat(), check('payment_date').exists(), check('reference_no').optional(), check('deposit_account_id').exists().isNumeric().toInt(), @@ -158,8 +159,7 @@ export default class PaymentReceivesController extends BaseController { check('branch_id').optional({ nullable: true }).isNumeric().toInt(), - check('entries').isArray({ min: 1 }), - + check('entries').isArray({}), check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.index').optional().isNumeric().toInt(), check('entries.*.invoice_id').exists().isNumeric().toInt(), diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index 8172ddace..b8a862343 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -237,4 +237,8 @@ module.exports = { endpoint: process.env.S3_ENDPOINT, bucket: process.env.S3_BUCKET || 'bigcapital-documents', }, + + loops: { + apiKey: process.env.LOOPS_API_KEY, + }, }; diff --git a/packages/server/src/database/seeds/core/20190423085242_seed_accounts.ts b/packages/server/src/database/seeds/core/20190423085242_seed_accounts.ts index e5e39dba9..7fcbd0e5f 100644 --- a/packages/server/src/database/seeds/core/20190423085242_seed_accounts.ts +++ b/packages/server/src/database/seeds/core/20190423085242_seed_accounts.ts @@ -12,8 +12,7 @@ export default class SeedAccounts extends TenantSeeder { 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); diff --git a/packages/server/src/database/seeds/data/accounts.js b/packages/server/src/database/seeds/data/accounts.js index a5f7182ba..8e55665bd 100644 --- a/packages/server/src/database/seeds/data/accounts.js +++ b/packages/server/src/database/seeds/data/accounts.js @@ -9,6 +9,28 @@ export const TaxPayableAccount = { predefined: 1, }; +export const UnearnedRevenueAccount = { + name: 'Unearned Revenue', + slug: 'unearned-revenue', + account_type: 'other-current-liability', + parent_account_id: null, + code: '50005', + active: true, + index: 1, + predefined: true, +}; + +export const PrepardExpenses = { + name: 'Prepaid Expenses', + slug: 'prepaid-expenses', + account_type: 'other-current-asset', + parent_account_id: null, + code: '100010', + active: true, + index: 1, + predefined: true, +}; + export default [ { name: 'Bank Account', @@ -323,4 +345,6 @@ export default [ index: 1, predefined: 0, }, + UnearnedRevenueAccount, + PrepardExpenses, ]; diff --git a/packages/server/src/interfaces/Ledger.ts b/packages/server/src/interfaces/Ledger.ts index d7045eb41..0c39eef4f 100644 --- a/packages/server/src/interfaces/Ledger.ts +++ b/packages/server/src/interfaces/Ledger.ts @@ -40,7 +40,7 @@ export interface ILedgerEntry { date: Date | string; transactionType: string; - transactionSubType: string; + transactionSubType?: string; transactionId: number; diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index 8595bed55..9e2ae276e 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -113,6 +113,7 @@ import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/ import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch'; import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude'; import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize'; +import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber'; export default () => { return new EventPublisher(); @@ -274,5 +275,8 @@ export const susbcribers = () => { // Plaid RecognizeSyncedBankTranasctions, + + // Loops + LoopsEventsSubscriber ]; }; diff --git a/packages/server/src/models/Bill.ts b/packages/server/src/models/Bill.ts index 422865cb0..e91f0ea33 100644 --- a/packages/server/src/models/Bill.ts +++ b/packages/server/src/models/Bill.ts @@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [ return notFoundBillsIds; } - static changePaymentAmount(billId, amount) { + static changePaymentAmount(billId, amount, trx) { const changeMethod = amount > 0 ? 'increment' : 'decrement'; - return this.query() + return this.query(trx) .where('id', billId) [changeMethod]('payment_amount', Math.abs(amount)); } diff --git a/packages/server/src/repositories/AccountRepository.ts b/packages/server/src/repositories/AccountRepository.ts index 8bc6bf7d1..19aaaa70f 100644 --- a/packages/server/src/repositories/AccountRepository.ts +++ b/packages/server/src/repositories/AccountRepository.ts @@ -2,7 +2,12 @@ import { Account } from 'models'; import TenantRepository from '@/repositories/TenantRepository'; import { IAccount } from '@/interfaces'; import { Knex } from 'knex'; -import { TaxPayableAccount } from '@/database/seeds/data/accounts'; +import { + PrepardExpenses, + TaxPayableAccount, + UnearnedRevenueAccount, +} from '@/database/seeds/data/accounts'; +import { TenantMetadata } from '@/system/models'; export default class AccountRepository extends TenantRepository { /** @@ -179,4 +184,67 @@ export default class AccountRepository extends TenantRepository { } return result; }; + + /** + * Finds or creates the unearned revenue. + * @param {Record} extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + public async findOrCreateUnearnedRevenue( + extraAttrs: Record = {}, + trx?: Knex.Transaction + ) { + // Retrieves the given tenant metadata. + const tenantMeta = await TenantMetadata.query().findOne({ + tenantId: this.tenantId, + }); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + let result = await this.model + .query(trx) + .findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...UnearnedRevenueAccount, + ..._extraAttrs, + }); + } + return result; + } + + /** + * Finds or creates the prepard expenses account. + * @param {Record} extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + public async findOrCreatePrepardExpenses( + extraAttrs: Record = {}, + trx?: Knex.Transaction + ) { + // Retrieves the given tenant metadata. + const tenantMeta = await TenantMetadata.query().findOne({ + tenantId: this.tenantId, + }); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + + let result = await this.model + .query(trx) + .findOne({ slug: PrepardExpenses.slug, ..._extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...PrepardExpenses, + ..._extraAttrs, + }); + } + return result; + } } diff --git a/packages/server/src/repositories/TenantRepository.ts b/packages/server/src/repositories/TenantRepository.ts index b24b9d079..ac85a82a7 100644 --- a/packages/server/src/repositories/TenantRepository.ts +++ b/packages/server/src/repositories/TenantRepository.ts @@ -4,12 +4,17 @@ import CachableRepository from './CachableRepository'; export default class TenantRepository extends CachableRepository { repositoryName: string; - + tenantId: number; + /** * Constructor method. - * @param {number} tenantId + * @param {number} tenantId */ constructor(knex, cache, i18n) { super(knex, cache, i18n); } -} \ No newline at end of file + + setTenantId(tenantId: number) { + this.tenantId = tenantId; + } +} diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts index abf6bd434..65d65a7c1 100644 --- a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts @@ -1,7 +1,7 @@ import { Inject, Service } from 'typedi'; -import { ExcludeBankTransaction } from './ExcludeBankTransaction'; import PromisePool from '@supercharge/promise-pool'; import { castArray } from 'lodash'; +import { ExcludeBankTransaction } from './ExcludeBankTransaction'; @Service() export class ExcludeBankTransactions { @@ -12,6 +12,7 @@ export class ExcludeBankTransactions { * Exclude bank transactions in bulk. * @param {number} tenantId * @param {number} bankTransactionIds + * @returns {Promise} */ public async excludeBankTransactions( tenantId: number, @@ -21,7 +22,7 @@ export class ExcludeBankTransactions { await PromisePool.withConcurrency(1) .for(_bankTransactionIds) - .process(async (bankTransactionId: number) => { + .process((bankTransactionId: number) => { return this.excludeBankTransaction.excludeBankTransaction( tenantId, bankTransactionId diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts index 840eb6259..846ea1fd8 100644 --- a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts @@ -21,7 +21,7 @@ export class UnexcludeBankTransactions { await PromisePool.withConcurrency(1) .for(_bankTransactionIds) - .process(async (bankTransactionId: number) => { + .process((bankTransactionId: number) => { return this.unexcludeBankTransaction.unexcludeBankTransaction( tenantId, bankTransactionId diff --git a/packages/server/src/services/Contacts/Customers/CustomersApplication.ts b/packages/server/src/services/Contacts/Customers/CustomersApplication.ts index 3cf222c02..ac3b1dd3b 100644 --- a/packages/server/src/services/Contacts/Customers/CustomersApplication.ts +++ b/packages/server/src/services/Contacts/Customers/CustomersApplication.ts @@ -45,9 +45,9 @@ export class CustomersApplication { /** * Creates a new customer. - * @param {number} tenantId - * @param {ICustomerNewDTO} customerDTO - * @param {ISystemUser} authorizedUser + * @param {number} tenantId + * @param {ICustomerNewDTO} customerDTO + * @param {ISystemUser} authorizedUser * @returns {Promise} */ public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => { @@ -56,9 +56,9 @@ export class CustomersApplication { /** * Edits details of the given customer. - * @param {number} tenantId - * @param {number} customerId - * @param {ICustomerEditDTO} customerDTO + * @param {number} tenantId + * @param {number} customerId + * @param {ICustomerEditDTO} customerDTO * @return {Promise} */ public editCustomer = ( @@ -75,9 +75,9 @@ export class CustomersApplication { /** * Deletes the given customer and associated transactions. - * @param {number} tenantId - * @param {number} customerId - * @param {ISystemUser} authorizedUser + * @param {number} tenantId + * @param {number} customerId + * @param {ISystemUser} authorizedUser * @returns {Promise} */ public deleteCustomer = ( @@ -94,9 +94,9 @@ export class CustomersApplication { /** * Changes the opening balance of the given customer. - * @param {number} tenantId - * @param {number} customerId - * @param {Date|string} openingBalanceEditDTO + * @param {number} tenantId + * @param {number} customerId + * @param {Date|string} openingBalanceEditDTO * @returns {Promise} */ public editOpeningBalance = ( diff --git a/packages/server/src/services/Loops/LoopsEventsSubscriber.ts b/packages/server/src/services/Loops/LoopsEventsSubscriber.ts new file mode 100644 index 000000000..33fe56e1a --- /dev/null +++ b/packages/server/src/services/Loops/LoopsEventsSubscriber.ts @@ -0,0 +1,51 @@ +import axios from 'axios'; +import config from '@/config'; +import { IAuthSignUpVerifiedEventPayload } from '@/interfaces'; +import events from '@/subscribers/events'; +import { SystemUser } from '@/system/models'; + +export class LoopsEventsSubscriber { + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.auth.signUpConfirmed, + this.triggerEventOnSignupVerified.bind(this) + ); + } + + /** + * Once the user verified sends the event to the Loops. + * @param {IAuthSignUpVerifiedEventPayload} param0 + */ + public async triggerEventOnSignupVerified({ + email, + userId, + }: IAuthSignUpVerifiedEventPayload) { + // Can't continue since the Loops the api key is not configured. + if (!config.loops.apiKey) { + return; + } + const user = await SystemUser.query().findById(userId); + + const options = { + method: 'POST', + url: 'https://app.loops.so/api/v1/events/send', + headers: { + Authorization: `Bearer ${config.loops.apiKey}`, + 'Content-Type': 'application/json', + }, + data: { + email, + userId, + firstName: user.firstName, + lastName: user.lastName, + eventName: 'USER_VERIFIED', + eventProperties: {}, + mailingLists: {}, + }, + }; + await axios(options); + } +} diff --git a/packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts b/packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts index 5b6f1451d..215d822da 100644 --- a/packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts +++ b/packages/server/src/services/Purchases/BillPayments/CommandBillPaymentDTOTransformer.ts @@ -4,6 +4,7 @@ import { omit, sumBy } from 'lodash'; import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { formatDateFields } from '@/utils'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; @Service() export class CommandBillPaymentDTOTransformer { @@ -23,11 +24,14 @@ export class CommandBillPaymentDTOTransformer { vendor: IVendor, oldBillPayment?: IBillPayment ): Promise { + const amount = + billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount'); + const initialDTO = { ...formatDateFields(omit(billPaymentDTO, ['attachments']), [ 'paymentDate', ]), - amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), + amount, currencyCode: vendor.currencyCode, exchangeRate: billPaymentDTO.exchangeRate || 1, entries: billPaymentDTO.entries, diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts index 8d0357e04..a84569ac9 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts @@ -36,7 +36,9 @@ export class PaymentReceiveDTOTransformer { paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, oldPaymentReceive?: IPaymentReceive ): Promise { - const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + const amount = + paymentReceiveDTO.amount ?? + sumBy(paymentReceiveDTO.entries, 'paymentAmount'); // Retreive the next invoice number. const autoNextNumber = @@ -54,7 +56,7 @@ export class PaymentReceiveDTOTransformer { ...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [ 'paymentDate', ]), - amount: paymentAmount, + amount, currencyCode: customer.currencyCode, ...(paymentReceiveNo ? { paymentReceiveNo } : {}), exchangeRate: paymentReceiveDTO.exchangeRate || 1, diff --git a/packages/server/src/services/Tenancy/TenancyService.ts b/packages/server/src/services/Tenancy/TenancyService.ts index 68414c827..213b2d9c9 100644 --- a/packages/server/src/services/Tenancy/TenancyService.ts +++ b/packages/server/src/services/Tenancy/TenancyService.ts @@ -77,7 +77,12 @@ export default class HasTenancyService { const knex = this.knex(tenantId); const i18n = this.i18n(tenantId); - return tenantRepositoriesLoader(knex, cache, i18n); + const repositories = tenantRepositoriesLoader(knex, cache, i18n); + + Object.values(repositories).forEach((repository) => { + repository.setTenantId(tenantId); + }); + return repositories; }); } diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 711dbce35..e90aeb309 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -40,6 +40,13 @@ export default { baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated', }, + /** + * User subscription events. + */ + subscription: { + onSubscribed: 'onOrganizationSubscribed', + }, + /** * Tenants managment service. */ diff --git a/packages/webapp/src/constants/accountTypes.tsx b/packages/webapp/src/constants/accountTypes.tsx index e3fa6b287..5cb08e13f 100644 --- a/packages/webapp/src/constants/accountTypes.tsx +++ b/packages/webapp/src/constants/accountTypes.tsx @@ -4,9 +4,9 @@ export const ACCOUNT_TYPE = { BANK: 'bank', ACCOUNTS_RECEIVABLE: 'accounts-receivable', INVENTORY: 'inventory', - OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + OTHER_CURRENT_ASSET: 'other-current-asset', FIXED_ASSET: 'fixed-asset', - NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + NON_CURRENT_ASSET: 'non-current-asset', ACCOUNTS_PAYABLE: 'accounts-payable', CREDIT_CARD: 'credit-card', diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx index 749ac981f..cd0135010 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx @@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components'; import { transformTableStateToQuery, compose } from '@/utils'; import { ManualJournalsListProvider } from './ManualJournalsListProvider'; -import ManualJournalsViewTabs from './ManualJournalsViewTabs'; import ManualJournalsDataTable from './ManualJournalsDataTable'; import ManualJournalsActionsBar from './ManualJournalActionsBar'; import withManualJournals from './withManualJournals'; @@ -29,7 +28,6 @@ function ManualJournalsTable({ - diff --git a/packages/webapp/src/containers/Accounts/AccountsChart.tsx b/packages/webapp/src/containers/Accounts/AccountsChart.tsx index 271e3260d..4be0c9c6c 100644 --- a/packages/webapp/src/containers/Accounts/AccountsChart.tsx +++ b/packages/webapp/src/containers/Accounts/AccountsChart.tsx @@ -2,15 +2,15 @@ import React, { useEffect } from 'react'; import '@/style/pages/Accounts/List.scss'; -import { DashboardPageContent, DashboardContentTable } from '@/components'; +import { DashboardPageContent, DashboardContentTable } from '@/components'; import { AccountsChartProvider } from './AccountsChartProvider'; -import AccountsViewsTabs from './AccountsViewsTabs'; import AccountsActionsBar from './AccountsActionsBar'; import AccountsDataTable from './AccountsDataTable'; import withAccounts from '@/containers/Accounts/withAccounts'; import withAccountsTableActions from './withAccountsTableActions'; + import { transformAccountsStateToQuery } from './utils'; import { compose } from '@/utils'; @@ -41,8 +41,6 @@ function AccountsChart({ - - diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index d94b67544..2228452af 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -141,7 +141,7 @@ function AccountTransactionsActionsBar({ }) .then(() => { AppToaster.show({ - message: 'The selected transactions have been unexcluded.', + message: 'The selected excluded transactions have been unexcluded.', intent: Intent.SUCCESS, }); }) @@ -207,7 +207,7 @@ function AccountTransactionsActionsBar({ onClick={handleExcludeUncategorizedBtnClick} className={Classes.MINIMAL} intent={Intent.DANGER} - disable={isExcludingLoading} + disabled={isExcludingLoading} /> )} {!isEmpty(excludedTransactionsIdsSelected) && ( @@ -217,7 +217,7 @@ function AccountTransactionsActionsBar({ onClick={handleUnexcludeUncategorizedBtnClick} className={Classes.MINIMAL} intent={Intent.DANGER} - disable={isUnexcludingLoading} + disabled={isUnexcludingLoading} /> )} diff --git a/packages/webapp/src/containers/CashFlow/withBankingActions.ts b/packages/webapp/src/containers/CashFlow/withBankingActions.ts index 3ac80122e..be394ab67 100644 --- a/packages/webapp/src/containers/CashFlow/withBankingActions.ts +++ b/packages/webapp/src/containers/CashFlow/withBankingActions.ts @@ -47,21 +47,37 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({ closeReconcileMatchingTransaction: () => dispatch(closeReconcileMatchingTransaction()), + /** + * Sets the selected uncategorized transactions. + * @param {Array} ids + */ setUncategorizedTransactionsSelected: (ids: Array) => dispatch( setUncategorizedTransactionsSelected({ transactionIds: ids, }), ), + + /** + * Resets the selected uncategorized transactions. + */ resetUncategorizedTransactionsSelected: () => dispatch(resetUncategorizedTransactionsSelected()), + /** + * Sets excluded selected transactions. + * @param {Array} ids + */ setExcludedTransactionsSelected: (ids: Array) => dispatch( setExcludedTransactionsSelected({ ids, }), ), + + /** + * Resets the excluded selected transactions + */ resetExcludedTransactionsSelected: () => dispatch(resetExcludedTransactionsSelected()), diff --git a/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx b/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx index cdf293905..3f9cddef2 100644 --- a/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx +++ b/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx @@ -6,7 +6,6 @@ import '@/style/pages/Customers/List.scss'; import { DashboardPageContent } from '@/components'; import CustomersActionsBar from './CustomersActionsBar'; -import CustomersViewsTabs from './CustomersViewsTabs'; import CustomersTable from './CustomersTable'; import { CustomersListProvider } from './CustomersListProvider'; @@ -42,7 +41,6 @@ function CustomersList({ - diff --git a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx index 65e9e69fa..c92ca6cdb 100644 --- a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx +++ b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx @@ -6,7 +6,6 @@ import '@/style/pages/Expense/List.scss'; import { DashboardPageContent } from '@/components'; import ExpenseActionsBar from './ExpenseActionsBar'; -import ExpenseViewTabs from './ExpenseViewTabs'; import ExpenseDataTable from './ExpenseDataTable'; import withExpenses from './withExpenses'; @@ -42,7 +41,6 @@ function ExpensesList({ - diff --git a/packages/webapp/src/containers/Items/ItemsList.tsx b/packages/webapp/src/containers/Items/ItemsList.tsx index 017a5302c..de6c10ea1 100644 --- a/packages/webapp/src/containers/Items/ItemsList.tsx +++ b/packages/webapp/src/containers/Items/ItemsList.tsx @@ -8,7 +8,6 @@ import { DashboardPageContent } from '@/components'; import { ItemsListProvider } from './ItemsListProvider'; import ItemsActionsBar from './ItemsActionsBar'; -import ItemsViewsTabs from './ItemsViewsTabs'; import ItemsDataTable from './ItemsDataTable'; import withItems from './withItems'; @@ -41,7 +40,6 @@ function ItemsList({ - diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx index 7e6bb795d..5afd9ffa3 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx @@ -7,7 +7,6 @@ import '@/style/pages/Bills/List.scss'; import { BillsListProvider } from './BillsListProvider'; import BillsActionsBar from './BillsActionsBar'; -import BillsViewsTabs from './BillsViewsTabs'; import BillsTable from './BillsTable'; import withBills from './withBills'; @@ -42,7 +41,6 @@ function BillsList({ - diff --git a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx index 02f3345b5..0ac4158fe 100644 --- a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx +++ b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx @@ -5,7 +5,6 @@ import '@/style/pages/VendorsCreditNote/List.scss'; import { DashboardPageContent } from '@/components'; import VendorsCreditNoteActionsBar from './VendorsCreditNoteActionsBar'; -import VendorsCreditNoteViewTabs from './VendorsCreditNoteViewTabs'; import VendorsCreditNoteDataTable from './VendorsCreditNoteDataTable'; import withVendorsCreditNotes from './withVendorsCreditNotes'; @@ -37,7 +36,6 @@ function VendorsCreditNotesList({ > - diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeDialogs.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeDialogs.tsx new file mode 100644 index 000000000..e03c01f52 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeDialogs.tsx @@ -0,0 +1,9 @@ +import { ExcessPaymentDialog } from './dialogs/PaymentMadeExcessDialog'; + +export function PaymentMadeDialogs() { + return ( + <> + + + ); +} diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.tsx index 792dd095e..72b2e033c 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeForm.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import intl from 'react-intl-universal'; import classNames from 'classnames'; -import { Formik, Form } from 'formik'; +import { Formik, Form, FormikHelpers } from 'formik'; import { Intent } from '@blueprintjs/core'; import { sumBy, defaultTo } from 'lodash'; import { useHistory } from 'react-router-dom'; @@ -14,6 +14,7 @@ import PaymentMadeFloatingActions from './PaymentMadeFloatingActions'; import PaymentMadeFooter from './PaymentMadeFooter'; import PaymentMadeFormBody from './PaymentMadeFormBody'; import PaymentMadeFormTopBar from './PaymentMadeFormTopBar'; +import { PaymentMadeDialogs } from './PaymentMadeDialogs'; import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; @@ -21,6 +22,7 @@ import { compose, orderingLinesIndexes } from '@/utils'; import withSettings from '@/containers/Settings/withSettings'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; import { EditPaymentMadeFormSchema, @@ -31,6 +33,7 @@ import { transformToEditForm, transformErrors, transformFormToRequest, + getPaymentExcessAmountFromValues, } from './utils'; /** @@ -42,6 +45,9 @@ function PaymentMadeForm({ // #withCurrentOrganization organization: { base_currency }, + + // #withDialogActions + openDialog, }) { const history = useHistory(); @@ -54,6 +60,7 @@ function PaymentMadeForm({ submitPayload, createPaymentMadeMutate, editPaymentMadeMutate, + isExcessConfirmed, } = usePaymentMadeFormContext(); // Form initial values. @@ -76,13 +83,11 @@ function PaymentMadeForm({ // Handle the form submit. const handleSubmitForm = ( values, - { setSubmitting, resetForm, setFieldError }, + { setSubmitting, resetForm, setFieldError }: FormikHelpers, ) => { setSubmitting(true); - // Total payment amount of entries. - const totalPaymentAmount = sumBy(values.entries, 'payment_amount'); - if (totalPaymentAmount <= 0) { + if (values.amount <= 0) { AppToaster.show({ message: intl.get('you_cannot_make_payment_with_zero_total_amount'), intent: Intent.DANGER, @@ -90,6 +95,16 @@ function PaymentMadeForm({ setSubmitting(false); return; } + const excessAmount = getPaymentExcessAmountFromValues(values); + + // Show the confirmation popup if the excess amount bigger than zero and + // has not been confirmed yet. + if (excessAmount > 0 && !isExcessConfirmed) { + openDialog('payment-made-excessed-payment'); + setSubmitting(false); + + return; + } // Transformes the form values to request body. const form = transformFormToRequest(values); @@ -119,11 +134,12 @@ function PaymentMadeForm({ } setSubmitting(false); }; - if (!isNewMode) { - editPaymentMadeMutate([paymentMadeId, form]).then(onSaved).catch(onError); + return editPaymentMadeMutate([paymentMadeId, form]) + .then(onSaved) + .catch(onError); } else { - createPaymentMadeMutate(form).then(onSaved).catch(onError); + return createPaymentMadeMutate(form).then(onSaved).catch(onError); } }; @@ -149,6 +165,7 @@ function PaymentMadeForm({ + @@ -163,4 +180,5 @@ export default compose( preferredPaymentAccount: parseInt(billPaymentSettings?.withdrawalAccount), })), withCurrentOrganization(), + withDialogActions, )(PaymentMadeForm); diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormFooterRight.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormFooterRight.tsx index 640901bd0..5f1de27df 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormFooterRight.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormFooterRight.tsx @@ -1,17 +1,23 @@ // @ts-nocheck import React from 'react'; import styled from 'styled-components'; +import { useFormikContext } from 'formik'; import { T, TotalLines, TotalLine, TotalLineBorderStyle, TotalLineTextStyle, + FormatNumber, } from '@/components'; -import { usePaymentMadeTotals } from './utils'; +import { usePaymentMadeExcessAmount, usePaymentMadeTotals } from './utils'; export function PaymentMadeFormFooterRight() { const { formattedSubtotal, formattedTotal } = usePaymentMadeTotals(); + const excessAmount = usePaymentMadeExcessAmount(); + const { + values: { currency_code: currencyCode }, + } = useFormikContext(); return ( @@ -25,6 +31,11 @@ export function PaymentMadeFormFooterRight() { value={formattedTotal} textStyle={TotalLineTextStyle.Bold} /> + } + textStyle={TotalLineTextStyle.Regular} + /> ); } diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.tsx index 7c9fd5c47..a82091896 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeader.tsx @@ -1,12 +1,12 @@ // @ts-nocheck -import React, { useMemo } from 'react'; +import React from 'react'; import classNames from 'classnames'; import { useFormikContext } from 'formik'; -import { sumBy } from 'lodash'; import { CLASSES } from '@/constants/classes'; import { Money, FormattedMessage as T } from '@/components'; import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields'; +import { usePaymentmadeTotalAmount } from './utils'; /** * Payment made header form. @@ -14,11 +14,10 @@ import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields'; function PaymentMadeFormHeader() { // Formik form context. const { - values: { entries, currency_code }, + values: { currency_code }, } = useFormikContext(); - // Calculate the payment amount of the entries. - const amountPaid = useMemo(() => sumBy(entries, 'payment_amount'), [entries]); + const totalAmount = usePaymentmadeTotalAmount(); return (
@@ -30,8 +29,9 @@ function PaymentMadeFormHeader() { +

- +

diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.tsx index 6a72e9968..c6b01ae9b 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import classNames from 'classnames'; +import { isEmpty, toSafeInteger } from 'lodash'; import { FormGroup, InputGroup, @@ -13,7 +14,6 @@ import { import { DateInput } from '@blueprintjs/datetime'; import { FastField, Field, useFormikContext, ErrorMessage } from 'formik'; import { FormattedMessage as T, VendorsSelect } from '@/components'; -import { toSafeInteger } from 'lodash'; import { CLASSES } from '@/constants/classes'; import { @@ -68,7 +68,7 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) { const fullAmount = safeSumBy(newEntries, 'payment_amount'); setFieldValue('entries', newEntries); - setFieldValue('full_amount', fullAmount); + setFieldValue('amount', fullAmount); }; // Handles the full-amount field blur. @@ -115,10 +115,10 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) { {/* ------------ Full amount ------------ */} - + {({ form: { - values: { currency_code }, + values: { currency_code, entries }, }, field: { value }, meta: { error, touched }, @@ -129,28 +129,30 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) { className={('form-group--full-amount', Classes.FILL)} intent={inputIntent({ error, touched })} labelInfo={} - helperText={} + helperText={} > { - setFieldValue('full_amount', value); + setFieldValue('amount', value); }} onBlurValue={onFullAmountBlur} /> - + {!isEmpty(entries) && ( + + )} )} diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx index f5d0c50d3..6a33713a7 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useState } from 'react'; import { Features } from '@/constants'; import { useFeatureCan } from '@/hooks/state'; import { @@ -71,6 +71,8 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) { const isFeatureLoading = isBranchesLoading; + const [isExcessConfirmed, setIsExcessConfirmed] = useState(false); + // Provider payload. const provider = { paymentMadeId, @@ -98,6 +100,9 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) { setSubmitPayload, setPaymentVendorId, + + isExcessConfirmed, + setIsExcessConfirmed, }; return ( diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialog.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialog.tsx new file mode 100644 index 000000000..c93c85a17 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialog.tsx @@ -0,0 +1,37 @@ +// @ts-nocheck +import React from 'react'; +import { Dialog, DialogSuspense } from '@/components'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import { compose } from '@/utils'; + +const ExcessPaymentDialogContent = React.lazy(() => + import('./PaymentMadeExcessDialogContent').then((module) => ({ + default: module.ExcessPaymentDialogContent, + })), +); + +/** + * Exess payment dialog of the payment made form. + */ +function ExcessPaymentDialogRoot({ dialogName, isOpen }) { + return ( + + + + + + ); +} + +export const ExcessPaymentDialog = compose(withDialogRedux())( + ExcessPaymentDialogRoot, +); + +ExcessPaymentDialog.displayName = 'ExcessPaymentDialog'; diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialogContent.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialogContent.tsx new file mode 100644 index 000000000..c57f67131 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialogContent.tsx @@ -0,0 +1,93 @@ +// @ts-nocheck +import * as R from 'ramda'; +import React from 'react'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { FormatNumber } from '@/components'; +import { usePaymentMadeFormContext } from '../../PaymentMadeFormProvider'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { usePaymentMadeExcessAmount } from '../../utils'; + +interface ExcessPaymentValues {} +function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) { + const { + submitForm, + values: { currency_code: currencyCode }, + } = useFormikContext(); + const { setIsExcessConfirmed } = usePaymentMadeFormContext(); + + // Handles the form submitting. + const handleSubmit = ( + values: ExcessPaymentValues, + { setSubmitting }: FormikHelpers, + ) => { + setSubmitting(true); + setIsExcessConfirmed(true); + + return submitForm().then(() => { + setSubmitting(false); + closeDialog(dialogName); + }); + }; + // Handle close button click. + const handleCloseBtn = () => { + closeDialog(dialogName); + }; + const excessAmount = usePaymentMadeExcessAmount(); + + return ( + +
+ + } + onClose={handleCloseBtn} + /> + +
+ ); +} + +export const ExcessPaymentDialogContent = R.compose(withDialogActions)( + ExcessPaymentDialogContentRoot, +); + +interface ExcessPaymentDialogContentFormProps { + excessAmount: string | number | React.ReactNode; + onClose?: () => void; +} + +function ExcessPaymentDialogContentForm({ + excessAmount, + onClose, +}: ExcessPaymentDialogContentFormProps) { + const { submitForm, isSubmitting } = useFormikContext(); + + const handleCloseBtn = () => { + onClose && onClose(); + }; + return ( + <> +
+

+ Would you like to record the excess amount of{' '} + {excessAmount} as credit payment from the vendor. +

+
+ +
+
+ + +
+
+ + ); +} diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/index.ts b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/index.ts new file mode 100644 index 000000000..dae9903b5 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/index.ts @@ -0,0 +1 @@ +export * from './PaymentMadeExcessDialog'; \ No newline at end of file diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx index fd469ce88..8b8323939 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx @@ -37,7 +37,7 @@ export const defaultPaymentMadeEntry = { // Default initial values of payment made. export const defaultPaymentMade = { - full_amount: '', + amount: '', vendor_id: '', payment_account_id: '', payment_date: moment(new Date()).format('YYYY-MM-DD'), @@ -53,10 +53,10 @@ export const defaultPaymentMade = { export const transformToEditForm = (paymentMade, paymentMadeEntries) => { const attachments = transformAttachmentsToForm(paymentMade); + const appliedAmount = safeSumBy(paymentMadeEntries, 'payment_amount'); return { ...transformToForm(paymentMade, defaultPaymentMade), - full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'), entries: [ ...paymentMadeEntries.map((paymentMadeEntry) => ({ ...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry), @@ -177,6 +177,30 @@ export const usePaymentMadeTotals = () => { }; }; +export const usePaymentmadeTotalAmount = () => { + const { + values: { amount }, + } = useFormikContext(); + + return amount; +}; + +export const usePaymentMadeAppliedAmount = () => { + const { + values: { entries }, + } = useFormikContext(); + + // Retrieves the invoice entries total. + return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]); +}; + +export const usePaymentMadeExcessAmount = () => { + const appliedAmount = usePaymentMadeAppliedAmount(); + const totalAmount = usePaymentmadeTotalAmount(); + + return Math.abs(totalAmount - appliedAmount); +}; + /** * Detarmines whether the bill has foreign customer. * @returns {boolean} @@ -191,3 +215,10 @@ export const usePaymentMadeIsForeignCustomer = () => { ); return isForeignCustomer; }; + +export const getPaymentExcessAmountFromValues = (values) => { + const appliedAmount = sumBy(values.entries, 'payment_amount'); + const totalAmount = values.amount; + + return Math.abs(totalAmount - appliedAmount); +}; diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx index f2fa51028..45427c1cc 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx @@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components'; import { PaymentMadesListProvider } from './PaymentMadesListProvider'; import PaymentMadeActionsBar from './PaymentMadeActionsBar'; import PaymentMadesTable from './PaymentMadesTable'; -import PaymentMadeViewTabs from './PaymentMadeViewTabs'; import withPaymentMades from './withPaymentMade'; import withPaymentMadeActions from './withPaymentMadeActions'; @@ -41,7 +40,6 @@ function PaymentMadeList({ - diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx index 13873aacc..360c5a6a0 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx @@ -5,7 +5,6 @@ import '@/style/pages/CreditNote/List.scss'; import { DashboardPageContent } from '@/components'; import CreditNotesActionsBar from './CreditNotesActionsBar'; -import CreditNotesViewTabs from './CreditNotesViewTabs'; import CreditNotesDataTable from './CreditNotesDataTable'; import withCreditNotes from './withCreditNotes'; @@ -36,8 +35,8 @@ function CreditNotesList({ tableStateChanged={creditNoteTableStateChanged} > + - diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx index 39c33cfc4..866871f4b 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx @@ -1,11 +1,10 @@ // @ts-nocheck import React from 'react'; -import { DashboardContentTable, DashboardPageContent } from '@/components'; +import { DashboardPageContent } from '@/components'; import '@/style/pages/SaleEstimate/List.scss'; import EstimatesActionsBar from './EstimatesActionsBar'; -import EstimatesViewTabs from './EstimatesViewTabs'; import EstimatesDataTable from './EstimatesDataTable'; import withEstimates from './withEstimates'; @@ -41,7 +40,6 @@ function EstimatesList({ - diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx index dc4577d0d..e846dc86a 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx @@ -6,7 +6,6 @@ import '@/style/pages/SaleInvoice/List.scss'; import { DashboardPageContent } from '@/components'; import { InvoicesListProvider } from './InvoicesListProvider'; -import InvoiceViewTabs from './InvoiceViewTabs'; import InvoicesDataTable from './InvoicesDataTable'; import InvoicesActionsBar from './InvoicesActionsBar'; @@ -43,7 +42,6 @@ function InvoicesList({ - diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx index a0c84bacb..be69cee4d 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React, { useMemo } from 'react'; +import React, { useMemo, useRef } from 'react'; import { sumBy, isEmpty, defaultTo } from 'lodash'; import intl from 'react-intl-universal'; import classNames from 'classnames'; @@ -21,6 +21,7 @@ import { PaymentReceiveInnerProvider } from './PaymentReceiveInnerProvider'; import withSettings from '@/containers/Settings/withSettings'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; import { EditPaymentReceiveFormSchema, @@ -36,6 +37,7 @@ import { transformFormToRequest, transformErrors, resetFormState, + getExceededAmountFromValues, } from './utils'; import { PaymentReceiveSyncIncrementSettingsToForm } from './components'; @@ -51,6 +53,9 @@ function PaymentReceiveForm({ // #withCurrentOrganization organization: { base_currency }, + + // #withDialogActions + openDialog, }) { const history = useHistory(); @@ -63,6 +68,7 @@ function PaymentReceiveForm({ submitPayload, editPaymentReceiveMutate, createPaymentReceiveMutate, + isExcessConfirmed, } = usePaymentReceiveFormContext(); // Payment receive number. @@ -94,18 +100,16 @@ function PaymentReceiveForm({ preferredDepositAccount, ], ); - // Handle form submit. const handleSubmitForm = ( values, { setSubmitting, resetForm, setFieldError }, ) => { setSubmitting(true); + const exceededAmount = getExceededAmountFromValues(values); - // Calculates the total payment amount of entries. - const totalPaymentAmount = sumBy(values.entries, 'payment_amount'); - - if (totalPaymentAmount <= 0) { + // Validates the amount should be bigger than zero. + if (values.amount <= 0) { AppToaster.show({ message: intl.get('you_cannot_make_payment_with_zero_total_amount'), intent: Intent.DANGER, @@ -113,6 +117,13 @@ function PaymentReceiveForm({ setSubmitting(false); return; } + // Show the confirm popup if the excessed amount bigger than zero and + // excess confirmation has not been confirmed yet. + if (exceededAmount > 0 && !isExcessConfirmed) { + setSubmitting(false); + openDialog('payment-received-excessed-payment'); + return; + } // Transformes the form values to request body. const form = transformFormToRequest(values); @@ -148,11 +159,11 @@ function PaymentReceiveForm({ }; if (paymentReceiveId) { - editPaymentReceiveMutate([paymentReceiveId, form]) + return editPaymentReceiveMutate([paymentReceiveId, form]) .then(onSaved) .catch(onError); } else { - createPaymentReceiveMutate(form).then(onSaved).catch(onError); + return createPaymentReceiveMutate(form).then(onSaved).catch(onError); } }; return ( @@ -202,4 +213,5 @@ export default compose( preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount, })), withCurrentOrganization(), + withDialogActions, )(PaymentReceiveForm); diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx index e60b27142..76ccb72a4 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useFormikContext } from 'formik'; import PaymentReceiveNumberDialog from '@/containers/Dialogs/PaymentReceiveNumberDialog'; +import { ExcessPaymentDialog } from './dialogs/ExcessPaymentDialog'; /** * Payment receive form dialogs. @@ -21,9 +22,12 @@ export default function PaymentReceiveFormDialogs() { }; return ( - + <> + + + ); } diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx index d35d7c6a7..2cee6c9f7 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx @@ -7,11 +7,16 @@ import { TotalLine, TotalLineBorderStyle, TotalLineTextStyle, + FormatNumber, } from '@/components'; -import { usePaymentReceiveTotals } from './utils'; +import { + usePaymentReceiveTotals, + usePaymentReceivedTotalExceededAmount, +} from './utils'; export function PaymentReceiveFormFootetRight() { const { formattedSubtotal, formattedTotal } = usePaymentReceiveTotals(); + const exceededAmount = usePaymentReceivedTotalExceededAmount(); return ( @@ -25,6 +30,11 @@ export function PaymentReceiveFormFootetRight() { value={formattedTotal} textStyle={TotalLineTextStyle.Bold} /> + } + textStyle={TotalLineTextStyle.Regular} + /> ); } diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx index d7e44fadf..c6d08f074 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx @@ -30,15 +30,9 @@ function PaymentReceiveFormHeader() { function PaymentReceiveFormBigTotal() { // Formik form context. const { - values: { currency_code, entries }, + values: { currency_code, amount }, } = useFormikContext(); - // Calculates the total payment amount from due amount. - const paymentFullAmount = useMemo( - () => sumBy(entries, 'payment_amount'), - [entries], - ); - return (
@@ -46,7 +40,7 @@ function PaymentReceiveFormBigTotal() {

- +

diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx index 8c08abd3e..c6f5a4729 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useState } from 'react'; import { Features } from '@/constants'; import { useFeatureCan } from '@/hooks/state'; import { DashboardInsider } from '@/components'; @@ -74,6 +74,8 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) { const { mutateAsync: editPaymentReceiveMutate } = useEditPaymentReceive(); const { mutateAsync: createPaymentReceiveMutate } = useCreatePaymentReceive(); + const [isExcessConfirmed, setIsExcessConfirmed] = useState(false); + // Provider payload. const provider = { paymentReceiveId, @@ -97,6 +99,9 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) { editPaymentReceiveMutate, createPaymentReceiveMutate, + + isExcessConfirmed, + setIsExcessConfirmed, }; return ( diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx index 9ed1e5503..98b9fe4e2 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx @@ -11,7 +11,7 @@ import { Button, } from '@blueprintjs/core'; import { DateInput } from '@blueprintjs/datetime'; -import { toSafeInteger } from 'lodash'; +import { isEmpty, toSafeInteger } from 'lodash'; import { FastField, Field, useFormikContext, ErrorMessage } from 'formik'; import { @@ -124,11 +124,11 @@ export default function PaymentReceiveHeaderFields() { {/* ------------ Full amount ------------ */} - + {({ form: { setFieldValue, - values: { currency_code }, + values: { currency_code, entries }, }, field: { value, onChange }, meta: { error, touched }, @@ -146,21 +146,23 @@ export default function PaymentReceiveHeaderFields() { { - setFieldValue('full_amount', value); + setFieldValue('amount', value); }} onBlurValue={onFullAmountBlur} /> - + {!isEmpty(entries) && ( + + )} )} diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialog.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialog.tsx new file mode 100644 index 000000000..44e660e76 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialog.tsx @@ -0,0 +1,37 @@ +// @ts-nocheck +import React from 'react'; +import { Dialog, DialogSuspense } from '@/components'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import { compose } from '@/utils'; + +const ExcessPaymentDialogContent = React.lazy(() => + import('./ExcessPaymentDialogContent').then((module) => ({ + default: module.ExcessPaymentDialogContent, + })), +); + +/** + * Excess payment dialog of the payment received form. + */ +function ExcessPaymentDialogRoot({ dialogName, isOpen }) { + return ( + + + + + + ); +} + +export const ExcessPaymentDialog = compose(withDialogRedux())( + ExcessPaymentDialogRoot, +); + +ExcessPaymentDialog.displayName = 'ExcessPaymentDialog'; diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialogContent.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialogContent.tsx new file mode 100644 index 000000000..868a75bdf --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialogContent.tsx @@ -0,0 +1,86 @@ +// @ts-nocheck +import * as Yup from 'yup'; +import * as R from 'ramda'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { FormatNumber } from '@/components'; +import { usePaymentReceiveFormContext } from '../../PaymentReceiveFormProvider'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { usePaymentReceivedTotalExceededAmount } from '../../utils'; + +interface ExcessPaymentValues {} + +export function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) { + const { + submitForm, + values: { currency_code: currencyCode }, + } = useFormikContext(); + const { setIsExcessConfirmed } = usePaymentReceiveFormContext(); + const exceededAmount = usePaymentReceivedTotalExceededAmount(); + + const handleSubmit = ( + values: ExcessPaymentValues, + { setSubmitting }: FormikHelpers, + ) => { + setSubmitting(true); + setIsExcessConfirmed(true); + + submitForm().then(() => { + closeDialog(dialogName); + setSubmitting(false); + }); + }; + const handleClose = () => { + closeDialog(dialogName); + }; + + return ( + +
+ + } + onClose={handleClose} + /> + +
+ ); +} + +export const ExcessPaymentDialogContent = R.compose(withDialogActions)( + ExcessPaymentDialogContentRoot, +); + +function ExcessPaymentDialogContentForm({ onClose, exceededAmount }) { + const { submitForm, isSubmitting } = useFormikContext(); + + const handleCloseBtn = () => { + onClose && onClose(); + }; + + return ( + <> +
+

+ Would you like to record the excess amount of{' '} + {exceededAmount} as credit payment from the customer. +

+
+ +
+
+ + +
+
+ + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/index.ts b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/index.ts new file mode 100644 index 000000000..9be100852 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/index.ts @@ -0,0 +1 @@ +export * from './ExcessPaymentDialog'; \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx index 71f75280a..3be7c0ea4 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx @@ -42,12 +42,12 @@ export const defaultPaymentReceive = { // Holds the payment number that entered manually only. payment_receive_no_manually: '', statement: '', - full_amount: '', + amount: '', currency_code: '', branch_id: '', exchange_rate: 1, entries: [], - attachments: [] + attachments: [], }; export const defaultRequestPaymentEntry = { @@ -249,6 +249,30 @@ export const usePaymentReceiveTotals = () => { }; }; +export const usePaymentReceivedTotalAppliedAmount = () => { + const { + values: { entries }, + } = useFormikContext(); + + // Retrieves the invoice entries total. + return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]); +}; + +export const usePaymentReceivedTotalAmount = () => { + const { + values: { amount }, + } = useFormikContext(); + + return amount; +}; + +export const usePaymentReceivedTotalExceededAmount = () => { + const totalAmount = usePaymentReceivedTotalAmount(); + const totalApplied = usePaymentReceivedTotalAppliedAmount(); + + return Math.abs(totalAmount - totalApplied); +}; + /** * Detarmines whether the payment has foreign customer. * @returns {boolean} @@ -273,3 +297,10 @@ export const resetFormState = ({ initialValues, values, resetForm }) => { }, }); }; + +export const getExceededAmountFromValues = (values) => { + const totalApplied = sumBy(values.entries, 'payment_amount'); + const totalAmount = values.amount; + + return totalAmount - totalApplied; +}; diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx index f2d8b2bb3..38a04e170 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx @@ -5,7 +5,6 @@ import '@/style/pages/PaymentReceive/List.scss'; import { DashboardPageContent } from '@/components'; import { PaymentReceivesListProvider } from './PaymentReceiptsListProvider'; -import PaymentReceiveViewTabs from './PaymentReceiveViewTabs'; import PaymentReceivesTable from './PaymentReceivesTable'; import PaymentReceiveActionsBar from './PaymentReceiveActionsBar'; @@ -41,7 +40,6 @@ function PaymentReceiveList({ - diff --git a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx index c558f6024..edc173dc0 100644 --- a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx +++ b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx @@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components'; import { VendorsListProvider } from './VendorsListProvider'; import VendorActionsBar from './VendorActionsBar'; -import VendorViewsTabs from './VendorViewsTabs'; import VendorsTable from './VendorsTable'; import withVendors from './withVendors'; @@ -42,7 +41,6 @@ function VendorsList({ - diff --git a/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx b/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx index 83a4c8935..c57fa2762 100644 --- a/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx +++ b/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { DashboardPageContent } from '@/components'; import WarehouseTransfersActionsBar from './WarehouseTransfersActionsBar'; -import WarehouseTransfersViewTabs from './WarehouseTransfersViewTabs'; import WarehouseTransfersDataTable from './WarehouseTransfersDataTable'; import withWarehouseTransfers from './withWarehouseTransfers'; import withWarehouseTransfersActions from './withWarehouseTransfersActions'; @@ -33,8 +32,8 @@ function WarehouseTransfersList({ tableStateChanged={warehouseTransferTableStateChanged} > + - diff --git a/packages/webapp/src/store/banking/banking.reducer.ts b/packages/webapp/src/store/banking/banking.reducer.ts index 7e0cca1e1..f0e0a3759 100644 --- a/packages/webapp/src/store/banking/banking.reducer.ts +++ b/packages/webapp/src/store/banking/banking.reducer.ts @@ -64,6 +64,11 @@ export const PlaidSlice = createSlice({ state.openReconcileMatchingTransaction.pending = 0; }, + /** + * Sets the selected uncategorized transactions. + * @param {StorePlaidState} state + * @param {PayloadAction<{ transactionIds: Array }>} action + */ setUncategorizedTransactionsSelected: ( state: StorePlaidState, action: PayloadAction<{ transactionIds: Array }>, @@ -71,10 +76,19 @@ export const PlaidSlice = createSlice({ state.uncategorizedTransactionsSelected = action.payload.transactionIds; }, + /** + * Resets the selected uncategorized transactions. + * @param {StorePlaidState} state + */ resetUncategorizedTransactionsSelected: (state: StorePlaidState) => { state.uncategorizedTransactionsSelected = []; }, + /** + * Sets excluded selected transactions. + * @param {StorePlaidState} state + * @param {PayloadAction<{ ids: Array }>} action + */ setExcludedTransactionsSelected: ( state: StorePlaidState, action: PayloadAction<{ ids: Array }>, @@ -82,6 +96,10 @@ export const PlaidSlice = createSlice({ state.excludedTransactionsSelected = action.payload.ids; }, + /** + * Resets the excluded selected transactions + * @param {StorePlaidState} state + */ resetExcludedTransactionsSelected: (state: StorePlaidState) => { state.excludedTransactionsSelected = []; }, diff --git a/packages/webapp/src/style/components/DataTable/DataTable.scss b/packages/webapp/src/style/components/DataTable/DataTable.scss index 2c78ebedc..54a5a2a12 100644 --- a/packages/webapp/src/style/components/DataTable/DataTable.scss +++ b/packages/webapp/src/style/components/DataTable/DataTable.scss @@ -128,18 +128,14 @@ cursor: auto; &, - &:hover { + &::before { height: 15px; width: 15px; } } - .bp4-control.bp4-checkbox { - - input:checked~.bp4-control-indicator, - input:indeterminate~.bp4-control-indicator { - border-color: #0052ff; - } + .bp4-control.bp4-checkbox input:not(:checked):not(:indeterminate) ~ .bp4-control-indicator{ + box-shadow: inset 0 0 0 1px #C5CBD3; } .skeleton { diff --git a/packages/webapp/src/style/pages/Dashboard/Dashboard.scss b/packages/webapp/src/style/pages/Dashboard/Dashboard.scss index db04b3351..da1a3249f 100644 --- a/packages/webapp/src/style/pages/Dashboard/Dashboard.scss +++ b/packages/webapp/src/style/pages/Dashboard/Dashboard.scss @@ -208,12 +208,16 @@ $dashboard-views-bar-height: 44px; } &.#{$ns}-minimal.#{$ns}-intent-danger { - color: #c23030; + color: rgb(194, 48, 48); + &:not(.bp4-disabled) &:hover, &:focus { background: rgba(219, 55, 55, 0.1); } + &.bp4-disabled{ + color: rgb(194, 48, 48, 0.6); + } } &.#{$ns}-minimal.#{$ns}-intent-success{ color: #1c6e42;