renamed
This commit is contained in:
Generated
+29758
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "front-new",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/material": "^5.11.8",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"axios": "^1.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/semi": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "argsIgnorePattern": "^_" }
|
||||
],
|
||||
"no-case-declarations": "off"
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Diagnose } from "../src/types";
|
||||
const data: Diagnose[] = [
|
||||
{
|
||||
code: "M24.2",
|
||||
name: "Disorder of ligament",
|
||||
latin: "Morbositas ligamenti",
|
||||
},
|
||||
{
|
||||
code: "M51.2",
|
||||
name: "Other specified intervertebral disc displacement",
|
||||
latin: "Alia dislocatio disci intervertebralis specificata",
|
||||
},
|
||||
{
|
||||
code: "S03.5",
|
||||
name: "Sprain and strain of joints and ligaments of other and unspecified parts of head",
|
||||
latin:
|
||||
"Distorsio et/sive distensio articulationum et/sive ligamentorum partium aliarum sive non specificatarum capitis",
|
||||
},
|
||||
{
|
||||
code: "J10.1",
|
||||
name: "Influenza with other respiratory manifestations, other influenza virus codeentified",
|
||||
latin:
|
||||
"Influenza cum aliis manifestationibus respiratoriis ab agente virali codeentificato",
|
||||
},
|
||||
{
|
||||
code: "J06.9",
|
||||
name: "Acute upper respiratory infection, unspecified",
|
||||
latin: "Infectio acuta respiratoria superior non specificata",
|
||||
},
|
||||
{
|
||||
code: "Z57.1",
|
||||
name: "Occupational exposure to radiation",
|
||||
},
|
||||
{
|
||||
code: "N30.0",
|
||||
name: "Acute cystitis",
|
||||
latin: "Cystitis acuta",
|
||||
},
|
||||
{
|
||||
code: "H54.7",
|
||||
name: "Unspecified visual loss",
|
||||
latin: "Amblyopia NAS",
|
||||
},
|
||||
{
|
||||
code: "J03.0",
|
||||
name: "Streptococcal tonsillitis",
|
||||
latin: "Tonsillitis (palatina) streptococcica",
|
||||
},
|
||||
{
|
||||
code: "L60.1",
|
||||
name: "Onycholysis",
|
||||
latin: "Onycholysis",
|
||||
},
|
||||
{
|
||||
code: "Z74.3",
|
||||
name: "Need for continuous supervision",
|
||||
},
|
||||
{
|
||||
code: "L20",
|
||||
name: "Atopic dermatitis",
|
||||
latin: "Atopic dermatitis",
|
||||
},
|
||||
{
|
||||
code: "F43.2",
|
||||
name: "Adjustment disorders",
|
||||
latin: "Perturbationes adaptationis",
|
||||
},
|
||||
{
|
||||
code: "S62.5",
|
||||
name: "Fracture of thumb",
|
||||
latin: "Fractura [ossis/ossium] pollicis",
|
||||
},
|
||||
{
|
||||
code: "H35.29",
|
||||
name: "Other proliferative retinopathy",
|
||||
latin: "Alia retinopathia proliferativa",
|
||||
},
|
||||
];
|
||||
|
||||
export default data;
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Patient, Gender } from '../src/types';
|
||||
|
||||
const patients: Patient[] = [
|
||||
{
|
||||
id: 'd2773336-f723-11e9-8f0b-362b9e155667',
|
||||
name: 'John McClane',
|
||||
dateOfBirth: '1986-07-09',
|
||||
ssn: '090786-122X',
|
||||
gender: Gender.Male,
|
||||
occupation: 'New york city cop',
|
||||
entries: [
|
||||
{
|
||||
id: 'd811e46d-70b3-4d90-b090-4535c7cf8fb1',
|
||||
date: '2015-01-02',
|
||||
type: 'Hospital',
|
||||
specialist: 'MD House',
|
||||
diagnosisCodes: ['S62.5'],
|
||||
description:
|
||||
"Healing time appr. 2 weeks. patient doesn't remember how he got the injury.",
|
||||
discharge: {
|
||||
date: '2015-01-16',
|
||||
criteria: 'Thumb has healed.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'd2773598-f723-11e9-8f0b-362b9e155667',
|
||||
name: 'Martin Riggs',
|
||||
dateOfBirth: '1979-01-30',
|
||||
ssn: '300179-777A',
|
||||
gender: Gender.Male,
|
||||
occupation: 'Cop',
|
||||
entries: [
|
||||
{
|
||||
id: 'fcd59fa6-c4b4-4fec-ac4d-df4fe1f85f62',
|
||||
date: '2019-08-05',
|
||||
type: 'OccupationalHealthcare',
|
||||
specialist: 'MD House',
|
||||
employerName: 'HyPD',
|
||||
diagnosisCodes: ['Z57.1', 'Z74.3', 'M51.2'],
|
||||
description:
|
||||
'Patient mistakenly found himself in a nuclear plant waste site without protection gear. Very minor radiation poisoning. ',
|
||||
sickLeave: {
|
||||
startDate: '2019-08-05',
|
||||
endDate: '2019-08-28',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'd27736ec-f723-11e9-8f0b-362b9e155667',
|
||||
name: 'Hans Gruber',
|
||||
dateOfBirth: '1970-04-25',
|
||||
ssn: '250470-555L',
|
||||
gender: Gender.Other,
|
||||
occupation: 'Technician',
|
||||
entries: [],
|
||||
},
|
||||
{
|
||||
id: 'd2773822-f723-11e9-8f0b-362b9e155667',
|
||||
name: 'Dana Scully',
|
||||
dateOfBirth: '1974-01-05',
|
||||
ssn: '050174-432N',
|
||||
gender: Gender.Female,
|
||||
occupation: 'Forensic Pathologist',
|
||||
entries: [
|
||||
{
|
||||
id: 'b4f4eca1-2aa7-4b13-9a18-4a5535c3c8da',
|
||||
date: '2019-10-20',
|
||||
specialist: 'MD House',
|
||||
type: 'HealthCheck',
|
||||
description: 'Yearly control visit. Cholesterol levels back to normal.',
|
||||
healthCheckRating: 0,
|
||||
},
|
||||
{
|
||||
id: 'fcd59fa6-c4b4-4fec-ac4d-df4fe1f85f62',
|
||||
date: '2019-09-10',
|
||||
specialist: 'MD House',
|
||||
type: 'OccupationalHealthcare',
|
||||
employerName: 'FBI',
|
||||
description: 'Prescriptions renewed.',
|
||||
},
|
||||
{
|
||||
id: '37be178f-a432-4ba4-aac2-f86810e36a15',
|
||||
date: '2018-10-05',
|
||||
specialist: 'MD House',
|
||||
type: 'HealthCheck',
|
||||
description:
|
||||
'Yearly control visit. Due to high cholesterol levels recommended to eat more vegetables.',
|
||||
healthCheckRating: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'd2773c6e-f723-11e9-8f0b-362b9e155667',
|
||||
name: 'Matti Luukkainen',
|
||||
dateOfBirth: '1971-04-09',
|
||||
ssn: '090471-8890',
|
||||
gender: Gender.Male,
|
||||
occupation: 'Digital evangelist',
|
||||
entries: [
|
||||
{
|
||||
id: '54a8746e-34c4-4cf4-bf72-bfecd039be9a',
|
||||
date: '2019-05-01',
|
||||
specialist: 'Dr Byte House',
|
||||
type: 'HealthCheck',
|
||||
description: 'Digital overdose, very bytestatic. Otherwise healthy.',
|
||||
healthCheckRating: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default patients;
|
||||
|
||||
+4996
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"tsc": "tsc",
|
||||
"dev": "ts-node-dev ./src/index.ts",
|
||||
"lint": "eslint --ext .ts ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"eslint": "^8.43.0",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import express from "express";
|
||||
import diagnoses from "../data/diagnoses";
|
||||
import {
|
||||
addPatient,
|
||||
addEntry,
|
||||
getPatient,
|
||||
getAllPatients,
|
||||
} from "./services/patients";
|
||||
import { toNewPatient,toNewEntry } from "./utils";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/api/ping", (_req, res) => {
|
||||
res.send("pong");
|
||||
});
|
||||
app.get("/api/diagnoses", (_req, res) => {
|
||||
res.json(diagnoses);
|
||||
});
|
||||
app.get("/api/patients", (_req, res) => {
|
||||
res.json(getAllPatients());
|
||||
});
|
||||
app.get("/api/patients/:id", (req, res) => {
|
||||
res.json(getPatient(req.params.id));
|
||||
});
|
||||
app.post("/api/patients/:id/entries", (req, res) => {
|
||||
const newEntry = toNewEntry(req.body);
|
||||
res.json(addEntry(newEntry, req.params.id));
|
||||
});
|
||||
app.post("/api/patients", (req, res) => {
|
||||
const newPatient = toNewPatient(req.body);
|
||||
res.json(addPatient(newPatient));
|
||||
});
|
||||
|
||||
const PORT = 3001;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running at port${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import patients from "../../data/patients";
|
||||
import { Patient, NewPatient, NonSensitivePatient, Entry } from "../types";
|
||||
import { v1 as uuid } from "uuid";
|
||||
|
||||
export const getAllPatients = (): NonSensitivePatient[] =>
|
||||
patients.map(({ id, name, dateOfBirth, gender, occupation, entries }) => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
dateOfBirth,
|
||||
gender,
|
||||
occupation,
|
||||
entries,
|
||||
};
|
||||
});
|
||||
|
||||
export const addPatient = (patient: NewPatient): Patient => {
|
||||
const newPatient = { ...patient, id: uuid() };
|
||||
patients.push(newPatient);
|
||||
return newPatient;
|
||||
};
|
||||
|
||||
export const getPatient = (id: string): NonSensitivePatient => {
|
||||
const patient = patients.find((el) => el.id === id);
|
||||
if (patient)
|
||||
return {
|
||||
id: patient.id,
|
||||
name: patient.name,
|
||||
dateOfBirth: patient.dateOfBirth,
|
||||
gender: patient.gender,
|
||||
occupation: patient.occupation,
|
||||
entries: patient.entries,
|
||||
};
|
||||
throw new Error("patient not found");
|
||||
};
|
||||
|
||||
export const addEntry = (entry:Omit<Entry,'id'>, id: string): NonSensitivePatient => {
|
||||
const patient = patients.find((el) => el.id === id);
|
||||
if (patient) {
|
||||
const newEntry = {...entry,id:uuid()} as Entry
|
||||
const entries = patient.entries.concat(newEntry);
|
||||
const updatedPatient = { ...patient, entries };
|
||||
return {
|
||||
id: updatedPatient.id,
|
||||
name: updatedPatient.name,
|
||||
dateOfBirth: updatedPatient.dateOfBirth,
|
||||
gender: updatedPatient.gender,
|
||||
occupation: updatedPatient.occupation,
|
||||
entries: updatedPatient.entries,
|
||||
};
|
||||
}
|
||||
throw new Error("patient not found");
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
export interface Diagnose {
|
||||
code: string;
|
||||
name: string;
|
||||
latin?: string;
|
||||
}
|
||||
interface HospitalEntry extends BaseEntry {
|
||||
type: "Hospital";
|
||||
diagnosisCodes: string[];
|
||||
discharge: {
|
||||
date: string;
|
||||
criteria: string;
|
||||
};
|
||||
}
|
||||
interface OccupationalHealthcareEntry extends BaseEntry {
|
||||
type: "OccupationalHealthcare";
|
||||
employerName: string;
|
||||
diagnosisCodes?: string[];
|
||||
sickLeave?: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
}
|
||||
interface HealthCheck extends BaseEntry {
|
||||
type: "HealthCheck";
|
||||
employerName?: string;
|
||||
healthCheckRating: number;
|
||||
}
|
||||
interface BaseEntry {
|
||||
id: string;
|
||||
date: string;
|
||||
specialist: string;
|
||||
description: string;
|
||||
}
|
||||
export type Entry = HospitalEntry | OccupationalHealthcareEntry | HealthCheck;
|
||||
|
||||
export interface Patient {
|
||||
id: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
occupation: string;
|
||||
name: string;
|
||||
ssn: string;
|
||||
entries: Entry[];
|
||||
}
|
||||
|
||||
export type NonSensitivePatient = Omit<Patient, "ssn">;
|
||||
|
||||
export type NewPatient = Omit<Patient, "id">;
|
||||
|
||||
export enum Gender {
|
||||
Male = "male",
|
||||
Female = "female",
|
||||
Other = "other",
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Entry, Gender, NewPatient } from "./types";
|
||||
|
||||
export const toNewPatient = (object: unknown): NewPatient => {
|
||||
if (!object || typeof object !== "object")
|
||||
throw new Error("Incorrect or request body");
|
||||
if (
|
||||
"name" in object &&
|
||||
"gender" in object &&
|
||||
"dateOfBirth" in object &&
|
||||
"occupation" in object &&
|
||||
"ssn" in object
|
||||
) {
|
||||
const newPatient: NewPatient = {
|
||||
name: parseString(object.name, "Incorrect or missing name"),
|
||||
gender: parseGender(object.gender),
|
||||
dateOfBirth: parseDate(object.dateOfBirth),
|
||||
occupation: parseString(object.occupation, "Incorrect or missing ssn"),
|
||||
ssn: parseString(object.name, "Incorrect or missing ssn"),
|
||||
entries: [],
|
||||
};
|
||||
return newPatient;
|
||||
}
|
||||
throw new Error("Incorrect data some fields are missing");
|
||||
};
|
||||
|
||||
const parseDate = (date: unknown): string => {
|
||||
if (!date || !isString(date) || !isDate(date))
|
||||
throw new Error("Incorrect or missing date");
|
||||
return date;
|
||||
};
|
||||
|
||||
const parseGender = (gender: unknown): Gender => {
|
||||
if (!gender || !isString(gender) || !isGender(gender))
|
||||
throw new Error("Incorrect or missing gender");
|
||||
return gender;
|
||||
};
|
||||
|
||||
const parseNumber = (number: unknown, message: string): number => {
|
||||
if (!number || !isNumber(number)) throw new Error(message);
|
||||
return number;
|
||||
};
|
||||
|
||||
const parseString = (name: unknown, message: string): string => {
|
||||
if (!name || !isString(name)) throw new Error(message);
|
||||
return name;
|
||||
};
|
||||
|
||||
const isString = (text: unknown): text is string => {
|
||||
return typeof text === "string" || text instanceof String;
|
||||
};
|
||||
|
||||
const isNumber = (number: unknown): number is number => {
|
||||
return typeof number === "number" || number instanceof Number;
|
||||
};
|
||||
|
||||
const isDate = (date: string): boolean => {
|
||||
return Boolean(Date.parse(date));
|
||||
};
|
||||
|
||||
const isGender = (gender: string): gender is Gender => {
|
||||
return Object.values(Gender)
|
||||
.map((v) => v.toString())
|
||||
.includes(gender);
|
||||
};
|
||||
type UnionOmit<T, K extends string | number | symbol> = T extends unknown
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
|
||||
export const toNewEntry = (object: unknown): UnionOmit<Entry, "id"> => {
|
||||
if (!object || typeof object !== "object") throw new Error("incorrect body");
|
||||
if (
|
||||
"date" in object &&
|
||||
"specialist" in object &&
|
||||
"description" in object &&
|
||||
"type" in object
|
||||
) {
|
||||
console.log(object.type)
|
||||
switch (object.type) {
|
||||
case "Hospital":
|
||||
if (
|
||||
"diagnosisCodes" in object &&
|
||||
"discharge" in object &&
|
||||
typeof object.discharge === "object" &&
|
||||
object.discharge !== null &&
|
||||
"date" in object.discharge &&
|
||||
"criteria" in object.discharge
|
||||
) {
|
||||
let diagnosisCodes = undefined;
|
||||
if (isDiagnosisCodes(object.diagnosisCodes))
|
||||
diagnosisCodes = [...object.diagnosisCodes];
|
||||
else throw new Error("error in diagnoses codes");
|
||||
return {
|
||||
date: parseString(object.date, "Incorrect date"),
|
||||
specialist: parseString(object.specialist, "Incorrect specialist"),
|
||||
description: parseString(
|
||||
object.description,
|
||||
"Incorrect description"
|
||||
),
|
||||
type: object.type,
|
||||
discharge: {
|
||||
date: parseString(object.discharge.date, "error in discharge date"),
|
||||
criteria: parseString(object.discharge.criteria, "error in discharge criteria"),
|
||||
},
|
||||
diagnosisCodes,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
"diagnosisCodes or discharge missing for type Hospital"
|
||||
);
|
||||
case "HealthCheck":
|
||||
if ("healthCheckRating" in object) {
|
||||
let employerName = undefined;
|
||||
if ("employerName" in object && employerName !== "undefined")
|
||||
employerName = object.employerName;
|
||||
return {
|
||||
date: parseString(object.date, "Incorrect date"),
|
||||
specialist: parseString(object.specialist, "Incorrect specialist"),
|
||||
description: parseString(
|
||||
object.description,
|
||||
"Incorrect description"
|
||||
),
|
||||
type: object.type,
|
||||
healthCheckRating: parseNumber(
|
||||
object.healthCheckRating,
|
||||
"Incorrect HealthCheckRating"
|
||||
),
|
||||
employerName: parseString(employerName, "Incorrect employername"),
|
||||
};
|
||||
}
|
||||
throw new Error("healthCheckRating missing for type HealthCheck");
|
||||
|
||||
case "OccupationalHealthcare":
|
||||
if ("employerName" in object) {
|
||||
let diagnosisCodes: undefined | string[] = undefined;
|
||||
let sickLeave: undefined | { startDate: string; endDate: string } =
|
||||
undefined;
|
||||
if (
|
||||
"diagnosisCodes" in object &&
|
||||
object.diagnosisCodes !== undefined
|
||||
) {
|
||||
diagnosisCodes = isDiagnosisCodes(object.diagnosisCodes)
|
||||
? object.diagnosisCodes
|
||||
: undefined;
|
||||
}
|
||||
if (
|
||||
"sickLeave" in object &&
|
||||
object.sickLeave !== undefined &&
|
||||
object.sickLeave !== null &&
|
||||
typeof object.sickLeave === "object" &&
|
||||
"startDate" in object.sickLeave &&
|
||||
"endDate" in object.sickLeave
|
||||
) {
|
||||
sickLeave = {
|
||||
startDate: parseString(object.sickLeave.startDate, ""),
|
||||
endDate: parseString(object.sickLeave.endDate, ""),
|
||||
};
|
||||
}
|
||||
return {
|
||||
date: parseString(object.date, "Incorrect date"),
|
||||
specialist: parseString(object.specialist, "Incorrect specialist"),
|
||||
description: parseString(
|
||||
object.description,
|
||||
"Incorrect description"
|
||||
),
|
||||
type: object.type,
|
||||
...(isDiagnosisCodes(diagnosisCodes) && {diagnosisCodes}),
|
||||
//diagnosisCodes,
|
||||
employerName: parseString(
|
||||
object.employerName,
|
||||
"Incorrect employername"
|
||||
),
|
||||
sickLeave,
|
||||
};
|
||||
}
|
||||
throw new Error("employerName missing in OccupationalHealthcare");
|
||||
default:
|
||||
throw new Error("wrong type");
|
||||
}
|
||||
}
|
||||
throw new Error("erro");
|
||||
};
|
||||
|
||||
const isDiagnosisCodes = (
|
||||
diagnosisCodes: unknown
|
||||
): diagnosisCodes is string[] => {
|
||||
if (diagnosisCodes instanceof Array) {
|
||||
diagnosisCodes.forEach((el) => {
|
||||
if (typeof el === "string") throw new Error("");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"outDir": "./build/",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";
|
||||
import { Button, Divider, Container, Typography } from "@mui/material";
|
||||
|
||||
import { apiBaseUrl } from "./constants";
|
||||
import { Diagnosis, Patient } from "./types";
|
||||
|
||||
import patientService from "./services/patients";
|
||||
import diagnosesService from "./services/diagnoses";
|
||||
import PatientListPage from "./components/PatientListPage";
|
||||
import SinglePatient from "./components/SinglePatient";
|
||||
|
||||
const App = () => {
|
||||
const [patients, setPatients] = useState<Patient[]>([]);
|
||||
const [diagnoses, setDiagnoses] = useState<Diagnosis[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
void axios.get<void>(`${apiBaseUrl}/ping`);
|
||||
|
||||
const fetchPatientList = async () => {
|
||||
const patients = await patientService.getAll();
|
||||
setPatients(patients);
|
||||
};
|
||||
const fetchDiagnosesList = async () => {
|
||||
const diagnoses = await diagnosesService.getAllDiagnoses();
|
||||
setDiagnoses(diagnoses);
|
||||
};
|
||||
void fetchPatientList();
|
||||
void fetchDiagnosesList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<Router>
|
||||
<Container>
|
||||
<Typography variant="h3" style={{ marginBottom: "0.5em" }}>
|
||||
Patientor
|
||||
</Typography>
|
||||
<Button component={Link} to="/" variant="contained" color="primary">
|
||||
Home
|
||||
</Button>
|
||||
<Divider hidden />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<PatientListPage
|
||||
patients={patients}
|
||||
setPatients={setPatients}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/patient/:id"
|
||||
element={
|
||||
<SinglePatient patients={patients} diagnoses={diagnoses} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Container>
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState, SyntheticEvent } from "react";
|
||||
import { Entry } from "../../types";
|
||||
|
||||
import {
|
||||
TextField,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Grid,
|
||||
Button,
|
||||
SelectChangeEvent,
|
||||
} from "@mui/material";
|
||||
|
||||
import { PatientFormValues, Gender } from "../../types";
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: PatientFormValues) => void;
|
||||
}
|
||||
|
||||
interface GenderOption {
|
||||
value: Gender;
|
||||
label: string;
|
||||
}
|
||||
const entries: Entry[] = [];
|
||||
const genderOptions: GenderOption[] = Object.values(Gender).map((v) => ({
|
||||
value: v,
|
||||
label: v.toString(),
|
||||
}));
|
||||
|
||||
const AddPatientForm = ({ onCancel, onSubmit }: Props) => {
|
||||
const [name, setName] = useState("");
|
||||
const [occupation, setOccupation] = useState("");
|
||||
const [ssn, setSsn] = useState("");
|
||||
const [dateOfBirth, setDateOfBirth] = useState("");
|
||||
const [gender, setGender] = useState(Gender.Other);
|
||||
|
||||
const onGenderChange = (event: SelectChangeEvent<string>) => {
|
||||
event.preventDefault();
|
||||
if (typeof event.target.value === "string") {
|
||||
const value = event.target.value;
|
||||
const gender = Object.values(Gender).find((g) => g.toString() === value);
|
||||
if (gender) {
|
||||
setGender(gender);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addPatient = (event: SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
onSubmit({
|
||||
name,
|
||||
occupation,
|
||||
ssn,
|
||||
dateOfBirth,
|
||||
gender,
|
||||
entries,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={addPatient}>
|
||||
<TextField
|
||||
label="Name"
|
||||
fullWidth
|
||||
value={name}
|
||||
onChange={({ target }) => setName(target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Social security number"
|
||||
fullWidth
|
||||
value={ssn}
|
||||
onChange={({ target }) => setSsn(target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Date of birth"
|
||||
placeholder="YYYY-MM-DD"
|
||||
fullWidth
|
||||
value={dateOfBirth}
|
||||
onChange={({ target }) => setDateOfBirth(target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Occupation"
|
||||
fullWidth
|
||||
value={occupation}
|
||||
onChange={({ target }) => setOccupation(target.value)}
|
||||
/>
|
||||
|
||||
<InputLabel style={{ marginTop: 20 }}>Gender</InputLabel>
|
||||
<Select
|
||||
label="Gender"
|
||||
fullWidth
|
||||
value={gender}
|
||||
onChange={onGenderChange}
|
||||
>
|
||||
{genderOptions.map((option) => (
|
||||
<MenuItem key={option.label} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
style={{ float: "left" }}
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
style={{
|
||||
float: "right",
|
||||
}}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPatientForm;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Dialog, DialogTitle, DialogContent, Divider, Alert } from '@mui/material';
|
||||
|
||||
import AddPatientForm from "./AddPatientForm";
|
||||
import { PatientFormValues } from "../../types";
|
||||
|
||||
interface Props {
|
||||
modalOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: PatientFormValues) => void;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const AddPatientModal = ({ modalOpen, onClose, onSubmit, error }: Props) => (
|
||||
<Dialog fullWidth={true} open={modalOpen} onClose={() => onClose()}>
|
||||
<DialogTitle>Add a new patient</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
<AddPatientForm onSubmit={onSubmit} onCancel={onClose}/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
export default AddPatientModal;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Rating } from '@mui/material';
|
||||
import { Favorite } from '@mui/icons-material';
|
||||
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
type BarProps = {
|
||||
rating: number;
|
||||
showText: boolean;
|
||||
};
|
||||
|
||||
const StyledRating = styled(Rating)({
|
||||
iconFilled: {
|
||||
color: "#ff6d75",
|
||||
},
|
||||
iconHover: {
|
||||
color: "#ff3d47",
|
||||
}
|
||||
});
|
||||
|
||||
const HEALTHBAR_TEXTS = [
|
||||
"The patient is in great shape",
|
||||
"The patient has a low risk of getting sick",
|
||||
"The patient has a high risk of getting sick",
|
||||
"The patient has a diagnosed condition",
|
||||
];
|
||||
|
||||
const HealthRatingBar = ({ rating, showText }: BarProps) => {
|
||||
return (
|
||||
<div className="health-bar">
|
||||
<StyledRating
|
||||
readOnly
|
||||
value={4 - rating}
|
||||
max={4}
|
||||
icon={<Favorite fontSize="inherit" />}
|
||||
/>
|
||||
|
||||
{showText ? <p>{HEALTHBAR_TEXTS[rating]}</p> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HealthRatingBar;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
Button,
|
||||
TableHead,
|
||||
Typography,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import axios from "axios";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { PatientFormValues, Patient } from "../../types";
|
||||
import AddPatientModal from "../AddPatientModal";
|
||||
|
||||
import HealthRatingBar from "../HealthRatingBar";
|
||||
|
||||
import patientService from "../../services/patients";
|
||||
|
||||
interface Props {
|
||||
patients: Patient[];
|
||||
setPatients: React.Dispatch<React.SetStateAction<Patient[]>>;
|
||||
}
|
||||
|
||||
const PatientListPage = ({ patients, setPatients }: Props) => {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const openModal = (): void => setModalOpen(true);
|
||||
|
||||
const closeModal = (): void => {
|
||||
setModalOpen(false);
|
||||
setError(undefined);
|
||||
};
|
||||
|
||||
const submitNewPatient = async (values: PatientFormValues) => {
|
||||
try {
|
||||
const patient = await patientService.create(values);
|
||||
setPatients(patients.concat(patient));
|
||||
setModalOpen(false);
|
||||
} catch (e: unknown) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
if (e?.response?.data && typeof e?.response?.data === "string") {
|
||||
const message = e.response.data.replace(
|
||||
"Something went wrong. Error: ",
|
||||
""
|
||||
);
|
||||
console.error(message);
|
||||
setError(message);
|
||||
} else {
|
||||
setError("Unrecognized axios error");
|
||||
}
|
||||
} else {
|
||||
console.error("Unknown error", e);
|
||||
setError("Unknown error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<Box>
|
||||
<Typography align="center" variant="h6">
|
||||
Patient list
|
||||
</Typography>
|
||||
</Box>
|
||||
<Table style={{ marginBottom: "1em" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Gender</TableCell>
|
||||
<TableCell>Occupation</TableCell>
|
||||
<TableCell>Health Rating</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.values(patients).map((patient: Patient) => (
|
||||
<TableRow key={patient.id}>
|
||||
<TableCell>
|
||||
<Link to={`/patient/${patient.id}`}>{patient.name}</Link>
|
||||
</TableCell>
|
||||
<TableCell>{patient.gender}</TableCell>
|
||||
<TableCell>{patient.occupation}</TableCell>
|
||||
<TableCell>
|
||||
<HealthRatingBar showText={false} rating={1} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<AddPatientModal
|
||||
modalOpen={modalOpen}
|
||||
onSubmit={submitNewPatient}
|
||||
error={error}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<Button variant="contained" onClick={() => openModal()}>
|
||||
Add New Patient
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PatientListPage;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import patientService from "../../services/patients";
|
||||
|
||||
const EntryForm = (): JSX.Element => {
|
||||
const [description, setDescription] = useState("");
|
||||
const [date, setDate] = useState("");
|
||||
const [specialist, setSpecialist] = useState("");
|
||||
const [healthCheckRating, setHealthCheckRating] = useState("");
|
||||
const [employerName, setEmployerName] = useState<string>("");
|
||||
|
||||
const addEntry = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
const newEntry = {
|
||||
description,
|
||||
date,
|
||||
specialist,
|
||||
healthCheckRating: Number(healthCheckRating),
|
||||
employerName,
|
||||
type: "HealthCheck" as const,
|
||||
};
|
||||
|
||||
patientService.createEntry(newEntry);
|
||||
|
||||
setDescription("");
|
||||
setDate("");
|
||||
setSpecialist("");
|
||||
setHealthCheckRating("");
|
||||
setEmployerName("");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>New HealthCheck entry</div>
|
||||
<form onSubmit={addEntry}>
|
||||
<label>
|
||||
Description
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Date
|
||||
<input
|
||||
type="text"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Specialist
|
||||
<input
|
||||
type="text"
|
||||
value={specialist}
|
||||
onChange={(e) => setSpecialist(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
HealthCheck rating
|
||||
<input
|
||||
type="text"
|
||||
value={healthCheckRating}
|
||||
onChange={(e) => setHealthCheckRating(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Employer name
|
||||
<input
|
||||
type="text"
|
||||
value={employerName}
|
||||
onChange={(e) => setEmployerName(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button>Add</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntryForm;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { HealthCheck } from "../../types";
|
||||
|
||||
interface HealthCheckProps {
|
||||
entry: HealthCheck;
|
||||
}
|
||||
const HealthCheckEntry = ({ entry }: HealthCheckProps): JSX.Element => {
|
||||
return (
|
||||
<li>
|
||||
<div>date: {entry.date}</div>
|
||||
<div>description: {entry.description}</div>
|
||||
<div>specialist: {entry.specialist}</div>
|
||||
{"employerName" in entry ? (
|
||||
<div>employer name: {entry.employerName}</div>
|
||||
) : null}
|
||||
<div>health check rating: {entry.healthCheckRating}</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
export default HealthCheckEntry;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Diagnosis, HospitalEntry } from "../../types";
|
||||
|
||||
interface HospitalEntryProps {
|
||||
entry: HospitalEntry;
|
||||
diagnoses: Diagnosis[];
|
||||
}
|
||||
const HospitalEntryElement = ({ entry, diagnoses }: HospitalEntryProps): JSX.Element => {
|
||||
return (
|
||||
<li>
|
||||
<div>date: {entry.date}</div>
|
||||
<div>description: {entry.description}</div>
|
||||
<div>specialist: {entry.specialist}</div>
|
||||
<div>
|
||||
discharge
|
||||
<div>date {entry.discharge.date}</div>
|
||||
<div>criteria {entry.discharge.criteria}</div>
|
||||
</div>
|
||||
{"diagnosisCodes" in entry ? (
|
||||
<ul>
|
||||
{entry.diagnosisCodes?.map((diagnosis, inx) => (
|
||||
<li key={inx}>
|
||||
{diagnosis}:
|
||||
{diagnoses.find((el) => el.code === diagnosis)?.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
export default HospitalEntryElement;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Diagnosis, OccupationalHealthcareEntry } from "../../types";
|
||||
|
||||
interface OccupationalEntryProps {
|
||||
entry: OccupationalHealthcareEntry;
|
||||
diagnoses: Diagnosis[];
|
||||
}
|
||||
const OccupationalEntry = ({
|
||||
entry,
|
||||
diagnoses,
|
||||
}: OccupationalEntryProps): JSX.Element => {
|
||||
return (
|
||||
<li >
|
||||
<div>date: {entry.date}</div>
|
||||
<div>description: {entry.description}</div>
|
||||
<div>specialist: {entry.specialist}</div>
|
||||
<div>employerName: {entry.employerName}</div>
|
||||
{"sickLeave" in entry ? (
|
||||
<>
|
||||
<div>sick leave</div>
|
||||
<div>start date:{entry.sickLeave?.startDate}</div>
|
||||
<div>end date:{entry.sickLeave?.endDate}</div>
|
||||
</>
|
||||
) : null}
|
||||
{"diagnosisCodes" in entry ? (
|
||||
<ul>
|
||||
{entry.diagnosisCodes?.map((diagnosis, inx) => (
|
||||
<li key={inx}>
|
||||
{diagnosis}:{diagnoses.find((el) => el.code === diagnosis)?.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
export default OccupationalEntry;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Diagnosis, Patient } from "../../types";
|
||||
import HospitalEntry from "./HospitalEntry";
|
||||
import OccupationalEntry from "./OccupationalEntry";
|
||||
import HealthCheckEntry from "./HealthCheck";
|
||||
import EntryForm from "./EntryForm";
|
||||
|
||||
interface SinglePatientProps {
|
||||
patients: Patient[];
|
||||
diagnoses: Diagnosis[];
|
||||
}
|
||||
const SinglePatient = ({
|
||||
patients,
|
||||
diagnoses,
|
||||
}: SinglePatientProps): JSX.Element => {
|
||||
const { id } = useParams();
|
||||
const patient = patients.find((el) => el.id === id);
|
||||
if (!patient) return <></>;
|
||||
return (
|
||||
<>
|
||||
<h3>{patient.name}</h3>
|
||||
<div>gender: {patient.gender}</div>
|
||||
{patient.ssn ? <div>ssn: {patient.ssn}</div> : null}
|
||||
{patient.dateOfBirth ? (
|
||||
<div>date of birth: {patient.dateOfBirth}</div>
|
||||
) : null}
|
||||
<div>ocupation: {patient.occupation}</div>
|
||||
<EntryForm />
|
||||
<div>entries</div>
|
||||
<ul>
|
||||
{patient.entries.map((entry) => {
|
||||
switch (entry.type) {
|
||||
case "Hospital":
|
||||
return (
|
||||
<HospitalEntry
|
||||
entry={entry}
|
||||
diagnoses={diagnoses}
|
||||
key={entry.id}
|
||||
/>
|
||||
);
|
||||
case "OccupationalHealthcare":
|
||||
return (
|
||||
<OccupationalEntry
|
||||
entry={entry}
|
||||
diagnoses={diagnoses}
|
||||
key={entry.id}
|
||||
/>
|
||||
);
|
||||
case "HealthCheck":
|
||||
return <HealthCheckEntry entry={entry} key={entry.id} />;
|
||||
default:
|
||||
const _exhaustiveCheck: never = entry;
|
||||
return _exhaustiveCheck;
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SinglePatient;
|
||||
@@ -0,0 +1 @@
|
||||
export const apiBaseUrl = 'http://localhost:3001/api';
|
||||
@@ -0,0 +1,5 @@
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
|
||||
@@ -0,0 +1,14 @@
|
||||
import axios from "axios";
|
||||
import { Diagnosis } from "../types";
|
||||
import { apiBaseUrl } from "../constants";
|
||||
|
||||
const getAllDiagnoses = async () => {
|
||||
const { data } = await axios.get<Diagnosis[]>(`${apiBaseUrl}/diagnoses`);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
getAllDiagnoses,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import axios from "axios";
|
||||
import { Patient, PatientFormValues, EntryFormValues, Entry } from "../types";
|
||||
|
||||
import { apiBaseUrl } from "../constants";
|
||||
|
||||
const getAll = async () => {
|
||||
const { data } = await axios.get<Patient[]>(`${apiBaseUrl}/patients`);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const create = async (object: PatientFormValues) => {
|
||||
const { data } = await axios.post<Patient>(`${apiBaseUrl}/patients`, object);
|
||||
|
||||
return data;
|
||||
};
|
||||
const createEntry = async (object: EntryFormValues) => {
|
||||
const { data } = await axios.post<Entry>(
|
||||
`${apiBaseUrl}/patients/:id/entries`,
|
||||
object
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
getAll,
|
||||
create,
|
||||
createEntry,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
export interface Diagnosis {
|
||||
code: string;
|
||||
name: string;
|
||||
latin?: string;
|
||||
}
|
||||
|
||||
export enum Gender {
|
||||
Male = "male",
|
||||
Female = "female",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export interface HospitalEntry extends BaseEntry {
|
||||
type: "Hospital";
|
||||
diagnosisCodes: string[];
|
||||
discharge: {
|
||||
date: string;
|
||||
criteria: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OccupationalHealthcareEntry extends BaseEntry {
|
||||
type: "OccupationalHealthcare";
|
||||
employerName: string;
|
||||
diagnosisCodes?: string[];
|
||||
sickLeave?: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
}
|
||||
export interface HealthCheck extends BaseEntry {
|
||||
type: "HealthCheck";
|
||||
employerName?: string;
|
||||
healthCheckRating: number;
|
||||
}
|
||||
|
||||
interface BaseEntry {
|
||||
id: string;
|
||||
date: string;
|
||||
specialist: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type Entry = HospitalEntry | OccupationalHealthcareEntry | HealthCheck;
|
||||
|
||||
export interface Patient {
|
||||
id: string;
|
||||
name: string;
|
||||
occupation: string;
|
||||
gender: Gender;
|
||||
ssn?: string;
|
||||
dateOfBirth?: string;
|
||||
entries: Entry[];
|
||||
}
|
||||
|
||||
export type PatientFormValues = Omit<Patient, "id">;
|
||||
type UnionOmit<T, K extends string | number | symbol> = T extends unknown
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
export type EntryFormValues = UnionOmit<Entry, "id">;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user