mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: Attachment files system.
This commit is contained in:
11
client/.flowconfig
Normal file
11
client/.flowconfig
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[ignore]
|
||||||
|
|
||||||
|
[include]
|
||||||
|
|
||||||
|
[libs]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
|
||||||
|
[options]
|
||||||
|
|
||||||
|
[strict]
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
"eslint-plugin-react": "7.18.0",
|
"eslint-plugin-react": "7.18.0",
|
||||||
"eslint-plugin-react-hooks": "^1.6.1",
|
"eslint-plugin-react-hooks": "^1.6.1",
|
||||||
"file-loader": "4.3.0",
|
"file-loader": "4.3.0",
|
||||||
|
"flow-bin": "^0.123.0",
|
||||||
"formik": "^2.1.4",
|
"formik": "^2.1.4",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"html-webpack-plugin": "4.0.0-beta.11",
|
"html-webpack-plugin": "4.0.0-beta.11",
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^4.13.1",
|
||||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||||
|
"p-progress": "^0.4.2",
|
||||||
"pnp-webpack-plugin": "1.6.0",
|
"pnp-webpack-plugin": "1.6.0",
|
||||||
"postcss-flexbugs-fixes": "4.1.0",
|
"postcss-flexbugs-fixes": "4.1.0",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-loader": "3.0.0",
|
||||||
@@ -65,6 +67,7 @@
|
|||||||
"react-body-classname": "^1.3.1",
|
"react-body-classname": "^1.3.1",
|
||||||
"react-dev-utils": "^10.2.0",
|
"react-dev-utils": "^10.2.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
|
"react-dropzone": "^11.0.1",
|
||||||
"react-grid-system": "^6.2.3",
|
"react-grid-system": "^6.2.3",
|
||||||
"react-hook-form": "^4.9.4",
|
"react-hook-form": "^4.9.4",
|
||||||
"react-intl": "^3.12.0",
|
"react-intl": "^3.12.0",
|
||||||
@@ -97,7 +100,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "PORT=8000 node scripts/start.js",
|
"start": "PORT=8000 node scripts/start.js",
|
||||||
"build": "node scripts/build.js",
|
"build": "node scripts/build.js",
|
||||||
"test": "node scripts/test.js"
|
"test": "node scripts/test.js",
|
||||||
|
"flow": "flow"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
@@ -115,6 +119,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/preset-flow": "^7.9.0",
|
||||||
"http-proxy-middleware": "^1.0.0",
|
"http-proxy-middleware": "^1.0.0",
|
||||||
"redux-devtools": "^3.5.0"
|
"redux-devtools": "^3.5.0"
|
||||||
},
|
},
|
||||||
|
|||||||
77
client/src/components/Dragzone.js
Normal file
77
client/src/components/Dragzone.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useState, useMemo, useCallback, useEffect } from 'react'
|
||||||
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
import { pullAt } from 'lodash';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
|
||||||
|
// const initialFile: {
|
||||||
|
// file: ?File,
|
||||||
|
// preview: string,
|
||||||
|
// metadata: ?object,
|
||||||
|
// uploaded: boolean,
|
||||||
|
// };
|
||||||
|
|
||||||
|
export default function Dropzone({
|
||||||
|
text = 'Drag/Drop files here or click here',
|
||||||
|
onDrop,
|
||||||
|
initialFiles = [],
|
||||||
|
onDeleteFile,
|
||||||
|
hint,
|
||||||
|
className,
|
||||||
|
}) {
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFiles([ ...initialFiles ]);
|
||||||
|
}, [initialFiles]);
|
||||||
|
|
||||||
|
const {getRootProps, getInputProps} = useDropzone({
|
||||||
|
accept: 'image/*',
|
||||||
|
onDrop: (acceptedFiles) => {
|
||||||
|
const _files = acceptedFiles.map((file) => ({
|
||||||
|
file,
|
||||||
|
preview: URL.createObjectURL(file),
|
||||||
|
uploaded: false,
|
||||||
|
}));
|
||||||
|
setFiles(_files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemove = useCallback((index) => {
|
||||||
|
const deletedFile = files.splice(index, 1);
|
||||||
|
setFiles([...files]);
|
||||||
|
onDeleteFile && onDeleteFile(deletedFile);
|
||||||
|
}, [files, onDeleteFile]);
|
||||||
|
|
||||||
|
const thumbs = files.map((file, index) => (
|
||||||
|
<div className={'dropzone-thumb'} key={file.name}>
|
||||||
|
<div><img src={file.preview} /></div>
|
||||||
|
<button onClick={() => handleRemove(index)}>
|
||||||
|
<Icon icon={'times'} iconSize={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
files.forEach(file => URL.revokeObjectURL(file.preview));
|
||||||
|
}, [files, onDrop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDrop && onDrop(files);
|
||||||
|
}, [files, onDrop]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={classNames('dropzone-container', className)}>
|
||||||
|
{(hint) && <div class="dropzone-hint">{ hint }</div>}
|
||||||
|
|
||||||
|
<div {...getRootProps({ className: 'dropzone' })}>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<p>{ text }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'dropzone-thumbs'}>
|
||||||
|
{ thumbs }
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,15 +23,34 @@ import Icon from 'components/Icon';
|
|||||||
import ItemCategoryConnect from 'connectors/ItemsCategory.connect';
|
import ItemCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||||
import MoneyInputGroup from 'components/MoneyInputGroup';
|
import MoneyInputGroup from 'components/MoneyInputGroup';
|
||||||
import {useHistory} from 'react-router-dom';
|
import {useHistory} from 'react-router-dom';
|
||||||
|
import Dragzone from 'components/Dragzone';
|
||||||
|
import MediaConnect from 'connectors/Media.connect';
|
||||||
|
import useMedia from 'hooks/useMedia';
|
||||||
|
|
||||||
const ItemForm = ({
|
const ItemForm = ({
|
||||||
requestSubmitItem,
|
requestSubmitItem,
|
||||||
|
|
||||||
accounts,
|
accounts,
|
||||||
categories,
|
categories,
|
||||||
|
|
||||||
|
requestSubmitMedia,
|
||||||
|
requestDeleteMedia,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedAccounts, setSelectedAccounts] = useState({});
|
const [selectedAccounts, setSelectedAccounts] = useState({});
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
const {
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
saveMedia,
|
||||||
|
deletedFiles,
|
||||||
|
setDeletedFiles,
|
||||||
|
deleteMedia,
|
||||||
|
} = useMedia({
|
||||||
|
saveCallback: requestSubmitMedia,
|
||||||
|
deleteCallback: requestDeleteMedia,
|
||||||
|
})
|
||||||
|
|
||||||
const ItemTypeDisplay = useMemo(() => ([
|
const ItemTypeDisplay = useMemo(() => ([
|
||||||
{ value: null, label: 'Select Item Type' },
|
{ value: null, label: 'Select Item Type' },
|
||||||
{ value: 'service', label: 'Service' },
|
{ value: 'service', label: 'Service' },
|
||||||
@@ -85,15 +104,27 @@ const ItemForm = ({
|
|||||||
...initialValues,
|
...initialValues,
|
||||||
},
|
},
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
requestSubmitItem(values).then((response) => {
|
const saveItem = (mediaIds) => {
|
||||||
AppToaster.show({
|
const formValues = { ...values, media_ids: mediaIds };
|
||||||
message: 'The_Items_has_been_Submit'
|
|
||||||
|
return requestSubmitItem(formValues).then((response) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The_Items_has_been_submit'
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
history.push('/dashboard/items');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
setSubmitting(false);
|
};
|
||||||
history.push('/dashboard/items');
|
|
||||||
})
|
Promise.all([
|
||||||
.catch((error) => {
|
saveMedia(),
|
||||||
setSubmitting(false);
|
deleteMedia(),
|
||||||
|
]).then(([savedMediaResponses]) => {
|
||||||
|
const mediaIds = savedMediaResponses.map(res => res.data.media.id);
|
||||||
|
return saveItem(mediaIds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -139,95 +170,126 @@ const ItemForm = ({
|
|||||||
setFieldValue(fieldKey, value);
|
setFieldValue(fieldKey, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initialAttachmentFiles = useMemo(() => {
|
||||||
|
return [];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDropFiles = useCallback((_files) => {
|
||||||
|
setFiles(_files.filter((file) => file.uploaded === false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteFile = useCallback((_deletedFiles) => {
|
||||||
|
_deletedFiles.forEach((deletedFile) => {
|
||||||
|
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
||||||
|
setDeletedFiles([
|
||||||
|
...deletedFiles, deletedFile.metadata.id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [setDeletedFiles, deletedFiles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='item-form'>
|
<div class='item-form'>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div class="item-form__primary-section">
|
<div class="item-form__primary-section">
|
||||||
<FormGroup
|
<Row>
|
||||||
medium={true}
|
<Col xs={7}>
|
||||||
label={'Item Type'}
|
<FormGroup
|
||||||
labelInfo={requiredSpan}
|
medium={true}
|
||||||
className={'form-group--item-type'}
|
label={'Item Type'}
|
||||||
intent={(errors.type && touched.type) && Intent.DANGER}
|
labelInfo={requiredSpan}
|
||||||
helperText={<ErrorMessage {...{errors, touched}} name="type" />}
|
className={'form-group--item-type'}
|
||||||
inline={true}
|
intent={(errors.type && touched.type) && Intent.DANGER}
|
||||||
>
|
helperText={<ErrorMessage {...{errors, touched}} name="type" />}
|
||||||
<HTMLSelect
|
inline={true}
|
||||||
fill={true}
|
>
|
||||||
options={ItemTypeDisplay}
|
<HTMLSelect
|
||||||
{...getFieldProps('type')}
|
fill={true}
|
||||||
/>
|
options={ItemTypeDisplay}
|
||||||
</FormGroup>
|
{...getFieldProps('type')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={'Item Name'}
|
label={'Item Name'}
|
||||||
labelInfo={requiredSpan}
|
labelInfo={requiredSpan}
|
||||||
className={'form-group--item-name'}
|
className={'form-group--item-name'}
|
||||||
intent={(errors.name && touched.name) && Intent.DANGER}
|
intent={(errors.name && touched.name) && Intent.DANGER}
|
||||||
helperText={<ErrorMessage {...{errors, touched}} name="name" />}
|
helperText={<ErrorMessage {...{errors, touched}} name="name" />}
|
||||||
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={'SKU'}
|
label={'SKU'}
|
||||||
labelInfo={infoIcon}
|
labelInfo={infoIcon}
|
||||||
className={'form-group--item-sku'}
|
className={'form-group--item-sku'}
|
||||||
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
||||||
helperText={<ErrorMessage {...{errors, touched}} name="sku" />}
|
helperText={<ErrorMessage {...{errors, touched}} name="sku" />}
|
||||||
inline={true}
|
inline={true}
|
||||||
>
|
>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
medium={true}
|
medium={true}
|
||||||
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
||||||
{...getFieldProps('sku')}
|
{...getFieldProps('sku')}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={'Category'}
|
label={'Category'}
|
||||||
labelInfo={infoIcon}
|
labelInfo={infoIcon}
|
||||||
inline={true}
|
inline={true}
|
||||||
intent={(errors.category_id && touched.category_id) && Intent.DANGER}
|
intent={(errors.category_id && touched.category_id) && Intent.DANGER}
|
||||||
helperText={<ErrorMessage {...{errors, touched}} name="category" />}
|
helperText={<ErrorMessage {...{errors, touched}} name="category" />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'form-group--select-list',
|
'form-group--select-list',
|
||||||
'form-group--category',
|
'form-group--category',
|
||||||
Classes.FILL,
|
Classes.FILL,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
items={categories}
|
items={categories}
|
||||||
itemRenderer={categoryItem}
|
itemRenderer={categoryItem}
|
||||||
itemPredicate={filterAccounts}
|
itemPredicate={filterAccounts}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
onItemSelect={onItemAccountSelect('category_id')}
|
onItemSelect={onItemAccountSelect('category_id')}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
fill={true}
|
fill={true}
|
||||||
rightIcon='caret-down'
|
rightIcon='caret-down'
|
||||||
text={getSelectedAccountLabel('category_id', 'Select category')}
|
text={getSelectedAccountLabel('category_id', 'Select category')}
|
||||||
/>
|
/>
|
||||||
</Select>
|
</Select>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={' '}
|
label={' '}
|
||||||
inline={true}
|
inline={true}
|
||||||
className={'form-group--active'}
|
className={'form-group--active'}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
inline={true}
|
inline={true}
|
||||||
label={'Active'}
|
label={'Active'}
|
||||||
defaultChecked={values.active}
|
defaultChecked={values.active}
|
||||||
{...getFieldProps('active')}
|
{...getFieldProps('active')}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col xs={3}>
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
className={'mt2'} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row gutterWidth={16} className={'item-form__accounts-section'}>
|
<Row gutterWidth={16} className={'item-form__accounts-section'}>
|
||||||
@@ -258,8 +320,7 @@ const ItemForm = ({
|
|||||||
intent={(errors.sell_account_id && touched.sell_account_id) && Intent.DANGER}
|
intent={(errors.sell_account_id && touched.sell_account_id) && Intent.DANGER}
|
||||||
helperText={<ErrorMessage {...{errors, touched}} name="sell_account_id" />}
|
helperText={<ErrorMessage {...{errors, touched}} name="sell_account_id" />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'form-group--sell-account',
|
'form-group--sell-account', 'form-group--select-list',
|
||||||
'form-group--select-list',
|
|
||||||
Classes.FILL)}
|
Classes.FILL)}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
@@ -392,4 +453,5 @@ export default compose(
|
|||||||
AccountsConnect,
|
AccountsConnect,
|
||||||
ItemsConnect,
|
ItemsConnect,
|
||||||
ItemCategoryConnect,
|
ItemCategoryConnect,
|
||||||
|
MediaConnect,
|
||||||
)(ItemForm);
|
)(ItemForm);
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
views: getResourceViews(state, 'manual_journals'),
|
views: getResourceViews(state, 'manual_journals'),
|
||||||
manualJournals: getManualJournalsItems(state, state.manualJournals.currentViewId),
|
manualJournals: getManualJournalsItems(state, state.manualJournals.currentViewId),
|
||||||
|
manualJournalsItems: state.manualJournals.items,
|
||||||
tableQuery: state.manualJournals.tableQuery,
|
tableQuery: state.manualJournals.tableQuery,
|
||||||
manualJournalsLoading: state.manualJournals.loading,
|
manualJournalsLoading: state.manualJournals.loading,
|
||||||
});
|
});
|
||||||
|
|||||||
16
client/src/connectors/Media.connect.js
Normal file
16
client/src/connectors/Media.connect.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {
|
||||||
|
submitMedia,
|
||||||
|
deleteMedia,
|
||||||
|
} from 'store/media/media.actions';
|
||||||
|
|
||||||
|
export const mapStateToProps = (state, props) => ({
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapDispatchToProps = (dispatch) => ({
|
||||||
|
requestSubmitMedia: (form, config) => dispatch(submitMedia({ form, config })),
|
||||||
|
requestDeleteMedia: (id) => dispatch(deleteMedia({ id })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps);
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { FormattedList } from 'react-intl';
|
import { FormattedList } from 'react-intl';
|
||||||
|
|
||||||
export default function MakeJournalEntriesFooter({
|
export default function MakeJournalEntriesFooter({
|
||||||
formik,
|
formik: { isSubmitting },
|
||||||
onSubmitClick,
|
onSubmitClick,
|
||||||
onCancelClick,
|
onCancelClick,
|
||||||
}) {
|
}) {
|
||||||
@@ -14,7 +14,7 @@ export default function MakeJournalEntriesFooter({
|
|||||||
<div>
|
<div>
|
||||||
<div class="form__floating-footer">
|
<div class="form__floating-footer">
|
||||||
<Button
|
<Button
|
||||||
disabled={formik.isSubmitting}
|
disabled={isSubmitting}
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
name={'save'}
|
name={'save'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -24,7 +24,7 @@ export default function MakeJournalEntriesFooter({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={formik.isSubmitting}
|
disabled={isSubmitting}
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
className={'ml1'}
|
className={'ml1'}
|
||||||
name={'save_and_new'}
|
name={'save_and_new'}
|
||||||
@@ -35,7 +35,7 @@ export default function MakeJournalEntriesFooter({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={formik.isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={'button-secondary ml1'}
|
className={'button-secondary ml1'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSubmitClick({ publish: false, redirect: false });
|
onSubmitClick({ publish: false, redirect: false });
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import React, {useMemo, useState, useEffect, useCallback} from 'react';
|
import React, {useMemo, useState, useEffect, useRef, useCallback} from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
import {
|
||||||
|
ProgressBar,
|
||||||
|
Classes,
|
||||||
|
Intent,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
|
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
|
||||||
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
|
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
|
||||||
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
|
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
|
||||||
@@ -7,12 +12,18 @@ import {useFormik} from "formik";
|
|||||||
import MakeJournalEntriesConnect from 'connectors/MakeJournalEntries.connect';
|
import MakeJournalEntriesConnect from 'connectors/MakeJournalEntries.connect';
|
||||||
import AccountsConnect from 'connectors/Accounts.connector';
|
import AccountsConnect from 'connectors/Accounts.connector';
|
||||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||||
import {compose} from 'utils';
|
import {compose, saveFilesInAsync} from 'utils';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import AppToaster from 'components/AppToaster';
|
import AppToaster from 'components/AppToaster';
|
||||||
import {pick} from 'lodash';
|
import {pick} from 'lodash';
|
||||||
|
import Dragzone from 'components/Dragzone';
|
||||||
|
import MediaConnect from 'connectors/Media.connect';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import ManualJournalsConnect from 'connectors/ManualJournals.connect';
|
||||||
|
import useMedia from 'hooks/useMedia';
|
||||||
|
|
||||||
function MakeJournalEntriesForm({
|
function MakeJournalEntriesForm({
|
||||||
|
requestSubmitMedia,
|
||||||
requestMakeJournalEntries,
|
requestMakeJournalEntries,
|
||||||
requestEditManualJournal,
|
requestEditManualJournal,
|
||||||
changePageTitle,
|
changePageTitle,
|
||||||
@@ -20,7 +31,21 @@ function MakeJournalEntriesForm({
|
|||||||
editJournal,
|
editJournal,
|
||||||
onFormSubmit,
|
onFormSubmit,
|
||||||
onCancelForm,
|
onCancelForm,
|
||||||
|
|
||||||
|
requestDeleteMedia,
|
||||||
|
manualJournalsItems
|
||||||
}) {
|
}) {
|
||||||
|
const { setFiles, saveMedia, deletedFiles, setDeletedFiles, deleteMedia } = useMedia({
|
||||||
|
saveCallback: requestSubmitMedia,
|
||||||
|
deleteCallback: requestDeleteMedia,
|
||||||
|
});
|
||||||
|
const handleDropFiles = useCallback((_files) => {
|
||||||
|
setFiles(_files.filter((file) => file.uploaded === false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const savedMediaIds = useRef([]);
|
||||||
|
const clearSavedMediaIds = () => { savedMediaIds.current = []; }
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editJournal && editJournal.id) {
|
if (editJournal && editJournal.id) {
|
||||||
changePageTitle('Edit Journal');
|
changePageTitle('Edit Journal');
|
||||||
@@ -61,7 +86,7 @@ function MakeJournalEntriesForm({
|
|||||||
note: '',
|
note: '',
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
const initialValues = useMemo(() => ({
|
const defaultInitialValues = useMemo(() => ({
|
||||||
journal_number: '',
|
journal_number: '',
|
||||||
date: moment(new Date()).format('YYYY-MM-DD'),
|
date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
description: '',
|
description: '',
|
||||||
@@ -74,20 +99,33 @@ function MakeJournalEntriesForm({
|
|||||||
],
|
],
|
||||||
}), [defaultEntry]);
|
}), [defaultEntry]);
|
||||||
|
|
||||||
|
const initialValues = useMemo(() => ({
|
||||||
|
...(editJournal) ? {
|
||||||
|
...pick(editJournal, Object.keys(defaultInitialValues)),
|
||||||
|
entries: editJournal.entries.map((entry) => ({
|
||||||
|
...pick(entry, Object.keys(defaultEntry)),
|
||||||
|
})),
|
||||||
|
} : {
|
||||||
|
...defaultInitialValues,
|
||||||
|
}
|
||||||
|
}), [editJournal, defaultInitialValues, defaultEntry]);
|
||||||
|
|
||||||
|
const initialAttachmentFiles = useMemo(() => {
|
||||||
|
return editJournal && editJournal.media
|
||||||
|
? editJournal.media.map((attach) => ({
|
||||||
|
preview: attach.attachment_file,
|
||||||
|
uploaded: true,
|
||||||
|
metadata: { ...attach },
|
||||||
|
})) : [];
|
||||||
|
}, [editJournal]);
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
...(editJournal) ? {
|
...initialValues,
|
||||||
...pick(editJournal, Object.keys(initialValues)),
|
|
||||||
entries: editJournal.entries.map((entry) => ({
|
|
||||||
...pick(entry, Object.keys(defaultEntry)),
|
|
||||||
}))
|
|
||||||
} : {
|
|
||||||
...initialValues,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSubmit: (values, actions) => {
|
onSubmit: async (values, actions) => {
|
||||||
const entries = values.entries.filter((entry) => (
|
const entries = values.entries.filter((entry) => (
|
||||||
(entry.credit || entry.debit)
|
(entry.credit || entry.debit)
|
||||||
));
|
));
|
||||||
@@ -99,6 +137,7 @@ function MakeJournalEntriesForm({
|
|||||||
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.
|
||||||
if (totalCredit !== totalDebit) {
|
if (totalCredit !== totalDebit) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: 'credit_and_debit_not_equal',
|
message: 'credit_and_debit_not_equal',
|
||||||
@@ -108,29 +147,51 @@ function MakeJournalEntriesForm({
|
|||||||
}
|
}
|
||||||
const form = { ...values, status: payload.publish, entries };
|
const form = { ...values, status: payload.publish, entries };
|
||||||
|
|
||||||
if (editJournal && editJournal.id) {
|
const saveJournal = (mediaIds) => new Promise((resolve, reject) => {
|
||||||
requestEditManualJournal(editJournal.id, form)
|
const requestForm = { ...form, media_ids: mediaIds };
|
||||||
.then((response) => {
|
|
||||||
AppToaster.show({
|
if (editJournal && editJournal.id) {
|
||||||
message: 'manual_journal_has_been_edited',
|
requestEditManualJournal(editJournal.id, requestForm)
|
||||||
|
.then((response) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'manual_journal_has_been_edited',
|
||||||
|
});
|
||||||
|
actions.setSubmitting(false);
|
||||||
|
saveInvokeSubmit({ action: 'update', ...payload });
|
||||||
|
clearSavedMediaIds([]);
|
||||||
|
resolve(response);
|
||||||
|
}).catch((error) => {
|
||||||
|
actions.setSubmitting(false);
|
||||||
|
reject(error);
|
||||||
});
|
});
|
||||||
actions.setSubmitting(false);
|
} else {
|
||||||
saveInvokeSubmit({ action: 'update', ...payload });
|
requestMakeJournalEntries(requestForm)
|
||||||
}).catch((error) => {
|
.then((response) => {
|
||||||
actions.setSubmitting(false);
|
AppToaster.show({
|
||||||
});
|
message: 'manual_journal_has_been_submit',
|
||||||
} else {
|
});
|
||||||
requestMakeJournalEntries(form)
|
actions.setSubmitting(false);
|
||||||
.then((response) => {
|
saveInvokeSubmit({ action: 'new', ...payload });
|
||||||
AppToaster.show({
|
clearSavedMediaIds();
|
||||||
message: 'manual_journal_has_been_submit',
|
resolve(response);
|
||||||
|
}).catch((error) => {
|
||||||
|
actions.setSubmitting(false);
|
||||||
|
reject(error);
|
||||||
});
|
});
|
||||||
actions.setSubmitting(false);
|
}
|
||||||
saveInvokeSubmit({ action: 'new', ...payload });
|
});
|
||||||
}).catch((error) => {
|
|
||||||
actions.setSubmitting(false);
|
Promise.all([
|
||||||
});
|
saveMedia(),
|
||||||
}
|
deleteMedia(),
|
||||||
|
]).then(([savedMediaResponses]) => {
|
||||||
|
const mediaIds = savedMediaResponses.map(res => res.data.media.id);
|
||||||
|
savedMediaIds.current = mediaIds;
|
||||||
|
|
||||||
|
return savedMediaResponses;
|
||||||
|
}).then(() => {
|
||||||
|
return saveJournal(savedMediaIds.current);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,22 +204,45 @@ function MakeJournalEntriesForm({
|
|||||||
onCancelForm && onCancelForm(payload);
|
onCancelForm && onCancelForm(payload);
|
||||||
}, [onCancelForm]);
|
}, [onCancelForm]);
|
||||||
|
|
||||||
|
const handleDeleteFile = useCallback((_deletedFiles) => {
|
||||||
|
_deletedFiles.forEach((deletedFile) => {
|
||||||
|
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
||||||
|
setDeletedFiles([
|
||||||
|
...deletedFiles, deletedFile.metadata.id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [setDeletedFiles, deletedFiles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="make-journal-entries">
|
<div class="make-journal-entries">
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<MakeJournalEntriesHeader formik={formik} />
|
<MakeJournalEntriesHeader formik={formik} />
|
||||||
<MakeJournalEntriesTable formik={formik} defaultRow={defaultEntry} />
|
|
||||||
|
<MakeJournalEntriesTable
|
||||||
|
initialValues={initialValues}
|
||||||
|
formik={formik}
|
||||||
|
defaultRow={defaultEntry} />
|
||||||
|
|
||||||
<MakeJournalEntriesFooter
|
<MakeJournalEntriesFooter
|
||||||
formik={formik}
|
formik={formik}
|
||||||
onSubmitClick={handleSubmitClick}
|
onSubmitClick={handleSubmitClick}
|
||||||
onCancelClick={handleCancelClick} />
|
onCancelClick={handleCancelClick} />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
|
ManualJournalsConnect,
|
||||||
MakeJournalEntriesConnect,
|
MakeJournalEntriesConnect,
|
||||||
AccountsConnect,
|
AccountsConnect,
|
||||||
DashboardConnect,
|
DashboardConnect,
|
||||||
|
MediaConnect,
|
||||||
)(MakeJournalEntriesForm);
|
)(MakeJournalEntriesForm);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, {useMemo, useCallback} from 'react';
|
import React, {useMemo, useCallback} from 'react';
|
||||||
import { useParams, useHistory } from 'react-router-dom';
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
import { useAsync } from 'react-use';
|
import useAsync from 'hooks/async';
|
||||||
import MakeJournalEntriesForm from './MakeJournalEntriesForm';
|
import MakeJournalEntriesForm from './MakeJournalEntriesForm';
|
||||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||||
@@ -22,6 +22,7 @@ function MakeJournalEntriesPage({
|
|||||||
(id) && fetchManualJournal(id),
|
(id) && fetchManualJournal(id),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const editJournal = useMemo(() =>
|
const editJournal = useMemo(() =>
|
||||||
getManualJournal(id) || null,
|
getManualJournal(id) || null,
|
||||||
[getManualJournal, id]);
|
[getManualJournal, id]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useState, useMemo, useCallback} from 'react';
|
import React, {useState, useMemo, useEffect, useCallback} from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Intent,
|
Intent,
|
||||||
@@ -74,34 +74,36 @@ const NoteCellRenderer = (chainedComponent) => (props) => {
|
|||||||
* Make journal entries table component.
|
* Make journal entries table component.
|
||||||
*/
|
*/
|
||||||
function MakeJournalEntriesTable({
|
function MakeJournalEntriesTable({
|
||||||
formik,
|
formik: { errors, values, setFieldValue },
|
||||||
accounts,
|
accounts,
|
||||||
onClickRemoveRow,
|
onClickRemoveRow,
|
||||||
onClickAddNewRow,
|
onClickAddNewRow,
|
||||||
defaultRow,
|
defaultRow,
|
||||||
|
initialValues,
|
||||||
}) {
|
}) {
|
||||||
const [rows, setRow] = useState([
|
const [rows, setRow] = useState([]);
|
||||||
...formik.values.entries.map((e) => ({ ...e, rowType: 'editor'})),
|
|
||||||
defaultRow,
|
useEffect(() => {
|
||||||
defaultRow,
|
setRow([
|
||||||
]);
|
...initialValues.entries.map((e) => ({ ...e, rowType: 'editor'})),
|
||||||
|
defaultRow,
|
||||||
|
defaultRow,
|
||||||
|
])
|
||||||
|
}, [initialValues, defaultRow])
|
||||||
|
|
||||||
// Handles update datatable data.
|
// Handles update datatable data.
|
||||||
const handleUpdateData = useCallback((rowIndex, columnId, value) => {
|
const handleUpdateData = useCallback((rowIndex, columnId, value) => {
|
||||||
const newRows = rows.map((row, index) => {
|
const newRows = rows.map((row, index) => {
|
||||||
if (index === rowIndex) {
|
if (index === rowIndex) {
|
||||||
return {
|
return { ...rows[rowIndex], [columnId]: value };
|
||||||
...rows[rowIndex],
|
|
||||||
[columnId]: value,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { ...row };
|
return { ...row };
|
||||||
});
|
});
|
||||||
setRow(newRows);
|
setRow(newRows);
|
||||||
formik.setFieldValue('entries', newRows.map(row => ({
|
setFieldValue('entries', newRows.map(row => ({
|
||||||
...omit(row, ['rowType']),
|
...omit(row, ['rowType']),
|
||||||
})));
|
})));
|
||||||
}, [rows, formik]);
|
}, [rows, setFieldValue]);
|
||||||
|
|
||||||
// Handles click remove datatable row.
|
// Handles click remove datatable row.
|
||||||
const handleRemoveRow = useCallback((rowIndex) => {
|
const handleRemoveRow = useCallback((rowIndex) => {
|
||||||
@@ -109,12 +111,12 @@ function MakeJournalEntriesTable({
|
|||||||
const newRows = rows.filter((row, index) => index !== removeIndex);
|
const newRows = rows.filter((row, index) => index !== removeIndex);
|
||||||
|
|
||||||
setRow([ ...newRows ]);
|
setRow([ ...newRows ]);
|
||||||
formik.setFieldValue('entries', newRows
|
setFieldValue('entries', newRows
|
||||||
.filter(row => row.rowType === 'editor')
|
.filter(row => row.rowType === 'editor')
|
||||||
.map(row => ({ ...omit(row, ['rowType']) })
|
.map(row => ({ ...omit(row, ['rowType']) })
|
||||||
));
|
));
|
||||||
onClickRemoveRow && onClickRemoveRow(removeIndex);
|
onClickRemoveRow && onClickRemoveRow(removeIndex);
|
||||||
}, [rows, formik, onClickRemoveRow]);
|
}, [rows, setFieldValue, onClickRemoveRow]);
|
||||||
|
|
||||||
// Memorized data table columns.
|
// Memorized data table columns.
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(() => [
|
||||||
@@ -196,7 +198,7 @@ function MakeJournalEntriesTable({
|
|||||||
rowClassNames={rowClassNames}
|
rowClassNames={rowClassNames}
|
||||||
payload={{
|
payload={{
|
||||||
accounts,
|
accounts,
|
||||||
errors: formik.errors.entries || [],
|
errors: errors.entries || [],
|
||||||
updateData: handleUpdateData,
|
updateData: handleUpdateData,
|
||||||
removeRow: handleRemoveRow,
|
removeRow: handleRemoveRow,
|
||||||
}}/>
|
}}/>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function BalanceSheet({
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchBalanceSheet({ ...query }),
|
fetchBalanceSheet({ ...query }),
|
||||||
]);
|
]);
|
||||||
});
|
}, false);
|
||||||
|
|
||||||
// Handle fetch the data of balance sheet.
|
// Handle fetch the data of balance sheet.
|
||||||
const handleFetchData = useCallback(() => { fetchHook.execute(); }, [fetchHook]);
|
const handleFetchData = useCallback(() => { fetchHook.execute(); }, [fetchHook]);
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
compose,
|
compose,
|
||||||
defaultExpanderReducer,
|
defaultExpanderReducer,
|
||||||
} from 'utils';
|
} from 'utils';
|
||||||
|
import SettingsConnect from 'connectors/Settings.connect';
|
||||||
|
|
||||||
function BalanceSheetTable({
|
function BalanceSheetTable({
|
||||||
companyName,
|
organizationSettings,
|
||||||
balanceSheetAccounts,
|
balanceSheetAccounts,
|
||||||
balanceSheetColumns,
|
balanceSheetColumns,
|
||||||
balanceSheetQuery,
|
balanceSheetQuery,
|
||||||
@@ -110,7 +111,7 @@ function BalanceSheetTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FinancialSheet
|
<FinancialSheet
|
||||||
companyName={companyName}
|
companyName={organizationSettings.name}
|
||||||
sheetType={'Balance Sheet'}
|
sheetType={'Balance Sheet'}
|
||||||
fromDate={balanceSheetQuery.from_date}
|
fromDate={balanceSheetQuery.from_date}
|
||||||
toDate={balanceSheetQuery.to_date}
|
toDate={balanceSheetQuery.to_date}
|
||||||
@@ -122,8 +123,9 @@ function BalanceSheetTable({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={balanceSheetAccounts}
|
data={balanceSheetAccounts}
|
||||||
onFetchData={handleFetchData}
|
onFetchData={handleFetchData}
|
||||||
|
expanded={expandedRows}
|
||||||
expandSubRows={true}
|
expandSubRows={true}
|
||||||
expanded={expandedRows} />
|
noInitialFetch={true} />
|
||||||
</FinancialSheet>
|
</FinancialSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -131,4 +133,5 @@ function BalanceSheetTable({
|
|||||||
export default compose(
|
export default compose(
|
||||||
BalanceSheetConnect,
|
BalanceSheetConnect,
|
||||||
BalanceSheetTableConnect,
|
BalanceSheetTableConnect,
|
||||||
|
SettingsConnect,
|
||||||
)(BalanceSheetTable);
|
)(BalanceSheetTable);
|
||||||
@@ -89,7 +89,8 @@ function JournalSheetTable({
|
|||||||
data={data}
|
data={data}
|
||||||
onFetchData={handleFetchData}
|
onFetchData={handleFetchData}
|
||||||
noResults={"This report does not contain any data."}
|
noResults={"This report does not contain any data."}
|
||||||
expanded={expandedRows} />
|
expanded={expandedRows}
|
||||||
|
noInitialFetch={true} />
|
||||||
</FinancialSheet>
|
</FinancialSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ function ProfitLossSheetTable({
|
|||||||
data={profitLossTableRows}
|
data={profitLossTableRows}
|
||||||
onFetchData={handleFetchData}
|
onFetchData={handleFetchData}
|
||||||
expanded={expandedRows}
|
expanded={expandedRows}
|
||||||
rowClassNames={rowClassNames} />
|
rowClassNames={rowClassNames}
|
||||||
|
noInitialFetch={true} />
|
||||||
</FinancialSheet>
|
</FinancialSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
59
client/src/hooks/useMedia.js
Normal file
59
client/src/hooks/useMedia.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { useState, useRef, useMemo, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ProgressBar,
|
||||||
|
Classes,
|
||||||
|
Intent,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import AppToaster from 'components/AppToaster';
|
||||||
|
import { saveFilesInAsync } from 'utils';
|
||||||
|
|
||||||
|
const useMedia = ({ saveCallback, deleteCallback }) => {
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
const [deletedFiles, setDeletedFiles] = useState([]);
|
||||||
|
const toastKey = useRef(0);
|
||||||
|
|
||||||
|
const openProgressToast = useCallback((amount) => ({
|
||||||
|
message: (
|
||||||
|
<ProgressBar
|
||||||
|
className={classNames("toast-progress", {
|
||||||
|
[Classes.PROGRESS_NO_STRIPES]: amount >= 100,
|
||||||
|
})}
|
||||||
|
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
|
||||||
|
value={amount / 100}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const saveMedia = useCallback(() => {
|
||||||
|
const notUploadedFiles = files.filter((file) => file.uploaded === false);
|
||||||
|
|
||||||
|
if (notUploadedFiles.length > 0) {
|
||||||
|
toastKey.current = AppToaster.show(openProgressToast(0));
|
||||||
|
|
||||||
|
const saveAction = (formData, attachment, progressCallback) => {
|
||||||
|
return saveCallback(formData, {
|
||||||
|
onUploadProgress: (progress) => { progressCallback(progress); }
|
||||||
|
}).then((res) => {
|
||||||
|
attachment.uploaded = true;
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return saveFilesInAsync(notUploadedFiles, saveAction).onProgress((progress) => {
|
||||||
|
if (progress > 0) {
|
||||||
|
AppToaster.show(openProgressToast(progress * 100), toastKey.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}, [files, openProgressToast, saveCallback]);
|
||||||
|
|
||||||
|
const deleteMedia = useCallback(() => {
|
||||||
|
return deletedFiles.length > 0
|
||||||
|
? deleteCallback(deletedFiles) : Promise.resolve();
|
||||||
|
}, [deletedFiles, deleteCallback]);
|
||||||
|
|
||||||
|
return { files, setFiles, saveMedia, deletedFiles, setDeletedFiles, deleteMedia };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMedia;
|
||||||
@@ -6,8 +6,8 @@ export default {
|
|||||||
return axios.get(`/api/${resource}`, params);
|
return axios.get(`/api/${resource}`, params);
|
||||||
},
|
},
|
||||||
|
|
||||||
post(resource, params) {
|
post(resource, params, config) {
|
||||||
return axios.post(`/api/${resource}`, params);
|
return axios.post(`/api/${resource}`, params, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
update(resource, slug, params) {
|
update(resource, slug, params) {
|
||||||
|
|||||||
@@ -11,27 +11,32 @@ const initialState = {
|
|||||||
tableQuery: {},
|
tableQuery: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultJournal = {
|
||||||
|
entries: [],
|
||||||
|
};
|
||||||
|
|
||||||
const reducer = createReducer(initialState, {
|
const reducer = createReducer(initialState, {
|
||||||
|
|
||||||
[t.MANUAL_JOURNAL_SET]: (state, action) => {
|
[t.MANUAL_JOURNAL_SET]: (state, action) => {
|
||||||
const { id, manualJournal } = action.payload;
|
const { id, manualJournal } = action.payload;
|
||||||
state.items[id] = manualJournal;
|
state.items[id] = { ...defaultJournal, ...manualJournal };
|
||||||
},
|
},
|
||||||
|
|
||||||
[t.MANUAL_JOURNAL_PUBLISH]: (state, action) => {
|
[t.MANUAL_JOURNAL_PUBLISH]: (state, action) => {
|
||||||
const { id } = action.payload;
|
const { id } = action.payload;
|
||||||
const item = state.items[id] || {};
|
const item = state.items[id] || {};
|
||||||
|
|
||||||
state.items[id] = {
|
state.items[id] = { ...item, status: 1 };
|
||||||
...item, status: 1,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[t.MANUAL_JOURNALS_ITEMS_SET]: (state, action) => {
|
[t.MANUAL_JOURNALS_ITEMS_SET]: (state, action) => {
|
||||||
const _manual_journals = {};
|
const _manual_journals = {};
|
||||||
|
|
||||||
action.manual_journals.forEach((manual_journal) => {
|
action.manual_journals.forEach((manual_journal) => {
|
||||||
_manual_journals[manual_journal.id] = manual_journal;
|
_manual_journals[manual_journal.id] = {
|
||||||
|
...defaultJournal,
|
||||||
|
...manual_journal,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
state.items = {
|
state.items = {
|
||||||
...state.items,
|
...state.items,
|
||||||
|
|||||||
13
client/src/store/media/media.actions.js
Normal file
13
client/src/store/media/media.actions.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import ApiService from "services/ApiService"
|
||||||
|
|
||||||
|
export const submitMedia = ({ form, config }) => {
|
||||||
|
return (dispatch) => {
|
||||||
|
return ApiService.post('media/upload', form, config);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMedia = ({ id }) => {
|
||||||
|
return (dispatch) => {
|
||||||
|
return ApiService.delete('media', { params: { id } });
|
||||||
|
}
|
||||||
|
};
|
||||||
0
client/src/store/media/media.reducers.js
Normal file
0
client/src/store/media/media.reducers.js
Normal file
0
client/src/store/media/media.types.js
Normal file
0
client/src/store/media/media.types.js
Normal file
@@ -3,7 +3,9 @@ import t from 'store/types';
|
|||||||
import { optionsArrayToMap } from 'utils';
|
import { optionsArrayToMap } from 'utils';
|
||||||
const initialState = {
|
const initialState = {
|
||||||
data: {
|
data: {
|
||||||
organization: {},
|
organization: {
|
||||||
|
name: 'Bigcapital, Limited Liabilities',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
|||||||
@import 'components/data-table';
|
@import 'components/data-table';
|
||||||
@import 'components/dialog';
|
@import 'components/dialog';
|
||||||
@import 'components/custom-scrollbar';
|
@import 'components/custom-scrollbar';
|
||||||
|
@import 'components/dragzone';
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
@import 'pages/dashboard';
|
@import 'pages/dashboard';
|
||||||
|
|||||||
74
client/src/style/components/dragzone.scss
Normal file
74
client/src/style/components/dragzone.scss
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
.dropzone{
|
||||||
|
flex: 1 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 18px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #AFAFAF;
|
||||||
|
border-style: dashed;
|
||||||
|
color: #999;
|
||||||
|
outline: none;
|
||||||
|
transition: border .24s ease-in-out;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
p{
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-thumbs{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-thumb{
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 100px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
img{
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button{
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: #db3737;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: -5px;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
.bp3-icon{
|
||||||
|
position: relative;
|
||||||
|
top: -3px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover button{
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-hint{
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
.make-journal-entries{
|
.make-journal-entries{
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
&__header{
|
&__header{
|
||||||
padding: 25px 27px 20px;
|
padding: 25px 27px 20px;
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__table{
|
&__table{
|
||||||
padding: 15px;
|
padding: 15px 15px 0;
|
||||||
|
|
||||||
.bp3-form-group{
|
.bp3-form-group{
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -154,4 +156,16 @@
|
|||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
padding-right: 14px;
|
padding-right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropzone-container{
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone{
|
||||||
|
|
||||||
|
width: 300px;
|
||||||
|
height: 75px;
|
||||||
|
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Currency from 'js-money/lib/currency';
|
import Currency from 'js-money/lib/currency';
|
||||||
|
import PProgress from 'p-progress';
|
||||||
import accounting from 'accounting';
|
import accounting from 'accounting';
|
||||||
|
|
||||||
|
|
||||||
@@ -152,3 +153,21 @@ export const checkRequiredProperties = (obj, properties) => {
|
|||||||
return (value === '' || value === null || value === undefined);
|
return (value === '' || value === null || value === undefined);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const saveFilesInAsync = (files, actionCb, extraTasks) => {
|
||||||
|
const opers = [];
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('attachment', file.file);
|
||||||
|
const oper = new PProgress((resolve, reject, progress) => {
|
||||||
|
actionCb(formData, file, (requestProgress) => {
|
||||||
|
progress(requestProgress);
|
||||||
|
})
|
||||||
|
.then((data) => { resolve(data); })
|
||||||
|
.catch(error => { reject(error); })
|
||||||
|
});
|
||||||
|
opers.push(oper);
|
||||||
|
});
|
||||||
|
return PProgress.all(opers);
|
||||||
|
}
|
||||||
|
|||||||
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
/node_modules/
|
/node_modules/
|
||||||
/.env
|
/.env
|
||||||
/.env.test
|
/.env.test
|
||||||
|
/storage
|
||||||
@@ -10,6 +10,8 @@ exports.up = function (knex) {
|
|||||||
table.integer('role_id').unique();
|
table.integer('role_id').unique();
|
||||||
table.string('language');
|
table.string('language');
|
||||||
table.date('last_login_at');
|
table.date('last_login_at');
|
||||||
|
|
||||||
|
table.date('invite_accepted_at');
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('media_links', table => {
|
||||||
|
table.increments();
|
||||||
|
table.string('model_name');
|
||||||
|
table.integer('media_id').unsigned();
|
||||||
|
table.integer('model_id').unsigned();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTableIfExists('media_links');
|
||||||
|
};
|
||||||
@@ -82,7 +82,7 @@ export default {
|
|||||||
const filter = {
|
const filter = {
|
||||||
filter_roles: [],
|
filter_roles: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 10,
|
page_size: 999,
|
||||||
...req.query,
|
...req.query,
|
||||||
};
|
};
|
||||||
if (filter.stringified_filter_roles) {
|
if (filter.stringified_filter_roles) {
|
||||||
@@ -90,8 +90,6 @@ export default {
|
|||||||
}
|
}
|
||||||
const { Resource, View, ManualJournal } = req.models;
|
const { Resource, View, ManualJournal } = req.models;
|
||||||
|
|
||||||
console.log(req.models);
|
|
||||||
|
|
||||||
const errorReasons = [];
|
const errorReasons = [];
|
||||||
const manualJournalsResource = await Resource.query()
|
const manualJournalsResource = await Resource.query()
|
||||||
.where('name', 'manual_journals')
|
.where('name', 'manual_journals')
|
||||||
@@ -185,6 +183,8 @@ export default {
|
|||||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.account_id').isNumeric().toInt(),
|
check('entries.*.account_id').isNumeric().toInt(),
|
||||||
check('entries.*.note').optional(),
|
check('entries.*.note').optional(),
|
||||||
|
check('media_ids').optional().isArray(),
|
||||||
|
check('media_ids.*').exists().isNumeric().toInt(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
@@ -198,9 +198,10 @@ export default {
|
|||||||
date: new Date(),
|
date: new Date(),
|
||||||
transaction_type: 'journal',
|
transaction_type: 'journal',
|
||||||
reference: '',
|
reference: '',
|
||||||
|
media_ids: [],
|
||||||
...req.body,
|
...req.body,
|
||||||
};
|
};
|
||||||
const { ManualJournal, Account } = req.models;
|
const { ManualJournal, Account, Media, MediaLink } = req.models;
|
||||||
|
|
||||||
let totalCredit = 0;
|
let totalCredit = 0;
|
||||||
let totalDebit = 0;
|
let totalDebit = 0;
|
||||||
@@ -233,6 +234,14 @@ export default {
|
|||||||
|
|
||||||
const storedAccountsIds = accounts.map((account) => account.id);
|
const storedAccountsIds = accounts.map((account) => account.id);
|
||||||
|
|
||||||
|
if (form.media_ids.length > 0) {
|
||||||
|
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
||||||
|
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
||||||
|
|
||||||
|
if (notFoundMedia.length > 0) {
|
||||||
|
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
||||||
|
}
|
||||||
|
}
|
||||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||||
}
|
}
|
||||||
@@ -279,8 +288,22 @@ export default {
|
|||||||
journalPoster.credit(jouranlEntry);
|
journalPoster.credit(jouranlEntry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save linked media to the journal model.
|
||||||
|
const bulkSaveMediaLink = [];
|
||||||
|
|
||||||
|
form.media_ids.forEach((mediaId) => {
|
||||||
|
const oper = MediaLink.query().insert({
|
||||||
|
model_name: 'Journal',
|
||||||
|
model_id: manualJournal.id,
|
||||||
|
media_id: mediaId,
|
||||||
|
});
|
||||||
|
bulkSaveMediaLink.push(oper);
|
||||||
|
});
|
||||||
|
|
||||||
// Saves the journal entries and accounts balance changes.
|
// Saves the journal entries and accounts balance changes.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
...bulkSaveMediaLink,
|
||||||
journalPoster.saveEntries(),
|
journalPoster.saveEntries(),
|
||||||
(form.status) && journalPoster.saveBalance(),
|
(form.status) && journalPoster.saveBalance(),
|
||||||
]);
|
]);
|
||||||
@@ -313,6 +336,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit the given manual journal.
|
||||||
|
*/
|
||||||
editManualJournal: {
|
editManualJournal: {
|
||||||
validation: [
|
validation: [
|
||||||
param('id').exists().isNumeric().toInt(),
|
param('id').exists().isNumeric().toInt(),
|
||||||
@@ -326,6 +352,8 @@ export default {
|
|||||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.account_id').isNumeric().toInt(),
|
check('entries.*.account_id').isNumeric().toInt(),
|
||||||
check('entries.*.note').optional(),
|
check('entries.*.note').optional(),
|
||||||
|
check('media_ids').optional().isArray(),
|
||||||
|
check('media_ids.*').isNumeric().toInt(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
@@ -339,14 +367,17 @@ export default {
|
|||||||
date: new Date(),
|
date: new Date(),
|
||||||
transaction_type: 'journal',
|
transaction_type: 'journal',
|
||||||
reference: '',
|
reference: '',
|
||||||
|
media_ids: [],
|
||||||
...req.body,
|
...req.body,
|
||||||
};
|
};
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const {
|
const {
|
||||||
ManualJournal, AccountTransaction, Account,
|
ManualJournal, AccountTransaction, Account, Media, MediaLink,
|
||||||
} = req.models;
|
} = req.models;
|
||||||
|
|
||||||
const manualJournal = await ManualJournal.query().where('id', id).first();
|
const manualJournal = await ManualJournal.query()
|
||||||
|
.where('id', id)
|
||||||
|
.withGraphFetched('media').first();
|
||||||
|
|
||||||
if (!manualJournal) {
|
if (!manualJournal) {
|
||||||
return res.status(4040).send({
|
return res.status(4040).send({
|
||||||
@@ -395,6 +426,16 @@ export default {
|
|||||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate if media ids was not already exists on the storage.
|
||||||
|
if (form.media_ids.length > 0) {
|
||||||
|
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
||||||
|
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
||||||
|
|
||||||
|
if (notFoundMedia.length > 0) {
|
||||||
|
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
||||||
|
}
|
||||||
|
}
|
||||||
if (errorReasons.length > 0) {
|
if (errorReasons.length > 0) {
|
||||||
return res.status(400).send({ errors: errorReasons });
|
return res.status(400).send({ errors: errorReasons });
|
||||||
}
|
}
|
||||||
@@ -439,7 +480,23 @@ export default {
|
|||||||
journal.credit(jouranlEntry);
|
journal.credit(jouranlEntry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save links of new inserted media that associated to the journal model.
|
||||||
|
const journalMediaIds = manualJournal.media.map((m) => m.id);
|
||||||
|
const newInsertedMedia = difference(form.media_ids, journalMediaIds);
|
||||||
|
const bulkSaveMediaLink = [];
|
||||||
|
|
||||||
|
newInsertedMedia.forEach((mediaId) => {
|
||||||
|
const oper = MediaLink.query().insert({
|
||||||
|
model_name: 'Journal',
|
||||||
|
model_id: manualJournal.id,
|
||||||
|
media_id: mediaId,
|
||||||
|
});
|
||||||
|
bulkSaveMediaLink.push(oper);
|
||||||
|
});
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
...bulkSaveMediaLink,
|
||||||
journal.deleteEntries(),
|
journal.deleteEntries(),
|
||||||
journal.saveEntries(),
|
journal.saveEntries(),
|
||||||
journal.saveBalance(),
|
journal.saveBalance(),
|
||||||
@@ -524,7 +581,9 @@ export default {
|
|||||||
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const manualJournal = await ManualJournal.query()
|
const manualJournal = await ManualJournal.query()
|
||||||
.where('id', id).first();
|
.where('id', id)
|
||||||
|
.withGraphFetched('media')
|
||||||
|
.first();
|
||||||
|
|
||||||
if (!manualJournal) {
|
if (!manualJournal) {
|
||||||
return res.status(404).send({
|
return res.status(404).send({
|
||||||
@@ -564,7 +623,9 @@ export default {
|
|||||||
}
|
}
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const {
|
const {
|
||||||
ManualJournal, AccountTransaction,
|
ManualJournal,
|
||||||
|
AccountTransaction,
|
||||||
|
MediaLink,
|
||||||
} = req.models;
|
} = req.models;
|
||||||
const manualJournal = await ManualJournal.query()
|
const manualJournal = await ManualJournal.query()
|
||||||
.where('id', id).first();
|
.where('id', id).first();
|
||||||
@@ -583,6 +644,11 @@ export default {
|
|||||||
journal.loadEntries(transactions);
|
journal.loadEntries(transactions);
|
||||||
journal.removeEntries();
|
journal.removeEntries();
|
||||||
|
|
||||||
|
await MediaLink.query()
|
||||||
|
.where('model_name', 'Journal')
|
||||||
|
.where('model_id', manualJournal.id)
|
||||||
|
.delete();
|
||||||
|
|
||||||
await ManualJournal.query()
|
await ManualJournal.query()
|
||||||
.where('id', manualJournal.id)
|
.where('id', manualJournal.id)
|
||||||
.delete();
|
.delete();
|
||||||
@@ -678,7 +744,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const filter = { ...req.query };
|
const filter = { ...req.query };
|
||||||
const { ManualJournal, AccountTransaction } = req.models;
|
const { ManualJournal, AccountTransaction, MediaLink } = req.models;
|
||||||
|
|
||||||
const manualJournals = await ManualJournal.query()
|
const manualJournals = await ManualJournal.query()
|
||||||
.whereIn('id', filter.ids);
|
.whereIn('id', filter.ids);
|
||||||
@@ -699,6 +765,11 @@ export default {
|
|||||||
journal.loadEntries(transactions);
|
journal.loadEntries(transactions);
|
||||||
journal.removeEntries();
|
journal.removeEntries();
|
||||||
|
|
||||||
|
await MediaLink.query()
|
||||||
|
.where('model_name', 'Journal')
|
||||||
|
.whereIn('model_id', filter.ids)
|
||||||
|
.delete();
|
||||||
|
|
||||||
await ManualJournal.query()
|
await ManualJournal.query()
|
||||||
.whereIn('id', filter.ids).delete();
|
.whereIn('id', filter.ids).delete();
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export default {
|
|||||||
check('custom_fields.*.value').exists(),
|
check('custom_fields.*.value').exists(),
|
||||||
|
|
||||||
check('note').optional(),
|
check('note').optional(),
|
||||||
|
|
||||||
|
check('media_ids').optional().isArray(),
|
||||||
|
check('media_ids.*').exists().isNumeric().toInt(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default {
|
|||||||
code: 'validation_error', ...validationErrors,
|
code: 'validation_error', ...validationErrors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { Media } = req.models;
|
const { Media, MediaLink } = req.models;
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const media = await Media.query().where('id', id).first();
|
const media = await Media.query().where('id', id).first();
|
||||||
|
|
||||||
@@ -137,6 +137,8 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.log('error', 'Delete item attachment file delete failed.', { error });
|
Logger.log('error', 'Delete item attachment file delete failed.', { error });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await MediaLink.query().where('media_id', media.id).delete();
|
||||||
await Media.query().where('id', media.id).delete();
|
await Media.query().where('id', media.id).delete();
|
||||||
|
|
||||||
return res.status(200).send();
|
return res.status(200).send();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Model } from 'objection';
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
|
|
||||||
export default class ManualJournal extends TenantModel {
|
export default class ManualJournal extends TenantModel {
|
||||||
@@ -7,4 +8,26 @@ export default class ManualJournal extends TenantModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'manual_journals';
|
return 'manual_journals';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const Media = require('@/models/Media');
|
||||||
|
|
||||||
|
return {
|
||||||
|
media: {
|
||||||
|
relation: Model.ManyToManyRelation,
|
||||||
|
modelClass: this.relationBindKnex(Media.default),
|
||||||
|
join: {
|
||||||
|
from: 'manual_journals.id',
|
||||||
|
through: {
|
||||||
|
from: 'media_links.model_id',
|
||||||
|
to: 'media_links.media_id',
|
||||||
|
},
|
||||||
|
to: 'media.id',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
server/src/models/MediaLink.js
Normal file
10
server/src/models/MediaLink.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import TenantModel from '@/models/TenantModel';
|
||||||
|
|
||||||
|
export default class MediaLink extends TenantModel {
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'media_links';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,7 +180,7 @@ export default class JournalPoster {
|
|||||||
|
|
||||||
async deleteEntries() {
|
async deleteEntries() {
|
||||||
if (this.deletedEntriesIds.length > 0) {
|
if (this.deletedEntriesIds.length > 0) {
|
||||||
await AccountTransaction.query()
|
await AccountTransaction.tenant().query()
|
||||||
.whereIn('id', this.deletedEntriesIds)
|
.whereIn('id', this.deletedEntriesIds)
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ exports.up = function (knex) {
|
|||||||
table.boolean('active');
|
table.boolean('active');
|
||||||
table.integer('role_id').unique();
|
table.integer('role_id').unique();
|
||||||
table.string('language');
|
table.string('language');
|
||||||
table.date('last_login_at');
|
|
||||||
table.integer('tenant_id').unsigned();
|
table.integer('tenant_id').unsigned();
|
||||||
|
|
||||||
|
table.date('last_login_at');
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// knex.seed.run({
|
// knex.seed.run({
|
||||||
|
|||||||
Reference in New Issue
Block a user