feat: universal search.

This commit is contained in:
a.bouhuolia
2021-08-21 18:59:49 +02:00
parent a7b0f1a8d2
commit 79c1b2ab67
82 changed files with 2497 additions and 317 deletions

View File

@@ -54,19 +54,20 @@ function ManualJournalActionsBar({
const handleBulkDelete = () => {};
// Handle tab change.
const handleTabChange = (customView) => {
setManualJournalsTableState({ customViewId: customView.id || null });
const handleTabChange = (view) => {
setManualJournalsTableState({ viewSlug: view ? view.slig : null });
};
// Handle click a refresh Journals
const handleRefreshBtnClick = () => { refresh(); };
console.log(manualJournalsFilterConditions, fields, 'XXX');
const handleRefreshBtnClick = () => {
refresh();
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'manual-journals'}
allMenuItem={true}
views={journalsViews}
onChange={handleTabChange}
/>
@@ -135,5 +136,5 @@ export default compose(
withManualJournalsActions,
withManualJournals(({ manualJournalsTableState }) => ({
manualJournalsFilterConditions: manualJournalsTableState.filterRoles,
}))
})),
)(ManualJournalActionsBar);

View File

@@ -0,0 +1,43 @@
import { RESOURCES_TYPES } from 'common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
/**
* Universal search manual journal item select action.
*/
function JournalUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.MANUAL_JOURNAL) {
openDrawer('journal-drawer', { manualJournalId: resourceId });
}
return null;
}
export const JournalUniversalSearchSelectAction = withDrawerActions(
JournalUniversalSearchSelectComponent,
);
/**
* Mappes the manual journal item to search item.
*/
const manualJournalsToSearch = (manualJournal) => ({
text: manualJournal.journal_number,
subText: manualJournal.formatted_date,
label: manualJournal.formatted_amount,
reference: manualJournal,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchJournalBind = () => ({
resourceType: RESOURCES_TYPES.MANUAL_JOURNAL,
optionItemLabel: 'Manual journal',
selectItemAction: JournalUniversalSearchSelectAction,
itemSelect: manualJournalsToSearch,
});

View File

@@ -0,0 +1,41 @@
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
function AccountUniversalSearchItemSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.ACCOUNT) {
openDrawer('account-drawer', { accountId: resourceId });
}
return null;
}
export const AccountUniversalSearchItemSelect = withDrawerActions(
AccountUniversalSearchItemSelectComponent,
);
/**
* Transformes account item to search item.
* @param {*} account
* @returns
*/
const accountToSearch = (account) => ({
text: `${account.name} - ${account.code}`,
label: account.formatted_amount,
reference: account,
});
/**
* Binds universal search account configure.
*/
export const universalSearchAccountBind = () => ({
resourceType: RESOURCES_TYPES.ACCOUNT,
optionItemLabel: 'Account',
selectItemAction: AccountUniversalSearchItemSelect,
itemSelect: accountToSearch,
});

View File

@@ -78,8 +78,8 @@ function AccountsActionsBar({
};
// Handle tab changing.
const handleTabChange = (customView) => {
setAccountsTableState({ customViewId: customView.id || null });
const handleTabChange = (view) => {
setAccountsTableState({ viewSlug: view ? view.slug : null });
};
// Handle inactive switch changing.
@@ -98,6 +98,8 @@ function AccountsActionsBar({
<NavbarGroup>
<DashboardActionViewsList
resourceName={'accounts'}
allMenuItem={true}
allMenuItemText={<T id={'all_accounts'} />}
views={resourceViews}
onChange={handleTabChange}
/>

View File

@@ -1,4 +1,4 @@
import React, { memo } from 'react';
import React from 'react';
import {
Position,
Classes,
@@ -6,14 +6,13 @@ import {
MenuItem,
Menu,
MenuDivider,
Intent,
Popover,
Button,
Intent
} from '@blueprintjs/core';
import { Icon, Money, If } from 'components';
import intl from 'react-intl-universal';
import { safeCallback } from 'utils';
/**
* Accounts table actions menu.
*/

View File

@@ -62,9 +62,9 @@ function CustomerActionsBar({
openAlert('customers-bulk-delete', { customersIds: customersSelectedRows });
};
const handleTabChange = (viewId) => {
const handleTabChange = (view) => {
setCustomersTableState({
customViewId: viewId.id || null,
viewSlug: view ? view.slug : null,
});
};
// Handle inactive switch changing.
@@ -82,6 +82,8 @@ function CustomerActionsBar({
<DashboardActionViewsList
resourceName={'customers'}
views={customersViews}
allMenuItem={true}
allMenuItemText={<T id={'all'} />}
onChange={handleTabChange}
/>
<NavbarDivider />

View File

@@ -0,0 +1,33 @@
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
function CustomerUniversalSearchSelectComponent({ resourceType, resourceId }) {
if (resourceType === RESOURCES_TYPES.CUSTOMER) {
}
return null;
}
const CustomerUniversalSearchSelectAction = withDrawerActions(
CustomerUniversalSearchSelectComponent
);
/**
* Transformes customers to search.
* @param {*} contact
* @returns
*/
const customersToSearch = (contact) => ({
text: contact.display_name,
label: contact.formatted_balance,
reference: contact,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchCustomerBind = () => ({
resourceType: RESOURCES_TYPES.CUSTOMER,
optionItemLabel: 'Customers',
selectItemAction: CustomerUniversalSearchSelectAction,
itemSelect: customersToSearch,
});

View File

@@ -0,0 +1,24 @@
import { RESOURCES_TYPES } from 'common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
/**
* Universal search bill item select action.
*/
function ExpenseUniversalSearchItemSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.EXPENSE) {
openDrawer('expense-drawer', { expenseId: resourceId });
}
return null;
}
export const ExpenseUniversalSearchItemSelect = withDrawerActions(
ExpenseUniversalSearchItemSelectComponent,
);

View File

@@ -37,7 +37,7 @@ function ExpensesActionsBar({
setExpensesTableState,
// #withExpenses
expensesFilterConditions
expensesFilterConditions,
}) {
// History context.
const history = useHistory();
@@ -57,9 +57,9 @@ function ExpensesActionsBar({
const handleBulkDelete = () => {};
// Handles the tab chaning.
const handleTabChange = (viewId) => {
const handleTabChange = (view) => {
setExpensesTableState({
customViewId: viewId.id || null,
viewSlug: view ? view.slug : null,
});
};
@@ -73,6 +73,7 @@ function ExpensesActionsBar({
<DashboardActionViewsList
resourceName={'expenses'}
views={expensesViews}
allMenuItem={true}
onChange={handleTabChange}
/>
<NavbarDivider />
@@ -140,5 +141,5 @@ export default compose(
withExpensesActions,
withExpenses(({ expensesTableState }) => ({
expensesFilterConditions: expensesTableState.filterRoles,
}))
})),
)(ExpensesActionsBar);

View File

@@ -1,64 +0,0 @@
import React from 'react';
import { Intent, Spinner } from '@blueprintjs/core';
import {
UniversalSearch,
ListSelect,
If,
FormattedMessage as T,
} from 'components';
import { defaultTo } from 'lodash';
import intl from 'react-intl-universal';
import { useAccounts } from 'hooks/query';
import UniversalSearchOptions from 'common/universalSearchOptions';
import { compose } from 'utils';
import withSearch from 'containers/GeneralSearch/withSearch';
function Search({ globalSearchShow }) {
const [query, setQuery] = React.useState();
const [labelState, setLabelState] = React.useState();
const { data: accounts, isFetching: isAccountsFetching } = useAccounts({
search_keyword: query,
});
const handleClick = (placeholder) => {
setLabelState(placeholder);
};
const MenuSelectType = (
<div style={{ display: 'flex' }}>
<If condition={isAccountsFetching}>
<Spinner tagName="div" intent={Intent.NONE} size={20} value={null} />
</If>
<ListSelect
items={UniversalSearchOptions}
onItemSelect={(holder) => handleClick(holder)}
filterable={false}
selectedItem={labelState?.name}
selectedItemProp={'name'}
textProp={'name'}
defaultText={intl.get('type')}
popoverProps={{ minimal: false, captureDismiss: true }}
buttonProps={{
minimal: true,
}}
/>
</div>
);
return (
<UniversalSearch
results={accounts}
isOpen={globalSearchShow}
onQueryChange={(q) => setQuery(q)}
inputProps={{
rightElement: MenuSelectType,
placeholder: `${defaultTo(labelState?.placeholder, '')}`,
}}
/>
);
}
export default compose(withSearch)(Search);

View File

@@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import t from 'store/types';
export const mapStateToProps = (state, props) => ({
resultSearch: state.globalSearch.searches,
globalSearchShow: state.globalSearch.isOpen,
});
export const mapDispatchToProps = (dispatch) => ({
openGlobalSearch: (result) => dispatch({ type: t.OPEN_SEARCH, }),
closeGlobalSearch: (result) => dispatch({ type: t.CLOSE_SEARCH }),
});
export default connect(mapStateToProps, mapDispatchToProps);

View File

@@ -58,8 +58,8 @@ function ItemsActionsBar({
};
// Handle tab changing.
const handleTabChange = (viewId) => {
setItemsTableState({ customViewId: viewId.id || null });
const handleTabChange = (view) => {
setItemsTableState({ viewSlug: view ? view.slug : null });
};
// Handle cancel/confirm items bulk.
@@ -82,6 +82,8 @@ function ItemsActionsBar({
<NavbarGroup>
<DashboardActionViewsList
resourceName={'items'}
allMenuItem={true}
allMenuItemText={<T id={'all_items'} />}
views={itemsViews}
onChange={handleTabChange}
/>

View File

@@ -0,0 +1,45 @@
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
/**
* Item univrsal search item select action.
*/
function ItemUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.ITEM) {
}
return null;
}
export const ItemUniversalSearchSelectAction = withDrawerActions(
ItemUniversalSearchSelectComponent,
);
/**
* Transformes items to search.
* @param {*} item
* @returns
*/
const transfromItemsToSearch = (item) => ({
text: item.name,
subText: item.code,
label: item.type,
reference: item,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchItemBind = () => ({
resourceType: RESOURCES_TYPES.ITEM,
optionItemLabel: 'Items',
selectItemAction: ItemUniversalSearchSelectAction,
itemSelect: transfromItemsToSearch,
});

View File

@@ -130,4 +130,4 @@ export function transformItemsTableState(tableState) {
...transformTableStateToQuery(tableState),
inactive_mode: tableState.inactiveMode,
};
}
}

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { formattedAmount } from 'utils';
import { T, Icon, Choose, If } from 'components';
import intl from 'react-intl-universal';
import { RESOURCES_TYPES } from 'common/resourcesTypes';
import withDrawerActions from '../../Drawer/withDrawerActions';
/**
* Universal search bill item select action.
*/
function BillUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.INVOICE) {
openDrawer('bill-drawer', { billId: resourceId });
}
return null;
}
export const BillUniversalSearchSelect = withDrawerActions(
BillUniversalSearchSelectComponent,
);
/**
* Status accessor.
*/
export function BillStatus({ bill }) {
return (
<Choose>
<Choose.When condition={bill.is_fully_paid && bill.is_open}>
<span class="fully-paid-text">
<T id={'paid'} />
</span>
</Choose.When>
<Choose.When condition={bill.is_open}>
<Choose>
<Choose.When condition={bill.is_overdue}>
<span className={'overdue-status'}>
{intl.get('overdue_by', { overdue: bill.overdue_days })}
</span>
</Choose.When>
<Choose.Otherwise>
<span className={'due-status'}>
{intl.get('due_in', { due: bill.remaining_days })}
</span>
</Choose.Otherwise>
</Choose>
<If condition={bill.is_partially_paid}>
<span className="partial-paid">
{intl.get('day_partially_paid', {
due: formattedAmount(bill.due_amount, bill.currency_code),
})}
</span>
</If>
</Choose.When>
<Choose.Otherwise>
<span class="draft">
<T id={'draft'} />
</span>
</Choose.Otherwise>
</Choose>
);
}
/**
* Bill universal search item.
*/
export function BillUniversalSearchItem(
item,
{ handleClick, modifiers, query },
) {
return (
<MenuItem
active={modifiers.active}
text={
<div>
<div>{item.text}</div>
<span class="bp3-text-muted">
{item.reference.bill_number}{' '}
<Icon icon={'caret-right-16'} iconSize={16} />
{item.reference.formatted_bill_date}
</span>
</div>
}
label={
<>
<div class="amount">{item.reference.formatted_amount}</div>
<BillStatus bill={item.reference} />
</>
}
onClick={handleClick}
className={'universal-search__item--bill'}
/>
);
}
const billsToSearch = (bill) => ({
text: bill.vendor.display_name,
reference: bill,
});
export const universalSearchBillBind = () => ({
resourceType: RESOURCES_TYPES.BILL,
optionItemLabel: 'Bills',
selectItemAction: BillUniversalSearchSelect,
itemRenderer: BillUniversalSearchItem,
itemSelect: billsToSearch,
});

View File

@@ -34,7 +34,7 @@ function BillActionsBar({
setBillsTableState,
// #withBills
billsConditionsRoles
billsConditionsRoles,
}) {
const history = useHistory();
@@ -50,13 +50,15 @@ function BillActionsBar({
};
// Handle tab change.
const handleTabChange = (customView) => {
const handleTabChange = (view) => {
setBillsTableState({
customViewId: customView.id || null,
viewSlug: view ? view.slug : null,
});
};
// Handle click a refresh bills
const handleRefreshBtnClick = () => { refresh(); };
const handleRefreshBtnClick = () => {
refresh();
};
return (
<DashboardActionsBar>
@@ -64,6 +66,8 @@ function BillActionsBar({
<DashboardActionViewsList
resourceName={'bills'}
views={billsViews}
allMenuItem={true}
allMenuItemText={<T id={'all'} />}
onChange={handleTabChange}
/>
<NavbarDivider />
@@ -127,6 +131,6 @@ function BillActionsBar({
export default compose(
withBillsActions,
withBills(({ billsTableState }) => ({
billsConditionsRoles: billsTableState.filterRoles
}))
billsConditionsRoles: billsTableState.filterRoles,
})),
)(BillActionsBar);

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Icon } from 'components';
import { RESOURCES_TYPES } from 'common/resourcesTypes';
import withDrawerActions from '../../Drawer/withDrawerActions';
import { highlightText } from 'utils';
/**
* Universal search bill item select action.
*/
function PaymentMadeUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.PAYMENT_MADE) {
openDrawer('payment-made-detail-drawer', { paymentMadeId: resourceId });
}
return null;
}
export const PaymentMadeUniversalSearchSelect = withDrawerActions(
PaymentMadeUniversalSearchSelectComponent,
);
/**
* Payment made universal search item.
*/
export function PaymentMadeUniversalSearchItem(
item,
{ handleClick, modifiers, query },
) {
return (
<MenuItem
active={modifiers.active}
text={
<div>
<div>{highlightText(item.text, query)}</div>
<span class="bp3-text-muted">
{highlightText(item.reference.payment_number, query)}{' '}
<Icon icon={'caret-right-16'} iconSize={16} />
{highlightText(item.reference.formatted_payment_date, query)}
</span>
</div>
}
label={<div class="amount">{item.reference.formatted_amount}</div>}
onClick={handleClick}
className={'universal-search__item--payment-made'}
/>
);
}
/**
* Payment made resource item to search item.
*/
const paymentMadeToSearch = (payment) => ({
text: payment.vendor.display_name,
subText: payment.formatted_payment_date,
label: payment.formatted_amount,
reference: payment,
});
/**
* Binds universal search payment made configure.
*/
export const universalSearchPaymentMadeBind = () => ({
resourceType: RESOURCES_TYPES.PAYMENT_MADE,
optionItemLabel: 'Payment made',
selectItemAction: PaymentMadeUniversalSearchSelect,
itemRenderer: PaymentMadeUniversalSearchItem,
itemSelect: paymentMadeToSearch,
});

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Choose, T, Icon } from 'components';
import { RESOURCES_TYPES } from "../../../../common/resourcesTypes";
import withDrawerActions from "../../../Drawer/withDrawerActions";
/**
* Estimate universal search item select action.
*/
function EstimateUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.ESTIMATE) {
openDrawer('estimate-drawer', { estimateId: resourceId });
}
return null;
}
export const EstimateUniversalSearchSelect = withDrawerActions(
EstimateUniversalSearchSelectComponent,
);
/**
* Status accessor.
*/
export const EstimateStatus = ({ estimate }) => (
<Choose>
<Choose.When condition={estimate.is_delivered && estimate.is_approved}>
<span class="approved">
<T id={'approved'} />
</span>
</Choose.When>
<Choose.When condition={estimate.is_delivered && estimate.is_rejected}>
<span class="reject">
<T id={'rejected'} />
</span>
</Choose.When>
<Choose.When
condition={
estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved
}
>
<span class="delivered">
<T id={'delivered'} />
</span>
</Choose.When>
<Choose.Otherwise>
<span class="draft">
<T id={'draft'} />
</span>
</Choose.Otherwise>
</Choose>
);
/**
* Estimate universal search item.
*/
export function EstimateUniversalSearchItem(
item,
{ handleClick, modifiers, query },
) {
return (
<MenuItem
text={
<div>
<div>{item.text}</div>
<span class="bp3-text-muted">
{item.reference.estimate_number}{' '}
<Icon icon={'caret-right-16'} iconSize={16} />
{item.reference.formatted_estimate_date}
</span>
</div>
}
label={
<>
<div class="amount">{item.reference.formatted_amount}</div>
<EstimateStatus estimate={item.reference} />
</>
}
onClick={handleClick}
className={'universal-search__item--estimate'}
/>
);
}
const transformEstimatesToSearch = (estimate) => ({
text: estimate.customer.display_name,
label: estimate.formatted_balance,
reference: estimate,
});
export const universalSearchEstimateBind = () => ({
resourceType: RESOURCES_TYPES.ESTIMATE,
optionItemLabel: 'Estimates',
selectItemAction: EstimateUniversalSearchSelect,
itemRenderer: EstimateUniversalSearchItem,
itemSelect: transformEstimatesToSearch
});

View File

@@ -35,7 +35,7 @@ function EstimateActionsBar({
setEstimatesTableState,
// #withEstimates
estimatesFilterRoles
estimatesFilterRoles,
}) {
const history = useHistory();
@@ -51,20 +51,24 @@ function EstimateActionsBar({
const { refresh } = useRefreshEstimates();
// Handle tab change.
const handleTabChange = (customView) => {
const handleTabChange = (view) => {
setEstimatesTableState({
customViewId: customView.id || null,
viewSlug: view ? view.slug : null,
});
};
// Handle click a refresh sale estimates
const handleRefreshBtnClick = () => { refresh(); };
const handleRefreshBtnClick = () => {
refresh();
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'estimates'}
allMenuItem={true}
allMenuItemText={<T id={'all'} />}
views={estimatesViews}
onChange={handleTabChange}
/>

View File

@@ -0,0 +1,121 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { T, Choose, Icon } from 'components';
import { highlightText } from 'utils';
import { RESOURCES_TYPES } from 'common/resourcesTypes';
import withDrawerActions from '../../Drawer/withDrawerActions';
/**
* Universal search invoice item select action.
*/
function InvoiceUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.INVOICE) {
openDrawer('invoice-drawer', { invoiceId: resourceId });
}
return null;
}
export const InvoiceUniversalSearchSelect = withDrawerActions(
InvoiceUniversalSearchSelectComponent,
);
/**
* Invoice status.
*/
function InvoiceStatus(customer) {
return (
<Choose>
<Choose.When condition={customer.is_fully_paid && customer.is_delivered}>
<span class="status status-success">
<T id={'paid'} />
</span>
</Choose.When>
<Choose.When condition={customer.is_delivered}>
<Choose>
<Choose.When condition={customer.is_overdue}>
<span className={'status status-warning'}>
{intl.get('overdue_by', { overdue: customer.overdue_days })}
</span>
</Choose.When>
<Choose.Otherwise>
<span className={'status status-warning'}>
{intl.get('due_in', { due: customer.remaining_days })}
</span>
</Choose.Otherwise>
</Choose>
</Choose.When>
<Choose.Otherwise>
<span class="status status--gray">
<T id={'draft'} />
</span>
</Choose.Otherwise>
</Choose>
);
}
/**
* Universal search invoice item.
*/
export function InvoiceUniversalSearchItem(
item,
{ handleClick, modifiers, query },
) {
return (
<MenuItem
active={modifiers.active}
text={
<div>
<div>{highlightText(item.text, query)}</div>
<span class="bp3-text-muted">
{highlightText(item.reference.invoice_no, query)}{' '}
<Icon icon={'caret-right-16'} iconSize={16} />
{highlightText(item.reference.formatted_invoice_date, query)}
</span>
</div>
}
label={
<>
<div class="amount">${item.reference.balance}</div>
<InvoiceStatus customer={item.reference} />
</>
}
onClick={handleClick}
className={'universal-search__item--invoice'}
/>
);
}
/**
* Transformes invoices to search.
* @param {*} invoice
* @returns
*/
const transformInvoicesToSearch = (invoice) => ({
id: invoice.id,
text: invoice.customer.display_name,
label: invoice.formatted_balance,
reference: invoice,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchInvoiceBind = () => ({
resourceType: RESOURCES_TYPES.INVOICE,
optionItemLabel: 'Invoices',
selectItemAction: InvoiceUniversalSearchSelect,
itemRenderer: InvoiceUniversalSearchItem,
itemSelect: transformInvoicesToSearch,
});

View File

@@ -51,8 +51,8 @@ function InvoiceActionsBar({
const { refresh } = useRefreshInvoices();
// Handle views tab change.
const handleTabChange = (customView) => {
setInvoicesTableState({ customViewId: customView.id || null });
const handleTabChange = (view) => {
setInvoicesTableState({ viewSlug: view ? view.slug : null });
};
// Handle click a refresh sale invoices
@@ -64,6 +64,7 @@ function InvoiceActionsBar({
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
allMenuItem={true}
resourceName={'invoices'}
views={invoicesViews}
onChange={handleTabChange}

View File

@@ -210,4 +210,4 @@ export function useInvoicesTableColumns() {
],
[],
);
}
}

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { RESOURCES_TYPES } from "../../../common/resourcesTypes";
import withDrawerActions from "../../Drawer/withDrawerActions";
import { highlightText } from 'utils';
import { Icon } from 'components';
/**
* Payment receive universal search item select action.
*/
function PaymentReceiveUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.PAYMENT_RECEIVE) {
openDrawer('payment-receive-drawer', { paymentReceiveId: resourceId });
}
return null;
}
export const PaymentReceiveUniversalSearchSelect = withDrawerActions(
PaymentReceiveUniversalSearchSelectComponent,
);
/**
* Payment receive universal search item.
*/
export function PaymentReceiveUniversalSearchItem(
item,
{ handleClick, modifiers, query },
) {
return (
<MenuItem
active={modifiers.active}
text={
<div>
<div>{highlightText(item.text, query)}</div>
<span class="bp3-text-muted">
{highlightText(item.reference.payment_receive_no, query)}{' '}
<Icon icon={'caret-right-16'} iconSize={16} />
{highlightText(item.reference.formatted_payment_date, query)}
</span>
</div>
}
label={<div class="amount">{item.reference.formatted_amount}</div>}
onClick={handleClick}
className={'universal-search__item--invoice'}
/>
);
}
/**
* Transformes payment receives to search.
* @param {*} payment
* @returns
*/
const paymentReceivesToSearch = (payment) => ({
text: payment.customer.display_name,
subText: payment.formatted_payment_date,
label: payment.formatted_amount,
reference: payment,
});
/**
* Binds universal search payment receive configure.
*/
export const universalSearchPaymentReceiveBind = () => ({
resourceType: RESOURCES_TYPES.PAYMENT_RECEIVE,
optionItemLabel: 'Payment receive',
selectItemAction: PaymentReceiveUniversalSearchSelect,
itemRenderer: PaymentReceiveUniversalSearchItem,
itemSelect: paymentReceivesToSearch,
});

View File

@@ -0,0 +1,100 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Icon, Choose, T } from 'components';
import { RESOURCES_TYPES } from "../../../common/resourcesTypes";
import withDrawerActions from "../../Drawer/withDrawerActions";
/**
* Receipt universal search item select action.
*/
function ReceiptUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
onAction,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.RECEIPT) {
openDrawer('receipt-drawer', { estimateId: resourceId });
}
return null;
}
export const ReceiptUniversalSearchSelect = withDrawerActions(
ReceiptUniversalSearchSelectComponent,
);
/**
* Status accessor.
*/
function ReceiptStatus({ receipt }) {
return (
<Choose>
<Choose.When condition={receipt.is_closed}>
<span class="closed"><T id={'closed'} /></span>
</Choose.When>
<Choose.Otherwise>
<span class="draft"><T id={'draft'} /></span>
</Choose.Otherwise>
</Choose>
);
}
/**
* Receipt universal search item.
*/
export function ReceiptUniversalSearchItem(
item,
{ handleClick, modifiers, query },
) {
return (
<MenuItem
active={modifiers.active}
text={
<div>
<div>{item.text}</div>
<span class="bp3-text-muted">
{item.reference.receipt_number}{' '}
<Icon icon={'caret-right-16'} iconSize={16} />
{item.reference.formatted_receipt_date}
</span>
</div>
}
label={
<>
<div class="amount">${item.reference.amount}</div>
<ReceiptStatus receipt={item.reference} />
</>
}
onClick={handleClick}
className={'universal-search__item--receipt'}
/>
);
}
/**
* Transformes receipt resource item to search item.
*/
const transformReceiptsToSearch = (receipt) => ({
text: receipt.customer.display_name,
label: receipt.formatted_amount,
reference: receipt,
});
/**
* Receipt universal search bind configuration.
*/
export const universalSearchReceiptBind = () => ({
resourceType: RESOURCES_TYPES.RECEIPT,
optionItemLabel: 'Receipts',
selectItemAction: ReceiptUniversalSearchSelect,
itemRenderer: ReceiptUniversalSearchItem,
itemSelect: transformReceiptsToSearch,
});

View File

@@ -49,22 +49,24 @@ function ReceiptActionsBar({
// Sale receipt refresh action.
const { refresh } = useRefreshReceipts();
const handleTabChange = (customView) => {
const handleTabChange = (view) => {
setReceiptsTableState({
customViewId: customView.id || null,
viewSlug: view ? view.slug : null,
});
};
// Handle click a refresh sale estimates
const handleRefreshBtnClick = () => { refresh(); };
console.log(receiptsFilterConditions, fields, 'XXX');
const handleRefreshBtnClick = () => {
refresh();
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'receipts'}
allMenuItem={true}
allMenuItemText={<T id={'all'} />}
views={receiptsViews}
onChange={handleTabChange}
/>

View File

@@ -0,0 +1,140 @@
import React from 'react';
import { debounce } from 'lodash';
import { isUndefined } from 'lodash';
import { useUniversalSearch } from 'hooks/query';
import { UniversalSearch } from 'components';
import { RESOURCES_TYPES } from 'common/resourcesTypes';
import { compose } from 'utils';
import withUniversalSearchActions from './withUniversalSearchActions';
import withUniversalSearch from './withUniversalSearch';
import DashboardUniversalSearchItemActions from './DashboardUniversalSearchItemActions';
import { DashboardUniversalSearchItem } from './components';
import DashboardUniversalSearchHotkeys from './DashboardUniversalSearchHotkeys';
import { getUniversalSearchTypeOptions } from './utils';
/**
* Dashboard universal search.
*/
function DashboardUniversalSearch({
// #withUniversalSearchActions
setSelectedItemUniversalSearch,
// #withUniversalSearch
globalSearchShow,
closeGlobalSearch,
defaultUniversalResourceType,
}) {
// Search keyword.
const [searchKeyword, setSearchKeyword] = React.useState('');
// Default search type.
const [defaultSearchType, setDefaultSearchType] = React.useState(
defaultUniversalResourceType || RESOURCES_TYPES.CUSTOMR,
);
// Search type.
const [searchType, setSearchType] = React.useState(defaultSearchType);
// Sync default search type with default universal resource type.
React.useEffect(() => {
if (
!isUndefined(defaultUniversalResourceType) &&
defaultSearchType !== defaultUniversalResourceType
) {
setSearchType(defaultUniversalResourceType);
setDefaultSearchType(defaultUniversalResourceType);
}
}, [defaultSearchType, defaultUniversalResourceType]);
// Fetch accounts list according to the given custom view id.
const {
data,
remove,
isFetching: isSearchFetching,
isLoading: isSearchLoading,
refetch,
} = useUniversalSearch(searchType, searchKeyword, {
keepPreviousData: true,
enabled: false,
});
// Handle query change.
const handleQueryChange = (query) => {
setSearchKeyword(query);
};
// Handle search type change.
const handleSearchTypeChange = (searchType) => {
remove();
setSearchType(searchType.key);
if (searchKeyword && searchType) {
refetch();
}
};
// Handle overlay of universal search close.
const handleClose = () => {
closeGlobalSearch();
};
// Handle universal search item select.
const handleItemSelect = (item) => {
setSelectedItemUniversalSearch(searchType, item.id);
closeGlobalSearch();
setSearchKeyword('');
};
const debounceFetch = React.useRef(
debounce(() => {
refetch();
}, 200),
);
React.useEffect(() => {
if (searchKeyword) {
debounceFetch.current();
}
}, [searchKeyword]);
// Handles the overlay once be closed.
const handleOverlayClosed = () => {
setSearchKeyword('');
};
const searchTypeOptions = React.useMemo(
() => getUniversalSearchTypeOptions(),
[],
);
return (
<div class="dashboard__universal-search">
<UniversalSearch
isOpen={globalSearchShow}
isLoading={isSearchFetching}
items={data}
overlayProps={{
onClose: handleClose,
onClosed: handleOverlayClosed,
}}
searchResource={searchType}
onQueryChange={handleQueryChange}
onSearchTypeChange={handleSearchTypeChange}
onItemSelect={handleItemSelect}
itemRenderer={DashboardUniversalSearchItem}
query={searchKeyword}
searchTypeOptions={searchTypeOptions}
/>
<DashboardUniversalSearchItemActions />
<DashboardUniversalSearchHotkeys />
</div>
);
}
export default compose(
withUniversalSearchActions,
withUniversalSearch(({ globalSearchShow, defaultUniversalResourceType }) => ({
globalSearchShow,
defaultUniversalResourceType,
})),
)(DashboardUniversalSearch);

View File

@@ -0,0 +1,26 @@
import { universalSearchInvoiceBind } from '../Sales/Invoices/InvoiceUniversalSearch';
import { universalSearchReceiptBind } from '../Sales/Receipts/ReceiptUniversalSearch';
import { universalSearchBillBind } from '../Purchases/Bills/BillUniversalSearch';
import { universalSearchEstimateBind } from '../Sales/Estimates/EstimatesLanding/EstimateUniversalSearch';
import { universalSearchPaymentReceiveBind } from '../Sales/PaymentReceives/PaymentReceiveUniversalSearch';
import { universalSearchPaymentMadeBind } from '../Purchases/PaymentMades/PaymentMadeUniversalSearch';
import { universalSearchItemBind } from '../Items/ItemsUniversalSearch';
import { universalSearchCustomerBind } from '../Customers/CustomersUniversalSearch';
import { universalSearchJournalBind } from '../Accounting/ManualJournalUniversalSearch';
import { universalSearchAccountBind } from '../Accounts/AccountUniversalSearch';
import { universalSearchVendorBind } from '../Vendors/VendorsUniversalSearch';
// Universal search binds.
export const universalSearchBinds = [
universalSearchItemBind,
universalSearchAccountBind,
universalSearchInvoiceBind,
universalSearchReceiptBind,
universalSearchEstimateBind,
universalSearchBillBind,
universalSearchPaymentReceiveBind,
universalSearchPaymentMadeBind,
universalSearchCustomerBind,
universalSearchVendorBind,
universalSearchJournalBind,
];

View File

@@ -0,0 +1,21 @@
import * as R from 'ramda';
import { useHotkeys } from 'react-hotkeys-hook';
import withUniversalSearchActions from './withUniversalSearchActions';
/**
* Universal search hotkey.
*/
function DashboardUniversalSearchHotkey({
openGlobalSearch,
}) {
useHotkeys('ctrl+o', (event, handle) => {
openGlobalSearch();
});
return null;
}
export default R.compose(
withUniversalSearchActions
)(DashboardUniversalSearchHotkey);

View File

@@ -0,0 +1,32 @@
import React from 'react';
import * as R from 'ramda';
import withUniversalSearch from './withUniversalSearch';
import { getUniversalSearchItemsActions } from './utils';
/**
* Universal search selected item action based on each resource type.
*/
function DashboardUniversalSearchItemActions({
searchSelectedResourceType,
searchSelectedResourceId,
}) {
const components = getUniversalSearchItemsActions();
return components.map((COMPONENT) => (
<COMPONENT
resourceId={searchSelectedResourceId}
resourceType={searchSelectedResourceType}
/>
));
}
export default R.compose(
withUniversalSearch(
({ searchSelectedResourceType, searchSelectedResourceId }) => ({
searchSelectedResourceType,
searchSelectedResourceId,
}),
),
)(DashboardUniversalSearchItemActions);

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { highlightText } from 'utils';
import { getUniversalSearchBind } from './utils';
/**
* Default univesal search item component.
*/
function UniversalSearchItemDetail(item, { handleClick, modifiers, query }) {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={
<div>
<div>{highlightText(item.text, query)}</div>
{item.subText && (
<span class="bp3-text-muted">
{highlightText(item.subText, query)}
</span>
)}
</div>
}
label={item.label ? highlightText(item.label, query) : ''}
onClick={handleClick}
/>
);
}
/**
*
* @param {*} props
* @param {*} actions
* @returns
*/
export const DashboardUniversalSearchItem = (props, actions) => {
const itemRenderer = getUniversalSearchBind(props._type, 'itemRenderer');
return typeof itemRenderer !== 'undefined'
? itemRenderer(props, actions)
: UniversalSearchItemDetail(props, actions);
};

View File

@@ -0,0 +1,44 @@
import { get } from 'lodash';
import { universalSearchBinds } from './DashboardUniversalSearchBinds';
/**
*
* @returns
*/
export const getUniversalSearchBinds = () => {
return universalSearchBinds.map((binder) => binder());
};
/**
*
* @param {*} resourceType
* @param {*} key
* @returns
*/
export const getUniversalSearchBind = (resourceType, key) => {
const resourceConfig = getUniversalSearchBinds().find(
(meta) => meta.resourceType === resourceType,
);
return key ? get(resourceConfig, key) : resourceConfig;
};
/**
*
* @returns
*/
export const getUniversalSearchTypeOptions = () => {
return getUniversalSearchBinds().map((bind) => ({
key: bind.resourceType,
label: bind.optionItemLabel,
}))
}
/**
*
* @returns
*/
export const getUniversalSearchItemsActions = () => {
return getUniversalSearchBinds()
.filter((bind) => bind.selectItemAction)
.map((bind) => bind.selectItemAction);
}

View File

@@ -0,0 +1,18 @@
import { connect } from 'react-redux';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { globalSearch } = state;
const mapped = {
globalSearchShow: globalSearch.isOpen,
defaultUniversalResourceType: globalSearch.defaultResourceType,
searchSelectedResourceType: globalSearch.selectedItem.resourceType,
searchSelectedResourceId: globalSearch.selectedItem.resourceId,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import t from 'store/types';
import {
universalSearchResetResourceType,
universalSearchSetResourceType,
universalSearchSetSelectedItem,
universalSearchResetSelectedItem
} from '../../store/search/search.actions';
export const mapDispatchToProps = (dispatch) => ({
openGlobalSearch: () => dispatch({ type: t.OPEN_SEARCH }),
closeGlobalSearch: () => dispatch({ type: t.CLOSE_SEARCH }),
setResourceTypeUniversalSearch: (resourceType) =>
dispatch(universalSearchSetResourceType(resourceType)),
resetResourceTypeUniversalSearch: () =>
dispatch(universalSearchResetResourceType()),
setSelectedItemUniversalSearch: (resourceType, resourceId) =>
dispatch(universalSearchSetSelectedItem(resourceType, resourceId)),
resetSelectedItemUniversalSearch: () =>
dispatch(universalSearchResetSelectedItem()),
});
export default connect(null, mapDispatchToProps);

View File

@@ -0,0 +1,31 @@
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
import withDrawerActions from '../Drawer/withDrawerActions';
function VendorUniversalSearchSelectComponent({ resourceType, resourceId }) {
if (resourceType === RESOURCES_TYPES.VENDOR) {
}
return null;
}
const VendorUniversalSearchSelectAction = withDrawerActions(
VendorUniversalSearchSelectComponent
);
/**
* Transformes vendor resource item to search.
*/
const vendorToSearch = (contact) => ({
text: contact.display_name,
label: contact.balance > 0 ? contact.formatted_balance + '' : '',
reference: contact,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchVendorBind = () => ({
resourceType: RESOURCES_TYPES.VENDOR,
optionItemLabel: 'Vendor',
selectItemAction: VendorUniversalSearchSelectAction,
itemSelect: vendorToSearch,
});