Compare commits
10 Commits
7be5ebcdaa
...
main
Author | SHA1 | Date | |
---|---|---|---|
c0d9cbfc42 | |||
d261d5200e | |||
9fe732e0e1 | |||
75620baeac | |||
ca975e589d | |||
b3067b86dc | |||
b6ba225e3d | |||
ed90f64076 | |||
12c075ef60 | |||
38e34e099a |
19
README.md
19
README.md
@ -1,3 +1,20 @@
|
||||
# vistassh-py
|
||||
|
||||
Python-based web interface for VistA roll-and-scroll terminal
|
||||
Python-based web interface for VistA roll-and-scroll terminal
|
||||
|
||||
## Getting started
|
||||
|
||||
You will need:
|
||||
- Python 3.10+ & pip
|
||||
- Node.js & npm
|
||||
|
||||
To install the dependencies and build the frontend, perform the following steps:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
To run, invoke `main.py`.
|
@ -9,7 +9,7 @@ import contextlib
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
from typing import Optional, Union, Sequence, NamedTuple, Callable
|
||||
from typing import Any, Optional, Union, Sequence, NamedTuple, Callable, AsyncGenerator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -28,7 +28,7 @@ class ExpectQ(object):
|
||||
"""Clear or restore buffer"""
|
||||
self.buffer = buffer
|
||||
clear = reset
|
||||
async def prompts(self, endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False):
|
||||
async def prompts(self, endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False) -> AsyncGenerator[tuple[Optional[str], Optional[int]], None]:
|
||||
len_endl = len(endl)
|
||||
while True:
|
||||
if (pos := self.buffer.rfind(endl)) >= 0:
|
||||
@ -43,7 +43,7 @@ class ExpectQ(object):
|
||||
if throw:
|
||||
raise
|
||||
yield None, None
|
||||
async def promptmatches(self, *mappings: Union[str, re.Pattern, tuple, list], endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False):
|
||||
async def promptmatches(self, *mappings: Union[str, re.Pattern, tuple, list], endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False) -> AsyncGenerator[tuple[Optional[ExpectMatch], Any], Optional[bool]]:
|
||||
for i, mapping in enumerate(mappings):
|
||||
try:
|
||||
match mapping:
|
||||
|
@ -26,5 +26,11 @@ async def cmd_listclinics(proc):
|
||||
proc.sendline('^')
|
||||
break
|
||||
proc.sendline('^Patient information AND OE/RR')
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
|
@ -74,7 +74,13 @@ async def cmd_reports(proc, mrn, alpha, omega):
|
||||
else:
|
||||
print(repr(before))
|
||||
assert False
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
text = re.sub(r'\r\n\s+>> CONTINUATION OF .+? <<(?:(?:\r\n)|(?:\s+page \d+))', '', '\r\n'.join(pages))
|
||||
positions = [m.start() for m in re.finditer(r'(?:(?:[ ]+----MICROBIOLOGY----[ ]+page \d+\r\n\r\n)|(?:[ ]+))Reporting Lab:', text)]
|
||||
|
@ -39,7 +39,13 @@ async def cmd_entries(proc, mrn, alpha, omega):
|
||||
proc.sendline(response)
|
||||
if prompt.index == 0 or prompt.index == 1:
|
||||
pages.append(re.sub(r'^\x1b\[H\x1b\[J\x1b\[2J\x1b\[H\r\n[^\r\n]+? Cumulative Vitals\/Measurements Report[ ]+Page \d+\r\n\r\n-{10,}\r\n(?:\d{2}\/\d{2}\/\d{2} \(continued\)\r\n\r\n)?|\r\n\r\n\*\*\*[^\r\n]+\r\n\r\n[^\r\n]+?VAF 10-7987j\r\nUnit:[^\r\n]+\r\nDivision:[^\r\n]+(?:\r\n)?$', '', prompt.before))
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
for m_date in re.finditer(r'^(?P<date>\d{2}\/\d{2}\/\d{2})\r\n(?P<body>.*?\r\n)(?:(?=\d{2}\/)|\r\n|$)', '\r\n'.join(pages), re.DOTALL|re.MULTILINE):
|
||||
g_date = m_date.group('date')
|
||||
|
33
ext_note.py
33
ext_note.py
@ -31,15 +31,24 @@ async def cmd_reports(proc, mrn, alpha, omega):
|
||||
proc.sendline(util.vista_strftime(omega))
|
||||
assert await expect.endswith('\r\n Thru: ')
|
||||
proc.sendline(util.vista_strftime(alpha))
|
||||
assert await expect.endswith('\r\nDo you want WORK copies or CHART copies? CHART// ')
|
||||
proc.sendline() # default CHART
|
||||
if await expect.endswith('\r\nDo you want to start each note on a new page? NO// '):
|
||||
proc.sendline() # default NO
|
||||
assert await expect.endswith('\r\nDEVICE: HOME// ')
|
||||
proc.sendline('HOME;;1023')
|
||||
assert await expect.earliest(' HOME(CRT)\r\n')
|
||||
found = True
|
||||
match await expect.endswith('\r\nDo you want WORK copies or CHART copies? CHART// ', '\r\nPrint Notes Beginning: '):
|
||||
case autoproc.ExpectMatch(index=0):
|
||||
proc.sendline() # default CHART
|
||||
if await expect.endswith('\r\nDo you want to start each note on a new page? NO// '):
|
||||
proc.sendline() # default NO
|
||||
assert await expect.endswith('\r\nDEVICE: HOME// ')
|
||||
proc.sendline('HOME;;1023')
|
||||
assert await expect.earliest(' HOME(CRT)\r\n')
|
||||
case autoproc.ExpectMatch(index=1):
|
||||
proc.sendline('^')
|
||||
assert await expect.endswith('\r\nSelect PATIENT NAME: ')
|
||||
proc.sendline('^')
|
||||
assert await expect.endswith('\r\nSelect Progress Notes Print Options Option: ')
|
||||
found = False
|
||||
case _: assert False
|
||||
pages = []
|
||||
while True:
|
||||
while found:
|
||||
match m_delimiter := await expect.endswith('\r\nType <Enter> to continue or \'^\' to exit: ', '\r\nSelect PATIENT NAME: '):
|
||||
case autoproc.ExpectMatch(index=0, before=before):
|
||||
if isnew(before) and len(pages) > 0:
|
||||
@ -59,5 +68,11 @@ async def cmd_reports(proc, mrn, alpha, omega):
|
||||
case _: assert False
|
||||
proc.sendline('^')
|
||||
proc.sendline('^Patient information AND OE/RR')
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
|
@ -77,7 +77,13 @@ async def cmd_entries(proc, mrn, alpha, omega):
|
||||
break
|
||||
case _: assert False
|
||||
proc.sendline('^Patient information AND OE/RR')
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
prev = None
|
||||
for m in re.finditer(r'\b\d{2}/\d{2}/\d{2}.*?\r\n\r\n', '\r\n'.join(pages).replace('\x1b[1m', '').replace('\x1b[m', ''), re.DOTALL):
|
||||
|
@ -25,7 +25,13 @@ async def cmd_lookup_patient(proc, query):
|
||||
res.append(prompt.before[:-24] if prompt.index == 0 and prompt.before.endswith('\r\nENTER \'^\' TO STOP, OR \r\n') else prompt.before)
|
||||
if 0 < prompt.index < 4:
|
||||
single = True
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
if single:
|
||||
return [re.search(r'[ ]{2}(?P<name>.+?)[ ]{2}(?:\((?P<alias>[^\)]*?)\))?[ ]{6}(?P<dob>\S+)[ ]{4}(?P<ssn>\S+(?:P \*\*Pseudo SSN\*\*)?)[ ]{5}(?P<yesno>\S+)[ ]{5}(?P<type>.+?)[ ]{6}(?P<no>[^\r\n]*)', res[0].replace('\r\n', '', 1)).groupdict()]
|
||||
@ -65,6 +71,12 @@ async def cmd_lookup_patient_ordinal(proc, query, ordinal, force=False):
|
||||
proc.sendline(response)
|
||||
case autoproc.ExpectMatch(index=5):
|
||||
proc.sendline()
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
return re.sub(r'\r\n\r\n(?:[^\r\n;]+);(?:\([^\)]*?\))? (?:\d+ )?(?:\d{3}-\d{2}-\d{4}P?) (?:[^\r\n]+?)[ ]*?(\r\n={10,}\r\n)\r\n', r'\1', '\r\n'.join(res))
|
||||
|
@ -58,7 +58,13 @@ async def cmd_patients(proc, alpha, omega):
|
||||
summary.extend({k.strip(): v.strip() for k, v in row.groupdict().items()} for row in re.finditer(r'(?P<last5>[A-Z]\d{4}) (?P<name>[^\r\n]{30}) (?P<uid>[^ \r\n]+) (?P<primarySite>[^ \r\n]+) (?P<dateOfDiagnosis>\d{2}/\d{2}/\d{4}) (?P<dateCaseLastChanged>\d{2}/\d{2}/\d{4})', prompt.before))
|
||||
elif prompt.index == 4:
|
||||
break
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
for item in parse_xml_rcrs(doc_rcrs, summary):
|
||||
yield item
|
||||
|
@ -63,7 +63,13 @@ async def cmd_appointments(proc, clinics='NPT-HEM/ONC ATTENDING', date='T', stor
|
||||
item['comment'] = '\r\n'.join(m.group(1) for m in re.finditer(r'^\s{15}(\w.*?)$', detail, re.MULTILINE))
|
||||
yield item
|
||||
proc.sendline('^Patient information AND OE/RR')
|
||||
assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
expect.clear()
|
||||
|
||||
async def vista_appointment_clinics(proc, expect):
|
||||
|
@ -23,6 +23,13 @@ async def task_smartcard(proc, config: Optional[configparser.ConfigParser]=None)
|
||||
if certificate:
|
||||
config.set('auth', 'certificate', certificate)
|
||||
proc.create_task(task_keepalive(proc, True), name='@task:keepalive')
|
||||
async for prompt, response in expect.promptmatches((
|
||||
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
|
||||
('Select Patient Information and OE/RR Option: ', None, True),
|
||||
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
|
||||
), throw=True):
|
||||
if prompt.index == 0:
|
||||
proc.sendline(response)
|
||||
return True
|
||||
|
||||
async def task_keepalive(proc, suppress=False):
|
||||
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -8,3 +8,6 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Override
|
||||
!/src/*
|
||||
|
54
frontend/src/lib/Navbar.svelte
Normal file
54
frontend/src/lib/Navbar.svelte
Normal file
@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { navlinks } from '$lib/stores.js';
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<div class="container-fluid">
|
||||
{#if $navlinks.length > 0}
|
||||
{@const first = $navlinks[0]}
|
||||
<a class="navbar-brand" href={first.href || '#'} target={first.target}>{first.name || 'VistA-SSH'}</a>
|
||||
{:else}
|
||||
<a class="navbar-brand" href="#">VistA-SSH</a>
|
||||
{/if}
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
{#if $navlinks.length > 0}
|
||||
{@const first = $navlinks[0]}
|
||||
{#if first.children}
|
||||
{#each first.children as item}{#if (item.name) && (item.href)}<li class="nav-item"><a class="nav-link" class:active={$page.url.pathname.includes(item.href)} href={item.href} target={item.target}>{item.name}</a></li>{/if}{/each}
|
||||
{/if}
|
||||
{#each $navlinks as parent, idx}
|
||||
{#if (idx > 0) && (parent.name)}
|
||||
{#if (parent.children) && (parent.children.length > 0)}
|
||||
<div class="btn-group">
|
||||
{#if parent.href}
|
||||
<a class="nav-link" class:active={$page.url.pathname.includes(parent.href)} href={parent.href} target={parent.target}>{parent.name}</a>
|
||||
<button type="button" class="nav-link btn dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">Toggle Dropdown</span></button>
|
||||
{:else}
|
||||
<button type="button" class="nav-link btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">{parent.name}</button>
|
||||
{/if}
|
||||
<ul class="dropdown-menu dropdown-menu-dark">{#each parent.children as item}{#if (item.name) && (item.href)}<li><a class="dropdown-item" class:active={$page.url.pathname.includes(item.href)} href={item.href} target={item.target}>{item.name}</a></li>{/if}{/each}</ul>
|
||||
</div>
|
||||
{:else if parent.href}
|
||||
<li class="nav-item"><a class="nav-link" class:active={$page.url.pathname.includes(parent.href)} href={parent.href} target={parent.target}>{parent.name}</a></li>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
<form role="search" method="get" action="/lookup" target="_blank">
|
||||
<input class="form-control me-2" type="search" name="q" placeholder="Patient lookup..." aria-label="Patient lookup">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
padding-top: 4.5rem;
|
||||
}
|
||||
</style>
|
37
frontend/src/lib/backend.js
Normal file
37
frontend/src/lib/backend.js
Normal file
@ -0,0 +1,37 @@
|
||||
export async function get_api_appointments({ fetch, clinics = [], date = 'T' } = {}) {
|
||||
if(clinics.constructor === Array) clinics = clinics.map(x => x.replace(/^\s+|\s+$/g, '').replace(/\s+/, ' ')).filter(x => x).join('^').replace(/\//g, '|');
|
||||
else clinics = clinics.replace(/^\s+|\s+$/g, '').replace(/\s+/, ' ').replace(/\//g, '|');
|
||||
if(clinics) return await (await (fetch || window.fetch)('/api/appointments/' + clinics + '/' + date)).json();
|
||||
else return [];
|
||||
}
|
||||
|
||||
export async function get_api_lookup({ fetch, query, ordinal, force = false } = {}) {
|
||||
if(query) {
|
||||
if(ordinal === undefined) return await (await (fetch || window.fetch)('/api/lookup/' + encodeURIComponent(query))).json();
|
||||
else return (await (fetch || window.fetch)('/api/lookup/' + encodeURIComponent(query) + '/' + (ordinal || '0') + (force ? '/force' : ''))).text()
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_api_rcrs_patients({ fetch, alpha = 'T-30', omega = 'N' } = {}) {
|
||||
return await (await (fetch || window.fetch)('/api/rcrs/patients/' + alpha + '/' + omega)).json();
|
||||
}
|
||||
|
||||
export async function get_api_rcrs_tumors({ fetch, alpha = 'T-30', omega = 'N' } = {}) {
|
||||
return await (await (fetch || window.fetch)('/api/rcrs/tumors/' + alpha + '/' + omega)).json();
|
||||
}
|
||||
|
||||
export async function get_api_measurements({ fetch, mrn, alpha = 'T-30', omega = 'N' } = {}) {
|
||||
return await (await (fetch || window.fetch)('/api/measurements/' + mrn + '/' + alpha + '/' + omega)).json();
|
||||
}
|
||||
|
||||
export async function get_api_labs({ fetch, mrn, alpha = 'T-30', omega = 'N' } = {}) {
|
||||
return await (await (fetch || window.fetch)('/api/labs/' + mrn + '/' + alpha + '/' + omega)).json();
|
||||
}
|
||||
|
||||
export async function get_api_notes({ fetch, mrn, alpha = 'T-30', omega = 'N' } = {}) {
|
||||
return await (await (fetch || window.fetch)('/api/notes/' + mrn + '/' + alpha + '/' + omega)).json();
|
||||
}
|
||||
|
||||
export async function get_api_orders({ fetch, mrn, alpha = 'T-30', omega = 'N' } = {}) {
|
||||
return await (await (fetch || window.fetch)('/api/orders/' + mrn + '/' + alpha + '/' + omega)).json();
|
||||
}
|
1
frontend/src/lib/index.js
Normal file
1
frontend/src/lib/index.js
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
18
frontend/src/lib/stores.js
Normal file
18
frontend/src/lib/stores.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export const navlinks = writable([]);
|
||||
navlinks.push = function(item) {
|
||||
navlinks.pop(item);
|
||||
navlinks.update(value => (value.unshift(item), value));
|
||||
return item;
|
||||
};
|
||||
navlinks.pop = function(item) {
|
||||
navlinks.update(value => {
|
||||
if(item !== undefined) {
|
||||
let idx = value.indexOf(item);
|
||||
if(idx >= 0) value.splice(idx, 1);
|
||||
} else value.shift();
|
||||
return value;
|
||||
});
|
||||
return item;
|
||||
}
|
194
frontend/src/lib/util.js
Normal file
194
frontend/src/lib/util.js
Normal file
@ -0,0 +1,194 @@
|
||||
export const comp = (...fs) => x0 => fs.reduceRight((x, f) => f(x), x0);
|
||||
export const flow = (...fs) => x0 => fs.reduce((x, f) => f(x), x0);
|
||||
export const pipe = (x0, ...fs) => fs.reduce((x, f) => f(x), x0);
|
||||
export const aflow = (f0, ...fs) => async (...args) => fs.reduce((x, f) => f(x), await f0(...args));
|
||||
|
||||
export function uniq(xs) {
|
||||
var seen = {};
|
||||
return xs.filter(x => seen.hasOwnProperty(x) ? false : (seen[x] = true));
|
||||
}
|
||||
|
||||
export function groupBy(xs, key) {
|
||||
return xs.reduce(function(rv, x) {
|
||||
var v = key instanceof Function ? key(x) : x[key];
|
||||
(rv[v] = rv[v] || []).push(x);
|
||||
return rv;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function groupByArray(xs, key) {
|
||||
var mapping = {};
|
||||
return xs.reduce(function(rv, x) {
|
||||
var v = key instanceof Function ? key(x) : x[key];
|
||||
var el = mapping[v];
|
||||
if(el) el.values.push(x);
|
||||
else rv.push(mapping[v] = { key: v, values: [x] });
|
||||
return rv;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function pivotByArray(xs, key, reducer) {
|
||||
var groups = groupByArray(xs, key);
|
||||
groups.forEach(function(group) {
|
||||
group.aggregate = group.values.reduce(reducer, {});
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function quantile_sorted(arr_sorted, quantile) {
|
||||
var pos = (arr_sorted.length - 1) * quantile, base = Math.floor(pos), rest = pos - base;
|
||||
return arr_sorted[base + 1] !== undefined ? arr_sorted[base] + rest * (arr_sorted[base + 1] - arr_sorted[base]) : arr_sorted[base];
|
||||
}
|
||||
|
||||
export function strtr(s, a, b) {
|
||||
var res = '';
|
||||
for(var i = 0; i < s.length; ++i) {
|
||||
var j = a.indexOf(s.charAt(i));
|
||||
res += j >= 0 ? b.charAt(j) : s.charAt(i);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function strtr_unscramble(name) {
|
||||
return name.length > 0 ? (name.charAt(0) + strtr(name.substring(1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'LKJIHGFEDCBAZYXWVUTSRQPONM')) : name;
|
||||
}
|
||||
|
||||
export function strHashCode(str) {
|
||||
var hash = 0;
|
||||
for(var i = 0; i < str.length; ++i) hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
return hash & hash; // convert to 32 bit
|
||||
}
|
||||
|
||||
export function strHashJenkins(str) {
|
||||
for(var hash = 0, i = str.length; i--;) hash += str.charCodeAt(i), hash += hash << 10, hash ^= hash >> 6;
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 11;
|
||||
return (hash + (hash << 15) & 4294967295) >>> 0
|
||||
}
|
||||
|
||||
export function strHashHex(str) {
|
||||
var hash = strHashJenkins(str), color = '#';
|
||||
for(var i = 0; i < 3; ++i) color += ('00' + ((hash >> (i * 8)) & 0xFF).toString(16)).slice(-2);
|
||||
return color;
|
||||
}
|
||||
|
||||
export function strHashHSL(str, lightness='50%') {
|
||||
var hash = strHashJenkins(str);
|
||||
return 'hsl(' + (hash%360) + ',' + (hash%100) + '%,' + lightness + ')';
|
||||
}
|
||||
|
||||
export function datetime_datestr(dt) {
|
||||
return dt ? dt.toLocaleDateString('sv-SE') : undefined;
|
||||
}
|
||||
|
||||
export function datetime_timestr(dt) {
|
||||
if(dt) {
|
||||
const res = dt.toLocaleTimeString('en-GB');
|
||||
return res.endsWith(':00') ? res.substring(0, 5) : res;
|
||||
}
|
||||
}
|
||||
|
||||
export function datetime_dtstr(dt) {
|
||||
if(dt) {
|
||||
const res = dt.toLocaleTimeString('en-GB');
|
||||
return dt.toLocaleDateString('sv-SE') + ' ' + (res.endsWith(':00') ? res.substring(0, 5) : res);
|
||||
}
|
||||
}
|
||||
|
||||
export function strftime_vista(date) {
|
||||
return 10000*(date.getFullYear() - 1700) + 100*(date.getMonth() + 1) + date.getDate() + date.getHours()/100 + date.getMinutes()/10000 + date.getSeconds()/1000000 + date.getMilliseconds()/1000000000;
|
||||
}
|
||||
|
||||
export function strfdate_vista(date) {
|
||||
return 10000*(date.getFullYear() - 1700) + 100*(date.getMonth() + 1) + date.getDate();
|
||||
}
|
||||
|
||||
export function strptime_vista(s) {
|
||||
s = +s;
|
||||
var date = Math.floor(s), time = s - date;
|
||||
return new Date(Math.floor(date/10000) + 1700, (Math.floor(date/100) + '').slice(-2) - 1, (date + '').slice(-2), Math.floor(time*100), (Math.floor(time*10000) + '').slice(-2), (Math.floor(time*1000000) + '').slice(-2), (Math.floor(time*1000000000) + '').slice(-3));
|
||||
}
|
||||
|
||||
export function escapeHTML(unsafe) {
|
||||
return unsafe.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export function escapeRegExp(unsafe) {
|
||||
return unsafe.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export function debounce(fn, delay) {
|
||||
var clock = null;
|
||||
return function() {
|
||||
window.clearTimeout(clock);
|
||||
var self = this, args = arguments;
|
||||
clock = window.setTimeout(function() { fn.apply(self, args) }, delay);
|
||||
}
|
||||
}
|
||||
|
||||
export function isInViewport(el, entirely = false) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return entirely ? (
|
||||
(rect.top >= 0) &&
|
||||
(rect.left >= 0) &&
|
||||
(rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)) &&
|
||||
(rect.right <= (window.innerWidth || document.documentElement.clientWidth))
|
||||
) : (
|
||||
(rect.bottom >= 0) &&
|
||||
(rect.right >= 0) &&
|
||||
(rect.top <= (window.innerHeight || document.documentElement.clientHeight)) &&
|
||||
(rect.left <= (window.innerWidth || document.documentElement.clientWidth))
|
||||
);
|
||||
}
|
||||
|
||||
export function filter_pattern(query, between, flags) {
|
||||
if((query = query.replace(/^\s+|\s+$/g, '')).length > 0) {
|
||||
if(query.startsWith('/')) {
|
||||
if(query.length > 1) {
|
||||
var m = /^\/(.*)\/([a-z]*)$/.exec(query);
|
||||
return m ? new RegExp(m[1], m[2]) : new RegExp(query.substring(1), flags || 'gims');
|
||||
}
|
||||
} else {
|
||||
query = query.split('|').map(x => filter_part(x, between || 5)).filter(x => x);
|
||||
if(query.length > 0) return new RegExp(query.join('|'), flags || 'gims');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filter_part(query, between) {
|
||||
if((query = query.replace(/^\s+|\s+$/g, '')).length > 0) {
|
||||
if(query.startsWith('"')) {
|
||||
query = query.substring(1, query.length - ((query.length > 1) && (query.endsWith('"')) ? 1 : 0));
|
||||
if(query.length > 0) return query.split(/\s+/).map(x => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\s*');
|
||||
} else return '(?:' + query.split(/\s+/).map(x => '(?<!\\w)' + x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\S*\\s+(?:\\S+\\s+){0,' + (+(between || 5)) + '}?') + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export function filter_test(pattern, haystack) {
|
||||
pattern.lastIndex = 0;
|
||||
return pattern.test(haystack);
|
||||
}
|
||||
|
||||
export function filter_mark(pattern, haystack, mark) {
|
||||
return haystack.replace(pattern, mark || '<mark>$&</mark>');
|
||||
}
|
||||
|
||||
export function filter_snippets(pattern, haystack, mark, before, after) {
|
||||
var res = [], context = new RegExp('(?:\\S+[ \t\v\xa0\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\ufeff]+){0,' + (+(before || 3)) + '}\\S*(' + pattern.source + ')\\S*(?:[ \t\v\xa0\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\ufeff]+\\S+){0,' + (+(after || before || 3)) + '}', pattern.flags), match;
|
||||
if(context.global) while((match = context.exec(haystack)) !== null) res.push(match[0].replace(pattern, mark || '<mark>$&</mark>').replace(/\s+/g, ' ').replace(/([\W_])\1{2,}/g, '$1$1'));
|
||||
else if((match = context.exec(haystack)) !== null) res.push(match[0].replace(pattern, mark || '<mark>$&</mark>').replace(/\s+/g, ' ').replace(/([\W_])\1{2,}/g, '$1$1'));
|
||||
return uniq(res);
|
||||
}
|
||||
|
||||
export function filter_snippets_lines(pattern, haystack, mark) {
|
||||
var res = [], context = new RegExp('[^\r\n]*(' + pattern.source + ')[^\r\n]*', pattern.flags), match;
|
||||
if(context.global) while((match = context.exec(haystack)) !== null) res.push(match[0].replace(pattern, mark || '<mark>$&</mark>').replace(/[\r\n]/g, ' '));
|
||||
else if((match = context.exec(haystack)) !== null) res.push(match[0].replace(pattern, mark || '<mark>$&</mark>').replace(/[\r\n]/g, ' '));
|
||||
return uniq(res);
|
||||
}
|
||||
|
||||
function Descendant() {}
|
||||
export function inherit(obj) {
|
||||
Descendant.prototype = obj;
|
||||
return new Descendant();
|
||||
}
|
@ -2,7 +2,12 @@ import { get_api_appointments } from '$lib/backend.js';
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ params, fetch }) {
|
||||
let clinics = await (await fetch('/api/config/user/clinics')).json();
|
||||
let clinics = [];
|
||||
try {
|
||||
clinics = await (await fetch('/api/config/user/clinics')).json();
|
||||
} catch(ex) {
|
||||
console.error(ex, ex.stack);
|
||||
}
|
||||
let appointments = await get_api_appointments({ fetch, clinics, date: 'T' });
|
||||
appointments.sort((a, b) => a.time_scheduled < b.time_scheduled ? -1 : a.time_scheduled > b.time_scheduled ? 1 : 0);
|
||||
return {
|
||||
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
function components(reports) {
|
||||
const res = [], re_lab_test = /^(?<name>\w[^\r\n]{26})(?<value>[^\r\n]{8}) (?:(?<flag>\w[ \*])|[ ]) (?<unit>[^\r\n]{10}) (?<range>[^\r\n]{16}) \[(?<site>\d+)\]$/gm;
|
||||
const res = [], re_lab_test = /^(?<name>\w[^\r\n]{26})(?<value>[^\r\n]{8}) (?:(?<flag>\w[ \*])| ) (?<unit>[^\r\n]{10}) (?<range>[^\r\n]{16}) \[(?<site>\d+)\]$/gm;
|
||||
reports.forEach(function(x) {
|
||||
let datestr = datetime_datestr(x._ts), timestr = datetime_timestr(x._ts), m, r;
|
||||
while(m = re_lab_test.exec(x.body)) {
|
||||
@ -258,9 +258,7 @@
|
||||
</th>
|
||||
{#each component_groups as group, idx}<th class="date" class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) }><div class="year">{group.datestr.substring(0, 4)}</div><div class="monthdate">{group.datestr.substring(5)}</div>{#if false}<div class="hourminute daily">{datetime_timestr(group.values[0]._ts)}</div>{/if}</th>{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
{#if pattern}
|
||||
<tbody>
|
||||
{#if pattern}
|
||||
{#each component_names as name}
|
||||
{#if filter_test(pattern, name)}
|
||||
<tr class="match">
|
||||
@ -271,8 +269,10 @@
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
<tbody>
|
||||
{/if}
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if pattern}
|
||||
{#each component_names as name}
|
||||
{#if !filter_test(pattern, name)}
|
||||
<tr>
|
||||
@ -283,9 +283,7 @@
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
{:else}
|
||||
<tbody>
|
||||
{:else}
|
||||
{#each component_names as name}
|
||||
<tr>
|
||||
<th>{name}</th>
|
||||
@ -294,8 +292,8 @@
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/if}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -325,9 +323,9 @@
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
table.table-sticky thead th {
|
||||
table.table-sticky thead {
|
||||
position: sticky;
|
||||
z-index: 1010;
|
||||
z-index: 1020;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
@ -339,7 +337,6 @@
|
||||
}
|
||||
table.table-sticky thead th.corner {
|
||||
padding: 0;
|
||||
z-index: 1020;
|
||||
}
|
||||
.navbar {
|
||||
position: sticky;
|
||||
|
@ -2,8 +2,12 @@
|
||||
export async function load({ params, fetch }) {
|
||||
let clinics = await (await fetch('/api/clinic/list')).json();
|
||||
clinics.reduce((acc, item) => (acc[item.name] = item, acc), clinics);
|
||||
let selection = await (await fetch('/api/config/user/clinics')).json();
|
||||
selection.forEach(x => clinics[x] ? clinics[x].active = true : false);
|
||||
try {
|
||||
let selection = await (await fetch('/api/config/user/clinics')).json();
|
||||
selection.forEach(x => clinics[x] ? clinics[x].active = true : false);
|
||||
} catch(ex) {
|
||||
console.error(ex, ex.stack);
|
||||
}
|
||||
return {
|
||||
clinics
|
||||
};
|
||||
|
Reference in New Issue
Block a user