Compare commits

...

8 Commits

30 changed files with 2472 additions and 157 deletions

View File

@ -14,6 +14,9 @@
import RouteSchedule from './RouteSchedule.vue'; import RouteSchedule from './RouteSchedule.vue';
import RoutePatientLookup from './RoutePatientLookup.vue'; import RoutePatientLookup from './RoutePatientLookup.vue';
import RoutePatientDetail from './RoutePatientDetail.vue'; import RoutePatientDetail from './RoutePatientDetail.vue';
import RoutePatientVisits from './RoutePatientVisits.vue';
import RoutePatientOrders from './RoutePatientOrders.vue';
import RouteScheduleOverview from './RouteScheduleOverview.vue';
import RouteRecall from './RouteRecall.vue'; import RouteRecall from './RouteRecall.vue';
export default { export default {
@ -41,6 +44,9 @@
{ path: '/', component: RouteSchedule }, { path: '/', component: RouteSchedule },
{ path: '/patient', component: RoutePatientLookup }, { path: '/patient', component: RoutePatientLookup },
{ path: '/patient/:id', component: RoutePatientDetail }, { path: '/patient/:id', component: RoutePatientDetail },
{ path: '/patient/:id/visits', component: RoutePatientVisits },
{ path: '/patient/:id/orders', component: RoutePatientOrders },
{ path: '/overview', component: RouteScheduleOverview },
{ path: '/recall', component: RouteRecall }, { path: '/recall', component: RouteRecall },
].forEach(route => this.$root.$router.addRoute(route)); ].forEach(route => this.$root.$router.addRoute(route));
await this.$root.$router.replace(this.$route); await this.$root.$router.replace(this.$route);

View File

@ -1,44 +1,36 @@
<template> <template>
<div class="autocomplete"> <div class="dropdown" :class="{ 'form-floating': label }">
<input type="text" @input="option_open" v-model="xvalue" @keydown.down="option_down" @keydown.up="option_up" @keydown.enter="option_enter" /> <input type="text" class="form-control" placeholder=" " @focus="option_open" @input="option_filter" v-model="x_modelValue" @keydown.down="option_down" @keydown.up="option_up" @keydown.enter="option_enter" />
<ul id="autocomplete-results" v-show="open" class="autocomplete-results"> <ul class="dropdown-menu shadow" :class="{ show: (open) && (results.length > 0) }">
<li class="loading" v-if="!items">Loading results...</li> <li class="loading" v-if="!items">Loading results...</li>
<li v-else v-for="(result, i) in results" :key="i" @click="option_click(result)" class="autocomplete-result" :class="{ 'is-active': i === index }">{{ result }}</li> <li v-else v-for="(result, i) in results" :key="i" @click="option_click(result)" class="dropdown-item" :class="{ 'is-active': i === index }">{{ result }}</li>
</ul> </ul>
<label v-if="label">{{label}}</label>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.autocomplete { .dropdown-menu {
position: relative; width: 100%;
} max-height: 10rem;
.autocomplete-results {
padding: 0;
margin: 0;
border: 1px solid #eeeeee;
height: 120px;
overflow: auto; overflow: auto;
} }
.autocomplete-result { .dropdown-item {
list-style: none; cursor: default;
text-align: left;
padding: 4px 2px;
cursor: pointer;
} }
.autocomplete-result.is-active, .dropdown-item.is-active,
.autocomplete-result:hover { .dropdown-item:hover {
background-color: #4AAE9B; background-color: var(--bs-primary);
color: white; color: var(--bs-body-bg);
} }
</style> </style>
<script> <script>
export default { export default {
props: { props: {
value: { modelValue: {
type: String, type: String,
default: '' default: ''
}, },
@ -46,26 +38,30 @@
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
} },
label: String
}, },
emits: [
'update:modelValue'
],
data() { data() {
return { return {
xvalue: '', x_modelValue: this.modelValue,
results: [], results: [],
open: false, open: false,
index: -1, index: -1,
}; };
}, },
watch: { watch: {
value(val) { modelValue(val) {
this.xvalue = val; this.x_modelValue = val;
}, },
xvalue(val) { x_modelValue(val) {
this.$emit('update:value', val); this.$emit('update:modelValue', val);
} }
}, },
mounted() { mounted() {
this.xvalue = this.value; this.x_modelValue = this.modelValue;
document.addEventListener('click', this.option_close) document.addEventListener('click', this.option_close)
}, },
destroyed() { destroyed() {
@ -74,7 +70,16 @@
methods: { methods: {
option_open() { option_open() {
if(this.items) { if(this.items) {
this.results = this.items.filter((item) => item.toLowerCase().indexOf(this.xvalue.toLowerCase()) > -1); this.results = this.items;
this.open = true;
}
},
option_filter() {
if(this.items) {
if(this.x_modelValue) {
var selection = this.x_modelValue.toLowerCase();
this.results = this.items.filter((item) => item.toLowerCase().indexOf(selection) >= 0);
} else this.results = this.items;
this.open = true; this.open = true;
} }
}, },
@ -85,12 +90,12 @@
if(this.index > 0) this.index--; if(this.index > 0) this.index--;
}, },
option_enter() { option_enter() {
this.xvalue = this.results[this.index]; this.x_modelValue = this.results[this.index];
this.open = false; this.open = false;
this.index = -1; this.index = -1;
}, },
option_click(result) { option_click(result) {
this.xvalue = result; this.x_modelValue = result;
this.open = false; this.open = false;
}, },
option_close(evt) { option_close(evt) {

84
htdocs/DateTimePicker.vue Normal file
View File

@ -0,0 +1,84 @@
<template>
<div v-if="structured" class="input-group">
<input type="date" class="form-control" placeholder="Date" v-model="datestr" />
<input v-if="!dateonly" type="time" class="form-control" placeholder="Time" v-model="timestr" />
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" @click="structured = false"></button>
</div>
</div>
<div v-else class="input-group">
<input type="text" class="form-control" placeholder="Date & Time" v-model="x_modelValue" />
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" @click="structured = true;">📅</button>
</div>
</div>
</template>
<script>
import { validtime, strptime } from './fmdatetime.mjs';
export default {
props: {
dateonly: Boolean,
modelValue: String
},
emits: [
'update:modelValue'
],
data() {
return {
structured: this.modelValue ? /^(\d{4})-(\d{1,2})-(\d{1,2})(?: (\d{1,2})\:(\d{1,2})(?:\:(\d{1,2}))?)?$/i.test(this.modelValue) : true,
x_modelValue: this.modelValue
};
},
computed: {
datetime: {
get() {
var m = /^(\d{4})-(\d{1,2})-(\d{1,2})(?: (\d{1,2})\:(\d{1,2})(?:\:(\d{1,2}))?)?$/i.exec(this.x_modelValue);
return m ? new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6]) : null;
},
set(value) {
this.x_modelValue = ('0000' + value.getFullYear()).slice(-4) + '-' + ('00' + (value.getMonth() + 1)).slice(-2) + '-' + ('00' + value.getDate()).slice(-2) + ' ' + ('00' + value.getHours()).slice(-2) + ':' + ('00' + value.getMinutes()).slice(-2) + ':' + ('00' + value.getSeconds()).slice(-2);
}
},
datestr: {
get() {
var res;
return this.datetime ? (res = new Date(this.datetime), res.setHours(0, 0, 0, 0), res.toLocaleDateString('sv-SE')) : null;
},
set(value) {
var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if(m) {
var res = this.datetime ? new Date(this.datetime) : new Date();
res.setFullYear(m[1], m[2] - 1, m[3]);
this.datetime = res;
}
}
},
timestr: {
get() {
var res;
return this.datetime ? (res = new Date(this.datetime), res.setFullYear(0, 0, 0), res.toLocaleTimeString('en-GB')) : null;
},
set(value) {
var m = /^(\d{2})\:(\d{2})(?:\:(\d{2}))?$/.exec(value);
if(m) {
var res = this.datetime ? new Date(this.datetime) : new Date();
res.setHours(m[1], m[2], m[3] || 0);
this.datetime = res;
}
}
}
},
watch: {
modelValue(value) { this.x_modelValue = value; },
x_modelValue(value) { if(validtime(value)) this.$emit('update:modelValue', value); },
structured(value) {
if(value) this.datetime = strptime(this.x_modelValue);
}
},
methods: {
strptime
}
};
</script>

View File

@ -133,7 +133,10 @@
console.log('Backend secret', this.secret); console.log('Backend secret', this.secret);
console.log(this.banner); console.log(this.banner);
var stop = watchEffect(() => { if(!this.x_client.connected.value) { stop(); this.x_client = this.x_server = this.x_user = null; this.fail = true; } }); var stop = watchEffect(() => { if(!this.x_client.connected.value) { stop(); this.x_client = this.x_server = this.x_user = null; this.fail = true; } });
} else this.fail = true; } else {
this.fail = true;
this.host = undefined;
}
}, },
async login(evt) { async login(evt) {
if(this.x_client) { if(this.x_client) {

View File

@ -13,6 +13,17 @@
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" to="/patient">Patient</router-link> <router-link class="nav-link" to="/patient">Patient</router-link>
</li> </li>
<template v-if="($route.matched.length > 0) && ($route.matched[0].path == '/patient/:id')">
<li class="nav-item">
<router-link class="nav-link" :to="'/patient/' + $route.params.id + '/visits'">Visits</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="'/patient/' + $route.params.id + '/orders'">Orders</router-link>
</li>
</template>
<li class="nav-item">
<router-link class="nav-link" to="/overview">Overview</router-link>
</li>
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" to="/recall">Recall</router-link> <router-link class="nav-link" to="/recall">Recall</router-link>
</li> </li>

View File

@ -0,0 +1,83 @@
<template>
<div v-if="(sensitive) && (!info)" class="alert alert-danger text-center mb-3 shadow" role="alert">
<h1>Warning: Restricted Record</h1>
<p>This record is protected by the Privacy Act of 1974 and the Health Insurance Portability and Accountability Act of 1996. If you elect to proceed, you will be required to prove you have a need to know. Accessing this patient is tracked, and your station Security Officer will contact you for your justification.</p>
<router-link class="btn btn-danger" :to="'/patient/' + dfn + '/orders?viewsensitive'">Proceed</router-link>
</div>
<div v-if="info">
<div class="card mb-3 shadow">
<div class="card-header">{{info.name}} <span :title="info.pid">{{info.pid.slice(-4)}}</span> #{{dfn}}</div>
<div class="card-body row" style="font-family: monospace;">
<div class="col" v-if="info.dob"><strong>DOB:</strong> {{strptime_vista(info.dob).toLocaleDateString('sv-SE')}}</div>
<div class="col" v-if="info.age"><strong>Age:</strong> {{info.age}}</div>
<div class="col" v-if="info.sex"><strong>Sex:</strong> {{info.sex}}</div>
<div class="col" v-if="info.sc_percentage"><strong>SC%:</strong> {{info.sc_percentage}}</div>
<div class="col" v-if="info.type"><strong>Type:</strong> {{info.type}}</div>
<div class="col" v-if="info.ward"><strong>Ward:</strong> {{info.ward}}</div>
<div class="col" v-if="info.room_bed"><strong>Room/bed:</strong> {{info.room_bed}}</div>
</div>
</div>
<div class="card mb-3 shadow">
<div class="card-header">Orders</div>
<div class="card-body"><ViewOrderMenu :client="client" :dfn="dfn" /></div>
</div>
<div class="card mb-3 shadow">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Orders</span>
<OrderFilterPicker :client="client" v-model="orders_filter" />
<DateRangePicker range="6M" direction="-1" v-model:date="orders_date" v-model:date_end="orders_date_begin" />
</div>
<div class="card-body"><ViewOrders :client="client" :dfn="dfn" :filter="orders_filter" :date_begin="orders_date_begin" :date_end="orders_date" /></div>
</div>
</div>
</template>
<script>
import { strptime_vista } from './util.mjs';
import DateRangePicker from './DateRangePicker.vue';
import OrderFilterPicker from './OrderFilterPicker.vue';
import ViewOrderMenu from './ViewOrderMenu.vue';
import ViewOrders from './ViewOrders.vue';
var now = new Date();
export default {
components: {
DateRangePicker, OrderFilterPicker, ViewOrderMenu, ViewOrders
},
props: {
client: Object
},
data() {
return {
dfn: null,
sensitive: false,
info: null,
orders_filter: 2,
orders_date: now,
orders_date_begin: now
};
},
watch: {
info(value) {
if((value) && (value.name)) document.title = value.name;
}
},
methods: {
strptime_vista,
async loadinfo(dfn, viewsensitive) {
this.dfn = dfn;
this.sensitive = viewsensitive ? false : await this.client.ORWPT_SELCHK(dfn);
this.info = this.sensitive ? null : await this.client.ORWPT16_ID_INFO(dfn);
}
},
async mounted() {
this.loadinfo(this.$route.params.id, this.$route.query.hasOwnProperty('viewsensitive'));
},
async beforeRouteUpdate(to, from, next) {
this.loadinfo(to.params.id, to.query.hasOwnProperty('viewsensitive'));
next();
}
};
</script>

View File

@ -0,0 +1,77 @@
<template>
<div v-if="(sensitive) && (!info)" class="alert alert-danger text-center mb-3 shadow" role="alert">
<h1>Warning: Restricted Record</h1>
<p>This record is protected by the Privacy Act of 1974 and the Health Insurance Portability and Accountability Act of 1996. If you elect to proceed, you will be required to prove you have a need to know. Accessing this patient is tracked, and your station Security Officer will contact you for your justification.</p>
<router-link class="btn btn-danger" :to="'/patient/' + dfn + '/orders?viewsensitive'">Proceed</router-link>
</div>
<div v-if="info">
<div class="card mb-3 shadow">
<div class="card-header">{{info.name}} <span :title="info.pid">{{info.pid.slice(-4)}}</span> #{{dfn}}</div>
<div class="card-body row" style="font-family: monospace;">
<div class="col" v-if="info.dob"><strong>DOB:</strong> {{strptime_vista(info.dob).toLocaleDateString('sv-SE')}}</div>
<div class="col" v-if="info.age"><strong>Age:</strong> {{info.age}}</div>
<div class="col" v-if="info.sex"><strong>Sex:</strong> {{info.sex}}</div>
<div class="col" v-if="info.sc_percentage"><strong>SC%:</strong> {{info.sc_percentage}}</div>
<div class="col" v-if="info.type"><strong>Type:</strong> {{info.type}}</div>
<div class="col" v-if="info.ward"><strong>Ward:</strong> {{info.ward}}</div>
<div class="col" v-if="info.room_bed"><strong>Room/bed:</strong> {{info.room_bed}}</div>
</div>
</div>
<div class="card mb-3 shadow">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Visits</span>
<DateRangePicker range="6M" direction="-1" v-model:date="visits_date" v-model:date_end="visits_date_begin" />
</div>
<div class="card-body"><ViewVisits :client="client" :dfn="dfn" :date_begin="visits_date_begin" :date_end="visits_date" /></div>
</div>
</div>
</template>
<script>
import { strptime_vista } from './util.mjs';
import DateRangePicker from './DateRangePicker.vue';
import OrderFilterPicker from './OrderFilterPicker.vue';
import ViewVisits from './ViewVisits.vue';
var now = new Date();
export default {
components: {
DateRangePicker, OrderFilterPicker, ViewVisits
},
props: {
client: Object
},
data() {
return {
dfn: null,
sensitive: false,
info: null,
orders_filter: 2,
visits_date: now,
visits_date_begin: now
};
},
watch: {
info(value) {
if((value) && (value.name)) document.title = value.name;
}
},
methods: {
strptime_vista,
async loadinfo(dfn, viewsensitive) {
this.dfn = dfn;
this.sensitive = viewsensitive ? false : await this.client.ORWPT_SELCHK(dfn);
this.info = this.sensitive ? null : await this.client.ORWPT16_ID_INFO(dfn);
}
},
async mounted() {
this.loadinfo(this.$route.params.id, this.$route.query.hasOwnProperty('viewsensitive'));
},
async beforeRouteUpdate(to, from, next) {
this.loadinfo(to.params.id, to.query.hasOwnProperty('viewsensitive'));
next();
}
};
</script>

View File

@ -0,0 +1,93 @@
<template>
<div>
<div class="card mb-3 shadow">
<div class="card-header">Overview</div>
<div class="card-body">
<ViewResourceLookup :client="client" v-model:selection="selection" />
</div>
</div>
<div class="card mb-3 shadow">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Daily</span>
</div>
<div class="card-body">
<table class="table" style="font-family: monospace;" v-if="appointments_daily.length > 0">
<thead>
<tr><th>Date</th><th>Count</th></tr>
</thead>
<tbody>
<tr v-for="row in appointments_daily">
<td>{{row.key}}</td>
<td>{{row.values.length}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { groupByArray, strtr_unscramble, strHashHSL, strftime_vista, debounce } from './util.mjs';
import ViewResourceLookup from './ViewResourceLookup.vue';
function dateonly(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
export default {
components: {
ViewResourceLookup
},
props: {
client: Object
},
data() {
var today = dateonly(new Date());
return {
appointments: [],
production: true,
date_begin: new Date(today.getFullYear(), today.getMonth(), today.getDate()),
date_end: new Date(today.getFullYear() + 1, today.getMonth(), today.getDate()),
dow: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
};
},
computed: {
selection: {
get() { return this.client.remotestate.resources ? (this.client.remotestate.resources.split(',').filter(x => x) || []) : [] },
set(value) { this.client.remotestate.resources = value.join(','); }
},
appointments_daily() {
return groupByArray(this.appointments, x => x.ApptDateDate);
}
},
watch: {
selection: {
handler(value) { this.$nextTick(() => this.debounced_selection(value)); },
immediate: true
}
},
methods: {
strHashHSL,
strtr_unscramble,
datefmt(date) {
return date ? date.toLocaleDateString('sv-SE') : '';
}
},
created() {
this.debounced_selection = debounce(async function(value) {
this.appointments = this.selection.length > 0 ? (await this.client.SDEC_CLINLET(this.selection.join('|') + '|', strftime_vista(this.date_begin), strftime_vista(this.date_end))) : [];
this.appointments.forEach(x => {
var obj = x.ApptDateObj = new Date(x.ApptDate);
var date = x.ApptDateDate = obj.toLocaleDateString('sv-SE');
//x.ApptDateWeek = obj.getFullYear() + '-' + Math.floor(((obj - new Date(obj.getFullYear(), 0, 1))/(24*60*60*1000) + obj.getDay())/7);
//x.ApptDateMonth = obj.getFullYear() + '-' + obj.getMonth();
});
}, 500);
},
async mounted() {
this.production = (await this.client.serverinfo()).result.production == '1';
}
};
</script>

View File

@ -0,0 +1,64 @@
<template>
<ul class="nav nav-tabs">
<li v-if="label" class="nav-link disabled">{{label}}</li>
<li class="nav-item"><span class="nav-link" :class="{ active: view_new }" @click="view_new = true">Location</span></li>
<li v-if="view_new" class="nav-item d-flex justify-content-between align-items-center">
<div class="input-group">
<span class="input-group-text">🔎</span>
<input class="form-control" placeholder="Filter..." v-model="query" />
</div>
</li>
<li class="nav-item"><span class="nav-link" :class="{ active: !view_new }" @click="view_new = false">Encounter</span></li>
<li v-if="!view_new" class="nav-item d-flex justify-content-between align-items-center"><DateRangePicker v-else range="6M" direction="-1" v-model:date="visits_date_end" v-model:date_end="visits_date_begin" /></li>
</ul>
<div class="scroller">
<ViewLocationLookupNew v-if="view_new" :client="client" v-model:query="query" :modelValue="x_modelValue ? x_modelValue.IEN : null" @update:modelValue="x_modelValue = { IEN: $event }" />
<ViewLocationLookupExisting v-else :client="client" :dfn="dfn" :date_begin="visits_date_begin" :date_end="visits_date_end" v-model="x_modelValue" />
</div>
</template>
<style scoped>
span.nav-link {
cursor: pointer;
}
div.scroller {
max-height: 25vh;
overflow-y: auto;
}
</style>
<script>
import DateRangePicker from './DateRangePicker.vue';
import ViewLocationLookupNew from './ViewLocationLookupNew.vue';
import ViewLocationLookupExisting from './ViewLocationLookupExisting.vue';
const now = new Date();
export default {
components: {
DateRangePicker, ViewLocationLookupNew, ViewLocationLookupExisting
},
props: {
client: Object,
dfn: String,
label: String,
modelValue: String
},
emits: {
'update:modelValue': Object
},
data() {
return {
view_new: true,
query: '',
visits_date_begin: now,
visits_date_end: now,
x_modelValue: this.modelValue
};
},
watch: {
modelValue(value) { this.x_modelValue = value; },
x_modelValue(value) { this.$emit('update:modelValue', value); }
}
};
</script>

View File

@ -0,0 +1,77 @@
<template>
<table v-if="resultset.length > 0" class="table table-striped">
<tbody>
<tr v-for="item in resultset" :class="{ 'table-active': (x_modelValue) && (x_modelValue.IEN) && (x_modelValue.datetime) && (item.location_ien == x_modelValue.IEN) && (item.datetime == x_modelValue.datetime) }" @click="x_modelValue = { IEN: item.location_ien, location: item.location, datetime: item.datetime, appointment_ien: item.IEN }">
<td>{{item.location}}</td>
<td>#{{item.location_ien}}</td>
<td style="text-align: right;">{{item.status}}</td>
<td style="text-align: right;">{{item.datestr}} {{item.timestr}}</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
td {
cursor: default;
}
.table-active, .table-active:nth-of-type(odd) > * {
color: #fff;
background-color: #0d6efd;
}
</style>
<script>
import { strftime_vista, strptime_vista, debounce } from './util.mjs';
function date_down(d) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function date_up(d) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
}
export default {
props: {
client: Object,
dfn: String,
date_begin: Date,
date_end: Date,
modelValue: Object
},
emits: {
'update:modelValue': Object
},
data() {
return {
resultset: [],
x_modelValue: this.modelValue
};
},
computed: {
params() {
return { dfn: this.dfn, date_begin: strftime_vista(date_down(this.date_begin)), date_end: strftime_vista(date_up(this.date_end)) };
}
},
watch: {
modelValue(value) { this.x_modelValue = value; },
x_modelValue(value) { this.$emit('update:modelValue', value); },
params: {
async handler(value) {
console.log(value);
try {
this.resultset = (await this.client.ORWCV_VST(value.dfn, value.date_begin, value.date_end, '')).map(item => Object.assign({
datestr: strptime_vista(item.datetime).toLocaleDateString('sv-SE'),
timestr: strptime_vista(item.datetime).toLocaleTimeString('en-GB'),
location_ien: item.apptinfo.split(';')[2]
}, item)).reverse();
} catch(ex) {
this.resultset = [];
console.warn(ex);
}
}, immediate: true
}
}
};
</script>

View File

@ -0,0 +1,124 @@
<template>
<div v-if="input" class="input-group">
<span class="input-group-text">🔎</span>
<input class="form-control" placeholder="Filter..." v-model="query_raw" />
</div>
<div class:scroller="input" ref="scroller">
<table class="table table-striped">
<tbody>
<tr v-for="item in resultset" :class="{ 'table-active': item.IEN == x_modelValue }" @click="x_modelValue = item.IEN">
<td>{{item.name}}</td>
<td style="text-align: right;">#{{item.IEN}}</td>
</tr>
<tr ref="bottom" />
</tbody>
</table>
</div>
</template>
<style scoped>
div.scroller {
max-height: 25vh;
overflow-y: auto;
}
td {
cursor: default;
}
.table-active, .table-active:nth-of-type(odd) > * {
color: #fff;
background-color: #0d6efd;
}
</style>
<script>
import { debounce } from './util.mjs';
export default {
props: {
client: Object,
query: String,
input: {
type: Boolean,
default: false
},
modelValue: String
},
emits: {
'update:modelValue': String
},
data() {
return {
resultset: [],
query_raw: this.query,
query_view: '',
has_more: false,
is_loading: true,
observer_bottom: null,
x_modelValue: this.modelValue
};
},
computed: {
params() {
return { query: this.query_view.length >= 3 ? this.query_view : '' };
}
},
watch: {
modelValue(value) { this.x_modelValue = value; },
x_modelValue(value) { this.$emit('update:modelValue', value); },
query(value) { this.query_raw = value; },
query_raw(value) {
this.$emit('update:query', value);
this.query_sync(value);
},
query_view: {
async handler(value) {
this.is_loading = true;
this.has_more = false;
try {
if(value.length >= 3) {
var batch = await this.client.ORWU1_NEWLOC(value.slice(0, -1) + String.fromCharCode(value.charCodeAt(value.length - 1) - 1), 1);
this.resultset = batch.filter(x => x.name.startsWith(value));
} else this.resultset = await this.client.ORWU1_NEWLOC('', 1);
this.has_more = this.resultset.length > 0;
} catch(ex) {
this.resultset = [];
this.has_more = false;
} finally {
this.is_loading = false;
this.$refs.scroller.scrollTo(0, 0);
}
}, immediate: true
}
},
methods: {
async handle_bottom([entry]) {
if((entry.isIntersecting) && (this.has_more) && (!this.is_loading)) {
this.is_loading = true;
this.has_more = false;
try {
var batch = await this.client.ORWU1_NEWLOC(this.resultset[this.resultset.length - 1].name, 1);
if(this.query_view.length >= 3) batch = batch.filter(x => x.name.startsWith(this.query_view));
if(batch.length > 0) {
Array.prototype.push.apply(this.resultset, batch);
this.has_more = true;
} else this.has_more = false;
} catch(ex) {
this.has_more = false;
} finally {
this.is_loading = false;
}
}
}
},
created() {
this.query_sync = debounce(function(value) { this.query_view = value.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ').toUpperCase(); }, 500);
},
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

@ -0,0 +1,71 @@
<template>
<ViewOrderDialogLab v-if="x_form_id == constants.OD_LAB" :client="client" :ien="ien" :dlgdef="dlgdef" :bldqrsp="bldqrsp" @submit="submit" />
<ViewOrderDialogMedOutpt v-else-if="x_form_id == constants.OD_MEDOUTPT" :client="client" :ien="ien" :dlgdef="dlgdef" :bldqrsp="bldqrsp" @submit="submit" />
<ViewOrderDialogGeneric v-else-if="x_form_id == constants.OD_RTC" :client="client" :ien="ien" :dfn="dfn" :location_ien="location_ien" :dlgname="dlgname" :dlgdef="dlgdef" @submit="submit" />
<ViewOrderDialogGeneric v-else-if="x_form_id" :client="client" :ien="ien" :dfn="dfn" :location_ien="location_ien" :dlgname="dlgname" :dlgdef="dlgdef" @submit="submit" />
</template>
<script>
import * as constants from './constants.mjs';
import ViewOrderDialogLab from './ViewOrderDialogLab.vue';
import ViewOrderDialogMedOutpt from './ViewOrderDialogMedOutpt.vue';
import ViewOrderDialogGeneric from './ViewOrderDialogGeneric.vue';
export default {
components: {
ViewOrderDialogLab, ViewOrderDialogMedOutpt, ViewOrderDialogGeneric
},
props: {
client: Object,
ien: String,
form_id: String,
dfn: String,
location_ien: String
},
emits: {
'update:form_id': Number
},
data() {
return {
constants,
dlgname: null,
dlgdef: null,
prompts: null,
inputs: null,
bldqrsp: null,
odslct: null,
x_form_id: this.form_id
};
},
methods: {
async submit(payload) {
if((this.ien) && (this.dfn) && (payload)) {
var user_ien = (await this.client.userinfo()).result[0];
var dgrp = await this.client.ORWDX_DGRP(this.dlgname.BaseDialogName);
var res = await client.ORWDX_SAVE(this.dfn, 0/*user_ien*/, 0/*location_ien*/, this.dlgname.BaseDialogName, dgrp, this.dlgname.BaseDialogIEN, ''/*order_ifn*/, payload, '', '', '', 0);
console.log(res);
}
}
},
created() {
this.$watch(
() => (this.client, this.ien, {}),
async () => {
if((this.client) && (this.ien)) {
var userinfo = await this.client.userinfo();
var user_ien = userinfo && userinfo.result ? userinfo.result[0] : '';
this.dlgname = await this.client.ORWDXM_DLGNAME(this.ien);
if(this.dlgname.BaseDialogIEN != this.ien) console.warn('IEN =', this.ien, '|', 'BaseDialogIEN =', this.dlgname.BaseDialogIEN);
this.dlgdef = await this.client.ORWDX_DLGDEF(this.dlgname.BaseDialogName);
if(this.bldqrsp = (await this.client.ORWDXM1_BLDQRSP(this.ien, '^^' + user_ien + '^^^^;;;^^^^', 0, 0))[0]) this.$emit('update:form_id', this.x_form_id = +this.bldqrsp.FormID);
} else {
this.dlgname = this.dlgdef = this.bldqrsp = null;
this.x_form_id = +this.form_id;
}
if(this.x_form_id == constants.OD_RTC) this.odslct = await client.ORWDSD1_ODSLCT(0, 0);
},
{ immediate: true }
);
}
};
</script>

View File

@ -0,0 +1,118 @@
<template>
<div v-if="prompts">
<div class="row">
<template v-for="item in prompts">
<template v-if="(item.hid == '1') || (item.type == 'H')"></template>
<div v-else-if="(odslct) && (odslct[item.id]) && (odslct[item.id].items)" class="col-12">
<div class="card mb-3">
<div class="card-header">{{item.prompt.replace(/\:\s*$/, '')}}</div>
<ul class="list-group list-group-flush" style="max-height: 25vh; overflow-y: auto;"><li v-for="item in odslct[item.id].items" class="list-group-item"><input class="form-check-input me-1" type="checkbox" /> {{item}}</li></ul>
<div v-if="item.help" class="card-footer form-text">{{item.help}}</div>
</div>
</div>
<div v-else-if="item.type == 'P'" class="col-12">
<div class="card mb-3">
<ViewLocationLookup :client="client" :dfn="dfn" :label="item.prompt.replace(/\:\s*$/, '')" />
<div v-if="item.help" class="form-text">{{item.help}}</div>
</div>
</div>
<div v-else-if="item.type == 'Y'" class="col col-12 col-md-6 col-lg-4 col-xl-3">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" role="switch" v-model="inputs[item.id]" />
<label class="form-check-label">{{item.prompt.replace(/\:\s*$/, '')}}</label>
<div v-if="item.help" class="form-text">{{item.help}}</div>
</div>
</div>
<div v-else-if="item.type == 'R'" class="col col-12 col-md-6 col-lg-4 col-xl-3">
<div class="mb-3">
<label class="form-label">{{item.prompt.replace(/\:\s*$/, '')}}</label>
<DateTimePicker dateonly v-model="inputs[item.id]" />
<div v-if="item.help" class="form-text">{{item.help}}</div>
</div>
</div>
<div v-else-if="item.type == 'N'" class="col col-12 col-md-6 col-lg-4 col-xl-3">
<div class="form-floating mb-3">
<input type="number" class="form-control" :placeholder="item.default || ' '" v-model="inputs[item.id]" />
<label>{{item.prompt.replace(/\:\s*$/, '')}}</label>
<div v-if="item.help" class="form-text">{{item.help}}</div>
</div>
</div>
<div v-else-if="item.type == 'W'" class="col-12">
<div class="card mb-3" style="background-color: #fdfbd7;">
<div class="card-header">{{item.prompt.replace(/\:\s*$/, '')}}</div>
<div class="card-body" style="font-family: monospace; white-space: pre;">{{item.detail}}</div>
</div>
</div>
<div v-else class="col col-12">
<div class="form-floating mb-3">
<input type="text" class="form-control" :placeholder="item.default || ' '" :disabled="item.type == 'P'" v-model="inputs[item.id]" />
<label>{{item.prompt.replace(/\:\s*$/, '')}}</label>
<div v-if="item.help" class="form-text">{{item.help}}</div>
</div>
</div>
</template>
</div>
<div v-if="(odslct) && (odslct.Info) && (odslct.Info.text)" class="card mb-3" style="background-color: #fdfbd7;"><div class="card-body" style="font-family: monospace; white-space: pre;">{{odslct.Info.text}}</div></div>
<div class="btn-group mb-3">
<button type="button" class="btn btn-primary" @click="e => $emit('submit', output)">Submit</button>
<button type="button" class="btn btn-danger" @click="e => $emit('cancel')">Cancel</button>
</div>
</div>
</template>
<script>
import ViewLocationLookup from './ViewLocationLookup.vue';
import DateTimePicker from './DateTimePicker.vue';
export default {
components: {
ViewLocationLookup, DateTimePicker
},
props: {
client: Object,
ien: String,
dfn: String,
location_ien: String,
dlgname: Object,
dlgdef: Object,
modelValue: Object
},
emits: [
'submit',
'cancel',
'update:modelValue'
],
data() {
return {
prompts: null,
inputs: null,
odslct: null
};
},
computed: {
output() {
if((this.dlgdef) && (this.prompts) && (this.inputs)) {
return this.dlgdef.reduce((acc, val) => (acc['"' + val.promptIEN + '","1"'] = this.inputs[val.promptID] || this.prompts[val.promptID].idflt, acc), {});
}
}
},
created() {
this.$watch(
() => (this.client, this.ien, this.dlgdef, {}),
async () => {
var bldqrsp, loadrsp;
if((this.client) && (this.ien) && (this.dlgdef) && (bldqrsp = (await this.client.ORWDXM1_BLDQRSP(this.ien, '^^^^^^;;;^^^^', 0, 0))[0])) {
this.inputs = this.dlgdef.reduce((acc, val) => (acc[val.promptID] = null, acc), {});
this.prompts = await client.ORWDXM_PROMPTS(this.dlgname.BaseDialogIEN);
if(this.prompts) Object.assign(this.inputs, this.prompts.reduce((acc, val) => (acc[val.id] = val.default, acc), {}));
if((bldqrsp.ResponseID) && (bldqrsp.ResponseID != '0')) {
loadrsp = await this.client.ORWDX_LOADRSP(bldqrsp.ResponseID, 0, 0);
if(loadrsp) Object.assign(this.inputs, loadrsp.reduce((acc, val) => (acc[val.promptID] = val.iValue, acc), {}));
}
} else this.prompts = this.inputs = null;
},
{ immediate: true }
);
}
};
</script>

View File

@ -0,0 +1,161 @@
<template>
<div v-if="options_facility && inputs" class="row">
<div class="col-6">
<div class="mb-3">
<ViewOrderableLookup :client="client" label="Lab tests" xref="S.LAB" :qocall="ien" v-model="inputs.ORDERABLE" />
</div>
</div>
<div class="col-6">
<template v-if="options_test">
<div v-if="(options_test['CollSamp']) && (options_test['CollSamp'].items)" class="form-floating mb-3">
<select class="form-select" v-model="inputs.SAMPLE"><option v-for="item in options_test['CollSamp'].items" :value="item.SampIEN">{{item.SampName}}</option></select>
<label>Collect Sample</label>
</div>
<div v-if="(options_test['Specimens']) && (options_test['Specimens'].items)" class="form-floating mb-3">
<select class="form-select" v-model="inputs.SPECIMEN"><option v-for="item in options_test['Specimens'].items" :value="item.value">{{item.text}}</option></select>
<label>Specimen</label>
</div>
<div v-if="(options_test['Urgencies']) && (options_test['Urgencies'].items)" class="form-floating mb-3">
<select class="form-select" v-model="inputs.URGENCY"><option v-for="item in options_test['Urgencies'].items" :value="item.value">{{item.text}}</option></select>
<label>Urgency</label>
</div>
</template>
</div>
<div class="col-3">
<div class="form-floating mb-3">
<select class="form-select" v-model="inputs.COLLECT"><option v-for="item in options_facility['Collection Types'].items" :value="item.value" :selected="item.default">{{item.text}}</option></select>
<label>Collection Type</label>
</div>
</div>
<div class="col-3">
<div class="mb-3">
<template v-if="(inputs.COLLECT == 'LC') && (options_facility['Lab Collection Times'])">
<Autocomplete label="Lab Collection Time" :items="Object.keys(mapping_START)" v-model="inputs.START" />
</template>
<template v-else-if="(inputs.COLLECT == 'WC') && (options_facility['Ward Collection Times'])">
<Autocomplete label="Ward Collection Time" :items="Object.keys(mapping_START)" v-model="inputs.START" />
</template>
<template v-else-if="(inputs.COLLECT == 'SP') && (options_facility['Send Patient Times'])">
<Autocomplete label="Send Patient Time" :items="Object.keys(mapping_START)" v-model="inputs.START" />
</template>
<template v-else>
<label>Collection Date/Time</label>
<DateTimePicker v-model="inputs.START" />
</template>
</div>
</div>
<div class="col-3">
<div class="form-floating mb-3">
<select class="form-select" v-model="inputs.SCHEDULE"><option v-for="item in options_facility['Schedules'].items" :value="item.value" :selected="item.default">{{item.text}}</option></select>
<label>How Often?</label>
</div>
</div>
<div class="col-3">
<div class="form-floating mb-3" title="Enter a number of days, or an 'X' followed by a number of times.">
<input type="text" class="form-control" placeholder=" " v-model="inputs.DAYS" />
<label>How Long?</label>
</div>
</div>
<div class="col-9">
<div v-if="preview" class="card mb-3" style="background-color: #fdfbd7;"><div class="card-body" style="font-family: monospace; white-space: pre-wrap;">{{preview}}</div></div>
<div v-if="(options_test) && (options_test['CollSamp']) && (options_test['CollSamp'].text)" class="card mb-3" style="background-color: #fdfbd7;"><div class="card-body" style="font-family: monospace; white-space: pre-wrap;">{{options_test['CollSamp'].text}}</div></div>
</div>
<div class="col-3">
<div class="btn-group-vertical mb-3" style="width: 100%;">
<button type="button" class="btn btn-primary" @click="e => $emit('submit', output)">Submit</button>
<button type="button" class="btn btn-danger" @click="e => $emit('cancel')">Cancel</button>
</div>
</div>
</div>
</template>
<script>
import { debounce } from './util.mjs';
import ViewOrderableLookup from './ViewOrderableLookup.vue';
import Autocomplete from './Autocomplete.vue';
import DateTimePicker from './DateTimePicker.vue';
export default {
components: {
ViewOrderableLookup, Autocomplete, DateTimePicker
},
props: {
client: Object,
ien: String,
dlgdef: Object,
bldqrsp: Object,
modelValue: Object
},
emits: [
'submit',
'cancel',
'update:modelValue'
],
data() {
return {
inputs: null,
options_facility: null,
options_test: null
};
},
computed: {
mapping_START() {
if((this.inputs) && (this.inputs.COLLECT) && (this.options_facility)) {
var mapping = this.inputs.COLLECT == 'LC' ? this.options_facility['Lab Collection Times'] : this.inputs.COLLECT == 'WC' ? this.options_facility['Ward Collection Times'] : this.inputs.COLLECT == 'SP' ? this.options_facility['Send Patient Times'] : null;
if((mapping) && (mapping.items)) return mapping.items.reduce((acc, val) => (acc[val.text] = val.value.substring(1), acc), {});
}
},
preview() {
if((this.options_test) && (this.inputs)) {
var res = [], inputs = this.inputs, x;
if(this.options_test['Test Name']) res.push(this.options_test['Test Name'].default);
if((inputs.SAMPLE) && (this.options_test['CollSamp']) && (x = this.options_test['CollSamp'].items.find(x => x.SampIEN == inputs.SAMPLE))) res.push(x.SampName.replace(/^\s+|\s+$/g, ''));
if((inputs.SPECIMEN) && (this.options_test['Specimens']) && (x = this.options_test['Specimens'].items.find(x => x.value == inputs.SPECIMEN))) res.push(x.text.replace(/^\s+|\s+$/g, ''));
if(inputs.COLLECT) res.push(inputs.COLLECT);
return res.join(' ');
}
},
output() {
if((this.dlgdef) && (this.inputs)) {
var inputs = Object.assign({}, this.inputs);
if((this.mapping_START) && (inputs.START) && (this.mapping_START.hasOwnProperty(inputs.START))) inputs.START = this.mapping_START[inputs.START];
return this.dlgdef.reduce((acc, val) => (acc['"' + val.promptIEN + '","1"'] = inputs[val.promptID], acc), {});
}
}
},
created() {
this.$watch(
() => this.client,
async () => {
this.options_facility = this.client ? await this.client.ORWDLR32_DEF(0, 0) : null;
},
{ immediate: true }
);
this.$watch(
() => (this.client, this.ien, this.dlgdef, {}),
async () => {
if((this.client) && (this.ien) && (this.dlgdef) && (this.bldqrsp)) {
this.inputs = this.dlgdef.reduce((acc, val) => (acc[val.promptID] = null, acc), {});
if((this.bldqrsp.ResponseID) && (this.bldqrsp.ResponseID != '0')) {
var loadrsp = await this.client.ORWDX_LOADRSP(this.bldqrsp.ResponseID, 0, 0);
if(loadrsp) Object.assign(this.inputs, loadrsp.reduce((acc, val) => (acc[val.promptID] = val.iValue, acc), {}));
}
} else this.inputs = null;
},
{ immediate: true }
);
this.$watch(
() => (this.client, this.inputs && this.inputs.ORDERABLE, {}),
async () => {
this.options_test = (this.client) && (this.inputs) && (this.inputs.ORDERABLE) ? await this.client.ORWDLR32_LOAD(this.inputs.ORDERABLE) : null;
}
);
this.$watch(
() => { return { dlgdef: this.dlgdef, inputs: this.inputs }; },
debounce(() => this.$emit('update:modelValue', this.output), 100),
{ deep: true }
);
}
};
</script>

View File

@ -0,0 +1,295 @@
<template>
<div v-if="options_med && inputs" class="row">
<div class="col-12">
<div class="mb-3">
<ViewOrderableRawLookup :client="client" label="Orderable" dgnm="O RX" :query="options_med ? options_med.Medication.default.text : null" v-model="common.ORDERABLE" />
</div>
</div>
<template v-for="(instance, idx) in inputs" :key="instance">
<ViewOrderDialogMedOutptInstance :multi="inputs.length > 1" :more="idx + 1 < inputs.length" :dlgdef="dlgdef" :options_med="options_med" :options_schedule="options_schedule" :loadrsp_group="loadrsp_groups[idx]" v-model="inputs[idx]" @add="e => inputs.splice(idx + 1, 0, Object.assign({}, instance))" @remove="e => inputs.splice(idx, 1)" />
</template>
<div class="col-4">
<div class="mb-3">
<Autocomplete label="Indication" :items="options_med['Indication'].items" v-model="common.INDICATION" />
</div>
</div>
<div class="col-8">
<div class="form-floating mb-3">
<input type="text" class="form-control" placeholder=" " v-model="common.COMMENT" />
<label>Comment</label>
</div>
</div>
<div class="col-2">
<div class="form-floating mb-3">
<input type="number" class="form-control" placeholder=" " v-model="common.SUPPLY" />
<label>Days Supply</label>
</div>
</div>
<div class="col-2">
<div class="form-floating mb-3">
<input type="number" class="form-control" placeholder=" " v-model="common.QTY" />
<label>{{base_shortform ? 'Qty (in ' + base_shortform + ')' : 'Quantity'}}</label>
</div>
</div>
<div class="col-2">
<div class="form-floating mb-3">
<input type="number" class="form-control" placeholder=" " v-model="common.REFILLS" />
<label>Refills</label>
</div>
</div>
<div class="col-3">
<div class="form-floating mb-3">
<select class="form-select" v-model="common.PICKUP">
<option value="M">Mail</option>
<option value="W">Window</option>
</select>
<label>Pickup</label>
</div>
</div>
<div class="col-3">
<div v-if="options_fill && options_fill.Priority && options_fill.Priority.items" class="form-floating mb-3">
<select class="form-select" v-model="common.URGENCY"><option v-for="item in options_fill.Priority.items" :value="item.value">{{item.text}}</option></select>
<label>Priority</label>
</div>
</div>
<div class="col-10">
<div class="card mb-3" style="background-color: #fdfbd7;"><div class="card-body" style="font-family: monospace; white-space: pre-wrap;">{{preview}}</div></div>
</div>
<div class="col-2">
<div class="btn-group-vertical mb-3" style="width: 100%;">
<button type="button" class="btn btn-primary" @click="e => $emit('submit', output)">Submit</button>
<button type="button" class="btn btn-danger" @click="e => $emit('cancel')">Cancel</button>
</div>
</div>
</div>
</template>
<script>
import { debounce, groupByArray } from './util.mjs';
import { translate } from './numberwords.mjs';
import ViewOrderableRawLookup from './ViewOrderableRawLookup.vue';
import Autocomplete from './Autocomplete.vue';
import DateTimePicker from './DateTimePicker.vue';
import ViewOrderDialogMedOutptInstance from './ViewOrderDialogMedOutptInstance.vue';
const C_COMMON = ['ORDERABLE', 'INDICATION', 'COMMENT', 'SUPPLY', 'QTY', 'REFILLS', 'PICKUP', 'URGENCY'];
const C_DAYS = {
'': 1,
'MONTH': 30,
'WEEK': 7,
'DAY': 1,
'HOUR': 1/24,
'MINUTE': 1/24/60
};
const C_WP = { '15': true, '758': true, '763': true };
function input_dose_parse(value) {
value = (value) && (value.indexOf('&') >= 0) ? value.split('&') : [parseFloat(value) || null, value ? value.replace(/^\s*([+-]?(?:\d+|\d+\.\d+))\s*|\s+$/g, '').toUpperCase() || null : null, null, null, value, null, null, null];
value.dose = value[0];
value.unit = value[1];
value.count = value[2];
value.form = value[3];
value.text = value[4];
value.id = value[5];
value.base_dose = value[6];
value.base_unit = value[7];
return value;
}
function greatest_common_base(values, products) {
for(var i = 0; i < values.length; ++i) {
var value = values[i];
if((isNaN(value.dose)) || (isNaN(parseFloat(value.dose)))) return;
products = products.filter(x => (x.dose) && (x.unit == value.unit) && ((Math.abs((value.dose/x.dose)%1) < 0.001) || ((x.split == '1') && (Math.abs((value.dose/x.dose)%1 - 0.5) < 0.001))));
if(products.length < 1) return;
}
return products.sort((a, b) => b.dose - a.dose)[0];
}
export default {
components: {
ViewOrderableRawLookup, Autocomplete, DateTimePicker, ViewOrderDialogMedOutptInstance
},
props: {
client: Object,
ien: String,
dlgdef: Object,
bldqrsp: Object,
modelValue: Object
},
emits: [
'submit',
'cancel',
'update:modelValue'
],
data() {
return {
loadrsp: null,
loadrsp_groups: [],
inputs: null,
options_schedule: null,
options_med: null,
lastorderable: null,
common: {}
};
},
computed: {
options_med_route() {
if((this.options_med) && (this.options_med['Route'])) {
var opt, rsp, res = this.options_med['Route'].items.slice();
res.mapping = res.reduce((acc, val) => (acc[val.value] = val, acc), {});
if((this.loadrsp) && (rsp = this.loadrsp['ROUTE']) && (!res.find(x => x.value == rsp.iValue))) res.push({ value: rsp.iValue, text: rsp.eValue, abbr: rsp.eValue, sig: rsp.eValue });
return res;
} else return [];
},
input_doses() {
return this.inputs ? this.inputs.map(x => input_dose_parse(x.DOSE)) : [];
},
base_product() {
return this.options_med ? greatest_common_base(this.input_doses, this.options_med['Dispense'].items) : undefined;
},
base_form() {
try {
return this.options_med['AllDoses'].items.find(x => x.id == this.base_product.id).dosefields.split('&')[3];
} catch(ex) {}
},
base_shortform() {
try {
var dose_unit = this.base_product.dose + this.base_product.unit;
return this.options_med['Dosage'].items.find(x => x.text == dose_unit).form;
} catch(ex) {
return this.base_form;
}
},
sig() {
if((this.options_med) && (this.inputs)) {
var res = [];
var verb = this.options_med['Verb'].default;
var input_doses = this.input_doses;
var base_product = this.base_product;
var base_form = this.base_form;
for(var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i], input_route = input.ROUTE, input_dose = input_doses[i];
var route = (this.options_med_route.find(x => x.value == input_route) || { sig: input_route }).sig;
var prep = this.options_med_route.mapping[input_route] ? this.options_med['Preposition'].default : null;
var schedule = (this.options_schedule) && (this.options_schedule[input.SCHEDULE]) ? this.options_schedule[input.SCHEDULE].text : input.SCHEDULE;
res.push((input_dose ? verb + ' ' + (base_product ? translate(input_dose.dose/base_product.dose).toUpperCase() + ' ' + (base_form || 'UNIT') + (input_dose.dose/base_product.dose >= 2 ? 'S' : '') : input_dose.text) : '') + (route ? ' ' + (prep ? prep + ' ' : '') + route : '') + (schedule ? ' ' + schedule : '') + (input.SCHTYPE == 'P' ? ' AS NEEDED' : '') + (input.DAYS ? ' FOR ' + input.DAYS : '') + (i + 1 >= this.inputs.length ? '' : (' ' + (input.CONJ == 'T' ? 'THEN' : input.CONJ == 'A' ? 'AND' : 'OR'))));
}
return res.join(' ');
}
},
preview() {
var base_product = this.base_product;
var product = base_product ? base_product.text + '\n' : this.options_med ? this.options_med['Medication'].default.text + '\n' : '';
return product + (this.sig ? this.sig + (this.common.COMMENT ? ' ' + this.common.COMMENT : '') + '\n' : '') + 'Quantity: ' + (+this.common.QTY) + ' Refills: ' + (+this.common.REFILLS) + (this.common.INDICATION ? '\nIndication: ' + this.common.INDICATION : '');
},
output() {
if((this.dlgdef) && (this.inputs)) {
var res = {};
var inputs = this.inputs.length > 0 ? this.inputs.slice() : [{}];
inputs[0] = this.common ? Object.assign({}, inputs[0], this.common) : Object.assign({}, inputs[0]);
inputs[0].SIG = this.sig;
if(this.base_product) {
inputs[0].STRENGTH = this.base_product.dose + this.base_product.unit;
inputs[0].DRUG = this.base_product.id;
}
for(var i = 0; i < inputs.length; ++i) {
var seq = i + 1, instance = inputs[i];
for(var j = 0, desc, val; j < this.dlgdef.length; ++j) {
desc = this.dlgdef[j];
if(val = instance[desc.promptID]) {
if(C_WP[desc.promptIEN]) {
res['"' + desc.promptIEN + '","' + seq + '"'] = 'ORDIALOG("WP",' + desc.promptIEN + ',' + seq + ')';
res['"WP","' + desc.promptIEN + '","' + seq + '","1","0"'] = instance[desc.promptID];
} else res['"' + desc.promptIEN + '","' + seq + '"'] = instance[desc.promptID];
}
}
}
return res;
}
}
},
created() {
this.$watch(
() => this.client,
async () => {
if(this.client) {
this.options_schedule = await this.client.ORWDPS1_SCHALL('', 0);
this.options_fill = await this.client.ORWDPS1_ODSLCT('O', '', '');
} else this.options_schedule = this.options_fill = null;
},
{ immediate: true }
);
this.$watch(
() => (this.client, this.dlgdef, this.bldqrsp, this.options_fill, {}),
async () => {
if((this.client) && (this.dlgdef) && (this.bldqrsp)) {
this.inputs = [this.dlgdef.reduce((acc, val) => (acc[val.promptID] = acc[val.promptID], acc), { ORDERABLE: this.common ? this.common.ORDERABLE : null })];
if(this.options_fill) {
if((this.options_fill.Pickup) && (this.options_fill.Pickup.default)) this.inputs.PICKUP = this.options_fill.Pickup.default.value;
if((this.options_fill.Refills) && (this.options_fill.Refills.default)) this.inputs.REFILLS = this.options_fill.Refills.default.value;
if((this.options_fill.Priority) && (this.options_fill.Priority.default)) this.inputs.URGENCY = this.options_fill.Priority.default.value;
}
this.loadrsp_groups = [];
if((this.bldqrsp.ResponseID) && (this.bldqrsp.ResponseID != '0')) {
this.loadrsp = await this.client.ORWDX_LOADRSP(this.bldqrsp.ResponseID, 0, 0);
if((this.loadrsp) && (this.loadrsp.length > 0)) {
this.loadrsp_groups = groupByArray(this.loadrsp, 'instance').sort((a, b) => a.key - b.key).map(x => x.values.reduce((acc, val) => (acc[val.promptID] = val, acc), x.values));
Object.assign(this.inputs[0], this.loadrsp_groups[0].reduce((acc, val) => (acc[val.promptID] = val.iValue != '^WP^' ? val.iValue : val.eValue, acc), {}));
for(var i = 1; i < this.loadrsp_groups.length; ++i) this.inputs.push(this.loadrsp_groups[i].reduce((acc, val) => (acc[val.promptID] = val.iValue != '^WP^' ? val.iValue : val.eValue, acc), {}));
}
}
this.common = C_COMMON.reduce((acc, val) => (acc[val] = this.inputs[0][val], acc), {});
} else {
this.inputs = this.loadrsp = null;
this.loadrsp_groups = [];
}
},
{ immediate: true }
);
this.$watch(
() => (this.client, (this.common) && (this.common.ORDERABLE), {}),
async () => {
this.options_med = (this.client) && (this.common) && (this.common.ORDERABLE) ? await this.client.ORWDPS2_OISLCT(this.common.ORDERABLE, '', '', 'Y', '') : null;
}
);
this.$watch(
() => ((this.common) && (this.common.ORDERABLE), {}),
() => {
if((this.common) && (this.common.ORDERABLE)) {
if((this.lastorderable) && (this.lastorderable != this.common.ORDERABLE)) {
for(var k in this.common) if((k != 'ORDERABLE') && (this.common.hasOwnProperty(k))) this.common[k] = null;
this.inputs = [{}];
this.loadrsp_groups = [];
}
this.lastorderable = this.common.ORDERABLE;
}
},
{ immediate: true }
);
this.$watch(
() => this.inputs ? this.inputs.map(x => x.DAYS ? parseFloat(x.DAYS)*C_DAYS[x.DAYS.replace(/^\s*([+-]?(?:\d+|\d+\.\d+))\s*|S\b|\s+$/g, '')] : 0).reduce((acc, val) => acc + val, 0) : 0,
(value) => { this.common.SUPPLY = value; },
{ immediate: true }
);
this.$watch(
() => (this.inputs) && (this.inputs.length > 0) && (this.base_product) && (this.common.SUPPLY > 0) ? this.inputs.map(x => [(x.DOSE) && (x.DOSE).indexOf('&') >= 0 ? x.DOSE.split('&')[2] : '~', x.SCHEDULE || '~', x.DAYS || '~']).reduce((acc, val) => (acc[0] += val[0] + '^', acc[1] += val[1] + '^', acc[2] += val[2] + '^', acc), ['', '', '']) : null,
debounce(async (value) => {
if(value) {
var res = await this.client.ORWDPS2_DAY2QTY(this.common.SUPPLY, value[0], value[1], value[2], '', '');
if(!isNaN(parseFloat(res))) this.common.QTY = res;
};
}, 500),
{ immediate: true }
);
this.$watch(
() => { return { dlgdef: this.dlgdef, inputs: this.inputs, common: this.common, sig: this.sig }; },
debounce(() => this.$emit('update:modelValue', this.output), 100),
{ deep: true, immediate: true }
);
}
};
</script>

View File

@ -0,0 +1,164 @@
<template>
<template v-if="(options_schedule) && (x_modelValue)">
<div :class="multi ? 'col-4' : 'col-5'">
<div class="mb-3">
<Autocomplete label="Dosage" :items="options_med_dosage.map(x => x.text)" v-model="x_dose" />
</div>
</div>
<div class="col-2">
<div class="mb-3">
<Autocomplete label="Route" :items="options_med_route.map(x => x.text)" v-model="x_route" />
</div>
</div>
<div class="col-3">
<div v-if="options_schedule" class="mb-3">
<Autocomplete label="Schedule" :items="options_schedule.map(x => x.value)" v-model="x_modelValue.SCHEDULE" />
</div>
</div>
<div :class="multi ? 'col-3' : 'col-2'">
<div v-if="multi" class="input-group mb-1">
<input type="number" class="form-control" placeholder="Duration" v-model="duration_num">
<select v-if="duration_num" class="form-select" v-model="duration_unit">
<option value="MONTH">month{{duration_num == 1 ? '' : 's'}}</option>
<option value="WEEK">week{{duration_num == 1 ? '' : 's'}}</option>
<option value="DAY" selected>day{{duration_num == 1 ? '' : 's'}}</option>
<option value="HOUR">hour{{duration_num == 1 ? '' : 's'}}</option>
<option value="MINUTE">minute{{duration_num == 1 ? '' : 's'}}</option>
</select>
<select v-if="more" class="form-select" v-model="x_modelValue.CONJ">
<option value="A">and</option>
<option value="T">then</option>
</select>
</div>
<div class="widgets mb-3">
<button type="button" class="btn" :class="{ 'btn-primary': x_modelValue.SCHTYPE == 'P', 'btn-outline-secondary': x_modelValue.SCHTYPE != 'P' }" @click="e => x_modelValue.SCHTYPE = x_modelValue.SCHTYPE == 'P' ? '' : 'P'">PRN</button>
<button v-if="multi" type="button" class="btn btn-outline-danger" @click="e => $emit('remove')">🗑</button>
<button type="button" class="btn btn-outline-success" @click="e => $emit('add')"></button>
</div>
</div>
</template>
</template>
<style scoped>
.widgets {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
gap: 0.25rem;
}
.widgets > button {
flex-grow: 1;
flex-shrink: 1;
}
</style>
<script>
import { debounce, groupByArray } from './util.mjs';
import Autocomplete from './Autocomplete.vue';
export default {
components: {
Autocomplete
},
props: {
dlgdef: Object,
options_med: Object,
options_schedule: Object,
loadrsp_group: Object,
multi: Boolean,
more: Boolean,
modelValue: Object
},
emits: [
'add',
'remove',
'update:modelValue'
],
data() {
return {
duration_num: (this.modelValue) && (this.modelValue.DAYS) ? parseFloat(this.modelValue.DAYS) : null,
duration_unit: (this.modelValue) && (this.modelValue.DAYS) ? this.modelValue.DAYS.replace(/^\s*([+-]?(?:\d+|\d+\.\d+))\s*|S\b|\s+$/g, '') : 'DAY',
x_modelValue: this.modelValue ? Object.assign({}, this.modelValue) : {}
};
},
computed: {
options_med_dosage() {
return (this.options_med) && (this.options_med['Dosage']) ? this.options_med['Dosage'].items : [];
},
options_med_route() {
if((this.options_med) && (this.options_med['Route'])) {
var opt, rsp, res = this.options_med['Route'].items.slice();
res.mapping = res.reduce((acc, val) => (acc[val.value] = val, acc), {});
if((this.loadrsp_group) && (rsp = this.loadrsp_group['ROUTE']) && (!res.find(x => x.value == rsp.iValue))) res.push({ value: rsp.iValue, text: rsp.eValue, abbr: rsp.eValue, sig: rsp.eValue });
return res;
} else return [];
},
x_dose: {
get() {
var text = this.x_modelValue.DOSE;
return this.findopt('Dosage', (x => x.value == text), 'text') || text;
},
set(value) {
this.x_modelValue.DOSE = this.findopt('Dosage', (x => x.text == value), 'value') || value;
}
},
x_route: {
get() {
var text = this.x_modelValue.ROUTE;
return (this.options_med_route.find(x => x.value == text) || { text }).text;
},
set(value) {
this.x_modelValue.ROUTE = (this.options_med_route.find(x => x.text == value) || { value }).value;
}
}
},
watch: {
x_modelValue: {
handler(value) { this.$emit('update:modelValue', value); }, deep: true
}
},
methods: {
findopt(section, predicate, key) {
try {
return this.options_med[section].items.find(predicate)[key];
} catch(ex) {}
}
},
created() {
this.$watch(
() => (this.dlgdef, this.modelValue, {}),
() => {
if(this.dlgdef) {
var res = this.dlgdef.reduce((acc, val) => (acc[val.promptID] = acc[val.promptID], acc), {});
if(this.loadrsp_group) Object.assign(res, this.loadrsp_group.reduce((acc, val) => (acc[val.promptID] = val.iValue != '^WP^' ? val.iValue : val.eValue, acc), {}));
if(this.modelValue) Object.assign(res, this.modelValue);
this.x_modelValue = this.x_modelValue ? Object.assign(this.x_modelValue, res) : res;
} else this.x_modelValue = null;
},
{ deep: true, immediate: true }
);
this.$watch(
() => (this.more, this.x_modelValue, {}),
() => {
if(this.x_modelValue) {
if(this.more) {
if(!this.x_modelValue.CONJ) this.x_modelValue.CONJ = 'T';
} else {
if(this.x_modelValue.CONJ) this.x_modelValue.CONJ = '';
}
}
},
{ immediate: true }
);
this.$watch(
() => (this.duration_num, this.duration_unit, this.x_modelValue, {}),
debounce(() => {
var value = this.duration_num ? this.duration_num + ' ' + this.duration_unit + (this.duration_num == 1 ? '' : 'S') : '';
if((this.x_modelValue) && (this.x_modelValue.DAYS != value)) this.x_modelValue.DAYS = value;
}, 250),
{ immediate: true }
);
}
};
</script>

97
htdocs/ViewOrderMenu.vue Normal file
View File

@ -0,0 +1,97 @@
<template>
<template v-if="bare">
<template v-if="selection">
<button v-if="selection.display_text" class="list-group-item list-group-item-action" @click="reselect">{{selection.display_text}} [ {{selection.type}} | {{selection.IEN}} ] ({{selection.col}}, {{selection.row}})</button>
<button v-if="selection.displayText" class="list-group-item list-group-item-action" @click="reselect">{{selection.displayText}} [ {{selection.type}} | {{selection.IEN}} ] | {{selection.windowFormId}} | {{selection.displayGroupId}}</button>
<ViewOrderDialog v-if="(dfn) && (selection.type) && ((selection.type == 'D') || (selection.type == 'Q'))" :client="client" :ien="selection.IEN" :form_id="selection.windowFormId" :dfn="dfn" />
<ViewOrderMenu v-else :key="selkey" :client="client" :ien="selection.IEN" :dfn="dfn" :bare="true" />
</template>
<div v-else-if="(columns) && (columns.length > 0)" class="list-group-item container">
<div class="row">
<div v-for="column in columns" class="col">
<div class="list-group">
<template v-for="item in column.values">
<button v-if="!item.displayonly" class="list-group-item list-group-item-action" @click="selection = item">{{item.display_text}} [{{item.type}} {{item.IEN}}] ({{item.col}}, {{item.row}})</button>
<div v-else class="list-group-item bg-light displayonly" :class="{ italic: item.displayonly == '1', bold: item.displayonly == '2' }">{{item.display_text}}</div>
</template>
</div>
</div>
</div>
</div>
<template v-else-if="(topmenu) && (topmenu.length > 0)">
<template v-for="item in topmenu">
<button v-if="item.displayText" class="list-group-item list-group-item-action" @click="selection = item">{{item.displayText}} [{{item.type}} {{item.IEN}}] | {{item.windowFormId}} | {{item.displayGroupId}}</button>
<hr v-else />
</template>
</template>
</template>
<div v-else class="list-group">
<button class="list-group-item list-group-item-action active" @click="reselect">Order entry</button>
<ViewOrderMenu :key="selkey" :client="client" :ien="ien" :dfn="dfn" :bare="true" />
</div>
</template>
<style scoped>
.displayonly {
cursor: default;
white-space: pre;
}
.displayonly.italic {
font-style: italic;
}
.displayonly.bold {
font-weight: bold;
}
hr + hr {
display: none;
}
</style>
<script>
import { groupByArray } from './util.mjs';
import ViewOrderDialog from './ViewOrderDialog.vue';
export default {
name: 'ViewOrderMenu',
components: {
ViewOrderDialog
},
props: {
client: Object,
ien: String,
dfn: String,
bare: {
default: false
}
},
data() {
return {
topmenu: null,
submenu: null,
selection: null,
selkey: 0
};
},
computed: {
columns() {
return (this.submenu) && (this.submenu.children.length) ? groupByArray(this.submenu.children, 'col').sort((a, b) => a.col - b.col).map(col => (col.values.sort((a, b) => a.row - b.row), col)) : [];
}
},
watch: {
ien: {
async handler(value) {
if(this.bare) {
if(value) this.submenu = await client.ORWDXM_MENU(value);
else this.topmenu = await this.client.ORWDX_WRLST();
}
}, immediate: true
}
},
methods: {
reselect() {
this.selkey++;
}
}
};
</script>

View File

@ -0,0 +1,154 @@
<template>
<div class="input-group">
<span v-if="label" class="input-group-text">{{label}}</span>
<select v-if="selectable_xref" class="form-select" style="flex: 0.15 1 auto;" v-model="x_xref">
<option v-for="item in ORDERABLE_XREF" :value="item.xref">{{item.name}}</option>
</select>
<input class="form-control" placeholder="Filter..." v-model="x_query" />
</div>
<div class="scroller" ref="scroller">
<table class="table table-striped">
<tbody>
<tr v-for="item in resultset" :class="{ 'table-active': item.IEN == x_modelValue }" @click="x_modelValue = item.IEN">
<td>{{item.IEN}}</td>
<td>{{item.synonym}}</td>
</tr>
<tr ref="bottom" />
</tbody>
</table>
</div>
</template>
<style scoped>
div.scroller {
max-height: 25vh;
overflow-y: auto;
}
td {
cursor: default;
}
.table-active, .table-active:nth-of-type(odd) > * {
color: #fff;
background-color: #0d6efd;
}
</style>
<script>
import { debounce } from './util.mjs';
import { ORDERABLE_XREF } from './constants.mjs';
export default {
props: {
client: Object,
xref: String,
label: String,
query: String,
qocall: {
type: String,
default: ''
},
modelValue: String
},
emits: {
'update:xref': String,
'update:query': String,
'update:modelValue': String
},
data() {
return {
ORDERABLE_XREF,
selectable_xref: !this.xref,
x_xref: this.xref || 'S.O RX',
x_query: this.query,
resultset: [],
has_more: false,
is_loading: true,
observer_bottom: null,
x_modelValue: this.modelValue
};
},
computed: {
query_view() {
return this.x_query ? this.x_query.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ').toUpperCase() : '';
}
},
watch: {
xref(value) { this.x_xref = value; },
x_xref(value) {
this.$emit('update:xref', value);
this.$emit('update:query', this.x_query = '');
},
modelValue(value) { this.x_modelValue = value; },
x_modelValue(value) { this.$emit('update:modelValue', value); },
query(value) { this.x_query = value; },
x_query(value) {
this.$emit('update:query', value);
}
},
methods: {
async handle_bottom([entry]) {
if((entry.isIntersecting) && (this.has_more) && (!this.is_loading)) {
this.is_loading = true;
this.has_more = false;
try {
var batch = await this.client.ORWDX_ORDITM(this.resultset[this.resultset.length - 1].synonym, 1, this.x_xref, this.qocall);
if(this.query_view.length >= 3) batch = batch.filter(x => x.synonym.startsWith(this.query_view));
if(batch.length > 0) {
Array.prototype.push.apply(this.resultset, batch);
this.has_more = true;
} else this.has_more = false;
} catch(ex) {
this.has_more = false;
} finally {
this.is_loading = false;
}
}
}
},
created() {
this.$watch(
() => (this.client, this.modelValue, {}),
async () => {
if((this.client) && (!this.x_query) && (this.modelValue) && (this.modelValue == parseInt(this.modelValue))) {
var item = await this.client.ORWDLR32_LOAD(this.modelValue);
this.x_query = item['Test Name'].default;
}
},
{ immediate: true }
);
this.$watch(
() => (this.client, this.xref, this.query_view, this.qocall, {}),
debounce(async function() {
if((this.client) && (this.xref)) {
this.is_loading = true;
this.has_more = false;
try {
var query = this.query_view;
if(query.length >= 3) {
var batch = await this.client.ORWDX_ORDITM(query.slice(0, -1) + String.fromCharCode(query.charCodeAt(query.length - 1) - 1), 1, this.xref, this.qocall);
this.resultset = batch.filter(x => x.synonym.startsWith(query));
if((this.resultset.length > 0) && ((this.resultset[0].name == query) || (this.resultset[0].synonym == query))) this.x_modelValue = this.resultset[0].IEN;
} else this.resultset = await this.client.ORWDX_ORDITM('', 1, this.xref, this.qocall);
this.has_more = this.resultset.length > 0;
} catch(ex) {
this.resultset = [];
this.has_more = false;
} finally {
this.is_loading = false;
this.$refs.scroller.scrollTo(0, 0);
}
}
}, 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

@ -0,0 +1,157 @@
<template>
<div class="input-group">
<span v-if="label" class="input-group-text">{{label}}</span>
<select v-if="selectable_dgnm" class="form-select" style="flex: 0.15 1 auto;" v-model="x_dgnm">
<option v-for="item in ORDERABLE_DGNM" :value="item.dgnm">{{item.name}}</option>
</select>
<input class="form-control" placeholder="Filter..." v-model="x_query" />
</div>
<div class="scroller" ref="scroller">
<table class="table table-striped">
<tbody>
<tr v-for="item in resultset" :class="{ 'table-active': item.IEN == x_modelValue }" @click="x_modelValue = item.IEN">
<td>{{item.IEN}}</td>
<td>{{item.description}}</td>
</tr>
<tr ref="bottom" />
</tbody>
</table>
</div>
</template>
<style scoped>
div.scroller {
max-height: 25vh;
overflow-y: auto;
}
td {
cursor: default;
}
.table-active, .table-active:nth-of-type(odd) > * {
color: #fff;
background-color: #0d6efd;
}
</style>
<script>
import { debounce } from './util.mjs';
import { ORDERABLE_DGNM } from './constants.mjs';
const SZ_WINDOW = 100;
export default {
props: {
client: Object,
dgnm: String,
label: String,
query: String,
modelValue: String
},
emits: {
'update:dgnm': String,
'update:query': String,
'update:modelValue': String
},
data() {
return {
ORDERABLE_DGNM,
selectable_dgnm: !this.dgnm,
x_dgnm: this.dgnm || 'O RX',
IEN: null,
count: null,
x_query: this.query,
resultset: [],
end: null,
has_more: false,
is_loading: true,
observer_bottom: null,
x_modelValue: this.modelValue
};
},
computed: {
query_view() {
return this.x_query ? this.x_query.replace(/^\s+$/g, '').replace(/\s+/g, ' ').toUpperCase() : ''; // allow trailing space
}
},
watch: {
dgnm(value) { this.x_dgnm = value; },
x_dgnm(value) {
this.$emit('update:dgnm', value);
this.$emit('update:query', this.x_query = '');
},
modelValue(value) { this.x_modelValue = value; },
x_modelValue(value) { this.$emit('update:modelValue', value); },
query(value) { this.x_query = value; },
x_query(value) {
this.$emit('update:query', value);
}
},
methods: {
async handle_bottom([entry]) {
if((entry.isIntersecting) && (this.has_more) && (this.end < this.count) && (!this.is_loading)) {
this.is_loading = true;
this.has_more = false;
try {
var batch = await this.client.ORWUL_FVSUB(this.IEN, this.end + 1, this.end = Math.min(this.end + SZ_WINDOW, this.count));
if(this.query_view.length >= 3) batch = batch.filter(x => x.description.startsWith(this.query_view));
if(batch.length > 0) {
Array.prototype.push.apply(this.resultset, batch);
this.has_more = true;
} else this.has_more = false;
} catch(ex) {
this.has_more = false;
} finally {
this.is_loading = false;
}
}
}
},
created() {
this.$watch(
() => (this.client, this.dgnm, {}),
async () => {
if((this.client) && (this.dgnm)) {
var res = await this.client.ORWUL_FV4DG(this.dgnm);
this.IEN = res.IEN;
this.count = res.count;
} else this.IEN = this.count = null;
},
{ immediate: true }
);
this.$watch(
() => (this.client, this.IEN, this.query_view, {}),
debounce(async function() {
if((this.client) && (this.IEN)) {
this.is_loading = true;
this.has_more = false;
try {
var query = this.query_view;
if(query.length >= 3) {
var row = await this.client.ORWUL_FVIDX(this.IEN, query.slice(0, -1) + String.fromCharCode(query.charCodeAt(query.length - 1) - 1) + '~');
var pos = Math.floor((row.index - 1)/SZ_WINDOW)*SZ_WINDOW;
var batch = await this.client.ORWUL_FVSUB(this.IEN, pos + 1, this.end = Math.min(pos + SZ_WINDOW, this.count));
this.resultset = batch.filter(x => x.description.startsWith(query));
} else this.resultset = await this.client.ORWUL_FVSUB(this.IEN, 1, this.end = Math.min(SZ_WINDOW, this.count));
this.has_more = this.resultset.length > 0;
} catch(ex) {
this.resultset = [];
this.has_more = false;
} finally {
this.is_loading = false;
this.$refs.scroller.scrollTo(0, 0);
}
}
}, 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

@ -1,104 +1,107 @@
<template> <template>
<table v-if="details.length > 0" class="table table-striped"> <table v-if="details.length > 0" class="table table-striped">
<template v-for="group in details"> <template v-for="group in details">
<thead> <thead>
<tr> <tr>
<th>{{map_groups[group.key].name}}</th> <th>{{map_groups[group.key].name}}</th>
<th>Practitioner</th> <th>Practitioner</th>
<th style="text-align: right;">Status</th> <th style="text-align: right;">Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="item in group.values"> <tr v-for="item in group.values">
<td><div v-for="line in item.text">{{line}}</div></td> <td><div v-for="line in item.text">{{line}}</div><div>{{item.IFN}}</div></td>
<td> <td>
<div v-if="item.PrvNam" :title="item.PrvID">{{item.PrvNam}}</div> <div v-if="item.PrvNam" :title="item.PrvID">{{item.PrvNam}}</div>
<div v-if="item.Nrs">Nurse: {{item.Nrs}}</div> <div v-if="item.Nrs">Nurse: {{item.Nrs}}</div>
<div v-if="item.Clk">Clerk: {{item.Clk}}</div> <div v-if="item.Clk">Clerk: {{item.Clk}}</div>
<div v-if="item.ChartRev">Chart: {{item.ChartRev}}</div> <div v-if="item.ChartRev">Chart: {{item.ChartRev}}</div>
<div v-if="item.LOC">{{item.LOC.split(':')[0]}}</div> <div v-if="item.LOC">{{item.LOC.split(':')[0]}}</div>
</td> </td>
<td style="text-align: right;"> <td style="text-align: right;">
<div v-if="item.Sts">{{name_of_status[item.Sts]}}</div> <div v-if="item.Sts">{{name_of_status[item.Sts]}}</div>
<div v-if="item.OrdTm">Ordered: {{strptime_vista(item.OrdTm).toLocaleDateString('sv-SE')}}</div> <div v-if="item.OrdTm">Ordered: {{strptime_vista(item.OrdTm).toLocaleDateString('sv-SE')}}</div>
<div v-if="item.StrtTm">Start: {{strptime_vista(item.StrtTm).toLocaleDateString('sv-SE')}}</div> <div v-if="item.StrtTm">Start: {{strptime_vista(item.StrtTm).toLocaleDateString('sv-SE')}}</div>
<div v-if="item.StopTm">Stop: {{strptime_vista(item.StopTm).toLocaleDateString('sv-SE')}}</div> <div v-if="item.StopTm">Stop: {{strptime_vista(item.StopTm).toLocaleDateString('sv-SE')}}</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</template> </template>
</table> </table>
</template> </template>
<style scoped> <style scoped>
th { th {
text-transform: lowercase; text-transform: lowercase;
} }
th::first-letter { th::first-letter {
text-transform: uppercase; text-transform: uppercase;
} }
</style> td:first-child {
max-width: 30rem;
<script> }
import { groupByArray, strftime_vista, strptime_vista } from './util.mjs'; </style>
const name_of_status = { <script>
0: 'Error', import { groupByArray, strftime_vista, strptime_vista } from './util.mjs';
1: 'Discontinued',
2: 'Complete', const name_of_status = {
3: 'Hold', 0: 'Error ⚠',
4: 'Flagged', 1: 'Discontinued ❌',
5: 'Pending', 2: 'Complete ✔️',
6: 'Active', 3: 'Hold ✋',
7: 'Expired', 4: 'Flagged 🚩',
8: 'Scheduled', 5: 'Pending ⏳',
9: 'Partial results', 6: 'Active ⭐',
10: 'Delayed', 7: 'Expired ❌',
11: 'Unreleased', 8: 'Scheduled ✔️',
12: 'DC/edit', 9: 'Partial results ⏳',
13: 'Cancelled', 10: 'Delayed 📅',
14: 'Lapsed', 11: 'Unreleased 🚧',
15: 'Renewed', 12: 'DC/edit ✎',
97: '', 13: 'Cancelled ❌',
98: 'New', 14: 'Lapsed ❌',
99: 'No status' 15: 'Renewed ⭐',
}; 97: '',
98: 'New ✨',
export default { 99: 'No status'
props: { };
client: Object,
dfn: String, export default {
filter: { default: 2 }, props: {
group: { default: 1 }, client: Object,
date_begin: Date, dfn: String,
date_end: Date filter: { default: 2 },
}, group: { default: 1 },
data() { date_begin: Date,
return { date_end: Date
orders: [], },
details: [], data() {
map_groups: {}, return {
name_of_status orders: [],
}; details: [],
}, map_groups: {},
computed: { name_of_status
params() { };
return { dfn: this.dfn, filter: this.filter, group: this.group, date_begin: strftime_vista(this.date_begin), date_end: strftime_vista(this.date_end) }; },
} computed: {
}, params() {
watch: { return { dfn: this.dfn, filter: this.filter, group: this.group, date_begin: strftime_vista(this.date_begin), date_end: strftime_vista(this.date_end) };
async params(value) { }
this.orders = await this.client.ORWORR_AGET(value.dfn, value.filter, value.group, value.date_begin, value.date_end); },
}, watch: {
async orders(value) { async params(value) {
this.details = this.orders.length > 0 ? groupByArray(await this.client.ORWORR_GET4LST(0, 0, value.map(x => x.ifn)), x => x.Grp) : []; this.orders = await this.client.ORWORR_AGET(value.dfn, value.filter, value.group, value.date_begin, value.date_end);
} },
}, async orders(value) {
methods: { this.details = this.orders.length > 0 ? groupByArray(await this.client.ORWORR_GET4LST(0, 0, value.map(x => x.ifn)), x => x.Grp) : [];
strptime_vista }
}, },
async mounted() { methods: {
this.map_groups = (await this.client.ORWORDG_ALLTREE()).reduce((acc, x) => (acc[x.ien] = x, acc), {}); strptime_vista
} },
}; async mounted() {
</script> this.map_groups = (await this.client.ORWORDG_ALLTREE()).reduce((acc, x) => (acc[x.ien] = x, acc), {});
}
};
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<table class="table" style="font-family: monospace;" v-if="appointments && appointments.length > 0"> <table class="table" style="font-family: monospace;" v-if="appointments && appointments.length > 0">
<thead> <thead>
<tr><th>Time</th><th>Clinic</th><th>Patient</th><th>Note</th><th>Assignee</th></tr> <tr><th>Time</th><th>Clinic</th><th>Patient</th><th>Note</th><th style="width: 16rem;">Assignee</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="row in appointments" :style="{ backgroundColor: strHashHSL(row.Clinic, '90%') }"> <tr v-for="row in appointments" :style="{ backgroundColor: strHashHSL(row.Clinic, '90%') }">
@ -10,7 +10,7 @@
<td v-if="production"><router-link :to="'/patient/$' + row.HRN">{{row.Name}} <span :title="row.HRN">{{row.HRN.slice(-4)}}</span></router-link></td> <td v-if="production"><router-link :to="'/patient/$' + row.HRN">{{row.Name}} <span :title="row.HRN">{{row.HRN.slice(-4)}}</span></router-link></td>
<td v-else><router-link :title="strtr_unscramble(row.Name)" :to="'/patient/$' + row.Name.charAt(0) + row.HRN.slice(-4) + '?name=' + row.Name">{{row.Name}} ${{row.HRN}}</router-link></td> <td v-else><router-link :title="strtr_unscramble(row.Name)" :to="'/patient/$' + row.Name.charAt(0) + row.HRN.slice(-4) + '?name=' + row.Name">{{row.Name}} ${{row.HRN}}</router-link></td>
<td>{{row.NOTE}} [{{row.APPT_MADE_BY}} on {{row.DATE_APPT_MADE}}]</td> <td>{{row.NOTE}} [{{row.APPT_MADE_BY}} on {{row.DATE_APPT_MADE}}]</td>
<td><Autocomplete :value="practitioner[row.Name]" @update:value="x => practitioner[row.Name] = x" :items="practitioner_list" /></td> <td><Autocomplete :modelValue="practitioner[row.Name]" @update:modelValue="x => practitioner[row.Name] = x" :items="practitioner_list" /></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -48,7 +48,7 @@
return this.client.remotestate.practitioner || (this.client.remotestate.practitioner = {}); return this.client.remotestate.practitioner || (this.client.remotestate.practitioner = {});
}, },
practitioner_list() { practitioner_list() {
return this.practitioner ? uniq(Object.values(this.practitioner)).sort() : []; return this.practitioner ? uniq(Object.values(this.practitioner).filter(x => x)).sort() : [];
} }
}, },
watch: { watch: {

54
htdocs/ViewVisits.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<table v-if="resultset.length > 0" class="table table-striped">
<thead>
<tr>
<th>Date/Time</th>
<th>Location</th>
<th style="text-align: right;">Status</th>
</tr>
</thead>
<tbody>
<tr v-for="item in resultset">
<td>{{item.timestr}} {{item.datestr}}</td>
<td>{{item.location}}</td>
<td style="text-align: right;">{{item.status}}</td>
</tr>
</tbody>
</table>
</template>
<script>
import { strftime_vista, strptime_vista } from './util.mjs';
export default {
props: {
client: Object,
dfn: String,
date_begin: Date,
date_end: Date
},
data() {
return {
resultset: []
};
},
computed: {
params() {
return { dfn: this.dfn, date_begin: strftime_vista(this.date_begin), date_end: strftime_vista(this.date_end) };
}
},
watch: {
async params(value) {
try {
this.resultset = (await this.client.ORWCV_VST(value.dfn, value.date_begin, value.date_end, '')).map(item => Object.assign({
datestr: strptime_vista(item.datetime).toLocaleDateString('sv-SE'),
timestr: strptime_vista(item.datetime).toLocaleTimeString('en-GB')
}, item));
} catch(ex) {
this.resultset = [];
console.warn(ex);
}
}
}
};
</script>

View File

@ -34,7 +34,7 @@
{ name: 'Coagulation', value: ['PT', 'INR', 'PTT'], selected: false }, { name: 'Coagulation', value: ['PT', 'INR', 'PTT'], selected: false },
{ name: 'Vitamins', value: ['FERRITIN', 'IRON', 'TIBC', 'B 12', 'FOLATE', 'VITAMIN D TOTAL 25-OH'], selected: false }, { name: 'Vitamins', value: ['FERRITIN', 'IRON', 'TIBC', 'B 12', 'FOLATE', 'VITAMIN D TOTAL 25-OH'], selected: false },
{ name: 'Thyroid', value: ['TSH', 'T4 (THYROXINE)'], selected: false }, { name: 'Thyroid', value: ['TSH', 'T4 (THYROXINE)'], selected: false },
{ name: 'Myeloma', value: ['PROTEIN,TOT SER (LC)', 'ALBUMIN [for SPEP](LC)', 'ALPHA-1 GLOBULIN S (LC)', 'ALPHA-2 GLOBULIN S (LC)', 'BETA GLOBULIN S (LC)', 'GAMMA GLOBULIN S (LC)', 'GLOBULIN,TOTAL S (LC)', 'A/G RATIO S (LC)', 'M-SPIKE S (LC)', 'IMMUNOFIXATION SERUM (LC)', 'FREE KAPPA LT CHAIN, S (LC)', 'FREE LAMBDA LT CHAIN, S (LC)', 'KAPPA/LAMBDA RATIO, S (LC)', 'KLRATIO', 'IMMUNOGLOBULIN G,QN (LC)', 'IMMUNOGLOBULIN A,QN (LC)', 'IMMUNOGLOBULIN M,QN (LC)', 'IGG', 'IGA', 'IGM', 'ALBUMIN [for RAND UR](LC)', 'ALPHA-1 GLOB RAND UR(LC)', 'ALPHA-2 GLOB RAND UR(LC)', 'BETA GLOB RAND UR(LC)', 'GAMMA GLOB RAND UR(LC)', 'M-SPIKE% RAND UR(LC)', 'PROTEIN,TOT UR(LC)', 'FKLCUR', 'FLLCUR', 'KAPPA/LAMBDA RATIO, UR (LC)', 'PROTEIN,24H CALC(LC)', 'ALBUMIN [for 24UPEP](LC)', 'ALPHA-1 GLOBULIN 24H(LC)', 'ALPHA-2 GLOBULIN 24H(LC)', 'BETA GLOBULIN 24H(LC)', 'GAMMA GLOBULIN 24H(LC)', 'M-SPIKE mg/24hr(LC)', 'FR KAPPA LTCH', 'FR LAMBDA LTCH'], selected: false } { name: 'Myeloma', value: ['PROTEIN,TOT SER (LC)', 'ALBUMIN [for SPEP](LC)', 'ALPHA-1 GLOBULIN S (LC)', 'ALPHA-2 GLOBULIN S (LC)', 'BETA GLOBULIN S (LC)', 'GAMMA GLOBULIN S (LC)', 'GLOBULIN,TOTAL S (LC)', 'A/G RATIO S (LC)', 'M-SPIKE S (LC)', 'IMMUNOFIXATION SERUM (LC)', 'FREE KAPPA LT CHAIN, S (LC)', 'FREE LAMBDA LT CHAIN, S (LC)', 'KAPPA/LAMBDA RATIO, S (LC)', 'KLRATIO', 'IMMUNOGLOBULIN G,QN (LC)', 'IMMUNOGLOBULIN A,QN (LC)', 'IMMUNOGLOBULIN M,QN (LC)', 'IGG', 'IGA', 'IGM', 'ALBUMIN [for RAND UR](LC):U', 'ALPHA-1 GLOB RAND UR(LC):U', 'ALPHA-2 GLOB RAND UR(LC):U', 'BETA GLOB RAND UR(LC):U', 'GAMMA GLOB RAND UR(LC):U', 'M-SPIKE% RAND UR(LC):U', 'PROTEIN,TOT UR(LC):U', 'FKLCUR:U', 'FLLCUR:U', 'KAPPA/LAMBDA RATIO, UR (LC):U', 'KLRATIO:U', 'PROTEIN,24H CALC(LC):U', 'ALBUMIN [for 24UPEP](LC):U', 'ALPHA-1 GLOBULIN 24H(LC):U', 'ALPHA-2 GLOBULIN 24H(LC):U', 'BETA GLOBULIN 24H(LC):U', 'GAMMA GLOBULIN 24H(LC):U', 'M-SPIKE% 24H(LC):U', 'M-SPIKE mg/24hr(LC):U', 'FR KAPPA LTCH:U', 'FR LAMBDA LTCH:U'], selected: false }
]; ];
reports.reduce((acc, x) => acc[x] = x, reports); reports.reduce((acc, x) => acc[x] = x, reports);

46
htdocs/constants.mjs Normal file
View File

@ -0,0 +1,46 @@
// Ordering Dialog Form IDs
export const OD_ACTIVITY = 100;
export const OD_ALLERGY = 105;
export const OD_CONSULT = 110;
export const OD_PROCEDURE = 112;
export const OD_DIET_TXT = 115;
export const OD_DIET = 117;
export const OD_LAB = 120;
export const OD_AP = 121;
export const OD_BB = 125;
export const OD_MEDINPT = 130;
export const OD_MEDS = 135;
export const OD_MEDOUTPT = 140;
export const OD_MEDNONVA = 145;
export const OD_NURSING = 150;
export const OD_MISC = 151;
export const OD_GENERIC = 152;
export const OD_IMAGING = 160;
export const OD_VITALS = 171; // use 170 for ORWD GENERIC VITALS, 171 for GMRVOR
export const OD_RTC = 175;
export const OD_MEDIV = 180;
export const OD_TEXTONLY = 999;
export const OM_NAV = 1001;
export const OM_QUICK = 1002;
export const OM_TABBED = 1003;
export const OM_TREE = 1004;
export const OM_ALLERGY = 1105;
export const OM_HTML = 1200;
export const OD_AUTOACK = 9999;
export const OD_CLINICMED = 1444;
export const OD_CLINICINF = 1555;
export const ORDERABLE_DGNM = [
{ dgnm: 'RX', name: 'Medication' },
{ dgnm: 'UD RX', name: 'Inpatient' },
{ dgnm: 'O RX', name: 'Outpatient' },
{ dgnm: 'NURS', name: 'Nursing' },
{ dgnm: 'DO', name: 'Diet' },
{ dgnm: 'LAB', name: 'Laboratory' },
{ dgnm: 'XRAY', name: 'Imaging' },
{ dgnm: 'CSLT', name: 'Consult' },
{ dgnm: 'PROC', name: 'Procedure' },
{ dgnm: 'NV RX', name: 'Non-VA' },
];
export const ORDERABLE_XREF = ORDERABLE_DGNM.map(x => { return { xref: 'S.' + x.dgnm, name: x.name }; });

86
htdocs/fmdatetime.mjs Normal file
View File

@ -0,0 +1,86 @@
const re_dt_fileman = /(?<dt_fileman>(\d{3})(\d{2})(\d{2})(?:\.(\d{2})?(\d{2})?(\d{2})?)?)/i; // George Timson's format
const re_dt_today = /(?<dt_today>TODAY|T)/i; // today
const re_dt_now = /(?<dt_now>NOW|N)/i; // now
const re_dt_mdy = /(?<dt_mdy>(\d{1,2})[^\w@?]+(\d{1,2})[^\w@?]+(\d{4}|\d{2})\s*)/i; // m/d/yy, m/d/yyyy
const re_dt_ymd = /(?<dt_ymd>(\d{4})[^\w@?]+(\d{1,2})[^\w@?]+(\d{1,2})\s*)/i; // yyyy/m/d
const re_dt_yyyymmdd = /(?<dt_yyyymmdd>(\d{4})(\d{2})(\d{2}))/i; // yyyymmdd
const re_dt_Mdy = /(?<dt_Mdy>([A-Z]{3,})[^\w@?]+(\d{1,2})[^\w@?]+(\d{4}|\d{2})\s*)/i; // M/d/yy, M/d/yyyy
const re_dt_dMy = /(?<dt_dMy>(\d{1,2})[^\w@?]+([A-Z]{3,})[^\w@?]+(\d{4}|\d{2})\s*)/i; // d/M/yy, d/M/yyyy
const re_dt_md = /(?<dt_md>(\d{1,2})[^\w@?]+(\d{1,2})\s*)/i; // m/d
const re_dt_offset = /(?<offset>([-+]\d+)(H|W|M)?)/i; // +#U
const re_dt_time = /(?:\s?@?(?<time>(\d{1,2})\:?(\d{1,2})(?:\:?(\d{1,2}))?))/i; // time
const re_dt_ext = /(?<ext>[<>])/i; // (nonstandard extension)
const rx_dt = new RegExp(`^${re_dt_fileman.source}\$|^(?:(?:${re_dt_today.source}|${re_dt_now.source}|${re_dt_mdy.source}|${re_dt_ymd.source}|${re_dt_yyyymmdd.source}|${re_dt_Mdy.source}|${re_dt_dMy.source}|${re_dt_md.source})?${re_dt_offset.source}?${re_dt_time.source}?${re_dt_ext.source}?)\$`, 'i');
export function validtime(s) {
return rx_dt.test(s);
}
export function strptime(s) {
// Parse VistA-style datetime strings into Date objects
var m, m1;
if(m = rx_dt.exec(s.replace(/^\s+|\s+$/g, '').toLowerCase())) {
m = m.groups;
if(m.dt_fileman) {
m1 = re_dt_fileman.exec(m.dt_fileman);
return new Date(1700 + +m1[2], +m1[3] - 1, +m1[4], +m1[5] || 0, +m1[6] || 0, +m1[7] || 0);
}
var date = new Date(); date.setHours(0, 0, 0, 0);
if(m.dt_today) ;
else if(m.dt_now) date = new Date();
else if(m.dt_mdy) date.setFullYear(strptime_year(+(m1 = re_dt_mdy.exec(m.dt_mdy))[4], date), +m1[2] - 1, +m1[3]);
else if(m.dt_ymd) date.setFullYear(+(m1 = re_dt_ymd.exec(m.dt_ymd))[2], +m1[3] - 1, +m1[4]);
else if(m.dt_yyyymmdd) date.setFullYear(+(m1 = re_dt_yyyymmdd.exec(m.dt_yyyymmdd))[2], +m1[3] - 1, +m1[4]);
else if(m.dt_Mdy) date.setFullYear(strptime_year(+(m1 = re_dt_Mdy.exec(m.dt_Mdy))[4], date), strptime_month(m1[2]) - 1, +m1[3]);
else if(m.dt_dMy) date.setFullYear(strptime_year(+(m1 = re_dt_dMy.exec(m.dt_dMy))[4], date), strptime_month(m1[3]) - 1, +m1[2]);
else if(m.dt_md) { date.setMonth(+(m1 = re_dt_md.exec(m.dt_md))[2] - 1); date.setDate(+m1[3]); }
if(m.time) {
if(m.dt_now) throw 'cannot specify NOW with time';
date.setHours(+(m1 = re_dt_time.exec(m.time))[2], +m1[3], +m1[4] || 0);
}
if(m.offset) {
m1 = re_dt_offset.exec(m.offset);
if((m1[3] == 'h') && ((m.time) || (m.dt_today))) throw 'cannot specify time or TODAY with H offset'
date = strptime_offset(date, +m1[2], m1[3] || 'd');
}
if(m.ext) {
if(m.ext == '<') date.setHours(0, 0, 0, 0);
else if(m.ext == '>') date.setHours(23, 59, 59, 999);
}
if(date.year < 1800) throw 'cannot specify year before 1800';
return date;
}
}
function strptime_year(y, today) {
// Promote years to 4 digits
return y >= 1000 ? y : (y + 2000) < (today.getFullYear() + 20) ? (y + 2000) : (y + 1900);
}
const strptime_month_mapping = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12};
function strptime_month(m) {
// Convert en-US month names to integers
return strptime_month_mapping[m.substring(0, 3)];
}
const strptime_offset_mapping = {'h': 60*60*1000, 'd': 24*60*60*1000, 'w': 7*24*60*60*1000, 'm': null};
function strptime_offset(base, offset, suffix) {
// Apply datetime offset
return suffix != 'm' ? new Date(base.getTime() + offset*strptime_offset_mapping[suffix]) : (base = new Date(base), base.setMonth(base.getMonth() + offset), base);
}
export function strftime(date) {
// Convert Date objects into conventional FileMan/Timson format
var d = '' + (date.getFullYear() - 1700) + ('00' + (date.getMonth() + 1)).slice(-2) + ('00' + date.getDate()).slice(-2);
var t = '' + ('00' + date.getHours()).slice(-2) + ('00' + date.getMinutes()).slice(-2) + ('00' + date.getSeconds()).slice(-2);
return t == '000000' ? d : (d + '.' + t);
}
export function datefloat(date) {
// Convert Python datetime.datetime objects into floating point FileMan/Timson format
var d = 10000*(date.getFullYear() - 1700) + 100*(date.getMonth() + 1) + date.getDate();
var t = date.getHours()/100 + date.getMinutes()/10000 + date.getSeconds()/1000000 + date.getMilliseconds()/1000000000;
return t == 0.0 ? d : (d + t);
}
export default window.fmdatetime = { validtime, strptime, strftime, datefloat };

56
htdocs/numberwords.mjs Normal file
View File

@ -0,0 +1,56 @@
function pos_int2words(n,
ones=['' ,'one' ,'two' ,'three' ,'four' ,'five' ,'six' ,'seven' ,'eight' ,'nine'],
teens=['ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'],
tens=['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']) {
if(n < 10) return ones[n];
else if(n < 20) return teens[n - 10];
else if(n < 100) return tens[Math.floor(n/10)] + (n%10 > 0 ? '-' + ones[n%10] : '');
else if(n < 1000) return ones[Math.floor(n/100)] + ' hundred ' + pos_int2words(n%100);
else if(n < 1000000) return pos_int2words(Math.floor(n/1000)) + ' thousand ' + pos_int2words(n%1000);
else return pos_int2words(Math.floor(n/1000000)) + ' million ' + pos_int2words(n%1000000);
}
export function int2words(n,
ones=['' ,'one' ,'two' ,'three' ,'four' ,'five' ,'six' ,'seven' ,'eight' ,'nine'],
teens=['ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'],
tens=['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']) {
if(n == 0) return 'zero';
else if(n < 0) return 'negative ' + pos_int2words(n, ones, teens, tens).replace(/\s+$/, '');
else return pos_int2words(n, ones, teens, tens).replace(/\s+$/, '');
}
export function cardinal2ordinal(s, re_magnitudes=/(hundred|thousand|(m|b|tr|quadr)illion)$/, re_low=/(zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)$/, ordinals={ zero: 'zeroth', one: 'first', two: 'second', three: 'third', four: 'fourth', five: 'fifth', six: 'sixth', seven: 'seventh', eight: 'eighth', nine: 'ninth', ten: 'tenth', eleven: 'eleventh', twelve: 'twelfth' }) {
if((s.slice(-4) == 'teen') || (re_magnitudes.test(s))) return s + 'th';
else if(s.charAt(s.length - 1) == 'y') return s.substring(0, s.length - 1) + 'ieth';
else return s.replace(re_low, (m, w) => ordinals[w]);
}
export function float2fraction(x, epsilon=0.0001) {
if(x == 0) return [0, 1];
const a = Math.abs(x);
let n = 0, d = 1, r;
while(Math.abs((r = n/d) - a)/a >= epsilon) if(r < a) n++;
else d++;
return [x < 0 ? -n : n, d];
}
function pos_fraction2words(x, epsilon=0.0001) {
const frac = float2fraction(x, epsilon), n = frac[0], d = frac[1];
if(d == 1) return int2words(x);
else if(d == 2) return int2words(n) + (n >= 2 ? ' halves' : ' half');
else if(d == 4) return int2words(n) + (n >= 2 ? ' quarters' : ' quarter');
else return int2words(n) + ' ' + cardinal2ordinal(int2words(d)) + (n >= 2 ? 's' : '');
}
export function num2words(n, epsilon=0.0001) {
if(n == 0) return 'zero';
else if(n < 0) return 'negative ' + num2words(-n);
var intpart = n|0;
if(n == intpart) return int2words(n);
else if(intpart == 0) return pos_fraction2words(n, epsilon);
else return num2words(intpart) + ' and ' + pos_fraction2words(n%1, epsilon);
}
export function translate(s, epsilon=0.0001) {
return ('' + s).replace(/([+-]?(?:\d+|\d+\.\d+))/g, (m, w) => num2words(w, epsilon));
}

View File

@ -53,7 +53,7 @@ function lab_parse1default(data) {
else x.comment = [line.substring(12)]; else x.comment = [line.substring(12)];
} else console.log('DANGLING:', line); } else console.log('DANGLING:', line);
} else if(m = line.match(/^\b(?<name>.*?)\s{2,}(?<value>.*?)(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/)) { } else if(m = line.match(/^\b(?<name>.*?)\s{2,}(?<value>.*?)(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/)) {
if(x = line.match(/^\b(?<name>.*?)(?<value>(?:positive|negative|reactive|not detected|collected - specimen in lab|test not performed))(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/i)) m = x; if(x = line.match(/^\b(?<name>.*?)(?<value>(?:positive|negative|reactive|nonreactive|not detected|collected - specimen in lab|test not performed))(?: (?<flag>L\*|L|H\*|H))?\s+(?:(?<unit>.{10}) (?<range>.*?)(?: \[(?<site>\d+)\])?)?$/i)) m = x;
if((m.groups.range) && (m.groups.range.startsWith('Ref: '))) m.groups.range = m.groups.range.substring(5); if((m.groups.range) && (m.groups.range.startsWith('Ref: '))) m.groups.range = m.groups.range.substring(5);
results.push(x = m.groups); results.push(x = m.groups);
if((x.value === '') && (m = x.name.match(/^(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|not detected|collected - specimen in lab|test not performed))\s*$/i))) { if((x.value === '') && (m = x.name.match(/^(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|not detected|collected - specimen in lab|test not performed))\s*$/i))) {
@ -61,7 +61,7 @@ function lab_parse1default(data) {
x.value = m.groups.value; x.value = m.groups.value;
} }
for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined; for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined;
} else if(m = line.match(/^\b(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|not detected|collected - specimen in lab|test not performed))\s*$/i)) { } else if(m = line.match(/^\b(?<name>.*?)(?<value>(?:[\d\.]+|positive|negative|reactive|nonreactive|not detected|collected - specimen in lab|test not performed))\s*$/i)) {
results.push(x = m.groups); results.push(x = m.groups);
for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined; for(var k in x) if(x[k]) x[k] = x[k] ? x[k].replace(/^\s+|\s+$/g, '') : undefined;
} else if(line.startsWith(' [')) { } else if(line.startsWith(' [')) {
@ -88,7 +88,7 @@ function lab_parse1default(data) {
results[(x = results[i]).name] = x; results[(x = results[i]).name] = x;
if(x.comment) x.comment = x.comment.join('\n'); if(x.comment) x.comment = x.comment.join('\n');
} }
if(res.accession.startsWith('HE ')) { if((res.accession) && (res.accession.startsWith('HE '))) {
if((results.hasOwnProperty('SEGS')) || (results.hasOwnProperty('BANDS'))) { if((results.hasOwnProperty('SEGS')) || (results.hasOwnProperty('BANDS'))) {
results.push(results['NEUTROPHIL%'] = { results.push(results['NEUTROPHIL%'] = {
name: 'NEUTROPHIL%', unit: '%', range: '42.2 - 75.2', name: 'NEUTROPHIL%', unit: '%', range: '42.2 - 75.2',
@ -181,7 +181,7 @@ export function measurement_parse(data) {
return res; return res;
} }
export function order_parse(data) { export function orderinfo_parse(data) {
var res = [], item, line; var res = [], item, line;
for(var i = 0; i < data.length; ++i) { for(var i = 0; i < data.length; ++i) {
if((line = data[i]).startsWith('~')) { if((line = data[i]).startsWith('~')) {
@ -213,3 +213,35 @@ export function order_parse(data) {
} }
return res; return res;
} }
export function orderoverrides_parse(data) {
var res = [], item, line;
for(var i = 0; i < data.length; ++i) {
if((line = data[i]).startsWith('~')) {
res.push(item = line.substring(1).split('^'));
item.promptIEN = item[0];
item.instance = item[1];
item.promptID = item[2];
} else if(item) {
if(line.startsWith('i')) item.iValue = line.substring(1);
else if(line.startsWith('e')) item.eValue = line.substring(1);
else if(line.startsWith('t')) {
item.eValue = (item.hasOwnProperty('eValue')) && (item.eValue.length > 0) ? item.eValue + '\r\n' + line.substring(1) : line.substring(1);
item.iValue = '^WP^';
} else console.log('INVALID:', line);
} else console.log('INVALID:', line);
}
return res;
}
export function orderoptions_parse(data) {
var res = {}, item, type, line;
for(var i = 0; i < data.length; ++i) {
if((line = data[i]).startsWith('~')) item = res[line.substring(1)] = {};
else if(item) {
type = { d: 'default', t: 'text', i: 'items' }[line.charAt(0)];
item[type] = (item.hasOwnProperty(type)) && (item[type].length > 0) ? item[type] + '\r\n' + line.substring(1) : line.substring(1);
} else console.log('INVALID:', line);
}
return res;
}

View File

@ -3,7 +3,7 @@ import { reactive, watch } from 'vue';
import vista from './vista.mjs'; import vista from './vista.mjs';
import cookie from './cookie.mjs'; import cookie from './cookie.mjs';
import { debounce } from './util.mjs'; import { debounce } from './util.mjs';
import { lab_parse, lab_reparse_results, measurement_parse, order_parse } from './reportparser.mjs'; import { lab_parse, lab_reparse_results, measurement_parse, orderinfo_parse, orderoverrides_parse, orderoptions_parse } from './reportparser.mjs';
import { TplFS, EncFS, randpassword as tplfs_randpassword } from './tplfs.mjs'; import { TplFS, EncFS, randpassword as tplfs_randpassword } from './tplfs.mjs';
export const localstate = reactive(cookie.get('state') ? JSON.parse(cookie.get('state')) : {}); export const localstate = reactive(cookie.get('state') ? JSON.parse(cookie.get('state')) : {});
@ -65,6 +65,20 @@ export function parsed_text(fn) {
} }
} }
function parse_caretseparated(rows, columns) {
return rows.map(function(row) {
row = row.split('^');
for(var i = columns.length - 1; i >= 0; --i) if(columns[i]) row[columns[i]] = row[i];
return row;
});
}
function parse_caretseparated1(row, columns) {
var res = row.split('^');
if(columns) for(var i = columns.length - 1; i >= 0; --i) if(columns[i]) res[columns[i]] = res[i];
return res;
}
export function caretseparated(fn, columns=null) { export function caretseparated(fn, columns=null) {
return async function(...args) { return async function(...args) {
if(columns) return (await fn(...args)).map(function(row) { if(columns) return (await fn(...args)).map(function(row) {
@ -84,21 +98,169 @@ export function caretseparated1(fn, columns=null) {
} }
} }
function parsed_caretseparated_detail(fn, columns, detailcolumn) {
if(!columns) columns = [];
if(!detailcolumn) detailcolumn = 'detail';
return columns ? async function(...args) {
var res = [], item = {}, rows = await fn(...args);
for(var i = 0; i < rows.length; ++i) {
var row = rows[i], prefix = row.charAt(0);
if(prefix == '~') {
item = row.substring(1).split('^');
for(var j = columns.length - 1; j >= 0; --j) if(columns[j]) item[columns[j]] = item[j];
res.push(item);
} else if(prefix == 't') {
if(item[detailcolumn]) item[detailcolumn] += '\r\n' + rows[i].substring(1);
else item[detailcolumn] = rows[i].substring(1);
}
}
return res;
} : async function(...args) {
var res = [], item = {}, rows = await fn(...args);
for(var i = 0; i < rows.length; ++i) {
var row = rows[i], prefix = row.charAt(0);
if(prefix == '~') res.push(item = row.substring(1).split('^'));
else if(prefix == 't') {
if(item[detailcolumn]) item[detailcolumn] += '\r\n' + rows[i].substring(1);
else item[detailcolumn] = rows[i].substring(1);
}
}
return res;
}
}
export function sliced(fn, start, end) { export function sliced(fn, start, end) {
return async function(...args) { return async function(...args) {
return (await fn(...args)).slice(start, end); return (await fn(...args)).slice(start, end);
} }
} }
export function mapped(fn, id='id') {
if(typeof id === 'function') return async function(...args) {
var res = await fn(...args);
for(var i = res.length - 1; i >= 0; --i) res[id(res[i])] = res[i];
return res;
};
else return async function(...args) {
var res = await fn(...args);
for(var i = res.length - 1; i >= 0; --i) res[res[i][id]] = res[i];
return res;
};
}
export function labreportparsed(fn) { export function labreportparsed(fn) {
return async function(...args) { return async function(...args) {
return lab_parse(await fn(...args)); return lab_parse(await fn(...args));
} }
} }
export function orderparsed(fn) { const parsed_orderdialogs_columns = ['IEN', 'windowFormId', 'displayGroupId', 'type', 'displayText'];
export function parsed_orderdialogs(fn, columns=parsed_orderdialogs_columns) {
return async function(...args) { return async function(...args) {
return order_parse(await fn(...args)); return (await fn(...args)).map(function(row) {
row = row.split('^');
row = [...row[0].split(';'), row[1]];
for(var i = columns.length - 1; i >= 0; --i) if(columns[i]) row[columns[i]] = row[i];
return row;
});
}
}
export function parsed_orderoverrides(fn) {
return async function(...args) {
return orderoverrides_parse(await fn(...args));
}
}
export function parsed_ordermenu(fn) {
return async function(...args) {
var resultset = await fn(...args);
var res = parse_caretseparated1(resultset[0], ['name', 'columns', 'path_switch']);
res.children = parse_caretseparated(resultset.slice(1), ['col', 'row', 'type', 'IEN', 'formid', 'autoaccept', 'display_text', 'mnemonic', 'displayonly']);
return res;
}
}
export function parsed_orderinfo(fn) {
return async function(...args) {
return orderinfo_parse(await fn(...args));
}
}
export function parsed_orderoptions_scheduling(fn) {
return async function(...args) {
var res = orderoptions_parse(await fn(...args));
for(var k in res) if(res.hasOwnProperty(k)) {
if(res[k].items) res[k].items = res[k].items.split('^');
res['~' + k.toUpperCase()] = res[k];
}
return res;
}
}
export function parsed_orderoptions_labfacility(fn) {
return async function(...args) {
var res = orderoptions_parse(await fn(...args)), val, defaultvalue;
for(var k in res) if(res.hasOwnProperty(k)) {
val = res[k];
if(val.default) {
val.default = parse_caretseparated1(val.default, ['value', 'text']);
defaultvalue = val.default.value;
} else defaultvalue = null;
if(val.items) val.items = val.items.split('\r\n').map(x => x ? (x = parse_caretseparated1(x, ['value', 'text']), x.default = x.value == defaultvalue, x) : null);
res['~' + k.toUpperCase()] = val;
}
return res;
}
}
export function parsed_orderoptions_labtest(fn) {
return async function(...args) {
var res = orderoptions_parse(await fn(...args)), val, defaultvalue;
if(res.hasOwnProperty('Test Name')) res['Test Name'].default = res['Test Name'].default.replace(/^\s+|\s+$/, '');
if(res.hasOwnProperty('Item ID')) res['Item ID'].default = parse_caretseparated1(res['Item ID'].default, ['value', 'text']);
if(res.hasOwnProperty('ReqCom')) res['ReqCom'].default = res['ReqCom'].default.replace(/^\s+|\s+$/, '');
if(res.hasOwnProperty('CollSamp')) res['CollSamp'].items = res['CollSamp'].items.split('\r\n').map(x => parse_caretseparated1(x, ['n', 'SampIEN', 'SampName', 'SpecPtr', 'TubeTop', 'unk_5', 'unk_6', 'LabCollect', 'unk_8', 'SpecName']));
res['Derived CollSamp'] = res['Unique CollSamp'] || res['Lab CollSamp'] || res['Default CollSamp'];
if(res.hasOwnProperty('Specimens')) res['Specimens'].items = res['Specimens'].items.split('\r\n').map(x => parse_caretseparated1(x, ['value', 'text']));
if(res.hasOwnProperty('Default Urgency')) res['Default Urgency'].default = res['Default Urgency'].default.split('\r\n').map(x => parse_caretseparated1(x, ['value', 'text', 'x']));
if(res.hasOwnProperty('Urgencies')) res['Urgencies'].items = res['Urgencies'].items.split('\r\n').map(x => parse_caretseparated1(x, ['value', 'text']));
return res;
}
}
export function parsed_orderoptions_medfill(fn) {
return async function(...args) {
var res = orderoptions_parse(await fn(...args));
if(res.hasOwnProperty('Pickup')) {
if(res['Pickup'].default) res['Pickup'].default = parse_caretseparated1(res['Pickup'].default, ['value', 'text']);
if(res['Pickup'].items) res['Pickup'].items = parse_caretseparated(res['Pickup'].items.split('\r\n'), ['value', 'text']);
}
if(res.hasOwnProperty('Priority')) {
if(res['Priority'].default) res['Priority'].default = parse_caretseparated1(res['Priority'].default, ['value', 'text']);
if(res['Priority'].items) res['Priority'].items = parse_caretseparated(res['Priority'].items.split('\r\n'), ['value', 'text']);
}
if(res.hasOwnProperty('Refills')) {
if(res['Refills'].default) res['Refills'].default = parse_caretseparated1(res['Refills'].default, ['value', 'text']);
if(res['Refills'].items) res['Refills'].items = parse_caretseparated(res['Refills'].items.split('\r\n'), ['value', 'text']);
}
return res;
}
}
export function parsed_orderoptions_meddose(fn) {
return async function(...args) {
var res = orderoptions_parse(await fn(...args));
if(res.hasOwnProperty('AllDoses')) res['AllDoses'].items = parse_caretseparated(res['AllDoses'].items.split('\r\n'), ['text', 'id', 'dosefields']);
if(res.hasOwnProperty('Dispense')) res['Dispense'].items = parse_caretseparated(res['Dispense'].items.split('\r\n'), ['id', 'dose', 'unit', 'text', 'split']);
if(res.hasOwnProperty('Dosage')) res['Dosage'].items = parse_caretseparated(res['Dosage'].items.split('\r\n'), ['medication', '_', '_', 'value', 'text', 'tier', '_', 'form']);
if(res.hasOwnProperty('Indication')) res['Indication'].items = res['Indication'].items.split('\r\n');
if((res.hasOwnProperty('Medication')) && (res['Medication'].default)) res['Medication'].default = parse_caretseparated1(res['Medication'].default, ['value', 'text']);
if(res.hasOwnProperty('Route')) {
if(res['Route'].default) res['Route'].default = parse_caretseparated1(res['Route'].default, ['value', 'abbr']);
res['Route'].items = parse_caretseparated(res['Route'].items.split('\r\n'), ['value', 'text', 'abbr', 'sig', '_']);
}
return res;
} }
} }
@ -197,7 +359,7 @@ export function Client(cid, secret) {
this.ORWORDG_ALLTREE = memoized(caretseparated(unwrapped(logged(() => this.callctx(['OR CPRS GUI CHART'], 'ORWORDG_ALLTREE'), 'ORWORDG_ALLTREE')), ['ien', 'name', 'parent', 'has_children'])); this.ORWORDG_ALLTREE = memoized(caretseparated(unwrapped(logged(() => this.callctx(['OR CPRS GUI CHART'], 'ORWORDG_ALLTREE'), 'ORWORDG_ALLTREE')), ['ien', 'name', 'parent', 'has_children']));
this.ORWORDG_REVSTS = memoized(caretseparated(unwrapped(logged(() => this.callctx(['OR CPRS GUI CHART'], 'ORWORDG_REVSTS'), 'ORWORDG_REVSTS')), ['ien', 'name', 'parent', 'has_children'])); this.ORWORDG_REVSTS = memoized(caretseparated(unwrapped(logged(() => this.callctx(['OR CPRS GUI CHART'], 'ORWORDG_REVSTS'), 'ORWORDG_REVSTS')), ['ien', 'name', 'parent', 'has_children']));
this.ORWORR_AGET = memoized(caretseparated(sliced(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWORR_AGET', ...args), 'ORWORR_AGET')), 1), ['ifn', 'dgrp', 'time'])); this.ORWORR_AGET = memoized(caretseparated(sliced(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWORR_AGET', ...args), 'ORWORR_AGET')), 1), ['ifn', 'dgrp', 'time']));
this.ORWORR_GET4LST = memoized(orderparsed(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWORR_GET4LST', ...args), 'ORWORR_GET4LST')))); this.ORWORR_GET4LST = memoized(parsed_orderinfo(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWORR_GET4LST', ...args), 'ORWORR_GET4LST'))));
this.TIU_TEMPLATE_GETROOTS = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETROOTS', ...args), 'TIU_TEMPLATE_GETROOTS')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); this.TIU_TEMPLATE_GETROOTS = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETROOTS', ...args), 'TIU_TEMPLATE_GETROOTS')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']);
this.TIU_TEMPLATE_GETPROOT = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETPROOT', ...args), 'TIU_TEMPLATE_GETPROOT')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']); this.TIU_TEMPLATE_GETPROOT = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_GETPROOT', ...args), 'TIU_TEMPLATE_GETPROOT')), ['IEN', 'type', 'status', 'name', 'exclude_from_group_boilerplate', 'blank_lines', 'personal_owner', 'has_children_flag', 'dialog', 'display_only', 'first_line', 'one_item_only', 'hide_dialog_items', 'hide_tree_items', 'indent_items', 'reminder_dialog_ien', 'reminder_dialog_name', 'locked', 'com_object_pointer', 'com_object_parameter', 'link_pointer', 'reminder_dialog_patient_specific_value']);
@ -209,6 +371,37 @@ export function Client(cid, secret) {
this.TIU_TEMPLATE_LOCK = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_LOCK', ...args), 'TIU_TEMPLATE_LOCK')); this.TIU_TEMPLATE_LOCK = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_LOCK', ...args), 'TIU_TEMPLATE_LOCK'));
this.TIU_TEMPLATE_UNLOCK = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_UNLOCK', ...args), 'TIU_TEMPLATE_UNLOCK')); this.TIU_TEMPLATE_UNLOCK = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'TIU_TEMPLATE_UNLOCK', ...args), 'TIU_TEMPLATE_UNLOCK'));
this.ORWCV_VST = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWCV_VST', ...args), 'ORWCV_VST')), ['apptinfo', 'datetime', 'location', 'status']));
this.ORWU1_NEWLOC = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWU1_NEWLOC', ...args), 'ORWU1_NEWLOC')), ['IEN', 'name']));
this.ORWDX_DGNM = memoized(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_DGNM', ...args), 'ORWDX_DGNM')));
this.ORWDX_DGRP = memoized(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_DGRP', ...args), 'ORWDX_DGRP')));
this.ORWDX_WRLST = memoized(parsed_orderdialogs(unwrapped(logged(() => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_WRLST'), 'ORWDX_WRLST'))));
this.ORWDX_ORDITM = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_ORDITM', ...args), 'ORWDX_ORDITM')), ['IEN', 'synonym', 'name']));
this.ORWDX_DLGID = memoized(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_DLGID', ...args), 'ORWDX_DLGID')));
this.ORWDX_DLGDEF = memoized(mapped(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_DLGDEF', ...args), 'ORWDX_DLGDEF')), ['promptID', 'promptIEN', 'fmtSeq', 'fmtCode', 'omit', 'lead', 'trail', 'newLine', 'wrap', 'children', 'isChild']), 'promptID'));
this.ORWDX_LOADRSP = memoized(mapped(parsed_orderoverrides(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_LOADRSP', ...args), 'ORWDX_LOADRSP')), ['promptID', 'promptIEN', 'fmtSeq', 'fmtCode', 'omit', 'lead', 'trail', 'newLine', 'wrap', 'children', 'isChild']), 'promptID'));
this.ORWDX_SAVE = unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDX_SAVE', ...args), 'ORWDX_SAVE'));
this.ORWDXM_MENU = memoized(parsed_ordermenu(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM_MENU', ...args), 'ORWDXM_MENU'))));
this.ORWDXM_DLGNAME = memoized(caretseparated1(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM_DLGNAME', ...args), 'ORWDXM_DLGNAME')), ['InternalName', 'DisplayName', 'BaseDialogIEN', 'BaseDialogName']));
this.ORWDXM_PROMPTS = memoized(mapped(parsed_caretseparated_detail(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM_PROMPTS', ...args), 'ORWDXM_PROMPTS')), ['id', 'req', 'hid', 'prompt', 'type', 'domain', 'default', 'idflt', 'help']), 'id'));
this.ORWDXM1_BLDQRSP = caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDXM1_BLDQRSP', ...args), 'ORWDXM1_BLDQRSP')), ['QuickLevel', 'ResponseID', 'Dialog', 'Type', 'FormID', 'DGrpLST']);
this.ORWUL_FV4DG = memoized(caretseparated1(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWUL_FV4DG', ...args), 'ORWUL_FV4DG')), ['IEN', 'count']));
this.ORWUL_FVSUB = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWUL_FVSUB', ...args), 'ORWUL_FVSUB')), ['IEN', 'description']));
this.ORWUL_FVIDX = memoized(caretseparated1(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWUL_FVIDX', ...args), 'ORWUL_FVIDX')), ['index', 'description']));
this.ORWDSD1_ODSLCT = memoized(parsed_orderoptions_scheduling(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDSD1_ODSLCT', ...args), 'ORWDSD1_ODSLCT'))));
this.ORWDLR32_DEF = memoized(parsed_orderoptions_labfacility(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDLR32_DEF', ...args), 'ORWDLR32_DEF'))));
this.ORWDLR32_LOAD = memoized(parsed_orderoptions_labtest(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDLR32_LOAD', ...args), 'ORWDLR32_LOAD'))));
this.ORWDPS1_SCHALL = memoized(mapped(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDPS1_SCHALL', ...args), 'ORWDPS1_SCHALL')), ['value', 'text', '_', 'times']), 'value'));
this.ORWDPS1_ODSLCT = memoized(parsed_orderoptions_medfill(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDPS1_ODSLCT', ...args), 'ORWDPS1_ODSLCT'))));
this.ORWDPS2_OISLCT = memoized(parsed_orderoptions_meddose(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDPS2_OISLCT', ...args), 'ORWDPS2_OISLCT'))));
this.ORWDPS2_DAY2QTY = memoized(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDPS2_DAY2QTY', ...args), 'ORWDPS2_DAY2QTY')));
this.ORWDPS2_QTY2DAY = memoized(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWDPS2_QTY2DAY', ...args), 'ORWDPS2_QTY2DAY')));
return this; return this;
} }
Client._registry = {}; Client._registry = {};

View File

@ -31,8 +31,8 @@ class CacheProxyRPC(util.CacheProxy):
persistent = util.Store().memo persistent = util.Store().memo
if volatile is None: if volatile is None:
volatile = util.Store().memo volatile = util.Store().memo
self._cache(('__call__', 'close', 'authenticate', 'keepalive', 'XWB_CREATE_CONTEXT', 'XWB_IM_HERE', 'TIU_TEMPLATE_GETROOTS', 'TIU_TEMPLATE_GETPROOT', 'TIU_TEMPLATE_GETBOIL', 'TIU_TEMPLATE_GET_DESCRIPTION', 'TIU_TEMPLATE_GETITEMS', 'TIU_TEMPLATE_SET ITEMS', 'TIU_TEMPLATE_CREATE/MODIFY', 'TIU_TEMPLATE_DELETE', 'TIU_TEMPLATE_LOCK', 'TIU_TEMPLATE_UNLOCK'), None) self._cache(('__call__', 'close', 'authenticate', 'keepalive', 'XWB_CREATE_CONTEXT', 'XWB_IM_HERE', 'TIU_TEMPLATE_GETROOTS', 'TIU_TEMPLATE_GETPROOT', 'TIU_TEMPLATE_GETBOIL', 'TIU_TEMPLATE_GET_DESCRIPTION', 'TIU_TEMPLATE_GETITEMS', 'TIU_TEMPLATE_SET ITEMS', 'TIU_TEMPLATE_CREATE/MODIFY', 'TIU_TEMPLATE_DELETE', 'TIU_TEMPLATE_LOCK', 'TIU_TEMPLATE_UNLOCK', 'ORWDXM1_BLDQRSP'), None)
self._cache(('SDEC_RESOURCE', 'ORWLRR_ALLTESTS_ALL', 'ORWORDG_ALLTREE', 'ORWORDG_REVSTS'), persistent, prefix=prefix, ttl=float('inf')) self._cache(('SDEC_RESOURCE', 'ORWU1_NEWLOC', 'ORWLRR_ALLTESTS_ALL', 'ORWORDG_ALLTREE', 'ORWORDG_REVSTS', 'ORWDX_DGNM', 'ORWDX_ORDITM'), persistent, prefix=prefix, ttl=float('inf'))
self._cache(('XWB_GET_BROKER_INFO', 'XUS_INTRO_MSG'), volatile, prefix=prefix, ttl=float('inf')) self._cache(('XWB_GET_BROKER_INFO', 'XUS_INTRO_MSG'), volatile, prefix=prefix, ttl=float('inf'))
self._cache(None, volatile, prefix=prefix, ttl=float('-inf')) self._cache(None, volatile, prefix=prefix, ttl=float('-inf'))
def _cache_persistent(self, persistent=None, prefix=''): def _cache_persistent(self, persistent=None, prefix=''):
@ -156,6 +156,7 @@ def get_port():
if __name__ == '__main__': if __name__ == '__main__':
import webbrowser import webbrowser
app = application() app = application()
port = get_port() port = get_port()
print(f'http://localhost:{port}/#{app.secret}') print(f'http://localhost:{port}/#{app.secret}')

10
util.py
View File

@ -166,13 +166,13 @@ class SyncProxy(object):
return value return value
re_dt_fileman = r'(?P<dt_fileman>(\d{3})(\d{2})(\d{2})(?:\.(\d{2})?(\d{2})?(\d{2})?)?)' # George Timson's format re_dt_fileman = r'(?P<dt_fileman>(\d{3})(\d{2})(\d{2})(?:\.(\d{2})?(\d{2})?(\d{2})?)?)' # George Timson's format
re_dt_today = r'(?P<dt_today>T)' # today re_dt_today = r'(?P<dt_today>TODAY|T)' # today
re_dt_now = r'(?P<dt_now>N)' # now re_dt_now = r'(?P<dt_now>NOW|N)' # now
re_dt_mdy = r'(?P<dt_mdy>(\d{1,2})[^\w@?]+(\d{1,2})[^\w@?]+(\d{4}|\d{2})\s*)' # m/d/yy, m/d/yyyy re_dt_mdy = r'(?P<dt_mdy>(\d{1,2})[^\w@?]+(\d{1,2})[^\w@?]+(\d{4}|\d{2})\s*)' # m/d/yy, m/d/yyyy
re_dt_ymd = r'(?P<dt_ymd>(\d{4})[^\w@?]+(\d{1,2})[^\w@?]+(\d{1,2})\s*)' # yyyy/m/d re_dt_ymd = r'(?P<dt_ymd>(\d{4})[^\w@?]+(\d{1,2})[^\w@?]+(\d{1,2})\s*)' # yyyy/m/d
re_dt_yyyymmdd = r'(?P<dt_yyyymmdd>(\d{4})(\d{2})(\d{2}))' # yyyymmdd re_dt_yyyymmdd = r'(?P<dt_yyyymmdd>(\d{4})(\d{2})(\d{2}))' # yyyymmdd
re_dt_Mdy = r'(?P<dt_Mdy>([A-Z]{3,})[^\w@?]+(\d{1,2})[^\w@?]+(\d{4}|\d{2})\s*)' # M/d/yy, M/d/yyyy re_dt_Mdy = r'(?P<dt_Mdy>([A-Z]{3,})[^\w@?]+(\d{1,2})[^\w@?]+(\d{4}|\d{2})\s*)' # M/d/yy, M/d/yyyy
re_dt_dMy = r'(?P<dt_dMy>((\d{1,2})[^\w@?]+[A-Z]{3,})[^\w@?]+(\d{4}|\d{2})\s*)' # d/M/yy, d/M/yyyy re_dt_dMy = r'(?P<dt_dMy>(\d{1,2})[^\w@?]+([A-Z]{3,})[^\w@?]+(\d{4}|\d{2})\s*)' # d/M/yy, d/M/yyyy
re_dt_md = r'(?P<dt_md>(\d{1,2})[^\w@?]+(\d{1,2})\s*)' # m/d re_dt_md = r'(?P<dt_md>(\d{1,2})[^\w@?]+(\d{1,2})\s*)' # m/d
re_dt_offset = r'(?P<offset>([-+]\d+)(H|W|M)?)' # +#U re_dt_offset = r'(?P<offset>([-+]\d+)(H|W|M)?)' # +#U
re_dt_time = r'(?:@?(?P<time>(\d{1,2}):?(\d{1,2})))' # time re_dt_time = r'(?:@?(?P<time>(\d{1,2}):?(\d{1,2})))' # time
@ -197,13 +197,13 @@ def vista_strptime(s: str) -> datetime.datetime:
time = datetime.time() time = datetime.time()
if m['time']: if m['time']:
if m['dt_now']: if m['dt_now']:
raise ValueError('cannot specify time with N or H offset') raise ValueError('cannot specify NOW with time')
m1 = re.match(re_dt_time, m['time'], flags=re.I) m1 = re.match(re_dt_time, m['time'], flags=re.I)
date = date.replace(hour=int(m1.group(2)), minute=int(m1.group(3))) date = date.replace(hour=int(m1.group(2)), minute=int(m1.group(3)))
if m['offset']: if m['offset']:
m1 = re.match(re_dt_offset, m['offset'], flags=re.I) m1 = re.match(re_dt_offset, m['offset'], flags=re.I)
if (offset_unit := m1.group(3)) == 'h' and (m['time'] or m['dt_today']): if (offset_unit := m1.group(3)) == 'h' and (m['time'] or m['dt_today']):
raise ValueError('cannot specify time or T with H offset') raise ValueError('cannot specify time or TODAY with H offset')
date = vista_strptime_offset(date, int(m1.group(2)), offset_unit or 'd') date = vista_strptime_offset(date, int(m1.group(2)), offset_unit or 'd')
if m['ext']: if m['ext']:
if m['ext'] == '<': if m['ext'] == '<':