mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-22 09:35:34 +07:00
continue edit & add rule threshold
This commit is contained in:
parent
1389eac272
commit
0f4dd0d522
@ -60,5 +60,20 @@ export function useFormValidation(schema, form, defaultErrors, valMode) {
|
||||
Object.assign(errors, defaultErrors);
|
||||
}
|
||||
|
||||
return { errors, validateField, resetErrors }
|
||||
function validateAll() {
|
||||
const result = schema.safeParse(form);
|
||||
|
||||
Object.assign(errors, defaultErrors);
|
||||
|
||||
if (!result.success) {
|
||||
for (const issue of result.error.issues) {
|
||||
const field = issue.path[0];
|
||||
if (field && field in errors) {
|
||||
errors[field] = issue.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { errors, validateField, resetErrors, validateAll }
|
||||
}
|
||||
@ -39,7 +39,7 @@ export function useForm({schema, initialForm, defaultErrors = {}, mode = 'create
|
||||
|
||||
return {
|
||||
...state, //form, resetForm, setForm, isSaving
|
||||
...val, //errors, validateField, resetErrors
|
||||
...val, //errors, validateField, resetErrors, validateAll
|
||||
...options, //selectOptions, loadingOptions, fetchOptions, lastFetched, clearDependentOptions
|
||||
save,
|
||||
reset,
|
||||
|
||||
@ -103,13 +103,13 @@ export const testGroupSchema = z
|
||||
|
||||
export const refNumSchema = z
|
||||
.object({
|
||||
AgeStart: z.string().optional(),
|
||||
AgeEnd: z.string().optional(),
|
||||
Low: z.string().optional(),
|
||||
High: z.string().optional(),
|
||||
LowSign: z.string().optional(),
|
||||
HighSign: z.string().optional(),
|
||||
NumRefType: z.string().optional()
|
||||
AgeStart: z.coerce.string().optional(),
|
||||
AgeEnd: z.coerce.string().optional(),
|
||||
Low: z.coerce.string().optional(),
|
||||
High: z.coerce.string().optional(),
|
||||
LowSign: z.string().optional(),
|
||||
HighSign: z.string().optional(),
|
||||
NumRefType: z.string().optional()
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const start = toDays(data.AgeStart);
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
import Group from './tabs/group.svelte';
|
||||
import { API } from "$lib/config/api";
|
||||
import { untrack } from "svelte";
|
||||
import { buildAgeText, daysToAge } from '$lib/utils/ageUtils';
|
||||
|
||||
let props = $props();
|
||||
|
||||
@ -128,15 +129,24 @@
|
||||
let showConfirm = $state(false);
|
||||
|
||||
async function handleEdit() {
|
||||
const result = await formState.save(masterDetail.mode);
|
||||
|
||||
if (result.status === 'success') {
|
||||
console.log('Test updated successfully');
|
||||
toast('Test Updated!');
|
||||
masterDetail.exitForm(true);
|
||||
} else {
|
||||
console.error('Failed to update test:', result.message);
|
||||
}
|
||||
const mainForm = masterDetail.formState.form;
|
||||
const testType = mainForm.TestType;
|
||||
const cleanMapData = mapData.map(({ options, ...rest }) => rest);
|
||||
|
||||
const payload = buildTestPayload({
|
||||
mainForm,
|
||||
activeFormStates,
|
||||
testType: testType,
|
||||
refNumData: refNumData,
|
||||
refTxtData: refTxtData,
|
||||
mapData: cleanMapData,
|
||||
});
|
||||
console.log(payload);
|
||||
|
||||
const result = await formState.save(masterDetail.mode, payload);
|
||||
|
||||
toast('Test Updated!');
|
||||
masterDetail?.exitForm(true);
|
||||
}
|
||||
|
||||
const primaryAction = $derived({
|
||||
@ -318,8 +328,13 @@
|
||||
|
||||
$effect(() => {
|
||||
const mainForm = formState.form;
|
||||
if (mainForm.refnum && Array.isArray(mainForm.refnum)) {
|
||||
refNumData = mainForm.refnum
|
||||
if (mainForm.refnum && Array.isArray(mainForm.refnum)) {
|
||||
refNumData = mainForm.refnum.map((row, index) => ({
|
||||
id: row.id ?? index + 1,
|
||||
...row,
|
||||
AgeStart: typeof row.AgeStart === 'number' ? buildAgeText(daysToAge(row.AgeStart)) : row.AgeStart,
|
||||
AgeEnd: typeof row.AgeEnd === 'number' ? buildAgeText(daysToAge(row.AgeEnd)) : row.AgeEnd,
|
||||
}));
|
||||
}
|
||||
})
|
||||
|
||||
@ -354,6 +369,69 @@
|
||||
formState.form[key] = '';
|
||||
}
|
||||
});
|
||||
|
||||
function handleTestTypeChange(value) {
|
||||
formState.form.TestType = value;
|
||||
|
||||
formState.form.ResultType = '';
|
||||
formState.errors.ResultType = null;
|
||||
formState.form.RefType = '';
|
||||
formState.errors.RefType = null;
|
||||
|
||||
calFormState.reset();
|
||||
refNumState.reset();
|
||||
refTxtState.reset();
|
||||
|
||||
resetRefNum?.();
|
||||
resetRefTxt?.();
|
||||
}
|
||||
|
||||
function handleResultTypeChange(value) {
|
||||
formState.form.ResultType = value;
|
||||
|
||||
formState.form.RefType = '';
|
||||
formState.errors.RefType = null;
|
||||
|
||||
calFormState.reset();
|
||||
refNumState.reset();
|
||||
refTxtState.reset();
|
||||
|
||||
resetRefNum?.();
|
||||
resetRefTxt?.();
|
||||
|
||||
let newRefType = '';
|
||||
if (value === 'TEXT') {
|
||||
newRefType = 'TEXT';
|
||||
}
|
||||
if (value === 'VSET') {
|
||||
newRefType = 'VSET';
|
||||
}
|
||||
if (value === 'NORES') {
|
||||
newRefType = 'NOREF';
|
||||
}
|
||||
|
||||
if (newRefType) {
|
||||
formState.form.RefType = newRefType;
|
||||
handleRefTypeChange(newRefType);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefTypeChange(value) {
|
||||
formState.form.RefType = value;
|
||||
|
||||
refNumState.reset();
|
||||
refTxtState.reset();
|
||||
|
||||
resetRefNum?.();
|
||||
resetRefTxt?.();
|
||||
|
||||
if (value === 'RANGE' || value === 'THOLD') {
|
||||
refNumState.form.NumRefType = value;
|
||||
}
|
||||
if (value === 'TEXT' || value === 'VSET') {
|
||||
refTxtState.form.TxtRefType = value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormPageContainer title="Edit Test" {primaryAction} {secondaryActions}>
|
||||
@ -380,7 +458,12 @@
|
||||
{formState}
|
||||
formFields={formFields}
|
||||
mode="edit"
|
||||
{hiddenFields}
|
||||
{disabledResultTypes}
|
||||
{disabledReferenceTypes}
|
||||
{hiddenFields}
|
||||
{handleTestTypeChange}
|
||||
{handleResultTypeChange}
|
||||
{handleRefTypeChange}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="calculation">
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { buildAgeText } from '$lib/utils/ageUtils';
|
||||
import { buildAgeText, daysToAge } from '$lib/utils/ageUtils';
|
||||
import PencilIcon from '@lucide/svelte/icons/pencil';
|
||||
import Trash2Icon from '@lucide/svelte/icons/trash-2';
|
||||
import { untrack } from 'svelte';
|
||||
@ -40,8 +40,8 @@
|
||||
if (refType === 'THOLD') return false;
|
||||
|
||||
return false;
|
||||
})
|
||||
|
||||
});
|
||||
// $inspect(props.refNumState.form.NumRefType)
|
||||
function snapshotForm() {
|
||||
const f = props.refNumState.form;
|
||||
return {
|
||||
@ -70,7 +70,7 @@
|
||||
if (currentRefType) {
|
||||
props.refNumState.form.NumRefType = currentRefType;
|
||||
}
|
||||
|
||||
|
||||
joinFields = {
|
||||
AgeStart: { DD: '', MM: '', YY: '' },
|
||||
AgeEnd: { DD: '', MM: '', YY: '' }
|
||||
@ -78,25 +78,148 @@
|
||||
editingId = null;
|
||||
}
|
||||
|
||||
function validateTholdContinuity(excludeId = null) {
|
||||
const form = props.refNumState.form;
|
||||
if (form.NumRefType !== 'THOLD') return null;
|
||||
|
||||
// Ambil semua record THOLD dalam group yang sama, exclude row yang sedang diedit
|
||||
const peers = tempNumeric.filter(row =>
|
||||
row.id !== excludeId &&
|
||||
row.NumRefType === 'THOLD' &&
|
||||
row.SpcType === form.SpcType &&
|
||||
row.Sex === form.Sex &&
|
||||
row.RangeType === form.RangeType
|
||||
);
|
||||
|
||||
console.log('peers:', JSON.stringify(peers));
|
||||
console.log('newLow:', form.Low, 'newLowSign:', form.LowSign);
|
||||
console.log('newHigh:', form.High, 'newHighSign:', form.HighSign);
|
||||
|
||||
if (peers.length === 0) return null;
|
||||
|
||||
const newLow = form.Low !== '' && form.Low != null ? Number(form.Low) : null;
|
||||
const newHigh = form.High !== '' && form.High != null ? Number(form.High) : null;
|
||||
const newLowSign = form.LowSign ?? '';
|
||||
const newHighSign = form.HighSign ?? '';
|
||||
|
||||
// Cari row yang high-nya paling dekat di bawah newLow
|
||||
const closestBelow = peers
|
||||
.filter(r => r.High !== '' && r.High != null)
|
||||
.map(r => ({ ...r, highNum: Number(r.High) }))
|
||||
.filter(r => r.highNum <= newLow)
|
||||
.sort((a, b) => b.highNum - a.highNum)[0];
|
||||
|
||||
if (closestBelow) {
|
||||
const prevHigh = closestBelow.highNum;
|
||||
const prevHighSign = closestBelow.HighSign ?? '';
|
||||
|
||||
if (prevHigh !== newLow) {
|
||||
// Ada celah
|
||||
return { field: 'Low', message: `Gap between intervals. Previous interval ends at ${prevHigh}, new interval starts at ${newLow}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (const row of peers) {
|
||||
const rowHigh = row.High !== '' && row.High != null ? Number(row.High) : null;
|
||||
const rowLow = row.Low !== '' && row.Low != null ? Number(row.Low) : null;
|
||||
const rowHighSign = row.HighSign ?? '';
|
||||
const rowLowSign = row.LowSign ?? '';
|
||||
|
||||
console.log('rowHigh:', rowHigh, typeof rowHigh);
|
||||
console.log('newLow:', newLow, typeof newLow);
|
||||
console.log('equal?:', rowHigh === newLow);
|
||||
|
||||
// No. 4 & 6 — tanpa celah & tanpa sign kontradiktif
|
||||
// Cek apakah interval baru harus dimulai tepat setelah interval sebelumnya berakhir
|
||||
if (rowHigh != null && newLow != null && rowHigh === newLow) {
|
||||
console.log('boundary check:', rowHighSign, newLowSign);
|
||||
|
||||
// rowHighSign <= 200, newLowSign harus > 200
|
||||
// rowHighSign < 200, newLowSign harus >= 200
|
||||
const prevEndInclusive = rowHighSign === '<=';
|
||||
const newStartInclusive = newLowSign === '>=';
|
||||
console.log('prevEndInclusive:', prevEndInclusive, 'newStartInclusive:', newStartInclusive);
|
||||
|
||||
if (prevEndInclusive && newStartInclusive) {
|
||||
return { field: 'LowSign', message: 'Sign contradicts previous interval (overlap at boundary). Use > instead of >=' };
|
||||
}
|
||||
if (!prevEndInclusive && !newStartInclusive) {
|
||||
return { field: 'LowSign', message: 'Gap between intervals. Use >= instead of >' };
|
||||
}
|
||||
}
|
||||
|
||||
if (rowLow != null && newHigh != null && rowLow === newHigh) {
|
||||
const prevStartInclusive = rowLowSign === '>=';
|
||||
const newEndInclusive = newHighSign === '<=';
|
||||
|
||||
if (prevStartInclusive && newEndInclusive) {
|
||||
return { field: 'HighSign', message: 'Sign contradicts next interval (overlap at boundary). Use < instead of <=' };
|
||||
}
|
||||
if (!prevStartInclusive && !newEndInclusive) {
|
||||
return { field: 'HighSign', message: 'Gap between intervals. Use <= instead of <' };
|
||||
}
|
||||
}
|
||||
|
||||
// No. 5 — overlap: cek apakah ada nilai yang bisa memenuhi kedua interval
|
||||
// Contoh: row adalah "< 100", new adalah "> 90" → overlap di 91–99
|
||||
if (rowHigh != null && newLow != null) {
|
||||
const prevEnd = rowHigh;
|
||||
const newStart = newLow;
|
||||
|
||||
const prevInclusive = rowHighSign === '<=';
|
||||
const newInclusive = newLowSign === '>=';
|
||||
|
||||
const isOverlap =
|
||||
newStart < prevEnd ||
|
||||
(newStart === prevEnd && prevInclusive && newInclusive);
|
||||
|
||||
if (isOverlap) {
|
||||
return { field: 'Low', message: 'This interval overlaps with an existing interval' };
|
||||
}
|
||||
}
|
||||
|
||||
if (rowLow != null && newHigh != null) {
|
||||
const prevStart = rowLow;
|
||||
const newEnd = newHigh;
|
||||
|
||||
const prevInclusive = rowLowSign === '>=';
|
||||
const newInclusive = newHighSign === '<=';
|
||||
|
||||
const isOverlap =
|
||||
newEnd > prevStart ||
|
||||
(newEnd === prevStart && prevInclusive && newInclusive);
|
||||
|
||||
if (isOverlap) {
|
||||
return { field: 'High', message: 'This interval overlaps with an existing interval' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
// $inspect(tempNumeric)
|
||||
function handleInsert() {
|
||||
// console.log(props.refNumState.form);
|
||||
// const low = Number(props.refNumState.form.Low);
|
||||
// const high = Number(props.refNumState.form.High);
|
||||
const newStart = toDays(props.refNumState.form.AgeStart);
|
||||
const newEnd = toDays(props.refNumState.form.AgeEnd);
|
||||
// const row = { id: ++idCounter, ...snapshotForm() };
|
||||
// tempNumeric = [...tempNumeric, row];
|
||||
// resetForm();
|
||||
|
||||
const isOverlap = tempNumeric.some((row) => {
|
||||
if (row.SpcType !== props.refNumState.form.SpcType) return false;
|
||||
if (row.Sex !== props.refNumState.form.Sex) return false;
|
||||
if (row.NumRefType !== props.refNumState.form.NumRefType) return false;
|
||||
if (row.RangeType !== props.refNumState.form.RangeType) return false;
|
||||
|
||||
if (row.id === editingId) return false;
|
||||
|
||||
const existingStart = toDays(row.AgeStart);
|
||||
const existingEnd = toDays(row.AgeEnd);
|
||||
|
||||
if (existingStart == null || existingEnd == null) return false;
|
||||
|
||||
if (row.RangeType !== props.refNumState.form.RangeType) return false;
|
||||
|
||||
return !(newEnd < existingStart || newStart > existingEnd);
|
||||
return (newStart <= existingEnd && newEnd >= existingStart);
|
||||
});
|
||||
|
||||
if (isOverlap) {
|
||||
@ -104,6 +227,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const tholdError = validateTholdContinuity();
|
||||
if (tholdError) {
|
||||
props.refNumState.errors[tholdError.field] = tholdError.message;
|
||||
return;
|
||||
}
|
||||
|
||||
const row = {
|
||||
id: ++idCounter,
|
||||
...snapshotForm()
|
||||
@ -113,33 +242,37 @@
|
||||
|
||||
resetForm();
|
||||
}
|
||||
$inspect(props.refNumState.form)
|
||||
|
||||
function handleEdit(row) {
|
||||
editingId = row.id;
|
||||
|
||||
const f = props.refNumState.form;
|
||||
f.SpcType = row.SpcType;
|
||||
f.Sex = row.Sex;
|
||||
f.AgeStart = row.AgeStart;
|
||||
f.AgeEnd = row.AgeEnd;
|
||||
f.NumRefType = row.NumRefType;
|
||||
f.RangeType = row.RangeType;
|
||||
f.LowSign = row.LowSign;
|
||||
f.Low = row.Low;
|
||||
f.HighSign = row.HighSign;
|
||||
f.High = row.High;
|
||||
f.Display = row.Display;
|
||||
f.Flag = row.Flag;
|
||||
f.Interpretation = row.Interpretation;
|
||||
f.Notes = row.Notes;
|
||||
|
||||
for (const key of ['AgeStart', 'AgeEnd']) {
|
||||
const val = row[key] ?? '';
|
||||
const match = val.match(/(\d+)Y\s*(\d+)M\s*(\d+)D/);
|
||||
joinFields[key] = match
|
||||
? { YY: match[1], MM: match[2], DD: match[3] }
|
||||
: { DD: '', MM: '', YY: '' };
|
||||
if (typeof val === 'number') {
|
||||
joinFields[key] = daysToAge(val);
|
||||
} else {
|
||||
const match = val.match(/(\d+)Y\s*(\d+)M\s*(\d+)D/);
|
||||
joinFields[key] = match
|
||||
? { YY: match[1], MM: match[2], DD: match[3] }
|
||||
: { DD: '', MM: '', YY: '' };
|
||||
}
|
||||
}
|
||||
|
||||
const f = props.refNumState.form;
|
||||
f.SpcType = row.SpcType ?? '';
|
||||
f.Sex = row.Sex ?? '';
|
||||
f.AgeStart = row.AgeStart ?? '';
|
||||
f.AgeEnd = row.AgeEnd ?? '';
|
||||
f.NumRefType = row.NumRefType ?? '';
|
||||
f.RangeType = row.RangeType ?? '';
|
||||
f.LowSign = row.LowSign ?? '';
|
||||
f.Low = row.Low ?? '';
|
||||
f.HighSign = row.HighSign ?? '';
|
||||
f.High = row.High ?? '';
|
||||
f.Display = row.Display ?? '';
|
||||
f.Flag = row.Flag ?? '';
|
||||
f.Interpretation = row.Interpretation ?? '';
|
||||
f.Notes = row.Notes ?? '';
|
||||
}
|
||||
|
||||
function handleUpdate() {
|
||||
@ -183,6 +316,24 @@ $inspect(props.refNumState.form)
|
||||
for (const key of ['AgeStart', 'AgeEnd']) {
|
||||
props.refNumState.form[key] = buildAgeText(joinFields[key]);
|
||||
}
|
||||
untrack(() => {
|
||||
props.refNumState.validateAll?.();
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const currentEditingId = editingId;
|
||||
if (currentEditingId === null) return;
|
||||
|
||||
untrack(() => {
|
||||
for (const key of ['AgeStart', 'AgeEnd']) {
|
||||
const val = props.refNumState.form[key] ?? '';
|
||||
const match = val.match(/(\d+)Y\s*(\d+)M\s*(\d+)D/);
|
||||
if (match) {
|
||||
joinFields[key] = { YY: match[1], MM: match[2], DD: match[3] };
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@ -216,6 +367,17 @@ $inspect(props.refNumState.form)
|
||||
props.refNumState.form.HighSign = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const maxId = tempNumeric.reduce((max, row) => {
|
||||
const rowId = typeof row.id === 'number' ? row.id : 0;
|
||||
return rowId > max ? rowId : max;
|
||||
}, 0);
|
||||
|
||||
if (maxId > idCounter) {
|
||||
idCounter = maxId;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
@ -277,7 +439,7 @@ $inspect(props.refNumState.form)
|
||||
-
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium flex justify-between">
|
||||
<!-- <Table.Cell class="font-medium flex justify-between">
|
||||
<div>
|
||||
{row.LowSign ? row.LowSign : ''}
|
||||
{row.Low || 'null'} –
|
||||
@ -287,8 +449,26 @@ $inspect(props.refNumState.form)
|
||||
<Badge variant="outline" class="border-dashed border-primary border-2"
|
||||
>{numRefTypeBadge(row.NumRefType)}</Badge
|
||||
>
|
||||
</Table.Cell> -->
|
||||
<Table.Cell class="font-medium flex justify-between">
|
||||
<div>
|
||||
{#if row.Low && row.High}
|
||||
{row.LowSign} {row.Low} – {row.HighSign} {row.High}
|
||||
{:else if row.Low}
|
||||
{row.LowSign} {row.Low}
|
||||
{:else if row.High}
|
||||
{row.HighSign} {row.High}
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</div>
|
||||
<Badge variant="outline" class="border-dashed border-primary border-2">
|
||||
{numRefTypeBadge(row.NumRefType)}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium">{row.NumRefType === "RANGE" ? "AUTO" : row.Flag}</Table.Cell>
|
||||
<Table.Cell class="font-medium"
|
||||
>{row.NumRefType === 'RANGE' ? 'AUTO' : row.Flag}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="font-medium">{row.Interpretation}</Table.Cell>
|
||||
<Table.Cell class="font-medium">{row.Notes}</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
@ -394,10 +394,12 @@
|
||||
bind:value={formState.form[txtKey]}
|
||||
oninput={() => {
|
||||
if (validateOn?.includes('input')) {
|
||||
formState.validateField('Low', formState.form[txtKey]);
|
||||
formState.validateField('LowSign');
|
||||
formState.validateField('High', formState.form[txtKey]);
|
||||
formState.validateField('HighSign');
|
||||
formState.validateField(txtKey, formState.form[txtKey]);
|
||||
formState.validateField(key);
|
||||
// formState.validateField('Low', formState.form[txtKey]);
|
||||
// formState.validateField('LowSign');
|
||||
// formState.validateField('High', formState.form[txtKey]);
|
||||
// formState.validateField('HighSign');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -33,20 +33,6 @@ export function daysToAge(totalDays) {
|
||||
return { YY, MM, DD };
|
||||
}
|
||||
|
||||
// export function toDays(ageString) {
|
||||
// if (!ageString || typeof ageString !== 'string') return null;
|
||||
|
||||
// const match = ageString.match(/(\d+)\s*Y?\s*(\d+)\s*M?\s*(\d+)\s*D?/i);
|
||||
|
||||
// if (!match) return null;
|
||||
|
||||
// const YY = parseInt(match[1]) || 0;
|
||||
// const MM = parseInt(match[2]) || 0;
|
||||
// const DD = parseInt(match[3]) || 0;
|
||||
|
||||
// return YY * 365 + MM * 30 + DD;
|
||||
// }
|
||||
|
||||
export function toDays(ageString) {
|
||||
if (!ageString || typeof ageString !== "string") return null;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user