diff --git a/CHANGELOG.md b/CHANGELOG.md index fcca15ca3..582810e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,43 @@ All notable changes to Bigcapital server-side will be in this file. +## [1.5.0] - 20-12-2021 +### Added +- Add credit note on sales module. +- Add vendor credit on purchases module. +- Optimize landed costs on purchase invoices. +- Display associated payment transactions on sale invoice drawer. +- Display associated pamyment transactions on purchase invoice drawer. +- Display item associate invoice, bill, estimate and receipt transactions. +- Transactions locking on all transactions or individual modules. +- Roles and permissions access control module. +- Optimize readonly details style of invoice, receipt, estimate, payment receive, + purchase invoice, expense, manual journal, inventory adjustment and cashflow transaction. + +### Changed +- Dashboard meta boot and authenticated user request query. + +## [1.4.0] - 11-09-2021 + +### Added +- Add SMS notification on sale invoice, receipt, customers payments modules. +- Customer quick create in customers list. +- Item quick create in items list. + +### Changes + change: BIG-171 alerts in global scope and lazy loading. +### Fixed + fix: BIG-140 - Reordering sell, cost and inventory account on item details. + fix: BIG-144 - Typo adjustment dialog success message. + fix: BIG-148 - Items entries ordered by index. + fix: BIG-132 AR/AP aging summary report filter by none transactions/zero contacts. + ## [1.2.0-RC] - 03-09-2021 Here we write upgrading notes for brands. It's a team effort to make them as straightforward as possible. ### Added - - Add slidable sub-sidebar to improve user experience instead of sub-menu. - Add Subscription guard to ensure the organization's subscription is active or redirect all routes to subscription billing page. @@ -35,13 +65,11 @@ straightforward as possible. - Add clickable datatable rows to display each row details. ### Changed - - Optimize style of datatable selection checkbox. - Disable animation in dashboard views tabs. - Optimize Arabic localization. ### Fixed - - fix: disable submit buttons in pereferences pages. - fix: inventory adjustment cost field max/min range to avoid out of range error. - fix: transactions by customers/vendors report localization. diff --git a/package.json b/package.json index 38afbca86..0ef5d4084 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "cross-env": "^7.0.2", "css-loader": "3.4.2", "deep-map-keys": "^2.0.1", + "dependency-graph": "^0.11.0", "dotenv": "8.2.0", "dotenv-expand": "5.1.0", "eslint": "^6.6.0", @@ -46,6 +47,7 @@ "eslint-plugin-jsx-a11y": "6.2.3", "eslint-plugin-react": "7.18.0", "eslint-plugin-react-hooks": "^1.6.1", + "fast-deep-equal": "^3.1.3", "file-loader": "4.3.0", "flow-bin": "^0.123.0", "formik": "^2.2.5", @@ -106,6 +108,7 @@ "semver": "6.3.0", "style-loader": "0.23.1", "styled-components": "^5.3.1", + "stylis-rtlcss": "^2.1.1", "terser-webpack-plugin": "2.3.4", "ts-pnp": "1.1.5", "url-loader": "2.3.0", diff --git a/src/common/TableStyle.js b/src/common/TableStyle.js new file mode 100644 index 000000000..d570a00fb --- /dev/null +++ b/src/common/TableStyle.js @@ -0,0 +1,6 @@ + + +export const TableStyle = { + Constrant: 'constrant', + Regular: 'regular' +} \ No newline at end of file diff --git a/src/common/abilityOption.js b/src/common/abilityOption.js index 1a5625ca3..7f586f047 100644 --- a/src/common/abilityOption.js +++ b/src/common/abilityOption.js @@ -17,6 +17,8 @@ export const AbilitySubject = { Preferences: 'Preferences', ExchangeRate: 'ExchangeRate', SubscriptionBilling: 'SubscriptionBilling', + CreditNote: 'CreditNote', + VendorCredit: 'VendorCredit', }; export const ItemAction = { @@ -46,7 +48,7 @@ export const SaleInvoiceAction = { Create: 'Create', Edit: 'Edit', Delete: 'Delete', - Writeoff: 'Writeoff', + Writeoff: 'bad-debt', NotifyBySms: 'NotifyBySms', }; @@ -66,6 +68,21 @@ export const PaymentReceiveAction = { NotifyBySms: 'NotifyBySms', }; +export const CreditNoteAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + Refund: 'Refund' +}; + +export const VendorCreditAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + Refund: 'Refund' +}; export const BillAction = { View: 'View', Create: 'Create', diff --git a/src/common/classes.js b/src/common/classes.js index e1df0b33f..6b0872396 100644 --- a/src/common/classes.js +++ b/src/common/classes.js @@ -39,6 +39,8 @@ const CLASSES = { PAGE_FORM_ITEM: 'page-form--item', PAGE_FORM_MAKE_JOURNAL: 'page-form--make-journal-entries', PAGE_FORM_EXPENSE: 'page-form--expense', + PAGE_FORM_CREDIT_NOTE:'page-form--credit-note', + PAGE_FORM_VENDOR_CREDIT_NOTE:'page-form--vendor-credit-note', FORM_GROUP_LIST_SELECT: 'form-group--select-list', diff --git a/src/common/drawers.js b/src/common/drawers.js index 3a1b21032..5ec6d05a2 100644 --- a/src/common/drawers.js +++ b/src/common/drawers.js @@ -14,4 +14,8 @@ export const DRAWERS = { QUICK_WRITE_VENDOR: 'quick-write-vendor', QUICK_CREATE_CUSTOMER: 'quick-create-customer', QUICK_CREATE_ITEM: 'quick-create-item', + CREDIT_NOTE_DETAIL_DRAWER: 'credit-note-detail-drawer', + VENDOR_CREDIT_DETAIL_DRAWER: 'vendor-credit-detail-drawer', + REFUND_CREDIT_NOTE_DETAIL_DRAWER:'refund-credit-detail-drawer', + REFUND_VENDOR_CREDIT_DETAIL_DRAWER:'refund-vendor-detail-drawer' }; diff --git a/src/common/index.js b/src/common/index.js new file mode 100644 index 000000000..25821fa35 --- /dev/null +++ b/src/common/index.js @@ -0,0 +1,4 @@ + + + +export * from './TableStyle'; \ No newline at end of file diff --git a/src/common/itemPaymentTranactionsOption.js b/src/common/itemPaymentTranactionsOption.js new file mode 100644 index 000000000..0811c5183 --- /dev/null +++ b/src/common/itemPaymentTranactionsOption.js @@ -0,0 +1,51 @@ +import intl from 'react-intl-universal'; +import { + AbilitySubject, + SaleEstimateAction, + SaleReceiptAction, + SaleInvoiceAction, + BillAction, +} from '../common/abilityOption'; +import { useAbilitiesFilter } from '../hooks'; + +export const getItemPaymentTransactions = () => [ + { + name: 'invoices', + label: intl.get('invoices'), + permission: { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.View, + }, + }, + { + name: 'estimates', + label: intl.get('estimates'), + permission: { + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.View, + }, + }, + { + name: 'receipts', + label: intl.get('receipts'), + permission: { + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.View, + }, + }, + { + name: 'bills', + label: intl.get('bills'), + permission: { + subject: AbilitySubject.Bill, + ability: BillAction.View, + }, + }, +]; + +export const useGetItemPaymentTransactionsMenu = () => { + const itemTransactionMenu = getItemPaymentTransactions(); + const abilitiesFilter = useAbilitiesFilter(); + + return abilitiesFilter(itemTransactionMenu); +}; diff --git a/src/common/permissionsSchema.js b/src/common/permissionsSchema.js new file mode 100644 index 000000000..38347abf8 --- /dev/null +++ b/src/common/permissionsSchema.js @@ -0,0 +1,659 @@ +import { chain } from 'lodash'; +import intl from 'react-intl-universal'; +import { + AbilitySubject, + AccountAction, + BillAction, + CreditNoteAction, + CustomerAction, + ExpenseAction, + ItemAction, + ManualJournalAction, + PaymentMadeAction, + PaymentReceiveAction, + ReportsAction, + SaleEstimateAction, + SaleInvoiceAction, + SaleReceiptAction, + VendorAction, + VendorCreditAction, +} from './abilityOption'; + +export const ModulePermissionsStyle = { + Columns: 'columns', + Vertical: 'vertical', +}; + +const PermissionColumn = { + View: 'view', + Create: 'create', + Delete: 'delete', + Edit: 'edit', +}; + +export const getPermissionsSchema = () => [ + { + label: intl.get('permissions.items_inventory'), + type: ModulePermissionsStyle.Columns, + serviceFullAccess: true, + columns: [ + { label: intl.get('permissions.column.view'), key: 'view' }, + { label: intl.get('permissions.column.create'), key: 'create' }, + { label: intl.get('permissions.column.edit'), key: 'edit' }, + { label: intl.get('permissions.column.delete'), key: 'delete' }, + ], + services: [ + { + label: intl.get('permissions.items'), + subject: AbilitySubject.Item, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: ItemAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: ItemAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: ItemAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: ItemAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: ItemAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: ItemAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: ItemAction.Edit }], + }, + ], + }, + { + label: intl.get('permissions.inventory_adjustment'), + subject: AbilitySubject.InventoryAdjustment, + permissions: [ + { + label: 'View', + key: ItemAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: 'Create', + key: ItemAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: ItemAction.View }], + }, + { + label: 'Edit', + key: ItemAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: ItemAction.Create }], + }, + { + label: 'Delete', + key: ItemAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: ItemAction.Edit }], + }, + ], + }, + ], + }, + { + label: intl.get('permissions.contacts'), + type: ModulePermissionsStyle.Columns, + serviceFullAccess: true, + moduleFullAccess: true, + columns: [ + { label: intl.get('permissions.column.view'), key: 'view' }, + { label: intl.get('permissions.column.create'), key: 'create' }, + { label: intl.get('permissions.column.edit'), key: 'edit' }, + { label: intl.get('permissions.column.delete'), key: 'delete' }, + ], + services: [ + { + label: intl.get('permissions.customers'), + subject: AbilitySubject.Customer, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: CustomerAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: CustomerAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: CustomerAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: CustomerAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: CustomerAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: CustomerAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: CustomerAction.Edit }], + }, + ], + }, + { + label: intl.get('permissions.vendors'), + subject: AbilitySubject.Vendor, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: VendorAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: VendorAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: VendorAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: VendorAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: VendorAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: VendorAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: VendorAction.Edit }], + }, + ], + }, + ], + }, + { + label: intl.get('permissions.sales'), + type: ModulePermissionsStyle.Columns, + serviceFullAccess: true, + moduleFullAccess: true, + columns: [ + { label: intl.get('permissions.column.view'), key: 'view' }, + { label: intl.get('permissions.column.create'), key: 'create' }, + { label: intl.get('permissions.column.edit'), key: 'edit' }, + { label: intl.get('permissions.column.delete'), key: 'delete' }, + ], + services: [ + { + label: intl.get('permissions.sale_invoice'), + subject: AbilitySubject.Invoice, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: SaleInvoiceAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: SaleInvoiceAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: SaleInvoiceAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: SaleInvoiceAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: SaleInvoiceAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: SaleInvoiceAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: SaleInvoiceAction.Edit }], + }, + { + label: intl.get('permissions.column.written_off_invoice'), + key: SaleInvoiceAction.Writeoff, + depend: [{ key: SaleInvoiceAction.Edit }], + }, + ], + }, + { + label: intl.get('permissions.sale_estimate'), + subject: AbilitySubject.Estimate, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: SaleEstimateAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: SaleEstimateAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: SaleEstimateAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: SaleEstimateAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: SaleEstimateAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: SaleEstimateAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: SaleEstimateAction.Edit }], + }, + ], + }, + { + label: intl.get('permissions.sale_receipt'), + subject: AbilitySubject.Receipt, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: SaleReceiptAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: SaleReceiptAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: SaleReceiptAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: SaleReceiptAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: SaleReceiptAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: SaleReceiptAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: SaleReceiptAction.Edit }], + }, + ], + }, + { + label: intl.get('permissions.credit_note'), + subject: AbilitySubject.CreditNote, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: CreditNoteAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: CreditNoteAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: CreditNoteAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: CreditNoteAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: CreditNoteAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: CreditNoteAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: CreditNoteAction.Edit }], + }, + { + label: intl.get('permissions.column.refund_credit_note'), + key: CreditNoteAction.Refund, + depend: [{ key: CreditNoteAction.View }], + }, + ], + }, + { + label: intl.get('permissions.payment_receive'), + subject: AbilitySubject.PaymentReceive, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: PaymentReceiveAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: PaymentReceiveAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: PaymentReceiveAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: PaymentReceiveAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: PaymentReceiveAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: PaymentReceiveAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: PaymentReceiveAction.Edit }], + }, + ], + }, + ], + }, + { + label: intl.get('permissions.purchases'), + type: ModulePermissionsStyle.Columns, + serviceFullAccess: true, + moduleFullAccess: true, + columns: [ + { label: intl.get('permissions.column.view'), key: 'view' }, + { label: intl.get('permissions.column.create'), key: 'create' }, + { label: intl.get('permissions.column.edit'), key: 'edit' }, + { label: intl.get('permissions.column.delete'), key: 'delete' }, + ], + services: [ + { + label: intl.get('permissions.bills'), + subject: AbilitySubject.Bill, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: BillAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: BillAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: BillAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: BillAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: BillAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: BillAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: BillAction.Edit }], + }, + ], + }, + { + label: intl.get('permissions.vendor_credits'), + subject: AbilitySubject.VendorCredit, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: VendorCreditAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: VendorCreditAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: VendorCreditAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: VendorCreditAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: VendorCreditAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: VendorCreditAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: VendorCreditAction.Edit }], + }, + { + label: intl.get('permissions.column.refund_vendor_credit'), + key: VendorCreditAction.Refund, + depend: [{ key: VendorCreditAction.View }], + }, + ], + }, + { + label: intl.get('permissions.payment_made'), + subject: AbilitySubject.PaymentMade, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: PaymentMadeAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: PaymentMadeAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: PaymentMadeAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: PaymentMadeAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: PaymentMadeAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: PaymentMadeAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: PaymentMadeAction.Edit }], + }, + ], + }, + ], + }, + { + label: intl.get('permissions.financial_accounting'), + type: ModulePermissionsStyle.Columns, + serviceFullAccess: true, + moduleFullAccess: true, + columns: [ + { label: intl.get('permissions.column.view'), key: 'view' }, + { label: intl.get('permissions.column.create'), key: 'create' }, + { label: intl.get('permissions.column.edit'), key: 'edit' }, + { label: intl.get('permissions.column.delete'), key: 'delete' }, + ], + services: [ + { + label: intl.get('permissions.manual_journals'), + subject: AbilitySubject.ManualJournal, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: ManualJournalAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: ManualJournalAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: ManualJournalAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: ManualJournalAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: ManualJournalAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: ManualJournalAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: ManualJournalAction.Edit }], + }, + ], + }, + { + label: intl.get('permissions.chart_of_accounts'), + subject: AbilitySubject.Account, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: AccountAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: AccountAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: AccountAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: AccountAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: AccountAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: AccountAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: AccountAction.Edit }], + }, + { + label: intl.get('permissions.column.transactions_locking'), + key: AccountAction.TransactionsLocking, + }, + ], + }, + { + label: intl.get('permissions.expenses'), + subject: AbilitySubject.Expense, + permissions: [ + { + label: intl.get('permissions.column.view'), + key: ExpenseAction.View, + relatedColumn: PermissionColumn.View, + }, + { + label: intl.get('permissions.column.create'), + key: ExpenseAction.Create, + relatedColumn: PermissionColumn.Create, + depend: [{ key: ExpenseAction.View }], + }, + { + label: intl.get('permissions.column.edit'), + key: ExpenseAction.Edit, + relatedColumn: PermissionColumn.Edit, + depend: [{ key: ExpenseAction.Create }], + }, + { + label: intl.get('permissions.column.delete'), + key: ExpenseAction.Delete, + relatedColumn: PermissionColumn.Delete, + depend: [{ key: ExpenseAction.Edit }], + }, + ], + }, + ], + }, + { + label: intl.get('permissions.reports'), + type: ModulePermissionsStyle.Vertical, + serviceFullAccess: true, + moduleFullAccess: true, + services: [ + { + label: intl.get('permissions.financial_reports'), + subject: AbilitySubject.Report, + permissions: [ + { + label: intl.get('permissions.balance_sheet'), + key: ReportsAction.READ_BALANCE_SHEET, + }, + { + label: intl.get('permissions.trial_balance_sheet'), + key: ReportsAction.READ_TRIAL_BALANCE_SHEET, + }, + { + label: intl.get('permissions.profit_loss_sheet'), + key: ReportsAction.READ_PROFIT_LOSS, + }, + { + label: intl.get('permissions.cash_flow_sheet'), + key: ReportsAction.READ_CASHFLOW, + }, + { + label: intl.get('permissions.journal_sheet'), + key: ReportsAction.READ_JOURNAL, + }, + { + label: intl.get('permissions.general_ledger'), + key: ReportsAction.READ_GENERAL_LEDGET, + }, + { + label: intl.get('permissions.a_r_aging_summary_report'), + key: ReportsAction.READ_AR_AGING_SUMMARY, + }, + { + label: intl.get('permissions.a_r_aging_summary_report'), + key: ReportsAction.READ_AP_AGING_SUMMARY, + }, + { + label: intl.get('permissions.purchases_by_items'), + key: ReportsAction.READ_PURCHASES_BY_ITEMS, + }, + { + label: intl.get('permissions.sales_by_items'), + key: ReportsAction.READ_SALES_BY_ITEMS, + }, + { + label: intl.get('permissions.customers_transactions'), + key: ReportsAction.READ_CUSTOMERS_TRANSACTIONS, + }, + { + label: intl.get('permissions.vendors_transactions'), + key: ReportsAction.READ_VENDORS_TRANSACTIONS, + }, + { + label: intl.get('permissions.customers_summary_balance'), + key: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE, + }, + { + label: intl.get('permissions.vendors_summary_balance'), + key: ReportsAction.READ_VENDORS_SUMMARY_BALANCE, + }, + { + label: intl.get('permissions.inventory_valuation_summary'), + key: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY, + }, + { + label: intl.get('permissions.inventory_items_details'), + key: ReportsAction.READ_INVENTORY_ITEM_DETAILS, + }, + { + label: intl.get('permissions.cashflow_account_transactions'), + key: ReportsAction.READ_CASHFLOW_ACCOUNT_TRANSACTION, + }, + ], + }, + ], + }, +]; + +export function getPermissionsSchemaService(subject) { + const permissions = getPermissionsSchema(); + + return chain(permissions) + .map((perm) => perm.services) + .flatten() + .find((service) => service.subject === subject) + .value(); +} + +export function getPermissionsSchemaServices() { + const permissions = getPermissionsSchema(); + + return chain(permissions) + .map((module) => module.services) + .flatten() + .value(); +} diff --git a/src/common/resourcesTypes.js b/src/common/resourcesTypes.js index 12ec85401..91e981aa8 100644 --- a/src/common/resourcesTypes.js +++ b/src/common/resourcesTypes.js @@ -11,4 +11,6 @@ export const RESOURCES_TYPES = { EXPENSE: 'expense', MANUAL_JOURNAL: 'manual_journal', ACCOUNT: 'account', + CREDIT_NOTE: 'credit_note', + VENDOR_CREDIT:'vendor_credit' }; diff --git a/src/common/tables.js b/src/common/tables.js index 9acd9bd5c..59d6ca127 100644 --- a/src/common/tables.js +++ b/src/common/tables.js @@ -14,6 +14,8 @@ export const TABLES = { EXPENSES: 'expenses', CASHFLOW_ACCOUNTS: 'cashflow_accounts', CASHFLOW_Transactions: 'cashflow_transactions', + CREDIT_NOTES: 'credit_notes', + VENDOR_CREDITS: 'vendor_credits', }; export const TABLE_SIZE = { diff --git a/src/components/Alert/index.js b/src/components/Alert/index.js index 680f8da5a..e0c3dd61a 100644 --- a/src/components/Alert/index.js +++ b/src/components/Alert/index.js @@ -1,17 +1,60 @@ import React from 'react'; import clsx from 'classnames'; +import styled from 'styled-components'; -import Style from './style.module.scss'; - -export function Alert({ title, description, intent }) { +export function Alert({ title, description, children, intent, className }) { return ( -
- {title &&

{title}

} - {description &&

{description}

} -
+ + {title && {title}} + {description && {description}} + {children && {children}} + ); } + +const AlertRoot = styled.div` + border: 1px solid rgb(223, 227, 230); + padding: 12px; + border-radius: 6px; + margin-bottom: 20px; + + ${(props) => + props.intent === 'danger' && + ` + border-color: rgb(249, 198, 198); + background: rgb(255, 248, 248); + + ${AlertDesc} { + color: #d95759; + } + ${AlertTitle} { + color: rgb(205, 43, 49); + } + `} + + ${(props) => + props.intent === 'primary' && + ` + background: #fff; + border-color: #98a8ee; + + ${AlertTitle} { + color: #1a3bd4; + } + ${AlertDesc} { + color: #455883; + } + `} +`; + +export const AlertTitle = styled.h3` + color: rgb(17, 24, 28); + margin-bottom: 4px; + font-size: 14px; + font-weight: 600; +`; + +export const AlertDesc = styled.p` + color: rgb(104, 112, 118); + margin: 0; +`; diff --git a/src/components/Alert/style.module.scss b/src/components/Alert/style.module.scss deleted file mode 100644 index be866addf..000000000 --- a/src/components/Alert/style.module.scss +++ /dev/null @@ -1,32 +0,0 @@ -.root { - border: 1px solid rgb(223, 227, 230); - padding: 12px; - border-radius: 6px; - margin-bottom: 20px; - - &_danger { - border-color: rgb(249, 198, 198); - background: rgb(255, 248, 248); - - .description { - color: #d95759; - } - - .title { - color: rgb(205, 43, 49); - } - } -} - - -.title { - color: rgb(17, 24, 28); - margin-bottom: 4px; - font-size: 14px; - font-weight: 600; -} - -.description { - color: rgb(104, 112, 118); - margin: 0; -} \ No newline at end of file diff --git a/src/components/BankAccounts/index.js b/src/components/BankAccounts/index.js index 0022eddfa..875889670 100644 --- a/src/components/BankAccounts/index.js +++ b/src/components/BankAccounts/index.js @@ -4,7 +4,6 @@ import styled from 'styled-components'; import { Classes } from '@blueprintjs/core'; import clsx from 'classnames'; import Icon from '../Icon'; -import { whenRtl, whenLtr } from 'utils/styled-components'; const ACCOUNT_TYPE = { CASH: 'cash', @@ -185,9 +184,7 @@ const MetaLineValue = styled.div` text-align: center; color: rgb(23, 43, 77); font-size: 11px; - - ${whenLtr(`margin-left: auto;`)} - ${whenRtl(`margin-right: auto;`)} + margin-left: auto; `; const BankAccountMeta = styled.div` @@ -204,7 +201,5 @@ const AccountIconWrap = styled.div` position: absolute; top: 14px; color: #abb3bb; - - ${whenLtr(`right: 12px;`)} - ${whenRtl(`left: 12px;`)} + right: 12px; `; diff --git a/src/components/Button/ButtonLink.js b/src/components/Button/ButtonLink.js index 655cdc615..739f12ecc 100644 --- a/src/components/Button/ButtonLink.js +++ b/src/components/Button/ButtonLink.js @@ -5,6 +5,7 @@ export const ButtonLink = styled.button` border: 0; background: transparent; cursor: pointer; + text-align: inherit; &:hover, &:active { diff --git a/src/components/Card.js b/src/components/Card.js deleted file mode 100644 index 65dd41eed..000000000 --- a/src/components/Card.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; - -export default function Card({ className, children }) { - return
{children}
; -} diff --git a/src/components/Card/index.js b/src/components/Card/index.js new file mode 100644 index 000000000..598085686 --- /dev/null +++ b/src/components/Card/index.js @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from 'styled-components'; + +export function Card({ className, children }) { + return {children}; +} + +const CardRoot = styled.div` + padding: 15px; + margin: 15px; + background: #fff; + border: 1px solid #d2dce2; +`; + +export const CardFooterActions = styled.div` + padding-top: 16px; + border-top: 1px solid #e0e7ea; + margin-top: 30px; + + .bp3-button { + min-width: 70px; + + + .bp3-button { + margin-left: 10px; + } + } +`; diff --git a/src/components/CommercialDoc/index.js b/src/components/CommercialDoc/index.js new file mode 100644 index 000000000..c8e2c593b --- /dev/null +++ b/src/components/CommercialDoc/index.js @@ -0,0 +1,25 @@ +import styled from 'styled-components'; +import { Card } from '../Card'; +import DataTable from '../DataTable'; + +export const CommercialDocBox = styled(Card)` + padding: 22px 20px; +`; + +export const CommercialDocHeader = styled.div` + margin-bottom: 25px; +`; + +export const CommercialDocTopHeader = styled.div` + margin-bottom: 25px; +`; + +export const CommercialDocEntriesTable = styled(DataTable)` + .tbody .tr:last-child .td { + border-bottom: 1px solid #d2dce2; + } +`; + +export const CommercialDocFooter = styled.div` + margin-top: 25px; +`; diff --git a/src/components/Customers/CustomerDrawerLink.js b/src/components/Customers/CustomerDrawerLink.js new file mode 100644 index 000000000..e99c20244 --- /dev/null +++ b/src/components/Customers/CustomerDrawerLink.js @@ -0,0 +1,25 @@ +import React from 'react'; +import * as R from 'ramda'; + +import { ButtonLink } from 'components'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; + +function CustomerDrawerLinkComponent({ + // #ownProps + children, + customerId, + + // #withDrawerActions + openDrawer, +}) { + // Handle view customer drawer. + const handleCustomerDrawer = () => { + openDrawer('customer-details-drawer', { customerId }); + }; + + return {children}; +} + +export const CustomerDrawerLink = R.compose(withDrawerActions)( + CustomerDrawerLinkComponent, +); diff --git a/src/components/Customers/index.js b/src/components/Customers/index.js new file mode 100644 index 000000000..124141b39 --- /dev/null +++ b/src/components/Customers/index.js @@ -0,0 +1 @@ +export * from './CustomerDrawerLink'; diff --git a/src/components/Dashboard/DashboardActionsBar.js b/src/components/Dashboard/DashboardActionsBar.js index b0ed85942..5d1795d5d 100644 --- a/src/components/Dashboard/DashboardActionsBar.js +++ b/src/components/Dashboard/DashboardActionsBar.js @@ -1,16 +1,19 @@ import React from 'react'; -import classnames from 'classnames'; +import clsx from 'classnames'; import { Navbar } from '@blueprintjs/core'; -export default function DashboardActionsBar({ children, name }) { +export default function DashboardActionsBar({ className, children, name }) { return (
- {children} + {children}
); } diff --git a/src/components/Dashboard/DashboardThemeProvider.js b/src/components/Dashboard/DashboardThemeProvider.js index 23300ddb9..7a4289e75 100644 --- a/src/components/Dashboard/DashboardThemeProvider.js +++ b/src/components/Dashboard/DashboardThemeProvider.js @@ -1,9 +1,16 @@ import React from 'react'; -import { ThemeProvider } from 'styled-components'; +import { ThemeProvider, StyleSheetManager } from 'styled-components'; +import rtlcss from 'stylis-rtlcss'; import { useAppIntlContext } from '../AppIntlProvider'; export function DashboardThemeProvider({ children }) { const { direction } = useAppIntlContext(); - return {children}; + return ( + + {children} + + ); } diff --git a/src/components/Datatable/TableWrapper.js b/src/components/Datatable/TableWrapper.js index 78646ea03..d98fab9b0 100644 --- a/src/components/Datatable/TableWrapper.js +++ b/src/components/Datatable/TableWrapper.js @@ -16,6 +16,7 @@ export default function TableWrapper({ children }) { expandable, virtualizedRows, className, + styleName, size, }, } = useContext(TableContext); @@ -28,6 +29,7 @@ export default function TableWrapper({ children }) { 'is-expandable': expandable, 'is-loading': loading, 'has-virtualized-rows': virtualizedRows, + [`table--${styleName}`]: styleName, })} > diff --git a/src/components/Details/index.js b/src/components/Details/index.js index ef1690f3c..ef480e1f2 100644 --- a/src/components/Details/index.js +++ b/src/components/Details/index.js @@ -17,6 +17,7 @@ const useDetailsMenuContext = () => React.useContext(DetailsMenuContext); export function DetailsMenu({ children, direction = DIRECTION.VERTICAL, + textAlign, minLabelSize, className, }) { @@ -27,6 +28,7 @@ export function DetailsMenu({ { 'details-menu--vertical': direction === DIRECTION.VERTICAL, 'details-menu--horizantal': direction === DIRECTION.HORIZANTAL, + [`align-${textAlign}`]: textAlign, }, className, )} diff --git a/src/components/Dialog/DialogFooterActions.js b/src/components/Dialog/DialogFooterActions.js index e24461203..bf7514527 100644 --- a/src/components/Dialog/DialogFooterActions.js +++ b/src/components/Dialog/DialogFooterActions.js @@ -2,6 +2,10 @@ import React from 'react'; import styled from 'styled-components'; import { Classes } from '@blueprintjs/core'; +/** + * Dialog footer actions. + * @returns {React.JSX} + */ export function DialogFooterActions({ alignment = 'right', children }) { return ( ; +} + +const DialogFooterRoot = styled.div` + flex: 0 0 auto; + margin: 0 20px; +`; + const DialogFooterActionsRoot = styled.div` ${(props) => props.alignment === 'right' ? 'margin-left: auto;' : 'margin-right: auto;'}; diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js index 871e38a58..d9d3609c5 100644 --- a/src/components/DialogsContainer.js +++ b/src/components/DialogsContainer.js @@ -25,7 +25,13 @@ import NotifyReceiptViaSMSDialog from '../containers/Dialogs/NotifyReceiptViaSMS 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 RefundCreditNoteDialog from '../containers/Dialogs/RefundCreditNoteDialog'; +import RefundVendorCreditDialog from '../containers/Dialogs/RefundVendorCreditDialog'; +import ReconcileCreditNoteDialog from '../containers/Dialogs/ReconcileCreditNoteDialog'; +import ReconcileVendorCreditDialog from '../containers/Dialogs/ReconcileVendorCreditDialog'; +import LockingTransactionsDialog from '../containers/Dialogs/LockingTransactionsDialog'; +import UnlockingTransactionsDialog from '../containers/Dialogs/UnlockingTransactionsDialog'; +import UnlockingPartialTransactionsDialog from '../containers/Dialogs/UnlockingPartialTransactionsDialog'; /** * Dialogs container. @@ -59,7 +65,15 @@ export default function DialogsContainer() { - + + + + + + + ); } diff --git a/src/components/Drawer/DrawerActionsBar.js b/src/components/Drawer/DrawerActionsBar.js new file mode 100644 index 000000000..d79be8b16 --- /dev/null +++ b/src/components/Drawer/DrawerActionsBar.js @@ -0,0 +1,12 @@ +import React from 'react'; +import styled from 'styled-components'; + +import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; + +export function DrawerActionsBar({ ...props }) { + return ; +} + +const DrawerActionsBarRoot = styled(DashboardActionsBar)` + border-bottom: 1px solid #d9d9da; +`; diff --git a/src/components/Drawer/DrawerMainTabs.js b/src/components/Drawer/DrawerMainTabs.js index 89ab0a52c..d2052e1f2 100644 --- a/src/components/Drawer/DrawerMainTabs.js +++ b/src/components/Drawer/DrawerMainTabs.js @@ -1,15 +1,54 @@ import React from 'react'; import { Tabs } from '@blueprintjs/core'; +import styled from 'styled-components'; /** * Drawer main tabs. */ export function DrawerMainTabs({ children, ...restProps }) { return ( -
+ {children} -
+ ); } + +const DrawerMainTabsRoot = styled.div` + .bp3-tabs { + .bp3-tab-list { + position: relative; + background-color: #fff; + padding: 0 15px; + border-bottom: 2px solid #e1e2e8; + + > *:not(:last-child) { + margin-right: 25px; + } + + &.bp3-large > .bp3-tab { + font-size: 15px; + color: #7f8596; + margin: 0 1rem; + + &[aria-selected='true'], + &:not([aria-disabled='true']):hover { + color: #0052cc; + } + } + .bp3-tab-indicator-wrapper .bp3-tab-indicator { + height: 2px; + bottom: -2px; + } + } + + .bp3-tab-panel { + margin-top: 0; + + .card { + margin: 15px; + } + } + } +`; diff --git a/src/components/Drawer/index.js b/src/components/Drawer/index.js index 005735517..d39c1d758 100644 --- a/src/components/Drawer/index.js +++ b/src/components/Drawer/index.js @@ -13,3 +13,5 @@ export function DrawerLoading({ loading, mount = false, children }) { export function DrawerBody({ children }) { return
{children}
; } + +export * from './DrawerActionsBar'; \ No newline at end of file diff --git a/src/components/DrawersContainer.js b/src/components/DrawersContainer.js index 135dcb8a3..e508519bd 100644 --- a/src/components/DrawersContainer.js +++ b/src/components/DrawersContainer.js @@ -17,6 +17,10 @@ import CashflowTransactionDetailDrawer from '../containers/Drawers/CashflowTrans import QuickCreateCustomerDrawer from '../containers/Drawers/QuickCreateCustomerDrawer'; import QuickCreateItemDrawer from '../containers/Drawers/QuickCreateItemDrawer'; import QuickWriteVendorDrawer from '../containers/Drawers/QuickWriteVendorDrawer'; +import CreditNoteDetailDrawer from '../containers/Drawers/CreditNoteDetailDrawer'; +import VendorCreditDetailDrawer from '../containers/Drawers/VendorCreditDetailDrawer'; +import RefundCreditNoteDetailDrawer from '../containers/Drawers/RefundCreditNoteDetailDrawer'; +import RefundVendorCreditDetailDrawer from '../containers/Drawers/RefundVendorCreditDetailDrawer'; import { DRAWERS } from 'common/drawers'; @@ -47,6 +51,14 @@ export default function DrawersContainer() { + + + + ); } diff --git a/src/components/Forms/FormikObserver.js b/src/components/Forms/FormikObserver.js new file mode 100644 index 000000000..a93b06864 --- /dev/null +++ b/src/components/Forms/FormikObserver.js @@ -0,0 +1,14 @@ + +import { useDeepCompareEffect } from 'hooks/utils'; + +export function FormikObserver({ onChange, values }) { + useDeepCompareEffect(() => { + onChange(values); + }, [values]); + + return null; +} + +FormikObserver.defaultProps = { + onChange: () => null, +}; diff --git a/src/components/Forms/index.js b/src/components/Forms/index.js index 8deddad40..c531c474b 100644 --- a/src/components/Forms/index.js +++ b/src/components/Forms/index.js @@ -1 +1,2 @@ -export * from './FormObserver'; \ No newline at end of file +export * from './FormObserver'; +export * from './FormikObserver'; \ No newline at end of file diff --git a/src/components/Preferences/PreferencesPage.js b/src/components/Preferences/PreferencesPage.js index a0e23dc97..f6be83dda 100644 --- a/src/components/Preferences/PreferencesPage.js +++ b/src/components/Preferences/PreferencesPage.js @@ -1,6 +1,8 @@ import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import classNames from 'classnames'; +import * as R from 'ramda'; + import { CLASSES } from 'common/classes'; import PreferencesTopbar from 'components/Preferences/PreferencesTopbar'; @@ -8,18 +10,28 @@ import PreferencesContentRoute from 'components/Preferences/PreferencesContentRo import DashboardErrorBoundary from 'components/Dashboard/DashboardErrorBoundary'; import PreferencesSidebar from 'components/Preferences/PreferencesSidebar'; +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; + import 'style/pages/Preferences/Page.scss'; /** * Preferences page. */ -export default function PreferencesPage() { +function PreferencesPage({ toggleSidebarExpand }) { + // Shrink the dashboard sidebar once open application preferences page. + React.useEffect(() => { + toggleSidebarExpand(false); + }, [toggleSidebarExpand]); + return ( -
+
@@ -32,3 +44,5 @@ export default function PreferencesPage() { ); } + +export default R.compose(withDashboardActions)(PreferencesPage); diff --git a/src/components/Table/index.js b/src/components/Table/index.js new file mode 100644 index 000000000..9c5afe084 --- /dev/null +++ b/src/components/Table/index.js @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +export const Table = styled.table` + width: 100%; + vertical-align: top; + border-color: #dee2e6; + border-spacing: 0; +`; +export const TBody = styled.tbody``; +export const TR = styled.tr``; +export const TD = styled.td` + padding: 0.5rem 0.5rem; + border-bottom-width: 1px; + border-bottom-color: inherit; + border-bottom-style: solid; + + ${(props) => + props.textAlign === 'right' && + ` + text-align: right;`} +`; + +export const TRDarkSingleLine = styled(TR)` + ${TD} { + border-bottom: 1px solid #000; + } +`; + +export const TRDarkDoubleLines = styled(TR)` + ${TD} { + border-bottom: 3px double #000; + } +`; diff --git a/src/components/Tags/CurrencyTag.js b/src/components/Tags/CurrencyTag.js new file mode 100644 index 000000000..a427c0230 --- /dev/null +++ b/src/components/Tags/CurrencyTag.js @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export const CurrencyTag = styled.span` + background: #3e9215; + color: #fff; + display: inline-block; + border-radius: 3px; + padding: 2px 4px; + line-height: 1; + margin-left: 4px; +`; diff --git a/src/components/Tags/index.js b/src/components/Tags/index.js new file mode 100644 index 000000000..7eca83954 --- /dev/null +++ b/src/components/Tags/index.js @@ -0,0 +1,3 @@ + + +export * from './CurrencyTag'; \ No newline at end of file diff --git a/src/components/TextStatus/index.js b/src/components/TextStatus/index.js new file mode 100644 index 000000000..186956a41 --- /dev/null +++ b/src/components/TextStatus/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import styled from 'styled-components'; + +export function TextStatus({ intent, children }) { + return {children}; +} + +const TextStatusRoot = styled.span` + ${(props) => + props.intent === 'warning' && + ` + color: #ec5b0a;`} + + ${(props) => + props.intent === 'success' && + ` + color: #2ba01d;`} + + ${(props) => + props.intent === 'none' && + ` + color: #777;`} + + ${(props) => + props.intent === 'primary' && + ` + color: #1652c8;`} +`; diff --git a/src/components/TotalLines/TotalLines.module.scss b/src/components/TotalLines/TotalLines.module.scss deleted file mode 100644 index 5c92246b5..000000000 --- a/src/components/TotalLines/TotalLines.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -.total_lines {} - - -.total_line { - display: flex; - border-bottom: 1px solid #d2dde2; - - :global .amount, - :global .title{ - padding: 8px; - } -} \ No newline at end of file diff --git a/src/components/TotalLines/index.js b/src/components/TotalLines/index.js index 793f70c0d..1b2d88fc6 100644 --- a/src/components/TotalLines/index.js +++ b/src/components/TotalLines/index.js @@ -1,23 +1,93 @@ import React from 'react'; -import clsx from 'classnames'; +import styled from 'styled-components'; -import TotalLinesCls from './TotalLines.module.scss'; +export const TotalLineBorderStyle = { + SingleDark: 'SingleDark', + DoubleDark: 'DoubleDark', +}; -export function TotalLines({ children, className }) { +export const TotalLineTextStyle = { + Regular: 'Regular', + Bold: 'Bold', +}; + +export function TotalLines({ + children, + amountColWidth, + labelColWidth, + className, +}) { return ( -
+ {children} -
+ ); } -export function TotalLine({ title, value, className }) { +export function TotalLine({ title, value, borderStyle, textStyle, className }) { return ( -
{title}
{value}
-
+ ); } + +export const TotalLinesRoot = styled.div` + display: table; + + ${(props) => + props.amountColWidth && + ` + .amount{ + width: ${props.amountColWidth} + } + `} + + ${(props) => + props.labelColWidth && + ` + .title{ + width: ${props.labelColWidth} + } + `} +`; + +export const TotalLineRoot = styled.div` + display: table-row; + + .amount, + .title { + display: table-cell; + padding: 8px; + border-bottom: 1px solid #d2dde2; + + ${(props) => + props.borderStyle === TotalLineBorderStyle.DoubleDark && + ` + border-bottom: 3px double #000; + `} + ${(props) => + props.borderStyle === TotalLineBorderStyle.SingleDark && + ` + border-bottom: 1px double #000; + `} + ${(props) => + props.textStyle === TotalLineTextStyle.Bold && + ` + font-weight: 600; + `} + } + + .amount { + text-align: right; + } +`; diff --git a/src/components/Typo/Paragraph.js b/src/components/Typo/Paragraph.js new file mode 100644 index 000000000..526425de3 --- /dev/null +++ b/src/components/Typo/Paragraph.js @@ -0,0 +1,6 @@ +import React from 'react'; +import clsx from 'classnames'; + +export function Paragraph({ className, children }) { + return

{children}

; +} \ No newline at end of file diff --git a/src/components/Typo/index.js b/src/components/Typo/index.js new file mode 100644 index 000000000..6d06a82b4 --- /dev/null +++ b/src/components/Typo/index.js @@ -0,0 +1,2 @@ + +export * from './Paragraph'; \ No newline at end of file diff --git a/src/components/Utils/Join.js b/src/components/Utils/Join.js new file mode 100644 index 000000000..a2314599e --- /dev/null +++ b/src/components/Utils/Join.js @@ -0,0 +1,13 @@ +import React from 'react'; + +export function Join({ items, sep }) { + return items.length > 0 + ? items.reduce((result, item) => ( + <> + {result} + {sep} + {item} + + )) + : null; +} diff --git a/src/components/Utils/index.js b/src/components/Utils/index.js index f1104e044..5be485ebc 100644 --- a/src/components/Utils/index.js +++ b/src/components/Utils/index.js @@ -1,4 +1,5 @@ export * from './FormatNumber'; -export * from './FormatDate'; \ No newline at end of file +export * from './FormatDate'; +export * from './Join'; \ No newline at end of file diff --git a/src/components/Vendors/VendorDrawerLink.js b/src/components/Vendors/VendorDrawerLink.js new file mode 100644 index 000000000..91b528e6f --- /dev/null +++ b/src/components/Vendors/VendorDrawerLink.js @@ -0,0 +1,23 @@ +import React from 'react'; +import * as R from 'ramda'; + +import { ButtonLink } from 'components'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; + +function VendorDrawerLinkComponent({ + // #ownProps + children, + vendorId, + + // #withDrawerActions + openDrawer, +}) { + // Handle view customer drawer. + const handleVendorDrawer = () => { + openDrawer('vendor-details-drawer', { vendorId }); + }; + + return {children}; +} + +export const VendorDrawerLink = R.compose(withDrawerActions)(VendorDrawerLinkComponent); diff --git a/src/components/Vendors/index.js b/src/components/Vendors/index.js new file mode 100644 index 000000000..8f9cc7544 --- /dev/null +++ b/src/components/Vendors/index.js @@ -0,0 +1 @@ +export * from './VendorDrawerLink' \ No newline at end of file diff --git a/src/components/index.js b/src/components/index.js index f30398145..5e14f2288 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -54,7 +54,6 @@ import Postbox from './Postbox'; import AccountsSuggestField from './AccountsSuggestField'; import MaterialProgressBar from './MaterialProgressBar'; import { MoneyFieldCell } from './DataTableCells'; -import Card from './Card'; import AvaterCell from './AvaterCell'; import { ItemsMultiSelect } from './Items'; @@ -87,6 +86,15 @@ export * from './Button'; export * from './IntersectionObserver'; export * from './SMSPreview'; export * from './Contacts'; +export * from './Utils/Join'; +export * from './Typo'; +export * from './TextStatus'; +export * from './Tags'; +export * from './CommercialDoc'; +export * from './Card'; +export * from './Customers' +export * from './Vendors' +export * from './Table'; const Hint = FieldHint; @@ -153,7 +161,6 @@ export { MaterialProgressBar, MoneyFieldCell, ItemsMultiSelect, - Card, AvaterCell, MoreMenuItems, }; diff --git a/src/config/sidebarMenu.js b/src/config/sidebarMenu.js index a847f8e52..1ea65f2a0 100644 --- a/src/config/sidebarMenu.js +++ b/src/config/sidebarMenu.js @@ -158,6 +158,10 @@ export default [ ability: SaleReceiptAction.View, }, }, + { + text: , + href: '/credit-notes', + }, { text: , href: '/payment-receives', @@ -233,6 +237,10 @@ export default [ ability: SaleReceiptAction.Create, }, }, + { + text: , + href: '/credit-notes/new', + }, { text: , href: '/payment-receives/new', @@ -254,6 +262,10 @@ export default [ ability: BillAction.View, }, }, + { + text: , + href: '/vendor-credits', + }, { text: , href: '/payment-mades', @@ -298,6 +310,14 @@ export default [ ability: BillAction.Create, }, }, + { + text: , + href: '/vendor-credits/new', + permission: { + subject: AbilitySubject.Bill, + ability: BillAction.Create, + }, + }, { text: , href: '/payment-mades/new', @@ -408,10 +428,10 @@ export default [ { text: , href: '/transactions-locking', - permission: { - subject: AbilitySubject.ManualJournal, - ability: ManualJournalAction.TransactionLocking, - }, + // permission: { + // subject: AbilitySubject.ManualJournal, + // ability: ManualJournalAction.TransactionLocking, + // }, }, { text: , diff --git a/src/containers/Accounting/JournalsLanding/ManualJournalsListProvider.js b/src/containers/Accounting/JournalsLanding/ManualJournalsListProvider.js index 595e9e3fb..15052ec05 100644 --- a/src/containers/Accounting/JournalsLanding/ManualJournalsListProvider.js +++ b/src/containers/Accounting/JournalsLanding/ManualJournalsListProvider.js @@ -46,8 +46,7 @@ function ManualJournalsListProvider({ query, tableStateChanged, ...props }) { isEmptyStatus, }; - const isPageLoading = - isManualJournalsLoading || isViewsLoading || isResourceMetaLoading; + const isPageLoading = isViewsLoading || isResourceMetaLoading; return ( diff --git a/src/containers/Accounting/JournalsLanding/components.js b/src/containers/Accounting/JournalsLanding/components.js index fab711e72..1384e3a87 100644 --- a/src/containers/Accounting/JournalsLanding/components.js +++ b/src/containers/Accounting/JournalsLanding/components.js @@ -101,13 +101,13 @@ export const StatusAccessor = (row) => { return ( - + - + @@ -179,6 +179,7 @@ export const ActionsMenu = ({ /> + } diff --git a/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.js b/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.js index a95429551..469f722d1 100644 --- a/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.js +++ b/src/containers/Accounting/MakeJournal/MakeJournalFormFloatingActions.js @@ -151,6 +151,7 @@ export default function MakeJournalFloatingAction() { disabled={isSubmitting} intent={Intent.PRIMARY} onClick={handleSubmitPublishBtnClick} + style={{ minWidth: '85px' }} text={} /> { + closeAlert(name); + }; + const handleConfirmCreditNoteDelete = () => { + deleteCreditNoteMutate(creditNoteId) + .then(() => { + AppToaster.show({ + message: intl.get('credit_note.alert.delete_message'), + intent: Intent.SUCCESS, + }); + closeDrawer('credit-note-detail-drawer'); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => { + handleDeleteErrors(errors); + }, + ) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelDeleteAlert} + onConfirm={handleConfirmCreditNoteDelete} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, + withDrawerActions, +)(CreditNoteDeleteAlert); diff --git a/src/containers/Alerts/CreditNotes/CreditNoteOpenedAlert.js b/src/containers/Alerts/CreditNotes/CreditNoteOpenedAlert.js new file mode 100644 index 000000000..58b9e8554 --- /dev/null +++ b/src/containers/Alerts/CreditNotes/CreditNoteOpenedAlert.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { FormattedMessage as T } from 'components'; +import intl from 'react-intl-universal'; +import { Intent, Alert } from '@blueprintjs/core'; + +import { useOpenCreditNote } from 'hooks/query'; +import { AppToaster } from 'components'; + +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; + +import { compose } from 'utils'; + +/** + * Credit note opened alert. + */ +function CreditNoteOpenedAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { creditNoteId }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: openCreditNoteMutate, isLoading } = useOpenCreditNote(); + + // Handle cancel opened credit note alert. + const handleAlertCancel = () => { + closeAlert(name); + }; + + // Handle confirm credit note opened. + const handleAlertConfirm = () => { + openCreditNoteMutate(creditNoteId) + .then(() => { + AppToaster.show({ + message: intl.get('credit_note_opened.alert.success_message'), + intent: Intent.SUCCESS, + }); + }) + .catch((error) => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleAlertCancel} + onConfirm={handleAlertConfirm} + loading={isLoading} + > +

+ +

+
+ ); +} +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(CreditNoteOpenedAlert); diff --git a/src/containers/Alerts/CreditNotes/ReconcileCreditNoteDeleteAlert.js b/src/containers/Alerts/CreditNotes/ReconcileCreditNoteDeleteAlert.js new file mode 100644 index 000000000..7322fbf9d --- /dev/null +++ b/src/containers/Alerts/CreditNotes/ReconcileCreditNoteDeleteAlert.js @@ -0,0 +1,87 @@ +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 withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; + +import { useDeleteReconcileCredit } from 'hooks/query'; +import { handleDeleteErrors } from '../../Sales/CreditNotes/CreditNotesLanding/utils'; +import { compose } from 'utils'; + +/** + * Reconcile credit note delete alert. + */ +function ReconcileCreditNoteDeleteAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { creditNoteId }, + + // #withAlertActions + closeAlert, + + // #withDrawerActions + closeDrawer, +}) { + const { isLoading, mutateAsync: deleteReconcileCreditMutate } = + useDeleteReconcileCredit(); + + // handle cancel delete credit note alert. + const handleCancelDeleteAlert = () => { + closeAlert(name); + }; + + const handleConfirmVendorCreditDelete = () => { + deleteReconcileCreditMutate(creditNoteId) + .then(() => { + AppToaster.show({ + message: intl.get('reconcile_credit_note.alert.success_message'), + intent: Intent.SUCCESS, + }); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => { + // handleDeleteErrors(errors); + }, + ) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelDeleteAlert} + onConfirm={handleConfirmVendorCreditDelete} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, + withDrawerActions, +)(ReconcileCreditNoteDeleteAlert); diff --git a/src/containers/Alerts/CreditNotes/RefundCreditNoteDeleteAlert.js b/src/containers/Alerts/CreditNotes/RefundCreditNoteDeleteAlert.js new file mode 100644 index 000000000..da575c2af --- /dev/null +++ b/src/containers/Alerts/CreditNotes/RefundCreditNoteDeleteAlert.js @@ -0,0 +1,76 @@ +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 { useDeleteRefundCreditNote } from 'hooks/query'; + +import withAlertActions from 'containers/Alert/withAlertActions'; +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; + +import { compose } from 'utils'; + +/** + * Refund credit transactions delete alert + */ +function RefundCreditNoteDeleteAlert({ + name, + // #withAlertStoreConnect + isOpen, + payload: { creditNoteId }, + // #withAlertActions + closeAlert, + + // #withDrawerActions + closeDrawer, +}) { + const { mutateAsync: deleteRefundCreditMutate, isLoading } = + useDeleteRefundCreditNote(); + + // Handle cancel delete. + const handleCancelAlert = () => { + closeAlert(name); + }; + + // Handle confirm delete . + const handleConfirmRefundCreditDelete = () => { + deleteRefundCreditMutate(creditNoteId) + .then(() => { + AppToaster.show({ + message: intl.get('refund_credit_transactions.alert.delete_message'), + intent: Intent.SUCCESS, + }); + closeDrawer('refund-credit-detail-drawer'); + }) + .catch(() => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelAlert} + onConfirm={handleConfirmRefundCreditDelete} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, + withDrawerActions, +)(RefundCreditNoteDeleteAlert); diff --git a/src/containers/Alerts/TransactionLocking/cancelUnlockingPartialAlert.js b/src/containers/Alerts/TransactionLocking/cancelUnlockingPartialAlert.js new file mode 100644 index 000000000..d3e8ba0bc --- /dev/null +++ b/src/containers/Alerts/TransactionLocking/cancelUnlockingPartialAlert.js @@ -0,0 +1,81 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { FormattedMessage as T } from 'components'; +import { Intent, Alert } from '@blueprintjs/core'; +import { AppToaster } from 'components'; + +import { useCancelUnlockingPartialTransactions } from 'hooks/query'; + +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; + +import { compose } from 'utils'; + +/** + * Cancel Unlocking partial transactions alerts. + */ +function CancelUnlockingPartialTarnsactions({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { module }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: cancelUnlockingPartial, isLoading } = + useCancelUnlockingPartialTransactions(); + + // Handle cancel. + const handleCancel = () => { + closeAlert(name); + }; + + // Handle confirm. + const handleConfirm = () => { + const values = { + module: module, + }; + cancelUnlockingPartial(values) + .then(() => { + AppToaster.show({ + message: intl.get( + 'unlocking_partial_transactions.alert.cancel_message', + ), + intent: Intent.SUCCESS, + }); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => {}, + ) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancel} + onConfirm={handleConfirm} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(CancelUnlockingPartialTarnsactions); diff --git a/src/containers/Alerts/VendorCeditNotes/ReconcileVendorCreditDeleteAlert.js b/src/containers/Alerts/VendorCeditNotes/ReconcileVendorCreditDeleteAlert.js new file mode 100644 index 000000000..bf493b47d --- /dev/null +++ b/src/containers/Alerts/VendorCeditNotes/ReconcileVendorCreditDeleteAlert.js @@ -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 withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; + +import { useDeleteReconcileVendorCredit } from 'hooks/query'; +import { compose } from 'utils'; + +/** + * Reconcile vendor credit delete alert. + */ +function ReconcileVendorCreditDeleteAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { vendorCreditId }, + + // #withAlertActions + closeAlert, + + // #withDrawerActions + closeDrawer, +}) { + const { isLoading, mutateAsync: deleteReconcileVendorCreditMutate } = + useDeleteReconcileVendorCredit(); + + // handle cancel delete credit note alert. + const handleCancelDeleteAlert = () => { + closeAlert(name); + }; + + const handleConfirmReconcileVendorCreditDelete = () => { + deleteReconcileVendorCreditMutate(vendorCreditId) + .then(() => { + AppToaster.show({ + message: intl.get('reconcile_vendor_credit.alert.success_message'), + intent: Intent.SUCCESS, + }); + // closeDrawer('vendor-credit-detail-drawer'); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => {}, + ) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelDeleteAlert} + onConfirm={handleConfirmReconcileVendorCreditDelete} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, + withDrawerActions, +)(ReconcileVendorCreditDeleteAlert); diff --git a/src/containers/Alerts/VendorCeditNotes/RefundVendorCreditDeleteAlert.js b/src/containers/Alerts/VendorCeditNotes/RefundVendorCreditDeleteAlert.js new file mode 100644 index 000000000..4e17eb2ec --- /dev/null +++ b/src/containers/Alerts/VendorCeditNotes/RefundVendorCreditDeleteAlert.js @@ -0,0 +1,78 @@ +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 { useDeleteRefundVendorCredit } from 'hooks/query'; + +import withAlertActions from 'containers/Alert/withAlertActions'; +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; + +import { compose } from 'utils'; + +/** + * Refund Vendor transactions delete alert. + */ +function RefundVendorCreditDeleteAlert({ + name, + // #withAlertStoreConnect + isOpen, + payload: { vendorCreditId }, + // #withAlertActions + closeAlert, + + // #withDrawerActions + closeDrawer, +}) { + const { mutateAsync: deleteRefundVendorCreditMutate, isLoading } = + useDeleteRefundVendorCredit(); + + // Handle cancel delete. + const handleCancelAlert = () => { + closeAlert(name); + }; + + // Handle confirm delete . + const handleConfirmRefundVendorCreditDelete = () => { + deleteRefundVendorCreditMutate(vendorCreditId) + .then(() => { + AppToaster.show({ + message: intl.get( + 'refund_vendor_credit_transactions.alert.delete_message', + ), + intent: Intent.SUCCESS, + }); + closeDrawer('refund-vendor-detail-drawer'); + }) + .catch(() => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelAlert} + onConfirm={handleConfirmRefundVendorCreditDelete} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, + withDrawerActions, +)(RefundVendorCreditDeleteAlert); diff --git a/src/containers/Alerts/VendorCeditNotes/VendorCreditDeleteAlert.js b/src/containers/Alerts/VendorCeditNotes/VendorCreditDeleteAlert.js new file mode 100644 index 000000000..165b7000c --- /dev/null +++ b/src/containers/Alerts/VendorCeditNotes/VendorCreditDeleteAlert.js @@ -0,0 +1,84 @@ +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 withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; +import { handleDeleteErrors } from '../../Purchases/CreditNotes/CreditNotesLanding/utils'; +import { useDeleteVendorCredit } from 'hooks/query'; +import { compose } from 'utils'; + +/** + * Vendor Credit delete alert. + */ +function VendorCreditDeleteAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { vendorCreditId }, + + // #withAlertActions + closeAlert, + + // #withDrawerActions + closeDrawer, +}) { + const { isLoading, mutateAsync: deleteVendorCreditMutate } = + useDeleteVendorCredit(); + + // handle cancel delete credit note alert. + const handleCancelDeleteAlert = () => { + closeAlert(name); + }; + const handleConfirmCreditDelete = () => { + deleteVendorCreditMutate(vendorCreditId) + .then(() => { + AppToaster.show({ + message: intl.get('vendor_credits.alert.delete_message'), + intent: Intent.SUCCESS, + }); + closeDrawer('vendor-credit-detail-drawer'); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => { + handleDeleteErrors(errors); + }, + ) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelDeleteAlert} + onConfirm={handleConfirmCreditDelete} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, + withDrawerActions, +)(VendorCreditDeleteAlert); diff --git a/src/containers/Alerts/VendorCeditNotes/VendorCreditOpenedAlert.js b/src/containers/Alerts/VendorCeditNotes/VendorCreditOpenedAlert.js new file mode 100644 index 000000000..56c0fe5bf --- /dev/null +++ b/src/containers/Alerts/VendorCeditNotes/VendorCreditOpenedAlert.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { FormattedMessage as T } from 'components'; +import intl from 'react-intl-universal'; +import { Intent, Alert } from '@blueprintjs/core'; + +import { useOpenVendorCredit } from 'hooks/query'; +import { AppToaster } from 'components'; + +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; + +import { compose } from 'utils'; + +/** + * Vendor credit opened alert. + */ +function VendorCreditOpenedAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { vendorCreditId }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: openVendorCreditMutate, isLoading } = + useOpenVendorCredit(); + + // Handle cancel opened credit note alert. + const handleAlertCancel = () => { + closeAlert(name); + }; + + // Handle confirm vendor credit as opened. + const handleAlertConfirm = () => { + openVendorCreditMutate(vendorCreditId) + .then(() => { + AppToaster.show({ + message: intl.get('vendor_credit_opened.alert.success_message'), + intent: Intent.SUCCESS, + }); + }) + .catch((error) => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleAlertCancel} + onConfirm={handleAlertConfirm} + loading={isLoading} + > +

+ +

+
+ ); +} +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(VendorCreditOpenedAlert); diff --git a/src/containers/AlertsContainer/registered.js b/src/containers/AlertsContainer/registered.js index d0e77fcfb..57d66288a 100644 --- a/src/containers/AlertsContainer/registered.js +++ b/src/containers/AlertsContainer/registered.js @@ -17,6 +17,9 @@ import AccountTransactionsAlerts from '../CashFlow/AccountTransactions/AccountTr import UsersAlerts from '../Preferences/Users/UsersAlerts'; import CurrenciesAlerts from '../Preferences/Currencies/CurrenciesAlerts'; import RolesAlerts from '../Preferences/Users/Roles/RolesAlerts'; +import CreditNotesAlerts from '../Sales/CreditNotes/CreditNotesAlerts'; +import VendorCreditNotesAlerts from '../Purchases/CreditNotes/VendorCreditNotesAlerts'; +import TransactionsLockingAlerts from '../TransactionsLocking/TransactionsLockingAlerts' export default [ ...AccountsAlerts, @@ -38,4 +41,7 @@ export default [ ...UsersAlerts, ...CurrenciesAlerts, ...RolesAlerts, + ...CreditNotesAlerts, + ...VendorCreditNotesAlerts, + ...TransactionsLockingAlerts ]; diff --git a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js index f6ebecbe4..8b1621db2 100644 --- a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js +++ b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js @@ -18,7 +18,6 @@ import { useAccountTransactionsContext } from './AccountTransactionsProvider'; import { handleCashFlowTransactionType } from './utils'; import { compose } from 'utils'; -import { whenRtl, whenLtr } from 'utils/styled-components'; /** * Account transactions data table. @@ -129,8 +128,7 @@ const CashflowTransactionsTable = styled(DashboardConstrantTable)` .tbody-inner { .tr .td:not(:first-child) { - ${whenLtr(`border-left: 1px solid #e6e6e6;`)} - ${whenRtl(`border-right: 1px solid #e6e6e6;`)} + border-left: 1px solid #e6e6e6; } } } diff --git a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js index a41068f09..bdec0f6b0 100644 --- a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js +++ b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js @@ -14,7 +14,6 @@ import { curry } from 'lodash/fp'; import { Icon } from '../../../components'; import { useAccountTransactionsContext } from './AccountTransactionsProvider'; -import { whenRtl, whenLtr } from 'utils/styled-components'; function AccountSwitchButton() { const { currentAccount } = useAccountTransactionsContext(); @@ -23,7 +22,7 @@ function AccountSwitchButton() { } - > + > {currentAccount.name} ); @@ -161,8 +160,7 @@ const AccountBalanceAmount = styled.span` font-weight: 600; display: inline-block; color: rgb(31, 50, 85); - ${whenLtr(`margin-left: 10px;`)} - ${whenRtl(`margin-right: 10px;`)} + margin-left: 10px; `; const AccountSwitchItemName = styled.div` @@ -180,7 +178,6 @@ const AccountSwitchItemUpdatedAt = styled.div` const AccountSwitchButtonBase = styled(Button)` .bp3-button-text { - ${whenLtr(`margin-right: 5px;`)} - ${whenRtl(`margin-left: 5px;`)} + margin-right: 5px; } `; diff --git a/src/containers/CashFlow/AccountTransactions/utils.js b/src/containers/CashFlow/AccountTransactions/utils.js index 59f8b43ea..567422ea6 100644 --- a/src/containers/CashFlow/AccountTransactions/utils.js +++ b/src/containers/CashFlow/AccountTransactions/utils.js @@ -71,6 +71,14 @@ export const handleCashFlowTransactionType = (reference, openDrawer) => { return openDrawer('payment-made-detail-drawer', { paymentMadeId: reference.reference_id, }); + case 'RefundCreditNote': + return openDrawer('refund-credit-detail-drawer', { + refundTransactionId: reference.reference_id, + }); + case 'RefundVendorCredit': + return openDrawer('refund-vendor-detail-drawer', { + refundTransactionId: reference.reference_id, + }); default: return openDrawer('cashflow-transaction-drawer', { diff --git a/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js b/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js index cf06161b9..dc6104046 100644 --- a/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js +++ b/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js @@ -271,6 +271,7 @@ function CashflowAccountContextMenu({
+ } diff --git a/src/containers/Customers/CustomersLanding/CustomersListProvider.js b/src/containers/Customers/CustomersLanding/CustomersListProvider.js index cd38e45a5..6fcb66ad1 100644 --- a/src/containers/Customers/CustomersLanding/CustomersListProvider.js +++ b/src/containers/Customers/CustomersLanding/CustomersListProvider.js @@ -53,7 +53,7 @@ function CustomersListProvider({ tableState, tableStateChanged, ...props }) { return ( diff --git a/src/containers/Customers/CustomersLanding/components.js b/src/containers/Customers/CustomersLanding/components.js index 9d878bbf4..2eacc0103 100644 --- a/src/containers/Customers/CustomersLanding/components.js +++ b/src/containers/Customers/CustomersLanding/components.js @@ -63,6 +63,7 @@ export function ActionsMenu({ + } text={intl.get('delete_customer')} diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js index bd307ddbb..20f094ed1 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js @@ -1,7 +1,10 @@ import React from 'react'; -import intl from 'react-intl-universal'; -import { MoneyFieldCell, DataTableEditable } from 'components'; +import styled from 'styled-components'; + +import { DataTableEditable } from 'components'; + import { compose, updateTableCell } from 'utils'; +import { useAllocateLandedCostEntriesTableColumns } from './utils'; /** * Allocate landed cost entries table. @@ -11,42 +14,7 @@ export default function AllocateLandedCostEntriesTable({ entries, }) { // Allocate landed cost entries table columns. - const columns = React.useMemo( - () => [ - { - Header: intl.get('item'), - accessor: 'item.name', - disableSortBy: true, - width: '150', - }, - { - Header: intl.get('quantity'), - accessor: 'quantity', - disableSortBy: true, - width: '100', - }, - { - Header: intl.get('rate'), - accessor: 'rate', - disableSortBy: true, - width: '100', - }, - { - Header: intl.get('amount'), - accessor: 'amount', - disableSortBy: true, - width: '100', - }, - { - Header: intl.get('cost'), - accessor: 'cost', - width: '150', - Cell: MoneyFieldCell, - disableSortBy: true, - }, - ], - [], - ); + const columns = useAllocateLandedCostEntriesTableColumns(); // Handle update data. const handleUpdateData = React.useCallback( @@ -60,7 +28,7 @@ export default function AllocateLandedCostEntriesTable({ ); return ( - ); } + +export const AllocateLandeedCostEntriesEditableTable = styled( + DataTableEditable, +)` + .table { + .thead .tr .th { + padding-top: 8px; + padding-bottom: 8px; + } + + .tbody .tr .td { + padding: 0.25rem; + } + } +`; diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js index de5b7e16e..121f86aa5 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js @@ -12,12 +12,18 @@ import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogP import withDialogActions from 'containers/Dialog/withDialogActions'; import { compose } from 'utils'; +/** + * Allocate landed cost floating actions. + * @returns {React.JSX} + */ function AllocateLandedCostFloatingActions({ // #withDialogActions closeDialog, }) { // Formik context. const { isSubmitting } = useFormikContext(); + + // Allocate landed cost dialog context. const { dialogName, costTransactionEntry, formattedUnallocatedCostAmount } = useAllocateLandedConstDialogContext(); @@ -27,7 +33,7 @@ function AllocateLandedCostFloatingActions({ }; return ( - + {costTransactionEntry && ( @@ -43,19 +49,23 @@ function AllocateLandedCostFloatingActions({ - + ); } export default compose(withDialogActions)(AllocateLandedCostFloatingActions); +const AllocateDialogFooter = styled(DialogFooter)` + display: flex; +`; + const UnallocatedAmount = styled.div` color: #3f5278; align-self: center; diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js index 5cb1a2fa2..e4ac664cd 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js @@ -11,21 +11,7 @@ import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogP import AllocateLandedCostFormContent from './AllocateLandedCostFormContent'; import withDialogActions from 'containers/Dialog/withDialogActions'; import { compose, transformToForm } from 'utils'; - -const defaultInitialItem = { - entry_id: '', - cost: '', -}; - -// Default form initial values. -const defaultInitialValues = { - transaction_type: 'Bill', - transaction_id: '', - transaction_entry_id: '', - amount: '', - allocation_method: 'quantity', - items: [defaultInitialItem], -}; +import { defaultInitialValues } from './utils'; /** * Allocate landed cost form. @@ -34,13 +20,8 @@ function AllocateLandedCostForm({ // #withDialogActions closeDialog, }) { - const { - dialogName, - bill, - billId, - createLandedCostMutate, - unallocatedCostAmount, - } = useAllocateLandedConstDialogContext(); + const { dialogName, bill, billId, createLandedCostMutate } = + useAllocateLandedConstDialogContext(); // Initial form values. const initialValues = { @@ -51,7 +32,6 @@ function AllocateLandedCostForm({ cost: '', })), }; - // Handle form submit. const handleFormSubmit = (values, { setSubmitting }) => { setSubmitting(true); @@ -78,20 +58,33 @@ function AllocateLandedCostForm({ setSubmitting(false); closeDialog(dialogName); }; - // Handle the request error. - const onError = () => { + const onError = (res) => { + const { errors } = res.response.data; setSubmitting(false); - AppToaster.show({ - message: 'Something went wrong!', - intent: Intent.DANGER, - }); + + if ( + errors.some( + (e) => e.type === 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', + ) + ) { + AppToaster.show({ + message: + 'The total located cost is bigger than the transaction line.', + intent: Intent.DANGER, + }); + } else { + AppToaster.show({ + message: 'Something went wrong!', + intent: Intent.DANGER, + }); + } }; createLandedCostMutate([billId, form]).then(onSuccess).catch(onError); }; // Computed validation schema. - const validationSchema = AllocateLandedCostFormSchema(unallocatedCostAmount); + const validationSchema = AllocateLandedCostFormSchema(); return ( +export const AllocateLandedCostFormSchema = () => Yup.object().shape({ - transaction_type: Yup.string() - .required() - .label(intl.get('transaction_type')), - transaction_id: Yup.string() - .required() - .label(intl.get('transaction_number')), - transaction_entry_id: Yup.string() - .required() - .label(intl.get('transaction_line')), - amount: Yup.number().max(maxAmount).label(intl.get('amount')), - allocation_method: Yup.string().required().trim(), + transaction_type: Yup.string().label(intl.get('transaction_type')), + transaction_date: Yup.date().label(intl.get('transaction_date')), + transaction_id: Yup.string().label(intl.get('transaction_number')), + transaction_entry_id: Yup.string().label(intl.get('transaction_line')), + amount: Yup.number().label(intl.get('amount')), + allocation_method: Yup.string().trim(), items: Yup.array().of( Yup.object().shape({ entry_id: Yup.number().nullable(), diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js index d71ebab2c..5dec5e145 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js @@ -103,8 +103,10 @@ export default function AllocateLandedCostFormFields() { selectedItem={value} selectedItemProp={'id'} textProp={'name'} - labelProp={'id'} - defaultText={intl.get('Select transaction')} + labelProp={'formatted_unallocated_cost_amount'} + defaultText={intl.get( + 'landed_cost.dialog.label_select_transaction', + )} popoverProps={{ minimal: true }} /> @@ -131,16 +133,17 @@ export default function AllocateLandedCostFormFields() { > { + onItemSelect={(entry) => { + const { id, unallocated_cost_amount: unallocatedAmount } = + entry; const { items, allocation_method } = form.values; + form.setFieldValue('amount', unallocatedAmount); form.setFieldValue('transaction_entry_id', id); - form.setFieldValue('amount', unallocated_cost_amount); - form.setFieldValue( 'items', allocateCostToEntries( - unallocated_cost_amount, + unallocatedAmount, allocation_method, items, ), @@ -150,7 +153,10 @@ export default function AllocateLandedCostFormFields() { selectedItem={value} selectedItemProp={'id'} textProp={'name'} - defaultText={intl.get('Select transaction entry')} + labelProp={'formatted_unallocated_cost_amount'} + defaultText={intl.get( + 'landed_cost.dialog.label_select_transaction_entry', + )} popoverProps={{ minimal: true }} /> diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/utils.js b/src/containers/Dialogs/AllocateLandedCostDialog/utils.js index a19fff723..1cfd6775a 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/utils.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/utils.js @@ -1,13 +1,25 @@ +import React from 'react'; import { sumBy, round } from 'lodash'; import * as R from 'ramda'; -import { defaultFastFieldShouldUpdate } from 'utils'; +import intl from 'react-intl-universal'; -/** - * Retrieve the landed cost transaction by the given id. - */ -export function getCostTransactionById(id, transactions) { - return transactions.find((trans) => trans.id === id); -} +import { defaultFastFieldShouldUpdate } from 'utils'; +import { MoneyFieldCell } from 'components'; + +export const defaultInitialItem = { + entry_id: '', + cost: '', +}; + +// Default form initial values. +export const defaultInitialValues = { + transaction_type: 'Bill', + transaction_id: '', + transaction_entry_id: '', + amount: '', + allocation_method: 'quantity', + items: [defaultInitialItem], +}; /** * Retrieve transaction entries of the given transaction id. @@ -17,10 +29,23 @@ export function getEntriesByTransactionId(transactions, id) { return transaction ? transaction.entries : []; } +/** + * + * @param {*} transaction + * @param {*} transactionEntryId + * @returns + */ export function getTransactionEntryById(transaction, transactionEntryId) { return transaction.entries.find((entry) => entry.id === transactionEntryId); } +/** + * + * @param {*} total + * @param {*} allocateType + * @param {*} entries + * @returns + */ export function allocateCostToEntries(total, allocateType, entries) { return R.compose( R.when( @@ -43,12 +68,12 @@ export function allocateCostToEntries(total, allocateType, entries) { export function allocateCostByValue(total, entries) { const totalAmount = sumBy(entries, 'amount'); - const _entries = entries.map((entry) => ({ + const entriesMapped = entries.map((entry) => ({ ...entry, percentageOfValue: entry.amount / totalAmount, })); - return _entries.map((entry) => ({ + return entriesMapped.map((entry) => ({ ...entry, cost: round(entry.percentageOfValue * total, 2), })); @@ -74,6 +99,13 @@ export function allocateCostByQuantity(total, entries) { })); } +/** + * Retrieve the landed cost transaction by the given id. + */ +export function getCostTransactionById(id, transactions) { + return transactions.find((trans) => trans.id === id); +} + /** * Detarmines the transactions selet field when should update. */ @@ -84,7 +116,55 @@ export function transactionsSelectShouldUpdate(newProps, oldProps) { ); } - +/** + * + * @param {*} entries + * @returns + */ export function resetAllocatedCostEntries(entries) { return entries.map((entry) => ({ ...entry, cost: 0 })); -} \ No newline at end of file +} + +/** + * Retrieves allocate landed cost entries table columns. + */ +export const useAllocateLandedCostEntriesTableColumns = () => { + return React.useMemo( + () => [ + { + Header: intl.get('item'), + accessor: 'item.name', + disableSortBy: true, + width: '150', + }, + { + Header: intl.get('quantity'), + accessor: 'quantity', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('rate'), + accessor: 'rate', + disableSortBy: true, + width: '100', + align: 'right', + }, + { + Header: intl.get('amount'), + accessor: 'amount', + disableSortBy: true, + align: 'right', + width: '100', + }, + { + Header: intl.get('cost'), + accessor: 'cost', + width: '150', + Cell: MoneyFieldCell, + disableSortBy: true, + }, + ], + [], + ); +}; diff --git a/src/containers/Dialogs/CreditNoteNumberDialog/CreditNoteNumberDialogContent.js b/src/containers/Dialogs/CreditNoteNumberDialog/CreditNoteNumberDialogContent.js new file mode 100644 index 000000000..ff1d56f16 --- /dev/null +++ b/src/containers/Dialogs/CreditNoteNumberDialog/CreditNoteNumberDialogContent.js @@ -0,0 +1,102 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { useSaveSettings } from 'hooks/query'; + +import { CreditNoteNumberDialogProvider } from './CreditNoteNumberDialogProvider'; +import ReferenceNumberForm from 'containers/JournalNumber/ReferenceNumberForm'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import withSettings from 'containers/Settings/withSettings'; +import withSettingsActions from 'containers/Settings/withSettingsActions'; +import { compose } from 'utils'; +import { + transformFormToSettings, + transformSettingsToForm, +} from 'containers/JournalNumber/utils'; + +/** + * credit note number dialog content + */ +function CreditNoteNumberDialogContent({ + // #ownProps + initialValues, + onConfirm, + + // #withSettings + nextNumber, + numberPrefix, + autoIncrement, + + // #withDialogActions + closeDialog, +}) { + const { mutateAsync: saveSettings } = useSaveSettings(); + const [referenceFormValues, setReferenceFormValues] = React.useState(null); + + // Handle the submit form. + const handleSubmitForm = (values, { setSubmitting }) => { + // Handle the form success. + const handleSuccess = () => { + setSubmitting(false); + closeDialog('credit-number-form'); + onConfirm(values); + }; + // Handle the form errors. + const handleErrors = () => { + setSubmitting(false); + }; + if (values.incrementMode === 'manual-transaction') { + handleSuccess(); + return; + } + // Transformes the form values to settings to save it. + const options = transformFormToSettings(values, 'credit_note'); + + // Save the settings. + saveSettings({ options }).then(handleSuccess).catch(handleErrors); + }; + + // Handle the dialog close. + const handleClose = () => { + closeDialog('credit-number-form'); + }; + // Handle form change. + const handleChange = (values) => { + setReferenceFormValues(values); + }; + + // Description. + const description = + referenceFormValues?.incrementMode === 'auto' + ? intl.get('credit_note.auto_increment.auto') + : intl.get('credit_note.auto_increment.manually'); + + return ( + + + + ); +} + +export default compose( + withDialogActions, + withSettingsActions, + withSettings(({ creditNoteSettings }) => ({ + autoIncrement: creditNoteSettings?.autoIncrement, + nextNumber: creditNoteSettings?.nextNumber, + numberPrefix: creditNoteSettings?.numberPrefix, + })), +)(CreditNoteNumberDialogContent); diff --git a/src/containers/Dialogs/CreditNoteNumberDialog/CreditNoteNumberDialogProvider.js b/src/containers/Dialogs/CreditNoteNumberDialog/CreditNoteNumberDialogProvider.js new file mode 100644 index 000000000..94daf467f --- /dev/null +++ b/src/containers/Dialogs/CreditNoteNumberDialog/CreditNoteNumberDialogProvider.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { DialogContent } from 'components'; +import { useSettingsCreditNotes } from 'hooks/query'; + +const CreditNoteNumberDialogContext = React.createContext(); + +/** + *Credit Note number dialog provider + */ +function CreditNoteNumberDialogProvider({ query, ...props }) { + const { isLoading: isSettingsLoading } = useSettingsCreditNotes(); + + // Provider payload. + const provider = { + isSettingsLoading, + }; + + return ( + + + + ); +} + +const useCreditNoteNumberDialogContext = () => + React.useContext(CreditNoteNumberDialogContext); + +export { CreditNoteNumberDialogProvider, useCreditNoteNumberDialogContext }; diff --git a/src/containers/Dialogs/CreditNoteNumberDialog/index.js b/src/containers/Dialogs/CreditNoteNumberDialog/index.js new file mode 100644 index 000000000..c1786c324 --- /dev/null +++ b/src/containers/Dialogs/CreditNoteNumberDialog/index.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { Dialog, DialogSuspense, FormattedMessage as T } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; +import { compose, saveInvoke } from 'utils'; + +const CreditNoteNumberDialogContent = React.lazy(() => + import('./CreditNoteNumberDialogContent'), +); + +/** + * Credit note number dialog. + */ +function CreditNoteNumberDialog({ + dialogName, + payload: { initialFormValues }, + isOpen, + onConfirm, +}) { + const handleConfirm = (values) => { + saveInvoke(onConfirm, values); + }; + + return ( + } + name={dialogName} + autoFocus={true} + canEscapeKeyClose={true} + isOpen={isOpen} + > + + + + + ); +} +export default compose(withDialogRedux())(CreditNoteNumberDialog); diff --git a/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js b/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js index c66fe492f..a09bc38ed 100644 --- a/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js +++ b/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js @@ -24,7 +24,7 @@ function InviteUserFormContent({ const handleClose = () => { closeDialog(dialogName); }; - console.log(roles, 'XX'); + return (
@@ -49,7 +49,7 @@ function InviteUserFormContent({ {({ form, field: { value }, meta: { error, touched } }) => ( } + label={} labelInfo={} helperText={} className={classNames(CLASSES.FILL, 'form-group--role_name')} diff --git a/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsDialogContent.js b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsDialogContent.js new file mode 100644 index 000000000..86c880e4c --- /dev/null +++ b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsDialogContent.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { LockingTransactionsFormProvider } from './LockingTransactionsFormProvider'; +import LockingTransactionsForm from './LockingTransactionsForm'; + +/** + * Locking transactions dialog content. + */ +export default function LockingTransactionsDialogContent({ + // #ownProps + dialogName, + moduleName, + isEnabled, +}) { + + return ( + + + + ); +} diff --git a/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsForm.js b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsForm.js new file mode 100644 index 000000000..d33226099 --- /dev/null +++ b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsForm.js @@ -0,0 +1,89 @@ +import React from 'react'; +import moment from 'moment'; +import { Intent } from '@blueprintjs/core'; +import { Formik } from 'formik'; +import intl from 'react-intl-universal'; + +import '../../../style/pages/TransactionsLocking/TransactionsLockingDialog.scss'; + +import { AppToaster } from 'components'; +import { CreateLockingTransactionsFormSchema } from './LockingTransactionsForm.schema'; + +import { useLockingTransactionsContext } from './LockingTransactionsFormProvider'; +import LockingTransactionsFormContent from './LockingTransactionsFormContent'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose, transformToForm } from 'utils'; + +const defaultInitialValues = { + module: '', + lock_to_date: moment(new Date()).format('YYYY-MM-DD'), + reason: '', +}; + +/** + * Locking Transactions Form. + */ +function LockingTransactionsForm({ + // #withDialogActions + closeDialog, +}) { + const { + dialogName, + moduleName, + transactionLocking, + isEnabled, + createLockingTransactionMutate, + } = useLockingTransactionsContext(); + + // Initial form values. + const initialValues = React.useMemo( + () => ({ + ...(isEnabled + ? { + ...transformToForm(transactionLocking, defaultInitialValues), + module: moduleName, + } + : { + ...defaultInitialValues, + module: moduleName, + }), + }), + [isEnabled], + ); + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + setSubmitting(true); + + // Handle request response success. + const onSuccess = (response) => { + AppToaster.show({ + message: intl.get('locking_transactions.dialog.success_message'), + intent: Intent.SUCCESS, + }); + closeDialog(dialogName); + }; + + // Handle request response errors. + const onError = ({ + response: { + data: { errors }, + }, + }) => { + setSubmitting(false); + }; + + createLockingTransactionMutate(values).then(onSuccess).catch(onError); + }; + + return ( + + ); +} +export default compose(withDialogActions)(LockingTransactionsForm); diff --git a/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsForm.schema.js b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsForm.schema.js new file mode 100644 index 000000000..d2f8199f9 --- /dev/null +++ b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsForm.schema.js @@ -0,0 +1,14 @@ +import * as Yup from 'yup'; +import intl from 'react-intl-universal'; +import { DATATYPES_LENGTH } from 'common/dataTypes'; + +const Schema = Yup.object().shape({ + lock_to_date: Yup.date().required().label(intl.get('date')), + module: Yup.string().required(), + reason: Yup.string() + .required() + .min(3) + .max(DATATYPES_LENGTH.TEXT) + .label(intl.get('reason')), +}); +export const CreateLockingTransactionsFormSchema = Schema; diff --git a/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormContent.js b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormContent.js new file mode 100644 index 000000000..c7e46a26c --- /dev/null +++ b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormContent.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { Form } from 'formik'; + +import LockingTransactionsFormFields from './LockingTransactionsFormFields'; +import LockingTransactionsFormFloatingActions from './LockingTransactionsFormFloatingActions'; + +/** + * locking Transactions form content. + */ +export default function LockingTransactionsFormContent() { + return ( + + + + + ); +} diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormFields.js b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormFields.js similarity index 75% rename from src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormFields.js rename to src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormFields.js index bede9d2fc..1bcac265a 100644 --- a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormFields.js +++ b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormFields.js @@ -14,28 +14,28 @@ import { } from 'utils'; /** - * Transactions locking form fields. + * locking Transactions form fields. */ -export default function TransactionsLockingFormFields() { - const dateFieldRef = useAutofocus(); +export default function LockingTransactionsFormFields() { + const reasonFieldRef = useAutofocus(); return (
- {/*------------ Date -----------*/} - + {/*------------ Locking Date -----------*/} + {({ form, field: { value }, meta: { error, touched } }) => ( } + label={} labelInfo={} intent={inputIntent({ error, touched })} - helperText={} + helperText={} minimal={true} className={classNames(CLASSES.FILL, 'form-group--date')} > { - form.setFieldValue('date', formattedDate); + form.setFieldValue('lock_to_date', formattedDate); })} value={tansformDateValue(value)} popoverProps={{ @@ -43,16 +43,16 @@ export default function TransactionsLockingFormFields() { minimal: true, }} intent={inputIntent({ error, touched })} - inputRef={(ref) => (dateFieldRef.current = ref)} /> )} - {/*------------ reasons -----------*/} + + {/*------------ Locking Reason -----------*/} {({ field, meta: { error, touched } }) => ( } + label={} labelInfo={} className={'form-group--reason'} intent={inputIntent({ error, touched })} @@ -62,6 +62,7 @@ export default function TransactionsLockingFormFields() { growVertically={true} large={true} intent={inputIntent({ error, touched })} + inputRef={(ref) => (reasonFieldRef.current = ref)} {...field} /> diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFloatingActions.js b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormFloatingActions.js similarity index 62% rename from src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFloatingActions.js rename to src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormFloatingActions.js index 51c29dedc..b76ee054e 100644 --- a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFloatingActions.js +++ b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormFloatingActions.js @@ -3,21 +3,21 @@ import { Intent, Button, Classes } from '@blueprintjs/core'; import { useFormikContext } from 'formik'; import { FormattedMessage as T } from 'components'; -import { useTransactionLockingContext } from './TransactionsLockingFormProvider'; +import { useLockingTransactionsContext } from './LockingTransactionsFormProvider'; import withDialogActions from 'containers/Dialog/withDialogActions'; import { compose } from 'utils'; /** - * Transactions locking floating actions. + * locking Transactions floating actions. */ -function TransactionsLockingFloatingActions({ +function LockingTransactionsFormFloatingActions({ // #withDialogActions closeDialog, }) { // Formik context. const { isSubmitting } = useFormikContext(); - const { dialogName } = useTransactionLockingContext(); + const { dialogName } = useLockingTransactionsContext(); // Handle cancel button click. const handleCancelBtnClick = (event) => { @@ -27,24 +27,22 @@ function TransactionsLockingFloatingActions({ return (
- +
); } -export default compose(withDialogActions)(TransactionsLockingFloatingActions); +export default compose(withDialogActions)( + LockingTransactionsFormFloatingActions, +); diff --git a/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormProvider.js b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormProvider.js new file mode 100644 index 000000000..4c70e4d50 --- /dev/null +++ b/src/containers/Dialogs/LockingTransactionsDialog/LockingTransactionsFormProvider.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { DialogContent } from 'components'; +import { + useCreateLockingTransactoin, + useEditTransactionsLocking, +} from 'hooks/query'; + +const LockingTransactionsContext = React.createContext(); + +/** + * Locking transactions form provider. + */ +function LockingTransactionsFormProvider({ + moduleName, + isEnabled, + dialogName, + ...props +}) { + // Create locking transactions mutations. + const { mutateAsync: createLockingTransactionMutate } = + useCreateLockingTransactoin(); + + const { data: transactionLocking, isLoading: isTransactionsLockingLoading } = + useEditTransactionsLocking(moduleName, { + enabled: !!isEnabled, + }); + + // State provider. + const provider = { + dialogName, + moduleName, + createLockingTransactionMutate, + transactionLocking, + isEnabled, + }; + return ( + + + + ); +} + +const useLockingTransactionsContext = () => + React.useContext(LockingTransactionsContext); + +export { LockingTransactionsFormProvider, useLockingTransactionsContext }; diff --git a/src/containers/Dialogs/LockingTransactionsDialog/index.js b/src/containers/Dialogs/LockingTransactionsDialog/index.js new file mode 100644 index 000000000..d4b360653 --- /dev/null +++ b/src/containers/Dialogs/LockingTransactionsDialog/index.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { Dialog, DialogSuspense, FormattedMessage as T } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; +import { compose } from 'utils'; + +const LockingTransactionsDialogContent = React.lazy(() => + import('./LockingTransactionsDialogContent'), +); + +/** + * Locking Transactions dialog + */ +function LockingTransactionsDialog({ + dialogName, + payload: { module, isEnabled }, + isOpen, +}) { + return ( + } + canEscapeKeyClose={true} + isOpen={isOpen} + className={'dialog--transaction--locking'} + > + + + + + ); +} + +export default compose(withDialogRedux())(LockingTransactionsDialog); diff --git a/src/containers/Dialogs/MoneyInDialog/MoneyInForm.js b/src/containers/Dialogs/MoneyInDialog/MoneyInForm.js index 39dae9941..bb5676293 100644 --- a/src/containers/Dialogs/MoneyInDialog/MoneyInForm.js +++ b/src/containers/Dialogs/MoneyInDialog/MoneyInForm.js @@ -86,7 +86,7 @@ function MoneyInForm({ }); }) .finally(() => { - setSubmitting(true); + setSubmitting(false); }); }; diff --git a/src/containers/Dialogs/MoneyOutDialog/MoneyOutForm.js b/src/containers/Dialogs/MoneyOutDialog/MoneyOutForm.js index 8b7f537c7..7d6436aaa 100644 --- a/src/containers/Dialogs/MoneyOutDialog/MoneyOutForm.js +++ b/src/containers/Dialogs/MoneyOutDialog/MoneyOutForm.js @@ -86,7 +86,7 @@ function MoneyOutForm({ }); }) .finally(() => { - setSubmitting(true); + setSubmitting(false); }); }; return ( diff --git a/src/containers/Dialogs/QuickPaymentReceiveFormDialog/QuickPaymentReceiveForm.js b/src/containers/Dialogs/QuickPaymentReceiveFormDialog/QuickPaymentReceiveForm.js index 1b2a68b49..5b5e7637a 100644 --- a/src/containers/Dialogs/QuickPaymentReceiveFormDialog/QuickPaymentReceiveForm.js +++ b/src/containers/Dialogs/QuickPaymentReceiveFormDialog/QuickPaymentReceiveForm.js @@ -106,6 +106,6 @@ export default compose( paymentReceiveNextNumber: paymentReceiveSettings?.nextNumber, paymentReceiveNumberPrefix: paymentReceiveSettings?.numberPrefix, paymentReceiveAutoIncrement: paymentReceiveSettings?.autoIncrement, - preferredDepositAccount: paymentReceiveSettings?.depositAccount, + preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount, })), )(QuickPaymentReceiveForm); diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteDialogContent.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteDialogContent.js new file mode 100644 index 000000000..62b18d718 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteDialogContent.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { ReconcileCreditNoteFormProvider } from './ReconcileCreditNoteFormProvider'; +import ReconcileCreditNoteForm from './ReconcileCreditNoteForm'; + +/** + * Reconcile credit note dialog content. + */ +export default function ReconcileCreditNoteDialogContent({ + // #ownProps + dialogName, + creditNoteId, +}) { + return ( + + + + ); +} diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteEntriesTable.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteEntriesTable.js new file mode 100644 index 000000000..b9ae7cb86 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteEntriesTable.js @@ -0,0 +1,80 @@ +import React from 'react'; +import styled from 'styled-components'; +import { defaultTo } from 'lodash'; + +import { DataTableEditable } from 'components'; +import { compose, updateTableCell } from 'utils'; +import { useDeepCompareEffect } from 'hooks/utils'; +import { + useReconcileCreditNoteTableColumns, + maxAmountCreditFromRemaining, + maxCreditNoteAmountEntries, +} from './utils'; +import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider'; + +/** + * Reconcile credit note entries table. + */ +export default function ReconcileCreditNoteEntriesTable({ + onUpdateData, + entries, + errors, +}) { + // Retrieve the reconcile credit note table columns. + const columns = useReconcileCreditNoteTableColumns(); + + // Reconcile credit note context provider. + const { + creditNote: { credits_remaining }, + } = useReconcileCreditNoteContext(); + + // Handle update data. + const handleUpdateData = React.useCallback( + (rowIndex, columnId, value) => { + const newRows = compose(updateTableCell(rowIndex, columnId, value))( + entries, + ); + onUpdateData(newRows); + }, + [onUpdateData, entries], + ); + // Deep compare entries to modify new entries. + useDeepCompareEffect(() => { + const newRows = compose( + maxCreditNoteAmountEntries(defaultTo(credits_remaining, 0)), + maxAmountCreditFromRemaining, + )(entries); + + onUpdateData(newRows); + }, [entries]); + + return ( + + ); +} + +export const ReconcileCreditNoteEditableTable = styled(DataTableEditable)` + .table { + max-height: 400px; + overflow: auto; + + .thead .tr .th { + padding-top: 8px; + padding-bottom: 8px; + } + + .tbody { + .tr .td { + padding: 2px 4px; + min-height: 38px; + } + } + } +`; diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.js new file mode 100644 index 000000000..5368cf3e5 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { Intent } from '@blueprintjs/core'; +import intl from 'react-intl-universal'; + +import '../../../style/pages/ReconcileCreditNote/ReconcileCreditNoteForm.scss'; +import { AppToaster } from 'components'; +import { CreateReconcileCreditNoteFormSchema } from './ReconcileCreditNoteForm.schema'; +import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider'; +import ReconcileCreditNoteFormContent from './ReconcileCreditNoteFormContent'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose, transformToForm } from 'utils'; +import { transformErrors } from './utils'; + +// Default form initial values. +const defaultInitialValues = { + entries: [ + { + invoice_id: '', + amount: '', + }, + ], +}; + +/** + * Reconcile credit note form. + */ +function ReconcileCreditNoteForm({ + // #withDialogActions + closeDialog, +}) { + const { + dialogName, + creditNoteId, + reconcileCreditNotes, + createReconcileCreditNoteMutate, + } = useReconcileCreditNoteContext(); + + // Initial form values. + const initialValues = { + entries: reconcileCreditNotes.map((entry) => ({ + ...entry, + invoice_id: entry.id, + amount: '', + })), + }; + + // Handle form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + setSubmitting(true); + + // Filters the entries. + const entries = values.entries + .filter((entry) => entry.invoice_id && entry.amount) + .map((entry) => transformToForm(entry, defaultInitialValues.entries[0])); + + const form = { + ...values, + entries: entries, + }; + // Handle the request success. + const onSuccess = (response) => { + AppToaster.show({ + message: intl.get('reconcile_credit_note.success_message'), + intent: Intent.SUCCESS, + }); + setSubmitting(false); + closeDialog(dialogName); + }; + // Handle the request error. + const onError = ({ + response: { + data: { errors }, + }, + }) => { + if (errors) { + transformErrors(errors, { setErrors }); + } + setSubmitting(false); + }; + + createReconcileCreditNoteMutate([creditNoteId, form]) + .then(onSuccess) + .catch(onError); + }; + + return ( + + ); +} + +export default compose(withDialogActions)(ReconcileCreditNoteForm); diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.schema.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.schema.js new file mode 100644 index 000000000..a6cd9dc09 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.schema.js @@ -0,0 +1,12 @@ +import * as Yup from 'yup'; + +const Schema = Yup.object().shape({ + entries: Yup.array().of( + Yup.object().shape({ + invoice_id: Yup.number().required(), + amount: Yup.number().nullable(), + }), + ), +}); + +export const CreateReconcileCreditNoteFormSchema = Schema; diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormContent.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormContent.js new file mode 100644 index 000000000..1c1e12d21 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormContent.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { Form } from 'formik'; +import { Choose } from 'components'; + +import ReconcileCreditNoteFormFields from './ReconcileCreditNoteFormFields'; +import ReconcileCreditNoteFormFloatingActions from './ReconcileCreditNoteFormFloatingActions'; +import { EmptyStatuCallout } from './utils'; +import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider'; + +/** + * Reconcile credit note form content. + */ +export default function ReconcileCreditNoteFormContent() { + const { isEmptyStatus } = useReconcileCreditNoteContext(); + return ( + + + + + +
+ + + +
+
+ ); +} diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFields.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFields.js new file mode 100644 index 000000000..8820c75dd --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFields.js @@ -0,0 +1,115 @@ +import React from 'react'; +import { FastField, useFormikContext } from 'formik'; +import { Classes } from '@blueprintjs/core'; +import styled from 'styled-components'; + +import { + T, + TotalLines, + TotalLine, + TotalLineBorderStyle, + TotalLineTextStyle, +} from 'components'; +import { subtract } from 'lodash'; +import { getEntriesTotal } from 'containers/Entries/utils'; +import ReconcileCreditNoteEntriesTable from './ReconcileCreditNoteEntriesTable'; +import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider'; +import { formattedAmount } from 'utils'; + +/** + * Reconcile credit note form fields. + */ +export default function ReconcileCreditNoteFormFields() { + const { + creditNote: { formatted_credits_remaining }, + } = useReconcileCreditNoteContext(); + + return ( +
+ + + + + {formatted_credits_remaining} + + + + {/*------------ Reconcile credit entries table -----------*/} + + {({ form: { setFieldValue }, field: { value }, meta: { error } }) => ( + { + setFieldValue('entries', newEntries); + }} + /> + )} + + + +
+ ); +} + +/** + * Reconcile credit note total lines. + * @returns {React.JSX} + */ +function ReconcileCreditNoteTotalLines() { + // Reconcile credit note context. + const { + creditNote: { credits_remaining, currency_code }, + } = useReconcileCreditNoteContext(); + + // Formik form context. + const { values } = useFormikContext(); + + // Calculate the total amount of credit entries. + const totalAmount = React.useMemo( + () => getEntriesTotal(values.entries), + [values.entries], + ); + // Calculate the total amount of credit remaining. + const creditsRemaining = subtract(credits_remaining, totalAmount); + + return ( + + + + } + value={formattedAmount(totalAmount, currency_code)} + borderStyle={TotalLineBorderStyle.SingleDark} + /> + } + value={formattedAmount(creditsRemaining, currency_code)} + borderStyle={TotalLineBorderStyle.SingleDark} + textStyle={TotalLineTextStyle.Bold} + /> + + + ); +} + +export const CreditRemainingRoot = styled.div` + display: flex; + justify-content: flex-end; + padding-bottom: 15px; +`; + +export const CreditRemainingBalance = styled.span` + font-weight: 600; + color: #343463; + margin-left: 5px; +`; + +export const ReconcileCreditNoteTotalLinesRoot = styled.div` + margin-top: 15px; +`; + +export const ReconcileTotalLines = styled(TotalLines)` + margin-left: auto; +`; diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFloatingActions.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFloatingActions.js new file mode 100644 index 000000000..4d3c4f203 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFloatingActions.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; +import { Intent, Button, Classes } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'components'; + +import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +/** + * Reconcile credit note floating actions. + */ +function ReconcileCreditNoteFormFloatingActions({ + // #withDialogActions + closeDialog, +}) { + // Formik context. + const { isSubmitting } = useFormikContext(); + + const { dialogName } = useReconcileCreditNoteContext(); + + // Handle cancel button click. + const handleCancelBtnClick = (event) => { + closeDialog(dialogName); + }; + + return ( +
+
+ + +
+
+ ); +} +export default compose(withDialogActions)( + ReconcileCreditNoteFormFloatingActions, +); diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormProvider.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormProvider.js new file mode 100644 index 000000000..92cc90bd1 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormProvider.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { DialogContent } from 'components'; +import { + useCreditNote, + useReconcileCreditNote, + useCreateReconcileCreditNote, +} from 'hooks/query'; +import { isEmpty } from 'lodash'; + +const ReconcileCreditNoteDialogContext = React.createContext(); + +/** + * Reconcile credit note provider. + */ +function ReconcileCreditNoteFormProvider({ + creditNoteId, + dialogName, + ...props +}) { + // Handle fetch reconcile credit note details. + const { isLoading: isReconcileCreditLoading, data: reconcileCreditNotes } = + useReconcileCreditNote(creditNoteId, { + enabled: !!creditNoteId, + }); + + // Handle fetch vendor credit details. + const { data: creditNote, isLoading: isCreditNoteLoading } = useCreditNote( + creditNoteId, + { + enabled: !!creditNoteId, + }, + ); + + // Create reconcile credit note mutations. + const { mutateAsync: createReconcileCreditNoteMutate } = + useCreateReconcileCreditNote(); + + // Detarmines the datatable empty status. + const isEmptyStatus = isEmpty(reconcileCreditNotes); + + // provider payload. + const provider = { + dialogName, + reconcileCreditNotes, + createReconcileCreditNoteMutate, + isEmptyStatus, + creditNote, + creditNoteId, + }; + + return ( + + + + ); +} + +const useReconcileCreditNoteContext = () => + React.useContext(ReconcileCreditNoteDialogContext); + +export { ReconcileCreditNoteFormProvider, useReconcileCreditNoteContext }; diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/index.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/index.js new file mode 100644 index 000000000..217d93b32 --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/index.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { FormattedMessage as T, Dialog, DialogSuspense } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; +import { compose } from 'utils'; + +const ReconcileCreditNoteDialogContent = React.lazy(() => + import('./ReconcileCreditNoteDialogContent'), +); + +/** + * Reconcile credit note dialog. + */ +function ReconcileCreditNoteDialog({ + dialogName, + payload: { creditNoteId }, + isOpen, +}) { + return ( + } + canEscapeKeyClose={true} + isOpen={isOpen} + className="dialog--reconcile-credit-form" + > + + + + + ); +} + +export default compose(withDialogRedux())(ReconcileCreditNoteDialog); diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/utils.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/utils.js new file mode 100644 index 000000000..100322f1b --- /dev/null +++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/utils.js @@ -0,0 +1,119 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Callout, Intent, Classes } from '@blueprintjs/core'; +import * as R from 'ramda'; +import clsx from 'classnames'; + +import { CLASSES } from 'common/classes'; +import { MoneyFieldCell, FormatDateCell, AppToaster, T } from 'components'; + +export const transformErrors = (errors, { setErrors }) => { + if (errors.some((e) => e.type === 'INVOICES_HAS_NO_REMAINING_AMOUNT')) { + AppToaster.show({ + message: 'The amount credit from the given invoice has no remaining amount.', + intent: Intent.DANGER, + }); + } + if ( + errors.find((error) => error.type === 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT') + ) { + AppToaster.show({ + message: 'The total amount bigger than from remaining credit note amount', + intent: Intent.DANGER, + }); + } +}; + +/** + * Empty status callout. + * @returns {React.JSX} + */ +export function EmptyStatuCallout() { + return ( +
+ +

+ +

+
+
+ ); +} + +/** + * Retrieves reconcile credit note table columns. + * @returns + */ +export const useReconcileCreditNoteTableColumns = () => { + return React.useMemo( + () => [ + { + Header: intl.get('invoice_date'), + accessor: 'formatted_invoice_date', + Cell: FormatDateCell, + disableSortBy: true, + width: '120', + }, + { + Header: intl.get('invoice_no'), + accessor: 'invoice_no', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('amount'), + accessor: 'formatted_amount', + disableSortBy: true, + align: 'right', + width: '100', + }, + { + Header: intl.get('reconcile_credit_note.column.remaining_amount'), + accessor: 'formatted_due_amount', + disableSortBy: true, + align: 'right', + width: '150', + className: clsx(CLASSES.FONT_BOLD), + }, + { + Header: intl.get('reconcile_credit_note.column.amount_to_credit'), + accessor: 'amount', + Cell: MoneyFieldCell, + disableSortBy: true, + width: '150', + }, + ], + [], + ); +}; + +/** + * Sets max credit amount from sale invoicue balance. + */ +export const maxAmountCreditFromRemaining = (entries) => { + return entries.map((entry) => ({ + ...entry, + amount: entry.amount ? Math.min(entry.balance, entry.amount) : '', + })); +}; + +/** + * Adjusts entries amount based on the given total. + */ +export const maxCreditNoteAmountEntries = R.curry((total, entries) => { + let balance = total; + + return entries.map((entry) => { + const oldBalance = balance; + balance -= entry.amount ? entry.amount : 0; + + return { + ...entry, + amount: entry.amount + ? Math.max(Math.min(entry.amount, oldBalance), 0) + : '', + }; + }); +}); diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditDialogContent.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditDialogContent.js new file mode 100644 index 000000000..651b33756 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditDialogContent.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { ReconcileVendorCreditFormProvider } from './ReconcileVendorCreditFormProvider'; +import ReconcileVendorCreditForm from './ReconcileVendorCreditForm'; + +export default function ReconcileVendorCreditDialogContent({ + // #ownProps + dialogName, + vendorCreditId, +}) { + return ( + + + + ); +} diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditEntriesTable.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditEntriesTable.js new file mode 100644 index 000000000..0155374ff --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditEntriesTable.js @@ -0,0 +1,83 @@ +import React from 'react'; +import styled from 'styled-components'; +import * as R from 'ramda'; +import { defaultTo } from 'lodash'; + +import { useDeepCompareEffect } from 'hooks/utils'; + +import { DataTableEditable } from 'components'; +import { compose, updateTableCell } from 'utils'; +import { + useReconcileVendorCreditTableColumns, + maxAmountCreditFromRemaining, +} from './utils'; +import { maxCreditNoteAmountEntries } from '../ReconcileCreditNoteDialog/utils'; +import { useReconcileVendorCreditContext } from './ReconcileVendorCreditFormProvider'; + +/** + * Reconcile vendor credit entries table. + */ +export default function ReconcileVendorCreditEntriesTable({ + onUpdateData, + entries, + errors, +}) { + // Reconcile vendor credit table columns. + const columns = useReconcileVendorCreditTableColumns(); + + // Reconcile vendor credit context. + const { + vendorCredit: { credits_remaining }, + } = useReconcileVendorCreditContext(); + + // Handle update data. + const handleUpdateData = React.useCallback( + (rowIndex, columnId, value) => { + const newRows = compose(updateTableCell(rowIndex, columnId, value))( + entries, + ); + onUpdateData(newRows); + }, + [onUpdateData, entries], + ); + + // Watches deeply entries to compose a new entries. + useDeepCompareEffect(() => { + const newEntries = R.compose( + maxCreditNoteAmountEntries(defaultTo(credits_remaining, 0)), + maxAmountCreditFromRemaining, + )(entries); + + onUpdateData(newEntries); + }, [entries]); + + return ( + + ); +} + +export const ReconcileVendorCreditEditableTable = styled(DataTableEditable)` + .table { + max-height: 400px; + overflow: auto; + + .thead .tr .th { + padding-top: 8px; + padding-bottom: 8px; + } + + .tbody { + .tr .td { + padding: 2px 4px; + min-height: 38px; + } + } + } +`; diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFloatingActions.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFloatingActions.js new file mode 100644 index 000000000..82289491c --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFloatingActions.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; +import { Intent, Button, Classes } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'components'; + +import { useReconcileVendorCreditContext } from './ReconcileVendorCreditFormProvider'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +function ReconcileVendorCreditFloatingActions({ + // #withDialogActions + closeDialog, +}) { + // Formik context. + const { isSubmitting } = useFormikContext(); + + const { dialogName } = useReconcileVendorCreditContext(); + + // Handle cancel button click. + const handleCancelBtnClick = (event) => { + closeDialog(dialogName); + }; + + return ( +
+
+ + +
+
+ ); +} +export default compose(withDialogActions)(ReconcileVendorCreditFloatingActions); diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditForm.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditForm.js new file mode 100644 index 000000000..b45cad8f3 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditForm.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { Intent } from '@blueprintjs/core'; +import intl from 'react-intl-universal'; + +import '../../../style/pages/ReconcileVendorCredit/ReconcileVendorCreditForm.scss'; + +import { AppToaster } from 'components'; +import { CreateReconcileVendorCreditFormSchema } from './ReconcileVendorCreditForm.schema'; +import { useReconcileVendorCreditContext } from './ReconcileVendorCreditFormProvider'; +import ReconcileVendorCreditFormContent from './ReconcileVendorCreditFormContent'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose, transformToForm } from 'utils'; + +// Default form initial values. +const defaultInitialValues = { + entries: [ + { + bill_id: '', + amount: '', + }, + ], +}; + +/** + * Reconcile vendor credit form. + */ +function ReconcileVendorCreditForm({ + // #withDialogActions + closeDialog, +}) { + const { + dialogName, + reconcileVendorCredits, + createReconcileVendorCreditMutate, + vendorCredit, + } = useReconcileVendorCreditContext(); + + // Initial form values. + const initialValues = { + entries: reconcileVendorCredits.map((entry) => ({ + ...entry, + bill_id: entry.id, + amount: '', + })), + }; + + // Handle form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + setSubmitting(true); + // Filters the entries. + const entries = values.entries + .filter((entry) => entry.bill_id && entry.amount) + .map((entry) => transformToForm(entry, defaultInitialValues.entries[0])); + + const form = { + ...values, + entries: entries, + }; + + // Handle the request success. + const onSuccess = (response) => { + AppToaster.show({ + message: intl.get('reconcile_vendor_credit.dialog.success_message'), + intent: Intent.SUCCESS, + }); + setSubmitting(false); + closeDialog(dialogName); + }; + + // Handle the request error. + const onError = ({ + response: { + data: { errors }, + }, + }) => { + // if (errors) { + // transformErrors(errors, { setErrors }); + // } + setSubmitting(false); + }; + + createReconcileVendorCreditMutate([vendorCredit.id, form]) + .then(onSuccess) + .catch(onError); + }; + + return ( + + ); +} +export default compose(withDialogActions)(ReconcileVendorCreditForm); diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditForm.schema.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditForm.schema.js new file mode 100644 index 000000000..d15b472d9 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditForm.schema.js @@ -0,0 +1,12 @@ +import * as Yup from 'yup'; + +const Schema = Yup.object().shape({ + entries: Yup.array().of( + Yup.object().shape({ + bill_id: Yup.number().required(), + amount: Yup.number().nullable(), + }), + ), +}); + +export const CreateReconcileVendorCreditFormSchema = Schema; diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormContent.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormContent.js new file mode 100644 index 000000000..c88b1de20 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormContent.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { Form } from 'formik'; +import { Choose } from 'components'; + +import { EmptyStatuCallout } from './utils'; +import ReconcileVendorCreditFormFields from './ReconcileVendorCreditFormFields'; +import ReconcileVendorCreditFloatingActions from './ReconcileVendorCreditFloatingActions'; +import { useReconcileVendorCreditContext } from './ReconcileVendorCreditFormProvider'; + +export default function ReconcileVendorCreditFormContent() { + const { isEmptyStatus } = useReconcileVendorCreditContext(); + + return ( + + + + + +
+ + + +
+
+ ); +} diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormFields.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormFields.js new file mode 100644 index 000000000..16c5d48f8 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormFields.js @@ -0,0 +1,113 @@ +import React from 'react'; +import { FastField, useFormikContext } from 'formik'; +import { Classes } from '@blueprintjs/core'; +import { subtract } from 'lodash'; +import styled from 'styled-components'; + +import { getEntriesTotal } from 'containers/Entries/utils'; +import { + T, + TotalLines, + TotalLine, + TotalLineBorderStyle, + TotalLineTextStyle, +} from 'components'; +import ReconcileVendorCreditEntriesTable from './ReconcileVendorCreditEntriesTable'; +import { useReconcileVendorCreditContext } from './ReconcileVendorCreditFormProvider'; +import { formattedAmount } from 'utils'; + +export default function ReconcileVendorCreditFormFields() { + const { + vendorCredit: { formatted_credits_remaining }, + } = useReconcileVendorCreditContext(); + + return ( +
+ + + + + {formatted_credits_remaining} + + + + + {({ + form: { setFieldValue, values }, + field: { value }, + meta: { error, touched }, + }) => ( + { + setFieldValue('entries', newEntries); + }} + /> + )} + + + +
+ ); +} + +/** + * Reconcile vendor credit total lines. + * @returns {React.JSX} + */ +function ReconcileVendorCreditTotalLines() { + const { + vendorCredit: { currency_code, credits_remaining }, + } = useReconcileVendorCreditContext(); + + const { values } = useFormikContext(); + + // Calculate the total amount of credit entries. + const totalAmount = React.useMemo( + () => getEntriesTotal(values.entries), + [values.entries], + ); + + // Calculate the total amount of credit remaining. + const creditsRemaining = subtract(credits_remaining, totalAmount); + + return ( + + + + } + value={formattedAmount(totalAmount, currency_code)} + borderStyle={TotalLineBorderStyle.SingleDark} + /> + } + value={formattedAmount(creditsRemaining, currency_code)} + borderStyle={TotalLineBorderStyle.SingleDark} + textStyle={TotalLineTextStyle.Bold} + /> + + + ); +} + +const CreditRemainingRoot = styled.div` + display: flex; + justify-content: flex-end; + padding-bottom: 15px; +`; + +const CreditRemainingBalance = styled.span` + font-weight: 600; + color: #343463; + margin-left: 5px; +`; + +export const ReconcileVendorCreditTotalLinesRoot = styled.div` + margin-top: 15px; +`; +export const ReconcileTotalLines = styled(TotalLines)` + margin-left: auto; +`; diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormProvider.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormProvider.js new file mode 100644 index 000000000..0b85497f7 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/ReconcileVendorCreditFormProvider.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { DialogContent } from 'components'; +import { + useVendorCredit, + useReconcileVendorCredit, + useCreateReconcileVendorCredit, +} from 'hooks/query'; +import { isEmpty } from 'lodash'; + +const ReconcileVendorCreditFormContext = React.createContext(); + +/** + * Reconcile vendor credit provider. + */ +function ReconcileVendorCreditFormProvider({ + vendorCreditId, + dialogName, + ...props +}) { + + // Handle fetch reconcile + const { + isLoading: isReconcileVendorCreditLoading, + data: reconcileVendorCredits, + } = useReconcileVendorCredit(vendorCreditId, { + enabled: !!vendorCreditId, + }); + + // Handle fetch vendor credit details. + const { data: vendorCredit, isLoading: isVendorCreditLoading } = + useVendorCredit(vendorCreditId, { + enabled: !!vendorCreditId, + }); + + // Create reconcile vendor credit mutations. + const { mutateAsync: createReconcileVendorCreditMutate } = + useCreateReconcileVendorCredit(); + + // Detarmines the datatable empty status. + const isEmptyStatus = isEmpty(reconcileVendorCredits); + + // provider. + const provider = { + dialogName, + reconcileVendorCredits, + createReconcileVendorCreditMutate, + isEmptyStatus, + vendorCredit, + }; + + return ( + + + + ); +} + +const useReconcileVendorCreditContext = () => + React.useContext(ReconcileVendorCreditFormContext); + +export { ReconcileVendorCreditFormProvider, useReconcileVendorCreditContext }; diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/index.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/index.js new file mode 100644 index 000000000..8dc713917 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/index.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { FormattedMessage as T, Dialog, DialogSuspense } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; +import { compose } from 'utils'; + +const ReconcileVendorCreditDialogContent = React.lazy(() => + import('./ReconcileVendorCreditDialogContent'), +); + +/** + * Reconcile vendor credit dialog. + */ +function ReconcileVendorCreditDialog({ + dialogName, + payload: { vendorCreditId }, + isOpen, +}) { + return ( + } + canEscapeKeyClose={true} + isOpen={isOpen} + className="dialog--reconcile-vendor-credit-form" + > + + + + + ); +} + +export default compose(withDialogRedux())(ReconcileVendorCreditDialog); diff --git a/src/containers/Dialogs/ReconcileVendorCreditDialog/utils.js b/src/containers/Dialogs/ReconcileVendorCreditDialog/utils.js new file mode 100644 index 000000000..73e4221e1 --- /dev/null +++ b/src/containers/Dialogs/ReconcileVendorCreditDialog/utils.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { Callout, Intent, Classes } from '@blueprintjs/core'; +import intl from 'react-intl-universal'; +import clsx from 'classnames'; + +import { CLASSES } from 'common/classes'; +import { T } from 'components'; +import { MoneyFieldCell, FormatDateCell } from 'components'; + +export const transformErrors = (errors, { setErrors }) => {}; + +export function EmptyStatuCallout() { + return ( +
+ +

+ +

+
+
+ ); +} + +/** + * Reconcile vendor credit table columns. + */ +export const useReconcileVendorCreditTableColumns = () => { + return React.useMemo( + () => [ + { + Header: intl.get('bill_date'), + accessor: 'formatted_bill_date', + Cell: FormatDateCell, + disableSortBy: true, + width: '120', + }, + { + Header: intl.get('reconcile_vendor_credit.column.bill_number'), + accessor: 'bill_number', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('amount'), + accessor: 'formatted_amount', + disableSortBy: true, + align: 'right', + width: '100', + }, + { + Header: intl.get('reconcile_vendor_credit.column.remaining_amount'), + accessor: 'formatted_due_amount', + disableSortBy: true, + align: 'right', + width: '150', + className: clsx(CLASSES.FONT_BOLD), + }, + { + Header: intl.get('reconcile_vendor_credit.column.amount_to_credit'), + accessor: 'amount', + Cell: MoneyFieldCell, + disableSortBy: true, + width: '150', + }, + ], + [], + ); +}; + +/** + * Sets max amount credit from purchase due amount. + */ +export const maxAmountCreditFromRemaining = (entries) => { + return entries.map((entry) => ({ + ...entry, + amount: entry.amount ? Math.min(entry.due_amount, entry.amount) : '', + })); +}; diff --git a/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteDialogContent.js b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteDialogContent.js new file mode 100644 index 000000000..4cc99ae85 --- /dev/null +++ b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteDialogContent.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import 'style/pages/RefundCreditNote/RefundCreditNote.scss'; +import { RefundCreditNoteFormProvider } from './RefundCreditNoteFormProvider'; +import RefundCreditNoteForm from './RefundCreditNoteForm'; + +/** + * Refund credit note dialog content. + */ +export default function RefundCreditNoteDialogContent({ + // #ownProps + dialogName, + creditNoteId, +}) { + return ( + + + + ); +} diff --git a/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFloatingActions.js b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFloatingActions.js new file mode 100644 index 000000000..07e01d359 --- /dev/null +++ b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFloatingActions.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { Intent, Button, Classes } from '@blueprintjs/core'; +import { useFormikContext } from 'formik'; +import { FormattedMessage as T } from 'components'; + +import { useRefundCreditNoteContext } from './RefundCreditNoteFormProvider'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +/** + * Refund credit note floating actions. + */ +function RefundCreditNoteFloatingActions({ + // #withDialogActions + closeDialog, +}) { + // Formik context. + const { isSubmitting } = useFormikContext(); + + // refund credit note dialog context. + const { dialogName } = useRefundCreditNoteContext(); + + // Handle close button click. + const handleCancelBtnClick = () => { + closeDialog(dialogName); + }; + + return ( +
+
+ +
+
+ ); +} +export default compose(withDialogActions)(RefundCreditNoteFloatingActions); diff --git a/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteForm.js b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteForm.js new file mode 100644 index 000000000..4250a14b1 --- /dev/null +++ b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteForm.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { Intent } from '@blueprintjs/core'; +import intl from 'react-intl-universal'; +import moment from 'moment'; +import { omit } from 'lodash'; + +import { AppToaster } from 'components'; +import { useRefundCreditNoteContext } from './RefundCreditNoteFormProvider'; +import { CreateRefundCreditNoteFormSchema } from './RefundCreditNoteForm.schema'; +import RefundCreditNoteFormContent from './RefundCreditNoteFormContent'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +const defaultInitialValues = { + from_account_id: '', + date: moment(new Date()).format('YYYY-MM-DD'), + reference_no: '', + description: '', + amount: '', +}; + +/** + * Refund credit note form. + */ +function RefundCreditNoteForm({ + // #withDialogActions + closeDialog, +}) { + const { dialogName, creditNote, createRefundCreditNoteMutate } = + useRefundCreditNoteContext(); + + // Initial form values + const initialValues = { + ...defaultInitialValues, + ...creditNote, + }; + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting }) => { + const form = { + ...omit(values, ['currency_code', 'credits_remaining']), + }; + + // Handle request response success. + const onSaved = (response) => { + AppToaster.show({ + message: intl.get('refund_credit_note.dialog.success_message'), + intent: Intent.SUCCESS, + }); + closeDialog(dialogName); + }; + // Handle request response errors. + const onError = ({ + response: { + data: { errors }, + }, + }) => { + setSubmitting(false); + }; + createRefundCreditNoteMutate([creditNote.id, form]) + .then(onSaved) + .catch(onError); + }; + + return ( + + ); +} +export default compose(withDialogActions)(RefundCreditNoteForm); diff --git a/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteForm.schema.js b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteForm.schema.js new file mode 100644 index 000000000..8ada6b557 --- /dev/null +++ b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteForm.schema.js @@ -0,0 +1,12 @@ +import * as Yup from 'yup'; +import intl from 'react-intl-universal'; +import { DATATYPES_LENGTH } from 'common/dataTypes'; + +const Schema = Yup.object().shape({ + date: Yup.date().required().label(intl.get('date')), + amount: Yup.number().required(), + reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING).nullable(), + from_account_id: Yup.number().required().label(intl.get('deposit_account_')), + description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT), +}); +export const CreateRefundCreditNoteFormSchema = Schema; diff --git a/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFormContent.js b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFormContent.js new file mode 100644 index 000000000..d763729bb --- /dev/null +++ b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFormContent.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { Form } from 'formik'; +import RefundCreditNoteFormFields from './RefundCreditNoteFormFields'; +import RefundCreditNoteFloatingActions from './RefundCreditNoteFloatingActions'; + +/** + * Refund credit note form content. + */ +export default function RefundCreditNoteFormContent() { + return ( +
+ + + + ); +} diff --git a/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFormFields.js b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFormFields.js new file mode 100644 index 000000000..5af43fe97 --- /dev/null +++ b/src/containers/Dialogs/RefundCreditNoteDialog/RefundCreditNoteFormFields.js @@ -0,0 +1,164 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { FastField, ErrorMessage } from 'formik'; +import { + Classes, + FormGroup, + InputGroup, + TextArea, + Position, + ControlGroup, +} from '@blueprintjs/core'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import { DateInput } from '@blueprintjs/datetime'; +import { + Icon, + FieldRequiredHint, + AccountsSuggestField, + InputPrependText, + MoneyInputGroup, + FormattedMessage as T, +} from 'components'; +import { + inputIntent, + momentFormatter, + tansformDateValue, + handleDateChange, +} from 'utils'; +import { useAutofocus } from 'hooks'; +import { ACCOUNT_TYPE } from 'common/accountTypes'; +import { useRefundCreditNoteContext } from './RefundCreditNoteFormProvider'; + +/** + * Refund credit note form fields. + */ +function RefundCreditNoteFormFields() { + const { accounts } = useRefundCreditNoteContext(); + const amountFieldRef = useAutofocus(); + return ( +
+ {/* ------------- Refund date ------------- */} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + className={classNames('form-group--select-list', CLASSES.FILL)} + intent={inputIntent({ error, touched })} + helperText={} + // inline={true} + > + { + form.setFieldValue('date', formattedDate); + })} + popoverProps={{ position: Position.BOTTOM, minimal: true }} + inputProps={{ + leftIcon: , + }} + /> + + )} + + {/* ------------- Amount ------------- */} + + {({ + form: { values, setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + labelInfo={} + className={classNames('form-group--amount', CLASSES.FILL)} + intent={inputIntent({ error, touched })} + helperText={} + // inline={true} + > + + + { + setFieldValue('amount', amount); + }} + intent={inputIntent({ error, touched })} + inputRef={(ref) => (amountFieldRef.current = ref)} + /> + + + )} + + {/* ------------ Reference No. ------------ */} + + {({ form, field, meta: { error, touched } }) => ( + } + className={classNames('form-group--reference', CLASSES.FILL)} + intent={inputIntent({ error, touched })} + helperText={} + // inline={true} + > + + + )} + + + {/* ------------ Form account ------------ */} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + className={classNames( + 'form-group--from_account_id', + 'form-group--select-list', + CLASSES.FILL, + )} + labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + // inline={true} + > + + form.setFieldValue('from_account_id', id) + } + inputProps={{ + placeholder: intl.get('select_account'), + }} + filterByTypes={[ + ACCOUNT_TYPE.BANK, + ACCOUNT_TYPE.CASH, + ACCOUNT_TYPE.FIXED_ASSET, + ]} + /> + + )} + + {/* --------- Statement --------- */} + + {({ form, field, meta: { error, touched } }) => ( + } + className={'form-group--description'} + // inline={true} + > +