Report viewer
This commit is contained in:
		@@ -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')));
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user