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

@@ -74,6 +74,8 @@ const CLASSES = {
UNIVERSAL_SEARCH_OVERLAY: 'universal-search-overlay',
UNIVERSAL_SEARCH_INPUT: 'universal-search__input',
UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS: 'universal-search-input-right-elements',
UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY: 'universal-search__type-select-overlay',
UNIVERSAL_SEARCH_TYPE_SELECT_BTN: 'universal-search__type-select-btn',
UNIVERSAL_SEARCH_FOOTER: 'universal-search__footer',
UNIVERSAL_SEARCH_ACTIONS: 'universal-search__actions',

View File

@@ -0,0 +1,14 @@
export const RESOURCES_TYPES = {
INVOICE: 'invoice',
ESTIMATE: 'estimate',
RECEIPT: 'receipt',
PAYMENT_RECEIVE: 'payment_receive',
PAYMENT_MADE: 'payment_made',
CUSTOMER: 'customer',
VENDOR: 'vendor',
ITEM: 'item',
BILL: 'bill',
EXPENSE: 'expense',
MANUAL_JOURNAL: 'manual_journal',
ACCOUNT: 'account',
};

View File

@@ -1,12 +0,0 @@
import intl from 'react-intl-universal';
export default [
{
name: 'Type1',
placeholder: 'Id tempor anim culpa esse id laboris.',
},
{
name: 'Type2',
placeholder: 'Laborum aliqua eiusmod voluptate aliqua',
},
];

View File

@@ -7,7 +7,7 @@ import Sidebar from 'components/Sidebar/Sidebar';
import DashboardContent from 'components/Dashboard/DashboardContent';
import DialogsContainer from 'components/DialogsContainer';
import PreferencesPage from 'components/Preferences/PreferencesPage';
import Search from 'containers/GeneralSearch/Search';
import DashboardUniversalSearch from 'containers/UniversalSearch/DashboardUniversalSearch';
import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
import GlobalHotkeys from './GlobalHotkeys';
import DashboardProvider from './DashboardProvider';
@@ -35,7 +35,7 @@ export default function Dashboard() {
</Route>
</Switch>
<Search />
<DashboardUniversalSearch />
<DialogsContainer />
<GlobalHotkeys />
<DrawersContainer />

View File

@@ -8,6 +8,7 @@ import {
Popover,
PopoverInteractionKind,
Position,
Divider,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { Icon } from 'components';
@@ -17,6 +18,8 @@ import { Icon } from 'components';
*/
export default function DashboardActionViewsList({
resourceName,
allMenuItem,
allMenuItemText,
views,
onChange,
}) {
@@ -28,9 +31,28 @@ export default function DashboardActionViewsList({
<MenuItem onClick={() => handleClickViewItem(view)} text={view.name} />
));
const handleAllTabClick = () => {
handleClickViewItem(null);
};
const content = (
<Menu>
{allMenuItem && (
<>
<MenuItem
onClick={handleAllTabClick}
text={allMenuItemText || 'All'}
/>
<Divider />
</>
)}
{viewsMenuItems}
</Menu>
);
return (
<Popover
content={<Menu>{viewsMenuItems}</Menu>}
content={content}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}

View File

@@ -27,6 +27,7 @@ export default function DashboardContentRoute() {
hint={route.hint}
sidebarExpand={route.sidebarExpand}
pageType={route.pageType}
defaultSearchResource={route.defaultSearchResource}
/>
</Route>
))}

View File

@@ -1,10 +1,13 @@
import React, { useEffect, Suspense } from 'react';
import { isUndefined } from 'lodash';
// import { isUndefined } from 'lodash';
import { CLASSES } from 'common/classes';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import { Spinner } from '@blueprintjs/core';
// import withUniversalSearch from '../../containers/UniversalSearch/withUniversalSearch';
import withUniversalSearchActions from '../../containers/UniversalSearch/withUniversalSearchActions';
/**
* Dashboard pages wrapper.
*/
@@ -16,12 +19,17 @@ function DashboardPage({
Component,
name,
hint,
defaultSearchResource,
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
changePageHint,
toggleSidebarExpand
toggleSidebarExpand,
// #withUniversalSearch
setResourceTypeUniversalSearch,
resetResourceTypeUniversalSearch,
}) {
// Hydrate the given page title.
useEffect(() => {
@@ -38,7 +46,7 @@ function DashboardPage({
return () => {
hint && changePageHint('');
}
};
}, [hint, changePageHint]);
// Hydrate the dashboard back link status.
@@ -53,7 +61,7 @@ function DashboardPage({
useEffect(() => {
const className = `page-${name}`;
name && document.body.classList.add(className);
return () => {
name && document.body.classList.remove(className);
};
@@ -61,15 +69,30 @@ function DashboardPage({
useEffect(() => {
toggleSidebarExpand(sidebarExpand);
}, [toggleSidebarExpand, sidebarExpand])
}, [toggleSidebarExpand, sidebarExpand]);
useEffect(() => {
if (defaultSearchResource) {
setResourceTypeUniversalSearch(defaultSearchResource);
}
return () => {
resetResourceTypeUniversalSearch();
};
}, [
defaultSearchResource,
resetResourceTypeUniversalSearch,
setResourceTypeUniversalSearch,
]);
return (
<div className={CLASSES.DASHBOARD_PAGE}>
<Suspense fallback={
<div class="dashboard__fallback-loading">
<Spinner size={40} value={null} />
</div>
}>
<Suspense
fallback={
<div class="dashboard__fallback-loading">
<Spinner size={40} value={null} />
</div>
}
>
<Component />
</Suspense>
</div>
@@ -78,4 +101,6 @@ function DashboardPage({
export default compose(
withDashboardActions,
// withUniversalSearch,
withUniversalSearchActions,
)(DashboardPage);

View File

@@ -16,7 +16,7 @@ import DashboardBreadcrumbs from 'components/Dashboard/DashboardBreadcrumbs';
import DashboardBackLink from 'components/Dashboard/DashboardBackLink';
import { Icon, Hint, If } from 'components';
import withUniversalSearch from 'containers/UniversalSearch/withUniversalSearch';
import withUniversalSearchActions from 'containers/UniversalSearch/withUniversalSearchActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import withSettings from 'containers/Settings/withSettings';
@@ -155,7 +155,7 @@ function DashboardTopbar({
}
export default compose(
withUniversalSearch,
withUniversalSearchActions,
withDashboard(({ pageTitle, pageHint, editViewId, sidebarExpended }) => ({
pageTitle,
editViewId,

View File

@@ -1,50 +0,0 @@
import React from 'react';
import { Omnibar } from '@blueprintjs/select';
import { MenuItem, Spinner } from '@blueprintjs/core';
import { FormattedMessage as T, Icon, ListSelect } from 'components';
import withSearch from 'containers/GeneralSearch/withSearch';
import { compose } from 'utils';
function UniversalSearch({
results,
onClose,
// withSearch
globalSearchShow,
closeGlobalSearch,
...props
}) {
const SearchRenderer = (
{ name, code, amount },
{ handleClick, modifiers, query },
) => {
return (
<MenuItem
text={`${name} - ${code}`}
label={amount}
onClick={handleClick}
/>
);
};
const handleClose = () => {
closeGlobalSearch(false);
};
return (
<Omnibar
className={'navbar--omnibar'}
items={results}
itemRenderer={SearchRenderer}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
resetOnSelect={true}
onClose={handleClose}
{...props}
/>
);
}
export default compose(withSearch)(UniversalSearch);

View File

@@ -0,0 +1,227 @@
import React from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { isUndefined } from 'lodash';
import {
Overlay,
InputGroup,
Tag,
MenuItem,
Spinner,
Intent,
} from '@blueprintjs/core';
import { QueryList } from '@blueprintjs/select';
import { CLASSES } from 'common/classes';
import { Icon, If, ListSelect, FormattedMessage as T } from 'components';
import {
UniversalSearchProvider,
useUniversalSearchContext,
} from './UniversalSearchProvider';
import { filterItemsByResourceType } from './utils';
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
/**
* Universal search input action.
*/
function UniversalSearchInputRightElements({ onSearchTypeChange }) {
const { isLoading, searchType, defaultSearchResource, searchTypeOptions } =
useUniversalSearchContext();
// Handle search type option change.
const handleSearchTypeChange = (option) => {
onSearchTypeChange && onSearchTypeChange(option);
};
return (
<div className={CLASSES.UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS}>
<If condition={isLoading}>
<Spinner tagName="div" intent={Intent.NONE} size={18} value={null} />
</If>
<ListSelect
items={searchTypeOptions}
onItemSelect={handleSearchTypeChange}
filterable={false}
initialSelectedItem={defaultSearchResource}
selectedItem={searchType}
selectedItemProp={'key'}
textProp={'label'}
// defaultText={intl.get('type')}
popoverProps={{
minimal: true,
captureDismiss: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY,
}}
buttonProps={{
minimal: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_BTN,
}}
/>
</div>
);
}
/**
* Universal search query list.
*/
function UniversalSearchQueryList(props) {
const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } =
props;
return (
<QueryList
{...restProps}
initialContent={null}
renderer={(listProps) => (
<UniversalSearchBar
isOpen={isOpen}
onSearchTypeChange={onSearchTypeChange}
{...listProps}
/>
)}
noResults={
!isLoading ? (
<MenuItem disabled={true} text={<T id={'no_results'} />} />
) : (
<MenuItem
disabled={true}
text={<T id={'universal_search.loading'} />}
/>
)
}
/>
);
}
/**
* Universal query search actions.
*/
function UniversalQuerySearchActions() {
return (
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTIONS)}>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_SELECT)}>
<Tag>ENTER</Tag>
<span class={'text'}>{intl.get('universal_search.enter_text')}</span>
</div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_CLOSE)}>
<Tag>ESC</Tag>{' '}
<span class={'text'}>{intl.get('universal_search.close_text')}</span>
</div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_ARROWS)}>
<Tag>
<Icon icon={'arrow-up-24'} iconSize={16} />
</Tag>
<Tag>
<Icon icon={'arrow-down-24'} iconSize={16} />
</Tag>
<span class="text">{intl.get('universal_seach.navigate_text')}</span>
</div>
</div>
);
}
/**
* Universal search input bar with items list.
*/
function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
const { handleKeyDown, handleKeyUp } = listProps;
const handlers = isOpen
? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }
: {};
return (
<div
className={classNames(
CLASSES.UNIVERSAL_SEARCH_OMNIBAR,
listProps.className,
)}
{...handlers}
>
<InputGroup
autoFocus={true}
large={true}
leftIcon={<Icon icon={'universal-search'} iconSize={20} />}
placeholder={intl.get('universal_search.placeholder')}
onChange={listProps.handleQueryChange}
value={listProps.query}
rightElement={
<UniversalSearchInputRightElements
onSearchTypeChange={onSearchTypeChange}
/>
}
/>
{listProps.itemList}
</div>
);
}
/**
* Universal search.
*/
export function UniversalSearch({
defaultSearchResource,
searchResource,
overlayProps,
isOpen,
isLoading,
onSearchTypeChange,
items,
searchTypeOptions,
...queryListProps
}) {
// Search type state.
const [searchType, setSearchType] = React.useState(
defaultSearchResource || RESOURCES_TYPES.CUSTOMER,
);
// Handle search resource type controlled mode.
React.useEffect(() => {
if (
!isUndefined(searchResource) &&
searchResource !== defaultSearchResource
) {
setSearchType(searchResource);
}
}, [searchResource, defaultSearchResource]);
// Handle search type change.
const handleSearchTypeChange = (searchTypeResource) => {
setSearchType(searchTypeResource.key);
onSearchTypeChange && onSearchTypeChange(searchTypeResource);
};
// Filters query list items based on the given search type.
const filteredItems = filterItemsByResourceType(items, searchType);
return (
<Overlay
hasBackdrop={true}
isOpen={isOpen}
className={classNames(CLASSES.UNIVERSAL_SEARCH_OVERLAY)}
{...overlayProps}
>
<UniversalSearchProvider
isLoading={isLoading}
searchType={searchType}
defaultSearchResource={defaultSearchResource}
searchTypeOptions={searchTypeOptions}
>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH)}>
<UniversalSearchQueryList
isOpen={isOpen}
isLoading={isLoading}
searchType={searchType}
onSearchTypeChange={handleSearchTypeChange}
{...queryListProps}
items={filteredItems}
/>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_FOOTER)}>
<UniversalQuerySearchActions />
</div>
</div>
</UniversalSearchProvider>
</Overlay>
);
}

View File

@@ -0,0 +1,29 @@
import React, { createContext } from 'react';
const UniversalSearchContext = createContext();
/**
* Universal search data provider.
*/
function UniversalSearchProvider({
isLoading,
defaultSearchResource,
searchType,
searchTypeOptions,
...props
}) {
// Provider payload.
const provider = {
isLoading,
searchType,
defaultSearchResource,
searchTypeOptions,
};
return <UniversalSearchContext.Provider value={provider} {...props} />;
}
const useUniversalSearchContext = () =>
React.useContext(UniversalSearchContext);
export { UniversalSearchProvider, useUniversalSearchContext };

View File

@@ -0,0 +1,4 @@
export const filterItemsByResourceType = (items, type) => {
return items.filter((item) => item._type === type);
}

View File

@@ -58,7 +58,6 @@ import AccountsSuggestField from './AccountsSuggestField';
import MaterialProgressBar from './MaterialProgressBar';
import { MoneyFieldCell } from './DataTableCells';
import Card from './Card';
import UniversalSearch from './UniversalSearch';
import { ItemsMultiSelect } from './Items';
@@ -139,5 +138,4 @@ export {
MoneyFieldCell,
ItemsMultiSelect,
Card,
UniversalSearch,
};

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

View File

@@ -0,0 +1,131 @@
import { useRequestQuery } from '../../useQueryRequest';
import { RESOURCES_TYPES } from 'common/resourcesTypes';
/**
*
* @param {string} type
* @param {string} searchKeyword
* @param {*} query
* @returns
*/
export function useResourceData(type, query, props) {
const url = getResourceUrlFromType(type);
return useRequestQuery(
['UNIVERSAL_SEARCH', type, query],
{ method: 'get', url, params: query },
{
select: transformResourceData(type),
defaultData: {
items: [],
},
...props,
},
);
}
/**
* Retrieve the resource url by the given resource type.
* @param {string} type
* @returns {string}
*/
function getResourceUrlFromType(type) {
const config = {
[RESOURCES_TYPES.INVOICE]: '/sales/invoices',
[RESOURCES_TYPES.ESTIMATE]: '/sales/estimates',
[RESOURCES_TYPES.ITEM]: '/items',
[RESOURCES_TYPES.RECEIPT]: '/sales/receipts',
[RESOURCES_TYPES.BILL]: '/purchases/bills',
[RESOURCES_TYPES.PAYMENT_RECEIVE]: '/sales/payment_receives',
[RESOURCES_TYPES.PAYMENT_MADE]: '/purchases/bill_payments',
[RESOURCES_TYPES.CUSTOMER]: '/customers',
[RESOURCES_TYPES.VENDOR]: '/vendors',
[RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals',
[RESOURCES_TYPES.ACCOUNT]: '/accounts',
};
return config[type] || '';
}
/**
* Transformes invoices to resource data.
*/
const transformInvoices = (response) => ({
items: response.data.sales_invoices,
});
/**
* Transformes items to resource data.
*/
const transformItems = (response) => ({
items: response.data.items,
});
/**
* Transformes payment receives to resource data.
*/
const transformPaymentReceives = (response) => ({
items: response.data.payment_receives,
});
/**
* Transformes customers to resoruce data.
*/
const transformCustomers = (response) => ({
items: response.data.customers,
});
/**
* Transformes customers to resoruce data.
*/
const transformVendors = (response) => ({
items: response.data.vendors,
});
const transformPaymentMades = (response) => ({
items: response.data.bill_payments,
});
const transformSaleReceipts = (response) => ({
items: response.data.data,
});
const transformBills = (response) => ({
items: response.data.bills,
});
const transformManualJournals = (response) => ({
items: response.data.manual_journals,
});
const transformsEstimates = (response) => ({
items: response.data.sales_estimates,
});
const transformAccounts = (response) => ({
items: response.data.accounts,
})
/**
* Detarmines the transformer based on the given resource type.
* @param {string} type - Resource type.
*/
const transformResourceData = (type) => (response) => {
const pairs = {
[RESOURCES_TYPES.ESTIMATE]: transformsEstimates,
[RESOURCES_TYPES.INVOICE]: transformInvoices,
[RESOURCES_TYPES.RECEIPT]: transformSaleReceipts,
[RESOURCES_TYPES.ITEM]: transformItems,
[RESOURCES_TYPES.PAYMENT_RECEIVE]: transformPaymentReceives,
[RESOURCES_TYPES.PAYMENT_MADE]: transformPaymentMades,
[RESOURCES_TYPES.CUSTOMER]: transformCustomers,
[RESOURCES_TYPES.VENDOR]: transformVendors,
[RESOURCES_TYPES.BILL]: transformBills,
[RESOURCES_TYPES.MANUAL_JOURNAL]: transformManualJournals,
[RESOURCES_TYPES.ACCOUNT]: transformAccounts
};
return {
...pairs[type](response),
_type: type,
};
};

View File

@@ -0,0 +1,41 @@
import { getUniversalSearchBind } from '../../../containers/UniversalSearch/utils';
import { useResourceData } from '../GenericResource';
/**
* Transformes the resource data to search entries based on
* the given resource type.
* @param {string} type
* @param {any} resource
* @returns
*/
function transfromResourceDataToSearch(resource) {
const selectItem = getUniversalSearchBind(resource._type, 'itemSelect');
return resource.items
.map((item) => ({
...selectItem ? selectItem(item) : {},
_type: resource._type,
}));
}
/**
*
* @param {*} type
* @param {*} searchKeyword
* @returns
*/
export function useUniversalSearch(type, searchKeyword, props) {
const { data, ...restProps } = useResourceData(
type,
{
search_keyword: searchKeyword,
},
props,
);
const searchData = transfromResourceDataToSearch(data);
return {
data: searchData,
...restProps,
};
}

View File

@@ -24,3 +24,5 @@ export * from './contacts';
export * from './subscriptions';
export * from './organization';
export * from './landedCost';
export * from './UniversalSearch/UniversalSearch';
export * from './GenericResource';

View File

@@ -99,7 +99,7 @@ export function useCloseReceipt(props) {
}
const transformReceipts = (res) => ({
receipts: res.data.sale_receipts,
receipts: res.data.data,
pagination: transformPagination(res.data.pagination),
filterMeta: res.data.filter_meta,
});

View File

@@ -1202,6 +1202,7 @@
"universal_search.enter_text": "To select",
"universal_search.close_text": "To close",
"universal_seach.navigate_text": "To navigate",
"universal_search.loading": "Loading...",
"pdf_preview.dialog.title": "PDF Preview",
"pdf_preview.download.button": "Download",
"pdf_preview.preview.button": "Preview",

View File

@@ -1,6 +1,7 @@
import React, { lazy } from 'react';
import { lazy } from 'react';
import intl from 'react-intl-universal';
import { RESOURCES_TYPES } from '../common/resourcesTypes';
// const BASE_URL = '/dashboard';
@@ -12,6 +13,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('accounts_chart'),
hotkey: 'shift+a',
pageTitle: intl.get('accounts_chart'),
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
},
// Accounting.
{
@@ -24,6 +26,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_journal'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
},
{
path: `/manual-journals/:id/edit`,
@@ -34,6 +37,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_journal'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
},
{
path: `/manual-journals`,
@@ -43,6 +47,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('manual_journals'),
hotkey: 'shift+m',
pageTitle: intl.get('manual_journals'),
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
},
{
path: `/items/categories`,
@@ -51,6 +56,7 @@ export const getDashboardRoutes = () => [
),
breadcrumb: intl.get('categories'),
pageTitle: intl.get('category_list'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
},
// Items.
{
@@ -60,6 +66,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('edit_item'),
pageTitle: intl.get('edit_item'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.ITEM,
},
{
path: `/items/new?duplicate=/:id`,
@@ -67,6 +74,7 @@ export const getDashboardRoutes = () => [
loader: () => import('containers/Items/ItemFormPage'),
}),
breadcrumb: intl.get('duplicate_item'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
},
{
path: `/items/new`,
@@ -76,6 +84,7 @@ export const getDashboardRoutes = () => [
hotkey: 'ctrl+shift+w',
pageTitle: intl.get('new_item'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.ITEM,
},
{
path: `/items`,
@@ -83,6 +92,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('items'),
hotkey: 'shift+w',
pageTitle: intl.get('items_list'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
},
// Inventory adjustments.
@@ -93,6 +103,7 @@ export const getDashboardRoutes = () => [
),
breadcrumb: intl.get('inventory_adjustments'),
pageTitle: intl.get('inventory_adjustment_list'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
},
// Financial Reports.
@@ -107,6 +118,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('general_ledger'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.INVENTORY_ADJUSTMENT,
},
{
path: `/financial-reports/balance-sheet`,
@@ -323,6 +335,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_expense'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
},
{
path: `/expenses/:id/edit`,
@@ -333,6 +346,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_expense'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
},
{
path: `/expenses`,
@@ -342,6 +356,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('expenses_list'),
pageTitle: intl.get('expenses_list'),
hotkey: 'shift+x',
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
},
// Customers
@@ -354,6 +369,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('edit_customer'),
pageTitle: intl.get('edit_customer'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
},
{
path: `/customers/new`,
@@ -365,6 +381,7 @@ export const getDashboardRoutes = () => [
hotkey: 'ctrl+shift+c',
pageTitle: intl.get('new_customer'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
},
{
path: `/customers`,
@@ -374,6 +391,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('customers'),
hotkey: 'shift+c',
pageTitle: intl.get('customers_list'),
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
},
{
path: `/customers/contact_duplicate=/:id`,
@@ -384,6 +402,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('duplicate_customer'),
pageTitle: intl.get('new_customer'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
},
// Vendors
@@ -396,6 +415,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('edit_vendor'),
pageTitle: intl.get('edit_vendor'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.VENDOR,
},
{
path: `/vendors/new`,
@@ -407,6 +427,7 @@ export const getDashboardRoutes = () => [
hotkey: 'ctrl+shift+v',
pageTitle: intl.get('new_vendor'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.VENDOR,
},
{
path: `/vendors`,
@@ -416,6 +437,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('vendors'),
hotkey: 'shift+v',
pageTitle: intl.get('vendors_list'),
defaultSearchResource: RESOURCES_TYPES.VENDOR,
},
{
path: `/vendors/contact_duplicate=/:id`,
@@ -426,6 +448,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('duplicate_vendor'),
pageTitle: intl.get('new_vendor'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.VENDOR,
},
// Estimates
@@ -439,6 +462,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_estimate'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
},
{
path: `/invoices/new?from_estimate_id=/:id`,
@@ -450,6 +474,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_estimate'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.INVOICE,
},
{
path: `/estimates/new`,
@@ -462,6 +487,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_estimate'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
},
{
path: `/estimates`,
@@ -472,6 +498,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('estimates_list'),
hotkey: 'shift+e',
pageTitle: intl.get('estimates_list'),
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
},
// Invoices.
@@ -485,6 +512,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_invoice'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.INVOICE,
},
{
path: `/invoices/new`,
@@ -497,6 +525,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_invoice'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.INVOICE,
},
{
path: `/invoices`,
@@ -506,6 +535,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('invoices_list'),
hotkey: 'shift+i',
pageTitle: intl.get('invoices_list'),
defaultSearchResource: RESOURCES_TYPES.INVOICE,
},
// Sales Receipts.
@@ -519,6 +549,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_receipt'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
},
{
path: `/receipts/new`,
@@ -531,6 +562,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_receipt'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
},
{
path: `/receipts`,
@@ -540,6 +572,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('receipts_list'),
hotkey: 'shift+r',
pageTitle: intl.get('receipts_list'),
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
},
// Payment receives
@@ -555,6 +588,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_payment_receive'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
},
{
path: `/payment-receives/new`,
@@ -568,6 +602,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_payment_receive'),
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
},
{
path: `/payment-receives`,
@@ -578,6 +613,7 @@ export const getDashboardRoutes = () => [
),
breadcrumb: intl.get('payment_receives_list'),
pageTitle: intl.get('payment_receives_list'),
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
},
// Bills
@@ -591,6 +627,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_bill'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.BILL,
},
{
path: `/bills/new`,
@@ -603,6 +640,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_bill'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.BILL,
},
{
path: `/bills`,
@@ -612,6 +650,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('bills_list'),
hotkey: 'shift+b',
pageTitle: intl.get('bills_list'),
defaultSearchResource: RESOURCES_TYPES.BILL,
},
// Subscription billing.
@@ -633,6 +672,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_payment_made'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
},
{
path: `/payment-mades/new`,
@@ -646,6 +686,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_payment_made'),
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
},
{
path: `/payment-mades`,
@@ -656,6 +697,7 @@ export const getDashboardRoutes = () => [
),
breadcrumb: intl.get('payment_made_list'),
pageTitle: intl.get('payment_made_list'),
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
},
// Homepage
{

View File

@@ -444,4 +444,28 @@ export default {
],
viewBox: '0 0 24 24',
},
'universal-search': {
path: [
'M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z'
],
viewBox: '0 0 20 20',
},
"arrow-down-24": {
path: [
'M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z',
],
viewBox: '0 0 24 24',
},
"arrow-up-24": {
path: [
'M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z'
],
viewBox: '0 0 24 24',
},
"caret-right-16": {
path: [
'M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z'
],
viewBox: '0 0 16 16',
},
};

View File

@@ -20,3 +20,36 @@ export function generalSearch(name, result) {
result,
};
}
export function universalSearchSetResourceType(resourceType) {
return {
type: t.UNIVERSAL_SEARCH_SET_RESOURCE_TYPE,
payload: {
resourceType,
},
};
}
export function universalSearchResetResourceType() {
return {
type: t.UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE,
};
}
export function universalSearchSetSelectedItem(resourceType, resourceId) {
return {
type: t.UNIVERSAL_SEARCH_SET_ITEM_SELECT,
payload: {
resourceType,
resourceId
}
};
}
export function universalSearchResetSelectedItem() {
return {
type: t.UNIVERSAL_SEARCH_RESET_ITEM_SELECT,
payload: {}
};
}

View File

@@ -1,8 +1,12 @@
import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit';
const DEFAULT_RESOURCE_TYPE = 'customer';
const initialState = {
isOpen: false,
defaultResourceType: DEFAULT_RESOURCE_TYPE,
selectedItem: {},
};
export default createReducer(initialState, {
@@ -13,4 +17,23 @@ export default createReducer(initialState, {
[t.CLOSE_SEARCH]: (state, action) => {
state.isOpen = false;
},
[t.UNIVERSAL_SEARCH_SET_RESOURCE_TYPE]: (state, action) => {
state.defaultResourceType = action.payload.resourceType;
},
[t.UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE]: (state, action) => {
state.defaultResourceType = DEFAULT_RESOURCE_TYPE;
},
[t.UNIVERSAL_SEARCH_SET_ITEM_SELECT]: (state, action) => {
state.selectedItem = {
resourceId: action.payload.resourceId,
resourceType: action.payload.resourceType,
};
},
[t.UNIVERSAL_SEARCH_RESET_ITEM_SELECT]: (state, action) => {
state.selectedItem = {};
},
});

View File

@@ -2,4 +2,8 @@ export default {
SEARCH_TYPE: 'SEARCH_TYPE',
OPEN_SEARCH: 'OPEN_SEARCH',
CLOSE_SEARCH: 'CLOSE_SEARCH',
UNIVERSAL_SEARCH_SET_RESOURCE_TYPE: 'UNIVERSAL_SEARCH_SET_RESOURCE_TYPE',
UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE: 'UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE',
UNIVERSAL_SEARCH_SET_ITEM_SELECT: 'UNIVERSAL_SEARCH_SET_ITEM_SELECT',
UNIVERSAL_SEARCH_RESET_ITEM_SELECT: 'UNIVERSAL_SEARCH_RESET_ITEM_SELECT'
};

View File

@@ -226,16 +226,16 @@ html[lang^="ar"] {
z-index: 9999999;
margin: 6px;
.bp3-button + .bp3-button{
margin-left: 8px;
}
.bp3-button{
border-color: rgba(0, 0, 0, 0.25);
color: rgb(25, 32, 37);
min-height: 30px;
padding-left: 14px;
padding-right: 14px;
& + .bp3-button{
margin-left: 8px;
}
}
}

View File

@@ -0,0 +1,219 @@
.universal-search {
position: fixed;
filter: blur(0);
opacity: 1;
background-color: #fff;
border-radius: 3px;
box-shadow:
0 0 0 1px rgba(16, 22, 26, .1),
0 4px 8px rgba(16, 22, 26, .2),
0 18px 46px 6px rgba(16, 22, 26, .2);
left: calc(50% - 250px);
top: 20vh;
width: 500px;
z-index: 20;
&.bp3-overlay-appear,
&.bp3-overlay-enter {
filter: blur(20px);
opacity: .2
}
&.bp3-overlay-appear-active,
&.bp3-overlay-enter-active {
filter: blur(0);
opacity: 1;
transition-delay: 0;
transition-duration: .2s;
transition-property: filter, opacity;
transition-timing-function: cubic-bezier(.4, 1, .75, .9)
}
&.bp3-overlay-exit {
filter: blur(0);
opacity: 1
}
&.bp3-overlay-exit-active {
filter: blur(20px);
opacity: .2;
transition-delay: 0;
transition-duration: .2s;
transition-property: filter, opacity;
transition-timing-function: cubic-bezier(.4, 1, .75, .9)
}
&__omnibar {
.bp3-input-group {
.bp3-icon {
svg {
stroke: currentColor;
fill: none;
fill-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
}
}
}
.bp3-input-group .bp3-input {
border: 0;
box-shadow: 0 0 0 0;
height: 50px;
line-height: 50px;
font-size: 20px;
}
.bp3-input-group.bp3-large .bp3-input:not(:first-child) {
padding-left: 50px;
}
.bp3-input {}
.bp3-input-group {
.bp3-icon {
margin: 16px;
color: #5c707f;
svg {
stroke-width: 2;
--text-opacity: 1;
}
}
}
.bp3-menu {
border-top: 1px solid #d3dce2;
.bp3-menu-item {
.bp3-text-muted {
font-size: 12px;
.bp3-icon {
color: #8499a7;
}
}
&.bp3-intent-primary{
&.bp3-active{
background-color: rgb(235, 241, 246);
color: #252b30;
.bp3-menu-item-label{
color: #5c7080;
}
}
}
&-label {
flex-direction: row;
}
}
}
.bp3-input-action {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
}
&__type-select-overlay {
.bp3-button {
margin: 0 !important;
}
}
&__footer {
padding: 12px 12px;
border-top: 1px solid #d3dce2;
}
&__actions {
display: flex;
}
&__action {
&:not(:first-of-type) {
margin-left: 14px;
}
.bp3-tag {
background: #708392;
}
&--arrows {
.bp3-tag {
padding: 0;
text-align: center;
line-height: 16px;
margin-left: 4px;
svg {
fill: #fff;
height: 100%;
display: block;
width: 100%;
padding: 2px;
}
}
}
.text {
margin-left: 6px;
}
}
&__footer {}
&-input-right-elements {
display: flex;
margin: 10px;
.bp3-spinner {
margin-right: 6px;
}
}
&__item {
&--invoice,
&--estimate,
&--bill,
&--receipt {
.amount {
color: #252b30;
}
.status {
font-size: 13px;
&.status-warning {
color: rgb(236, 91, 10);
}
&.status-success {
color: #249017;
}
}
}
}
}
.universal-search-overlay .bp3-overlay-backdrop {
background: rgba(0, 10, 30, 0.3);
}

View File

@@ -1,3 +1,4 @@
import React from 'react';
import moment from 'moment';
import _ from 'lodash';
import * as R from 'ramda';
@@ -751,4 +752,39 @@ export const RESORUCE_TYPE = {
ACCOUNTS: 'account',
ITEMS: 'items',
}
function escapeRegExpChars(text) {
return text.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
}
export function highlightText(text, query) {
let lastIndex = 0;
const words = query
.split(/\s+/)
.filter((word) => word.length > 0)
.map(escapeRegExpChars);
if (words.length === 0) {
return [text];
}
const regexp = new RegExp(words.join('|'), 'gi');
const tokens = [];
while (true) {
const match = regexp.exec(text);
if (!match) {
break;
}
const length = match[0].length;
const before = text.slice(lastIndex, regexp.lastIndex - length);
if (before.length > 0) {
tokens.push(before);
}
lastIndex = regexp.lastIndex;
tokens.push(<strong key={lastIndex}>{match[0]}</strong>);
}
const rest = text.slice(lastIndex);
if (rest.length > 0) {
tokens.push(rest);
}
return tokens;
}

View File

@@ -19,14 +19,16 @@ export default class VendorsController extends ContactsController {
router() {
const router = Router();
router.post('/', [
...this.contactDTOSchema,
...this.contactNewDTOSchema,
...this.vendorDTOSchema,
],
router.post(
'/',
[
...this.contactDTOSchema,
...this.contactNewDTOSchema,
...this.vendorDTOSchema,
],
this.validationResult,
asyncMiddleware(this.newVendor.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.post(
'/:id/opening_balance',
@@ -37,36 +39,38 @@ export default class VendorsController extends ContactsController {
],
this.validationResult,
asyncMiddleware(this.editOpeningBalanceVendor.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.post('/:id', [
...this.contactDTOSchema,
...this.contactEditDTOSchema,
...this.vendorDTOSchema,
],
router.post(
'/:id',
[
...this.contactDTOSchema,
...this.contactEditDTOSchema,
...this.vendorDTOSchema,
],
this.validationResult,
asyncMiddleware(this.editVendor.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.delete('/:id', [
...this.specificContactSchema,
],
router.delete(
'/:id',
[...this.specificContactSchema],
this.validationResult,
asyncMiddleware(this.deleteVendor.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.get('/:id', [
...this.specificContactSchema,
],
router.get(
'/:id',
[...this.specificContactSchema],
this.validationResult,
asyncMiddleware(this.getVendor.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.get('/', [
...this.vendorsListSchema,
],
router.get(
'/',
[...this.vendorsListSchema],
this.validationResult,
asyncMiddleware(this.getVendorsList.bind(this)),
asyncMiddleware(this.getVendorsList.bind(this))
);
return router;
}
@@ -102,22 +106,26 @@ export default class VendorsController extends ContactsController {
query('page_size').optional().isNumeric().toInt(),
query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim()
query('search_keyword').optional({ nullable: true }).isString().trim(),
];
}
/**
* Creates a new vendor.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async newVendor(req: Request, res: Response, next: NextFunction) {
const contactDTO: IVendorNewDTO = this.matchedBodyData(req);
const { tenantId, user } = req;
try {
const vendor = await this.vendorsService.newVendor(tenantId, contactDTO, user);
const vendor = await this.vendorsService.newVendor(
tenantId,
contactDTO,
user
);
return res.status(200).send({
id: vendor.id,
@@ -130,9 +138,9 @@ export default class VendorsController extends ContactsController {
/**
* Edits the given vendor details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editVendor(req: Request, res: Response, next: NextFunction) {
const contactDTO: IVendorEditDTO = this.matchedBodyData(req);
@@ -140,7 +148,12 @@ export default class VendorsController extends ContactsController {
const { id: contactId } = req.params;
try {
await this.vendorsService.editVendor(tenantId, contactId, contactDTO, user);
await this.vendorsService.editVendor(
tenantId,
contactId,
contactDTO,
user
);
return res.status(200).send({
id: contactId,
@@ -157,24 +170,26 @@ export default class VendorsController extends ContactsController {
* @param {Response} res -
* @param {NextFunction} next -
*/
async editOpeningBalanceVendor(req: Request, res: Response, next: NextFunction) {
async editOpeningBalanceVendor(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req;
const { id: vendorId } = req.params;
const {
openingBalance,
openingBalanceAt,
} = this.matchedBodyData(req);
const { openingBalance, openingBalanceAt } = this.matchedBodyData(req);
try {
await this.vendorsService.changeOpeningBalance(
tenantId,
vendorId,
openingBalance,
openingBalanceAt,
openingBalanceAt
);
return res.status(200).send({
id: vendorId,
message: 'The opening balance of the given vendor has been changed successfully.',
message:
'The opening balance of the given vendor has been changed successfully.',
});
} catch (error) {
next(error);
@@ -183,16 +198,16 @@ export default class VendorsController extends ContactsController {
/**
* Deletes the given vendor from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteVendor(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: contactId } = req.params;
try {
await this.vendorsService.deleteVendor(tenantId, contactId, user)
await this.vendorsService.deleteVendor(tenantId, contactId, user);
return res.status(200).send({
id: contactId,
@@ -205,18 +220,21 @@ export default class VendorsController extends ContactsController {
/**
* Retrieve details of the given vendor id.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getVendor(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: vendorId } = req.params;
try {
const vendor = await this.vendorsService.getVendor(tenantId, vendorId, user)
return res.status(200).send({ vendor });
const vendor = await this.vendorsService.getVendor(
tenantId,
vendorId,
user
);
return res.status(200).send(this.transfromToResponse({ vendor }));
} catch (error) {
next(error);
}
@@ -224,9 +242,9 @@ export default class VendorsController extends ContactsController {
/**
* Retrieve vendors datatable list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getVendorsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -241,14 +259,11 @@ export default class VendorsController extends ContactsController {
};
try {
const {
vendors,
pagination,
filterMeta,
} = await this.vendorsService.getVendorsList(tenantId, vendorsFilter);
const { vendors, pagination, filterMeta } =
await this.vendorsService.getVendorsList(tenantId, vendorsFilter);
return res.status(200).send({
vendors,
vendors: this.transfromToResponse(vendors),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
@@ -259,7 +274,7 @@ export default class VendorsController extends ContactsController {
/**
* Handle service errors.
* @param {Error} error -
* @param {Error} error -
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
@@ -289,4 +304,4 @@ export default class VendorsController extends ContactsController {
}
next(error);
}
}
}

View File

@@ -8,6 +8,7 @@ import { IExpenseDTO } from 'interfaces';
import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from 'data/DataTypes';
import HasItemEntries from 'services/Sales/HasItemsEntries';
@Service()
export default class ExpensesController extends BaseController {
@@ -304,7 +305,7 @@ export default class ExpensesController extends BaseController {
await this.expensesService.getExpensesList(tenantId, filter);
return res.status(200).send({
expenses,
expenses: this.transfromToResponse(expenses),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
@@ -328,7 +329,7 @@ export default class ExpensesController extends BaseController {
tenantId,
expenseId
);
return res.status(200).send({ expense });
return res.status(200).send(this.transfromToResponse({ expense }));
} catch (error) {
next(error);
}

View File

@@ -173,7 +173,7 @@ export default class ItemsController extends BaseController {
}
/**
* Validate list query schema
* Validate list query schema.
*/
get validateListQuerySchema() {
return [

View File

@@ -221,7 +221,7 @@ export default class ManualJournalsController extends BaseController {
manualJournalId
);
return res.status(200).send({
manual_journal: manualJournal,
manual_journal: this.transfromToResponse(manualJournal),
});
} catch (error) {
next(error);
@@ -301,7 +301,7 @@ export default class ManualJournalsController extends BaseController {
} = await this.manualJournalsService.getManualJournals(tenantId, filter);
return res.status(200).send({
manual_journals: manualJournals,
manual_journals: this.transfromToResponse(manualJournals),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});

View File

@@ -264,7 +264,7 @@ export default class BillsController extends BaseController {
try {
const bill = await this.billsService.getBill(tenantId, billId);
return res.status(200).send({ bill });
return res.status(200).send(this.transfromToResponse({ bill }));
} catch (error) {
next(error);
}
@@ -312,7 +312,7 @@ export default class BillsController extends BaseController {
await this.billsService.getBills(tenantId, filter);
return res.status(200).send({
bills,
bills: this.transfromToResponse(bills),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});

View File

@@ -335,7 +335,7 @@ export default class BillsPayments extends BaseController {
);
return res.status(200).send({
bill_payments: billPayments,
bill_payments: this.transfromToResponse(billPayments),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});

View File

@@ -302,6 +302,10 @@ export default class SalesEstimatesController extends BaseController {
);
// Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ estimate }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleEstimatesPdf.saleEstimatePdf(
@@ -314,10 +318,6 @@ export default class SalesEstimatesController extends BaseController {
});
res.send(pdfContent);
},
// JSON content type.
default: () => {
return res.status(200).send(this.transfromToResponse({ estimate }));
},
});
} catch (error) {
next(error);

View File

@@ -277,6 +277,10 @@ export default class SaleInvoicesController extends BaseController {
);
// Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ saleInvoice }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoicePdf.saleInvoicePdf(
@@ -289,10 +293,6 @@ export default class SaleInvoicesController extends BaseController {
});
res.send(pdfContent);
},
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ saleInvoice }));
},
});
} catch (error) {
next(error);

View File

@@ -243,11 +243,13 @@ export default class SalesReceiptsController extends BaseController {
};
try {
const { salesReceipts, pagination, filterMeta } =
const { data, pagination, filterMeta } =
await this.saleReceiptService.salesReceiptsList(tenantId, filter);
const response = this.transfromToResponse({
salesReceipts, pagination, filterMeta
data,
pagination,
filterMeta,
});
return res.status(200).send(response);
} catch (error) {
@@ -272,6 +274,11 @@ export default class SalesReceiptsController extends BaseController {
);
res.format({
'application/json': () => {
return res
.status(200)
.send(this.transfromToResponse({ saleReceipt }));
},
'application/pdf': async () => {
const pdfContent = await this.saleReceiptsPdf.saleReceiptPdf(
tenantId,
@@ -283,10 +290,7 @@ export default class SalesReceiptsController extends BaseController {
});
res.send(pdfContent);
},
'application/json': () => {
return res.status(200).send(this.transfromToResponse({ saleReceipt }));
}
})
});
} catch (error) {
next(error);
}

View File

@@ -0,0 +1,28 @@
import { Service } from 'typedi';
import { IAccount } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
@Service()
export default class AccountTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedAmount',
];
};
/**
* Retrieve formatted account amount.
* @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: IAccount): string => {
return formatNumber(account.amount, {
currencyCode: account.currencyCode,
});
};
}

View File

@@ -23,6 +23,7 @@ import AccountTypesUtils from 'lib/AccountTypes';
import { ERRORS } from './constants';
import { flatToNestedArray } from 'utils';
import I18nService from 'services/I18n/I18nService';
import AccountTransformer from './AccountTransform';
@Service()
export default class AccountsService {
@@ -41,6 +42,9 @@ export default class AccountsService {
@Inject()
i18nService: I18nService;
@Inject()
accountTransformer: AccountTransformer;
/**
* Retrieve account type or throws service error.
* @param {number} tenantId -
@@ -326,7 +330,10 @@ export default class AccountsService {
*/
public async getAccount(tenantId: number, accountId: number) {
const account = await this.getAccountOrThrowError(tenantId, accountId);
return this.transformAccountResponse(tenantId, account);
return this.accountTransformer.transform(
this.transformAccountResponse(tenantId, account)
);
}
/**
@@ -609,13 +616,11 @@ export default class AccountsService {
/**
* Parsees accounts list filter DTO.
* @param filterDTO
* @returns
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
@@ -735,10 +740,12 @@ export default class AccountsService {
key: 'base_currency',
});
const _accounts = accounts.map((account) => ({
...account.toJSON(),
currencyCode: baseCurrency,
}));
const _accounts = this.accountTransformer.transform(
accounts.map((account) => ({
...account.toJSON(),
currencyCode: baseCurrency,
}))
);
return flatToNestedArray(
this.i18nService.i18nMapper(_accounts, ['account_type_label'], tenantId),
{

View File

@@ -0,0 +1,43 @@
import { Service, Container } from 'typedi';
import { isNull } from 'lodash';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
import { IContact } from 'interfaces';
@Service()
export default class ContactTransfromer extends Transformer {
/**
* Retrieve formatted expense amount.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedBalance = (contact: IContact): string => {
return formatNumber(contact.balance, {
currencyCode: contact.currencyCode,
});
};
/**
* Retrieve formatted expense landed cost amount.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedOpeningBalance = (contact: IContact): string => {
return !isNull(contact.openingBalance)
? formatNumber(contact.openingBalance, {
currencyCode: contact.currencyCode,
})
: '';
};
/**
* Retriecve fromatted date.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedOpeningBalanceAt = (contact: IContact): string => {
return !isNull(contact.openingBalanceAt)
? this.formatDate(contact.openingBalanceAt)
: '';
};
}

View File

@@ -0,0 +1,17 @@
import { Service } from 'typedi';
import ContactTransfromer from '../ContactTransformer';
@Service()
export default class CustomerTransfromer extends ContactTransfromer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedBalance',
'formattedOpeningBalance',
'formattedOpeningBalanceAt'
];
};
}

View File

@@ -28,6 +28,7 @@ import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
import CustomerTransfromer from './Customers/CustomerTransformer';
const ERRORS = {
CUSTOMER_HAS_TRANSACTIONS: 'CUSTOMER_HAS_TRANSACTIONS',
@@ -61,6 +62,9 @@ export default class CustomersService {
@Inject('SalesEstimates')
estimatesService: ISalesEstimatesService;
@Inject()
customerTransformer: CustomerTransfromer;
/**
* Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
@@ -262,7 +266,10 @@ export default class CustomersService {
customerId,
'customer'
);
return this.transformContactToCustomer(contact);
return R.compose(
this.customerTransformer.transform,
this.transformContactToCustomer,
)(contact);
}
/**

View File

@@ -0,0 +1,17 @@
import { Service } from 'typedi';
import ContactTransfromer from '../ContactTransformer';
@Service()
export default class VendorTransfromer extends ContactTransfromer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedBalance',
'formattedOpeningBalance',
'formattedOpeningBalanceAt'
];
};
}

View File

@@ -23,6 +23,7 @@ import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
import events from 'subscribers/events';
import VendorTransfromer from './Vendors/VendorTransformer';
const ERRORS = {
VENDOR_HAS_TRANSACTIONS: 'VENDOR_HAS_TRANSACTIONS',
@@ -51,6 +52,9 @@ export default class VendorsService {
@Inject('BillPayments')
billPaymentsService: IBillPaymentsService;
@Inject()
vendorTransformer: VendorTransfromer;
/**
* Converts vendor to contact DTO.
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
@@ -139,8 +143,8 @@ export default class VendorsService {
/**
* Validate the given vendor has no associated transactions.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} tenantId
* @param {number} vendorId
*/
private async validateAssociatedTransactions(
tenantId: number,
@@ -151,8 +155,10 @@ export default class VendorsService {
await this.billsService.validateVendorHasNoBills(tenantId, vendorId);
// Validate vendor has no paymentys.
await this.billPaymentsService.validateVendorHasNoPayments(tenantId, vendorId);
await this.billPaymentsService.validateVendorHasNoPayments(
tenantId,
vendorId
);
} catch (error) {
throw new ServiceError(ERRORS.VENDOR_HAS_TRANSACTIONS);
}
@@ -196,7 +202,9 @@ export default class VendorsService {
* @param {number} vendorId
*/
public async getVendor(tenantId: number, vendorId: number) {
return this.contactService.getContact(tenantId, vendorId, 'vendor');
const vendor = this.contactService.getContact(tenantId, vendorId, 'vendor');
return this.vendorTransformer.transform(vendor);
}
/**
@@ -257,9 +265,7 @@ export default class VendorsService {
}
private parseVendorsListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
@@ -297,7 +303,7 @@ export default class VendorsService {
.pagination(filter.page - 1, filter.pageSize);
return {
vendors: results,
vendors: this.vendorTransformer.transform(results),
pagination,
filterMeta: dynamicList.getResponseMeta(),
};

View File

@@ -0,0 +1,62 @@
import { Service, Container } from 'typedi';
import { Transformer } from 'lib/Transformer/Transformer';
import { formatNumber } from 'utils';
import { IExpense } from 'interfaces';
@Service()
export default class ExpenseTransfromer extends Transformer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedLandedCostAmount',
'formattedAllocatedCostAmount',
'formattedDate'
];
};
/**
* Retrieve formatted expense amount.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedAmount = (expense: IExpense): string => {
return formatNumber(expense.totalAmount, {
currencyCode: expense.currencyCode,
});
};
/**
* Retrieve formatted expense landed cost amount.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedLandedCostAmount = (expense: IExpense): string => {
return formatNumber(expense.landedCostAmount, {
currencyCode: expense.currencyCode,
});
};
/**
* Retrieve formatted allocated cost amount.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedAllocatedCostAmount = (expense: IExpense): string => {
return formatNumber(expense.allocatedCostAmount, {
currencyCode: expense.currencyCode,
});
};
/**
* Retriecve fromatted date.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedDate = (expense: IExpense): string => {
return this.formatDate(expense.paymentDate);
}
}

View File

@@ -25,6 +25,7 @@ import events from 'subscribers/events';
import ContactsService from 'services/Contacts/ContactsService';
import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes';
import EntriesService from 'services/Entries';
import ExpenseTransfromer from './ExpenseTransformer';
const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found',
@@ -58,6 +59,9 @@ export default class ExpensesService implements IExpensesService {
@Inject()
entriesService: EntriesService;
@Inject()
expenseTransfromer: ExpenseTransfromer;
/**
* Retrieve the payment account details or returns not found server error in case the
* given account not found on the storage.
@@ -681,7 +685,7 @@ export default class ExpensesService implements IExpensesService {
.pagination(filter.page - 1, filter.pageSize);
return {
expenses: results,
expenses: this.expenseTransfromer.transform(results),
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
@@ -706,7 +710,7 @@ export default class ExpensesService implements IExpensesService {
if (!expense) {
throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
}
return expense;
return this.expenseTransfromer.transform(expense);
}
/**

View File

@@ -0,0 +1,44 @@
import { IManualJournal } from 'interfaces';
import { Transformer } from 'lib/Transformer/Transformer';
import { Service } from 'typedi';
import { formatNumber } from 'utils';
@Service()
export default class ManualJournalTransfromer extends Transformer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return ['formattedAmount', 'formattedDate', 'formattedPublishedAt'];
};
/**
* Retrieve formatted journal amount.
* @param {IManualJournal} manualJournal
* @returns {string}
*/
protected formattedAmount = (manualJorunal: IManualJournal): string => {
return formatNumber(manualJorunal.amount, {
currencyCode: manualJorunal.currencyCode,
});
};
/**
* Retrieve formatted date.
* @param {IManualJournal} manualJournal
* @returns {string}
*/
protected formattedDate = (manualJorunal: IManualJournal): string => {
return this.formatDate(manualJorunal.date);
};
/**
* Retrieve formatted published at date.
* @param {IManualJournal} manualJournal
* @returns {string}
*/
protected formattedPublishedAt = (manualJorunal: IManualJournal): string => {
return this.formatDate(manualJorunal.publishedAt);
};
}

View File

@@ -24,6 +24,7 @@ import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService';
import AutoIncrementOrdersService from 'services/Sales/AutoIncrementOrdersService';
import { ERRORS } from './constants';
import ManualJournalTransfromer from './ManualJournalTransformer';
@Service()
export default class ManualJournalsService implements IManualJournalsService {
@@ -45,6 +46,9 @@ export default class ManualJournalsService implements IManualJournalsService {
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
manualJournalTransformer: ManualJournalTransfromer;
/**
* Validates the manual journal existance.
* @param {number} tenantId
@@ -815,7 +819,7 @@ export default class ManualJournalsService implements IManualJournalsService {
.pagination(filter.page - 1, filter.pageSize);
return {
manualJournals: results,
manualJournals: this.manualJournalTransformer.transform(results),
pagination,
filterMeta: dynamicService.getResponseMeta(),
};
@@ -842,7 +846,7 @@ export default class ManualJournalsService implements IManualJournalsService {
.withGraphFetched('transactions')
.withGraphFetched('media');
return manualJournal;
return this.manualJournalTransformer.transform(manualJournal);
}
/**

View File

@@ -10,10 +10,7 @@ export default class PaymentReceiveTransfromer extends Transformer {
* @returns {Array}
*/
protected includeAttributes = (): string[] => {
return [
'formattedPaymentDate',
'formattedAmount',
];
return ['formattedPaymentDate', 'formattedAmount'];
};
/**

View File

@@ -421,7 +421,7 @@ export default class SalesReceiptService implements ISalesReceiptsService {
tenantId: number,
filterDTO: ISaleReceiptFilter
): Promise<{
salesReceipts: ISaleReceipt[];
data: ISaleReceipt[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
@@ -451,7 +451,7 @@ export default class SalesReceiptService implements ISalesReceiptsService {
.pagination(filter.page - 1, filter.pageSize);
return {
salesReceipts: this.saleReceiptTransformer.transform(results),
data: this.saleReceiptTransformer.transform(results),
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};