Consult viewer
This commit is contained in:
		@@ -48,6 +48,7 @@
 | 
			
		||||
	import RoutePatientOrders from './RoutePatientOrders.vue';
 | 
			
		||||
	import RoutePatientReports from './RoutePatientReports.vue';
 | 
			
		||||
	import RoutePatientDocuments from './RoutePatientDocuments.vue';
 | 
			
		||||
	import RoutePatientConsults from './RoutePatientConsults.vue';
 | 
			
		||||
	import RoutePlanner from './RoutePlanner.vue';
 | 
			
		||||
	import RouteRecall from './RouteRecall.vue';
 | 
			
		||||
	import RouteInbox from './RouteInbox.vue';
 | 
			
		||||
@@ -97,6 +98,8 @@
 | 
			
		||||
							{ path: 'reports', component: RoutePatientReports },
 | 
			
		||||
							{ path: 'document', component: RoutePatientDocuments },
 | 
			
		||||
							{ path: 'document/:tiu_da', component: RoutePatientDocuments },
 | 
			
		||||
							{ path: 'consult', component: RoutePatientConsults },
 | 
			
		||||
							{ path: 'consult/:ien', component: RoutePatientConsults },
 | 
			
		||||
						] },
 | 
			
		||||
						{ path: '/planner', component: RoutePlanner },
 | 
			
		||||
						{ path: '/recall', component: RouteRecall },
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,7 @@
 | 
			
		||||
						{ name: 'Orders', href: '/patient/' + this.patient_dfn + '/orders' },
 | 
			
		||||
						{ name: 'Reports', href: '/patient/' + this.patient_dfn + '/reports' },
 | 
			
		||||
						{ name: 'Documents', href: '/patient/' + this.patient_dfn + '/document' },
 | 
			
		||||
						{ name: 'Consults', href: '/patient/' + this.patient_dfn + '/consult' },
 | 
			
		||||
					]
 | 
			
		||||
				} : null;
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										195
									
								
								htdocs/RoutePatientConsults.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								htdocs/RoutePatientConsults.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,195 @@
 | 
			
		||||
<template>
 | 
			
		||||
	<Subtitle value="Consults" />
 | 
			
		||||
	<Subtitle :value="patient_info.name" />
 | 
			
		||||
	<div class="row">
 | 
			
		||||
		<div class="selector col-12" :class="{ 'col-xl-4': selection_text }">
 | 
			
		||||
			<div class="card mb-3 shadow">
 | 
			
		||||
				<div class="card-header">{{resultset.length > 0 ? resultset.length : 'No'}} record{{resultset.length == 1 ? '' : 's'}}</div>
 | 
			
		||||
				<ul class="scroller list-group list-group-flush" :class="{ 'list-skinny': selection_text }" ref="scroller">
 | 
			
		||||
					<router-link v-for="item in resultset" :to="'/patient/' + patient_dfn + '/consult/' + item.IEN" replace custom v-slot="{ navigate, href }">
 | 
			
		||||
						<li :key="item" class="record list-group-item" :class="{ 'active': selection == item.IEN }" :title="datetimestring(strptime_vista(item.time)) + ' #' + item.IEN + '\n(' + item.status + ') ' + item.text" @click="navigate">
 | 
			
		||||
							<div class="row">
 | 
			
		||||
								<div class="cell col-4"><router-link :to="href" replace>{{datestring(strptime_vista(item.time))}}</router-link></div>
 | 
			
		||||
								<div class="cell col-8">
 | 
			
		||||
									<template v-if="item.status == 'p'">⏳</template>
 | 
			
		||||
									<template v-else-if="item.status == 'a'">👍</template>
 | 
			
		||||
									<template v-else-if="item.status == 's'">📆</template>
 | 
			
		||||
									<template v-else-if="item.status == 'c'">✔</template>
 | 
			
		||||
									<template v-else-if="item.status == 'x'">❌</template>
 | 
			
		||||
									<template v-else-if="item.status == 'dc'">🗑</template>
 | 
			
		||||
									<template v-else>({{item.status}})</template>
 | 
			
		||||
									{{item.text}}
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</li>
 | 
			
		||||
					</router-link>
 | 
			
		||||
				</ul>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="selection_text" class="col-12 col-xl-8">
 | 
			
		||||
			<div class="card mb-3 shadow">
 | 
			
		||||
				<div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
					<span>{{doctitle(selection_text) || 'Consult'}} #{{selection}}</span>
 | 
			
		||||
					<router-link class="close" :to="'/patient/' + patient_dfn + '/consult'" replace>❌</router-link>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="detail card-body" ref="detail">{{selection_text}}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
	div.selector {
 | 
			
		||||
		position: sticky;
 | 
			
		||||
		top: 1.15rem;
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
	}
 | 
			
		||||
	ul.scroller.list-skinny {
 | 
			
		||||
		max-height: 25vh;
 | 
			
		||||
		overflow-y: auto;
 | 
			
		||||
	}
 | 
			
		||||
	li.record {
 | 
			
		||||
		cursor: default;
 | 
			
		||||
		border-top: 1px solid #dee2e6;
 | 
			
		||||
		padding: 0.25rem 0.75rem;
 | 
			
		||||
		scroll-margin-top: 3.6875rem;
 | 
			
		||||
	}
 | 
			
		||||
	li.record:nth-child(even) {
 | 
			
		||||
		background-color: rgba(0, 0, 0, 0.05);
 | 
			
		||||
	}
 | 
			
		||||
	ul.scroller.list-skinny li.record {
 | 
			
		||||
		scroll-margin-top: 0;
 | 
			
		||||
	}
 | 
			
		||||
	li.record a {
 | 
			
		||||
		color: inherit;
 | 
			
		||||
	}
 | 
			
		||||
	li.record.active {
 | 
			
		||||
		color: #fff;
 | 
			
		||||
		background-color: #0d6efd;
 | 
			
		||||
	}
 | 
			
		||||
	li.bottom {
 | 
			
		||||
		overflow-anchor: none;
 | 
			
		||||
	}
 | 
			
		||||
	div.cell {
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		text-overflow: ellipsis;
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
	}
 | 
			
		||||
	a.close {
 | 
			
		||||
		cursor: default;
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
	}
 | 
			
		||||
	div.detail {
 | 
			
		||||
		scroll-margin-top: calc(3.6875rem + 2.5625rem + 25vh);
 | 
			
		||||
		font-family: monospace;
 | 
			
		||||
		white-space: pre-wrap;
 | 
			
		||||
	}
 | 
			
		||||
	@media (min-width: 1200px) {
 | 
			
		||||
		div.selector {
 | 
			
		||||
			position: static;
 | 
			
		||||
		}
 | 
			
		||||
		ul.scroller.list-skinny {
 | 
			
		||||
			max-height: 75vh;
 | 
			
		||||
		}
 | 
			
		||||
		div.detail {
 | 
			
		||||
			max-height: 75vh;
 | 
			
		||||
			scroll-margin-top: 0;
 | 
			
		||||
			overflow-y: auto;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
	import { debounce, strptime_vista } from './util.mjs';
 | 
			
		||||
 | 
			
		||||
	import Subtitle from './Subtitle.vue';
 | 
			
		||||
 | 
			
		||||
	const SZ_WINDOW = 100;
 | 
			
		||||
 | 
			
		||||
	export default {
 | 
			
		||||
		components: {
 | 
			
		||||
			Subtitle
 | 
			
		||||
		},
 | 
			
		||||
		props: {
 | 
			
		||||
			client: Object,
 | 
			
		||||
			sensitive: Boolean,
 | 
			
		||||
			patient_dfn: String,
 | 
			
		||||
			patient_info: Object
 | 
			
		||||
		},
 | 
			
		||||
		data() {
 | 
			
		||||
			return {
 | 
			
		||||
				dfn: null,
 | 
			
		||||
				has_more: '',
 | 
			
		||||
				is_loading: false,
 | 
			
		||||
				resultset: [],
 | 
			
		||||
				selection: null,
 | 
			
		||||
				selection_text: null
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
		watch: {
 | 
			
		||||
			'$route.params.ien': {
 | 
			
		||||
				async handler(value) {
 | 
			
		||||
					this.selection = value;
 | 
			
		||||
				}, immediate: true
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		methods: {
 | 
			
		||||
			strptime_vista,
 | 
			
		||||
			datestring(date) {
 | 
			
		||||
				return date.toLocaleDateString('sv-SE');
 | 
			
		||||
			},
 | 
			
		||||
			datetimestring(date) {
 | 
			
		||||
				return date.toLocaleDateString('sv-SE') + ' ' + date.toLocaleTimeString('en-GB');
 | 
			
		||||
			},
 | 
			
		||||
			doctitle(doc) {
 | 
			
		||||
				if(doc) {
 | 
			
		||||
					var m = doc.match(/^Orderable Item:\s*(.*)$/m);
 | 
			
		||||
					if(m) return m[1];
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		created() {
 | 
			
		||||
			this.$watch(
 | 
			
		||||
				() => (this.client, this.patient_dfn, {}),
 | 
			
		||||
				debounce(async () => {
 | 
			
		||||
					if((this.client) && (this.patient_dfn)) this.resultset = await this.client.ORQQCN_LIST(this.patient_dfn);
 | 
			
		||||
					else this.resultset = [];
 | 
			
		||||
				}, 500),
 | 
			
		||||
				{ immediate: true }
 | 
			
		||||
			);
 | 
			
		||||
			this.$watch(
 | 
			
		||||
				() => (this.client, this.selection, {}),
 | 
			
		||||
				async function() {
 | 
			
		||||
					try {
 | 
			
		||||
						this.selection_text = (this.client) && (this.selection) ? await this.client.ORQQCN_DETAIL(this.selection) : null;
 | 
			
		||||
					} catch(ex) {
 | 
			
		||||
						this.selection_text = null;
 | 
			
		||||
						console.warn(ex);
 | 
			
		||||
					}
 | 
			
		||||
					if(this.$refs.scroller) {
 | 
			
		||||
						if(this.selection_text) {	// scroll to selected item
 | 
			
		||||
							await this.$nextTick();
 | 
			
		||||
							var active = this.$refs.scroller.querySelectorAll(':scope > .active');
 | 
			
		||||
							if(active.length > 0) (Element.prototype.scrollIntoViewIfNeeded || Element.prototype.scrollIntoView).call(active[0]);
 | 
			
		||||
							if(this.$refs.detail) {	// scroll to top of detail panel
 | 
			
		||||
								this.$refs.detail.scrollIntoView();
 | 
			
		||||
								this.$refs.detail.scrollTop = 0;
 | 
			
		||||
							}
 | 
			
		||||
						} else { // scroll to topmost item
 | 
			
		||||
							var offset = this.$refs.scroller.getBoundingClientRect().top;
 | 
			
		||||
							for(var children = this.$refs.scroller.children, count = children.length, i = 0; i < count; ++i) if(children[i].getBoundingClientRect().top >= offset) {
 | 
			
		||||
								await this.$nextTick();
 | 
			
		||||
								var behavior = document.documentElement.style.scrollBehavior;
 | 
			
		||||
								document.documentElement.style.scrollBehavior = 'auto';	// inhibit Bootstrap smooth scrolling
 | 
			
		||||
								children[i].scrollIntoView();
 | 
			
		||||
								document.documentElement.style.scrollBehavior = behavior;
 | 
			
		||||
								break;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				{ immediate: true }
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -336,6 +336,9 @@ export function Client(cid, secret) {
 | 
			
		||||
	this.TIU_DELETE_RECORD = aflow((...args) => this.call({ method: 'TIU_DELETE_RECORD', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap);
 | 
			
		||||
	this.TIU_SIGN_RECORD = aflow((...args) => this.call({ method: 'TIU_SIGN_RECORD', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap);
 | 
			
		||||
 | 
			
		||||
	this.ORQQCN_LIST = aflow((...args) => this.call({ method: 'ORQQCN_LIST', context: ['OR CPRS GUI CHART'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_array, f_split('^', 'IEN', 'time', 'status', 'orderable', 'type', 'has_children', 'text', 'order_ifn', 'type_abbr'));
 | 
			
		||||
	this.ORQQCN_DETAIL = aflow((...args) => this.call({ method: 'ORQQCN_DETAIL', context: ['OR CPRS GUI CHART'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_text);
 | 
			
		||||
 | 
			
		||||
	this.ORWPCE_NOTEVSTR = aflow((...args) => this.call({ method: 'ORWPCE_NOTEVSTR', context: ['OR CPRS GUI CHART'], ttl: 86400, stale: true }, ...args), d_log, d_unwrap);
 | 
			
		||||
	this.ORWPCE_DELETE = aflow((...args) => this.call({ method: 'ORWPCE_DELETE', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user