mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
- feat: Update react-query package to V 2.1.1.
- feat: Favicon setup. - feat: Fix accounts inactivate/activate 1 account. - feat: Seed accounts, expenses and manual journals resource fields. - feat: Validate make journal receivable/payable without contact. - feat: Validate make journal contact without receivable or payable. - feat: More components abstractions. - feat: Use reselect.js to memorize components properties. - fix: Journal type of manual journal. - fix: Sidebar style optimization. - fix: Data-table check-box style optimization. - fix: Data-table spinner style dimensions. - fix: Submit journal with contact_id and contact_type.
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import { useRouteMatch, useHistory } from 'react-router-dom';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
@@ -28,12 +29,13 @@ import withExpensesActions from 'containers/Expenses/withExpensesActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function ExpenseActionsBar({
|
||||
function ExpensesActionsBar({
|
||||
// #withResourceDetail
|
||||
resourceFields,
|
||||
|
||||
//#withExpenses
|
||||
expensesViews,
|
||||
|
||||
//#withExpensesActions
|
||||
addExpensesTableQueries,
|
||||
|
||||
@@ -44,17 +46,20 @@ function ExpenseActionsBar({
|
||||
const { path } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
|
||||
const viewsMenuItems = expensesViews.map((view) => {
|
||||
return (
|
||||
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
|
||||
);
|
||||
});
|
||||
const viewsMenuItems = expensesViews.map((view) => (
|
||||
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
|
||||
));
|
||||
|
||||
const onClickNewExpense = useCallback(() => {
|
||||
history.push('/expenses/new');
|
||||
}, [history]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
initialCondition: {
|
||||
fieldKey: 'reference_no',
|
||||
compatator: 'contains',
|
||||
value: '',
|
||||
},
|
||||
fields: resourceFields,
|
||||
onFilterChange: (filterConditions) => {
|
||||
addExpensesTableQueries({
|
||||
@@ -97,6 +102,7 @@ function ExpenseActionsBar({
|
||||
onClick={onClickNewExpense}
|
||||
/>
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={filterDropdown}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
@@ -118,6 +124,11 @@ function ExpenseActionsBar({
|
||||
/>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||
@@ -133,7 +144,14 @@ function ExpenseActionsBar({
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
resourceName: 'expenses',
|
||||
});
|
||||
|
||||
const withExpensesActionsBar = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withExpensesActionsBar,
|
||||
withDialogActions,
|
||||
withResourceDetail(({ resourceFields }) => ({
|
||||
resourceFields,
|
||||
@@ -142,4 +160,4 @@ export default compose(
|
||||
expensesViews,
|
||||
})),
|
||||
withExpensesActions,
|
||||
)(ExpenseActionsBar);
|
||||
)(ExpensesActionsBar);
|
||||
|
||||
@@ -29,10 +29,11 @@ import withViewDetails from 'containers/Views/withViewDetails';
|
||||
import withExpenses from 'containers/Expenses/withExpenses';
|
||||
import withExpensesActions from 'containers/Expenses/withExpensesActions';
|
||||
|
||||
function ExpenseDataTable({
|
||||
function ExpensesDataTable({
|
||||
//#withExpenes
|
||||
expenses,
|
||||
expensesCurrentPage,
|
||||
expensesLoading,
|
||||
expensesPagination,
|
||||
|
||||
// #withDashboardActions
|
||||
changeCurrentView,
|
||||
@@ -101,7 +102,7 @@ function ExpenseDataTable({
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'view_details' })} />
|
||||
<MenuDivider />
|
||||
<If condition={expenses.published}>
|
||||
<If condition={expense.published}>
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'publish_expense' })}
|
||||
onClick={handlePublishExpense(expense)}
|
||||
@@ -142,7 +143,7 @@ function ExpenseDataTable({
|
||||
{
|
||||
id: 'payment_date',
|
||||
Header: formatMessage({ id: 'payment_date' }),
|
||||
accessor: () => moment().format('YYYY-MM-DD'),
|
||||
accessor: () => moment().format('YYYY MMM DD'),
|
||||
width: 150,
|
||||
className: 'payment_date',
|
||||
},
|
||||
@@ -250,15 +251,19 @@ function ExpenseDataTable({
|
||||
<LoadingIndicator loading={loading} mount={false}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={expenses}
|
||||
data={expensesCurrentPage}
|
||||
onFetchData={handleDataTableFetchData}
|
||||
manualSortBy={true}
|
||||
selectionColumn={true}
|
||||
noInitialFetch={true}
|
||||
sticky={true}
|
||||
loading={expensesLoading && !initialMount}
|
||||
loading={expensesLoading}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
rowContextMenu={onRowContextMenu}
|
||||
pagination={true}
|
||||
pagesCount={expensesPagination.pagesCount}
|
||||
initialPageSize={expensesPagination.pageSize}
|
||||
initialPageIndex={expensesPagination.page - 1}
|
||||
/>
|
||||
</LoadingIndicator>
|
||||
</div>
|
||||
@@ -269,9 +274,10 @@ export default compose(
|
||||
withDialogActions,
|
||||
withDashboardActions,
|
||||
withExpensesActions,
|
||||
withExpenses(({ expenses, expensesLoading }) => ({
|
||||
expenses,
|
||||
withExpenses(({ expensesCurrentPage, expensesLoading, expensesPagination }) => ({
|
||||
expensesCurrentPage,
|
||||
expensesLoading,
|
||||
expensesPagination,
|
||||
})),
|
||||
withViewDetails,
|
||||
)(ExpenseDataTable);
|
||||
)(ExpensesDataTable);
|
||||
|
||||
@@ -99,7 +99,6 @@ function ExpenseForm({
|
||||
description: Yup.string()
|
||||
.trim()
|
||||
.label(formatMessage({ id: 'description' })),
|
||||
|
||||
publish: Yup.boolean().label(formatMessage({ id: 'publish' })),
|
||||
categories: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
@@ -258,8 +257,6 @@ function ExpenseForm({
|
||||
},
|
||||
});
|
||||
|
||||
console.log(formik.values, 'VALUES');
|
||||
|
||||
const handleSubmitClick = useCallback(
|
||||
(payload) => {
|
||||
setPayload(payload);
|
||||
@@ -285,8 +282,6 @@ function ExpenseForm({
|
||||
},
|
||||
[setDeletedFiles, deletedFiles],
|
||||
);
|
||||
// @todo @mohamed
|
||||
const fetchHook = useQuery('expense-form', () => requestFetchExpensesTable());
|
||||
|
||||
return (
|
||||
<div className={'dashboard__insider--expense-form'}>
|
||||
@@ -334,5 +329,5 @@ export default compose(
|
||||
withAccountsActions,
|
||||
withDashboardActions,
|
||||
withMediaActions,
|
||||
withExpneseDetail,
|
||||
withExpneseDetail(),
|
||||
)(ExpenseForm);
|
||||
|
||||
@@ -14,9 +14,13 @@ import moment from 'moment';
|
||||
import { momentFormatter, compose } from 'utils';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'components/Icon';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import { ListSelect } from 'components';
|
||||
import {
|
||||
ListSelect,
|
||||
ErrorMessage,
|
||||
Icon,
|
||||
FieldRequiredHint,
|
||||
Hint,
|
||||
} from 'components';
|
||||
import withCurrencies from 'containers/Currencies/withCurrencies';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
@@ -35,11 +39,6 @@ function ExpenseFormHeader({
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
// @todo @mohamed reusable components.
|
||||
const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
|
||||
|
||||
const requiredSpan = useMemo(() => <span className="required">*</span>, []);
|
||||
|
||||
const currencyCodeRenderer = useCallback((item, { handleClick }) => {
|
||||
return (
|
||||
<MenuItem key={item.id} text={item.currency_code} onClick={handleClick} />
|
||||
@@ -121,7 +120,7 @@ function ExpenseFormHeader({
|
||||
<FormGroup
|
||||
label={<T id={'beneficiary'} />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
labelInfo={infoIcon}
|
||||
labelInfo={<Hint />}
|
||||
intent={errors.beneficiary && touched.beneficiary && Intent.DANGER}
|
||||
helperText={
|
||||
<ErrorMessage name={'beneficiary'} {...{ errors, touched }} />
|
||||
@@ -150,7 +149,7 @@ function ExpenseFormHeader({
|
||||
'form-group--select-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
labelInfo={requiredSpan}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
intent={
|
||||
errors.payment_account_id &&
|
||||
touched.payment_account_id &&
|
||||
@@ -183,7 +182,7 @@ function ExpenseFormHeader({
|
||||
<Col width={300}>
|
||||
<FormGroup
|
||||
label={<T id={'payment_date'} />}
|
||||
labelInfo={infoIcon}
|
||||
labelInfo={<Hint />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
intent={
|
||||
errors.payment_date && touched.payment_date && Intent.DANGER
|
||||
@@ -234,10 +233,7 @@ function ExpenseFormHeader({
|
||||
<Col width={200}>
|
||||
<FormGroup
|
||||
label={<T id={'ref_no'} />}
|
||||
className={classNames(
|
||||
'form-group--ref_no',
|
||||
Classes.FILL,
|
||||
)}
|
||||
className={classNames('form-group--ref_no', Classes.FILL)}
|
||||
intent={
|
||||
errors.reference_no && touched.reference_no && Intent.DANGER
|
||||
}
|
||||
|
||||
@@ -1,113 +1,94 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import {
|
||||
Alignment,
|
||||
Navbar,
|
||||
NavbarGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams, withRouter } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import { pick, debounce } from 'lodash';
|
||||
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
import Icon from 'components/Icon';
|
||||
import { DashboardViewsTabs } from 'components';
|
||||
|
||||
import withExpenses from './withExpenses';
|
||||
import withExpensesActions from './withExpensesActions';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withViewDetails from 'containers/Views/withViewDetails';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function ExpenseViewTabs({
|
||||
//#withExpenses
|
||||
// #withExpenses
|
||||
expensesViews,
|
||||
|
||||
//#withExpensesActions
|
||||
// #withViewDetails
|
||||
viewItem,
|
||||
|
||||
// #withExpensesActions
|
||||
addExpensesTableQueries,
|
||||
changeExpensesView,
|
||||
|
||||
// #withDashboardActions
|
||||
setTopbarEditView,
|
||||
|
||||
// #ownProps
|
||||
customViewChanged,
|
||||
onViewChanged,
|
||||
changePageSubtitle,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { custom_view_id: customViewId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
changeExpensesView(customViewId || -1);
|
||||
setTopbarEditView(customViewId);
|
||||
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
|
||||
|
||||
addExpensesTableQueries({
|
||||
custom_view_id: customViewId,
|
||||
});
|
||||
return () => {
|
||||
setTopbarEditView(null);
|
||||
changePageSubtitle('');
|
||||
changeExpensesView(null);
|
||||
};
|
||||
}, [customViewId, addExpensesTableQueries, changeExpensesView]);
|
||||
|
||||
const debounceChangeHistory = useRef(
|
||||
debounce((toUrl) => {
|
||||
history.push(toUrl);
|
||||
}, 250),
|
||||
);
|
||||
|
||||
const handleTabsChange = (viewId) => {
|
||||
const toPath = viewId ? `${viewId}/custom_view` : '';
|
||||
debounceChangeHistory.current(`/expenses/${toPath}`);
|
||||
setTopbarEditView(viewId);
|
||||
};
|
||||
|
||||
const tabs = expensesViews.map((view) => ({
|
||||
...pick(view, ['name', 'id']),
|
||||
}));
|
||||
|
||||
// Handle click a new view tab.
|
||||
const handleClickNewView = () => {
|
||||
setTopbarEditView(null);
|
||||
history.push('/custom_views/expenses/new');
|
||||
};
|
||||
|
||||
const handleViewLinkClick = () => {
|
||||
setTopbarEditView(customViewId);
|
||||
};
|
||||
|
||||
useUpdateEffect(() => {
|
||||
customViewChanged && customViewChanged(customViewId);
|
||||
|
||||
addExpensesTableQueries({
|
||||
custom_view_id: customViewId || null,
|
||||
});
|
||||
onViewChanged && onViewChanged(customViewId);
|
||||
}, [customViewId]);
|
||||
|
||||
useEffect(() => {
|
||||
addExpensesTableQueries({
|
||||
custom_view_id: customViewId,
|
||||
});
|
||||
}, [customViewId, addExpensesTableQueries]);
|
||||
|
||||
const tabs = expensesViews.map((view) => {
|
||||
const baseUrl = '/expenses/new';
|
||||
const link = (
|
||||
<Link
|
||||
to={`${baseUrl}/${view.id}/custom_view`}
|
||||
onClick={handleViewLinkClick}
|
||||
>
|
||||
{view.name}
|
||||
</Link>
|
||||
);
|
||||
return <Tab id={`custom_view_${view.id}`} title={link} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<Navbar className={'navbar--dashboard-views'}>
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<Tabs
|
||||
id="navbar"
|
||||
large={true}
|
||||
selectedTabId={`custom_view_${customViewId}`}
|
||||
className="tabs--dashboard-views"
|
||||
>
|
||||
<Tab
|
||||
id="all"
|
||||
title={
|
||||
<Link to={``}>
|
||||
<T id={'all'} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
{tabs}
|
||||
<Button
|
||||
className="button--new-view"
|
||||
icon={<Icon icon="plus" />}
|
||||
onClick={handleClickNewView}
|
||||
minimal={true}
|
||||
/>
|
||||
</Tabs>
|
||||
<DashboardViewsTabs
|
||||
initialViewId={customViewId}
|
||||
baseUrl={'/expenses'}
|
||||
tabs={tabs}
|
||||
onNewViewTabClick={handleClickNewView}
|
||||
onChange={handleTabsChange}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
// Mapping view id from matched route params.
|
||||
viewId: ownProps.match.params.custom_view_id,
|
||||
});
|
||||
|
||||
@@ -115,6 +96,7 @@ const withExpensesViewTabs = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
withViewDetails(),
|
||||
withExpensesViewTabs,
|
||||
withExpenses(({ expensesViews }) => ({
|
||||
expensesViews,
|
||||
|
||||
@@ -12,18 +12,21 @@ import ExpenseDataTable from 'containers/Expenses/ExpenseDataTable';
|
||||
import ExpenseActionsBar from 'containers/Expenses/ExpenseActionsBar';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withResourceActions from 'containers/Resources/withResourcesActions';
|
||||
import withExpenses from 'containers/Expenses/withExpenses';
|
||||
import withExpensesActions from 'containers/Expenses/withExpensesActions';
|
||||
import withViewsActions from 'containers/Views/withViewsActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
function ExpensesList({
|
||||
// #withDashboardActions
|
||||
changePageTitle,
|
||||
|
||||
// #withViewsActions
|
||||
requestFetchResourceViews,
|
||||
requestFetchResourceFields,
|
||||
|
||||
// #withExpenses
|
||||
expensesTableQuery,
|
||||
@@ -44,13 +47,18 @@ function ExpensesList({
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [bulkDelete, setBulkDelete] = useState(false);
|
||||
|
||||
const fetchViews = useQuery('expenses-resource-views', () => {
|
||||
return requestFetchResourceViews('expenses');
|
||||
});
|
||||
const fetchResourceViews = useQuery(
|
||||
['resource-views', 'expenses'],
|
||||
(key, resourceName) => requestFetchResourceViews(resourceName),
|
||||
);
|
||||
|
||||
const fetchExpenses = useQuery(
|
||||
['expenses-table', expensesTableQuery],
|
||||
() => requestFetchExpensesTable(),
|
||||
const fetchResourceFields = useQuery(
|
||||
['resource-fields', 'expenses'],
|
||||
(key, resourceName) => requestFetchResourceFields(resourceName),
|
||||
);
|
||||
|
||||
const fetchExpenses = useQuery(['expenses-table', expensesTableQuery], () =>
|
||||
requestFetchExpensesTable(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,6 +140,8 @@ function ExpensesList({
|
||||
// Handle fetch data of manual jouranls datatable.
|
||||
const handleFetchData = useCallback(
|
||||
({ pageIndex, pageSize, sortBy }) => {
|
||||
const page = pageIndex + 1;
|
||||
|
||||
addExpensesTableQueries({
|
||||
...(sortBy.length > 0
|
||||
? {
|
||||
@@ -139,6 +149,8 @@ function ExpensesList({
|
||||
sort_order: sortBy[0].desc ? 'desc' : 'asc',
|
||||
}
|
||||
: {}),
|
||||
page_size: pageSize,
|
||||
page,
|
||||
});
|
||||
},
|
||||
[addExpensesTableQueries],
|
||||
@@ -166,7 +178,7 @@ function ExpensesList({
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={fetchViews.isFetching}
|
||||
loading={fetchResourceViews.isFetching || fetchResourceFields.isFetching}
|
||||
name={'expenses'}
|
||||
>
|
||||
<ExpenseActionsBar
|
||||
@@ -187,6 +199,7 @@ function ExpensesList({
|
||||
<ExpenseViewTabs />
|
||||
|
||||
<ExpenseDataTable
|
||||
loading={fetchExpenses.isFetching}
|
||||
onDeleteExpense={handleDeleteExpense}
|
||||
onFetchData={handleFetchData}
|
||||
onEditExpense={handleEidtExpense}
|
||||
@@ -237,4 +250,5 @@ export default compose(
|
||||
withExpensesActions,
|
||||
withExpenses(({ expensesTableQuery }) => ({ expensesTableQuery })),
|
||||
withViewsActions,
|
||||
withResourceActions
|
||||
)(ExpensesList);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getExpenseById } from 'store/expenses/expenses.reducer';
|
||||
import { getExpenseByIdFactory } from 'store/expenses/expenses.selectors';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
expenseDetail: getExpenseById(state, props.expenseId),
|
||||
});
|
||||
export default () => {
|
||||
const getExpenseById = getExpenseByIdFactory();
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
const mapStateToProps = (state, props) => ({
|
||||
expenseDetail: getExpenseById(state, props),
|
||||
});
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
@@ -1,14 +1,26 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getResourceViews } from 'store/customViews/customViews.selectors';
|
||||
import { getExpensesItems } from 'store/expenses/expenses.selectors';
|
||||
import {
|
||||
getExpensesCurrentPageFactory,
|
||||
getExpenseByIdFactory,
|
||||
getExpensesTableQuery,
|
||||
getExpensesPaginationMetaFactory,
|
||||
} from 'store/expenses/expenses.selectors';
|
||||
|
||||
export default (mapState) => {
|
||||
const getExpensesItems = getExpensesCurrentPageFactory();
|
||||
const getExpensesPaginationMeta = getExpensesPaginationMetaFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const query = getExpensesTableQuery(state, props);
|
||||
|
||||
|
||||
const mapped = {
|
||||
expenses: getExpensesItems(state, state.expenses.currentViewId),
|
||||
expensesCurrentPage: getExpensesItems(state, props, query),
|
||||
expensesViews: getResourceViews(state, props, 'expenses'),
|
||||
expensesItems: state.expenses.items,
|
||||
expensesTableQuery: state.expenses.tableQuery,
|
||||
expensesTableQuery: query,
|
||||
expensesPagination: getExpensesPaginationMeta(state, props),
|
||||
expensesLoading: state.expenses.loading,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
|
||||
@@ -20,7 +20,7 @@ export const mapDispatchToProps = (dispatch) => ({
|
||||
requestPublishExpense: (id) => dispatch(publishExpense({ id })),
|
||||
requestDeleteBulkExpenses: (ids) => dispatch(deleteBulkExpenses({ ids })),
|
||||
|
||||
changeCurrentView: (id) =>
|
||||
changeExpensesView: (id) =>
|
||||
dispatch({
|
||||
type: t.EXPENSES_SET_CURRENT_VIEW,
|
||||
currentViewId: parseInt(id, 10),
|
||||
|
||||
Reference in New Issue
Block a user