Report viewer
This commit is contained in:
parent
38b13f9ad6
commit
71db3a6186
@ -21,6 +21,7 @@
|
||||
import RoutePatientDetail from './RoutePatientDetail.vue';
|
||||
import RoutePatientVisits from './RoutePatientVisits.vue';
|
||||
import RoutePatientOrders from './RoutePatientOrders.vue';
|
||||
import RoutePatientReports from './RoutePatientReports.vue';
|
||||
import RoutePlanner from './RoutePlanner.vue';
|
||||
import RouteRecall from './RouteRecall.vue';
|
||||
import RouteInbox from './RouteInbox.vue';
|
||||
@ -63,6 +64,12 @@
|
||||
{ path: '', component: RoutePatientDetail },
|
||||
{ path: 'visits', component: RoutePatientVisits },
|
||||
{ path: 'orders', component: RoutePatientOrders },
|
||||
{ path: 'reports', component: RoutePatientReports },
|
||||
{ path: 'reports/bloodbank', component: RoutePatientReports, props: { report_name: 'Blood Bank' } },
|
||||
{ path: 'reports/microbiology', component: RoutePatientReports, props: { report_name: 'Microbiology' } },
|
||||
{ path: 'reports/pathology', component: RoutePatientReports, props: { report_name: 'Pathology' } },
|
||||
{ path: 'reports/radiology', component: RoutePatientReports, props: { report_name: 'Radiology' } },
|
||||
{ path: 'reports/notes', component: RoutePatientReports, props: { report_name: 'Notes' } },
|
||||
] },
|
||||
{ path: '/planner', component: RoutePlanner },
|
||||
{ path: '/recall', component: RouteRecall },
|
||||
|
@ -51,7 +51,8 @@
|
||||
items: [
|
||||
{ name: 'Patient', href: '/patient/' + this.patient_dfn + (this.sensitive && '?viewsensitive' || '') },
|
||||
{ name: 'Visits', href: '/patient/' + this.patient_dfn + '/visits' + (this.sensitive && '?viewsensitive' || '') },
|
||||
{ name: 'Orders', href: '/patient/' + this.patient_dfn + '/orders' + (this.sensitive && '?viewsensitive' || '') }
|
||||
{ name: 'Orders', href: '/patient/' + this.patient_dfn + '/orders' + (this.sensitive && '?viewsensitive' || '') },
|
||||
{ name: 'Reports', href: '/patient/' + this.patient_dfn + '/reports' + (this.sensitive && '?viewsensitive' || '') },
|
||||
]
|
||||
} : null;
|
||||
}
|
||||
|
147
htdocs/RoutePatientReports.vue
Normal file
147
htdocs/RoutePatientReports.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<Subtitle :value="selection ? selection.name : 'Reports'" />
|
||||
<Subtitle :value="patient_info.name" />
|
||||
<div class="card mb-3 shadow">
|
||||
<ul class="card-header nav nav-pills nav-fill">
|
||||
<li v-for="report in reports" class="nav-item" @click="selection = report">
|
||||
<router-link class="nav-link" :to="'/patient/' + patient_dfn + '/reports/' + report.name.toLowerCase().replace(/\s+/g, '') + (sensitive ? '?viewsensitive' : '')">{{report.name}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-around align-items-center">
|
||||
<DateRangePicker range="2Y" direction="-1" v-model:date="date_end" v-model:date_end="date_begin" />
|
||||
<div class="limit input-group">
|
||||
<span class="input-group-text">Limit</span>
|
||||
<input type="number" step="1" class="form-control" v-model="limit" />
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="(selection) && (resultset) && (resultset.length)" class="list-group-item"><ViewReport :resultset="resultset" :table="selection.table" :detail="selection.detail" /></li>
|
||||
</ul>
|
||||
<div v-if="(selection) && (resultset) && (resultset.length)" class="card-footer">{{resultset.length}} record{{resultset.length == 1 ? '' : 's'}} loaded<template v-if="resultset.length == limit"> (may be truncated)</template></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.router-link-exact-active {
|
||||
color: #fff;
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
div.limit {
|
||||
width: 20rem;
|
||||
}
|
||||
div.card-footer {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { debounce, strftime_vista } from './util.mjs';
|
||||
|
||||
import Subtitle from './Subtitle.vue';
|
||||
import DateRangePicker from './DateRangePicker.vue';
|
||||
import ViewReport from './ViewReport.vue';
|
||||
|
||||
const reports = [
|
||||
{
|
||||
name: 'Blood Bank',
|
||||
rpt_id: '2:BLOOD BANK REPORT~;;0',
|
||||
detail: null,
|
||||
table: []
|
||||
},
|
||||
{
|
||||
name: 'Microbiology',
|
||||
rpt_id: 'OR_MIC:MICROBIOLOGY~MI;ORDV05;38;',
|
||||
detail: 7,
|
||||
table: [
|
||||
{ subscript: 2, title: 'Collection Date/Time' },
|
||||
{ subscript: 3, title: 'Test Name' },
|
||||
{ subscript: 4, title: 'Sample' },
|
||||
{ subscript: 5, title: 'Specimen' },
|
||||
{ subscript: 6, title: 'Accession #' },
|
||||
{ subscript: 8, title: '[+]' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Pathology',
|
||||
rpt_id: 'OR_APR:ANATOMIC PATHOLOGY~SP;ORDV02A;0;',
|
||||
detail: 5,
|
||||
table: [
|
||||
{ subscript: 2, title: 'Collection Date/Time' },
|
||||
{ subscript: 3, title: 'Specimen' },
|
||||
{ subscript: 4, title: 'Accession #' },
|
||||
{ subscript: 6, title: '[+]' },
|
||||
//{ subscript: 7, title: '#7' },
|
||||
//{ subscript: 8, title: '#8' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Radiology',
|
||||
rpt_id: 'OR_R18:IMAGING~RIM;ORDV08;0;',
|
||||
detail: 6,
|
||||
table: [
|
||||
{ subscript: 2, title: 'Procedure Date/Time' },
|
||||
{ subscript: 3, title: 'Procedure Name' },
|
||||
{ subscript: 4, title: 'Report Status' },
|
||||
{ subscript: 5, title: 'Case #' },
|
||||
{ subscript: 7, title: '[+]' },
|
||||
//{ subscript: 8, title: '#8' },
|
||||
//{ subscript: 9, title: '#9' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Notes',
|
||||
rpt_id: 'OR_PN:PROGRESS NOTES~TIUPRG;ORDV04;15;',
|
||||
detail: 6,
|
||||
table: [
|
||||
{ subscript: 3, title: 'Record Date/Time' },
|
||||
{ subscript: 4, title: 'Type' },
|
||||
{ subscript: 5, title: 'Author' },
|
||||
{ subscript: 7, title: '[+]' },
|
||||
//{ subscript: 8, title: '#8' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Subtitle, DateRangePicker, ViewReport
|
||||
},
|
||||
props: {
|
||||
client: Object,
|
||||
sensitive: Boolean,
|
||||
patient_dfn: String,
|
||||
patient_info: Object,
|
||||
report_name: String
|
||||
},
|
||||
data() {
|
||||
var now = new Date();
|
||||
return {
|
||||
date_begin: now,
|
||||
date_end: now,
|
||||
limit: 100,
|
||||
reports,
|
||||
selection: null,
|
||||
resultset: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
async handler(value) {
|
||||
await this.$nextTick();
|
||||
this.selection = this.report_name ? reports.find(x => x.name == this.report_name) : null;
|
||||
}, immediate: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$watch(
|
||||
() => (this.client, this.patient_dfn, this.selection, this.date_begin, this.date_end, this.limit, {}),
|
||||
debounce(async () => {
|
||||
var limit = Math.floor(Math.abs(this.limit));
|
||||
this.resultset = [];
|
||||
if((this.client) && (this.patient_dfn) && (this.selection)) this.resultset = await this.client.ORWRP_REPORT_TEXT(this.patient_dfn, this.selection.rpt_id + (this.selection.rpt_id.endsWith(';') ? Math.round(this.limit) : ''), '', Math.round(this.limit), '', strftime_vista(this.date_begin), strftime_vista(this.date_end));
|
||||
}, 500),
|
||||
{ immediate: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
65
htdocs/ViewReport.vue
Normal file
65
htdocs/ViewReport.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div v-if="(resultset) && (resultset.length == 1) && (!resultset[0][0].startsWith('1^'))" class="detail">{{resultset[0].join('\r\n')}}</div>
|
||||
<div v-else-if="(resultset_calculated) && (resultset_calculated.length)" class="accordion">
|
||||
<div v-for="item in resultset_calculated" class="accordion-item" :key="item">
|
||||
<h2 class="accordion-header">
|
||||
<button type="button" class="accordion-button report-row" :class="{ collapsed: !show[item.hash] }" @click="show[item.hash] = !show[item.hash]">
|
||||
<span v-for="entry in table" class="report-col">{{item[entry.subscript]}}</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse" :class="{ show: show[item.hash] }">
|
||||
<div class="detail accordion-body">{{item[detail]}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.report-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.report-col {
|
||||
flex: 1;
|
||||
}
|
||||
.detail {
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { strHashJenkins } from './util.mjs';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
client: Object,
|
||||
resultset: Array,
|
||||
table: Array,
|
||||
detail: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
resultset_calculated() {
|
||||
return this.resultset ? this.resultset.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('\r\n');
|
||||
res.hash = strHashJenkins(item.join(''));
|
||||
return res;
|
||||
}) : [];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -170,6 +170,22 @@ export const d_parse_notifications_fastuser = data => d_split(data, '^', 'info',
|
||||
return row;
|
||||
});
|
||||
|
||||
export const d_parse_multireport = data => {
|
||||
if(data.length < 1) return [];
|
||||
var brk, max = 0, grp;
|
||||
for(var i = 0; i < data.length; ++i) {
|
||||
brk = (grp = data[i]).indexOf('^');
|
||||
if(brk >= 0) {
|
||||
grp = +grp.substring(0, brk);
|
||||
if(grp >= max) max = grp;
|
||||
else break;
|
||||
} else return [data];
|
||||
}
|
||||
var res = [], data = data.slice(), max = max + '^', grp = x => x.startsWith(max);
|
||||
while(((brk = data.findIndex(grp)) >= 0) || (brk = data.length)) res.push(data.splice(0, brk + 1));
|
||||
return res;
|
||||
};
|
||||
|
||||
export function memoized(fn) {
|
||||
var cache = {};
|
||||
return async function(...args) {
|
||||
@ -268,6 +284,8 @@ export function Client(cid, secret) {
|
||||
this.ORWLRR_INTERIM = aflow((...args) => this.call({ method: 'ORWLRR_INTERIM', context: ['OR CPRS GUI CHART'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, lab_parse);
|
||||
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.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')));
|
||||
this.ORWORR_AGET = memoized(aflow((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWORR_AGET', ...args), d_log, d_unwrap, f_slice(1), f_split('^', 'ifn', 'dgrp', 'time')));
|
||||
|
Loading…
Reference in New Issue
Block a user