master
Bas Kloosterman 1 year ago
parent 3e81a54556
commit 553d6893c3
  1. BIN
      kisservice/.DS_Store
  2. BIN
      kisservice/app/.DS_Store
  3. 6
      kisservice/app/.babelrc
  4. 32
      kisservice/app/dist/main.js
  5. 11060
      kisservice/app/package-lock.json
  6. 30
      kisservice/app/package.json
  7. 25
      kisservice/app/src/App.js
  8. 54
      kisservice/app/src/Connection.js
  9. 43
      kisservice/app/src/Connections.js
  10. 13
      kisservice/app/src/Home.js
  11. 81
      kisservice/app/src/Index.css
  12. 21
      kisservice/app/src/Patient.js
  13. 34
      kisservice/app/src/Registrations.js
  14. 34
      kisservice/app/src/index.js
  15. 26
      kisservice/app/webpack.config.js
  16. 34039
      kisservice/assets/index.js
  17. 1
      kisservice/assets/index.js.map
  18. 3
      kisservice/assets/js/index.js
  19. 51
      kisservice/assets/js/index.js.LICENSE.txt
  20. 1
      kisservice/assets/js/index.js.map
  21. BIN
      kisservice/bin/ediviewer
  22. 1
      kisservice/bin/medeur.json
  23. 442
      kisservice/bin/template.html
  24. BIN
      kisservice/data/data.db
  25. 72
      kisservice/main.go
  26. 51
      kisservice/model/db.go
  27. 408
      kisservice/openapisrv.go
  28. 277
      kisservice/srv.go
  29. 13
      kisservice/templates/index.html

BIN
kisservice/.DS_Store vendored

Binary file not shown.

Binary file not shown.

@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
]
}

@ -0,0 +1,32 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (() => {
eval("throw new Error(\"Module parse failed: Unexpected token (8:12)\\nYou may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders\\n| const root = createRoot(container);\\n| \\n> root.render(<App tab='home' />);\");\n\n//# sourceURL=webpack://app/./src/index.js?");
/***/ })
/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module doesn't tell about it's top-level declarations so it can't be inlined
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/index.js"]();
/******/
/******/ })()
;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"style-loader": "^3.3.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-json-pretty": "^2.2.0",
"react-router-dom": "^6.3.0"
}
}

@ -0,0 +1,25 @@
import React from "react";
import {
Link,
NavLink,
Outlet,
} from "react-router-dom";
import "./Index.css";
const App = () => {
return (
<div>
<nav className="c-main-nav">
<p style={{marginRight: 50}}><Link to="/">KIS</Link></p>
<div className="c-main-nav__main">
<NavLink to="connecties">Verbindingen</NavLink>
<NavLink to="registraties">Registratieverzoeken</NavLink>
</div>
</nav>
<div className="c-main-content">
<Outlet/>
</div>
</div>
)
};
export default App;

@ -0,0 +1,54 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import "./Index.css";
const Subscriptions = ({service}) => {
if (!service) {
return null
}
return (<table className="c-table">
<thead>
<tr>
<th>Naam</th>
<th>Bsn</th>
<th>Geboortedatum</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
{service.Subscriptions.map(x => {
return (<tr key={x.ID}>
<td>{x.SubjectName}</td>
<td>{x.SubjectExternalId}</td>
<td>{x.SubjectBirthdate}</td>
<td><Link to={`/connecties/${service.ConnectionID}/${service.ID}/${x.ID}`}>Bekijk dossier</Link></td>
</tr>)
})}
</tbody>
</table>
)
}
const Connection = () => {
let params = useParams();
const [connection, setConnection] = useState(null)
const [service, setService] = useState(null)
useEffect(() => {
fetch(`/api/connections/${params.connId}`).then(x => x.json()).then(x => setConnection(x) )
}, [])
useEffect(() => {
fetch(`/api/connections/${params.connId}/${params.serviceId}`).then(x => x.json()).then(x => setService(x) )
}, [])
console.log('connection', connection)
console.log('service', service)
return (
<div>
<h1 className="t-page-header">Verbinding</h1>
{(connection && service) ? (<h2>{connection.OrganisationDisplayName} ({connection.OrganisationId}) | {service.Service.Name}</h2>) : null}
{<Subscriptions service={service}/>}
</div>
);
};
export default Connection;

@ -0,0 +1,43 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import "./Index.css";
const subscriptionsCount = (s) => {
const l = s.Subscriptions.length
return l == 1 ? "1 patiënt aangemeld" : `${l} patiënten aangemeld`
}
const App = () => {
const [connections, setConnections] = useState([])
useEffect(() => {
fetch('/api/connections').then(x => x.json()).then(x => setConnections(x) )
}, [])
return (
<div>
<h1 className="t-page-header">Verbindingen</h1>
<table className="c-table">
<thead>
<tr>
<th>AGB</th>
<th>Naam</th>
<th>Geactiveerde diensten</th>
</tr>
</thead>
<tbody>
{connections.map(x => {
return (<tr key={x.ID}>
<td>{x.OrganisationId}</td>
<td>{x.OrganisationDisplayName}</td>
<td>{x.Services.length ? x.Services.map((s) => {
return <span key={s.Service.ID} style={{marginRight: 10}}><Link to={`/connecties/${x.ID}/${s.ID}`} >{s.Service.Name} ({subscriptionsCount(s)})</Link></span>
}) : '-'}</td>
</tr>)
})}
</tbody>
</table>
</div>
);
};
export default App;

@ -0,0 +1,13 @@
import React from "react";
import "./Index.css";
const App = () => {
return (
<div>
<h1 className="t-page-header">Welkom bij KIS</h1>
<p>Dit systeem is beschikbaar op: <code>kis.openkv.mcsr.nl:8877</code></p>
</div>
);
};
export default App;

@ -0,0 +1,81 @@
body {
font-family: Helvetica, Arial, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
h2 {
margin-bottom: 35px;
}
code {
font-size: 120%;
background: rgba(0,0,0,0.1);
padding: 10px;
}
.c-main-nav {
padding: 15px;
display: flex;
width: 100%;
margin-bottom: 50px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.1);
}
.c-main-nav__main {
display: flex;
justify-content: space-between;
flex:1;
}
.c-main-nav a {display: block; padding: 5px; color: #137ad4; text-decoration: none}
.c-main-nav p a {color: black}
.c-main-nav .active {font-weight:bold; text-decoration: underline}
.t-page-header {
margin-bottom: 50px;
}
.c-table {
width: 100%;
}
.c-table table a {color: #137ad4; text-decoration: none}
.c-main-content {
padding: 25px;
width: 90%;
max-width: 1024;
margin: auto;
}
.c-table th, td {
text-align: left;
padding: 10px;
}
.c-table th {
background-color: rgba(0,0,0,0.1);
}
.c-button {
padding: 8px 17px;
font-size: 95%;
color: white;
background-color: #137ad4;
border: 0;
outline: 0;
cursor: pointer;
margin-bottom: 10px;
display: inline-block;
text-decoration: none;
}
.c-modal {
position: fixed;
top: 50%;
left: 50%;
}

@ -0,0 +1,21 @@
import React from "react";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import "./Index.css";
const Patient = () => {
let params = useParams();
const [patient, setPatient] = useState(null)
useEffect(() => {
fetch(`/api/connections/${params.connId}/${params.serviceId}/${params.patientId}`).then(x => x.text()).then(x => setPatient(x) )
}, [])
return (
<div>
<h1 className="t-page-header">Patient</h1>
{patient ? <div dangerouslySetInnerHTML={{__html: patient}}></div> : null}
</div>
);
};
export default Patient;

@ -0,0 +1,34 @@
import React from "react";
import { useEffect, useState } from "react";
import "./Index.css";
const App = () => {
const [registrations, setRegistrations] = useState([])
useEffect(() => {
fetch('/api/registrations').then(x => x.json()).then(x => setRegistrations(x) )
}, [])
console.log('registrations', registrations)
return (
<div>
<h1 className="t-page-header">Registratie verzoeken</h1>
<table className="c-table">
<tr>
<th>AGB</th>
<th>Naam</th>
<th>Referentie</th>
<th>Tijdelijk wachtwoord</th>
</tr>
{registrations.map(x => {
return (<tr key={x.ID}>
<td>{x.OrganisationId}</td>
<td>{x.OrganisationDisplayName}</td>
<td>{x.Reference}</td>
<td>{x.PSK}</td>
</tr>)
})}
</table>
</div>
);
};
export default App;

@ -0,0 +1,34 @@
import React from "react";
import { createRoot } from "react-dom/client";
import {
BrowserRouter,
Routes,
Route,
} from "react-router-dom";
import App from "./App";
import Home from "./Home";
import Registrations from "./Registrations";
import Connections from "./Connections";
import Connection from "./Connection";
import Patient from "./Patient";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<BrowserRouter basename="/ui">
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="/registraties" element={<Registrations />}/>
<Route path="/connecties" element={<Connections />}/>
<Route path="/connecties/:connId/:serviceId" element={<Connection />}/>
<Route path="/connecties/:connId/:serviceId/:patientId" element={<Patient />}/>
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
</Route>
</Routes>
</BrowserRouter>);

@ -0,0 +1,26 @@
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname, "../assets/js"),
filename: "index.js",
clean: true,
},
devtool: "source-map",
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,51 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* React Router DOM v6.3.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.3.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -0,0 +1 @@
{"format":[{"name":"UNB","description":"Interchange header"},{"name":"UNH","description":"Head of message"},{"name":"BGM","children":[{"name":"DTM","description":"Datum aangemaakt"},{"name":"RFF","description":"Referenties"},{"name":"FTX","description":"Bericht afhankelijke mededeling"}],"description":"Begin van het bericht"},{"name":"S01","children":[{"name":"NAD","description":"Naam en adres"},{"name":"ADR","description":"Adres"},{"name":"COM","description":"Telefoon- en fax-nummers"},{"name":"RFF"},{"name":"DTM"},{"name":"LAN"},{"name":"SPR","description":"Type medewerker"},{"name":"QUA","description":"Kwalificatie"},{"name":"FTX","description":"Vrije tekst"}],"description":"Medebehandelaar"},{"name":"S02","children":[{"name":"PNA","description":"Naam van de patient"},{"name":"ADR","description":"Adres-patient"},{"name":"COM","description":"Telefoon- en fax-nummers"},{"name":"RFF","description":"Referenties"},{"name":"DTM","description":"Tijdsindicatie"},{"name":"NAT"},{"name":"LAN"},{"name":"LOC"},{"name":"PDI","description":"Demografische gegevens"},{"name":"QUA","description":"Kwalificatie"},{"name":"STS"},{"name":"INS","description":"Verzekeringsgegevens"},{"name":"REL"},{"name":"FTX","description":"Vrije tekst"},{"name":"S03","children":[{"name":"DTM","description":"Tijdsindicatie"},{"name":"S04","children":[{"name":"CIN","description":"Diagnose signalering of risico-factor"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Referentie naar groep 1"},{"name":"FTX","description":"Vrije tekst"},{"name":"DTM","description":"Tijdsindicatie"}],"description":"Medisch kenmerk"},{"name":"S05","children":[{"name":"INV"},{"name":"DTM"}]}],"description":"Contact onafhankelijke medische gegevens"},{"name":"S06","children":[{"name":"DTM","description":"Datum/tijd van een contact"},{"name":"RFF","description":"Verantwoordelijke persoon"},{"name":"S07","children":[{"name":"FTX"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"DTM","description":"Datum/tijd van een contact"}],"description":"Ongeclassificeerde journaalregel"},{"name":"S08","children":[{"name":"INV","description":"Meting identificatie"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"RSL","description":"Resultaat meting"},{"name":"RND","description":"Normaalwaarden"},{"name":"FTX","description":"Resultaat"},{"name":"DTM","description":"Datum/tijd"}],"description":"Metingen"},{"name":"S09","children":[{"name":"CIN","description":"Diagnose"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"FTX","description":"Diagnose in vrije tekst"},{"name":"DTM","description":"Datum/tijd"}],"description":"Diagnoses"},{"name":"S10","children":[{"name":"SPR","description":"Specialisme"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"PRC","description":"Soort verwijzing"},{"name":"FTX","description":"Beschrijving"},{"name":"DTM","description":"Datum/tijd"}],"description":"Verwijzingen of terugverwijzingen"},{"name":"S11","children":[{"name":"CLI","description":"Type therapie"},{"name":"PTY","description":"Prioriteit"},{"name":"RFF","description":"Probleem/episode"},{"name":"FTX","description":"Vrije tekst"},{"name":"QTY","description":"Hoeveelheid"},{"name":"DNL","children":[{"name":"DSG","description":"Dose administration"},{"name":"FTX","description":"Vrije tekst"}],"description":"Dosering Nederlandse stijl"},{"name":"SPC","children":[{"name":"QTY","description":"Hoeveelheid"}],"description":"Afzonderlijke stoffen van recept"},{"name":"CIN","description":"Indicatie"},{"name":"SPR","description":"Specialisme voorschrijver"},{"name":"DTM","description":"Datum/tijd"}],"description":"Therapie"}],"description":"Journaalregel"}],"description":"Pati\u00ebnt"},{"name":"UNT","description":"Einde van het bericht"},{"name":"UNZ","description":"Einde uitwisseling"}]}

@ -0,0 +1,442 @@
<style>
.container { margin: 1em auto; font-size: 12pt; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; }
@media screen and (max-width: 700px) {
.container { width: 700px; }
}
@media screen and (min-width: 1200px) {
.container { width: 1200px; }
}
.container h3 { margin-bottom: 8px; }
.container ul { list-style-type: none; }
.container li + li { margin-top: 10px; }
.container td { vertical-align: top; }
.container .seg { margin-top: 8px; padding: 8px 10px; border: 0 solid #e7e7e7; border-width: 0 1px 1px 1px; }
.container .seg-hdr { margin-top: 8px; border-top-width: 1px; background: #F8F8F8; }
.container .seg-hdr-white { margin-top: 8px; background: #FFFFFF; border-color: transparent; }
.container a { color: royalblue }
.container table.patient th { text-align: left; padding-right: 1em }
.container table.SOEPkind { margin: -6px 0 }
.container table.SOEPkind tr { height: 1em }
.container table.SOEPkind th { text-align: left; width: 32px; height: 1em; font-weight: normal }
.container table.results th { text-align: left }
.container .error { position: fixed; top: 10px; margin-left: 32px; border: 2px solid #000000; background-color: #FF2222; }
.row + .row { margin-top: 4px; }
.table { font-size: 100%; width: 100%; border-spacing: 0; border-collapse: collapse; }
.table tr { font-size: 100% }
.table td { font-size: 100% }
.table th { font-size: 100%; text-align: left; }
.table tbody td { font-size: 100%; text-align: left; padding: 2px 8px 2px 0; }
.table thead th { font-size: 100%; padding: 0 0 2px 0; }
.table tr.highlight { background: #E3E9E3 }
.column-attn { width: 1em; }
.column-code { width: 4em; }
.column-date { width: 6.5em; }
.column-small-name { width: 10em; }
.column-medium-name { width: 14em; }
.column-large-name { width: 28em; }
.column-name { width: 20em; }
.column-instr{ width: 15em; }
.column-quantity { width: 12em; }
.no-margin { margin-bottom: 0px; }
.seg-date { float: left; width: 6.5em; }
.seg-content-offset { margin-left: 6.5em }
.column-result-id { width: 16em }
.column-result-name { width: 10em }
.container h3.fold-toggle { margin-bottom: 8px }
.container h4.fold-toggle { margin-bottom: 0; margin-top: 4px }
.container h3.fold-toggle.fold-toggle-hidden { margin-bottom: 0 }
.container h4.fold-toggle.fold-toggle-hidden { margin-bottom: 4px }
.fold-toggle .fold-marker:before,
.container h3.fold-toggle:before,
.container h4.fold-toggle:before { content: "▾ " }
.fold-toggle.fold-toggle-hidden .fold-marker:before,
.container h3.fold-toggle.fold-toggle-hidden:before,
.container h4.fold-toggle.fold-toggle-hidden:before { content: "▸ " }
.fold-toggle { cursor: pointer; }
.fold-hidden {display: none}
.hidden
{
display: none !important
}
.container textarea
{
border: 1px solid #333;
padding: .5em;
font-size: 100%
}
.container input[type=submit]
{
font-size: 90%;
padding: .25em
}
.container button
{
font-size: 75%;
padding: .25em
}
</style>
<div class="container">
{{define "person-row"}}
<tr id="person-{{.Anchor}}">
<td class="column-small-name">{{if .Name}}{{.Name}}{{else}}<em>Geen naam</em>{{end}}</td>
<td class="column-small-name">{{.Function}}</td>
<td class="column-medium-name">{{.Address}}</td>
<td class="column-small-name">{{if .AGB}}AGB: {{.AGB}}{{end}}</td>
</tr>{{end}}
{{define "medi-group-epi"}}
{{$root := .}}
<table class="table">
<thead>
<tr>
<th class="column-date">Datum</th>
<th class="column-code">Type</th>
<th class="column-date">ICPC</th>
<th class="column-name">Beschrijving</th>
{{if eq .Extra.verbose_icpc_description "true"}}
<th class="column-desc">Beschrijving arts</th>
{{else}}
{{end}}
<th class="column-desc"></th>
</tr>
</thead>
<tbody>
{{range .Episodes}}
<tr{{if .Anchor}} id="medigroup-{{.Anchor}}"{{end}}>
<td>{{.Date}}</td>
<td>{{.Type}}</td>
<td>{{.ICPC}}</td>
{{if eq $root.Extra.verbose_icpc_description "true"}}
<td>{{.Title}}</td>
<td>{{.Description}}</td>
{{else}}
{{if eq .Description ""}}
<td colspan="2">{{.Title}}</td>
{{else}}
<td colspan="2">{{.Description}}</td>
{{end}}
{{end}}
</tr>{{end}}
</tbody>
</table>
{{end}}
{{define "medi-group-ica"}}
{{$root := .}}
<table class="table">
<thead>
<tr>
<th class="column-date">Datum</th>
<th class="column-code">Type</th>
<th class="column-date">ICPC</th>
<th class="column-name" colspan="2">Beschrijving</th>
{{if eq $root.Extra.verbose_icpc_description "true"}}
<th class="column-desc"></th>
{{else}}
{{end}}
</tr>
</thead>
<tbody>
{{range .Indicators}}
<tr{{if .Anchor}} id="medigroup-{{.Anchor}}"{{end}}>
<td>{{.Date}}</td>
<td>{{.Type}}</td>
<td>{{.ICPC}}</td>
<td>{{.Title}}</td>
{{if eq $root.Extra.verbose_icpc_description "true"}}
<td>{{.Description}}</td>
{{else}}
<td></td>
{{end}}
</tr>{{end}}
</tbody>
</table>
{{end}}
<div class="seg seg-hdr" data-his="{{.HIS}}">
<h3>Huisarts</h3>
<table class="table">
{{template "person-row" .GP}}
{{if .Pharmacy.Anchor}}{{template "person-row" .Pharmacy}}{{end}}
</table>
{{if .Practitioners}}
<h4 data-fold-toggle="practicioners" data-fold-hide="1">Medebehandelaren</h4>
<div data-fold="practicioners">
<table class="table">{{range .Practitioners}}
{{template "person-row" .}}{{end}}
</table>
</div>
{{end}}
</div>
<div class="seg seg-hdr">
<h3>Patiënt</h3>
<table class="patient">
<tr>
<th>Naam:
<td>{{.Patient.Name}}
{{if .Patient.BSN}}
<tr>
<th>BSN:
<td>{{.Patient.BSN}}
{{end}}
<tr>
<th>Geboortedatum:
<td>{{.Patient.Birthdate}}
<tr>
<th>Adres:
<td>{{.Patient.Address}}
<tr>
<th>Geslacht:
<td>{{.Patient.Gender}}
{{range .Patient.OtherFields}}
<tr>
<th>{{.Key}}:
<td{{if eq .Key "Toegepast filter"}} title="Een filter is een (standaard) filter dat door een huisarts over alle of een deel van de patiëntendossiers wordt toegepast. De minimale professionele samenvatting (PS) bevat een samenvatting van alleen zeer recente informatie uit het huisartsdossier, met alleen actuele / chronische medicatie. Dit is minder informatie dan de standaard NHG PS, die journaalregels tot 4 maanden terug, of de laatste 5 consultverslagen kan bevatten. Houd er svp rekening mee dat geen enkel type PS alle, of gegarandeerd volledige of correcte informatie bevat."{{end}}>{{.Value}}
{{end}}
</table>
</div>
{{if .Memo}}
<div class="seg seg-hdr">
<h3>Memo</h3>
{{.Memo}}
</div>
{{end}}
{{if .Episodes}}
<div class="seg seg-hdr">
<h3>Episodelijst</h3>
{{template "medi-group-epi" .}}
</div>
{{end}}
{{if .Indicators}}
<div class="seg seg-hdr">
<h3>Contra-indicaties, interacties en allergie&#235;n</h3>
{{template "medi-group-ica" .}}
</div>
{{end}}
{{if .Medication}}
<div class="seg seg-hdr">
<h3 data-fold-toggle="med-table">Medicatieoverzicht</h3>
<table data-fold="med-table" class="table">
<thead>
<tr>
<th class="column-large-name">Recept</th>
<th class="column-instr">Gebruiksvoorschrift</th>
<th class="column-quantity">Hoeveelheid</th>
<th class="column-date">Ingang</th>
<th class="column-date">Vervalt</th>
</tr>
</thead>
<tbody>
{{range $index, $med := .Medication}}
{{if gt (len $med.Fold) 1}}
<tr data-fold-toggle="medfold-{{$index}}" data-fold-hide="1">
<td>{{$med.Recipe}}</td>
<td>{{$med.Instructions}}</td>
<td>{{ YieldDosage $med }}</td>
<td>{{$med.StartDate}}</td>
<td>{{$med.EndDate}}</td>
</tr>
{{range $fold := $med.Fold}}
<tr data-fold="medfold-{{$index}}">
<td></td>
<td>{{ if ne $med.Instructions $fold.Instructions}}{{$fold.Instructions}}{{else}}{{end}}</td>
<td>{{ YieldDosage $fold }}</td>
<td>{{$fold.StartDate}}</td>
<td>{{$fold.EndDate}}</td>
</tr>
{{end}}
{{else}}
<tr>
<td>{{$med.Recipe}}</td>
<td>{{$med.Instructions}}</td>
<td>{{ YieldDosage $med }}</td>
<td>{{$med.StartDate}}</td>
<td>{{$med.EndDate}}</td>
</tr>
{{end}}
{{end }}
</tbody>
</table>
</div>
{{end}}
{{if .Journal}}
<div class="seg seg-hdr-white">
<h3>Journaal</h3>
</div>
{{range .Journal}}
<div class="seg seg-hdr">
<div class="seg-date">{{.Date}}</div>
<div class="seg-content-offset" title="{{.AuthorName}}{{if .Kind}} ({{.Kind}}){{end}}">
{{if .Results}}
<div class="row">
<table class="results">
<thead>
<tr>
<th class="column-result-id">Identificatie</th>
<th class="column-result-name">Resultaat</th>
<th class="column-result-desc">Beschrijving</th>
</tr>
</thead>
<tbody>
{{range .Results}}
<tr {{if .Episode}}data-episode="{{.Episode}}"{{end}}><td>{{.Kind}}</td>
<td>{{.Result}}</td>
<td>{{if eq .Identification "Buiten normaalwaarde"}}<strong>{{.Identification}}</strong>{{else}}{{.Identification}}{{end}}
{{if and .Identification .Description}}<br>{{end}}
{{.Description}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{range .Lines}}
<div class="row"{{if .Episode}} data-episode="{{.Episode}}"{{end}}>
<table class="SOEPkind">
<tr>
{{if .SOEP}}
<th style="padding-top: 10px; vertical-align: top;">{{.SOEP}}:
<td style="padding-top: 10px; vertical-align: top;">{{ConvertLinebreaks .Text}}
{{end}}
{{if .Kind}}
<th style="padding-top: 10px; vertical-align: top;">{{.Kind}}{{if .Text}}:{{end}}
<td style="padding-top: 10px; vertical-align: top;">{{ConvertLinebreaks .Text}}
{{end}}
</tr>
</table>
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}
{{if .Extra.csrf_token }}
<div class="seg seg-hdr-white">
<br>
<h3 id="feedback-header"><button onClick="feedbackHeaderClicked()">Notitie schrijven</button></h3>
<form id="feedback-form" class="hidden" method="POST" onSubmit="sendFeedback(); return false"> <!-- TODO: contruct proper URL in golang -->
<input type="hidden" name="csrf_token" value="{{.Extra.csrf_token}}" id="csrf">
<p><br><textarea id="feedback-textarea" rows="7" cols="50"></textarea>
<p><br><input type="submit" value="Opsturen">
</form>
</div>
{{end}}
</div>
<script>
(function() {
var folds = document.querySelectorAll('[data-fold-toggle]');
for (var i = 0; i < folds.length; i++) {
(function(btn) {
var t = btn.getAttribute('data-fold-toggle');
var fds = document.querySelectorAll('[data-fold="' + t + '"]');
btn.classList.add('fold-toggle');
var hidden = false;
var sync = function() {
if (hidden) {
btn.classList.add('fold-toggle-hidden');
} else {
btn.classList.remove('fold-toggle-hidden');
}
for (var j = 0; j < fds.length; j++) {
var e = fds[j].classList;
if (hidden) {
e.add('fold-hidden');
} else {
e.remove('fold-hidden');
}
}
setTimeout(send_height_to_parent, 0);
};
if (btn.getAttribute('data-fold-hide') == '1') {
hidden = true;
}
btn.addEventListener('click', function() {
hidden = !hidden;
sync();
});
sync();
})(folds[i]);
}
})();
function feedbackHeaderClicked()
{
toggle('feedback-form')
var textarea = document.getElementById('feedback-textarea')
textarea.focus()
}
function toggle(id)
{
var elem = document.getElementById(id)
if (elem)
{
var className = elem.getAttribute('class')
if (className == 'hidden')
{
elem.setAttribute('class', '')
}
else
{
elem.setAttribute('class', 'hidden')
}
}
}
function getXMLHttpRequest()
{
if (window.XMLHttpRequest)
{
return new XMLHttpRequest()
}
else if (window.ActiveXObject)
{
return new ActiveXObject('Microsoft.XMLHTTP')
}
}
</script></body></html>

Binary file not shown.

@ -0,0 +1,72 @@
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"sync"
"google.golang.org/grpc"
"whiteboxsystems.nl/openkvpoc/openkv"
)
var rpcAddr = "0.0.0.0:7777"
var uiAddr = "0.0.0.0:7075"
var binFolder = "./bin"
func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
wg := &sync.WaitGroup{}
if os.Getenv("BIN_FOLDER") != "" {
binFolder = os.Getenv("BIN_FOLDER")
}
openapisrv := NewServer()
openapisrv.LoadData("./data/data.db")
opts := []grpc.ServerOption{
// grpc.UnaryInterceptor(openapisrv.EnsureValidModule),
}
grpcServer := grpc.NewServer(opts...)
go func() {
lis, err := net.Listen("tcp", rpcAddr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
openkv.RegisterOpenKVServer(grpcServer, openapisrv)
log.Printf("RPC Listening on %v", rpcAddr)
wg.Add(1)
grpcServer.Serve(lis)
}()
srv := NewUIServer(uiAddr)
srv.data = openapisrv.data
go func() {
wg.Add(1)
srv.ListenAndServe()
}()
<-stop
go func() {
grpcServer.GracefulStop()
wg.Done()
log.Println("Shutdown RPC server")
}()
go func() {
log.Println("Shutdown UI server...")
srv.Shutdown(context.Background())
wg.Done()
log.Println("UI Server shutdown...")
}()
wg.Wait()
}

@ -0,0 +1,51 @@
package model
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
func GetDB(location string) (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open(location), &gorm.Config{})
if err != nil {
return nil, err
}
// Migrate the schema
db.AutoMigrate(&sharedmodel.Registration{})
db.AutoMigrate(&sharedmodel.Connection{})
db.AutoMigrate(&sharedmodel.Service{})
db.AutoMigrate(&sharedmodel.AuthConfig{})
db.AutoMigrate(&sharedmodel.ProtocolConfig{})
db.AutoMigrate(&sharedmodel.ServiceConfig{})
db.AutoMigrate(&sharedmodel.Subscription{})
var cnt int64
db.Model(&sharedmodel.Service{}).Count(&cnt)
if cnt == 0 {
db.Create(&sharedmodel.Service{
Name: "KIS",
Description: "Ketenzorg informatie systeem",
SubscriptionPolicy: openkv.SubscriptionPolicy_optin,
ConsentPolicy: openkv.ConsentPolicy_presumed,
ServiceID: "voorbeeld:kiss",
FetchProtocols: sharedmodel.ProtocolArray{
{
"https://whiteboxsystems.nl/protospecs/whitebox-fetch/http",
[]openkv.AuthMethod{openkv.AuthMethod_APIToken},
},
},
PushProtocols: sharedmodel.ProtocolArray{
{
"https://whiteboxsystems.nl/protospecs/whitebox-push/http",
[]openkv.AuthMethod{openkv.AuthMethod_APIToken},
},
},
})
}
return db, nil
}

@ -0,0 +1,408 @@
package main
import (
"context"
"fmt"
"log"
"github.com/gofrs/uuid"
"google.golang.org/grpc/metadata"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/kisservice/model"
"whiteboxsystems.nl/openkvpoc/openkv"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
var errNotAuthorized = fmt.Errorf("Not Authorized")
var errInvalidService = fmt.Errorf("Invalid service")
var errActiveServiceConfig = fmt.Errorf("Service not activated")
type OpenKVServer struct {
openkv.UnimplementedOpenKVServer
data *gorm.DB
}
func (srv *OpenKVServer) LoadData(location string) error {
var err error
srv.data, err = model.GetDB(location)
return err
}
func requireConnection(db *gorm.DB, ctx context.Context) (*sharedmodel.Connection, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Printf("No metadata")
return nil, errNotAuthorized
}
connection := &sharedmodel.Connection{}
if a, ok := md["authorization"]; !ok {
log.Printf("No token provided")
return nil, errNotAuthorized
} else {
if err := db.Preload("AuthMethod").Raw(`
SELECT conn.*
FROM connections conn
JOIN auth_configs a on conn.auth_config_id = a.id WHERE a.method = ? and a.raw = ?
`, openkv.AuthMethod_APIToken, a[0]).Scan(connection).Error; err != nil {
log.Printf("Invalid token; err: %v;", err)
return nil, errNotAuthorized
}
}
return connection, nil
}
func requireService(db *gorm.DB, conn *sharedmodel.Connection, serviceID string) (*sharedmodel.ServiceConfig, error) {
service := &sharedmodel.Service{}
if err := db.Where("service_id = ?", serviceID).First(service).Error; err != nil {
return nil, errInvalidService
}
srvConfig := &sharedmodel.ServiceConfig{}
if err := db.Where("connection_id = ? and enabled = ? and service_id = ?", conn.ID, true, service.ID).First(srvConfig).Error; err != nil {
return nil, errActiveServiceConfig
}
return srvConfig, nil
}
func (srv *OpenKVServer) GetMetadata(
ctx context.Context, in *openkv.GetMetadataRequest,
) (*openkv.GetMetadataResponse, error) {
log.Printf("Got metadata request")
services := []*openkv.ServiceDefinition{}
presentServices := []sharedmodel.Service{}
srv.data.Find(&presentServices)
for _, service := range presentServices {
services = append(services, &openkv.ServiceDefinition{
Name: service.Name,
Description: service.Description,
Id: service.ServiceID,
SubscriptionPolicy: service.SubscriptionPolicy,
ConsentPolicy: service.ConsentPolicy,
FetchProtocols: service.GetFetchProtocols(),
PushProtocols: service.GetPushProtocols(),
})
}
resp := &openkv.GetMetadataResponse{
Supplier: "Voorbeeld",
System: "Voorbeeld KIS",
Services: services,
Success: true,
}
return resp, nil
}
func (srv *OpenKVServer) Register(
ctx context.Context, in *openkv.RegisterRequest,
) (*openkv.RegisterResponse, error) {
ref, _ := uuid.NewV4()
psk, _ := uuid.NewV4()
reg := &sharedmodel.Registration{
Reference: ref.String(),
OrganisationId: in.OrganisationId,
OrganisationIdSystem: in.OrganisationIdSystem,
OrganisationDisplayName: in.OrganisationDisplayName,
PSK: psk.String()[0:6],
Status: sharedmodel.RegistrationStatusPending,
}
reg.SetAuthConfig(in.Auth)
srv.data.Create(reg)
resp := &openkv.RegisterResponse{
Reference: ref.String(),
Success: true,
}
log.Printf("Got registration request from %v; ref: %v; PSK: %v", reg.OrganisationDisplayName, reg.Reference, reg.PSK)
return resp, nil
}
func (srv *OpenKVServer) CompleteRegistration(
ctx context.Context, in *openkv.CompleteRegistrationRequest,
) (*openkv.CompleteRegistrationResponse, error) {
registration := &sharedmodel.Registration{}
if err := srv.data.Preload("AuthConfig").Where("reference = ? and status = ?", in.Reference, sharedmodel.RegistrationStatusPending).First(registration).Error; err != nil {
log.Printf("Invalid ref")
return nil, errNotAuthorized
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Printf("No metadata")
return nil, errNotAuthorized
}
// The keys within metadata.MD are normalized to lowercase.
// See: https://godoc.org/google.golang.org/grpc/metadata#New
if a, ok := md["authorization"]; !ok {
log.Printf("No token provided")
return nil, errNotAuthorized
} else {
if a[0] != registration.AuthConfig.Raw {
log.Printf("Invalid token; eXpected: %v; got: %v", registration.AuthConfig.Raw, a[0])
return nil, errNotAuthorized
}
}
resp := &openkv.CompleteRegistrationResponse{}
if in.RegistrationToken != registration.PSK {
resp.Error = &openkv.Error{
Code: 1,
Message: "Invalid PSK",
}
return resp, nil
}
conn := &sharedmodel.Connection{
OrganisationId: registration.OrganisationId,
OrganisationIdSystem: registration.OrganisationIdSystem,
OrganisationDisplayName: registration.OrganisationDisplayName,
AuthConfig: registration.AuthConfig.Clone(),
}
srv.data.Create(conn)
registration.Status = sharedmodel.RegistrationStatusCompleted
srv.data.Save(registration)
resp.Success = true
return resp, nil
}
func (srv *OpenKVServer) ConfigService(
ctx context.Context, in *openkv.ConfigServiceRequest,
) (*openkv.ConfigServiceResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
service := &sharedmodel.Service{}
if err := srv.data.Where("service_id = ?", in.Service).First(service).Error; err != nil {
return nil, fmt.Errorf("Invalid service: %v", service.ServiceID)
}
cnf := &sharedmodel.ServiceConfig{}
if err := srv.data.Where("connection_id = ? and service_id = ?", conn.ID, service.ID).First(cnf); err != nil {
cnf.ConnectionID = conn.ID
cnf.ServiceID = service.ID
}
log.Printf("Update service config %v for conn: %v", cnf.Service.Name, conn.ID)
cnf.Enabled = in.Enabled
// TODO actually init authdata
cnf.PushProtocol = &sharedmodel.ProtocolConfig{
Protocol: in.Push.Protocol,
AuthConfig: sharedmodel.NewAuthConfig(in.Push.Auth),
}
cnf.PushProtocol.SetConfig(in.Push.Config)
cnf.PushProtocol.AuthConfig.Raw = "3333"
cnf.FetchProtocol = &sharedmodel.ProtocolConfig{
Protocol: in.Fetch.Protocol,
AuthConfig: sharedmodel.NewAuthConfig(in.Fetch.Auth),
}
cnf.FetchProtocol.SetConfig(in.Fetch.Config)
cnf.FetchProtocol.AuthConfig.Raw = "3333"
if cnf.ID == 0 {
if err := srv.data.Create(cnf).Error; err != nil {
return nil, err
}
} else {
if err := srv.data.Save(cnf).Error; err != nil {
return nil, err
}
}
// If disabled unsubscribe all subscriptions
if !cnf.Enabled {
srv.data.Unscoped().Where("service_config_id = ?", cnf.ID).Delete(&sharedmodel.Subscription{})
}
resp := &openkv.ConfigServiceResponse{
Success: true,
Service: in.Service,
Enabled: in.Enabled,
Fetch: &openkv.ServiceConfig{
Protocol: "https://hl7.nl/fhir",
Auth: &openkv.AuthConfig{
Method: openkv.AuthMethod_APIToken,
Config: &openkv.AuthConfig_ApiTokenConfig{&openkv.APITokenConfig{Token: "3333"}},
},
},
}
return resp, nil
}
func (srv *OpenKVServer) UpdateSubscriptions(
ctx context.Context, in *openkv.UpdateSubscriptionsRequest,
) (*openkv.UpdateSubscriptionsResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
serviceConfig, err := requireService(srv.data, conn, in.ServiceId)
if err != nil {
return nil, err
}
subscriptionErrors := []*openkv.SubscriptionError{}
if err := srv.data.Transaction(func(tx *gorm.DB) error {
for idx, sd := range in.SubscriptionData {
subscription := &sharedmodel.Subscription{}
err := srv.data.Where(
"subject_external_id = ? and subject_external_id_system = ? and service_config_id = ?",
sd.Subject.ExternalId,
sd.Subject.ExternalIdSystem,
serviceConfig.ID,
).First(subscription).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
} else if err != nil && sd.Subscribe {
sub := &sharedmodel.Subscription{
SubjectExternalId: sd.Subject.ExternalId,
SubjectExternalIdSystem: sd.Subject.ExternalIdSystem,
SubjectName: sd.Subject.Name,
SubjectBirthdate: sd.Subject.Birthdate,
ServiceConfigID: serviceConfig.ID,
}
// TODO check if it is valid metadata for the specified protocol
sub.SetProtocolMeta(sd.ProtocolMeta)
if err := srv.data.Create(sub).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be persisted; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err),
},
})
}
log.Printf("add subscription: %v", sd.Subject.ExternalId)
continue
} else if err != nil && !sd.Subscribe {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrCodeAlreadySubscribed,
Message: fmt.Sprintf("Subject with id: %v (%v) already unsubscribed", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem),
},
})
continue
} else if !sd.Subscribe {
if err := srv.data.Unscoped().Delete(subscription).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be removed; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err)},
})
}
log.Printf("delete subscription: %v", sd.Subject.ExternalId)
continue
}
subscription.SubjectExternalId = sd.Subject.ExternalId
subscription.SubjectExternalIdSystem = sd.Subject.ExternalIdSystem
subscription.SubjectName = sd.Subject.Name
subscription.SubjectBirthdate = sd.Subject.Birthdate
subscription.SetProtocolMeta(sd.ProtocolMeta)
if err := srv.data.Save(subscription).Error; err != nil {
subscriptionErrors = append(subscriptionErrors, &openkv.SubscriptionError{
Index: int32(idx),
Error: &openkv.Error{
Code: openkv.ErrServiceException,
Message: fmt.Sprintf("Subject with id: %v (%v) could not be updated; %v", sd.Subject.ExternalId, sd.Subject.ExternalIdSystem, err)},
})
continue
}
log.Printf("update subscription: %v", sd.Subject.ExternalId)
}
return nil
}); err != nil {
return nil, err
}
resp := &openkv.UpdateSubscriptionsResponse{
Success: true,
Errors: subscriptionErrors,
}
return resp, nil
}
func (srv *OpenKVServer) ListSubscriptions(
ctx context.Context, in *openkv.ListSubscriptionsRequest,
) (*openkv.ListSubscriptionsResponse, error) {
conn, err := requireConnection(srv.data, ctx)
if err != nil {
return nil, err
}
serviceConfig, err := requireService(srv.data, conn, in.ServiceId)
if err != nil {
return nil, err
}
subscriptions := []*sharedmodel.Subscription{}
srv.data.Where("service_config_id = ?", serviceConfig.ID).Find(&subscriptions)
subs := []*openkv.SubscriptionData{}
for _, s := range subscriptions {
meta := map[string]string{}
s.GetProtocolMeta(&meta)
subs = append(subs, &openkv.SubscriptionData{
Subject: &openkv.PatientMeta{
ExternalId: s.SubjectExternalId,
ExternalIdSystem: s.SubjectExternalIdSystem,
Name: s.SubjectName,
Birthdate: s.SubjectBirthdate,
},
ProtocolMeta: meta,
})
}
resp := &openkv.ListSubscriptionsResponse{
Success: true,
SubscriptionData: subs,
}
return resp, nil
}
func NewServer() *OpenKVServer {
return &OpenKVServer{}
}

@ -0,0 +1,277 @@
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"sync"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"whiteboxsystems.nl/openkvpoc/kisservice/model"
"whiteboxsystems.nl/openkvpoc/sharedmodel"
)
type UIService struct {
srv *http.Server
inited bool
data *gorm.DB
}
func (srv *UIService) LoadData(location string) error {
var err error
srv.data, err = model.GetDB(location)
return err
}
func (srv *UIService) Addr() string {
if srv.srv == nil {
return ""
}
return srv.srv.Addr
}
func (srv *UIService) ListenAndServe() {
if !srv.inited {
srv.init()
}
log.Println("Listening on %v", srv.srv.Addr)
srv.srv.ListenAndServe()
}
func (srv *UIService) Shutdown(ctx context.Context) error {
return srv.srv.Shutdown(ctx)
}
func (srv *UIService) init() {
r := srv.srv.Handler.(*gin.Engine)
r.LoadHTMLGlob("templates/*")
r.Static("/assets", "./assets")
r.Use(srv.Authenticate)
r.GET("/", func(c *gin.Context) {
c.Redirect(301, "/ui")
})
r.GET("/ui", srv.GetIndex)
r.GET("/ui/*page", srv.GetIndex)
r.GET("/api/connections", srv.GetConnections)
r.GET("/api/connections/:connID", srv.GetConnection)
r.GET("/api/connections/:connID/:serviceID", srv.GetSubscriptions)
r.GET("/api/connections/:connID/:serviceID/:patientID", srv.GetPatient)
r.GET("/api/registrations", srv.GetRegistrations)
// r.GET("/api/systems/:sysid/patients", srv.GetPatients)
// r.GET("/api/systems/:sysid/patients/:patid", srv.GetPatient)
srv.inited = true
}
func (srv *UIService) GetIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}
func (srv *UIService) GetConnection(c *gin.Context) {
connID := c.Param("connID")
connection := &sharedmodel.Connection{}
srv.data.Where("id = ?", connID).Find(&connection)
c.JSON(200, connection)
}
func (srv *UIService) GetSubscriptions(c *gin.Context) {
connID := c.Param("connID")
serviceID := c.Param("serviceID")
serviceConfig := &sharedmodel.ServiceConfig{}
srv.data.Preload("Service").Preload("Subscriptions").Where("connection_id = ? and id = ?", connID, serviceID).Find(&serviceConfig)
c.JSON(200, serviceConfig)
}
func (srv *UIService) GetPatient(c *gin.Context) {
connID := c.Param("connID")
serviceID := c.Param("serviceID")
patientID := c.Param("patientID")
patient := &sharedmodel.Subscription{}
serviceConfig := &sharedmodel.ServiceConfig{}
if err := srv.data.Preload("FetchProtocol").Preload("FetchProtocol.AuthConfig").Preload("Service").Where("connection_id = ? and id = ?", connID, serviceID).Find(&serviceConfig).Error; err != nil {
c.AbortWithError(500, err)
return
}
srv.data.Where("service_config_id = ? and id = ?", serviceID, patientID).Find(&patient)
protoconfig := map[string]string{}
protometa := map[string]string{}
err := serviceConfig.FetchProtocol.UnmarshalConfig(&protoconfig)
log.Println(err, protoconfig)
err = patient.GetProtocolMeta(&protometa)
log.Println(err, protometa)
url := fmt.Sprintf("%v/%v/%v", protoconfig["url"], "patients", protometa["patientID"])
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", serviceConfig.FetchProtocol.AuthConfig.Raw)
resp, err := http.DefaultClient.Do(req)
if err != nil {
c.AbortWithError(500, err)
return
}
cmd := exec.Command(binFolder+"/ediviewer", binFolder+"/medeur.json", binFolder+"/template.html", "/dev/stdin")
sin, err := cmd.StdinPipe()
if err != nil {
log.Println("[ediviewer] Failed to open stdin pipe:", err)
c.AbortWithError(500, err)
return
}
defer sin.Close()
serr, err := cmd.StderrPipe()
if err != nil {
log.Println("[ediviewer] Failed to open stderr pipe:", err)
c.AbortWithError(500, err)
return
}
defer serr.Close()
sout, err := cmd.StdoutPipe()
if err != nil {
log.Println("[ediviewer] Failed to open stdout pipe:", err)
c.AbortWithError(500, err)
return
}
defer sout.Close()
if err := cmd.Start(); err != nil {
log.Println("[ediviewer] Failed to start:", err)
c.AbortWithError(500, err)
return
}
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
if _, err := io.Copy(sin, resp.Body); err != nil {
log.Println("[ediviewer] Error reading EDIFACT:", err)
c.AbortWithError(500, err)
}
}()
go func() {
defer wg.Done()
if _, err := io.Copy(c.Writer, sout); err != nil {
log.Println("[ediviewer] Error output EDIFACT:", err)
c.AbortWithError(500, err)
}
}()
go func() {
defer wg.Done()
if _, err := io.Copy(os.Stderr, serr); err != nil {
log.Println("[ediviewer] Error stderr EDIFACT:", err)
c.AbortWithError(500, err)
}
}()
wg.Wait()
if err := cmd.Wait(); err != nil {
log.Println("[ediviewer] Failed:", err)
c.AbortWithError(500, err)
}
}
func (srv *UIService) Authenticate(c *gin.Context) {
// authHeader := c.Request.Header.Get("Authorization")
// log.Printf("authHeader: %v", authHeader)
// if authHeader != "1111" {
// c.Status(401)
// c.Abort()
// }
}
func (srv *UIService) GetConnections(c *gin.Context) {
connections := []*sharedmodel.Connection{}
srv.data.Preload("Services").Preload("Services.Service").Preload("Services.Subscriptions").Find(&connections)
c.JSON(200, connections)
}
func (srv *UIService) GetRegistrations(c *gin.Context) {
registrations := []*sharedmodel.Registration{}
srv.data.Where("status = ?", sharedmodel.RegistrationStatusPending).Find(&registrations)
c.JSON(200, registrations)
}
// func (srv *UIService) GetSystems(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
// func (srv *UIService) GetPatients(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
// func (srv *UIService) GetPatient(c *gin.Context) {
// id := c.Param("id")
// for _, p := range patients {
// if p.PatientID == id {
// f, err := os.Open(path.Join("./data/patients", p.EDI))
// if err != nil {
// c.Error(err)
// return
// }
// io.Copy(c.Writer, f)
// return
// }
// }
// c.JSON(404, nil)
// }
func NewUIServer(addr string) *UIService {
srv := &UIService{srv: &http.Server{
Addr: addr,
Handler: gin.Default(),
}}
return srv
}

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KIS</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/js/index.js"></script>
</body>
</html>
Loading…
Cancel
Save