mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 07:40:32 +00:00
feat: add Auto increment in credit note number.
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { useSaveSettings } from 'hooks/query';
|
||||||
|
|
||||||
|
import { CreditNoteNumberDialogProvider } from './CreditNoteNumberDialogProvider';
|
||||||
|
import ReferenceNumberForm from 'containers/JournalNumber/ReferenceNumberForm';
|
||||||
|
|
||||||
|
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||||
|
import withSettings from 'containers/Settings/withSettings';
|
||||||
|
import withSettingsActions from 'containers/Settings/withSettingsActions';
|
||||||
|
import { compose } from 'utils';
|
||||||
|
import {
|
||||||
|
transformFormToSettings,
|
||||||
|
transformSettingsToForm,
|
||||||
|
} from 'containers/JournalNumber/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* credit note number dialog content
|
||||||
|
*/
|
||||||
|
function CreditNoteNumberDialogContent({
|
||||||
|
// #ownProps
|
||||||
|
initialValues,
|
||||||
|
onConfirm,
|
||||||
|
|
||||||
|
// #withSettings
|
||||||
|
nextNumber,
|
||||||
|
numberPrefix,
|
||||||
|
autoIncrement,
|
||||||
|
|
||||||
|
// #withDialogActions
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
const { mutateAsync: saveSettings } = useSaveSettings();
|
||||||
|
const [referenceFormValues, setReferenceFormValues] = React.useState(null);
|
||||||
|
|
||||||
|
// Handle the submit form.
|
||||||
|
const handleSubmitForm = (values, { setSubmitting }) => {
|
||||||
|
// Handle the form success.
|
||||||
|
const handleSuccess = () => {
|
||||||
|
setSubmitting(false);
|
||||||
|
closeDialog('credit-number-form');
|
||||||
|
onConfirm(values);
|
||||||
|
};
|
||||||
|
// Handle the form errors.
|
||||||
|
const handleErrors = () => {
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
if (values.incrementMode === 'manual-transaction') {
|
||||||
|
handleSuccess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Transformes the form values to settings to save it.
|
||||||
|
const options = transformFormToSettings(values, 'credit_note');
|
||||||
|
|
||||||
|
// Save the settings.
|
||||||
|
saveSettings({ options }).then(handleSuccess).catch(handleErrors);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle the dialog close.
|
||||||
|
const handleClose = () => {
|
||||||
|
closeDialog('credit-number-form');
|
||||||
|
};
|
||||||
|
// Handle form change.
|
||||||
|
const handleChange = (values) => {
|
||||||
|
setReferenceFormValues(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Description.
|
||||||
|
const description =
|
||||||
|
referenceFormValues?.incrementMode === 'auto'
|
||||||
|
? intl.get('credit_note.auto_increment.auto')
|
||||||
|
: intl.get('credit_note.auto_increment.manually');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreditNoteNumberDialogProvider>
|
||||||
|
<ReferenceNumberForm
|
||||||
|
initialValues={{
|
||||||
|
...transformSettingsToForm({
|
||||||
|
nextNumber,
|
||||||
|
numberPrefix,
|
||||||
|
autoIncrement,
|
||||||
|
}),
|
||||||
|
...initialValues,
|
||||||
|
}}
|
||||||
|
description={description}
|
||||||
|
onSubmit={handleSubmitForm}
|
||||||
|
onClose={handleClose}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</CreditNoteNumberDialogProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withDialogActions,
|
||||||
|
withSettingsActions,
|
||||||
|
withSettings(({ creditNoteSettings }) => ({
|
||||||
|
autoIncrement: creditNoteSettings?.autoIncrement,
|
||||||
|
nextNumber: creditNoteSettings?.nextNumber,
|
||||||
|
numberPrefix: creditNoteSettings?.numberPrefix,
|
||||||
|
})),
|
||||||
|
)(CreditNoteNumberDialogContent);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DialogContent } from 'components';
|
||||||
|
import { useSettingsCreditNotes } from 'hooks/query';
|
||||||
|
|
||||||
|
const CreditNoteNumberDialogContext = React.createContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Credit Note number dialog provider
|
||||||
|
*/
|
||||||
|
function CreditNoteNumberDialogProvider({ query, ...props }) {
|
||||||
|
const { isLoading: isSettingsLoading } = useSettingsCreditNotes();
|
||||||
|
|
||||||
|
// Provider payload.
|
||||||
|
const provider = {
|
||||||
|
isSettingsLoading,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent isLoading={isSettingsLoading}>
|
||||||
|
<CreditNoteNumberDialogContext.Provider value={provider} {...props} />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCreditNoteNumberDialogContext = () =>
|
||||||
|
React.useContext(CreditNoteNumberDialogContext);
|
||||||
|
|
||||||
|
export { CreditNoteNumberDialogProvider, useCreditNoteNumberDialogContext };
|
||||||
40
src/containers/Dialogs/CreditNoteNumberDialog/index.js
Normal file
40
src/containers/Dialogs/CreditNoteNumberDialog/index.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
||||||
|
import withDialogRedux from 'components/DialogReduxConnect';
|
||||||
|
import { compose, saveInvoke } from 'utils';
|
||||||
|
|
||||||
|
const CreditNoteNumberDialogContent = React.lazy(() =>
|
||||||
|
import('./CreditNoteNumberDialogContent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit note number dialog.
|
||||||
|
*/
|
||||||
|
function CreditNoteNumberDialog({
|
||||||
|
dialogName,
|
||||||
|
payload: { initialFormValues },
|
||||||
|
isOpen,
|
||||||
|
onConfirm,
|
||||||
|
}) {
|
||||||
|
const handleConfirm = (values) => {
|
||||||
|
saveInvoke(onConfirm, values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={<T id={'credit_note_number_settings'} />}
|
||||||
|
name={dialogName}
|
||||||
|
autoFocus={true}
|
||||||
|
canEscapeKeyClose={true}
|
||||||
|
isOpen={isOpen}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<CreditNoteNumberDialogContent
|
||||||
|
initialValues={{ ...initialFormValues }}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
/>
|
||||||
|
</DialogSuspense>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default compose(withDialogRedux())(CreditNoteNumberDialog);
|
||||||
@@ -15,6 +15,7 @@ import CreditNoteFormHeader from './CreditNoteFormHeader';
|
|||||||
import CreditNoteItemsEntriesEditorField from './CreditNoteItemsEntriesEditorField';
|
import CreditNoteItemsEntriesEditorField from './CreditNoteItemsEntriesEditorField';
|
||||||
import CreditNoteFormFooter from './CreditNoteFormFooter';
|
import CreditNoteFormFooter from './CreditNoteFormFooter';
|
||||||
import CreditNoteFloatingActions from './CreditNoteFloatingActions';
|
import CreditNoteFloatingActions from './CreditNoteFloatingActions';
|
||||||
|
import CreditNoteFormDialogs from './CreditNoteFormDialogs';
|
||||||
|
|
||||||
import { AppToaster } from 'components';
|
import { AppToaster } from 'components';
|
||||||
|
|
||||||
@@ -41,6 +42,9 @@ import withCurrentOrganization from 'containers/Organization/withCurrentOrganiza
|
|||||||
*/
|
*/
|
||||||
function CreditNoteForm({
|
function CreditNoteForm({
|
||||||
// #withSettings
|
// #withSettings
|
||||||
|
creditAutoIncrement,
|
||||||
|
creditNumberPrefix,
|
||||||
|
creditNextNumber,
|
||||||
|
|
||||||
// #withCurrentOrganization
|
// #withCurrentOrganization
|
||||||
organization: { base_currency },
|
organization: { base_currency },
|
||||||
@@ -56,6 +60,9 @@ function CreditNoteForm({
|
|||||||
editCreditNoteMutate,
|
editCreditNoteMutate,
|
||||||
} = useCreditNoteFormContext();
|
} = useCreditNoteFormContext();
|
||||||
|
|
||||||
|
// Credit number.
|
||||||
|
const creditNumber = transactionNumber(creditNumberPrefix, creditNextNumber);
|
||||||
|
|
||||||
// Initial values.
|
// Initial values.
|
||||||
const initialValues = React.useMemo(
|
const initialValues = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -63,6 +70,9 @@ function CreditNoteForm({
|
|||||||
? { ...transformToEditForm(creditNote), currency_code: base_currency }
|
? { ...transformToEditForm(creditNote), currency_code: base_currency }
|
||||||
: {
|
: {
|
||||||
...defaultCreditNote,
|
...defaultCreditNote,
|
||||||
|
...(creditAutoIncrement && {
|
||||||
|
credit_note_number: creditNumber,
|
||||||
|
}),
|
||||||
entries: orderingLinesIndexes(defaultCreditNote.entries),
|
entries: orderingLinesIndexes(defaultCreditNote.entries),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -144,10 +154,17 @@ function CreditNoteForm({
|
|||||||
<CreditNoteItemsEntriesEditorField />
|
<CreditNoteItemsEntriesEditorField />
|
||||||
<CreditNoteFormFooter />
|
<CreditNoteFormFooter />
|
||||||
<CreditNoteFloatingActions />
|
<CreditNoteFloatingActions />
|
||||||
|
<CreditNoteFormDialogs />
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default compose(
|
||||||
export default compose(withCurrentOrganization())(CreditNoteForm);
|
withSettings(({ creditNoteSettings }) => ({
|
||||||
|
creditAutoIncrement: creditNoteSettings?.autoIncrement,
|
||||||
|
creditNextNumber: creditNoteSettings?.nextNumber,
|
||||||
|
creditNumberPrefix: creditNoteSettings?.numberPrefix,
|
||||||
|
})),
|
||||||
|
withCurrentOrganization(),
|
||||||
|
)(CreditNoteForm);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CreditNoteNumberDialog from '../../../Dialogs/CreditNoteNumberDialog';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit note form dialogs.
|
||||||
|
*/
|
||||||
|
export default function CreditNoteFormDialogs() {
|
||||||
|
// Update the form once the credit number form submit confirm.
|
||||||
|
const handleCreditNumberFormConfirm = ({ incrementNumber, manually }) => {
|
||||||
|
setFieldValue('credit_note_number', incrementNumber || '');
|
||||||
|
setFieldValue('credit_note_no_manually', manually);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setFieldValue } = useFormikContext();
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<CreditNoteNumberDialog
|
||||||
|
dialogName={'credit-number-form'}
|
||||||
|
onConfirm={handleCreditNumberFormConfirm}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormGroup, InputGroup, Position } from '@blueprintjs/core';
|
import {
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
Position,
|
||||||
|
ControlGroup,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import { DateInput } from '@blueprintjs/datetime';
|
import { DateInput } from '@blueprintjs/datetime';
|
||||||
import { FastField, Field, ErrorMessage } from 'formik';
|
import { FastField, Field, ErrorMessage } from 'formik';
|
||||||
import { CLASSES } from 'common/classes';
|
import { CLASSES } from 'common/classes';
|
||||||
@@ -7,10 +12,14 @@ import classNames from 'classnames';
|
|||||||
import {
|
import {
|
||||||
ContactSelecetList,
|
ContactSelecetList,
|
||||||
FieldRequiredHint,
|
FieldRequiredHint,
|
||||||
|
InputPrependButton,
|
||||||
Icon,
|
Icon,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
} from 'components';
|
} from 'components';
|
||||||
import { customerNameFieldShouldUpdate } from './utils';
|
import {
|
||||||
|
customerNameFieldShouldUpdate,
|
||||||
|
useObserveCreditNoSettings,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
|
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
|
||||||
import withSettings from 'containers/Settings/withSettings';
|
import withSettings from 'containers/Settings/withSettings';
|
||||||
@@ -27,10 +36,40 @@ import {
|
|||||||
/**
|
/**
|
||||||
* Credit note form header fields.
|
* Credit note form header fields.
|
||||||
*/
|
*/
|
||||||
function CreditNoteFormHeaderFields() {
|
function CreditNoteFormHeaderFields({
|
||||||
|
// #withDialogActions
|
||||||
|
openDialog,
|
||||||
|
|
||||||
|
// #withSettings
|
||||||
|
creditAutoIncrement,
|
||||||
|
creditNumberPrefix,
|
||||||
|
creditNextNumber,
|
||||||
|
}) {
|
||||||
// Credit note form context.
|
// Credit note form context.
|
||||||
const { customers } = useCreditNoteFormContext();
|
const { customers } = useCreditNoteFormContext();
|
||||||
|
|
||||||
|
// Handle credit number changing.
|
||||||
|
const handleCreditNumberChange = () => {
|
||||||
|
openDialog('credit-number-form');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle credit no. field blur.
|
||||||
|
const handleCreditNoBlur = (form, field) => (event) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
|
||||||
|
if (field.value !== newValue && creditAutoIncrement) {
|
||||||
|
openDialog('credit-number-form', {
|
||||||
|
initialFormValues: {
|
||||||
|
manualTransactionNo: newValue,
|
||||||
|
incrementMode: 'manual-transaction',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Syncs credit number settings with form.
|
||||||
|
useObserveCreditNoSettings(creditNumberPrefix, creditNextNumber);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
|
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
|
||||||
{/* ----------- Customer name ----------- */}
|
{/* ----------- Customer name ----------- */}
|
||||||
@@ -96,11 +135,34 @@ function CreditNoteFormHeaderFields() {
|
|||||||
label={<T id={'credit_note.label_credit_note'} />}
|
label={<T id={'credit_note.label_credit_note'} />}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
inline={true}
|
inline={true}
|
||||||
className={classNames('form-group--credit_note_number', CLASSES.FILL)}
|
className={classNames(
|
||||||
|
'form-group--credit_note_number',
|
||||||
|
CLASSES.FILL,
|
||||||
|
)}
|
||||||
intent={inputIntent({ error, touched })}
|
intent={inputIntent({ error, touched })}
|
||||||
helperText={<ErrorMessage name="credit_note_number" />}
|
helperText={<ErrorMessage name="credit_note_number" />}
|
||||||
>
|
>
|
||||||
<InputGroup minimal={true} {...field} />
|
<ControlGroup fill={true}>
|
||||||
|
<InputGroup
|
||||||
|
minimal={true}
|
||||||
|
value={field.value}
|
||||||
|
asyncControl={true}
|
||||||
|
onBlur={handleCreditNoBlur(form, field)}
|
||||||
|
/>
|
||||||
|
<InputPrependButton
|
||||||
|
buttonProps={{
|
||||||
|
onClick: handleCreditNumberChange,
|
||||||
|
icon: <Icon icon={'settings-18'} />,
|
||||||
|
}}
|
||||||
|
tooltip={true}
|
||||||
|
tooltipProps={{
|
||||||
|
content: (
|
||||||
|
<T id={'setting_your_auto_generated_credit_note_number'} />
|
||||||
|
),
|
||||||
|
position: Position.BOTTOM_LEFT,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ControlGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -121,5 +183,11 @@ function CreditNoteFormHeaderFields() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default compose(
|
||||||
export default CreditNoteFormHeaderFields;
|
withDialogActions,
|
||||||
|
withSettings(({ creditNoteSettings }) => ({
|
||||||
|
creditAutoIncrement: creditNoteSettings?.autoIncrement,
|
||||||
|
creditNextNumber: creditNoteSettings?.nextNumber,
|
||||||
|
creditNumberPrefix: creditNoteSettings?.numberPrefix,
|
||||||
|
})),
|
||||||
|
)(CreditNoteFormHeaderFields);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useEditCreditNote,
|
useEditCreditNote,
|
||||||
useItems,
|
useItems,
|
||||||
useCustomers,
|
useCustomers,
|
||||||
|
useSettingsCreditNotes,
|
||||||
} from 'hooks/query';
|
} from 'hooks/query';
|
||||||
|
|
||||||
const CreditNoteFormContext = React.createContext();
|
const CreditNoteFormContext = React.createContext();
|
||||||
@@ -40,6 +41,9 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle fetching settings.
|
||||||
|
useSettingsCreditNotes();
|
||||||
|
|
||||||
// Create and edit credit note mutations.
|
// Create and edit credit note mutations.
|
||||||
const { mutateAsync: createCreditNoteMutate } = useCreateCreditNote();
|
const { mutateAsync: createCreditNoteMutate } = useCreateCreditNote();
|
||||||
const { mutateAsync: editCreditNoteMutate } = useEditCreditNote();
|
const { mutateAsync: editCreditNoteMutate } = useEditCreditNote();
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import {
|
|||||||
defaultFastFieldShouldUpdate,
|
defaultFastFieldShouldUpdate,
|
||||||
transformToForm,
|
transformToForm,
|
||||||
repeatValue,
|
repeatValue,
|
||||||
|
transactionNumber,
|
||||||
orderingLinesIndexes,
|
orderingLinesIndexes,
|
||||||
} from 'utils';
|
} from 'utils';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
updateItemsEntriesTotal,
|
updateItemsEntriesTotal,
|
||||||
@@ -115,3 +117,15 @@ export const entriesFieldShouldUpdate = (newProps, oldProps) => {
|
|||||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs invoice no. settings with form.
|
||||||
|
*/
|
||||||
|
export const useObserveCreditNoSettings = (prefix, nextNumber) => {
|
||||||
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const creditNo = transactionNumber(prefix, nextNumber);
|
||||||
|
setFieldValue('credit_note_number', creditNo);
|
||||||
|
}, [setFieldValue, prefix, nextNumber]);
|
||||||
|
};
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function CreditNotesActionsBar({
|
|||||||
|
|
||||||
// Handle table row size change.
|
// Handle table row size change.
|
||||||
const handleTableRowSizeChange = (size) => {
|
const handleTableRowSizeChange = (size) => {
|
||||||
addSetting('creditNotes', 'tableSize', size);
|
addSetting('creditNote', 'tableSize', size);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ const commonInvalidateQueries = (queryClient) => {
|
|||||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||||
queryClient.invalidateQueries(t.ACCOUNT);
|
queryClient.invalidateQueries(t.ACCOUNT);
|
||||||
|
|
||||||
|
// Invalidate settings.
|
||||||
|
queryClient.invalidateQueries([t.SETTING, t.SETTING_CREDIT_NOTES]);
|
||||||
|
|
||||||
// Invalidate financial reports.
|
// Invalidate financial reports.
|
||||||
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
|
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
|
||||||
};
|
};
|
||||||
@@ -95,8 +98,6 @@ const transformInvoices = (res) => ({
|
|||||||
* Retrieve credit notes list with pagination meta.
|
* Retrieve credit notes list with pagination meta.
|
||||||
*/
|
*/
|
||||||
export function useCreditNotes(query, props) {
|
export function useCreditNotes(query, props) {
|
||||||
|
|
||||||
|
|
||||||
return useRequestQuery(
|
return useRequestQuery(
|
||||||
[t.CREDIT_NOTES, query],
|
[t.CREDIT_NOTES, query],
|
||||||
{ method: 'get', url: 'sales/credit_notes', params: query },
|
{ method: 'get', url: 'sales/credit_notes', params: query },
|
||||||
|
|||||||
Reference in New Issue
Block a user