Unverified Commit 1939e8f7 authored by pom421's avatar pom421 Committed by GitHub
Browse files

Backoffice (#13)

* refactor: add a custom hook for data pagination

* feat: add missing privilege

* refactor: new model files for db to/from JS transformations

User stored in token and in session storage now have sub hospital object and not a flat user object

* feat: user administration page

* feat: add empty page with minimum React syntax

* fix: scope for API

* fix: bug when API throw an error

* fix: downgrade on Next 9.2 since debug bug in 9.3

See https://github.com/zeit/next.js/pull/11041
fix

* fix(tests)

* refactor(api) : big effort to make it more coherent (WIP)

- add a services directory, to help to factorize functions for API endpoints
- refactor errors (remove createError, add InternalError for 500 error)
- beginning to refactor the URL of API endpoint, to be more REST-like

* refactor(api): attacks endpoint

* fix: upgrade Next in 9.3.2 to avoid testing problems

* refactor(api): refactor askers API endpoint

* refactor(employments): API endpoint + page + knex upsert

* refactor(api): statistics

- getReachableScope is removed, now use buildScope 
- change rule of defaultStartDate (take the endDate by default seems more suited)
- change test in consequence

* fix(statistics): default startDate must be 1st january to have results

* refactor(api): all acts operations

All URL are REST like now

* refactor(api): cors management for left API endpoints (login, logout..)

* feat: add detail administration user

* feat: detail user page administration (WIP)

* feat: administration add Back button + Remove button

* fix(act): Remove act was not operationnal after refactor

* feat(administration): add react-hook-form + reset password

* fix(tarteaucitron): fix warning for non standard zoom property

* fix: fix PropTypes for SearchButton component

* feat: create/update user is operationnal (WIP)

* feat: add/update use is ok. Miss scope property management

* feat(admin): order by users.created_at

* feat(config): add feature flags to open or close a feature

* feat(faq): new version of FAQ page
parent 85f39196
......@@ -9,15 +9,18 @@ DEBUG_MODE=true
API_URL=http://localhost:3000/api
POSTGRES_USER=medle
POSTGRES_PASSWORD=mypass
POSTGRES_PASSWORD=bHrdeGk63cHQa7
## BE CAREFUL WITH THE SSL MODE !! In local, set it to false, for Azure hosted, set it to true
POSTGRES_SSL=false
DATABASE_URL=psql://medle:mypass@localhost:5434/medle
DATABASE_URL=psql://medle:bHrdeGk63cHQa7@localhost:5434/medle
#POSTGRES_SSL=true
#DATABASE_URL=psql://dev1%40medledevserver:01VihjO5xderUnTqmzcb@medledevserver.postgres.database.azure.com:5432/dev1
#TEST_CURRENT_DATE=10/09/2019
JWT_SECRET=myotherpass
JWT_SECRET=Hq!sB$eTeWj63H
MATOMO_SITE_ID=16
MATOMO_URL=https://matomo.fabrique.social.gouv.fr
......
......@@ -30,17 +30,27 @@
{
"type": "node",
"request": "launch",
"name": "Next: Node",
"runtimeExecutable": "${workspaceFolder}/node_modules/next/dist/bin/next",
// "runtimeArgs": [
// "--inspect"
// ],
"env": {
"NODE_OPTIONS": "--inspect-brk"
"name": "Next: Node old",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"debug",
"--",
"--inspect-brk=39015"
],
"port": 39015,
"console": "integratedTerminal"
},
"port": 9229,
"console": "integratedTerminal"
}
{
"type": "node",
"request": "attach",
"name": "Next: Node",
"skipFiles": [
"<node_internals>/**"
],
"port": 9229
}
],
"compounds": [
{
......
......@@ -26,6 +26,13 @@ const nextConfig = {
API_URL: process.env.API_URL,
TEST_CURRENT_DATE: process.env.TEST_CURRENT_DATE,
DEBUG_MODE: process.env.DEBUG_MODE,
FEATURE_FLAGS: {
notification: false,
administration: false,
directory: false,
resources: false,
parameters: false,
},
},
serverRuntimeConfig: {
// Will only be available on the server side. Needs getInitialProps on page to be available
......
......@@ -5,6 +5,7 @@
"scripts": {
"dev": "env-cmd -f ./.env.dev next dev",
"build": "node -r dotenv/config node_modules/next/dist/bin/next build",
"debug": "NODE_OPTIONS='--inspect' node -r dotenv/config node_modules/next/dist/bin/next dev",
"start": "node -r dotenv/config node_modules/next/dist/bin/next start",
"test": "jest",
"lint": "eslint .",
......@@ -30,22 +31,25 @@
"js-cookie": "^2.2.1",
"jsonwebtoken": "^8.5.1",
"knex": "^0.20.11",
"knex-upsert": "^0.0.4",
"micro-cors": "^0.1.1",
"moize": "^5.4.5",
"moment": "^2.24.0",
"next": "^9.3.0",
"next": "^9.3.2",
"next-connect": "^0.6.0",
"next-cookies": "^2.0.3",
"pg": "^7.12.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-hook-form": "^5.2.0",
"react-select": "^3.0.8",
"react-switch": "^5.0.1",
"reactstrap": "^8.0.1",
"recharts": "^2.0.0-beta.1",
"remark-emoji": "^2.1.0",
"remark-images": "^2.0.0",
"styled-components": "^5.0.1"
"styled-components": "^5.0.1",
"yup": "^0.28.3"
},
"devDependencies": {
"@socialgouv/eslint-config-react": "^0.17.0",
......
......@@ -240,7 +240,9 @@ div#tarteaucitronServices {
* Common value
*/
#tarteaucitron * {
zoom: 1;
/* zoom: 1 */
transform: scale(1);
transform-origin: 0 0;
}
#tarteaucitronRoot div#tarteaucitron {
......
......@@ -12,7 +12,7 @@ const moment = require("moment")
// const API_URL = "http://localhost:3000"
// const API_URL = "https://medle.fabrique.social.gouv.fr"
const API_URL = "http://40.89.136.101"
const ACT_SEARCH_ENDPOINT = "/api/askers/search"
const ACTS_ENDPOINT = "/api/askers/search"
const ACT_LOGIN = "/api/login"
const USER_LOGIN = "medle@tours.fr"
const USER_PASSWORD = "test"
......@@ -77,7 +77,7 @@ const fetchAskersThirdParty = async options => {
}
const fetchExistingAskers = async options => {
const response = await fetch(`${API_URL}${ACT_SEARCH_ENDPOINT}?all=true`, options)
const response = await fetch(`${API_URL}${ACTS_ENDPOINT}?all=true`, options)
const json = await response.json()
if (!json) return {}
......
......@@ -3,7 +3,7 @@ import PropTypes from "prop-types"
import AsyncSelect from "react-select/async"
import moize from "moize"
import { API_URL, ASKERS_SEARCH_ENDPOINT, ASKERS_VIEW_ENDPOINT } from "../config"
import { API_URL, ASKERS_ENDPOINT } from "../config"
import { isEmpty } from "../utils/misc"
import { handleAPIResponse } from "../utils/errors"
import { logError } from "../utils/logger"
......@@ -13,7 +13,7 @@ const getSuggestions = async value => {
let json
try {
const response = await fetch(`${API_URL}${ASKERS_SEARCH_ENDPOINT}${bonus}`)
const response = await fetch(`${API_URL}${ASKERS_ENDPOINT}${bonus}`)
json = await handleAPIResponse(response)
} catch (error) {
logError(error)
......@@ -25,7 +25,7 @@ const getAskerById = async id => {
let json
try {
const response = await fetch(`${API_URL}${ASKERS_VIEW_ENDPOINT}/${id}`)
const response = await fetch(`${API_URL}${ASKERS_ENDPOINT}/${id}`)
json = await handleAPIResponse(response)
} catch (error) {
logError(error)
......
......@@ -29,6 +29,7 @@ import GroupIcon from "@material-ui/icons/Group"
import { logout } from "../utils/auth"
import { colors } from "../theme"
import { isAllowed, ACT_MANAGEMENT, ACT_CONSULTATION, EMPLOYMENT_CONSULTATION } from "../utils/roles"
import { isOpenFeature } from "../config"
const Header = ({ currentUser }) => {
const [isOpen, setIsOpen] = useState(false)
......@@ -48,10 +49,12 @@ const Header = ({ currentUser }) => {
className="pt-2 mt-2 ml-auto d-flex justify-content-end align-items-md-center align-items-start pt-md-0"
navbar
>
<NavItem className="">
<NotificationsNoneIcon className="mr-2 text-black-50" width={30} />
<span className="d-sm-inline d-md-none text-black-50">Notifs</span>
</NavItem>
{isOpenFeature("notification") && (
<NavItem className="">
<NotificationsNoneIcon className="mr-2 text-black-50" width={30} />
<span className="d-sm-inline d-md-none text-black-50">Notifs</span>
</NavItem>
)}
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav>
<AccountCircleIcon className="text-black-50" width={30} />
......@@ -68,7 +71,13 @@ const Header = ({ currentUser }) => {
<DropdownItem>Profil</DropdownItem>
</a>
</Link>
<DropdownItem>Administration</DropdownItem>
{isOpenFeature("administration") && (
<DropdownItem>
<Link href="/administration/users">
<a>Administration</a>
</Link>
</DropdownItem>
)}
<DropdownItem divider />
<DropdownItem onClick={logout}>Se déconnecter</DropdownItem>
</DropdownMenu>
......@@ -85,7 +94,7 @@ Header.propTypes = {
currentUser: PropTypes.object,
}
const Footer = () => (
export const Footer = () => (
<footer className="pt-4 pb-5 m-0">
<Container>
<Row>
......@@ -198,22 +207,28 @@ const Sidebar = ({ page, currentUser }) => {
</a>
</Link>
{/* <Link href="/_error"> */}
<a className="list-group-item list-group-item-action">
<PhoneIcon width={30} /> <br />
{"Annuaire"}
</a>
{isOpenFeature("directory") && (
<a className="list-group-item list-group-item-action">
<PhoneIcon width={30} /> <br />
{"Annuaire"}
</a>
)}{" "}
{/* </Link> */}
{/* <Link href="/_error"> */}
<a className="list-group-item list-group-item-action">
<LocalLibraryIcon width={30} /> <br />
{"Ressources"}
</a>
{isOpenFeature("resources") && (
<a className="list-group-item list-group-item-action">
<LocalLibraryIcon width={30} /> <br />
{"Ressources"}
</a>
)}{" "}
{/* </Link> */}
{/* <Link href="/_error"> */}
<a className="list-group-item list-group-item-action">
<SettingsIcon width={30} /> <br />
{"Paramètres"}
</a>
{isOpenFeature("parameters") && (
<a className="list-group-item list-group-item-action">
<SettingsIcon width={30} /> <br />
{"Paramètres"}
</a>
)}{" "}
{/* </Link> */}
</div>
<style jsx>{`
......
import React, { useState } from "react"
import Link from "next/link"
import PropTypes from "prop-types"
import {
Collapse,
Nav,
Navbar,
NavbarBrand,
NavbarToggler,
NavItem,
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
} from "reactstrap"
import NotificationsNoneIcon from "@material-ui/icons/NotificationsNone"
import AccountCircleIcon from "@material-ui/icons/AccountCircle"
import ApartmentIcon from "@material-ui/icons/Apartment"
import AccountBalanceIcon from "@material-ui/icons/AccountBalance"
import BusinessCenterIcon from "@material-ui/icons/BusinessCenter"
import WhatshotIcon from "@material-ui/icons/Whatshot"
import ReceiptIcon from "@material-ui/icons/Receipt"
import FaceIcon from "@material-ui/icons/Face"
import { Footer } from "./Layout"
import { logout } from "../utils/auth"
import { isAllowed, ADMIN, ACT_CONSULTATION, EMPLOYMENT_CONSULTATION } from "../utils/roles"
const Header = ({ currentUser }) => {
const [isOpen, setIsOpen] = useState(false)
const toggle = () => setIsOpen(!isOpen)
return (
<header style={{ border: "1px solid #9c27b0" }}>
<Navbar expand="md" className="justify-content-between align-items-center" light>
<NavbarBrand>
<img src={"/images/logo.png"} alt="Logo" title="Logo" width="100"></img>
</NavbarBrand>
<NavbarToggler onClick={toggle} />
{currentUser && (
<Collapse isOpen={isOpen} navbar>
<Nav
className="ml-auto d-flex justify-content-end align-items-md-center align-items-start mt-2 pt-2 pt-md-0"
navbar
>
<NavItem className="">
<NotificationsNoneIcon className="mr-2 text-black-50" width={30} />
<span className="d-sm-inline d-md-none text-black-50">Notifs</span>
</NavItem>
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav>
<AccountCircleIcon className="text-black-50" width={30} />
<span className="d-sm-inline d-md-none text-black-50">&nbsp;Mon compte</span>
</DropdownToggle>
<DropdownMenu right>
{currentUser && (
<DropdownItem>{currentUser.firstName + " " + currentUser.lastName} </DropdownItem>
)}
<DropdownItem divider />
<Link href="/profile">
<a>
<DropdownItem>Profil</DropdownItem>
</a>
</Link>
<DropdownItem>Administration</DropdownItem>
<DropdownItem divider />
<DropdownItem onClick={logout}>Se déconnecter</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
</Nav>
</Collapse>
)}
</Navbar>
</header>
)
}
Header.propTypes = {
currentUser: PropTypes.object,
}
const Sidebar = ({ page, currentUser }) => {
if (!currentUser) return ""
return (
<>
<div className="list-group list-group-flush text-center">
{isAllowed(currentUser.role, ADMIN) && (
<Link href="/administration/users">
<a
className={
"list-group-item list-group-item-action " + (page === "users" ? "selected" : "unselected")
}
>
<FaceIcon className="text-black-50" width={30} />
<br />
{"Utilisateurs"}
</a>
</Link>
)}
{isAllowed(currentUser.role, ACT_CONSULTATION) && (
<Link href="/administration/hospitals">
<a
className={
"list-group-item list-group-item-action " + (page === "actsList" ? "selected" : "unselected")
}
>
<ApartmentIcon width={30} /> <br />
{"Établissements"}
</a>
</Link>
)}
{isAllowed(currentUser.role, EMPLOYMENT_CONSULTATION) && (
<Link href="/administration/askers">
<a
className={
"list-group-item list-group-item-action " +
(page === "fillEmployments" ? "selected" : "unselected")
}
>
<AccountBalanceIcon width={30} /> <br />
{"Demandeurs"}
</a>
</Link>
)}
{isAllowed(currentUser.role, EMPLOYMENT_CONSULTATION) && (
<Link href="/administration/attacks">
<a
className={
"list-group-item list-group-item-action " +
(page === "fillEmployments" ? "selected" : "unselected")
}
>
<WhatshotIcon width={30} /> <br />
{"Attentats"}
</a>
</Link>
)}
{isAllowed(currentUser.role, EMPLOYMENT_CONSULTATION) && (
<Link href="/administration/employments">
<a
className={
"list-group-item list-group-item-action " +
(page === "fillEmployments" ? "selected" : "unselected")
}
>
<BusinessCenterIcon width={30} /> <br />
{"Emplois"}
</a>
</Link>
)}
{isAllowed(currentUser.role, EMPLOYMENT_CONSULTATION) && (
<Link href="/administration/acts">
<a
className={
"list-group-item list-group-item-action " +
(page === "fillEmployments" ? "selected" : "unselected")
}
>
<ReceiptIcon width={30} /> <br />
{"Actes"}
</a>
</Link>
)}
</div>
<style jsx>{`
a {
font-variant: small-caps;
font-size: 12px;
font-family: "Source Sans Pro";
color: #9b9b9b;
}
a.selected {
border-left: 5px solid #307df6;
background-color: #e7f1fe !important;
}
a.unselected {
border-left: 5px solid #fff;
}
`}</style>
</>
)
}
Sidebar.propTypes = {
page: PropTypes.string,
currentUser: PropTypes.object,
}
const Layout = ({ children, page, currentUser }) => {
return (
<>
<div className="d-flex flex-column justifiy-content-between min-vh-100">
<Header currentUser={currentUser} />
<div id="wrapper" className="d-flex">
<div id="sidebar-wrapper" className="border-right">
<Sidebar page={page} currentUser={currentUser} />
</div>
<div id="page-content-wrapper">
<main className="pb-5">{children}</main>
</div>
</div>
<Footer />
</div>
<style jsx>{`
#sidebar-wrapper {
min-height: 100vh;
width: 140px;
}
#sidebar-wrapper .sidebar-heading {
padding: 0.875rem 1.25rem;
font-size: 1.2rem;
}
#page-content-wrapper {
min-width: calc(100vw - 140px);
}
`}</style>
</>
)
}
Layout.propTypes = { children: PropTypes.node.isRequired, page: PropTypes.string, currentUser: PropTypes.object }
export default Layout
import React from "react"
import PropTypes from "prop-types"
export const SearchButton = ({ children, className }) => {
return (
<button type="button" className={`btn ${className}`}>
{children}
</button>
)
}
SearchButton.propTypes = {
children: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
className: PropTypes.string,
}
......@@ -10,17 +10,24 @@ export const timeout = {
}
export const API_URL = publicRuntimeConfig ? publicRuntimeConfig.API_URL : "http://localhost:3000"
export const ACT_DECLARATION_ENDPOINT = "/actDeclaration"
export const ACT_SEARCH_ENDPOINT = "/acts/search"
export const ACT_DETAIL_ENDPOINT = "/actDetail"
export const ACT_DELETE_ENDPOINT = "/actDelete"
export const ACT_EDIT_ENDPOINT = "/actEdit"
export const ASKERS_SEARCH_ENDPOINT = "/askers/search"
export const ASKERS_VIEW_ENDPOINT = "/askers"
export const isOpenFeature = feature => {
const flags = publicRuntimeConfig.FEATURE_FLAGS || {}
return !!flags[feature]
}
export const LOGIN_ENDPOINT = "/login"
export const LOGOUT_ENDPOINT = "/logout"
export const RESET_PWD_ENDPOINT = "/reset"
export const ACTS_ENDPOINT = "/acts"
export const ASKERS_ENDPOINT = "/askers"
export const EMPLOYMENTS_ENDPOINT = "/employments"
export const ATTACKS_ENDPOINT = "/attacks"
export const HOSPITALS_ENDPOINT = "/hospitals"
export const GLOBAL_STATISTICS_ENDPOINT = "/statistics/global"
export const LIVING_STATISTICS_ENDPOINT = "/statistics/living"
export const DEACEASED_STATISTICS_ENDPOINT = "/statistics/deceased"
export const ADMIN_USERS_ENDPOINT = "/administration/users"
......@@ -20,30 +20,30 @@ Medlé... comme MÉDecine LÉgale ! 🙈
Différents types d'acteurs du monde de la médecine légale ont accès aux données notées sur Medlé :
- Toute personne employée dans une UMJ (Unité Médico-Judiciaire) et IML (Institut Médico-légal)
- Le personnel administratif (DG, DRH, DAM...) en charge de la structure choisies par l'administrateur, peuvent utiliser Medlé.
- Toute personne employée dans une UMJ (Unité Médico-Judiciaire) et IML (Institut Médico-légal),
- Le personnel administratif de l'établissement de santé (DRH, DAM...),
- Les ministères de la Santé, de l'Intérieur, de la Justice ainsi que d'autres le cas échéant,
- Les ARS,
- Les Cours d'Appel,
- Les TGI.
- Les Cours d'Appel (CA),
- Les Tribunaux de Grande Instance (TGI).
D'ici mi-2020, toutes les UMJ et IML hospitalières de France seront sur la plateforme.
Toutes les UMJ et IML hospitalières du territoire national seront sur la plateforme courant 2020.
Différents accès seront donnés : certains utilisateurs auront la possibilité d'ajouter des données, d'autres y auront accès en lecture seule.
## Qu'entend-on par "Organisation des structures dans le cadre de la réforme de 2011 et de la circulaire interministérielle Crim – 2012 – 12 / E6 -25.04.2012 du 25 avril 2012" ?
## Qu'entend-on par "Organisation des structures dans le cadre de la réforme de 2011 et de la circulaire interministérielle NOR : JUSD1221959C du 25 avril 2012" ?
Les organisations de chaque structure et les effectifs afférents sont fixées par le schéma directeur qui s'appliquent à l'annexe 2 de la circulaire interministérielle du 25 avril 2012 pour la thanatologie et la médecine légale du vivant.
Les organisations de chaque structure et les effectifs afférents sont fixées par le schéma directeur qui s'appliquent à l'[annexe 2 de la circulaire interministérielle du 25 avril 2012](http://www.textes.justice.gouv.fr/art_pix/JUSD1221959C.pdf) pour la thanatologie et la médecine légale du vivant.
# Utiliser Medlé
## Comment me créer un compte ?
Pour toute demande de création de compte, merci d'effectuer une demande par email à l'adresse [contact.medle@fabrique.social.gouv.fr](mailto:contact.medle@fabrique.social.gouv.fr). Il est préférable de préciser l'établissement auquel vous êtes rattaché, votre adresse email (qui vous servira d'identifiant) et l'objet de votre utilisation : déclaration des actes, déclaration des ETP,...
Pour toute demande de création de compte, merci d'effectuer une demande par email à l'adresse [contact.medle@fabrique.social.gouv.fr](mailto:contact.medle@fabrique.social.gouv.fr). Il est préférable de préciser l'établissement auquel vous êtes rattaché, votre adresse email (qui vous servira d'identifiant) et l'objet de votre utilisation : déclaration des actes, déclaration des ETP, lecture seule...
## Qui peut être administrateur de la structure ?
L'administrateur de la structure a, en plus des droits des utilisateurs classiques, la possibilité de créer et supprimer des comptes. Il s'agit en général
L'administrateur de la structure a, en plus les droits des utilisateurs classiques, la possibilité de créer et supprimer des comptes. Il s'agit en général:
- du/de la responsable de la structure UMJ/IML pour ce qui concerne l'activité.
- du/des directeurs de l'établissement pour ce qui concerne les effectifs (Equivalent Temps Plein (ETP)
......@@ -54,7 +54,7 @@ Pour l'instant, si vous souhaitez vous créer un compte, vous devez passer par v
Pour toute suppression de compte, merci d'effectuer une demande par email à l'adresse [contact.medle@fabrique.social.gouv.fr](mailto:contact.medle@fabrique.social.gouv.fr). Afin de gagner du temps, vous pouvez préciser votre identifiant (adresse email), votre nom et votre prénom.