Compare commits
3 Commits
704942fd3a
...
85cefa1b7b
Author | SHA1 | Date | |
---|---|---|---|
85cefa1b7b | |||
b1038fb577 | |||
f64527122e |
@ -18,7 +18,7 @@
|
|||||||
import RoutePatientDetail from './RoutePatientDetail.vue';
|
import RoutePatientDetail from './RoutePatientDetail.vue';
|
||||||
import RoutePatientVisits from './RoutePatientVisits.vue';
|
import RoutePatientVisits from './RoutePatientVisits.vue';
|
||||||
import RoutePatientOrders from './RoutePatientOrders.vue';
|
import RoutePatientOrders from './RoutePatientOrders.vue';
|
||||||
import RouteScheduleOverview from './RouteScheduleOverview.vue';
|
import RoutePlanner from './RoutePlanner.vue';
|
||||||
import RouteRecall from './RouteRecall.vue';
|
import RouteRecall from './RouteRecall.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -48,7 +48,7 @@
|
|||||||
{ path: '/patient/:id', component: RoutePatientDetail },
|
{ path: '/patient/:id', component: RoutePatientDetail },
|
||||||
{ path: '/patient/:id/visits', component: RoutePatientVisits },
|
{ path: '/patient/:id/visits', component: RoutePatientVisits },
|
||||||
{ path: '/patient/:id/orders', component: RoutePatientOrders },
|
{ path: '/patient/:id/orders', component: RoutePatientOrders },
|
||||||
{ path: '/overview', component: RouteScheduleOverview },
|
{ path: '/planner', component: RoutePlanner },
|
||||||
{ 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);
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<router-link class="nav-link" to="/overview">Overview</router-link>
|
<router-link class="nav-link" to="/planner">Planner</router-link>
|
||||||
</li>
|
</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>
|
||||||
|
46
htdocs/RoutePlanner.vue
Normal file
46
htdocs/RoutePlanner.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card mb-3 shadow">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Planner</span>
|
||||||
|
<select v-if="(resourcelist_selected) && (resourcelist_selected.length > 0)" class="form-select form-select-sm" style="width: auto;" v-model="resource"><option v-for="row in resourcelist_selected" :value="row.RESOURCEID">{{row.RESOURCE_NAME}} #{{row.RESOURCEID}}</option></select>
|
||||||
|
<DateRangePicker range="1M" direction="1" v-model:date="date_begin" v-model:date_end="date_end" />
|
||||||
|
</div>
|
||||||
|
<div class="card-body"><ViewPlanner :client="client" :resource="resource" :date_begin="date_begin" :date_end="date_end" /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DateRangePicker from './DateRangePicker.vue';
|
||||||
|
import ViewPlanner from './ViewPlanner.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
DateRangePicker, ViewPlanner
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
client: Object
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
var now = new Date();
|
||||||
|
return {
|
||||||
|
resource: null,
|
||||||
|
date_begin: now,
|
||||||
|
date_end: now,
|
||||||
|
resourcelist_all: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resourcelist_selected() {
|
||||||
|
if((this.client) && (this.client.remotestate.resources) && (this.resourcelist_all)) {
|
||||||
|
var resourcemap = this.client.remotestate.resources.split(',').filter(x => x).reduce((acc, val) => (acc[val] = true, acc), {});
|
||||||
|
var res = this.resourcelist_all.filter(x => resourcemap.hasOwnProperty(x.RESOURCEID));
|
||||||
|
if((res.length > 0) && (!this.resource)) this.resource = res[0].RESOURCEID;
|
||||||
|
return res;
|
||||||
|
} else return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.resourcelist_all = await this.client.SDEC_RESOURCE();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,93 +0,0 @@
|
|||||||
<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>
|
|
128
htdocs/ViewPlanner.vue
Normal file
128
htdocs/ViewPlanner.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<table v-if="(resultset) && (resultset.length > 0)" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr><th v-for="x in dow">{{x}}</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="week in resultset">
|
||||||
|
<td v-for="day in [0, 1, 2, 3, 4, 5, 6]" class="datebox">
|
||||||
|
<template v-if="week.values[day]">
|
||||||
|
<div class="datebox" :style="{ backgroundColor: resultset.max > 0 ? 'rgba(220, 53, 69, ' + week.values[day].length/resultset.max + ')' : null }"><span class="occupancy hidden">#{{week.values[day].length}}</span> {{day > 0 ? week.values[day][0]._START_OBJ.getDate() : week.key.toLocaleDateString('sv-SE')}} <span class="occupancy">#{{week.values[day].length}}</span></div>
|
||||||
|
<template v-for="appointment in week.values[day]">
|
||||||
|
<div v-if="appointment._BREAK" class="vacancy" :title="appointment._START_OBJ.toLocaleTimeString('en-GB').substring(0, 5) + '–' + appointment._END_OBJ.toLocaleTimeString('en-GB').substring(0, 5)" />
|
||||||
|
<div v-else :title="appointment._START_OBJ.toLocaleTimeString('en-GB').substring(0, 5) + '–' + appointment._END_OBJ.toLocaleTimeString('en-GB').substring(0, 5) + '\n' + appointment.PATIENTNAME + ' ' + appointment.HRN.slice(-4) + '\n' + appointment.NOTE"><span v-if="appointment._CONCURRENCY > 0" class="concurrency hidden">*<template v-if="appointment._CONCURRENCY > 1">{{appointment._CONCURRENCY}}</template></span>{{appointment._START_OBJ.toLocaleTimeString('en-GB').substring(0, 5)}} {{appointment.PATIENTNAME.substring(0, 1)}}{{appointment.HRN.slice(-4)}}<span v-if="appointment._CONCURRENCY > 0" class="concurrency">*<template v-if="appointment._CONCURRENCY > 1">{{appointment._CONCURRENCY}}</template></span></div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<div v-else class="datebox">{{day > 0 ? (new Date(week.key.getTime() + 1000*60*60*24*day)).getDate() : week.key.toLocaleDateString('sv-SE')}}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
table {
|
||||||
|
font-family: monospace;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
tbody, td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
div.datebox {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
div.vacancy {
|
||||||
|
width: 2em;
|
||||||
|
margin: auto;
|
||||||
|
border-bottom: 0.25em dotted #bbb;
|
||||||
|
}
|
||||||
|
span.occupancy {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
span.concurrency {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
span.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { groupBy, groupByArray, strfdate_vista, debounce } from './util.mjs';
|
||||||
|
|
||||||
|
const C_DAY = 1000*60*60*24;
|
||||||
|
const C_WEEK = C_DAY*7;
|
||||||
|
|
||||||
|
function dateonly(date) {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
function infill_weeks(res, date_begin, date_end) {
|
||||||
|
for(var i = res.length - 1; i > 0; --i) {
|
||||||
|
var cur = res[i], prev = res[i - 1];
|
||||||
|
while(cur.key - prev.key > C_WEEK) res.splice(i, 0, cur = { key: new Date(cur.key.getTime() - C_WEEK), values: [] });
|
||||||
|
}
|
||||||
|
var item;
|
||||||
|
if(res.length < 1) res.push({ key: date_begin, values: [] });
|
||||||
|
else {
|
||||||
|
item = res[0];
|
||||||
|
while(item.key - date_begin >= C_WEEK) res.splice(0, 0, item = { key: new Date(item.key.getTime() - C_WEEK), values: [] });
|
||||||
|
}
|
||||||
|
item = res[res.length - 1];
|
||||||
|
while(date_end - item.key >= C_WEEK) res.push(item = { key: new Date(item.key.getTime() + C_WEEK), values: [] });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyze_week(res) {
|
||||||
|
for(var k in res) if(res.hasOwnProperty(k)) analyze_day(res[k]);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyze_day(res) {
|
||||||
|
for(var i = res.length - 1; i > 0; --i) {
|
||||||
|
var item = res[i];
|
||||||
|
item._CONCURRENCY = 0;
|
||||||
|
for(var j = i - 1; j >= 0; --j) if(item._START_OBJ < res[j]._END_OBJ) item._CONCURRENCY++;
|
||||||
|
if((item._CONCURRENCY < 1) && (item._START_OBJ > res[i - 1]._END_OBJ)) res.splice(i, 0, { _BREAK: true, _START_OBJ: res[i - 1]._END_OBJ, _END_OBJ: item._START_OBJ });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
client: Object,
|
||||||
|
resource: String,
|
||||||
|
date_begin: Date,
|
||||||
|
date_end: Date
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
var now = new Date();
|
||||||
|
return {
|
||||||
|
resultset: [],
|
||||||
|
dow: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$watch(
|
||||||
|
() => (this.client, this.resource, this.date_begin, this.date_end, {}),
|
||||||
|
debounce(async () => {
|
||||||
|
if((this.client) && (this.resource) && (this.date_begin) && (this.date_end)) {
|
||||||
|
var date_begin = new Date(this.date_begin.getTime() - C_DAY*this.date_begin.getDay()), weekref = date_begin;
|
||||||
|
var date_end = new Date(this.date_end.getTime() + C_DAY*(6 - this.date_end.getDay()));
|
||||||
|
var resultset = (await this.client.SDEC_CRSCHED(this.resource, strfdate_vista(date_begin), strfdate_vista(date_end) + '@2359')).filter(x => (x.CANCELLED == '0') && (x.NOSHOW == '0')).map(x => {
|
||||||
|
var _START_OBJ = x._START_OBJ = new Date(x.START_TIME);
|
||||||
|
x._START_DATE = _START_OBJ.toLocaleDateString('sv-SE');
|
||||||
|
x._END_OBJ = new Date(x.END_TIME);
|
||||||
|
x._WEEK_DAY = _START_OBJ.getDay();
|
||||||
|
x._WEEK_NUM = Math.floor((x._START_OBJ - weekref)/(C_WEEK));
|
||||||
|
return x;
|
||||||
|
}).sort((a, b) => a._START_OBJ - b._START_OBJ);
|
||||||
|
this.resultset = infill_weeks(groupByArray(resultset, '_WEEK_NUM').map(week => ({ key: new Date(weekref.getTime() + C_WEEK*week.key), values: analyze_week(groupBy(week.values, '_WEEK_DAY')) })), date_begin, date_end);
|
||||||
|
this.resultset.max = Math.max.apply(null, groupByArray(resultset, '_START_DATE').map(x => x.values.length));
|
||||||
|
} else this.resultset = [];
|
||||||
|
}, 500),
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -364,10 +364,10 @@ export function Client(cid, secret) {
|
|||||||
this.ORQQVI_VITALS = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORQQVI_VITALS', ...args), 'ORQQVI_VITALS')), ['measurement_ien', 'type', 'value', 'datetime', 'value_american', 'value_metric']));
|
this.ORQQVI_VITALS = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORQQVI_VITALS', ...args), 'ORQQVI_VITALS')), ['measurement_ien', 'type', 'value', 'datetime', 'value_american', 'value_metric']));
|
||||||
this.ORQQVI_VITALS_FOR_DATE_RANGE = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORQQVI_VITALS_FOR_DATE_RANGE', ...args), 'ORQQVI_VITALS_FOR_DATE_RANGE')), ['measurement_ien', 'type', 'value', 'datetime']));
|
this.ORQQVI_VITALS_FOR_DATE_RANGE = memoized(caretseparated(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORQQVI_VITALS_FOR_DATE_RANGE', ...args), 'ORQQVI_VITALS_FOR_DATE_RANGE')), ['measurement_ien', 'type', 'value', 'datetime']));
|
||||||
|
|
||||||
this.GMV_EXTRACT_REC = memoized(async (dfn, oredt, orsdt) => measurement_parse(await unwrapped(logged((...args0) => this.callctx(['OR CPRS GUI CHART'], 'GMV_EXTRACT_REC', args0.join('^')), 'GMV_EXTRACT_REC'))(dfn, oredt, '', orsdt)));
|
this.GMV_EXTRACT_REC = async (dfn, oredt, orsdt) => measurement_parse(await unwrapped(logged((...args) => this.call({ method: 'GMV_EXTRACT_REC', context: ['OR CPRS GUI CHART'], ttl: 60}, args.join('^')), 'GMV_EXTRACT_REC'))(dfn, oredt, '', orsdt));
|
||||||
|
|
||||||
this.ORWLRR_INTERIM = memoized(labreportparsed(unwrapped(logged((...args) => this.callctx(['OR CPRS GUI CHART'], 'ORWLRR_INTERIM', ...args), 'ORWLRR_INTERIM'))));
|
this.ORWLRR_INTERIM = labreportparsed(unwrapped(logged((...args) => this.call({ method: 'ORWLRR_INTERIM', context: ['OR CPRS GUI CHART'], ttl: 60 }, ...args), 'ORWLRR_INTERIM')));
|
||||||
this.ORWLRR_INTERIM_RESULTS = memoized(async (...args) => lab_reparse_results(await this.ORWLRR_INTERIM(...args)));
|
this.ORWLRR_INTERIM_RESULTS = async (...args) => lab_reparse_results(await this.ORWLRR_INTERIM(...args));
|
||||||
|
|
||||||
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']));
|
||||||
|
Loading…
Reference in New Issue
Block a user