Compare commits

...

116 Commits

Author SHA1 Message Date
elforjani13
43c3f5e3de feat: add easysms integrate api. 2021-12-01 19:24:02 +02:00
elforjani13
a99fc78fe1 feat: EasySMS integrate. 2021-11-29 20:28:28 +02:00
elforjani13
346696f673 fix: ability option key. 2021-11-27 20:23:40 +02:00
elforjani13
68227b81e8 fix: pause transactions locaking. 2021-11-27 19:44:06 +02:00
elforjani13
0c806366cd feat: handle error delete role. 2021-11-27 18:51:35 +02:00
a.bouhuolia
8b28d6894f feat: avoid display any dashboard before boot complete loading. 2021-11-27 17:53:54 +02:00
a.bouhuolia
3db00f6f70 feat: universal search permissions access control. 2021-11-27 17:22:29 +02:00
a.bouhuolia
56ab0a68e2 feat: add default role permissions in new mode. 2021-11-27 16:43:33 +02:00
elforjani13
4ac32b3aad feat: handle error roles table. 2021-11-27 13:52:37 +02:00
elforjani13
f98b429fcc feat: add localiztion. 2021-11-27 11:50:21 +02:00
a.bouhuolia
012f204c5c Merge branch 'featrue/roles-permission' into develop 2021-11-26 23:56:34 +02:00
a.bouhuolia
7c62466c5b Merge branch 'featrue/roles-permission' of https://github.com/bigcapitalhq/client into featrue/roles-permission 2021-11-26 23:38:49 +02:00
a.bouhuolia
111ade2ac8 feat: shortcuts keyword table permissions access control. 2021-11-26 23:38:39 +02:00
elforjani13
408e3cbb0b Merge branch 'featrue/roles-permission' of https://github.com/bigcapitalhq/client into featrue/roles-permission 2021-11-26 22:40:50 +02:00
elforjani13
9cc770f168 feat: edit user dialog handle error. 2021-11-26 22:40:06 +02:00
a.bouhuolia
383a9aad3b Merge branch 'featrue/roles-permission' of https://github.com/bigcapitalhq/client into featrue/roles-permission 2021-11-26 22:27:37 +02:00
a.bouhuolia
1be30fd142 feat: application and dashboard async booting. 2021-11-26 22:27:27 +02:00
elforjani13
63cb3f9fef fix: Abilities keys. 2021-11-26 20:56:00 +02:00
elforjani13
ca3ff3fd8f Merge branch 'featrue/roles-permission' of https://github.com/bigcapitalhq/client into featrue/roles-permission 2021-11-26 19:51:29 +02:00
elforjani13
313d0f3d0f fix: role field in invite & User dialog. 2021-11-26 19:48:19 +02:00
elforjani13
64bf223458 feat: handle errors. 2021-11-26 19:46:48 +02:00
elforjani13
97c421e2f1 feat: roles data & delete alert. 2021-11-26 19:43:06 +02:00
a.bouhuolia
ccad55dd4a feat: dashboard quick new access control. 2021-11-26 19:37:36 +02:00
a.bouhuolia
a21d70a59d feat: sidebar permission access control. 2021-11-26 19:07:18 +02:00
a.bouhuolia
c2ccb7f879 Merge branch 'featrue/roles-permission' of https://github.com/bigcapitalhq/client into featrue/roles-permission 2021-11-26 16:13:51 +02:00
a.bouhuolia
fe9ca215ab feat: handle forbidden request error. 2021-11-26 16:11:42 +02:00
a.bouhuolia
c14b35356b feat: dashboard meta boot and authenticated user request query. 2021-11-26 16:09:42 +02:00
a.bouhuolia
6fd8a24802 feat: dashboard meta boot. 2021-11-26 16:08:49 +02:00
a.bouhuolia
80531b7fdb feat: save @casl package dependency. 2021-11-25 22:54:16 +02:00
elforjani13
600a835dad feat: home page ability. 2021-11-25 16:03:40 +02:00
elforjani13
3dff8763d4 feat: ability reports home page. 2021-11-25 15:20:44 +02:00
elforjani13
e197d66d9f Merge branch 'feature/draft' into featrue/roles-permission 2021-11-25 14:37:09 +02:00
elforjani13
5dfb592ecc feat: ability context. 2021-11-25 14:36:34 +02:00
elforjani13
2630e0235d feat: add cashflow & vendor ability. 2021-11-25 13:02:09 +02:00
elforjani13
8b4dfe4ded feat: add role name in edit & invite user dialog. 2021-11-25 12:21:29 +02:00
elforjani13
9ceee6d02e feat: reports abilities. 2021-11-24 14:35:38 +02:00
elforjani13
553334f063 feat: ability home page option. 2021-11-24 14:09:24 +02:00
elforjani13
aef8eb7907 feat: empty status ability. 2021-11-24 11:47:18 +02:00
elforjani13
cc1f4cc26b feat: ability option. 2021-11-23 22:38:16 +02:00
elforjani13
719302b241 feat: add expense ability. 2021-11-23 22:15:02 +02:00
elforjani13
3db52e9c63 feat: add manual journal ability. 2021-11-23 21:56:36 +02:00
elforjani13
7393d68b7a feat: add account ability. 2021-11-23 21:42:16 +02:00
elforjani13
6ec86d3cf7 feat: add customer & vendor ability. 2021-11-23 21:26:34 +02:00
elforjani13
1cba4b5f18 feat: add bill & payment made ability. 2021-11-23 20:29:35 +02:00
elforjani13
3a8e1f5238 feat: add payment receive ability. 2021-11-23 20:15:29 +02:00
elforjani13
371e374dc5 feat: add receipt ability. 2021-11-23 20:08:05 +02:00
elforjani13
c2650c76e8 feat: add invoice ability. 2021-11-23 20:03:26 +02:00
elforjani13
fc74346695 feat: add estimate ability. 2021-11-23 19:37:49 +02:00
elforjani13
fca4dedeac feat: add item & inventory adjustment ability. 2021-11-23 19:29:58 +02:00
elforjani13
e5d02043ad feat: item & inventory. 2021-11-23 16:51:40 +02:00
elforjani13
afee2e90e0 ability. 2021-11-23 15:24:53 +02:00
elforjani13
d45005d8c2 fix: delete role alert. 2021-11-22 19:58:43 +02:00
elforjani13
15e7f34879 feat: delete & edit role. 2021-11-22 19:53:40 +02:00
elforjani13
a54ddf27c7 feat: delete & edit role. 2021-11-21 23:56:29 +02:00
elforjani13
955ae97c19 feat: add roles permission schema. 2021-11-21 18:21:10 +02:00
elforjani13
ddbadb67c8 feat: submit roles permission schema. 2021-11-21 17:10:49 +02:00
elforjani13
b853eb1e75 fix: roles form. 2021-11-21 01:45:14 +02:00
elforjani13
c139e129bf feat: roles permission & style & component. 2021-11-21 01:13:41 +02:00
elforjani13
3d3827b683 feat: add transactions locking icon. 2021-11-20 20:55:57 +02:00
a.bouhuolia
cf6d8d6038 feat: add hints to transactions locking item. 2021-11-20 20:40:09 +02:00
a.bouhuolia
d12b965bac feat: optimize transactions locking page style. 2021-11-20 20:38:02 +02:00
elforjani13
fe8f41f200 feat: Transactions locking. 2021-11-18 17:28:11 +02:00
elforjani13
b32abc0417 fix: fix localization. 2021-11-14 11:53:35 +02:00
a.bouhuolia
11d7029568 Merge branch 'develop' 2021-11-11 17:56:23 +02:00
a.bouhuolia
1990ce7562 fix: personal phone number placeholder. 2021-11-11 17:50:58 +02:00
a.bouhuolia
b6f0f6c2d5 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 17:20:57 +02:00
a.bouhuolia
4c58e49169 fix: SMS notification types. 2021-11-11 17:20:53 +02:00
elforjani13
376a16fd65 fix: force-width 2021-11-11 16:15:45 +02:00
elforjani13
918cd4aef3 feat: add display name defaultText 2021-11-11 15:39:42 +02:00
elforjani13
ec844637c3 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 15:23:52 +02:00
elforjani13
5803760c61 fix: rowClassNames. 2021-11-11 15:23:23 +02:00
a.bouhuolia
2e34df5d63 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 12:47:14 +02:00
a.bouhuolia
35d755e417 fix: SMS notification messages context menu. 2021-11-11 12:46:58 +02:00
elforjani13
66641ca56e fix: rename in eng file. 2021-11-11 12:13:14 +02:00
a.bouhuolia
307aaf0aa4 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 11:27:06 +02:00
a.bouhuolia
eb5a82d413 feat: optimize SMS notifications RTL. 2021-11-11 11:26:49 +02:00
elforjani13
ce9169b24d fix: add calloutCodes. 2021-11-11 11:19:58 +02:00
a.bouhuolia
22069f4795 feat: optimize Arabic localization of SMS notifications module. 2021-11-11 10:21:41 +02:00
a.bouhuolia
567b4da7e9 fix: merge conflict quick create list field. 2021-11-11 00:05:57 +02:00
a.bouhuolia
06345a5615 Merge branch 'feature/notify-via-SMS' into develop 2021-11-10 23:58:34 +02:00
a.bouhuolia
6b8178f643 feat: Reset to defailt SMS message. 2021-11-10 23:53:39 +02:00
a.bouhuolia
449ff724e1 Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-10 22:02:46 +02:00
a.bouhuolia
95e75f0e8f fix: notify invoice notification key query. 2021-11-10 22:02:00 +02:00
elforjani13
1a63ac69d8 fix: rename sms messages. 2021-11-10 21:31:28 +02:00
a.bouhuolia
da67217d74 feat: quick create action on select/suggest items fields. 2021-11-10 20:49:50 +02:00
elforjani13
e0c03141f0 fix: localization arabic. 2021-11-10 16:32:19 +02:00
elforjani13
4d563e3ddd feat: add localization arabic. 2021-11-10 15:17:17 +02:00
elforjani13
8bad78b0d3 fix: cashflow statement row_types. 2021-11-10 12:16:17 +02:00
a.bouhuolia
56fdf245d3 Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-09 18:19:24 +02:00
a.bouhuolia
5a8c61396f feat: SMS message preview with variables. 2021-11-09 18:16:22 +02:00
elforjani13
5fcf32dcaa feat add localization again. 2021-11-09 16:39:13 +02:00
elforjani13
acf457c0a0 Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-09 16:24:31 +02:00
elforjani13
e205c0b9a3 feat add localization. 2021-11-09 16:20:18 +02:00
a.bouhuolia
85f1c5584b feat: SMS notification handle response errors. 2021-11-09 13:56:59 +02:00
a.bouhuolia
9e5fddf294 feat: SMS notification handle errors. 2021-11-09 13:49:16 +02:00
a.bouhuolia
3039e43767 feat: SMS message text preview words break. 2021-11-09 12:41:31 +02:00
a.bouhuolia
7371557482 feat: optimize style of SMS notifications module. 2021-11-09 12:34:55 +02:00
a.bouhuolia
4b5e06f50c feat: SMS notification module. 2021-11-09 11:08:47 +02:00
a.bouhuolia
8daefb6946 fix: add notification id to sms messages templates table. 2021-11-09 09:57:12 +02:00
a.bouhuolia
6bf605f9ea Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-09 09:56:53 +02:00
a.bouhuolia
48221a7af1 feat: Optimize SMS notification module. 2021-11-09 09:51:38 +02:00
elforjani13
7a1c9caa70 feat: add context menu in sms message table. 2021-11-08 16:41:36 +02:00
elforjani13
8c2d138976 fix: disable sort in SMS integration table. 2021-11-08 16:10:54 +02:00
elforjani13
5b09d8279e feat: handle error sms messgae dialog. 2021-11-08 15:13:41 +02:00
elforjani13
92d8096f3a feat: add Invalidate queries. 2021-11-08 15:00:44 +02:00
elforjani13
adc6b336e0 fix: handle error. 2021-11-08 14:54:11 +02:00
elforjani13
6d67d6163d feat: handle error. 2021-11-08 13:20:49 +02:00
elforjani13
4d89f1e0e0 feat: add notify by sms . 2021-11-07 20:11:15 +02:00
elforjani13
7706d2992c feat: add notify via SMS Form. 2021-11-07 16:40:02 +02:00
elforjani13
6dcb98a438 feat: add preferneces menu. 2021-11-07 13:44:20 +02:00
elforjani13
834d365a97 feat: Add SMS Integration & SMS Message Form. 2021-11-07 13:39:29 +02:00
elforjani13
d26ef01afc feat: notify by SMS. 2021-11-06 21:47:17 +02:00
elforjani13
2bd4c5f724 fix: SMS message templates. 2021-11-06 00:08:25 +02:00
elforjani13
2c71d07512 feat: add localization. 2021-11-04 17:05:38 +02:00
elforjani13
17a4744e58 feat: add SMS message template. 2021-11-04 16:55:37 +02:00
elforjani13
46f6380fe6 feat: add notify via SMS. 2021-11-04 15:46:14 +02:00
301 changed files with 9542 additions and 2084 deletions

View File

@@ -10,6 +10,8 @@
"@blueprintjs/select": "^3.11.2",
"@blueprintjs/table": "^3.8.3",
"@blueprintjs/timezone": "^3.6.2",
"@casl/ability": "^5.4.3",
"@casl/react": "^2.3.0",
"@reduxjs/toolkit": "^1.2.5",
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",

161
src/common/abilityOption.js Normal file
View File

@@ -0,0 +1,161 @@
export const AbilitySubject = {
Item: 'Item',
InventoryAdjustment: 'InventoryAdjustment',
Estimate: 'SaleEstimate',
Invoice: 'SaleInvoice',
Receipt: 'SaleReceipt',
PaymentReceive: 'PaymentReceive',
Bill: 'Bill',
PaymentMade: 'PaymentMade',
Customer: 'Customer',
Vendor: 'Vendor',
Account: 'Account',
ManualJournal: 'ManualJournal',
Expense: 'Expense',
Cashflow: 'Cashflow',
Report: 'Report',
Preferences: 'Preferences',
ExchangeRate: 'ExchangeRate',
SubscriptionBilling: 'SubscriptionBilling',
};
export const ItemAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};
export const InventoryAdjustmentAction = {
Create: 'Create',
Edit: 'Edit',
View: 'View',
Delete: 'Delete',
};
export const SaleEstimateAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
NotifyBySms: 'NotifyBySms',
};
export const SaleInvoiceAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
Writeoff: 'bad-debt',
NotifyBySms: 'NotifyBySms',
};
export const SaleReceiptAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
NotifyBySms: 'NotifyBySms',
};
export const PaymentReceiveAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
NotifyBySms: 'NotifyBySms',
};
export const BillAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
NotifyBySms: 'NotifyBySms',
};
export const PaymentMadeAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};
export const CustomerAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};
export const VendorAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};
export const AccountAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
TransactionsLocking: 'TransactionsLocking',
};
export const ManualJournalAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
TransactionLocking: 'TransactionLocking',
};
export const ExpenseAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};
export const CashflowAction = {
View: 'View',
Create: 'Create',
Delete: 'Delete',
};
export const ReportsAction = {
ALL: 'all',
READ_BALANCE_SHEET: 'read-balance-sheet',
READ_TRIAL_BALANCE_SHEET: 'read-trial-balance-sheet',
READ_PROFIT_LOSS: 'read-profit-loss',
READ_JOURNAL: 'read-journal',
READ_GENERAL_LEDGET: 'read-general-ledger',
READ_CASHFLOW: 'read-cashflow',
READ_AR_AGING_SUMMARY: 'read-ar-aging-summary',
READ_AP_AGING_SUMMARY: 'read-ap-aging-summary',
READ_PURCHASES_BY_ITEMS: 'read-purchases-by-items',
READ_SALES_BY_ITEMS: 'read-sales-by-items',
READ_CUSTOMERS_TRANSACTIONS: 'read-customers-transactions',
READ_VENDORS_TRANSACTIONS: 'read-vendors-transactions',
READ_CUSTOMERS_SUMMARY_BALANCE: 'read-customers-summary-balance',
READ_VENDORS_SUMMARY_BALANCE: 'read-vendors-summary-balance',
READ_INVENTORY_VALUATION_SUMMARY: 'read-inventory-valuation-summary',
READ_INVENTORY_ITEM_DETAILS: 'read-inventory-item-details',
READ_CASHFLOW_ACCOUNT_TRANSACTION: 'read-cashflow-account-transactions',
};
export const PreferencesAbility = {
Mutate: 'Mutate',
};
export const ExchangeRateAbility = {
View: 'view',
Create: 'create',
Delete: 'delete',
};
export const SubscriptionBillingAbility = {
View: 'view',
Payment: 'payment',
};

View File

@@ -66,6 +66,8 @@ const CLASSES = {
PREFERENCES_PAGE_INSIDE_CONTENT_USERS: 'preferences-page__inside-content--users',
PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies',
PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT: 'preferences-page__inside-content--accountant',
PREFERENCES_PAGE_INSIDE_CONTENT_SMS_INTEGRATION: 'preferences-page__inside-content--sms-integration',
PREFERENCES_PAGE_INSIDE_CONTENT_ROLES_FORM: 'preferences-page__inside-content--roles-form',
FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report',

View File

@@ -10,4 +10,8 @@ export const DRAWERS = {
BILL_DRAWER: 'bill-drawer',
INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-drawer',
CASHFLOW_TRNASACTION_DRAWER: 'cashflow-transaction-drawer',
QUICK_WRITE_VENDOR: 'quick-write-vendor',
QUICK_CREATE_CUSTOMER: 'quick-create-customer',
QUICK_CREATE_ITEM: 'quick-create-item',
};

View File

@@ -1,5 +1,21 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import {
SaleInvoiceAction,
SaleEstimateAction,
AbilitySubject,
SaleReceiptAction,
CustomerAction,
PaymentReceiveAction,
BillAction,
VendorAction,
PaymentMadeAction,
AccountAction,
ManualJournalAction,
ExpenseAction,
ItemAction,
ReportsAction,
} from '../common/abilityOption';
export const accountsReceivable = [
{
@@ -9,21 +25,29 @@ export const accountsReceivable = [
title: <T id={'sales_invoices'} />,
description: <T id={'tracking_sales_invoices_with_your_customers'} />,
link: '/invoices',
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.View,
},
{
title: <T id={'sales_estimates'} />,
description: <T id={'manage_your_sales_estimates_to_create_quotes'} />,
link: '/estimates',
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.View,
},
{
title: <T id={'sales_receipts'} />,
description: <T id={'manage_sales_receipts_for_sales_that_get_paid'} />,
link: '/receipts',
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.View,
},
{
title: <T id={'customers'} />,
description: <T id={'manage_the_customers_relations_with_customer'} />,
link: '/customers',
subject: AbilitySubject.Customer,
ability: CustomerAction.View,
},
{
title: <T id={'customers_payments'} />,
@@ -31,6 +55,8 @@ export const accountsReceivable = [
<T id={'manage_payment_transactions_from_your_customers'} />
),
link: '/payment-receives',
subject: AbilitySubject.PaymentReceive,
ability: PaymentReceiveAction.View,
},
],
},
@@ -46,6 +72,8 @@ export const accountsPayable = [
<T id={'manage_the_purchase_invoices_with_your_vendors'} />
),
link: '/bills',
subject: AbilitySubject.Bill,
ability: BillAction.View,
},
{
title: <T id={'vendors'} />,
@@ -53,11 +81,15 @@ export const accountsPayable = [
<T id={'manage_the_vendors_relations_with_vendor_relations'} />
),
link: '/vendors',
subject: AbilitySubject.Vendor,
ability: VendorAction.View,
},
{
title: <T id={'vendors_payments'} />,
description: <T id={'manage_payments_transactions_to_your_vendors'} />,
link: '/payment-mades',
subject: AbilitySubject.PaymentMade,
ability: PaymentMadeAction.View,
},
],
},
@@ -77,21 +109,35 @@ export const financialAccounting = [
/>
),
link: '/accounts',
subject: AbilitySubject.Account,
ability: AccountAction.View,
},
{
title: <T id={'manual_journal'}/>,
description:<T id={'manage_manual_journal_transactions_on_accounts'}/>,
title: <T id={'manual_journal'} />,
description: (
<T id={'manage_manual_journal_transactions_on_accounts'} />
),
link: '/manual-journals',
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.View,
},
{
title: <T id={'expenses'}/>,
description:<T id={'track_your_indirect_expenses_under_specific_categories'}/>,
title: <T id={'expenses'} />,
description: (
<T id={'track_your_indirect_expenses_under_specific_categories'} />
),
link: '/expenses',
subject: AbilitySubject.Expense,
ability: ExpenseAction.View,
},
{
title: <T id={'financial_statements'}/>,
description:<T id={'show_financial_reports_about_your_organization'}/>,
title: <T id={'financial_statements'} />,
description: (
<T id={'show_financial_reports_about_your_organization'} />
),
link: '/financial-reports',
subject: AbilitySubject.Report,
ability: ReportsAction.ALL,
},
],
},
@@ -102,19 +148,27 @@ export const productsServices = [
sectionTitle: <T id={'products_services_inventory'} />,
shortcuts: [
{
title: <T id={'products_services'}/>,
description:<T id={'manage_your_products_inventory_or_non_inventory'}/>,
title: <T id={'products_services'} />,
description: (
<T id={'manage_your_products_inventory_or_non_inventory'} />
),
link: '/items',
subject: AbilitySubject.Item,
ability: ItemAction.View,
},
{
title: <T id={'products_services_categories'}/>,
description:<T id={'group_your_products_and_service'}/>,
title: <T id={'products_services_categories'} />,
description: <T id={'group_your_products_and_service'} />,
link: 'items/categories',
},
{
title: <T id={'inventory_adjustments'}/>,
description: <T id={'manage_your_inventory_adjustment_of_inventory_items'}/>,
title: <T id={'inventory_adjustments'} />,
description: (
<T id={'manage_your_inventory_adjustment_of_inventory_items'} />
),
link: '/inventory-adjustments',
subject: AbilitySubject.InventoryAdjustment,
ability: SaleInvoiceAction.View,
},
],
},

View File

@@ -1,110 +1,228 @@
import React from 'react';
import intl from 'react-intl-universal';
import {
AbilitySubject,
AccountAction,
BillAction,
CashflowAction,
CustomerAction,
ExpenseAction,
ItemAction,
ManualJournalAction,
ReportsAction,
SaleEstimateAction,
SaleInvoiceAction,
SaleReceiptAction,
VendorAction,
} from './abilityOption';
export default [
{
shortcut_key: 'Shift + I',
description: intl.get('jump_to_the_invoices'),
permission: {
ability: SaleInvoiceAction.View,
subject: AbilitySubject.Invoice,
},
},
{
shortcut_key: 'Shift + E',
description: intl.get('jump_to_the_estimates'),
permission: {
ability: SaleEstimateAction.View,
subject: AbilitySubject.Estimate,
},
},
{
shortcut_key: 'Shift + R',
description: intl.get('jump_to_the_receipts'),
permission: {
ability: SaleReceiptAction.View,
subject: AbilitySubject.Receipt,
},
},
{
shortcut_key: 'Shift + X',
description: intl.get('jump_to_the_expenses'),
permission: {
ability: ExpenseAction.View,
subject: AbilitySubject.Expense,
},
},
{
shortcut_key: 'Shift + C',
description: intl.get('jump_to_the_customers'),
permission: {
ability: CustomerAction.View,
subject: AbilitySubject.Customer,
},
},
{
shortcut_key: 'Shift + V',
description: intl.get('jump_to_the_vendors'),
permission: {
ability: VendorAction.View,
subject: AbilitySubject.Vendor,
},
},
{
shortcut_key: 'Shift + A',
description: intl.get('jump_to_the_chart_of_accounts'),
permission: {
ability: AccountAction.View,
subject: AbilitySubject.Account,
},
},
{
shortcut_key: 'Shift + B',
description: intl.get('jump_to_the_bills'),
permission: {
ability: BillAction.View,
subject: AbilitySubject.Bill,
},
},
{
shortcut_key: 'Shift + M',
description: intl.get('jump_to_the_manual_journals'),
permission: {
ability: ManualJournalAction.View,
subject: AbilitySubject.ManualJournal,
},
},
{
shortcut_key: 'Shift + W',
description: intl.get('jump_to_the_items'),
permission: {
ability: ItemAction.View,
subject: AbilitySubject.Item,
},
},
{
shortcut_key: 'Shift + D',
description: intl.get('jump_to_the_add_money_in'),
permission: {
ability: CashflowAction.Create,
subject: AbilitySubject.Cashflow,
},
},
{
shortcut_key: 'Shift + Q',
description: intl.get('jump_to_the_add_money_out'),
permission: {
ability: CashflowAction.Create,
subject: AbilitySubject.Cashflow,
},
},
{
shortcut_key: 'Shift + 1',
description: intl.get('jump_to_the_balance_sheet'),
permission: {
ability: ReportsAction.READ_BALANCE_SHEET,
subject: AbilitySubject.Report,
},
},
{
shortcut_key: 'Shift + 2',
description: intl.get('jump_to_the_profit_loss_sheet'),
permission: {
ability: ReportsAction.READ_PROFIT_LOSS,
subject: AbilitySubject.Report,
},
},
{
shortcut_key: 'Shift + 3',
description: intl.get('jump_to_the_journal_sheet'),
permission: {
ability: ReportsAction.READ_JOURNAL,
subject: AbilitySubject.Report,
},
},
{
shortcut_key: 'Shift + 4',
description: intl.get('jump_to_the_general_ledger_sheet'),
permission: {
ability: ReportsAction.READ_GENERAL_LEDGET,
subject: AbilitySubject.Report,
},
},
{
shortcut_key: 'Shift + 5',
description: intl.get('jump_to_the_trial_balance_sheet'),
permission: {
ability: ReportsAction.READ_TRIAL_BALANCE_SHEET,
subject: AbilitySubject.Report,
},
},
{
shortcut_key: 'Ctrl + Shift + I ',
description: intl.get('create_a_new_invoice'),
permission: {
ability: SaleInvoiceAction.Create,
subject: AbilitySubject.Invoice,
},
},
{
shortcut_key: 'Ctrl + Shift + E ',
description: intl.get('create_a_new_estimate'),
permission: {
ability: SaleEstimateAction.Create,
subject: AbilitySubject.Estimate,
},
},
{
shortcut_key: 'Ctrl + Shift + R ',
description: intl.get('create_a_new_receipt'),
permission: {
ability: SaleReceiptAction.Create,
subject: AbilitySubject.Receipt,
},
},
{
shortcut_key: 'Ctrl + Shift + X ',
description: intl.get('create_a_new_expense'),
permission: {
ability: ExpenseAction.Create,
subject: AbilitySubject.Expense,
},
},
{
shortcut_key: 'Ctrl + Shift + C ',
description: intl.get('create_a_new_customer'),
permission: {
ability: CustomerAction.Create,
subject: AbilitySubject.Customer,
},
},
{
shortcut_key: 'Ctrl + Shift + V ',
description: intl.get('create_a_new_vendor'),
permission: {
ability: VendorAction.Create,
subject: AbilitySubject.Vendor,
},
},
{
shortcut_key: 'Ctrl + Shift + B ',
description: intl.get('create_a_new_bill'),
permission: {
ability: BillAction.Create,
subject: AbilitySubject.Bill,
},
},
{
shortcut_key: 'Ctrl + Shift + M ',
description: intl.get('create_a_new_journal'),
permission: {
ability: ManualJournalAction.Create,
subject: AbilitySubject.ManualJournal,
},
},
{
shortcut_key: 'Ctrl + Shift + W ',
description: intl.get('create_a_new_item'),
permission: {
ability: ItemAction.Create,
subject: AbilitySubject.Item,
},
},
{
shortcut_key: 'Ctrl + / ',

View File

@@ -1,10 +1,71 @@
import intl from 'react-intl-universal';
import {
AbilitySubject,
SaleInvoiceAction,
CustomerAction,
VendorAction,
ManualJournalAction,
ExpenseAction,
} from '../common/abilityOption';
import { useAbilitiesFilter } from '../hooks';
export const getQuickNewActions = () => [
{ path: 'invoices/new', name: intl.get('sale_invoice') },
{ path: 'bills/new', name: intl.get('purchase_invoice') },
{ path: 'make-journal-entry', name: intl.get('manual_journal') },
{ path: 'expenses/new', name: intl.get('expense') },
{ path: 'customers/new', name: intl.get('customer') },
{ path: 'vendors/new', name: intl.get('vendor') },
{
path: 'invoices/new',
name: intl.get('sale_invoice'),
permission: {
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.Create,
},
},
{
path: 'bills/new',
name: intl.get('purchase_invoice'),
permission: {
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.Create,
},
},
{
path: 'make-journal-entry',
name: intl.get('manual_journal'),
permission: {
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.Create,
},
},
{
path: 'expenses/new',
name: intl.get('expense'),
permission: {
subject: AbilitySubject.Expense,
ability: ExpenseAction.Create,
},
},
{
path: 'customers/new',
name: intl.get('customer'),
permission: {
subject: AbilitySubject.Customer,
ability: CustomerAction.Create,
},
},
{
path: 'vendors/new',
name: intl.get('vendor'),
permission: {
subject: AbilitySubject.Vendor,
ability: VendorAction.Vendor,
},
},
];
/**
* Retrieve the dashboard quick new menu items.
*/
export const useGetQuickNewMenu = () => {
const quickNewMenu = getQuickNewActions();
const abilitiesFilter = useAbilitiesFilter();
return abilitiesFilter(quickNewMenu);
};

View File

@@ -1,13 +1,56 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import * as R from 'ramda';
import classNames from 'classnames';
import intl from 'react-intl-universal'
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils';
import { CLASSES } from 'common/classes';
export default function AccountsSelectList({
import withDialogActions from 'containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts select list.
*/
function AccountsSelectList({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
@@ -21,6 +64,8 @@ export default function AccountsSelectList({
filterByNormal,
filterByRootTypes,
allowCreate,
buttonProps = {},
}) {
const flattenAccounts = useMemo(
@@ -51,6 +96,7 @@ export default function AccountsSelectList({
[initialAccountId, filteredAccounts],
);
// Select account item.
const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null,
);
@@ -76,31 +122,25 @@ export default function AccountsSelectList({
);
}, []);
const onAccountSelect = useCallback(
// Handle the account item select.
const handleAccountSelect = useCallback(
(account) => {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
},
[setSelectedAccount, onAccountSelected],
);
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
if (account.id) {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
return (
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
openDialog('account-form');
}
},
[],
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={filteredAccounts}
@@ -113,11 +153,13 @@ export default function AccountsSelectList({
inline: popoverFill,
}}
filterable={true}
onItemSelect={onAccountSelect}
onItemSelect={handleAccountSelect}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
>
<Button
disabled={disabled}
@@ -127,3 +169,5 @@ export default function AccountsSelectList({
</Select>
);
}
export default R.compose(withDialogActions)(AccountsSelectList);

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
@@ -10,10 +11,55 @@ import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle input value renderer.
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts suggest field.
*/
export default function AccountsSuggestField({
function AccountsSuggestField({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
@@ -26,6 +72,8 @@ export default function AccountsSuggestField({
filterByNormal,
filterByRootTypes = [],
allowCreate,
...suggestProps
}) {
const flattenAccounts = useMemo(
@@ -69,23 +117,6 @@ export default function AccountsSuggestField({
}
}, [selectedAccountId, filteredAccounts, setSelectedAccount]);
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
}
},
[],
);
// Account item of select accounts field.
const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
return (
@@ -98,28 +129,31 @@ export default function AccountsSuggestField({
);
}, []);
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
const onAccountSelect = useCallback(
const handleAccountSelect = useCallback(
(account) => {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
if (account.id) {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
openDialog('account-form');
}
},
[setSelectedAccount, onAccountSelected],
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Suggest
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
onItemSelect={onAccountSelect}
onItemSelect={handleAccountSelect}
selectedItem={selectedAccount}
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
@@ -129,7 +163,11 @@ export default function AccountsSuggestField({
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
{...suggestProps}
/>
);
}
export default R.compose(withDialogActions)(AccountsSuggestField);

View File

@@ -1,23 +1,13 @@
import styled from 'styled-components';
import { Button } from '@blueprintjs/core';
export const ButtonLink = styled(Button)`
line-height: inherit;
export const ButtonLink = styled.button`
color: #0052cc;
border: 0;
background: transparent;
cursor: pointer;
&.bp3-small {
min-height: auto;
min-width: auto;
padding: 0;
}
&:not([class*='bp3-intent-']) {
&,
&:hover {
color: #0052cc;
background: transparent;
}
&:hover {
text-decoration: underline;
}
&:hover,
&:active {
text-decoration: underline;
}
`;

View File

@@ -11,12 +11,14 @@ export default function ContactSelecetList({
contactsList,
initialContactId,
selectedContactId,
selectedContactType,
createNewItemFrom,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const contacts = useMemo(
() =>
@@ -65,7 +67,7 @@ export default function ContactSelecetList({
);
// Filter Contact List
const filterContacts = (query, contact, index, exactMatch) => {
const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
@@ -83,7 +85,7 @@ export default function ContactSelecetList({
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={filterContacts}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={onContactSelect}
@@ -92,8 +94,9 @@ export default function ContactSelecetList({
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_')
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}

View File

@@ -0,0 +1,86 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { itemPredicate, handleContactRenderer } from './utils';
export default function ContactSelectField({
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,116 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
itemPredicate,
handleContactRenderer,
createNewItemRenderer,
createNewItemFromQuery,
} from './utils';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
function CustomerSelectField({
// #withDrawerActions
openDrawer,
// #ownProps
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
allowCreate,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
if (contact.id) {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
} else {
openDrawer(DRAWERS.QUICK_CREATE_CUSTOMER);
}
},
[setSelectedContact, onContactSelected, openDrawer],
);
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(CustomerSelectField);

View File

@@ -0,0 +1,115 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
itemPredicate,
handleContactRenderer,
createNewItemFromQuery,
createNewItemRenderer,
} from './utils';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
function VendorSelectField({
// #withDrawerActions
openDrawer,
// #ownProps
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
allowCreate,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
if (contact.id) {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
} else {
openDrawer(DRAWERS.QUICK_WRITE_VENDOR);
}
},
[setSelectedContact, onContactSelected, openDrawer],
);
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(VendorSelectField);

View File

@@ -0,0 +1,5 @@
import ContactSelectField from './ContactSelectField';
import CustomerSelectField from './CustomerSelectField';
import VendorSelectField from './VendorSelectField';
export { ContactSelectField, CustomerSelectField, VendorSelectField };

View File

@@ -0,0 +1,44 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
// Filter Contact List
export const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
}
};
export const handleContactRenderer = (contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={handleClick}
/>
);
// Creates a new item from query.
export const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle quick create new customer.
export const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};

View File

@@ -1,28 +0,0 @@
import React from 'react';
import { useUser } from 'hooks/query';
import withAuthentication from '../../containers/Authentication/withAuthentication';
const AuthenticatedUserContext = React.createContext();
function AuthenticatedUserComponent({ authenticatedUserId, children }) {
const { data: user, ...restProps } = useUser(authenticatedUserId);
return (
<AuthenticatedUserContext.Provider
value={{
user,
...restProps,
}}
children={children}
/>
);
}
export const AuthenticatedUser = withAuthentication(
({ authenticatedUserId }) => ({
authenticatedUserId,
}),
)(AuthenticatedUserComponent);
export const useAuthenticatedUser = () =>
React.useContext(AuthenticatedUserContext);

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { Ability } from '@casl/ability';
import { createContextualCan } from '@casl/react';
import { useDashboardMeta } from '../../hooks/query';
export const AbilityContext = React.createContext();
export const Can = createContextualCan(AbilityContext.Consumer);
/**
* Dashboard ability provider.
*/
export function DashboardAbilityProvider({ children }) {
const {
data: { abilities },
} = useDashboardMeta();
// Ability instance.
const ability = new Ability(abilities);
return (
<AbilityContext.Provider value={ability}>
{children}
</AbilityContext.Provider>
);
}

View File

@@ -1,18 +1,53 @@
import React from 'react';
import * as R from 'ramda';
import { useUser, useCurrentOrganization } from '../../hooks/query';
import {
useAuthenticatedAccount,
useCurrentOrganization,
useDashboardMeta,
} from '../../hooks/query';
import { useSplashLoading } from '../../hooks/state';
import { useWatch, useWatchImmediate, useWhen } from '../../hooks';
import withAuthentication from '../../containers/Authentication/withAuthentication';
import { setCookie, getCookie } from '../../utils';
/**
* Dashboard async booting.
* Dashboard meta async booting.
*/
function DashboardBootJSX({ authenticatedUserId }) {
export function useDashboardMetaBoot() {
const {
data: dashboardMeta,
isLoading: isDashboardMetaLoading,
isSuccess: isDashboardMetaSuccess,
} = useDashboardMeta({
keepPreviousData: true,
});
const [startLoading, stopLoading] = useSplashLoading();
useWatchImmediate((value) => {
value && startLoading();
}, isDashboardMetaLoading);
useWatchImmediate(() => {
isDashboardMetaSuccess && stopLoading();
}, isDashboardMetaSuccess);
return {
isLoading: isDashboardMetaLoading,
};
}
/**
* Dashboard async booting.
* @returns {{ isLoading: boolean }}
*/
export function useDashboardBoot() {
const { isLoading } = useDashboardMetaBoot();
return { isLoading };
}
/**
* Application async booting.
*/
export function useApplicationBoot() {
// Fetches the current user's organization.
const {
isSuccess: isCurrentOrganizationSuccess,
@@ -22,7 +57,7 @@ function DashboardBootJSX({ authenticatedUserId }) {
// Authenticated user.
const { isSuccess: isAuthUserSuccess, isLoading: isAuthUserLoading } =
useUser(authenticatedUserId);
useAuthenticatedAccount();
// Initial locale cookie value.
const localeCookie = getCookie('locale');
@@ -86,11 +121,8 @@ function DashboardBootJSX({ authenticatedUserId }) {
isBooted.current = true;
},
);
return null;
}
export const DashboardBoot = R.compose(
withAuthentication(({ authenticatedUserId }) => ({
authenticatedUserId,
})),
)(DashboardBootJSX);
return {
isLoading: isOrgLoading || isAuthUserLoading,
};
}

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
import DashboardFooter from 'components/Dashboard/DashboardFooter';
import DashboardErrorBoundary from './DashboardErrorBoundary';
export default React.forwardRef(({}, ref) => {
@@ -11,7 +10,6 @@ export default React.forwardRef(({}, ref) => {
<div className="dashboard-content" id="dashboard" ref={ref}>
<DashboardTopbar />
<DashboardContentRoutes />
<DashboardFooter />
</div>
</ErrorBoundary>
);

View File

@@ -1,8 +1,16 @@
import React from 'react';
import { DashboardAbilityProvider } from '../../components';
import { useDashboardBoot } from './DashboardBoot';
/**
* Dashboard provider.
*/
export default function DashboardProvider({ children }) {
return children;
const { isLoading } = useDashboardBoot();
// Avoid display any dashboard component before complete booting.
if (isLoading) {
return null;
}
return <DashboardAbilityProvider>{children}</DashboardAbilityProvider>;
}

View File

@@ -23,6 +23,7 @@ import withDashboard from 'containers/Dashboard/withDashboard';
import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
import { useGetUniversalSearchTypeOptions } from '../../containers/UniversalSearch/utils';
function DashboardTopbarSubscriptionMessage() {
return (
@@ -142,11 +143,8 @@ function DashboardTopbar({
<Navbar class="dashboard__topbar-navbar">
<NavbarGroup>
<If condition={isSubscriptionActive}>
<Button
<DashboardQuickSearchButton
onClick={() => openGlobalSearch(true)}
className={Classes.MINIMAL}
icon={<Icon icon={'search-24'} iconSize={20} />}
text={<T id={'quick_find'} />}
/>
<QuickNewDropdown />
@@ -195,3 +193,23 @@ export default compose(
'main',
),
)(DashboardTopbar);
/**
* Dashboard quick search button.
*/
function DashboardQuickSearchButton({ ...rest }) {
const searchTypeOptions = useGetUniversalSearchTypeOptions();
// Can't continue if there is no any search type option.
if (searchTypeOptions.length <= 0) {
return null;
}
return (
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'search-24'} iconSize={20} />}
text={<T id={'quick_find'} />}
{...rest}
/>
);
}

View File

@@ -1,31 +1,15 @@
import React from 'react';
import * as R from 'ramda';
import { AuthenticatedUser } from './AuthenticatedUser';
import { DashboardBoot } from '../../components';
import withDashboard from '../../containers/Dashboard/withDashboard';
import { useApplicationBoot } from '../../components';
/**
* Private pages provider.
*/
function PrivatePagesProviderComponent({
splashScreenCompleted,
export function PrivatePagesProvider({
// #ownProps
children,
}) {
return (
<AuthenticatedUser>
<DashboardBoot />
const { isLoading } = useApplicationBoot();
{splashScreenCompleted ? children : null}
</AuthenticatedUser>
);
return <React.Fragment>{!isLoading ? children : null}</React.Fragment>;
}
export const PrivatePagesProvider = R.compose(
withDashboard(({ splashScreenCompleted }) => ({
splashScreenCompleted,
})),
)(PrivatePagesProviderComponent);

View File

@@ -14,10 +14,14 @@ import { firstLettersArgs } from 'utils';
import { useAuthActions } from 'hooks/state';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
import { useAuthenticatedUser } from './AuthenticatedUser';
import { useAuthenticatedAccount } from 'hooks/query'
import { compose } from 'utils';
/**
* Dashboard topbar user.
*/
function DashboardTopbarUser({
openDialog,
@@ -28,7 +32,7 @@ function DashboardTopbarUser({
const { setLogout } = useAuthActions();
// Retrieve authenticated user information.
const { user } = useAuthenticatedUser();
const { data: user } = useAuthenticatedAccount();
const onClickLogout = () => {
setLogout();

View File

@@ -1,3 +1,4 @@
export * from './SplashScreen';
export * from './DashboardBoot';
export * from './DashboardThemeProvider';
export * from './DashboardAbilityProvider';

View File

@@ -17,6 +17,8 @@ export default function AccountCellRenderer({
accountsDataProp,
filterAccountsByRootTypes,
filterAccountsByTypes,
fieldProps,
formGroupProps,
},
row: { index, original },
cell: { value: initialValue },
@@ -53,6 +55,7 @@ export default function AccountCellRenderer({
'form-group--account',
Classes.FILL,
)}
{...formGroupProps}
>
<AccountsSuggestField
accounts={accounts}
@@ -66,6 +69,7 @@ export default function AccountCellRenderer({
}}
openOnKeyDown={true}
blurOnSelectClose={false}
{...fieldProps}
/>
</FormGroup>
);

View File

@@ -1,15 +1,17 @@
import React, { useCallback, useRef } from 'react';
// import ItemsListField from 'components/ItemsListField';
import ItemsSuggestField from 'components/ItemsSuggestField';
import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import ItemsSuggestField from 'components/ItemsSuggestField';
import { useCellAutoFocus } from 'hooks';
/**
* Items list cell.
*/
export default function ItemsListCell({
column: { id, filterSellable, filterPurchasable },
column: { id, filterSellable, filterPurchasable, fieldProps, formGroupProps },
row: { index },
cell: { value: initialValue },
payload: { items, updateData, errors, autoFocus },
@@ -19,6 +21,7 @@ export default function ItemsListCell({
// Auto-focus the items list input field.
useCellAutoFocus(fieldRef, autoFocus, id, index);
// Handle the item selected.
const handleItemSelected = useCallback(
(item) => {
updateData(index, id, item.id);
@@ -32,6 +35,7 @@ export default function ItemsListCell({
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames('form-group--select-list', Classes.FILL)}
{...formGroupProps}
>
<ItemsSuggestField
items={items}
@@ -45,6 +49,7 @@ export default function ItemsListCell({
}}
openOnKeyDown={true}
blurOnSelectClose={false}
{...fieldProps}
/>
</FormGroup>
);

View File

@@ -0,0 +1,51 @@
import React from 'react';
import classNames from 'classnames';
import { Classes, Switch, FormGroup, Intent } from '@blueprintjs/core';
import { safeInvoke } from 'utils';
/**
* Switch editable cell.
*/
const SwitchEditableCell = ({
row: { index, original },
column: { id, switchProps, onSwitchChange },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = React.useState(initialValue);
// Handle the switch change.
const onChange = (e) => {
const newValue = e.target.checked;
setValue(newValue);
safeInvoke(payload.updateData, index, id, newValue);
safeInvoke(onSwitchChange, e, newValue, original);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<Switch
value={value}
onChange={onChange}
checked={initialValue}
minimal={true}
className="ml2"
{...switchProps}
/>
</FormGroup>
);
};
export default SwitchEditableCell;

View File

@@ -0,0 +1,42 @@
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import { Classes, TextArea, FormGroup, Intent } from '@blueprintjs/core';
const TextAreaEditableCell = ({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<TextArea
growVertically={true}
large={true}
value={value}
onChange={onChange}
onBlur={onBlur}
fill={true}
/>
</FormGroup>
);
};
export default TextAreaEditableCell;

View File

@@ -6,7 +6,9 @@ import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell';
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
import NumericInputCell from './NumericInputCell';
import CheckBoxFieldCell from './CheckBoxFieldCell'
import CheckBoxFieldCell from './CheckBoxFieldCell';
import SwitchFieldCell from './SwitchFieldCell';
import TextAreaCell from './TextAreaCell';
export {
AccountsListFieldCell,
@@ -18,5 +20,7 @@ export {
DivFieldCell,
EmptyDiv,
NumericInputCell,
CheckBoxFieldCell
CheckBoxFieldCell,
SwitchFieldCell,
TextAreaCell,
};

View File

@@ -9,16 +9,16 @@ function DialogComponent(props) {
const { name, children, closeDialog, onClose } = props;
const handleClose = (event) => {
closeDialog(name)
closeDialog(name);
onClose && onClose(event);
};
return (
<Dialog {...props} onClose={handleClose}>
{ children }
{children}
</Dialog>
);
}
export default compose(
withDialogActions,
)(DialogComponent);
const DialogRoot = compose(withDialogActions)(DialogComponent);
export { DialogRoot as Dialog };

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Spinner, Classes } from '@blueprintjs/core';
import classNames from 'classnames';
export default function DialogContent(props) {
export function DialogContent(props) {
const { isLoading, children } = props;
const loadingContent = (

View File

@@ -0,0 +1,26 @@
import React from 'react';
import styled from 'styled-components';
import { Classes } from '@blueprintjs/core';
export function DialogFooterActions({ alignment = 'right', children }) {
return (
<DialogFooterActionsRoot
className={Classes.DIALOG_FOOTER_ACTIONS}
alignment={alignment}
>
{children}
</DialogFooterActionsRoot>
);
}
const DialogFooterActionsRoot = styled.div`
margin-left: -10px;
margin-right: -10px;
justify-content: ${(props) =>
props.alignment === 'right' ? 'flex-end' : 'flex-start'};
.bp3-button {
margin-left: 10px;
margin-left: 10px;
}
`;

View File

@@ -5,7 +5,7 @@ function LoadingContent() {
return (<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>);
}
export default function DialogSuspense({
export function DialogSuspense({
children
}) {
return (

View File

@@ -0,0 +1,6 @@
export * from './Dialog';
export * from './DialogFooterActions';
export * from './DialogSuspense';
export * from './DialogContent';

View File

@@ -20,6 +20,13 @@ import ReceiptPdfPreviewDialog from '../containers/Dialogs/ReceiptPdfPreviewDial
import MoneyInDialog from '../containers/Dialogs/MoneyInDialog';
import MoneyOutDialog from '../containers/Dialogs/MoneyOutDialog';
import BadDebtDialog from '../containers/Dialogs/BadDebtDialog';
import NotifyInvoiceViaSMSDialog from '../containers/Dialogs/NotifyInvoiceViaSMSDialog';
import NotifyReceiptViaSMSDialog from '../containers/Dialogs/NotifyReceiptViaSMSDialog';
import NotifyEstimateViaSMSDialog from '../containers/Dialogs/NotifyEstimateViaSMSDialog';
import NotifyPaymentReceiveViaSMSDialog from '../containers/Dialogs/NotifyPaymentReceiveViaSMSDialog';
import SMSMessageDialog from '../containers/Dialogs/SMSMessageDialog';
import TransactionsLockingDialog from '../containers/Dialogs/TransactionsLockingDialog';
import EasySMSIntegrationDialog from '../containers/Dialogs/EasySMSIntegrationDialog';
/**
* Dialogs container.
@@ -45,7 +52,16 @@ export default function DialogsContainer() {
<ReceiptPdfPreviewDialog dialogName={'receipt-pdf-preview'} />
<MoneyInDialog dialogName={'money-in'} />
<MoneyOutDialog dialogName={'money-out'} />
<NotifyInvoiceViaSMSDialog dialogName={'notify-invoice-via-sms'} />
<NotifyReceiptViaSMSDialog dialogName={'notify-receipt-via-sms'} />
<NotifyEstimateViaSMSDialog dialogName={'notify-estimate-via-sms'} />
<NotifyPaymentReceiveViaSMSDialog dialogName={'notify-payment-via-sms'} />
<BadDebtDialog dialogName={'write-off-bad-debt'} />
<SMSMessageDialog dialogName={'sms-message-form'} />
<TransactionsLockingDialog dialogName={'transactions-locking'} />
<EasySMSIntegrationDialog dialogName={'easysms-integrate'} />
</div>
);
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import ListSelect from "./ListSelect";
import intl from 'react-intl-universal';
import ListSelect from './ListSelect';
export default function DisplayNameList({
salutation,
@@ -9,25 +10,32 @@ export default function DisplayNameList({
...restProps
}) {
const formats = [
{ format: '{1} {2} {3}', values: [salutation, firstName, lastName], required: [1] },
{
format: '{1} {2} {3}',
values: [salutation, firstName, lastName],
required: [1],
},
{ format: '{1} {2}', values: [firstName, lastName], required: [] },
{ format: '{1}, {2}', values: [firstName, lastName], required: [1, 2] },
{ format: '{1}', values: [company], required: [1] }
{ format: '{1}', values: [company], required: [1] },
];
const formatOptions = formats
.filter((format) => !format.values.some((value, index) => {
return !value && format.required.indexOf(index + 1) !== -1;
}))
.filter(
(format) =>
!format.values.some((value, index) => {
return !value && format.required.indexOf(index + 1) !== -1;
}),
)
.map((formatOption) => {
const { format, values } = formatOption;
let label = format;
values.forEach((value, index) => {
const replaceWith = (value || '');
const replaceWith = value || '';
label = label.replace(`{${index + 1}}`, replaceWith).trim();
});
return { label: label.replace(/\s+/g, " ") };
return { label: label.replace(/\s+/g, ' ') };
});
return (
@@ -35,9 +43,9 @@ export default function DisplayNameList({
items={formatOptions}
selectedItemProp={'label'}
textProp={'label'}
defaultText={'Select display name as'}
defaultText={intl.get('select_display_name_as')}
filterable={false}
{ ...restProps }
{...restProps}
/>
);
}
}

View File

@@ -3,6 +3,7 @@ import { Position, Drawer } from '@blueprintjs/core';
import 'style/components/Drawer.scss';
import { DrawerProvider } from './DrawerProvider';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { compose } from 'utils';
@@ -27,7 +28,7 @@ function DrawerComponent(props) {
portalClassName={'drawer-portal'}
{...props}
>
{children}
<DrawerProvider {...props}>{children}</DrawerProvider>
</Drawer>
);
}

View File

@@ -0,0 +1,16 @@
import React, { createContext, useContext } from 'react';
const DrawerContext = createContext();
/**
* Account form provider.
*/
function DrawerProvider({ ...props }) {
const provider = { ...props };
return <DrawerContext.Provider value={provider} {...props} />;
}
const useDrawerContext = () => useContext(DrawerContext);
export { DrawerProvider, useDrawerContext };

View File

@@ -14,6 +14,9 @@ import CustomerDetailsDrawer from '../containers/Drawers/CustomerDetailsDrawer';
import VendorDetailsDrawer from '../containers/Drawers/VendorDetailsDrawer';
import InventoryAdjustmentDetailDrawer from '../containers/Drawers/InventoryAdjustmentDetailDrawer';
import CashflowTransactionDetailDrawer from '../containers/Drawers/CashflowTransactionDetailDrawer';
import QuickCreateCustomerDrawer from '../containers/Drawers/QuickCreateCustomerDrawer';
import QuickCreateItemDrawer from '../containers/Drawers/QuickCreateItemDrawer';
import QuickWriteVendorDrawer from '../containers/Drawers/QuickWriteVendorDrawer';
import { DRAWERS } from 'common/drawers';
@@ -38,7 +41,12 @@ export default function DrawersContainer() {
<InventoryAdjustmentDetailDrawer
name={DRAWERS.INVENTORY_ADJUSTMENT_DRAWER}
/>
<CashflowTransactionDetailDrawer name={DRAWERS.CASHFLOW_TRNASACTION_DRAWER} />
<CashflowTransactionDetailDrawer
name={DRAWERS.CASHFLOW_TRNASACTION_DRAWER}
/>
<QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} />
<QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} />
<QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} />
</div>
);
}

View File

@@ -1,13 +1,68 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { Suggest } from '@blueprintjs/select';
import classNames from 'classnames';
import * as R from 'ramda';
import intl from 'react-intl-universal';
import { CLASSES } from 'common/classes';
import { FormattedMessage as T } from 'components';
export default function ItemsSuggestField({
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
// Creates a new item from query.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle quick create new customer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};
// Item renderer.
const itemRenderer = (item, { modifiers, handleClick }) => (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={handleClick}
/>
);
// Filters items.
const filterItemsPredicater = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
}
};
// Handle input value renderer.
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
function ItemsSuggestField({
items,
initialItemId,
selectedItemId,
@@ -18,6 +73,10 @@ export default function ItemsSuggestField({
sellable = false,
purchasable = false,
popoverFill = false,
allowCreate = true,
openDrawer,
...suggestProps
}) {
// Filters items based on filter props.
@@ -36,28 +95,23 @@ export default function ItemsSuggestField({
// Find initial item object.
const initialItem = useMemo(
() => filteredItems.some((a) => a.id === initialItemId),
[initialItemId],
[initialItemId, filteredItems],
);
const [selectedItem, setSelectedItem] = useState(initialItem || null);
const onItemSelect = useCallback(
(item) => {
setSelectedItem({ ...item });
onItemSelected && onItemSelected(item);
if (item.id) {
setSelectedItem({ ...item });
onItemSelected && onItemSelected(item);
} else {
openDrawer(DRAWERS.QUICK_CREATE_ITEM);
}
},
[setSelectedItem, onItemSelected],
[setSelectedItem, onItemSelected, openDrawer],
);
const itemRenderer = useCallback((item, { modifiers, handleClick }) => (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={handleClick}
/>
));
useEffect(() => {
if (typeof selectedItemId !== 'undefined') {
const item = selectedItemId
@@ -67,27 +121,12 @@ export default function ItemsSuggestField({
}
}, [selectedItemId, filteredItems, setSelectedItem]);
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
// Filters items.
const filterItemsPredicater = useCallback(
(query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
}
},
[],
);
return (
<Suggest
items={filteredItems}
@@ -104,7 +143,12 @@ export default function ItemsSuggestField({
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...suggestProps}
/>
);
}
export default R.compose(withDrawerActions)(ItemsSuggestField);

View File

@@ -0,0 +1,36 @@
import React from 'react';
import {
Button,
Popover,
PopoverInteractionKind,
Position,
MenuItem,
Menu,
} from '@blueprintjs/core';
import { Icon, FormattedMessage as T } from 'components';
function MoreMenuItems({ payload: { onNotifyViaSMS } }) {
return (
<Popover
minimal={true}
content={
<Menu>
<MenuItem
onClick={onNotifyViaSMS}
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
/>
</Menu>
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
modifiers={{
offset: { offset: '0, 4' },
}}
>
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
</Popover>
);
}
export default MoreMenuItems;

View File

@@ -1,46 +0,0 @@
import React from 'react';
import {
Button,
PopoverInteractionKind,
MenuItem,
Position,
} from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { Icon } from 'components';
function MoreVertMenutItems({ text, items, onItemSelect, buttonProps }) {
// Menu items renderer.
const itemsRenderer = (item, { handleClick, modifiers, query }) => (
<MenuItem text={item.name} label={item.label} onClick={handleClick} />
);
const handleMenuSelect = (type) => {
onItemSelect && onItemSelect(type);
};
return (
<Select
items={items}
itemRenderer={itemsRenderer}
onItemSelect={handleMenuSelect}
popoverProps={{
minimal: true,
position: Position.BOTTOM_LEFT,
interactionKind: PopoverInteractionKind.CLICK,
modifiers: {
offset: { offset: '0, 4' },
},
}}
filterable={false}
>
<Button
text={text}
icon={<Icon icon={'more-vert'} iconSize={16} />}
minimal={true}
{...buttonProps}
/>
</Select>
);
}
export default MoreVertMenutItems;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import styled from 'styled-components';
import { Icon } from 'components';
/**
* SMS Message preview.
*/
export function SMSMessagePreview({
message,
iconWidth = '265px',
iconHeight = '287px',
iconColor = '#adadad',
}) {
return (
<SMSMessagePreviewBase>
<Icon
icon={'sms-message-preview'}
width={iconWidth}
height={iconHeight}
color={iconColor}
/>
<SMSMessageText>{message}</SMSMessageText>
</SMSMessagePreviewBase>
);
}
const SMSMessageText = styled.div`
position: absolute;
top: 60px;
padding: 12px;
color: #fff;
border-radius: 12px;
margin-left: 12px;
margin-right: 12px;
word-break: break-word;
background: #2fa2e4;
font-size: 13px;
line-height: 1.6;
`;
const SMSMessagePreviewBase = styled.div`
position: relative;
width: 265px;
margin: 0 auto;
`;

View File

@@ -2,16 +2,19 @@ import React from 'react';
import SidebarContainer from 'components/Sidebar/SidebarContainer';
import SidebarHead from 'components/Sidebar/SidebarHead';
import SidebarMenu from 'components/Sidebar/SidebarMenu';
import { useGetSidebarMenu } from './utils';
import 'style/containers/Dashboard/Sidebar.scss';
export default function Sidebar({ dashboardContentRef }) {
const menu = useGetSidebarMenu();
return (
<SidebarContainer>
<SidebarHead />
<div className="sidebar__menu">
<SidebarMenu />
<SidebarMenu menu={menu} />
</div>
<div class="sidebar__version">0.0.1-beta version.</div>

View File

@@ -3,7 +3,7 @@ import { Button, Popover, Menu, Position } from '@blueprintjs/core';
import Icon from 'components/Icon';
import { compose, firstLettersArgs } from 'utils';
import withCurrentOrganization from '../../containers/Organization/withCurrentOrganization';
import { useAuthenticatedUser } from '../Dashboard/AuthenticatedUser';
import { useAuthenticatedAccount } from '../../hooks/query';
// Popover modifiers.
const POPOVER_MODIFIERS = {
@@ -18,7 +18,7 @@ function SidebarHead({
organization,
}) {
// Retrieve authenticated user information.
const { user } = useAuthenticatedUser();
const { data: user } = useAuthenticatedAccount();
return (
<div className="sidebar__head">

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Menu, MenuDivider } from '@blueprintjs/core';
import { useHistory, useLocation } from 'react-router-dom';
import sidebarMenuList from 'config/sidebarMenu';
import { Choose } from 'components';
import Icon from 'components/Icon';
import MenuItem from 'components/MenuItem';
@@ -24,7 +24,7 @@ function SidebarMenuItemSpace({ space }) {
return <div class="bp3-menu-spacer" style={{ height: `${space}px` }} />;
}
function SidebarMenu({ isSubscriptionActive }) {
function SidebarMenu({ menu, isSubscriptionActive }) {
const history = useHistory();
const location = useLocation();
@@ -93,7 +93,7 @@ function SidebarMenu({ isSubscriptionActive }) {
});
};
const filterItems = sidebarMenuList.filter(
const filterItems = menu.filter(
(item) => isSubscriptionActive || item.enableBilling,
);
const items = menuItemsMapper(filterItems);

View File

@@ -0,0 +1,48 @@
import sidebarMenuList from 'config/sidebarMenu';
import { isArray, isEmpty } from 'lodash';
import { useAbilityContext } from 'hooks/utils';
export function useGetSidebarMenu() {
const ability = useAbilityContext();
return sidebarMenuList
.map((item) => {
const children = isArray(item.children)
? item.children.filter((childItem) => {
return isArray(childItem.permission)
? childItem.permission.some((perm) =>
ability.can(perm.ability, perm.subject),
)
: childItem?.permission?.ability && childItem?.permission?.subject
? ability.can(
childItem.permission.ability,
childItem.permission.subject,
)
: true;
})
: [];
return {
...item,
...(isArray(item.children)
? {
children,
}
: {}),
};
})
.filter((item) => {
return isArray(item.permission)
? item.permission.some((per) =>
ability.can(per.ability, per.subject),
)
: item?.permission?.ability && item?.permission?.subject
? ability.can(item.permission.ability, item.permission.subject)
: true;
})
.filter((item) =>
isEmpty(item.children) && !item.href && !item.label && !item.divider
? false
: true,
);
}

View File

@@ -23,9 +23,6 @@ import AccountsSelectList from './AccountsSelectList';
import AccountsTypesSelect from './AccountsTypesSelect';
import LoadingIndicator from './LoadingIndicator';
import DashboardActionViewsList from './Dashboard/DashboardActionViewsList';
import Dialog from './Dialog/Dialog';
import DialogContent from './Dialog/DialogContent';
import DialogSuspense from './Dialog/DialogSuspense';
import InputPrependButton from './Forms/InputPrependButton';
import CategoriesSelectList from './CategoriesSelectList';
import Row from './Grid/Row';
@@ -61,8 +58,9 @@ import Card from './Card';
import AvaterCell from './AvaterCell';
import { ItemsMultiSelect } from './Items';
import MoreVertMenutItems from './MoreVertMenutItems';
import MoreMenuItems from './MoreMenutItems';
export * from './Dialog';
export * from './Menu';
export * from './AdvancedFilter/AdvancedFilterDropdown';
export * from './AdvancedFilter/AdvancedFilterPopover';
@@ -83,10 +81,12 @@ export * from './MultiSelectTaggable';
export * from './Utils/FormatNumber';
export * from './Utils/FormatDate';
export * from './BankAccounts';
export * from './IntersectionObserver'
export * from './IntersectionObserver';
export * from './Datatable/CellForceWidth';
export * from './Button';
export * from './IntersectionObserver';
export * from './SMSPreview';
export * from './Contacts';
const Hint = FieldHint;
@@ -120,9 +120,6 @@ export {
LoadingIndicator,
DashboardActionViewsList,
AppToaster,
Dialog,
DialogContent,
DialogSuspense,
InputPrependButton,
CategoriesSelectList,
Col,
@@ -158,5 +155,5 @@ export {
ItemsMultiSelect,
Card,
AvaterCell,
MoreVertMenutItems,
MoreMenuItems,
};

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { ReportsAction, AbilitySubject } from '../common/abilityOption';
export const financialReportMenus = [
{
@@ -11,6 +12,8 @@ export const financialReportMenus = [
<T id={'reports_a_company_s_assets_liabilities_and_shareholders'} />
),
link: '/financial-reports/balance-sheet',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_BALANCE_SHEET,
},
{
title: <T id={'trial_balance_sheet'} />,
@@ -18,11 +21,15 @@ export const financialReportMenus = [
<T id={'summarizes_the_credit_and_debit_balance_of_each_account'} />
),
link: '/financial-reports/trial-balance-sheet',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_TRIAL_BALANCE_SHEET,
},
{
title: <T id={'profit_loss_report'} />,
desc: <T id={'reports_the_revenues_costs_and_expenses'} />,
link: '/financial-reports/profit-loss-sheet',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_PROFIT_LOSS,
},
{
title: <T id={'cash_flow_statement'} />,
@@ -30,16 +37,22 @@ export const financialReportMenus = [
<T id={'reports_inflow_and_outflow_of_cash_and_cash_equivalents'} />
),
link: '/financial-reports/cash-flow',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CASHFLOW,
},
{
title: <T id={'journal_report'} />,
desc: <T id={'the_debit_and_credit_entries_of_system_transactions'} />,
link: '/financial-reports/journal-sheet',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_JOURNAL,
},
{
title: <T id={'general_ledger_report'} />,
desc: <T id={'reports_every_transaction_going_in_and_out_of_your'} />,
link: '/financial-reports/general-ledger',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_GENERAL_LEDGET,
},
{
title: <T id={'receivable_aging_summary'} />,
@@ -47,11 +60,15 @@ export const financialReportMenus = [
<T id={'summarize_total_unpaid_balances_of_customers_invoices'} />
),
link: '/financial-reports/receivable-aging-summary',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_AR_AGING_SUMMARY,
},
{
title: <T id={'payable_aging_summary'} />,
desc: <T id={'summarize_total_unpaid_balances_of_vendors_purchase'} />,
link: '/financial-reports/payable-aging-summary',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_AP_AGING_SUMMARY,
},
],
},
@@ -71,6 +88,8 @@ export const SalesAndPurchasesReportMenus = [
/>
),
link: '/financial-reports/purchases-by-items',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_PURCHASES_BY_ITEMS,
},
{
title: <T id={'sales_by_items'} />,
@@ -82,6 +101,8 @@ export const SalesAndPurchasesReportMenus = [
/>
),
link: '/financial-reports/sales-by-items',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_SALES_BY_ITEMS,
},
{
title: <T id={'inventory_valuation'} />,
@@ -93,6 +114,8 @@ export const SalesAndPurchasesReportMenus = [
/>
),
link: '/financial-reports/inventory-valuation',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
},
{
title: <T id={'customers_balance_summary'} />,
@@ -104,6 +127,8 @@ export const SalesAndPurchasesReportMenus = [
/>
),
link: '/financial-reports/customers-balance-summary',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
},
{
title: <T id={'vendors_balance_summary'} />,
@@ -111,6 +136,8 @@ export const SalesAndPurchasesReportMenus = [
<T id={'summerize_the_total_amount_your_business_owes_each_vendor'} />
),
link: '/financial-reports/vendors-balance-summary',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE,
},
{
title: <T id={'customers_transactions'} />,
@@ -120,6 +147,8 @@ export const SalesAndPurchasesReportMenus = [
/>
),
link: '/financial-reports/transactions-by-customers',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
},
{
title: <T id={'vendors_transactions'} />,
@@ -131,6 +160,8 @@ export const SalesAndPurchasesReportMenus = [
/>
),
link: '/financial-reports/transactions-by-vendors',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_TRANSACTIONS,
},
{
title: <T id={'inventory_item_details'} />,
@@ -138,6 +169,8 @@ export const SalesAndPurchasesReportMenus = [
<T id={'reports_every_transaction_going_in_and_out_of_your_items'} />
),
link: '/financial-reports/inventory-item-details',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS,
},
],
},

View File

@@ -1,29 +1,34 @@
import React from 'react'
import { FormattedMessage as T } from 'components';
import React from 'react';
import { FormattedMessage as T } from 'components';
export default [
{
text: <T id={'general'}/>,
text: <T id={'general'} />,
disabled: false,
href: '/preferences/general',
},
{
text: <T id={'users'}/>,
text: <T id={'users'} />,
href: '/preferences/users',
},
{
text: <T id={'currencies'}/>,
text: <T id={'currencies'} />,
href: '/preferences/currencies',
},
{
text: <T id={'accountant'}/>,
text: <T id={'accountant'} />,
disabled: false,
href: '/preferences/accountant',
},
{
text: <T id={'items'}/>,
text: <T id={'items'} />,
disabled: false,
href: '/preferences/items',
},
{
text: <T id={'sms_integration.label'} />,
disabled: false,
href: '/preferences/sms-message',
},
];

View File

@@ -1,5 +1,26 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import {
ReportsAction,
AbilitySubject,
ItemAction,
InventoryAdjustmentAction,
SaleEstimateAction,
SaleInvoiceAction,
SaleReceiptAction,
PaymentReceiveAction,
BillAction,
PaymentMadeAction,
CustomerAction,
VendorAction,
AccountAction,
ManualJournalAction,
ExpenseAction,
CashflowAction,
PreferencesAbility,
ExchangeRateAbility,
SubscriptionBillingAbility,
} from '../common/abilityOption';
export default [
{
@@ -11,6 +32,32 @@ export default [
{
text: <T id={'sales_inventory'} />,
label: true,
permission: [
{
subject: AbilitySubject.Item,
ability: ItemAction.View,
},
{
subject: AbilitySubject.InventoryAdjustment,
ability: InventoryAdjustmentAction.View,
},
{
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.View,
},
{
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.View,
},
{
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.View,
},
{
subject: AbilitySubject.PaymentReceive,
ability: PaymentReceiveAction.View,
},
],
},
{
text: <T id={'items'} />,
@@ -18,37 +65,70 @@ export default [
{
text: <T id={'items'} />,
href: '/items',
permission: {
subject: AbilitySubject.Item,
ability: ItemAction.View,
},
},
{
text: <T id={'inventory_adjustments'} />,
href: '/inventory-adjustments',
permission: {
subject: AbilitySubject.InventoryAdjustment,
ability: InventoryAdjustmentAction.View,
},
},
{
text: <T id={'category_list'} />,
href: '/items/categories',
permission: {
subject: AbilitySubject.Item,
ability: ItemAction.View,
},
},
{
text: <T id={'New tasks'} />,
label: true,
permission: [
{
subject: AbilitySubject.Item,
ability: ItemAction.Create,
},
],
},
{
divider: true,
permission: [
{
subject: AbilitySubject.Item,
ability: ItemAction.Create,
},
],
},
{
text: <T id={'New inventory item'} />,
href: '/items/new',
permission: {
subject: AbilitySubject.Item,
ability: ItemAction.Create,
},
},
{
text: <T id={'New service'} />,
href: '/items/new',
permission: {
subject: AbilitySubject.Item,
ability: ItemAction.Create,
},
},
{
text: <T id={'New item category'} />,
href: '/items/categories/new',
permission: {
subject: AbilitySubject.Item,
ability: ItemAction.Create,
},
},
// {
// text: <T id={'New inventory adjustment'} />,
// },
],
},
{
@@ -57,43 +137,109 @@ export default [
{
text: <T id={'estimates'} />,
href: '/estimates',
newTabHref: '/estimates/new',
permission: {
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.View,
},
},
{
text: <T id={'invoices'} />,
href: '/invoices',
newTabHref: '/invoices/new',
permission: {
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.View,
},
},
{
text: <T id={'receipts'} />,
href: '/receipts',
permission: {
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.View,
},
},
{
text: <T id={'payment_receives'} />,
href: '/payment-receives',
permission: {
subject: AbilitySubject.PaymentReceive,
ability: PaymentReceiveAction.View,
},
},
{
text: <T id={'New tasks'} />,
label: true,
permission: [
{
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.Create,
},
{
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.Create,
},
{
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.Create,
},
{
subject: AbilitySubject.PaymentReceive,
ability: PaymentReceiveAction.Create,
},
],
},
{
divider: true,
permission: [
{
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.Create,
},
{
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.Create,
},
{
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.Create,
},
{
subject: AbilitySubject.PaymentReceive,
ability: PaymentReceiveAction.Create,
},
],
},
{
text: <T id={'new_estimate'} />,
href: '/estimates/new',
permission: {
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.Create,
},
},
{
text: <T id={'new_invoice'} />,
href: '/invoices/new',
permission: {
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.Create,
},
},
{
text: <T id={'new_receipt'} />,
href: '/receipts/new',
permission: {
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.Create,
},
},
{
text: <T id={'new_payment_receive'} />,
href: '/payment-receives/new',
permission: {
subject: AbilitySubject.PaymentReceive,
ability: PaymentReceiveAction.Create,
},
},
],
},
@@ -103,27 +249,62 @@ export default [
{
text: <T id={'bills'} />,
href: '/bills',
newTabHref: '/bills/new',
permission: {
subject: AbilitySubject.Bill,
ability: BillAction.View,
},
},
{
text: <T id={'payment_mades'} />,
href: '/payment-mades',
newTabHref: '/payment-mades/new',
permission: {
subject: AbilitySubject.PaymentMade,
ability: PaymentMadeAction.View,
},
},
{
text: <T id={'New tasks'} />,
label: true,
permission: [
{
subject: AbilitySubject.Bill,
ability: BillAction.Create,
},
{
subject: AbilitySubject.PaymentMade,
ability: PaymentMadeAction.Create,
},
],
},
{
divider: true,
permission: [
{
subject: AbilitySubject.Bill,
ability: BillAction.Create,
},
{
subject: AbilitySubject.PaymentMade,
ability: PaymentMadeAction.Create,
},
],
},
{
text: <T id={'New purchase invoice'} />,
href: '/bills/new',
permission: {
subject: AbilitySubject.Bill,
ability: BillAction.Create,
},
},
{
text: <T id={'new_payment_made'} />,
href: '/payment-mades/new',
permission: {
subject: AbilitySubject.PaymentMade,
ability: PaymentMadeAction.Create,
},
},
],
},
@@ -133,33 +314,77 @@ export default [
{
text: <T id={'customers'} />,
href: '/customers',
newTabHref: '/customers/new',
permission: {
subject: AbilitySubject.Customer,
ability: CustomerAction.View,
},
},
{
text: <T id={'vendors'} />,
href: '/vendors',
newTabHref: '/vendors/new',
permission: {
subject: AbilitySubject.Vendor,
ability: VendorAction.Create,
},
},
{
text: <T id={'New tasks'} />,
label: true,
permission: [
{
subject: AbilitySubject.Customer,
ability: CustomerAction.View,
},
{
subject: AbilitySubject.Vendor,
ability: VendorAction.View,
},
],
},
{
divider: true,
permission: [
{
subject: AbilitySubject.Customer,
ability: CustomerAction.View,
},
{
subject: AbilitySubject.Vendor,
ability: VendorAction.View,
},
],
},
{
text: <T id={'new_customer'} />,
href: '/customers/new',
permission: {
subject: AbilitySubject.Customer,
ability: CustomerAction.View,
},
},
{
text: <T id={'new_vendor'} />,
href: '/vendors/new',
permission: {
subject: AbilitySubject.Vendor,
ability: VendorAction.View,
},
},
],
},
{
text: <T id={'accounting'} />,
label: true,
permission: [
{
subject: AbilitySubject.Account,
ability: AccountAction.View,
},
{
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.View,
},
],
},
{
text: <T id={'financial'} />,
@@ -167,25 +392,57 @@ export default [
{
text: <T id={'accounts_chart'} />,
href: '/accounts',
permission: {
subject: AbilitySubject.Account,
ability: AccountAction.View,
},
},
{
text: <T id={'manual_journals'} />,
href: '/manual-journals',
permission: {
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.View,
},
},
// {
// text: <T id={'sidebar.transactions_locaking'} />,
// href: '/transactions-locking',
// permission: {
// subject: AbilitySubject.ManualJournal,
// ability: ManualJournalAction.TransactionLocking,
// },
// },
{
text: <T id={'exchange_rate'} />,
href: '/exchange-rates',
permission: {
subject: AbilitySubject.ExchangeRate,
ability: ExchangeRateAbility.View,
},
},
{
text: <T id={'New tasks'} />,
label: true,
permission: {
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.Create,
},
},
{
divider: true,
permission: {
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.Create,
},
},
{
text: <T id={'make_journal_entry'} />,
href: '/make-journal-entry',
permission: {
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.Create,
},
},
],
},
@@ -195,29 +452,61 @@ export default [
{
text: <T id={'siebar.cashflow.label_cash_and_bank_accounts'} />,
href: '/cashflow-accounts',
permission: {
subject: AbilitySubject.Cashflow,
ability: CashflowAction.View,
},
},
{
text: <T id={'New tasks'} />,
label: true,
permission: [
{
subject: AbilitySubject.Cashflow,
ability: CashflowAction.Create,
},
],
},
{
divider: true,
permission: [
{
subject: AbilitySubject.Cashflow,
ability: CashflowAction.Create,
},
],
},
{
text: <T id={'cash_flow.label.add_money_in'} />,
href: '/cashflow-accounts',
permission: {
subject: AbilitySubject.Cashflow,
ability: CashflowAction.Create,
},
},
{
text: <T id={'cash_flow.label.add_money_out'} />,
href: '/cashflow-accounts',
permission: {
subject: AbilitySubject.Cashflow,
ability: CashflowAction.Create,
},
},
{
text: <T id={'cash_flow.label.add_cash_account'} />,
href: '/cashflow-accounts',
permission: {
subject: AbilitySubject.Cashflow,
ability: CashflowAction.Create,
},
},
{
text: <T id={'cash_flow.label.add_bank_account'} />,
href: '/cashflow-accounts',
permission: {
subject: AbilitySubject.Cashflow,
ability: CashflowAction.Create,
},
},
],
},
@@ -227,17 +516,33 @@ export default [
{
text: <T id={'expenses'} />,
href: '/expenses',
permission: {
subject: AbilitySubject.Expense,
ability: ExpenseAction.View,
},
},
{
text: <T id={'New tasks'} />,
label: true,
permission: {
subject: AbilitySubject.Expense,
ability: ExpenseAction.Create,
},
},
{
divider: true,
permission: {
subject: AbilitySubject.Expense,
ability: ExpenseAction.Create,
},
},
{
text: <T id={'new_expense'} />,
href: '/expenses/new',
permission: {
subject: AbilitySubject.Expense,
ability: ExpenseAction.Create,
},
},
],
},
@@ -247,80 +552,216 @@ export default [
{
text: <T id={'balance_sheet'} />,
href: '/financial-reports/balance-sheet',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_BALANCE_SHEET,
},
},
{
text: <T id={'trial_balance_sheet'} />,
href: '/financial-reports/trial-balance-sheet',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_TRIAL_BALANCE_SHEET,
},
},
{
text: <T id={'journal'} />,
href: '/financial-reports/journal-sheet',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_JOURNAL,
},
},
{
text: <T id={'general_ledger'} />,
href: '/financial-reports/general-ledger',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_GENERAL_LEDGET,
},
},
{
text: <T id={'profit_loss_sheet'} />,
href: '/financial-reports/profit-loss-sheet',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_PROFIT_LOSS,
},
},
{
text: <T id={'cash_flow_statement'} />,
href: '/financial-reports/cash-flow',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CASHFLOW_ACCOUNT_TRANSACTION,
},
},
{
text: <T id={'AR_Aging_Summary'} />,
href: '/financial-reports/receivable-aging-summary',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_AR_AGING_SUMMARY,
},
},
{
text: <T id={'AP_Aging_Summary'} />,
href: '/financial-reports/payable-aging-summary',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_AP_AGING_SUMMARY,
},
},
{
text: <T id={'Sales/Purchases'} />,
label: true,
permission: [
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_PURCHASES_BY_ITEMS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_SALES_BY_ITEMS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_TRANSACTIONS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE,
},
],
},
{
divider: true,
permission: [
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_PURCHASES_BY_ITEMS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_SALES_BY_ITEMS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_TRANSACTIONS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE,
},
],
},
{
text: <T id={'purchases_by_items'} />,
href: '/financial-reports/purchases-by-items',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_PURCHASES_BY_ITEMS,
},
},
{
text: <T id={'sales_by_items'} />,
href: '/financial-reports/sales-by-items',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_SALES_BY_ITEMS,
},
},
{
text: <T id={'customers_transactions'} />,
href: '/financial-reports/transactions-by-customers',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
},
},
{
text: <T id={'vendors_transactions'} />,
href: '/financial-reports/transactions-by-vendors',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_TRANSACTIONS,
},
},
{
text: <T id={'customers_balance_summary'} />,
href: '/financial-reports/customers-balance-summary',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
},
},
{
text: <T id={'vendors_balance_summary'} />,
href: '/financial-reports/vendors-balance-summary',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE,
},
},
{
text: <T id={'inventory'} />,
label: true,
permission: [
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
},
],
},
{
divider: true,
permission: [
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS,
},
{
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
},
],
},
{
text: <T id={'inventory_item_details'} />,
href: '/financial-reports/inventory-item-details',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS,
},
},
{
text: <T id={'inventory_valuation'} />,
href: '/financial-reports/inventory-valuation',
permission: {
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
},
},
],
},
@@ -328,14 +769,32 @@ export default [
text: <T id={'system'} />,
enableBilling: true,
label: true,
permission: [
{
subject: AbilitySubject.Preferences,
ability: PreferencesAbility.Mutate,
},
{
subject: AbilitySubject.SubscriptionBilling,
ability: SubscriptionBillingAbility.View,
},
],
},
{
text: <T id={'preferences'} />,
href: '/preferences',
permission: {
subject: AbilitySubject.Preferences,
ability: PreferencesAbility.Mutate,
},
},
{
text: <T id={'billing'} />,
href: '/billing',
enableBilling: true,
permission: {
subject: AbilitySubject.SubscriptionBilling,
ability: SubscriptionBillingAbility.View,
},
},
];

View File

@@ -26,8 +26,11 @@ import withManualJournals from './withManualJournals';
import withSettingsActions from '../../Settings/withSettingsActions';
import withSettings from '../../Settings/withSettings';
import { If, DashboardActionViewsList } from 'components';
import { Can, If, DashboardActionViewsList } from 'components';
import {
ManualJournalAction,
AbilitySubject,
} from '../../../common/abilityOption';
import { compose } from 'utils';
/**
@@ -86,13 +89,14 @@ function ManualJournalActionsBar({
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'journal_entry'} />}
onClick={onClickNewManualJournal}
/>
<Can I={ManualJournalAction.Create} a={AbilitySubject.ManualJournal}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'journal_entry'} />}
onClick={onClickNewManualJournal}
/>
</Can>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: manualJournalsFilterConditions,

View File

@@ -2,7 +2,11 @@ import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
import { FormattedMessage as T } from 'components';
import { Can, FormattedMessage as T } from 'components';
import {
AbilitySubject,
ManualJournalAction,
} from '../../../common/abilityOption';
export default function ManualJournalsEmptyStatus() {
const history = useHistory();
@@ -17,19 +21,21 @@ export default function ManualJournalsEmptyStatus() {
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/make-journal-entry');
}}
>
<T id={'make_journal'} />
</Button>
<Can I={ManualJournalAction.Create} a={AbilitySubject.ManualJournal}>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/make-journal-entry');
}}
>
<T id={'make_journal'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</Can>
</>
}
/>

View File

@@ -13,7 +13,18 @@ import {
} from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { FormattedMessage as T, Choose, Money, If, Icon } from 'components';
import {
Can,
FormattedMessage as T,
Choose,
Money,
If,
Icon,
} from 'components';
import {
ManualJournalAction,
AbilitySubject,
} from '../../../common/abilityOption';
import { safeCallback } from 'utils';
/**
@@ -150,25 +161,31 @@ export const ActionsMenu = ({
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<If condition={!original.is_published}>
<Can I={ManualJournalAction.Edit} a={AbilitySubject.ManualJournal}>
<MenuDivider />
<If condition={!original.is_published}>
<MenuItem
icon={<Icon icon="arrow-to-top" />}
text={intl.get('publish_journal')}
onClick={safeCallback(onPublish, original)}
/>
</If>
</Can>
<Can I={ManualJournalAction.Edit} a={AbilitySubject.ManualJournal}>
<MenuItem
icon={<Icon icon="arrow-to-top" />}
text={intl.get('publish_journal')}
onClick={safeCallback(onPublish, original)}
icon={<Icon icon="pen-18" />}
text={intl.get('edit_journal')}
onClick={safeCallback(onEdit, original)}
/>
</If>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_journal')}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
text={intl.get('delete_journal')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Can>
<Can I={ManualJournalAction.Delete} a={AbilitySubject.ManualJournal}>
<MenuItem
text={intl.get('delete_journal')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Can>
</Menu>
);
};

View File

@@ -128,6 +128,7 @@ export const useJournalTableEntriesColumns = () => {
className: 'account',
disableSortBy: true,
width: 160,
fieldProps: { allowCreate: true }
},
{
Header: CreditHeaderCell,

View File

@@ -1,6 +1,10 @@
import intl from 'react-intl-universal';
import { RESOURCES_TYPES } from 'common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
import {
AbilitySubject,
ManualJournalAction,
} from '../../common/abilityOption';
/**
* Universal search manual journal item select action.
@@ -44,4 +48,8 @@ export const universalSearchJournalBind = () => ({
optionItemLabel: intl.get('manual_journals'),
selectItemAction: JournalUniversalSearchSelectAction,
itemSelect: manualJournalsToSearch,
permission: {
ability: ManualJournalAction.View,
subject: AbilitySubject.ManualJournal,
},
});

View File

@@ -1,7 +1,10 @@
import intl from 'react-intl-universal';
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
import { AbilitySubject, AccountAction } from '../../common/abilityOption';
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
function AccountUniversalSearchItemSelectComponent({
// #ownProps
resourceType,
@@ -42,4 +45,8 @@ export const universalSearchAccountBind = () => ({
optionItemLabel: intl.get('accounts'),
selectItemAction: AccountUniversalSearchItemSelect,
itemSelect: accountToSearch,
permission: {
ability: AccountAction.View,
subject: AbilitySubject.Account,
},
});

View File

@@ -15,6 +15,7 @@ import { FormattedMessage as T } from 'components';
import {
AdvancedFilterPopover,
If,
Can,
DashboardActionViewsList,
DashboardFilterButton,
DashboardRowsHeightButton,
@@ -30,6 +31,8 @@ import withAlertActions from 'containers/Alert/withAlertActions';
import withAccountsTableActions from './withAccountsTableActions';
import withSettings from '../Settings/withSettings';
import withSettingsActions from '../Settings/withSettingsActions';
import { AccountAction, AbilitySubject } from '../../common/abilityOption';
import { compose } from 'utils';
/**
@@ -116,13 +119,14 @@ function AccountsActionsBar({
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_account'} />}
onClick={onClickNewAccount}
/>
<Can I={AccountAction.Create} a={AbilitySubject.Account}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_account'} />}
onClick={onClickNewAccount}
/>
</Can>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: accountsFilterConditions,
@@ -183,11 +187,13 @@ function AccountsActionsBar({
onChange={handleTableRowSizeChange}
/>
<NavbarDivider />
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={accountsInactiveMode}
onChange={handleInactiveSwitchChange}
/>
<Can I={AccountAction.Edit} a={AbilitySubject.Account}>
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={accountsInactiveMode}
onChange={handleInactiveSwitchChange}
/>
</Can>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button

View File

@@ -8,9 +8,10 @@ import {
MenuDivider,
Intent,
} from '@blueprintjs/core';
import { Icon, Money, If } from 'components';
import { Can, Icon, Money, If } from 'components';
import intl from 'react-intl-universal';
import { safeCallback } from 'utils';
import { AbilitySubject, AccountAction } from '../../common/abilityOption';
/**
* Accounts table actions menu.
@@ -34,38 +35,45 @@ export function ActionsMenu({
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_account')}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
icon={<Icon icon="plus" />}
text={intl.get('new_child_account')}
onClick={safeCallback(onNewChild, original)}
/>
<MenuDivider />
<If condition={original.active}>
<Can I={AccountAction.Edit} a={AbilitySubject.Account}>
<MenuDivider />
<MenuItem
text={intl.get('inactivate_account')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivate, original)}
icon={<Icon icon="pen-18" />}
text={intl.get('edit_account')}
onClick={safeCallback(onEdit, original)}
/>
</If>
<If condition={!original.active}>
<MenuItem
text={intl.get('activate_account')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivate, original)}
icon={<Icon icon="plus" />}
text={intl.get('new_child_account')}
onClick={safeCallback(onNewChild, original)}
/>
</If>
<MenuItem
text={intl.get('delete_account')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
<MenuDivider />
</Can>
<Can I={AccountAction.Edit} a={AbilitySubject.Account}>
<If condition={original.active}>
<MenuItem
text={intl.get('inactivate_account')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivate, original)}
/>
</If>
<If condition={!original.active}>
<MenuItem
text={intl.get('activate_account')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivate, original)}
/>
</If>
</Can>
<Can I={AccountAction.Edit} a={AbilitySubject.Account}>
<MenuItem
text={intl.get('delete_account')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Can>
</Menu>
);
}

View File

@@ -0,0 +1,68 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormattedMessage as T, FormattedHTMLMessage } from 'components';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { useSettingEasySMSDisconnect } from 'hooks/query';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* Easy SMS disconnect alert.
*/
function EasySMSDisconnectAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: {},
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: disconnectEasySMS, isLoading } =
useSettingEasySMSDisconnect();
// Handle cancel Disconnect alert.
const handleCancelDisconnect = () => {
closeAlert(name);
};
// Handle confirm Disconnect alert.
const handleConfirmDisconnect = () => {
disconnectEasySMS()
.then(() => {
AppToaster.show({
message: intl.get('easysms.disconnect.alert.success_message'),
intent: Intent.SUCCESS,
});
})
.catch(() => {})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'easysms.label.disconnect'} />}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelDisconnect}
onConfirm={handleConfirmDisconnect}
loading={isLoading}
>
<p>Ea aliqua elit reprehenderit pariatur consequat voluptate quis.</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(EasySMSDisconnectAlert);

View File

@@ -0,0 +1,83 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormattedMessage as T, FormattedHTMLMessage } from 'components';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { useDeleteRole } from 'hooks/query';
import { handleDeleteErrors } from '../../Preferences/Users/Roles/utils';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* Role delete alert.
*/
function RoleDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { roleId },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: deleteRole, isLoading } = useDeleteRole();
// Handle cancel delete role alert.
const handleCancelDelete = () => {
closeAlert(name);
};
// Handle confirm delete role.
const handleConfirmDeleteRole = () => {
deleteRole(roleId)
.then(() => {
AppToaster.show({
message: intl.get('roles.permission_schema.delete.alert_message'),
intent: Intent.SUCCESS,
});
})
.catch(
({
response: {
data: { errors },
},
}) => {
handleDeleteErrors(errors);
},
)
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelDelete}
onConfirm={handleConfirmDeleteRole}
loading={isLoading}
>
<p>
<FormattedHTMLMessage
id={
'roles.permission_schema.once_delete_this_role_you_will_able_to_restore_it'
}
/>
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(RoleDeleteAlert);

View File

@@ -16,6 +16,8 @@ import ExpensesAlerts from '../Expenses/ExpensesAlerts';
import AccountTransactionsAlerts from '../CashFlow/AccountTransactions/AccountTransactionsAlerts';
import UsersAlerts from '../Preferences/Users/UsersAlerts';
import CurrenciesAlerts from '../Preferences/Currencies/CurrenciesAlerts';
import RolesAlerts from '../Preferences/Users/Roles/RolesAlerts';
import EasySMSIntegrationAlerts from '../Preferences/EasySMSIntegration/EasySMSIntegrationAlerts';
export default [
...AccountsAlerts,
@@ -36,4 +38,6 @@ export default [
...AccountTransactionsAlerts,
...UsersAlerts,
...CurrenciesAlerts,
...RolesAlerts,
...EasySMSIntegrationAlerts,
];

View File

@@ -4,9 +4,10 @@ import intl from 'react-intl-universal';
import { Intent, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
import { MaterialProgressBar } from 'components';
import { FormatDateCell, If, Icon } from 'components';
import { Can, FormatDateCell, If, Icon } from 'components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
import { TRANSACRIONS_TYPE } from 'common/cashflowOptions';
import { AbilitySubject, CashflowAction } from '../../../common/abilityOption';
import { safeCallback } from 'utils';
export function ActionsMenu({
@@ -20,15 +21,17 @@ export function ActionsMenu({
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<If condition={TRANSACRIONS_TYPE.includes(original.reference_type)}>
<MenuDivider />
<MenuItem
text={intl.get('delete_transaction')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</If>
<Can I={CashflowAction.Delete} a={AbilitySubject.Cashflow}>
<If condition={TRANSACRIONS_TYPE.includes(original.reference_type)}>
<MenuDivider />
<MenuItem
text={intl.get('delete_transaction')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</If>
</Can>
</Menu>
);
}
@@ -83,7 +86,7 @@ export function useAccountTransactionsColumns() {
className: 'deposit',
textOverview: true,
align: 'right',
clickable: true
clickable: true,
},
{
id: 'withdrawal',
@@ -93,7 +96,7 @@ export function useAccountTransactionsColumns() {
width: 150,
textOverview: true,
align: 'right',
clickable: true
clickable: true,
},
{
id: 'running_balance',
@@ -103,7 +106,7 @@ export function useAccountTransactionsColumns() {
width: 150,
textOverview: true,
align: 'right',
clickable: true
clickable: true,
},
{
id: 'balance',

View File

@@ -7,8 +7,9 @@ import {
Alignment,
Switch,
} from '@blueprintjs/core';
import { Icon, FormattedMessage as T } from 'components';
import { Can, Icon, FormattedMessage as T } from 'components';
import { useRefreshCashflowAccounts } from 'hooks/query';
import { CashflowAction, AbilitySubject } from '../../../common/abilityOption';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
@@ -56,19 +57,22 @@ function CashFlowAccountsActionsBar({
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus-24'} iconSize={20} />}
text={<T id={'cash_flow.label.add_cash_account'} />}
onClick={handleAddBankAccount}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus-24'} iconSize={20} />}
text={<T id={'cash_flow.label.add_bank_account'} />}
onClick={handleAddCashAccount}
/>
<NavbarDivider />
<Can I={CashflowAction.Create} a={AbilitySubject.Cashflow}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus-24'} iconSize={20} />}
text={<T id={'cash_flow.label.add_cash_account'} />}
onClick={handleAddBankAccount}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus-24'} iconSize={20} />}
text={<T id={'cash_flow.label.add_bank_account'} />}
onClick={handleAddCashAccount}
/>
<NavbarDivider />
</Can>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
@@ -85,13 +89,15 @@ function CashFlowAccountsActionsBar({
text={<T id={'import'} />}
/>
<NavbarDivider />
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={false}
onChange={handleInactiveSwitchChange}
/>
<Can I={CashflowAction.Edit} a={AbilitySubject.Cashflow}>
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={false}
onChange={handleInactiveSwitchChange}
/>
</Can>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}

View File

@@ -13,7 +13,13 @@ import {
If,
Icon,
T,
Can,
} from '../../../components';
import {
AccountAction,
CashflowAction,
AbilitySubject,
} from '../../../common/abilityOption';
import { useCashFlowAccountsContext } from './CashFlowAccountsProvider';
@@ -222,48 +228,56 @@ function CashflowAccountContextMenu({
text={intl.get('view_details')}
onClick={safeCallback(onViewClick)}
/>
<MenuDivider />
<MenuItem
text={<T id={'cash_flow_money_in'} />}
icon={<Icon icon={'arrow-downward'} iconSize={16} />}
>
<CashflowAccountMoneyInContextMenu onClick={onMoneyInClick} />
</MenuItem>
<MenuItem
text={<T id={'cash_flow_money_out'} />}
icon={<Icon icon={'arrow-upward'} iconSize={16} />}
>
<CashflowAccountMoneyOutContextMenu onClick={onMoneyOutClick} />
</MenuItem>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_account')}
onClick={safeCallback(onEditClick)}
/>
<MenuDivider />
<If condition={account.active}>
<Can I={CashflowAction.Create} a={AbilitySubject.Cashflow}>
<MenuDivider />
<MenuItem
text={intl.get('inactivate_account')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivateClick)}
/>
</If>
<If condition={!account.active}>
text={<T id={'cash_flow_money_in'} />}
icon={<Icon icon={'arrow-downward'} iconSize={16} />}
>
<CashflowAccountMoneyInContextMenu onClick={onMoneyInClick} />
</MenuItem>
<MenuItem
text={intl.get('activate_account')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivateClick)}
text={<T id={'cash_flow_money_out'} />}
icon={<Icon icon={'arrow-upward'} iconSize={16} />}
>
<CashflowAccountMoneyOutContextMenu onClick={onMoneyOutClick} />
</MenuItem>
</Can>
<Can I={CashflowAction.Edit} a={AbilitySubject.Cashflow}>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_account')}
onClick={safeCallback(onEditClick)}
/>
</If>
<MenuItem
text={intl.get('delete_account')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDeleteClick)}
/>
</Can>
<Can I={AccountAction.Edit} a={AbilitySubject.Account}>
<MenuDivider />
<If condition={account.active}>
<MenuItem
text={intl.get('inactivate_account')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivateClick)}
/>
</If>
<If condition={!account.active}>
<MenuItem
text={intl.get('activate_account')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivateClick)}
/>
</If>
</Can>
<Can I={CashflowAction.Delete} a={AbilitySubject.Cashflow}>
<MenuItem
text={intl.get('delete_account')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDeleteClick)}
/>
</Can>
</Menu>
);
}

View File

@@ -9,23 +9,24 @@ import {
Menu,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik';
import styled from 'styled-components';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import { Icon } from 'components';
import { useCustomerFormContext } from './CustomerFormProvider';
import { safeInvoke } from 'utils';
/**
* Customer floating actions bar.
*/
export default function CustomerFloatingActions() {
const history = useHistory();
export default function CustomerFloatingActions({ onCancel }) {
// Customer form context.
const { isNewMode,setSubmitPayload } = useCustomerFormContext();
const { isNewMode, setSubmitPayload } = useCustomerFormContext();
// Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext();
@@ -37,7 +38,7 @@ export default function CustomerFloatingActions() {
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
history.goBack();
safeInvoke(onCancel, event);
};
// handle clear button clicl.
@@ -55,7 +56,7 @@ export default function CustomerFloatingActions() {
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup>
{/* ----------- Save and New ----------- */}
<Button
<SaveButton
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
@@ -83,6 +84,7 @@ export default function CustomerFloatingActions() {
/>
</Popover>
</ButtonGroup>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
@@ -99,3 +101,7 @@ export default function CustomerFloatingActions() {
</div>
);
}
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -1,152 +1,14 @@
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import AppToaster from 'components/AppToaster';
import CustomerFormPrimarySection from './CustomerFormPrimarySection';
import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
import CustomersTabs from './CustomersTabs';
import CustomerFloatingActions from './CustomerFloatingActions';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { compose, transformToForm } from 'utils';
import { useCustomerFormContext } from './CustomerFormProvider';
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
const defaultInitialValues = {
customer_type: 'business',
salutation: '',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
email: '',
work_phone: '',
personal_phone: '',
website: '',
note: '',
active: true,
billing_address_country: '',
billing_address_1: '',
billing_address_2: '',
billing_address_city: '',
billing_address_state: '',
billing_address_postcode: '',
billing_address_phone: '',
shipping_address_country: '',
shipping_address_1: '',
shipping_address_2: '',
shipping_address_city: '',
shipping_address_state: '',
shipping_address_postcode: '',
shipping_address_phone: '',
opening_balance: '',
currency_code: '',
opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
};
import React from 'react';
import { CustomerFormProvider } from './CustomerFormProvider';
import CustomerFormFormik from './CustomerFormFormik';
/**
* Customer form.
* Abstructed customer form.
*/
function CustomerForm({ organization: { base_currency } }) {
const {
customer,
customerId,
submitPayload,
contactDuplicate,
editCustomerMutate,
createCustomerMutate,
isNewMode,
} = useCustomerFormContext();
// const isNewMode = !customerId;
const history = useHistory();
/**
* Initial values in create and edit mode.
*/
const initialValues = useMemo(
() => ({
...defaultInitialValues,
currency_code: base_currency,
...transformToForm(contactDuplicate || customer, defaultInitialValues),
}),
[customer, contactDuplicate, base_currency],
);
//Handles the form submit.
const handleFormSubmit = (
values,
{ setSubmitting, resetForm, setErrors },
) => {
const formValues = { ...values };
const onSuccess = () => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_customer_has_been_created_successfully'
: 'the_item_customer_has_been_edited_successfully',
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
if (!submitPayload.noRedirect) {
history.push('/customers');
}
};
const onError = () => {
setSubmitting(false);
};
if (isNewMode) {
createCustomerMutate(formValues).then(onSuccess).catch(onError);
} else {
editCustomerMutate([customer.id, formValues])
.then(onSuccess)
.catch(onError);
}
};
export default function CustomerForm({ customerId }) {
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_CUSTOMER)}>
<Formik
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions />
</Form>
</Formik>
</div>
<CustomerFormProvider customerId={customerId}>
<CustomerFormFormik />
</CustomerFormProvider>
);
}
export default compose(withCurrentOrganization())(CustomerForm);

View File

@@ -6,7 +6,6 @@ import intl from 'react-intl-universal';
import { inputIntent } from 'utils';
export default function CustomerFormAfterPrimarySection({}) {
return (
<div class="customer-form__after-primary-section-content">
{/*------------ Customer email -----------*/}
@@ -31,21 +30,21 @@ export default function CustomerFormAfterPrimarySection({}) {
inline={true}
>
<ControlGroup>
<FastField name={'work_phone'}>
<FastField name={'personal_phone'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
intent={inputIntent({ error, touched })}
placeholder={intl.get('work')}
placeholder={intl.get('personal')}
{...field}
/>
)}
</FastField>
<FastField name={'personal_phone'}>
<FastField name={'work_phone'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
intent={inputIntent({ error, touched })}
placeholder={intl.get('Mobile')}
placeholder={intl.get('work')}
{...field}
/>
)}

View File

@@ -0,0 +1,127 @@
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import AppToaster from 'components/AppToaster';
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
import CustomerFormPrimarySection from './CustomerFormPrimarySection';
import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
import CustomersTabs from './CustomersTabs';
import CustomerFloatingActions from './CustomerFloatingActions';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { compose, transformToForm, saveInvoke } from 'utils';
import { useCustomerFormContext } from './CustomerFormProvider';
import { defaultInitialValues } from './utils';
import 'style/pages/Customers/Form.scss';
/**
* Customer form.
*/
function CustomerFormFormik({
organization: { base_currency },
// #ownProps
initialValues: initialCustomerValues,
onSubmitSuccess,
onSubmitError,
onCancel,
className,
}) {
const {
customer,
submitPayload,
contactDuplicate,
editCustomerMutate,
createCustomerMutate,
isNewMode,
} = useCustomerFormContext();
/**
* Initial values in create and edit mode.
*/
const initialValues = useMemo(
() => ({
...defaultInitialValues,
currency_code: base_currency,
...transformToForm(contactDuplicate || customer, defaultInitialValues),
...initialCustomerValues,
}),
[customer, contactDuplicate, base_currency, initialCustomerValues],
);
// Handles the form submit.
const handleFormSubmit = (values, formArgs) => {
const { setSubmitting, resetForm } = formArgs;
const formValues = { ...values };
const onSuccess = () => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_customer_has_been_created_successfully'
: 'the_item_customer_has_been_edited_successfully',
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
saveInvoke(onSubmitSuccess, values, formArgs, submitPayload);
};
const onError = () => {
setSubmitting(false);
saveInvoke(onSubmitError, values, formArgs, submitPayload);
};
if (isNewMode) {
createCustomerMutate(formValues).then(onSuccess).catch(onError);
} else {
editCustomerMutate([customer.id, formValues])
.then(onSuccess)
.catch(onError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_CUSTOMER,
className,
)}
>
<Formik
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions onCancel={onCancel} />
</Form>
</Formik>
</div>
);
}
export default compose(withCurrentOrganization())(CustomerFormFormik);

View File

@@ -1,20 +1,74 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { useParams, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { DashboardCard } from 'components';
import CustomerForm from './CustomerForm';
import { CustomerFormProvider } from './CustomerFormProvider';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import 'style/pages/Customers/PageForm.scss';
import CustomerFormFormik from './CustomerFormFormik';
import {
CustomerFormProvider,
useCustomerFormContext,
} from './CustomerFormProvider';
export default function CustomerFormPage() {
const { id } = useParams();
/**
* Customer form page loading.
* @returns {JSX}
*/
function CustomerFormPageLoading({ children }) {
const { isFormLoading } = useCustomerFormContext();
return (
<CustomerFormProvider customerId={id}>
<DashboardCard page>
<CustomerForm />
</DashboardCard>
<CustomerDashboardInsider loading={isFormLoading}>
{children}
</CustomerDashboardInsider>
);
}
/**
* Customer form page.
* @returns {JSX}
*/
export default function CustomerFormPage() {
const history = useHistory();
const { id } = useParams();
const customerId = parseInt(id, 10);
// Handle the form submit success.
const handleSubmitSuccess = (values, formArgs, submitPayload) => {
if (!submitPayload.noRedirect) {
history.push('/customers');
}
};
// Handle the form cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return (
<CustomerFormProvider customerId={customerId}>
<CustomerFormPageLoading>
<DashboardCard page>
<CustomerFormPageFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</CustomerFormPageLoading>
</CustomerFormProvider>
);
}
}
const CustomerFormPageFormik = styled(CustomerFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const CustomerDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;

View File

@@ -1,6 +1,5 @@
import React, { useState, createContext } from 'react';
import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useCustomer,
useCurrencies,
@@ -24,7 +23,7 @@ function CustomerFormProvider({ customerId, ...props }) {
// Handle fetch contact duplicate details.
const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
contactId,
{ enabled: !!contactId, },
{ enabled: !!contactId },
);
// Handle fetch Currencies data table
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
@@ -38,6 +37,9 @@ function CustomerFormProvider({ customerId, ...props }) {
// determines whether the form new or duplicate mode.
const isNewMode = contactId || !customerId;
const isFormLoading =
isCustomerLoading || isCurrenciesLoading || isContactLoading;
const provider = {
customerId,
customer,
@@ -48,24 +50,14 @@ function CustomerFormProvider({ customerId, ...props }) {
isCustomerLoading,
isCurrenciesLoading,
isFormLoading,
setSubmitPayload,
editCustomerMutate,
createCustomerMutate,
};
return (
<DashboardInsider
loading={
isCustomerLoading ||
isCurrenciesLoading ||
isContactLoading
}
name={'customer-form'}
>
<CustomerFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
return <CustomerFormContext.Provider value={provider} {...props} />;
}
const useCustomerFormContext = () => React.useContext(CustomerFormContext);

View File

@@ -0,0 +1,38 @@
import moment from 'moment';
export const defaultInitialValues = {
customer_type: 'business',
salutation: '',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
email: '',
work_phone: '',
personal_phone: '',
website: '',
note: '',
active: true,
billing_address_country: '',
billing_address_1: '',
billing_address_2: '',
billing_address_city: '',
billing_address_state: '',
billing_address_postcode: '',
billing_address_phone: '',
shipping_address_country: '',
shipping_address_1: '',
shipping_address_2: '',
shipping_address_city: '',
shipping_address_state: '',
shipping_address_postcode: '',
shipping_address_phone: '',
opening_balance: '',
currency_code: '',
opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
};

View File

@@ -14,6 +14,7 @@ import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import {
If,
Icon,
Can,
FormattedMessage as T,
DashboardActionViewsList,
AdvancedFilterPopover,
@@ -29,6 +30,7 @@ import withCustomersActions from './withCustomersActions';
import withAlertActions from 'containers/Alert/withAlertActions';
import withSettingsActions from '../../Settings/withSettingsActions';
import withSettings from '../../Settings/withSettings';
import { CustomerAction, AbilitySubject } from '../../../common/abilityOption';
import { compose } from 'utils';
@@ -103,15 +105,15 @@ function CustomerActionsBar({
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_customer'} />}
onClick={onClickNewCustomer}
/>
<NavbarDivider />
<Can I={CustomerAction.Create} a={AbilitySubject.Item}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_customer'} />}
onClick={onClickNewCustomer}
/>
<NavbarDivider />
</Can>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: customersFilterConditions,
@@ -152,11 +154,13 @@ function CustomerActionsBar({
onChange={handleTableRowSizeChange}
/>
<NavbarDivider />
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={accountsInactiveMode}
onChange={handleInactiveSwitchChange}
/>
<Can I={CustomerAction.Edit} a={AbilitySubject.Customer}>
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={accountsInactiveMode}
onChange={handleInactiveSwitchChange}
/>
</Can>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button

View File

@@ -2,7 +2,8 @@ import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
import { FormattedMessage as T } from 'components';
import { Can, FormattedMessage as T } from 'components';
import { AbilitySubject, CustomerAction } from '../../../common/abilityOption';
export default function CustomersEmptyStatus() {
const history = useHistory();
@@ -17,19 +18,21 @@ export default function CustomersEmptyStatus() {
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/customers/new');
}}
>
<T id={'new_customer'} />
</Button>
<Can I={CustomerAction.Create} a={AbilitySubject.Customer}>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/customers/new');
}}
>
<T id={'new_customer'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</Can>
</>
}
/>

View File

@@ -4,7 +4,8 @@ import clsx from 'classnames';
import intl from 'react-intl-universal';
import { Icon, Money, If, AvaterCell } from 'components';
import { Can, Icon, Money, If, AvaterCell } from 'components';
import { CustomerAction, AbilitySubject } from '../../../common/abilityOption';
import { safeCallback } from 'utils';
@@ -29,37 +30,46 @@ export function ActionsMenu({
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_customer')}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
icon={<Icon icon="duplicate-16" />}
text={intl.get('duplicate')}
onClick={safeCallback(onDuplicate, original)}
/>
<If condition={original.active}>
<Can I={CustomerAction.Edit} a={AbilitySubject.Customer}>
<MenuDivider />
<MenuItem
text={intl.get('inactivate_customer')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivate, original)}
icon={<Icon icon="pen-18" />}
text={intl.get('edit_customer')}
onClick={safeCallback(onEdit, original)}
/>
</If>
<If condition={!original.active}>
</Can>
<Can I={CustomerAction.Create} a={AbilitySubject.Customer}>
<MenuItem
text={intl.get('activate_customer')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivate, original)}
icon={<Icon icon="duplicate-16" />}
text={intl.get('duplicate')}
onClick={safeCallback(onDuplicate, original)}
/>
</If>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={intl.get('delete_customer')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Can>
<Can I={CustomerAction.Edit} a={AbilitySubject.Customer}>
<If condition={original.active}>
<MenuItem
text={intl.get('inactivate_customer')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivate, original)}
/>
</If>
<If condition={!original.active}>
<MenuItem
text={intl.get('activate_customer')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivate, original)}
/>
</If>
</Can>
<Can I={CustomerAction.Delete} a={AbilitySubject.Customer}>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={intl.get('delete_customer')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Can>
</Menu>
);
}

View File

@@ -1,4 +1,5 @@
import intl from 'react-intl-universal';
import { AbilitySubject, CustomerAction } from '../../common/abilityOption';
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
@@ -42,4 +43,8 @@ export const universalSearchCustomerBind = () => ({
optionItemLabel: intl.get('customers'),
selectItemAction: CustomerUniversalSearchSelectAction,
itemSelect: customersToSearch,
permission: {
ability: CustomerAction.View,
subject: AbilitySubject.Customer,
},
});

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { EasySMSIntegrationProvider } from './EasySMSIntegrationProvider';
import EasySMSIntegrationForm from './EasySMSIntegrationForm';
/**
* EasySMS integration dialog content.
*/
export default function EasySMSIntegrationDialogContent({
// #ownProps
dialogName,
}) {
return (
<EasySMSIntegrationProvider dialogName={dialogName}>
<EasySMSIntegrationForm />
</EasySMSIntegrationProvider>
);
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import '../../../style/pages/EasySMSIntegration/EasySMSIntegration.scss';
import { AppToaster } from 'components';
import EasySMSIntegrationFormContent from './EasySMSIntegrationFormContent';
import { CreateEasySMSIntegrationSchema } from './EasySMSIntegrationForm.schema';
import { useEasySMSIntegration } from './EasySMSIntegrationProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const defaultInitialValues = {
token: '',
};
/**
* EasySMS Integration form.
*/
function EasySMSIntegrationForm({
// #withDialogActions
closeDialog,
}) {
const { dialogName, easySMSIntegrateMutate } = useEasySMSIntegration();
// Initial form values.
const initialValues = {
...defaultInitialValues,
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('easysms.integrate.dialog.success_message'),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
easySMSIntegrateMutate(values).then(onSuccess).catch(onError);
};
return (
<Formik
validationSchema={CreateEasySMSIntegrationSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={EasySMSIntegrationFormContent}
/>
);
}
export default compose(withDialogActions)(EasySMSIntegrationForm);

View File

@@ -0,0 +1,7 @@
import * as Yup from 'yup';
const Schema = Yup.object().shape({
token: Yup.string().required(),
});
export const CreateEasySMSIntegrationSchema = Schema;

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { Form, FastField, ErrorMessage, useFormikContext } from 'formik';
import { Classes, FormGroup, InputGroup, Button } from '@blueprintjs/core';
import { FieldRequiredHint, FormattedMessage as T } from 'components';
import { useEasySMSIntegration } from './EasySMSIntegrationProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, compose } from 'utils';
/**
* EasySMS Integration form content.
*/
function EasySMSIntegrationFormContent({
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
// EasySMS Integration dialog context.
const { dialogName } = useEasySMSIntegration();
// Handle close button click.
const handleCloseBtnClick = () => {
closeDialog(dialogName);
};
return (
<Form>
<div className={Classes.DIALOG_BODY}>
{/*------------ Token -----------*/}
<FastField name={'token'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'easysms.integrate.dialog.label.token'} />}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="token" />}
className={'form-group--token'}
>
<InputGroup intent={inputIntent({ error, touched })} {...field} />
</FormGroup>
)}
</FastField>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
disabled={isSubmitting}
onClick={handleCloseBtnClick}
style={{ minWidth: '75px' }}
>
<T id={'close'} />
</Button>
<Button
loading={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
{<T id={'save'} />}
</Button>
</div>
</div>
</Form>
);
}
export default compose(withDialogActions)(EasySMSIntegrationFormContent);

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { DialogContent } from 'components';
import { useSettingEasySMSIntegrate } from 'hooks/query';
const EasySMSIntegrationDialogContext = React.createContext();
/**
* Easy SMS integration dialog provider.
*/
function EasySMSIntegrationProvider({ dialogName, ...props }) {
// easysms integrate mutations.
const { mutateAsync: easySMSIntegrateMutate } = useSettingEasySMSIntegrate();
// State provider.
const provider = {
dialogName,
easySMSIntegrateMutate,
};
return (
<DialogContent>
<EasySMSIntegrationDialogContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useEasySMSIntegration = () =>
React.useContext(EasySMSIntegrationDialogContext);
export { EasySMSIntegrationProvider, useEasySMSIntegration };

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'redux';
const EasySMSIntegrationDialogContent = React.lazy(() =>
import('./EasySMSIntegrationDialogContent'),
);
/**
* EasySms integration dialog.
*/
function EasySMSIntegrationDialog({ dialogName, payload = {}, isOpen }) {
return (
<Dialog
name={dialogName}
title={'EasySMS Inegration'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--easysms-integrate'}
>
<DialogSuspense>
<EasySMSIntegrationDialogContent dialogName={dialogName} />
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(EasySMSIntegrationDialog);

View File

@@ -2,10 +2,8 @@ import * as Yup from 'yup';
import intl from 'react-intl-universal';
const Schema = Yup.object().shape({
email: Yup.string()
.email()
.required()
.label(intl.get('email')),
email: Yup.string().email().required().label(intl.get('email')),
role_id: Yup.string().required().label(intl.get('roles.label.role_name_')),
});
export const InviteUserFormSchema = Schema;
export const InviteUserFormSchema = Schema;

View File

@@ -1,7 +1,11 @@
import React from 'react';
import { FormGroup, InputGroup, Intent, Button } from '@blueprintjs/core';
import { FastField, Form, useFormikContext, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'components';
import {
ListSelect,
FieldRequiredHint,
FormattedMessage as T,
} from 'components';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { inputIntent } from 'utils';
@@ -15,23 +19,24 @@ function InviteUserFormContent({
closeDialog,
}) {
const { isSubmitting } = useFormikContext();
const { isEditMode, dialogName } = useInviteUserFormContext();
const { isEditMode, dialogName, roles } = useInviteUserFormContext();
const handleClose = () => {
closeDialog(dialogName);
};
console.log(roles, 'XX');
return (
<Form>
<div className={CLASSES.DIALOG_BODY}>
<p className="mb2">
<T id={'your_access_to_your_team'} />
</p>
{/* ----------- Email ----------- */}
<FastField name={'email'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'email'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--email', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="email" />}
@@ -40,6 +45,31 @@ function InviteUserFormContent({
</FormGroup>
)}
</FastField>
{/* ----------- Role name ----------- */}
<FastField name={'role_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'roles.label.role_name'} />}
labelInfo={<FieldRequiredHint />}
helperText={<ErrorMessage name="role_id" />}
className={classNames(CLASSES.FILL, 'form-group--role_name')}
intent={inputIntent({ error, touched })}
>
<ListSelect
items={roles}
onItemSelect={({ id }) => {
form.setFieldValue('role_id', id);
}}
selectedItem={value}
selectedItemProp={'id'}
textProp={'name'}
// labelProp={'id '}
popoverProps={{ minimal: true }}
intent={inputIntent({ error, touched })}
/>
</FormGroup>
)}
</FastField>
</div>
<div className={CLASSES.DIALOG_FOOTER}>

View File

@@ -1,5 +1,5 @@
import React, { createContext } from 'react';
import { useCreateInviteUser, useUsers } from 'hooks/query';
import { useCreateInviteUser, useUsers, useRoles } from 'hooks/query';
import { DialogContent } from 'components';
const InviteUserFormContext = createContext();
@@ -14,6 +14,9 @@ function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) {
// fetch users list.
const { isLoading: isUsersLoading } = useUsers();
// fetch roles list.
const { data: roles, isLoading: isRolesLoading } = useRoles();
// Provider state.
const provider = {
inviteUserMutate,
@@ -21,10 +24,14 @@ function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) {
userId,
isUsersLoading,
isEditMode,
roles,
};
return (
<DialogContent isLoading={isUsersLoading} name={'invite-form'}>
<DialogContent
isLoading={isUsersLoading || isRolesLoading}
name={'invite-form'}
>
<InviteUserFormContext.Provider value={provider} {...props} />
</DialogContent>
);

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { NotifyEstimateViaSMSFormProvider } from './NotifyEstimateViaSMSFormProvider';
import NotifyEstimateViaSMSForm from './NotifyEstimateViaSMSForm';
export default function NotifyEstimateViaSMSDialogContent({
// #ownProps
dialogName,
estimate,
}) {
return (
<NotifyEstimateViaSMSFormProvider
estimateId={estimate}
dialogName={dialogName}
>
<NotifyEstimateViaSMSForm />
</NotifyEstimateViaSMSFormProvider>
);
}

View File

@@ -0,0 +1,81 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useEstimateViaSMSContext } from './NotifyEstimateViaSMSFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const notificationType = {
key: 'sale-estimate-details',
label: intl.get('sms_notification.estimate_details.type'),
};
function NotifyEstimateViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
estimateId,
dialogName,
estimateSMSDetail,
createNotifyEstimateBySMSMutate,
} = useEstimateViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('notify_estimate_via_sms.dialog.success_message'),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
setSubmitting(false);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
createNotifyEstimateBySMSMutate([estimateId, values])
.then(onSuccess)
.catch(onError);
};
const initialValues = {
...estimateSMSDetail,
notification_key: notificationType.key,
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={[notificationType]}
onCancel={handleFormCancel}
onSubmit={handleFormSubmit}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyEstimateViaSMSForm);

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { DialogContent } from 'components';
import {
useEstimateSMSDetail,
useCreateNotifyEstimateBySMS,
} from 'hooks/query';
const NotifyEstimateViaSMSContext = React.createContext();
function NotifyEstimateViaSMSFormProvider({
estimateId,
dialogName,
...props
}) {
const { data: estimateSMSDetail, isLoading: isEstimateSMSDetailLoading } =
useEstimateSMSDetail(estimateId, {
enabled: !!estimateId,
});
// Create notfiy estimate by sms mutations.
const { mutateAsync: createNotifyEstimateBySMSMutate } =
useCreateNotifyEstimateBySMS();
// State provider.
const provider = {
estimateId,
dialogName,
estimateSMSDetail,
createNotifyEstimateBySMSMutate,
};
return (
<DialogContent isLoading={isEstimateSMSDetailLoading}>
<NotifyEstimateViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useEstimateViaSMSContext = () =>
React.useContext(NotifyEstimateViaSMSContext);
export { NotifyEstimateViaSMSFormProvider, useEstimateViaSMSContext };

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const NotifyEstimateViaSMSDialogContent = React.lazy(() =>
import('./NotifyEstimateViaSMSDialogContent'),
);
function NotifyEstimateViaSMSDialog({
dialogName,
payload: { estimateId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyEstimateViaSMSDialogContent
dialogName={dialogName}
estimate={estimateId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(NotifyEstimateViaSMSDialog);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { NotifyInvoiceViaSMSFormProvider } from './NotifyInvoiceViaSMSFormProvider';
import NotifyInvoiceViaSMSForm from './NotifyInvoiceViaSMSForm';
export default function NotifyInvoiceViaSMSDialogContent({
// #ownProps
dialogName,
invoiceId,
}) {
return (
<NotifyInvoiceViaSMSFormProvider
invoiceId={invoiceId}
dialogName={dialogName}
>
<NotifyInvoiceViaSMSForm />
</NotifyInvoiceViaSMSFormProvider>
);
}

View File

@@ -0,0 +1,108 @@
import React from 'react';
import intl from 'react-intl-universal';
import { pick } from 'lodash';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useNotifyInvoiceViaSMSContext } from './NotifyInvoiceViaSMSFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const transformFormValuesToRequest = (values) => {
return pick(values, ['notification_key']);
};
// Momerize the notification types.
const notificationTypes = [
{
key: 'details',
label: intl.get('sms_notification.invoice_details.type'),
},
{
key: 'reminder',
label: intl.get('sms_notification.invoice_reminder.type'),
},
];
/**
* Notify Invoice Via SMS Form.
*/
function NotifyInvoiceViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
createNotifyInvoiceBySMSMutate,
invoiceId,
invoiceSMSDetail,
dialogName,
notificationType,
setNotificationType,
} = useNotifyInvoiceViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('notify_invoice_via_sms.dialog.success_message'),
intent: Intent.SUCCESS,
});
setSubmitting(false);
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
// Transformes the form values to request.
const requestValues = transformFormValuesToRequest(values);
// Submits invoice SMS notification.
createNotifyInvoiceBySMSMutate([invoiceId, requestValues])
.then(onSuccess)
.catch(onError);
};
// Handle the form cancel.
const handleFormCancel = React.useCallback(() => {
closeDialog(dialogName);
}, [closeDialog, dialogName]);
const initialValues = {
notification_key: notificationType,
...invoiceSMSDetail,
};
// Handle form values change.
const handleValuesChange = (values) => {
if (values.notification_key !== notificationType) {
setNotificationType(values.notification_key);
}
};
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationTypes}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
onValuesChange={handleValuesChange}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyInvoiceViaSMSForm);

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { DialogContent } from 'components';
import { useCreateNotifyInvoiceBySMS, useInvoiceSMSDetail } from 'hooks/query';
const NotifyInvoiceViaSMSContext = React.createContext();
/**
* Invoice SMS notification provider.
*/
function NotifyInvoiceViaSMSFormProvider({ invoiceId, dialogName, ...props }) {
const [notificationType, setNotificationType] = React.useState('details');
// Retrieve the invoice sms notification message details.
const { data: invoiceSMSDetail, isLoading: isInvoiceSMSDetailLoading } =
useInvoiceSMSDetail(
invoiceId,
{
notification_key: notificationType,
},
{
enabled: !!invoiceId,
keepPreviousData: true,
},
);
// Create notfiy invoice by sms mutations.
const { mutateAsync: createNotifyInvoiceBySMSMutate } =
useCreateNotifyInvoiceBySMS();
// State provider.
const provider = {
invoiceId,
invoiceSMSDetail,
dialogName,
createNotifyInvoiceBySMSMutate,
notificationType,
setNotificationType,
};
return (
<DialogContent isLoading={isInvoiceSMSDetailLoading}>
<NotifyInvoiceViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useNotifyInvoiceViaSMSContext = () =>
React.useContext(NotifyInvoiceViaSMSContext);
export { NotifyInvoiceViaSMSFormProvider, useNotifyInvoiceViaSMSContext };

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const NotifyInvoiceViaSMSDialogContent = React.lazy(() =>
import('./NotifyInvoiceViaSMSDialogContent'),
);
function NotifyInvoiceViaSMSDialog({
dialogName,
payload: { invoiceId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyInvoiceViaSMSDialogContent
dialogName={dialogName}
invoiceId={invoiceId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(NotifyInvoiceViaSMSDialog);

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { DialogContent } from 'components';
import {
useCreateNotifyPaymentReceiveBySMS,
usePaymentReceiveSMSDetail,
} from 'hooks/query';
const NotifyPaymentReceiveViaSMSContext = React.createContext();
function NotifyPaymentReceiveViaFormProvider({
paymentReceiveId,
dialogName,
...props
}) {
// Create notfiy receipt via sms mutations.
const { mutateAsync: createNotifyPaymentReceivetBySMSMutate } =
useCreateNotifyPaymentReceiveBySMS();
const {
data: paymentReceiveMSDetail,
isLoading: isPaymentReceiveSMSDetailLoading,
} = usePaymentReceiveSMSDetail(paymentReceiveId, {
enabled: !!paymentReceiveId,
});
// State provider.
const provider = {
paymentReceiveId,
dialogName,
paymentReceiveMSDetail,
createNotifyPaymentReceivetBySMSMutate,
};
return (
<DialogContent isLoading={isPaymentReceiveSMSDetailLoading}>
<NotifyPaymentReceiveViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useNotifyPaymentReceiveViaSMSContext = () =>
React.useContext(NotifyPaymentReceiveViaSMSContext);
export {
NotifyPaymentReceiveViaFormProvider,
useNotifyPaymentReceiveViaSMSContext,
};

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { NotifyPaymentReceiveViaFormProvider } from './NotifyPaymentReceiveViaFormProvider';
import NotifyPaymentReceiveViaSMSForm from './NotifyPaymentReceiveViaSMSForm';
export default function NotifyPaymentReceiveViaSMSContent({
// #ownProps
dialogName,
paymentReceive,
}) {
return (
<NotifyPaymentReceiveViaFormProvider
paymentReceiveId={paymentReceive}
dialogName={dialogName}
>
<NotifyPaymentReceiveViaSMSForm />
</NotifyPaymentReceiveViaFormProvider>
);
}

View File

@@ -0,0 +1,87 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useNotifyPaymentReceiveViaSMSContext } from './NotifyPaymentReceiveViaFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const notificationType = {
key: 'payment-receive-details',
label: intl.get('sms_notification.payment_details.type'),
};
/**
* Notify Payment Recive Via SMS Form.
*/
function NotifyPaymentReceiveViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
dialogName,
paymentReceiveId,
paymentReceiveMSDetail,
createNotifyPaymentReceivetBySMSMutate,
} = useNotifyPaymentReceiveViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
'notify_payment_receive_via_sms.dialog.success_message',
),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
createNotifyPaymentReceivetBySMSMutate([paymentReceiveId, values])
.then(onSuccess)
.catch(onError);
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
// Form initial values.
const initialValues = React.useMemo(
() => ({
...paymentReceiveMSDetail,
notification_key: notificationType.key,
}),
[paymentReceiveMSDetail],
);
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationType}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyPaymentReceiveViaSMSForm);

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const NotifyPaymentReceiveViaSMSDialogContent = React.lazy(() =>
import('./NotifyPaymentReceiveViaSMSContent'),
);
function NotifyPaymentReciveViaSMSDialog({
dialogName,
payload: { paymentReceiveId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyPaymentReceiveViaSMSDialogContent
dialogName={dialogName}
paymentReceive={paymentReceiveId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(NotifyPaymentReciveViaSMSDialog);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { NotifyReceiptViaSMSFormProvider } from './NotifyReceiptViaSMSFormProvider';
import NotifyReceiptViaSMSForm from './NotifyReceiptViaSMSForm';
export default function NotifyReceiptViaSMSDialogContent({
// #ownProps
dialogName,
receipt,
}) {
return (
<NotifyReceiptViaSMSFormProvider
receiptId={receipt}
dialogName={dialogName}
>
<NotifyReceiptViaSMSForm />
</NotifyReceiptViaSMSFormProvider>
);
}

View File

@@ -0,0 +1,85 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useNotifyReceiptViaSMSContext } from './NotifyReceiptViaSMSFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const notificationType = {
key: 'sale-receipt-details',
label: intl.get('sms_notification.receipt_details.type'),
};
/**
* Notify Receipt Via SMS Form.
*/
function NotifyReceiptViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
dialogName,
receiptId,
receiptSMSDetail,
createNotifyReceiptBySMSMutate,
} = useNotifyReceiptViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('notify_receipt_via_sms.dialog.success_message'),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
createNotifyReceiptBySMSMutate([receiptId, values])
.then(onSuccess)
.catch(onError);
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
// Initial values.
const initialValues = React.useMemo(
() => ({
...receiptSMSDetail,
notification_key: notificationType.key,
}),
[receiptSMSDetail],
);
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationType}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyReceiptViaSMSForm);

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