added server data
This commit is contained in:
parent
c615678981
commit
6d72ed4735
@ -6,16 +6,18 @@ import { useProjectInfra } from "../../../hooks/useProjects";
|
||||
import { PmsGrid } from "../../../services/pmsGrid";
|
||||
|
||||
export default function BuildingTable() {
|
||||
const project = useSelectedProject()
|
||||
const project = useSelectedProject();
|
||||
|
||||
const {projectInfra} = useProjectInfra(project,null)
|
||||
const { projectInfra } = useProjectInfra(project, null);
|
||||
const columns = [
|
||||
{ key: "buildingName", title: "Building", sortable: true,},
|
||||
{ key: "buildingName", title: "Building", sortable: true,className: "text-start", },
|
||||
{ key: "plannedWork", title: "Planned Work" },
|
||||
{ key: "completedWork", title: "Completed Work" },
|
||||
{ key: "percentage", title: "Completion %" },
|
||||
];
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<PmsGrid
|
||||
columns={columns}
|
||||
@ -27,9 +29,7 @@ export default function BuildingTable() {
|
||||
resizing: true,
|
||||
selection: true,
|
||||
}}
|
||||
renderExpanded={(building) => (
|
||||
<FloorTable floors={building.floors} />
|
||||
)}
|
||||
renderExpanded={(building) => <FloorTable floors={building.floors} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||
import { CollectionTableSkeleton } from "./CollectionSkeleton";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { PmsGrid } from "../../services/pmsGrid";
|
||||
import PmGridCollection from "./PmGridCollection";
|
||||
|
||||
const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@ -28,16 +30,16 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
||||
const selectedProject = useSelectedProject();
|
||||
const searchDebounce = useDebounce(searchString, 500);
|
||||
|
||||
const { data, isLoading, isError, error } = useCollections(
|
||||
selectedProject,
|
||||
searchDebounce,
|
||||
localToUtc(fromDate),
|
||||
localToUtc(toDate),
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
true,
|
||||
isPending
|
||||
);
|
||||
// const { data, isLoading, isError, error } = useCollections(
|
||||
// selectedProject,
|
||||
// searchDebounce,
|
||||
// localToUtc(fromDate),
|
||||
// localToUtc(toDate),
|
||||
// ITEMS_PER_PAGE,
|
||||
// currentPage,
|
||||
// true,
|
||||
// isPending
|
||||
// );
|
||||
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
||||
useCollectionContext();
|
||||
|
||||
@ -147,149 +149,152 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) return <CollectionTableSkeleton />;
|
||||
if (isError) return <p>{error.message}</p>;
|
||||
// if (isLoading) return <CollectionTableSkeleton />;
|
||||
// if (isError) return <p>{error.message}</p>;
|
||||
|
||||
return (
|
||||
<div className="card px-sm-4 px-0">
|
||||
<div
|
||||
className="card-datatable table-responsive page-min-h"
|
||||
id="horizontal-example"
|
||||
>
|
||||
<div className="dataTables_wrapper no-footer mx-3 pb-2">
|
||||
<table className="table dataTable text-nowrap">
|
||||
<thead>
|
||||
<tr className="table_header_border">
|
||||
{collectionColumns.map((col) => (
|
||||
<th key={col.key} className={col.align}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
{(isAdmin ||
|
||||
canAddPayment ||
|
||||
canViewCollection ||
|
||||
canEditCollection ||
|
||||
canCreate) && <th>Action</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.isArray(data?.data) && data.data.length > 0 ? (
|
||||
data.data.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{collectionColumns.map((col) => (
|
||||
<td key={col.key} className={col.align}>
|
||||
{col.getValue(row)}
|
||||
</td>
|
||||
))}
|
||||
{(isAdmin ||
|
||||
canAddPayment ||
|
||||
canViewCollection ||
|
||||
canEditCollection ||
|
||||
canCreate) && (
|
||||
<td
|
||||
className="sticky-action-column text-center"
|
||||
style={{ padding: "12px 8px" }}
|
||||
>
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-offset="0,8"
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>
|
||||
</button>
|
||||
// <div className="card px-sm-4 px-0">
|
||||
// <div
|
||||
// className="card-datatable table-responsive page-min-h"
|
||||
// id="horizontal-example"
|
||||
// >
|
||||
// <div className="dataTables_wrapper no-footer mx-3 pb-2">
|
||||
// <table className="table dataTable text-nowrap">
|
||||
// <thead>
|
||||
// <tr className="table_header_border">
|
||||
// {collectionColumns.map((col) => (
|
||||
// <th key={col.key} className={col.align}>
|
||||
// {col.label}
|
||||
// </th>
|
||||
// ))}
|
||||
// {(isAdmin ||
|
||||
// canAddPayment ||
|
||||
// canViewCollection ||
|
||||
// canEditCollection ||
|
||||
// canCreate) && <th>Action</th>}
|
||||
// </tr>
|
||||
// </thead>
|
||||
// <tbody>
|
||||
// {Array.isArray(data?.data) && data.data.length > 0 ? (
|
||||
// data.data.map((row, i) => (
|
||||
// <tr key={i}>
|
||||
// {collectionColumns.map((col) => (
|
||||
// <td key={col.key} className={col.align}>
|
||||
// {col.getValue(row)}
|
||||
// </td>
|
||||
// ))}
|
||||
// {(isAdmin ||
|
||||
// canAddPayment ||
|
||||
// canViewCollection ||
|
||||
// canEditCollection ||
|
||||
// canCreate) && (
|
||||
// <td
|
||||
// className="sticky-action-column text-center"
|
||||
// style={{ padding: "12px 8px" }}
|
||||
// >
|
||||
// <div className="dropdown z-2">
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||
// data-bs-toggle="dropdown"
|
||||
// aria-expanded="false"
|
||||
// >
|
||||
// <i
|
||||
// className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||
// data-bs-toggle="tooltip"
|
||||
// data-bs-offset="0,8"
|
||||
// data-bs-placement="top"
|
||||
// data-bs-custom-class="tooltip-dark"
|
||||
// title="More Action"
|
||||
// ></i>
|
||||
// </button>
|
||||
|
||||
<ul className="dropdown-menu dropdown-menu-end">
|
||||
{/* View */}
|
||||
// <ul className="dropdown-menu dropdown-menu-end">
|
||||
// {/* View */}
|
||||
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => setViewCollection(row.id)}
|
||||
>
|
||||
<i className="bx bx-show me-2 text-primary"></i>
|
||||
<span>View</span>
|
||||
</a>
|
||||
</li>
|
||||
// <li>
|
||||
// <a
|
||||
// className="dropdown-item cursor-pointer"
|
||||
// onClick={() => setViewCollection(row.id)}
|
||||
// >
|
||||
// <i className="bx bx-show me-2 text-primary"></i>
|
||||
// <span>View</span>
|
||||
// </a>
|
||||
// </li>
|
||||
|
||||
{/* Only if not completed */}
|
||||
{!row?.markAsCompleted && (
|
||||
<>
|
||||
{/* Add Payment */}
|
||||
{(isAdmin || canAddPayment) && (
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() =>
|
||||
setAddPayment({
|
||||
isOpen: true,
|
||||
invoiceId: row.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="bx bx-wallet me-2 text-warning"></i>
|
||||
<span>Add Payment</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
// {/* Only if not completed */}
|
||||
// {!row?.markAsCompleted && (
|
||||
// <>
|
||||
// {/* Add Payment */}
|
||||
// {(isAdmin || canAddPayment) && (
|
||||
// <li>
|
||||
// <a
|
||||
// className="dropdown-item cursor-pointer"
|
||||
// onClick={() =>
|
||||
// setAddPayment({
|
||||
// isOpen: true,
|
||||
// invoiceId: row.id,
|
||||
// })
|
||||
// }
|
||||
// >
|
||||
// <i className="bx bx-wallet me-2 text-warning"></i>
|
||||
// <span>Add Payment</span>
|
||||
// </a>
|
||||
// </li>
|
||||
// )}
|
||||
|
||||
{/* Mark Payment */}
|
||||
{isAdmin && (
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() =>
|
||||
setProcessedPayment({
|
||||
isOpen: true,
|
||||
invoiceId: row.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="bx bx-check-circle me-2 text-success"></i>
|
||||
<span>Mark Payment</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr style={{ height: "200px" }}>
|
||||
<td
|
||||
colSpan={collectionColumns.length + 1}
|
||||
className="text-center border-0 align-middle"
|
||||
>
|
||||
No Collections Found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{data?.data?.length > 0 && (
|
||||
<div className="d-flex justify-content-start mt-2">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={data?.totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
// {/* Mark Payment */}
|
||||
// {isAdmin && (
|
||||
// <li>
|
||||
// <a
|
||||
// className="dropdown-item cursor-pointer"
|
||||
// onClick={() =>
|
||||
// setProcessedPayment({
|
||||
// isOpen: true,
|
||||
// invoiceId: row.id,
|
||||
// })
|
||||
// }
|
||||
// >
|
||||
// <i className="bx bx-check-circle me-2 text-success"></i>
|
||||
// <span>Mark Payment</span>
|
||||
// </a>
|
||||
// </li>
|
||||
// )}
|
||||
// </>
|
||||
// )}
|
||||
// </ul>
|
||||
// </div>
|
||||
// </td>
|
||||
// )}
|
||||
// </tr>
|
||||
// ))
|
||||
// ) : (
|
||||
// <tr style={{ height: "200px" }}>
|
||||
// <td
|
||||
// colSpan={collectionColumns.length + 1}
|
||||
// className="text-center border-0 align-middle"
|
||||
// >
|
||||
// No Collections Found
|
||||
// </td>
|
||||
// </tr>
|
||||
// )}
|
||||
// </tbody>
|
||||
// </table>
|
||||
// {data?.data?.length > 0 && (
|
||||
// <div className="d-flex justify-content-start mt-2">
|
||||
// <Pagination
|
||||
// currentPage={currentPage}
|
||||
// totalPages={data?.totalPages}
|
||||
// onPageChange={paginate}
|
||||
// />
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
<div className="card p-2">
|
||||
<PmGridCollection selectedProject={selectedProject} fromDate={localToUtc(fromDate)} toDate={localToUtc(toDate)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
95
src/components/collections/PmGridCollection.jsx
Normal file
95
src/components/collections/PmGridCollection.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { PmsGrid } from "../../services/pmsGrid";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { formatFigure } from "../../utils/appUtils";
|
||||
import { CollectionRepository } from "../../repositories/ColllectionRepository";
|
||||
|
||||
const PmGridCollection = ({ selectedProject, fromDate, toDate, isPending }) => {
|
||||
const columns = [
|
||||
{ key: "invoiceNumber", title: "Invoice Number", className: "text-start" },
|
||||
{ key: "title", title: "Title", sortable: true, className: "text-start" },
|
||||
{
|
||||
key: "clientSubmitedDate",
|
||||
title: "Submission Date",
|
||||
className: "text-start",
|
||||
},
|
||||
{
|
||||
key: "exceptedPaymentDate",
|
||||
title: "Expected Payment Date",
|
||||
className: "text-start",
|
||||
},
|
||||
{ key: "totalAmount", title: "Total Amount" },
|
||||
{ key: "balanceAmount", title: "Balance" },
|
||||
{ key: "isActive", title: "Status" },
|
||||
];
|
||||
|
||||
// --- SERVER SIDE FETCHER (correct) ---
|
||||
const fetcher = async ({ page, pageSize, search }) => {
|
||||
const response = await CollectionRepository.getCollections(
|
||||
selectedProject,
|
||||
search || "",
|
||||
fromDate,
|
||||
toDate,
|
||||
pageSize,
|
||||
page,
|
||||
true, // isActive
|
||||
isPending
|
||||
);
|
||||
|
||||
const api = response.data;
|
||||
|
||||
return {
|
||||
rows: api.data.map((item) => ({
|
||||
id: item.id,
|
||||
invoiceNumber: item.invoiceNumber,
|
||||
title: item.title,
|
||||
|
||||
clientSubmitedDate: formatUTCToLocalTime(item.clientSubmitedDate),
|
||||
exceptedPaymentDate: formatUTCToLocalTime(item.exceptedPaymentDate),
|
||||
|
||||
totalAmount: formatFigure(item.basicAmount + item.taxAmount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
}),
|
||||
|
||||
balanceAmount: formatFigure(item.balanceAmount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
}),
|
||||
|
||||
isActive: item.isActive ? (
|
||||
<span className="badge bg-label-primary">
|
||||
<span className="badge badge-dot bg-primary me-1"></span>
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge bg-label-danger">
|
||||
<span className="badge badge-dot bg-danger me-1"></span>
|
||||
In-Active
|
||||
</span>
|
||||
),
|
||||
})),
|
||||
|
||||
// MUST use totalRows only
|
||||
total: api.totalEntities,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<PmsGrid
|
||||
columns={columns}
|
||||
serverMode
|
||||
fetcher={fetcher}
|
||||
rowKey="id"
|
||||
features={{
|
||||
search: true,
|
||||
pagination: true,
|
||||
pinning: true,
|
||||
resizing: true,
|
||||
selection: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PmGridCollection;
|
||||
@ -61,6 +61,7 @@ import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage
|
||||
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
|
||||
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
|
||||
import ManageJob from "../components/ServiceProject/ManageJob";
|
||||
import DemoBOQGrid from "../services/pmsGrid/BasicTable";
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
|
||||
@ -738,7 +738,7 @@ export default function DemoBOQGrid() {
|
||||
reorder: true,
|
||||
columnVisibility: true,
|
||||
pageSizeSelector: true,
|
||||
// groupByKey: "status",
|
||||
groupByKey: "status",
|
||||
aggregation: true,
|
||||
expand: true,
|
||||
maxHeight: "70vh",
|
||||
|
||||
@ -170,7 +170,40 @@ export default function PmsGrid({
|
||||
Export CSV
|
||||
</button>
|
||||
)}
|
||||
{features.grouping && (
|
||||
<div className="dropdown">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
Group By
|
||||
</button>
|
||||
|
||||
<div className="dropdown-menu p-2">
|
||||
{visibleColumns
|
||||
.filter((c) => c.groupable)
|
||||
.map((c) => (
|
||||
<div
|
||||
key={c.key}
|
||||
className="dropdown-item"
|
||||
onClick={() => grid.setGroupBy(c.key)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{c.title}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{grid.groupBy && (
|
||||
<div
|
||||
className="dropdown-item text-danger"
|
||||
onClick={() => grid.setGroupBy(null)}
|
||||
>
|
||||
Clear Grouping
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4 ">
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from "react";
|
||||
|
||||
/*
|
||||
@ -7,7 +6,14 @@ import { useState, useMemo, useCallback, useEffect } from "react";
|
||||
- rowKey
|
||||
- initialPageSize
|
||||
*/
|
||||
export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id", initialPageSize = 25, columns = [] }) {
|
||||
export function useGridCore({
|
||||
data,
|
||||
serverMode = false,
|
||||
fetcher,
|
||||
rowKey = "id",
|
||||
initialPageSize = 5,
|
||||
columns = [],
|
||||
}) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(initialPageSize);
|
||||
const [search, setSearch] = useState("");
|
||||
@ -26,9 +32,11 @@ export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id",
|
||||
if (!data) return [];
|
||||
const q = (search || "").toLowerCase();
|
||||
const filtered = q
|
||||
? data.filter(r =>
|
||||
Object.values(r).some(v =>
|
||||
String(v ?? "").toLowerCase().includes(q)
|
||||
? data.filter((r) =>
|
||||
Object.values(r).some((v) =>
|
||||
String(v ?? "")
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
)
|
||||
)
|
||||
: data;
|
||||
@ -41,7 +49,8 @@ export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id",
|
||||
if (A == null && B == null) return 0;
|
||||
if (A == null) return -1 * dir;
|
||||
if (B == null) return 1 * dir;
|
||||
if (typeof A === "number" && typeof B === "number") return (A - B) * dir;
|
||||
if (typeof A === "number" && typeof B === "number")
|
||||
return (A - B) * dir;
|
||||
return String(A).localeCompare(String(B)) * dir;
|
||||
});
|
||||
}
|
||||
@ -69,8 +78,8 @@ export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id",
|
||||
}, [serverMode, fetchServer]);
|
||||
|
||||
// selection
|
||||
const toggleSelect = useCallback(id => {
|
||||
setSelected(prev => {
|
||||
const toggleSelect = useCallback((id) => {
|
||||
setSelected((prev) => {
|
||||
const s = new Set(prev);
|
||||
s.has(id) ? s.delete(id) : s.add(id);
|
||||
return s;
|
||||
@ -78,10 +87,10 @@ export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id",
|
||||
}, []);
|
||||
|
||||
const selectAllOnPage = useCallback(
|
||||
rows => {
|
||||
setSelected(prev => {
|
||||
(rows) => {
|
||||
setSelected((prev) => {
|
||||
const s = new Set(prev);
|
||||
rows.forEach(r => s.add(r[rowKey]));
|
||||
rows.forEach((r) => s.add(r[rowKey]));
|
||||
return s;
|
||||
});
|
||||
},
|
||||
@ -89,26 +98,26 @@ export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id",
|
||||
);
|
||||
|
||||
const deselectAllOnPage = useCallback(
|
||||
rows => {
|
||||
setSelected(prev => {
|
||||
(rows) => {
|
||||
setSelected((prev) => {
|
||||
const s = new Set(prev);
|
||||
rows.forEach(r => s.delete(r[rowKey]));
|
||||
rows.forEach((r) => s.delete(r[rowKey]));
|
||||
return s;
|
||||
});
|
||||
},
|
||||
[rowKey]
|
||||
);
|
||||
|
||||
const toggleExpand = useCallback(id => {
|
||||
setExpanded(prev => {
|
||||
const toggleExpand = useCallback((id) => {
|
||||
setExpanded((prev) => {
|
||||
const s = new Set(prev);
|
||||
s.has(id) ? s.delete(id) : s.add(id);
|
||||
return s;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const changeSort = useCallback(key => {
|
||||
setSortBy(prev =>
|
||||
const changeSort = useCallback((key) => {
|
||||
setSortBy((prev) =>
|
||||
prev.key === key
|
||||
? { key, dir: prev.dir === "asc" ? "desc" : "asc" }
|
||||
: { key, dir: "asc" }
|
||||
@ -117,7 +126,7 @@ export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id",
|
||||
}, []);
|
||||
|
||||
const visibleColumns = useMemo(
|
||||
() => colState.filter(c => c.visible).sort((a, b) => a.order - b.order),
|
||||
() => colState.filter((c) => c.visible).sort((a, b) => a.order - b.order),
|
||||
[colState]
|
||||
);
|
||||
|
||||
@ -126,7 +135,9 @@ export function useGridCore({ data, serverMode = false, fetcher, rowKey = "id",
|
||||
|
||||
// update columns externally (reorder/pin/resize)
|
||||
const updateColumn = useCallback((key, patch) => {
|
||||
setColState(prev => prev.map(c => (c.key === key ? { ...c, ...patch } : c)));
|
||||
setColState((prev) =>
|
||||
prev.map((c) => (c.key === key ? { ...c, ...patch } : c))
|
||||
);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user