new folder structure for the WFM added and credit note outgoing csv read flow added
This commit is contained in:
parent
07577b4156
commit
e4948e5cab
@ -1 +1 @@
|
|||||||
import{a as s}from"./index-DyTD6GiQ.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CxsBWvVP.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-BATWUvr6.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
import{a as s}from"./index-CrKGije_.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CxsBWvVP.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-BATWUvr6.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||||
File diff suppressed because one or more lines are too long
1
build/assets/index-By1vJg8g.css
Normal file
1
build/assets/index-By1vJg8g.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -13,7 +13,7 @@
|
|||||||
<!-- Preload essential fonts and icons -->
|
<!-- Preload essential fonts and icons -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<script type="module" crossorigin src="/assets/index-DyTD6GiQ.js"></script>
|
<script type="module" crossorigin src="/assets/index-CrKGije_.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-BATWUvr6.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-BATWUvr6.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-8M2GI2Jv.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-By1vJg8g.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -73,6 +73,13 @@ RATE_LIMIT_MAX_REQUESTS=100
|
|||||||
MAX_FILE_SIZE_MB=10
|
MAX_FILE_SIZE_MB=10
|
||||||
ALLOWED_FILE_TYPES=pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif
|
ALLOWED_FILE_TYPES=pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif
|
||||||
|
|
||||||
|
# WFM Folder Structure Configuration
|
||||||
|
WFM_BASE_PATH=C:\\WFM
|
||||||
|
WFM_INCOMING_GST_CLAIMS_PATH=WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS_GST
|
||||||
|
WFM_INCOMING_NON_GST_CLAIMS_PATH=WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS_NON_GST
|
||||||
|
WFM_OUTGOING_GST_CLAIMS_PATH=WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_GST
|
||||||
|
WFM_OUTGOING_NON_GST_CLAIMS_PATH=WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_NON_GST
|
||||||
|
|
||||||
# TAT Monitoring
|
# TAT Monitoring
|
||||||
TAT_CHECK_INTERVAL_MINUTES=30
|
TAT_CHECK_INTERVAL_MINUTES=30
|
||||||
TAT_REMINDER_THRESHOLD_1=50
|
TAT_REMINDER_THRESHOLD_1=50
|
||||||
|
|||||||
@ -1024,6 +1024,8 @@ export class DealerClaimController {
|
|||||||
taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
|
taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
|
||||||
|
|
||||||
// Construct CSV with pipe separator
|
// Construct CSV with pipe separator
|
||||||
const headers = [
|
const headers = [
|
||||||
'TRNS_UNIQ_NO',
|
'TRNS_UNIQ_NO',
|
||||||
@ -1034,15 +1036,15 @@ export class DealerClaimController {
|
|||||||
'CLAIM_DOC_TYP',
|
'CLAIM_DOC_TYP',
|
||||||
'CLAIM_TYPE',
|
'CLAIM_TYPE',
|
||||||
'CLAIM_DATE',
|
'CLAIM_DATE',
|
||||||
'CLAIM_AMT',
|
'CLAIM_AMT'
|
||||||
'GST_AMT',
|
|
||||||
'GST_PERCENTAGE'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = items.map(item => {
|
if (!isNonGst) {
|
||||||
const isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
|
headers.push('GST_AMT', 'GST_PERCENTAGE');
|
||||||
|
}
|
||||||
|
|
||||||
// For Non-GST, we hide HSN (often stored in transactionCode) and GST details
|
const rows = items.map(item => {
|
||||||
|
// For Non-GST, we hide HSN (often stored in transactionCode)
|
||||||
const trnsUniqNo = isNonGst ? '' : (item.transactionCode || '');
|
const trnsUniqNo = isNonGst ? '' : (item.transactionCode || '');
|
||||||
const claimNumber = requestNumber;
|
const claimNumber = requestNumber;
|
||||||
const invNumber = invoice?.invoiceNumber || '';
|
const invNumber = invoice?.invoiceNumber || '';
|
||||||
@ -1053,11 +1055,7 @@ export class DealerClaimController {
|
|||||||
const claimDate = invoice?.createdAt ? new Date(invoice.createdAt).toISOString().split('T')[0] : '';
|
const claimDate = invoice?.createdAt ? new Date(invoice.createdAt).toISOString().split('T')[0] : '';
|
||||||
const claimAmt = item.assAmt;
|
const claimAmt = item.assAmt;
|
||||||
|
|
||||||
// Zero out tax for Non-GST
|
const rowItems = [
|
||||||
const totalTax = isNonGst ? 0 : (Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0));
|
|
||||||
const gstPercentage = isNonGst ? 0 : (item.gstRt || 0);
|
|
||||||
|
|
||||||
return [
|
|
||||||
trnsUniqNo,
|
trnsUniqNo,
|
||||||
claimNumber,
|
claimNumber,
|
||||||
invNumber,
|
invNumber,
|
||||||
@ -1066,10 +1064,15 @@ export class DealerClaimController {
|
|||||||
claimDocTyp,
|
claimDocTyp,
|
||||||
claimType,
|
claimType,
|
||||||
claimDate,
|
claimDate,
|
||||||
claimAmt,
|
claimAmt
|
||||||
totalTax.toFixed(2),
|
];
|
||||||
gstPercentage
|
|
||||||
].join('|');
|
if (!isNonGst) {
|
||||||
|
const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0);
|
||||||
|
rowItems.push(totalTax.toFixed(2), item.gstRt || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowItems.join('|');
|
||||||
});
|
});
|
||||||
|
|
||||||
const csvContent = [headers.join('|'), ...rows].join('\n');
|
const csvContent = [headers.join('|'), ...rows].join('\n');
|
||||||
@ -1112,4 +1115,43 @@ export class DealerClaimController {
|
|||||||
return ResponseHandler.error(res, 'Failed to re-trigger WFM push', 500, errorMessage);
|
return ResponseHandler.error(res, 'Failed to re-trigger WFM push', 500, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch parsed WFM credit note CSV from outgoing folder
|
||||||
|
* GET /api/v1/dealer-claims/:requestId/credit-note-wfm
|
||||||
|
*/
|
||||||
|
async fetchCreditNoteWfm(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const identifier = req.params.requestId;
|
||||||
|
|
||||||
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
||||||
|
if (!workflow) {
|
||||||
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
||||||
|
const requestNumber = (workflow as any).requestNumber || (workflow as any).request_number;
|
||||||
|
|
||||||
|
const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
|
||||||
|
if (!claimDetails) {
|
||||||
|
return ResponseHandler.error(res, 'Dealer claim details not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isNonGst = false;
|
||||||
|
if (claimDetails.activityType) {
|
||||||
|
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||||
|
const taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
|
||||||
|
isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { wfmFileService } = await import('../services/wfmFile.service');
|
||||||
|
const creditNoteData = await wfmFileService.getCreditNoteDetails(claimDetails.dealerCode, requestNumber, isNonGst);
|
||||||
|
|
||||||
|
return ResponseHandler.success(res, creditNoteData, 'Credit note data fetched successfully');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[DealerClaimController] Error fetching credit note WFM data:', error);
|
||||||
|
return ResponseHandler.error(res, 'Failed to fetch credit note CSV data', 500, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,6 +107,7 @@ router.post('/:requestId/wfm/retrigger', authenticateToken, validateParams(reque
|
|||||||
*/
|
*/
|
||||||
router.get('/:requestId/e-invoice/csv', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.downloadInvoiceCsv.bind(dealerClaimController)));
|
router.get('/:requestId/e-invoice/csv', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.downloadInvoiceCsv.bind(dealerClaimController)));
|
||||||
router.post('/:requestId/credit-note', authenticateToken, validateParams(requestIdParamsSchema), upload.single('creditNoteFile'), asyncHandler(malwareScanMiddleware), asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
router.post('/:requestId/credit-note', authenticateToken, validateParams(requestIdParamsSchema), upload.single('creditNoteFile'), asyncHandler(malwareScanMiddleware), asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
||||||
|
router.get('/:requestId/credit-note-wfm', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.fetchCreditNoteWfm.bind(dealerClaimController)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route POST /api/v1/dealer-claims/:requestId/credit-note/send
|
* @route POST /api/v1/dealer-claims/:requestId/credit-note/send
|
||||||
|
|||||||
@ -3596,9 +3596,13 @@ export class DealerClaimService {
|
|||||||
|
|
||||||
if (invoiceItems.length > 0) {
|
if (invoiceItems.length > 0) {
|
||||||
let sapRefNo = '';
|
let sapRefNo = '';
|
||||||
|
let isNonGst = false;
|
||||||
|
|
||||||
if (claimDetails.activityType) {
|
if (claimDetails.activityType) {
|
||||||
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||||
sapRefNo = activity?.sapRefNo || '';
|
sapRefNo = activity?.sapRefNo || '';
|
||||||
|
const taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
|
||||||
|
isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date: any) => {
|
const formatDate = (date: any) => {
|
||||||
@ -3610,10 +3614,8 @@ export class DealerClaimService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const csvData = invoiceItems.map((item: any) => {
|
const csvData = invoiceItems.map((item: any) => {
|
||||||
const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0);
|
const row: any = {
|
||||||
|
TRNS_UNIQ_NO: isNonGst ? '' : (item.transactionCode || ''),
|
||||||
return {
|
|
||||||
TRNS_UNIQ_NO: item.transactionCode || '',
|
|
||||||
CLAIM_NUMBER: requestNumber,
|
CLAIM_NUMBER: requestNumber,
|
||||||
INV_NUMBER: invoice.invoiceNumber || '',
|
INV_NUMBER: invoice.invoiceNumber || '',
|
||||||
DEALER_CODE: claimDetails.dealerCode,
|
DEALER_CODE: claimDetails.dealerCode,
|
||||||
@ -3621,13 +3623,19 @@ export class DealerClaimService {
|
|||||||
CLAIM_DOC_TYP: sapRefNo,
|
CLAIM_DOC_TYP: sapRefNo,
|
||||||
CLAIM_TYPE: claimDetails.activityType,
|
CLAIM_TYPE: claimDetails.activityType,
|
||||||
CLAIM_DATE: formatDate(invoice.invoiceDate || new Date()),
|
CLAIM_DATE: formatDate(invoice.invoiceDate || new Date()),
|
||||||
CLAIM_AMT: item.assAmt,
|
CLAIM_AMT: item.assAmt
|
||||||
GST_AMT: totalTax.toFixed(2),
|
|
||||||
GST_PERCENTAGE: item.gstRt
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isNonGst) {
|
||||||
|
const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0);
|
||||||
|
row.GST_AMT = totalTax.toFixed(2);
|
||||||
|
row.GST_PERCENTAGE = item.gstRt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
});
|
});
|
||||||
|
|
||||||
await wfmFileService.generateIncomingClaimCSV(csvData, `CN_${claimDetails.dealerCode}_${requestNumber}.csv`);
|
await wfmFileService.generateIncomingClaimCSV(csvData, `CN_${claimDetails.dealerCode}_${requestNumber}.csv`, isNonGst);
|
||||||
|
|
||||||
await invoice.update({
|
await invoice.update({
|
||||||
wfmPushStatus: 'SUCCESS',
|
wfmPushStatus: 'SUCCESS',
|
||||||
|
|||||||
@ -8,13 +8,17 @@ import logger from '../utils/logger';
|
|||||||
*/
|
*/
|
||||||
export class WFMFileService {
|
export class WFMFileService {
|
||||||
private basePath: string;
|
private basePath: string;
|
||||||
private incomingClaimsPath: string;
|
private incomingGstClaimsPath: string;
|
||||||
private outgoingClaimsPath: string;
|
private incomingNonGstClaimsPath: string;
|
||||||
|
private outgoingGstClaimsPath: string;
|
||||||
|
private outgoingNonGstClaimsPath: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.basePath = process.env.WFM_BASE_PATH || 'C:\\WFM';
|
this.basePath = process.env.WFM_BASE_PATH || 'C:\\WFM';
|
||||||
this.incomingClaimsPath = process.env.WFM_INCOMING_CLAIMS_PATH || 'WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS';
|
this.incomingGstClaimsPath = process.env.WFM_INCOMING_GST_CLAIMS_PATH || 'WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS_GST';
|
||||||
this.outgoingClaimsPath = process.env.WFM_OUTGOING_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS';
|
this.incomingNonGstClaimsPath = process.env.WFM_INCOMING_NON_GST_CLAIMS_PATH || 'WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS_NON_GST';
|
||||||
|
this.outgoingGstClaimsPath = process.env.WFM_OUTGOING_GST_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_GST';
|
||||||
|
this.outgoingNonGstClaimsPath = process.env.WFM_OUTGOING_NON_GST_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_NON_GST';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,13 +36,14 @@ export class WFMFileService {
|
|||||||
* @param data The data to be written to the CSV
|
* @param data The data to be written to the CSV
|
||||||
* @param fileName The name of the file (e.g., CLAIM_12345.csv)
|
* @param fileName The name of the file (e.g., CLAIM_12345.csv)
|
||||||
*/
|
*/
|
||||||
async generateIncomingClaimCSV(data: any[], fileName: string): Promise<string> {
|
async generateIncomingClaimCSV(data: any[], fileName: string, isNonGst: boolean = false): Promise<string> {
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
|
|
||||||
while (retryCount <= maxRetries) {
|
while (retryCount <= maxRetries) {
|
||||||
try {
|
try {
|
||||||
const targetDir = path.join(this.basePath, this.incomingClaimsPath);
|
const targetPath = isNonGst ? this.incomingNonGstClaimsPath : this.incomingGstClaimsPath;
|
||||||
|
const targetDir = path.join(this.basePath, targetPath);
|
||||||
this.ensureDirectoryExists(targetDir);
|
this.ensureDirectoryExists(targetDir);
|
||||||
|
|
||||||
const filePath = path.join(targetDir, fileName.endsWith('.csv') ? fileName : `${fileName}.csv`);
|
const filePath = path.join(targetDir, fileName.endsWith('.csv') ? fileName : `${fileName}.csv`);
|
||||||
@ -75,8 +80,42 @@ export class WFMFileService {
|
|||||||
/**
|
/**
|
||||||
* Get the absolute path for an outgoing claim file
|
* Get the absolute path for an outgoing claim file
|
||||||
*/
|
*/
|
||||||
getOutgoingPath(fileName: string): string {
|
getOutgoingPath(fileName: string, isNonGst: boolean = false): string {
|
||||||
return path.join(this.basePath, this.outgoingClaimsPath, fileName);
|
const targetPath = isNonGst ? this.outgoingNonGstClaimsPath : this.outgoingGstClaimsPath;
|
||||||
|
return path.join(this.basePath, targetPath, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get credit note details from outgoing CSV
|
||||||
|
*/
|
||||||
|
async getCreditNoteDetails(dealerCode: string, requestNumber: string, isNonGst: boolean = false): Promise<any[]> {
|
||||||
|
const fileName = `CN_${dealerCode}_${requestNumber}.csv`;
|
||||||
|
const filePath = this.getOutgoingPath(fileName, isNonGst);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const lines = fileContent.split('\n').filter(line => line.trim() !== '');
|
||||||
|
if (lines.length <= 1) return []; // Only headers or empty
|
||||||
|
|
||||||
|
const headers = lines[0].split('|');
|
||||||
|
const data = lines.slice(1).map(line => {
|
||||||
|
const values = line.split('|');
|
||||||
|
const row: any = {};
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
row[header.trim()] = values[index]?.trim() || '';
|
||||||
|
});
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[WFMFileService] Error reading credit note CSV: ${fileName}`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user