TIU-based document view

This commit is contained in:
Jiang Yio 2023-05-10 21:55:15 -04:00
parent 71db3a6186
commit 0fe07e59af
4 changed files with 211 additions and 0 deletions

View File

@ -22,6 +22,7 @@
import RoutePatientVisits from './RoutePatientVisits.vue'; import RoutePatientVisits from './RoutePatientVisits.vue';
import RoutePatientOrders from './RoutePatientOrders.vue'; import RoutePatientOrders from './RoutePatientOrders.vue';
import RoutePatientReports from './RoutePatientReports.vue'; import RoutePatientReports from './RoutePatientReports.vue';
import RoutePatientDocuments from './RoutePatientDocuments.vue';
import RoutePlanner from './RoutePlanner.vue'; import RoutePlanner from './RoutePlanner.vue';
import RouteRecall from './RouteRecall.vue'; import RouteRecall from './RouteRecall.vue';
import RouteInbox from './RouteInbox.vue'; import RouteInbox from './RouteInbox.vue';
@ -70,6 +71,8 @@
{ path: 'reports/pathology', component: RoutePatientReports, props: { report_name: 'Pathology' } }, { path: 'reports/pathology', component: RoutePatientReports, props: { report_name: 'Pathology' } },
{ path: 'reports/radiology', component: RoutePatientReports, props: { report_name: 'Radiology' } }, { path: 'reports/radiology', component: RoutePatientReports, props: { report_name: 'Radiology' } },
{ path: 'reports/notes', component: RoutePatientReports, props: { report_name: 'Notes' } }, { path: 'reports/notes', component: RoutePatientReports, props: { report_name: 'Notes' } },
{ path: 'document', component: RoutePatientDocuments },
{ path: 'document/:tiu_da', component: RoutePatientDocuments },
] }, ] },
{ path: '/planner', component: RoutePlanner }, { path: '/planner', component: RoutePlanner },
{ path: '/recall', component: RouteRecall }, { path: '/recall', component: RouteRecall },

View File

@ -53,6 +53,7 @@
{ name: 'Visits', href: '/patient/' + this.patient_dfn + '/visits' + (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' || '') }, { name: 'Reports', href: '/patient/' + this.patient_dfn + '/reports' + (this.sensitive && '?viewsensitive' || '') },
{ name: 'Documents', href: '/patient/' + this.patient_dfn + '/document' + (this.sensitive && '?viewsensitive' || '') },
] ]
} : null; } : null;
} }

View File

@ -0,0 +1,199 @@
<template>
<Subtitle value="Documents" />
<Subtitle :value="patient_info.name" />
<div class="row">
<div class="selector col-12 col-xl-4">
<div class="card mb-3 shadow">
<div class="card-header"><template v-if="resultset.length > 0">{{resultset.length}}<template v-if="has_more">+</template></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" ref="scroller">
<router-link v-for="item in resultset" :to="'/patient/' + patient_dfn + '/document/' + item.IEN + (sensitive ? '?viewsensitive' : '')" replace custom v-slot="{ navigate, href }">
<li :key="item" class="record" :class="{ 'active': selection == item.IEN }" :title="datetimestring(strptime_vista(item.time)) + '\n' + item.title + '\n' + item.location + '\n' + item.author.byline" @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">{{item.title}}</div>
<div class="cell secondary col-7 col-lg-4 col-xl-7">{{item.location}}</div>
<div class="cell secondary col-5 col-lg-8 col-xl-5">{{item.author.byline}}</div>
</div>
</li>
</router-link>
<li ref="bottom" />
</ul>
</div>
</div>
<div v-if="selection_text" class="col-12 col-xl-8">
<div class="card mb-3 shadow">
<div class="card-header">{{doctitle(selection_text) || 'Document'}}</div>
<div class="detail card-body">{{selection_text}}</div>
</div>
</div>
</div>
</template>
<style scoped>
div.selector {
position: sticky;
top: 1.15rem;
z-index: 1;
}
ul.scroller {
max-height: 25vh;
overflow-y: auto;
}
li.record {
cursor: default;
border-top: 1px solid #dee2e6;
padding: 0.25rem 0.75rem;
}
li.record:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05);
}
li.record a {
color: inherit;
}
li.record.active {
color: #fff;
background-color: #0d6efd;
}
div.cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
div.detail {
font-family: monospace;
white-space: pre-wrap;
}
@media (min-width: 1200px) {
div.selector {
position: static;
}
ul.scroller {
max-height: 75vh;
}
div.cell.secondary {
font-size: 0.8em;
}
div.detail {
max-height: 75vh;
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,
observer_bottom: null
};
},
watch: {
'$route.params.tiu_da': {
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 brk = doc.indexOf('\r\n');
if(brk >= 0) {
doc = doc.substring(0, brk);
brk = doc.indexOf(': ');
if(brk >= 0) return doc.substring(brk + 2).replace(/^\s+|\s+$/g, '');
else return doc.replace(/^\s+|\s+$/g, '');
}
}
},
async load_more() {
try {
this.is_loading = true;
if((this.client) && (this.patient_dfn)) {
if(this.dfn != this.patient_dfn) {
this.dfn = this.patient_dfn;
this.has_more = '';
this.resultset = [];
}
var res = await client.TIU_DOCUMENTS_BY_CONTEXT(3, 1, this.patient_dfn, -1, -1, 0, SZ_WINDOW, 'D', 1, 0, 1, this.has_more);
if((res) && (res.length > 0)) {
res = res.slice();
var last = res[res.length - 1];
if((last.title == 'SHOW MORE') && (!last.author) && (!last.visit)) {
this.has_more = last.IEN;
res.splice(res.length - 1, 1);
}
if(this.resultset.length > 0) Array.prototype.push.apply(this.resultset, res);
else this.resultset = res;
}
} else {
this.dfn = null;
this.has_more = '';
this.resultset = [];
}
} catch(ex) {
console.warn(ex);
} finally {
this.is_loading = false;
}
},
handle_bottom([entry]) {
if((entry.isIntersecting) && (this.has_more) && (!this.is_loading)) this.load_more();
}
},
created() {
this.$watch(
() => (this.client, this.patient_dfn, {}),
debounce(this.load_more, 500),
{ immediate: true }
);
this.$watch(
() => (this.client, this.selection, {}),
debounce(async function() {
try {
this.selection_text = (this.client) && (this.selection) ? await this.client.TIU_GET_RECORD_TEXT(this.selection) : null;
} catch(ex) {
this.selection_text = null;
console.warn(ex);
}
}, 500),
{ immediate: true }
);
},
mounted() {
this.observer_bottom = new IntersectionObserver(this.handle_bottom, { root: this.$refs.scroller, rootMargin: '25%' });
this.observer_bottom.observe(this.$refs.bottom);
},
destroyed() {
if(this.observer_bottom) this.observer_bottom.disconnect();
}
};
</script>

View File

@ -186,6 +186,12 @@ export const d_parse_multireport = data => {
return res; return res;
}; };
export const d_parse_tiudocumentlist = data => d_split(data, '^', 'IEN', 'title', 'time', 'patient', 'author', 'location', 'status', 'visit').map(row => {
row.author = row.author ? d_split1(row.author, ';', 'IEN', 'byline', 'name') : null;
row.visit = row.visit ? d_split1(row.visit, ';', 'date', 'time') : null;
return row;
});
export function memoized(fn) { export function memoized(fn) {
var cache = {}; var cache = {};
return async function(...args) { return async function(...args) {
@ -300,6 +306,8 @@ export function Client(cid, secret) {
this.TIU_TEMPLATE_DELETE = aflow((...args) => this.call({ method: 'TIU_TEMPLATE_DELETE', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap); this.TIU_TEMPLATE_DELETE = aflow((...args) => this.call({ method: 'TIU_TEMPLATE_DELETE', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap);
this.TIU_TEMPLATE_LOCK = aflow((...args) => this.call({ method: 'TIU_TEMPLATE_LOCK', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap); this.TIU_TEMPLATE_LOCK = aflow((...args) => this.call({ method: 'TIU_TEMPLATE_LOCK', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap);
this.TIU_TEMPLATE_UNLOCK = aflow((...args) => this.call({ method: 'TIU_TEMPLATE_UNLOCK', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap); this.TIU_TEMPLATE_UNLOCK = aflow((...args) => this.call({ method: 'TIU_TEMPLATE_UNLOCK', context: ['OR CPRS GUI CHART'], ttl: 0, stale: false }, ...args), d_log, d_unwrap);
this.TIU_DOCUMENTS_BY_CONTEXT = aflow((...args) => this.call({ method: 'TIU_DOCUMENTS_BY_CONTEXT', context: ['OR CPRS GUI CHART'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_array, d_parse_tiudocumentlist);
this.TIU_GET_RECORD_TEXT = aflow((...args) => this.call({ method: 'TIU_GET_RECORD_TEXT', context: ['OR CPRS GUI CHART'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_text);
this.ORWCV_VST = memoized(aflow((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWCV_VST', ...args), d_log, d_unwrap, f_split('^', 'apptinfo', 'datetime', 'location', 'status'))); this.ORWCV_VST = memoized(aflow((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWCV_VST', ...args), d_log, d_unwrap, f_split('^', 'apptinfo', 'datetime', 'location', 'status')));