This commit is contained in:
Ahmed Bouhuolia
2020-03-16 00:06:15 +02:00
parent 56701951b7
commit 73711384f6
7925 changed files with 18478 additions and 959 deletions

View File

@@ -1,64 +1,112 @@
import * as React from "react";
import React, { useEffect } from "react";
import {Link} from 'react-router-dom';
import * as Yup from 'yup';
import {useFormik} from 'formik';
import {connect} from 'react-redux';
import {useIntl} from 'react-intl';
import login from 'store/actions/auth';
import {Button, InputGroup, Intent, FormGroup} from "@blueprintjs/core";
import {
Button,
InputGroup,
Intent,
FormGroup,
} from "@blueprintjs/core";
import login from 'store/authentication/authentication.actions';
import {hasErrorType} from 'store/authentication/authentication.reducer';
import AuthenticationToaster from 'components/AppToaster';
import t from 'store/types';
const loginValidationSchema = Yup.object().shape({
credential: Yup.string().required('Required'),
password: Yup.string().required('Required'),
});
function Login({ login }) {
const ERRORS_TYPES = {
INVALID_DETAILS: 'INVALID_DETAILS',
USER_INACTIVE: 'USER_INACTIVE',
};
function Login({
login,
errors,
clearErrors,
hasError,
}) {
const intl = useIntl();
// Validation schema.
const loginValidationSchema = Yup.object().shape({
crediential: Yup
.string()
.required(intl.formatMessage({'id': 'required'}))
.email(intl.formatMessage({id: 'invalid_email_or_phone_numner'})),
password: Yup
.string()
.required(intl.formatMessage({id: 'required'}))
.min(4),
});
// Formik validation schema and submit handler.
const formik = useFormik({
initialValues: {
credential: '',
crediential: '',
password: '',
},
validationSchema: loginValidationSchema,
onSubmit: (values) => {
onSubmit: async (values) => {
login({
credential: values.credential,
crediential: values.crediential,
password: values.password,
}).then(() => {
}).catch((errors) => {
});
},
});
useEffect(() => {
const toastBuilders = [];
if (hasError(ERRORS_TYPES.INVALID_DETAILS)) {
toastBuilders.push({
message: intl.formatMessage({ id: 'invalid_email_or_phone_numner' }),
intent: Intent.WARNING,
});
}
if (hasError(ERRORS_TYPES.USER_INACTIVE)) {
toastBuilders.push({
message: intl.formatMessage({ id: 'the_user_has_been_suspended_from_admin' }),
intent: Intent.WARNING,
});
}
toastBuilders.forEach(builder => {
AuthenticationToaster.show(builder);
});
}, [hasError, intl]);
// Handle unmount component
useEffect(() => () => {
if (errors.length > 0) {
clearErrors();
}
});
return (
<div className="login-page">
<form onSubmit={formik.handleSubmit}>
<FormGroup
className={'form-group--email-phone-number'}
intent={formik.errors.credential && Intent.DANGER}
helperText={formik.errors.credential && formik.errors.credential}>
className={'form-group--crediential'}
intent={formik.errors.crediential && Intent.DANGER}
helperText={formik.errors.crediential && formik.errors.crediential}>
<InputGroup
leftIcon="user"
placeholder={intl.formatMessage({'id': 'email_or_phone_number'})}
large={true}
intent={formik.errors.credential && Intent.DANGER}
{...formik.getFieldProps('credential')} />
intent={formik.errors.crediential && Intent.DANGER}
{...formik.getFieldProps('crediential')} />
</FormGroup>
<FormGroup
className={'form-group--password'}
intent={formik.errors.credential && Intent.DANGER}
intent={formik.errors.password && Intent.DANGER}
helperText={formik.errors.password && formik.errors.password}>
<InputGroup
leftIcon="info-sign"
placeholder={intl.formatMessage({'id': 'password'})}
large={true}
intent={formik.errors.credential && Intent.DANGER}
intent={formik.errors.password && Intent.DANGER}
type={"password"}
{...formik.getFieldProps('password')} />
</FormGroup>
@@ -76,11 +124,17 @@ function Login({ login }) {
</Link>
</div>
</div>
)
);
}
const mapStateToProps = (state) => ({
hasError: (errorType) => hasErrorType(state, errorType),
errors: state.authentication.errors,
});
const mapDispatchToProps = (dispatch) => ({
login: form => dispatch(login({ form })),
clearErrors: () => dispatch({ type: t.LOGIN_CLEAR_ERRORS }),
});
export default connect(null, mapDispatchToProps)(Login);
export default connect(mapStateToProps, mapDispatchToProps)(Login);

View File

@@ -0,0 +1,51 @@
import React, {useMemo} from 'react';
import {
Intent,
Button,
} from '@blueprintjs/core';
import { FormattedList } from 'react-intl';
export default function MakeJournalEntriesFooter({
formik,
}) {
const creditSum = useMemo(() => {
return formik.values.entries.reduce((sum, entry) => {
return entry.credit + sum;
}, 0);
}, [formik.values.entries]);
const debitSum = useMemo(() => {
return formik.values.entries.reduce((sum, entry) => {
return entry.debit + sum;
}, 0);
}, [formik.values.entries]);
return (
<div>
<table>
<tbody>
<tr>
<td><strong>Total</strong></td>
<td>{ creditSum }</td>
<td>{ debitSum }</td>
</tr>
</tbody>
</table>
<div class="form__floating-footer">
<Button
intent={Intent.PRIMARY}
type="submit">
Save
</Button>
<Button
intent={Intent.PRIMARY}
type="submit"
className={'ml2'}>
Save & New
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import React, {useState, useEffect} from 'react';
import * as Yup from 'yup';
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
import {Formik, useFormik} from "formik";
import MakeJournalEntriesConnect from 'connectors/MakeJournalEntries.connect';
import AccountsConnect from 'connectors/Accounts.connector';
import DashboardConnect from 'connectors/Dashboard.connector';
import {compose} from 'utils';
import useAsync from 'hooks/async';
import moment from 'moment';
import AppToaster from 'components/AppToaster';
function MakeJournalEntriesForm({
makeJournalEntries,
fetchAccounts,
changePageTitle,
}) {
useEffect(() => {
changePageTitle('New Journal');
}, []);
const fetchHook = useAsync(async () => {
await Promise.all([
fetchAccounts(),
]);
});
const validationSchema = Yup.object().shape({
date: Yup.date().required(),
reference: Yup.string(),
description: Yup.string(),
entries: Yup.array().of(
Yup.object().shape({
credit: Yup.number().nullable(),
debit: Yup.number().nullable(),
}),
)
});
const defaultEntry = {
account_id: null,
credit: null,
debit: null,
note: '',
};
const formik = useFormik({
enableReinitialize: true,
validationSchema,
initialValues: {
reference: '',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
entries: [
defaultEntry,
defaultEntry,
defaultEntry,
defaultEntry,
],
},
onSubmit: (values) => {
const form = values.entries.filter((entry) => (
(entry.credit || entry.debit)
));
makeJournalEntries(values).then((response) => {
AppToaster.show({
message: 'the_account_has_been_submit',
});
}).catch((error) => {
});
},
});
console.log(formik.errors);
return (
<form onSubmit={formik.handleSubmit}>
<MakeJournalEntriesHeader formik={formik} />
<MakeJournalEntriesTable formik={formik} />
<MakeJournalEntriesFooter formik={formik} />
</form>
);
}
export default compose(
MakeJournalEntriesConnect,
AccountsConnect,
DashboardConnect,
)(MakeJournalEntriesForm);

View File

@@ -0,0 +1,75 @@
import React from 'react';
import * as Yup from 'yup';
import {
InputGroup,
FormGroup,
Intent,
Position,
} from '@blueprintjs/core';
import {DatePicker, DateInput} from '@blueprintjs/datetime';
import {Formik, useFormik} from "formik";
import {useIntl} from 'react-intl';
import {Row, Col} from 'react-grid-system';
import moment from 'moment';
import {momentFormatter} from 'utils';
export default function MakeJournalEntriesHeader({
formik
}) {
const intl = useIntl();
const handleDateChange = (date) => {
const formatted = moment(date).format('YYYY-MM-DD');
formik.setFieldValue('date', formatted);
};
return (
<div class="make-journal-entries__header">
<Row>
<Col sm={4}>
<FormGroup
label={intl.formatMessage({'id': 'reference'})}
className={'form-group--reference'}
intent={formik.errors.name && Intent.DANGER}
helperText={formik.errors.name && formik.errors.label}
fill={true}>
<InputGroup
intent={formik.errors.name && Intent.DANGER}
fill={true}
{...formik.getFieldProps('reference')} />
</FormGroup>
</Col>
<Col sm={3}>
<FormGroup
label={intl.formatMessage({'id': 'date'})}
intent={formik.errors.date && Intent.DANGER}
helperText={formik.errors.date && formik.errors.date}
minimal={true}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
defaultValue={new Date()}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM }} />
</FormGroup>
</Col>
<Col sm={4}>
<FormGroup
label={intl.formatMessage({'id': 'description'})}
className={'form-group--description'}
intent={formik.errors.name && Intent.DANGER}
helperText={formik.errors.name && formik.errors.label}
fill={true}>
<InputGroup
intent={formik.errors.name && Intent.DANGER}
fill={true}
{...formik.getFieldProps('description')} />
</FormGroup>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import MakeJournalEntriesForm from './MakeJournalEntriesForm';
import DashboardConnect from 'connectors/Dashboard.connector';
import {compose} from 'utils';
function MakeJournalEntriesPage() {
return (
<MakeJournalEntriesForm />
);
}
export default compose(
DashboardConnect,
)(MakeJournalEntriesPage);

View File

@@ -0,0 +1,206 @@
import React, {useState, useMemo} from 'react';
import DataTable from 'components/DataTable';
import {
FormGroup,
InputGroup,
MenuItem,
Button,
Intent,
} from '@blueprintjs/core';
import {Select} from '@blueprintjs/select';
import Icon from 'components/Icon';
import AccountsConnect from 'connectors/Accounts.connector.js';
import {compose} from 'utils';
function MakeJournalEntriesTable({
formik,
accounts,
}) {
const [selectAccountsState, setSelectedAccount] = useState(false);
// Account item of select accounts field.
const accountItem = (item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} label={item.code} key={item.id} onClick={handleClick} />)
};
// Filters accounts options by account name or code.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
// Handle account field change.
const onChangeAccount = (index) => (account) => {
setSelectedAccount({
...selectAccountsState,
[index + 1]: account,
});
formik.setFieldValue(`entries[${index}].account_id`, account.id);
};
const [data, setData] = useState([
...formik.values.entries,
]);
const updateData = (rowIndex, columnId, value) => {
setData((old) =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
}
}
return row
})
)
}
const CellInputGroupRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
}) => {
const [state, setState] = useState(initialValue);
return (
<InputGroup
fill={true}
value={state}
onChange={(event) => {
setState(event.target.value);
}}
onBlur={(event) => {
formik.setFieldValue(`entries[${index}].[${id}]`, event.target.value);
updateData(index, id, state);
}}
/>);
}
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
}) => {
const onClickRemoveRole = () => {
};
return (
<Button
icon={<Icon icon="mines" />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole()} />
);
}
const columns = useMemo(() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: {index} }) => (
<span>{ index + 1 }</span>
),
className: "actions",
},
{
Header: 'Account',
accessor: 'account',
Cell: ({ row: { index } }) => (
<FormGroup
className="{'form-group--account'}"
inline={true}>
<Select
items={accounts}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
onItemSelect={onChangeAccount(index)}>
<Button
rightIcon="caret-down"
text={(selectAccountsState[(index + 1)])
? selectAccountsState[(index + 1)].name : "Select Account"}
/>
</Select>
</FormGroup>
),
className: "account",
},
{
Header: 'Note',
accessor: 'note',
Cell: CellInputGroupRenderer,
className: "note",
},
{
Header: 'Credit',
accessor: 'credit',
Cell: CellInputGroupRenderer,
className: "credit",
},
{
Header: 'Debit',
accessor: 'debit',
Cell: CellInputGroupRenderer,
className: "debit",
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: "actions",
}
]);
const onClickNewRow = () => {
setData([
...data,
{
credit: 0,
debit: 0,
account_id: null,
note: '',
}
]);
}
return (
<div class="make-journal-entries__table">
<DataTable
columns={columns}
data={data} />
<div class="mt1">
<Button
minimal={true}
intent={Intent.PRIMARY}
onClick={onClickNewRow}>
+ New Entry
</Button>
<Button
minimal={true}
intent={Intent.PRIMARY}
onClick={onClickNewRow}>
- Clear all entries
</Button>
</div>
</div>
);
}
export default compose(
AccountsConnect,
)(MakeJournalEntriesTable);

View File

@@ -1,26 +1,180 @@
import React, { useEffect } from 'react';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import React, { useEffect, useState } from 'react';
import {
Route,
Switch,
useParams,
useRouteMatch
} from 'react-router-dom';
import useAsync from 'hooks/async';
import { Alert, Intent } from '@blueprintjs/core';
import AppToaster from 'components/AppToaster';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { connect } from 'react-redux';
import t from 'store/types';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import AccountsViewsTabs from 'components/Accounts/AccountsViewsTabs';
import AccountsDataTable from 'components/Accounts/AccountsDataTable';
import DashboardActionsBar from 'components/Accounts/AccountsActionsBar';
import AccountsConnect from 'connectors/Accounts.connector';
import DashboardConnect from 'connectors/Dashboard.connector';
import CustomViewConnect from 'connectors/CustomView.connector';
import { compose } from 'utils';
function AccountsChart({
changePageTitle,
fetchAccounts,
deleteAccount,
inactiveAccount,
fetchResourceViews
}) {
const [state, setState] = useState({
deleteAlertActive: false,
restoreAlertActive: false,
inactiveAlertActive: false,
targetAccount: {},
});
const fetchHook = useAsync(async () => {
await Promise.all([
fetchResourceViews('accounts'),
]);
});
function AccountsChart({ changePageTitle }) {
useEffect(() => {
changePageTitle('Chart of Accounts');
});
}, []);
/**
* Handle click and cancel/confirm account delete
*/
const handleDeleteAccount = (account) => {
setState({
deleteAlertActive: true,
deleteAccount: account,
});
};
const handleCancelAccountDelete = () => {
setState({ deleteAlertActive: false });
};
const handleConfirmAccountDelete = () => {
const { targetAccount: account } = state;
deleteAccount(account.id).then(() => {
setState({ deleteAlertActive: false });
fetchAccounts();
AppToaster.show({ message: 'the_account_has_been_deleted' });
});
};
/**
* Handle cancel/confirm account inactive.
*/
const handleInactiveAccount = (account) => {
setState({ inactiveAlertActive: true, targetAccount: account });
};
const handleCancelInactiveAccount = () => {
setState({ inactiveAlertActive: false });
};
const handleConfirmAccountActive = () => {
const { targetAccount: account } = state;
inactiveAccount(account.id).then(() => {
setState({ inactiveAlertActive: true });
fetchAccounts();
AppToaster.show({ message: 'the_account_has_been_inactivated' });
});
};
/**
* Handle cancel/confirm account restore.
*/
const handleCancelAccountRestore = () => {
setState({ restoreAlertActive: false });
};
const handleEditAccount = (account) => {
};
const handleRestoreAccount = (account) => {
};
const handleConfirmAccountRestore = (account) => {
};
const handleDeleteBulkAccounts = (accounts) => {
};
return (
<React.Fragment>
<DashboardInsider loading={fetchHook.pending} name={'accounts-chart'}>
<DashboardActionsBar />
<DashboardPageContent>
<Switch>
<Route
exact={true}
path={[
'/dashboard/accounts/:custom_view_id/custom_view',
'/dashboard/accounts'
]}>
<AccountsViewsTabs onDeleteBulkAccounts={handleDeleteBulkAccounts} />
<AccountsDataTable
onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount}
onRestoreAccount={handleRestoreAccount}
onEditAccount={handleEditAccount} />
</Route>
</Switch>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={state.deleteAlertActive}
onCancel={handleCancelAccountDelete}
onConfirm={handleConfirmAccountDelete}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Inactivate"
icon="trash"
intent={Intent.WARNING}
isOpen={state.inactiveAlertActive}
onCancel={handleCancelInactiveAccount}
onConfirm={handleConfirmAccountActive}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={state.restoreAlertActive}
onCancel={handleCancelAccountRestore}
onConfirm={handleConfirmAccountRestore}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</DashboardPageContent>
</React.Fragment>
</DashboardInsider>
);
}
const mapActionsToProps = (dispatch) => ({
changePageTitle: pageTitle => dispatch({
type: t.CHANGE_DASHBOARD_PAGE_TITLE, pageTitle
}),
});
export default connect(null, mapActionsToProps)(AccountsChart);
export default compose(
AccountsConnect,
CustomViewConnect,
DashboardConnect,
)(AccountsChart);

View File

@@ -0,0 +1,273 @@
import React, { useState} from 'react';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
MenuItem,
Checkbox,
} from "@blueprintjs/core";
import {Select} from '@blueprintjs/select';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { useIntl } from 'react-intl';
import { omit } from 'lodash';
import { compose } from 'utils';
import useAsync from 'hooks/async';
import Dialog from 'components/Dialog';
import AppToaster from 'components/AppToaster';
import DialogConnect from 'connectors/Dialog.connector';
import DialogReduxConnect from 'components/DialogReduxConnect';
import AccountFormDialogConnect from 'connectors/AccountFormDialog.connector';
function AccountFormDialog ({
name,
payload,
isOpen,
accountsTypes,
accounts,
fetchAccounts,
fetchAccountTypes,
closeDialog,
submitAccount,
fetchAccount,
editAccount
}) {
const intl = useIntl();
const accountFormValidationSchema = Yup.object().shape({
name: Yup
.string()
.required(intl.formatMessage({ 'id': 'required' })),
code: Yup
.number(intl.formatMessage({ id: 'field_name_must_be_number' })),
account_type_id: Yup
.string()
.nullable()
.required(intl.formatMessage({ 'id': 'required' })),
description: Yup.string().trim(),
});
// Formik
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...payload.action === 'edit' && editAccount,
},
validationSchema: accountFormValidationSchema,
onSubmit: (values) => {
const exclude = ['subaccount'];
if (payload.action === 'edit') {
editAccount({
payload: payload.id,
form: { ...omit(values, exclude) }
}).then((response) => {
closeDialog(name);
AppToaster.show({
message: 'the_account_has_been_edited',
});
});
} else {
submitAccount({ form: { ...omit(values, exclude) } }).then(response => {
closeDialog(name);
AppToaster.show({
message: 'the_account_has_been_submit',
});
});
}
},
});
const [state, setState] = useState({
loading: true,
dialogActive: true,
selectedAccountType: null,
selectedSubaccount: null,
});
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
const normalizedTitle = accountType.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Account type item of select filed.
const accountTypeItem = (item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />);
};
// Account item of select accounts field.
const accountItem = (item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} label={item.code} key={item.id} onClick={handleClick} />)
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
const handleClose = () => { closeDialog(name); };
const fetchHook = useAsync(async () => {
await Promise.all([
fetchAccounts(),
fetchAccountTypes(),
// Fetch the target in case edit mode.
...(payload.action === 'edit') ? [
fetchAccount(payload.id),
] : [],
]);
}, false);
const onDialogOpening = async () => { fetchHook.execute(); }
const onChangeAccountType = (accountType) => {
setState({ ...state, selectedAccountType: accountType.name });
formik.setFieldValue('account_type_id', accountType.id);
};
const onChangeSubaccount = (account) => {
setState({ ...state, selectedSubaccount: account });
formik.setFieldValue('parent_account_id', account.id);
};
const onDialogClosed = () => {
formik.resetForm();
setState({
...state,
selectedSubaccount: null,
selectedAccountType: null,
});
};
return (
<Dialog
name={name}
title={payload.action === 'edit' ? 'Edit Account' : 'New Account'}
className={{'dialog--loading': state.isLoading, 'dialog--account-form': true }}
onClosed={onDialogClosed}
onOpening={onDialogOpening}
isOpen={isOpen}
isLoading={fetchHook.pending}
>
<form onSubmit={formik.handleSubmit}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={'Account Type'}
className="{'form-group--account-type'}"
inline={true}
helperText={formik.errors.account_type_id && formik.errors.account_type_id}
intent={formik.errors.account_type_id && Intent.DANGER}>
<Select
items={accountsTypes}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={accountTypeItem}
itemPredicate={filterAccountTypeItems}
popoverProps={{ minimal: true }}
onItemSelect={onChangeAccountType}>
<Button
rightIcon="caret-down"
text={state.selectedAccountType || 'Select account type'} />
</Select>
</FormGroup>
<FormGroup
label={'Account Name'}
className={'form-group--account-name'}
intent={formik.errors.name && Intent.DANGER}
helperText={formik.errors.name && formik.errors.name}
inline={true}>
<InputGroup
medium={true}
intent={formik.errors.name && Intent.DANGER}
{...formik.getFieldProps('name')} />
</FormGroup>
<FormGroup
label={'Account Code'}
className={'form-group--account-code'}
intent={formik.errors.code && Intent.DANGER}
helperText={formik.errors.code && formik.errors.code}
inline={true}>
<InputGroup
medium={true}
intent={formik.errors.code && Intent.DANGER}
{...formik.getFieldProps('code')} />
</FormGroup>
<FormGroup
label={' '}
className={'form-group--subaccount'}
inline={true}>
<Checkbox
inline={true}
label={'Sub account?'}
{...formik.getFieldProps('subaccount')} />
</FormGroup>
{ (formik.values.subaccount) &&
<FormGroup
label={'Sub Account'}
className="{'form-group--sub-account'}"
inline={true}>
<Select
items={accounts}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
onItemSelect={onChangeSubaccount}
{...formik.getFieldProps('parent_account_id')}>
<Button
rightIcon="caret-down"
text={state.selectedSubaccount ? state.selectedSubaccount.name : "Select Parent Account"}
/>
</Select>
</FormGroup> }
<FormGroup
label={'Description'}
className={'form-group--description'}
intent={formik.errors.description && Intent.DANGER}
helperText={formik.errors.description && formik.errors.credential}
inline={true}>
<TextArea growVertically={true} large={true} {...formik.getFieldProps('description')} />
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}>Close</Button>
<Button intent={Intent.PRIMARY} type="submit">
{ payload.action === 'edit' ? 'Edit' : 'Submit' }
</Button>
</div>
</div>
</form>
</Dialog>
);
}
export default compose(
AccountFormDialogConnect,
DialogReduxConnect,
DialogConnect,
)(AccountFormDialog);

View File

@@ -0,0 +1,201 @@
import React from 'react';
import { useIntl } from "react-intl"
import {useFormik} from 'formik';
import * as Yup from 'yup';
import {
Dialog,
Button,
FormGroup,
InputGroup,
Intent,
TextArea,
MenuItem,
Checkbox,
Classes,
HTMLSelect,
} from '@blueprintjs/core';
import UserFormDialogConnect from 'connectors/UserFormDialog.connector';
import DialogReduxConnect from 'components/DialogReduxConnect';
import AppToaster from 'components/AppToaster';
import useAsync from 'hooks/async';
import {objectKeysTransform} from 'utils';
import {pick, snakeCase} from 'lodash';
function UserFormDialog({
fetchUser,
submitUser,
editUser,
name,
payload,
isOpen,
userDetails,
closeDialog,
}) {
const intl = useIntl();
const fetchHook = useAsync(async () => {
await Promise.all([
...(payload.action === 'edit') ? [
fetchUser(payload.user.id),
] : [],
]);
}, false);
const validationSchema = Yup.object().shape({
first_name: Yup.string().required(),
last_name: Yup.string().required(),
email: Yup.string().email().required(),
phone_number: Yup.string().required(),
password: Yup.string().min(5).required(),
});
const initialValues = {
status: 1,
...payload.action === 'edit' &&
pick(
objectKeysTransform(payload.user, snakeCase),
Object.keys(validationSchema.fields)
),
password: '',
};
const formik = useFormik({
enableReinitialize: true,
initialValues,
validationSchema,
onSubmit: (values) => {
const form = {
...values,
confirm_password: values.password,
};
if (payload.action === 'edit') {
editUser(payload.user.id, form).then((response) => {
AppToaster.show({
message: 'the_user_details_has_been_updated',
});
closeDialog(name);
});
} else {
submitUser(form).then((response) => {
AppToaster.show({
message: 'the_user_has_been_invited',
});
closeDialog(name);
});
}
},
});
const statusOptions = [
{value: 1, label: 'Active'},
{value: 2, label: 'Inactive'},
];
const onDialogOpening = () => { fetchHook.execute(); };
const onDialogClosed = () => {
formik.resetForm();
};
const handleClose = () => { closeDialog(name); };
return (
<Dialog
isOpen={isOpen}
name={name}
title={payload.action === 'edit' ? 'Edit User' : 'New User'}
isLoading={fetchHook.pending}
onClosed={onDialogClosed}
onOpening={onDialogOpening}>
<form onSubmit={formik.handleSubmit}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={'First Name'}
className={'form-group--first-name'}
intent={formik.errors.first_name && Intent.DANGER}
helperText={formik.errors.first_name && formik.errors.first_name}
inline={true}>
<InputGroup
intent={formik.errors.first_name && Intent.DANGER}
{...formik.getFieldProps('first_name')} />
</FormGroup>
<FormGroup
label={'Last Name'}
className={'form-group--last-name'}
intent={formik.errors.last_name && Intent.DANGER}
helperText={formik.errors.last_name && formik.errors.last_name}
inline={true}>
<InputGroup
intent={formik.errors.last_name && Intent.DANGER}
{...formik.getFieldProps('last_name')} />
</FormGroup>
<FormGroup
label={'Email'}
className={'form-group--email'}
intent={formik.errors.email && Intent.DANGER}
helperText={formik.errors.email && formik.errors.email}
inline={true}>
<InputGroup
intent={formik.errors.email && Intent.DANGER}
{...formik.getFieldProps('email')} />
</FormGroup>
<FormGroup
label={'Phone Number'}
className={'form-group--phone-number'}
intent={formik.errors.phone_number && Intent.DANGER}
helperText={formik.errors.phone_number && formik.errors.phone_number}
inline={true}>
<InputGroup
intent={formik.errors.phone_number && Intent.DANGER}
{...formik.getFieldProps('phone_number')} />
</FormGroup>
<FormGroup
label={'Password'}
className={'form-group--password'}
intent={formik.errors.password && Intent.DANGER}
helperText={formik.errors.password && formik.errors.password}
inline={true}>
<InputGroup
intent={formik.errors.password && Intent.DANGER}
className={Classes.FILL}
{...formik.getFieldProps('password')} />
</FormGroup>
<FormGroup
label={'Status'}
className={'form-group--status'}
intent={formik.errors.status && Intent.DANGER}
helperText={formik.errors.status && formik.errors.status}
inline={true}>
<HTMLSelect
options={statusOptions}
className={Classes.FILL}
{...formik.getFieldProps(`status`)} />
</FormGroup>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}>Close</Button>
<Button intent={Intent.PRIMARY} type="submit">
{ payload.action === 'edit' ? 'Edit' : 'Submit' }
</Button>
</div>
</div>
</div>
</form>
</Dialog>
);
}
export default UserFormDialogConnect(DialogReduxConnect(UserFormDialog));

View File

@@ -0,0 +1,40 @@
import React, {useEffect} from 'react';
import { useAsync } from 'react-use';
import {useParams} from 'react-router-dom';
import Connector from 'connectors/ExpenseForm.connector';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ExpenseForm from 'components/Expenses/ExpenseForm';
function ExpenseFormContainer({
fetchAccounts,
fetchCurrencies,
accounts,
changePageTitle,
submitExpense,
editExpense,
currencies,
}) {
const { id } = useParams();
useEffect(() => {
if (id) {
changePageTitle('Edit Expense Details');
} else {
changePageTitle('New Expense');
}
}, []);
const fetchHook = useAsync(async () => {
await Promise.all([
fetchAccounts(),
fetchCurrencies(),
]);
});
return (
<DashboardInsider isLoading={fetchHook.loading} name={'expense-form'}>
<ExpenseForm {...{submitExpense, editExpense, accounts, currencies} } />
</DashboardInsider>
);
}
export default Connector(ExpenseFormContainer);

View File

@@ -0,0 +1,71 @@
import React, {useEffect, useState} from 'react';
import { useAsync } from 'react-use';
import {Alert, Intent} from '@blueprintjs/core';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ExpensesActionsBar from 'components/Expenses/ExpensesActionsBar';
import ExpensesViewsTabs from 'components/Expenses/ExpensesViewsTabs';
import ExpensesTable from 'components/Expenses/ExpensesTable';
import connector from 'connectors/ExpensesList.connector';
import AppToaster from 'components/AppToaster';
function ExpensesList({
fetchExpenses,
deleteExpense,
// fetchViews,
expenses,
getResourceViews,
changePageTitle,
}) {
useEffect(() => {
changePageTitle('Expenses List');
}, []);
const [deleteExpenseState, setDeleteExpense] = useState();
const handleDeleteExpense = (expense) => { setDeleteExpense(expense); };
const handleCancelAccountDelete = () => { setDeleteExpense(false); }
const handleConfirmAccountDelete = () => {
deleteExpense(deleteExpenseState.id).then(() => {
setDeleteExpense(false);
AppToaster.show({
message: 'the_expense_has_been_deleted',
});
});
};
const fetchHook = useAsync(async () => {
await Promise.all([
fetchExpenses(),
// getResourceViews('expenses'),
]);
});
return (
<DashboardInsider loading={false}>
<ExpensesActionsBar />
<ExpensesViewsTabs />
<DashboardPageContent>
<ExpensesTable expenses={expenses} onDeleteExpense={handleDeleteExpense} />
</DashboardPageContent>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={deleteExpenseState}
onCancel={handleCancelAccountDelete}
onConfirm={handleConfirmAccountDelete}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</DashboardInsider>
);
};
export default connector(ExpensesList);

View File

@@ -0,0 +1,48 @@
import React, {useEffect} from 'react';
import DashboardConnect from 'connectors/Dashboard.connector';
import {compose} from 'utils';
import useAsync from 'hooks/async';
import FinancialStatementConnect from 'connectors/FinancialStatements.connector';
import {useIntl} from 'react-intl';
import BalanceSheetHeader from './BalanceSheet/BalanceSheetHeader';
import LoadingIndicator from 'components/LoadingIndicator';
import BalanceSheetTable from './BalanceSheet/BalanceSheetTable';
function BalanceSheet({
fetchBalanceSheet,
changePageTitle,
}) {
const intl = useIntl();
const handleDateChange = () => {};
const fetchHook = useAsync(async () => {
await Promise.all([
fetchBalanceSheet({}),
]);
});
useEffect(() => {
changePageTitle('Balance Sheet');
}, []);
const handleFilterSubmit = (filter) => {
};
return (
<div class="financial-statement">
<BalanceSheetHeader onSubmitFilter={handleFilterSubmit} />
<div class="financial-statement__body">
<LoadingIndicator loading={fetchHook.pending}>
<BalanceSheetTable />
</LoadingIndicator>
</div>
</div>
);
}
export default compose(
DashboardConnect,
FinancialStatementConnect,
)(BalanceSheet);

View File

@@ -0,0 +1,171 @@
import React, {useState, useMemo} from 'react';
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
import {Row, Col} from 'react-grid-system';
import {
Button,
FormGroup,
Position,
MenuItem,
RadioGroup,
Radio,
HTMLSelect,
Intent,
} from "@blueprintjs/core";
import {Select} from '@blueprintjs/select';
import {DateInput} from '@blueprintjs/datetime';
import {useIntl} from 'react-intl';
import {momentFormatter, handleStringChange} from 'utils';
import moment from 'moment';
export default function BalanceSheetHeader({
onSubmitFilter,
}) {
const intl = useIntl();
const [filter, setFilter] = useState({
from_date: null,
to_date: null,
accounting_basis: 'cash',
display_columns_by: 'total',
});
const setFilterByName = (name, value) => {
setFilter({
...filter,
[name]: value,
});
};
const handleFieldChange = (event) => {
setFilterByName(event.target.name, event.target.value);
};
const displayColumnsByOptions = [
{key: 'total', name: 'Total'},
{key: 'year', name: 'Year'},
{key: 'month', name: 'Month'},
{key: 'week', name: 'Week'},
{key: 'day', name: 'Day'},
{key: 'quarter', name: 'Quarter'}
];
const selectedDisplayColumnOpt = useMemo(() => {
return displayColumnsByOptions.find(o => o.key === filter.display_columns_by);
}, [filter.display_columns_by, displayColumnsByOptions]);
// Account type item of select filed.
const accountTypeItem = (item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />);
};
const onItemSelectDisplayColumns = (item) => {
setFilterByName('display_columns_by', item.key);
};
const handleDateChange = (name) => (date) => {
setFilterByName(name, moment(date).format('YYYY-MM-DD'));
};
const handleSubmitClick = () => {
onSubmitFilter(filter);
};
const dateRangeOptions = [
{value: 'today', label: 'Today', },
{value: 'this_week', label: 'This Week'},
{value: 'this_month', label: 'This Month'},
{value: 'this_quarter', label: 'This Quarter'},
{value: 'this_year', label: 'This Year'},
];
return (
<FinancialStatementHeader>
<Row>
<Col sm={4}>
<FormGroup
label={intl.formatMessage({'id': 'report_date_range'})}
minimal={true}
fill={true}>
<HTMLSelect
fill={true}
options={dateRangeOptions} />
</FormGroup>
</Col>
<Col sm={4}>
<FormGroup
label={intl.formatMessage({'id': 'from_date'})}
minimal={true}
fill={true}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
defaultValue={new Date()}
onChange={handleDateChange('from_date')}
popoverProps={{ position: Position.BOTTOM }}
fill={true} />
</FormGroup>
</Col>
<Col sm={4}>
<FormGroup
label={intl.formatMessage({'id': 'to_date'})}
minimal={true}
fill={true}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
defaultValue={new Date()}
onChange={handleDateChange('to_date')}
popoverProps={{ position: Position.BOTTOM }}
fill={true} />
</FormGroup>
</Col>
</Row>
<Row>
<Col sm={4}>
<FormGroup
label={'Display report columns'}
className="{'form-group-display-columns-by'}"
inline={true}>
<Select
items={displayColumnsByOptions}
noResults={<MenuItem disabled={true} text="No results." />}
filterable={false}
itemRenderer={accountTypeItem}
popoverProps={{ minimal: true }}
onItemSelect={onItemSelectDisplayColumns}>
<Button
rightIcon="caret-down"
fill={true}
text={selectedDisplayColumnOpt ? selectedDisplayColumnOpt.name : 'Select'} />
</Select>
</FormGroup>
</Col>
<Col sm={4}>
<RadioGroup
inline={true}
label={intl.formatMessage({'id': 'accounting_basis'})}
name="accounting_bahandleRadioChangesis"
selectedValue={filter.accounting_basis}
onChange={handleStringChange((value) => {
setFilterByName('accounting_basis', value);
})}
>
<Radio label="Cash" value="cash" />
<Radio label="Accural" value="accural" />
</RadioGroup>
</Col>
<Col sm={4}>
<Button intent={Intent.PRIMARY} type="submit" onClick={handleSubmitClick}>
{ 'Calculate Report' }
</Button>
</Col>
</Row>
</FinancialStatementHeader>
)
}

View File

@@ -0,0 +1,41 @@
import React, {useMemo, useState} from 'react';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
export default function BalanceSheetTable({
}) {
const columns = useMemo(() => [
{
Header: 'Account Name',
accessor: 'index',
className: "actions",
},
{
Header: 'Code',
accessor: 'note',
className: "note",
},
{
Header: 'Total',
accessor: 'total',
className: "credit",
},
]);
const [data, setData] = useState([]);
return (
<FinancialSheet
companyTitle={'Facebook, Incopration'}
sheetType={'Balance Sheet'}
date={''}>
<DataTable
columns={columns}
data={data} />
</FinancialSheet>
)
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function FinancialStatementHeader({ children }) {
return (
<div class="financial-statement__header">
{ children }
</div>
);
}

View File

@@ -4,7 +4,7 @@ import t from 'store/types';
const DashboardHomepage = ({ changePageTitle }) => {
useEffect(() => {
changePageTitle('Homepage')
changePageTitle('Craigs Design and Landscaping Services')
});
return (
<div>asdasd</div>

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function AccountantPreferences() {
return (
<div class="preferences__inside-content--accountant">
</div>
);
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import {Tabs, Tab} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import PreferencesSubContent from 'components/Preferences/PreferencesSubContent';
export default function AccountsPreferences() {
const history = useHistory();
const onChangeTabs = (currentTabId) => {
switch(currentTabId) {
default:
history.push('/dashboard/preferences/accounts/general');
break;
case 'custom_fields':
history.push('/dashboard/preferences/accounts/custom_fields');
break;
}
};
return (
<div class="preferences__inside-content preferences__inside-content--accounts">
<Tabs
animate={true}
large={true}
onChange={onChangeTabs}>
<Tab id="general" title="General" />
<Tab id="custom_fields" title="Custom Fields" />
</Tabs>
<PreferencesSubContent preferenceTab="accounts" />
</div>
);
}

View File

@@ -0,0 +1,81 @@
import React, { useEffect } from 'react';
import {
Popover,
Button,
Menu,
MenuDivider,
MenuItem,
Position,
Icon
} from '@blueprintjs/core';
import {
GridComponent,
ColumnsDirective,
ColumnDirective,
} from '@syncfusion/ej2-react-grids';
import useAsync from 'hooks/async';
import {connect} from 'react-redux';
import {
fetchResourceFields,
} from 'store/customFields/customFields.actions';
function AccountsCustomFields({ fetchResourceFields, fields }) {
const fetchHook = useAsync(async () => {
await Promise.all([
// fetchResourceFields('accounts'),
]);
}, false);
useEffect(() => { fetchHook.execute(); }, []);
const actionMenuList = (column) => (
<Menu>
<MenuItem text="View Details" />
<MenuDivider />
<MenuItem text="Edit Account" />
<MenuItem text="New Account" />
<MenuDivider />
<MenuItem text="Inactivate Account" />
<MenuItem text="Delete Account" />
</Menu>
);
const statusRowTemplate = (column) => {
return ('Active');
};
const actionsRowTemplate = (column) => (
<Popover content={actionMenuList(column)} position={Position.RIGHT_BOTTOM}>
<Button icon={<Icon icon="ellipsis-h" />} />
</Popover>
);
const columns = [
{field: 'label_name', headerText: 'Field Label'},
{field: 'data_type', headerText: 'Type'},
{template: statusRowTemplate, headerText: 'Status'},
{template: actionsRowTemplate, headerText: ''},
];
return (
<div class="preferences__inside-content-tab preferences__inside-content-tab--custom-fields">
<GridComponent dataSource={fields}>
<ColumnsDirective>
{columns.map((column) => {
return (<ColumnDirective
field={column.field}
headerText={column.headerText}
template={column.template} />);
})}
</ColumnsDirective>
</GridComponent>
</div>
);
}
const mapStateToProps = (state) => ({
fields: state.fields.custom_fields['accounts'] || [],
});
const mapDispatchToProps = (dispatch) => ({
fetchResourceFields: (resourceSlug) => dispatch(fetchResourceFields({ resourceSlug })),
});
export default connect(mapStateToProps, mapDispatchToProps)(AccountsCustomFields);

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function AccountsGeneralPreferences() {
return (
<div class="preferences__inside-content preferences__inside-content--general">
</div>
);
}

View File

@@ -0,0 +1,88 @@
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import {connect} from 'react-redux';
import {
Button,
FormGroup,
InputGroup,
Intent,
} from "@blueprintjs/core";
import { useAsync } from 'react-use';
import {optionsMapToArray} from 'utils';
import AppToaster from 'components/AppToaster';
import {
savePreferences,
fetchPreferences,
} from 'store/preferences/preferences.actions';
function GeneralPreferences({ savePreferences, fetchPreferences }) {
const validationSchema = Yup.object().shape({
organization_name: Yup.string().required(),
organization_industry: Yup.string().required(),
});
const asyncHook = useAsync(async () => {
await fetchPreferences();
});
const formik = useFormik({
enableReinitialize: true,
initialValues: {
},
validationSchema: validationSchema,
onSubmit: (values) => {
const options = optionsMapToArray(values).map(option => {
return {...option, group: 'general'};
});
savePreferences(options).then((response) => {
AppToaster.show({
message: 'preferences_have_been_updated',
});
}).catch(error => {
});
},
});
return (
<form onSubmit={formik.handleSubmit}>
<FormGroup
label={'Organization Name'}
className="{'form-group--organization-name'}"
inline={true}
helperText={formik.errors.organization_name && formik.errors.organization_name}
intent={formik.errors.organization_name && Intent.DANGER}>
<InputGroup
medium={true}
intent={formik.errors.organization_name && Intent.DANGER}
{...formik.getFieldProps('organization_name')} />
</FormGroup>
<FormGroup
label={'Organization Industry'}
className="{'form-group--organization-industry'}"
inline={true}
helperText={formik.errors.organization_industry && formik.errors.organization_industry}
intent={formik.errors.organization_industry && Intent.DANGER}>
<InputGroup
medium={true}
intent={formik.errors.organization_industry && Intent.DANGER}
{...formik.getFieldProps('organization_industry')} />
</FormGroup>
<div class="divider mt3 mb2"></div>
<Button intent={Intent.PRIMARY} type="submit">{ 'Save' }</Button>
</form>
)
}
const mapDispatchToProps = (dispatch) => ({
savePreferences: (options) => dispatch(savePreferences({ options })),
fetchPreferences: (keys) => dispatch(fetchPreferences())
});
export default connect(null, mapDispatchToProps)(GeneralPreferences);

View File

@@ -0,0 +1,49 @@
import React from 'react';
import {
Tabs,
Tab,
Button,
Intent,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import PreferencesSubContent from 'components/Preferences/PreferencesSubContent';
import connector from 'connectors/UsersPreferences.connector';
function UsersPreferences({
openDialog,
}) {
const history = useHistory();
const onChangeTabs = (currentTabId) => {
};
const onClickNewUser = () => {
openDialog('user-form');
};
return (
<div class="preferences__inside-content preferences__inside-content--users-roles">
<div class="preferences__tabs">
<Tabs
animate={true}
large={true}
onChange={onChangeTabs}>
<Tab id="users" title="Users" />
<Tab id="roles" title="Roles" />
</Tabs>
<div class="preferences__tabs-extra-actions">
<Button
intent={Intent.PRIMARY}
onClick={onClickNewUser}>New User</Button>
<Button
intent={Intent.PRIMARY}
onClick={onClickNewUser}>New Role</Button>
</div>
</div>
<PreferencesSubContent preferenceTab="users" />
</div>
);
}
export default connector(UsersPreferences);

View File

@@ -0,0 +1,143 @@
import React, {useState} from 'react';
import {useAsync} from 'react-use';
import {
GridComponent,
ColumnsDirective,
ColumnDirective,
} from '@syncfusion/ej2-react-grids';
import {
Alert,
Popover,
Button,
Menu,
MenuItem,
MenuDivider,
Position,
Intent,
} from '@blueprintjs/core';
import Icon from 'components/Icon';
import LoadingIndicator from 'components/LoadingIndicator';
import {snakeCase} from 'lodash';
import connector from 'connectors/UsersList.connector';
import AppToaster from 'components/AppToaster';
function UsersListPreferences({
fetchUsers,
usersList,
openDialog,
closeDialog,
deleteUser,
}) {
const [deleteUserState, setDeleteUserState] = useState(false);
const [inactiveUserState, setInactiveUserState] = useState(false);
const asyncHook = useAsync(async () => {
await Promise.all([
fetchUsers(),
]);
}, []);
const onInactiveUser = (user) => {
};
const onDeleteUser = (user) => { setDeleteUserState(user); };
const handleCancelUserDelete = () => { setDeleteUserState(false); };
const onEditUser = (user) => () => {
const form = Object.keys(user).reduce((obj, key) => {
const camelKey = snakeCase(key);
obj[camelKey] = user[key];
return obj;
}, {});
openDialog('user-form', { action: 'edit', user: form, });
};
const handleConfirmUserDelete = () => {
if (!deleteUserState) { return; }
deleteUser(deleteUserState.id).then((response) => {
setDeleteUserState(false);
AppToaster.show({
message: 'the_user_has_been_deleted',
});
});
};
const actionMenuList = (user) =>
(<Menu>
<MenuItem text="Edit User" onClick={onEditUser(user)} />
<MenuItem text="New Account" />
<MenuDivider />
<MenuItem text="Inactivate User" onClick={() => onInactiveUser(user)} />
<MenuItem text="Delete User" onClick={() => onDeleteUser(user)} />
</Menu>);
const columns = [
{
field: '',
headerText: 'Avatar',
},
{
field: 'fullName',
headerText: 'Full Name',
},
{
field: 'email',
headerText: 'Email',
},
{
field: 'phoneNumber',
headerText: 'Phone Number',
},
{
field: '',
headerText: 'Status',
template: (user) => user.active
? (<span>Active</span>) : (<span>Inactive</span>)
},
{
template: (user) => (
<Popover content={actionMenuList(user)} position={Position.RIGHT_BOTTOM}>
<Button icon={<Icon icon="ellipsis-h" />} />
</Popover>
),
}
]
return (
<LoadingIndicator loading={asyncHook.loading}>
<GridComponent
dataSource={{result: usersList.results}}>
<ColumnsDirective>
{columns.map((column) => {
return (<ColumnDirective
field={column.field}
headerText={column.headerText}
template={column.template}
allowSorting={true}
customAttributes={column.customAttributes}
/>);
})}
</ColumnsDirective>
</GridComponent>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={deleteUserState}
onCancel={handleCancelUserDelete}
onConfirm={handleConfirmUserDelete}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</LoadingIndicator>
);
}
export default connector(UsersListPreferences);

View File

@@ -0,0 +1,92 @@
import React, {useEffect, useState} from 'react';
import { useAsync } from 'react-use';
import { useParams } from 'react-router-dom';
import { Intent, Alert } from '@blueprintjs/core';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ViewForm from 'components/Views/ViewForm';
import DashboardConnect from 'connectors/Dashboard.connector';
import ResourceConnect from 'connectors/Resource.connector';
import ViewConnect from 'connectors/View.connector';
import {compose} from 'utils';
import AppToaster from 'components/AppToaster';
function ViewFormPage({
changePageTitle,
fetchResourceFields,
fetchResourceColumns,
fetchView,
getResourceColumns,
getResourceFields,
submitView,
getViewMeta,
deleteView,
}) {
const { resource_slug: resourceSlug, view_id: viewId } = useParams();
const columns = getResourceColumns('accounts');
const fields = getResourceFields('accounts');
const viewForm = (viewId) ? getViewMeta(viewId) : null;
const [stateDeleteView, setStateDeleteView] = useState(null);
useEffect(() => {
if (viewId) {
changePageTitle('Edit Custom View');
} else {
changePageTitle('New Custom View');
}
}, [viewId]);
const fetchHook = useAsync(async () => {
await Promise.all([
fetchResourceColumns('accounts'),
fetchResourceFields('accounts'),
...(viewId) ? [
fetchView(viewId),
] : [],
]);
}, []);
const handleDeleteView = (view) => { setStateDeleteView(view); };
const handleCancelDeleteView = () => { setStateDeleteView(null); };
const handleConfirmDeleteView = () => {
deleteView(stateDeleteView.id).then((response) => {
setStateDeleteView(null);
AppToaster.show({
message: 'the_custom_view_has_been_deleted',
});
})
};
return (
<DashboardInsider name={'view-form'} loading={fetchHook.loading}>
<DashboardPageContent>
<ViewForm
columns={columns}
fields={fields}
viewForm={viewForm}
onDelete={handleDeleteView} />
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={stateDeleteView}
onCancel={handleCancelDeleteView}
onConfirm={handleConfirmDeleteView}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</DashboardPageContent>
</DashboardInsider>
);
}
export default compose(
DashboardConnect,
ResourceConnect,
ViewConnect,
)(ViewFormPage);