mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE-208,212): enhance PurchaseRequestForm with area and location fields, update validation and data handling for new inputs
This commit is contained in:
@@ -7,6 +7,16 @@ type PurchaseRequestFormSchemaType = {
|
|||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
supplier_id: number;
|
supplier_id: number;
|
||||||
|
area?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
area_id: number;
|
||||||
|
location?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
location_id: number;
|
||||||
credit_term: number | string;
|
credit_term: number | string;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
purchase_items: {
|
purchase_items: {
|
||||||
@@ -90,6 +100,22 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSche
|
|||||||
.required('Supplier wajib diisi!')
|
.required('Supplier wajib diisi!')
|
||||||
.min(1, 'Supplier wajib diisi!')
|
.min(1, 'Supplier wajib diisi!')
|
||||||
.typeError('Supplier wajib diisi!'),
|
.typeError('Supplier wajib diisi!'),
|
||||||
|
area: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
area_id: Yup.number()
|
||||||
|
.required('Area wajib diisi!')
|
||||||
|
.min(1, 'Area wajib diisi!')
|
||||||
|
.typeError('Area wajib diisi!'),
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
location_id: Yup.number()
|
||||||
|
.required('Lokasi wajib diisi!')
|
||||||
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
|
.typeError('Lokasi wajib diisi!'),
|
||||||
credit_term: Yup.number()
|
credit_term: Yup.number()
|
||||||
.required('Termin kredit wajib diisi!')
|
.required('Termin kredit wajib diisi!')
|
||||||
.min(1, 'Termin kredit tidak boleh negatif!')
|
.min(1, 'Termin kredit tidak boleh negatif!')
|
||||||
@@ -118,6 +144,10 @@ export const getPurchaseRequestFormInitialValues = (
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
supplier_id: initialValues?.supplier?.id ?? 0,
|
supplier_id: initialValues?.supplier?.id ?? 0,
|
||||||
|
area: null,
|
||||||
|
area_id: 0,
|
||||||
|
location: null,
|
||||||
|
location_id: 0,
|
||||||
credit_term: initialValues?.credit_term ?? '',
|
credit_term: initialValues?.credit_term ?? '',
|
||||||
notes: initialValues?.notes ?? null,
|
notes: initialValues?.notes ?? null,
|
||||||
purchase_items: [],
|
purchase_items: [],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -8,6 +8,10 @@ import { Icon } from '@iconify/react';
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
|
|
||||||
@@ -17,8 +21,13 @@ import {
|
|||||||
getPurchaseRequestFormInitialValues,
|
getPurchaseRequestFormInitialValues,
|
||||||
UpdatePurchaseRequestFormSchema,
|
UpdatePurchaseRequestFormSchema,
|
||||||
} from './PurchaseRequestForm.schema';
|
} from './PurchaseRequestForm.schema';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import {
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
SupplierApi,
|
||||||
|
AreaApi,
|
||||||
|
LocationApi,
|
||||||
|
WarehouseApi,
|
||||||
|
} from '@/services/api/master-data';
|
||||||
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||||
import { PurchaseApi } from '@/services/api/purchasing';
|
import { PurchaseApi } from '@/services/api/purchasing';
|
||||||
|
|
||||||
@@ -46,6 +55,46 @@ const PurchaseRequestForm = ({
|
|||||||
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
|
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
|
||||||
useState('');
|
useState('');
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [
|
||||||
|
productWarehouseSelectInputValue,
|
||||||
|
setProductWarehouseSelectInputValue,
|
||||||
|
] = useState('');
|
||||||
|
|
||||||
|
// ===== INTERFACES =====
|
||||||
|
interface WarehouseOptionType extends OptionType {
|
||||||
|
area?: string;
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductWarehouseOptionType extends OptionType {
|
||||||
|
product_id: number;
|
||||||
|
warehouse_id: number;
|
||||||
|
warehouse_name: string;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== HELPER FUNCTIONS =====
|
||||||
|
const getPurchaseItemError = (
|
||||||
|
idx: number,
|
||||||
|
field: 'warehouse_id' | 'product_warehouse_id' | 'product_id' | 'sub_qty'
|
||||||
|
): { isError: boolean; errorMessage: string } => {
|
||||||
|
const touchedItem = formik.touched.purchase_items?.[idx];
|
||||||
|
const errorItem = formik.errors.purchase_items?.[idx] as
|
||||||
|
| Record<string, string>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!touchedItem || !errorItem) {
|
||||||
|
return { isError: false, errorMessage: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTouched = (touchedItem as Record<string, boolean>)?.[field];
|
||||||
|
const errorMessage = errorItem?.[field] || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
isError: Boolean(isTouched && errorMessage),
|
||||||
|
errorMessage: isTouched && errorMessage ? errorMessage : '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// ===== FORM HANDLERS =====
|
// ===== FORM HANDLERS =====
|
||||||
const createPurchaseRequestHandler = useCallback(
|
const createPurchaseRequestHandler = useCallback(
|
||||||
@@ -94,37 +143,86 @@ const PurchaseRequestForm = ({
|
|||||||
}, [deleteModal, initialValues?.id, router]);
|
}, [deleteModal, initialValues?.id, router]);
|
||||||
|
|
||||||
// ===== API DATA FETCHING =====
|
// ===== API DATA FETCHING =====
|
||||||
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`;
|
||||||
SupplierApi.basePath,
|
const { data: allProductWarehouses } = useSWR(
|
||||||
SupplierApi.getAllFetcher
|
allProductWarehousesUrl,
|
||||||
|
ProductWarehouseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
// ===== USE SELECT HOOKS =====
|
||||||
WarehouseApi.basePath,
|
const {
|
||||||
|
inputValue: supplierSelectInputValue,
|
||||||
|
setInputValue: setSupplierSelectInputValue,
|
||||||
|
options: supplierOptions,
|
||||||
|
isLoadingOptions: isLoadingSuppliers,
|
||||||
|
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
inputValue: areaSelectInputValue,
|
||||||
|
setInputValue: setAreaSelectInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreas,
|
||||||
|
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
inputValue: locationSelectInputValue,
|
||||||
|
setInputValue: setLocationSelectInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocations,
|
||||||
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
inputValue: warehouseSelectInputValue,
|
||||||
|
setInputValue: setWarehouseSelectInputValue,
|
||||||
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
|
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
||||||
|
const { data: warehouses } = useSWR(
|
||||||
|
warehousesUrl,
|
||||||
WarehouseApi.getAllFetcher
|
WarehouseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== DATA PROCESSING =====
|
// ===== DATA PROCESSING =====
|
||||||
const supplierOptions = useMemo(() => {
|
|
||||||
if (!isResponseSuccess(suppliers)) return [];
|
|
||||||
return (
|
|
||||||
suppliers?.data.map((supplier) => ({
|
|
||||||
value: supplier.id,
|
|
||||||
label: supplier.name,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [suppliers]);
|
|
||||||
|
|
||||||
const warehouseOptions = useMemo(() => {
|
const warehouseOptions = useMemo(() => {
|
||||||
if (!isResponseSuccess(warehouses)) return [];
|
if (!isResponseSuccess(warehouses)) return [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
warehouses?.data.map((warehouse) => ({
|
warehouses?.data.map((w) => ({
|
||||||
value: warehouse.id,
|
value: w.id,
|
||||||
label: warehouse.name,
|
label: w.name,
|
||||||
|
area: w.area?.name,
|
||||||
|
location:
|
||||||
|
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
||||||
|
? w.location?.name
|
||||||
|
: undefined,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
}, [warehouses]);
|
}, [warehouses]);
|
||||||
|
|
||||||
|
// ===== PRODUCT WAREHOUSE FETCHING =====
|
||||||
|
const getProductWarehousesUrl = useCallback(() => {
|
||||||
|
const productWarehouseParams = new URLSearchParams({
|
||||||
|
search: productWarehouseSelectInputValue,
|
||||||
|
});
|
||||||
|
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
|
||||||
|
}, [productWarehouseSelectInputValue]);
|
||||||
|
|
||||||
|
const productWarehousesUrl = getProductWarehousesUrl();
|
||||||
|
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
|
||||||
|
useSWR(productWarehousesUrl, ProductWarehouseApi.getAllFetcher);
|
||||||
|
|
||||||
|
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
||||||
|
? productWarehouses?.data.map((pw) => ({
|
||||||
|
value: pw.product.id,
|
||||||
|
label: pw.product.name,
|
||||||
|
product_id: pw.product.id,
|
||||||
|
warehouse_id: pw.warehouse.id,
|
||||||
|
warehouse_name: pw.warehouse.name,
|
||||||
|
quantity: pw.quantity,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
||||||
() => getPurchaseRequestFormInitialValues(initialValues),
|
() => getPurchaseRequestFormInitialValues(initialValues),
|
||||||
[initialValues]
|
[initialValues]
|
||||||
@@ -184,18 +282,28 @@ const PurchaseRequestForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
const supplierChangeHandler = (val: string) => {
|
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const supplierId = parseInt(val) || 0;
|
const supplier = val as OptionType | null;
|
||||||
formik.setFieldValue('supplier_id', supplierId);
|
formik.setFieldTouched('supplier', true);
|
||||||
|
formik.setFieldValue('supplier', supplier);
|
||||||
|
formik.setFieldTouched('supplier_id', true);
|
||||||
|
formik.setFieldValue('supplier_id', (supplier as OptionType)?.value || 0);
|
||||||
|
};
|
||||||
|
|
||||||
const selectedSupplier = supplierOptions.find(
|
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
(option) => option.value === supplierId
|
const area = val as OptionType | null;
|
||||||
);
|
formik.setFieldTouched('area', true);
|
||||||
if (selectedSupplier) {
|
formik.setFieldValue('area', area);
|
||||||
formik.setFieldValue('supplier', selectedSupplier);
|
formik.setFieldTouched('area_id', true);
|
||||||
} else {
|
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
|
||||||
formik.setFieldValue('supplier', null);
|
};
|
||||||
}
|
|
||||||
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const location = val as OptionType | null;
|
||||||
|
formik.setFieldTouched('location', true);
|
||||||
|
formik.setFieldValue('location', location);
|
||||||
|
formik.setFieldTouched('location_id', true);
|
||||||
|
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Purchase Items Handlers
|
// Purchase Items Handlers
|
||||||
@@ -293,21 +401,22 @@ const PurchaseRequestForm = ({
|
|||||||
: 'grid grid-cols-1 md:grid-cols-2 gap-6'
|
: 'grid grid-cols-1 md:grid-cols-2 gap-6'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Vendor'
|
label='Vendor'
|
||||||
name='supplier_id'
|
placeholder='Pilih Vendor...'
|
||||||
value={formik.values.supplier_id}
|
value={formik.values.supplier}
|
||||||
onChange={(e) => supplierChangeHandler(e.target.value)}
|
onChange={supplierChangeHandler}
|
||||||
onBlur={formik.handleBlur}
|
options={supplierOptions}
|
||||||
|
onInputChange={setSupplierSelectInputValue}
|
||||||
|
isLoading={isLoadingSuppliers}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.supplier_id &&
|
formik.touched.supplier_id &&
|
||||||
Boolean(formik.errors.supplier_id)
|
Boolean(formik.errors.supplier_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.supplier_id as string}
|
errorMessage={formik.errors.supplier_id as string}
|
||||||
readOnly={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
type='number'
|
isClearable
|
||||||
placeholder='Masukkan Supplier ID'
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
@@ -326,26 +435,39 @@ const PurchaseRequestForm = ({
|
|||||||
placeholder='Masukkan Credit Term'
|
placeholder='Masukkan Credit Term'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Area'
|
label='Area'
|
||||||
name='area_id'
|
placeholder='Pilih Area...'
|
||||||
onChange={(e) => {}}
|
value={formik.values.area}
|
||||||
onBlur={formik.handleBlur}
|
onChange={areaChangeHandler}
|
||||||
readOnly={type === 'detail'}
|
options={areaOptions}
|
||||||
type='number'
|
onInputChange={setAreaSelectInputValue}
|
||||||
placeholder='Pilih Area'
|
isLoading={isLoadingAreas}
|
||||||
|
isError={
|
||||||
|
formik.touched.area_id && Boolean(formik.errors.area_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.area_id as string}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
name='location_id'
|
placeholder='Pilih Lokasi...'
|
||||||
onChange={(e) => {}}
|
value={formik.values.location}
|
||||||
onBlur={formik.handleBlur}
|
onChange={locationChangeHandler}
|
||||||
readOnly={type === 'detail'}
|
options={locationOptions}
|
||||||
type='number'
|
onInputChange={setLocationSelectInputValue}
|
||||||
placeholder='Pilih Lokasi'
|
isLoading={isLoadingLocations}
|
||||||
|
isError={
|
||||||
|
formik.touched.location_id &&
|
||||||
|
Boolean(formik.errors.location_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.location_id as string}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={type === 'detail' ? 'col-span-1' : 'col-span-2'}>
|
<div className={type === 'detail' ? 'col-span-1' : 'col-span-2'}>
|
||||||
@@ -442,43 +564,76 @@ const PurchaseRequestForm = ({
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
name={`purchase_items.${idx}.warehouse_id`}
|
value={item.warehouse}
|
||||||
value={item.warehouse_id || ''}
|
onChange={(val) => {
|
||||||
onChange={(e) =>
|
const warehouse = val as OptionType | null;
|
||||||
handlePurchaseItemChange(
|
formik.setFieldValue(
|
||||||
idx,
|
`purchase_items.${idx}.warehouse`,
|
||||||
'warehouse_id',
|
warehouse
|
||||||
e.target.value
|
);
|
||||||
)
|
formik.setFieldValue(
|
||||||
|
`purchase_items.${idx}.warehouse_id`,
|
||||||
|
(warehouse as OptionType)?.value || 0
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={warehouseOptions}
|
||||||
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
|
isLoading={isLoadingWarehouses}
|
||||||
|
isError={
|
||||||
|
getPurchaseItemError(idx, 'warehouse_id').isError
|
||||||
}
|
}
|
||||||
onBlur={formik.handleBlur}
|
errorMessage={
|
||||||
type='number'
|
getPurchaseItemError(idx, 'warehouse_id')
|
||||||
placeholder='Masukkan Warehouse ID'
|
.errorMessage
|
||||||
readOnly={type === 'detail'}
|
}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Gudang'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'min-w-24',
|
wrapper: 'min-w-32',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<SelectInput
|
||||||
name={`purchase_items.${idx}.product_warehouse_id`}
|
required
|
||||||
value={item.product_warehouse_id || ''}
|
value={item.product_warehouse}
|
||||||
onChange={(e) =>
|
onChange={(val) => {
|
||||||
handlePurchaseItemChange(
|
const productWarehouse =
|
||||||
idx,
|
val as ProductWarehouseOptionType | null;
|
||||||
'product_warehouse_id',
|
formik.setFieldValue(
|
||||||
e.target.value
|
`purchase_items.${idx}.product_warehouse`,
|
||||||
)
|
productWarehouse
|
||||||
|
);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`purchase_items.${idx}.product_warehouse_id`,
|
||||||
|
(productWarehouse as ProductWarehouseOptionType)
|
||||||
|
?.value || 0
|
||||||
|
);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`purchase_items.${idx}.product_id`,
|
||||||
|
(productWarehouse as ProductWarehouseOptionType)
|
||||||
|
?.product_id || 0
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={productWarehouseOptions}
|
||||||
|
onInputChange={setProductWarehouseSelectInputValue}
|
||||||
|
isLoading={isLoadingProductWarehouses}
|
||||||
|
isError={
|
||||||
|
getPurchaseItemError(idx, 'product_warehouse_id')
|
||||||
|
.isError
|
||||||
}
|
}
|
||||||
onBlur={formik.handleBlur}
|
errorMessage={
|
||||||
type='number'
|
getPurchaseItemError(idx, 'product_warehouse_id')
|
||||||
placeholder='Product Warehouse ID'
|
.errorMessage
|
||||||
readOnly={type === 'detail'}
|
}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Produk'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'min-w-24',
|
wrapper: 'min-w-32',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user