Access to VistA Imaging System
This commit is contained in:
parent
83c9f11c73
commit
3aa6a64f36
@ -49,6 +49,7 @@
|
||||
import RoutePatientReports from './RoutePatientReports.vue';
|
||||
import RoutePatientDocuments from './RoutePatientDocuments.vue';
|
||||
import RoutePatientConsults from './RoutePatientConsults.vue';
|
||||
import RoutePatientImaging from './RoutePatientImaging.vue';
|
||||
import RoutePlanner from './RoutePlanner.vue';
|
||||
import RouteRecall from './RouteRecall.vue';
|
||||
import RouteInbox from './RouteInbox.vue';
|
||||
@ -100,6 +101,8 @@
|
||||
{ path: 'document/:tiu_da', component: RoutePatientDocuments },
|
||||
{ path: 'consult', component: RoutePatientConsults },
|
||||
{ path: 'consult/:ien', component: RoutePatientConsults },
|
||||
{ path: 'imaging', component: RoutePatientImaging },
|
||||
{ path: 'imaging/:ien', component: RoutePatientImaging },
|
||||
] },
|
||||
{ path: '/planner', component: RoutePlanner },
|
||||
{ path: '/recall', component: RouteRecall },
|
||||
|
@ -61,6 +61,7 @@
|
||||
{ name: 'Reports', href: '/patient/' + this.patient_dfn + '/reports' },
|
||||
{ name: 'Documents', href: '/patient/' + this.patient_dfn + '/document' },
|
||||
{ name: 'Consults', href: '/patient/' + this.patient_dfn + '/consult' },
|
||||
{ name: 'Imaging', href: '/patient/' + this.patient_dfn + '/imaging' },
|
||||
]
|
||||
} : null;
|
||||
}
|
||||
|
228
htdocs/RoutePatientImaging.vue
Normal file
228
htdocs/RoutePatientImaging.vue
Normal file
@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<Subtitle value="Imaging" />
|
||||
<Subtitle :value="patient_info.name" />
|
||||
<div class="row">
|
||||
<div class="selector col-12" :class="{ 'col-xl-4': selection_data }">
|
||||
<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_data }" ref="scroller">
|
||||
<router-link v-for="item in resultset" :to="'/patient/' + patient_dfn + '/imaging/' + item.Info.IEN" replace custom v-slot="{ navigate, href }">
|
||||
<li :key="item" class="record list-group-item" :class="{ 'active': selection == item.Info.IEN }" :title="'Site: ' + item['Site'] + '\nNote Title: ' + item['Note Title~~W0'] + '\nProc DT: ' + item['Proc DT~S1'] + '\nProcedure: ' + item['Procedure'] + '\n# Img: ' + item['# Img~S2'] + '\nShort Desc: ' + item['Short Desc'] + '\nPkg: ' + item['Pkg'] + '\nClass: ' + item['Class'] + '\nType: ' + item['Type'] + '\nSpecialty: ' + item['Specialty'] + '\nOrigin: ' + item['Origin'] + '\nCap Dt: ' + item['Cap Dt~S1~W0'] + '\nCap by: ' + item['Cap by~~W0'] + '\nImage ID: ' + item['Image ID~S2~W0'] + '\nCreation Date: ' + item.Info['Document Date']" @click="navigate">
|
||||
<div class="row">
|
||||
<div class="cell col-4"><router-link :to="href" replace>{{item['Proc DT~S1']}}</router-link></div>
|
||||
<div class="cell col-7">{{doctitledesc(item)}}</div>
|
||||
<div class="cell col-1">#{{item['# Img~S2']}}</div>
|
||||
<div class="cell col-3">{{doctypespec(item)}}</div>
|
||||
<div class="cell col-3">{{docclspkgproc(item)}}</div>
|
||||
<div class="cell col-3">{{item['Site']}} {{item['Origin']}}</div>
|
||||
</div>
|
||||
</li>
|
||||
</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selection_data" 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>{{doctitledesc(selection_data) || 'Image'}} #{{selection}}</span>
|
||||
<router-link class="close" :to="'/patient/' + patient_dfn + '/imaging'" replace>❌</router-link>
|
||||
</div>
|
||||
<div class="detail card-body" ref="detail">
|
||||
<p v-if="selection_info">{{selection_info}}</p>
|
||||
<ul v-if="(selection_images) && (selection_images.length > 0)" class="list-group list-group-flush">
|
||||
<li v-for="info in selection_images" class="list-group-item"><router-link :to="'/v1/vista/' + client.cid + '/imaging/' + info['Image Path'].replace(/\\/g, '/').replace(/^\/+/, '') + '?view'" target="_blank">{{info['Short Desc']}}</router-link></li>
|
||||
</ul>
|
||||
</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_info: null,
|
||||
selection_images: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selection_data() {
|
||||
var selection = this.selection, resultset = this.resultset;
|
||||
if((selection) && (resultset) && (resultset.length > 0)) return resultset.find(item => item['Image ID~S2~W0'] == selection);
|
||||
}
|
||||
},
|
||||
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');
|
||||
},
|
||||
docclspkgproc(item) {
|
||||
var res = [], cls = item['Class'], pkg = item['Pkg'], proc = item['Procedure'];
|
||||
if(cls) res.push(cls);
|
||||
if((pkg) && (pkg != 'NONE') && (pkg != cls)) res.push(pkg);
|
||||
if((proc) && (proc != cls) && (proc != pkg)) res.push(proc);
|
||||
return res.join(' • ');
|
||||
},
|
||||
doctypespec(item) {
|
||||
var res = [], type = item['Type'], spec = item['Specialty'];
|
||||
if(type) res.push(type);
|
||||
if(spec) res.push(spec);
|
||||
return res.join(' • ');
|
||||
},
|
||||
doctitledesc(item) {
|
||||
var title = item['Note Title~~W0'].replace(/^\s+|\s+$/g, ''), desc = item['Short Desc'].replace(/^\s+|\s+$/g, '');
|
||||
if(title) {
|
||||
if((desc == title) || (desc == '+' + title)) return title;
|
||||
else return title + ' • ' + desc;
|
||||
} else return desc;
|
||||
},
|
||||
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.MAG4_IMAGE_LIST('E', '', '', '', ['IXCLASS^^CLIN^CLIN/ADMIN^ADMIN/CLIN', 'IDFN^^' + this.patient_dfn]);
|
||||
else this.resultset = [];
|
||||
}, 500),
|
||||
{ immediate: true }
|
||||
);
|
||||
this.$watch(
|
||||
() => (this.client, this.selection, {}),
|
||||
async function() {
|
||||
try {
|
||||
this.selection_info = (this.client) && (this.selection) ? await this.client.MAG4_GET_IMAGE_INFO(this.selection) : null;
|
||||
} catch(ex) {
|
||||
this.selection_info = null;
|
||||
console.warn(ex);
|
||||
}
|
||||
try {
|
||||
this.selection_images = (this.client) && (this.selection) && (this.selection_data) ? (this.selection_data['# Img~S2'] == '1' ? [await this.client.MAGG_IMAGE_INFO(this.selection, '1')] : await this.client.MAGG_GROUP_IMAGES(this.selection, '1')) : null;
|
||||
} catch(ex) {
|
||||
this.selection_images = null;
|
||||
console.warn(ex);
|
||||
}
|
||||
if(this.$refs.scroller) {
|
||||
if(this.selection_data) { // 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>
|
@ -50,6 +50,37 @@ export const d_parse_array = data => data !== '' ? data : [];
|
||||
|
||||
export const d_parse_authinfo = data => data ? { duz: data[0] != '0' ? data[0] : null, device_lock: data[1] != '0', change_verify: data[2] != '0', message: data[3], reserved: data[4], greeting_lines: data[5], greeting: data.slice(6), success: (data[0] != '0') && (data[2] == '0') } : { success: false }
|
||||
|
||||
export const d_parse_imagelist = data => {
|
||||
var descriptor = d_split1(data[0], '^', 'success', 'filter', 'more'), res;
|
||||
if(descriptor.success == '1') {
|
||||
var headers = d_split1(data[1], '^');
|
||||
res = data.slice(2).map(function(row) {
|
||||
row = row.split('|');
|
||||
var values = headers.reduce((acc, val, idx) => (acc[val] = acc[idx], acc), row[0].split('^'))
|
||||
values.Info = d_split1(row[1], '^', 'IEN', 'Image Path', 'Abstract Path', 'Short Desc', 'Procedure Time', 'Object Type', 'Procedure', 'Display Date', 'Pointer', 'Abs Type', 'Availability', 'DICOM Series', 'DICOM Image', 'Count', 'Site IEN', 'Site', 'Error', 'BIGPath', 'Patient DFN', 'Patient Name', 'Image Class', 'Cap Dt', 'Document Date', 'Group IEN', 'Group Ch1', 'RPC Server', 'RPC Port', 'Controlled Image', 'Viewable Status', 'Status', 'Image Annotated', 'Image TIU Note Completed', 'Annotation Operation Status', 'Annotation Operation Status Description', 'Package');
|
||||
values.Info['Group Ch1'] = values.Info['Group Ch1'] ? d_split1(':', 'IEN', 'Type') : null
|
||||
return values;
|
||||
});
|
||||
res.descriptor = descriptor;
|
||||
} else {
|
||||
res = data.slice(1);
|
||||
res.descriptor = descriptor;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export const d_parse_imageinfo = data => {
|
||||
var res = d_split1(data[0], '^', 'Code', 'IEN', 'Image Path', 'Abstract Path', 'Short Desc', 'Procedure Time', 'Object Type', 'Procedure', 'Display Date', 'Pointer', 'Abs Type', 'Availability', 'DICOM Series', 'DICOM Image', 'Count', 'Site IEN', 'Site', 'Error', 'BIGPath', 'Patient DFN', 'Patient Name', 'Image Class', 'Cap Dt', 'Document Date', 'Group IEN', 'Group Ch1', 'RPC Server', 'RPC Port', 'Controlled Image', 'Viewable Status', 'Status', 'Image Annotated', 'Image TIU Note Completed', 'Annotation Operation Status', 'Annotation Operation Status Description', 'Package');
|
||||
res.Patient = d_split1(data[1], '^', 'IEN', 'name');
|
||||
return res;
|
||||
};
|
||||
|
||||
export const d_parse_imagegroup = data => {
|
||||
var res = d_split(data.slice(1), '^', 'B2', 'IEN', 'Image Path', 'Abstract Path', 'Short Desc', 'Procedure Time', 'Object Type', 'Procedure', 'Display Date', 'Pointer', 'Abs Type', 'Availability', 'DICOM Series', 'DICOM Image', 'Count', 'Site IEN', 'Site', 'Error', 'BIGPath', 'Patient DFN', 'Patient Name', 'Image Class', 'Cap Dt', 'Document Date', 'Group IEN', 'Group Ch1', 'RPC Server', 'RPC Port', 'Controlled Image', 'Viewable Status', 'Status', 'Image Annotated', 'Image TIU Note Completed', 'Annotation Operation Status', 'Annotation Operation Status Description', 'Package');
|
||||
res.Result = d_split1(data[0], '^', 'code', 'count');
|
||||
return res;
|
||||
};
|
||||
|
||||
export const d_parse_orderdialogs = (data, columns=['IEN', 'windowFormId', 'displayGroupId', 'type', 'displayText']) => data.map(function(row) {
|
||||
row = row.split('^');
|
||||
row = [...row[0].split(';'), row[1]];
|
||||
@ -344,6 +375,11 @@ export function Client(cid, secret) {
|
||||
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);
|
||||
|
||||
this.MAG4_IMAGE_LIST = memoized(aflow((...args) => this.call({ method: 'MAG4_IMAGE_LIST', context: ['MAG WINDOWS'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_array, d_parse_imagelist));
|
||||
this.MAG4_GET_IMAGE_INFO = memoized(aflow((...args) => this.call({ method: 'MAG4_GET_IMAGE_INFO', context: ['MAG WINDOWS'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_text));
|
||||
this.MAGG_IMAGE_INFO = memoized(aflow((...args) => this.call({ method: 'MAGG_IMAGE_INFO', context: ['MAG WINDOWS'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_array, d_parse_imageinfo));
|
||||
this.MAGG_GROUP_IMAGES = memoized(aflow((...args) => this.call({ method: 'MAGG_GROUP_IMAGES', context: ['MAG WINDOWS'], ttl: 60, stale: false }, ...args), d_log, d_unwrap, d_parse_array, d_parse_imagegroup));
|
||||
|
||||
this.ORWCV_VST = memoized(aflow((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWCV_VST', ...args), d_log, d_unwrap, d_parse_array, f_split('^', 'apptinfo', 'datetime', 'location', 'status')));
|
||||
|
||||
this.ORWU_NEWPERS = memoized(aflow((...args) => this.call({ method: 'ORWU_NEWPERS', context: ['OR CPRS GUI CHART'], ttl: 86400, stale: true }, ...args), d_log, d_unwrap, f_split('^', 'DUZ', 'name', 'description')));
|
||||
|
24
main.py
24
main.py
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import json
|
||||
import secrets
|
||||
import string
|
||||
@ -160,6 +161,29 @@ def application():
|
||||
logger.exception(request.url)
|
||||
return jsonify_error(ex, id=request.json.get('id'))
|
||||
|
||||
@app.get('/v1/vista/<cid>/imaging/<path:path>')
|
||||
def cb_imaging(cid, path):
|
||||
if 'view' in request.args:
|
||||
adapter = 'view.' + path.rsplit('.', 1)[1].lower() + '.html'
|
||||
if os.path.isfile('./htdocs/adapter/' + adapter):
|
||||
return send_from_directory('./htdocs/adapter', adapter)
|
||||
client = clients[cid]
|
||||
frag = path.replace('\\', '/').strip('/').split('/')
|
||||
winshare = '\\\\' + '\\'.join(frag[:2]).upper()
|
||||
for item in client.MAG_GET_NETLOC('ALL', context=['MAG WINDOWS']):
|
||||
if item.split('^')[1] == winshare:
|
||||
break
|
||||
else:
|
||||
raise PermissionError(path)
|
||||
try:
|
||||
open('//' + '/'.join(frag)).close()
|
||||
except PermissionError as ex:
|
||||
credentials = client.MAGGUSER2(context=['MAG WINDOWS'])[2].split('^')
|
||||
import subprocess, XWBHash
|
||||
subprocess.run(['net', 'use', winshare, '/d'])
|
||||
subprocess.run(['net', 'use', winshare, '/user:' + credentials[0], XWBHash.decrypt(credentials[1])])
|
||||
return send_from_directory('//' + '/'.join(frag[:2]), '/'.join(frag[2:]), as_attachment=('dl' in request.args))
|
||||
|
||||
@app.get('/<path:path>')
|
||||
def cb_static(path):
|
||||
return send_from_directory('./htdocs', path if '.' in path.rsplit('/', 1)[-1] else 'index.html')
|
||||
|
Loading…
Reference in New Issue
Block a user