Compare commits
2 Commits
c7a541a9e3
...
f6408e0188
Author | SHA1 | Date | |
---|---|---|---|
f6408e0188 | |||
baa8103167 |
@ -12,20 +12,19 @@
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group">
|
||||
<button v-for="report in reports" class="btn" :class="{ 'btn-primary': report.enabled, 'btn-outline-primary': !report.enabled }" @click="toggle(report)">{{report.name}}</button>
|
||||
<button v-for="report in reports" class="btn" :class="{ 'btn-primary': report.enabled, 'btn-outline-primary': !report.enabled }" @click="enable(report)">{{report.name}}<input type="checkbox" class="form-check-input" :checked="report.enabled" @click.stop="report.enabled = !report.enabled" /></button>
|
||||
</div>
|
||||
<button class="btn" :class="{ 'btn-success': unify, 'btn-outline-success': !unify }" @click="unify = !unify">Unify</button>
|
||||
<DateRangePicker range="1M" direction="-1" v-model:date="date_end" v-model:date_end="date_begin" />
|
||||
<DateRangePicker range="Range" direction="-1" v-model:date="date_end" v-model:date_end="date_begin" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="resultset.length > 0" class="row">
|
||||
<div class="row">
|
||||
<div class="selector col-12" :class="{ 'col-xl-4': selection }">
|
||||
<div class="card mb-3 shadow">
|
||||
<div class="card-header"><template v-if="resultset.length > 0"><template v-if="resultset.length > rs_filtered.length">{{rs_filtered.length}} of </template>{{resultset.length}}</template><template v-else-if="is_loading">Loading</template><template v-else>No</template> record{{resultset.length == 1 ? '' : 's'}}</div>
|
||||
<ul class="scroller list-group list-group-flush" :class="{ 'list-skinny': selection }" ref="scroller">
|
||||
<li v-for="item in rs_filtered" :key="item" class="record list-group-item" :class="{ 'active': (selection) && (selection.id == item.id) }" @click="selection = item"><span class="badge emblem" :class="[item.emblem]" /> <span class="datetime date">{{datestring(item.time)}}</span> <span class="datetime time">{{timestring(item.time)}}</span> • <span class="title"><span v-for="title in item.title">{{title}}</span></span><template v-if="item.snippets"><div v-for="snippet in item.snippets" class="snippet" v-html="snippet" /></template></li>
|
||||
<li class="bottom list-group-item" ref="bottom" />
|
||||
<li class="bottom list-group-item" ref="bottom"><button v-if="date_next" class="btn btn-outline-primary" :disabled="is_loading" @click="date_begin = date_next"><template v-if="is_loading">Loading</template><template v-else>Load</template> back to {{datestring(date_next)}}<template v-if="is_loading">…</template></button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -56,6 +55,12 @@
|
||||
top: 3.65rem;
|
||||
z-index: 2;
|
||||
}
|
||||
div.filter input.form-check-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
div.selector {
|
||||
position: sticky;
|
||||
top: 1.15rem;
|
||||
@ -77,8 +82,8 @@
|
||||
color: #fff;
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
li.bottom {
|
||||
padding: 0;
|
||||
li.bottom button {
|
||||
width: 100%;
|
||||
}
|
||||
span.badge.emblem:empty {
|
||||
display: inline-block;
|
||||
@ -159,7 +164,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { uniq, debounce, strftime_vista } from './util.mjs';
|
||||
import { flow, uniq, debounce, strftime_vista, strptime_vista } from './util.mjs';
|
||||
|
||||
import Subtitle from './Subtitle.vue';
|
||||
import DateRangePicker from './DateRangePicker.vue';
|
||||
@ -167,11 +172,24 @@
|
||||
const SZ_WINDOW = 100;
|
||||
const SZ_RANGE = 40000;
|
||||
|
||||
function f_parse_columns(item) {
|
||||
var res = [], line, brk, sub;
|
||||
for(var i = 0; i < item.length; ++i) {
|
||||
brk = (line = item[i]).indexOf('^');
|
||||
if(brk >= 0) {
|
||||
if(res[sub = line.substring(0, brk)]) res[sub].push(line.substring(brk + 1));
|
||||
else res[sub] = [line.substring(brk + 1)];
|
||||
}
|
||||
}
|
||||
for(var k in res) if(res[k]) res[k] = res[k].join('\n');
|
||||
return res;
|
||||
}
|
||||
|
||||
const create_reports = () => [
|
||||
{
|
||||
name: 'Notes',
|
||||
rpt_id: 'OR_PN:PROGRESS NOTES~TIUPRG;ORDV04;15;',
|
||||
map(x) {
|
||||
map: flow(f_parse_columns, function(x) {
|
||||
var time = new Date(x[3]);
|
||||
return {
|
||||
time,
|
||||
@ -180,13 +198,14 @@
|
||||
title: [x[4], x[5], '#' + x[2]],
|
||||
detail: escape_html(x[6])
|
||||
};
|
||||
},
|
||||
}),
|
||||
loader: reportloader_chunk,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'Labs',
|
||||
rpt_id: 'OR_OV_R:LAB OVERVIEW (COLLECTED SPECIMENS)~OV;ORDV02C;32;',
|
||||
map(x) {
|
||||
map: flow(f_parse_columns, function(x) {
|
||||
var time = new Date(x[2]);
|
||||
return {
|
||||
time,
|
||||
@ -195,12 +214,13 @@
|
||||
title: [x[3], x[6], x[8], x[10], '#' + x[12]],
|
||||
detail: escape_html(x[15])
|
||||
};
|
||||
}
|
||||
}),
|
||||
loader: reportloader_alpha
|
||||
},
|
||||
{
|
||||
name: 'Microbiology',
|
||||
rpt_id: 'OR_MIC:MICROBIOLOGY~MI;ORDV05;38;',
|
||||
map(x) {
|
||||
map: flow(f_parse_columns, function(x) {
|
||||
var time = new Date(x[2]);
|
||||
return {
|
||||
time,
|
||||
@ -209,27 +229,29 @@
|
||||
title: [x[3], x[4], x[5], '#' + x[6]],
|
||||
detail: escape_html(x[7])
|
||||
};
|
||||
}
|
||||
}),
|
||||
loader: reportloader_chunk
|
||||
},
|
||||
{
|
||||
name: 'Blood Bank',
|
||||
rpt_id: '2:BLOOD BANK REPORT~;;0',
|
||||
singleton: true,
|
||||
map(x) {
|
||||
var time = new Date();
|
||||
var now = new Date();
|
||||
return {
|
||||
time,
|
||||
time: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
|
||||
id: 'BB',
|
||||
emblem: 'emblem-bloodbank',
|
||||
title: ['BLOOD BANK'],
|
||||
detail: escape_html(x)
|
||||
detail: escape_html(x.join('\n'))
|
||||
};
|
||||
}
|
||||
},
|
||||
loader: reportloader_full
|
||||
},
|
||||
{
|
||||
name: 'Pathology',
|
||||
rpt_id: 'OR_APR:ANATOMIC PATHOLOGY~SP;ORDV02A;0;',
|
||||
map(x) {
|
||||
map: flow(f_parse_columns, function(x) {
|
||||
var time = new Date(x[2]);
|
||||
return {
|
||||
time,
|
||||
@ -238,12 +260,13 @@
|
||||
title: [x[3], '#' + x[4]],
|
||||
detail: escape_html(x[5])
|
||||
};
|
||||
}
|
||||
}),
|
||||
loader: reportloader_full
|
||||
},
|
||||
{
|
||||
name: 'Radiology',
|
||||
rpt_id: 'OR_R18:IMAGING~RIM;ORDV08;0;',
|
||||
map(x) {
|
||||
map: flow(f_parse_columns, function(x) {
|
||||
var time = new Date(x[2]);
|
||||
return {
|
||||
time,
|
||||
@ -252,10 +275,126 @@
|
||||
title: [x[3], x[4], x[5], '#' + x[9]],
|
||||
detail: escape_html(x[6])
|
||||
};
|
||||
}
|
||||
}),
|
||||
loader: reportloader_chunk
|
||||
},
|
||||
];
|
||||
|
||||
function data_limit(data, dt_alpha, dt_omega) {
|
||||
for(var i = 0, x, none = true; i < data.length; ++i) if(((x = data[i]).time <= dt_omega) || (!x.time) || (isNaN(x.time))) {
|
||||
data = data.slice(i);
|
||||
none = false;
|
||||
break;
|
||||
}
|
||||
if(none) return [];
|
||||
for(var i = data.length - 1, x, none = true; i >= 0; --i) if(((x = data[i]).time >= dt_alpha) || (!x.time) || (isNaN(x.time))) return i < data.length - 1 ? data.slice(0, i + 1) : data;
|
||||
return [];
|
||||
}
|
||||
|
||||
function data_endtime(data) {
|
||||
for(var i = data.length - 1; i >= 0; --i) if((data[i].time) && (!isNaN(data[i].time))) return data[i].time;
|
||||
}
|
||||
|
||||
function data_interval(data) {
|
||||
data = data.slice(data.length - SZ_WINDOW - 1).filter(x => (x.time) && (!isNaN(x.time)));
|
||||
return data.length > 1 ? (data[0].time - data[data.length - 1].time)/(data.length - 1)*SZ_WINDOW : 86400000*SZ_WINDOW;
|
||||
}
|
||||
|
||||
function reportloader_full(dfn, rpt_id, fn_map, omega) {
|
||||
var cachekey = dfn + ';' + rpt_id, dt_omega = strptime_vista(omega), data = null, dt_end;
|
||||
async function fn(client, alpha) {
|
||||
var dt_alpha = strptime_vista(alpha);
|
||||
if(!data) {
|
||||
data = (reportloader_full[cachekey] || (reportloader_full[cachekey] = await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', -1, -1))).map(fn_map).sort((a, b) => b.time - a.time), dt_end = null;
|
||||
dt_end = data_endtime(data);
|
||||
}
|
||||
var res = alpha !== undefined ? data_limit(data, dt_alpha, dt_omega) : [];
|
||||
if((data.length > 0) && ((dt_alpha > dt_end) || (alpha === undefined)) && ((res.length < 1) || (res[res.length - 1] !== data[data.length - 1]))) res.next = strftime_vista(res.dt_next = dt_end); // lookahead
|
||||
return res;
|
||||
}
|
||||
fn.omega = omega;
|
||||
return fn;
|
||||
}
|
||||
|
||||
function reportloader_alpha(dfn, rpt_id, fn_map, omega) {
|
||||
var dt_omega = strptime_vista(omega), cursor = Math.floor(strftime_vista(new Date())) + 0.235959999, interval = 86400000*3650, data = [], dt_end, hasmore = true;
|
||||
async function fn(client, alpha) {
|
||||
var dt_alpha = strptime_vista(alpha);
|
||||
if(alpha !== undefined) {
|
||||
if((hasmore) && (cursor >= alpha)) {
|
||||
while(cursor >= alpha) cursor = Math.floor(strftime_vista(new Date(strptime_vista(cursor) - interval)));
|
||||
data = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', cursor, -1)).map(fn_map).sort((a, b) => b.time - a.time);
|
||||
dt_end = data_endtime(data);
|
||||
}
|
||||
var res = data_limit(data, dt_alpha, dt_omega);
|
||||
} else var res = [];
|
||||
if((data.length > 0) && ((dt_alpha > dt_end) || (alpha === undefined)) && ((res.length < 1) || (res[res.length - 1] !== data[data.length - 1]))) res.next = strftime_vista(res.dt_next = dt_end); // lookahead
|
||||
else if(hasmore) {
|
||||
var count = data.length;
|
||||
cursor = Math.floor(strftime_vista(new Date(strptime_vista(cursor) - interval)));
|
||||
data = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', cursor, -1)).map(fn_map).sort((a, b) => b.time - a.time);
|
||||
if(data.length > count) res.next = strftime_vista(res.dt_next = dt_end = data_endtime(data));
|
||||
else {
|
||||
data = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', -1, -1)).map(fn_map).sort((a, b) => b.time - a.time);
|
||||
cursor = Math.floor(res.next = strftime_vista(res.dt_next = dt_end = data_endtime(data)));
|
||||
hasmore = false;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
fn.omega = omega;
|
||||
return fn;
|
||||
}
|
||||
|
||||
function reportloader_omega(dfn, rpt_id, fn_map, omega) {
|
||||
var dt_omega = strptime_vista(omega), cursor = Math.floor(omega) + 0.235959999, data = [], idmap = {};
|
||||
async function fn(client, alpha) {
|
||||
var dt_alpha = strptime_vista(alpha), batch;
|
||||
if(cursor >= alpha) {
|
||||
batch = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', Math.floor(alpha).toFixed(9), cursor.toFixed(9))).map(fn_map).sort((a, b) => b.time - a.time);
|
||||
cursor = strftime_vista(new Date(strptime_vista(Math.floor(alpha)) - 86400000)) + 0.235959999;
|
||||
batch = batch.filter(x => idmap[x.id] ? console.warn('Duplicate record', x) : true);
|
||||
batch.reduce((acc, val) => (acc[val.id] = val, acc), idmap);
|
||||
Array.prototype.push.apply(data, batch);
|
||||
}
|
||||
var dt_cursor = strptime_vista(cursor), interval = data_interval(data); // lookahead
|
||||
while(((batch = (await client.ORWRP_REPORT_TEXT_LONGCACHE(dfn, rpt_id, '', SZ_RANGE, '', Math.floor(strftime_vista(new Date(dt_cursor - interval))).toFixed(9), cursor.toFixed(9))).map(fn_map)).length < SZ_WINDOW) && (interval <= 86400000*SZ_RANGE)) interval *= 2;
|
||||
var res = alpha !== undefined ? data_limit(data, dt_alpha, dt_omega) : [];
|
||||
res.next = strftime_vista(res.dt_next = new Date(dt_cursor - interval));
|
||||
return res;
|
||||
}
|
||||
fn.omega = omega;
|
||||
return fn;
|
||||
}
|
||||
|
||||
function reportloader_chunk(dfn, rpt_id, fn_map, omega) {
|
||||
var dt_omega = strptime_vista(omega), cursor = Math.floor(omega) + 0.235959999, data = [], idmap = {}, hasmore = true;
|
||||
if(rpt_id.endsWith(';')) rpt_id += SZ_WINDOW;
|
||||
async function fn(client, alpha) {
|
||||
if(alpha !== undefined) {
|
||||
var dt_alpha = strptime_vista(alpha), batch;
|
||||
while((hasmore) && (cursor >= alpha)) {
|
||||
batch = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', -1, cursor.toFixed(9))).map(fn_map).sort((a, b) => b.time - a.time);
|
||||
cursor = data_endtime(batch); if(cursor) cursor = strftime_vista(cursor);
|
||||
hasmore = (cursor) && (batch.length >= SZ_WINDOW);
|
||||
batch = batch.filter(x => idmap[x.id] ? console.warn('Duplicate record', x) : true);
|
||||
batch.reduce((acc, val) => (acc[val.id] = val, acc), idmap);
|
||||
Array.prototype.push.apply(data, batch);
|
||||
}
|
||||
var res = data_limit(data, dt_alpha, dt_omega);
|
||||
} else var res = [];
|
||||
if(hasmore) { // lookahead
|
||||
var batch = (await client.ORWRP_REPORT_TEXT_LONGCACHE(dfn, rpt_id, '', SZ_RANGE, '', -1, cursor.toFixed(9))).map(fn_map).sort((a, b) => b.time - a.time);
|
||||
res.dt_next = data_endtime(batch);
|
||||
if(res.dt_next) res.next = strftime_vista(res.dt_next);
|
||||
}
|
||||
if((!res.dt_next) && (cursor) && (data.length > 0) && ((res.length < 1) || (res[res.length - 1] !== data[data.length - 1]))) res.dt_next = strptime_vista(res.next = cursor);
|
||||
return res;
|
||||
}
|
||||
fn.omega = omega;
|
||||
return fn;
|
||||
}
|
||||
|
||||
const escape_div = document.createElement('div');
|
||||
function escape_html(s) {
|
||||
escape_div.textContent = s;
|
||||
@ -286,12 +425,15 @@
|
||||
is_loading: false,
|
||||
date_end: now,
|
||||
date_begin: now,
|
||||
date_next: null,
|
||||
query: '',
|
||||
x_query: '',
|
||||
unify: false,
|
||||
reports: create_reports(),
|
||||
loaders: {},
|
||||
resultsets: {},
|
||||
selection: null
|
||||
selection: null,
|
||||
observer_scroller: null,
|
||||
observer_viewport: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -338,54 +480,35 @@
|
||||
timestring(date) {
|
||||
return date.toLocaleTimeString('en-GB').substring(0, 5);
|
||||
},
|
||||
toggle(report) {
|
||||
if(report.enabled) report.enabled = false;
|
||||
else if(this.unify) report.enabled = true;
|
||||
else {
|
||||
enable(report) {
|
||||
if(!report.enabled) {
|
||||
var reports = this.reports;
|
||||
for(var i = reports.length - 1; i >= 0; --i) reports[i].enabled = false;
|
||||
report.enabled = true;
|
||||
}
|
||||
},
|
||||
async load_more() {
|
||||
async loader_setup() {
|
||||
try {
|
||||
this.is_loading = true;
|
||||
if((this.client) && (this.patient_dfn)) {
|
||||
if(this.dfn != this.patient_dfn) {
|
||||
this.dfn = this.patient_dfn;
|
||||
this.loaders = {};
|
||||
this.resultsets = {};
|
||||
}
|
||||
var dfn = this.patient_dfn, resultsets = this.resultsets, resultset, reports = this.reports, report, alpha = strftime_vista(this.date_begin).toFixed(4), omega = strftime_vista(this.date_end).toFixed(4);
|
||||
for(var i = 0; i < reports.length; ++i) if(((report = reports[i]).enabled) && (!((resultset = resultsets[i]) && (alpha == resultset.alpha) && (omega == resultset.omega)))) {
|
||||
var data = [], batch, count, idmap = {}, omega0 = omega;
|
||||
do {
|
||||
batch = await this.client.ORWRP_REPORT_TEXT(dfn, report.rpt_id + (report.rpt_id.endsWith(';') ? SZ_WINDOW : ''), '', SZ_RANGE, '', alpha, omega0);
|
||||
count = batch.length;
|
||||
if(report.singleton) data = [report.map(batch[0].join('\n'))];
|
||||
else if((batch = batch.map(item => {
|
||||
var res = [], line, brk, sub;
|
||||
for(var i = 0; i < item.length; ++i) {
|
||||
brk = (line = item[i]).indexOf('^');
|
||||
if(brk >= 0) {
|
||||
if(res[sub = line.substring(0, brk)]) res[sub].push(line.substring(brk + 1));
|
||||
else res[sub] = [line.substring(brk + 1)];
|
||||
}
|
||||
}
|
||||
for(var k in res) if(res[k]) res[k] = res[k].join('\n');
|
||||
res = report.map(res);
|
||||
return idmap[res.id] ? console.warn('Duplicate record', res) : res;
|
||||
}).filter(x => x)).length > 0) {
|
||||
Array.prototype.push.apply(data, batch.sort((a, b) => b.time - a.time));
|
||||
batch.map(x => x.id).reduce((acc, val) => (acc[val] = true, acc), idmap);
|
||||
omega0 = strftime_vista(data[data.length - 1].time);
|
||||
};
|
||||
} while(count >= SZ_WINDOW);
|
||||
data.alpha = alpha;
|
||||
data.omega = omega;
|
||||
resultsets[i] = data;
|
||||
var dfn = this.patient_dfn, loaders = this.loaders, resultsets = this.resultsets, reports = this.reports, report, omega = strftime_vista(this.date_end), alpha = this.date_begin != this.date_end ? strftime_vista(this.date_begin) : undefined, next = [];
|
||||
for(var i = 0; i < reports.length; ++i) if((report = reports[i]).enabled) {
|
||||
if((!loaders[i]) || (loaders[i].omega != omega)) loaders[i] = report.loader(dfn, report.rpt_id, report.map, omega);
|
||||
resultsets[i] = await loaders[i](this.client, alpha);
|
||||
if(resultsets[i].next) next.push(resultsets[i].next);
|
||||
}
|
||||
if(next.length > 0) {
|
||||
this.date_next = strptime_vista(Math.floor(Math.max(...next)));
|
||||
if(!alpha) this.date_begin = this.date_next;
|
||||
}
|
||||
} else {
|
||||
this.dfn = null;
|
||||
this.loaders = {};
|
||||
this.resultsets = {};
|
||||
}
|
||||
} catch(ex) {
|
||||
@ -398,7 +521,7 @@
|
||||
created() {
|
||||
this.$watch(
|
||||
() => (this.client, this.patient_dfn, this.reports.map(x => x.enabled), this.date_begin, this.date_end, {}),
|
||||
debounce(() => this.load_more(), 500),
|
||||
debounce(() => this.loader_setup(), 500),
|
||||
{ immediate: true }
|
||||
);
|
||||
this.$watch(
|
||||
@ -406,6 +529,16 @@
|
||||
debounce(value => this.query = value, 500),
|
||||
{ immediate: true }
|
||||
);
|
||||
},
|
||||
mounted() {
|
||||
this.observer_scroller = new IntersectionObserver(() => { if((this.date_next) && (!this.query.replace(/^\s+|\s+$/g, '')) && (this.selection)) this.date_begin = this.date_next; }, { root: this.$refs.scroller, rootMargin: '25%' });
|
||||
this.observer_scroller.observe(this.$refs.bottom);
|
||||
this.observer_viewport = new IntersectionObserver(() => { if((this.date_next) && (!this.query.replace(/^\s+|\s+$/g, '')) && (!this.selection)) this.date_begin = this.date_next; }, { rootMargin: '25%' });
|
||||
this.observer_viewport.observe(this.$refs.bottom);
|
||||
},
|
||||
destroyed() {
|
||||
if(this.observer_viewport) this.observer_viewport.disconnect();
|
||||
if(this.observer_scroller) this.observer_scroller.disconnect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -292,6 +292,7 @@ export function Client(cid, secret) {
|
||||
this.ORWLRR_INTERIM_RESULTS = async (...args) => lab_reparse_results(await this.ORWLRR_INTERIM(...args));
|
||||
|
||||
this.ORWRP_REPORT_TEXT = aflow((...args) => this.call({ method: 'ORWRP_REPORT_TEXT', context: ['OR CPRS GUI CHART'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_array, d_parse_multireport);
|
||||
this.ORWRP_REPORT_TEXT_LONGCACHE = aflow((...args) => this.call({ method: 'ORWRP_REPORT_TEXT', context: ['OR CPRS GUI CHART'], ttl: 86400, stale: true }, ...args), d_log, d_unwrap, d_parse_array, d_parse_multireport);
|
||||
|
||||
this.ORWORDG_ALLTREE = memoized(aflow(() => this.callctx(['OR CPRS GUI CHART'], 'ORWORDG_ALLTREE'), d_log, d_unwrap, f_split('^', 'ien', 'name', 'parent', 'has_children')));
|
||||
this.ORWORDG_REVSTS = memoized(aflow(() => this.callctx(['OR CPRS GUI CHART'], 'ORWORDG_REVSTS'), d_log, d_unwrap, f_split('^', 'ien', 'name', 'parent', 'has_children')));
|
||||
|
Loading…
Reference in New Issue
Block a user