UI updates for audit.
This commit is contained in:
@@ -15,45 +15,60 @@ ipcMain.on(ipcTypes.default.audit.toMain.browseForFile, async (event, { sheetNam
|
||||
});
|
||||
if (!result.canceled) {
|
||||
try {
|
||||
var obj = xlsx.parse(result.filePaths[0], { cellDates: true });
|
||||
store.set("auditFilePath", result.filePaths);
|
||||
var obj = xlsx.parse(result.filePaths[0], { cellDates: true }); // parses a file
|
||||
|
||||
const detailSheet = obj.find((sheet) => sheet.name === sheetName);
|
||||
const claimsArray = [];
|
||||
let foundHeaderRow, foundTotalRow;
|
||||
detailSheet.data.forEach((line) => {
|
||||
//Check the first element. If it's claim number, we have our header row. the next one is important.
|
||||
if (!foundHeaderRow && line[0] === "Claim Number") {
|
||||
foundHeaderRow = true;
|
||||
} else if (foundHeaderRow && !foundTotalRow && line[0] && line[0] !== "Grand Total") {
|
||||
//Add it to the array
|
||||
const row = {
|
||||
clm_no: line[0].startsWith("00") ? line[0].slice(2) : line[0],
|
||||
close_date: line[1],
|
||||
v_model_yr: line[3],
|
||||
v_makedesc: line[4],
|
||||
v_model: line[5],
|
||||
under20kmiles: line[6],
|
||||
pan_total: line[7],
|
||||
paa_total: line[8],
|
||||
pal_total: line[9],
|
||||
pam_total: line[10],
|
||||
eligible_db_price_total: Math.round((line[11] + Number.EPSILON) * 100) / 100,
|
||||
eligible_act_price_total: Math.round((line[12] + Number.EPSILON) * 100) / 100,
|
||||
expected_rps_dollars: Math.round((line[15] + Number.EPSILON) * 100) / 100,
|
||||
actual_rps_dollars: Math.round((line[16] + Number.EPSILON) * 100) / 100
|
||||
};
|
||||
claimsArray.push(row);
|
||||
} else {
|
||||
// foundTotalRow = true;
|
||||
}
|
||||
event.sender.send(ipcTypes.default.audit.toRenderer.auditFilePath, {
|
||||
filePath: result.filePaths[0],
|
||||
sheets: obj.map((sheet) => sheet.name)
|
||||
});
|
||||
|
||||
event.sender.send(ipcTypes.default.audit.toRenderer.auditClaimsArray, claimsArray);
|
||||
} catch (error) {
|
||||
console.log("ot some sort of err", error);
|
||||
console.log("Got some sort of err", error);
|
||||
log.error("Error when trying to read audit xlsx file", error);
|
||||
event.sender.send(ipcTypes.default.audit.toRenderer.auditError, error.message);
|
||||
event.sender.send(ipcTypes.default.audit.toRenderer.auditError, error.meFssage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(ipcTypes.default.audit.toMain.runAudit, async (event, { sheetName }) => {
|
||||
try {
|
||||
const filePaths = store.get("auditFilePath");
|
||||
var obj = xlsx.parse(filePaths[0], { cellDates: true }); // parses a file
|
||||
|
||||
const detailSheet = obj.find((sheet) => sheet.name === sheetName);
|
||||
const claimsArray = [];
|
||||
let foundHeaderRow, foundTotalRow;
|
||||
detailSheet.data.forEach((line) => {
|
||||
//Check the first element. If it's claim number, we have our header row. the next one is important.
|
||||
if (!foundHeaderRow && line[0] === "Claim Number") {
|
||||
foundHeaderRow = true;
|
||||
} else if (foundHeaderRow && !foundTotalRow && line[0] && line[0] !== "Grand Total") {
|
||||
//Add it to the array
|
||||
const row = {
|
||||
clm_no: line[0].startsWith("00") ? line[0].slice(2) : line[0],
|
||||
close_date: line[1],
|
||||
v_model_yr: line[3],
|
||||
v_makedesc: line[4],
|
||||
v_model: line[5],
|
||||
under20kmiles: line[6],
|
||||
pan_total: line[7],
|
||||
paa_total: line[8],
|
||||
pal_total: line[9],
|
||||
pam_total: line[10],
|
||||
eligible_db_price_total: Math.round((line[11] + Number.EPSILON) * 100) / 100,
|
||||
eligible_act_price_total: Math.round((line[12] + Number.EPSILON) * 100) / 100,
|
||||
expected_rps_dollars: Math.round((line[15] + Number.EPSILON) * 100) / 100,
|
||||
actual_rps_dollars: Math.round((line[16] + Number.EPSILON) * 100) / 100
|
||||
};
|
||||
claimsArray.push(row);
|
||||
} else {
|
||||
// foundTotalRow = true;
|
||||
}
|
||||
});
|
||||
|
||||
event.sender.send(ipcTypes.default.audit.toRenderer.auditClaimsArray, claimsArray);
|
||||
} catch (error) {
|
||||
console.log("ot some sort of err", error);
|
||||
log.error("Error when trying to read audit xlsx file", error);
|
||||
event.sender.send(ipcTypes.default.audit.toRenderer.auditError, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -146,7 +146,12 @@
|
||||
},
|
||||
"1.2.0": {
|
||||
"title": "Release Notes for 1.2.0",
|
||||
"date": "05/51/2024",
|
||||
"date": "05/21/2024",
|
||||
"notes": "New Features\n* Introducing Score Card Auditing. Simply select your MPI scorecard and instantly compare it to your RPS score card to find discrepancies. Available in your side bar\n\nImprovements\n* Added additional models to SUV and Van databases for better detection.\n* Only -01 and -99 claims will now be brought into RPS.\n* Under the hood fixes and improvements."
|
||||
},
|
||||
"1.2.1": {
|
||||
"title": "Release Notes for 1.2.1",
|
||||
"date": "05/23/2024",
|
||||
"notes": "Improvements\n* UI improvements for audit functionality."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"productName": "ImEX RPS",
|
||||
"author": "ImEX Systems Inc. <support@thinkimex.com>",
|
||||
"description": "ImEX RPS",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"main": "electron/main.js",
|
||||
"homepage": "./",
|
||||
"dependencies": {
|
||||
|
||||
@@ -48,11 +48,6 @@ export function ReportingDatesMolecule({ queryReportingData }) {
|
||||
<DatePicker.RangePicker
|
||||
format="MM/DD/YYYY"
|
||||
ranges={{
|
||||
Today: [dayjs(), dayjs()],
|
||||
"Last 14 days": [dayjs().subtract(14, "day"), dayjs()],
|
||||
"Last 7 days": [dayjs().subtract(7, "day"), dayjs()],
|
||||
"Next 7 days": [dayjs(), dayjs().add(7, "day")],
|
||||
"Next 14 days": [dayjs(), dayjs().add(14, "day")],
|
||||
"Last Month": [
|
||||
dayjs().startOf("month").subtract(1, "month"),
|
||||
dayjs().startOf("month").subtract(1, "month").endOf("month")
|
||||
@@ -71,10 +66,11 @@ export function ReportingDatesMolecule({ queryReportingData }) {
|
||||
dayjs().startOf("quarter"),
|
||||
dayjs().startOf("quarter").add(1, "quarter").subtract(1, "day")
|
||||
],
|
||||
"Last 3 Months": [
|
||||
"Last 3 Months (Exlcusive)": [
|
||||
dayjs().startOf("month").subtract(3, "month"),
|
||||
dayjs().startOf("month").subtract(1, "month").endOf("month")
|
||||
]
|
||||
],
|
||||
"Last 3 Months (Inclusive)": [dayjs().startOf("month").subtract(2, "month"), dayjs().endOf("month")]
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, Col, Divider, Space, Table, Tooltip } from "antd";
|
||||
import { Card, Col, Divider, Empty, Result, Space, Table, Tooltip } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
@@ -28,16 +28,19 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
title: "Claim No.",
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
dataIndex: "clm_no",
|
||||
render: (text, record) => (
|
||||
<Link onClick={() => setSelectedJobId(record.id)} to={"/"}>
|
||||
<Space>{text}</Space>
|
||||
</Link>
|
||||
)
|
||||
render: (text, record) =>
|
||||
record.id ? (
|
||||
<Link onClick={() => setSelectedJobId(record.id)} to={"/"}>
|
||||
{text}
|
||||
</Link>
|
||||
) : (
|
||||
text
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "close_date",
|
||||
width: "12%",
|
||||
title: "[RPS] R4P",
|
||||
title: "[ImEX RPS] R4P",
|
||||
dataIndex: "close_date",
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => dateSort(a.close_date, b.close_date),
|
||||
@@ -59,7 +62,7 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
{
|
||||
key: "actual_rps",
|
||||
width: "12%",
|
||||
title: " Actual RPS ",
|
||||
title: " Actual RPS",
|
||||
dataIndex: ["audit", "jobRpsDollars"],
|
||||
render: (text, record) =>
|
||||
record?.jobRpsDollars
|
||||
@@ -74,11 +77,19 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
width: "12%",
|
||||
title: "Claim No.",
|
||||
sorter: (a, b) => alphaSort(a.rps.clm_no, b.rps.clm_no),
|
||||
dataIndex: ["rps", "clm_no"]
|
||||
dataIndex: ["rps", "clm_no"],
|
||||
render: (text, record) =>
|
||||
record.rps?.id ? (
|
||||
<Link onClick={() => setSelectedJobId(record.rps.id)} to={"/"}>
|
||||
{text}
|
||||
</Link>
|
||||
) : (
|
||||
text
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "close_date",
|
||||
title: "[RPS] R4P",
|
||||
title: "[ImEX RPS] R4P",
|
||||
dataIndex: "close_date",
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => dateSort(a.rps.close_date, b.rps.close_date),
|
||||
@@ -99,11 +110,11 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
{
|
||||
key: "expected_rps",
|
||||
width: "12%",
|
||||
title: "Expected RPS (RPS/Audit)",
|
||||
title: "Expected RPS (ImEX RPS/Audit)",
|
||||
dataIndex: ["audit", "expectedRpsDollars"],
|
||||
render: (text, record) => (
|
||||
<Space split={<Divider type="vertical" />}>
|
||||
<Tooltip title="RPS Expected Savings">{record.rps.expectedRpsDollars.toFormat()}</Tooltip>
|
||||
<Tooltip title="ImEX RPS Expected Savings">{record.rps.expectedRpsDollars.toFormat()}</Tooltip>
|
||||
<Tooltip title="MPI Audit Expected Savings">
|
||||
{Dinero({ amount: Math.round(record.audit.expected_rps_dollars * 100) }).toFormat()}
|
||||
</Tooltip>
|
||||
@@ -113,11 +124,11 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
{
|
||||
key: "actual_rps",
|
||||
width: "12%",
|
||||
title: " Actual RPS (RPS/Audit)",
|
||||
title: " Actual RPS (ImEX RPS/Audit)",
|
||||
dataIndex: ["audit", "jobRpsDollars"],
|
||||
render: (text, record) => (
|
||||
<Space split={<Divider type="vertical" />}>
|
||||
<Tooltip title="RPS Actual Savings">{record.rps.jobRpsDollars.toFormat()}</Tooltip>
|
||||
<Tooltip title="ImeX RPS Actual Savings">{record.rps.jobRpsDollars.toFormat()}</Tooltip>
|
||||
<Tooltip title="MPI Audit Actual Savings">
|
||||
{Dinero({ amount: Math.round(record.audit.actual_rps_dollars * 100) }).toFormat()}
|
||||
</Tooltip>
|
||||
@@ -125,27 +136,51 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
if (Object.keys(selectAuditData).length === 0)
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Empty
|
||||
// image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
|
||||
// imageStyle={{
|
||||
// height: 60
|
||||
// }}
|
||||
description={
|
||||
<span>No audit has been run yet. Please select a date range and audit file to view results.</span>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col span={24}>
|
||||
<Card title="Jobs in RPS, not found in MPI Audit">
|
||||
<Card title="Jobs in ImEX RPS, not found in MPI Audit">
|
||||
<Table
|
||||
loading={auditLoading}
|
||||
columns={missingColumns}
|
||||
pagination={false}
|
||||
dataSource={selectAuditData?.missingFromRps}
|
||||
rowKey="clm_no"
|
||||
locale={{
|
||||
emptyText: <Result status="success" subTitle="No discrepancies found." />
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title="Jobs in MPI Audit, not in RPS">
|
||||
<Card title="Jobs in MPI Audit, not in ImEX RPS">
|
||||
<Table
|
||||
loading={auditLoading}
|
||||
columns={missingColumns}
|
||||
pagination={false}
|
||||
dataSource={selectAuditData?.missingFromAudit}
|
||||
rowKey="clm_no"
|
||||
locale={{
|
||||
emptyText: <Result status="success" subTitle="No discrepancies found." />
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -157,6 +192,9 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
pagination={false}
|
||||
dataSource={selectAuditData?.expectedMismatch}
|
||||
rowKey="clm_no"
|
||||
locale={{
|
||||
emptyText: <Result status="success" subTitle="No discrepancies found." />
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -168,6 +206,9 @@ export function AuditResultsOrganism({ auditLoading, setSelectedJobId, selectAud
|
||||
pagination={false}
|
||||
dataSource={selectAuditData?.actualMismatch}
|
||||
rowKey="clm_no"
|
||||
locale={{
|
||||
emptyText: <Result status="success" subTitle="No discrepancies found." />
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PrinterFilled } from "@ant-design/icons";
|
||||
import { Alert, Button, Card, Col, DatePicker, Form, Input, Result, Row, Space } from "antd";
|
||||
import React, { useRef } from "react";
|
||||
import { Alert, Button, Card, Col, DatePicker, Form, Input, Result, Row, Select, Space } from "antd";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { useReactToPrint } from "react-to-print";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -26,13 +26,36 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AuditPage);
|
||||
|
||||
export function AuditPage({ auditError, queryReportingData, bodyshop }) {
|
||||
const handleBrowseForFile = async ({ sheetName, dateRange }) => {
|
||||
queryReportingData({
|
||||
startDate: dateRange[0] || dayjs("2024-03-01"),
|
||||
endDate: dateRange[1] || dayjs("2024-03-31")
|
||||
const [form] = Form.useForm();
|
||||
const [sheets, setSheets] = useState([]);
|
||||
useEffect(() => {
|
||||
ipcRenderer.on(ipcTypes.audit.toRenderer.auditFilePath, async (event, { filePath, sheets }) => {
|
||||
setSheets(sheets);
|
||||
if (sheets.includes("Shop RPS Claim Detail")) {
|
||||
form.setFieldsValue({ sheetName: "Shop RPS Claim Detail" });
|
||||
}
|
||||
form.setFieldsValue({ filePath });
|
||||
});
|
||||
ipcRenderer.send(ipcTypes.audit.toMain.browseForFile, { sheetName });
|
||||
|
||||
return () => {
|
||||
//ipcRenderer.removeListener(ipcTypes.audit.toRenderer.auditFilePath);
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
const handleBrowseForFile = async () => {
|
||||
const { dateRange } = form.getFieldsValue();
|
||||
queryReportingData({
|
||||
startDate: dateRange[0],
|
||||
endDate: dateRange[1]
|
||||
});
|
||||
ipcRenderer.send(ipcTypes.audit.toMain.browseForFile, {});
|
||||
};
|
||||
|
||||
const handleRunAudit = async ({ sheetName, dateRange }) => {
|
||||
console.log("🚀 ~ handleRunAudit ~ sheetName:", sheetName);
|
||||
ipcRenderer.send(ipcTypes.audit.toMain.runAudit, { sheetName });
|
||||
};
|
||||
|
||||
const componentRef = useRef();
|
||||
const handlePrint = useReactToPrint({
|
||||
content: () => componentRef.current,
|
||||
@@ -53,7 +76,7 @@ export function AuditPage({ auditError, queryReportingData, bodyshop }) {
|
||||
<Row gutter={[16, 16]} ref={componentRef}>
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Form onFinish={handleBrowseForFile}>
|
||||
<Form form={form} onFinish={handleRunAudit}>
|
||||
<Form.Item
|
||||
label="1. Ready for Payment Date Between"
|
||||
name="dateRange"
|
||||
@@ -101,17 +124,44 @@ export function AuditPage({ auditError, queryReportingData, bodyshop }) {
|
||||
|
||||
<Space align="middle" wrap>
|
||||
<Form.Item
|
||||
label="2. Sheet Name"
|
||||
label="2. File Path"
|
||||
name="filePath"
|
||||
tooltip="File path to the MPI score card you want to audit. Ensure that this scorecard has not been modified or altered in any way."
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input disabled width="200px" />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const disabled = !form.getFieldsValue().dateRange;
|
||||
return (
|
||||
<Button disabled={disabled} onClick={handleBrowseForFile}>
|
||||
Select MPI Audit XLS File
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="3. Sheet Name"
|
||||
tooltip="The name of the sheet which contains detailed RPS claim data."
|
||||
name="sheetName"
|
||||
initialValue="Shop RPS Claim Detail"
|
||||
required
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input width="200px" />
|
||||
{/* <Input width="200px" /> */}
|
||||
<Select
|
||||
style={{ minWidth: "200px" }}
|
||||
options={sheets.map((sheet) => ({ label: sheet, value: sheet }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Button type="primary" htmlType="submit">
|
||||
Run Audit
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Select MPI Audit XLS File
|
||||
</Button>
|
||||
<Button onClick={handlePrint}>
|
||||
<PrinterFilled />
|
||||
</Button>
|
||||
@@ -139,8 +189,8 @@ function NoAuditAccess({ features }) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title="You do not currently have access to the audit feature of RPS."
|
||||
subTitle="Auditing allows you to instantly and automatically find discrepancies between the data you have recorded in RPS and the scorecard provided to your by your SRA."
|
||||
title="You do not currently have access to the audit feature of ImEX RPS."
|
||||
subTitle="Auditing allows you to instantly and automatically find discrepancies between the data you have recorded in ImEX RPS and the scorecard provided to your by your SRA."
|
||||
extra={[
|
||||
<Button
|
||||
size="large"
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
overflow-y: auto;
|
||||
background-color: rgb(244, 244, 244);
|
||||
& > * {
|
||||
margin: 0.7rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
overflow-y: auto;
|
||||
background-color: rgb(244, 244, 244);
|
||||
& > .reporting-cards > * {
|
||||
margin: 0.7rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,12 @@
|
||||
},
|
||||
|
||||
"audit": {
|
||||
"toMain": { "browseForFile": "audit__browseForFile" },
|
||||
"toRenderer": { "auditClaimsArray": "audit__filepath", "auditError": "audit__auditError" }
|
||||
"toMain": { "browseForFile": "audit__browseForFile", "runAudit": "audit__runAudit", "readFile": "audit__readFile" },
|
||||
"toRenderer": {
|
||||
"auditFilePath": "audit__filepath",
|
||||
"auditClaimsArray": "audit_claimsArray",
|
||||
"auditError": "audit__auditError"
|
||||
}
|
||||
},
|
||||
"estimate": {
|
||||
"toRenderer": {
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
setReportingData,
|
||||
setScoreCard,
|
||||
setReportingError,
|
||||
setAuditResults
|
||||
setAuditResults,
|
||||
setAuditError
|
||||
} from "./reporting.actions";
|
||||
import ReportingApplicationTypes from "./reporting.types";
|
||||
|
||||
@@ -46,11 +47,18 @@ export function* onCalculateAudit() {
|
||||
yield takeLatest(ReportingApplicationTypes.CALCULATE_AUDIT, handleCalculateAudit);
|
||||
}
|
||||
export function* handleCalculateAudit({ payload: claimsArrayFromAudit }) {
|
||||
if (claimsArrayFromAudit.length === 0) {
|
||||
yield put(
|
||||
setAuditError(
|
||||
"The excel file did not return any matching results for this sheet. Please ensure you have selected the correct sheet. This is typically 'Shop RPS Claim Detail' but may vary."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rpsJobs = yield select((state) => state.reporting.data);
|
||||
//Get List of Claims delta.
|
||||
const missingFromRps = rpsJobs.filter((job) => !claimsArrayFromAudit.find((c) => c.clm_no.includes(job.clm_no)));
|
||||
const missingFromAudit = claimsArrayFromAudit.filter((c) => !rpsJobs.find((job) => c.clm_no.includes(job.clm_no)));
|
||||
console.log("Missing From RPS/From Audit", missingFromRps.length, missingFromAudit.length);
|
||||
|
||||
//For the items in both spots, highlight the discrepancy.
|
||||
const claimsArrayHashObject = {};
|
||||
|
||||
Reference in New Issue
Block a user