Compare commits

...

15 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
53f37f4f48 Merge pull request #546 from bigcapitalhq/remove-views-tabs
feat: Remove the views tabs bar from all tables
2024-07-25 19:21:50 +02:00
Ahmed Bouhuolia
0a7b522b87 chore: remove unused import 2024-07-25 19:21:16 +02:00
Ahmed Bouhuolia
9e6500ac79 feat: remove the views tabs bar from all tables 2024-07-25 19:17:54 +02:00
Ahmed Bouhuolia
b93cb546f4 Merge pull request #545 from bigcapitalhq/excessed-payments-as-credit
Excessed payments as credit
2024-07-25 18:57:31 +02:00
Ahmed Bouhuolia
6d17f9cbeb feat: record excessed payments as credit 2024-07-25 18:46:24 +02:00
Ahmed Bouhuolia
fe214b1b2d feat: push CHANGELOG 2024-07-17 16:53:47 +02:00
Ahmed Bouhuolia
6b6b73b77c feat: send signup event to Loops (#531)
* feat: send signup event to Loops

* feat: fix
2024-07-17 15:56:05 +02:00
Ahmed Bouhuolia
107a6f793b Merge pull request #526 from bigcapitalhq/monthly-plans
feat: upgrade the subscription plans
2024-07-14 14:21:57 +02:00
Ahmed Bouhuolia
67d155759e feat: backend the new monthly susbcription plans 2024-07-14 14:19:04 +02:00
Ahmed Bouhuolia
7e2e87256f Merge pull request #527 from bigcapitalhq/fix-sync-removed-transactions
fix: sync the removed bank transactions from the source
2024-07-13 21:56:13 +02:00
Ahmed Bouhuolia
df7790d7c1 fix: sync the removed bank transactions from the source 2024-07-13 21:54:44 +02:00
Ahmed Bouhuolia
72128a72c4 feat: add variant ids to new subscription plans 2024-07-13 19:53:52 +02:00
Ahmed Bouhuolia
eb3f23554f feat: upgrade the subscription plans 2024-07-13 18:19:18 +02:00
Ahmed Bouhuolia
69ddf43b3e fix: duplicated event emitter 2024-07-13 03:23:25 +02:00
Ahmed Bouhuolia
249eadaeaa Merge pull request #525 from bigcapitalhq/fix-plaid-transactions-syncing
fix: Plaid transactions syncing
2024-07-12 23:44:27 +02:00
71 changed files with 1244 additions and 313 deletions

View File

@@ -2,6 +2,14 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
* feat: Categorize & match bank transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
* feat: Reconcile match transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/522
* fix: Issues in matching transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/523
* fix: Cashflow transactions types by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/524
## [v0.17.5] - 17-06-2024 ## [v0.17.5] - 17-06-2024
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501 * fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501

View File

@@ -111,6 +111,7 @@ export default class BillsPayments extends BaseController {
check('vendor_id').exists().isNumeric().toInt(), check('vendor_id').exists().isNumeric().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
check('amount').exists().isNumeric().toFloat(),
check('payment_account_id').exists().isNumeric().toInt(), check('payment_account_id').exists().isNumeric().toInt(),
check('payment_number').optional({ nullable: true }).trim().escape(), check('payment_number').optional({ nullable: true }).trim().escape(),
check('payment_date').exists(), check('payment_date').exists(),
@@ -118,7 +119,7 @@ export default class BillsPayments extends BaseController {
check('reference').optional().trim().escape(), check('reference').optional().trim().escape(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(), 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.*.index').optional().isNumeric().toInt(),
check('entries.*.bill_id').exists().isNumeric().toInt(), check('entries.*.bill_id').exists().isNumeric().toInt(),
check('entries.*.payment_amount').exists().isNumeric().toFloat(), check('entries.*.payment_amount').exists().isNumeric().toFloat(),

View File

@@ -150,6 +150,7 @@ export default class PaymentReceivesController extends BaseController {
check('customer_id').exists().isNumeric().toInt(), check('customer_id').exists().isNumeric().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
check('amount').exists().isNumeric().toFloat(),
check('payment_date').exists(), check('payment_date').exists(),
check('reference_no').optional(), check('reference_no').optional(),
check('deposit_account_id').exists().isNumeric().toInt(), 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('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.*.id').optional({ nullable: true }).isNumeric().toInt(),
check('entries.*.index').optional().isNumeric().toInt(), check('entries.*.index').optional().isNumeric().toInt(),
check('entries.*.invoice_id').exists().isNumeric().toInt(), check('entries.*.invoice_id').exists().isNumeric().toInt(),

View File

@@ -237,4 +237,8 @@ module.exports = {
endpoint: process.env.S3_ENDPOINT, endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET || 'bigcapital-documents', bucket: process.env.S3_BUCKET || 'bigcapital-documents',
}, },
loops: {
apiKey: process.env.LOOPS_API_KEY,
},
}; };

View File

@@ -12,8 +12,7 @@ export default class SeedAccounts extends TenantSeeder {
description: this.i18n.__(account.description), description: this.i18n.__(account.description),
currencyCode: this.tenant.metadata.baseCurrency, currencyCode: this.tenant.metadata.baseCurrency,
seededAt: new Date(), seededAt: new Date(),
}) }));
);
return knex('accounts').then(async () => { return knex('accounts').then(async () => {
// Inserts seed entries. // Inserts seed entries.
return knex('accounts').insert(data); return knex('accounts').insert(data);

View File

@@ -9,6 +9,28 @@ export const TaxPayableAccount = {
predefined: 1, 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 [ export default [
{ {
name: 'Bank Account', name: 'Bank Account',
@@ -323,4 +345,6 @@ export default [
index: 1, index: 1,
predefined: 0, predefined: 0,
}, },
UnearnedRevenueAccount,
PrepardExpenses,
]; ];

View File

@@ -40,7 +40,7 @@ export interface ILedgerEntry {
date: Date | string; date: Date | string;
transactionType: string; transactionType: string;
transactionSubType: string; transactionSubType?: string;
transactionId: number; transactionId: number;

View File

@@ -113,6 +113,7 @@ import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch'; import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude'; import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize'; import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
export default () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -274,5 +275,8 @@ export const susbcribers = () => {
// Plaid // Plaid
RecognizeSyncedBankTranasctions, RecognizeSyncedBankTranasctions,
// Loops
LoopsEventsSubscriber
]; ];
}; };

View File

@@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [
return notFoundBillsIds; return notFoundBillsIds;
} }
static changePaymentAmount(billId, amount) { static changePaymentAmount(billId, amount, trx) {
const changeMethod = amount > 0 ? 'increment' : 'decrement'; const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.query() return this.query(trx)
.where('id', billId) .where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount)); [changeMethod]('payment_amount', Math.abs(amount));
} }

View File

@@ -2,7 +2,12 @@ import { Account } from 'models';
import TenantRepository from '@/repositories/TenantRepository'; import TenantRepository from '@/repositories/TenantRepository';
import { IAccount } from '@/interfaces'; import { IAccount } from '@/interfaces';
import { Knex } from 'knex'; 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 { export default class AccountRepository extends TenantRepository {
/** /**
@@ -179,4 +184,67 @@ export default class AccountRepository extends TenantRepository {
} }
return result; return result;
}; };
/**
* Finds or creates the unearned revenue.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateUnearnedRevenue(
extraAttrs: Record<string, string> = {},
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<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreatePrepardExpenses(
extraAttrs: Record<string, string> = {},
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;
}
} }

View File

@@ -4,12 +4,17 @@ import CachableRepository from './CachableRepository';
export default class TenantRepository extends CachableRepository { export default class TenantRepository extends CachableRepository {
repositoryName: string; repositoryName: string;
tenantId: number;
/** /**
* Constructor method. * Constructor method.
* @param {number} tenantId * @param {number} tenantId
*/ */
constructor(knex, cache, i18n) { constructor(knex, cache, i18n) {
super(knex, cache, i18n); super(knex, cache, i18n);
} }
}
setTenantId(tenantId: number) {
this.tenantId = tenantId;
}
}

View File

@@ -73,8 +73,6 @@ export class PlaidUpdateTransactions {
added.concat(modified), added.concat(modified),
trx trx
); );
// Sync removed transactions.
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
// Sync transactions cursor. // Sync transactions cursor.
await this.plaidSync.syncTransactionsCursor( await this.plaidSync.syncTransactionsCursor(
tenantId, tenantId,

View File

@@ -37,7 +37,7 @@ export class CreateUncategorizedTransaction {
tenantId, tenantId,
async (trx: Knex.Transaction) => { async (trx: Knex.Transaction) => {
await this.eventPublisher.emitAsync( await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorizedCreated, events.cashflow.onTransactionUncategorizedCreating,
{ {
tenantId, tenantId,
createUncategorizedTransactionDTO, createUncategorizedTransactionDTO,

View File

@@ -45,9 +45,9 @@ export class CustomersApplication {
/** /**
* Creates a new customer. * Creates a new customer.
* @param {number} tenantId * @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO * @param {ICustomerNewDTO} customerDTO
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @returns {Promise<ICustomer>} * @returns {Promise<ICustomer>}
*/ */
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => { public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
@@ -56,9 +56,9 @@ export class CustomersApplication {
/** /**
* Edits details of the given customer. * Edits details of the given customer.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
* @param {ICustomerEditDTO} customerDTO * @param {ICustomerEditDTO} customerDTO
* @return {Promise<ICustomer>} * @return {Promise<ICustomer>}
*/ */
public editCustomer = ( public editCustomer = (
@@ -75,9 +75,9 @@ export class CustomersApplication {
/** /**
* Deletes the given customer and associated transactions. * Deletes the given customer and associated transactions.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public deleteCustomer = ( public deleteCustomer = (
@@ -94,9 +94,9 @@ export class CustomersApplication {
/** /**
* Changes the opening balance of the given customer. * Changes the opening balance of the given customer.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
* @param {Date|string} openingBalanceEditDTO * @param {Date|string} openingBalanceEditDTO
* @returns {Promise<ICustomer>} * @returns {Promise<ICustomer>}
*/ */
public editOpeningBalance = ( public editOpeningBalance = (

View File

@@ -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);
}
}

View File

@@ -4,6 +4,7 @@ import { omit, sumBy } from 'lodash';
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces'; import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { formatDateFields } from '@/utils'; import { formatDateFields } from '@/utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export class CommandBillPaymentDTOTransformer { export class CommandBillPaymentDTOTransformer {
@@ -23,11 +24,14 @@ export class CommandBillPaymentDTOTransformer {
vendor: IVendor, vendor: IVendor,
oldBillPayment?: IBillPayment oldBillPayment?: IBillPayment
): Promise<IBillPayment> { ): Promise<IBillPayment> {
const amount =
billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount');
const initialDTO = { const initialDTO = {
...formatDateFields(omit(billPaymentDTO, ['attachments']), [ ...formatDateFields(omit(billPaymentDTO, ['attachments']), [
'paymentDate', 'paymentDate',
]), ]),
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), amount,
currencyCode: vendor.currencyCode, currencyCode: vendor.currencyCode,
exchangeRate: billPaymentDTO.exchangeRate || 1, exchangeRate: billPaymentDTO.exchangeRate || 1,
entries: billPaymentDTO.entries, entries: billPaymentDTO.entries,

View File

@@ -36,7 +36,9 @@ export class PaymentReceiveDTOTransformer {
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
oldPaymentReceive?: IPaymentReceive oldPaymentReceive?: IPaymentReceive
): Promise<IPaymentReceive> { ): Promise<IPaymentReceive> {
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); const amount =
paymentReceiveDTO.amount ??
sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Retreive the next invoice number. // Retreive the next invoice number.
const autoNextNumber = const autoNextNumber =
@@ -54,7 +56,7 @@ export class PaymentReceiveDTOTransformer {
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [ ...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
'paymentDate', 'paymentDate',
]), ]),
amount: paymentAmount, amount,
currencyCode: customer.currencyCode, currencyCode: customer.currencyCode,
...(paymentReceiveNo ? { paymentReceiveNo } : {}), ...(paymentReceiveNo ? { paymentReceiveNo } : {}),
exchangeRate: paymentReceiveDTO.exchangeRate || 1, exchangeRate: paymentReceiveDTO.exchangeRate || 1,

View File

@@ -1,4 +1,3 @@
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
import config from '@/config'; import config from '@/config';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { import {
@@ -10,7 +9,6 @@ import {
} from './utils'; } from './utils';
import { Plan } from '@/system/models'; import { Plan } from '@/system/models';
import { Subscription } from './Subscription'; import { Subscription } from './Subscription';
import { isEmpty } from 'lodash';
@Service() @Service()
export class LemonSqueezyWebhooks { export class LemonSqueezyWebhooks {
@@ -18,7 +16,7 @@ export class LemonSqueezyWebhooks {
private subscriptionService: Subscription; private subscriptionService: Subscription;
/** /**
* handle the LemonSqueezy webhooks. * Handles the Lemon Squeezy webhooks.
* @param {string} rawBody * @param {string} rawBody
* @param {string} signature * @param {string} signature
* @returns {Promise<void>} * @returns {Promise<void>}
@@ -74,7 +72,7 @@ export class LemonSqueezyWebhooks {
const variantId = attributes.variant_id as string; const variantId = attributes.variant_id as string;
// We assume that the Plan table is up to date. // We assume that the Plan table is up to date.
const plan = await Plan.query().findOne('slug', 'early-adaptor'); const plan = await Plan.query().findOne('lemonVariantId', variantId);
if (!plan) { if (!plan) {
throw new Error(`Plan with variantId ${variantId} not found.`); throw new Error(`Plan with variantId ${variantId} not found.`);
@@ -82,26 +80,9 @@ export class LemonSqueezyWebhooks {
// Update the subscription in the database. // Update the subscription in the database.
const priceId = attributes.first_subscription_item.price_id; const priceId = attributes.first_subscription_item.price_id;
// Get the price data from Lemon Squeezy.
const priceData = await getPrice(priceId);
if (priceData.error) {
throw new Error(
`Failed to get the price data for the subscription ${eventBody.data.id}.`
);
}
const isUsageBased =
attributes.first_subscription_item.is_usage_based;
const price = isUsageBased
? priceData.data?.data.attributes.unit_price_decimal
: priceData.data?.data.attributes.unit_price;
// Create a new subscription of the tenant. // Create a new subscription of the tenant.
if (webhookEvent === 'subscription_created') { if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion( await this.subscriptionService.newSubscribtion(tenantId, plan.slug);
tenantId,
'early-adaptor'
);
} }
} }
} else if (webhookEvent.startsWith('order_')) { } else if (webhookEvent.startsWith('order_')) {

View File

@@ -77,7 +77,12 @@ export default class HasTenancyService {
const knex = this.knex(tenantId); const knex = this.knex(tenantId);
const i18n = this.i18n(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;
}); });
} }

View File

@@ -40,6 +40,13 @@ export default {
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated', baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
}, },
/**
* User subscription events.
*/
subscription: {
onSubscribed: 'onOrganizationSubscribed',
},
/** /**
* Tenants managment service. * Tenants managment service.
*/ */

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('subscription_plans', (table) => {
table.string('lemon_variant_id').nullable().index();
});
};
exports.down = (knex) => {
return knex.schema.table('subscription_plans', (table) => {
table.dropColumn('lemon_variant_id');
});
};

View File

@@ -0,0 +1,96 @@
exports.up = function (knex) {
return knex('subscription_plans').insert([
// Capital Basic
{
name: 'Capital Basic (Monthly)',
slug: 'capital-basic-monthly',
price: 10,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446152',
// lemon_variant_id: '450016',
},
{
name: 'Capital Basic (Annually)',
slug: 'capital-basic-annually',
price: 90,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446153',
// lemon_variant_id: '450018',
},
// # Capital Essential
{
name: 'Capital Essential (Monthly)',
slug: 'capital-essential-monthly',
price: 20,
active: true,
currency: 'USD',
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446155',
// lemon_variant_id: '450028',
},
{
name: 'Capital Essential (Annually)',
slug: 'capital-essential-annually',
price: 180,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446156',
// lemon_variant_id: '450029',
},
// # Capital Plus
{
name: 'Capital Plus (Monthly)',
slug: 'capital-plus-monthly',
price: 25,
active: true,
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446165',
// lemon_variant_id: '450031',
},
{
name: 'Capital Plus (Annually)',
slug: 'capital-plus-annually',
price: 228,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446164',
// lemon_variant_id: '450032',
},
// # Capital Big
{
name: 'Capital Big (Monthly)',
slug: 'capital-big-monthly',
price: 40,
active: true,
invoice_period: 1,
invoice_interval: 'month',
lemon_variant_id: '446167',
// lemon_variant_id: '450024',
},
{
name: 'Capital Big (Annually)',
slug: 'capital-big-annually',
price: 360,
active: true,
invoice_period: 1,
invoice_interval: 'year',
lemon_variant_id: '446168',
// lemon_variant_id: '450025',
},
]);
};
exports.down = function (knex) {};

View File

@@ -23,9 +23,10 @@
color: #fff; color: #fff;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
text-transform: uppercase;
} }
.label { .label {
font-size: 14px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #2F343C; color: #2F343C;
@@ -47,13 +48,31 @@
} }
.price { .price {
font-size: 18px; font-size: 18px;
line-height: 1; line-height: 1;
font-weight: 500; font-weight: 500;
color: #404854; color: #252A31;
} }
.pricePer{ .pricePer{
color: #738091; color: #738091;
font-size: 12px; font-size: 12px;
line-height: 1; line-height: 1;
}
.featureItem{
flex: 1;
color: #1C2127;
}
.featurePopover :global .bp4-popover-content{
border-radius: 0;
}
.featurePopoverContent{
font-size: 12px
}
.featurePopoverLabel {
text-transform: uppercase;
letter-spacing: 0.4px;
font-size: 12px;
font-weight: 500;
} }

View File

@@ -1,4 +1,11 @@
import { Button, ButtonProps, Intent } from '@blueprintjs/core'; import {
Button,
ButtonProps,
Intent,
Position,
Text,
Tooltip,
} from '@blueprintjs/core';
import clsx from 'classnames'; import clsx from 'classnames';
import { Box, Group, Stack } from '../Layout'; import { Box, Group, Stack } from '../Layout';
import styles from './PricingPlan.module.scss'; import styles from './PricingPlan.module.scss';
@@ -64,7 +71,7 @@ export interface PricingPriceProps {
*/ */
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => { PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
return ( return (
<Stack spacing={6} className={styles.priceRoot}> <Stack spacing={4} className={styles.priceRoot}>
<h4 className={styles.price}>{price}</h4> <h4 className={styles.price}>{price}</h4>
<span className={styles.pricePer}>{subPrice}</span> <span className={styles.pricePer}>{subPrice}</span>
</Stack> </Stack>
@@ -101,7 +108,7 @@ export interface PricingFeaturesProps {
*/ */
PricingPlan.Features = ({ children }: PricingFeaturesProps) => { PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
return ( return (
<Stack spacing={10} className={styles.features}> <Stack spacing={14} className={styles.features}>
{children} {children}
</Stack> </Stack>
); );
@@ -109,15 +116,41 @@ PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
export interface PricingFeatureLineProps { export interface PricingFeatureLineProps {
children: React.ReactNode; children: React.ReactNode;
hintContent?: string;
hintLabel?: string;
} }
/** /**
* Displays a single feature line within a list of features. * Displays a single feature line within a list of features.
* @param children - The content of the feature line. * @param children - The content of the feature line.
*/ */
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => { PricingPlan.FeatureLine = ({
return ( children,
<Group noWrap spacing={12}> hintContent,
hintLabel,
}: PricingFeatureLineProps) => {
return hintContent ? (
<Tooltip
content={
<Stack spacing={5}>
{hintLabel && (
<Text className={styles.featurePopoverLabel}>{hintLabel}</Text>
)}
<Text className={styles.featurePopoverContent}>{hintContent}</Text>
</Stack>
}
position={Position.TOP_LEFT}
popoverClassName={styles.featurePopover}
modifiers={{ offset: { enabled: true, offset: '0,10' } }}
minimal
>
<Group noWrap spacing={8} style={{ cursor: 'help' }}>
<CheckCircled height={12} width={12} />
<Box className={styles.featureItem}>{children}</Box>
</Group>
</Tooltip>
) : (
<Group noWrap spacing={8}>
<CheckCircled height={12} width={12} /> <CheckCircled height={12} width={12} />
<Box className={styles.featureItem}>{children}</Box> <Box className={styles.featureItem}>{children}</Box>
</Group> </Group>

View File

@@ -4,9 +4,9 @@ export const ACCOUNT_TYPE = {
BANK: 'bank', BANK: 'bank',
ACCOUNTS_RECEIVABLE: 'accounts-receivable', ACCOUNTS_RECEIVABLE: 'accounts-receivable',
INVENTORY: 'inventory', INVENTORY: 'inventory',
OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', OTHER_CURRENT_ASSET: 'other-current-asset',
FIXED_ASSET: 'fixed-asset', FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', NON_CURRENT_ASSET: 'non-current-asset',
ACCOUNTS_PAYABLE: 'accounts-payable', ACCOUNTS_PAYABLE: 'accounts-payable',
CREDIT_CARD: 'credit-card', CREDIT_CARD: 'credit-card',

View File

@@ -1,10 +1,140 @@
// @ts-nocheck interface SubscriptionPlanFeature {
// Subscription plans. text: string;
export const plans = [ hint?: string;
label?: string;
]; style?: Record<string, string>;
}
interface SubscriptionPlan {
name: string;
slug: string;
description: string;
features: SubscriptionPlanFeature[];
featured?: boolean;
monthlyPrice: string;
monthlyPriceLabel: string;
annuallyPrice: string;
annuallyPriceLabel: string;
monthlyVariantId: string;
annuallyVariantId: string;
}
// Payment methods. export const SubscriptionPlans = [
export const paymentMethods = [ {
name: 'Capital Basic',
]; slug: 'capital_basic',
description: 'Good for service businesses that just started.',
features: [
{
text: 'Unlimited Sale Invoices',
hintLabel: 'Unlimited Sale Invoices',
hint: 'Good for service businesses that just started for service businesses that just started',
},
{ text: 'Unlimated Sale Estimates' },
{ text: 'Track GST and VAT' },
{ text: 'Connect Banks for Automatic Importing' },
{ text: 'Chart of Accounts' },
{
text: 'Manual Journals',
hintLabel: 'Manual Journals',
hint: 'Write manual journals entries for financial transactions not automatically captured by the system to adjust financial statements.',
},
{
text: 'Basic Financial Reports & Insights',
hint: 'Balance sheet, profit & loss statement, cashflow statement, general ledger, journal sheet, A/P aging summary, A/R aging summary',
},
{ text: 'Unlimited User Seats' },
],
monthlyPrice: '$10',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$7.5',
annuallyPriceLabel: 'Per month',
monthlyVariantId: '446152',
// monthlyVariantId: '450016',
annuallyVariantId: '446153',
// annuallyVariantId: '450018',
},
{
name: 'Capital Essential',
slug: 'capital_plus',
description: 'Good for have inventory and want more financial reports.',
features: [
{ text: 'All Capital Basic features' },
{ text: 'Purchase Invoices' },
{
text: 'Multi Currency Transactions',
hintLabel: 'Multi Currency',
hint: 'Pay and get paid and do manual journals in any currency with real time exchange rates conversions.',
},
{
text: 'Transactions Locking',
hintLabel: 'Transactions Locking',
hint: 'Transaction Locking freezes transactions to prevent any additions, modifications, or deletions of transactions recorded during the specified date.',
},
{
text: 'Inventory Tracking',
hintLabel: 'Inventory Tracking',
hint: 'Track goods in the stock, cost of goods, and get notifications when quantity is low.',
},
{ text: 'Smart Financial Reports' },
{ text: 'Advanced Inventory Reports' },
],
monthlyPrice: '$20',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$15',
annuallyPriceLabel: 'Per month',
// monthlyVariantId: '450028',
monthlyVariantId: '446155',
// annuallyVariantId: '450029',
annuallyVariantId: '446156',
},
{
name: 'Capital Plus',
slug: 'essentials',
description: 'Good for business want financial and access control.',
features: [
{ text: 'All Capital Essential features' },
{ text: 'Custom User Roles Access' },
{ text: 'Vendor Credits' },
{
text: 'Budgeting',
hint: 'Create multiple budgets and compare targets with actuals to understand how your business is performing.',
},
{ text: 'Analysis Cost Center' },
],
monthlyPrice: '$25',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$19',
annuallyPriceLabel: 'Per month',
featured: true,
// monthlyVariantId: '450031',
monthlyVariantId: '446165',
// annuallyVariantId: '450032',
annuallyVariantId: '446164',
},
{
name: 'Capital Big',
slug: 'essentials',
description: 'Good for businesses have multiple branches.',
features: [
{ text: 'All Capital Plus features' },
{
text: 'Multiple Branches',
hintLabel: '',
hint: 'Track the organization transactions and accounts in multiple branches.',
},
{
text: 'Multiple Warehouses',
hintLabel: 'Multiple Warehouses',
hint: 'Track the organization inventory in multiple warehouses and transfer goods between them.',
},
],
monthlyPrice: '$40',
monthlyPriceLabel: 'Per month',
annuallyPrice: '$30',
annuallyPriceLabel: 'Per month',
// monthlyVariantId: '450024',
monthlyVariantId: '446167',
// annuallyVariantId: '450025',
annuallyVariantId: '446168',
},
] as SubscriptionPlan[];

View File

@@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components';
import { transformTableStateToQuery, compose } from '@/utils'; import { transformTableStateToQuery, compose } from '@/utils';
import { ManualJournalsListProvider } from './ManualJournalsListProvider'; import { ManualJournalsListProvider } from './ManualJournalsListProvider';
import ManualJournalsViewTabs from './ManualJournalsViewTabs';
import ManualJournalsDataTable from './ManualJournalsDataTable'; import ManualJournalsDataTable from './ManualJournalsDataTable';
import ManualJournalsActionsBar from './ManualJournalActionsBar'; import ManualJournalsActionsBar from './ManualJournalActionsBar';
import withManualJournals from './withManualJournals'; import withManualJournals from './withManualJournals';
@@ -29,7 +28,6 @@ function ManualJournalsTable({
<ManualJournalsActionsBar /> <ManualJournalsActionsBar />
<DashboardPageContent> <DashboardPageContent>
<ManualJournalsViewTabs />
<ManualJournalsDataTable /> <ManualJournalsDataTable />
</DashboardPageContent> </DashboardPageContent>
</ManualJournalsListProvider> </ManualJournalsListProvider>

View File

@@ -2,15 +2,15 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import '@/style/pages/Accounts/List.scss'; import '@/style/pages/Accounts/List.scss';
import { DashboardPageContent, DashboardContentTable } from '@/components';
import { DashboardPageContent, DashboardContentTable } from '@/components';
import { AccountsChartProvider } from './AccountsChartProvider'; import { AccountsChartProvider } from './AccountsChartProvider';
import AccountsViewsTabs from './AccountsViewsTabs';
import AccountsActionsBar from './AccountsActionsBar'; import AccountsActionsBar from './AccountsActionsBar';
import AccountsDataTable from './AccountsDataTable'; import AccountsDataTable from './AccountsDataTable';
import withAccounts from '@/containers/Accounts/withAccounts'; import withAccounts from '@/containers/Accounts/withAccounts';
import withAccountsTableActions from './withAccountsTableActions'; import withAccountsTableActions from './withAccountsTableActions';
import { transformAccountsStateToQuery } from './utils'; import { transformAccountsStateToQuery } from './utils';
import { compose } from '@/utils'; import { compose } from '@/utils';
@@ -41,8 +41,6 @@ function AccountsChart({
<AccountsActionsBar /> <AccountsActionsBar />
<DashboardPageContent> <DashboardPageContent>
<AccountsViewsTabs />
<DashboardContentTable> <DashboardContentTable>
<AccountsDataTable /> <AccountsDataTable />
</DashboardContentTable> </DashboardContentTable>

View File

@@ -6,7 +6,6 @@ import '@/style/pages/Customers/List.scss';
import { DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import CustomersActionsBar from './CustomersActionsBar'; import CustomersActionsBar from './CustomersActionsBar';
import CustomersViewsTabs from './CustomersViewsTabs';
import CustomersTable from './CustomersTable'; import CustomersTable from './CustomersTable';
import { CustomersListProvider } from './CustomersListProvider'; import { CustomersListProvider } from './CustomersListProvider';
@@ -42,7 +41,6 @@ function CustomersList({
<CustomersActionsBar /> <CustomersActionsBar />
<DashboardPageContent> <DashboardPageContent>
<CustomersViewsTabs />
<CustomersTable /> <CustomersTable />
</DashboardPageContent> </DashboardPageContent>
</CustomersListProvider> </CustomersListProvider>

View File

@@ -6,7 +6,6 @@ import '@/style/pages/Expense/List.scss';
import { DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import ExpenseActionsBar from './ExpenseActionsBar'; import ExpenseActionsBar from './ExpenseActionsBar';
import ExpenseViewTabs from './ExpenseViewTabs';
import ExpenseDataTable from './ExpenseDataTable'; import ExpenseDataTable from './ExpenseDataTable';
import withExpenses from './withExpenses'; import withExpenses from './withExpenses';
@@ -42,7 +41,6 @@ function ExpensesList({
<ExpenseActionsBar /> <ExpenseActionsBar />
<DashboardPageContent> <DashboardPageContent>
<ExpenseViewTabs />
<ExpenseDataTable /> <ExpenseDataTable />
</DashboardPageContent> </DashboardPageContent>
</ExpensesListProvider> </ExpensesListProvider>

View File

@@ -8,7 +8,6 @@ import { DashboardPageContent } from '@/components';
import { ItemsListProvider } from './ItemsListProvider'; import { ItemsListProvider } from './ItemsListProvider';
import ItemsActionsBar from './ItemsActionsBar'; import ItemsActionsBar from './ItemsActionsBar';
import ItemsViewsTabs from './ItemsViewsTabs';
import ItemsDataTable from './ItemsDataTable'; import ItemsDataTable from './ItemsDataTable';
import withItems from './withItems'; import withItems from './withItems';
@@ -41,7 +40,6 @@ function ItemsList({
<ItemsActionsBar /> <ItemsActionsBar />
<DashboardPageContent> <DashboardPageContent>
<ItemsViewsTabs />
<ItemsDataTable /> <ItemsDataTable />
</DashboardPageContent> </DashboardPageContent>
</ItemsListProvider> </ItemsListProvider>

View File

@@ -7,7 +7,6 @@ import '@/style/pages/Bills/List.scss';
import { BillsListProvider } from './BillsListProvider'; import { BillsListProvider } from './BillsListProvider';
import BillsActionsBar from './BillsActionsBar'; import BillsActionsBar from './BillsActionsBar';
import BillsViewsTabs from './BillsViewsTabs';
import BillsTable from './BillsTable'; import BillsTable from './BillsTable';
import withBills from './withBills'; import withBills from './withBills';
@@ -42,7 +41,6 @@ function BillsList({
<BillsActionsBar /> <BillsActionsBar />
<DashboardPageContent> <DashboardPageContent>
<BillsViewsTabs />
<BillsTable /> <BillsTable />
</DashboardPageContent> </DashboardPageContent>
</BillsListProvider> </BillsListProvider>

View File

@@ -5,7 +5,6 @@ import '@/style/pages/VendorsCreditNote/List.scss';
import { DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import VendorsCreditNoteActionsBar from './VendorsCreditNoteActionsBar'; import VendorsCreditNoteActionsBar from './VendorsCreditNoteActionsBar';
import VendorsCreditNoteViewTabs from './VendorsCreditNoteViewTabs';
import VendorsCreditNoteDataTable from './VendorsCreditNoteDataTable'; import VendorsCreditNoteDataTable from './VendorsCreditNoteDataTable';
import withVendorsCreditNotes from './withVendorsCreditNotes'; import withVendorsCreditNotes from './withVendorsCreditNotes';
@@ -37,7 +36,6 @@ function VendorsCreditNotesList({
> >
<VendorsCreditNoteActionsBar /> <VendorsCreditNoteActionsBar />
<DashboardPageContent> <DashboardPageContent>
<VendorsCreditNoteViewTabs />
<VendorsCreditNoteDataTable /> <VendorsCreditNoteDataTable />
</DashboardPageContent> </DashboardPageContent>
</VendorsCreditNoteListProvider> </VendorsCreditNoteListProvider>

View File

@@ -0,0 +1,9 @@
import { ExcessPaymentDialog } from './dialogs/PaymentMadeExcessDialog';
export function PaymentMadeDialogs() {
return (
<>
<ExcessPaymentDialog dialogName={'payment-made-excessed-payment'} />
</>
);
}

View File

@@ -2,7 +2,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames'; import classNames from 'classnames';
import { Formik, Form } from 'formik'; import { Formik, Form, FormikHelpers } from 'formik';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { sumBy, defaultTo } from 'lodash'; import { sumBy, defaultTo } from 'lodash';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@@ -14,6 +14,7 @@ import PaymentMadeFloatingActions from './PaymentMadeFloatingActions';
import PaymentMadeFooter from './PaymentMadeFooter'; import PaymentMadeFooter from './PaymentMadeFooter';
import PaymentMadeFormBody from './PaymentMadeFormBody'; import PaymentMadeFormBody from './PaymentMadeFormBody';
import PaymentMadeFormTopBar from './PaymentMadeFormTopBar'; import PaymentMadeFormTopBar from './PaymentMadeFormTopBar';
import { PaymentMadeDialogs } from './PaymentMadeDialogs';
import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider'; import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
@@ -21,6 +22,7 @@ import { compose, orderingLinesIndexes } from '@/utils';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { import {
EditPaymentMadeFormSchema, EditPaymentMadeFormSchema,
@@ -31,6 +33,7 @@ import {
transformToEditForm, transformToEditForm,
transformErrors, transformErrors,
transformFormToRequest, transformFormToRequest,
getPaymentExcessAmountFromValues,
} from './utils'; } from './utils';
/** /**
@@ -42,6 +45,9 @@ function PaymentMadeForm({
// #withCurrentOrganization // #withCurrentOrganization
organization: { base_currency }, organization: { base_currency },
// #withDialogActions
openDialog,
}) { }) {
const history = useHistory(); const history = useHistory();
@@ -54,6 +60,7 @@ function PaymentMadeForm({
submitPayload, submitPayload,
createPaymentMadeMutate, createPaymentMadeMutate,
editPaymentMadeMutate, editPaymentMadeMutate,
isExcessConfirmed,
} = usePaymentMadeFormContext(); } = usePaymentMadeFormContext();
// Form initial values. // Form initial values.
@@ -76,13 +83,11 @@ function PaymentMadeForm({
// Handle the form submit. // Handle the form submit.
const handleSubmitForm = ( const handleSubmitForm = (
values, values,
{ setSubmitting, resetForm, setFieldError }, { setSubmitting, resetForm, setFieldError }: FormikHelpers<any>,
) => { ) => {
setSubmitting(true); setSubmitting(true);
// Total payment amount of entries.
const totalPaymentAmount = sumBy(values.entries, 'payment_amount');
if (totalPaymentAmount <= 0) { if (values.amount <= 0) {
AppToaster.show({ AppToaster.show({
message: intl.get('you_cannot_make_payment_with_zero_total_amount'), message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
intent: Intent.DANGER, intent: Intent.DANGER,
@@ -90,6 +95,16 @@ function PaymentMadeForm({
setSubmitting(false); setSubmitting(false);
return; 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. // Transformes the form values to request body.
const form = transformFormToRequest(values); const form = transformFormToRequest(values);
@@ -119,11 +134,12 @@ function PaymentMadeForm({
} }
setSubmitting(false); setSubmitting(false);
}; };
if (!isNewMode) { if (!isNewMode) {
editPaymentMadeMutate([paymentMadeId, form]).then(onSaved).catch(onError); return editPaymentMadeMutate([paymentMadeId, form])
.then(onSaved)
.catch(onError);
} else { } else {
createPaymentMadeMutate(form).then(onSaved).catch(onError); return createPaymentMadeMutate(form).then(onSaved).catch(onError);
} }
}; };
@@ -149,6 +165,7 @@ function PaymentMadeForm({
<PaymentMadeFormBody /> <PaymentMadeFormBody />
<PaymentMadeFooter /> <PaymentMadeFooter />
<PaymentMadeFloatingActions /> <PaymentMadeFloatingActions />
<PaymentMadeDialogs />
</PaymentMadeInnerProvider> </PaymentMadeInnerProvider>
</Form> </Form>
</Formik> </Formik>
@@ -163,4 +180,5 @@ export default compose(
preferredPaymentAccount: parseInt(billPaymentSettings?.withdrawalAccount), preferredPaymentAccount: parseInt(billPaymentSettings?.withdrawalAccount),
})), })),
withCurrentOrganization(), withCurrentOrganization(),
withDialogActions,
)(PaymentMadeForm); )(PaymentMadeForm);

View File

@@ -1,17 +1,23 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useFormikContext } from 'formik';
import { import {
T, T,
TotalLines, TotalLines,
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FormatNumber,
} from '@/components'; } from '@/components';
import { usePaymentMadeTotals } from './utils'; import { usePaymentMadeExcessAmount, usePaymentMadeTotals } from './utils';
export function PaymentMadeFormFooterRight() { export function PaymentMadeFormFooterRight() {
const { formattedSubtotal, formattedTotal } = usePaymentMadeTotals(); const { formattedSubtotal, formattedTotal } = usePaymentMadeTotals();
const excessAmount = usePaymentMadeExcessAmount();
const {
values: { currency_code: currencyCode },
} = useFormikContext();
return ( return (
<PaymentMadeTotalLines labelColWidth={'180px'} amountColWidth={'180px'}> <PaymentMadeTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
@@ -25,6 +31,11 @@ export function PaymentMadeFormFooterRight() {
value={formattedTotal} value={formattedTotal}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />
<TotalLine
title={'Excess Amount'}
value={<FormatNumber value={excessAmount} currency={currencyCode} />}
textStyle={TotalLineTextStyle.Regular}
/>
</PaymentMadeTotalLines> </PaymentMadeTotalLines>
); );
} }

View File

@@ -1,12 +1,12 @@
// @ts-nocheck // @ts-nocheck
import React, { useMemo } from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { sumBy } from 'lodash';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { Money, FormattedMessage as T } from '@/components'; import { Money, FormattedMessage as T } from '@/components';
import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields'; import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields';
import { usePaymentmadeTotalAmount } from './utils';
/** /**
* Payment made header form. * Payment made header form.
@@ -14,11 +14,10 @@ import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields';
function PaymentMadeFormHeader() { function PaymentMadeFormHeader() {
// Formik form context. // Formik form context.
const { const {
values: { entries, currency_code }, values: { currency_code },
} = useFormikContext(); } = useFormikContext();
// Calculate the payment amount of the entries. const totalAmount = usePaymentmadeTotalAmount();
const amountPaid = useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
@@ -30,8 +29,9 @@ function PaymentMadeFormHeader() {
<span class="big-amount__label"> <span class="big-amount__label">
<T id={'amount_received'} /> <T id={'amount_received'} />
</span> </span>
<h1 class="big-amount__number"> <h1 class="big-amount__number">
<Money amount={amountPaid} currency={currency_code} /> <Money amount={totalAmount} currency={currency_code} />
</h1> </h1>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty, toSafeInteger } from 'lodash';
import { import {
FormGroup, FormGroup,
InputGroup, InputGroup,
@@ -13,7 +14,6 @@ import {
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik'; import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
import { FormattedMessage as T, VendorsSelect } from '@/components'; import { FormattedMessage as T, VendorsSelect } from '@/components';
import { toSafeInteger } from 'lodash';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { import {
@@ -68,7 +68,7 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
const fullAmount = safeSumBy(newEntries, 'payment_amount'); const fullAmount = safeSumBy(newEntries, 'payment_amount');
setFieldValue('entries', newEntries); setFieldValue('entries', newEntries);
setFieldValue('full_amount', fullAmount); setFieldValue('amount', fullAmount);
}; };
// Handles the full-amount field blur. // Handles the full-amount field blur.
@@ -115,10 +115,10 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
</FastField> </FastField>
{/* ------------ Full amount ------------ */} {/* ------------ Full amount ------------ */}
<Field name={'full_amount'}> <Field name={'amount'}>
{({ {({
form: { form: {
values: { currency_code }, values: { currency_code, entries },
}, },
field: { value }, field: { value },
meta: { error, touched }, meta: { error, touched },
@@ -129,28 +129,30 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
className={('form-group--full-amount', Classes.FILL)} className={('form-group--full-amount', Classes.FILL)}
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
labelInfo={<Hint />} labelInfo={<Hint />}
helperText={<ErrorMessage name="full_amount" />} helperText={<ErrorMessage name="amount" />}
> >
<ControlGroup> <ControlGroup>
<InputPrependText text={currency_code} /> <InputPrependText text={currency_code} />
<MoneyInputGroup <MoneyInputGroup
value={value} value={value}
onChange={(value) => { onChange={(value) => {
setFieldValue('full_amount', value); setFieldValue('amount', value);
}} }}
onBlurValue={onFullAmountBlur} onBlurValue={onFullAmountBlur}
/> />
</ControlGroup> </ControlGroup>
<Button {!isEmpty(entries) && (
onClick={handleReceiveFullAmountClick} <Button
className={'receive-full-amount'} onClick={handleReceiveFullAmountClick}
small={true} className={'receive-full-amount'}
minimal={true} small={true}
> minimal={true}
<T id={'receive_full_amount'} /> ( >
<Money amount={payableFullAmount} currency={currency_code} />) <T id={'receive_full_amount'} /> (
</Button> <Money amount={payableFullAmount} currency={currency_code} />)
</Button>
)}
</FormGroup> </FormGroup>
)} )}
</Field> </Field>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext, useState } from 'react';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { import {
@@ -71,6 +71,8 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) {
const isFeatureLoading = isBranchesLoading; const isFeatureLoading = isBranchesLoading;
const [isExcessConfirmed, setIsExcessConfirmed] = useState<boolean>(false);
// Provider payload. // Provider payload.
const provider = { const provider = {
paymentMadeId, paymentMadeId,
@@ -98,6 +100,9 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) {
setSubmitPayload, setSubmitPayload,
setPaymentVendorId, setPaymentVendorId,
isExcessConfirmed,
setIsExcessConfirmed,
}; };
return ( return (

View File

@@ -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 (
<Dialog
name={dialogName}
title={'Excess Payment'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 500 }}
>
<DialogSuspense>
<ExcessPaymentDialogContent dialogName={dialogName} />
</DialogSuspense>
</Dialog>
);
}
export const ExcessPaymentDialog = compose(withDialogRedux())(
ExcessPaymentDialogRoot,
);
ExcessPaymentDialog.displayName = 'ExcessPaymentDialog';

View File

@@ -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<ExcessPaymentValues>,
) => {
setSubmitting(true);
setIsExcessConfirmed(true);
return submitForm().then(() => {
setSubmitting(false);
closeDialog(dialogName);
});
};
// Handle close button click.
const handleCloseBtn = () => {
closeDialog(dialogName);
};
const excessAmount = usePaymentMadeExcessAmount();
return (
<Formik initialValues={{}} onSubmit={handleSubmit}>
<Form>
<ExcessPaymentDialogContentForm
excessAmount={
<FormatNumber value={excessAmount} currency={currencyCode} />
}
onClose={handleCloseBtn}
/>
</Form>
</Formik>
);
}
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 (
<>
<div className={Classes.DIALOG_BODY}>
<p style={{ marginBottom: 20 }}>
Would you like to record the excess amount of{' '}
<strong>{excessAmount}</strong> as credit payment from the vendor.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
onClick={() => submitForm()}
>
Save Payment as Credit
</Button>
<Button onClick={handleCloseBtn}>Cancel</Button>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1 @@
export * from './PaymentMadeExcessDialog';

View File

@@ -37,7 +37,7 @@ export const defaultPaymentMadeEntry = {
// Default initial values of payment made. // Default initial values of payment made.
export const defaultPaymentMade = { export const defaultPaymentMade = {
full_amount: '', amount: '',
vendor_id: '', vendor_id: '',
payment_account_id: '', payment_account_id: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'), payment_date: moment(new Date()).format('YYYY-MM-DD'),
@@ -53,10 +53,10 @@ export const defaultPaymentMade = {
export const transformToEditForm = (paymentMade, paymentMadeEntries) => { export const transformToEditForm = (paymentMade, paymentMadeEntries) => {
const attachments = transformAttachmentsToForm(paymentMade); const attachments = transformAttachmentsToForm(paymentMade);
const appliedAmount = safeSumBy(paymentMadeEntries, 'payment_amount');
return { return {
...transformToForm(paymentMade, defaultPaymentMade), ...transformToForm(paymentMade, defaultPaymentMade),
full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'),
entries: [ entries: [
...paymentMadeEntries.map((paymentMadeEntry) => ({ ...paymentMadeEntries.map((paymentMadeEntry) => ({
...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry), ...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. * Detarmines whether the bill has foreign customer.
* @returns {boolean} * @returns {boolean}
@@ -191,3 +215,10 @@ export const usePaymentMadeIsForeignCustomer = () => {
); );
return isForeignCustomer; return isForeignCustomer;
}; };
export const getPaymentExcessAmountFromValues = (values) => {
const appliedAmount = sumBy(values.entries, 'payment_amount');
const totalAmount = values.amount;
return Math.abs(totalAmount - appliedAmount);
};

View File

@@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components';
import { PaymentMadesListProvider } from './PaymentMadesListProvider'; import { PaymentMadesListProvider } from './PaymentMadesListProvider';
import PaymentMadeActionsBar from './PaymentMadeActionsBar'; import PaymentMadeActionsBar from './PaymentMadeActionsBar';
import PaymentMadesTable from './PaymentMadesTable'; import PaymentMadesTable from './PaymentMadesTable';
import PaymentMadeViewTabs from './PaymentMadeViewTabs';
import withPaymentMades from './withPaymentMade'; import withPaymentMades from './withPaymentMade';
import withPaymentMadeActions from './withPaymentMadeActions'; import withPaymentMadeActions from './withPaymentMadeActions';
@@ -41,7 +40,6 @@ function PaymentMadeList({
<PaymentMadeActionsBar /> <PaymentMadeActionsBar />
<DashboardPageContent> <DashboardPageContent>
<PaymentMadeViewTabs />
<PaymentMadesTable /> <PaymentMadesTable />
</DashboardPageContent> </DashboardPageContent>
</PaymentMadesListProvider> </PaymentMadesListProvider>

View File

@@ -5,7 +5,6 @@ import '@/style/pages/CreditNote/List.scss';
import { DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import CreditNotesActionsBar from './CreditNotesActionsBar'; import CreditNotesActionsBar from './CreditNotesActionsBar';
import CreditNotesViewTabs from './CreditNotesViewTabs';
import CreditNotesDataTable from './CreditNotesDataTable'; import CreditNotesDataTable from './CreditNotesDataTable';
import withCreditNotes from './withCreditNotes'; import withCreditNotes from './withCreditNotes';
@@ -36,8 +35,8 @@ function CreditNotesList({
tableStateChanged={creditNoteTableStateChanged} tableStateChanged={creditNoteTableStateChanged}
> >
<CreditNotesActionsBar /> <CreditNotesActionsBar />
<DashboardPageContent> <DashboardPageContent>
<CreditNotesViewTabs />
<CreditNotesDataTable /> <CreditNotesDataTable />
</DashboardPageContent> </DashboardPageContent>
</CreditNotesListProvider> </CreditNotesListProvider>

View File

@@ -1,11 +1,10 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { DashboardContentTable, DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import '@/style/pages/SaleEstimate/List.scss'; import '@/style/pages/SaleEstimate/List.scss';
import EstimatesActionsBar from './EstimatesActionsBar'; import EstimatesActionsBar from './EstimatesActionsBar';
import EstimatesViewTabs from './EstimatesViewTabs';
import EstimatesDataTable from './EstimatesDataTable'; import EstimatesDataTable from './EstimatesDataTable';
import withEstimates from './withEstimates'; import withEstimates from './withEstimates';
@@ -41,7 +40,6 @@ function EstimatesList({
<EstimatesActionsBar /> <EstimatesActionsBar />
<DashboardPageContent> <DashboardPageContent>
<EstimatesViewTabs />
<EstimatesDataTable /> <EstimatesDataTable />
</DashboardPageContent> </DashboardPageContent>
</EstimatesListProvider> </EstimatesListProvider>

View File

@@ -6,7 +6,6 @@ import '@/style/pages/SaleInvoice/List.scss';
import { DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import { InvoicesListProvider } from './InvoicesListProvider'; import { InvoicesListProvider } from './InvoicesListProvider';
import InvoiceViewTabs from './InvoiceViewTabs';
import InvoicesDataTable from './InvoicesDataTable'; import InvoicesDataTable from './InvoicesDataTable';
import InvoicesActionsBar from './InvoicesActionsBar'; import InvoicesActionsBar from './InvoicesActionsBar';
@@ -43,7 +42,6 @@ function InvoicesList({
<InvoicesActionsBar /> <InvoicesActionsBar />
<DashboardPageContent> <DashboardPageContent>
<InvoiceViewTabs />
<InvoicesDataTable /> <InvoicesDataTable />
</DashboardPageContent> </DashboardPageContent>
</InvoicesListProvider> </InvoicesListProvider>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React, { useMemo } from 'react'; import React, { useMemo, useRef } from 'react';
import { sumBy, isEmpty, defaultTo } from 'lodash'; import { sumBy, isEmpty, defaultTo } from 'lodash';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -21,6 +21,7 @@ import { PaymentReceiveInnerProvider } from './PaymentReceiveInnerProvider';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { import {
EditPaymentReceiveFormSchema, EditPaymentReceiveFormSchema,
@@ -36,6 +37,7 @@ import {
transformFormToRequest, transformFormToRequest,
transformErrors, transformErrors,
resetFormState, resetFormState,
getExceededAmountFromValues,
} from './utils'; } from './utils';
import { PaymentReceiveSyncIncrementSettingsToForm } from './components'; import { PaymentReceiveSyncIncrementSettingsToForm } from './components';
@@ -51,6 +53,9 @@ function PaymentReceiveForm({
// #withCurrentOrganization // #withCurrentOrganization
organization: { base_currency }, organization: { base_currency },
// #withDialogActions
openDialog,
}) { }) {
const history = useHistory(); const history = useHistory();
@@ -63,6 +68,7 @@ function PaymentReceiveForm({
submitPayload, submitPayload,
editPaymentReceiveMutate, editPaymentReceiveMutate,
createPaymentReceiveMutate, createPaymentReceiveMutate,
isExcessConfirmed,
} = usePaymentReceiveFormContext(); } = usePaymentReceiveFormContext();
// Payment receive number. // Payment receive number.
@@ -94,18 +100,16 @@ function PaymentReceiveForm({
preferredDepositAccount, preferredDepositAccount,
], ],
); );
// Handle form submit. // Handle form submit.
const handleSubmitForm = ( const handleSubmitForm = (
values, values,
{ setSubmitting, resetForm, setFieldError }, { setSubmitting, resetForm, setFieldError },
) => { ) => {
setSubmitting(true); setSubmitting(true);
const exceededAmount = getExceededAmountFromValues(values);
// Calculates the total payment amount of entries. // Validates the amount should be bigger than zero.
const totalPaymentAmount = sumBy(values.entries, 'payment_amount'); if (values.amount <= 0) {
if (totalPaymentAmount <= 0) {
AppToaster.show({ AppToaster.show({
message: intl.get('you_cannot_make_payment_with_zero_total_amount'), message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
intent: Intent.DANGER, intent: Intent.DANGER,
@@ -113,6 +117,13 @@ function PaymentReceiveForm({
setSubmitting(false); setSubmitting(false);
return; 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. // Transformes the form values to request body.
const form = transformFormToRequest(values); const form = transformFormToRequest(values);
@@ -148,11 +159,11 @@ function PaymentReceiveForm({
}; };
if (paymentReceiveId) { if (paymentReceiveId) {
editPaymentReceiveMutate([paymentReceiveId, form]) return editPaymentReceiveMutate([paymentReceiveId, form])
.then(onSaved) .then(onSaved)
.catch(onError); .catch(onError);
} else { } else {
createPaymentReceiveMutate(form).then(onSaved).catch(onError); return createPaymentReceiveMutate(form).then(onSaved).catch(onError);
} }
}; };
return ( return (
@@ -202,4 +213,5 @@ export default compose(
preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount, preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount,
})), })),
withCurrentOrganization(), withCurrentOrganization(),
withDialogActions,
)(PaymentReceiveForm); )(PaymentReceiveForm);

View File

@@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import PaymentReceiveNumberDialog from '@/containers/Dialogs/PaymentReceiveNumberDialog'; import PaymentReceiveNumberDialog from '@/containers/Dialogs/PaymentReceiveNumberDialog';
import { ExcessPaymentDialog } from './dialogs/ExcessPaymentDialog';
/** /**
* Payment receive form dialogs. * Payment receive form dialogs.
@@ -21,9 +22,12 @@ export default function PaymentReceiveFormDialogs() {
}; };
return ( return (
<PaymentReceiveNumberDialog <>
dialogName={'payment-receive-number-form'} <PaymentReceiveNumberDialog
onConfirm={handleUpdatePaymentNumber} dialogName={'payment-receive-number-form'}
/> onConfirm={handleUpdatePaymentNumber}
/>
<ExcessPaymentDialog dialogName={'payment-received-excessed-payment'} />
</>
); );
} }

View File

@@ -7,11 +7,16 @@ import {
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FormatNumber,
} from '@/components'; } from '@/components';
import { usePaymentReceiveTotals } from './utils'; import {
usePaymentReceiveTotals,
usePaymentReceivedTotalExceededAmount,
} from './utils';
export function PaymentReceiveFormFootetRight() { export function PaymentReceiveFormFootetRight() {
const { formattedSubtotal, formattedTotal } = usePaymentReceiveTotals(); const { formattedSubtotal, formattedTotal } = usePaymentReceiveTotals();
const exceededAmount = usePaymentReceivedTotalExceededAmount();
return ( return (
<PaymentReceiveTotalLines labelColWidth={'180px'} amountColWidth={'180px'}> <PaymentReceiveTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
@@ -25,6 +30,11 @@ export function PaymentReceiveFormFootetRight() {
value={formattedTotal} value={formattedTotal}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />
<TotalLine
title={'Exceeded Amount'}
value={<FormatNumber value={exceededAmount} />}
textStyle={TotalLineTextStyle.Regular}
/>
</PaymentReceiveTotalLines> </PaymentReceiveTotalLines>
); );
} }

View File

@@ -30,15 +30,9 @@ function PaymentReceiveFormHeader() {
function PaymentReceiveFormBigTotal() { function PaymentReceiveFormBigTotal() {
// Formik form context. // Formik form context.
const { const {
values: { currency_code, entries }, values: { currency_code, amount },
} = useFormikContext(); } = useFormikContext();
// Calculates the total payment amount from due amount.
const paymentFullAmount = useMemo(
() => sumBy(entries, 'payment_amount'),
[entries],
);
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
<div class="big-amount"> <div class="big-amount">
@@ -46,7 +40,7 @@ function PaymentReceiveFormBigTotal() {
<T id={'amount_received'} /> <T id={'amount_received'} />
</span> </span>
<h1 class="big-amount__number"> <h1 class="big-amount__number">
<Money amount={paymentFullAmount} currency={currency_code} /> <Money amount={amount} currency={currency_code} />
</h1> </h1>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext, useState } from 'react';
import { Features } from '@/constants'; import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state'; import { useFeatureCan } from '@/hooks/state';
import { DashboardInsider } from '@/components'; import { DashboardInsider } from '@/components';
@@ -74,6 +74,8 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
const { mutateAsync: editPaymentReceiveMutate } = useEditPaymentReceive(); const { mutateAsync: editPaymentReceiveMutate } = useEditPaymentReceive();
const { mutateAsync: createPaymentReceiveMutate } = useCreatePaymentReceive(); const { mutateAsync: createPaymentReceiveMutate } = useCreatePaymentReceive();
const [isExcessConfirmed, setIsExcessConfirmed] = useState<boolean>(false);
// Provider payload. // Provider payload.
const provider = { const provider = {
paymentReceiveId, paymentReceiveId,
@@ -97,6 +99,9 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
editPaymentReceiveMutate, editPaymentReceiveMutate,
createPaymentReceiveMutate, createPaymentReceiveMutate,
isExcessConfirmed,
setIsExcessConfirmed,
}; };
return ( return (

View File

@@ -11,7 +11,7 @@ import {
Button, Button,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { toSafeInteger } from 'lodash'; import { isEmpty, toSafeInteger } from 'lodash';
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik'; import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
import { import {
@@ -124,11 +124,11 @@ export default function PaymentReceiveHeaderFields() {
</FastField> </FastField>
{/* ------------ Full amount ------------ */} {/* ------------ Full amount ------------ */}
<Field name={'full_amount'}> <Field name={'amount'}>
{({ {({
form: { form: {
setFieldValue, setFieldValue,
values: { currency_code }, values: { currency_code, entries },
}, },
field: { value, onChange }, field: { value, onChange },
meta: { error, touched }, meta: { error, touched },
@@ -146,21 +146,23 @@ export default function PaymentReceiveHeaderFields() {
<MoneyInputGroup <MoneyInputGroup
value={value} value={value}
onChange={(value) => { onChange={(value) => {
setFieldValue('full_amount', value); setFieldValue('amount', value);
}} }}
onBlurValue={onFullAmountBlur} onBlurValue={onFullAmountBlur}
/> />
</ControlGroup> </ControlGroup>
<Button {!isEmpty(entries) && (
onClick={handleReceiveFullAmountClick} <Button
className={'receive-full-amount'} onClick={handleReceiveFullAmountClick}
small={true} className={'receive-full-amount'}
minimal={true} small={true}
> minimal={true}
<T id={'receive_full_amount'} /> ( >
<Money amount={totalDueAmount} currency={currency_code} />) <T id={'receive_full_amount'} /> (
</Button> <Money amount={totalDueAmount} currency={currency_code} />)
</Button>
)}
</FormGroup> </FormGroup>
)} )}
</Field> </Field>

View File

@@ -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 (
<Dialog
name={dialogName}
title={'Excess Payment'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 500 }}
>
<DialogSuspense>
<ExcessPaymentDialogContent dialogName={dialogName} />
</DialogSuspense>
</Dialog>
);
}
export const ExcessPaymentDialog = compose(withDialogRedux())(
ExcessPaymentDialogRoot,
);
ExcessPaymentDialog.displayName = 'ExcessPaymentDialog';

View File

@@ -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<ExcessPaymentValues>,
) => {
setSubmitting(true);
setIsExcessConfirmed(true);
submitForm().then(() => {
closeDialog(dialogName);
setSubmitting(false);
});
};
const handleClose = () => {
closeDialog(dialogName);
};
return (
<Formik initialValues={{}} onSubmit={handleSubmit}>
<Form>
<ExcessPaymentDialogContentForm
exceededAmount={
<FormatNumber value={exceededAmount} currency={currencyCode} />
}
onClose={handleClose}
/>
</Form>
</Formik>
);
}
export const ExcessPaymentDialogContent = R.compose(withDialogActions)(
ExcessPaymentDialogContentRoot,
);
function ExcessPaymentDialogContentForm({ onClose, exceededAmount }) {
const { submitForm, isSubmitting } = useFormikContext();
const handleCloseBtn = () => {
onClose && onClose();
};
return (
<>
<div className={Classes.DIALOG_BODY}>
<p style={{ marginBottom: 20 }}>
Would you like to record the excess amount of{' '}
<strong>{exceededAmount}</strong> as credit payment from the customer.
</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
disabled={isSubmitting}
onClick={() => submitForm()}
>
Save Payment as Credit
</Button>
<Button onClick={handleCloseBtn}>Cancel</Button>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1 @@
export * from './ExcessPaymentDialog';

View File

@@ -42,12 +42,12 @@ export const defaultPaymentReceive = {
// Holds the payment number that entered manually only. // Holds the payment number that entered manually only.
payment_receive_no_manually: '', payment_receive_no_manually: '',
statement: '', statement: '',
full_amount: '', amount: '',
currency_code: '', currency_code: '',
branch_id: '', branch_id: '',
exchange_rate: 1, exchange_rate: 1,
entries: [], entries: [],
attachments: [] attachments: [],
}; };
export const defaultRequestPaymentEntry = { 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. * Detarmines whether the payment has foreign customer.
* @returns {boolean} * @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;
};

View File

@@ -5,7 +5,6 @@ import '@/style/pages/PaymentReceive/List.scss';
import { DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import { PaymentReceivesListProvider } from './PaymentReceiptsListProvider'; import { PaymentReceivesListProvider } from './PaymentReceiptsListProvider';
import PaymentReceiveViewTabs from './PaymentReceiveViewTabs';
import PaymentReceivesTable from './PaymentReceivesTable'; import PaymentReceivesTable from './PaymentReceivesTable';
import PaymentReceiveActionsBar from './PaymentReceiveActionsBar'; import PaymentReceiveActionsBar from './PaymentReceiveActionsBar';
@@ -41,7 +40,6 @@ function PaymentReceiveList({
<PaymentReceiveActionsBar /> <PaymentReceiveActionsBar />
<DashboardPageContent> <DashboardPageContent>
<PaymentReceiveViewTabs />
<PaymentReceivesTable /> <PaymentReceivesTable />
</DashboardPageContent> </DashboardPageContent>
</PaymentReceivesListProvider> </PaymentReceivesListProvider>

View File

@@ -3,3 +3,7 @@
margin: 0 auto; margin: 0 auto;
padding: 0 40px; padding: 0 40px;
} }
.periodSwitch {
margin: 0;
}

View File

@@ -1,32 +1,65 @@
// @ts-nocheck // @ts-nocheck
import { AppToaster, Group, T } from '@/components';
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import * as R from 'ramda';
import { AppToaster } from '@/components';
import { useGetLemonSqueezyCheckout } from '@/hooks/query';
import { PricingPlan } from '@/components/PricingPlan/PricingPlan'; import { PricingPlan } from '@/components/PricingPlan/PricingPlan';
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
import {
WithPlansProps,
withPlans,
} from '@/containers/Subscriptions/withPlans';
interface SubscriptionPricingFeature {
text: string;
hint?: string;
hintLabel?: string;
style?: Record<string, string>;
}
interface SubscriptionPricingProps { interface SubscriptionPricingProps {
slug: string; slug: string;
label: string; label: string;
description: string; description: string;
features?: Array<String>; features?: Array<SubscriptionPricingFeature>;
featured?: boolean; featured?: boolean;
price: string; monthlyPrice: string;
pricePeriod: string; monthlyPriceLabel: string;
annuallyPrice: string;
annuallyPriceLabel: string;
monthlyVariantId?: string;
annuallyVariantId?: string;
} }
function SubscriptionPricing({ interface SubscriptionPricingCombinedProps
featured, extends SubscriptionPricingProps,
WithPlansProps {}
function SubscriptionPlanRoot({
label, label,
description, description,
featured,
features, features,
price, monthlyPrice,
pricePeriod, monthlyPriceLabel,
}: SubscriptionPricingProps) { annuallyPrice,
annuallyPriceLabel,
monthlyVariantId,
annuallyVariantId,
// #withPlans
plansPeriod,
}: SubscriptionPricingCombinedProps) {
const { mutateAsync: getLemonCheckout, isLoading } = const { mutateAsync: getLemonCheckout, isLoading } =
useGetLemonSqueezyCheckout(); useGetLemonSqueezyCheckout();
const handleClick = () => { const handleClick = () => {
getLemonCheckout({ variantId: '338516' }) const variantId =
SubscriptionPlansPeriod.Monthly === plansPeriod
? monthlyVariantId
: annuallyVariantId;
getLemonCheckout({ variantId })
.then((res) => { .then((res) => {
const checkoutUrl = res.data.data.attributes.url; const checkoutUrl = res.data.data.attributes.url;
window.LemonSqueezy.Url.Open(checkoutUrl); window.LemonSqueezy.Url.Open(checkoutUrl);
@@ -42,37 +75,34 @@ function SubscriptionPricing({
return ( return (
<PricingPlan featured={featured}> <PricingPlan featured={featured}>
{featured && <PricingPlan.Featured>Most Popular</PricingPlan.Featured>} {featured && <PricingPlan.Featured>Most Popular</PricingPlan.Featured>}
<PricingPlan.Header label={label} description={description} /> <PricingPlan.Header label={label} description={description} />
<PricingPlan.Price price={price} subPrice={pricePeriod} />
{plansPeriod === SubscriptionPlansPeriod.Monthly ? (
<PricingPlan.Price price={monthlyPrice} subPrice={monthlyPriceLabel} />
) : (
<PricingPlan.Price
price={annuallyPrice}
subPrice={annuallyPriceLabel}
/>
)}
<PricingPlan.BuyButton loading={isLoading} onClick={handleClick}> <PricingPlan.BuyButton loading={isLoading} onClick={handleClick}>
Subscribe Subscribe
</PricingPlan.BuyButton> </PricingPlan.BuyButton>
<PricingPlan.Features> <PricingPlan.Features>
{features?.map((feature) => ( {features?.map((feature) => (
<PricingPlan.FeatureLine>{feature}</PricingPlan.FeatureLine> <PricingPlan.FeatureLine
hintLabel={feature.hintLabel}
hintContent={feature.hint}
>
{feature.text}
</PricingPlan.FeatureLine>
))} ))}
</PricingPlan.Features> </PricingPlan.Features>
</PricingPlan> </PricingPlan>
); );
} }
export function SubscriptionPlans({ plans }) { export const SubscriptionPlan = R.compose(
return ( withPlans(({ plansPeriod }) => ({ plansPeriod })),
<Group spacing={18} noWrap align='stretch'> )(SubscriptionPlanRoot);
{plans.map((plan, index) => (
<SubscriptionPricing
key={index}
slug={plan.slug}
label={plan.name}
description={plan.description}
features={plan.features}
featured={plan.featured}
price={plan.price}
pricePeriod={plan.pricePeriod}
/>
))}
</Group>
);
}

View File

@@ -0,0 +1,28 @@
import { Group } from '@/components';
import { SubscriptionPlan } from './SubscriptionPlan';
import { useSubscriptionPlans } from './hooks';
export function SubscriptionPlans() {
const subscriptionPlans = useSubscriptionPlans();
return (
<Group spacing={14} noWrap align="stretch">
{subscriptionPlans.map((plan, index) => (
<SubscriptionPlan
key={index}
slug={plan.slug}
label={plan.name}
description={plan.description}
features={plan.features}
featured={plan.featured}
monthlyPrice={plan.monthlyPrice}
monthlyPriceLabel={plan.monthlyPriceLabel}
annuallyPrice={plan.annuallyPrice}
annuallyPriceLabel={plan.annuallyPriceLabel}
monthlyVariantId={plan.monthlyVariantId}
annuallyVariantId={plan.annuallyVariantId}
/>
))}
</Group>
);
}

View File

@@ -0,0 +1,46 @@
import { ChangeEvent } from 'react';
import * as R from 'ramda';
import { Intent, Switch, Tag, Text } from '@blueprintjs/core';
import { Group } from '@/components';
import withSubscriptionPlansActions, {
WithSubscriptionPlansActionsProps,
} from '@/containers/Subscriptions/withSubscriptionPlansActions';
import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer';
import styles from './SetupSubscription.module.scss';
interface SubscriptionPlansPeriodsSwitchCombinedProps
extends WithSubscriptionPlansActionsProps {}
function SubscriptionPlansPeriodSwitcherRoot({
// #withSubscriptionPlansActions
changeSubscriptionPlansPeriod,
}: SubscriptionPlansPeriodsSwitchCombinedProps) {
// Handles the period switch change.
const handleSwitchChange = (event: ChangeEvent<HTMLInputElement>) => {
changeSubscriptionPlansPeriod(
event.currentTarget.checked
? SubscriptionPlansPeriod.Annually
: SubscriptionPlansPeriod.Monthly,
);
};
return (
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}>
<Text>Pay Monthly</Text>
<Switch
large
onChange={handleSwitchChange}
className={styles.periodSwitch}
/>
<Text>
Pay Yearly{' '}
<Tag minimal intent={Intent.NONE}>
25% Off All Year
</Tag>
</Text>
</Group>
);
}
export const SubscriptionPlansPeriodSwitcher = R.compose(
withSubscriptionPlansActions,
)(SubscriptionPlansPeriodSwitcherRoot);

View File

@@ -1,29 +1,21 @@
// @ts-nocheck
import { Callout } from '@blueprintjs/core'; import { Callout } from '@blueprintjs/core';
import { SubscriptionPlans } from './SubscriptionPlan'; import { SubscriptionPlans } from './SubscriptionPlans';
import withPlans from '../../Subscriptions/withPlans'; import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
import { compose } from '@/utils';
/** /**
* Billing plans. * Billing plans.
*/ */
function SubscriptionPlansSectionRoot({ plans }) { export function SubscriptionPlansSection() {
return ( return (
<section> <section>
<Callout <Callout style={{ marginBottom: '2rem' }} icon={null}>
style={{ marginBottom: '1.5rem' }} Simple plans. Simple prices. Only pay for what you really need. All
icon={null} plans come with award-winning 24/7 customer support. Prices do not
title={'Early Adopter Plan'} include applicable taxes.
>
We're looking for 200 early adopters, when you subscribe you'll get the
full features and unlimited users for a year regardless of the
subscribed plan.
</Callout> </Callout>
<SubscriptionPlans plans={plans} />
<SubscriptionPlansPeriodSwitcher />
<SubscriptionPlans />
</section> </section>
); );
} }
export const SubscriptionPlansSection = compose(
withPlans(({ plans }) => ({ plans })),
)(SubscriptionPlansSectionRoot);

View File

@@ -0,0 +1,5 @@
import { SubscriptionPlans } from '@/constants/subscriptionModels';
export const useSubscriptionPlans = () => {
return SubscriptionPlans;
};

View File

@@ -1,17 +1,35 @@
// @ts-nocheck import { MapStateToProps, connect } from 'react-redux';
import { connect } from 'react-redux';
import { import {
getPlansPeriodSelector,
getPlansSelector, getPlansSelector,
} from '@/store/plans/plans.selectors'; } from '@/store/plans/plans.selectors';
import { ApplicationState } from '@/store/reducers';
export default (mapState) => { export interface WithPlansProps {
const mapStateToProps = (state, props) => { plans: ReturnType<ReturnType<typeof getPlansSelector>>;
plansPeriod: ReturnType<ReturnType<typeof getPlansPeriodSelector>>;
}
type MapState<Props> = (
mapped: WithPlansProps,
state: ApplicationState,
props: Props,
) => any;
export function withPlans<Props>(mapState?: MapState<Props>) {
const mapStateToProps: MapStateToProps<
WithPlansProps,
Props,
ApplicationState
> = (state, props) => {
const getPlans = getPlansSelector(); const getPlans = getPlansSelector();
const getPlansPeriod = getPlansPeriodSelector();
const mapped = { const mapped = {
plans: getPlans(state, props), plans: getPlans(state),
plansPeriod: getPlansPeriod(state),
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };
return connect(mapStateToProps); return connect(mapStateToProps);
}; }

View File

@@ -1,9 +1,22 @@
// @ts-nocheck import { MapDispatchToProps, connect } from 'react-redux';
import { connect } from 'react-redux'; import {
import { initSubscriptionPlans } from '@/store/plans/plans.actions'; SubscriptionPlansPeriod,
changePlansPeriod,
initSubscriptionPlans,
} from '@/store/plans/plans.reducer';
export const mapDispatchToProps = (dispatch) => ({ export interface WithSubscriptionPlansActionsProps {
initSubscriptionPlans: () => void;
changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) => void;
}
export const mapDispatchToProps: MapDispatchToProps<
WithSubscriptionPlansActionsProps,
{}
> = (dispatch: any) => ({
initSubscriptionPlans: () => dispatch(initSubscriptionPlans()), initSubscriptionPlans: () => dispatch(initSubscriptionPlans()),
changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) =>
dispatch(changePlansPeriod({ period })),
}); });
export default connect(null, mapDispatchToProps); export default connect(null, mapDispatchToProps);

View File

@@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components';
import { VendorsListProvider } from './VendorsListProvider'; import { VendorsListProvider } from './VendorsListProvider';
import VendorActionsBar from './VendorActionsBar'; import VendorActionsBar from './VendorActionsBar';
import VendorViewsTabs from './VendorViewsTabs';
import VendorsTable from './VendorsTable'; import VendorsTable from './VendorsTable';
import withVendors from './withVendors'; import withVendors from './withVendors';
@@ -42,7 +41,6 @@ function VendorsList({
<VendorActionsBar /> <VendorActionsBar />
<DashboardPageContent> <DashboardPageContent>
<VendorViewsTabs />
<VendorsTable /> <VendorsTable />
</DashboardPageContent> </DashboardPageContent>
</VendorsListProvider> </VendorsListProvider>

View File

@@ -3,7 +3,6 @@ import React from 'react';
import { DashboardPageContent } from '@/components'; import { DashboardPageContent } from '@/components';
import WarehouseTransfersActionsBar from './WarehouseTransfersActionsBar'; import WarehouseTransfersActionsBar from './WarehouseTransfersActionsBar';
import WarehouseTransfersViewTabs from './WarehouseTransfersViewTabs';
import WarehouseTransfersDataTable from './WarehouseTransfersDataTable'; import WarehouseTransfersDataTable from './WarehouseTransfersDataTable';
import withWarehouseTransfers from './withWarehouseTransfers'; import withWarehouseTransfers from './withWarehouseTransfers';
import withWarehouseTransfersActions from './withWarehouseTransfersActions'; import withWarehouseTransfersActions from './withWarehouseTransfersActions';
@@ -33,8 +32,8 @@ function WarehouseTransfersList({
tableStateChanged={warehouseTransferTableStateChanged} tableStateChanged={warehouseTransferTableStateChanged}
> >
<WarehouseTransfersActionsBar /> <WarehouseTransfersActionsBar />
<DashboardPageContent> <DashboardPageContent>
<WarehouseTransfersViewTabs />
<WarehouseTransfersDataTable /> <WarehouseTransfersDataTable />
</DashboardPageContent> </DashboardPageContent>
</WarehouseTransfersListProvider> </WarehouseTransfersListProvider>

View File

@@ -1,70 +1,46 @@
// @ts-nocheck import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { createReducer } from '@reduxjs/toolkit'; import { SubscriptionPlans } from '@/constants/subscriptionModels';
import t from '@/store/types';
const getSubscriptionPlans = () => [ export enum SubscriptionPlansPeriod {
{ Monthly = 'monthly',
name: 'Capital Basic', Annually = 'Annually',
slug: 'capital_basic', }
description: 'Good for service businesses that just started.',
features: [
'Sale Invoices and Estimates',
'Tracking Expenses',
'Customize Invoice',
'Manual Journals',
'Bank Reconciliation',
'Chart of Accounts',
'Taxes',
'Basic Financial Reports & Insights',
],
price: '$29',
pricePeriod: 'Per Year',
},
{
name: 'Capital Plus',
slug: 'capital_plus',
description:
'Good for businesses have inventory and want more financial reports.',
features: [
'All Capital Basic features',
'Manage Bills',
'Inventory Tracking',
'Multi Currencies',
'Predefined user roles.',
'Transactions locking.',
'Smart Financial Reports.',
],
price: '$29',
pricePeriod: 'Per Year',
featured: true,
},
{
name: 'Capital Big',
slug: 'essentials',
description: 'Good for businesses have multiple inventory or branches.',
features: [
'All Capital Plus features',
'Multiple Warehouses',
'Multiple Branches',
'Invite >= 15 Users',
],
price: '$29',
pricePeriod: 'Per Year',
},
];
const initialState = { interface StorePlansState {
plans: [], plans: any;
periods: [], plansPeriod: SubscriptionPlansPeriod;
}; }
export default createReducer(initialState, { export const SubscriptionPlansSlice = createSlice({
/** name: 'plans',
* Initialize the subscription plans. initialState: {
*/ plans: [],
[t.INIT_SUBSCRIPTION_PLANS]: (state) => { periods: [],
const plans = getSubscriptionPlans(); plansPeriod: 'monthly',
} as StorePlansState,
reducers: {
/**
* Initialize the subscription plans.
* @param {StorePlansState} state
*/
initSubscriptionPlans: (state: StorePlansState) => {
const plans = SubscriptionPlans;
state.plans = plans;
},
state.plans = plans; /**
* Changes the plans period (monthly or annually).
* @param {StorePlansState} state
* @param {PayloadAction<{ period: SubscriptionPlansPeriod }>} action
*/
changePlansPeriod: (
state: StorePlansState,
action: PayloadAction<{ period: SubscriptionPlansPeriod }>,
) => {
state.plansPeriod = action.payload.period;
},
}, },
}); });
export const { initSubscriptionPlans, changePlansPeriod } =
SubscriptionPlansSlice.actions;

View File

@@ -2,19 +2,21 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
const plansSelector = (state) => state.plans.plans; const plansSelector = (state) => state.plans.plans;
const planSelector = (state, props) => state.plans.plans const planSelector = (state, props) =>
.find((plan) => plan.slug === props.planSlug); state.plans.plans.find((plan) => plan.slug === props.planSlug);
const plansPeriodSelector = (state) => state.plans.plansPeriod;
// Retrieve manual jounral current page results. // Retrieve manual jounral current page results.
export const getPlansSelector = () => createSelector( export const getPlansSelector = () =>
plansSelector, createSelector(plansSelector, (plans) => {
(plans) => {
return plans; return plans;
}, });
);
// Retrieve plan details. // Retrieve plan details.
export const getPlanSelector = () => createSelector( export const getPlanSelector = () =>
planSelector, createSelector(planSelector, (plan) => plan);
(plan) => plan,
) // Retrieves the plans period (monthly or annually).
export const getPlansPeriodSelector = () =>
createSelector(plansPeriodSelector, (periods) => periods);

View File

@@ -32,13 +32,17 @@ import paymentMades from './PaymentMades/paymentMades.reducer';
import organizations from './organizations/organizations.reducers'; import organizations from './organizations/organizations.reducers';
import subscriptions from './subscription/subscription.reducer'; import subscriptions from './subscription/subscription.reducer';
import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer'; import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer';
import plans from './plans/plans.reducer'; import { SubscriptionPlansSlice } from './plans/plans.reducer';
import creditNotes from './CreditNote/creditNote.reducer'; import creditNotes from './CreditNote/creditNote.reducer';
import vendorCredit from './VendorCredit/VendorCredit.reducer'; import vendorCredit from './VendorCredit/VendorCredit.reducer';
import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer'; import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer';
import projects from './Project/projects.reducer'; import projects from './Project/projects.reducer';
import { PlaidSlice } from './banking/banking.reducer'; import { PlaidSlice } from './banking/banking.reducer';
export interface ApplicationState {
}
const appReducer = combineReducers({ const appReducer = combineReducers({
authentication, authentication,
organizations, organizations,
@@ -69,7 +73,7 @@ const appReducer = combineReducers({
paymentReceives, paymentReceives,
paymentMades, paymentMades,
inventoryAdjustments, inventoryAdjustments,
plans, plans: SubscriptionPlansSlice.reducer,
creditNotes, creditNotes,
vendorCredit, vendorCredit,
warehouseTransfers, warehouseTransfers,