167 lines
5.2 KiB
Svelte
167 lines
5.2 KiB
Svelte
<script>
|
|
import { tick } from 'svelte';
|
|
import { debounce, escapeHTML, escapeRegExp, strHashHSL, isInViewport, filter_pattern, filter_test, filter_mark } from '$lib/util.js';
|
|
import { get_api_orders } from '$lib/backend.js';
|
|
|
|
export let data;
|
|
|
|
let query = '', pattern = null, selection = null, all_reports = decorate(data.reports);
|
|
let debounced_pattern = debounce((/*query*/) => (pattern = query ? filter_pattern(escapeHTML(query)) : null), 200);
|
|
|
|
$: debounced_pattern(query); // argument `query` is for reactivity hinting only
|
|
|
|
function decorate(xs) {
|
|
return xs.map(x => Object.assign({ _content: escapeHTML(Object.values(x).join('\x00')), _uid: x.body, _ts: new Date(x.datetime_entered) }, x));
|
|
}
|
|
|
|
async function loadmore(evt, factor = 1.5) {
|
|
if(loadmore.loading) return;
|
|
loadmore.loading = true;
|
|
try {
|
|
let reports = await get_api_orders({ mrn: data.mrn, omega: 'T-' + (data.offset + 1), alpha: 'T-' + (data.offset += (loadmore.limit = (factor*loadmore.limit)|0)) });
|
|
Array.prototype.push.apply(all_reports, decorate(reports));
|
|
all_reports = all_reports; // reactivity hint
|
|
} finally {
|
|
loadmore.loading = false;
|
|
}
|
|
}
|
|
loadmore.loading = false;
|
|
loadmore.limit = 30;
|
|
|
|
(async function loadinit(target = 16, requests = 4) {
|
|
for(let i = 0; (i < requests) && (all_reports.length < target); ++i) await loadmore();
|
|
})();
|
|
|
|
const observer = new IntersectionObserver((entries) => { if((!query) && (entries[0].isIntersecting)) loadmore(null); }, { root: null, rootMargin: '0px', threshold: 0.5 });
|
|
let bottom = null;
|
|
$: {
|
|
observer.disconnect();
|
|
if(bottom) observer.observe(bottom);
|
|
}
|
|
|
|
let reportlist;
|
|
async function scroll(selection) {
|
|
if(selection) {
|
|
await tick();
|
|
const el = reportlist.querySelector('.active');
|
|
if((el) && (!isInViewport(el, true))) el.scrollIntoView({ block: 'center' });
|
|
} else {
|
|
const items = reportlist.children;
|
|
for(let i = 0, el; i < items.length; ++i) if(isInViewport(el = items[i])) {
|
|
await tick();
|
|
el.scrollIntoView({ block: 'start' });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$: if(reportlist) scroll(selection);
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Orders</title>
|
|
</svelte:head>
|
|
|
|
{#if selection}
|
|
<div class="halfpane rightpane">
|
|
<nav class="navbar bg-body-secondary">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand">{selection.text || ''}</span>
|
|
<button type="button" class="btn btn-outline-light" on:click={() => selection = null}>❌</button>
|
|
</div>
|
|
</nav>
|
|
<div class="container-fluid"><dl class="report">{#each Object.entries(selection) as entry}{#if entry[0].charAt(0) != '_'}<dt>{entry[0]}</dt><dd>{entry[1]}</dd>{/if}{/each}</dl></div>
|
|
</div>
|
|
{/if}
|
|
<div class={selection ? 'halfpane leftpane' : ''}>
|
|
<div class="card {selection ? '' : 'mb-3 shadow'}">
|
|
<nav class="navbar bg-body-tertiary">
|
|
<form class="container-fluid">
|
|
<div class="input-group">
|
|
<span class="input-group-text">Orders</span>
|
|
<input type="text" class="form-control" placeholder="Filter..." bind:value={query}>
|
|
{#if query}<button type="button" class="btn btn-outline-secondary" on:click={() => query = ''}>❌</button>{/if}
|
|
</div>
|
|
</form>
|
|
</nav>
|
|
<ul class="list-group list-group-flush" bind:this={reportlist}>
|
|
{#if pattern}
|
|
{#each all_reports as row}
|
|
{#if filter_test(pattern, row._content)}
|
|
<li class="list-group-item" class:active={(selection) && (selection._uid == row._uid)} on:click={() => selection = selection !== row ? row : null}>
|
|
<div class="singleline" style="font-weight: bold;">{row.text || ''}</div>
|
|
<div class="report">{@html pattern ? filter_mark(pattern, escapeHTML(row.body)) : escapeHTML(row.body)}</div>
|
|
</li>
|
|
{/if}
|
|
{/each}
|
|
{:else}
|
|
{#each all_reports as row}
|
|
<li class="list-group-item" class:active={(selection) && (selection._uid == row._uid)} on:click={() => selection = selection !== row ? row : null}>
|
|
<div class="singleline" style="font-weight: bold;">{row.text || ''}</div>
|
|
<div class="report">{@html pattern ? filter_mark(pattern, escapeHTML(row.body)) : escapeHTML(row.body)}</div>
|
|
</li>
|
|
{/each}
|
|
{/if}
|
|
<li class="list-group-item" style="padding: 0;" bind:this={bottom}>{#if loadmore.loading}<button type="button" class="btn btn-primary w-100" disabled>Loading...</button>{:else}<button type="button" class="btn btn-primary w-100" on:click={loadmore}>Load more</button>{/if}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
:global(div.report mark) {
|
|
padding: 0;
|
|
font-weight: bold;
|
|
}
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
:root {
|
|
scroll-behavior: auto;
|
|
}
|
|
}
|
|
.navbar {
|
|
position: sticky;
|
|
z-index: 1020;
|
|
top: 3.5rem;
|
|
}
|
|
.leftpane {
|
|
display: none;
|
|
}
|
|
li.active {
|
|
scroll-margin-top: 3.5rem;
|
|
}
|
|
div.singleline {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.report {
|
|
font-family: monospace;
|
|
white-space: pre-wrap;
|
|
}
|
|
@media screen and (min-width: 720px) {
|
|
.halfpane {
|
|
position: absolute;
|
|
top: 3.5rem;
|
|
bottom: 0;
|
|
width: 50%;
|
|
overflow: auto;
|
|
}
|
|
.leftpane {
|
|
display: block;
|
|
width: 33%;
|
|
left: 0;
|
|
z-index: -1;
|
|
direction: rtl;
|
|
}
|
|
.leftpane > * {
|
|
direction: ltr;
|
|
}
|
|
.rightpane {
|
|
width: 67%;
|
|
right: 0;
|
|
box-shadow: var(--bs-box-shadow);
|
|
}
|
|
.halfpane .navbar {
|
|
top: 0;
|
|
}
|
|
}
|
|
</style>
|