re-structure to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 01:02:31 +02:00
parent 8242ec64ba
commit 7a0a13f9d5
10400 changed files with 46966 additions and 17223 deletions

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
import * as Yup from 'yup';
const Schema = Yup.object().shape({
accounting_basis: Yup.string().required(),
account_code_required: Yup.boolean().nullable(),
account_code_unique: Yup.boolean().nullable(),
withdrawal_account: Yup.number().nullable(),
preferred_deposit_account: Yup.number().nullable(),
preferred_advance_deposit: Yup.number().nullable(),
});
export const AccountantSchema = Schema;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import React from 'react';
import AccountantFormPage from './AccountantFormPage';
import { AccountantFormProvider } from './AccountantFormProvider';
/**
* Accountant preferences.
*/
export default function AccountantPreferences() {
return (
<AccountantFormProvider>
<AccountantFormPage />
</AccountantFormProvider>
);
}

View File

@@ -0,0 +1,244 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Form, FastField, useFormikContext } from 'formik';
import {
FormGroup,
RadioGroup,
Radio,
Checkbox,
Button,
Intent,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
FormattedMessage as T,
AccountsSelectList,
FieldRequiredHint,
CardFooterActions,
} from '@/components';
import { handleStringChange, inputIntent } from '@/utils';
import { ACCOUNT_TYPE } from '@/constants/accountTypes';
import { useAccountantFormContext } from './AccountantFormProvider';
/**
* Accountant form.
*/
export default function AccountantForm() {
const history = useHistory();
const { isSubmitting } = useFormikContext();
const handleCloseClick = () => {
history.go(-1);
};
const { accounts } = useAccountantFormContext();
return (
<Form>
{/* ----------- Accounts ----------- */}
<FormGroup
label={
<strong>
<T id={'accounts'} />
</strong>
}
className={'accounts-checkbox'}
>
{/*------------ account code required -----------*/}
<FastField name={'account_code_required'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true}>
<Checkbox
inline={true}
label={
<T
id={'make_account_code_required_when_create_a_new_accounts'}
/>
}
name={'account_code_required'}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------ account code unique -----------*/}
<FastField name={'account_code_unique'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true}>
<Checkbox
inline={true}
label={
<T
id={
'should_account_code_be_unique_when_create_a_new_account'
}
/>
}
name={'account_code_unique'}
{...field}
/>
</FormGroup>
)}
</FastField>
</FormGroup>
{/* ----------- Accounting basis ----------- */}
<FastField name={'accounting_basis'}>
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
label={
<strong>
<T id={'accounting_basis_'} />
</strong>
}
>
<RadioGroup
inline={true}
selectedValue={value}
onChange={handleStringChange((_value) => {
setFieldValue('accounting_basis', _value);
})}
>
<Radio label={intl.get('cash')} value="cash" />
<Radio label={intl.get('accrual')} value="accrual" />
</RadioGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Deposit customer account ----------- */}
<FastField name={'preferred_deposit_account'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={
<strong>
<T id={'deposit_customer_account'} />
</strong>
}
helperText={
<T
id={
'select_a_preferred_account_to_deposit_into_it_after_customer_make_payment'
}
/>
}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={({ id }) => {
setFieldValue('preferred_deposit_account', id);
}}
selectedAccountId={value}
defaultSelectText={<T id={'select_payment_account'} />}
filterByTypes={[
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Withdrawal vendor account ----------- */}
<FastField name={'withdrawal_account'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={
<strong>
<T id={'withdrawal_vendor_account'} />
</strong>
}
helperText={
<T
id={
'select_a_preferred_account_to_deposit_into_it_after_customer_make_payment'
}
/>
}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={({ id }) => {
setFieldValue('withdrawal_account', id);
}}
selectedAccountId={value}
defaultSelectText={<T id={'select_payment_account'} />}
filterByTypes={[
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Withdrawal customer account ----------- */}
<FastField name={'preferred_advance_deposit'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={
<strong>
<T id={'customer_advance_deposit'} />
</strong>
}
helperText={
<T
id={
'select_a_preferred_account_to_deposit_into_it_vendor_advanced_deposits'
}
/>
}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={({ id }) => {
setFieldValue('preferred_advance_deposit', id);
}}
selectedAccountId={value}
defaultSelectText={<T id={'select_payment_account'} />}
// filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]}
/>
</FormGroup>
)}
</FastField>
<CardFooterActions>
<Button intent={Intent.PRIMARY} loading={isSubmitting} type="submit">
<T id={'save'} />
</Button>
<Button disabled={isSubmitting} onClick={handleCloseClick}>
<T id={'close'} />
</Button>
</CardFooterActions>
</Form>
);
}

View File

@@ -0,0 +1,92 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { pick } from 'lodash';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import withSettings from '@/containers/Settings/withSettings';
import AccountantForm from './AccountantForm';
import { AccountantSchema } from './Accountant.schema';
import { useAccountantFormContext } from './AccountantFormProvider';
import { transformToOptions } from './utils';
import { compose, transformGeneralSettings } from '@/utils';
import '@/style/pages/Preferences/Accounting.scss';
// Accountant preferences.
function AccountantFormPage({
//# withDashboardActions
changePreferencesPageTitle,
// #withSettings
organizationSettings,
paymentReceiveSettings,
accountsSettings,
billPaymentSettings,
}) {
const { saveSettingMutate } = useAccountantFormContext();
const accountantSettings = {
...billPaymentSettings,
...accountsSettings,
...pick(organizationSettings, ['accountingBasis']),
...pick(paymentReceiveSettings, ['preferredDepositAccount', 'preferredAdvanceDeposit']),
};
const initialValues = {
...transformGeneralSettings(accountantSettings),
};
useEffect(() => {
changePreferencesPageTitle(intl.get('accountant'));
}, [changePreferencesPageTitle]);
const handleFormSubmit = (values, { setSubmitting }) => {
const options = transformToOptions(values);
setSubmitting(true);
const onSuccess = () => {
AppToaster.show({
message: intl.get('the_accountant_preferences_has_been_saved'),
intent: Intent.SUCCESS,
});
setSubmitting(false);
};
const onError = (errors) => {
setSubmitting(false);
};
saveSettingMutate({ options }).then(onSuccess).catch(onError);
};
return (
<Formik
initialValues={initialValues}
validationSchema={AccountantSchema}
onSubmit={handleFormSubmit}
component={AccountantForm}
/>
);
}
export default compose(
withSettings(
({
organizationSettings,
paymentReceiveSettings,
accountsSettings,
billPaymentSettings,
}) => ({
organizationSettings,
paymentReceiveSettings,
accountsSettings,
billPaymentSettings,
}),
),
withDashboardActions,
)(AccountantFormPage);

View File

@@ -0,0 +1,59 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import styled from 'styled-components';
import { Card } from '@/components';
import { CLASSES } from '@/constants/classes';
import { useAccounts, useSaveSettings, useSettings } from '@/hooks/query';
import PreferencesPageLoader from '../PreferencesPageLoader';
const AccountantFormContext = React.createContext();
/**
* Accountant data provider.
*/
function AccountantFormProvider({ ...props }) {
// Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts();
//Fetches Organization Settings.
const { isLoading: isSettingsLoading } = useSettings();
// Save Organization Settings.
const { mutateAsync: saveSettingMutate } = useSaveSettings();
// Provider state.
const provider = {
accounts,
isAccountsLoading,
saveSettingMutate,
};
const isLoading = isSettingsLoading || isAccountsLoading;
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT,
)}
>
<AccountantFormCard>
{isLoading ? (
<PreferencesPageLoader />
) : (
<AccountantFormContext.Provider value={provider} {...props} />
)}
</AccountantFormCard>
</div>
);
}
const useAccountantFormContext = () => React.useContext(AccountantFormContext);
export { AccountantFormProvider, useAccountantFormContext };
const AccountantFormCard = styled(Card)`
padding: 25px;
`;

View File

@@ -0,0 +1,38 @@
// @ts-nocheck
export const transformToOptions = (option) => {
return [
{
key: 'accounting_basis',
value: option.accounting_basis,
group: 'organization',
},
{
key: 'withdrawal_account',
value: option.withdrawal_account,
group: 'bill_payments',
},
{
key: 'preferred_deposit_account',
value: option.preferred_deposit_account,
group: 'payment_receives',
},
{
key: 'preferred_advance_deposit',
value: option.preferred_advance_deposit,
group: 'payment_receives',
},
{
key: 'account_code_required',
value: option.account_code_required,
group: 'accounts',
},
{
key: 'account_code_unique',
value: option.account_code_unique,
group: 'accounts',
},
];
};

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import BranchesDataTable from './BranchesDataTable';
import BranchesEmptyStatus from './BranchesEmptyStatus';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { useBranchesContext } from './BranchesProvider';
import { compose } from '@/utils';
function Branches({
// #withDashboardActions
changePreferencesPageTitle,
}) {
const { isEmptyStatus } = useBranchesContext();
React.useEffect(() => {
changePreferencesPageTitle(intl.get('branches.label'));
}, [changePreferencesPageTitle]);
return (
<React.Fragment>
{isEmptyStatus ? <BranchesEmptyStatus /> : <BranchesDataTable />}
</React.Fragment>
);
}
export default compose(withDashboardActions)(Branches);

View File

@@ -0,0 +1,33 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { Features } from '@/constants';
import { FeatureCan, FormattedMessage as T, Icon } from '@/components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function BranchesActions({
//#ownProps
openDialog,
}) {
const handleClickNewBranche = () => {
openDialog('branch-form');
};
return (
<React.Fragment>
<FeatureCan feature={Features.Branches}>
<Button
icon={<Icon icon="plus" iconSize={12} />}
onClick={handleClickNewBranche}
intent={Intent.PRIMARY}
>
<T id={'branches.label.new_branch'} />
</Button>
</FeatureCan>
</React.Fragment>
);
}
export default compose(withDialogActions)(BranchesActions);

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
import React from 'react';
const BranchDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Branches/BranchDeleteAlert'),
);
export default [{ name: 'branch-delete', component: BranchDeleteAlert }];

View File

@@ -0,0 +1,98 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent } from '@blueprintjs/core';
import '@/style/pages/Preferences/branchesList.scss';
import { DataTable, Card, AppToaster, TableSkeletonRows } from '@/components';
import { useBranchesTableColumns, ActionsMenu } from './components';
import { useBranchesContext } from './BranchesProvider';
import { useMarkBranchAsPrimary } from '@/hooks/query';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { compose } from '@/utils';
/**
* Branches data table.
*/
function BranchesDataTable({
// #withDialogAction
openDialog,
// #withAlertActions
openAlert,
}) {
// Table columns.
const columns = useBranchesTableColumns();
// MarkBranchAsPrimary
const { mutateAsync: markBranchAsPrimaryMutate } = useMarkBranchAsPrimary();
const { branches, isBranchesLoading, isBranchesFetching } =
useBranchesContext();
// Handle edit branch.
const handleEditBranch = ({ id }) => {
openDialog('branch-form', { branchId: id, action: 'edit' });
};
// Handle delete branch.
const handleDeleteBranch = ({ id }) => {
openAlert('branch-delete', { branchId: id });
};
// Handle mark branch as primary.
const handleMarkBranchAsPrimary = ({ id }) => {
markBranchAsPrimaryMutate(id).then(() => {
AppToaster.show({
message: intl.get('branch.alert.mark_primary_message'),
intent: Intent.SUCCESS,
});
});
};
return (
<BranchesTableCard>
<BranchesTable
columns={columns}
data={branches}
loading={isBranchesLoading}
headerLoading={isBranchesLoading}
progressBarLoading={isBranchesFetching}
TableLoadingRenderer={TableSkeletonRows}
noInitialFetch={true}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditBranch,
onDelete: handleDeleteBranch,
onMarkPrimary: handleMarkBranchAsPrimary,
}}
/>
</BranchesTableCard>
);
}
export default compose(withDialogActions, withAlertActions)(BranchesDataTable);
const BranchesTableCard = styled(Card)`
padding: 0;
`;
const BranchesTable = styled(DataTable)`
.table .tr {
min-height: 38px;
.td.td-name {
.bp3-icon {
margin: 0;
margin-left: 2px;
vertical-align: top;
color: #e1b31d;
}
}
}
`;

View File

@@ -0,0 +1,40 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, EmptyStatus } from '@/components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function BranchesEmptyStatus({
// #withDialogActions
openDialog,
}) {
// Handle activate action branch.
const handleActivateBranch = () => {
openDialog('branch-activate', {});
};
return (
<EmptyStatus
title={<T id={'branches.empty_status.title'} />}
description={
<p>
<T id={'branches.empty_status.description'} />
</p>
}
action={
<React.Fragment>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={handleActivateBranch}
>
<T id={'branches.activate_button'} />
</Button>
</React.Fragment>
}
/>
);
}
export default compose(withDialogActions)(BranchesEmptyStatus);

View File

@@ -0,0 +1,53 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { useBranches } from '@/hooks/query';
import { useFeatureCan } from '@/hooks/state';
import { Features } from '@/constants';
import { isEmpty } from 'lodash';
const BranchesContext = React.createContext();
/**
* Branches data provider.
*/
function BranchesProvider({ query, ...props }) {
// Features guard.
const { featureCan } = useFeatureCan();
const isBranchFeatureCan = featureCan(Features.Branches);
// Fetches the branches list.
const {
isLoading: isBranchesLoading,
isFetching: isBranchesFetching,
data: branches,
} = useBranches(query, { enabled: isBranchFeatureCan });
// Detarmines the datatable empty status.
const isEmptyStatus =
(isEmpty(branches) && !isBranchesLoading) || !isBranchFeatureCan;
// Provider state.
const provider = {
branches,
isBranchesLoading,
isBranchesFetching,
isEmptyStatus,
};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_BRANCHES,
)}
>
<BranchesContext.Provider value={provider} {...props} />
</div>
);
}
const useBranchesContext = () => React.useContext(BranchesContext);
export { BranchesProvider, useBranchesContext };

View File

@@ -0,0 +1,92 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
import { safeCallback } from '@/utils';
import { Icon, If } from '@/components';
/**
* Context menu of Branches.
*/
export function ActionsMenu({
payload: { onEdit, onDelete, onMarkPrimary },
row: { original },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('branches.action.edit_branch')}
onClick={safeCallback(onEdit, original)}
/>
<If condition={!original.primary}>
<MenuItem
icon={<Icon icon={'check'} iconSize={18} />}
text={intl.get('branches.action.mark_as_primary')}
onClick={safeCallback(onMarkPrimary, original)}
/>
</If>
<MenuDivider />
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={intl.get('branches.action.delete_branch')}
onClick={safeCallback(onDelete, original)}
intent={Intent.DANGER}
/>
</Menu>
);
}
/**
* Branch name cell.
*/
function BranchNameCell({ value, row: { original } }) {
return (
<span>
{value} {original.primary && <Icon icon={'star-18dp'} iconSize={16} />}
</span>
);
}
/**
* Retrieve branches table columns
* @returns
*/
export function useBranchesTableColumns() {
return React.useMemo(
() => [
{
id: 'name',
Header: intl.get('branches.column.branch_name'),
accessor: 'name',
Cell: BranchNameCell,
width: '120',
disableSortBy: true,
textOverview: true,
},
{
id: 'code',
Header: intl.get('branches.column.code'),
accessor: 'code',
width: '100',
disableSortBy: true,
textOverview: true,
},
{
Header: intl.get('branches.column.address'),
accessor: 'address',
width: '180',
disableSortBy: true,
textOverview: true,
},
{
Header: intl.get('branches.column.phone_number'),
accessor: 'phone_number',
width: '120',
disableSortBy: true,
},
],
[],
);
}

View File

@@ -0,0 +1,16 @@
// @ts-nocheck
import React from 'react';
import { BranchesProvider } from './BranchesProvider';
import Branches from './Branches';
/**
* Branches .
*/
export default function BranchesPreferences() {
return (
<BranchesProvider>
<Branches />
</BranchesProvider>
);
}

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
/**
* Handle delete errors.
*/
export const handleDeleteErrors = (errors) => {
if (errors.find((error) => error.type === 'COULD_NOT_DELETE_ONLY_BRANCH')) {
AppToaster.show({
message: intl.get('branch.error.could_not_delete_only_branch'),
intent: Intent.DANGER,
});
}
if (errors.some((e) => e.type === 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS')) {
AppToaster.show({
message: intl.get('branche.error.branch_has_associated_transactions'),
intent: Intent.DANGER,
});
}
};

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import styled from 'styled-components';
import { Card } from '@/components';
import { CLASSES } from '@/constants/classes';
import CurrenciesList from './CurrenciesList';
export default function PreferencesCurrenciesPage() {
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES,
)}
>
<CurrenciesCard>
<CurrenciesList />
</CurrenciesCard>
</div>
);
}
const CurrenciesCard = styled(Card)`
padding: 0;
`;

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { compose } from '@/utils';
import { Icon, FormattedMessage as T } from '@/components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
function CurrenciesActions({ openDialog }) {
const handleClickNewCurrency = useCallback(() => {
openDialog('currency-form');
}, [openDialog]);
return (
<div class="users-actions">
<Button
icon={<Icon icon="plus" iconSize={12} />}
onClick={handleClickNewCurrency}
intent={Intent.PRIMARY}
>
<T id={'new_currency'} />
</Button>
</div>
);
}
export default compose(withDialogActions)(CurrenciesActions);

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
import React from 'react';
const CurrencyDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Currencies/CurrencyDeleteAlert'),
);
export default [{ name: 'currency-delete', component: CurrencyDeleteAlert }];

View File

@@ -0,0 +1,70 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { compose } from '@/utils';
import { DataTable, TableSkeletonRows } from '@/components';
import { useCurrenciesContext } from './CurrenciesProvider';
import { ActionMenuList, useCurrenciesTableColumns } from './components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
/**
* Currencies table.
*/
function CurrenciesDataTable({
// #ownProps
tableProps,
// #withDialog.
openDialog,
// #withAlertActions
openAlert,
}) {
const { currencies, isCurrenciesLoading } = useCurrenciesContext();
// Table columns.
const columns = useCurrenciesTableColumns();
// Handle Edit Currency.
const handleEditCurrency = useCallback(
(currency) => {
openDialog('currency-form', {
action: 'edit',
currency: currency,
});
},
[openDialog],
);
// Handle delete currency.
const handleDeleteCurrency = ({ currency_code }) => {
openAlert('currency-delete', { currency_code: currency_code });
};
return (
<DataTable
columns={columns}
data={currencies}
loading={isCurrenciesLoading}
progressBarLoading={isCurrenciesLoading}
TableLoadingRenderer={TableSkeletonRows}
ContextMenu={ActionMenuList}
noInitialFetch={true}
payload={{
onDeleteCurrency: handleDeleteCurrency,
onEditCurrency: handleEditCurrency,
}}
rowContextMenu={ActionMenuList}
{...tableProps}
/>
);
}
export default compose(
withDialogActions,
withAlertActions,
)(CurrenciesDataTable);

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import intl from 'react-intl-universal';
import { CurrenciesProvider } from './CurrenciesProvider';
import CurrenciesDataTable from './CurrenciesDataTable';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { compose } from '@/utils';
function CurrenciesList({
// #withDashboardActions
changePreferencesPageTitle,
}) {
useEffect(() => {
changePreferencesPageTitle(intl.get('currencies'));
}, [changePreferencesPageTitle]);
return (
<CurrenciesProvider>
<CurrenciesDataTable />
</CurrenciesProvider>
);
}
export default compose(withDashboardActions)(CurrenciesList);

View File

@@ -0,0 +1,24 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
import { useCurrencies } from '@/hooks/query';
const CurrenciesContext = createContext();
/**
* currencies provider.
*/
function CurrenciesProvider({ ...props }) {
// fetches the currencies list.
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
const state = {
currencies,
isCurrenciesLoading,
};
return <CurrenciesContext.Provider value={state} {...props} />;
}
const useCurrenciesContext = () => useContext(CurrenciesContext);
export { CurrenciesProvider, useCurrenciesContext };

View File

@@ -0,0 +1,85 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import {
Menu,
Popover,
Button,
Position,
MenuItem,
MenuDivider,
Intent,
} from '@blueprintjs/core';
import { Icon } from '@/components';
import { safeCallback } from '@/utils';
/**
* Row actions menu list.
*/
export function ActionMenuList({
row: { original },
payload: { onEditCurrency, onDeleteCurrency },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_currency')}
onClick={safeCallback(onEditCurrency, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={intl.get('delete_currency')}
onClick={safeCallback(onDeleteCurrency, original)}
intent={Intent.DANGER}
/>
</Menu>
);
}
/**
* Actions cell.
*/
export const ActionsCell = (props) => {
return (
<Popover
position={Position.RIGHT_BOTTOM}
content={<ActionMenuList {...props} />}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
};
export function useCurrenciesTableColumns() {
return useMemo(
() => [
{
Header: intl.get('currency_name'),
accessor: 'currency_name',
width: 150,
},
{
Header: intl.get('currency_code'),
accessor: 'currency_code',
className: 'currency_code',
width: 120,
},
{
Header: intl.get('currency_sign'),
width: 120,
accessor: 'currency_sign',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[],
);
}

View File

@@ -0,0 +1,9 @@
// @ts-nocheck
import React from 'react';
import { Redirect } from 'react-router-dom';
export default function DefaultRoute() {
const defaultTab = '/preferences/general';
return (<Redirect from='/preferences' to={defaultTab} />);
}

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import * as Yup from 'yup';
import intl from 'react-intl-universal';
const Schema = Yup.object().shape({
name: Yup.string()
.required()
.label(intl.get('organization_name_')),
industry: Yup.string()
.nullable()
.label(intl.get('organization_industry_')),
location: Yup.string()
.nullable()
.label(intl.get('location')),
base_currency: Yup.string()
.required()
.label(intl.get('base_currency_')),
fiscal_year: Yup.string()
.required()
.label(intl.get('fiscal_year_')),
language: Yup.string()
.required()
.label(intl.get('language')),
timezone: Yup.string()
.required()
.label(intl.get('time_zone_')),
date_format: Yup.string()
.required()
.label(intl.get('date_format_')),
});
export const PreferencesGeneralSchema = Schema;

View File

@@ -0,0 +1,16 @@
// @ts-nocheck
import React from 'react';
import GeneralFormPage from './GeneralFormPage';
import { GeneralFormProvider } from './GeneralFormProvider';
/**
* Preferences - General form.
*/
export default function GeneralPreferences() {
return (
<GeneralFormProvider>
<GeneralFormPage />
</GeneralFormProvider>
);
}

View File

@@ -0,0 +1,274 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import classNames from 'classnames';
import { Form } from 'formik';
import { Button, FormGroup, InputGroup, Intent } from '@blueprintjs/core';
import { TimezonePicker } from '@blueprintjs/timezone';
import { ErrorMessage, FastField } from 'formik';
import { useHistory } from 'react-router-dom';
import {
ListSelect,
FieldRequiredHint,
FormattedMessage as T,
} from '@/components';
import { inputIntent } from '@/utils';
import { CLASSES } from '@/constants/classes';
import { getCountries } from '@/constants/countries';
import { getAllCurrenciesOptions } from '@/constants/currencies';
import { getFiscalYear } from '@/constants/fiscalYearOptions';
import { getLanguages } from '@/constants/languagesOptions';
import { useGeneralFormContext } from './GeneralFormProvider';
import { shouldBaseCurrencyUpdate } from './utils';
/**
* Preferences general form.
*/
export default function PreferencesGeneralForm({ isSubmitting }) {
const history = useHistory();
const FiscalYear = getFiscalYear();
const Countries = getCountries();
const Languages = getLanguages();
const Currencies = getAllCurrenciesOptions();
const { dateFormats, baseCurrencyMutateAbility } = useGeneralFormContext();
const baseCurrencyDisabled = baseCurrencyMutateAbility.length > 0;
// Handle close click.
const handleCloseClick = () => {
history.go(-1);
};
return (
<Form>
{/* ---------- Organization name ---------- */}
<FastField name={'name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'organization_name'} />}
labelInfo={<FieldRequiredHint />}
inline={true}
intent={inputIntent({ error, touched })}
className={'form-group--org-name'}
helperText={<T id={'shown_on_sales_forms_and_purchase_orders'} />}
>
<InputGroup medium={'true'} {...field} />
</FormGroup>
)}
</FastField>
{/* ---------- Industry ---------- */}
<FastField name={'industry'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'organization_industry'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="industry" />}
className={'form-group--org-industry'}
>
<InputGroup medium={'true'} {...field} />
</FormGroup>
)}
</FastField>
{/* ---------- Location ---------- */}
<FastField name={'location'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'business_location'} />}
className={classNames(
'form-group--business-location',
CLASSES.FILL,
)}
inline={true}
helperText={<ErrorMessage name="location" />}
intent={inputIntent({ error, touched })}
>
<ListSelect
items={Countries}
onItemSelect={({ value }) => {
form.setFieldValue('location', value);
}}
selectedItem={value}
selectedItemProp={'value'}
defaultText={<T id={'select_business_location'} />}
textProp={'name'}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</FastField>
{/* ---------- Base currency ---------- */}
<FastField
name={'base_currency'}
baseCurrencyDisabled={baseCurrencyDisabled}
shouldUpdate={shouldBaseCurrencyUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'base_currency'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--base-currency', CLASSES.FILL)}
inline={true}
intent={inputIntent({ error, touched })}
helperText={
<T
id={
'you_can_t_change_the_base_currency_as_there_are_transactions'
}
/>
}
>
<ListSelect
items={Currencies}
onItemSelect={(currency) => {
form.setFieldValue('base_currency', currency.key);
}}
selectedItem={value}
selectedItemProp={'key'}
defaultText={<T id={'select_base_currency'} />}
textProp={'name'}
labelProp={'key'}
popoverProps={{ minimal: true }}
disabled={baseCurrencyDisabled}
/>
</FormGroup>
)}
</FastField>
{/* --------- Fiscal Year ----------- */}
<FastField name={'fiscal_year'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'fiscal_year'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--fiscal-year', CLASSES.FILL)}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<T id={'for_reporting_you_can_specify_any_month'} />}
>
<ListSelect
items={FiscalYear}
onItemSelect={(option) => {
form.setFieldValue('fiscal_year', option.key);
}}
selectedItem={value}
selectedItemProp={'key'}
defaultText={<T id={'select_fiscal_year'} />}
textProp={'name'}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</FastField>
{/* ---------- Language ---------- */}
<FastField name={'language'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'language'} />}
labelInfo={<FieldRequiredHint />}
inline={true}
className={classNames('form-group--language', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="language" />}
>
<ListSelect
items={Languages}
selectedItemProp={'value'}
textProp={'name'}
defaultText={<T id={'select_language'} />}
selectedItem={value}
onItemSelect={(item) =>
form.setFieldValue('language', item.value)
}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</FastField>
{/* ---------- Time zone ---------- */}
<FastField name={'timezone'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'time_zone'} />}
labelInfo={<FieldRequiredHint />}
inline={true}
className={classNames(
'form-group--time-zone',
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="timezone" />}
>
<TimezonePicker
value={value}
onChange={(timezone) => {
form.setFieldValue('timezone', timezone);
}}
valueDisplayFormat="composite"
placeholder={<T id={'select_time_zone'} />}
/>
</FormGroup>
)}
</FastField>
{/* --------- Data format ----------- */}
<FastField name={'date_format'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'date_format'} />}
labelInfo={<FieldRequiredHint />}
inline={true}
className={classNames('form-group--date-format', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="date_format" />}
>
<ListSelect
items={dateFormats}
onItemSelect={(dateFormat) => {
form.setFieldValue('date_format', dateFormat.key);
}}
selectedItem={value}
selectedItemProp={'key'}
defaultText={<T id={'select_date_format'} />}
textProp={'label'}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</FastField>
<CardFooterActions>
<Button loading={isSubmitting} intent={Intent.PRIMARY} type="submit">
<T id={'save'} />
</Button>
<Button onClick={handleCloseClick}>
<T id={'close'} />
</Button>
</CardFooterActions>
</Form>
);
}
const CardFooterActions = styled.div`
padding-top: 16px;
border-top: 1px solid #e0e7ea;
margin-top: 30px;
.bp3-button {
min-width: 70px;
+ .bp3-button {
margin-left: 10px;
}
}
`;

View File

@@ -0,0 +1,79 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import '@/style/pages/Preferences/GeneralForm.scss';
import { AppToaster } from '@/components';
import GeneralForm from './GeneralForm';
import { PreferencesGeneralSchema } from './General.schema';
import { useGeneralFormContext } from './GeneralFormProvider';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { compose, transformToForm } from '@/utils';
const defaultValues = {
name: '',
industry: '',
location: '',
base_currency: '',
language: '',
fiscal_year: '',
date_format: '',
timezone: '',
};
/**
* Preferences - General form Page.
*/
function GeneralFormPage({
// #withDashboardActions
changePreferencesPageTitle,
}) {
const { updateOrganization, organization } = useGeneralFormContext();
useEffect(() => {
changePreferencesPageTitle(intl.get('general'));
}, [changePreferencesPageTitle]);
// Initial values.
const initialValues = {
...transformToForm(organization.metadata, defaultValues),
};
// Handle the form submit.
const handleFormSubmit = (values, { setSubmitting, resetForm }) => {
// Handle request success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('preferences.general.success_message'),
intent: Intent.SUCCESS,
});
setSubmitting(false);
// Reboot the application if the application's language is mutated.
if (organization.metadata?.language !== values.language) {
window.location.reload();
}
};
// Handle request error.
const onError = (errors) => {
setSubmitting(false);
};
updateOrganization({ ...values })
.then(onSuccess)
.catch(onError);
};
return (
<Formik
initialValues={initialValues}
validationSchema={PreferencesGeneralSchema}
onSubmit={handleFormSubmit}
component={GeneralForm}
/>
);
}
export default compose(withDashboardActions)(GeneralFormPage);

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
import React, { createContext } from 'react';
import classNames from 'classnames';
import styled from 'styled-components';
import { Card } from '@/components';
import { CLASSES } from '@/constants/classes';
import {
useCurrentOrganization,
useUpdateOrganization,
useDateFormats,
useOrgBaseCurrencyMutateAbilities,
} from '@/hooks/query';
import PreferencesPageLoader from '../PreferencesPageLoader';
const GeneralFormContext = createContext();
/**
* General form provider.
*/
function GeneralFormProvider({ ...props }) {
// Fetches current organization information.
const { isLoading: isOrganizationLoading, data: organization } =
useCurrentOrganization();
const { data: dateFormats, isLoading: isDateFormatsLoading } =
useDateFormats();
const { data: baseCurrencyMutateAbility } =
useOrgBaseCurrencyMutateAbilities();
// Mutate organization information.
const { mutateAsync: updateOrganization } = useUpdateOrganization();
// Provider state.
const provider = {
isOrganizationLoading,
isDateFormatsLoading,
updateOrganization,
baseCurrencyMutateAbility,
organization,
dateFormats,
};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_GENERAL,
)}
>
<GeneralFormCard>
{isOrganizationLoading || isDateFormatsLoading ? (
<PreferencesPageLoader />
) : (
<GeneralFormContext.Provider value={provider} {...props} />
)}
</GeneralFormCard>
</div>
);
}
const useGeneralFormContext = () => React.useContext(GeneralFormContext);
export { GeneralFormProvider, useGeneralFormContext };
const GeneralFormCard = styled(Card)`
padding: 25px;
`;

View File

@@ -0,0 +1,9 @@
// @ts-nocheck
import { defaultFastFieldShouldUpdate } from '@/utils';
export const shouldBaseCurrencyUpdate = (newProps, oldProps) => {
return (
newProps.baseCurrencyDisabled !== oldProps.baseCurrencyDisabled ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};

View File

@@ -0,0 +1,10 @@
// @ts-nocheck
import * as Yup from 'yup';
const Schema = Yup.object().shape({
preferred_sell_account: Yup.number().nullable(),
preferred_cost_account: Yup.number().nullable(),
preferred_inventory_account: Yup.number().nullable(),
});
export const ItemPreferencesSchema = Schema;

View File

@@ -0,0 +1,150 @@
// @ts-nocheck
import React from 'react';
import { Form, FastField, useFormikContext } from 'formik';
import { FormGroup, Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
AccountsSelectList,
FieldRequiredHint,
FormattedMessage as T,
CardFooterActions
} from '@/components';
import { inputIntent } from '@/utils';
import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from '@/constants/accountTypes';
import { useItemPreferencesFormContext } from './ItemPreferencesFormProvider';
/**
* Item preferences form.
*/
export default function ItemForm() {
const history = useHistory();
const { accounts } = useItemPreferencesFormContext();
const { isSubmitting } = useFormikContext();
const handleCloseClick = () => {
history.go(-1);
};
return (
<Form>
{/* ----------- preferred sell account ----------- */}
<FastField name={'preferred_sell_account'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={
<strong>
<T id={'preferred_sell_account'} />
</strong>
}
helperText={
<T
id={
'select_a_preferred_account_to_deposit_into_it_after_customer_make_payment'
}
/>
}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={({ id }) => {
setFieldValue('preferred_sell_account', id);
}}
selectedAccountId={value}
defaultSelectText={<T id={'select_payment_account'} />}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
/>
</FormGroup>
)}
</FastField>
{/* ----------- preferred cost account ----------- */}
<FastField name={'preferred_cost_account'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={
<strong>
<T id={'preferred_cost_account'} />
</strong>
}
helperText={
<T
id={
'select_a_preferred_account_to_deposit_into_it_after_customer_make_payment'
}
/>
}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={({ id }) => {
setFieldValue('preferred_cost_account', id);
}}
selectedAccountId={value}
defaultSelectText={<T id={'select_payment_account'} />}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
/>
</FormGroup>
)}
</FastField>
{/* ----------- preferred inventory account ----------- */}
<FastField name={'preferred_inventory_account'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={
<strong>
<T id={'preferred_inventory_account'} />
</strong>
}
helperText={
<T
id={
'select_a_preferred_account_to_deposit_into_it_vendor_advanced_deposits'
}
/>
}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={({ id }) => {
setFieldValue('preferred_inventory_account', id);
}}
selectedAccountId={value}
defaultSelectText={<T id={'select_payment_account'} />}
filterByTypes={[ACCOUNT_TYPE.INVENTORY]}
/>
</FormGroup>
)}
</FastField>
<CardFooterActions>
<Button intent={Intent.PRIMARY} loading={isSubmitting} type="submit">
<T id={'save'} />
</Button>
<Button onClick={handleCloseClick} disabled={isSubmitting}>
<T id={'close'} />
</Button>
</CardFooterActions>
</Form>
);
}

View File

@@ -0,0 +1,78 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
import { omit } from 'lodash';
import { ItemPreferencesSchema } from './ItemPreferences.schema';
import ItemPreferencesForm from './ItemPreferencesForm';
import { useItemPreferencesFormContext } from './ItemPreferencesFormProvider';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import withSettings from '@/containers/Settings/withSettings';
import { compose, optionsMapToArray, transformGeneralSettings } from '@/utils';
import '@/style/pages/Preferences/Accounting.scss';
// item form page preferences.
function ItemPreferencesFormPage({
// #withSettings
itemsSettings,
// #withDashboardActions
changePreferencesPageTitle,
}) {
const { saveSettingMutate } = useItemPreferencesFormContext();
const itemPerferencesSettings = {
...omit(itemsSettings, ['tableSize']),
};
// Initial values.
const initialValues = {
preferred_sell_account: '',
preferred_cost_account: '',
preferred_inventory_account: '',
...transformGeneralSettings(itemPerferencesSettings),
};
useEffect(() => {
changePreferencesPageTitle(intl.get('items'));
}, [changePreferencesPageTitle]);
// Handle form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const options = optionsMapToArray(values).map((option) => ({
...option,
group: 'items',
}));
const onSuccess = () => {
AppToaster.show({
message: intl.get('the_items_preferences_has_been_saved'),
intent: Intent.SUCCESS,
});
setSubmitting(false);
};
const onError = (errors) => {
setSubmitting(false);
};
saveSettingMutate({ options }).then(onSuccess).catch(onError);
};
return (
<Formik
initialValues={initialValues}
validationSchema={ItemPreferencesSchema}
onSubmit={handleFormSubmit}
component={ItemPreferencesForm}
/>
);
}
export default compose(
withSettings(({ itemsSettings }) => ({ itemsSettings })),
withDashboardActions,
)(ItemPreferencesFormPage);

View File

@@ -0,0 +1,61 @@
// @ts-nocheck
import React, { useContext, createContext } from 'react';
import classNames from 'classnames';
import styled from 'styled-components';
import { CLASSES } from '@/constants/classes';
import { Card } from '@/components';
import { useSettingsItems, useAccounts, useSaveSettings } from '@/hooks/query';
import PreferencesPageLoader from '../PreferencesPageLoader';
const ItemFormContext = createContext();
/**
* Item data provider.
*/
function ItemPreferencesFormProvider({ ...props }) {
// Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts();
const {
isLoading: isItemsSettingsLoading,
isFetching: isItemsSettingsFetching,
} = useSettingsItems();
// Save Organization Settings.
const { mutateAsync: saveSettingMutate } = useSaveSettings();
// Provider state.
const provider = {
accounts,
saveSettingMutate,
};
const isLoading = isAccountsLoading || isItemsSettingsLoading;
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT,
)}
>
<ItemsPreferencesCard>
{isLoading ? (
<PreferencesPageLoader />
) : (
<ItemFormContext.Provider value={provider} {...props} />
)}
</ItemsPreferencesCard>
</div>
);
}
const useItemPreferencesFormContext = () => useContext(ItemFormContext);
export { useItemPreferencesFormContext, ItemPreferencesFormProvider };
const ItemsPreferencesCard = styled(Card)`
padding: 25px;
`;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import React from 'react';
import ItemPreferencesFormPage from './ItemPreferencesFormPage';
import { ItemPreferencesFormProvider } from './ItemPreferencesFormProvider';
/**
* items preferences.
*/
export default function ItemsPreferences() {
return (
<ItemPreferencesFormProvider>
<ItemPreferencesFormPage />
</ItemPreferencesFormProvider>
);
}

View File

@@ -0,0 +1,24 @@
// @ts-nocheck
import React from 'react';
import ContentLoader from 'react-content-loader';
export default function PreferencesPageLoader(props) {
return (
<ContentLoader
speed={2}
width={400}
height={250}
viewBox="0 0 400 250"
backgroundColor="#f3f3f3"
foregroundColor="#e6e6e6"
{...props}
>
<rect x="0" y="82" rx="2" ry="2" width="200" height="20" />
<rect x="0" y="112" rx="2" ry="2" width="385" height="30" />
<rect x="0" y="0" rx="2" ry="2" width="200" height="20" />
<rect x="-1" y="30" rx="2" ry="2" width="385" height="30" />
<rect x="0" y="164" rx="2" ry="2" width="200" height="20" />
<rect x="0" y="194" rx="2" ry="2" width="385" height="30" />
</ContentLoader>
);
}

View File

@@ -0,0 +1,43 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { useSettings, useSettingSMSNotifications } from '@/hooks/query';
const SMSIntegrationContext = React.createContext();
/**
* SMS Integration provider.
*/
function SMSIntegrationProvider({ ...props }) {
//Fetches Organization Settings.
const { isLoading: isSettingsLoading } = useSettings();
const {
data: notifications,
isLoading: isSMSNotificationsLoading,
isFetching: isSMSNotificationsFetching,
} = useSettingSMSNotifications();
// Provider state.
const provider = {
notifications,
isSMSNotificationsLoading,
isSMSNotificationsFetching,
};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_SMS_INTEGRATION,
)}
>
<SMSIntegrationContext.Provider value={provider} {...props} />
</div>
);
}
const useSMSIntegrationContext = () => React.useContext(SMSIntegrationContext);
export { SMSIntegrationProvider, useSMSIntegrationContext };

View File

@@ -0,0 +1,53 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import classNames from 'classnames';
import { Tabs, Tab } from '@blueprintjs/core';
import { CLASSES } from '@/constants/classes';
import SMSMessagesDataTable from './SMSMessagesDataTable';
import { Card } from '@/components';
import '@/style/pages/Preferences/SMSIntegration.scss';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { compose } from '@/utils';
/**
* SMS Integration Tabs.
* @returns {React.JSX}
*/
function SMSIntegrationTabs({
// #withDashboardActions
changePreferencesPageTitle,
}) {
React.useEffect(() => {
changePreferencesPageTitle(intl.get('sms_integration.label'));
}, [changePreferencesPageTitle]);
return (
<SMSIntegrationCard>
<div className={classNames(CLASSES.PREFERENCES_PAGE_TABS)}>
<Tabs animate={true} defaultSelectedTabId={'sms_messages'}>
<Tab
id="overview"
title={intl.get('sms_integration.label.overview')}
/>
<Tab
id="sms_messages"
title={intl.get('sms_integration.label.sms_messages')}
panel={<SMSMessagesDataTable />}
/>
</Tabs>
</div>
</SMSIntegrationCard>
);
}
export default compose(withDashboardActions)(SMSIntegrationTabs);
const SMSIntegrationCard = styled(Card)`
padding: 0;
`;

View File

@@ -0,0 +1,99 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent } from '@blueprintjs/core';
import { DataTable, AppToaster, TableSkeletonRows } from '@/components';
import { useSMSIntegrationTableColumns, ActionsMenu } from './components';
import { useSMSIntegrationContext } from './SMSIntegrationProvider';
import { useSettingEditSMSNotification } from '@/hooks/query';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
/**
* SMS Message data table.
*/
function SMSMessagesDataTable({
// #withDialogAction
openDialog,
}) {
// Edit SMS message notification mutations.
const { mutateAsync: editSMSNotificationMutate } =
useSettingEditSMSNotification();
const toggleSmsNotification = (notificationKey, value) => {
editSMSNotificationMutate({
notification_key: notificationKey,
is_notification_enabled: value,
}).then(() => {
AppToaster.show({
message: intl.get(
'sms_messages.notification_switch_change_success_message',
),
intent: Intent.SUCCESS,
});
});
};
// Handle notification switch change.
const handleNotificationSwitchChange = React.useCallback(
(event, value, notification) => {
toggleSmsNotification(notification.key, value);
},
[editSMSNotificationMutate],
);
// Table columns.
const columns = useSMSIntegrationTableColumns({
onSwitchChange: handleNotificationSwitchChange,
});
const {
notifications,
isSMSNotificationsLoading,
isSMSNotificationsFetching,
} = useSMSIntegrationContext();
// handle edit message link click
const handleEditMessageText = ({ key }) => {
openDialog('sms-message-form', { notificationkey: key });
};
const handleEnableNotification = (notification) => {
toggleSmsNotification(notification.key, true);
};
const handleDisableNotification = (notification) => {
toggleSmsNotification(notification.key, false);
};
return (
<SMSNotificationsTable
columns={columns}
data={notifications}
loading={isSMSNotificationsLoading}
progressBarLoading={isSMSNotificationsFetching}
TableLoadingRenderer={TableSkeletonRows}
ContextMenu={ActionsMenu}
payload={{
onEditMessageText: handleEditMessageText,
onEnableNotification: handleEnableNotification,
onDisableNotification: handleDisableNotification,
}}
/>
);
}
export default compose(withDialogActions)(SMSMessagesDataTable);
const SMSNotificationsTable = styled(DataTable)`
.table .tbody .tr .td {
align-items: flex-start;
}
.table .tbody .td {
padding: 0.8rem;
}
`;

View File

@@ -0,0 +1,145 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent, Button, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
import { SwitchFieldCell } from '@/components/DataTableCells';
import { safeInvoke } from '@/utils';
/**
* Notification accessor.
*/
export const NotificationAccessor = (row) => {
return (
<span className="notification">
<NotificationLabel>{row.notification_label}</NotificationLabel>
<NotificationDescription>
{row.notification_description}
</NotificationDescription>
</span>
);
};
/**
* SMS notification message cell.
*/
export const SMSMessageCell = ({
payload: { onEditMessageText },
row: { original },
}) => (
<div>
<MessageBox>{original.sms_message}</MessageBox>
<MessageBoxActions>
<Button
minimal={true}
small={true}
intent={Intent.NONE}
onClick={() => safeInvoke(onEditMessageText, original)}
>
{intl.get('sms_messages.label_edit_message')}
</Button>
</MessageBoxActions>
</div>
);
/**
* Context menu of SMS notification messages.
*/
export function ActionsMenu({
payload: { onEditMessageText, onEnableNotification, onDisableNotification },
row: { original },
}) {
return (
<Menu>
<MenuItem
text={intl.get('sms_notifications.edit_message_text')}
onClick={() => safeInvoke(onEditMessageText, original)}
/>
<MenuDivider />
{!original.is_notification_enabled ? (
<MenuItem
text={intl.get('sms_notifications.enable_notification')}
onClick={() => safeInvoke(onEnableNotification, original)}
/>
) : (
<MenuItem
text={intl.get('sms_notifications.disable_notification')}
onClick={() => safeInvoke(onDisableNotification, original)}
/>
)}
</Menu>
);
}
/**
* Retrieve SMS notifications messages table columns
* @returns
*/
export function useSMSIntegrationTableColumns({ onSwitchChange }) {
return React.useMemo(
() => [
{
id: 'notification',
Header: intl.get('sms_messages.column.notification'),
accessor: NotificationAccessor,
className: 'notification',
width: '180',
disableSortBy: true,
},
{
Header: intl.get('sms_messages.column.service'),
accessor: 'module_formatted',
className: 'service',
width: '80',
disableSortBy: true,
},
{
Header: intl.get('sms_messages.column.message'),
accessor: 'sms_message',
Cell: SMSMessageCell,
className: 'sms_message',
width: '180',
disableSortBy: true,
},
{
Header: intl.get('sms_messages.column.auto'),
accessor: 'is_notification_enabled',
Cell: SwitchFieldCell,
className: 'is_notification_enabled',
disableResizing: true,
disableSortBy: true,
width: '80',
onSwitchChange,
},
],
[onSwitchChange],
);
}
const NotificationLabel = styled.div`
font-weight: 500;
`;
const NotificationDescription = styled.div`
font-size: 14px;
margin-top: 6px;
display: block;
opacity: 0.75;
`;
const MessageBox = styled.div`
padding: 10px;
background-color: #fbfbfb;
border: 1px dashed #dcdcdc;
font-size: 14px;
line-height: 1.45;
`;
const MessageBoxActions = styled.div`
margin-top: 2px;
button {
font-size: 12px;
}
`;

View File

@@ -0,0 +1,16 @@
// @ts-nocheck
import React from 'react';
import { SMSIntegrationProvider } from './SMSIntegrationProvider';
import SMSIntegrationTabs from './SMSIntegrationTabs';
/**
* SMS SMS Integration
*/
export default function SMSIntegration() {
return (
<SMSIntegrationProvider>
<SMSIntegrationTabs />
</SMSIntegrationProvider>
);
}

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import React from 'react';
const RoleDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Roles/RoleDeleteAlert'),
);
/**
* Roles alerts
*/
export default [{ name: 'role-delete', component: RoleDeleteAlert }];

View File

@@ -0,0 +1,55 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import { Intent, Button } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from '@/components';
/**
* Role form floating actions.
* @returns {React.JSX}
*/
export function RoleFormFloatingActions() {
// Formik form context.
const { isSubmitting } = useFormikContext();
// History context.
const history = useHistory();
// Handle close click.
const handleCloseClick = () => {
history.go(-1);
};
return (
<RoleFormFloatingActionsRoot>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
type="submit"
style={{ minWidth: '90px' }}
>
<T id={'save'} />
</Button>
<Button onClick={handleCloseClick} disabled={isSubmitting}>
<T id={'cancel'} />
</Button>
</RoleFormFloatingActionsRoot>
);
}
const RoleFormFloatingActionsRoot = styled.div`
position: fixed;
bottom: 0;
width: 100%;
background: #fff;
padding: 14px 18px;
border-top: 1px solid #d2dde2;
box-shadow: 0px -1px 4px 0px rgb(0 0 0 / 5%);
.bp3-button {
margin-right: 10px;
}
`;

View File

@@ -0,0 +1,64 @@
// @ts-nocheck
import React from 'react';
import { ErrorMessage, FastField } from 'formik';
import { FormGroup, InputGroup, TextArea } from '@blueprintjs/core';
import { inputIntent } from '@/utils';
import { FormattedMessage as T, FieldRequiredHint, Card } from '@/components';
import { useAutofocus } from '@/hooks';
/**
* Role form header.
* @returns {React.JSX}
*/
export function RoleFormHeader() {
const roleNameFieldRef = useAutofocus();
return (
<Card>
{/* ---------- Name ---------- */}
<FastField name={'role_name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={
<strong>
<T id={'roles.label.role_name'} />
</strong>
}
labelInfo={<FieldRequiredHint />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="role_name" />}
inline={true}
>
<InputGroup
medium={true}
inputRef={(ref) => (roleNameFieldRef.current = ref)}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ---------- Description ---------- */}
<FastField name={'role_description'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'role_description'} />}
inline={true}
>
<TextArea
growVertically={true}
height={280}
{...field}
placeholder="Max. 500 characters"
/>
</FormGroup>
)}
</FastField>
</Card>
);
}

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { FormikObserver } from '@/components';
/**
* Role form observer.
* @returns {React.JSX}
*/
export function RoleFormObserver() {
const { values } = useFormikContext();
// Handles the form change.
const handleFormChange = () => {};
return <FormikObserver onChange={handleFormChange} values={values} />;
}

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
const Schema = Yup.object().shape({
role_name: Yup.string().required().label(intl.get('roles.label.role_name_')),
role_description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
permissions: Yup.object().shape({
subject: Yup.string(),
ability: Yup.string(),
value: Yup.boolean(),
}),
});
export const CreateRolesFormSchema = Schema;
export const EditRolesFormSchema = Schema;

View File

@@ -0,0 +1,109 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { useHistory } from 'react-router-dom';
import { Formik } from 'formik';
import { isEmpty } from 'lodash';
import { Intent } from '@blueprintjs/core';
import '@/style/pages/Preferences/Roles/Form.scss';
import { AppToaster, FormattedMessage as T } from '@/components';
import { CreateRolesFormSchema, EditRolesFormSchema } from './RolesForm.schema';
import { useRolesFormContext } from './RolesFormProvider';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import RolesFormContent from './RolesFormContent';
import {
getNewRoleInitialValues,
transformToArray,
transformToObject,
} from './utils';
import { handleDeleteErrors } from '../utils';
import { compose, transformToForm } from '@/utils';
const defaultValues = {
role_name: '',
role_description: '',
permissions: {},
serviceFullAccess: {},
};
/**
* Preferences - Roles Form.
*/
function RolesForm({
// #withDashboardActions
changePreferencesPageTitle,
}) {
// History context.
const history = useHistory();
// Role form context.
const {
isNewMode,
createRolePermissionMutate,
editRolePermissionMutate,
permissionsSchema,
role,
roleId,
} = useRolesFormContext();
// Initial values.
const initialValues = {
...defaultValues,
...(!isEmpty(role)
? transformToForm(transformToObject(role), defaultValues)
: getNewRoleInitialValues(permissionsSchema)),
};
React.useEffect(() => {
changePreferencesPageTitle(<T id={'roles.label'} />);
}, [changePreferencesPageTitle]);
// Handle the form submit.
const handleFormSubmit = (values, { setSubmitting }) => {
const permission = transformToArray(values);
const form = {
...values,
permissions: permission,
};
setSubmitting(true);
const onSuccess = () => {
AppToaster.show({
message: intl.get(
isNewMode
? 'roles.permission_schema.success_message'
: 'roles.permission_schema.upload_message',
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
history.push('/preferences/users');
};
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
handleDeleteErrors(errors);
};
if (isNewMode) {
createRolePermissionMutate(form).then(onSuccess).catch(onError);
} else {
editRolePermissionMutate([roleId, form]).then(onSuccess).catch(onError);
}
};
return (
<Formik
initialValues={initialValues}
validationSchema={isNewMode ? CreateRolesFormSchema : EditRolesFormSchema}
onSubmit={handleFormSubmit}
>
<RolesFormContent />
</Formik>
);
}
export default compose(withDashboardActions)(RolesForm);

View File

@@ -0,0 +1,23 @@
// @ts-nocheck
import React from 'react';
import { Form } from 'formik';
import { RoleFormHeader } from './RoleFormHeader';
import { RolesPermissionList } from './components';
import { RoleFormFloatingActions } from './RoleFormFloatingActions';
import { RoleFormObserver } from './RoleFormObserver';
/**
* Preferences - Roles Form content.
* @returns {React.JSX}
*/
export default function RolesFormContent() {
return (
<Form>
<RoleFormHeader />
<RolesPermissionList />
<RoleFormFloatingActions />
<RoleFormObserver />
</Form>
);
}

View File

@@ -0,0 +1,19 @@
// @ts-nocheck
import React from 'react';
import { useParams } from 'react-router-dom';
import { RolesFormProvider } from './RolesFormProvider';
import RolesForm from './RolesForm';
/**
* Roles Form page.
*/
export default function RolesFormPage() {
const { id } = useParams();
const idInteger = parseInt(id, 10);
return (
<RolesFormProvider roleId={idInteger}>
<RolesForm />
</RolesFormProvider>
);
}

View File

@@ -0,0 +1,74 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import {
useCreateRolePermissionSchema,
useEditRolePermissionSchema,
usePermissionsSchema,
useRolePermission,
} from '@/hooks/query';
import PreferencesPageLoader from '@/containers/Preferences/PreferencesPageLoader';
const RolesFormContext = React.createContext();
/**
* Roles Form page provider.
*/
function RolesFormProvider({ roleId, ...props }) {
// Create and edit roles mutations.
const { mutateAsync: createRolePermissionMutate } =
useCreateRolePermissionSchema();
const { mutateAsync: editRolePermissionMutate } =
useEditRolePermissionSchema();
// Retrieve permissions schema.
const {
data: permissionsSchema,
isLoading: isPermissionsSchemaLoading,
isFetching: isPermissionsSchemaFetching,
} = usePermissionsSchema();
const { data: role, isLoading: isPermissionLoading } = useRolePermission(
roleId,
{
enabled: !!roleId,
},
);
// Detarmines whether the new or edit mode.
const isNewMode = !roleId;
// Provider state.
const provider = {
isNewMode,
roleId,
role,
permissionsSchema,
isPermissionsSchemaLoading,
isPermissionsSchemaFetching,
createRolePermissionMutate,
editRolePermissionMutate,
};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ROLES_FORM,
)}
>
{isPermissionsSchemaLoading || isPermissionLoading ? (
<PreferencesPageLoader />
) : (
<RolesFormContext.Provider value={provider} {...props} />
)}
</div>
);
}
const useRolesFormContext = () => React.useContext(RolesFormContext);
export { RolesFormProvider, useRolesFormContext };

View File

@@ -0,0 +1,513 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { Field } from 'formik';
import { Checkbox, Popover } from '@blueprintjs/core';
import {
getPermissionsSchema,
ModulePermissionsStyle,
} from '@/constants/permissionsSchema';
import { Card, If, ButtonLink, Choose, T } from '@/components';
import {
getSerivceColumnPermission,
getServiceExtraPermissions,
FULL_ACCESS_CHECKBOX_STATE,
handleCheckboxFullAccessChange,
handleCheckboxPermissionChange,
} from './utils';
// Module permissions context.
const ModulePermissionsContext = React.createContext();
const ModuleServiceContext = React.createContext();
/**
* Retrieves the module permissions provider.
* @returns {React.JSX}
*/
const useModulePermissionsProvider = () =>
React.useContext(ModulePermissionsContext);
/**
* Module permissions service context provider.
*/
const useModulePermissionsServiceProvider = () =>
React.useContext(ModuleServiceContext);
/**
* Module permissions context state provider.
* @returns {React.JSX}
*/
function ModulePermissionsProvider({ module, children }) {
return (
<ModulePermissionsContext.Provider value={{ module }}>
{children}
</ModulePermissionsContext.Provider>
);
}
/**
* Module permissions service context state provider.
* @returns {React.JSX}
*/
function ModulePermissionsServiceProvider({ service, children }) {
return (
<ModuleServiceContext.Provider value={{ service }}>
{children}
</ModuleServiceContext.Provider>
);
}
/**
* Permissions body columns.
* @returns {React.JSX}
*/
function PermissionBodyColumn({ column }) {
// Module permssions service context.
const { service } = useModulePermissionsServiceProvider();
// Retrieve the related permission of the given column key.
const permission = getSerivceColumnPermission(service, column.key);
// Display empty cell if the current column key has no related permissions.
if (!permission) {
return <td class={'permission-checkbox'}></td>;
}
return (
<td class={'permission-checkbox'}>
<Field
name={`permissions.${service.subject}/${permission.key}`}
type="checkbox"
>
{({ field, form }) => (
<PermissionCheckbox
inline={true}
{...field}
onChange={handleCheckboxPermissionChange(form, permission, service)}
/>
)}
</Field>
</td>
);
}
/**
*
* @returns {React.JSX}
*/
function ModulePermissionsTableColumns({ columns }) {
return columns.map((column) => <PermissionBodyColumn column={column} />);
}
/**
* Module columns permissions extra permissions popover.
* @returns {React.JSX}
*/
function ModuleExtraPermissionsPopover() {
const { service } = useModulePermissionsServiceProvider();
// Retrieve the extra permissions of the given service.
const extraPermissions = getServiceExtraPermissions(service);
return (
<Popover>
<MorePermissionsLink>
<T id={'permissions.more_permissions'} />
</MorePermissionsLink>
<ExtraPermissionsRoot>
{extraPermissions.map((permission) => (
<Field
name={`permissions.${service.subject}/${permission.key}`}
type="checkbox"
>
{({ form, field }) => (
<PermissionCheckbox
inline={true}
label={permission.label}
{...field}
onChange={handleCheckboxPermissionChange(
form,
permission,
service,
)}
/>
)}
</Field>
))}
</ExtraPermissionsRoot>
</Popover>
);
}
/**
* Module permissions extra permissions.
* @returns {React.JSX}
*/
function ModulePermissionExtraPermissions() {
const { service } = useModulePermissionsServiceProvider();
// Retrieve the extra permissions of the given service.
const extraPermissions = getServiceExtraPermissions(service);
return (
<td>
<If condition={extraPermissions.length > 0}>
<ModuleExtraPermissionsPopover />
</If>
</td>
);
}
/**
* Module permissions table head.
* @returns {React.JSX}
*/
function ModulePermissionsTableHead() {
const {
module: { serviceFullAccess, columns },
} = useModulePermissionsProvider();
return (
<thead>
<tr>
<th></th>
<If condition={serviceFullAccess}>
<th class={'full'}>
<T id={'permissions.column.full_access'} />
</th>
</If>
{columns.map((column) => (
<th class={'permission'}>{column.label}</th>
))}
<th></th>
</tr>
</thead>
);
}
/**
* Module permissions service full access.
* @returns {React.JSX}
*/
function ModulePermissionsServiceFullAccess() {
// Module permissions provider.
const { module } = useModulePermissionsProvider();
// Module service provider.
const { service } = useModulePermissionsServiceProvider();
return (
<If condition={module.serviceFullAccess}>
<td class="full-access-permission">
<Field name={`serviceFullAccess.${service.subject}`} type="checkbox">
{({ form, field }) => (
<PermissionCheckbox
inline={true}
{...field}
indeterminate={
field.value === FULL_ACCESS_CHECKBOX_STATE.INDETARMINE
}
onChange={handleCheckboxFullAccessChange(service, form)}
/>
)}
</Field>
</td>
</If>
);
}
/**
* Module permissions table body.
* @returns {React.JSX}
*/
function ModulePermissionsTableBody() {
const {
module: { services, columns },
} = useModulePermissionsProvider();
return (
<tbody>
{services.map((service) => (
<ModulePermissionsServiceProvider service={service}>
<tr>
<td className="service-label">{service.label} </td>
<ModulePermissionsServiceFullAccess />
<ModulePermissionsTableColumns columns={columns} />
<ModulePermissionExtraPermissions />
</tr>
</ModulePermissionsServiceProvider>
))}
</tbody>
);
}
/**
* Module permissions table.
* @returns {React.JSX}
*/
function ModulePermissionsTable() {
return (
<ModulePermissionsTableRoot>
<ModulePermissionsTableHead />
<ModulePermissionsTableBody />
</ModulePermissionsTableRoot>
);
}
/**
* Module vertical table cells.
* @returns {React.JSX}
*/
function ModuleVerticalTableCells() {
const { service } = useModulePermissionsServiceProvider();
return (
<td class={'permissions'}>
{service.permissions.map((permission) => (
<div>
<Field
name={`permissions.${service.subject}/${permission.key}`}
type="checkbox"
>
{({ form, field }) => (
<PermissionCheckbox
inline={true}
label={permission.label}
{...field}
onChange={handleCheckboxPermissionChange(
form,
permission,
service,
)}
/>
)}
</Field>
</div>
))}
</td>
);
}
/**
* Module permissions vertical services.
* @returns {React.JSX}
*/
function ModulePermissionsVerticalServices() {
const { module } = useModulePermissionsProvider();
return (
<ModulePermissionsVerticalServicesRoot>
<ModulePermissionsVerticalTable>
<tbody>
{module.services.map((service) => (
<ModulePermissionsServiceProvider service={service}>
<tr>
<td class={'service-label'}>{service.label} </td>
<ModuleVerticalTableCells />
</tr>
</ModulePermissionsServiceProvider>
))}
</tbody>
</ModulePermissionsVerticalTable>
</ModulePermissionsVerticalServicesRoot>
);
}
/**
* Module permissions body.
* @returns {React.JSX}
*/
function ModulePermissionsBody() {
const { module } = useModulePermissionsProvider();
return (
<ModulePermissionBodyRoot>
<Choose>
<Choose.When
condition={module.type === ModulePermissionsStyle.Vertical}
>
<ModulePermissionsVerticalServices />
</Choose.When>
<Choose.When condition={module.type === ModulePermissionsStyle.Columns}>
<ModulePermissionsTable />
</Choose.When>
</Choose>
</ModulePermissionBodyRoot>
);
}
/**
* Permissions module.
* @returns {React.JSX}
*/
function ModulePermissions({ module }) {
return (
<ModulePermissionsRoot>
<ModulePermissionsProvider module={module}>
<ModulePermissionHead>
<ModulePermissionTitle>{module.label} </ModulePermissionTitle>
</ModulePermissionHead>
<ModulePermissionsBody />
</ModulePermissionsProvider>
</ModulePermissionsRoot>
);
}
/**
* Permissions modules list.
* @return {React.JSX}
*/
export const RolesPermissionList = () => {
const permissions = getPermissionsSchema();
return (
<ModulesPermission>
{permissions.map((module) => (
<ModulePermissions module={module} />
))}
</ModulesPermission>
);
};
const PermissionCheckbox = styled(Checkbox)`
&.bp3-control.bp3-checkbox .bp3-control-indicator {
border-radius: 2px;
border-color: #555;
&,
&:before {
width: 15px;
height: 15px;
}
}
`;
const ModulesPermission = styled.div``;
const ModulePermissionsRoot = styled(Card)`
padding: 0 !important;
`;
const ModulePermissionHead = styled.div`
border-bottom: 1px solid #d9d9d9;
height: 38px;
padding: 0 15px;
display: flex;
`;
const ModulePermissionTitle = styled.div`
font-weight: 500;
font-size: 16px;
line-height: 38px;
color: #878787;
`;
const ModulePermissionBodyRoot = styled.div``;
const ModulePermissionsTableRoot = styled.table`
border-spacing: 0;
thead {
tr th {
font-weight: 400;
vertical-align: top;
&.full,
&.permission {
min-width: 70px;
}
&.full {
background-color: #fcfcfc;
}
}
}
thead,
tbody {
tr td,
tr th {
border-bottom: 1px solid #eee;
border-left: 1px solid #eee;
padding: 10px;
&:first-of-type {
border-left: 0;
}
}
tr:last-of-type td {
border-bottom: 0;
}
tr td:last-of-type,
tr th:last-of-type {
width: 100%;
}
}
tbody {
tr td.service-label {
min-width: 250px;
}
tr td {
.bp3-control.bp3-inline {
margin: 0;
}
&.full-access-permission {
background-color: #fcfcfc;
}
&.full-access-permission,
&.permission-checkbox {
text-align: center;
}
}
}
`;
const MorePermissionsLink = styled(ButtonLink)`
font-size: 12px;
`;
const ExtraPermissionsRoot = styled.div`
display: flex;
flex-direction: column;
padding: 15px;
`;
const ModulePermissionsVerticalServicesRoot = styled.div``;
const ModulePermissionsVerticalTable = styled.table`
border-spacing: 0;
tbody {
tr td {
padding: 10px;
vertical-align: top;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
&.service-label {
min-width: 250px;
color: #333;
}
&:first-of-type {
border-left: 0;
}
&.permissions {
width: 100%;
}
}
tr:last-of-type td {
border-bottom: 0;
}
}
`;

View File

@@ -0,0 +1,313 @@
// @ts-nocheck
import { chain, isEmpty, castArray, memoize } from 'lodash';
import * as R from 'ramda';
import { DepGraph } from 'dependency-graph';
import {
getPermissionsSchema,
getPermissionsSchemaService,
getPermissionsSchemaServices,
} from '@/constants/permissionsSchema';
export const FULL_ACCESS_CHECKBOX_STATE = {
INDETARMINE: -1,
ON: true,
OFF: false,
};
/**
* Transformes the permissions object to array.
* @returns
*/
export const transformToArray = ({ permissions }) => {
return Object.keys(permissions).map((index) => {
const [value, key] = index.split('/');
return {
subject: value,
ability: key,
value: permissions[index],
};
});
};
function transformPermissions(permissions) {
return Object.keys(permissions).map((permissionKey) => {
const [subject, key] = permissionKey.split('/');
const value = permissions[permissionKey];
return { key, subject, value };
});
}
/**
* Transformes permissions array to object.
* @param {*} permissions -
* @returns
*/
export const transformPermissionsToObject = (permissions) => {
const output = {};
permissions.forEach((item) => {
output[`${item.subject}/${item.ability}`] = !!item.value;
});
return output;
};
/**
*
* @param {*} role
* @returns
*/
export const transformToObject = (role) => {
const permissions = transformPermissionsToObject(role.permissions);
const serviceFullAccess = getInitialServicesFullAccess(permissions);
return {
role_name: role.name,
role_description: role.description,
permissions,
serviceFullAccess,
};
};
export const getDefaultValuesFromSchema = (schema) => {
return schema
.map((item) => {
const abilities = [
...(item.abilities || []),
...(item.extra_abilities || []),
];
return abilities
.filter((ability) => ability.default)
.map((ability) => ({
subject: item.subject,
ability: ability.key,
value: ability.default,
}));
})
.flat();
};
/**
* Retrieve initial values of full access services.
* @param {*} formPermissions
* @returns
*/
export const getInitialServicesFullAccess = (formPermissions) => {
const services = getPermissionsSchemaServices();
return chain(services)
.map((service) => {
const { subject } = service;
const isFullChecked = isServiceFullChecked(subject, formPermissions);
const isFullUnchecked = isServiceFullUnchecked(subject, formPermissions);
const value = detarmineCheckboxState(isFullChecked, isFullUnchecked);
return [service.subject, value];
})
.fromPairs()
.value();
};
/**
*
* @param {*} schema
* @returns
*/
export const getNewRoleInitialValues = (schema) => {
const permissions = transformPermissionsToObject(
getDefaultValuesFromSchema(schema),
);
const serviceFullAccess = getInitialServicesFullAccess(permissions);
return {
permissions,
serviceFullAccess,
};
};
/**
*
* @param {*} service
* @param {*} columnKey
* @returns
*/
export function getSerivceColumnPermission(service, columnKey) {
return service.permissions.find((permission) => {
return permission.relatedColumn === columnKey;
});
}
/**
*
* @param {*} service
* @returns
*/
export function getServiceExtraPermissions(service) {
return service.permissions.filter((permission) => {
return !permission.relatedColumn;
});
}
/**
* Detarmines the given service subject is full permissions checked.
*/
export function isServiceFullChecked(subject, permissions) {
const serviceSchema = getPermissionsSchemaService(subject);
return serviceSchema.permissions.every(
(permission) => permissions[`${subject}/${permission.key}`],
);
}
/**
* Detarmines the given service subject is fully associated permissions unchecked.
* @param {string} subject -
* @param {Object} permissionsMap -
*/
export function isServiceFullUnchecked(subject, permissionsMap) {
const serviceSchema = getPermissionsSchemaService(subject);
return serviceSchema.permissions.every(
(permission) => !permissionsMap[`${subject}/${permission.key}`],
);
}
/**
* Handles permission checkbox change.
*/
export const handleCheckboxPermissionChange = R.curry(
(form, permission, service, event) => {
const { subject } = service;
const isChecked = event.currentTarget.checked;
const permissionsGraph = memoizedPermissionsGraph();
const dependencies = isChecked
? permissionsGraph.dependenciesOf(`${subject}/${permission.key}`)
: permissionsGraph.dependantsOf(`${subject}/${permission.key}`);
const newDependsPermiss = chain(dependencies)
.map((dep) => [dep, isChecked])
.fromPairs()
.value();
const newValues = {
...form.values,
permissions: {
...form.values.permissions,
[`${subject}/${permission.key}`]: isChecked,
...newDependsPermiss,
},
};
const isFullChecked = isServiceFullChecked(subject, newValues.permissions);
const isFullUnchecked = isServiceFullUnchecked(
subject,
newValues.permissions,
);
form.setFieldValue(`permissions.${subject}/${permission.key}`, isChecked);
form.setFieldValue(
`serviceFullAccess.${subject}`,
detarmineCheckboxState(isFullChecked, isFullUnchecked),
);
dependencies.forEach((depKey) => {
form.setFieldValue(`permissions.${depKey}`, isChecked);
});
},
);
/**
* Detarmines the permission checkbox state.
* @param {boolean} isFullChecked
* @param {boolean} isFullUnchecked
* @returns {FULL_ACCESS_CHECKBOX_STATE}
*/
function detarmineCheckboxState(isFullChecked, isFullUnchecked) {
return isFullChecked
? FULL_ACCESS_CHECKBOX_STATE.ON
: isFullUnchecked
? FULL_ACCESS_CHECKBOX_STATE.OFF
: FULL_ACCESS_CHECKBOX_STATE.INDETARMINE;
}
/**
* Retreive the service all permissions paths.
* @param {string} subject
* @returns {string[]}
*/
export function getServiceAllPermissionsPaths(subject) {
const service = getPermissionsSchemaService(subject);
return service.permissions.map(
(perm) => `permissions.${subject}/${perm.key}`,
);
}
/**
* Handle full access service checkbox change.
*/
export const handleCheckboxFullAccessChange = R.curry(
(service, form, event) => {
const isChecked = event.currentTarget.checked;
const permsPaths = getServiceAllPermissionsPaths(service.subject);
form.setFieldValue(`serviceFullAccess.${service.subject}`, isChecked);
permsPaths.forEach((permissionPath) => {
form.setFieldValue(
permissionPath,
isChecked
? FULL_ACCESS_CHECKBOX_STATE.ON
: FULL_ACCESS_CHECKBOX_STATE.OFF,
);
});
},
);
/**
* Retrieves all flatten modules permissions.
*/
export function getAllFlattenPermissionsSchema() {
const permissions = getPermissionsSchema();
return chain(permissions)
.map((module) => module.services)
.flatten()
.map((module) =>
module.permissions.map((permission) => ({
subject: module.subject,
...permission,
})),
)
.flatten()
.value();
}
/**
* Retrieve the permissions schema dependencies graph.
* @returns {DepGraph}
*/
export const getPermissionsSchemaGraph = () => {
const graph = new DepGraph();
const permissions = getAllFlattenPermissionsSchema();
permissions.forEach((permission) => {
graph.addNode(`${permission.subject}/${permission.key}`, permission);
});
const nodesOrder = graph.overallOrder();
nodesOrder.forEach((key) => {
const node = graph.getNodeData(key);
if (isEmpty(node.depend)) return;
const depends = castArray(node.depend);
depends.forEach((dependRelation) => {
const subject = dependRelation.subject || node.subject;
graph.addDependency(key, `${subject}/${dependRelation.key}`);
});
});
return graph;
};
const memoizedPermissionsGraph = memoize(getPermissionsSchemaGraph);

View File

@@ -0,0 +1,78 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { DataTable, AppToaster, TableSkeletonRows } from '@/components';
import { useRolesTableColumns, ActionsMenu } from './components';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import { useRolesContext } from './RolesListProvider';
import { compose } from '@/utils';
/**
* Roles data table.
*/
function RolesDataTable({
// #withAlertsActions
openAlert,
}) {
// History context.
const history = useHistory();
// Retrieve roles table columns
const columns = useRolesTableColumns();
// Roles table context.
const { roles, isRolesFetching, isRolesLoading } = useRolesContext();
// handles delete the given role.
const handleDeleteRole = ({ id, predefined }) => {
if (predefined) {
AppToaster.show({
message: intl.get('roles.error.you_cannot_delete_predefined_roles'),
intent: Intent.DANGER,
});
} else {
openAlert('role-delete', { roleId: id });
}
};
// Handles the edit of the given role.
const handleEditRole = ({ id, predefined }) => {
if (predefined) {
AppToaster.show({
message: intl.get('roles.error.you_cannot_edit_predefined_roles'),
intent: Intent.DANGER,
});
} else {
history.push(`/preferences/roles/${id}`);
}
};
return (
<RolesTable
columns={columns}
data={roles}
loading={isRolesLoading}
headerLoading={isRolesFetching}
progressBarLoading={isRolesFetching}
TableLoadingRenderer={TableSkeletonRows}
ContextMenu={ActionsMenu}
payload={{
onDeleteRole: handleDeleteRole,
onEditRole: handleEditRole,
}}
/>
);
}
const RolesTable = styled(DataTable)`
.table .tr {
min-height: 42px;
}
`;
export default compose(withAlertsActions)(RolesDataTable);

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import React from 'react';
import { RolesListProvider } from './RolesListProvider';
import RolesDataTable from './RolesDataTable';
/**
* Roles list.
*/
function RolesListPrefernces() {
return (
<RolesListProvider>
<RolesDataTable />
</RolesListProvider>
);
}
export default RolesListPrefernces;

View File

@@ -0,0 +1,40 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { useRoles } from '@/hooks/query';
const RolesListContext = React.createContext();
/**
* Roles list provider.
*/
function RolesListProvider({ ...props }) {
// Fetch roles list.
const {
data: roles,
isFetching: isRolesFetching,
isLoading: isRolesLoading,
} = useRoles();
// Provider state.
const provider = {
roles,
isRolesFetching,
isRolesLoading,
};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_USERS,
)}
>
<RolesListContext.Provider value={provider} {...props} />
</div>
);
}
const useRolesContext = () => React.useContext(RolesListContext);
export { RolesListProvider, useRolesContext };

View File

@@ -0,0 +1,60 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Intent, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
import { safeCallback } from '@/utils';
import { Icon } from '@/components';
/**
* Context menu of roles.
*/
export function ActionsMenu({
payload: { onDeleteRole, onEditRole },
row: { original },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('roles.edit_roles')}
onClick={safeCallback(onEditRole, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={intl.get('roles.delete_roles')}
onClick={safeCallback(onDeleteRole, original)}
intent={Intent.DANGER}
/>
</Menu>
);
}
/**
* Retrieve Roles table columns.
* @returns
*/
export function useRolesTableColumns() {
return React.useMemo(
() => [
{
id: 'name',
Header: intl.get('roles.column.name'),
accessor: 'name',
className: 'name',
width: '80',
textOverview: true,
},
{
id: 'description',
Header: intl.get('roles.column.description'),
accessor: 'description',
className: 'description',
width: '180',
textOverview: true,
},
],
[],
);
}

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
// handle delete errors.
export const handleDeleteErrors = (errors) => {
if (errors.find((error) => error.type === 'ROLE_PREFINED')) {
AppToaster.show({
message: intl.get('roles.error.role_is_predefined'),
intent: Intent.DANGER,
});
}
if (errors.find((error) => error.type === 'INVALIDATE_PERMISSIONS')) {
AppToaster.show({
message: intl.get('roles.error.the_submit_role_has_invalid_permissions'),
intent: Intent.DANGER,
});
}
if (
errors.find(
(error) => error.type === 'CANNOT_DELETE_ROLE_ASSOCIATED_TO_USERS',
)
) {
AppToaster.show({
message: intl.get(
'roles.error.you_cannot_delete_role_that_associated_to_users',
),
intent: Intent.DANGER,
});
}
};

View File

@@ -0,0 +1,53 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import styled from 'styled-components';
import { Tabs, Tab } from '@blueprintjs/core';
import '@/style/pages/Preferences/Users.scss';
import { Card } from '@/components';
import { CLASSES } from '@/constants/classes';
import PreferencesSubContent from '@/components/Preferences/PreferencesSubContent';
import withUserPreferences from '@/containers/Preferences/Users/withUserPreferences';
/**
* Preferences page - Users page.
*/
function UsersPreferences({ openDialog }) {
const onChangeTabs = (currentTabId) => {};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_USERS,
)}
>
<UsersPereferencesCard>
<div className={classNames(CLASSES.PREFERENCES_PAGE_TABS)}>
<Tabs animate={true} onChange={onChangeTabs}>
<Tab
id="users"
title={intl.get('users')}
panel={<PreferencesSubContent preferenceTab="users" />}
/>
<Tab
id="roles"
title={intl.get('roles')}
panel={<PreferencesSubContent preferenceTab="roles" />}
/>
</Tabs>
</div>
</UsersPereferencesCard>
</div>
);
}
export default withUserPreferences(UsersPreferences);
const UsersPereferencesCard = styled(Card)`
padding: 0;
`;

View File

@@ -0,0 +1,41 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Button, Intent } from '@blueprintjs/core';
import { Icon, FormattedMessage as T } from '@/components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function UsersActions({ openDialog, closeDialog }) {
const history = useHistory();
const onClickNewUser = () => {
openDialog('invite-user');
};
const onClickNewRole = () => {
history.push('/preferences/roles');
};
return (
<div className="preferences-actions">
<Button
icon={<Icon icon="plus" iconSize={12} />}
onClick={onClickNewUser}
intent={Intent.PRIMARY}
>
<T id={'invite_user'} />
</Button>
<Button
icon={<Icon icon="plus" iconSize={12} />}
onClick={onClickNewRole}
>
<T id={'new_role'} />
</Button>
</div>
);
}
export default compose(withDialogActions)(UsersActions);

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import React from 'react';
const UserDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Users/UserDeleteAlert'),
);
const UserActivateAlert = React.lazy(
() => import('@/containers/Alerts/Users/UserActivateAlert'),
);
const UserInactivateAlert = React.lazy(
() => import('@/containers/Alerts/Users/UserInactivateAlert'),
);
export default [
{ name: 'user-delete', component: UserDeleteAlert },
{ name: 'user-activate', component: UserActivateAlert },
{ name: 'user-inactivate', component: UserInactivateAlert },
];

View File

@@ -0,0 +1,107 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { compose } from '@/utils';
import { DataTable, TableSkeletonRows, AppToaster } from '@/components';
import { useResendInvitation } from '@/hooks/query';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { ActionsMenu, useUsersListColumns } from './components';
import { useUsersListContext } from './UsersProvider';
import { Intent } from '@blueprintjs/core';
/**
* Users datatable.
*/
function UsersDataTable({
// #withDialogActions
openDialog,
// #withAlertActions
openAlert,
}) {
// Handle edit user action.
const handleEditUserAction = useCallback(
(user) => {
openDialog('user-form', { action: 'edit', userId: user.id });
},
[openDialog],
);
// Handle inactivate user action.
const handleInactivateUser = useCallback(
(user) => {
openAlert('user-inactivate', { userId: user.id });
},
[openAlert],
);
// Handle activate user action.
const handleActivateuser = useCallback(
(user) => {
openAlert('user-activate', { userId: user.id });
},
[openAlert],
);
// Handle delete user action.
const handleDeleteUser = useCallback(
(user) => {
openAlert('user-delete', { userId: user.id });
},
[openAlert],
);
const { mutateAsync: resendInviation } = useResendInvitation();
const handleResendInvitation = useCallback((user) => {
resendInviation(user.id)
.then(() => {
AppToaster.show({
message: 'User invitation has been re-sent to the user.',
intent: Intent.SUCCESS,
});
})
.catch(
({
response: {
data: { errors },
},
}) => {
if (errors.find((e) => e.type === 'USER_RECENTLY_INVITED')) {
AppToaster.show({
message:
'This person was recently invited. No need to invite them again just yet.',
intent: Intent.DANGER,
});
}
},
);
});
// Users list columns.
const columns = useUsersListColumns();
// Users list context.
const { users, isUsersLoading, isUsersFetching } = useUsersListContext();
return (
<DataTable
columns={columns}
data={users}
loading={isUsersLoading}
headerLoading={isUsersLoading}
progressBarLoading={isUsersFetching}
TableLoadingRenderer={TableSkeletonRows}
noInitialFetch={true}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditUserAction,
onActivate: handleActivateuser,
onInactivate: handleInactivateUser,
onDelete: handleDeleteUser,
onResendInvitation: handleResendInvitation,
}}
/>
);
}
export default compose(withDialogActions, withAlertActions)(UsersDataTable);

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import intl from 'react-intl-universal';
import { UsersListProvider } from './UsersProvider';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import UsersDataTable from './UsersDataTable';
import { compose } from '@/utils';
/**
* Users list.
*/
function UsersListPreferences({
// #withDashboardActions
changePreferencesPageTitle,
}) {
useEffect(() => {
changePreferencesPageTitle(intl.get('users'));
}, [changePreferencesPageTitle]);
return (
<UsersListProvider>
<UsersDataTable />
</UsersListProvider>
);
}
export default compose(withDashboardActions)(UsersListPreferences);

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { useUsers } from '@/hooks/query';
const UsersListContext = createContext();
/**
* Users list provider.
*/
function UsersListProvider(props) {
const { data: users, isLoading, isFetching } = useUsers();
const state = {
isUsersLoading: isLoading,
isUsersFetching: isFetching,
users,
};
return (
<UsersListContext.Provider value={state} {...props} />
);
}
const useUsersListContext = () => React.useContext(UsersListContext);
export { UsersListProvider, useUsersListContext };

View File

@@ -0,0 +1,162 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { FormattedMessage as T, Icon, If } from '@/components';
import {
Intent,
Button,
Popover,
Menu,
MenuDivider,
Tag,
MenuItem,
Position,
} from '@blueprintjs/core';
import { safeCallback, firstLettersArgs } from '@/utils';
/**
* Avatar cell.
*/
function AvatarCell(row) {
return <span className={'avatar'}>{firstLettersArgs(row.email)}</span>;
}
/**
* Users table actions menu.
*/
export function ActionsMenu({
row: { original },
payload: { onEdit, onInactivate, onActivate, onDelete, onResendInvitation },
}) {
return (
<Menu>
<If condition={original.invite_accepted_at}>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_user')}
onClick={safeCallback(onEdit, original)}
/>
<MenuDivider />
{original.active ? (
<MenuItem
text={intl.get('inactivate_user')}
onClick={safeCallback(onInactivate, original)}
icon={<Icon icon="pause-16" iconSize={16} />}
/>
) : (
<MenuItem
text={intl.get('activate_user')}
onClick={safeCallback(onActivate, original)}
icon={<Icon icon="play-16" iconSize={16} />}
/>
)}
</If>
<If condition={!original.invite_accepted_at}>
<MenuItem
text={'Resend invitation'}
onClick={safeCallback(onResendInvitation, original)}
icon={<Icon icon="send" iconSize={16} />}
/>
</If>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={intl.get('delete_user')}
onClick={safeCallback(onDelete, original)}
intent={Intent.DANGER}
/>
</Menu>
);
}
/**
* Status accessor.
*/
function StatusAccessor(user) {
return !user.is_invite_accepted ? (
<Tag minimal={true}>
<T id={'inviting'} />
</Tag>
) : user.active ? (
<Tag intent={Intent.SUCCESS} minimal={true}>
<T id={'activate'} />
</Tag>
) : (
<Tag intent={Intent.WARNING} minimal={true}>
<T id={'inactivate'} />
</Tag>
);
}
/**
* Actions cell.
*/
function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
function FullNameAccessor(user) {
return user.is_invite_accepted ? user.full_name : user.email;
}
export const useUsersListColumns = () => {
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
accessor: AvatarCell,
width: 40,
},
{
id: 'full_name',
Header: intl.get('full_name'),
accessor: FullNameAccessor,
width: 150,
},
{
id: 'email',
Header: intl.get('email'),
accessor: 'email',
width: 150,
},
{
id: 'role_name',
Header: intl.get('users.column.role_name'),
accessor: 'role.name',
width: 120,
},
// {
// id: 'phone_number',
// Header: intl.get('phone_number'),
// accessor: 'phone_number',
// width: 120,
// },
{
id: 'status',
Header: intl.get('status'),
accessor: StatusAccessor,
width: 80,
className: 'status',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[],
);
};

View File

@@ -0,0 +1,14 @@
// @ts-nocheck
import { connect } from 'react-redux';
import t from '@/store/types';
export const mapStateToProps = (state, props) => {};
export const mapDispatchToProps = (dispatch) => ({
openDialog: (name, payload) =>
dispatch({ type: t.OPEN_DIALOG, name, payload }),
closeDialog: (name, payload) =>
dispatch({ type: t.CLOSE_DIALOG, name, payload }),
});
export default connect(null, mapDispatchToProps);

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import '@/style/pages/Preferences/warehousesList.scss';
import WarehousesGrid from './WarehousesGrid';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { compose } from '@/utils';
/**
* Warehouses.
* @returns
*/
function Warehouses({
// #withDashboardActions
changePreferencesPageTitle,
}) {
React.useEffect(() => {
changePreferencesPageTitle(intl.get('warehouses.label'));
}, [changePreferencesPageTitle]);
return (
<React.Fragment>
<WarehousesGrid />
</React.Fragment>
);
}
export default compose(withDashboardActions)(Warehouses);

View File

@@ -0,0 +1,36 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { Features } from '@/constants';
import { FeatureCan, FormattedMessage as T, Icon } from '@/components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
/**
* Warehouse actions.
*/
function WarehousesActions({
//#ownProps
openDialog,
}) {
const handleClickNewWarehouse = () => {
openDialog('warehouse-form');
};
return (
<React.Fragment>
<FeatureCan feature={Features.Warehouses}>
<Button
icon={<Icon icon="plus" iconSize={12} />}
onClick={handleClickNewWarehouse}
intent={Intent.PRIMARY}
>
<T id={'warehouses.label.new_warehouse'} />
</Button>
</FeatureCan>
</React.Fragment>
);
}
export default compose(withDialogActions)(WarehousesActions);

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import React from 'react';
const WarehouseDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Warehouses/WarehouseDeleteAlert'),
);
/**
* Warehouses alerts.
*/
export default [{ name: 'warehouse-delete', component: WarehouseDeleteAlert }];

View File

@@ -0,0 +1,41 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, EmptyStatus } from '@/components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function WarehousesEmptyStatus({
// #withDialogActions
openDialog,
}) {
// Handle activate action warehouse.
const handleActivateWarehouse = () => {
openDialog('warehouse-activate', {});
};
return (
<EmptyStatus
title={<T id={'warehouses.empty_status.title'} />}
description={
<p>
<T id={'warehouses.empty_status.description'} />
</p>
}
action={
<React.Fragment>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={handleActivateWarehouse}
>
<T id={'warehouses.activate_button'} />
</Button>
</React.Fragment>
}
/>
);
}
export default compose(withDialogActions)(WarehousesEmptyStatus);

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import React from 'react';
import WarehousesEmptyStatus from './WarehousesEmptyStatus';
import { useWarehousesContext } from './WarehousesProvider';
import { WarehousesList, WarehousesSkeleton } from './components';
import WarehousesGridItems from './WarehousesGridItems';
/**
* Warehouses grid.
*/
export default function WarehousesGrid() {
// Retrieve list context.
const {
warehouses,
isWarehouesLoading,
isEmptyStatus,
} = useWarehousesContext();
return (
<React.Fragment>
<WarehousesList>
{isWarehouesLoading ? (
<WarehousesSkeleton />
) : isEmptyStatus ? (
<WarehousesEmptyStatus />
) : (
<WarehousesGridItems warehouses={warehouses} />
)}
</WarehousesList>
</React.Fragment>
);
}

View File

@@ -0,0 +1,84 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { ContextMenu2 } from '@blueprintjs/popover2';
import { AppToaster } from '@/components';
import { WarehouseContextMenu, WarehousesGridItemBox } from './components';
import { useMarkWarehouseAsPrimary } from '@/hooks/query';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
/**
* warehouse grid item.
*/
function WarehouseGridItem({
// #withAlertsActions
openAlert,
// #withDialogActions
openDialog,
warehouse,
}) {
const { mutateAsync: markWarehouseAsPrimaryMutate } =
useMarkWarehouseAsPrimary();
// Handle edit warehouse.
const handleEditWarehouse = () => {
openDialog('warehouse-form', { warehouseId: warehouse.id, action: 'edit' });
};
// Handle delete warehouse.
const handleDeleteWarehouse = () => {
openAlert('warehouse-delete', { warehouseId: warehouse.id });
};
// Handle mark primary warehouse.
const handleMarkWarehouseAsPrimary = () => {
markWarehouseAsPrimaryMutate(warehouse.id).then(() => {
AppToaster.show({
message: intl.get('warehouse.alert.mark_primary_message'),
intent: Intent.SUCCESS,
});
});
};
return (
<ContextMenu2
content={
<WarehouseContextMenu
warehouse={warehouse}
onEditClick={handleEditWarehouse}
onDeleteClick={handleDeleteWarehouse}
onMarkPrimary={handleMarkWarehouseAsPrimary}
/>
}
>
<WarehousesGridItemBox
title={warehouse.name}
code={warehouse.code}
city={warehouse.city}
country={warehouse.country}
email={warehouse.email}
phoneNumber={warehouse.phone_number}
primary={warehouse.primary}
/>
</ContextMenu2>
);
}
const WarehousesGridItem = compose(
withAlertsActions,
withDialogActions,
)(WarehouseGridItem);
/**
* warehouses grid items,
*/
export default function WarehousesGridItems({ warehouses }) {
return warehouses.map((warehouse) => (
<WarehousesGridItem warehouse={warehouse} />
));
}

View File

@@ -0,0 +1,56 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { useWarehouses } from '@/hooks/query';
import { isEmpty } from 'lodash';
import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state';
const WarehousesContext = React.createContext();
/**
* Warehouses data provider.
*/
function WarehousesProvider({ query, ...props }) {
// Features guard.
const { featureCan } = useFeatureCan();
const isWarehouseFeatureCan = featureCan(Features.Warehouses);
// Fetch warehouses list.
const { data: warehouses, isLoading: isWarehouesLoading } = useWarehouses(
query,
{ enabled: isWarehouseFeatureCan },
);
// Detarmines the datatable empty status.
const isEmptyStatus = isEmpty(warehouses) || !isWarehouseFeatureCan;
// Provider state.
const provider = {
warehouses,
isWarehouesLoading,
isEmptyStatus,
};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_WAREHOUSES,
)}
>
<React.Fragment>
<WarehousesContext.Provider value={provider} {...props} />
</React.Fragment>
</div>
);
}
const useWarehousesContext = () => React.useContext(WarehousesContext);
export { WarehousesProvider, useWarehousesContext };
const WarehousePreference = styled.div``;

View File

@@ -0,0 +1,189 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import {
Menu,
MenuItem,
MenuDivider,
Intent,
Classes,
} from '@blueprintjs/core';
import { Icon, If } from '@/components';
import { safeCallback } from '@/utils';
const WAREHOUSES_SKELETON_N = 4;
/**
* Warehouse grid item box context menu.
* @returns {JSX.Element}
*/
export function WarehouseContextMenu({
onEditClick,
onDeleteClick,
onMarkPrimary,
warehouse,
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('warehouses.action.edit_warehouse')}
onClick={safeCallback(onEditClick)}
/>
<If condition={!warehouse.primary}>
<MenuItem
icon={<Icon icon={'check'} iconSize={18} />}
text={intl.get('warehouses.action.make_as_parimary')}
onClick={safeCallback(onMarkPrimary)}
/>
</If>
<MenuDivider />
<MenuItem
text={intl.get('warehouses.action.delete_warehouse')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDeleteClick)}
/>
</Menu>
);
}
/**
* Warehouse grid item box skeleton.
* @returns {JSX.Element}
*/
function WarehouseGridItemSkeletonBox() {
return (
<WarehouseBoxRoot>
<WarehouseHeader>
<WarehouseTitle className={Classes.SKELETON}>X</WarehouseTitle>
<WarehouseCode className={Classes.SKELETON}>X</WarehouseCode>
</WarehouseHeader>
<WarehouseContent>
<WarehouseItem className={Classes.SKELETON}>X</WarehouseItem>
<WarehouseItem className={Classes.SKELETON}>X</WarehouseItem>
</WarehouseContent>
</WarehouseBoxRoot>
);
}
/**
* Warehouse grid item box.
* @returns {JSX.Element}
*/
export function WarehousesGridItemBox({
title,
code,
city,
country,
email,
phoneNumber,
primary,
}) {
return (
<WarehouseBoxRoot>
<WarehouseHeader>
<WarehouseTitle>
{title} {primary && <Icon icon={'star-18dp'} iconSize={16} />}
</WarehouseTitle>
<WarehouseCode>{code}</WarehouseCode>
<WarehouseIcon>
<Icon icon="warehouse-16" iconSize={20} />
</WarehouseIcon>
</WarehouseHeader>
<WarehouseContent>
{city && <WarehouseItem>{city}</WarehouseItem>}
{country && <WarehouseItem>{country}</WarehouseItem>}
{email && <WarehouseItem>{email}</WarehouseItem>}
{phoneNumber && <WarehouseItem>{phoneNumber}</WarehouseItem>}
</WarehouseContent>
</WarehouseBoxRoot>
);
}
export function WarehousesSkeleton() {
return [...Array(WAREHOUSES_SKELETON_N)].map((key, value) => (
<WarehouseGridItemSkeletonBox />
));
}
export const WarehousesList = styled.div`
display: flex;
flex-wrap: wrap;
margin: 15px;
height: 100%;
`;
export const WarehouseBoxRoot = styled.div`
display: flex;
flex-direction: column;
flex-shrink: 0;
border-radius: 5px;
border: 1px solid #c8cad0;
background: #fff;
margin: 5px 5px 8px;
width: 200px;
height: 160px;
transition: all 0.1s ease-in-out;
padding: 12px;
position: relative;
&:hover {
border-color: #0153cc;
}
`;
export const WarehouseHeader = styled.div`
position: relative;
padding-right: 24px;
padding-top: 2px;
`;
export const WarehouseTitle = styled.div`
font-size: 14px;
font-style: inherit;
color: #000;
white-space: nowrap;
font-weight: 500;
line-height: 1;
.bp3-icon {
margin: 0;
margin-left: 2px;
vertical-align: top;
color: #e1b31d;
}
`;
const WarehouseCode = styled.div`
display: block;
font-size: 11px;
color: #6b7176;
margin-top: 4px;
`;
const WarehouseIcon = styled.div`
position: absolute;
top: 0;
color: #abb3bb;
right: 0;
`;
const WarehouseContent = styled.div`
width: 100%;
margin-top: auto;
`;
const WarehouseItem = styled.div`
font-size: 11px;
color: #000;
text-overflow: ellipsis;
overflow: hidden;
&:not(:last-of-type) {
margin-bottom: 5px;
}
`;

View File

@@ -0,0 +1,16 @@
// @ts-nocheck
import React from 'react';
import { WarehousesProvider } from './WarehousesProvider';
import Warehouses from './Warehouses';
/**
* Warehouses Preferences.
* @returns
*/
export default function WarehousesPerences() {
return (
<WarehousesProvider>
<Warehouses />
</WarehousesProvider>
);
}

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from '@/components';
/**
* Handle delete errors.
*/
export const handleDeleteErrors = (errors) => {
if (
errors.find((error) => error.type === 'COULD_NOT_DELETE_ONLY_WAERHOUSE')
) {
AppToaster.show({
message: intl.get('warehouse.error.could_not_delete_only_waerhouse'),
intent: Intent.DANGER,
});
}
if (errors.some((e) => e.type === 'WAREHOUSE_HAS_ASSOCIATED_TRANSACTIONS')) {
AppToaster.show({
message: intl.get(
'warehouse.error.warehouse_has_associated_transactions',
),
intent: Intent.DANGER,
});
}
};