feat: expand sidebar once open form editor page.

feat: rounding money amount.
feat: optimize page form structure.
feat: refactoring make journal and expense form with FastField component.
This commit is contained in:
Ahmed Bouhuolia
2020-11-29 00:06:59 +02:00
parent 53dd447540
commit 011542e2a3
118 changed files with 3883 additions and 2660 deletions

View File

@@ -8,6 +8,7 @@ const CLASSES = {
DATATABLE_EDITOR: 'datatable-editor',
DATATABLE_EDITOR_ACTIONS: 'datatable-editor__actions',
DATATABLE_EDITOR_ITEMS_ENTRIES: 'items-entries-table',
DATATABLE_EDITOR_HAS_TOTAL_ROW: 'has-total-row',
PAGE_FORM: 'page-form',
PAGE_FORM_HEADER: 'page-form__header',
@@ -16,6 +17,7 @@ const CLASSES = {
PAGE_FORM_HEADER_BIG_NUMBERS: 'page-form__big-numbers',
PAGE_FORM_TABS: 'page-form__tabs',
PAGE_FORM_BODY: 'page-form__body',
PAGE_FORM_STRIP_STYLE: 'page-form--strip',
PAGE_FORM_FOOTER: 'page-form__footer',
PAGE_FORM_FLOATING_ACTIONS: 'page-form__floating-actions',
@@ -29,6 +31,8 @@ const CLASSES = {
PAGE_FORM_CUSTOMER: 'page-form--customer',
PAGE_FORM_VENDOR: 'page-form--customer',
PAGE_FORM_ITEM: 'page-form--item',
PAGE_FORM_MAKE_JOURNAL: 'page-form--make-journal-entries',
PAGE_FORM_EXPENSE: 'page-form--expense',
FORM_GROUP_LIST_SELECT: 'form-group--select-list',

View File

@@ -16,7 +16,8 @@ export default function AccountsSelectList({
popoverFill = false,
filterByRootTypes = [],
filterByTypes = [],
filterByNormal
filterByNormal,
buttonProps = {}
}) {
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
@@ -113,6 +114,7 @@ export default function AccountsSelectList({
<Button
disabled={disabled}
text={selectedAccount ? selectedAccount.name : defaultSelectText}
{...buttonProps}
/>
</Select>
);

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import React, { useCallback, useState } from 'react';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { ListSelect } from 'components';
import { MenuItem } from '@blueprintjs/core';
@@ -8,6 +9,7 @@ export default function CurrencySelectList({
selectedCurrencyCode,
defaultSelectText = <T id={'select_currency_code'} />,
onCurrencySelected,
className,
...restProps
}) {
const [selectedCurrency, setSelectedCurrency] = useState(null);
@@ -54,6 +56,7 @@ export default function CurrencySelectList({
itemPredicate={filterCurrencies}
itemRenderer={currencyCodeRenderer}
popoverProps={{ minimal: true }}
className={classNames('form-group--select-list', className)}
{...restProps}
/>
);

View File

@@ -12,47 +12,39 @@ import PreferencesSidebar from 'components/Preferences/PreferencesSidebar';
import Search from 'containers/GeneralSearch/Search';
import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import { compose } from 'utils';
function Dashboard({
// #withSettings
requestFetchOptions,
}) {
const fetchOptions = useQuery(
['options'], () => requestFetchOptions(),
);
const fetchOptions = useQuery(['options'], () => requestFetchOptions());
return (
<DashboardLoadingIndicator isLoading={fetchOptions.isFetching}>
<Switch>
<Route path="/preferences">
<DashboardSplitPane>
<Sidebar />
<PreferencesSidebar />
</DashboardSplitPane>
<PreferencesContent />
</Route>
<DashboardLoadingIndicator isLoading={fetchOptions.isFetching}>
<Switch>
<Route path="/preferences">
<DashboardSplitPane>
<Sidebar />
<PreferencesSidebar />
</DashboardSplitPane>
<PreferencesContent />
</Route>
<Route path="/">
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
</Route>
</Switch>
<Route path="/">
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
</Route>
</Switch>
<Search />
<DialogsContainer />
</DashboardLoadingIndicator>
<Search />
<DialogsContainer />
</DashboardLoadingIndicator>
);
}
export default compose(
withSettingsActions,
)(Dashboard);
export default compose(withSettingsActions)(Dashboard);

View File

@@ -27,7 +27,7 @@ function DashboardSplitPane({
<SplitPane
allowResize={sidebarExpended}
split="vertical"
minSize={180}
minSize={190}
maxSize={300}
defaultSize={sidebarExpended ? defaultSize : 50}
size={sidebarExpended ? defaultSize : 50}

View File

@@ -29,6 +29,10 @@ function DashboardTopbar({
// #withDashboardActions
toggleSidebarExpend,
recordSidebarPreviousExpand,
// #withDashboard
sidebarExpended,
// #withGlobalSearch
openGlobalSearch,
@@ -41,13 +45,20 @@ function DashboardTopbar({
const handleSidebarToggleBtn = () => {
toggleSidebarExpend();
recordSidebarPreviousExpand();
};
return (
<div class="dashboard__topbar">
<div class="dashboard__topbar-left">
<div class="dashboard__topbar-sidebar-toggle">
<Tooltip
content={<T id={'close_sidebar'} />}
content={
!sidebarExpended ? (
<T id={'open_sidebar'} />
) : (
<T id={'close_sidebar'} />
)
}
position={Position.RIGHT}
>
<Button minimal={true} onClick={handleSidebarToggleBtn}>
@@ -147,10 +158,11 @@ function DashboardTopbar({
export default compose(
withSearch,
withDashboard(({ pageTitle, pageSubtitle, editViewId }) => ({
withDashboard(({ pageTitle, pageSubtitle, editViewId, sidebarExpended }) => ({
pageTitle,
pageSubtitle,
editViewId,
sidebarExpended
})),
withDashboardActions,
)(DashboardTopbar);

View File

@@ -1,19 +1,20 @@
import React, { useCallback, useState, useEffect } from 'react';
import { FormGroup, Intent } from '@blueprintjs/core';
import MoneyInputGroup from 'components/MoneyInputGroup';
import { MoneyInputGroup } from 'components';
import { CLASSES } from 'common/classes';
// Input form cell renderer.
const MoneyFieldCellRenderer = ({
row: { index },
row: { index, moneyInputGroupProps = {} },
column: { id },
cell: { value: initialValue },
payload: { errors, updateData },
}) => {
const [value, setValue] = useState(initialValue);
const handleFieldChange = useCallback((e, value) => {
const handleFieldChange = useCallback((value) => {
setValue(value);
}, []);
}, [setValue]);
function isNumeric(data) {
return (
@@ -21,7 +22,7 @@ const MoneyFieldCellRenderer = ({
);
}
const onBlur = () => {
const handleFieldBlur = () => {
const updateValue = isNumeric(value) ? parseFloat(value) : value;
updateData(index, id, updateValue);
};
@@ -34,15 +35,14 @@ const MoneyFieldCellRenderer = ({
return (
<FormGroup
intent={error ? Intent.DANGER : null}>
intent={error ? Intent.DANGER : null}
className={CLASSES.FILL}>
<MoneyInputGroup
value={value}
prefix={'$'}
onChange={handleFieldChange}
inputGroupProps={{
fill: true,
onBlur,
}}
onBlur={handleFieldBlur}
{...moneyInputGroupProps}
/>
</FormGroup>
);

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState, useEffect } from 'react';
import { FormGroup, Intent } from '@blueprintjs/core';
import MoneyInputGroup from 'components/MoneyInputGroup';
import { MoneyInputGroup } from 'components';
const PercentFieldCell = ({
cell: { value: initialValue },
@@ -10,14 +10,15 @@ const PercentFieldCell = ({
}) => {
const [value, setValue] = useState(initialValue);
const onBlur = (e) => {
updateData(index, id, parseInt(e.target.value, 10));
const handleBlurChange = (newValue) => {
const parsedValue = newValue === '' || newValue === undefined
? '' : parseInt(newValue, 10);
updateData(index, id, parsedValue);
};
const onChange = useCallback((e) => {
setValue(e.target.value);
}, []);
const handleChange = useCallback((value) => {
setValue(value);
}, [setValue]);
useEffect(() => {
setValue(initialValue);
@@ -29,12 +30,8 @@ const PercentFieldCell = ({
<FormGroup intent={error ? Intent.DANGER : null}>
<MoneyInputGroup
value={value}
suffix={'%'}
onChange={onChange}
inputGroupProps={{
fill: true,
onBlur,
}}
onChange={handleChange}
onBlurValue={handleBlurChange}
/>
</FormGroup>
);

View File

@@ -5,6 +5,7 @@ import ContactsListFieldCell from './ContactsListFieldCell';
import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell';
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
export {
AccountsListFieldCell,
MoneyFieldCell,

View File

@@ -3,7 +3,7 @@ import {
Checkbox as BPCheckbox,
} from '@blueprintjs/core';
export default function CheckboxComponent(props) {
export default function CheckboxComponent(props: any) {
const { field, form, ...rest } = props;
const [value, setValue] = useState(field.value || false);

View File

@@ -0,0 +1,120 @@
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
export type Separator = ',' | '.';
export type CurrencyInputProps = Overwrite<
React.InputHTMLAttributes<HTMLInputElement>,
{
/**
* Allow decimals
*
* Default = true
*/
allowDecimals?: boolean;
/**
* Allow user to enter negative value
*
* Default = true
*/
allowNegativeValue?: boolean;
/**
* Component id
*/
id?: string;
/**
* Maximum characters the user can enter
*/
maxLength?: number;
/**
* Class names
*/
className?: string;
/**
* Limit length of decimals allowed
*
* Default = 2
*/
decimalsLimit?: number;
/**
* Default value
*/
defaultValue?: number | string;
/**
* Disabled
*
* Default = false
*/
disabled?: boolean;
/**
* Value will always have the specified length of decimals
*/
fixedDecimalLength?: number;
/**
* Handle change in value
*/
onChange?: (value: string | undefined, name?: string) => void;
/**
* Handle value onBlur
*
*/
onBlurValue?: (value: string | undefined, name?: string) => void;
/**
* Placeholder
*/
placeholder?: string;
/**
* Specify decimal precision for padding/trimming
*/
precision?: number;
/**
* Include a prefix eg. £
*/
prefix?: string;
/**
* Incremental value change on arrow down and arrow up key press
*/
step?: number;
/**
* Separator between integer part and fractional part of value. Cannot be a number
*
* Default = "."
*/
decimalSeparator?: string;
/**
* Separator between thousand, million and billion. Cannot be a number
*
* Default = ","
*/
groupSeparator?: string;
/**
* Disable auto adding separator between values eg. 1000 > 1,000
*
* Default = false
*/
turnOffSeparators?: boolean;
/**
* Disable abbreviations eg. 1k > 1,000, 2m > 2,000,000
*
* Default = false
*/
turnOffAbbreviations?: boolean;
}
>;

View File

@@ -0,0 +1,190 @@
import React, { FC, useState, useEffect, useRef } from 'react';
import { InputGroup } from '@blueprintjs/core';
import { CurrencyInputProps } from './CurrencyInputProps';
import {
isNumber,
cleanValue,
fixedDecimalValue,
formatValue,
padTrimValue,
CleanValueOptions,
} from './utils';
export const CurrencyInput: FC<CurrencyInputProps> = ({
allowDecimals = true,
allowNegativeValue = true,
id,
name,
className,
decimalsLimit,
defaultValue,
disabled = false,
maxLength: userMaxLength,
value: userValue,
onChange,
onBlurValue,
fixedDecimalLength,
placeholder,
precision,
prefix,
step,
decimalSeparator = '.',
groupSeparator = ',',
turnOffSeparators = false,
turnOffAbbreviations = false,
...props
}: CurrencyInputProps) => {
if (decimalSeparator === groupSeparator) {
throw new Error('decimalSeparator cannot be the same as groupSeparator');
}
if (isNumber(decimalSeparator)) {
throw new Error('decimalSeparator cannot be a number');
}
if (isNumber(groupSeparator)) {
throw new Error('groupSeparator cannot be a number');
}
const formatValueOptions = {
decimalSeparator,
groupSeparator,
turnOffSeparators,
prefix,
};
const cleanValueOptions: Partial<CleanValueOptions> = {
decimalSeparator,
groupSeparator,
allowDecimals,
decimalsLimit: decimalsLimit || fixedDecimalLength || 2,
allowNegativeValue,
turnOffAbbreviations,
prefix,
};
const _defaultValue =
defaultValue !== undefined
? formatValue({ value: String(defaultValue), ...formatValueOptions })
: '';
const [stateValue, setStateValue] = useState(_defaultValue);
const [cursor, setCursor] = useState(0);
const inputRef = useRef<HTMLInputElement | null>(null);
const onFocus = (): number => (stateValue ? stateValue.length : 0);
const processChange = (value: string, selectionStart?: number | null): void => {
const valueOnly = cleanValue({ value, ...cleanValueOptions });
if (!valueOnly) {
onChange && onChange(undefined, name);
setStateValue('');
return;
}
if (userMaxLength && valueOnly.replace(/-/g, '').length > userMaxLength) {
return;
}
if (valueOnly === '-') {
onChange && onChange(undefined, name);
setStateValue(value);
return;
}
const formattedValue = formatValue({ value: valueOnly, ...formatValueOptions });
/* istanbul ignore next */
if (selectionStart !== undefined && selectionStart !== null) {
const cursor = selectionStart + (formattedValue.length - value.length) || 1;
setCursor(cursor);
}
setStateValue(formattedValue);
onChange && onChange(valueOnly, name);
};
const handleOnChange = ({
target: { value, selectionStart },
}: React.ChangeEvent<HTMLInputElement>): void => {
processChange(value, selectionStart);
};
const handleOnBlur = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>): void => {
const valueOnly = cleanValue({ value, ...cleanValueOptions });
if (valueOnly === '-' || !valueOnly) {
onBlurValue && onBlurValue(undefined, name);
setStateValue('');
return;
}
const fixedDecimals = fixedDecimalValue(valueOnly, decimalSeparator, fixedDecimalLength);
// Add padding or trim value to precision
const newValue = padTrimValue(fixedDecimals, decimalSeparator, precision || fixedDecimalLength);
onChange && onChange(newValue, name);
onBlurValue && onBlurValue(newValue, name);
const formattedValue = formatValue({ value: newValue, ...formatValueOptions });
setStateValue(formattedValue);
};
const handleOnKeyDown = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
if (step && (key === 'ArrowUp' || key === 'ArrowDown')) {
const currentValue =
Number(
userValue !== undefined
? userValue
: cleanValue({ value: stateValue, ...cleanValueOptions })
) || 0;
const newValue =
key === 'ArrowUp'
? String(currentValue + Number(step))
: String(currentValue - Number(step));
processChange(newValue);
}
};
/* istanbul ignore next */
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.setSelectionRange(cursor, cursor);
}
}, [cursor, inputRef]);
const formattedPropsValue =
userValue !== undefined
? formatValue({ value: String(userValue), ...formatValueOptions })
: undefined;
const handleInputRef = (ref: HTMLInputElement | null) => {
inputRef.current = ref;
return null;
};
return (
<InputGroup
type="text"
inputMode="decimal"
id={id}
name={name}
className={className}
onChange={handleOnChange}
onBlur={handleOnBlur}
onFocus={onFocus}
onKeyDown={handleOnKeyDown}
placeholder={placeholder}
disabled={disabled}
value={
formattedPropsValue !== undefined && stateValue !== '-' ? formattedPropsValue : stateValue
}
inputRef={handleInputRef}
{...props}
/>
);
};
export default CurrencyInput;

View File

@@ -0,0 +1,11 @@
import { addSeparators } from '../addSeparators';
describe('Separators', () => {
it('should add separator in string', () => {
expect(addSeparators('1000000')).toEqual('1,000,000');
});
it('should use custom separator when provided', () => {
expect(addSeparators('1000000', '.')).toEqual('1.000.000');
});
});

View File

@@ -0,0 +1,220 @@
import { cleanValue } from '../cleanValue';
describe('cleanValue', () => {
it('should remove group separator in string', () => {
expect(
cleanValue({
value: '1,000,000',
})
).toEqual('1000000');
});
it('should handle period decimal separator in string', () => {
expect(
cleanValue({
value: '1.000.000,12',
decimalSeparator: ',',
groupSeparator: '.',
})
).toEqual('1000000,12');
});
it('should remove prefix', () => {
expect(
cleanValue({
value: '£1000000',
prefix: '£',
})
).toEqual('1000000');
expect(
cleanValue({
value: '$5.5',
prefix: '$',
})
).toEqual('5.5');
});
it('should remove extra decimals', () => {
expect(
cleanValue({
value: '100.0000',
})
).toEqual('100.00');
});
it('should remove decimals if not allowed', () => {
expect(
cleanValue({
value: '100.0000',
allowDecimals: false,
decimalsLimit: 0,
})
).toEqual('100');
});
it('should include decimals if allowed', () => {
expect(
cleanValue({
value: '100.123',
allowDecimals: true,
decimalsLimit: 0,
})
).toEqual('100.123');
});
it('should format value', () => {
expect(
cleanValue({
value: '£1,234,567.89',
prefix: '£',
})
).toEqual('1234567.89');
});
describe('negative values', () => {
it('should handle negative value', () => {
expect(
cleanValue({
value: '-£1,000',
decimalSeparator: '.',
groupSeparator: ',',
allowDecimals: true,
decimalsLimit: 2,
prefix: '£',
})
).toEqual('-1000');
});
it('should handle negative value with decimal', () => {
expect(
cleanValue({
value: '-£99,999.99',
decimalSeparator: '.',
groupSeparator: ',',
allowDecimals: true,
decimalsLimit: 2,
prefix: '£',
})
).toEqual('-99999.99');
});
it('should handle not allow negative value if allowNegativeValue is false', () => {
expect(
cleanValue({
value: '-£1,000',
decimalSeparator: '.',
groupSeparator: ',',
allowDecimals: true,
decimalsLimit: 2,
allowNegativeValue: false,
prefix: '£',
})
).toEqual('1000');
});
});
it('should handle values placed before prefix', () => {
expect(
cleanValue({
value: '2£1',
prefix: '£',
})
).toEqual('12');
expect(
cleanValue({
value: '-2£1',
prefix: '£',
})
).toEqual('-12');
expect(
cleanValue({
value: '2-£1',
prefix: '£',
})
).toEqual('-12');
expect(
cleanValue({
value: '2-£1.99',
prefix: '£',
decimalsLimit: 5,
})
).toEqual('-1.992');
});
describe('abbreviations', () => {
it('should return empty string if abbreviation only', () => {
expect(
cleanValue({
value: 'k',
turnOffAbbreviations: true,
})
).toEqual('');
expect(
cleanValue({
value: 'm',
turnOffAbbreviations: true,
})
).toEqual('');
expect(
cleanValue({
value: 'b',
turnOffAbbreviations: true,
})
).toEqual('');
});
it('should return empty string if prefix and abbreviation only', () => {
expect(
cleanValue({
value: '$k',
prefix: '$',
turnOffAbbreviations: true,
})
).toEqual('');
expect(
cleanValue({
value: '£m',
prefix: '£',
turnOffAbbreviations: true,
})
).toEqual('');
});
it('should ignore abbreviations if turnOffAbbreviations is true', () => {
expect(
cleanValue({
value: '1k',
turnOffAbbreviations: true,
})
).toEqual('1');
expect(
cleanValue({
value: '-2k',
turnOffAbbreviations: true,
})
).toEqual('-2');
expect(
cleanValue({
value: '25.6m',
turnOffAbbreviations: true,
})
).toEqual('25.6');
expect(
cleanValue({
value: '9b',
turnOffAbbreviations: true,
})
).toEqual('9');
});
});
});

View File

@@ -0,0 +1,29 @@
import { fixedDecimalValue } from '../fixedDecimalValue';
describe('fixedDecimalValue', () => {
it('should return original value if no match', () => {
expect(fixedDecimalValue('abc', '.', 2)).toEqual('abc');
});
it('should work with 2 fixed decimal length', () => {
expect(fixedDecimalValue('1', '.', 2)).toEqual('1');
expect(fixedDecimalValue('12', '.', 2)).toEqual('1.2');
expect(fixedDecimalValue('123', '.', 2)).toEqual('1.23');
expect(fixedDecimalValue('12345', '.', 2)).toEqual('123.45');
expect(fixedDecimalValue('123.4567', '.', 2)).toEqual('123.45');
});
it('should work with 4 fixed decimal length', () => {
expect(fixedDecimalValue('12', ',', 4)).toEqual('1,2');
expect(fixedDecimalValue('123', ',', 4)).toEqual('1,23');
expect(fixedDecimalValue('1234', ',', 4)).toEqual('1,234');
expect(fixedDecimalValue('12345', ',', 4)).toEqual('1,2345');
});
it('should trim decimals if too long', () => {
expect(fixedDecimalValue('1.23', '.', 2)).toEqual('1.23');
expect(fixedDecimalValue('1.2345', '.', 2)).toEqual('1.23');
expect(fixedDecimalValue('1,2345678', ',', 3)).toEqual('1,234');
expect(fixedDecimalValue('123,45678', ',', 3)).toEqual('123,456');
});
});

View File

@@ -0,0 +1,172 @@
import { formatValue } from '../formatValue';
describe('formatValue', () => {
it('should return empty if blank value', () => {
expect(
formatValue({
value: '',
})
).toEqual('');
});
it('should add separator', () => {
expect(
formatValue({
value: '1234567',
})
).toEqual('1,234,567');
});
it('should handle period separator', () => {
expect(
formatValue({
value: '1234567',
decimalSeparator: '.',
groupSeparator: '.',
})
).toEqual('1.234.567');
});
it('should handle comma separator for decimals', () => {
expect(
formatValue({
value: '1234567,89',
decimalSeparator: '.',
groupSeparator: '.',
})
).toEqual('1.234.567,89');
});
it('should handle - as separator for decimals', () => {
expect(
formatValue({
value: '1234567-89',
decimalSeparator: '-',
groupSeparator: '.',
})
).toEqual('1.234.567-89');
});
it('should handle empty decimal separator', () => {
expect(
formatValue({
value: '1234567-89',
decimalSeparator: '',
groupSeparator: '.',
})
).toEqual('1.234.567-89');
});
it('should NOT add separator if "turnOffSeparators" is true', () => {
expect(
formatValue({
value: '1234567',
turnOffSeparators: true,
})
).toEqual('1234567');
});
it('should NOT add separator if "turnOffSeparators" is true even if decimal and group separators specified', () => {
expect(
formatValue({
value: '1234567',
decimalSeparator: '.',
groupSeparator: ',',
turnOffSeparators: true,
})
).toEqual('1234567');
});
it('should add prefix', () => {
expect(
formatValue({
value: '123',
prefix: '£',
})
).toEqual('£123');
});
it('should include "."', () => {
expect(
formatValue({
value: '1234567.',
})
).toEqual('1,234,567.');
});
it('should include decimals', () => {
expect(
formatValue({
value: '1234.567',
})
).toEqual('1,234.567');
});
it('should format value', () => {
expect(
formatValue({
value: '1234567.89',
prefix: '£',
})
).toEqual('£1,234,567.89');
});
it('should handle 0 value', () => {
expect(
formatValue({
value: '0',
prefix: '£',
})
).toEqual('£0');
});
describe('negative values', () => {
it('should handle negative values', () => {
expect(
formatValue({
value: '-1234',
prefix: '£',
})
).toEqual('-£1,234');
});
it('should return negative sign if only negative sign', () => {
expect(
formatValue({
value: '-',
prefix: '£',
})
).toEqual('-');
});
});
it('should handle negative value and "-" as groupSeparator', () => {
expect(
formatValue({
value: '-1234',
groupSeparator: '-',
prefix: '£',
})
).toEqual('-£1-234');
});
it('should handle negative value and "-" as decimalSeparator', () => {
expect(
formatValue({
value: '-12-34',
decimalSeparator: '-',
prefix: '£',
})
).toEqual('-£12-34');
});
it('should handle negative value and "-" as groupSeparator', () => {
expect(
formatValue({
value: '-123456',
groupSeparator: '-',
prefix: '£',
})
).toEqual('-£123-456');
});
});

View File

@@ -0,0 +1,43 @@
import { isNumber } from '../isNumber';
describe('isNumber', () => {
it('should return true for 0', () => {
expect(isNumber('0')).toBe(true);
});
it('should return true for -3', () => {
expect(isNumber('-3')).toBe(true);
});
it('should return true for 9', () => {
expect(isNumber('9')).toBe(true);
});
it('should return true for abc1', () => {
expect(isNumber('abc1')).toBe(true);
});
it('should return true for a.1', () => {
expect(isNumber('a.1')).toBe(true);
});
it('should return false for space', () => {
expect(isNumber(' ')).toBe(false);
});
it('should return false for comma', () => {
expect(isNumber(',')).toBe(false);
});
it('should return false for period', () => {
expect(isNumber('.')).toBe(false);
});
it('should return false for -', () => {
expect(isNumber('-')).toBe(false);
});
it('should return false for +', () => {
expect(isNumber('+')).toBe(false);
});
});

View File

@@ -0,0 +1,38 @@
import { padTrimValue } from '../padTrimValue';
describe('padTrimValue', () => {
it('should return original value if no precision', () => {
const value = padTrimValue('1000000');
expect(value).toEqual('1000000');
});
it('should return blank value if no value', () => {
const value = padTrimValue('', '.', 2);
expect(value).toEqual('');
});
it('should return blank value if no only negative', () => {
const value = padTrimValue('-', '.', 2);
expect(value).toEqual('');
});
it('should pad with 0 if no decimals', () => {
const value = padTrimValue('99', '.', 3);
expect(value).toEqual('99.000');
});
it('should pad with 0 if decimal length is less than precision', () => {
const value = padTrimValue('10.5', '.', 5);
expect(value).toEqual('10.50000');
});
it('should trim if decimal length is larger than precision', () => {
const value = padTrimValue('10.599', '.', 2);
expect(value).toEqual('10.59');
});
it('should trim handle comma as decimal separator', () => {
const value = padTrimValue('9,9', ',', 3);
expect(value).toEqual('9,900');
});
});

View File

@@ -0,0 +1,77 @@
import { abbrValue, parseAbbrValue } from '../parseAbbrValue';
describe('abbrValue', () => {
it('should not convert value under 1000', () => {
expect(abbrValue(999)).toEqual('999');
});
it('should convert thousand to k', () => {
expect(abbrValue(1000)).toEqual('1k');
expect(abbrValue(1500)).toEqual('1.5k');
expect(abbrValue(10000)).toEqual('10k');
});
it('should work with comma as decimal separator', () => {
expect(abbrValue(1500, ',')).toEqual('1,5k');
});
it('should work with decimal places option', () => {
expect(abbrValue(123456, '.')).toEqual('0.123456M');
expect(abbrValue(123456, '.', 2)).toEqual('0.12M');
});
});
describe('parseAbbrValue', () => {
it('should return undefined if cannot parse', () => {
expect(parseAbbrValue('1km')).toEqual(undefined);
expect(parseAbbrValue('2mb')).toEqual(undefined);
expect(parseAbbrValue('3a')).toEqual(undefined);
});
it('should return undefined if no abbreviation', () => {
expect(parseAbbrValue('1.23')).toEqual(undefined);
expect(parseAbbrValue('100')).toEqual(undefined);
expect(parseAbbrValue('20000')).toEqual(undefined);
});
it('should return undefined for only letter', () => {
expect(parseAbbrValue('k')).toBeUndefined();
expect(parseAbbrValue('m')).toBeUndefined();
expect(parseAbbrValue('b')).toBeUndefined();
});
it('should return 0 for 0', () => {
expect(parseAbbrValue('0k')).toEqual(0);
expect(parseAbbrValue('0m')).toEqual(0);
expect(parseAbbrValue('0b')).toEqual(0);
});
it('should parse k', () => {
expect(parseAbbrValue('1k')).toEqual(1000);
expect(parseAbbrValue('2K')).toEqual(2000);
expect(parseAbbrValue('1.1239999k')).toEqual(1123.9999);
expect(parseAbbrValue('1.5k')).toEqual(1500);
expect(parseAbbrValue('50.12K')).toEqual(50120);
expect(parseAbbrValue('100K')).toEqual(100000);
});
it('should parse m', () => {
expect(parseAbbrValue('1m')).toEqual(1000000);
expect(parseAbbrValue('1.5m')).toEqual(1500000);
expect(parseAbbrValue('45.123456m')).toEqual(45123456);
expect(parseAbbrValue('83.5m')).toEqual(83500000);
expect(parseAbbrValue('100M')).toEqual(100000000);
});
it('should parse b', () => {
expect(parseAbbrValue('1b')).toEqual(1000000000);
expect(parseAbbrValue('1.5b')).toEqual(1500000000);
expect(parseAbbrValue('65.5513b')).toEqual(65551300000);
expect(parseAbbrValue('100B')).toEqual(100000000000);
});
it('should work with comma as decimal separator', () => {
expect(parseAbbrValue('1,2k', ',')).toEqual(1200);
expect(parseAbbrValue('2,3m', ',')).toEqual(2300000);
});
});

View File

@@ -0,0 +1,23 @@
import { removeInvalidChars } from '../removeInvalidChars';
describe('removeInvalidChars', () => {
it('should remove letters in string', () => {
expect(removeInvalidChars('1,000ab,0cd00.99', [',', '.'])).toEqual('1,000,000.99');
});
it('should remove special characters in string', () => {
expect(removeInvalidChars('1.00ji0.0*&0^0', ['.'])).toEqual('1.000.000');
});
it('should keep abbreviations', () => {
expect(removeInvalidChars('9k', ['k'])).toEqual('9k');
expect(removeInvalidChars('1m', ['m'])).toEqual('1m');
expect(removeInvalidChars('5b', ['b'])).toEqual('5b');
});
it('should keep abbreviations (case insensitive)', () => {
expect(removeInvalidChars('9K', ['k'])).toEqual('9K');
expect(removeInvalidChars('1M', ['m'])).toEqual('1M');
expect(removeInvalidChars('5B', ['b'])).toEqual('5B');
});
});

View File

@@ -0,0 +1,11 @@
import { removeSeparators } from '../removeSeparators';
describe('removeSeparators', () => {
it('should remove separators in string', () => {
expect(removeSeparators('1,000,000')).toEqual('1000000');
});
it('should use custom separator when provided', () => {
expect(removeSeparators('1.000.000', '.')).toEqual('1000000');
});
});

View File

@@ -0,0 +1,6 @@
/**
* Add group separator to value eg. 1000 > 1,000
*/
export const addSeparators = (value: string, separator = ','): string => {
return value.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
};

View File

@@ -0,0 +1,66 @@
import { parseAbbrValue } from './parseAbbrValue';
import { removeSeparators } from './removeSeparators';
import { removeInvalidChars } from './removeInvalidChars';
import { escapeRegExp } from './escapeRegExp';
export type CleanValueOptions = {
value: string;
decimalSeparator?: string;
groupSeparator?: string;
allowDecimals?: boolean;
decimalsLimit?: number;
allowNegativeValue?: boolean;
turnOffAbbreviations?: boolean;
prefix?: string;
};
/**
* Remove prefix, separators and extra decimals from value
*/
export const cleanValue = ({
value,
groupSeparator = ',',
decimalSeparator = '.',
allowDecimals = true,
decimalsLimit = 2,
allowNegativeValue = true,
turnOffAbbreviations = false,
prefix = '',
}: CleanValueOptions): string => {
const abbreviations = turnOffAbbreviations ? [] : ['k', 'm', 'b'];
const isNegative = value.includes('-');
const [prefixWithValue, preValue] = RegExp(`(\\d+)-?${escapeRegExp(prefix)}`).exec(value) || [];
const withoutPrefix = prefix ? value.replace(prefixWithValue, '').concat(preValue) : value;
const withoutSeparators = removeSeparators(withoutPrefix, groupSeparator);
const withoutInvalidChars = removeInvalidChars(withoutSeparators, [
groupSeparator,
decimalSeparator,
...abbreviations,
]);
let valueOnly = withoutInvalidChars;
if (!turnOffAbbreviations) {
// disallow letter without number
if (abbreviations.some((letter) => letter === withoutInvalidChars.toLowerCase())) {
return '';
}
const parsed = parseAbbrValue(withoutInvalidChars, decimalSeparator);
if (parsed) {
valueOnly = String(parsed);
}
}
const includeNegative = isNegative && allowNegativeValue ? '-' : '';
if (String(valueOnly).includes(decimalSeparator)) {
const [int, decimals] = withoutInvalidChars.split(decimalSeparator);
const trimmedDecimals = decimalsLimit ? decimals.slice(0, decimalsLimit) : decimals;
const includeDecimals = allowDecimals ? `${decimalSeparator}${trimmedDecimals}` : '';
return `${includeNegative}${int}${includeDecimals}`;
}
return `${includeNegative}${valueOnly}`;
};

View File

@@ -0,0 +1,8 @@
/**
* Escape regex char
*
* See: https://stackoverflow.com/questions/17885855/use-dynamic-variable-string-as-regex-pattern-in-javascript
*/
export const escapeRegExp = (stringToGoIntoTheRegex: string): string => {
return stringToGoIntoTheRegex.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};

View File

@@ -0,0 +1,27 @@
export const fixedDecimalValue = (
value: string,
decimalSeparator: string,
fixedDecimalLength?: number
): string => {
if (fixedDecimalLength && value.length > 1) {
if (value.includes(decimalSeparator)) {
const [int, decimals] = value.split(decimalSeparator);
if (decimals.length > fixedDecimalLength) {
return `${int}${decimalSeparator}${decimals.slice(0, fixedDecimalLength)}`;
}
}
const reg =
value.length > fixedDecimalLength
? new RegExp(`(\\d+)(\\d{${fixedDecimalLength}})`)
: new RegExp(`(\\d)(\\d+)`);
const match = value.match(reg);
if (match) {
const [, int, decimals] = match;
return `${int}${decimalSeparator}${decimals}`;
}
}
return value;
};

View File

@@ -0,0 +1,78 @@
import { addSeparators } from './addSeparators';
type Props = {
/**
* Value to format
*/
value: number | string | undefined;
/**
* Decimal separator
*
* Default = '.'
*/
decimalSeparator?: string;
/**
* Group separator
*
* Default = ','
*/
groupSeparator?: string;
/**
* Turn off separators
*
* This will override Group separators
*
* Default = false
*/
turnOffSeparators?: boolean;
/**
* Prefix
*/
prefix?: string;
};
/**
* Format value with decimal separator, group separator and prefix
*/
export const formatValue = (props: Props): string => {
const {
value: _value,
groupSeparator = ',',
decimalSeparator = '.',
turnOffSeparators = false,
prefix,
} = props;
if (_value === '' || _value === undefined) {
return '';
}
const value = String(_value);
if (value === '-') {
return '-';
}
const isNegative = RegExp('^-\\d+').test(value);
const hasDecimalSeparator = decimalSeparator && value.includes(decimalSeparator);
const valueOnly = isNegative ? value.replace('-', '') : value;
const [int, decimals] = hasDecimalSeparator ? valueOnly.split(decimalSeparator) : [valueOnly];
const formattedInt = turnOffSeparators ? int : addSeparators(int, groupSeparator);
const includePrefix = prefix ? prefix : '';
const includeNegative = isNegative ? '-' : '';
const includeDecimals =
hasDecimalSeparator && decimals
? `${decimalSeparator}${decimals}`
: hasDecimalSeparator
? `${decimalSeparator}`
: '';
return `${includeNegative}${includePrefix}${formattedInt}${includeDecimals}`;
};

View File

@@ -0,0 +1,5 @@
export * from './cleanValue';
export * from './fixedDecimalValue';
export * from './formatValue';
export * from './isNumber';
export * from './padTrimValue';

View File

@@ -0,0 +1 @@
export const isNumber = (input: string): boolean => RegExp(/\d/, 'gi').test(input);

View File

@@ -0,0 +1,22 @@
export const padTrimValue = (value: string, decimalSeparator = '.', precision?: number): string => {
if (!precision || value === '' || value === undefined) {
return value;
}
if (!value.match(/\d/g)) {
return '';
}
const [int, decimals] = value.split(decimalSeparator);
let newValue = decimals || '';
if (newValue.length < precision) {
while (newValue.length < precision) {
newValue += '0';
}
} else {
newValue = newValue.slice(0, precision);
}
return `${int}${decimalSeparator}${newValue}`;
};

View File

@@ -0,0 +1,42 @@
import { escapeRegExp } from './escapeRegExp';
/**
* Abbreviate number eg. 1000 = 1k
*
* Source: https://stackoverflow.com/a/9345181
*/
export const abbrValue = (value: number, decimalSeparator = '.', _decimalPlaces = 10): string => {
if (value > 999) {
let valueLength = ('' + value).length;
const p = Math.pow;
const d = p(10, _decimalPlaces);
valueLength -= valueLength % 3;
const abbrValue = Math.round((value * d) / p(10, valueLength)) / d + ' kMGTPE'[valueLength / 3];
return abbrValue.replace('.', decimalSeparator);
}
return String(value);
};
type AbbrMap = { [key: string]: number };
const abbrMap: AbbrMap = { k: 1000, m: 1000000, b: 1000000000 };
/**
* Parse a value with abbreviation e.g 1k = 1000
*/
export const parseAbbrValue = (value: string, decimalSeparator = '.'): number | undefined => {
const reg = new RegExp(`(\\d+(${escapeRegExp(decimalSeparator)}\\d+)?)([kmb])$`, 'i');
const match = value.match(reg);
if (match) {
const [, digits, , abbr] = match;
const multiplier = abbrMap[abbr.toLowerCase()];
if (digits && multiplier) {
return Number(digits.replace(decimalSeparator, '.')) * multiplier;
}
}
return undefined;
};

View File

@@ -0,0 +1,10 @@
import { escapeRegExp } from './escapeRegExp';
/**
* Remove invalid characters
*/
export const removeInvalidChars = (value: string, validChars: ReadonlyArray<string>): string => {
const chars = escapeRegExp(validChars.join(''));
const reg = new RegExp(`[^\\d${chars}]`, 'gi');
return value.replace(reg, '');
};

View File

@@ -0,0 +1,9 @@
import { escapeRegExp } from './escapeRegExp';
/**
* Remove group separator from value eg. 1,000 > 1000
*/
export const removeSeparators = (value: string, separator = ','): string => {
const reg = new RegExp(escapeRegExp(separator), 'g');
return value.replace(reg, '');
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import {formattedAmount} from 'utils';
import { formattedAmount } from 'utils';
export default function Money({ amount, currency }) {
return (

View File

@@ -1,109 +0,0 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { InputGroup } from '@blueprintjs/core';
const joinIntegerAndDecimal = (integer, decimal, separator) => {
let output = `${integer}`;
if (separator) {
output += separator;
output += decimal ? decimal : '';
}
return output;
};
const hasSeparator = (input, separator) => {
return -1 !== input.indexOf(separator);
};
const addThousandSeparator = (integer, separator) => {
return integer.replace(/(\d)(?=(?:\d{3})+\b)/gm, `$1${separator}`);
};
const toString = (number) => `${number}`;
const onlyNumbers = (input) => {
return toString(input).replace(/\D+/g, '') || '0';
};
const formatter = (value, options) => {
const input = toString(value);
const navigate = input.indexOf('-') >= 0 ? '-' : '';
const parts = toString(input).split(options.decimal);
const integer = parseInt(onlyNumbers(parts[0]), 10);
const decimal = parts[1] ? onlyNumbers(parts[1]) : null;
const integerThousand = addThousandSeparator(
toString(integer),
options.thousands,
);
const separator = hasSeparator(input, options.decimal)
? options.decimal
: false;
return `${navigate}${options.prefix}${joinIntegerAndDecimal(
integerThousand,
decimal,
separator,
)}${options.suffix}`;
};
const unformatter = (input, options) => {
const navigate = input.indexOf('-') >= 0 ? '-' : '';
const parts = toString(input).split(options.decimal);
const integer = parseInt(onlyNumbers(parts[0]), 10);
const decimal = parts[1] ? onlyNumbers(parts[1]) : null;
const separator = hasSeparator(input, options.decimal)
? options.decimal
: false;
return `${navigate}${joinIntegerAndDecimal(integer, decimal, separator)}`;
};
export default function MoneyFieldGroup({
value,
prefix = '',
suffix = '',
thousands = ',',
decimal = '.',
precision = 2,
inputGroupProps,
onChange,
disabled = false,
}) {
const [state, setState] = useState(value);
const options = useMemo(
() => ({
prefix,
suffix,
thousands,
decimal,
precision,
}),
[prefix, suffix, thousands, decimal, precision],
);
const handleChange = useCallback(
(event) => {
const formatted = formatter(event.target.value, options);
const value = unformatter(event.target.value, options);
setState(formatted);
onChange && onChange(event, value);
},
[onChange, options],
);
useEffect(() => {
const formatted = formatter(value, options);
setState(formatted);
}, [value, options, setState]);
return (
<InputGroup
value={state}
onChange={handleChange}
{...inputGroupProps}
disabled={disabled}
/>
);
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
Money,
} from 'components';
export default function PageFormBigNumber({ label, amount, currencyCode }) {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
<div class="big-amount">
<span class="big-amount__label">{ label }</span>
<h1 class="big-amount__number">
<Money amount={0} currency={'LYD'} />
</h1>
</div>
</div>
);
}

View File

@@ -42,7 +42,9 @@ export default function SidebarMenu() {
) : null;
return item.spacer ? (
<div class="bp3-menu-spacer"></div>
<div class="bp3-menu-spacer" style={{
'height': `${item.spacer}px`,
}}></div>
) : item.divider ? (
<MenuDivider key={index} title={item.title} />
) : item.label ? (

View File

@@ -35,11 +35,12 @@ import ContactSelecetList from './ContactSelecetList';
import CurrencySelectList from './CurrencySelectList'
import SalutationList from './SalutationList';
import DisplayNameList from './DisplayNameList';
import MoneyInputGroup from './MoneyInputGroup';
import MoneyInputGroup from './Forms/MoneyInputGroup';
import Dragzone from './Dragzone';
import EmptyStatus from './EmptyStatus';
import DashboardCard from './Dashboard/DashboardCard';
import InputPrependText from './Forms/InputPrependText';
import PageFormBigNumber from './PageFormBigNumber';
const Hint = FieldHint;
@@ -86,4 +87,5 @@ export {
EmptyStatus,
DashboardCard,
InputPrependText,
PageFormBigNumber
};

View File

@@ -3,7 +3,7 @@ import { FormattedMessage as T } from 'react-intl';
export default [
{
divider: true,
spacer: 1,
},
{
text: <T id={'homepage'} />,
@@ -11,7 +11,7 @@ export default [
href: '/homepage',
},
{
divider: true,
spacer: 1,
},
{
text: 'Sales & inventory',

View File

@@ -0,0 +1,39 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
const Schema = Yup.object().shape({
journal_number: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_number_' })),
journal_type: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_type' })),
date: Yup.date()
.required()
.label(formatMessage({ id: 'date' })),
currency_code: Yup.string(),
reference: Yup.string().min(1).max(255),
description: Yup.string().min(1).max(1024),
entries: Yup.array().of(
Yup.object().shape({
credit: Yup.number().nullable(),
debit: Yup.number().nullable(),
account_id: Yup.number()
.nullable()
.when(['credit', 'debit'], {
is: (credit, debit) => credit || debit,
then: Yup.number().required(),
}),
contact_id: Yup.number().nullable(),
contact_type: Yup.string().nullable(),
note: Yup.string().max(255).nullable(),
}),
),
});
export const CreateJournalSchema = Schema;
export const EditJournalSchema = Schema;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { FastField } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
import { orderingLinesIndexes, repeatValue } from 'utils';
export default function MakeJournalEntriesField({
defaultRow,
linesNumber = 4,
}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<FastField name={'entries'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<MakeJournalEntriesTable
onChange={(entries) => {
form.setFieldValue('entries', entries);
}}
entries={value}
error={error}
onClickAddNewRow={() => {
form.setFieldValue('entries', [...value, defaultRow]);
}}
onClickClearAllLines={() => {
form.setFieldValue(
'entries',
orderingLinesIndexes([...repeatValue(defaultRow, linesNumber)])
);
}}
/>
)}
</FastField>
</div>
);
}

View File

@@ -1,58 +0,0 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
export default function MakeJournalEntriesFooter({
isSubmitting,
onSubmitClick,
onCancelClick,
manualJournalId,
}) {
return (
<div>
<div class="form__floating-footer">
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
name={'save'}
onClick={() => {
onSubmitClick({ publish: true, redirect: true });
}}
>
{manualJournalId ? <T id={'edit'} /> : <T id={'save'} />}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
onClick={() => {
onSubmitClick({ publish: true, redirect: false });
}}
>
<T id={'save_new'} />
</Button>
<Button
disabled={isSubmitting}
className={'button-secondary ml1'}
onClick={() => {
onSubmitClick({ publish: false, redirect: true });
}}
>
<T id={'save_as_draft'} />
</Button>
<Button
className={'button-secondary ml1'}
onClick={() => {
onCancelClick && onCancelClick();
}}
>
<T id={'cancel'} />
</Button>
</div>
</div>
);
}

View File

@@ -1,20 +1,21 @@
import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import React, { useMemo, useEffect, useCallback } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { pick, setWith } from 'lodash';
import { pick } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
CreateJournalSchema,
EditJournalSchema,
} from './MakeJournalEntries.schema';
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
import MakeJournalFormFloatingActions from './MakeJournalFormFloatingActions';
import MakeJournalEntriesField from './MakeJournalEntriesField';
import MakeJournalNumberWatcher from './MakeJournalNumberWatcher';
import MakeJournalFormFooter from './MakeJournalFormFooter';
import withJournalsActions from 'containers/Accounting/withJournalsActions';
import withManualJournalDetail from 'containers/Accounting/withManualJournalDetail';
@@ -25,25 +26,31 @@ import withSettings from 'containers/Settings/withSettings';
import AppToaster from 'components/AppToaster';
import Dragzone from 'components/Dragzone';
import withMediaActions from 'containers/Media/withMediaActions';
import useMedia from 'hooks/useMedia';
import {
compose,
repeatValue,
orderingLinesIndexes,
defaultToTransform,
} from 'utils';
import { transformErrors } from './utils';
import withManualJournalsActions from './withManualJournalsActions';
import withManualJournals from './withManualJournals';
const ERROR = {
JOURNAL_NUMBER_ALREADY_EXISTS: 'JOURNAL.NUMBER.ALREADY.EXISTS',
CUSTOMERS_NOT_WITH_RECEVIABLE_ACC: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT',
VENDORS_NOT_WITH_PAYABLE_ACCOUNT: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT',
PAYABLE_ENTRIES_HAS_NO_VENDORS: 'PAYABLE.ENTRIES.HAS.NO.VENDORS',
RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
CREDIT_DEBIT_SUMATION_SHOULD_NOT_EQUAL_ZERO:
'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
const defaultEntry = {
index: 0,
account_id: '',
credit: '',
debit: '',
contact_id: '',
note: '',
};
const defaultInitialValues = {
journal_number: '',
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
currency_code: '',
entries: [...repeatValue(defaultEntry, 4)],
};
/**
@@ -77,27 +84,9 @@ function MakeJournalEntriesForm({
onCancelForm,
}) {
const { formatMessage } = useIntl();
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const isNewMode = manualJournalId;
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => {
savedMediaIds.current = [];
};
const journalNumber = journalNumberPrefix
const journalNumber = isNewMode
? `${journalNumberPrefix}-${journalNextNumber}`
: journalNextNumber;
@@ -122,73 +111,6 @@ function MakeJournalEntriesForm({
formatMessage,
]);
const validationSchema = Yup.object().shape({
journal_number: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_number_' })),
journal_type: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_type' })),
date: Yup.date()
.required()
.label(formatMessage({ id: 'date' })),
currency_code: Yup.string(),
reference: Yup.string().min(1).max(255),
description: Yup.string().min(1).max(1024),
entries: Yup.array().of(
Yup.object().shape({
credit: Yup.number().decimalScale(13).nullable(),
debit: Yup.number().decimalScale(13).nullable(),
account_id: Yup.number()
.nullable()
.when(['credit', 'debit'], {
is: (credit, debit) => credit || debit,
then: Yup.number().required(),
}),
contact_id: Yup.number().nullable(),
contact_type: Yup.string().nullable(),
note: Yup.string().max(255).nullable(),
}),
),
});
const saveInvokeSubmit = useCallback(
(payload) => {
onFormSubmit && onFormSubmit(payload);
},
[onFormSubmit],
);
const [payload, setPayload] = useState({});
const defaultEntry = useMemo(
() => ({
index: 0,
account_id: null,
credit: 0,
debit: 0,
contact_id: null,
note: '',
}),
[],
);
const defaultInitialValues = useMemo(
() => ({
journal_number: journalNumber,
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
currency_code: '',
entries: [...repeatValue(defaultEntry, 4)],
}),
[defaultEntry, journalNumber],
);
const initialValues = useMemo(
() => ({
...(manualJournal
@@ -198,300 +120,124 @@ function MakeJournalEntriesForm({
...pick(entry, Object.keys(defaultEntry)),
})),
}
: {
: {
...defaultInitialValues,
journal_number: journalNumber,
entries: orderingLinesIndexes(defaultInitialValues.entries),
}),
}),
[manualJournal, defaultInitialValues, defaultEntry],
[manualJournal, journalNumber],
);
const initialAttachmentFiles = useMemo(() => {
return manualJournal && manualJournal.media
? manualJournal.media.map((attach) => ({
preview: attach.attachment_file,
uploaded: true,
metadata: { ...attach },
}))
: [];
}, [manualJournal]);
// Transform API errors in toasts messages.
const transformErrors = (resErrors, { setErrors, errors }) => {
const getError = (errorType) => resErrors.find((e) => e.type === errorType);
const toastMessages = [];
let error;
let newErrors = { ...errors, entries: [] };
const setEntriesErrors = (indexes, prop, message) =>
indexes.forEach((i) => {
const index = Math.max(i - 1, 0);
newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message);
});
if ((error = getError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'contact_id', 'error');
}
if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) {
toastMessages.push(
formatMessage({
id: 'should_select_customers_with_entries_have_receivable_account',
}),
);
setEntriesErrors(error.indexes, 'contact_id', 'error');
}
if ((error = getError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) {
toastMessages.push(
formatMessage({
id: 'customers_should_selected_with_receivable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_id', 'error');
}
if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_id', 'error');
}
if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) {
newErrors = setWith(
newErrors,
'journal_number',
formatMessage({
id: 'journal_number_is_already_used',
}),
);
}
setErrors({ ...newErrors });
if (toastMessages.length > 0) {
AppToaster.show({
message: toastMessages.map((message) => {
return <div>- {message}</div>;
}),
intent: Intent.DANGER,
});
}
};
const {
values,
errors,
setFieldError,
setFieldValue,
handleSubmit,
getFieldProps,
touched,
isSubmitting,
} = useFormik({
validationSchema,
initialValues,
onSubmit: (values, { setErrors, setSubmitting, resetForm }) => {
setSubmitting(true);
const entries = values.entries.filter(
(entry) => entry.debit || entry.credit,
);
const getTotal = (type = 'credit') => {
return entries.reduce((total, item) => {
return item[type] ? item[type] + total : total;
}, 0);
};
const totalCredit = getTotal('credit');
const totalDebit = getTotal('debit');
// Validate the total credit should be eqials total debit.
if (totalCredit !== totalDebit) {
AppToaster.show({
message: formatMessage({
id: 'should_total_of_credit_and_debit_be_equal',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
} else if (totalCredit === 0 || totalDebit === 0) {
AppToaster.show({
message: formatMessage({
id: 'amount_cannot_be_zero_or_empty',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
}
const form = { ...values, status: payload.publish, entries };
const saveJournal = (mediaIds) =>
new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mediaIds };
if (manualJournal && manualJournal.id) {
requestEditManualJournal(manualJournal.id, requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_journal_has_been_successfully_edited' },
{ number: values.journal_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'update', ...payload });
clearSavedMediaIds([]);
resetForm();
resolve(response);
})
.catch((errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
});
} else {
requestMakeJournalEntries(requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_journal_has_been_successfully_created' },
{ number: values.journal_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'new', ...payload });
clearSavedMediaIds();
resetForm();
resolve(response);
})
.catch((errors) => {
transformErrors(errors, { setErrors });
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);
});
},
});
// Observes journal number settings changes.
useEffect(() => {
if (journalNumberChanged) {
setFieldValue('journal_number', journalNumber);
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
setJournalNumberChanged(false);
}
}, [
journalNumber,
journalNumberChanged,
setJournalNumberChanged,
setFieldValue,
changePageSubtitle,
]);
const handleSubmitClick = useCallback(
(payload) => {
setPayload(payload);
// formik.resetForm();
handleSubmit();
},
[setPayload, handleSubmit],
);
const handleCancelClick = useCallback(
(payload) => {
onCancelForm && onCancelForm(payload);
},
[onCancelForm],
);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
// Handle click on add a new line/row.
const handleClickAddNewRow = useCallback(() => {
setFieldValue(
'entries',
orderingLinesIndexes([...values.entries, defaultEntry]),
);
}, [values.entries, defaultEntry, setFieldValue]);
// Handle click `Clear all lines` button.
const handleClickClearLines = useCallback(() => {
setFieldValue(
'entries',
orderingLinesIndexes([...repeatValue(defaultEntry, 4)]),
);
}, [defaultEntry, setFieldValue]);
// Handle journal number field change.
const handleJournalNumberChanged = useCallback(
(journalNumber) => {
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, '')
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
},
[changePageSubtitle],
);
return (
<div class="make-journal-entries">
<form onSubmit={handleSubmit}>
<MakeJournalEntriesHeader
manualJournal={manualJournalId}
errors={errors}
touched={touched}
values={values}
setFieldValue={setFieldValue}
getFieldProps={getFieldProps}
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalEntriesTable
values={values.entries}
errors={errors}
setFieldValue={setFieldValue}
defaultRow={defaultEntry}
onClickClearAllLines={handleClickClearLines}
onClickAddNewRow={handleClickAddNewRow}
/>
<MakeJournalEntriesFooter
isSubmitting={isSubmitting}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
manualJournal={manualJournalId}
/>
</form>
<Dragzone
const handleSubmit = (values, { setErrors, setSubmitting, resetForm }) => {
setSubmitting(true);
const entries = values.entries.filter(
(entry) => entry.debit || entry.credit,
);
const getTotal = (type = 'credit') => {
return entries.reduce((total, item) => {
return item[type] ? item[type] + total : total;
}, 0);
};
const totalCredit = getTotal('credit');
const totalDebit = getTotal('debit');
// Validate the total credit should be eqials total debit.
if (totalCredit !== totalDebit) {
AppToaster.show({
message: formatMessage({
id: 'should_total_of_credit_and_debit_be_equal',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
} else if (totalCredit === 0 || totalDebit === 0) {
AppToaster.show({
message: formatMessage({
id: 'amount_cannot_be_zero_or_empty',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
}
const form = { ...values, entries };
const handleError = (error) => {
transformErrors(error, { setErrors });
setSubmitting(false);
};
const handleSuccess = (errors) => {
AppToaster.show({
message: formatMessage(
{
id: isNewMode
? 'the_journal_has_been_successfully_created'
: 'the_journal_has_been_successfully_edited',
},
{ number: values.journal_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
};
if (isNewMode) {
requestEditManualJournal(manualJournal.id, form)
.then(handleSuccess)
.catch(handleError);
} else {
requestMakeJournalEntries(form).then(handleSuccess).catch(handleError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_MAKE_JOURNAL
)}
>
<Formik
initialValues={initialValues}
validationSchema={isNewMode ? CreateJournalSchema : EditJournalSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, values }) => (
<Form>
<MakeJournalEntriesHeader
manualJournal={manualJournalId}
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalNumberWatcher journalNumber={journalNumber} />
<MakeJournalEntriesField defaultRow={defaultEntry} />
<MakeJournalFormFooter />
<MakeJournalFormFloatingActions
isSubmitting={isSubmitting}
manualJournal={manualJournalId}
/>
</Form>
)}
</Formik>
{/* <Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
/> */}
</div>
);
}
@@ -503,11 +249,8 @@ export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ manualJournalsSettings }) => ({
journalNextNumber: manualJournalsSettings.nextNumber,
journalNumberPrefix: manualJournalsSettings.numberPrefix,
journalNextNumber: parseInt(manualJournalsSettings?.nextNumber, 10),
journalNumberPrefix: manualJournalsSettings?.numberPrefix,
})),
withManualJournalsActions,
withManualJournals(({ journalNumberChanged }) => ({
journalNumberChanged,
})),
)(MakeJournalEntriesForm);

View File

@@ -1,238 +1,12 @@
import React, { useMemo, useState, useCallback } from 'react';
import {
InputGroup,
FormGroup,
Intent,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { Row, Col } from 'react-grid-system';
import moment from 'moment';
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { momentFormatter, tansformDateValue, saveInvoke } from 'utils';
import {
ErrorMessage,
Hint,
FieldHint,
FieldRequiredHint,
Icon,
InputPrependButton,
CurrencySelectList,
} from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrencies from 'containers/Currencies/withCurrencies';
import { compose } from 'utils';
function MakeJournalEntriesHeader({
errors,
touched,
values,
setFieldValue,
getFieldProps,
// #ownProps
manualJournal,
onJournalNumberChanged,
// #withCurrencies
currenciesList,
// #withDialog
openDialog,
}) {
const [selectedItems, setSelectedItems] = useState({});
const handleDateChange = useCallback(
(date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('date', formatted);
},
[setFieldValue],
);
const handleJournalNumberChange = useCallback(() => {
openDialog('journal-number-form', {});
}, [openDialog]);
// Handle journal number field blur event.
const handleJournalNumberChanged = (event) => {
saveInvoke(onJournalNumberChanged, event.currentTarget.value);
};
const onItemsSelect = useCallback(
(filedName) => {
return (filed) => {
setSelectedItems({
...selectedItems,
[filedName]: filed,
});
setFieldValue(filedName, filed.currency_code);
};
},
[setFieldValue, selectedItems],
);
import MakeJournalEntriesHeaderFields from "./MakeJournalEntriesHeaderFields";
export default function MakeJournalEntriesHeader() {
return (
<div class="make-journal-entries__header">
<Row>
<Col width={260}>
<FormGroup
label={<T id={'journal_number'} />}
labelInfo={
<>
<FieldRequiredHint />
<FieldHint />
</>
}
className={'form-group--journal-number'}
intent={
errors.journal_number && touched.journal_number && Intent.DANGER
}
helperText={
<ErrorMessage name="journal_number" {...{ errors, touched }} />
}
fill={true}
>
<ControlGroup fill={true}>
<InputGroup
intent={
errors.journal_number &&
touched.journal_number &&
Intent.DANGER
}
fill={true}
{...getFieldProps('journal_number')}
onBlur={handleJournalNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleJournalNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated journal number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
</Col>
<Col width={220}>
<FormGroup
label={<T id={'date'} />}
labelInfo={<FieldRequiredHint />}
intent={errors.date && touched.date && Intent.DANGER}
helperText={<ErrorMessage name="date" {...{ errors, touched }} />}
minimal={true}
className={classNames(CLASSES.FILL)}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
onChange={handleDateChange}
value={tansformDateValue(values.date)}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FormGroup>
</Col>
<Col width={400}>
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={errors.name && touched.name && Intent.DANGER}
helperText={
<ErrorMessage name="description" {...{ errors, touched }} />
}
fill={true}
>
<InputGroup
intent={errors.name && touched.name && Intent.DANGER}
fill={true}
{...getFieldProps('description')}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col width={260}>
<FormGroup
label={<T id={'reference'} />}
labelInfo={
<Hint
content={<T id={'journal_reference_hint'} />}
position={Position.RIGHT}
/>
}
className={'form-group--reference'}
intent={errors.reference && touched.reference && Intent.DANGER}
helperText={
<ErrorMessage name="reference" {...{ errors, touched }} />
}
fill={true}
>
<InputGroup
intent={errors.reference && touched.reference && Intent.DANGER}
fill={true}
{...getFieldProps('reference')}
/>
</FormGroup>
</Col>
<Col width={220}>
<FormGroup
label={<T id={'journal_type'} />}
className={classNames(
'form-group--account-type',
'form-group--select-list',
CLASSES.FILL,
)}
>
<InputGroup
intent={
errors.journal_type && touched.journal_type && Intent.DANGER
}
fill={true}
{...getFieldProps('journal_type')}
/>
</FormGroup>
</Col>
<Col width={230}>
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--currency',
CLASSES.FILL,
)}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={values.currency_code}
onCurrencySelected={onItemsSelect('currency_code')}
defaultSelectText={values.currency_code}
/>
</FormGroup>
</Col>
</Row>
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<MakeJournalEntriesHeaderFields />
</div>
);
}
export default compose(
withDialogActions,
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
)(MakeJournalEntriesHeader);
)
}

View File

@@ -0,0 +1,188 @@
import React, { useCallback } from 'react';
import {
InputGroup,
FormGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { FastField, ErrorMessage } from 'formik';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { momentFormatter, tansformDateValue, saveInvoke } from 'utils';
import {
Hint,
FieldHint,
FieldRequiredHint,
Icon,
InputPrependButton,
CurrencySelectList,
} from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrencies from 'containers/Currencies/withCurrencies';
import { compose, inputIntent, handleDateChange } from 'utils';
function MakeJournalEntriesHeader({
// #ownProps
manualJournal,
onJournalNumberChanged,
// #withCurrencies
currenciesList,
// #withDialog
openDialog,
}) {
const handleJournalNumberChange = useCallback(() => {
openDialog('journal-number-form', {});
}, [openDialog]);
// Handle journal number field blur event.
const handleJournalNumberChanged = (event) => {
saveInvoke(onJournalNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
<FastField name={'date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'posting_date'} />}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="date" />}
minimal={true}
inline={true}
className={classNames(CLASSES.FILL)}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('date', formattedDate);
})}
value={tansformDateValue(value)}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
<FastField name={'journal_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'journal_no'} />}
labelInfo={
<>
<FieldRequiredHint />
<FieldHint />
</>
}
className={'form-group--journal-number'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="journal_number" />}
fill={true}
inline={true}
>
<ControlGroup fill={true}>
<InputGroup
fill={true}
{...field}
onBlur={handleJournalNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleJournalNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated journal number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
<FastField name={'reference'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
labelInfo={
<Hint
content={<T id={'journal_reference_hint'} />}
position={Position.RIGHT}
/>
}
className={'form-group--reference'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
fill={true}
inline={true}
>
<InputGroup fill={true} {...field} />
</FormGroup>
)}
</FastField>
<FastField name={'journal_type'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'journal_type'} />}
className={classNames(
'form-group--account-type',
CLASSES.FILL,
)}
inline={true}
>
<InputGroup
intent={inputIntent({ error, touched })}
fill={true}
{...field}
/>
</FormGroup>
)}
</FastField>
<FastField name={'currency'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--currency',
CLASSES.FILL,
)}
inline={true}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={value}
onCurrencySelected={(currencyItem) => {
form.setFieldValue('currency_code', currencyItem.currency_code);
}}
defaultSelectText={value}
/>
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withDialogActions,
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
)(MakeJournalEntriesHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -28,10 +29,24 @@ function MakeJournalEntriesPage({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
@@ -88,4 +103,5 @@ export default compose(
withManualJournalsActions,
withCurrenciesActions,
withSettingsActions,
withDashboardActions,
)(MakeJournalEntriesPage);

View File

@@ -2,91 +2,32 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Tooltip, Position, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import { Hint } from 'components';
import { compose, formattedAmount, transformUpdatedRows } from 'utils';
import {
compose,
formattedAmount,
transformUpdatedRows,
saveInvoke,
} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
} from 'components/DataTableCells';
import {
ContactHeaderCell,
ActionsCellRenderer,
TotalAccountCellRenderer,
TotalCreditDebitCellRenderer,
NoteCellRenderer,
} from './components';
import withAccounts from 'containers/Accounts/withAccounts';
import withCustomers from 'containers/Customers/withCustomers';
// Contact header cell.
function ContactHeaderCell() {
return (
<>
<T id={'contact'} />
<Hint
content={<T id={'contact_column_hint'} />}
position={Position.LEFT_BOTTOM}
/>
</>
);
}
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 2) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
// Total text cell renderer.
const TotalAccountCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 2) {
return <span>{'Total USD'}</span>;
}
return chainedComponent(props);
};
// Total credit/debit cell renderer.
const TotalCreditDebitCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === props.row.index + 2) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return chainedComponent(props);
};
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 2) {
return '';
}
return chainedComponent(props);
};
/**
* Make journal entries table component.
*/
@@ -101,22 +42,19 @@ function MakeJournalEntriesTable({
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
defaultRow,
values,
errors, setFieldValue,
onChange,
entries,
error,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
useEffect(() => {
setRows([...values.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [values, setRows]);
setRows([...entries.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [entries, setRows]);
// Final table rows editor rows and total and final blank row.
const tableRows = useMemo(
() => [...rows, { rowType: 'total' }, { rowType: 'final_space' }],
[rows],
);
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
// Memorized data table columns.
const columns = useMemo(
@@ -138,8 +76,7 @@ function MakeJournalEntriesTable({
Cell: TotalAccountCellRenderer(AccountsListFieldCell),
className: 'account',
disableSortBy: true,
disableResizing: true,
width: 250,
width: 140,
},
{
Header: formatMessage({ id: 'credit_currency' }, { currency: 'USD' }),
@@ -147,8 +84,7 @@ function MakeJournalEntriesTable({
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'credit'),
className: 'credit',
disableSortBy: true,
disableResizing: true,
width: 150,
width: 100,
},
{
Header: formatMessage({ id: 'debit_currency' }, { currency: 'USD' }),
@@ -156,8 +92,7 @@ function MakeJournalEntriesTable({
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'debit'),
className: 'debit',
disableSortBy: true,
disableResizing: true,
width: 150,
width: 100,
},
{
Header: ContactHeaderCell,
@@ -165,9 +100,8 @@ function MakeJournalEntriesTable({
accessor: 'contact_id',
Cell: NoteCellRenderer(ContactsListFieldCell),
className: 'contact',
disableResizing: true,
disableSortBy: true,
width: 200,
width: 120,
},
{
Header: formatMessage({ id: 'note' }),
@@ -175,6 +109,7 @@ function MakeJournalEntriesTable({
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'note',
width: 200,
},
{
Header: '',
@@ -190,9 +125,9 @@ function MakeJournalEntriesTable({
);
// Handles click new line.
const onClickNewRow = useCallback(() => {
onClickAddNewRow && onClickAddNewRow();
}, [defaultRow, rows, onClickAddNewRow]);
const onClickNewRow = () => {
saveInvoke(onClickAddNewRow);
};
// Handles update datatable data.
const handleUpdateData = useCallback(
@@ -203,8 +138,8 @@ function MakeJournalEntriesTable({
columnIdOrObj,
value,
);
setFieldValue(
'entries',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
@@ -212,26 +147,27 @@ function MakeJournalEntriesTable({
})),
);
},
[rows, setFieldValue],
[rows, onChange],
);
const handleRemoveRow = useCallback(
(rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 2) { return; }
if (rows.length <= 2) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
setFieldValue(
'entries',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({ ...omit(row, ['rowType']) })),
);
onClickRemoveRow && onClickRemoveRow(removeIndex);
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, setFieldValue, onClickRemoveRow],
[rows, onChange, onClickRemoveRow],
);
// Rows class names callback.
@@ -243,11 +179,16 @@ function MakeJournalEntriesTable({
);
const handleClickClearAllLines = () => {
onClickClearAllLines && onClickClearAllLines();
saveInvoke(onClickClearAllLines);
};
return (
<div class="make-journal-entries__table datatable-editor">
<div
className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW,
)}
>
<DataTable
columns={columns}
data={tableRows}
@@ -255,7 +196,7 @@ function MakeJournalEntriesTable({
sticky={true}
payload={{
accounts: accountsList,
errors: errors.entries || [],
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
contacts: [
@@ -266,8 +207,7 @@ function MakeJournalEntriesTable({
],
}}
/>
<div class="mt1">
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
<Button
small={true}
className={'button--secondary button--new-line'}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import { saveInvoke } from 'utils';
import { CLASSES } from 'common/classes';
export default function MakeJournalFloatingAction({
isSubmitting,
onSubmitClick,
onCancelClick,
manualJournalId,
}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
name={'save'}
onClick={() => {
saveInvoke(onSubmitClick, { publish: true, redirect: true });
}}
>
{manualJournalId ? <T id={'edit'} /> : <T id={'save'} />}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
onClick={() => {
saveInvoke(onSubmitClick, { publish: true, redirect: false });
}}
>
<T id={'save_new'} />
</Button>
<Button
disabled={isSubmitting}
className={'button-secondary ml1'}
onClick={() => {
saveInvoke(onSubmitClick, { publish: false, redirect: true });
}}
>
<T id={'save_as_draft'} />
</Button>
<Button
className={'button-secondary ml1'}
onClick={() => {
saveInvoke(onCancelClick);
}}
>
<T id={'cancel'} />
</Button>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { FastField } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { ErrorMessage, Row, Col } from 'components';
import Dragzone from 'components/Dragzone';
import { inputIntent } from 'utils';
export default function MakeJournalFormFooter() {
return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Row>
<Col md={8}>
<FastField name={'description'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="description" />}
fill={true}
>
<TextArea fill={true} {...field} />
</FormGroup>
)}
</FastField>
</Col>
<Col md={4}>
<Dragzone
initialFiles={[]}
// onDrop={handleDropFiles}
// onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { useEffect } from 'react';
import { compose } from 'redux';
import { useFormikContext } from 'formik';
import withManualJournalsActions from './withManualJournalsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withManualJournals from './withManualJournals';
import { defaultToTransform } from 'utils';
/**
*
*/
function MakeJournalNumberChangingWatcher({
journalNumber,
journalNumberChanged,
setJournalNumberChanged,
changePageSubtitle
}) {
const { setFieldValue } = useFormikContext();
// Observes journal number settings changes.
useEffect(() => {
if (journalNumberChanged) {
setFieldValue('journal_number', journalNumber);
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
setJournalNumberChanged(false);
}
}, [
journalNumber,
journalNumberChanged,
setJournalNumberChanged,
setFieldValue,
changePageSubtitle,
]);
return null;
}
export default compose(
withManualJournals(({ journalNumberChanged }) => ({
journalNumberChanged,
})),
withManualJournalsActions,
withDashboardActions,
)(MakeJournalNumberChangingWatcher);

View File

@@ -1,13 +1,7 @@
import React from 'react';
import {
Intent,
Classes,
Tooltip,
Position,
Tag,
} from '@blueprintjs/core';
import { Intent, Classes, Tooltip, Position, Tag, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { Choose, Money, If, Icon } from 'components';
import { Choose, Money, If, Icon, Hint } from 'components';
import withAccountDetails from 'containers/Accounts/withAccountDetail';
import { compose } from 'utils';
@@ -99,4 +93,77 @@ export function NoteAccessor(row) {
</Tooltip>
</If>
);
}
}
// Contact header cell.
export function ContactHeaderCell() {
return (
<>
<T id={'contact'} />
<Hint
content={<T id={'contact_column_hint'} />}
position={Position.LEFT_BOTTOM}
/>
</>
);
}
// Actions cell renderer.
export const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 1) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
// Total text cell renderer.
export const TotalAccountCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return <span>{'Total USD'}</span>;
}
return chainedComponent(props);
};
// Total credit/debit cell renderer.
export const TotalCreditDebitCellRenderer = (chainedComponent, type) => (
props,
) => {
if (props.data.length === props.row.index + 1) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span><Money amount={total} currency={'USD'} /></span>;
}
return chainedComponent(props);
};
export const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return chainedComponent(props);
};

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { formatMessage } from 'services/intl';
import { setWith } from 'lodash';
const ERROR = {
JOURNAL_NUMBER_ALREADY_EXISTS: 'JOURNAL.NUMBER.ALREADY.EXISTS',
CUSTOMERS_NOT_WITH_RECEVIABLE_ACC: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT',
VENDORS_NOT_WITH_PAYABLE_ACCOUNT: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT',
PAYABLE_ENTRIES_HAS_NO_VENDORS: 'PAYABLE.ENTRIES.HAS.NO.VENDORS',
RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
CREDIT_DEBIT_SUMATION_SHOULD_NOT_EQUAL_ZERO:
'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
};
// Transform API errors in toasts messages.
export const transformErrors = (resErrors, { setErrors, errors }) => {
const getError = (errorType) => resErrors.find((e) => e.type === errorType);
const toastMessages = [];
let error;
let newErrors = { ...errors, entries: [] };
const setEntriesErrors = (indexes, prop, message) =>
indexes.forEach((i) => {
const index = Math.max(i - 1, 0);
newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message);
});
if ((error = getError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'contact_id', 'error');
}
if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) {
toastMessages.push(
formatMessage({
id: 'should_select_customers_with_entries_have_receivable_account',
}),
);
setEntriesErrors(error.indexes, 'contact_id', 'error');
}
if ((error = getError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) {
toastMessages.push(
formatMessage({
id: 'customers_should_selected_with_receivable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_id', 'error');
}
if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_id', 'error');
}
if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) {
newErrors = setWith(
newErrors,
'journal_number',
formatMessage({
id: 'journal_number_is_already_used',
}),
);
}
setErrors({ ...newErrors });
if (toastMessages.length > 0) {
AppToaster.show({
message: toastMessages.map((message) => {
return <div>- {message}</div>;
}),
intent: Intent.DANGER,
});
}
};

View File

@@ -41,11 +41,22 @@ const mapActionsToProps = (dispatch) => ({
type: t.SIDEBAR_EXPEND_TOGGLE,
}),
changePreferencesPageTitle: (pageTitle) =>
dispatch({
type: 'CHANGE_PREFERENCES_PAGE_TITLE',
pageTitle,
}),
changePreferencesPageTitle: (pageTitle) => dispatch({
type: 'CHANGE_PREFERENCES_PAGE_TITLE',
pageTitle,
}),
setSidebarShrink: () => dispatch({
type: t.SIDEBAR_SHRINK,
}),
setSidebarExpand: () => dispatch({
type: t.SIDEBAR_SHRINK,
}),
resetSidebarPreviousExpand: () => dispatch({
type: t.RESET_SIDEBAR_PREVIOUS_EXPAND,
}),
recordSidebarPreviousExpand: () => dispatch({
type: t.RECORD_SIDEBAR_PREVIOUS_EXPAND,
}),
});
export default connect(null, mapActionsToProps);

View File

@@ -229,6 +229,7 @@ function ItemsEntriesTable({
className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES,
CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW,
)}
>
<DataTable
@@ -243,7 +244,7 @@ function ItemsEntriesTable({
removeRow: handleRemoveRow,
}}
/>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS, 'mt1')}>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
<Button
small={true}
className={'button--secondary button--new-line'}

View File

@@ -141,6 +141,7 @@ export default function ExpenseFloatingFooter({
>
<Button rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} />
</Popover>
{/* ----------- Clear ----------- */}
<Button
className={'ml1'}

View File

@@ -1,21 +1,19 @@
import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { defaultTo, pick } from 'lodash';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick } from 'lodash';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import ExpenseFormHeader from './ExpenseFormHeader';
import ExpenseTable from './ExpenseTable';
import ExpenseFormBody from './ExpenseFormBody';
import ExpenseFloatingFooter from './ExpenseFloatingActions';
import ExpenseFormFooter from './ExpenseFormFooter';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withExpenseDetail from 'containers/Expenses/withExpenseDetail';
@@ -30,12 +28,28 @@ import {
CreateExpenseFormSchema,
EditExpenseFormSchema,
} from './ExpenseForm.schema';
import useMedia from 'hooks/useMedia';
import { compose, repeatValue, transformToForm } from 'utils';
import {
transformErrors,
} from './utils';
import { compose, repeatValue, orderingLinesIndexes } from 'utils';
const MIN_LINES_NUMBER = 4;
const ERROR = {
EXPENSE_ALREADY_PUBLISHED: 'EXPENSE.ALREADY.PUBLISHED',
const defaultCategory = {
index: 0,
amount: '',
expense_account_id: '',
description: '',
};
const defaultInitialValues = {
payment_account_id: '',
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: '',
categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
};
/**
@@ -46,106 +60,49 @@ function ExpenseForm({
requestSubmitMedia,
requestDeleteMedia,
//#withExpensesActions
// #withExpensesActions
requestSubmitExpense,
requestEditExpense,
requestFetchExpensesTable,
// #withDashboard
changePageTitle,
changePageSubtitle,
//#withExpenseDetail
// #withExpenseDetail
expense,
// #withSettings
baseCurrency,
preferredPaymentAccount,
// #own Props
// #ownProps
expenseId,
onFormSubmit,
onCancelForm,
}) {
const [submitPayload, setSubmitPayload] = useState({});
const history = useHistory();
const isNewMode = !expenseId;
const { formatMessage } = useIntl();
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const validationSchema = isNewMode
? CreateExpenseFormSchema
: EditExpenseFormSchema;
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => {
savedMediaIds.current = [];
};
useEffect(() => {
if (expense && expense.id) {
changePageTitle(formatMessage({ id: 'edit_expense' }));
} else {
if (isNewMode) {
changePageTitle(formatMessage({ id: 'new_expense' }));
} else {
changePageTitle(formatMessage({ id: 'edit_expense' }));
}
}, [changePageTitle, expense, formatMessage]);
const saveInvokeSubmit = useCallback(
(payload) => {
onFormSubmit && onFormSubmit(payload);
},
[onFormSubmit],
);
const defaultCategory = useMemo(
() => ({
index: 0,
amount: 0,
expense_account_id: null,
description: '',
}),
[],
);
const defaultInitialValues = useMemo(
() => ({
payment_account_id: parseInt(preferredPaymentAccount),
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: baseCurrency,
categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
}),
[defaultCategory],
);
const orderingCategoriesIndex = (categories) => {
return categories.map((category, index) => ({
...category,
index: index + 1,
}));
};
}, [changePageTitle, isNewMode, formatMessage]);
const initialValues = useMemo(
() => ({
...(expense
? {
...pick(expense, Object.keys(defaultInitialValues)),
currency_code: baseCurrency,
payment_account_id: defaultTo(preferredPaymentAccount, ''),
categories: [
...expense.categories.map((category) => ({
...pick(category, Object.keys(defaultCategory)),
@@ -158,225 +115,87 @@ function ExpenseForm({
}
: {
...defaultInitialValues,
categories: orderingCategoriesIndex(
categories: orderingLinesIndexes(
defaultInitialValues.categories,
),
}),
}),
[expense, defaultInitialValues, defaultCategory],
[expense, baseCurrency, preferredPaymentAccount],
);
const initialAttachmentFiles = useMemo(() => {
return expense && expense.media
? expense.media.map((attach) => ({
preview: attach.attachment_file,
uploaded: true,
metadata: { ...attach },
}))
: [];
}, [expense]);
const handleSubmit = (values, { setSubmitting, setErrors, resetForm }) => {
setSubmitting(true);
const totalAmount = values.categories.reduce((total, item) => {
return total + item.amount;
}, 0);
// Transform API errors in toasts messages.
const transformErrors = (errors, { setErrors }) => {
const hasError = (errorType) => errors.some((e) => e.type === errorType);
if (hasError(ERROR.EXPENSE_ALREADY_PUBLISHED)) {
setErrors(
AppToaster.show({
message: formatMessage({
id: 'the_expense_is_already_published',
}),
if (totalAmount <= 0) {
AppToaster.show({
message: formatMessage({
id: 'amount_cannot_be_zero_or_empty',
}),
);
intent: Intent.DANGER,
});
return;
}
const categories = values.categories.filter(
(category) =>
category.amount && category.index && category.expense_account_id,
);
const form = {
...values,
publish: 1,
categories,
};
// Handle request success.
const handleSuccess = (response) => {
AppToaster.show({
message: formatMessage(
{ id: isNewMode ?
'the_expense_has_been_successfully_created' :
'the_expense_has_been_successfully_edited' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
};
// Handle request error
const handleError = (error) => {
transformErrors(error, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
requestSubmitExpense(form).then(handleSuccess).catch(handleError);
} else {
requestEditExpense(expense.id, form).then(handleSuccess).catch(handleError);
}
};
const {
values,
errors,
touched,
isSubmitting,
setFieldValue,
handleSubmit,
getFieldProps,
submitForm,
resetForm,
} = useFormik({
enableReinitialize: true,
validationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting, setErrors, resetForm }) => {
setSubmitting(true);
const totalAmount = values.categories.reduce((total, item) => {
return total + item.amount;
}, 0);
if (totalAmount <= 0) {
AppToaster.show({
message: formatMessage({
id: 'amount_cannot_be_zero_or_empty',
}),
intent: Intent.DANGER,
});
return;
}
const categories = values.categories.filter(
(category) =>
category.amount && category.index && category.expense_account_id,
);
const form = {
...values,
publish: submitPayload.publish,
categories,
};
const saveExpense = (mdeiaIds) =>
new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mdeiaIds };
if (expense && expense.id) {
requestEditExpense(expense.id, requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expense_has_been_successfully_edited' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'update', ...submitPayload });
clearSavedMediaIds([]);
resetForm();
})
.catch((errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
});
} else {
requestSubmitExpense(requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expense_has_been_successfully_created' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
if (submitPayload.resetForm) {
resetForm();
}
saveInvokeSubmit({ action: 'new', ...submitPayload });
clearSavedMediaIds();
})
.catch((errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
});
}
});
Promise.all([saveMedia(), deleteMedia()])
.then(([savedMediaResponses]) => {
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
savedMediaIds.current = mediaIds;
return savedMediaResponses;
})
.then(() => {
return saveExpense(savedMediaIds.current);
});
},
});
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
const handleCancelClick = useCallback(() => {
history.goBack();
}, [history]);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
// Handle click on add a new line/row.
const handleClickAddNewRow = () => {
setFieldValue(
'categories',
orderingCategoriesIndex([...values.categories, defaultCategory]),
);
};
const handleClearAllLines = () => {
setFieldValue(
'categories',
orderingCategoriesIndex([
...repeatValue(defaultCategory, MIN_LINES_NUMBER),
]),
);
};
return (
<div className={'expense-form'}>
<form onSubmit={handleSubmit}>
<ExpenseFormHeader
errors={errors}
touched={touched}
values={values}
setFieldValue={setFieldValue}
getFieldProps={getFieldProps}
/>
<ExpenseTable
categories={values.categories}
onClickAddNewRow={handleClickAddNewRow}
onClickClearAllLines={handleClearAllLines}
errors={errors}
setFieldValue={setFieldValue}
defaultRow={defaultCategory}
/>
<div class="expense-form-footer">
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
>
<TextArea growVertically={true} {...getFieldProps('description')} />
</FormGroup>
<Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</div>
<ExpenseFloatingFooter
isSubmitting={isSubmitting}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
onSubmitForm={submitForm}
onResetForm={resetForm}
expense={expense}
/>
</form>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_EXPENSE
)}>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
{({ isSubmitting, values }) => (
<Form>
<ExpenseFormHeader />
<ExpenseFormBody />
<ExpenseFormFooter />
<ExpenseFloatingFooter />
</Form>
)}
</Formik>
</div>
);
}
@@ -389,6 +208,6 @@ export default compose(
withExpenseDetail(),
withSettings(({ organizationSettings, expenseSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
preferredPaymentAccount: expenseSettings?.preferredPaymentAccount,
preferredPaymentAccount: parseInt(expenseSettings?.preferredPaymentAccount, 10),
})),
)(ExpenseForm);

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
beneficiary: Yup.string().label(formatMessage({ id: 'beneficiary' })),
@@ -25,11 +26,11 @@ const Schema = Yup.object().shape({
categories: Yup.array().of(
Yup.object().shape({
index: Yup.number().min(1).max(DATATYPES_LENGTH.INT_10).nullable(),
amount: Yup.number().decimalScale(13).nullable(),
amount: Yup.number().nullable(),
expense_account_id: Yup.number()
.nullable()
.when(['amount'], {
is: (amount) => amount,
is: (amount) => !isBlank(amount),
then: Yup.number().required(),
}),
description: Yup.string().max(DATATYPES_LENGTH.TEXT).nullable(),

View File

@@ -0,0 +1,12 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import ExpenseFormEntriesField from './ExpenseFormEntriesField';
export default function ExpenseFormBody() {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<ExpenseFormEntriesField />
</div>
)
}

View File

@@ -2,11 +2,19 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import { Hint } from 'components';
import { compose, formattedAmount, transformUpdatedRows } from 'utils';
import {
compose,
formattedAmount,
transformUpdatedRows,
saveInvoke,
} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
@@ -92,17 +100,17 @@ function ExpenseTable({
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
defaultRow,
categories,
errors,
setFieldValue,
entries,
error,
onChange,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
useEffect(() => {
setRows([...categories.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [categories]);
setRows([...entries.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [entries]);
// Final table rows editor rows and total and final blank row.
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
@@ -126,8 +134,7 @@ function ExpenseTable({
Cell: TotalExpenseCellRenderer(AccountsListFieldCell),
className: 'expense_account_id',
disableSortBy: true,
disableResizing: true,
width: 250,
width: 40,
filterAccountsByRootType: ['expense'],
},
{
@@ -135,8 +142,7 @@ function ExpenseTable({
accessor: 'amount',
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'),
disableSortBy: true,
disableResizing: true,
width: 180,
width: 40,
className: 'amount',
},
{
@@ -145,6 +151,7 @@ function ExpenseTable({
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: '',
@@ -168,8 +175,8 @@ function ExpenseTable({
columnIdOrObj,
value,
);
setFieldValue(
'categories',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
@@ -177,7 +184,7 @@ function ExpenseTable({
})),
);
},
[rows, setFieldValue],
[rows, onChange],
);
// Handles click remove datatable row.
@@ -187,12 +194,11 @@ function ExpenseTable({
if (rows.length <= 1) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
setFieldValue(
'categories',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row, index) => ({
@@ -200,18 +206,18 @@ function ExpenseTable({
index: index + 1,
})),
);
onClickRemoveRow && onClickRemoveRow(removeIndex);
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, setFieldValue, onClickRemoveRow],
[rows, onChange, onClickRemoveRow],
);
// Invoke when click on add new line button.
const onClickNewRow = () => {
onClickAddNewRow && onClickAddNewRow();
saveInvoke(onClickAddNewRow);
};
// Invoke when click on clear all lines button.
const handleClickClearAllLines = () => {
onClickClearAllLines && onClickClearAllLines();
saveInvoke(onClickClearAllLines);
};
// Rows classnames callback.
const rowClassNames = useCallback(
@@ -222,7 +228,12 @@ function ExpenseTable({
);
return (
<div className={'dashboard__insider--expense-form__table'}>
<div
className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW,
)}
>
<DataTable
columns={columns}
data={tableRows}
@@ -230,12 +241,12 @@ function ExpenseTable({
sticky={true}
payload={{
accounts: accountsList,
errors: errors.categories || [],
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
}}
/>
<div className={'mt1'}>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
<Button
small={true}
className={'button--secondary button--new-line'}

View File

@@ -0,0 +1,21 @@
import { FastField } from 'formik';
import React from 'react';
import ExpenseFormEntries from './ExpenseFormEntries';
export default function ExpenseFormEntriesField({
}) {
return (
<FastField name={'categories'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<ExpenseFormEntries
entries={value}
error={error}
onChange={(entries) => {
form.setFieldValue('categories', entries)
}}
/>
)}
</FastField>
)
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { FastField } from 'formik';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { inputIntent } from 'utils';
import { Row, Dragzone, Col } from 'components';
import { CLASSES } from 'common/classes';
export default function ExpenseFormFooter({}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Row>
<Col md={8}>
<FastField name={'description'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
</Col>
<Col md={4}>
<Dragzone
initialFiles={[]}
// onDrop={handleDropFiles}
// onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</Col>
</Row>
</div>
);
}

View File

@@ -1,226 +1,22 @@
import React, { useMemo, useCallback, useState } from 'react';
import {
InputGroup,
FormGroup,
Intent,
Position,
MenuItem,
Classes,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { Row, Col } from 'react-grid-system';
import moment from 'moment';
import { momentFormatter, compose, tansformDateValue } from 'utils';
import React from 'react';
import classNames from 'classnames';
import {
CurrencySelectList,
ContactSelecetList,
ErrorMessage,
AccountsSelectList,
FieldRequiredHint,
Hint,
} from 'components';
import withCurrencies from 'containers/Currencies/withCurrencies';
import withAccounts from 'containers/Accounts/withAccounts';
import withCustomers from 'containers/Customers/withCustomers';
function ExpenseFormHeader({
// #ownProps
errors,
touched,
setFieldValue,
getFieldProps,
values,
import { CLASSES } from 'common/classes';
//withCurrencies
currenciesList,
// #withAccounts
accountsList,
accountsTypes,
// #withCustomers
customers,
}) {
const [selectedItems, setSelectedItems] = useState({});
const handleDateChange = useCallback(
(date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('payment_date', formatted);
},
[setFieldValue],
);
// Handles change account.
const onChangeAccount = useCallback(
(account) => {
setFieldValue('payment_account_id', account.id);
},
[setFieldValue],
);
const onItemsSelect = useCallback(
(filedName) => {
return (filed) => {
setSelectedItems({
...selectedItems,
[filedName]: filed,
});
setFieldValue(filedName, filed.currency_code);
};
},
[setFieldValue, selectedItems],
);
// handle change customer
const onChangeCustomer = useCallback(
(filedName) => {
return (customer) => {
setFieldValue(filedName, customer.id);
};
},
[setFieldValue],
);
import ExpenseFormHeaderFields from './ExpenseFormHeaderFields';
import { PageFormBigNumber } from 'components';
// Expense form header.
export default function ExpenseFormHeader() {
return (
<div className={'dashboard__insider--expense-form__header'}>
<Row>
<Col width={300}>
<FormGroup
label={<T id={'assign_to_customer'} />}
className={classNames('form-group--select-list', Classes.FILL)}
labelInfo={<Hint />}
intent={errors.beneficiary && touched.beneficiary && Intent.DANGER}
helperText={
<ErrorMessage
name={'assign_to_customer'}
{...{ errors, touched }}
/>
}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={values.customer_id}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={onChangeCustomer('customer_id')}
/>
</FormGroup>
</Col>
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<ExpenseFormHeaderFields />
<Col width={400}>
<FormGroup
label={<T id={'payment_account'} />}
className={classNames(
'form-group--payment_account',
'form-group--select-list',
Classes.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={
errors.payment_account_id &&
touched.payment_account_id &&
Intent.DANGER
}
helperText={
<ErrorMessage
name={'payment_account_id'}
{...{ errors, touched }}
/>
}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={onChangeAccount}
defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={values.payment_account_id}
filterByTypes={['current_asset']}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col width={300}>
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={<Hint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={
errors.payment_date && touched.payment_date && Intent.DANGER
}
helperText={
<ErrorMessage name="payment_date" {...{ errors, touched }} />
}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(values.payment_date)}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
</Col>
<Col width={200}>
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--currency',
Classes.FILL,
)}
intent={
errors.currency_code && touched.currency_code && Intent.DANGER
}
helperText={
<ErrorMessage name="currency_code" {...{ errors, touched }} />
}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={values.currency_code}
onCurrencySelected={onItemsSelect('currency_code')}
defaultSelectText={values.currency_code}
/>
</FormGroup>
</Col>
<Col width={200}>
<FormGroup
label={<T id={'ref_no'} />}
className={classNames('form-group--ref_no', Classes.FILL)}
intent={
errors.reference_no && touched.reference_no && Intent.DANGER
}
helperText={
<ErrorMessage name="reference_no" {...{ errors, touched }} />
}
>
<InputGroup
intent={
errors.reference_no && touched.reference_no && Intent.DANGER
}
minimal={true}
{...getFieldProps('reference_no')}
/>
</FormGroup>
</Col>
</Row>
<PageFormBigNumber
label={'Expense Amount'}
amount={0}
currencyCode={'LYD'}
/>
</div>
);
}
export default compose(
withAccounts(({ accountsList, accountsTypes }) => ({
accountsList,
accountsTypes,
})),
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
withCustomers(({ customers }) => ({
customers,
})),
)(ExpenseFormHeader);
)
}

View File

@@ -0,0 +1,164 @@
import React from 'react';
import { InputGroup, FormGroup, Position, Classes } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import {
momentFormatter,
compose,
tansformDateValue,
inputIntent,
handleDateChange,
} from 'utils';
import classNames from 'classnames';
import {
CurrencySelectList,
ContactSelecetList,
ErrorMessage,
AccountsSelectList,
FieldRequiredHint,
Hint,
} from 'components';
import withCurrencies from 'containers/Currencies/withCurrencies';
import withAccounts from 'containers/Accounts/withAccounts';
import withCustomers from 'containers/Customers/withCustomers';
function ExpenseFormHeader({
//withCurrencies
currenciesList,
// #withAccounts
accountsList,
accountsTypes,
// #withCustomers
customers,
}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
<FastField name={'payment_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={<Hint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="payment_date" />}
inline={true}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('payment_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
<FastField name={'payment_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_account'} />}
className={classNames(
'form-group--payment_account',
'form-group--select-list',
Classes.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'payment_account_id'} />}
inline={true}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('payment_account_id', account.id);
}}
defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={value}
filterByTypes={['current_asset']}
/>
</FormGroup>
)}
</FastField>
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--currency',
Classes.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="currency_code" />}
inline={true}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={value}
onCurrencySelected={(currencyItem) => {
form.setFieldValue('currency_code', currencyItem.currency_code);
}}
defaultSelectText={value}
/>
</FormGroup>
)}
</FastField>
<FastField name={'reference_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference_no'} />}
className={classNames('form-group--ref_no', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference_no" />}
inline={true}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer'} />}
className={classNames('form-group--select-list', Classes.FILL)}
labelInfo={<Hint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'assign_to_customer'} />}
inline={true}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
/>
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withAccounts(({ accountsList, accountsTypes }) => ({
accountsList,
accountsTypes,
})),
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
withCustomers(({ customers }) => ({
customers,
})),
)(ExpenseFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -9,6 +9,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -25,10 +26,24 @@ function Expenses({
// #withCustomersActions
requestFetchCustomers,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
@@ -83,4 +98,5 @@ export default compose(
withCurrenciesActions,
withExpensesActions,
withCustomersActions,
withDashboardActions,
)(Expenses);

View File

@@ -0,0 +1,21 @@
import { AppToaster } from 'components';
import { formatMessage } from 'services/intl';
const ERROR = {
EXPENSE_ALREADY_PUBLISHED: 'EXPENSE.ALREADY.PUBLISHED',
};
// Transform API errors in toasts messages.
export const transformErrors = (errors, { setErrors }) => {
const hasError = (errorType) => errors.some((e) => e.type === errorType);
if (hasError(ERROR.EXPENSE_ALREADY_PUBLISHED)) {
setErrors(
AppToaster.show({
message: formatMessage({
id: 'the_expense_is_already_published',
}),
}),
);
}
};

View File

@@ -41,7 +41,7 @@ const ItemsCategoryList = ({
(category) => () => {
openDialog('item-category-form', { action: 'edit', id: category.id });
},
[],
[openDialog],
);
const handleDeleteCategory = useCallback(

View File

@@ -12,7 +12,7 @@ import { EditBillFormSchema, CreateBillFormSchema } from './BillForm.schema';
import BillFormHeader from './BillFormHeader';
import BillFloatingActions from './BillFloatingActions';
import BillFormFooter from './BillFormFooter';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions';
@@ -23,6 +23,7 @@ import { AppToaster } from 'components';
import { ERROR } from 'common/errors';
import { compose, repeatValue, defaultToTransform, orderingLinesIndexes } from 'utils';
import BillFormBody from './BillFormBody';
const MIN_LINES_NUMBER = 5;
@@ -31,7 +32,7 @@ const defaultBill = {
item_id: '',
rate: '',
discount: '',
quantity: '',
quantity: 1,
description: '',
};
@@ -198,7 +199,11 @@ function BillForm({
}, [history]);
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_BILL)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_BILL,
)}>
<Formik
validationSchema={isNewMode ? CreateBillFormSchema : EditBillFormSchema}
initialValues={initialValues}
@@ -207,10 +212,7 @@ function BillForm({
{({ isSubmitting, values }) => (
<Form>
<BillFormHeader onBillNumberChanged={handleBillNumberChanged} />
<EditableItemsEntriesTable
defaultEntry={defaultBill}
filterPurchasableItems={true}
/>
<BillFormBody defaultBill={defaultBill} />
<BillFormFooter
oninitialFiles={[]}
// onDropFiles={handleDeleteFile}

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const BillFormSchema = Yup.object().shape({
vendor_id: Yup.number()
@@ -34,7 +35,7 @@ const BillFormSchema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
total: Yup.number().nullable(),

View File

@@ -0,0 +1,15 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
export default function BillFormBody({ defaultBill }) {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
defaultEntry={defaultBill}
filterPurchasableItems={true}
/>
</div>
);
}

View File

@@ -1,164 +1,20 @@
import React from 'react';
import { FormGroup, InputGroup, Position } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { ContactSelecetList, FieldRequiredHint, Row, Col } from 'components';
import withVendors from 'containers/Vendors/withVendors';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
handleDateChange,
inputIntent,
saveInvoke,
} from 'utils';
import BillFormHeaderFields from './BillFormHeaderFields';
import { PageFormBigNumber } from 'components';
/**
* Fill form header.
*/
function BillFormHeader({
onBillNumberChanged,
//#withVendors
vendorItems,
}) {
const handleBillNumberBlur = (event) => {
saveInvoke(onBillNumberChanged, event.currentTarget.value);
};
export default function BillFormHeader({ onBillNumberChanged }) {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={'page-form__primary-section'}>
{/* ------- Vendor name ------ */}
<FastField name={'vendor_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'vendor_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--vendor')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor_id'} />}
>
<ContactSelecetList
contactsList={vendorItems}
selectedContactId={value}
defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<BillFormHeaderFields onBillNumberChanged={onBillNumberChanged} />
<Row className={'row--bill-date'}>
<Col md={7}>
{/* ------- Bill date ------- */}
<FastField name={'bill_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('bill_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col md={5}>
{/* ------- Due date ------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames(
'form-group--due-date',
'form-group--select-list',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
{/* ------- Bill number ------- */}
<FastField name={'bill_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_number'} />}
inline={true}
className={('form-group--bill_number', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_number" />}
>
<InputGroup
minimal={true}
{...field}
onBlur={handleBillNumberBlur}
/>
</FormGroup>
)}
</FastField>
{/* ------- Reference ------- */}
<FastField name={'reference_no'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
<PageFormBigNumber label={'Due Amount'} amount={0} currencyCode={'LYD'} />
</div>
);
}
export default compose(
withVendors(({ vendorItems }) => ({
vendorItems,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(BillFormHeader);

View File

@@ -0,0 +1,161 @@
import React from 'react';
import { FormGroup, InputGroup, Position } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { ContactSelecetList, FieldRequiredHint, Icon } from 'components';
import withVendors from 'containers/Vendors/withVendors';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
handleDateChange,
inputIntent,
saveInvoke,
} from 'utils';
/**
* Fill form header.
*/
function BillFormHeader({
onBillNumberChanged,
//#withVendors
vendorItems,
}) {
const handleBillNumberBlur = (event) => {
saveInvoke(onBillNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ------- Vendor name ------ */}
<FastField name={'vendor_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'vendor_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--vendor')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor_id'} />}
>
<ContactSelecetList
contactsList={vendorItems}
selectedContactId={value}
defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ------- Bill date ------- */}
<FastField name={'bill_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('bill_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{ leftIcon: <Icon icon={'date-range'} /> }}
/>
</FormGroup>
)}
</FastField>
{/* ------- Due date ------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames(
'form-group--due-date',
'form-group--select-list',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ------- Bill number ------- */}
<FastField name={'bill_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_number'} />}
inline={true}
className={('form-group--bill_number', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_number" />}
>
<InputGroup
minimal={true}
{...field}
onBlur={handleBillNumberBlur}
/>
</FormGroup>
)}
</FastField>
{/* ------- Reference ------- */}
<FastField name={'reference_no'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withVendors(({ vendorItems }) => ({
vendorItems,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(BillFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withBillActions from './withBillActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -28,10 +29,24 @@ function Bills({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
// Handle fetch accounts
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
@@ -89,4 +104,5 @@ export default compose(
withItemsActions,
withAccountsActions,
withSettingsActions,
withDashboardActions
)(Bills);

View File

@@ -34,10 +34,22 @@ function PaymentMade({
// #withDashboardActions
changePageTitle,
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const { id: paymentMadeId } = useParams();
const { formatMessage } = useIntl();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
// Handle page title change in new and edit mode.
useEffect(() => {
if (paymentMadeId) {

View File

@@ -31,7 +31,7 @@ const ERRORS = {
PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE',
};
// Default payment made entry values.
// Default payment made entry values.x
const defaultPaymentMadeEntry = {
bill_id: '',
payment_amount: '',
@@ -96,7 +96,7 @@ function PaymentMadeForm({
const validationSchema = isNewMode
? CreatePaymentMadeFormSchema
: EditPaymentMadeFormSchema;
// Form initial values.
const initialValues = useMemo(
() => ({
@@ -302,7 +302,11 @@ function PaymentMadeForm({
return (
<div
className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_PAYMENT_MADE)}
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_PAYMENT_MADE,
)}
>
<form onSubmit={handleSubmit}>
<PaymentMadeHeader
@@ -318,17 +322,20 @@ function PaymentMadeForm({
onPaymentNumberChanged={handlePaymentNoChanged}
amountPaid={fullAmountPaid}
/>
<PaymentMadeItemsTable
fullAmount={fullAmount}
paymentEntries={localPaymentEntries}
vendorId={values.vendor_id}
paymentMadeId={paymentMadeId}
onUpdateData={handleUpdataData}
onClickClearAllLines={handleClearAllLines}
errors={errors?.entries}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
vendorPayableBillsEntrie={[]}
/>
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentMadeItemsTable
fullAmount={fullAmount}
paymentEntries={localPaymentEntries}
vendorId={values.vendor_id}
paymentMadeId={paymentMadeId}
onUpdateData={handleUpdataData}
onClickClearAllLines={handleClearAllLines}
errors={errors?.entries}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
vendorPayableBillsEntrie={[]}
/>
</div>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />}

View File

@@ -1,13 +1,12 @@
import React, { useMemo, useCallback } from 'react';
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Intent,
Position,
MenuItem,
Classes,
ControlGroup,
} from '@blueprintjs/core';
import { sumBy } from 'lodash';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import moment from 'moment';
@@ -19,12 +18,15 @@ import {
ContactSelecetList,
ErrorMessage,
FieldRequiredHint,
InputPrependText,
Money,
Hint,
Icon,
} from 'components';
import withVender from 'containers/Vendors/withVendors';
import withAccounts from 'containers/Accounts/withAccounts';
import withSettings from 'containers/Settings/withSettings';
/**
* Payment made header form.
@@ -42,6 +44,9 @@ function PaymentMadeFormHeader({
getFieldProps,
values,
//#withSettings
baseCurrency,
onFullAmountChanged,
//#withVender
@@ -83,7 +88,7 @@ function PaymentMadeFormHeader({
};
const handlePaymentNumberBlur = (event) => {
onPaymentNumberChanged && onPaymentNumberChanged(event.currentTarget.value)
onPaymentNumberChanged && onPaymentNumberChanged(event.currentTarget.value);
};
return (
@@ -104,7 +109,7 @@ function PaymentMadeFormHeader({
<ContactSelecetList
contactsList={vendorItems}
selectedContactId={values.vendor_id}
defaultSelectText={ <T id={'select_vender_account'} /> }
defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={onChangeSelect('vendor_id')}
disabled={!isNewMode}
popoverFill={true}
@@ -117,7 +122,9 @@ function PaymentMadeFormHeader({
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={errors.payment_date && touched.payment_date && Intent.DANGER}
intent={
errors.payment_date && touched.payment_date && Intent.DANGER
}
helperText={
<ErrorMessage name="payment_date" {...{ errors, touched }} />
}
@@ -127,6 +134,9 @@ function PaymentMadeFormHeader({
value={tansformDateValue(values.payment_date)}
onChange={handleDateChange('payment_date')}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
@@ -135,26 +145,32 @@ function PaymentMadeFormHeader({
label={<T id={'full_amount'} />}
inline={true}
className={('form-group--full-amount', Classes.FILL)}
intent={
errors.full_amount && touched.full_amount && Intent.DANGER
}
intent={errors.full_amount && touched.full_amount && Intent.DANGER}
labelInfo={<Hint />}
helperText={
<ErrorMessage name="full_amount" {...{ errors, touched }} />
}
>
<InputGroup
intent={
errors.full_amount && touched.full_amount && Intent.DANGER
}
minimal={true}
value={values.full_amount}
{...getFieldProps('full_amount')}
onBlur={handleFullAmountBlur}
/>
<ControlGroup>
<InputPrependText text={baseCurrency} />
<InputGroup
intent={
errors.full_amount && touched.full_amount && Intent.DANGER
}
minimal={true}
value={values.full_amount}
{...getFieldProps('full_amount')}
onBlur={handleFullAmountBlur}
/>
</ControlGroup>
<a onClick={handleReceiveFullAmountClick} href="#" className={'receive-full-amount'}>
Receive full amount (<Money amount={payableFullAmount} currency={'USD'} />)
<a
onClick={handleReceiveFullAmountClick}
href="#"
className={'receive-full-amount'}
>
Receive full amount (
<Money amount={payableFullAmount} currency={'USD'} />)
</a>
</FormGroup>
@@ -200,7 +216,7 @@ function PaymentMadeFormHeader({
name={'payment_account_id'}
{...{ errors, touched }}
/>
}
}
>
<AccountsSelectList
accounts={accountsList}
@@ -218,7 +234,9 @@ function PaymentMadeFormHeader({
inline={true}
className={classNames('form-group--reference', Classes.FILL)}
intent={errors.reference && touched.reference && Intent.DANGER}
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
helperText={
<ErrorMessage name="reference" {...{ errors, touched }} />
}
>
<InputGroup
intent={errors.reference && touched.reference && Intent.DANGER}
@@ -249,4 +267,7 @@ export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(PaymentMadeFormHeader);

View File

@@ -14,7 +14,7 @@ import {
} from './EstimateForm.schema';
import EstimateFormHeader from './EstimateFormHeader';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import EstimateFormBody from './EstimateFormBody';
import EstimateFloatingActions from './EstimateFloatingActions';
import EstimateFormFooter from './EstimateFormFooter';
import EstimateNumberWatcher from './EstimateNumberWatcher';
@@ -43,9 +43,9 @@ const MIN_LINES_NUMBER = 4;
const defaultEstimate = {
index: 0,
item_id: '',
rate: '',
discount: 0,
quantity: '',
rate: 0,
discount: '',
quantity: 1,
description: '',
};
@@ -242,7 +242,11 @@ const EstimateForm = ({
);
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_ESTIMATE)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_ESTIMATE,
)}>
<Formik
validationSchema={
isNewMode ? CreateEstimateFormSchema : EditEstimateFormSchema
@@ -256,7 +260,7 @@ const EstimateForm = ({
onEstimateNumberChanged={handleEstimateNumberChange}
/>
<EstimateNumberWatcher estimateNumber={estimateNumber} />
<EditableItemsEntriesTable filterSellableItems={true} />
<EstimateFormBody />
<EstimateFormFooter />
<EstimateFloatingActions
isSubmitting={isSubmitting}

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
customer_id: Yup.number()
@@ -40,7 +41,7 @@ const Schema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
discount: Yup.number().nullable().min(0).max(DATATYPES_LENGTH.INT_10),

View File

@@ -0,0 +1,15 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
export default function EstimateFormBody() {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable filterSellableItems={true} />
</div>
)
}

View File

@@ -1,183 +1,26 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import React from 'react';
import { compose } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
Row,
Col,
} from 'components';
import { PageFormBigNumber } from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
import { formatMessage } from 'services/intl';
import EstimateFormHeaderFields from './EstimateFormHeaderFields';
function EstimateFormHeader({
//#withCustomers
customers,
// #withDialogActions
openDialog,
// #ownProps
onEstimateNumberChanged,
}) {
const handleEstimateNumberChange = useCallback(() => {
openDialog('estimate-number-form', {});
}, [openDialog]);
const handleEstimateNumberChanged = (event) => {
saveInvoke(onEstimateNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={'page-form__primary-section'}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(
CLASSES.FILL,
'form-group--customer',
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<EstimateFormHeaderFields
onEstimateNumberChanged={onEstimateNumberChanged}
/>
<Row>
<Col md={8} className={'col--estimate-date'}>
{/* ----------- Estimate date ----------- */}
<FastField name={'estimate_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(
CLASSES.FILL,
'form-group--estimate-date',
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('estimate_date', formatMessage);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col md={4} className={'col--expiration-date'}>
{/* ----------- Expiration date ----------- */}
<FastField name={'expiration_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'expiration_date'} />}
inline={true}
className={classNames(
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
'form-group--expiration-date',
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="expiration_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('expiration_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
{/* ----------- Estimate number ----------- */}
<FastField name={'estimate_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate'} />}
inline={true}
className={('form-group--estimate-number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleEstimateNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleEstimateNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated estimate number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
<PageFormBigNumber label={'Amount'} amount={0} currencyCode={'LYD'} />
</div>
);
}

View File

@@ -0,0 +1,182 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
Row,
Col,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
import { formatMessage } from 'services/intl';
function EstimateFormHeader({
//#withCustomers
customers,
// #withDialogActions
openDialog,
// #ownProps
onEstimateNumberChanged,
}) {
const handleEstimateNumberChange = useCallback(() => {
openDialog('estimate-number-form', {});
}, [openDialog]);
const handleEstimateNumberChanged = (event) => {
saveInvoke(onEstimateNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--customer')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Estimate date ----------- */}
<FastField name={'estimate_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(CLASSES.FILL, 'form-group--estimate-date')}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('estimate_date', formatMessage);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Expiration date ----------- */}
<FastField name={'expiration_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'expiration_date'} />}
inline={true}
className={classNames(
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
'form-group--expiration-date',
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="expiration_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('expiration_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Estimate number ----------- */}
<FastField name={'estimate_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate'} />}
inline={true}
className={('form-group--estimate-number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleEstimateNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleEstimateNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated estimate number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withDialogActions,
)(EstimateFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -9,6 +9,7 @@ import withCustomersActions from 'containers/Customers/withCustomersActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withEstimateActions from './withEstimateActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -24,10 +25,24 @@ function Estimates({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchEstimate = useQuery(
['estimate', id],
(key, _id) => requsetFetchEstimate(_id),
@@ -78,4 +93,5 @@ export default compose(
withCustomersActions,
withItemsActions,
withSettingsActions,
withDashboardActions,
)(Estimates);

View File

@@ -44,8 +44,8 @@ const defaultInvoice = {
index: 0,
item_id: '',
rate: '',
discount: 0,
quantity: '',
discount: '',
quantity: 1,
description: '',
};
@@ -232,7 +232,11 @@ function InvoiceForm({
);
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_INVOICE)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_INVOICE
)}>
<Formik
validationSchema={
isNewMode ? CreateInvoiceFormSchema : EditInvoiceFormSchema
@@ -246,10 +250,13 @@ function InvoiceForm({
onInvoiceNumberChanged={handleInvoiceNumberChanged}
/>
<InvoiceNumberChangeWatcher invoiceNumber={invoiceNumber} />
<EditableItemsEntriesTable
defaultEntry={defaultInvoice}
filterSellableItems={true}
/>
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
defaultEntry={defaultInvoice}
filterSellableItems={true}
/>
</div>
<InvoiceFormFooter />
<InvoiceFloatingActions
isSubmitting={isSubmitting}
@@ -269,7 +276,6 @@ export default compose(
withDashboardActions,
withMediaActions,
withInvoiceDetail(),
withSettings(({ invoiceSettings }) => ({
invoiceNextNumber: invoiceSettings?.nextNumber,
invoiceNumberPrefix: invoiceSettings?.numberPrefix,

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
customer_id: Yup.string()
@@ -39,7 +40,7 @@ const Schema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
discount: Yup.number().nullable().min(0).max(DATATYPES_LENGTH.INT_10),

View File

@@ -1,188 +1,28 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
Row,
Col,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
function InvoiceFormHeader({
//#withCustomers
customers,
//#withDialogActions
openDialog,
import InvoiceFormHeaderFields from './InvoiceFormHeaderFields';
import { PageFormBigNumber } from 'components';
/**
* Invoice form header section.
*/
export default function InvoiceFormHeader({
// #ownProps
onInvoiceNumberChanged,
}) {
const handleInvoiceNumberChange = useCallback(() => {
openDialog('invoice-number-form', {});
}, [openDialog]);
const handleInvoiceNumberChanged = (event) => {
saveInvoke(onInvoiceNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(
'form-group--customer-name',
CLASSES.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<InvoiceFormHeaderFields
onInvoiceNumberChanged={onInvoiceNumberChanged}
/>
<Row>
<Col md={7} className={'col--invoice-date'}>
{/* ----------- Invoice date ----------- */}
<FastField name={'invoice_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(
'form-group--invoice-date',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('invoice_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col md={5} className={'col--due-date'}>
{/* ----------- Due date ----------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames(
'form-group--due-date',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
{/* ----------- Invoice number ----------- */}
<FastField name={'invoice_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_no'} />}
inline={true}
className={classNames('form-group--invoice-no', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_no" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleInvoiceNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleInvoiceNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated invoice number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
<PageFormBigNumber
label={'Due Amount'}
amount={0}
currencyCode={'LYD'}
/>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withDialogActions,
)(InvoiceFormHeader);
}

View File

@@ -0,0 +1,176 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
function InvoiceFormHeaderFields({
// #withCustomers
customers,
// #withDialogActions
openDialog,
// #ownProps
onInvoiceNumberChanged,
}) {
const handleInvoiceNumberChange = useCallback(() => {
openDialog('invoice-number-form', {});
}, [openDialog]);
const handleInvoiceNumberChanged = (event) => {
saveInvoke(onInvoiceNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames('form-group--customer-name', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Invoice date ----------- */}
<FastField name={'invoice_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--invoice-date', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('invoice_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Due date ----------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames('form-group--due-date', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Invoice number ----------- */}
<FastField name={'invoice_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_no'} />}
inline={true}
className={classNames('form-group--invoice-no', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_no" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleInvoiceNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleInvoiceNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated invoice number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withDialogActions,
)(InvoiceFormHeaderFields);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -9,6 +9,7 @@ import withCustomersActions from 'containers/Customers/withCustomersActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withInvoiceActions from './withInvoiceActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -24,15 +25,29 @@ function Invoices({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchInvoice = useQuery(
['invoice', id],
(key, _id) => requsetFetchInvoice(_id),
{ enabled: !!id },
);
);
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
@@ -64,8 +79,8 @@ function Invoices({
name={'invoice-form'}
>
<InvoiceForm
onFormSubmit={handleFormSubmit}
invoiceId={id}
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
</DashboardInsider>
@@ -77,4 +92,5 @@ export default compose(
withCustomersActions,
withItemsActions,
withSettingsActions,
withDashboardActions,
)(Invoices);

View File

@@ -371,6 +371,7 @@ function PaymentReceiveForm({
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_PAYMENT_RECEIVE,
)}
>
@@ -388,16 +389,18 @@ function PaymentReceiveForm({
amountReceived={fullAmountReceived}
onPaymentReceiveNumberChanged={handlePaymentReceiveNumberChanged}
/>
<PaymentReceiveItemsTable
paymentReceiveId={paymentReceiveId}
customerId={values.customer_id}
fullAmount={fullAmount}
onUpdateData={handleUpdataData}
paymentReceiveEntries={localPaymentEntries}
errors={errors?.entries}
onClickClearAllLines={handleClearAllLines}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
/>
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentReceiveItemsTable
paymentReceiveId={paymentReceiveId}
customerId={values.customer_id}
fullAmount={fullAmount}
onUpdateData={handleUpdataData}
paymentReceiveEntries={localPaymentEntries}
errors={errors?.entries}
onClickClearAllLines={handleClearAllLines}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
/>
</div>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />}

View File

@@ -142,6 +142,9 @@ function PaymentReceiveFormHeader({
value={tansformDateValue(values.payment_date)}
onChange={handleDateChange('payment_date')}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: (<Icon icon={'date-range'} />),
}}
/>
</FormGroup>

View File

@@ -32,10 +32,24 @@ function PaymentReceiveFormPage({
requestFetchPaymentReceive,
// #withCustomersActions
requestFetchCustomers
requestFetchCustomers,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const { id: paymentReceiveId } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
// Fetches payment recevie details.
const fetchPaymentReceive = useQuery(
['payment-receive', paymentReceiveId],

View File

@@ -16,7 +16,7 @@ import {
} from './ReceiptForm.schema';
import ReceiptFromHeader from './ReceiptFormHeader';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import ReceiptFormBody from './ReceiptFormBody';
import ReceiptFormFloatingActions from './ReceiptFormFloatingActions';
import ReceiptFormFooter from './ReceiptFormFooter';
import ReceiptNumberWatcher from './ReceiptNumberWatcher';
@@ -245,7 +245,11 @@ function ReceiptForm({
);
return (
<div className={classNames(CLASSES.PAGE_FORM_RECEIPT, CLASSES.PAGE_FORM)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_RECEIPT,
)}>
<Formik
validationSchema={
isNewMode ? CreateReceiptFormSchema : EditReceiptFormSchema
@@ -259,7 +263,7 @@ function ReceiptForm({
onReceiptNumberChanged={handleReceiptNumberChanged}
/>
<ReceiptNumberWatcher receiptNumber={receiptNumber} />
<EditableItemsEntriesTable filterSellableItems={true} />
<ReceiptFormBody />
<ReceiptFormFooter />
<ReceiptFormFloatingActions
isSubmitting={isSubmitting}

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
customer_id: Yup.string()
@@ -40,7 +41,7 @@ const Schema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
discount: Yup.number().nullable().min(0).max(DATATYPES_LENGTH.INT_10),

View File

@@ -0,0 +1,13 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
export default function ExpenseFormBody() {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable filterSellableItems={true} />
</div>
)
}

View File

@@ -1,195 +1,28 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import React from 'react';
import classNames from 'classnames';
import { FastField, ErrorMessage } from 'formik';
import { Money } from 'components';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
saveInvoke,
handleDateChange,
inputIntent,
} from 'utils';
function ReceiptFormHeader({
//#withCustomers
customers,
//#withAccouts
accountsList,
//#withDialogActions
openDialog,
import ReceiptFormHeaderFields from './ReceiptFormHeaderFields';
export default function ReceiptFormHeader({
// #ownProps
onReceiptNumberChanged,
}) {
const handleReceiptNumberChange = useCallback(() => {
openDialog('receipt-number-form', {});
}, [openDialog]);
const handleReceiptNumberChanged = (event) => {
saveInvoke(onReceiptNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--customer')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<ReceiptFormHeaderFields
onReceiptNumberChanged={onReceiptNumberChanged}
/>
{/* ----------- Deposit account ----------- */}
<FastField name={'deposit_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'deposit_account'} />}
className={classNames(
'form-group--deposit-account',
CLASSES.FILL,
)}
inline={true}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'deposit_account_id'} />}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('deposit_account_id', account.id);
}}
defaultSelectText={<T id={'select_deposit_account'} />}
selectedAccountId={value}
filterByTypes={['current_asset']}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt date ----------- */}
<FastField name={'receipt_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt_date'} />}
inline={true}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('receipt_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt number ----------- */}
<FastField name={'receipt_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt'} />}
inline={true}
className={('form-group--receipt_number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleReceiptNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleReceiptNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated receipt number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
<div class="big-amount">
<span class="big-amount__label">Due Amount</span>
<h1 class="big-amount__number">
<Money amount={0} currency={'LYD'} />
</h1>
</div>
</div>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(ReceiptFormHeader);

View File

@@ -0,0 +1,195 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { FastField, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
saveInvoke,
handleDateChange,
inputIntent,
} from 'utils';
function ReceiptFormHeader({
//#withCustomers
customers,
//#withAccouts
accountsList,
//#withDialogActions
openDialog,
// #ownProps
onReceiptNumberChanged,
}) {
const handleReceiptNumberChange = useCallback(() => {
openDialog('receipt-number-form', {});
}, [openDialog]);
const handleReceiptNumberChanged = (event) => {
saveInvoke(onReceiptNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--customer')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Deposit account ----------- */}
<FastField name={'deposit_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'deposit_account'} />}
className={classNames('form-group--deposit-account', CLASSES.FILL)}
inline={true}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'deposit_account_id'} />}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('deposit_account_id', account.id);
}}
defaultSelectText={<T id={'select_deposit_account'} />}
selectedAccountId={value}
filterByTypes={['current_asset']}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt date ----------- */}
<FastField name={'receipt_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt_date'} />}
inline={true}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('receipt_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt number ----------- */}
<FastField name={'receipt_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt'} />}
inline={true}
className={('form-group--receipt_number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleReceiptNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleReceiptNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated receipt number',
position: Position.BOTTOM_LEFT,
}}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(ReceiptFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withReceiptActions from './withReceiptActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -28,10 +29,24 @@ function Receipts({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchReceipt = useQuery(
['receipt', id],
(key, _id) => requestFetchReceipt(_id),
@@ -86,4 +101,5 @@ export default compose(
withItemsActions,
withAccountsActions,
withSettingsActions,
withDashboardActions
)(Receipts);

View File

@@ -482,6 +482,7 @@ export default {
contact: 'Contact',
contacts: 'Contacts',
close_sidebar: 'Close sidebar.',
open_sidebar: 'Open sidebar.',
recalc_report: 'Re-calc Report',
remove_the_line: 'Remove the line',
no_results: 'No results',
@@ -851,4 +852,6 @@ export default {
save_continue_editing: 'Save (continue editing)',
reset: 'Reset ',
save_and_send: 'Save and Send',
posting_date: 'Posting date',
customer: 'Customer',
};

View File

@@ -252,7 +252,7 @@ export default [
},
// Invoices.
{
{
path: `/invoices/:id/edit`,
component: LazyLoader({
loader: () => import('containers/Sales/Invoice/Invoices'),

View File

@@ -298,4 +298,56 @@ export default {
path: ['M7 14l5-5 5 5z'],
viewBox: '0 0 24 24',
},
'date-range': {
path: ['M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z'],
viewBox: '0 0 24 24',
},
'hash': {
path: [
'M14.1683,13.45l.976-3.7971H10.0652L9.1144,13.45Zm2.0976,0H20.57l-.7427,2.1451H15.7281l-1.31,5.2292-2.1511.0208,1.3489-5.25h-5.04l-1.31,5.2292-2.1511.0208,1.35-5.25H2L2.7429,13.45H7.0154l.976-3.7971H3.4307l.7428-2.1451H8.544l1.15-4.4716L11.732,3,10.603,7.5074h5.0954l1.1454-4.4567L18.8864,3l-1.129,4.5072H22l-.7428,2.1451H17.22L16.2689,13.45Z',
],
viewBox: '0 0 24 24',
},
"bookmark": {
path: [
'M19 5H5v16l7-5 7 5V4z',
],
viewBox: '0 0 24 24',
},
"person": {
path: [
'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z',
],
viewBox: '0 0 24 24',
},
"info": {
path: [
'M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z'
],
viewBox: '0 0 24 24',
},
"currency": {
path: [
'M10.9571,13.44a6.9356,6.9356,0,0,0,0-9.2751,4.8008,4.8008,0,1,1,0,9.2751Zm-5.156.1632a4.8008,4.8008,0,1,1,4.8008-4.8008A4.8008,4.8008,0,0,1,5.8011,13.603Z',
],
viewBox: '0 0 18 18',
},
"short-text-16": {
path: [
'M13.6668,7H2V5H13.6668Zm-5.5862,2h-6v2h6Z',
],
viewBox: '0 0 16 16',
},
"tag-16": {
path: [
"M14,4v9H5L2,8.5,5,4Z",
],
viewBox: '0 0 16 16',
},
"payments": {
path: [
'M19 14V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zm-9-1c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm13-6v11c0 1.1-.9 2-2 2H4v-2h17V7h2z',
],
viewBox: '0 0 24 24',
}
};

View File

@@ -9,6 +9,7 @@ const initialState = {
pageHint: '',
preferencesPageTitle: '',
sidebarExpended: true,
previousSidebarExpended: null,
dialogs: {},
topbarEditViewId: null,
requestsLoading: 0,
@@ -62,9 +63,25 @@ const reducerInstance = createReducer(initialState, {
state.requestsLoading = Math.max(requestsLoading, 0);
},
[t.RECORD_SIDEBAR_PREVIOUS_EXPAND]: (state) => {
state.previousSidebarExpended = state.sidebarExpended;
},
[t.SIDEBAR_EXPEND_TOGGLE]: (state) => {
state.sidebarExpended = !state.sidebarExpended;
}
},
[t.SIDEBAR_EXPAND]: (state) => {
state.sidebarExpended = true;
},
[t.SIDEBAR_SHRINK]: (state) => {
state.sidebarExpended = false;
},
[t.RESET_SIDEBAR_PREVIOUS_EXPAND]: (state) => {
state.sidebarExpended = state.previousSidebarExpended;
},
});
export default persistReducer({

View File

@@ -13,4 +13,8 @@ export default {
SET_DASHBOARD_REQUEST_LOADING: 'SET_DASHBOARD_REQUEST_LOADING',
SET_DASHBOARD_REQUEST_COMPLETED: 'SET_DASHBOARD_REQUEST_COMPLETED',
SIDEBAR_EXPEND_TOGGLE: 'SIDEBAR_EXPEND_TOGGLE',
SIDEBAR_EXPAND: 'SIDEBAR_EXPAND',
SIDEBAR_SHRINK: 'SIDEBAR_SHRINK',
RESET_SIDEBAR_PREVIOUS_EXPAND: 'RESET_SIDEBAR_PREVIOUS_EXPAND',
RECORD_SIDEBAR_PREVIOUS_EXPAND: 'RECORD_SIDEBAR_PREVIOUS_EXPAND',
};

View File

@@ -220,49 +220,65 @@ body.authentication {
// .page-form__content
// .page-form__floating-actions
.page-form{
$self: '.page-form';
padding-bottom: 20px;
&__header{
background-color: #fbfbfb;
padding: 30px 20px 20px;
padding-bottom: 6px;
}
&__primary-section{
}
&__header-fields{
width: 85%;
}
&__footer{
padding: 15px;
margin: 15px 0 0 0;
}
&__floating-actions{
position: fixed;
bottom: 0;
width: 100%;
background: #fff;
padding: 14px 18px;
border-top: 1px solid #ececec;
border-top: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0px -1px 4px 0px rgba(0, 0, 0, 0.05);
}
&--strip{
#{$self}__header-fields{
width: 85%;
}
#{$self}__body,
#{$self}__footer{
max-width: 1200px;
}
#{$self}__header{
background-color: #fbfbfb;
padding: 30px 20px 0;
border-bottom: 1px solid #d2dce2;
.bp3-form-group.bp3-inline label.bp3-label{
font-weight: 500;
}
}
#{$self}__body{
padding-top: 15px;
padding-left: 20px;
padding-right: 20px;
}
#{$self}__footer{
margin: 25px 0 0 0;
padding-left: 20px;
padding-right: 20px;
}
}
}
.datatable-editor{
padding: 15px 15px 0;
.bp3-form-group {
margin-bottom: 0;
}
.bp3-form-group {
margin-bottom: 0;
}
.table {
border: 1px solid #d2dce2;
border-bottom: transparent;
border-left: transparent;
.th,
.td {
border-left: 1px dotted rgb(195, 195, 195);
border-left: 1px solid #e2e2e2;
&.index {
text-align: center;
@@ -281,10 +297,10 @@ body.authentication {
.thead {
.tr .th {
padding: 10px 10px;
background-color: #f2f5fa;
background-color: #f0f2f8;
font-size: 14px;
font-weight: 500;
color: #1e1c3e;
color: #1c1940;
&.index > div {
width: 100%;
@@ -294,12 +310,13 @@ body.authentication {
.tbody {
.tr .td {
padding: 7px;
border-bottom: 1px dotted rgb(195, 195, 195);
min-height: 46px;
padding: 5px;
border-bottom: 0;
border-top: 1px dashed #AAA;
min-height: 42px;
&.index {
background-color: #f2f5fa;
background-color: #f0f2f8;
> span {
margin-top: auto;
@@ -310,23 +327,48 @@ body.authentication {
.tr {
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
.form-group--select-list .bp3-button {
border-color: #e5e5e5;
border-color: #ffffff;
color: #222;
border-radius: 3px;
padding-left: 8px;
padding-right: 8px;
}
.form-group--select-list {
.bp3-form-group:not(.bp3-intent-danger) .bp3-input{
border-radius: 2px;
padding-left: 4px;
padding-right: 4px;
&:focus{
box-shadow: 0 0 0 2px #116cd0;
}
}
.form-group--select-list .bp3-button{
padding-left: 6px;
padding-right: 6px;
&:after{
display: none;
}
}
.form-group--select-list,
.bp3-form-group {
&.bp3-intent-danger {
.bp3-button:not(.bp3-minimal) {
border-color: #db3737;
.bp3-button:not(.bp3-minimal),
.bp3-input {
border-color: #F7B6B6;
}
}
}
&:first-of-type{
.td{
border-top: 0;
}
}
&:last-of-type {
.td {
border-bottom: transparent;
.bp3-button,
.bp3-input-group {
@@ -351,6 +393,7 @@ body.authentication {
}
&.row--total {
.account.td,
.debit.td,
.credit.td {
@@ -373,6 +416,7 @@ body.authentication {
width: 100%;
}
}
}
}
}
@@ -408,6 +452,7 @@ body.authentication {
}
&__actions{
margin-top: 12px;
.bp3-button{
padding-left: 10px;
@@ -421,9 +466,30 @@ body.authentication {
}
}
}
}
&.has-total-row{
.table .tbody-inner .tr:last-of-type{
.td{
border-top-width: 2px;
border-top-color: #E9E9EF;
border-top-style: solid;
min-height: 40px;
&:not(.index) {
background-color: #FCFCFD;
}
&.index span{
display: none;
}
}
}
}
}
.cloud-spinner{
position: relative;
@@ -482,7 +548,6 @@ body.authentication {
}
}
.datatable-empty-status{
max-width: 550px;
width: 100%;
@@ -517,4 +582,9 @@ body.authentication {
}
}
}
}
.dropzone-container{
max-width: 250px;
margin-left: auto;
}

Some files were not shown because too many files have changed in this diff Show More