This commit is contained in:
QkoSad
2023-08-08 16:02:54 +03:00
commit 0a7a469d56
315 changed files with 426907 additions and 0 deletions
+29758
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -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

+43
View File
@@ -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

+25
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+30
View File
@@ -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;
+116
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+27
View File
@@ -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"
}
}
+39
View File
@@ -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");
};
+54
View File
@@ -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",
}
+193
View File
@@ -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;
};
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES6",
"outDir": "./build/",
"module": "commonjs",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
}
}
+67
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
export const apiBaseUrl = 'http://localhost:3001/api';
+5
View File
@@ -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,
};
+60
View File
@@ -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">;
+26
View File
@@ -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"
]
}