WIP Fix & last tasks

This commit is contained in:
elforjani3
2020-05-31 21:33:45 +02:00
parent 2e8ffa2aa9
commit 41d106e1a7
30 changed files with 1483 additions and 948 deletions

View File

@@ -3,25 +3,26 @@ import { Route, Switch } from 'react-router-dom';
import DashboardTopbarUser from 'components/Dashboard/TopbarUser'; import DashboardTopbarUser from 'components/Dashboard/TopbarUser';
import UsersActions from 'containers/Preferences/Users/UsersActions'; import UsersActions from 'containers/Preferences/Users/UsersActions';
import CurrenciesActions from 'containers/Preferences/Currencies/CurrenciesActions'; import CurrenciesActions from 'containers/Preferences/Currencies/CurrenciesActions';
import { compose } from 'utils';
import { connect } from 'react-redux';
function PreferencesTopbar({ pageTitle }) {
export default function PreferencesTopbar() {
return ( return (
<div class="dashboard__preferences-topbar"> <div class="dashboard__preferences-topbar">
<h2>Accounts</h2> {/* <h2>Accounts</h2> */}
<div class="dashboard__title">
<h2>{pageTitle}</h2>
</div>
<div class="preferences__topbar-actions"> <div class="preferences__topbar-actions">
<Route pathname="/preferences"> <Route pathname="/preferences">
<Switch> <Switch>
<Route <Route exact path={'/preferences/users'} component={UsersActions} />
exact
path={'/preferences/users'}
component={UsersActions} />
<Route <Route
exact exact
path={'/preferences/currencies'} path={'/preferences/currencies'}
component={CurrenciesActions} /> component={CurrenciesActions}
/>
</Switch> </Switch>
</Route> </Route>
</div> </div>
@@ -31,4 +32,10 @@ export default function PreferencesTopbar() {
</div> </div>
</div> </div>
); );
} }
const mapStateToProps = (state) => ({
pageTitle: state.dashboard.preferencesPageTitle,
});
export default compose(connect(mapStateToProps))(PreferencesTopbar);

View File

@@ -1,27 +1,34 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import t from 'store/types'; import t from 'store/types';
const mapActionsToProps = (dispatch) => ({ const mapActionsToProps = (dispatch) => ({
changePageTitle: (pageTitle) => dispatch({ changePageTitle: (pageTitle) =>
type: t.CHANGE_DASHBOARD_PAGE_TITLE, pageTitle dispatch({
}), type: t.CHANGE_DASHBOARD_PAGE_TITLE,
pageTitle,
}),
changePageSubtitle: (pageSubtitle) => dispatch({ changePageSubtitle: (pageSubtitle) =>
type: t.ALTER_DASHBOARD_PAGE_SUBTITLE, pageSubtitle, dispatch({
}), type: t.ALTER_DASHBOARD_PAGE_SUBTITLE,
pageSubtitle,
}),
setTopbarEditView: (id) => dispatch({ setTopbarEditView: (id) =>
type: t.SET_TOPBAR_EDIT_VIEW, id, dispatch({
}), type: t.SET_TOPBAR_EDIT_VIEW,
id,
}),
setDashboardRequestLoading: () => dispatch({ setDashboardRequestLoading: () =>
type: t.SET_DASHBOARD_REQUEST_LOADING, dispatch({
}), type: t.SET_DASHBOARD_REQUEST_LOADING,
}),
setDashboardRequestCompleted: () => dispatch({ setDashboardRequestCompleted: () =>
type: t.SET_DASHBOARD_REQUEST_COMPLETED, dispatch({
}), type: t.SET_DASHBOARD_REQUEST_COMPLETED,
}),
}); });
export default connect(null, mapActionsToProps); export default connect(null, mapActionsToProps);

View File

@@ -1,6 +1,12 @@
import React, {useMemo, useState, useEffect, useRef, useCallback} from 'react'; import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useFormik } from "formik"; import { useFormik } from 'formik';
import moment from 'moment'; import moment from 'moment';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -20,8 +26,7 @@ import Dragzone from 'components/Dragzone';
import MediaConnect from 'connectors/Media.connect'; import MediaConnect from 'connectors/Media.connect';
import useMedia from 'hooks/useMedia'; import useMedia from 'hooks/useMedia';
import {compose} from 'utils'; import { compose } from 'utils';
function MakeJournalEntriesForm({ function MakeJournalEntriesForm({
// #withMedia // #withMedia
@@ -39,10 +44,16 @@ function MakeJournalEntriesForm({
manualJournalId, manualJournalId,
manualJournal, manualJournal,
onFormSubmit, onFormSubmit,
onCancelForm, onCancelForm,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { setFiles, saveMedia, deletedFiles, setDeletedFiles, deleteMedia } = useMedia({ const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia, saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia, deleteCallback: requestDeleteMedia,
}); });
@@ -51,79 +62,95 @@ function MakeJournalEntriesForm({
}, []); }, []);
const savedMediaIds = useRef([]); const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => { savedMediaIds.current = []; } const clearSavedMediaIds = () => {
savedMediaIds.current = [];
};
useEffect(() => { useEffect(() => {
if (manualJournal && manualJournal.id) { if (manualJournal && manualJournal.id) {
changePageTitle(formatMessage({id:'edit_journal'})); changePageTitle(formatMessage({ id: 'edit_journal' }));
changePageSubtitle(`No. ${manualJournal.journal_number}`); changePageSubtitle(`No. ${manualJournal.journal_number}`);
} else { } else {
changePageTitle(formatMessage({id:'new_journal'})); changePageTitle(formatMessage({ id: 'new_journal' }));
} }
}, [changePageTitle, changePageSubtitle, manualJournal,formatMessage]); }, [changePageTitle, changePageSubtitle, manualJournal, formatMessage]);
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
journal_number: Yup.string().required().label(formatMessage({id:'journal_number_'})), journal_number: Yup.string()
date: Yup.date().required().label(formatMessage({id:'date'})), .required()
.label(formatMessage({ id: 'journal_number_' })),
date: Yup.date().label(formatMessage({ id: 'date' })),
reference: Yup.string(), reference: Yup.string(),
description: Yup.string(), description: Yup.string(),
entries: Yup.array().of( entries: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
credit: Yup.number().nullable(), credit: Yup.number().nullable(),
debit: Yup.number().nullable(), debit: Yup.number().nullable(),
account_id: Yup.number().nullable().when(['credit', 'debit'], { account_id: Yup.number()
is: (credit, debit) => credit || debit, .nullable()
then: Yup.number().required(), .when(['credit', 'debit'], {
}), is: (credit, debit) => credit || debit,
then: Yup.number().required(),
}),
note: Yup.string().nullable(), note: Yup.string().nullable(),
}), }),
) ),
}); });
const saveInvokeSubmit = useCallback((payload) => { const saveInvokeSubmit = useCallback(
onFormSubmit && onFormSubmit(payload) (payload) => {
}, [onFormSubmit]); onFormSubmit && onFormSubmit(payload);
},
[onFormSubmit],
);
const [payload, setPayload] = useState({}); const [payload, setPayload] = useState({});
const defaultEntry = useMemo(() => ({ const defaultEntry = useMemo(
account_id: null, () => ({
credit: 0, account_id: null,
debit: 0, credit: 0,
note: '', debit: 0,
}), []); note: '',
}),
[],
);
const defaultInitialValues = useMemo(() => ({ const defaultInitialValues = useMemo(
journal_number: '', () => ({
date: moment(new Date()).format('YYYY-MM-DD'), journal_number: '',
description: '', date: moment(new Date()).format('YYYY-MM-DD'),
reference: '', description: '',
entries: [ reference: '',
defaultEntry, entries: [defaultEntry, defaultEntry, defaultEntry, defaultEntry],
defaultEntry, }),
defaultEntry, [defaultEntry],
defaultEntry, );
],
}), [defaultEntry]);
const initialValues = useMemo(() => ({ const initialValues = useMemo(
...(manualJournal) ? { () => ({
...pick(manualJournal, Object.keys(defaultInitialValues)), ...(manualJournal
entries: manualJournal.entries.map((entry) => ({ ? {
...pick(entry, Object.keys(defaultEntry)), ...pick(manualJournal, Object.keys(defaultInitialValues)),
})), entries: manualJournal.entries.map((entry) => ({
} : { ...pick(entry, Object.keys(defaultEntry)),
...defaultInitialValues, })),
} }
}), [manualJournal, defaultInitialValues, defaultEntry]); : {
...defaultInitialValues,
}),
}),
[manualJournal, defaultInitialValues, defaultEntry],
);
const initialAttachmentFiles = useMemo(() => { const initialAttachmentFiles = useMemo(() => {
return manualJournal && manualJournal.media return manualJournal && manualJournal.media
? manualJournal.media.map((attach) => ({ ? manualJournal.media.map((attach) => ({
preview: attach.attachment_file, preview: attach.attachment_file,
uploaded: true, uploaded: true,
metadata: { ...attach }, metadata: { ...attach },
})) : []; }))
: [];
}, [manualJournal]); }, [manualJournal]);
const formik = useFormik({ const formik = useFormik({
@@ -133,113 +160,136 @@ function MakeJournalEntriesForm({
...initialValues, ...initialValues,
}, },
onSubmit: async (values, { setErrors, setSubmitting, resetForm }) => { onSubmit: async (values, { setErrors, setSubmitting, resetForm }) => {
const entries = values.entries.filter((entry) => ( const entries = values.entries.filter(
(entry.credit || entry.debit) (entry) => entry.credit || entry.debit,
)); );
const getTotal = (type = 'credit') => { const getTotal = (type = 'credit') => {
return entries.reduce((total, item) => { return entries.reduce((total, item) => {
return item[type] ? item[type] + total : total; return item[type] ? item[type] + total : total;
}, 0); }, 0);
} };
const totalCredit = getTotal('credit'); const totalCredit = getTotal('credit');
const totalDebit = getTotal('debit'); const totalDebit = getTotal('debit');
// Validate the total credit should be eqials total debit. // Validate the total credit should be eqials total debit.
if (totalCredit !== totalDebit) { if (totalCredit !== totalDebit) {
AppToaster.show({ AppToaster.show({
message: formatMessage({id:'credit_and_debit_not_equal'}), message: formatMessage({ id: 'credit_and_debit_not_equal' }),
}); });
setSubmitting(false); setSubmitting(false);
return; return;
} }
const form = { ...values, status: payload.publish, entries }; const form = { ...values, status: payload.publish, entries };
const saveJournal = (mediaIds) => new Promise((resolve, reject) => { const saveJournal = (mediaIds) =>
const requestForm = { ...form, media_ids: mediaIds }; new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mediaIds };
if (manualJournal && manualJournal.id) { if (manualJournal && manualJournal.id) {
requestEditManualJournal(manualJournal.id, requestForm) requestEditManualJournal(manualJournal.id, requestForm)
.then((response) => { .then((response) => {
AppToaster.show({ AppToaster.show({
message: formatMessage({ message: formatMessage(
id: 'the_journal_has_been_successfully_edited', {
}, { id: 'the_journal_has_been_successfully_edited',
number: values.journal_number, },
}), {
intent: Intent.SUCCESS, number: values.journal_number,
}); },
setSubmitting(false); ),
saveInvokeSubmit({ action: 'update', ...payload }); intent: Intent.SUCCESS,
clearSavedMediaIds([]);
resetForm();
resolve(response);
}).catch((errors) => {
if (errors.find(e => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')) {
setErrors({
journal_number: formatMessage({ id: 'journal_number_is_already_used' }),
}); });
} setSubmitting(false);
setSubmitting(false); saveInvokeSubmit({ action: 'update', ...payload });
}); clearSavedMediaIds([]);
} else { resetForm();
requestMakeJournalEntries(requestForm) resolve(response);
.then((response) => { })
AppToaster.show({ .catch((errors) => {
message: formatMessage({ if (
id: 'the_journal_has_been_successfully_created', errors.find((e) => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')
}, { ) {
number: values.journal_number, setErrors({
}), journal_number: formatMessage({
intent: Intent.SUCCESS, id: 'journal_number_is_already_used',
}),
});
}
setSubmitting(false);
}); });
setSubmitting(false); } else {
saveInvokeSubmit({ action: 'new', ...payload }); requestMakeJournalEntries(requestForm)
clearSavedMediaIds(); .then((response) => {
resetForm(); AppToaster.show({
resolve(response); message: formatMessage(
}).catch((errors) => { {
if (errors.find(e => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')) { id: 'the_journal_has_been_successfully_created',
setErrors({ },
journal_number: formatMessage({ id: 'journal_number_is_already_used' }), {
number: values.journal_number,
},
),
intent: Intent.SUCCESS,
}); });
} setSubmitting(false);
setSubmitting(false); saveInvokeSubmit({ action: 'new', ...payload });
}); clearSavedMediaIds();
} resetForm();
}); resolve(response);
})
.catch((errors) => {
if (
errors.find((e) => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')
) {
setErrors({
journal_number: formatMessage({
id: 'journal_number_is_already_used',
}),
});
}
setSubmitting(false);
});
}
});
Promise.all([ Promise.all([saveMedia(), deleteMedia()])
saveMedia(), .then(([savedMediaResponses]) => {
deleteMedia(), const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
]).then(([savedMediaResponses]) => { savedMediaIds.current = mediaIds;
const mediaIds = savedMediaResponses.map(res => res.data.media.id);
savedMediaIds.current = mediaIds;
return savedMediaResponses; return savedMediaResponses;
}).then(() => { })
return saveJournal(savedMediaIds.current); .then(() => {
}); return saveJournal(savedMediaIds.current);
});
}, },
}); });
const handleSubmitClick = useCallback((payload) => { const handleSubmitClick = useCallback(
setPayload(payload); (payload) => {
formik.handleSubmit(); setPayload(payload);
}, [setPayload, formik]); formik.handleSubmit();
},
[setPayload, formik],
);
const handleCancelClick = useCallback((payload) => { const handleCancelClick = useCallback(
onCancelForm && onCancelForm(payload); (payload) => {
}, [onCancelForm]); onCancelForm && onCancelForm(payload);
},
[onCancelForm],
);
const handleDeleteFile = useCallback((_deletedFiles) => { const handleDeleteFile = useCallback(
_deletedFiles.forEach((deletedFile) => { (_deletedFiles) => {
if (deletedFile.uploaded && deletedFile.metadata.id) { _deletedFiles.forEach((deletedFile) => {
setDeletedFiles([ if (deletedFile.uploaded && deletedFile.metadata.id) {
...deletedFiles, deletedFile.metadata.id, setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
]); }
} });
}); },
}, [setDeletedFiles, deletedFiles]); [setDeletedFiles, deletedFiles],
);
return ( return (
<div class="make-journal-entries"> <div class="make-journal-entries">
@@ -249,19 +299,22 @@ function MakeJournalEntriesForm({
<MakeJournalEntriesTable <MakeJournalEntriesTable
initialValues={initialValues} initialValues={initialValues}
formik={formik} formik={formik}
defaultRow={defaultEntry} /> defaultRow={defaultEntry}
/>
<MakeJournalEntriesFooter <MakeJournalEntriesFooter
formik={formik} formik={formik}
onSubmitClick={handleSubmitClick} onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick} /> onCancelClick={handleCancelClick}
/>
</form> </form>
<Dragzone <Dragzone
initialFiles={initialAttachmentFiles} initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles} onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile} onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'} /> hint={'Attachments: Maxiumum size: 20MB'}
/>
</div> </div>
); );
} }
@@ -272,4 +325,4 @@ export default compose(
withAccountsActions, withAccountsActions,
withDashboardActions, withDashboardActions,
MediaConnect, MediaConnect,
)(MakeJournalEntriesForm); )(MakeJournalEntriesForm);

View File

@@ -54,7 +54,7 @@ export default function MakeJournalEntriesHeader({
minimal={true}> minimal={true}>
<DateInput <DateInput
{...momentFormatter('YYYY/MM/DD')} {...momentFormatter('YYYY-MM-DD')}
defaultValue={new Date()} defaultValue={new Date()}
onChange={handleDateChange} onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM }} /> popoverProps={{ position: Position.BOTTOM }} />

View File

@@ -5,6 +5,7 @@ export default (mapState) => {
const mapped = { const mapped = {
currencies: state.currencies.data, currencies: state.currencies.data,
currenciesList: Object.values(state.currencies.data), currenciesList: Object.values(state.currencies.data),
currenciesLoading:state.currencies.loading,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -10,7 +10,11 @@ const mapActionsToProps = (dispatch) => ({
changePageSubtitle: (pageSubtitle) => dispatch({ changePageSubtitle: (pageSubtitle) => dispatch({
type: t.ALTER_DASHBOARD_PAGE_SUBTITLE, pageSubtitle, type: t.ALTER_DASHBOARD_PAGE_SUBTITLE, pageSubtitle,
}), }),
changePreferencesPageTitle: (pageTitle) =>
dispatch({
type: 'CHANGE_PREFERENCES_PAGE_TITLE',
pageTitle,
}),
setTopbarEditView: (id) => dispatch({ setTopbarEditView: (id) => dispatch({
type: t.SET_TOPBAR_EDIT_VIEW, id, type: t.SET_TOPBAR_EDIT_VIEW, id,
}), }),

View File

@@ -28,8 +28,7 @@ import Icon from 'components/Icon';
import ErrorMessage from 'components/ErrorMessage'; import ErrorMessage from 'components/ErrorMessage';
import { fetchAccountTypes } from 'store/accounts/accounts.actions'; import { fetchAccountTypes } from 'store/accounts/accounts.actions';
import { ListSelect } from 'components';
import {ListSelect} from 'components';
function AccountFormDialog({ function AccountFormDialog({
name, name,
@@ -52,15 +51,18 @@ function AccountFormDialog({
// #withDialog // #withDialog
closeDialog, closeDialog,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const accountFormValidationSchema = Yup.object().shape({ const accountFormValidationSchema = Yup.object().shape({
name: Yup.string().required().label(formatMessage({id:'account_name_'})), name: Yup.string()
.required()
.label(formatMessage({ id: 'account_name_' })),
code: Yup.number(), code: Yup.number(),
account_type_id: Yup.string() account_type_id: Yup.string()
.nullable() .nullable()
.required().label(formatMessage({id:'account_type_id'})), .required()
description: Yup.string().trim() .label(formatMessage({ id: 'account_type_id' })),
description: Yup.string().trim(),
}); });
const initialValues = useMemo( const initialValues = useMemo(
@@ -69,14 +71,14 @@ function AccountFormDialog({
name: '', name: '',
description: '', description: '',
}), }),
[] [],
); );
const [selectedAccountType, setSelectedAccountType] = useState(null); const [selectedAccountType, setSelectedAccountType] = useState(null);
const [selectedSubaccount, setSelectedSubaccount] = useState( const [selectedSubaccount, setSelectedSubaccount] = useState(
payload.action === 'new_child' payload.action === 'new_child'
? accounts.find((a) => a.id === payload.id) ? accounts.find((a) => a.id === payload.id)
: null : null,
); );
const transformApiErrors = (errors) => { const transformApiErrors = (errors) => {
@@ -96,48 +98,67 @@ function AccountFormDialog({
validationSchema: accountFormValidationSchema, validationSchema: accountFormValidationSchema,
onSubmit: (values, { setSubmitting, setErrors }) => { onSubmit: (values, { setSubmitting, setErrors }) => {
const exclude = ['subaccount']; const exclude = ['subaccount'];
const toastAccountName = (values.code) ? `${values.code} - ${values.name}` : values.name; const toastAccountName = values.code
? `${values.code} - ${values.name}`
: values.name;
if (payload.action === 'edit') { if (payload.action === 'edit') {
requestEditAccount({ requestEditAccount({
payload: payload.id, payload: payload.id,
form: { ...omit(values, [...exclude, 'account_type_id']) } form: { ...omit(values, [...exclude, 'account_type_id']) },
}).then((response) => { }).then((response) => {
closeDialog(name); closeDialog(name);
AppToaster.show({ AppToaster.show({
message: formatMessage({ message: formatMessage(
id: 'service_has_been_successful_edited', {
}, { id: 'service_has_been_successful_edited',
name: toastAccountName, },
service: formatMessage({ id: 'account' }), {
}), name: toastAccountName,
service: formatMessage({ id: 'account' }),
},
),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
}); });
} else { } else {
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then((response) => { requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(
closeDialog(name); (response) => {
AppToaster.show({ closeDialog(name);
message: formatMessage({ AppToaster.show({
id: 'service_has_been_successful_created', message: formatMessage(
}, { {
name: toastAccountName, id: 'service_has_been_successful_created',
service: formatMessage({ id: 'account' }), },
}), {
intent: Intent.SUCCESS, name: toastAccountName,
position: Position.BOTTOM, service: formatMessage({ id: 'account' }),
}); },
}); ),
intent: Intent.SUCCESS,
position: Position.BOTTOM,
});
},
);
} }
}, },
}); });
const { errors, values, touched } = useMemo(() => formik, [formik]); const { errors, values, touched } = useMemo(() => formik, [formik]);
// const filteredAccounts = accounts.filter(
// (account) => account.accountTypeId === formik.values.account_type_id,
// );
const filteredAccounts = accounts.filter((account) => {
return account.account_type_id === values.account_type_id;
});
console.log(filteredAccounts, 'ERR');
console.log(accounts,'ER/u');
// Set default account type. // Set default account type.
useEffect(() => { useEffect(() => {
if (account && account.account_type_id) { if (account && account.account_type_id) {
const defaultType = accountsTypes.find( const defaultType = accountsTypes.find(
(t) => t.id === account.account_type_id (t) => t.id === account.account_type_id,
); );
defaultType && setSelectedAccountType(defaultType); defaultType && setSelectedAccountType(defaultType);
@@ -187,7 +208,7 @@ function AccountFormDialog({
); );
} }
}, },
[] [],
); );
// Handles dialog close. // Handles dialog close.
@@ -199,7 +220,7 @@ function AccountFormDialog({
const fetchAccountsList = useQuery( const fetchAccountsList = useQuery(
'accounts-list', 'accounts-list',
() => requestFetchAccounts(), () => requestFetchAccounts(),
{ manual: true } { manual: true },
); );
// Fetches accounts types. // Fetches accounts types.
@@ -208,14 +229,14 @@ function AccountFormDialog({
async () => { async () => {
await requestFetchAccountTypes(); await requestFetchAccountTypes();
}, },
{ manual: true } { manual: true },
); );
// Fetch the given account id on edit mode. // Fetch the given account id on edit mode.
const fetchAccount = useQuery( const fetchAccount = useQuery(
payload.action === 'edit' && ['account', payload.id], payload.action === 'edit' && ['account', payload.id],
(key, id) => requestFetchAccount(id), (key, id) => requestFetchAccount(id),
{ manual: true } { manual: true },
); );
const isFetching = const isFetching =
@@ -228,13 +249,13 @@ function AccountFormDialog({
fetchAccountsList.refetch(); fetchAccountsList.refetch();
fetchAccountsTypes.refetch(); fetchAccountsTypes.refetch();
fetchAccount.refetch(); fetchAccount.refetch();
}, [ fetchAccount, fetchAccountsList, fetchAccountsTypes]); }, [fetchAccount, fetchAccountsList, fetchAccountsTypes]);
const onChangeAccountType = useCallback( const onChangeAccountType = useCallback(
(accountType) => { (accountType) => {
formik.setFieldValue('account_type_id', accountType.id); formik.setFieldValue('account_type_id', accountType.id);
}, },
[formik] [formik],
); );
// Handles change sub-account. // Handles change sub-account.
@@ -243,7 +264,7 @@ function AccountFormDialog({
setSelectedSubaccount(account); setSelectedSubaccount(account);
formik.setFieldValue('parent_account_id', account.id); formik.setFieldValue('parent_account_id', account.id);
}, },
[setSelectedSubaccount, formik] [setSelectedSubaccount, formik],
); );
const onDialogClosed = useCallback(() => { const onDialogClosed = useCallback(() => {
@@ -252,22 +273,28 @@ function AccountFormDialog({
setSelectedAccountType(null); setSelectedAccountType(null);
}, [formik]); }, [formik]);
const infoIcon = useMemo(() => <Icon icon='info-circle' iconSize={12} />, []); const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
const subAccountLabel = useMemo(() => { const subAccountLabel = useMemo(() => {
return ( return (
<span> <span>
<T id={'sub_account'}/> <Icon icon='info-circle' iconSize={12} /> <T id={'sub_account'} /> <Icon icon="info-circle" iconSize={12} />
</span> </span>
); );
}, []); }, []);
const requiredSpan = useMemo(() => <span class='required'>*</span>, []); const requiredSpan = useMemo(() => <span class="required">*</span>, []);
return ( return (
<Dialog <Dialog
name={name} name={name}
title={payload.action === 'edit' ? <T id={'edit_account'}/> : <T id={'new_account'}/>} title={
payload.action === 'edit' ? (
<T id={'edit_account'} />
) : (
<T id={'new_account'} />
)
}
className={{ className={{
'dialog--loading': isFetching, 'dialog--loading': isFetching,
'dialog--account-form': true, 'dialog--account-form': true,
@@ -283,41 +310,40 @@ function AccountFormDialog({
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
<FormGroup <FormGroup
label={<T id={'account_type'}/>} label={<T id={'account_type'} />}
labelInfo={requiredSpan} labelInfo={requiredSpan}
className={classNames( className={classNames(
'form-group--account-type', 'form-group--account-type',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.FILL,
)} )}
inline={true} inline={true}
helperText={<ErrorMessage name='account_type_id' {...formik} />} helperText={<ErrorMessage name="account_type_id" {...formik} />}
intent={ intent={
errors.account_type_id && touched.account_type_id && Intent.DANGER errors.account_type_id && touched.account_type_id && Intent.DANGER
} }
> >
<ListSelect <ListSelect
items={accountsTypes} items={accountsTypes}
noResults={<MenuItem disabled={true} text='No results.' />} noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={accountTypeItem} itemRenderer={accountTypeItem}
itemPredicate={filterAccountTypeItems} itemPredicate={filterAccountTypeItems}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onChangeAccountType} onItemSelect={onChangeAccountType}
selectedItem={formik.values.account_type_id} selectedItem={formik.values.account_type_id}
selectedItemProp={'id'} selectedItemProp={'id'}
defaultText={<T id={'select_account_type'} />} defaultText={<T id={'select_account_type'} />}
labelProp={'name'} labelProp={'name'}
buttonProps={{ disabled: payload.action === 'edit' }} /> buttonProps={{ disabled: payload.action === 'edit' }}
/>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'account_name'}/>} label={<T id={'account_name'} />}
labelInfo={requiredSpan} labelInfo={requiredSpan}
className={'form-group--account-name'} className={'form-group--account-name'}
intent={errors.name && touched.name && Intent.DANGER} intent={errors.name && touched.name && Intent.DANGER}
helperText={<ErrorMessage name='name' {...formik} />} helperText={<ErrorMessage name="name" {...formik} />}
inline={true} inline={true}
> >
<InputGroup <InputGroup
@@ -328,10 +354,10 @@ function AccountFormDialog({
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'account_code'}/>} label={<T id={'account_code'} />}
className={'form-group--account-code'} className={'form-group--account-code'}
intent={errors.code && touched.code && Intent.DANGER} intent={errors.code && touched.code && Intent.DANGER}
helperText={<ErrorMessage name='code' {...formik} />} helperText={<ErrorMessage name="code" {...formik} />}
inline={true} inline={true}
labelInfo={infoIcon} labelInfo={infoIcon}
> >
@@ -356,37 +382,31 @@ function AccountFormDialog({
{values.subaccount && ( {values.subaccount && (
<FormGroup <FormGroup
label={<T id={'parent_account'}/>} label={<T id={'parent_account'} />}
className={classNames( className={classNames(
'form-group--parent-account', 'form-group--parent-account',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.FILL,
)} )}
inline={true} inline={true}
> >
<Select <ListSelect
items={accounts} items={filteredAccounts}
noResults={<MenuItem disabled={true} text='No results.' />} noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={accountItem} itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater} itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onChangeSubaccount} onItemSelect={onChangeSubaccount}
{...formik.getFieldProps('parent_account_id')} selectedItem={formik.values.parent_account_id}
> selectedItemProp={'id'}
<Button defaultText={<T id={'select_parent_account'} />}
rightIcon='caret-down' labelProp={'name'}
text={ />
selectedSubaccount
? selectedSubaccount.name
: <T id={'select_parent_account'}/>
}
/>
</Select>
</FormGroup> </FormGroup>
)} )}
<FormGroup <FormGroup
label={<T id={'description'}/>} label={<T id={'description'} />}
className={'form-group--description'} className={'form-group--description'}
intent={formik.errors.description && Intent.DANGER} intent={formik.errors.description && Intent.DANGER}
helperText={formik.errors.description && formik.errors.credential} helperText={formik.errors.description && formik.errors.credential}
@@ -402,13 +422,19 @@ function AccountFormDialog({
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}><T id={'close'}/></Button> <Button onClick={handleClose}>
<T id={'close'} />
</Button>
<Button <Button
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
type='submit' type="submit"
> >
{payload.action === 'edit' ? <T id={'edit'}/> : <T id={'submit'}/>} {payload.action === 'edit' ? (
<T id={'edit'} />
) : (
<T id={'submit'} />
)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -9,11 +9,10 @@ import {
import * as Yup from 'yup'; import * as Yup from 'yup';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { useQuery } from 'react-query'; import { useQuery, queryCache } from 'react-query';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { pick } from 'lodash'; import { pick } from 'lodash';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import Dialog from 'components/Dialog'; import Dialog from 'components/Dialog';
import DialogReduxConnect from 'components/DialogReduxConnect'; import DialogReduxConnect from 'components/DialogReduxConnect';
@@ -27,8 +26,6 @@ import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
import { compose } from 'utils'; import { compose } from 'utils';
function CurrencyDialog({ function CurrencyDialog({
name, name,
payload, payload,
@@ -46,18 +43,28 @@ function CurrencyDialog({
requestSubmitCurrencies, requestSubmitCurrencies,
requestEditCurrency, requestEditCurrency,
}) { }) {
const {formatMessage} = useIntl(); const { formatMessage } = useIntl();
const fetchCurrencies = useQuery('currencies', () =>
requestFetchCurrencies(),
);
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
currency_name: Yup.string().required().label(formatMessage({id:'currency_name_'})), currency_name: Yup.string()
.required()
.label(formatMessage({ id: 'currency_name_' })),
currency_code: Yup.string() currency_code: Yup.string()
.max(4) .max(4)
.required().label(formatMessage({id:'currency_code_'})), .required()
.label(formatMessage({ id: 'currency_code_' })),
}); });
const initialValues = useMemo(() => ({ const initialValues = useMemo(
currency_name: '', () => ({
currency_code: '', currency_name: '',
}), []); currency_code: '',
}),
[],
);
const { const {
errors, errors,
@@ -75,29 +82,37 @@ function CurrencyDialog({
validationSchema: validationSchema, validationSchema: validationSchema,
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
if (payload.action === 'edit') { if (payload.action === 'edit') {
requestEditCurrency(currency.id, values).then((response) => { requestEditCurrency(currency.id, values)
closeDialog(name); .then((response) => {
AppToaster.show({ closeDialog(name);
message: formatMessage({id:'the_currency_has_been_successfully_edited'}), AppToaster.show({
intent: Intent.SUCCESS, message: formatMessage({
id: 'the_currency_has_been_successfully_edited',
}),
intent: Intent.SUCCESS,
});
setSubmitting(false);
queryCache.refetchQueries('currencies', { force: true });
})
.catch((error) => {
setSubmitting(false);
}); });
setSubmitting(false);
})
.catch((error) => {
setSubmitting(false);
});
} else { } else {
requestSubmitCurrencies(values).then((response) => { requestSubmitCurrencies(values)
closeDialog(name); .then((response) => {
AppToaster.show({ closeDialog(name);
message: formatMessage({id:'the_currency_has_been_successfully_created'}), AppToaster.show({
intent: Intent.SUCCESS, message: formatMessage({
id: 'the_currency_has_been_successfully_created',
}),
intent: Intent.SUCCESS,
});
setSubmitting(false);
queryCache.refetchQueries('currencies', { force: true });
})
.catch((error) => {
setSubmitting(false);
}); });
setSubmitting(false);
})
.catch((error) => {
setSubmitting(false);
});
} }
}, },
}); });
@@ -106,10 +121,6 @@ function CurrencyDialog({
closeDialog(name); closeDialog(name);
}, [name, closeDialog]); }, [name, closeDialog]);
const fetchCurrencies = useQuery('currencies',
() => { requestFetchCurrencies(); },
{ manual: true });
const onDialogOpening = useCallback(() => { const onDialogOpening = useCallback(() => {
fetchCurrencies.refetch(); fetchCurrencies.refetch();
}, [fetchCurrencies]); }, [fetchCurrencies]);
@@ -117,19 +128,25 @@ function CurrencyDialog({
const onDialogClosed = useCallback(() => { const onDialogClosed = useCallback(() => {
resetForm(); resetForm();
closeDialog(name); closeDialog(name);
}, [closeDialog, name,resetForm]); }, [closeDialog, name, resetForm]);
const requiredSpan = useMemo(() => <span className={'required'}>*</span>, []); const requiredSpan = useMemo(() => <span className={'required'}>*</span>, []);
return ( return (
<Dialog <Dialog
name={name} name={name}
title={payload.action === 'edit' ? <T id={'edit_currency'}/> : <T id={'new_currency'}/>} title={
payload.action === 'edit' ? (
<T id={'edit_currency'} />
) : (
<T id={'new_currency'} />
)
}
className={classNames( className={classNames(
{ {
'dialog--loading': fetchCurrencies.isFetching, 'dialog--loading': fetchCurrencies.isFetching,
}, },
'dialog--currency-form' 'dialog--currency-form',
)} )}
isOpen={isOpen} isOpen={isOpen}
onClosed={onDialogClosed} onClosed={onDialogClosed}
@@ -140,31 +157,43 @@ function CurrencyDialog({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
<FormGroup <FormGroup
label={<T id={'currency_name'}/>} label={<T id={'currency_name'} />}
labelInfo={requiredSpan} labelInfo={requiredSpan}
className={'form-group--currency-name'} className={'form-group--currency-name'}
intent={(errors.currency_name && touched.currency_name) && Intent.DANGER} intent={
helperText={<ErrorMessage name='currency_name' {...{errors, touched}} />} errors.currency_name && touched.currency_name && Intent.DANGER
}
helperText={
<ErrorMessage name="currency_name" {...{ errors, touched }} />
}
inline={true} inline={true}
> >
<InputGroup <InputGroup
medium={true} medium={true}
intent={(errors.currency_name && touched.currency_name) && Intent.DANGER} intent={
errors.currency_name && touched.currency_name && Intent.DANGER
}
{...getFieldProps('currency_name')} {...getFieldProps('currency_name')}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'currency_code'}/>} label={<T id={'currency_code'} />}
labelInfo={requiredSpan} labelInfo={requiredSpan}
className={'form-group--currency-code'} className={'form-group--currency-code'}
intent={(errors.currency_code && touched.currency_code) && Intent.DANGER} intent={
helperText={<ErrorMessage name='currency_code' {...{errors, touched}} />} errors.currency_code && touched.currency_code && Intent.DANGER
}
helperText={
<ErrorMessage name="currency_code" {...{ errors, touched }} />
}
inline={true} inline={true}
> >
<InputGroup <InputGroup
medium={true} medium={true}
intent={(errors.currency_code && touched.currency_code) && Intent.DANGER} intent={
errors.currency_code && touched.currency_code && Intent.DANGER
}
{...getFieldProps('currency_code')} {...getFieldProps('currency_code')}
/> />
</FormGroup> </FormGroup>
@@ -172,9 +201,19 @@ function CurrencyDialog({
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}><T id={'cancel'} /></Button> <Button onClick={handleClose}>
<Button intent={Intent.PRIMARY} type='submit' disabled={isSubmitting}> <T id={'cancel'} />
{payload.action === 'edit' ? <T id={'edit'} /> : <T id={'submit'} /> } </Button>
<Button
intent={Intent.PRIMARY}
type="submit"
disabled={isSubmitting}
>
{payload.action === 'edit' ? (
<T id={'edit'} />
) : (
<T id={'submit'} />
)}
</Button> </Button>
</div> </div>
</div> </div>
@@ -190,8 +229,8 @@ const mapStateToProps = (state, props) => {
name: 'currency-form', name: 'currency-form',
payload: { action: 'new', currencyCode: null, ...dialogPayload }, payload: { action: 'new', currencyCode: null, ...dialogPayload },
currencyCode: dialogPayload?.currencyCode || null, currencyCode: dialogPayload?.currencyCode || null,
} };
} };
const withCurrencyFormDialog = connect(mapStateToProps); const withCurrencyFormDialog = connect(mapStateToProps);

View File

@@ -12,20 +12,19 @@ import { pick } from 'lodash';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Select } from '@blueprintjs/select'; import { useQuery, queryCache } from 'react-query';
import { useQuery } from 'react-query';
import moment from 'moment'; import moment from 'moment';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { momentFormatter } from 'utils'; import { momentFormatter } from 'utils';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import Dialog from 'components/Dialog'; import Dialog from 'components/Dialog';
import ErrorMessage from 'components/ErrorMessage'; import ErrorMessage from 'components/ErrorMessage';
import classNames from 'classnames'; import classNames from 'classnames';
import { ListSelect } from 'components';
import withExchangeRatesDialog from './ExchangeRateDialog.container'; import withExchangeRatesDialog from './ExchangeRateDialog.container';
function ExchangeRateDialog({ function ExchangeRateDialog({
name, name,
payload, payload,
@@ -43,24 +42,37 @@ function ExchangeRateDialog({
requestEditExchangeRate, requestEditExchangeRate,
requestFetchCurrencies, requestFetchCurrencies,
editExchangeRate, editExchangeRate,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [selectedItems, setSelectedItems] = useState({}); const [selectedItems, setSelectedItems] = useState({});
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
exchange_rate: Yup.number().required().label(formatMessage({id:'exchange_rate_'})), exchange_rate: Yup.number()
currency_code: Yup.string().max(3).required(formatMessage({id:'currency_code_'})), .required()
date: Yup.date().required().label(formatMessage({id:'date'})), .label(formatMessage({ id: 'exchange_rate_' })),
currency_code: Yup.string()
.max(3)
.required(formatMessage({ id: 'currency_code_' })),
date: Yup.date()
.required()
.label(formatMessage({ id: 'date' })),
}); });
const initialValues = useMemo(() => ({ const initialValues = useMemo(
exchange_rate: '', () => ({
currency_code: '', exchange_rate: '',
date: moment(new Date()).format('YYYY-MM-DD'), currency_code: '',
}), []); date: moment(new Date()).format('YYYY-MM-DD'),
}),
[],
);
const fetchExchangeRatesDialog = useQuery('exchange-rates-dialog', () =>
requestFetchExchangeRates(),
);
const { const {
values,
touched, touched,
errors, errors,
isSubmitting, isSubmitting,
@@ -75,15 +87,19 @@ function ExchangeRateDialog({
...(payload.action === 'edit' && ...(payload.action === 'edit' &&
pick(editExchangeRate, Object.keys(initialValues))), pick(editExchangeRate, Object.keys(initialValues))),
}, },
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting, setErrors }) => {
if (payload.action === 'edit') { if (payload.action === 'edit') {
requestEditExchangeRate(payload.id, values) requestEditExchangeRate(payload.id, values)
.then((response) => { .then((response) => {
closeDialog(name); closeDialog(name);
AppToaster.show({ AppToaster.show({
message: formatMessage({id:'the_exchange_rate_has_been_successfully_edited'}) message: formatMessage({
id: 'the_exchange_rate_has_been_successfully_edited',
}),
intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
queryCache.removeQueries('exchange-rates-dialog', { force: true });
}) })
.catch((error) => { .catch((error) => {
setSubmitting(false); setSubmitting(false);
@@ -93,30 +109,40 @@ function ExchangeRateDialog({
.then((response) => { .then((response) => {
closeDialog(name); closeDialog(name);
AppToaster.show({ AppToaster.show({
message: formatMessage({id:'the_exchange_rate_has_been_successfully_created'}) message: formatMessage({
id: 'the_exchange_rate_has_been_successfully_created',
}),
intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
queryCache.refetchQueries('exchange-rates-table', { force: true });
}) })
.catch((error) => { .catch((errors) => {
setSubmitting(false); if (
errors.find((e) => e.type === 'EXCHANGE.RATE.DATE.PERIOD.DEFINED')
) {
setErrors({
exchange_rate: formatMessage({
id:
'there_is_exchange_rate_in_this_date_with_the_same_currency',
}),
});
}
}); });
} }
}, },
}); });
const requiredSpan = useMemo(() => <span class='required'>*</span>, []); const requiredSpan = useMemo(() => <span class="required">*</span>, []);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
closeDialog(name); closeDialog(name);
}, [name, closeDialog]); }, [name, closeDialog]);
const fetchExchangeRatesDialog = useQuery('exchange-rates-dialog',
() => requestFetchExchangeRates());
const onDialogClosed = useCallback(() => { const onDialogClosed = useCallback(() => {
resetForm(); resetForm();
closeDialog(name); closeDialog(name);
}, [closeDialog, name,resetForm]); }, [closeDialog, name, resetForm]);
const onDialogOpening = useCallback(() => { const onDialogOpening = useCallback(() => {
fetchExchangeRatesDialog.refetch(); fetchExchangeRatesDialog.refetch();
@@ -127,7 +153,7 @@ function ExchangeRateDialog({
const formatted = moment(date).format('YYYY-MM-DD'); const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('date', formatted); setFieldValue('date', formatted);
}, },
[setFieldValue] [setFieldValue],
); );
const onItemsSelect = useCallback( const onItemsSelect = useCallback(
@@ -140,19 +166,19 @@ function ExchangeRateDialog({
setFieldValue(filedName, filed.currency_code); setFieldValue(filedName, filed.currency_code);
}; };
}, },
[setFieldValue, selectedItems] [setFieldValue, selectedItems],
); );
const filterCurrencyCode = (query, currency_code, _index, exactMatch) => { const filterCurrencyCode = (query, currency, _index, exactMatch) => {
const normalizedTitle = currency_code.currency_code.toLowerCase(); const normalizedTitle = currency.currency_code.toLowerCase();
const normalizedQuery = query.toLowerCase(); const normalizedQuery = query.toLowerCase();
if (exactMatch) { if (exactMatch) {
return normalizedTitle === normalizedQuery; return normalizedTitle === normalizedQuery;
} else { } else {
return ( return (
`${currency_code.currency_code} ${normalizedTitle}`.indexOf( `${currency.currency_code} ${normalizedTitle}`.indexOf(
normalizedQuery normalizedQuery,
) >= 0 ) >= 0
); );
} }
@@ -169,20 +195,19 @@ function ExchangeRateDialog({
); );
}, []); }, []);
const getSelectedItemLabel = useCallback((fieldName, defaultLabel) => {
return typeof selectedItems[fieldName] !== 'undefined'
? selectedItems[fieldName].currency_code
: defaultLabel;
}, [selectedItems]);
return ( return (
<Dialog <Dialog
name={name} name={name}
title={payload.action === 'edit' title={
? <T id={'edit_exchange_rate'}/> : <T id={'new_exchange_rate'}/>} payload.action === 'edit' ? (
<T id={'edit_exchange_rate'} />
) : (
<T id={'new_exchange_rate'} />
)
}
className={classNames( className={classNames(
{'dialog--loading': fetchExchangeRatesDialog.isFetching}, { 'dialog--loading': fetchExchangeRatesDialog.isFetching },
'dialog--exchangeRate-form' 'dialog--exchangeRate-form',
)} )}
isOpen={isOpen} isOpen={isOpen}
onClosed={onDialogClosed} onClosed={onDialogClosed}
@@ -193,11 +218,11 @@ function ExchangeRateDialog({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
<FormGroup <FormGroup
label={<T id={'date'}/>} label={<T id={'date'} />}
inline={true} inline={true}
labelInfo={requiredSpan} labelInfo={requiredSpan}
intent={errors.date && touched.date && Intent.DANGER} intent={errors.date && touched.date && Intent.DANGER}
helperText={<ErrorMessage name='date' {...{errors, touched}} />} helperText={<ErrorMessage name="date" {...{ errors, touched }} />}
> >
<DateInput <DateInput
fill={true} fill={true}
@@ -205,56 +230,71 @@ function ExchangeRateDialog({
defaultValue={new Date()} defaultValue={new Date()}
onChange={handleDateChange} onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM }} popoverProps={{ position: Position.BOTTOM }}
// disabled={payload.action === 'edit'} disabled={payload.action === 'edit'}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'exchange_rate'}/>} label={<T id={'exchange_rate'} />}
labelInfo={requiredSpan} labelInfo={requiredSpan}
intent={errors.exchange_rate && touched.exchange_rate && Intent.DANGER} intent={
helperText={<ErrorMessage name='exchange_rate' {...{errors, touched}} />} errors.exchange_rate && touched.exchange_rate && Intent.DANGER
}
helperText={
<ErrorMessage name="exchange_rate" {...{ errors, touched }} />
}
inline={true} inline={true}
> >
<InputGroup <InputGroup
medium={true} medium={true}
intent={errors.exchange_rate && touched.exchange_rate && Intent.DANGER} intent={
errors.exchange_rate && touched.exchange_rate && Intent.DANGER
}
{...getFieldProps('exchange_rate')} {...getFieldProps('exchange_rate')}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'currency_code'}/>} label={<T id={'currency_code'} />}
labelInfo={requiredSpan} labelInfo={requiredSpan}
className={classNames('form-group--select-list', Classes.FILL)} className={classNames('form-group--select-list', Classes.FILL)}
inline={true} inline={true}
intent={(errors.currency_code && touched.currency_code) && Intent.DANGER} intent={
helperText={<ErrorMessage name='currency_code' {...{errors, touched}} />} errors.currency_code && touched.currency_code && Intent.DANGER
}
helperText={
<ErrorMessage name="currency_code" {...{ errors, touched }} />
}
> >
<Select <ListSelect
items={currenciesList} items={currenciesList}
noResults={<MenuItem disabled={true} text='No results.' />} noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={currencyCodeRenderer} itemRenderer={currencyCodeRenderer}
itemPredicate={filterCurrencyCode} itemPredicate={filterCurrencyCode}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onItemsSelect('currency_code')} onItemSelect={onItemsSelect('currency_code')}
> selectedItem={values.currency_code}
<Button selectedItemProp={'currency_code'}
rightIcon='caret-down' defaultText={<T id={'select_currency_code'} />}
fill={true} labelProp={'currency_code'}
text={getSelectedItemLabel( />
'currency_code',
'select Currency Code'
)}
/>
</Select>
</FormGroup> </FormGroup>
</div> </div>
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}><T id={'close'}/></Button> <Button onClick={handleClose}>
<Button intent={Intent.PRIMARY} type='submit' disabled={isSubmitting}> <T id={'close'} />
{payload.action === 'edit' ? <T id={'edit'}/> : <T id={'submit'}/>} </Button>
<Button
intent={Intent.PRIMARY}
type="submit"
disabled={isSubmitting}
>
{payload.action === 'edit' ? (
<T id={'edit'} />
) : (
<T id={'submit'} />
)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -41,12 +41,19 @@ function InviteUserDialog({
}, false); }, false);
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
first_name: Yup.string().required().label(formatMessage({id:'first_name_'})), first_name: Yup.string()
last_name: Yup.string().required().label(formatMessage({id:'last_name_'})), .required()
.label(formatMessage({ id: 'first_name_' })),
last_name: Yup.string()
.required()
.label(formatMessage({ id: 'last_name_' })),
email: Yup.string() email: Yup.string()
.email() .email()
.required().label(formatMessage({id:'email'})), .required()
phone_number: Yup.number().required().label(formatMessage({id:'phone_number_'})), .label(formatMessage({ id: 'email' })),
phone_number: Yup.number()
.required()
.label(formatMessage({ id: 'phone_number_' })),
}); });
const initialValues = useMemo( const initialValues = useMemo(
@@ -56,7 +63,7 @@ function InviteUserDialog({
email: '', email: '',
phone_number: '', phone_number: '',
}), }),
[] [],
); );
const formik = useFormik({ const formik = useFormik({
@@ -65,7 +72,7 @@ function InviteUserDialog({
...(payload.action === 'edit' && ...(payload.action === 'edit' &&
pick( pick(
objectKeysTransform(payload.user, snakeCase), objectKeysTransform(payload.user, snakeCase),
Object.keys(initialValues) Object.keys(initialValues),
)), )),
}, },
validationSchema, validationSchema,
@@ -78,10 +85,13 @@ function InviteUserDialog({
.then((response) => { .then((response) => {
closeDialog(name); closeDialog(name);
AppToaster.show({ AppToaster.show({
message: formatMessage({id:'the_user_details_has_been_updated'}), message: formatMessage({
id: 'the_user_details_has_been_updated',
}),
intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
queryCache.refetchQueries('users-table',{force:true}) queryCache.refetchQueries('users-table', { force: true });
}) })
.catch((error) => { .catch((error) => {
setSubmitting(false); setSubmitting(false);
@@ -99,10 +109,10 @@ function InviteUserDialog({
formik.resetForm(); formik.resetForm();
}, [formik.resetForm]); }, [formik.resetForm]);
// Handles dialog close. // Handles dialog close.
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
closeDialog(name); closeDialog(name);
}, [closeDialog, name]); }, [closeDialog, name]);
return ( return (
<Dialog <Dialog
@@ -126,7 +136,7 @@ function InviteUserDialog({
label={<T id={'first_name'} />} label={<T id={'first_name'} />}
className={'form-group--first-name'} className={'form-group--first-name'}
intent={errors.first_name && touched.first_name && Intent.DANGER} intent={errors.first_name && touched.first_name && Intent.DANGER}
helperText={<ErrorMessage name='first_name' {...formik} />} helperText={<ErrorMessage name="first_name" {...formik} />}
inline={true} inline={true}
> >
<InputGroup <InputGroup
@@ -139,7 +149,7 @@ function InviteUserDialog({
label={<T id={'last_name'} />} label={<T id={'last_name'} />}
className={'form-group--last-name'} className={'form-group--last-name'}
intent={errors.last_name && touched.last_name && Intent.DANGER} intent={errors.last_name && touched.last_name && Intent.DANGER}
helperText={<ErrorMessage name='last_name' {...formik} />} helperText={<ErrorMessage name="last_name" {...formik} />}
inline={true} inline={true}
> >
<InputGroup <InputGroup
@@ -152,7 +162,7 @@ function InviteUserDialog({
label={<T id={'email'} />} label={<T id={'email'} />}
className={'form-group--email'} className={'form-group--email'}
intent={errors.email && touched.email && Intent.DANGER} intent={errors.email && touched.email && Intent.DANGER}
helperText={<ErrorMessage name='email' {...formik} />} helperText={<ErrorMessage name="email" {...formik} />}
inline={true} inline={true}
> >
<InputGroup <InputGroup
@@ -168,7 +178,7 @@ function InviteUserDialog({
intent={ intent={
errors.phone_number && touched.phone_number && Intent.DANGER errors.phone_number && touched.phone_number && Intent.DANGER
} }
helperText={<ErrorMessage name='phone_number' {...formik} />} helperText={<ErrorMessage name="phone_number" {...formik} />}
inline={true} inline={true}
> >
<InputGroup <InputGroup
@@ -182,9 +192,15 @@ function InviteUserDialog({
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}><T id={'close'}/></Button> <Button onClick={handleClose}>
<Button intent={Intent.PRIMARY} type='submit' disabled={formik.isSubmitting} > <T id={'close'} />
{payload.action === 'edit' ? <T id={'edit'}/> : ''} </Button>
<Button
intent={Intent.PRIMARY}
type="submit"
disabled={formik.isSubmitting}
>
{payload.action === 'edit' ? <T id={'edit'} /> : ''}
</Button> </Button>
</div> </div>
</div> </div>
@@ -195,5 +211,5 @@ function InviteUserDialog({
export default compose( export default compose(
UserListDialogConnect, UserListDialogConnect,
DialogReduxConnect DialogReduxConnect,
)(InviteUserDialog); )(InviteUserDialog);

View File

@@ -6,7 +6,7 @@ import {
InputGroup, InputGroup,
Intent, Intent,
TextArea, TextArea,
MenuItem MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select'; import { Select } from '@blueprintjs/select';
import { pick } from 'lodash'; import { pick } from 'lodash';
@@ -16,10 +16,11 @@ import { useFormik } from 'formik';
import { compose } from 'utils'; import { compose } from 'utils';
import { useQuery, queryCache } from 'react-query'; import { useQuery, queryCache } from 'react-query';
import classNames from 'classnames'; import classNames from 'classnames';
import {connect} from 'react-redux'; import { connect } from 'react-redux';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import ErrorMessage from 'components/ErrorMessage'; import ErrorMessage from 'components/ErrorMessage';
import { ListSelect } from 'components';
import Dialog from 'components/Dialog'; import Dialog from 'components/Dialog';
import DialogConnect from 'connectors/Dialog.connector'; import DialogConnect from 'connectors/Dialog.connector';
@@ -32,7 +33,6 @@ import withItemCategoriesActions from 'containers/Items/withItemCategoriesAction
import Icon from 'components/Icon'; import Icon from 'components/Icon';
function ItemCategoryDialog({ function ItemCategoryDialog({
name, name,
payload, payload,
@@ -57,23 +57,30 @@ function ItemCategoryDialog({
const [selectedParentCategory, setParentCategory] = useState(null); const [selectedParentCategory, setParentCategory] = useState(null);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fetchList = useQuery(['items-categories-list'], const fetchList = useQuery(['items-categories-list'], () =>
() => requestFetchItemCategories()); requestFetchItemCategories(),
);
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
name: Yup.string().required().label(formatMessage({id:'category_name_'})), name: Yup.string()
.required()
.label(formatMessage({ id: 'category_name_' })),
parent_category_id: Yup.string().nullable(), parent_category_id: Yup.string().nullable(),
description: Yup.string().trim() description: Yup.string().trim(),
}); });
const initialValues = useMemo(() => ({ const initialValues = useMemo(
name: '', () => ({
description: '', name: '',
parent_category_id: null description: '',
}), []); parent_category_id: null,
}),
[],
);
// Formik // Formik
const { const {
values,
errors, errors,
touched, touched,
setFieldValue, setFieldValue,
@@ -85,24 +92,28 @@ function ItemCategoryDialog({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
...(payload.action === 'edit' && ...(payload.action === 'edit' &&
pick(itemCategory, Object.keys(initialValues))) pick(itemCategory, Object.keys(initialValues))),
}, },
validationSchema, validationSchema,
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
if (payload.action === 'edit') { if (payload.action === 'edit') {
requestEditItemCategory(payload.id, values).then(response => { requestEditItemCategory(payload.id, values)
closeDialog(name); .then((response) => {
AppToaster.show({ closeDialog(name);
message: formatMessage({ AppToaster.show({
id: 'the_item_category_has_been_successfully_edited', message: formatMessage({
}), id: 'the_item_category_has_been_successfully_edited',
intent: Intent.SUCCESS, }),
intent: Intent.SUCCESS,
});
setSubmitting(false);
queryCache.refetchQueries('items-categories-table', {
force: true,
});
})
.catch((error) => {
setSubmitting(false);
}); });
setSubmitting(false);
queryCache.refetchQueries('items-categories-table', { force: true });
}).catch((error) => {
setSubmitting(false);
});
} else { } else {
requestSubmitItemCategory(values) requestSubmitItemCategory(values)
.then((response) => { .then((response) => {
@@ -114,60 +125,84 @@ function ItemCategoryDialog({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
queryCache.refetchQueries('items-categories-table', { force: true }); queryCache.refetchQueries('items-categories-table', {
force: true,
});
}) })
.catch((error) => { .catch((error) => {
setSubmitting(false); setSubmitting(false);
}); });
} }
} },
}); });
const filterItemCategory = useCallback((query, category, _index, exactMatch) => { const filterItemCategory = useCallback(
const normalizedTitle = category.name.toLowerCase(); (query, category, _index, exactMatch) => {
const normalizedQuery = query.toLowerCase(); const normalizedTitle = category.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) { if (exactMatch) {
return normalizedTitle === normalizedQuery; return normalizedTitle === normalizedQuery;
} else { } else {
return normalizedTitle.indexOf(normalizedQuery) >= 0; return normalizedTitle.indexOf(normalizedQuery) >= 0;
} }
}, []); },
[],
);
const parentCategoryItem = useCallback((category, { handleClick, modifiers, query }) => { const parentCategoryItem = useCallback(
return ( (category, { handleClick, modifiers, query }) => {
<MenuItem text={category.name} key={category.id} onClick={handleClick} /> return (
); <MenuItem
}, []); text={category.name}
key={category.id}
onClick={handleClick}
/>
);
},
[],
);
// Handle the dialog closing. // Handle the dialog closing.
const handleClose = useCallback(() => { closeDialog(name); }, [name, closeDialog]); const handleClose = useCallback(() => {
closeDialog(name);
}, [name, closeDialog]);
// Handle the dialog opening. // Handle the dialog opening.
const onDialogOpening = useCallback(() => { const onDialogOpening = useCallback(() => {
fetchList.refetch(); fetchList.refetch();
}, [fetchList]); }, [fetchList]);
const onChangeParentCategory = useCallback((parentCategory) => { const onChangeParentCategory = useCallback(
setParentCategory(parentCategory); (parentCategory) => {
setFieldValue('parent_category_id', parentCategory.id); setParentCategory(parentCategory);
}, [setFieldValue]); setFieldValue('parent_category_id', parentCategory.id);
},
[setFieldValue],
);
const onDialogClosed = useCallback(() => { const onDialogClosed = useCallback(() => {
resetForm(); resetForm();
closeDialog(name); closeDialog(name);
}, [resetForm, closeDialog, name]); }, [resetForm, closeDialog, name]);
const requiredSpan = useMemo(() => (<span class="required">*</span>), []); const requiredSpan = useMemo(() => <span class="required">*</span>, []);
const infoIcon = useMemo(() => (<Icon icon="info-circle" iconSize={12} />), []); const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
return ( return (
<Dialog <Dialog
name={name} name={name}
title={payload.action === 'edit' ? <T id={'edit_category'}/> : <T id={'new_category'}/>} title={
className={classNames({ payload.action === 'edit' ? (
'dialog--loading': fetchList.isFetching, <T id={'edit_category'} />
}, ) : (
<T id={'new_category'} />
)
}
className={classNames(
{
'dialog--loading': fetchList.isFetching,
},
'dialog--category-form', 'dialog--category-form',
)} )}
isOpen={isOpen} isOpen={isOpen}
@@ -179,22 +214,22 @@ function ItemCategoryDialog({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
<FormGroup <FormGroup
label={<T id={'category_name'}/>} label={<T id={'category_name'} />}
labelInfo={requiredSpan} labelInfo={requiredSpan}
className={'form-group--category-name'} className={'form-group--category-name'}
intent={(errors.name && touched.name) && Intent.DANGER} intent={errors.name && touched.name && Intent.DANGER}
helperText={(<ErrorMessage name="name" {...{ errors, touched }} />)} helperText={<ErrorMessage name="name" {...{ errors, touched }} />}
inline={true} inline={true}
> >
<InputGroup <InputGroup
medium={true} medium={true}
intent={(errors.name && touched.name) && Intent.DANGER} intent={errors.name && touched.name && Intent.DANGER}
{...getFieldProps('name')} {...getFieldProps('name')}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'parent_category'}/>} label={<T id={'parent_category'} />}
labelInfo={infoIcon} labelInfo={infoIcon}
className={classNames( className={classNames(
'form-group--select-list', 'form-group--select-list',
@@ -202,29 +237,39 @@ function ItemCategoryDialog({
Classes.FILL, Classes.FILL,
)} )}
inline={true} inline={true}
helperText={(<ErrorMessage name="parent_category_id" {...{errors, touched}} />)} helperText={
intent={(errors.parent_category_id && touched.parent_category_id) && Intent.DANGER} <ErrorMessage
name="parent_category_id"
{...{ errors, touched }}
/>
}
intent={
errors.parent_category_id &&
touched.parent_category_id &&
Intent.DANGER
}
> >
<Select <ListSelect
items={categoriesList} items={categoriesList}
noResults={<MenuItem disabled={true} text='No results.' />} noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={parentCategoryItem} itemRenderer={parentCategoryItem}
itemPredicate={filterItemCategory} itemPredicate={filterItemCategory}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onChangeParentCategory} onItemSelect={onChangeParentCategory}
> selectedItem={values.parent_category_id}
<Button selectedItemProp={'id'}
text={selectedParentCategory defaultText={<T id={'select_parent_category'} />}
? selectedParentCategory.name : 'Select Parent Category'} labelProp={'name'}
/> />
</Select>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'description'}/>} label={<T id={'description'} />}
className={'form-group--description'} className={'form-group--description'}
intent={(errors.description && touched.description) && Intent.DANGER} intent={errors.description && touched.description && Intent.DANGER}
helperText={(<ErrorMessage name="description" {...{errors, touched}} />)} helperText={
<ErrorMessage name="description" {...{ errors, touched }} />
}
inline={true} inline={true}
> >
<TextArea <TextArea
@@ -237,9 +282,19 @@ function ItemCategoryDialog({
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}><T id={'close'}/></Button> <Button onClick={handleClose}>
<Button intent={Intent.PRIMARY} type='submit' disabled={isSubmitting}> <T id={'close'} />
{payload.action === 'edit' ? <T id={'edit'}/> : <T id={'submit'}/>} </Button>
<Button
intent={Intent.PRIMARY}
type="submit"
disabled={isSubmitting}
>
{payload.action === 'edit' ? (
<T id={'edit'} />
) : (
<T id={'submit'} />
)}
</Button> </Button>
</div> </div>
</div> </div>
@@ -248,26 +303,25 @@ function ItemCategoryDialog({
); );
} }
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const dialogPayload = getDialogPayload(state, 'item-category-form'); const dialogPayload = getDialogPayload(state, 'item-category-form');
return { return {
name: 'item-category-form', name: 'item-category-form',
payload: {action: 'new', id: null, ...dialogPayload}, payload: { action: 'new', id: null, ...dialogPayload },
itemCategoryId: dialogPayload?.id || null, itemCategoryId: dialogPayload?.id || null,
}; };
}; };
const withItemCategoryDialog = connect(mapStateToProps); const withItemCategoryDialog = connect(mapStateToProps);
export default compose( export default compose(
withItemCategoryDialog, withItemCategoryDialog,
DialogConnect, DialogConnect,
DialogReduxConnect, DialogReduxConnect,
withItemCategoryDetail, withItemCategoryDetail,
withItemCategories(({ categoriesList }) => ({ withItemCategories(({ categoriesList }) => ({
categoriesList categoriesList,
})), })),
withItemCategoriesActions withItemCategoriesActions,
)(ItemCategoryDialog); )(ItemCategoryDialog);

View File

@@ -13,6 +13,7 @@ import {
import { objectKeysTransform } from 'utils'; import { objectKeysTransform } from 'utils';
import { pick, snakeCase } from 'lodash'; import { pick, snakeCase } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { queryCache, useQuery } from 'react-query';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import DialogReduxConnect from 'components/DialogReduxConnect'; import DialogReduxConnect from 'components/DialogReduxConnect';
@@ -30,12 +31,17 @@ function UserFormDialog({
closeDialog, closeDialog,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fetchHook = useAsync(async () => { // const fetchHook = useAsync(async () => {
await Promise.all([ // await Promise.all([
...(payload.action === 'edit' ? [requestFetchUser(payload.user.id)] : []), // ...(payload.action === 'edit' ? [requestFetchUser(payload.user.id)] : []),
]); // ]);
}, false); // }, false);
const fetchHook = useQuery(
payload.action === 'edit' && ['user-invite-table', payload.user.id],
(key, id) => requestFetchUser(id),
{ manual: true }
);
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
email: Yup.string() email: Yup.string()
.email() .email()
@@ -63,23 +69,30 @@ function UserFormDialog({
enableReinitialize: true, enableReinitialize: true,
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: (values) => { onSubmit: (values,{setSubmitting}) => {
const form = { ...values }; const form = { ...values };
requestSubmitInvite(form).then((response) => { requestSubmitInvite(form).then((response) => {
closeDialog(name);
AppToaster.show({ AppToaster.show({
message: formatMessage({ message: formatMessage({
id: 'teammate_invited_to_organization_account', id: 'teammate_invited_to_organization_account',
}), }),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
closeDialog(name); setSubmitting(false);
}); queryCache.refetchQueries('users-table', { force: true });
}).catch((errors)=>{
setSubmitting(false)
})
}, },
}); });
const onDialogOpening = () => {
fetchHook.execute(); // Handle the dialog opening.
}; const onDialogOpening =useCallback(()=>{
fetchHook.refetch();
},[fetchHook]);
const onDialogClosed = useCallback(() => { const onDialogClosed = useCallback(() => {
resetForm(); resetForm();
@@ -104,6 +117,7 @@ function UserFormDialog({
'dialog--loading': fetchHook.pending, 'dialog--loading': fetchHook.pending,
'dialog--invite-form': true, 'dialog--invite-form': true,
})} })}
//
autoFocus={true} autoFocus={true}
canEscapeKeyClose={true} canEscapeKeyClose={true}
isOpen={isOpen} isOpen={isOpen}

View File

@@ -1,8 +1,12 @@
import React, { useEffect, useState, useCallback,useMemo } from 'react'; import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useQuery } from 'react-query'; import { useQuery, queryCache } from 'react-query';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Alert, Intent } from '@blueprintjs/core'; import { Alert, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl, FormattedHTMLMessage } from 'react-intl'; import {
FormattedMessage as T,
useIntl,
FormattedHTMLMessage,
} from 'react-intl';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
@@ -10,13 +14,13 @@ import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ExchangeRateTable from './ExchangeRateTable'; import ExchangeRateTable from './ExchangeRateTable';
import ExchangeRateActionsBar from './ExchangeRateActionsBar'; import ExchangeRateActionsBar from './ExchangeRateActionsBar';
import withDialog from 'connectors/Dialog.connector';
import withDashboardActions from 'containers/Dashboard/withDashboard'; import withDashboardActions from 'containers/Dashboard/withDashboard';
import withResourceActions from 'containers/Resources/withResourcesActions'; import withResourceActions from 'containers/Resources/withResourcesActions';
import withExchangeRatesActions from 'containers/ExchangeRates/withExchangeRatesActions'; import withExchangeRatesActions from 'containers/ExchangeRates/withExchangeRatesActions';
import { compose } from 'utils'; import { compose } from 'utils';
function ExchangeRate({ function ExchangeRate({
// #withDashboard // #withDashboard
changePageTitle, changePageTitle,
@@ -29,45 +33,56 @@ function ExchangeRate({
requestDeleteExchangeRate, requestDeleteExchangeRate,
addExchangeRatesTableQueries, addExchangeRatesTableQueries,
requestDeleteBulkExchangeRates, requestDeleteBulkExchangeRates,
// #withDialog
openDialog,
}) { }) {
const { id } = useParams(); const { id } = useParams();
const [deleteExchangeRate, setDeleteExchangeRate] = useState(false); const [deleteExchangeRate, setDeleteExchangeRate] = useState(false);
const [selectedRows, setSelectedRows] = useState([]); const [selectedRows, setSelectedRows] = useState([]);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [bulkDelete, setBulkDelete] = useState(false); const [bulkDelete, setBulkDelete] = useState(false);
const [filter, setFilter] = useState({});
const fetchExchangeRates = useQuery('exchange-rates-table', const fetchExchangeRates = useQuery('exchange-rates-table', () =>
() => requestFetchExchangeRates()); requestFetchExchangeRates(),
);
useEffect(() => { useEffect(() => {
id id
? changePageTitle(formatMessage({id:'exchange_rate_details'})) ? changePageTitle(formatMessage({ id: 'exchange_rate_details' }))
: changePageTitle(formatMessage({id:'exchange_rate_list'})); : changePageTitle(formatMessage({ id: 'exchange_rate_list' }));
}, [id, changePageTitle,formatMessage]); }, [id, changePageTitle, formatMessage]);
const handelDeleteExchangeRate = useCallback( const handelDeleteExchangeRate = useCallback(
(exchange_rate) => { (exchange_rate) => {
setDeleteExchangeRate(exchange_rate); setDeleteExchangeRate(exchange_rate);
}, },
[setDeleteExchangeRate] [setDeleteExchangeRate],
); );
const handelEditExchangeRate = (exchange_rate) => {}; const handelEditExchangeRate = (exchange_rate) => {
openDialog('exchangeRate-form', { action: 'edit', id: exchange_rate.id });
};
const handelCancelExchangeRateDelete = useCallback(() => { const handelCancelExchangeRateDelete = useCallback(() => {
setDeleteExchangeRate(false); setDeleteExchangeRate(false);
}, [setDeleteExchangeRate]); }, [setDeleteExchangeRate]);
const handelConfirmExchangeRateDelete = useCallback(() => { const handelConfirmExchangeRateDelete = useCallback(() => {
requestDeleteExchangeRate(deleteExchangeRate.id).then(() => { requestDeleteExchangeRate(deleteExchangeRate.id)
setDeleteExchangeRate(false); .then(() => {
AppToaster.show({ setDeleteExchangeRate(false);
message: formatMessage({id:'the_exchange_rate_has_been_successfully_deleted'}), AppToaster.show({
message: formatMessage({
id: 'the_exchange_rates_has_been_successfully_deleted',
}),
intent: Intent.SUCCESS,
});
})
.catch(() => {
setDeleteExchangeRate(false);
}); });
}); }, [deleteExchangeRate, requestDeleteExchangeRate, formatMessage]);
}, [deleteExchangeRate, requestDeleteExchangeRate,formatMessage]);
// Handle fetch data of Exchange_rates datatable. // Handle fetch data of Exchange_rates datatable.
const handleFetchData = useCallback( const handleFetchData = useCallback(
@@ -81,58 +96,58 @@ function ExchangeRate({
: {}), : {}),
}); });
}, },
[addExchangeRatesTableQueries] [addExchangeRatesTableQueries],
); );
// Handle selected rows change.
const handleSelectedRowsChange = useCallback( const handleSelectedRowsChange = useCallback(
(exchange_rates) => { (exchange_rates) => {
setSelectedRows(exchange_rates); setSelectedRows(exchange_rates);
}, },
[setSelectedRows] [setSelectedRows],
); );
// Handle Exchange Rates bulk delete. // Handle Exchange Rates bulk delete.
const handleBulkDelete = useCallback( const handleBulkDelete = useCallback(
(exchangeRates) => { (exchangeRatesIds) => {
setBulkDelete(exchangeRates); setBulkDelete(exchangeRatesIds);
}, },
[setBulkDelete] [setBulkDelete],
); );
//Handel cancel itemCategories bulk delete. //Handel cancel itemCategories bulk delete.
const handleCancelBulkDelete =useCallback(()=>{ const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false) setBulkDelete(false);
},[]) }, []);
// handle confirm Exchange Rates bulk delete. // handle confirm Exchange Rates bulk delete.
const handleConfirmBulkDelete = useCallback(() => { const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkExchangeRates(bulkDelete) requestDeleteBulkExchangeRates(bulkDelete)
.then(() => { .then(() => {
setBulkDelete(false); setBulkDelete(false);
AppToaster.show({ AppToaster.show({
message: formatMessage({ message: formatMessage({
id: 'the_exchange_rates_has_been_successfully_deleted', id: 'the_exchange_rates_has_been_successfully_deleted',
}), }),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
});
})
.catch((errors) => {
setBulkDelete(false);
}); });
}) }, [requestDeleteBulkExchangeRates, bulkDelete, formatMessage]);
.catch((errors) => {
setBulkDelete(false);
});
}, [requestDeleteBulkExchangeRates, bulkDelete,formatMessage]);
// Calculates the data table selected rows count. // Calculates the data table selected rows count.
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [selectedRows]); const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
selectedRows,
]);
return ( return (
<DashboardInsider> <DashboardInsider loading={fetchExchangeRates.isFetching}>
<ExchangeRateActionsBar <ExchangeRateActionsBar
onDeleteExchangeRate={handelDeleteExchangeRate} onDeleteExchangeRate={handelDeleteExchangeRate}
selectedRows={selectedRows} selectedRows={selectedRows}
onBulkDelete={handleBulkDelete} onBulkDelete={handleBulkDelete}
/> />
<DashboardPageContent> <DashboardPageContent>
<ExchangeRateTable <ExchangeRateTable
@@ -143,33 +158,38 @@ const handleConfirmBulkDelete = useCallback(() => {
/> />
<Alert <Alert
cancelButtonText={<T id={'cancel'} />} cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'move_to_trash'} />} confirmButtonText={<T id={'delete'} />}
icon='trash' icon="trash"
intent={Intent.DANGER} intent={Intent.DANGER}
isOpen={deleteExchangeRate} isOpen={deleteExchangeRate}
onCancel={handelCancelExchangeRateDelete} onCancel={handelCancelExchangeRateDelete}
onConfirm={handelConfirmExchangeRateDelete} onConfirm={handelConfirmExchangeRateDelete}
> >
<p> <p>
<FormattedHTMLMessage id={'once_delete_this_exchange_rate_you_will_able_to_restore_it'}/> <FormattedHTMLMessage
id={'once_delete_this_exchange_rate_you_will_able_to_restore_it'}
/>
</p> </p>
</Alert> </Alert>
<Alert <Alert
cancelButtonText={<T id={'cancel'} />} cancelButtonText={<T id={'cancel'} />}
confirmButtonText={`${formatMessage({id:'delete'})} (${selectedRowsCount})`} confirmButtonText={`${formatMessage({
icon='trash' id: 'delete',
intent={Intent.DANGER} })} (${selectedRowsCount})`}
isOpen={bulkDelete} icon="trash"
onCancel={handleCancelBulkDelete} intent={Intent.DANGER}
onConfirm={handleConfirmBulkDelete} isOpen={bulkDelete}
> onCancel={handleCancelBulkDelete}
<p> onConfirm={handleConfirmBulkDelete}
<FormattedHTMLMessage >
id={'once_delete_these_exchange_rates_you_will_not_able_restore_them'} <p>
/> <FormattedHTMLMessage
</p> id={
</Alert> 'once_delete_these_exchange_rates_you_will_not_able_restore_them'
}
/>
</p>
</Alert>
</DashboardPageContent> </DashboardPageContent>
</DashboardInsider> </DashboardInsider>
); );
@@ -178,5 +198,6 @@ const handleConfirmBulkDelete = useCallback(() => {
export default compose( export default compose(
withExchangeRatesActions, withExchangeRatesActions,
withResourceActions, withResourceActions,
withDashboardActions withDashboardActions,
withDialog,
)(ExchangeRate); )(ExchangeRate);

View File

@@ -11,7 +11,7 @@ import {
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { If } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialog from 'connectors/Dialog.connector'; import withDialog from 'connectors/Dialog.connector';
@@ -19,8 +19,7 @@ import FilterDropdown from 'components/FilterDropdown';
import withResourceDetail from 'containers/Resources/withResourceDetails'; import withResourceDetail from 'containers/Resources/withResourceDetails';
import { compose } from 'utils'; import { compose } from 'utils';
import { FormattedMessage as T ,useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
function ExchangeRateActionsBar({ function ExchangeRateActionsBar({
// #withDialog. // #withDialog.
@@ -28,15 +27,15 @@ function ExchangeRateActionsBar({
// #withResourceDetail // #withResourceDetail
resourceFields, resourceFields,
// #ownProps // #ownProps
selectedRows = [], selectedRows = [],
onDeleteExchangeRate, onDeleteExchangeRate,
onFilterChanged, onFilterChanged,
onBulkDelete onBulkDelete,
}) { }) {
const [filterCount, setFilterCount] = useState(0); const [filterCount, setFilterCount] = useState(0);
const {formatMessage} =useIntl() const { formatMessage } = useIntl();
const onClickNewExchangeRate = () => { const onClickNewExchangeRate = () => {
openDialog('exchangeRate-form', {}); openDialog('exchangeRate-form', {});
@@ -51,27 +50,20 @@ const {formatMessage} =useIntl()
}, },
}); });
// const handelDeleteExchangeRate = useCallback(
// (exchangeRate) => {
// onDeleteExchangeRate(exchangeRate);
// },
// [selectedRows, onDeleteExchangeRate]
// );
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [ const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows, selectedRows,
]); ]);
const handelBulkDelete =useCallback(()=>{ const handelBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map(r=>r.id)); onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
},[onBulkDelete,selectedRows]) }, [onBulkDelete, selectedRows]);
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='plus' />} icon={<Icon icon="plus" />}
text={<T id={'new_exchange_rate'} />} text={<T id={'new_exchange_rate'} />}
onClick={onClickNewExchangeRate} onClick={onClickNewExchangeRate}
/> />
@@ -87,29 +79,30 @@ const {formatMessage} =useIntl()
filterCount <= 0 ? ( filterCount <= 0 ? (
<T id={'filter'} /> <T id={'filter'} />
) : ( ) : (
`${filterCount} ${formatMessage({id:'filters_applied'})}` `${filterCount} ${formatMessage({ id: 'filters_applied' })}`
) )
} }
icon={<Icon icon='filter' />} icon={<Icon icon="filter" />}
/> />
</Popover> </Popover>
{hasSelectedRows && ( <If condition={hasSelectedRows}>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='trash' iconSize={15} />} icon={<Icon icon="trash" iconSize={15} />}
text={<T id={'delete'} />} text={<T id={'delete'} />}
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={handelBulkDelete} onClick={handelBulkDelete}
/> />
)} </If>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='file-import' />} icon={<Icon icon="file-import" />}
text={<T id={'import'} />} text={<T id={'import'} />}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='file-export' />} icon={<Icon icon="file-export" />}
text={<T id={'export'} />} text={<T id={'export'} />}
/> />
</NavbarGroup> </NavbarGroup>
@@ -128,5 +121,5 @@ export default compose(
withDialog, withDialog,
withResourceDetail(({ resourceFields }) => ({ withResourceDetail(({ resourceFields }) => ({
resourceFields, resourceFields,
})) })),
)(ExchangeRateActionsBar); )(ExchangeRateActionsBar);

View File

@@ -1,16 +1,15 @@
import React, { useCallback, useMemo,useState } from 'react'; import React, { useCallback, useMemo, useState, useEffect } from 'react';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import DialogConnect from 'connectors/Dialog.connector'; import DialogConnect from 'connectors/Dialog.connector';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import { Button, Popover, Menu, MenuItem, Position } from '@blueprintjs/core'; import { Button, Popover, Menu, MenuItem, Position,Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import LoadingIndicator from 'components/LoadingIndicator';
import withExchangeRatesActions from 'containers/ExchangeRates/withExchangeRatesActions'; import withExchangeRatesActions from 'containers/ExchangeRates/withExchangeRatesActions';
import withExchangeRates from 'containers/ExchangeRates/withExchangeRates'; import withExchangeRates from 'containers/ExchangeRates/withExchangeRates';
import { compose } from 'utils'; import { compose } from 'utils';
function ExchangeRateTable({ function ExchangeRateTable({
// #withExchangeRates // #withExchangeRates
exchangeRatesList, exchangeRatesList,
@@ -25,7 +24,6 @@ function ExchangeRateTable({
onEditExchangeRate, onEditExchangeRate,
onSelectedRowsChange, onSelectedRowsChange,
}) { }) {
const [initialMount, setInitialMount] = useState(false); const [initialMount, setInitialMount] = useState(false);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -47,69 +45,81 @@ function ExchangeRateTable({
/> />
<MenuItem <MenuItem
text={<T id={'delete_exchange_rate'} />} text={<T id={'delete_exchange_rate'} />}
intent={Intent.DANGER}
onClick={handleDeleteExchangeRate(ExchangeRate)} onClick={handleDeleteExchangeRate(ExchangeRate)}
/> />
</Menu> </Menu>
), ),
[handelEditExchangeRate, handleDeleteExchangeRate] [handelEditExchangeRate, handleDeleteExchangeRate],
); );
const columns = useMemo(() => [ const columns = useMemo(
{ () => [
id: 'date', {
Header: formatMessage({ id: 'date' }), id: 'date',
// accessor: 'date', Header: formatMessage({ id: 'date' }),
width: 150, // accessor: 'date',
}, width: 150,
{ },
id: 'currency_code', {
Header: formatMessage({ id: 'currency_code' }), id: 'currency_code',
accessor: 'currency_code', Header: formatMessage({ id: 'currency_code' }),
className: 'currency_code', accessor: 'currency_code',
width: 150, className: 'currency_code',
}, width: 150,
{ },
id: 'exchange_rate', {
Header: formatMessage({ id: 'exchange_rate' }), id: 'exchange_rate',
accessor: 'exchange_rate', Header: formatMessage({ id: 'exchange_rate' }),
className: 'exchange_rate', accessor: 'exchange_rate',
width: 150, className: 'exchange_rate',
}, width: 150,
{ },
id: 'actions', {
Header: '', id: 'actions',
Cell: ({ cell }) => ( Header: '',
<Popover Cell: ({ cell }) => (
content={actionMenuList(cell.row.original)} <Popover
position={Position.RIGHT_BOTTOM} content={actionMenuList(cell.row.original)}
> position={Position.RIGHT_BOTTOM}
<Button icon={<Icon icon='ellipsis-h' />} /> >
</Popover> <Button icon={<Icon icon="ellipsis-h" />} />
), </Popover>
className: 'actions', ),
width: 50, className: 'actions',
disableResizing: false, width: 50,
}, disableResizing: false,
], [actionMenuList,formatMessage]); },
],
[actionMenuList, formatMessage],
);
const selectionColumn = useMemo(() => ({ const selectionColumn = useMemo(
minWidth: 42, () => ({
width: 42, minWidth: 42,
maxWidth: 42, width: 42,
}), []); maxWidth: 42,
}),
[],
);
const handelFetchData = useCallback( const handelFetchData = useCallback(
(...params) => { (...params) => {
onFetchData && onFetchData(...params); onFetchData && onFetchData(...params);
}, },
[onFetchData] [onFetchData],
); );
const handelSelectedRowsChange = useCallback((selectRows) => { const handelSelectedRowsChange = useCallback(
onSelectedRowsChange && onSelectedRowsChange(selectRows.map((c) => c.original)); (selectRows) => {
}, [onSelectedRowsChange]); onSelectedRowsChange &&
onSelectedRowsChange(selectRows.map((c) => c.original));
},
[onSelectedRowsChange],
);
return ( return (
<LoadingIndicator loading={loading} mount={false}>
<DataTable <DataTable
columns={columns} columns={columns}
data={exchangeRatesList} data={exchangeRatesList}
@@ -122,14 +132,15 @@ function ExchangeRateTable({
onSelectedRowsChange={handelSelectedRowsChange} onSelectedRowsChange={handelSelectedRowsChange}
spinnerProps={{ size: 30 }} spinnerProps={{ size: 30 }}
/> />
</LoadingIndicator>
); );
} }
export default compose( export default compose(
DialogConnect, DialogConnect,
withExchangeRatesActions, withExchangeRatesActions,
withExchangeRates(({ exchangeRatesList ,exchangeRatesLoading }) => ({ withExchangeRates(({ exchangeRatesList, exchangeRatesLoading }) => ({
exchangeRatesList, exchangeRatesList,
exchangeRatesLoading exchangeRatesLoading,
})) })),
)(ExchangeRateTable); )(ExchangeRateTable);

View File

@@ -183,7 +183,7 @@ const ItemCategoryList = ({
> >
<p> <p>
<FormattedHTMLMessage <FormattedHTMLMessage
id={'once_delete_these_item_categories_you_will_not_able_restore_them'} id={'once_delete_these_items_you_will_not_able_restore_them'}
/> />
</p> </p>
</Alert> </Alert>

View File

@@ -5,6 +5,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
Position, Position,
Intent
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
@@ -37,6 +38,7 @@ const ItemsCategoryList = ({
onClick={() => handelEditCategory(category)} /> onClick={() => handelEditCategory(category)} />
<MenuItem <MenuItem
text={<T id={'delete_category'}/>} text={<T id={'delete_category'}/>}
intent={Intent.DANGER}
onClick={() => handleDeleteCategory(category)} onClick={() => handleDeleteCategory(category)}
/> />
</Menu> </Menu>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState, useMemo } from 'react'; import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { import {
Button, Button,
Popover, Popover,
@@ -8,7 +8,7 @@ import {
Alert, Alert,
Intent, Intent,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useQuery } from 'react-query'; import { useQuery, queryCache } from 'react-query';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -18,133 +18,165 @@ import LoadingIndicator from 'components/LoadingIndicator';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import withDashboard from 'connectors/Dashboard.connector'; import withDashboard from 'containers/Dashboard/withDashboard';
import withCurrencies from 'containers/Currencies/withCurrencies'; import withCurrencies from 'containers/Currencies/withCurrencies';
import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions'; import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
import { FormattedMessage as T, useIntl } from 'react-intl'; import {
FormattedMessage as T,
FormattedHTMLMessage,
useIntl,
} from 'react-intl';
function CurrenciesList({ function CurrenciesList({
// #withCurrencies // #withCurrencies
currenciesList,
currenciesList,
currenciesLoading,
// #withCurrenciesActions // #withCurrenciesActions
requestDeleteCurrency, requestDeleteCurrency,
requestFetchCurrencies, requestFetchCurrencies,
// #withDialog // #withDialog
openDialog, openDialog,
changePreferencesPageTitle,
// #ownProps
onFetchData, onFetchData,
}) { }) {
const [deleteCurrencyState, setDeleteCurrencyState] = useState(false); const [deleteCurrencyState, setDeleteCurrencyState] = useState(false);
const { formatMessage } = useIntl() const [tableLoading, setTableLoading] = useState(false);
const fetchCurrencies = useQuery(['currencies-table'], const [initialMount, setInitialMount] = useState(false);
() => requestFetchCurrencies());
// const handleEditCurrency = (currency) => { const { formatMessage } = useIntl();
// openDialog('currency-form', {
// action: 'edit',
// currencyCode: currency.currency_code,
// });
// };
const handleEditCurrency =useCallback((currency)=>{
openDialog('currency-form', {
action: 'edit',
currencyCode: currency.currency_code,
});
},[openDialog])
const fetchCurrencies = useQuery('currencies-table', () =>
requestFetchCurrencies(),
);
// const onDeleteCurrency = (currency) => { useEffect(() => {
// setDeleteCurrencyState(currency); changePreferencesPageTitle(formatMessage({ id: 'currencies' }));
// }; }, [changePreferencesPageTitle, formatMessage]);
const onDeleteCurrency =useCallback((currency)=>{setDeleteCurrencyState(currency);},[]) const handleEditCurrency = useCallback(
(currency) => {
openDialog('currency-form', {
action: 'edit',
currencyCode: currency.currency_code,
});
},
[openDialog],
);
const onDeleteCurrency = useCallback((currency) => {
setDeleteCurrencyState(currency);
}, []);
const handleCancelCurrencyDelete = () => { const handleCancelCurrencyDelete = () => {
setDeleteCurrencyState(false); setDeleteCurrencyState(false);
}; };
const handleConfirmCurrencyDelete = useCallback(() => { const handleConfirmCurrencyDelete = useCallback(
requestDeleteCurrency(deleteCurrencyState.currency_code).then( (refetch) => {
(response) => { requestDeleteCurrency(deleteCurrencyState.currency_code)
setDeleteCurrencyState(false); .then((response) => {
AppToaster.show({ setDeleteCurrencyState(false);
message: formatMessage({id:'the_currency_has_been_successfully_deleted'}), AppToaster.show({
message: formatMessage({
id: 'the_currency_has_been_successfully_deleted',
}),
intent: Intent.SUCCESS,
});
})
.catch((errors) => {
setDeleteCurrencyState(false);
}); });
}
);
}, [requestDeleteCurrency,deleteCurrencyState,formatMessage]);
const actionMenuList = useCallback((currency) => (
<Menu>
<MenuItem
text={<T id={'edit_currency'} />}
onClick={() => handleEditCurrency(currency)} />
<MenuItem
text={<T id={'delete_currency'} />}
onClick={() => onDeleteCurrency(currency)}
/>
</Menu>
), [handleEditCurrency,onDeleteCurrency]);
const columns = useMemo(() => [
{
Header: formatMessage({id:'currency_name'}),
accessor: 'currency_name',
width: 100,
}, },
{ [deleteCurrencyState, requestDeleteCurrency, formatMessage],
Header: formatMessage({id:'currency_code'}), );
accessor: 'currency_code',
className: 'currency_code',
width: 100,
},
{
Header: 'Currency sign',
width: 50,
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_TOP}
>
<Button icon={<Icon icon='ellipsis-h' />} />
</Popover>
),
className: 'actions',
width: 50,
},
], [actionMenuList,formatMessage]);
const handleDatatableFetchData = useCallback(() => { const actionMenuList = useCallback(
(currency) => (
<Menu>
<MenuItem
text={<T id={'edit_currency'} />}
onClick={() => handleEditCurrency(currency)}
/>
<MenuItem
text={<T id={'delete_currency'} />}
onClick={() => onDeleteCurrency(currency)}
intent={Intent.DANGER}
/>
</Menu>
),
[handleEditCurrency, onDeleteCurrency],
);
const columns = useMemo(
() => [
{
Header: formatMessage({ id: 'currency_name' }),
accessor: 'currency_name',
width: 150,
},
{
Header: formatMessage({ id: 'currency_code' }),
accessor: 'currency_code',
className: 'currency_code',
width: 120,
},
{
Header: 'Currency sign',
width: 120,
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_TOP}
>
<Button icon={<Icon icon="ellipsis-h" />} />
</Popover>
),
className: 'actions',
width: 50,
},
],
[actionMenuList, formatMessage],
);
const handleDataTableFetchData = useCallback(() => {
onFetchData && onFetchData(); onFetchData && onFetchData();
}, [onFetchData]); }, []);
return ( return (
<LoadingIndicator> <LoadingIndicator loading={fetchCurrencies.isFetching}>
<DataTable <DataTable
noInitialFetch={true}
columns={columns} columns={columns}
data={currenciesList} data={currenciesList}
loading={fetchCurrencies.isFetching} onFetchData={handleDataTableFetchData()}
loading={currenciesLoading && !initialMount}
selectionColumn={false} selectionColumn={false}
manualSortBy={true}
expandable={false}
/> />
<Alert <Alert
cancelButtonText={<T id={'cancel'}/>} cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'move_to_trash'}/>} confirmButtonText={<T id={'delete'} />}
icon='trash' icon="trash"
intent={Intent.DANGER} intent={Intent.DANGER}
isOpen={deleteCurrencyState} isOpen={deleteCurrencyState}
onCancel={handleCancelCurrencyDelete} onCancel={handleCancelCurrencyDelete}
onConfirm={handleConfirmCurrencyDelete} onConfirm={handleConfirmCurrencyDelete}
> >
<p> <p>
Are you sure you want to move <b>filename</b> to Trash? You will be <FormattedHTMLMessage
able to restore it later, but it will become private to you. id={'once_delete_this_currency_you_will_able_to_restore_it'}
/>
</p> </p>
</Alert> </Alert>
</LoadingIndicator> </LoadingIndicator>
@@ -152,10 +184,10 @@ function CurrenciesList({
} }
export default compose( export default compose(
DialogConnect,
withDashboard,
withCurrencies(({ currenciesList }) => ({ withCurrencies(({ currenciesList }) => ({
currenciesList, currenciesList,
})), })),
withCurrenciesActions, withCurrenciesActions,
DialogConnect,
withDashboard
)(CurrenciesList); )(CurrenciesList);

View File

@@ -1,4 +1,4 @@
import React, { useState} from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { import {
@@ -8,81 +8,153 @@ import {
Intent, Intent,
MenuItem, MenuItem,
Classes, Classes,
Spinner,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone'; import { TimezonePicker } from '@blueprintjs/timezone';
import { Select } from '@blueprintjs/select'; import { useQuery, queryCache } from 'react-query';
import { useQuery } from 'react-query';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import { compose, optionsMapToArray } from 'utils'; import { compose, optionsMapToArray } from 'utils';
import ErrorMessage from 'components/ErrorMessage'; import ErrorMessage from 'components/ErrorMessage';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import { ListSelect } from 'components';
import { If } from 'components';
import withDashboard from 'containers/Dashboard/withDashboard';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import withSettingsActions from 'containers/Settings/withSettingsActions'; import withSettingsActions from 'containers/Settings/withSettingsActions';
function GeneralPreferences({ function GeneralPreferences({
// #withSettings // #withSettings
organizationSettings, organizationSettings,
//# withDashboard
changePreferencesPageTitle,
// #withSettingsActions // #withSettingsActions
requestSubmitOptions, requestSubmitOptions,
requestFetchOptions, requestFetchOptions,
}) { }) {
const {formatMessage} = useIntl(); const { formatMessage } = useIntl();
const [selectedItems, setSelectedItems] = useState({}); const [selectedItems, setSelectedItems] = useState({});
const [timeZone, setTimeZone] = useState(''); const [timeZone, setTimeZone] = useState('');
const fetchHook = useQuery(['settings'], const fetchHook = useQuery(
() => { requestFetchOptions(); }); ['settings'],
() => {
requestFetchOptions();
},
{ manual: true },
);
const businessLocation = [ useEffect(() => {
{ id: 218, name: 'LIBYA', code: 'LY' }, changePreferencesPageTitle(formatMessage({ id: 'general' }));
{ id: 380, name: 'UKRAINE', code: 'UA' }, }, [changePreferencesPageTitle, formatMessage]);
{ id: 212, name: 'Morocco', code: 'MA' },
]; const businessLocation = [{ id: 218, name: 'LIBYA', value: 'libya' }];
const languagesDisplay = [ const languagesDisplay = [
{ id: 0, name: 'EN', label: 'English' }, { id: 0, name: 'English', value: 'EN' },
{ id: 1, name: 'Arb', label: 'Arabic' }, { id: 1, name: 'Arabic', value: 'Arab ' },
]; ];
const currencies = [ const currencies = [
{ id: 0, name: 'US Dollar', code: 'USD' }, { id: 0, name: 'US Dollar', value: 'USD' },
{ id: 1, name: 'Euro', code: 'EUR' }, { id: 1, name: 'Euro', value: 'EUR' },
{ id: 2, name: 'Libyan Dinar ', code: 'LYD' }, { id: 1, name: 'Libyan Dinar ', value: 'LYD' },
]; ];
const fiscalYear = [ const fiscalYear = [
{ id: 0, name: 'January-July', label: 'January-July' }, { id: 0, name: 'January - December', value: 'january' },
{ id: 1, name: 'August-December', label: 'August-December' }, { id: 1, name: 'February - January', value: 'february' },
{ id: 2, name: 'March - February', value: 'March' },
{ id: 3, name: 'April - March', value: 'april' },
{ id: 4, name: 'May - April', value: 'may' },
{ id: 5, name: 'June - May', value: 'june' },
{ id: 6, name: 'July - June', value: 'july' },
{ id: 7, name: 'August - July', value: 'August' },
{ id: 8, name: 'September - August', value: 'september' },
{ id: 9, name: 'October - September', value: 'october' },
{ id: 10, name: 'November - October', value: 'november' },
{ id: 11, name: 'December - November', value: 'December' },
]; ];
const dateFormat = [ const dateFormat = [
{ id: 1, name: '04/11/2020', format: 'MM-DD-YY' }, {
{ id: 3, name: '2020/03/21', format: 'YY/MM/DD' }, id: 1,
{ id: 4, name: 'Apr 11, 2020', format: 'MMMM yyyy' }, name: 'MM/DD/YY',
{ id: 6, name: 'Saturday, Apr 11, 2020', format: 'EEEE, MMM d, yyyy' }, label: `${moment().format('MM/DD/YYYY')}`,
value: 'mm/dd/yy',
},
{
id: 2,
name: 'DD/MM/YY',
label: `${moment().format('DD/MM/YYYY')}`,
value: 'dd/mm/yy',
},
{
id: 3,
name: 'YY/MM/DD',
label: `${moment().format('YYYY/MM/DD')}`,
value: 'yy/mm/dd',
},
{
id: 4,
name: 'MM-DD-YY',
label: `${moment().format('MM-DD-YYYY')}`,
value: 'mm-dd-yy',
},
{
id: 5,
name: 'DD-MM-YY',
label: `${moment().format('DD-MM-YYYY')}`,
value: 'dd-mm-yy',
},
{
id: 6,
name: 'YY-MM-DD',
label: `${moment().format('YYYY-MM-DD')}`,
value: 'yy-mm-dd',
},
]; ];
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
name: Yup.string().required().label(formatMessage({id:'organization_name_'})), name: Yup.string()
industry: Yup.string().required().label(formatMessage({id:'organization_industry_'})), .required()
location: Yup.string().required().label(formatMessage({id:'location'})), .label(formatMessage({ id: 'organization_name_' })),
base_currency: Yup.string().required( industry: Yup.string()
formatMessage({ id: 'required' }) .required()
), .label(formatMessage({ id: 'organization_industry_' })),
fiscal_year: Yup.string().required().label(formatMessage({id:'base_currency_'})), location: Yup.string()
language: Yup.string().required().label(formatMessage({id:'language'})), .required()
// time_zone: Yup.object().required()..label(formatMessage({id:''})), .label(formatMessage({ id: 'location' })),
date_format: Yup.date().required().label(formatMessage({id:'date_format_'})), base_currency: Yup.string().required(formatMessage({ id: 'required' })),
fiscal_year: Yup.string()
.required()
.label(formatMessage({ id: 'base_currency_' })),
language: Yup.string()
.required()
.label(formatMessage({ id: 'language' })),
// time_zone: Yup.string()
// .required()
// .label(formatMessage({ id: 'time_zone' })),
date_format: Yup.string()
.required()
.label(formatMessage({ id: 'date_format_' })),
}); });
const query = queryCache.refetchQueries('settings');
const { const {
values,
errors, errors,
touched, touched,
setFieldValue, setFieldValue,
getFieldProps, getFieldProps,
handleSubmit, handleSubmit,
resetForm,
isSubmitting,
} = useFormik({ } = useFormik({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
@@ -93,13 +165,17 @@ function GeneralPreferences({
const options = optionsMapToArray(values).map((option) => { const options = optionsMapToArray(values).map((option) => {
return { key: option.key, ...option, group: 'organization' }; return { key: option.key, ...option, group: 'organization' };
}); });
requestSubmitOptions({ options }) requestSubmitOptions({ options })
.then((response) => { .then((response) => {
AppToaster.show({ AppToaster.show({
message: 'The_Options_has_been_Submit', message: formatMessage({
id: 'the_options_has_been_successfully_created',
}),
intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
resetForm();
queryCache.refetchQueries('settings', { force: true });
}) })
.catch((error) => { .catch((error) => {
setSubmitting(false); setSubmitting(false);
@@ -112,7 +188,6 @@ function GeneralPreferences({
className={'preferences-menu'} className={'preferences-menu'}
key={item.id} key={item.id}
text={item.name} text={item.name}
label={item.code}
onClick={handleClick} onClick={handleClick}
/> />
); );
@@ -122,7 +197,7 @@ function GeneralPreferences({
className={'preferences-menu'} className={'preferences-menu'}
key={item.id} key={item.id}
text={item.name} text={item.name}
label={item.code} label={item.value}
onClick={handleClick} onClick={handleClick}
/> />
); );
@@ -140,7 +215,6 @@ function GeneralPreferences({
className={'preferences-menu'} className={'preferences-menu'}
key={item.id} key={item.id}
text={item.name} text={item.name}
label={item.label}
onClick={handleClick} onClick={handleClick}
/> />
); );
@@ -149,8 +223,8 @@ function GeneralPreferences({
<MenuItem <MenuItem
className={'preferences-menu'} className={'preferences-menu'}
key={item.id} key={item.id}
text={item.format} text={item.name}
label={item.name} label={item.label}
onClick={handleClick} onClick={handleClick}
/> />
); );
@@ -161,218 +235,228 @@ function GeneralPreferences({
...selectedItems, ...selectedItems,
[filedName]: filed, [filedName]: filed,
}); });
setFieldValue(filedName, filed.name); setFieldValue(filedName, filed.value);
}; };
}; };
const getSelectedItemLabel = (filedName, defaultLabel) => { const filterItems = (query, item, _index, exactMatch) => {
return typeof selectedItems[filedName] !== 'undefined' const normalizedTitle = item.name.toLowerCase();
? selectedItems[filedName].name const normalizedQuery = query.toLowerCase();
: defaultLabel;
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
}; };
const handleTimezoneChange = (timezone) => setTimeZone(timezone); const handleTimezoneChange = (timezone) => setTimeZone(timezone);
return ( return (
<div className='preferences__inside-content--general'> <div
className={classNames({
'preferences__inside-content--general': true,
preferences__loading: fetchHook.pending,
})}
>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<FormGroup <FormGroup
label={<T id={'organization_name'}/>} label={<T id={'organization_name'} />}
inline={true} inline={true}
intent={(errors.name && touched.name) && Intent.DANGER} intent={errors.name && touched.name && Intent.DANGER}
helperText={<ErrorMessage name='name' {...{errors, touched}} />} helperText={<ErrorMessage name="name" {...{ errors, touched }} />}
> >
<InputGroup <InputGroup
medium={'true'} medium={'true'}
intent={(errors.name && touched.name) && Intent.DANGER} intent={errors.name && touched.name && Intent.DANGER}
{...getFieldProps('name')} {...getFieldProps('name')}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'organization_industry'}/>} label={<T id={'organization_industry'} />}
inline={true} inline={true}
intent={(errors.industry && touched.industry) && Intent.DANGER} intent={errors.industry && touched.industry && Intent.DANGER}
helperText={<ErrorMessage name='industry' {...{errors, touched}} />} helperText={<ErrorMessage name="industry" {...{ errors, touched }} />}
> >
<InputGroup <InputGroup
medium={'true'} medium={'true'}
intent={(errors.industry && touched.industry) && Intent.DANGER} intent={errors.industry && touched.industry && Intent.DANGER}
{...getFieldProps('industry')} {...getFieldProps('industry')}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'business_location'}/>} label={<T id={'business_location'} />}
className={classNames( className={classNames(
'form-group--business-location', 'form-group--business-location',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.FILL,
)} )}
inline={true} inline={true}
helperText={<ErrorMessage name='location' {...{errors, touched}} />} helperText={<ErrorMessage name="location" {...{ errors, touched }} />}
intent={(errors.location && touched.location) && Intent.DANGER} intent={errors.location && touched.location && Intent.DANGER}
> >
<Select <ListSelect
items={businessLocation} items={businessLocation}
noResults={<MenuItem disabled={true} text='No result.' />} noResults={<MenuItem disabled={true} text="No result." />}
itemRenderer={businessLocationItem} itemRenderer={businessLocationItem}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onItemsSelect('location')} onItemSelect={onItemsSelect('location')}
> selectedItem={values.location}
<Button selectedItemProp={'value'}
fill={true} defaultText={<T id={'select_business_location'} />}
rightIcon='caret-down' labelProp={'name'}
text={getSelectedItemLabel( />
'location',
organizationSettings.location
)}
/>
</Select>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'base_currency'}/>} label={<T id={'base_currency'} />}
className={classNames( className={classNames(
'form-group--base-currency', 'form-group--base-currency',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.LOADING,
Classes.FILL,
)} )}
inline={true} inline={true}
helperText={<ErrorMessage name='base_currency' {...{ errors, touched }} />} helperText={
intent={(errors.base_currency && touched.base_currency) && Intent.DANGER} <ErrorMessage name="base_currency" {...{ errors, touched }} />
}
intent={
errors.base_currency && touched.base_currency && Intent.DANGER
}
> >
<Select <ListSelect
items={currencies} items={currencies}
noResults={<MenuItem disabled={true} text='No result.' />} noResults={<MenuItem disabled={true} text="No result." />}
itemRenderer={currencyItem} itemRenderer={currencyItem}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onItemsSelect('base_currency')} onItemSelect={onItemsSelect('base_currency')}
> itemPredicate={filterItems}
<Button selectedItem={values.base_currency}
fill={true} selectedItemProp={'value'}
rightIcon='caret-down' defaultText={<T id={'select_base_currency'} />}
text={getSelectedItemLabel( labelProp={'name'}
'base_currency', />
organizationSettings.base_currency
)}
/>
</Select>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'fiscal_year'}/>} label={<T id={'fiscal_year'} />}
className={classNames( className={classNames(
'form-group--fiscal-year', 'form-group--fiscal-year',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.FILL,
)} )}
inline={true} inline={true}
helperText={<ErrorMessage name='fiscal_year' {...{errors, touched}} />} helperText={
intent={(errors.fiscal_year && touched.fiscal_year) && Intent.DANGER} <ErrorMessage name="fiscal_year" {...{ errors, touched }} />
}
intent={errors.fiscal_year && touched.fiscal_year && Intent.DANGER}
> >
<Select <ListSelect
items={fiscalYear} items={fiscalYear}
noResults={<MenuItem disabled={true} text='No result.' />} noResults={<MenuItem disabled={true} text="No result." />}
itemRenderer={fiscalYearItem} itemRenderer={fiscalYearItem}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onItemsSelect('fiscal_year')} onItemSelect={onItemsSelect('fiscal_year')}
> itemPredicate={filterItems}
<Button selectedItem={values.fiscal_year}
fill={true} selectedItemProp={'value'}
rightIcon='caret-down' defaultText={<T id={'select_fiscal_year'} />}
text={getSelectedItemLabel( labelProp={'name'}
'fiscal_year', />
organizationSettings.fiscal_year
)}
/>
</Select>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'language'}/>} label={<T id={'language'} />}
inline={true} inline={true}
className={classNames( className={classNames(
'form-group--language', 'form-group--language',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.FILL,
)} )}
intent={(errors.language && touched.language) && Intent.DANGER} intent={errors.language && touched.language && Intent.DANGER}
helperText={<ErrorMessage name='language' {...{errors, touched}} />} helperText={<ErrorMessage name="language" {...{ errors, touched }} />}
> >
<Select <ListSelect
items={languagesDisplay} items={languagesDisplay}
noResults={<MenuItem disabled={true} text='No results.' />} noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={languageItem} itemRenderer={languageItem}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
onItemSelect={onItemsSelect('language')} onItemSelect={onItemsSelect('language')}
> itemPredicate={filterItems}
<Button selectedItem={values.language}
rightIcon='caret-down' selectedItemProp={'value'}
text={getSelectedItemLabel( defaultText={<T id={'select_language'} />}
'language', labelProp={'name'}
organizationSettings.language />
)}
/>
</Select>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'time_zone'}/>} label={<T id={'time_zone'} />}
inline={true} inline={true}
className={classNames( className={classNames(
'form-group--time-zone', 'form-group--time-zone',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.FILL,
)} )}
intent={(errors.time_zone && touched.time_zone) && Intent.DANGER} intent={errors.time_zone && touched.time_zone && Intent.DANGER}
helperText={<ErrorMessage name='time_zone' {...{errors, touched}} />} helperText={
<ErrorMessage name="time_zone" {...{ errors, touched }} />
}
> >
<TimezonePicker <TimezonePicker
value={timeZone} value={timeZone}
onChange={handleTimezoneChange} onChange={handleTimezoneChange}
showLocalTimezone={true} valueDisplayFormat="composite"
valueDisplayFormat='composite' placeholder={<T id={'select_time_zone'} />}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
label={<T id={'date_format'}/>} label={<T id={'date_format'} />}
inline={true} inline={true}
className={classNames( className={classNames(
'form-group--language', 'form-group--language',
'form-group--select-list', 'form-group--select-list',
Classes.FILL Classes.FILL,
)} )}
intent={(errors.date_format && touched.date_format) && Intent.DANGER} intent={errors.date_format && touched.date_format && Intent.DANGER}
helperText={<ErrorMessage name='date_format' {...{errors, touched}} />} helperText={
<ErrorMessage name="date_format" {...{ errors, touched }} />
}
> >
<Select <ListSelect
items={dateFormat} items={dateFormat}
noResults={<MenuItem disabled={true} text='No result.' />} noResults={<MenuItem disabled={true} text="No result." />}
itemRenderer={date_format} itemRenderer={date_format}
popoverProp={{ minimal: true }} popoverProp={{ minimal: true }}
onItemSelect={onItemsSelect('date_format')} onItemSelect={onItemsSelect('date_format')}
> itemPredicate={filterItems}
<Button selectedItem={values.date_format}
rightIcon='caret-down' selectedItemProp={'value'}
text={getSelectedItemLabel( defaultText={<T id={'select_date_format'} />}
'date_format', labelProp={'name'}
organizationSettings.date_format />
)}
/>
</Select>
</FormGroup> </FormGroup>
<div className={'preferences__floating-footer '}> <div className={'preferences__floating-footer '}>
<Button <Button
className={'preferences-button'} className={'preferences-button'}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
type='submit' type="submit"
loading={isSubmitting}
> >
<T id={'save'}/> <T id={'save'} />
</Button>
<Button onClick={'handleClose'}>
<T id={'close'} />
</Button> </Button>
<Button onClick={'handleClose'}><T id={'close'}/></Button>
</div> </div>
</form> </form>
<If condition={fetchHook.isFetching || isSubmitting}>
<div className={'preferences__loading-overlay'}>
<Spinner size={40} />
</div>
</If>
</div> </div>
); );
} }
@@ -380,4 +464,5 @@ function GeneralPreferences({
export default compose( export default compose(
withSettings, withSettings,
withSettingsActions, withSettingsActions,
withDashboard,
)(GeneralPreferences); )(GeneralPreferences);

View File

@@ -16,7 +16,7 @@ function UsersActions({
}, [openDialog]); }, [openDialog]);
return ( return (
<div claass="preferences-actions"> <div className="preferences-actions">
<Button <Button
icon={<Icon icon='plus' iconSize={12} />} icon={<Icon icon='plus' iconSize={12} />}
onClick={onClickNewUser} onClick={onClickNewUser}

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo, useCallback } from 'react'; import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { queryCache, useQuery } from 'react-query'; import { queryCache, useQuery } from 'react-query';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import { import {
@@ -21,6 +21,7 @@ import {
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import LoadingIndicator from 'components/LoadingIndicator'; import LoadingIndicator from 'components/LoadingIndicator';
import { If } from 'components';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
@@ -34,7 +35,7 @@ import { compose } from 'utils';
function UsersListPreferences({ function UsersListPreferences({
// #withDialog // #withDialog
openDialog, openDialog,
closeDialog, changePreferencesPageTitle,
// #withUsers // #withUsers
usersList, usersList,
@@ -52,9 +53,14 @@ function UsersListPreferences({
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fetchUsers = useQuery('users-table', () => requestFetchUsers()); const fetchUsers = useQuery('users-table', () => requestFetchUsers());
const onInactiveUser = (user) => { useEffect(() => {
changePreferencesPageTitle(formatMessage({ id: 'users' }));
}, [changePreferencesPageTitle, formatMessage]);
const onInactiveUser = useCallback((user) => {
setInactiveUserState(user); setInactiveUserState(user);
}; }, []);
// Handle cancel inactive user alert // Handle cancel inactive user alert
const handleCancelInactiveUser = useCallback(() => { const handleCancelInactiveUser = useCallback(() => {
@@ -63,38 +69,42 @@ function UsersListPreferences({
// handel confirm user activation // handel confirm user activation
const handleConfirmUserActive = useCallback(() => { const handleConfirmUserActive = useCallback(() => {
requestInactiveUser(inactiveUserState.id).then(() => { requestInactiveUser(inactiveUserState.id)
setInactiveUserState(false); .then(() => {
AppToaster.show({ setInactiveUserState(false);
message: formatMessage({ AppToaster.show({
id: 'the_user_has_been_successfully_inactivated', message: formatMessage({
}), id: 'the_user_has_been_successfully_inactivated',
}),
intent: Intent.SUCCESS,
});
queryCache.refetchQueries('users-table', { force: true });
})
.catch((error) => {
setInactiveUserState(false);
}); });
}); }, [inactiveUserState, requestInactiveUser, formatMessage]);
}, [
inactiveUserState,
requestInactiveUser,
requestFetchUsers,
formatMessage,
]);
const onDeleteUser = (user) => { const onDeleteUser = useCallback((user) => {
setDeleteUserState(user); setDeleteUserState(user);
}; }, []);
const handleCancelUserDelete = () => { const handleCancelUserDelete = () => {
setDeleteUserState(false); setDeleteUserState(false);
}; };
const onEditUser = (user) => () => { const onEditUser = useCallback(
const form = Object.keys(user).reduce((obj, key) => { (user) => () => {
const camelKey = snakeCase(key); const form = Object.keys(user).reduce((obj, key) => {
obj[camelKey] = user[key]; const camelKey = snakeCase(key);
return obj; obj[camelKey] = user[key];
}, {}); return obj;
}, {});
openDialog('userList-form', { action: 'edit', user: form }); openDialog('userList-form', { action: 'edit', user: form });
}; },
[openDialog],
);
// Handle confirm User delete // Handle confirm User delete
@@ -121,19 +131,24 @@ function UsersListPreferences({
const actionMenuList = useCallback( const actionMenuList = useCallback(
(user) => ( (user) => (
<Menu> <Menu>
<MenuItem text={<T id={'edit_user'} />} onClick={onEditUser(user)} /> <If condition={user.invite_accepted_at}>
<MenuDivider /> <MenuItem text={<T id={'edit_user'} />} onClick={onEditUser(user)} />
<MenuItem <MenuDivider />
text={<T id={'inactivate_user'} />}
onClick={() => onInactiveUser(user)} <MenuItem
/> text={<T id={'inactivate_user'} />}
onClick={() => onInactiveUser(user)}
/>
</If>
<MenuItem <MenuItem
text={<T id={'delete_user'} />} text={<T id={'delete_user'} />}
onClick={() => onDeleteUser(user)} onClick={() => onDeleteUser(user)}
intent={Intent.DANGER}
/> />
</Menu> </Menu>
), ),
[], [onInactiveUser, onDeleteUser, onEditUser],
); );
const columns = useMemo( const columns = useMemo(
@@ -160,7 +175,11 @@ function UsersListPreferences({
id: 'status', id: 'status',
Header: 'Status', Header: 'Status',
accessor: (user) => accessor: (user) =>
user.active ? ( !user.invite_accepted_at ? (
<Tag minimal={true}>
<T id={'inviting'} />
</Tag>
) : user.active ? (
<Tag intent={Intent.SUCCESS} minimal={true}> <Tag intent={Intent.SUCCESS} minimal={true}>
<T id={'activate'} /> <T id={'activate'} />
</Tag> </Tag>
@@ -193,7 +212,7 @@ function UsersListPreferences({
const handelDataTableFetchData = useCallback(() => { const handelDataTableFetchData = useCallback(() => {
onFetchData && onFetchData(); onFetchData && onFetchData();
}, []); }, [onFetchData]);
return ( return (
<LoadingIndicator> <LoadingIndicator>

View File

@@ -283,8 +283,7 @@ export default {
'The exchange rate has been successfully edited', 'The exchange rate has been successfully edited',
the_exchange_rate_has_been_successfully_created: the_exchange_rate_has_been_successfully_created:
'The exchange rate has been successfully created', 'The exchange rate has been successfully created',
the_exchange_rate_has_been_successfully_deleted:
'The exchange rate has been successfully deleted.',
the_user_details_has_been_updated: 'The user details has been updated', the_user_details_has_been_updated: 'The user details has been updated',
the_category_has_been_successfully_created: the_category_has_been_successfully_created:
'The category has been successfully created.', 'The category has been successfully created.',
@@ -329,6 +328,7 @@ export default {
journal_sheet: 'Journal Sheet', journal_sheet: 'Journal Sheet',
run_report: 'Run Report', run_report: 'Run Report',
num: 'Num.', num: 'Num.',
inviting: 'INVITING',
acc_code: 'Acc. Code', acc_code: 'Acc. Code',
display_report_columns: 'Display report columns', display_report_columns: 'Display report columns',
select_display_columns_by: 'Select display columns by...', select_display_columns_by: 'Select display columns by...',
@@ -337,7 +337,25 @@ export default {
'The currency has been successfully edited', 'The currency has been successfully edited',
the_currency_has_been_successfully_created: the_currency_has_been_successfully_created:
'The currency has been successfully created', 'The currency has been successfully created',
the_currency_has_been_successfully_deleted:
'The currency has been successfully deleted',
once_delete_this_currency_you_will_able_to_restore_it: `Once you delete this currency, you won\'t be able to restore it later. Are you sure you want to delete this currency?`,
once_delete_this_exchange_rate_you_will_able_to_restore_it: `Once you delete this exchange rate, you won\'t be able to restore it later. Are you sure you want to delete this exchange rate?`,
once_delete_these_exchange_rates_you_will_not_able_restore_them: `Once you delete these exchange rates, you won't be able to retrieve them later. Are you sure you want to delete them?`,
once_delete_this_item_category_you_will_able_to_restore_it: `Once you delete this item, you won\'t be able to restore it later. Are you sure you want to delete this item?`,
select_business_location: 'Select Business Location',
select_base_currency: 'Select Base Currency',
select_fiscal_year: 'Select Fiscal Year',
select_language: 'Select Language',
select_date_format: 'Select Date Format',
select_time_zone: 'Select Time Zone',
select_currency_code: 'Select Currency Code',
select_parent_category: 'Select Parent Category',
the_options_has_been_successfully_created:
'The options has been successfully created',
there_is_exchange_rate_in_this_date_with_the_same_currency:
'There is exchange rate in this date with the same currency.',
the_exchange_rates_has_been_successfully_deleted:'The exchange rates has been successfully deleted',
// Name Labels // Name Labels
expense_account_id: 'Expense account', expense_account_id: 'Expense account',
payment_account_id: 'Payment account', payment_account_id: 'Payment account',
@@ -363,14 +381,20 @@ export default {
date_format_: 'Date format', date_format_: 'Date format',
category_name_: 'Category name', category_name_: 'Category name',
view_name_: 'View name', view_name_: 'View name',
time_zone: 'Time zone',
location: 'Location',
the_items_has_been_successfully_deleted: the_items_has_been_successfully_deleted:
'The items have been successfully deleted.', 'The items have been successfully deleted.',
once_delete_these_items_you_will_not_able_restore_them: once_delete_these_items_you_will_not_able_restore_them:
"Once you delete these items, you won't be able to retrieve them later. Are you sure you want to delete them?", "Once you delete these items, you won't be able to retrieve them later. Are you sure you want to delete them?",
ops_something_went_wrong: 'Something went wrong! Please try again.', ops_something_went_wrong: 'Something went wrong! Please try again.',
session_expired: 'Session Expired!', session_expired: 'Session Expired!',
this_report_does_not_contain_any_data_between_date_period: 'This report does not contain any data between date period.', this_report_does_not_contain_any_data_between_date_period:
welcome_organization_account_has_been_created: '👋 Welcome, You organization account has been created, Sign in now!', 'This report does not contain any data between date period.',
the_phone_number_already_used_in_another_account: 'he phone number is already used in another account', welcome_organization_account_has_been_created:
the_email_already_used_in_another_account: 'The email is already used in another account', '👋 Welcome, You organization account has been created, Sign in now!',
the_phone_number_already_used_in_another_account:
'he phone number is already used in another account',
the_email_already_used_in_another_account:
'The email is already used in another account',
}; };

View File

@@ -33,13 +33,34 @@ export const fetchExchangeRates = () => {
}; };
export const submitExchangeRate = ({ form }) => { export const submitExchangeRate = ({ form }) => {
return (dispatch) => { return (dispatch) =>
return ApiService.post('exchange_rates', form); new Promise((resolve, reject) => {
}; ApiService.post('exchange_rates', form).then((response) => {
resolve(response);
}).catch((error)=>{
const {response} = error
const {data} = response;
reject(data?.errors)
})
});
}; };
// export const deleteExchangeRate = (id) => {
// return (dispatch) => ApiService.delete(`exchange_rates/${id}`);
// }
export const deleteExchangeRate = (id) => { export const deleteExchangeRate = (id) => {
return (dispatch) => ApiService.delete(`exchange_rates/${id}`); return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.delete(`exchange_rates/${id}`)
.then((response) => {
dispatch({ type: t.EXCHANGE_RATE_DELETE, id });
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);
});
});
}; };
export const editExchangeRate = (id, form) => { export const editExchangeRate = (id, form) => {
@@ -59,23 +80,24 @@ export const editExchangeRate = (id, form) => {
if (errors) { if (errors) {
dispatch({ type: t.CLEAR_EXCHANGE_RATE_FORM_ERRORS, errors }); dispatch({ type: t.CLEAR_EXCHANGE_RATE_FORM_ERRORS, errors });
} }
reject(error); reject(data?.errors);
}); });
}); });
}; };
export const deleteBulkExchangeRates = ({ ids }) => { export const deleteBulkExchangeRates = ({ ids }) => {
return dispatch => new Promise((resolve, reject) => { return (dispatch) =>
ApiService.delete(`exchange_rates/bulk`, { params: { ids }}).then((response) => { new Promise((resolve, reject) => {
dispatch({ ApiService.delete(`exchange_rates/bulk`, { params: { ids } })
type: t.EXCHANGE_RATES_BULK_DELETE, .then((response) => {
payload: { ids } dispatch({
}); type: t.EXCHANGE_RATES_BULK_DELETE,
resolve(response); payload: { ids },
}).catch((error) => { });
reject(error); resolve(response);
})
.catch((error) => {
reject(error);
});
}); });
}); };
};

View File

@@ -21,17 +21,17 @@ export default createReducer(initialState, {
state.loading = action.loading; state.loading = action.loading;
}, },
[t.EXCHANGE_RATES_BULK_DELETE]:(state,action)=>{ [t.EXCHANGE_RATES_BULK_DELETE]: (state, action) => {
const { ids } = action.payload;
const {ids} =action.payload; ids.forEach((id) => {
const {exchange_rate} = {...state.exchangeRates}; if (typeof state.exchangeRates[id] !== 'undefined') {
ids.forEach((id)=>{ delete state.exchangeRates[id];
if(typeof exchange_rate[id] !=='undefined'){
delete exchange_rate[id]
} }
}); });
state.exchangeRates =exchange_rate },
} [t.EXCHANGE_RATE_DELETE]: (state, action) => {
if (typeof state.exchangeRates[action.id] !== 'undefined') {
delete state.exchangeRates[action.id];
}
},
}); });

View File

@@ -8,7 +8,17 @@ export const submitCurrencies = ({ form }) => {
}; };
export const deleteCurrency = ({ currency_code }) => { export const deleteCurrency = ({ currency_code }) => {
return (dispatch) => ApiService.delete(`currencies/${currency_code}`); return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.delete(`currencies/${currency_code}`)
.then((response) => {
dispatch({ type: t.CURRENCY_CODE_DELETE, currency_code });
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);
});
});
}; };
export const editCurrency = ({ id, form }) => { export const editCurrency = ({ id, form }) => {
@@ -36,12 +46,20 @@ export const editCurrency = ({ id, form }) => {
export const fetchCurrencies = () => { export const fetchCurrencies = () => {
return (dispatch) => return (dispatch) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
dispatch({
type: t.CURRENCIES_TABLE_LOADING,
loading: true,
});
ApiService.get('currencies') ApiService.get('currencies')
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: t.CURRENCIES_REGISTERED_SET, type: t.CURRENCIES_REGISTERED_SET,
currencies: response.data.currencies, currencies: response.data.currencies,
}); });
dispatch({
type: t.CURRENCIES_TABLE_LOADING,
loading: false,
});
resolve(response); resolve(response);
}) })
.catch((error) => { .catch((error) => {

View File

@@ -17,4 +17,12 @@ export default createReducer(initialState, {
..._currencies, ..._currencies,
}; };
}, },
[t.CURRENCIES_TABLE_LOADING]: (state, action) => {
state.loading = action.loading;
},
[t.CURRENCY_CODE_DELETE]: (state, action) => {
if (typeof state.data[action.currency_code] !== 'undefined') {
delete state.data[action.currency_code];
}
},
}); });

View File

@@ -1,4 +1,6 @@
export default { export default {
CURRENCIES_REGISTERED_SET: 'CURRENCIES_REGISTERED_SET', CURRENCIES_REGISTERED_SET: 'CURRENCIES_REGISTERED_SET',
CLEAR_CURRENCY_FORM_ERRORS: 'CLEAR_CURRENCY_FORM_ERRORS', CLEAR_CURRENCY_FORM_ERRORS: 'CLEAR_CURRENCY_FORM_ERRORS',
CURRENCIES_TABLE_LOADING: 'CURRENCIES_TABLE_LOADING',
CURRENCY_CODE_DELETE: 'CURRENCY_CODE_DELETE',
}; };

View File

@@ -10,18 +10,16 @@ export default createReducer(initialState, {
[t.ITEMS_CATEGORY_LIST_SET]: (state, action) => { [t.ITEMS_CATEGORY_LIST_SET]: (state, action) => {
const _categories = {}; const _categories = {};
action.categories.forEach(category => { action.categories.forEach((category) => {
_categories[category.id] = category; _categories[category.id] = category;
}); });
state.categories = { state.categories = {
...state.categories, ...state.categories,
..._categories ..._categories,
}; };
}, },
[t.ITEM_CATEGORIES_TABLE_SET]: (state, action) => { [t.ITEM_CATEGORIES_TABLE_SET]: (state, action) => {},
},
[t.CATEGORY_DELETE]: (state, action) => { [t.CATEGORY_DELETE]: (state, action) => {
const { id } = action.payload; const { id } = action.payload;
@@ -31,21 +29,19 @@ export default createReducer(initialState, {
} }
}, },
[t.ITEM_CATEGORIES_TABLE_LOADING]: (state, action ) => { [t.ITEM_CATEGORIES_TABLE_LOADING]: (state, action) => {
const { loading } = action.payload; const { loading } = action.payload;
state.loading = !!loading; state.loading = !!loading;
}, },
[t.ITEM_CATEGORIES_BULK_DELETE]:(state,action)=>{ [t.ITEM_CATEGORIES_BULK_DELETE]: (state, action) => {
const {ids} =action.payload; const { ids } = action.payload;
const {categories} = {...state.categories};
ids.forEach((id)=>{
if(typeof categories[id] !=='undefined'){ ids.forEach((id) => {
delete categories[id] if (typeof state.categories[id] !== 'undefined') {
delete state.categories[id];
} }
}); });
state.categories =categories },
}
}); });
export const getCategoryId = (state, id) => { export const getCategoryId = (state, id) => {

View File

@@ -12,7 +12,7 @@
&__inside-content { &__inside-content {
.#{$ns}-tab-list { .#{$ns}-tab-list {
border-bottom: 1px solid #E5E5E5; border-bottom: 1px solid #e5e5e5;
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
align-items: baseline; align-items: baseline;
@@ -25,27 +25,40 @@
} }
} }
&__tabs-extra-actions{ &__tabs-extra-actions {
margin-left: auto; margin-left: auto;
} }
&__topbar-actions{ &__topbar-actions {
margin-left: auto; margin-left: auto;
padding-right: 15px; padding-right: 15px;
margin-right: 15px; margin-right: 15px;
border-right: 1px solid #e5e5e5; border-right: 1px solid #e5e5e5;
.bp3-button + .bp3-button{ .bp3-button + .bp3-button {
margin-left: 10px; margin-left: 10px;
} }
} }
&-menu { &-menu {
width: 374px; width: 448px;
} }
&-button { &-button {
margin-right: 10px; margin-right: 10px;
} }
&__loading-overlay{
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(252, 253, 255, 0.5);
display: flex;
justify-content: center;
}
&__floating-footer { &__floating-footer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@@ -95,7 +108,7 @@
color: #333; color: #333;
&:hover, &:hover,
&.#{$ns}-active{ &.#{$ns}-active {
background-color: #ebf1f5; background-color: #ebf1f5;
color: #333; color: #333;
} }
@@ -106,30 +119,57 @@
// Preference // Preference
//--------------------------------- //---------------------------------
.preferences__inside-content--general { .preferences__inside-content--general {
&:not(.preferences--loading){
margin-bottom: 25px;
}
.bp3-form-group { .bp3-form-group {
margin: 25px 20px 20px; margin: 25px 20px 20px;
.bp3-label { .bp3-label {
min-width: 140px; min-width: 140px;
} }
.bp3-form-content { .bp3-form-content {
width: 384px; width: 55%;
.bp3-input-group .bp3-input {
position: relative;
// width:580px;
width: 120%;
}
} }
} }
.form-group--time-zone {
.bp3-menu {
max-height: 300px;
max-width: 400px;
overflow: auto;
padding: 0;
}
.bp3-text-muted {
color: #000000;
}
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
background: #fff;
box-shadow: 0 0 0 transparent;
border: 1px solid #ced4da;
padding: 8px;
}
}
.preferences-page__loading-overlay{
background: rgba(252, 253, 255, 0.9);
}
} }
// Users/Roles List. // Users/Roles List.
// --------------------------------- // ---------------------------------
.preferences__inside-content--users-roles{ .preferences__inside-content--users-roles {
.bigcapital-datatable {
.bigcapital-datatable{
.table .th, .table .th,
.table .td{ .table .td {
padding: 0.8rem 0.5rem; padding: 0.8rem 0.5rem;
} }
.td.status{ .td.status {
text-transform: uppercase; text-transform: uppercase;
} }
} }
} }

View File

@@ -53,7 +53,8 @@ export const momentFormatter = (format) => {
return { return {
formatDate: date => moment(date).format(format), formatDate: date => moment(date).format(format),
parseDate: str => moment(str, format).toDate(), parseDate: str => moment(str, format).toDate(),
placeholder: `${format} (moment)`, // placeholder: `${format} (moment)`,
placeholder: `${format}`,
}; };
} }