From 4e2c82fa696d31053dd700e28001179fd4af044c Mon Sep 17 00:00:00 2001 From: Salah-BOUYAHIA Date: Tue, 1 Oct 2024 14:41:28 +0200 Subject: [PATCH] WIP --- .../Dashboard/ExportModal/ExportModal.tsx | 8 +- .../Dashboard/ExportModal/exportUtils.ts | 99 ++++- .../Routes/AppNavigation/config.tsx | 39 ++ .../Routes/LeftSideBar/LeftSideBar.tsx | 20 + src/components/Tables/index.tsx | 11 + src/components/ui/HeaderPage/index.tsx | 21 + src/config.tsx | 6 +- src/pages/Export/index.tsx | 48 +++ .../components/ExportForm/index.tsx | 391 ++++++++++++++++++ .../components/ExportTable/index.tsx | 272 ++++++++++++ src/pages/ExportRequest/index.tsx | 33 ++ src/pages/ExportRequest/styles.ts | 87 ++++ src/pages/FeasibilityReports/index.tsx | 33 ++ src/services/aphp/callApi.ts | 28 ++ src/services/aphp/serviceExportCohort.ts | 67 +++ src/services/apiDatamodel.ts | 18 + src/styles/sideBarTransition.ts | 25 ++ 17 files changed, 1183 insertions(+), 23 deletions(-) create mode 100644 src/components/Tables/index.tsx create mode 100644 src/components/ui/HeaderPage/index.tsx create mode 100644 src/pages/Export/index.tsx create mode 100644 src/pages/ExportRequest/components/ExportForm/index.tsx create mode 100644 src/pages/ExportRequest/components/ExportTable/index.tsx create mode 100644 src/pages/ExportRequest/index.tsx create mode 100644 src/pages/ExportRequest/styles.ts create mode 100644 src/pages/FeasibilityReports/index.tsx create mode 100644 src/services/aphp/serviceExportCohort.ts create mode 100644 src/services/apiDatamodel.ts create mode 100644 src/styles/sideBarTransition.ts diff --git a/src/components/Dashboard/ExportModal/ExportModal.tsx b/src/components/Dashboard/ExportModal/ExportModal.tsx index fe77ded51..3dc8e481f 100644 --- a/src/components/Dashboard/ExportModal/ExportModal.tsx +++ b/src/components/Dashboard/ExportModal/ExportModal.tsx @@ -695,12 +695,14 @@ const ExportTable: React.FC = ({ renderOption={(props, option) =>
  • {option.name}
  • } renderInput={(params) => } value={exportTable.fhir_filter} - onChange={(_, value) => _onChangeValue(value)} + onChange={(_, value) => { + _onChangeValue(value) + }} /> )} - + {/* Filtrer cette table en respectant les contraintes relationnelles avec les autres tables sélectionnées: @@ -722,7 +724,7 @@ const ExportTable: React.FC = ({ - + */} {exportTable.checked && exportTable.count > (exportTable.resourceType === ResourceType.DOCUMENTS ? 5000 : exportLinesLimit) && ( diff --git a/src/components/Dashboard/ExportModal/exportUtils.ts b/src/components/Dashboard/ExportModal/exportUtils.ts index 44bb55a55..d2ef05444 100644 --- a/src/components/Dashboard/ExportModal/exportUtils.ts +++ b/src/components/Dashboard/ExportModal/exportUtils.ts @@ -378,8 +378,8 @@ export const fetchAllResourcesCount = async (cohortId: string) => { observationCount, imagingCount, encounterVisitCount, - encounterDetailsCounts, - questionnaireResponseCount + encounterDetailsCounts + // questionnaireResponseCount ] = await Promise.all([ fetchPatientCount(cohortId), fetchConditionCount(cohortId), @@ -391,8 +391,8 @@ export const fetchAllResourcesCount = async (cohortId: string) => { fetchObservationCount(cohortId), fetchImagingCount(cohortId), fetchEncounterCount(cohortId, true), - fetchEncounterCount(cohortId), - fetchQuestionnaireCount(cohortId) + fetchEncounterCount(cohortId) + // fetchQuestionnaireCount(cohortId) ]) return { @@ -406,8 +406,8 @@ export const fetchAllResourcesCount = async (cohortId: string) => { observationCount, imagingCount, encounterVisitCount, - encounterDetailsCounts, - questionnaireResponseCount + encounterDetailsCounts + // questionnaireResponseCount } } catch (error) { console.error('Erreur lors de fetchQuestionnaireCount', error) @@ -435,19 +435,7 @@ export const fetchResourceCount = async (cohortId: string, table: ExportCSVTable [ResourceType.IMAGING]: fetchImagingCount } - const fetcher = - fetchers[ - resourceType as - | ResourceType.PATIENT - | ResourceType.CONDITION - | ResourceType.PROCEDURE - | ResourceType.CLAIM - | ResourceType.DOCUMENTS - | ResourceType.MEDICATION_REQUEST - | ResourceType.MEDICATION_ADMINISTRATION - | ResourceType.OBSERVATION - | ResourceType.IMAGING - ] + const fetcher = fetchers[resourceType as keyof typeof fetchers] return (await fetcher(cohortId, filters)) ?? 0 } catch (error) { @@ -471,3 +459,76 @@ export const getRightCount = (counts: Counts, tableResourceType: ResourcesWithEx return countMapping[tableResourceType] ?? 0 } + +export const fetchResourceCount2 = async (cohortId: string, resourceType: ResourceType, fhirFilter?: any) => { + try { + const filters = await mapRequestParamsToSearchCriteria(fhirFilter?.filter ?? '', resourceType) + const fetchers = { + [ResourceType.PATIENT]: fetchPatientCount, + [ResourceType.CONDITION]: fetchConditionCount, + [ResourceType.PROCEDURE]: fetchProcedureCount, + [ResourceType.CLAIM]: fetchClaimCount, + [ResourceType.DOCUMENTS]: fetchDocumentsCount, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [ResourceType.MEDICATION_REQUEST]: (cohortId: string, filters: any) => + fetchMedicationCount(cohortId, ResourceType.MEDICATION_REQUEST, filters), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [ResourceType.MEDICATION_ADMINISTRATION]: (cohortId: string, filters: any) => + fetchMedicationCount(cohortId, ResourceType.MEDICATION_ADMINISTRATION, filters), + [ResourceType.OBSERVATION]: fetchObservationCount, + [ResourceType.IMAGING]: fetchImagingCount + } + + const fetcher = fetchers[resourceType as keyof typeof fetchers] + + return (await fetcher(cohortId, filters)) ?? 0 + } catch (error) { + console.error('Erreur lors du fetch du count de la ressource', error) + throw error + } +} + +export const getResourceType = (tableName: string): ResourceType => { + const resourceType = { + imaging_study: ResourceType.IMAGING, + drug_exposure_administration: ResourceType.MEDICATION_ADMINISTRATION, + measurement: ResourceType.OBSERVATION, + imaging_series: ResourceType.UNKNOWN, + condition_occurrence: ResourceType.CONDITION, + iris: ResourceType.UNKNOWN, + visit_detail: ResourceType.UNKNOWN, + person: ResourceType.PATIENT, + note: ResourceType.DOCUMENTS, + fact_relationship: ResourceType.UNKNOWN, + care_site: ResourceType.UNKNOWN, + visit_occurrence: ResourceType.UNKNOWN, + cost: ResourceType.CLAIM, + procedure_occurrence: ResourceType.PROCEDURE, + drug_exposure_prescription: ResourceType.MEDICATION_REQUEST, + questionnaireresponse: ResourceType.UNKNOWN + }[tableName] + + return resourceType ?? ResourceType.UNKNOWN +} + +export const getExportTableLabel = (tableName: string) => { + const tableLabel = { + imaging_study: 'Fait - Imagerie - Étude', + drug_exposure_administration: 'Fait - Médicaments - Administration', + measurement: 'Fait - Biologie', + imaging_series: 'Fait - Imagerie - Séries', + condition_occurrence: 'Fait - PMSI - Diagnostics', + care_site: 'Structure hospitalière', + iris: 'Zone géographique', + visit_detail: 'Détail de prise en charge', + person: 'Patient', + note: 'Fait - Documents cliniques', + fact_relationship: 'Référentiel', + visit_occurrence: 'Prise en charge', + cost: 'Fait - PMSI - GHM', + procedure_occurrence: 'Fait - PMSI - Actes', + drug_exposure_prescription: 'Fait - Médicaments - Prescription', + QuestionnaireResponse: 'Formulaires' + }[tableName] + return tableLabel ?? '-' +} diff --git a/src/components/Routes/AppNavigation/config.tsx b/src/components/Routes/AppNavigation/config.tsx index 8b8358826..5bc9e80bb 100644 --- a/src/components/Routes/AppNavigation/config.tsx +++ b/src/components/Routes/AppNavigation/config.tsx @@ -13,6 +13,9 @@ import CareSiteView from 'views/Scope/CareSiteView' import MyCohorts from 'views/MyCohorts' import MyRequests from 'views/MyRequests' import DownloadPopup from 'views/DownloadPopup/DownloadPopup' +import Export from 'pages/Export' +import ExportRequest from 'pages/ExportRequest' +import FeasibilityReports from 'pages/FeasibilityReports' // import { ODD_CONTACT } from '../../../constants' @@ -217,6 +220,42 @@ const configRoutes: configRoute[] = [ element: , exact: false }, + + /** + * Cohort360: Export Page + */ + { + path: '/exports', + name: '/exports', + isPrivate: true, + element: , + exact: true, + displaySideBar: true + }, + { + path: '/exports/new', + name: '/exports/new', + isPrivate: true, + element: , + exact: true, + displaySideBar: true + }, + + /** + * Cohort360: Feasibility Reports Page + */ + { + path: '/feasibility-reports', + name: '/feasibility-reports', + isPrivate: true, + element: , + exact: true, + displaySideBar: true + }, + + /** + * Cohort360: Export download Page + */ { path: '/download/:resource/:itemId', name: '/download/:resource/:itemId', diff --git a/src/components/Routes/LeftSideBar/LeftSideBar.tsx b/src/components/Routes/LeftSideBar/LeftSideBar.tsx index b9c11cc4e..91c8ddbe7 100644 --- a/src/components/Routes/LeftSideBar/LeftSideBar.tsx +++ b/src/components/Routes/LeftSideBar/LeftSideBar.tsx @@ -370,6 +370,26 @@ const LeftSideBar: React.FC<{ open?: boolean }> = (props) => { Mes requêtes + + navigate('/exports')} + underline="hover" + className={classes.nestedTitle} + > + Mes exports + + + + navigate('/feasibility-reports')} + underline="hover" + className={classes.nestedTitle} + > + Mes rapports de faisabilite + + diff --git a/src/components/Tables/index.tsx b/src/components/Tables/index.tsx new file mode 100644 index 000000000..00f8ebb5a --- /dev/null +++ b/src/components/Tables/index.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +const TableLines: React.FC = () => { + return ( +
    +

    Tables lines

    +
    + ) +} + +export default TableLines diff --git a/src/components/ui/HeaderPage/index.tsx b/src/components/ui/HeaderPage/index.tsx new file mode 100644 index 000000000..8cabbfcc2 --- /dev/null +++ b/src/components/ui/HeaderPage/index.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +import { Grid, Typography } from '@mui/material' + +type HeaderPageProps = { + id: string + title: string +} + +const HeaderPage: React.FC = (props) => { + const { id, title } = props + return ( + + + {title} + + + ) +} + +export default HeaderPage diff --git a/src/config.tsx b/src/config.tsx index 88096e7e4..639c0aeda 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -19,6 +19,7 @@ type AppConfig = { export: { enabled: boolean exportLinesLimit: number + exportTables: string } observation: { enabled: boolean @@ -144,6 +145,7 @@ type AppConfig = { wsProtocol: string fhirUrl: string backendUrl: string + datamodelUrl: string oidc?: { issuer: string redirectUri: string @@ -196,7 +198,8 @@ let config: AppConfig = { }, export: { enabled: true, - exportLinesLimit: 300000 + exportLinesLimit: 300000, + exportTables: '' }, observation: { enabled: true, @@ -291,6 +294,7 @@ let config: AppConfig = { wsProtocol: 'ws://', backendUrl: '/api/back', fhirUrl: '/api/fhir', + datamodelUrl: '/api/datamodel2', sessionTimeout: 780000, refreshTokenInterval: 180000, codeDisplayJWT: 'ArrowUp,ArrowUp,ArrowDown,ArrowDown,ArrowLeft,ArrowRight,ArrowLeft,ArrowRight,b,a,Enter', diff --git a/src/pages/Export/index.tsx b/src/pages/Export/index.tsx new file mode 100644 index 000000000..8072a305c --- /dev/null +++ b/src/pages/Export/index.tsx @@ -0,0 +1,48 @@ +import React from 'react' + +import { Grid, CssBaseline } from '@mui/material' + +import HeaderPage from 'components/ui/HeaderPage' +import DataTable from 'components/DataTable/DataTable' +import Tables from 'components/Tables' + +import { useAppSelector } from 'state' + +import sideBarTransition from 'styles/sideBarTransition' + +const exportColumnsTable = [ + { label: 'N° de cohorte' }, + { label: 'Nom de la cohorte' }, + { label: 'Nombre de patient' }, + { label: "Nom de l'export" }, + { label: 'Date de l’export' }, + { label: 'Statut' }, + { label: 'Actions' } +] + +const Export: React.FC = () => { + const { classes, cx } = sideBarTransition() + const openDrawer = useAppSelector((state) => state.drawer) + + return ( + + + + + + +

    salut

    +
    +
    +
    +
    + ) +} + +export default Export diff --git a/src/pages/ExportRequest/components/ExportForm/index.tsx b/src/pages/ExportRequest/components/ExportForm/index.tsx new file mode 100644 index 000000000..2b6699eb3 --- /dev/null +++ b/src/pages/ExportRequest/components/ExportForm/index.tsx @@ -0,0 +1,391 @@ +import React, { useState, useCallback, useEffect } from 'react' + +import { + Grid, + Typography, + TextField, + Select, + MenuItem, + FormControlLabel, + Checkbox, + Tooltip, + IconButton, + Button, + Autocomplete +} from '@mui/material' + +import InfoIcon from '@mui/icons-material/Info' +import { IndeterminateCheckBoxOutlined } from '@mui/icons-material' + +import ExportTable from '../ExportTable' + +import { fetchExportableCohorts } from 'services/aphp/callApi' +import { + fetchExportTablesRelationsInfo, + fetchExportTablesInfo, + postExportCohort +} from 'services/aphp/serviceExportCohort' + +import { Cohort } from 'types' +import { TableSetting, TableInfo } from 'types/export' + +import useStyles from '../../styles' + +const tableSettingsInitialState: TableSetting[] = [ + { + tableName: 'person', + isChecked: true, + columns: null, + fhirFilter: null, + respectTableRelationships: true + } +] + +const ExportForm: React.FC = () => { + const { classes } = useStyles() + const [error, setError] = useState('') + const [oneFile, setOneFile] = useState(false) + const [exportTypeFile, setExportTypeFile] = useState<'csv' | 'xlsx'>('csv') + const [tablesSettings, setTablesSettings] = useState(tableSettingsInitialState) + const [exportCohort, setExportCohort] = useState(null) + const [exportCohortList, setExportCohortList] = useState([]) + const [exportTableList, setExportTableList] = useState(null) + const checkedTables = tablesSettings.filter((tableSetting) => tableSetting.isChecked) + const [compatibilitiesTables, setCompatibilitiesTables] = useState(null) + const [conditions, setConditions] = useState(false) + const [motivation, setMotivation] = useState('') + + console.log('manelle compatibilitiesTables', exportTableList) + console.log('manelle exportTableList', exportTableList) + + const _fetchExportableCohorts = useCallback(async () => { + try { + const response = await fetchExportableCohorts() + setExportCohortList(response) + } catch (error) { + console.error(error) + } + }, []) + + const _fetchExportTablesInfo = useCallback(async () => { + try { + const response = await fetchExportTablesInfo() + setExportTableList(response) + } catch (error) { + return [] + } + }, []) + + const _fetchCompatibilitiesTables = useCallback(async () => { + const tableList = tablesSettings + .map((tableSetting) => { + if (tableSetting.isChecked) { + return tableSetting.tableName + } + }) + .filter((table) => table !== undefined) + try { + const response = await fetchExportTablesRelationsInfo(tableList) + console.log('manelle type response', response) + setCompatibilitiesTables(response) + } catch (e) { + console.log('error', e) + return [] + } + }, [tablesSettings]) + + useEffect(() => { + _fetchExportableCohorts() + _fetchExportTablesInfo() + }, [_fetchExportableCohorts, _fetchExportTablesInfo]) + + useEffect(() => { + if (oneFile) { + _fetchCompatibilitiesTables() + } + }, [oneFile, _fetchCompatibilitiesTables]) + + const addNewTableSetting = (arg: TableSetting) => { + setTablesSettings([...tablesSettings, arg]) + } + + const onChangeTableSettings = (tableName: string, key: any, value: any) => { + const newTableSettings: TableSetting[] = tablesSettings.map((tableSetting) => { + if (tableSetting.tableName === tableName) { + return { + ...tableSetting, + [key]: value + } + } else { + return tableSetting + } + }) + setTablesSettings(newTableSettings) + } + + const resetSelectedTables = () => { + const newSelectedTables = tablesSettings.map((tableSetting) => ({ + ...tableSetting, + isChecked: tableSetting.tableName === 'person' + })) + setTablesSettings(newSelectedTables) + } + + const handleSelectAllTables = () => { + const newSelectedTables = tablesSettings.map((tableSetting) => ({ + ...tableSetting, + isChecked: tableSetting.tableName !== 'person' ? tablesSettings.length !== checkedTables.length : true + })) + setTablesSettings(newSelectedTables) + } + + const handleSubmitPayload = () => { + console.log('manelle tableSettings', tablesSettings) + console.log('manelle reason', motivation) + console.log('manelle conditions', conditions) + console.log('manelle exportCohort', exportCohort) + console.log('manelle veux un export') + console.log('manelle exportTypeFile', exportTypeFile) + console.log('manelle oneFile', oneFile) + + const tableToExport = tablesSettings.filter((tableSetting) => tableSetting.isChecked) + console.log('manelle tableToExport', tableToExport) + + postExportCohort({ + cohortId: exportCohort ?? { uuid: '' }, + motivation: motivation, + group_tables: oneFile, + outputFormat: exportTypeFile, + tables: tableToExport + }) + } + + return ( + + + Pour effectuer un export de données, veuillez renseigner un motif, sélectionner uniquement les tables que vous + voulez exporter et accepter les conditions de l'entrepôt de données de santé (EDS).
    + Tous les champs sont obligatoires +
    + + setMotivation(e.target.value)} + error={error} + /> + + + Le motif doit comporter au moins 10 caractères + + + + Sélectionner la cohorte à exporter + { + return `${option.name}` + }} + renderInput={(params) => } + value={exportCohort} + onChange={(_, value) => { + setExportCohort(value) + }} + /> + + + + { + setOneFile(!oneFile) + resetSelectedTables() + }} + /> + } + label={'Regrouper plusieurs tables en un seul fichier'} + /> + + Cette fonctionnalité permet d'intégrer, dans chaque fichier généré correspondant à une table sélectionnée, + les données patient et visites associées. + + } + > + + + + + + Type de fichier : + + + + + + + Tables exportées + + + + window.open( + `https://id.pages.data.aphp.fr/pfm/bigdata/eds-central-database/latest/data_catalog/`, + '_blank' + ) + } + > + + + + + {oneFile !== true && ( + + tableSetting.isChecked).length !== tablesSettings.length && + tablesSettings.filter((tableSetting) => tableSetting.isChecked).length > 0 + } + indeterminateIcon={} + checked={ + tablesSettings.filter((tableSetting) => tableSetting.isChecked).length === tablesSettings.length + } + onChange={handleSelectAllTables} + /> + } + label={ + tablesSettings.filter((tableSetting) => tableSetting.isChecked).length === tablesSettings.length + ? 'Tout désélectionner' + : 'Tout sélectionner' + } + labelPlacement="start" + /> + + )} + + + + {exportTableList?.map((exportTable, index: number) => ( +
    + +
    + ))} +
    + + + + Conditions de l'EDS + + + + + Le niveau d’habilitation dont vous disposez dans Cohort360 vous autorise à exporter des données à caractère + personnel conformément à la réglementation et aux règles institutionnelles d’utilisation des données du + Système d’Information clinique de l’AP-HP. Vous êtes garant des données exportées et vous vous engagez à : + + + + N’exporter, parmi les catégories de données accessibles, que les données strictement nécessaires et + pertinentes au regard des objectifs de la recherche + + + + A stocker temporairement les données extraites sur un répertoire dont l’accès est techniquement restreint + aux personnes dûment habilitées et authentifiées, présentes dans les locaux du responsable de la + recherche. + + + + A ne pas utiliser du matériel ou des supports de stockage n’appartenant pas à l’AP-HP, à ne pas sortir les + données des locaux de l’AP-HP ou sur un support amovible emporté hors AP-HP. + + + A procéder à la destruction de toutes données exportées, dès qu’il n’y a plus nécessité d’en disposer dans + le cadre de la recherche dans le périmètre concerné. + + + + A ne pas communiquer les données à des tiers non autorisés + + + + A informer les chefs de services des UF de Responsabilité où ont été collectées les données exportées + + + + A ne pas croiser les données avec tout autre jeu de données, sans autorisation auprès de la CNIL + + + + setConditions(!conditions)} + /> + } + labelPlacement="end" + label={ + + Je reconnais avoir lu et j'accepte les conditions ci-dessus + + } + /> + + + +
    + ) +} + +export default ExportForm diff --git a/src/pages/ExportRequest/components/ExportTable/index.tsx b/src/pages/ExportRequest/components/ExportTable/index.tsx new file mode 100644 index 000000000..dd55b7a3f --- /dev/null +++ b/src/pages/ExportRequest/components/ExportTable/index.tsx @@ -0,0 +1,272 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react' + +import { Grid, Typography, TextField, Checkbox, Autocomplete, CircularProgress, Alert } from '@mui/material' + +import useStyles from '../../styles' +import { getResourceType, getExportTableLabel, fetchResourceCount2 } from 'components/Dashboard/ExportModal/exportUtils' +import { ResourceType } from 'types/requestCriterias' +import { getProviderFilters } from 'services/aphp/serviceFilters' +import { useAppSelector } from 'state' +import { AppConfig } from 'config' + +import { Cohort } from 'types' +import { TableInfo, TableSetting } from 'types/export' + +type ExportTableProps = { + exportTable: TableInfo + exportTableSettings: TableSetting[] + exportCohort: Cohort | null + setError: (arg: any) => void + addNewTableSetting: (newTableSetting: TableSetting) => void + onChangeTableSettings: (tableName: string, key: string, value: any) => void + compatibilitiesTables: string[] | null + exportTypeFile: 'xlsx' | 'csv' + oneFile: boolean +} + +const AlertLimitXlsx: React.FC = () => { + const message = + "Attention, le format excel étant limité à 32.000 caractères par cellule, le contenu de certains comptes rendus peut être limité aux 32.000 premiers caractères. Si vous souhaitez tout de même obtenir l'intégralité du texte, vous pouvez choisir le format csv qui n'est pas limité en taille." + + return ( + + {message} + + ) +} + +const ExportTable: React.FC = ({ + exportTable, + exportTableSettings, + exportCohort, + setError, + addNewTableSetting, + onChangeTableSettings, + compatibilitiesTables, + exportTypeFile, + oneFile +}) => { + const userId = useAppSelector((state) => state.me?.id) + const { classes } = useStyles() + const [filters, setFilters] = useState([]) + const [count, setCount] = useState(null) + const [countLoading, setCountLoading] = useState(false) + const [countError, setCountError] = useState(false) + const cohortId = exportCohort?.group_id + const exportColumns = exportTable.columns || [] + const tableSetting = exportTableSettings.filter((e) => e.tableName === exportTable.name)[0] + const exportTableResourceType = getResourceType(exportTable.name) + const tableLabel = getExportTableLabel(exportTable.name) + const [checkedTable, setCheckedTable] = useState(tableSetting.isChecked || false) + const appConfig = useContext(AppConfig) + const limit = appConfig.features.export.exportLinesLimit + + const getFilterList = useCallback(async () => { + try { + const filtersResp = await getProviderFilters(userId, exportTableResourceType) + + setFilters(filtersResp) + } catch (error) { + console.error("Erreur lors de la récupération des filtres de l'utilisateur", error) + setFilters([]) + } + }, [exportTableResourceType, userId]) + + const getFilterCount = useCallback(async () => { + try { + setCountLoading(true) + const count = await fetchResourceCount2(cohortId ?? '', exportTableResourceType, tableSetting?.fhirFilter) + setCount(count) + setCountLoading(false) + } catch (error) { + setCountError(true) + setError(error) + } + }, [cohortId, exportTableResourceType, setError, tableSetting?.fhirFilter]) + + useEffect(() => { + if (tableSetting?.isChecked !== null) { + setCheckedTable(tableSetting?.isChecked) + } + }, [tableSetting?.isChecked]) + + useEffect(() => { + if (exportTableResourceType !== ResourceType.UNKNOWN) { + getFilterList() + } + }, [getFilterList, exportTableResourceType]) + + useEffect(() => { + if (tableSetting === undefined) { + const newTableSetting = { + tableName: exportTable.name, + isChecked: false, + columns: null, + fhirFilter: null, + respectTableRelationships: true + } + addNewTableSetting(newTableSetting) + } + }) + + const isCompatibleTable = (tableName: string) => { + const table = compatibilitiesTables?.find((table) => table === tableName) + return table + } + + useEffect(() => { + if (ResourceType.UNKNOWN !== exportTableResourceType) { + getFilterCount() + } + }, [exportTableResourceType, getFilterCount]) + + return ( + + + + + {tableLabel}   + +
    + + {'['} + + + {exportTable.name} + + + {']'} + +
    +
    + + + {exportTableResourceType !== ResourceType.UNKNOWN && ( + <> + {countLoading ? ( + + ) : ( + + {count} ligne(s) + + )} + + )} + + + + { + e.stopPropagation() + onChangeTableSettings( + exportTable.name, + 'isChecked', + tableSetting?.isChecked !== undefined ? !tableSetting.isChecked : true + ) + }} + /> + +
    + {exportTypeFile === 'xlsx' && exportTableResourceType === ResourceType.DOCUMENTS && ( + + + + )} + + + + Selectionner les colonnes a exporter : + + { + return `${option}` + }} + renderOption={(props, option, { selected }) => { + const { key, ...optionProps } = props + return ( +
  • + + {option} +
  • + ) + }} + renderInput={(params) => { + return + }} + value={tableSetting?.columns || []} + onChange={(_, value) => onChangeTableSettings(exportTable.name, 'columns', value)} + /> +
    + + {exportTableResourceType !== ResourceType.UNKNOWN && ( + <> + + Filtrer cette table avec un filtre : + + { + return `${option.name}` + }} + renderInput={(params) => } + value={tableSetting?.fhirFilter} + onChange={(_, value) => { + onChangeTableSettings(exportTable.name, 'fhirFilter', value) + }} + /> + + )} + +
    + {count !== null && (exportTableResourceType === ResourceType.DOCUMENTS ? count > 5000 : count > limit) && ( + + + La table sélectionnée dépasse la limite de{' '} + <>{console.log('manelle exportTableResourceType', exportTableResourceType)} + {exportTableResourceType === ResourceType.DOCUMENTS ? 5000 : limit} lignes autorisées. Veuillez affiner + votre sélection à l'aide de filtres ou désélectionner la table. + + + )} +
    + ) +} + +export default ExportTable diff --git a/src/pages/ExportRequest/index.tsx b/src/pages/ExportRequest/index.tsx new file mode 100644 index 000000000..ecb2575e4 --- /dev/null +++ b/src/pages/ExportRequest/index.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +import { CssBaseline, Grid } from '@mui/material' +import sideBarTransition from 'styles/sideBarTransition' +import HeaderPage from 'components/ui/HeaderPage' + +import { useAppSelector } from 'state' +import ExportForm from './components/ExportForm' + +const ExportRequest = () => { + const { classes, cx } = sideBarTransition() + const openDrawer = useAppSelector((state) => state.drawer) + + return ( + + + + + + + + + + ) +} + +export default ExportRequest diff --git a/src/pages/ExportRequest/styles.ts b/src/pages/ExportRequest/styles.ts new file mode 100644 index 000000000..e2a002253 --- /dev/null +++ b/src/pages/ExportRequest/styles.ts @@ -0,0 +1,87 @@ +import { makeStyles } from 'tss-react/mui' + +const useStyles = makeStyles()(() => ({ + autocomplete: { + margin: '2px', + backgroundColor: 'white', + width: '60%' + }, + agreeCheckbox: { + padding: '6px' + }, + selectAllExportTablesCheckbox: { + padding: '0em' + }, + conditionItem: { + margin: '4px 0 4px 36px', + position: 'relative', + color: 'rgba(0, 0, 0, 0.8)', + '&::before': { + position: 'absolute', + content: "'•'", + color: 'rgba(0, 0, 0, 0.7)', + left: -20, + top: 'calc(50% - 11px)', + fontSize: 26, + lineHeight: '22px' + } + }, + dialogHeader: { + color: '#0063AF', + marginBlock: 8, + textDecoration: 'underline', + textUnderlineOffset: '4px' + }, + helperText: { + marginLeft: 0, + fontStyle: 'italic' + }, + selectAgreeConditions: { + display: 'flex', + gap: '8px', + margin: 0, + paddingLeft: '3px', + '& span': { + fontWeight: '800' + } + }, + selectAllTables: { + display: 'flex', + gap: '12px', + margin: 0, + color: '#0063AF', + '& span': { + fontWeight: '800' + } + }, + textBody1: { + color: 'rgba(0, 0, 0, 0.6)', + fontWeight: '700' + }, + textBody2: { + color: 'rgba(0, 0, 0, 0.8)', + fontWeight: '600' + }, + fileType: { + marginTop: 12, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + selectFileType: { + marginLeft: 8, + borderRadius: 25, + backgroundColor: '#FFF', + '& .MuiSelect-select': { + borderRadius: 25 + } + }, + tableLabel: { + color: '#888', + fontStyle: 'italic', + fontWeight: '600', + padding: '0 5px 0 4px' + } +})) + +export default useStyles diff --git a/src/pages/FeasibilityReports/index.tsx b/src/pages/FeasibilityReports/index.tsx new file mode 100644 index 000000000..9a065fa5c --- /dev/null +++ b/src/pages/FeasibilityReports/index.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +import { Grid, CssBaseline } from '@mui/material' + +import HeaderPage from 'components/ui/HeaderPage' + +import { useAppSelector } from 'state' + +import sideBarTransition from 'styles/sideBarTransition' + +const FeasibilityReports: React.FC = () => { + const { classes, cx } = sideBarTransition() + const openDrawer = useAppSelector((state) => state.drawer) + + return ( + + + + + + + + + ) +} + +export default FeasibilityReports diff --git a/src/services/aphp/callApi.ts b/src/services/aphp/callApi.ts index 0efff21af..aca8258f2 100644 --- a/src/services/aphp/callApi.ts +++ b/src/services/aphp/callApi.ts @@ -1,4 +1,5 @@ import apiFhir from '../apiFhir' +import apiDatamodel from 'services/apiDatamodel' import { AccessExpiration, AccessExpirationsProps, @@ -1247,3 +1248,30 @@ export const fetchCohortInfo = async (cohortId: string) => { const response = await apiBackend.get>(`/cohort/cohorts/?group_id=${cohortId}`) return response } + +export const fetchExportableCohorts = async () => { + const response = await apiBackend.get>(`/cohort/cohorts/?exportable=true`) + return response.data.results +} + +type fetchExportTableInfoProps = { + tableNames?: string + relationLink?: string[] +} + +export const fetchExportTableInfo = async (args: fetchExportTableInfoProps) => { + const { tableNames, relationLink } = args + + let options: string[] = [] + if (tableNames) options = [...options, `?tables=${tableNames}`] + if (relationLink) options = [...options, `/relations?tables=${relationLink}`] + + let queryParams = '' + if (options.length != 0) { + queryParams = `${options.reduce(paramsReducer)}` + } + + const response = await apiDatamodel.get(`/models${queryParams}`) + + return response.data +} diff --git a/src/services/aphp/serviceExportCohort.ts b/src/services/aphp/serviceExportCohort.ts new file mode 100644 index 000000000..0de3d225a --- /dev/null +++ b/src/services/aphp/serviceExportCohort.ts @@ -0,0 +1,67 @@ +import { fetchExportTableInfo } from 'services/aphp/callApi' +import { getConfig } from 'config' +import { AxiosResponse, isAxiosError, AxiosError } from 'axios' +import { Export, Cohort } from 'types' +import apiBackend from 'services/apiBackend' +import { TableSetting } from 'types/export' + +export const fetchExportTablesInfo = () => { + try { + const response = fetchExportTableInfo({ tableNames: getConfig().features.export.exportTables }) + return response + } catch (error) { + return [] + } +} + +export const fetchExportTablesRelationsInfo = async (tableList: string[]) => { + try { + const call = await fetchExportTableInfo({ relationLink: tableList }) + const hamiltonian = + call?.verifiedRelations?.find((table: any) => table.relation === 'Hamiltonian')?.candidates || [] + const centralTable = + call?.verifiedRelations?.find((table: any) => table.relation === 'CentralTable')?.candidates || [] + const result: string[] = hamiltonian.concat(centralTable).concat(tableList) + const response: string[] = Array.from(new Set(result)) + return response + } catch (error) { + return [] + } +} + +export const postExportCohort = async ({ + cohortId, + motivation, + group_tables, + outputFormat, + tables +}: { + cohortId: Cohort + motivation: string + group_tables: boolean + outputFormat: string + tables: TableSetting[] +}): Promise | AxiosError> => { + try { + const nominative = true + const shift_date = false + + return await apiBackend.post('/exports/', { + motivation, + export_tables: tables.map((table: TableSetting) => ({ + table_name: table.tableName, + cohort_result_source: cohortId?.uuid, + respect_table_relationships: table.respectTableRelationships, + columns: table.columns, + ...(table.fhirFilter && { fhir_filter: table.fhirFilter?.uuid }) + })), + nominative: nominative, + shift_date: shift_date, + output_format: outputFormat, + group_tables: group_tables + }) + } catch (error) { + if (isAxiosError(error)) return error + else throw error + } +} diff --git a/src/services/apiDatamodel.ts b/src/services/apiDatamodel.ts new file mode 100644 index 000000000..000c6e492 --- /dev/null +++ b/src/services/apiDatamodel.ts @@ -0,0 +1,18 @@ +import axios from 'axios' +import { getConfig, onUpdateConfig } from 'config' + +const apiDatamodel = axios.create({ + baseURL: getConfig().system.datamodelUrl, + headers: { + Accept: 'application/json' + } +}) +onUpdateConfig((newConfig) => { + apiDatamodel.defaults.baseURL = newConfig.system.datamodelUrl +}) + +apiDatamodel.interceptors.response.use((response) => { + return response +}) + +export default apiDatamodel diff --git a/src/styles/sideBarTransition.ts b/src/styles/sideBarTransition.ts new file mode 100644 index 000000000..576e502fd --- /dev/null +++ b/src/styles/sideBarTransition.ts @@ -0,0 +1,25 @@ +import { makeStyles } from 'tss-react/mui' +import { Theme } from '@mui/material/styles' +import { smallDrawerWidth, largeDrawerWidth } from 'components/Routes/LeftSideBar/LeftSideBar' + +const useStyles = makeStyles()((theme: Theme) => ({ + appBar: { + marginLeft: smallDrawerWidth, + width: `calc(100% - ${smallDrawerWidth}px)`, + zIndex: theme.zIndex.drawer + 1, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + }) + }, + appBarShift: { + marginLeft: largeDrawerWidth, + width: `calc(100% - ${largeDrawerWidth}px)`, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen + }) + } +})) + +export default useStyles