diff --git a/package.json b/package.json index 99018e6..26a1c61 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "@types/react-dom": "^16.9.0", "@types/react-router": "^5.1.4", "@types/react-router-dom": "^5.1.3", - "fhir.js": "^0.0.22", "interact.js": "^1.2.8", "interactjs": "^1.8.2", "moment": "^2.24.0", @@ -46,4 +45,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index 78d11e1..de8cb6f 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -13,7 +13,7 @@ interface Props { const Header = ({ patient }: Props) => { return ( - <BPNavbar id="navbar" className="bp3-dark"> + <BPNavbar id="navbar"> <BPNavbar.Group align={Alignment.LEFT}> <BPNavbar.Heading> <Link className="linkNavbar" to={ROUTE_HOME}> @@ -32,8 +32,8 @@ const Header = ({ patient }: Props) => { <Icon icon="user" /> <div>Nom médecin</div> <BPNavbar.Divider /> - <Button icon="more" minimal /> - <Button icon="log-out" minimal /> + <Button icon="more" minimal className="headerButtons" /> + <Button icon="log-out" minimal className="headerButtons" /> </BPNavbar.Group> </BPNavbar> ); diff --git a/src/components/header/style.css b/src/components/header/style.css index 3d47ca8..fedbda7 100644 --- a/src/components/header/style.css +++ b/src/components/header/style.css @@ -1,7 +1,8 @@ @import "~@blueprintjs/core/lib/css/blueprint.css"; #navbar { - background-color: black; + background-color: #6ad4a9; + color: white; } .bp3-icon { @@ -14,11 +15,16 @@ } #titleNavbar { - margin: 0px 0px 0px 9px; - color: white; + margin: 2px 0px 0px 9px; cursor: pointer; text-decoration: none; + color: white; } + .linkNavbar:hover { text-decoration: none; } + +.headerButtons svg { + fill: white !important; +} \ No newline at end of file diff --git a/src/components/patientPage/hospitSummary/index.tsx b/src/components/patientPage/hospitSummary/index.tsx index 7a7cd03..6d214e7 100644 --- a/src/components/patientPage/hospitSummary/index.tsx +++ b/src/components/patientPage/hospitSummary/index.tsx @@ -4,23 +4,20 @@ import "./style.css"; const HospitSummary = () => { return ( - <> - <div className="fullHeight"> - <H3> - <Icon icon={"pulse"} /> Hospitalisation - </H3> - <div className="centeredName"> - <H5 className="marginRight">{"Chirurgie cardiaque".toUpperCase()}</H5> - <span className="bp3-text-muted">15/05/2019 - 19/05/2019</span> - </div> - Les informations sur l'hospitalisation sélectionnée seront indiquées - ici. - <br /> - <br /> - Par défaut, cette carte affichera les infos de la dernière - hospitalisation. + <div className="fullHeight"> + <H3> + <Icon icon={"pulse"} /> Hospitalisation + </H3> + <div className="centeredName"> + <H5 className="marginRight">{"Chirurgie cardiaque".toUpperCase()}</H5> + <span className="bp3-text-muted">15/05/2019 - 19/05/2019</span> </div> - </> + Les informations sur l'hospitalisation sélectionnée seront indiquées ici. + <br /> + <br /> + Par défaut, cette carte affichera les infos de la dernière + hospitalisation. + </div> ); }; diff --git a/src/components/patientPage/index.tsx b/src/components/patientPage/index.tsx index 6a65cc5..958e84d 100644 --- a/src/components/patientPage/index.tsx +++ b/src/components/patientPage/index.tsx @@ -20,7 +20,8 @@ const PatientPage = ({ patientId }: Props) => { React.useEffect(() => { const fetchPatientData = async () => { - const patient: any = await getPatientData(patientId); + const patient: any = await getPatientData(patientId, true); + setPatientData(patient); }; fetchPatientData(); diff --git a/src/components/patientPage/patientCard/index.tsx b/src/components/patientPage/patientCard/index.tsx index bd1b7dc..e5463e0 100644 --- a/src/components/patientPage/patientCard/index.tsx +++ b/src/components/patientPage/patientCard/index.tsx @@ -16,42 +16,20 @@ const PatientCard = ({ patient }: Props) => { getPatientNumberCard and getSubjectNumberCard are now rendering PatientInfo elements with click option which print the results on the console. */ const getPatientNumberCard = ( - object: "observations" | "conditions", + object: + | "observations" + | "conditions" + | "allergyIntolerances" + | "episodesOfCare", writtenName: string ) => { - if (patient[object]) { - return ( - <div - onClick={() => { - console.log(writtenName + " : ", patient[object].entry); - }} - > - <PatientGeneralInfo - type={writtenName} - content={patient[object].total.toString()} - /> - </div> - ); - } - }; + const resourceNumber = patient[object] ? patient[object].length : 0; - const getSubjectNumberCard = ( - object: "allergyIntolerances" | "episodesOfCare", - writtenName: string - ) => { - if (patient[object]) - return ( - <div - onClick={() => { - console.log(writtenName + " : ", patient[object].entry); - }} - > - <PatientGeneralInfo - type={writtenName} - content={patient[object].total.toString()} - /> - </div> - ); + return ( + <div> + <PatientGeneralInfo type={writtenName} content={resourceNumber} /> + </div> + ); }; const getSubjectNameDiv = () => { @@ -98,13 +76,13 @@ const PatientCard = ({ patient }: Props) => { /> } - {getSubjectNumberCard("allergyIntolerances", "Allergies")} + {getPatientNumberCard("allergyIntolerances", "Allergies")} {getPatientNumberCard("observations", "Observations")} {getPatientNumberCard("conditions", "Conditions")} - {getSubjectNumberCard("episodesOfCare", "Hospitalisations")} + {getPatientNumberCard("episodesOfCare", "Hospitalisations")} </> ); }; diff --git a/src/components/patientPage/patientCard/patientAgeInfo/index.tsx b/src/components/patientPage/patientCard/patientAgeInfo/index.tsx index bc6c077..019d7af 100644 --- a/src/components/patientPage/patientCard/patientAgeInfo/index.tsx +++ b/src/components/patientPage/patientCard/patientAgeInfo/index.tsx @@ -17,7 +17,7 @@ const PatientAgeInfo = ({ type, birthDate, age }: Props) => { <Tag round={true}>{type}</Tag> </div> <div className="patientInfoContent"> - {birthDate}{" "} + {birthDate} <div className="secondContent"> ({age?.toString()} ans) </div> </div> </div> diff --git a/src/components/patientPage/patientCard/patientGeneralInfo/index.tsx b/src/components/patientPage/patientCard/patientGeneralInfo/index.tsx index ddc6aa8..0e856c2 100644 --- a/src/components/patientPage/patientCard/patientGeneralInfo/index.tsx +++ b/src/components/patientPage/patientCard/patientGeneralInfo/index.tsx @@ -9,7 +9,8 @@ interface Props { } const PatientGeneralInfo = ({ type, content }: Props) => { - if (content) + if (content !== undefined) + //Avoid to write "Inconnu" instead of 0 return ( <div className="patientInfo"> <div className="patientTag"> diff --git a/src/components/patients/index.tsx b/src/components/patients/index.tsx index ca34199..b43efb3 100644 --- a/src/components/patients/index.tsx +++ b/src/components/patients/index.tsx @@ -2,19 +2,55 @@ import React from "react"; import Header from "components/header"; import PatientTable from "components/patients/patientTable"; import SearchTool from "components/patients/searchTool"; +import { + getPatients, + getPatientsPerQuery, + requestNextPatients +} from "services/api"; +import { PatientBundle } from "types"; import { Card, Elevation } from "@blueprintjs/core"; + import "./style.css"; +interface Props { + onSearch: Function; + searchItem: any; +} + const Patients = () => { + const [patientBundle, setPatientBundle] = React.useState({} as PatientBundle); + const getNextPatients = async () => { + const patBundle = await requestNextPatients(patientBundle); + if (patBundle) setPatientBundle(patBundle); + }; + const handleSearch = async (searchName: String, searchParams: any) => { + const bundle: PatientBundle = await getPatientsPerQuery( + searchName, + searchParams + ); + setPatientBundle(bundle); + }; + + React.useEffect(() => { + const fetchPatients = async () => { + const bundle: PatientBundle = await getPatients(); + setPatientBundle(bundle); + }; + fetchPatients(); + }, []); + return ( <> <Header /> <div className="homeSearch"> - <Card elevation={Elevation.THREE} className="searchTool"> - <SearchTool /> + <Card elevation={Elevation.ZERO} className="searchTool"> + <SearchTool onSearch={handleSearch} /> </Card> - <Card elevation={Elevation.THREE} className="patientTable"> - <PatientTable /> + <Card elevation={Elevation.ZERO} className="patientTable"> + <PatientTable + bundle={patientBundle} + updateNextPatients={getNextPatients} + /> </Card> </div> </> diff --git a/src/components/patients/patientTable/index.tsx b/src/components/patients/patientTable/index.tsx index 8e22255..73c01fb 100644 --- a/src/components/patients/patientTable/index.tsx +++ b/src/components/patients/patientTable/index.tsx @@ -1,90 +1,87 @@ import React from "react"; -import { Link } from "react-router-dom"; -import { Cell, Column, Table } from "@blueprintjs/table"; -import { Icon, H3 } from "@blueprintjs/core"; -import { ROUTE_PATIENT } from "../../../constants"; -import { getPatients, getCount } from "services/api"; -import { Patient } from "types"; +import { Icon, H3, Button } from "@blueprintjs/core"; +import { PatientBundle } from "types"; +import PatientCardTable from "components/patients/patientTable/patientCardTable"; +import { PATIENT_SHOWN } from "../../../constants"; import "./style.css"; -const PatientTable = () => { - const [patients, setPatients] = React.useState([] as Patient[]); - const [patientCount, setPatientCount] = React.useState(""); +interface Props { + bundle: PatientBundle; + updateNextPatients: Function; +} - const renderPatientAttribute = ( - attribute: "id" | "identifier" | "firstName" | "lastName" | "age", - index: number - ) => ( - <Cell> - <React.Fragment> - <Link to={`${ROUTE_PATIENT}/${patients[index].id}`}> - {patients[index][attribute] || "unknown"} - </Link> - </React.Fragment> - </Cell> - ); +const PatientTable = ({ bundle, updateNextPatients }: Props) => { + const [pageIndex, setPageIndex] = React.useState(0); + const [leftDisabled, setLeftDisabled] = React.useState(true); + + const getNextPage = async () => { + /* + * Show next patient table page + */ + if ((pageIndex + 2) * PATIENT_SHOWN >= bundle.patients.length) { + /* + * fetch next bundle page (nexLink) to get more patients + */ + if (bundle.nextLink) { + updateNextPatients(); + } + } + setPageIndex(pageIndex + 1); + setLeftDisabled(false); + }; + + const getPatientCardTable = () => { + /* + * getPatientCardTable function + * reander each PatientCardTable for each patient + */ + const patientcards = + Object.keys(bundle).length !== 0 && + bundle.patients + .slice(pageIndex * PATIENT_SHOWN, (pageIndex + 1) * PATIENT_SHOWN) + .map(x => <PatientCardTable patient={x} key={x.id} />); - React.useEffect(() => { - const fetchPatients = async () => { - const patients: Patient[] = await getPatients(); - setPatients(patients); - const count = await getCount("Patient", { _summary: "count" }); - setPatientCount(count); - }; - fetchPatients(); - }, []); + return patientcards; + }; + const getPreviousPage = async () => { + /* + * Show previous patient table page + */ + if (pageIndex > 0) { + setPageIndex(pageIndex - 1); + } + if (pageIndex <= 0) setLeftDisabled(true); + }; return ( <> - <H3> - <Icon icon={"inbox-search"} className="icon-title" /> Résultats - </H3> - <div className="table"> - <Table - enableColumnReordering={true} - enableColumnResizing={true} - numRows={patients.length} + <div className="patientArray"> + <H3> + <Icon icon={"inbox-search"} className="icon-title" /> Résultats + </H3> + {getPatientCardTable()} + </div> + <div className="buttons"> + <Button + className="leftButton" + icon="direction-left" + onClick={() => getPreviousPage()} + disabled={leftDisabled} + > + Précédent + </Button> + <Button + className="rightButton" + rightIcon="direction-right" + onClick={() => getNextPage()} > - <Column - key="id" - name="id" - cellRenderer={(index: number) => - renderPatientAttribute("id", index) - } - /> - <Column - key="identifier" - name="Identifiant" - cellRenderer={(index: number) => - renderPatientAttribute("identifier", index) - } - /> - <Column - key="firstName" - name="Prénom" - cellRenderer={(index: number) => - renderPatientAttribute("firstName", index) - } - /> - <Column - key="lastName" - name="Nom" - cellRenderer={(index: number) => - renderPatientAttribute("lastName", index) - } - /> - <Column - key="Age" - name="Age" - cellRenderer={(index: number) => - renderPatientAttribute("age", index) - } - /> - </Table> + Suivant + </Button> </div> <div className="infoPatient"> - {patientCount && `${patientCount} patients identifiés`} + {bundle.total !== undefined && + `${bundle.total} patient-e-s identifié-e-s`} </div> </> ); diff --git a/src/components/patients/patientTable/patientCardTable/index.tsx b/src/components/patients/patientTable/patientCardTable/index.tsx new file mode 100644 index 0000000..158b270 --- /dev/null +++ b/src/components/patients/patientTable/patientCardTable/index.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { Icon, Card, H5, Tag } from "@blueprintjs/core"; +import { Link } from "react-router-dom"; +import { ROUTE_PATIENT } from "../../../../constants"; + +import { Patient } from "types"; + +import "./style.css"; + +interface Props { + patient: Patient; +} + +const PatientCardTable = ({ patient }: Props) => { + const getSubjectNameDiv = () => { + if (!patient.lastName && !patient.firstName) + return ( + <div className="aligned"> + <Icon icon="id-number" /> + <H5 className="marginRight">Nom inconnu</H5> + </div> + ); + + return ( + <div className="aligned"> + <Icon icon="id-number" /> + <H5>{patient.lastName}</H5> + <span className="bp3-text-muted">{patient.firstName}</span> + </div> + ); + }; + const getPatientCard = () => { + /* + Function getPatientCard + Get the PatientData object corresponding to patientId and generate jsx. + TODO: adapt this function to get data from the rest API + + Return page content with patient data. + */ + // case : rendering, patient found + return ( + <> + <div className="nameCard">{getSubjectNameDiv()}</div> + + <div className="nipCard"> + {patient.identifier && ( + <> + <Tag round={true}>NIP</Tag> + <div className="nipText">{patient.identifier}</div> + </> + )} + </div> + + <div className="birthDateCard"> + {patient.birthDate && ( + <> + <Tag round={true}>Date de naissance</Tag> + <div> {patient.birthDate}</div> + </> + )} + </div> + </> + ); + }; + + return ( + <Link to={`${ROUTE_PATIENT}/${patient.id}`} className="disabled-link-style"> + <Card interactive={true} className="card aligned"> + {getPatientCard()} + </Card> + </Link> + ); +}; + +export default PatientCardTable; diff --git a/src/components/patients/patientTable/patientCardTable/style.css b/src/components/patients/patientTable/patientCardTable/style.css new file mode 100644 index 0000000..32c2c26 --- /dev/null +++ b/src/components/patients/patientTable/patientCardTable/style.css @@ -0,0 +1,47 @@ +.aligned { + display: flex; + align-content: space-between; +} + +.card { + margin: 5px; + padding: 2px; + vertical-align: middle; + height: 38px; +} + +.disabled-link-style:hover { + color: inherit; + text-decoration: none; +} + +.disabled-link-style { + color: inherit; + text-decoration: none; +} + +.nipCard { + width: 40%; + display: flex; + overflow-wrap: break-word; +} + +.nipText { + overflow-y: auto; + width: 80%; +} + +.nameCard { + text-align: left; + overflow-y: auto; + width: 30%; +} + +.birthDateCard { + width: 30%; + display: flex; +} + +.patientList { + overflow-x: auto; +} \ No newline at end of file diff --git a/src/components/patients/patientTable/style.css b/src/components/patients/patientTable/style.css index ee6697a..7418c1f 100644 --- a/src/components/patients/patientTable/style.css +++ b/src/components/patients/patientTable/style.css @@ -1,12 +1,26 @@ -.table { - height: 90%; -} - .icon-title { transform: translateY(-20%); } .infoPatient { text-align: right; - margin-top: 20px; + position: absolute; + right: 20px; + bottom: 20px; +} + +.leftButton { + text-align: left; + bottom: 20px; + left: 50px; +} + +.rightButton { + text-align: right; + bottom: 20px; + right: 50px; +} + +.patientArray { + height: 98% } \ No newline at end of file diff --git a/src/components/patients/searchTool/index.tsx b/src/components/patients/searchTool/index.tsx index 1f8c841..c2d95c7 100644 --- a/src/components/patients/searchTool/index.tsx +++ b/src/components/patients/searchTool/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Icon, H3, Button } from "@blueprintjs/core"; import "./style.css"; import SearchItem from "components/patients/searchTool/searchItem"; +import SearchName from "components/patients/searchTool/searchName"; interface searchForm { label: string; @@ -9,15 +10,17 @@ interface searchForm { text: string; } -const SearchTool = () => { - const newSearchForm: searchForm = { - label: "", - symbol: "", - text: "" - }; - const [searchForms, setSearchForms] = React.useState([ - newSearchForm - ] as searchForm[]); +interface Props { + onSearch: Function; +} + +const SearchTool = ({ onSearch }: Props) => { + const [searchForms, setSearchForms] = React.useState([] as searchForm[]); + const [advancedSearchStyle, setAdvancedSearchStyle] = React.useState( + "hidden" + ); + + let [nameSearch, setNameSearch] = React.useState(""); const addSearchForm = () => { const newSearchForm: searchForm = { @@ -34,10 +37,16 @@ const SearchTool = () => { setSearchForms([...searchForms]); }; + const changeStyle = () => { + advancedSearchStyle === "hidden" + ? setAdvancedSearchStyle("advancedSearch") + : setAdvancedSearchStyle("hidden"); + }; + const search = () => { //This function will search for patients corresponding to the request and will show the patient list on the patient table. // For now, it only show the search parameters. - console.log(searchForms); + onSearch(nameSearch, searchForms); }; return ( @@ -45,18 +54,30 @@ const SearchTool = () => { <H3> <Icon icon={"search-template"} className="icon-title" /> Recherche </H3> - <div className="div-searchItems"> - {searchForms.map((searchForm, index) => ( - <div className="div-searchItem" key={index}> - <SearchItem searchItem={searchForm} onRemove={handleRemove} /> - </div> - ))} - <Button - className="buttonAdd" - icon="plus" - intent="primary" - onClick={addSearchForm} - /> + <div className="searchItem"> + <SearchName launchSearch={search} setNameSearch={setNameSearch} /> + </div> + <Button onClick={changeStyle} minimal> + Recherche avancée + </Button> + <div className="searchItems"> + <div className={advancedSearchStyle}> + {searchForms.map((searchForm, index) => ( + <div key={index}> + <SearchItem + searchItem={searchForm} + onRemove={handleRemove} + launchSearch={search} + /> + </div> + ))} + <Button + className="buttonAdd" + icon="plus" + intent="primary" + onClick={addSearchForm} + /> + </div> </div> <div className="validationButton"> diff --git a/src/components/patients/searchTool/searchItem/index.tsx b/src/components/patients/searchTool/searchItem/index.tsx index 74c4bd1..418222d 100644 --- a/src/components/patients/searchTool/searchItem/index.tsx +++ b/src/components/patients/searchTool/searchItem/index.tsx @@ -13,9 +13,10 @@ import "./style.css"; interface Props { onRemove: Function; searchItem: any; + launchSearch: Function; } -const SearchItem = ({ searchItem, onRemove }: Props) => { +const SearchItem = ({ searchItem, onRemove, launchSearch }: Props) => { const [label, setLabel] = React.useState(SEARCH_FIELDS[0].name); const [symbol, setSymbol] = React.useState(SEARCH_FIELDS[0].operations[0]); const [inputText, setInputText] = React.useState(""); @@ -77,6 +78,14 @@ const SearchItem = ({ searchItem, onRemove }: Props) => { } }; + const enterPressed = (event: any) => { + let code = event.keyCode || event.which; + if (code === 13) { + //enter keycode + launchSearch(); + } + }; + return ( <div className="searchItem"> <Card elevation={Elevation.TWO} className="searchCard"> @@ -108,6 +117,7 @@ const SearchItem = ({ searchItem, onRemove }: Props) => { setInputText(evt.target.value); onFormChange(label, symbol, evt.target.value); }} + onKeyPress={enterPressed} /> </div> </Card> diff --git a/src/components/patients/searchTool/searchItem/style.css b/src/components/patients/searchTool/searchItem/style.css index 88a515c..5480475 100644 --- a/src/components/patients/searchTool/searchItem/style.css +++ b/src/components/patients/searchTool/searchItem/style.css @@ -1,27 +1,37 @@ .searchItem { display: flex; + width: 100%; } + .buttonDelete { margin-left: 5px; } + .searchCard { - width: 95%; + width: 80%; padding: 5px; display: flex; } + .formElement-label { width: 25%; margin-right: 2px; } + .formElement-symbol { width: 25%; margin-right: 2px; } + .formElement-input { display: inline-block; width: 50%; } +.searchItem { + margin: 3px; +} + .formElement-wo-input-symbol { width: 50%; } @@ -33,4 +43,4 @@ .formElement-input-invisible { display: none; -} +} \ No newline at end of file diff --git a/src/components/patients/searchTool/searchName/index.tsx b/src/components/patients/searchTool/searchName/index.tsx new file mode 100644 index 0000000..180f77a --- /dev/null +++ b/src/components/patients/searchTool/searchName/index.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { InputGroup } from "@blueprintjs/core"; + +import "./style.css"; + +interface Props { + launchSearch: Function; + setNameSearch: Function; +} + +const SearchName = ({ launchSearch, setNameSearch }: Props) => { + /* + onFormChange updates the searchItem object with current label, symbol and inputText. + */ + + const enterPressed = (event: any) => { + let code = event.keyCode || event.which; + if (code === 13) { + //enter keycode + launchSearch(); + } + }; + + return ( + <div className="searchItem searchCard"> + <div className="nameItem"> + <InputGroup + placeholder="Recherche par nom ou identifiant" + onChange={(evt: any) => { + setNameSearch(evt.target.value); + }} + onKeyPress={enterPressed} + /> + </div> + </div> + ); +}; + +export default SearchName; diff --git a/src/components/patients/searchTool/searchName/style.css b/src/components/patients/searchTool/searchName/style.css new file mode 100644 index 0000000..11e2e62 --- /dev/null +++ b/src/components/patients/searchTool/searchName/style.css @@ -0,0 +1,14 @@ +.searchItem { + display: flex; +} + +.searchCard { + width: 95%; + padding: 5px; + display: flex; +} + +.nameItem { + display: inline-block; + width: 100%; +} \ No newline at end of file diff --git a/src/components/patients/searchTool/style.css b/src/components/patients/searchTool/style.css index 6a5fd5d..17e1313 100644 --- a/src/components/patients/searchTool/style.css +++ b/src/components/patients/searchTool/style.css @@ -9,14 +9,15 @@ float: right; } -.div-searchItem { - margin: 3px; -} - -.div-searchItems { - height: 90%; +.searchItems { + height: 80%; overflow-x: auto; } + .icon-title { transform: translateY(-20%); } + +.hidden { + display: none; +} \ No newline at end of file diff --git a/src/constants/index.tsx b/src/constants/index.tsx index 2703410..534b96f 100644 --- a/src/constants/index.tsx +++ b/src/constants/index.tsx @@ -1,3 +1,13 @@ +/* + Parameters +*/ + +export const PATIENT_SHOWN = 17; +export const PATIENT_REQUESTED = 20; + +/* + Routes and URLs +*/ export const ROUTE_HOME = "/"; export const ROUTE_PATIENT = "/patient"; @@ -8,33 +18,13 @@ export const URL_SERVER = "http://hapi.fhir.org/baseR4/"; Search and types are described in FHIR documentation : https://www.hl7.org/fhir/search.html#ptype */ -const OPERATION_NUMBER = [">", "<", "=", "≠"]; +const OPERATION_NUMBER = [">", "<", "="]; const OPERATION_TEXT = ["Commence par", "Contient", "Exact"]; const OPERATION_BOOLEAN = ["Oui", "Non"]; export const SEARCH_FIELDS = [ - { - name: "Logical id", - operations: OPERATION_TEXT, - isInputText: true - }, - { - name: "Identifier", - operations: OPERATION_TEXT, - isInputText: true - }, - { - name: "Nom", - operations: OPERATION_TEXT, - isInputText: true - }, - { - name: "Prénom", - operations: OPERATION_TEXT, - isInputText: true - }, { name: "Age", operations: OPERATION_NUMBER, diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..2d1b33e --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,365 @@ +import { URL_SERVER, PATIENT_REQUESTED } from "../constants"; +import { Patient, Bundle, PatientBundle } from "types"; +import { AppToaster } from "services/toaster"; + +const makeRequest = async ( + resource: string, + total?: boolean, + parameters?: string, + count = PATIENT_REQUESTED +) => { + /* + * Function makeRequest + * resource: the resource name we want to fetch + * parameters (optional) + * count: number of maximum resource we want to fetch (todo:make this func able to request for all patients) + */ + const url: string = `${URL_SERVER}${resource}?${parameters || ""}`; + + let response = await makeRequestByURL(url, total, count); + return response; +}; + +const makeRequestByURL = async ( + url: string, + total?: boolean, + count = PATIENT_REQUESTED +) => { + /* + * function makeRequestByURL + * url: the url of the data we want to fetch + * total: boolean, the bundle must contain the total number of items corresponding to the item search ? + * count: number of maximum resource we want to fetch + */ + let response = await new Promise<any>((resolve, reject) => { + let xmlhttp = new XMLHttpRequest(); + xmlhttp.open("GET", `${url}&_count=${count}`); + xmlhttp.onload = () => { + if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { + let resources = JSON.parse(xmlhttp.response); + resolve(resources); + } + }; + xmlhttp.onerror = function() { + reject({ status: xmlhttp.status, statusText: xmlhttp.statusText }); + }; + xmlhttp.send(); + }); + + let bundleResult: Bundle = { + entry: response.entry || [] + }; + + if (response.link) { + response.link.map((x: any) => { + if (x.relation === "next") { + bundleResult.nextLink = x.url; + } + return false; + }); + } + + /* + if total : get the total number of matches + */ + if (total) + if (response.data && response.data.total) { + //update with bundle total if available + bundleResult.total = response.data.total; + } else { + // Get count + let numberResponse = await new Promise<any>((resolve, reject) => { + let xmlhttp = new XMLHttpRequest(); + xmlhttp.open("GET", `${url}&_summary=count`); + xmlhttp.onload = () => { + if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { + let resources = JSON.parse(xmlhttp.response); + resolve(resources); + } + }; + xmlhttp.onerror = function() { + reject({ status: xmlhttp.status, statusText: xmlhttp.statusText }); + }; + xmlhttp.send(); + }); + bundleResult.total = numberResponse.total; + } + + return bundleResult; +}; + +const getAge = (birthDate: Date) => { + const today = new Date(); + const monthDifference = today.getMonth() - birthDate.getMonth(); + + const age = today.getFullYear() - birthDate.getFullYear(); + if ( + monthDifference < 0 || + (monthDifference === 0 && today.getDate() < birthDate.getDate()) + ) + return age - 1; + return age; +}; + +export const getPatients = async ( + param?: string, + count = PATIENT_REQUESTED +) => { + /* + * getPatients return the lists of patients depending on param. + * param (optional): if empty, will return the whole list. + */ + + let response: PatientBundle = (await makeRequest( + "Patient", + false, + param, + count + )) as PatientBundle; + + return addPatientsToBundle(response); +}; + +export const getPatientsPerQuery = async ( + searchName: String, + searchParams: any +) => { + /* + * Function getPatientsPerQuery used to get PatientBundle according to query parameters + * searchNameParams: object containing name parameters search + * searchParams: object[] containing list of other parameters + * TODO : for now, this function is limited by the server response to 500 resources. Must find a way to improve it (searching computation done by the server ideally ?) + */ + let bundles: Bundle[] = []; + let params = ""; + + if (searchName) { + // First API call : searching for names + params = searchName + .split(" ") + .map((x: string) => `&name=${x}`) + .join(); + let bundlePatient = await makeRequest("Patient", false, params, 10000); + + let entries = bundlePatient.entry; + + // Second API call : searching for identifier + params = searchName + .split(" ") + .map((x: string) => `&identifier=${x}`) + .join(); + + bundlePatient = await makeRequest("Patient", false, params, 10000); + + bundlePatient.entry = bundlePatient.entry.concat(entries); + + bundles.push(bundlePatient); + } + + bundles = bundles.concat( + await Promise.all( + searchParams.map((x: any) => { + switch (x.label) { + case "Age": + const correspondingDate: Date = new Date(); + correspondingDate.setFullYear( + correspondingDate.getFullYear() - parseInt(x.text) + ); + const yyyy = correspondingDate.getFullYear(); + const mm = + (correspondingDate.getMonth() + 1 > 9 ? "" : "0") + + (correspondingDate.getMonth() + 1); + const dd = + (correspondingDate.getDate() > 9 ? "" : "0") + + correspondingDate.getDate(); + + switch (x.symbol) { + case "=": + params = `&birthdate=lt${yyyy}-${mm}-${dd}`; + params += `&birthdate=gt${yyyy - 1}-${mm}-${dd}`; + return makeRequest("Patient", false, params, 10000); + case ">": + params = `&birthdate=lt${yyyy}-${mm}-${dd}`; + return makeRequest("Patient", false, params, 10000); + case "<": + params = `&birthdate=gt${yyyy}-${mm}-${dd}`; + return makeRequest("Patient", false, params, 10000); + } + return {}; + case "Diabète": + return getPatientPerCondition("73211009"); + default: + console.info(`Paramètre ${x.label} non reconnu`); + } + return []; + }) + ) + ); + + let finalBundle: Bundle = bundles[0]; + bundles.map((bundle: Bundle) => { + if (finalBundle !== bundle && bundle.entry.length > 0) { + let listId = bundle.entry.map((x: any) => x.resource.id); + finalBundle.entry = finalBundle.entry.filter( + (entry: any) => listId.indexOf(entry.resource.id) >= 0 + ); + } + return false; + }); + finalBundle.total = finalBundle.entry.length; + + return addPatientsToBundle(finalBundle); +}; + +export const requestNextPatients = async (bundle: PatientBundle) => { + /* + * function requestNextPatients + * Return the same bundle with more patients, fetched from the nextLink attribute. + */ + if (!bundle.nextLink) { + AppToaster.show({ message: "No link available." }); + return; + } else { + let newBundle: PatientBundle = (await makeRequestByURL( + bundle.nextLink + )) as PatientBundle; + + newBundle.entry = bundle.entry.concat(newBundle.entry); + + newBundle = addPatientsToBundle(newBundle); + newBundle.patients = bundle.patients.concat(newBundle.patients); + newBundle.total = bundle.total; + return newBundle; + } +}; + +export const addPatientsToBundle = (bundle: Bundle) => { + /* + * addPatientsToBundle transforms a bundle in a PatientBundle (generate Patient objects) + */ + + let response: PatientBundle = bundle as PatientBundle; + response.patients = response.entry.map((entry: any) => { + const patient: Patient = { + id: entry.resource.id, + birthDate: entry.resource.birthDate, + age: + entry.resource.birthDate && getAge(new Date(entry.resource.birthDate)) + }; + if (entry.resource.name) { + if (entry.resource.name[0].given) + patient.firstName = entry.resource.name[0].given.join(", "); + if (entry.resource.name[0].family) + patient.lastName = entry.resource.name[0].family; + } + if (entry.resource.identifier) { + patient.identifier = entry.resource.identifier + .map((e: any) => e.value) + .join(", "); + } + return patient; + }); + + return response; +}; + +export const getPatientData = async (patientId: string, detailed?: boolean) => { + /* + * getPatientData requests for data from Patient resource of id patientId. Used to load all data to show all patient hospitalizations. + * patientId + * detailed: get detailed informations about patient or not ? (slower, more API calls) + * return a Patient object. + */ + let response: any = await makeRequest("Patient", false, `_id=${patientId}`); + + if (!response.entry) return; //patient not found + const patientData = response.entry[0]; + + const patient: Patient = { + id: patientData.resource.id + }; + + // Completing patient information with available data + if (patientData.resource.identifier) { + patient.identifier = patientData.resource.identifier + .map((e: any) => { + return e.value; + }) + .join(", "); + } + + if (patientData.resource.birthDate) { + patient.age = getAge(new Date(patientData.resource.birthDate)); + patient.birthDate = patientData.resource.birthDate; + } + if (patientData.resource.name) { + if (patientData.resource.name[0].given) + patient.firstName = patientData.resource.name[0].given.join(", "); + if (patientData.resource.name[0].family) + patient.lastName = patientData.resource.name[0].family; + } + + if (detailed) { + response = await getPatientResources("AllergyIntolerance", patientId); + patient.allergyIntolerances = response.entry; + + response = await getSubjectResources("Observation", patientId); + patient.observations = response.entry; + + response = await getSubjectResources("Condition", patientId); + patient.conditions = response.entry; + + response = await getPatientResources("EpisodeOfCare", patientId); + patient.episodesOfCare = response.entry; + } + return patient; +}; + +/* + Return the list of patients affected by a condition + Example : 73211009 = diabetes +*/ +export const getPatientPerCondition = async (conditionId: string) => { + /* + get all conditions + http://hapi.fhir.org/baseR4/Condition?code=73211009 + */ + let response: any = await makeRequest( + "Condition", + true, + `&code=${conditionId}`, + 10000 + ); + if (!response) return; + + const refList = response.entry.map((x: any) => { + return x.resource.subject.reference.replace("Patient/", ""); + }); + + response = await makeRequest("Patient", true, `?_id=${refList.join(",")}`); + + return response; +}; + +/* + Function getSubjectResources returns all resources of type resourceType where attribute subject is a Patient of type patientId + */ +export const getSubjectResources = async ( + resourceType: "Observation" | "Condition", + patientId: string +) => { + return await makeRequest( + resourceType, + true, + `&subject:Patient._id=${patientId}` + ); +}; +/* + Function getPatientResources returns all resources of type resourceType where attribute patient has id patientId + */ +export const getPatientResources = async ( + resourceType: "AllergyIntolerance" | "EpisodeOfCare", + patientId: string +) => { + return await makeRequest(resourceType, true, `patient=${patientId}`); +}; diff --git a/src/services/api.tsx b/src/services/api.tsx deleted file mode 100644 index 8335607..0000000 --- a/src/services/api.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { URL_SERVER } from "../constants"; -import { Patient } from "types"; -import newFhirClient from "fhir.js"; - -const client = newFhirClient({ - baseUrl: URL_SERVER -}); - -const getAge = (birthDate: Date) => { - const today = new Date(); - const monthDifference = today.getMonth() - birthDate.getMonth(); - - const age = today.getFullYear() - birthDate.getFullYear(); - if ( - monthDifference < 0 || - (monthDifference === 0 && today.getDate() < birthDate.getDate()) - ) - return age - 1; - return age; -}; - -/* getCount function returns the number of resources of a type"; */ -export const getCount = async (resource: string, queryParameters: object) => { - const response = await client.search({ - type: resource, - query: queryParameters - }); - return response.data.total; -}; - -export const getPatients = async () => { - const response = await client.search({ - type: "Patient", - query: { _count: 100 } - }); - - return response.data.entry.map( - ({ resource: { id, identifier, birthDate, name } }: any) => { - const patient: Patient = { - id: id, - age: birthDate && getAge(new Date(birthDate)) - }; - if (name) { - if (name[0].given) patient.firstName = name[0].given.join(", "); - if (name[0].family) patient.lastName = name[0].family; - } - if (identifier) { - patient.identifier = identifier - .map((e: any) => { - return e.value; - }) - .join(", "); - } - - return patient; - } - ); -}; - -export const getPatientData = async (patientId: string) => { - /* - getPatientData requests for data from Patient resourcce of id patientId - return a Patient object. - */ - let response = await client.search({ - type: "Patient", - patient: patientId - }); - if (!response.data.entry) return; //if there is no patient found, response.data.entry is undefined. - const patientData = response.data.entry[0]; - - const patient: Patient = { - id: patientData.resource.id - }; - - // Completing patient information with available data - if (patientData.resource.identifier) { - patient.identifier = patientData.resource.identifier - .map((e: any) => { - return e.value; - }) - .join(", "); - } - - if (patientData.resource.birthDate) { - patient.age = getAge(new Date(patientData.resource.birthDate)); - patient.birthDate = patientData.resource.birthDate; - } - if (patientData.resource.name) { - if (patientData.resource.name[0].given) - patient.firstName = patientData.resource.name[0].given.join(", "); - if (patientData.resource.name[0].family) - patient.lastName = patientData.resource.name[0].family; - } - - response = await getPatientResources("AllergyIntolerance", patientId); - patient.allergyIntolerances = response.data; - - response = await getSubjectResources("Observation", patientId); - patient.observations = response.data; - - response = await getSubjectResources("Condition", patientId); - patient.conditions = response.data; - - response = await getPatientResources("EpisodeOfCare", patientId); - patient.episodesOfCare = response.data; - - return patient; -}; - -/* - Function getSubjectResources returns all resources of type resourceType where attribute subject is a Patient of type patientId - */ -export const getSubjectResources = (resourceType: string, patientId: string) => - client.search({ - type: resourceType, - query: { subject: { $type: "Patient", $id: patientId } } - }); -/* - Function getPatientResources returns all resources of type resourceType where attribute patient has id patientId - */ -export const getPatientResources = (resourceType: string, patientId: string) => - client.search({ - type: resourceType, - patient: patientId - }); diff --git a/src/services/toaster.ts b/src/services/toaster.ts new file mode 100644 index 0000000..397c065 --- /dev/null +++ b/src/services/toaster.ts @@ -0,0 +1,7 @@ +import { Position, Toaster } from "@blueprintjs/core"; + +/** Singleton toaster instance. Create separate instances for different options. */ +export const AppToaster = Toaster.create({ + className: "recipe-toaster", + position: Position.TOP +}); diff --git a/src/types.tsx b/src/types.tsx index ec46ec6..88c9b65 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -21,3 +21,20 @@ export interface Patient { /* list of episode of care resources linked to this patient*/ episodesOfCare?: any; } + +export interface Bundle { + /* list of entries */ + entry: any[]; + /* total number of patients matching the criteria */ + total?: number; + + /* parameters leading to this request */ + parameters?: any; + + /* link to get more entries */ + nextLink?: string; +} + +export interface PatientBundle extends Bundle { + patients: Patient[]; +}