├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── Dockerfile.test ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── app.tsx ├── assets │ └── img │ │ ├── arkhn_logo_only_black.svg │ │ ├── arkhn_logo_only_white.svg │ │ └── logo.png ├── components │ ├── Lines │ │ ├── index.jsx │ │ └── index.test.jsx │ ├── bar.jsx │ ├── barGroup.jsx │ ├── responsive │ │ └── barGroup.jsx │ └── responsiveLines.jsx ├── index.html ├── routes.tsx ├── style.less └── views │ └── main │ ├── components │ ├── Admissions.jsx │ ├── Beds │ │ ├── components │ │ │ └── Drawer.tsx │ │ └── index.jsx │ ├── Bloc.jsx │ ├── Consultations.jsx │ ├── Gestes.jsx │ ├── GestesRevenu.jsx │ ├── Resources │ │ ├── components │ │ │ └── Drawer.tsx │ │ └── index.jsx │ └── Threshold │ │ ├── index.test.jsx │ │ ├── index.tsx │ │ └── style.less │ ├── index.tsx │ └── style.less ├── test └── setup.js ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-react" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-export-default-from", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | working_directory: ~ 5 | docker: 6 | - image: circleci/node:chakracore-10.13 7 | steps: 8 | - checkout 9 | - setup_remote_docker 10 | - run: 11 | name: Build and run docker container for server tests ; Update codecov results 12 | command: | 13 | docker build -f Dockerfile.test -t arkhn/dashboard:latest . 14 | docker run --name dashboard_test arkhn/dashboard:latest 15 | docker cp dashboard_test:/usr/src/app/coverage . 16 | bash <(curl -s https://codecov.io/bash) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/* 3 | 4 | coverage 5 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM node:10.15-alpine 2 | WORKDIR /usr/src/app 3 | COPY package.json yarn.lock ./ 4 | RUN yarn install 5 | COPY . . 6 | CMD yarn run test --coverage 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dashboard 2 | 3 | [![Arkhn](https://img.shields.io/badge/ARKHN-PRODUCT-000000.svg?style=for-the-badge&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABHNCSVQICAgIfAhkiAAABhRJREFUaIHtmVtMU3ccxw+l9H4KlbZQbrI1gcguMsacLJske2IPxCy6RF2m22ByG8gpp5SLiSRNsaWlXAq1UIYYp4kZmcPNLIoxPiwxMzG6kO3JGAKCpcA6Slvac6l7Wd3ZAXoudZ4t2+fx2/5+5/PNnwP0FAD+5z9Gdnb2G1qt9m2uPRLG6XROTUxMTHPtkRCZmZmvYRgWxXE8mp+fX8a1D2uGhoYuP/mDsbGx77n2YYVard6NoigeK4JhWDQvL+9Nrr0YMzg4OPmExMjIyHdcezFCpVK9jCAITi6CYVg0Ozu7lGs/2tjt9kvkEjGcTucVrv1osWPHjl3hcBjbrgiKorhGoynh2pMSq9V6cbsSMRwOx9dce8ZFoVAUhkIhlKoIgiB4ZmZmMde+22I2m89TlYgxMDDwFde+W5KWllYQCAQQukUQBMHVavUrXHtvoru7e4JuiRh2u/0S195/QS6Xa5mcRoxwOIwplcqXuPZ/itFoHGdaIkZPT88Frv0BAAAAEARf8Pv9EbLg4uLiOjlbWFjwb3Uq6enpuxL14CW6QKfTtYEgKCBmHo8n4HK5zpPfOzY2dnFubu43YiYUCpP1en17oh4JFZHJZPk6ne4YOTebzc5IJOIl5wiC+Ewmk4OcNzU1HU5LSytIxCWhIhAEGeRyuZCYeb3eoMvlsm83Mz4+3jc/P79GzMRiMV+v13ck4sK6iEQiyYUg6GNybrFYRiKRyNJ2cxiG+SwWyzA5hyDoQ7lcrmXrw7pIc3OzQaFQiIjZ8vJyyOl0WqlmR0dH7Y8fP14nZmKxmA/DMOt7hVURiUSSA8NwFTm3Wq3ucDjsoZpHUXTVZDJtOpXm5uaPQBB8kY0TqyINDQ0w+TRWV1c3hoaGKE8jhtvt7l1aWgoQMxAEBTqdro2NE+MiIpFIYzAYjpNzm802trGxsUB3D4IgK93d3U5y3tLSckwqleYx9WJcpL6+Hk5PTxcTM5/PFx4cHOxhusvlcvV6vd4gMQNBUABBEON7hVERoVCY2dbWVkvO7Xb72VAo9IjpxREE8VoslhFyDsPwJxKJJJfJLkZFamtrdSqVSkLM1tbWIn19faeZ7CHidDqtKysrIWKWmpoqhCCI0b1Cu4hAIFC3t7fXkfO+vr5zwWBwnslFiYTDYY/FYhkl5y0tLZ+KxeIsuntoF6mpqdFlZGTIiNn6+jrS29vL+jRiDA8P23w+X5iYKRQKUWNjo4HuDlpFUlJSlO3t7fXkvL+//3wgEJile7Ht2NjYWLBYLG5yrtfrq0UikYbODlpFqqurIY1GAxKzYDCI2my2bnqq1Dgcjp61tbUIMVMqlZKGhgY9nXnKInw+X3Hy5MnPyXl/f/8Fv9//kL5qfEKh0COr1foFOW9tbT0uFAozqOYpi1RVVUFZWVlyYhaNRp/Y7fZ+ZqrUOByOIQzDosRMrVZL6+rqYKrZuEX4fH5aR0dH46YhHi9penp6as+ePfuZ625NcXHxe9euXfuWz+dvcmptba0RCASqePNxi5SXl7+fk5OTutVrJSUlO2/fvn35zJkzV0AQ3MlM+0+kUmnOwMDA5N27d6/u3bt3y3/jMzIyZBUVFR+wvQYAAABQVlZ24P79+/PxHiAsLS0Fjhw50gYAwNOPvAaDoYv8vs7OTuKvav7Bgwd1i4uLmz7HE5mZmVnYt2/f4YRKxODxeLITJ070Uj3yuXnz5s+FhYXlVEW0Wu1b169f/ynerlAohMIwPJicnLzlT0RCaDSa3ZOTkz/EE0BRFDeZTOe6urqc5NeMRqP71KlT7q2+OyEyNTX1Y25u7uvPvACJpIqKis8ePHiwEk8Gx/EonYzI7Ozsr5WVlfXAM3i6QxuBQKA0Go1nid8TsgXDsKjZbP6Szt+Lv42CgoJ3bty4McO2xK1bt34pKip6l7MCJFIOHTrU6vF4Nj1d3I7l5eXg0aNHO5OSkoTU658zMpksb3h4+Jt49wKO41G32301NTWV1UOG50ppaWnlnTt3HpJL3Lt3b66srOwA136M4PF40rq6utN+vz8SCASQpqYmG4/HA6kn/6GoVKoijUbzKtce/xp+B+F302hEELoiAAAAAElFTkSuQmCC)](https://arkhn.org/) 4 | [![GitHub license](https://img.shields.io/badge/LICENSE-APACHE2-blue.svg?style=for-the-badge)](https://github.com/arkhn/dashboard/blob/master/LICENSE) 5 | ![CircleCI branch](https://img.shields.io/circleci/project/github/arkhn/dashboard/master.svg?style=for-the-badge&logo=circleci) 6 | [![Codecov branch](https://img.shields.io/codecov/c/github/arkhn/dashboard/master.svg?style=for-the-badge&logo=codecov)](https://codecov.io/gh/arkhn/dashboard/branch/master) 7 | 8 | ## Install 9 | 10 | ``` 11 | yarn install 12 | ``` 13 | 14 | ## Start contributing 15 | 16 | We have reported several issues with the label `Good first issue` which can be a good way to start! Also of course, feel free to contact us on Slack in you have trouble with the project. 17 | 18 | If you're enthusiastic about our project, :star: it to show your support! :heart: 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | coverageDirectory: "coverage", 6 | globals: { 7 | "ts-jest": { 8 | tsConfig: "tsconfig.json" 9 | } 10 | }, 11 | setupFiles: ["/test/setup.js"], 12 | testEnvironment: "node", 13 | testPathIgnorePatterns: ["/node_modules/"], 14 | transform: { 15 | "\\.(js|jsx)$": "/node_modules/babel-jest", 16 | "^.+\\.(ts|tsx)?$": "ts-jest", 17 | ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": 18 | "jest-transform-stub" 19 | }, 20 | transformIgnorePatterns: ["/node_modules/"] 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "run-script-os", 6 | "start:darwin:linux": "NODE_ENV=development webpack-dev-server --port 1832 --hot --host 0.0.0.0 --mode development", 7 | "start:win32": "set NODE_ENV=development & webpack-dev-server --port 1832 --hot --host 0.0.0.0 --mode development", 8 | "build": "webpack --mode production", 9 | "test": "yarn run jest" 10 | }, 11 | "dependencies": { 12 | "@babel/core": "^7.4.3", 13 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 14 | "@babel/preset-env": "^7.4.3", 15 | "@babel/preset-react": "^7.0.0", 16 | "@blueprintjs/core": "^3.15.1", 17 | "@blueprintjs/icons": "^3.2.0", 18 | "@blueprintjs/select": "^3.2.0", 19 | "@types/enzyme": "^3.9.1", 20 | "@types/jest": "^24.0.11", 21 | "@types/node": "^11.13.2", 22 | "@types/react": "^16.4.18", 23 | "@types/react-addons-css-transition-group": "^15.0.3", 24 | "@types/react-dom": "^16.0.9", 25 | "@types/react-hot-loader": "^4.1.0", 26 | "@types/react-json-pretty": "^1.3.4", 27 | "@types/react-redux": "^6.0.9", 28 | "@types/react-router": "^4.0.32", 29 | "@types/react-router-dom": "^4.3.1", 30 | "@types/redux-logger": "^3.0.6", 31 | "@types/webpack": "^4.4.17", 32 | "@types/webpack-env": "^1.13.6", 33 | "@vx/axis": "0.0.184", 34 | "@vx/glyph": "^0.0.183", 35 | "@vx/gradient": "0.0.183", 36 | "@vx/group": "0.0.183", 37 | "@vx/legend": "^0.0.183", 38 | "@vx/mock-data": "0.0.185", 39 | "@vx/responsive": "0.0.188", 40 | "@vx/scale": "0.0.182", 41 | "@vx/shape": "0.0.184", 42 | "@vx/tooltip": "^0.0.184", 43 | "awesome-typescript-loader": "^5.2.1", 44 | "babel-jest": "^24.7.1", 45 | "babel-loader": "^8.0.5", 46 | "css-loader": "^1.0.0", 47 | "d3-time-format": "^2.1.3", 48 | "enzyme": "^3.9.0", 49 | "enzyme-adapter-react-16": "^1.12.1", 50 | "favicons-webpack-plugin": "^0.0.9", 51 | "file-loader": "^2.0.0", 52 | "html-webpack-plugin": "^3.2.0", 53 | "jest": "^24.7.1", 54 | "less": "^3.8.1", 55 | "less-loader": "^4.0.5", 56 | "raw-loader": "^1.0.0", 57 | "react": "^16.5.2", 58 | "react-addons-css-transition-group": "^15.6.0", 59 | "react-dom": "^16.5.2", 60 | "react-hot-loader": "^4.3.12", 61 | "react-json-pretty": "^1.7.9", 62 | "react-redux": "^5.0.7", 63 | "react-router": "^4.3.1", 64 | "react-router-dom": "^4.3.1", 65 | "redux": "^4.0.1", 66 | "redux-logger": "^3.0.6", 67 | "style-loader": "^0.23.1", 68 | "svg-inline-loader": "^0.8.0", 69 | "ts-jest": "^24.0.2", 70 | "ts-loader": "^5.2.2", 71 | "typescript": "^3.1.3", 72 | "webpack": "^4.22.0", 73 | "webpack-cli": "^3.1.2", 74 | "webpack-dev-server": "^3.1.10" 75 | }, 76 | "devDependencies": { 77 | "husky": "^1.3.1", 78 | "jest-transform-stub": "^2.0.0", 79 | "prettier": "1.17.0", 80 | "pretty-quick": "^1.10.0", 81 | "run-script-os": "^1.0.5" 82 | }, 83 | "husky": { 84 | "hooks": { 85 | "pre-commit": "pretty-quick --staged" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import "./style.less"; 5 | import Routes from "./routes"; 6 | 7 | ReactDOM.render(, document.getElementById("application-wrapper")); 8 | -------------------------------------------------------------------------------- /src/assets/img/arkhn_logo_only_black.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 43 | 72 | 111 | 126 | 129 | 136 | 140 | 141 | 323 | -------------------------------------------------------------------------------- /src/assets/img/arkhn_logo_only_white.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 43 | 72 | 111 | 114 | 121 | 126 | 127 | 142 | 324 | -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkhn/dashboard/17151878f81f164282558ca2eaddba37ad649584/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/components/Lines/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Group } from "@vx/group"; 3 | import { LinePath } from "@vx/shape"; 4 | import { curveMonotoneX } from "@vx/curve"; 5 | import { genDateValue } from "@vx/mock-data"; 6 | import { scaleTime, scaleLinear } from "@vx/scale"; 7 | import { extent, max } from "d3-array"; 8 | 9 | function genLines(num) { 10 | return new Array(num).fill(1).map(() => { 11 | return genDateValue(25); 12 | }); 13 | } 14 | 15 | const series = genLines(12); 16 | const data = series.reduce((rec, d) => { 17 | return rec.concat(d); 18 | }, []); 19 | 20 | // accessors 21 | const x = d => d.date; 22 | const y = d => d.value; 23 | 24 | const Lines = ({ width, height }) => { 25 | // bounds 26 | const xMax = width; 27 | const yMax = height / 8; 28 | 29 | // scales 30 | const xScale = scaleTime({ 31 | range: [0, xMax], 32 | domain: extent(data, x) 33 | }); 34 | const yScale = scaleLinear({ 35 | range: [yMax, 0], 36 | domain: [0, max(data, y)] 37 | }); 38 | 39 | return ( 40 | 41 | 42 | {xMax > 8 && 43 | series.map((d, i) => { 44 | return ( 45 | 46 | xScale(x(d))} 49 | y={d => yScale(y(d))} 50 | stroke={"#ffffff"} 51 | strokeWidth={1} 52 | curve={i % 2 == 0 ? curveMonotoneX : undefined} 53 | /> 54 | 55 | ); 56 | })} 57 | 58 | ); 59 | }; 60 | 61 | export default Lines; 62 | -------------------------------------------------------------------------------- /src/components/Lines/index.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import Lines from "./index"; 4 | 5 | describe("Lines component", () => { 6 | it("Renders without crashing", () => { 7 | shallow(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/bar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Bar } from "@vx/shape"; 3 | import { Group } from "@vx/group"; 4 | import { GradientTealBlue } from "@vx/gradient"; 5 | import { letterFrequency } from "@vx/mock-data"; 6 | import { scaleBand, scaleLinear } from "@vx/scale"; 7 | 8 | const data = letterFrequency.slice(5); 9 | 10 | // accessors 11 | const x = d => d.letter; 12 | const y = d => +d.frequency * 100; 13 | 14 | export default ({ width, height }) => { 15 | // bounds 16 | const xMax = width; 17 | const yMax = height - 120; 18 | 19 | // scales 20 | const xScale = scaleBand({ 21 | rangeRound: [0, xMax], 22 | domain: data.map(x), 23 | padding: 0.4 24 | }); 25 | const yScale = scaleLinear({ 26 | rangeRound: [yMax, 0], 27 | domain: [0, Math.max(...data.map(y))] 28 | }); 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | {data.map((d, i) => { 36 | const letter = x(d); 37 | const barWidth = xScale.bandwidth(); 38 | const barHeight = yMax - yScale(y(d)); 39 | const barX = xScale(letter); 40 | const barY = yMax - barHeight; 41 | return ( 42 | { 50 | alert(`clicked: ${JSON.stringify(Object.values(d))}`); 51 | }} 52 | /> 53 | ); 54 | })} 55 | 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/barGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Group } from "@vx/group"; 3 | import { BarGroup } from "@vx/shape"; 4 | import { AxisBottom } from "@vx/axis"; 5 | import { cityTemperature } from "@vx/mock-data"; 6 | import { scaleBand, scaleLinear, scaleOrdinal } from "@vx/scale"; 7 | import { timeParse, timeFormat } from "d3-time-format"; 8 | 9 | const blue = "#aeeef8"; 10 | const green = "#e5fd3d"; 11 | const purple = "#9caff6"; 12 | const bg = "#612efb"; 13 | 14 | const data = cityTemperature.slice(0, 8); 15 | const keys = Object.keys(data[0]).filter(d => d !== "date"); 16 | 17 | const parseDate = timeParse("%Y%m%d"); 18 | const format = timeFormat("%b %d"); 19 | const formatDate = date => format(parseDate(date)); 20 | 21 | // accessors 22 | const x0 = d => d.date; 23 | 24 | // scales 25 | const x0Scale = scaleBand({ 26 | domain: data.map(x0), 27 | padding: 0.2 28 | }); 29 | const x1Scale = scaleBand({ 30 | domain: keys, 31 | padding: 0.1 32 | }); 33 | const yScale = scaleLinear({ 34 | domain: [0, Math.max(...data.map(d => Math.max(...keys.map(key => d[key]))))] 35 | }); 36 | const color = scaleOrdinal({ 37 | domain: keys, 38 | range: [blue, green, purple] 39 | }); 40 | 41 | export default ({ 42 | width, 43 | height, 44 | margin = { 45 | top: 40 46 | } 47 | }) => { 48 | // bounds 49 | const xMax = width; 50 | const yMax = height - margin.top - 100; 51 | 52 | x0Scale.rangeRound([0, xMax]); 53 | x1Scale.rangeRound([0, x0Scale.bandwidth()]); 54 | yScale.range([yMax, 0]); 55 | 56 | return ( 57 | 58 | 59 | 60 | 70 | {barGroups => { 71 | return barGroups.map(barGroup => { 72 | return ( 73 | 77 | {barGroup.bars.map(bar => { 78 | return ( 79 | { 90 | const { key, value } = bar; 91 | alert(JSON.stringify({ key, value })); 92 | }} 93 | /> 94 | ); 95 | })} 96 | 97 | ); 98 | }); 99 | }} 100 | 101 | 102 | ({ 110 | fill: green, 111 | fontSize: 11, 112 | textAnchor: "middle" 113 | })} 114 | /> 115 | 116 | ); 117 | }; 118 | -------------------------------------------------------------------------------- /src/components/responsive/barGroup.jsx: -------------------------------------------------------------------------------- 1 | import { ParentSize } from "@vx/responsive"; 2 | import React from "react"; 3 | 4 | export default class App extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | const { width, height } = this.props; 11 | return ( 12 | 13 | {({ width: w, height: h }) => { 14 | return ( 15 | 25 | ); 26 | }} 27 | 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/responsiveLines.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ParentSize } from "@vx/responsive"; 3 | 4 | import Lines from "./lines"; 5 | 6 | function Nav() { 7 | return ( 8 |
    9 |
  • 🤖
  • 10 |
  • Home
  • 11 |
  • Profile
  • 12 |
  • Favorites
  • 13 |
  • Settings
  • 14 |
15 | ); 16 | } 17 | 18 | export default class App extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { showNav: true }; 22 | this.toggleNav = this.toggleNav.bind(this); 23 | } 24 | toggleNav() { 25 | this.setState(prevState => { 26 | return { 27 | showNav: !prevState.showNav 28 | }; 29 | }); 30 | } 31 | render() { 32 | const { width, height } = this.props; 33 | return ( 34 |
35 |
41 |
43 |
44 |
45 | 46 |
47 |
48 | 49 | {({ width: w, height: h }) => { 50 | return ( 51 | 61 | ); 62 | }} 63 | 64 |
65 |
66 | 67 | 96 |
97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Arkhn | Dashboard 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Route } from "react-router"; 3 | import { BrowserRouter, Switch } from "react-router-dom"; 4 | 5 | import Main from "./views/main"; 6 | 7 | const Routes = () => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default Routes; 16 | -------------------------------------------------------------------------------- /src/style.less: -------------------------------------------------------------------------------- 1 | @import "~@blueprintjs/core/lib/css/blueprint.css"; 2 | @import "~@blueprintjs/core/lib/less/variables"; 3 | 4 | @window-padding: 20px; 5 | @border-radius: 6px; 6 | 7 | body { 8 | padding: 0; 9 | margin: 0; 10 | } 11 | 12 | #application-wrapper { 13 | height: 100%; 14 | width: 100%; 15 | box-sizing: border-box; 16 | } 17 | 18 | // .primary { 19 | // color: @blue3; 20 | // } 21 | // 22 | // .success { 23 | // color: @green3; 24 | // } 25 | // 26 | // .warning { 27 | // color: @orange3; 28 | // } 29 | // 30 | // .danger { 31 | // color: @red3; 32 | // } 33 | -------------------------------------------------------------------------------- /src/views/main/components/Admissions.jsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "@blueprintjs/core"; 2 | import { AxisBottom, AxisLeft } from "@vx/axis"; 3 | import { Group } from "@vx/group"; 4 | import { LegendItem, LegendLabel, LegendOrdinal } from "@vx/legend"; 5 | import { ParentSize } from "@vx/responsive"; 6 | import { scaleBand, scaleLinear, scaleOrdinal } from "@vx/scale"; 7 | import { BarGroup, LinePath } from "@vx/shape"; 8 | import { withTooltip, Tooltip } from "@vx/tooltip"; 9 | import { timeParse, timeFormat } from "d3-time-format"; 10 | import React from "react"; 11 | 12 | const black = Colors.BLACK; 13 | 14 | const color1 = Colors.BLUE2; 15 | const color12 = Colors.BLUE1; 16 | const color2 = Colors.BLUE4; 17 | const color22 = Colors.BLUE3; 18 | 19 | const axisColor = Colors.DARK_GRAY1; 20 | const tooltipColor = Colors.DARK_GRAY5; 21 | 22 | const data = [ 23 | { date: "20190403", Entrées: 57, Sorties: 58 }, 24 | { date: "20190404", Entrées: 71, Sorties: 55 }, 25 | { date: "20190405", Entrées: 63, Sorties: 53 }, 26 | { date: "20190406", Entrées: 40, Sorties: 45 }, 27 | { date: "20190407", Entrées: 55, Sorties: 47 }, 28 | { date: "20190408", Entrées: 63, Sorties: 69 }, 29 | { date: "20190409", Entrées: 60, Sorties: 75 } 30 | ]; 31 | const dataLastYear = [ 32 | { date: "20180403", Entrées: 48, Sorties: 45 }, 33 | { date: "20180404", Entrées: 43, Sorties: 47 }, 34 | { date: "20180405", Entrées: 45, Sorties: 53 }, 35 | { date: "20180406", Entrées: 37, Sorties: 52 }, 36 | { date: "20180407", Entrées: 59, Sorties: 58 }, 37 | { date: "20180408", Entrées: 61, Sorties: 52 }, 38 | { date: "20180409", Entrées: 55, Sorties: 54 } 39 | ]; 40 | const keys = ["Entrées", "Sorties"]; 41 | 42 | const parseDate = timeParse("%Y%m%d"); 43 | const format = timeFormat("%d %b"); 44 | const formatDate = date => format(parseDate(date)); 45 | 46 | // accessors 47 | const x0 = d => d.date; 48 | 49 | // scales 50 | const x0Scale = scaleBand({ 51 | domain: data.map(x0), 52 | padding: 0.2 53 | }); 54 | 55 | const x0ScaleLastYear = scaleBand({ 56 | domain: dataLastYear.map(x0), 57 | padding: 0.2 58 | }); 59 | 60 | const x1Scale = scaleBand({ 61 | domain: keys, 62 | padding: 0.1 63 | }); 64 | 65 | const x1ScaleLastYear = scaleBand({ 66 | domain: keys, 67 | padding: 0.1 68 | }); 69 | 70 | const yScale = scaleLinear({ 71 | domain: [ 72 | 0, 73 | Math.max( 74 | ...data 75 | .concat(dataLastYear) 76 | .map(d => Math.max(...keys.map(key => d[key]))) 77 | ) 78 | ] 79 | }); 80 | 81 | const color = scaleOrdinal({ 82 | domain: keys, 83 | range: [color1, color2] 84 | }); 85 | 86 | let tooltipTimeout; 87 | 88 | const CoreComponent = withTooltip( 89 | ({ 90 | w, 91 | h, 92 | padding = { top: 40, left: 40 }, 93 | tooltipOpen, 94 | tooltipLeft, 95 | tooltipTop, 96 | tooltipData, 97 | hideTooltip, 98 | showTooltip 99 | }) => { 100 | // bounds 101 | const xMax = w - padding.left; 102 | const yMax = h - padding.top - 50; 103 | 104 | x0Scale.rangeRound([0, xMax]); 105 | x0ScaleLastYear.rangeRound([0, xMax]); 106 | x1Scale.rangeRound([0, x0Scale.bandwidth()]); 107 | yScale.range([yMax, 0]); 108 | 109 | return ( 110 |
111 | 112 | 119 | Entrées et Sorties 120 | 121 | 131 | {barGroups => { 132 | return barGroups.map(barGroup => { 133 | return ( 134 | 138 | {barGroup.bars.map(bar => { 139 | return ( 140 | { 152 | const { key, value } = bar; 153 | alert(JSON.stringify({ key, value })); 154 | }} 155 | onMouseLeave={event => { 156 | tooltipTimeout = setTimeout(() => { 157 | hideTooltip(); 158 | }, 300); 159 | }} 160 | onMouseMove={event => { 161 | if (tooltipTimeout) clearTimeout(tooltipTimeout); 162 | const top = bar.y; 163 | const left = barGroup.x0 + bar.x; 164 | 165 | showTooltip({ 166 | tooltipData: { 167 | bar 168 | }, 169 | tooltipTop: top, 170 | tooltipLeft: left 171 | }); 172 | }} 173 | /> 174 | ); 175 | })} 176 | 177 | ); 178 | }); 179 | }} 180 | 181 | ({ 189 | fill: axisColor, 190 | fontSize: 11, 191 | textAnchor: "middle" 192 | })} 193 | /> 194 | ({ 200 | dy: "0.4em", 201 | fill: axisColor, 202 | fontSize: 11, 203 | textAnchor: "end" 204 | })} 205 | /> 206 | 207 | 208 |
209 | `${label.toUpperCase()}`} 212 | > 213 | {labels => { 214 | return ( 215 |
216 | {labels.map((label, i) => { 217 | const size = 10; 218 | return ( 219 | { 223 | alert(`clicked: ${JSON.stringify(label)}`); 224 | }} 225 | > 226 | 227 | 228 | 229 | 230 | {label.text} 231 | 232 | 233 | ); 234 | })} 235 |
236 | ); 237 | }} 238 |
239 |
240 | {tooltipOpen && ( 241 | 250 |
251 | {tooltipData.bar.key} 252 |
253 |
{tooltipData.bar.value}
254 |
255 | )} 256 |
257 | ); 258 | } 259 | ); 260 | 261 | export default ({ padding = { top: 40, left: 40 } }) => { 262 | return ( 263 | 264 | {({ width: w, height: h }) => { 265 | return ; 266 | }} 267 | 268 | ); 269 | }; 270 | -------------------------------------------------------------------------------- /src/views/main/components/Beds/components/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Threshold from "../../Threshold"; 4 | 5 | interface IProps { 6 | warningBeds: number; 7 | setWarningBeds: any; 8 | notifyWarningBeds: boolean; 9 | setNotifyWarningBeds: any; 10 | dangerBeds: number; 11 | setDangerBeds: any; 12 | notifyDangerBeds: boolean; 13 | setNotifyDangerBeds: any; 14 | visible: boolean; 15 | } 16 | 17 | const Component = ({ 18 | warningBeds, 19 | setWarningBeds, 20 | notifyWarningBeds, 21 | setNotifyWarningBeds, 22 | dangerBeds, 23 | setDangerBeds, 24 | notifyDangerBeds, 25 | setNotifyDangerBeds, 26 | visible 27 | }: IProps) => { 28 | return ( 29 |
30 | 37 | 44 |
45 | ); 46 | }; 47 | 48 | export default Component; 49 | -------------------------------------------------------------------------------- /src/views/main/components/Beds/index.jsx: -------------------------------------------------------------------------------- 1 | import { ProgressBar } from "@blueprintjs/core"; 2 | import React from "react"; 3 | 4 | export default ({ beds, totalBeds }) => { 5 | return ( 6 |
7 |
8 | Total: {beds} / {totalBeds} 9 | 14 |
15 |

Critiques

16 |
17 | Surveillance continue: 12/15 18 | 19 |
20 |
21 | Réanimation: 5/8 22 | 23 |
24 |
25 | Soins intensifs: 2/4 26 | 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/views/main/components/Bloc.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => { 4 | return ( 5 |
6 |
7 | Retard cumulé: 10 heures 8 |
9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/views/main/components/Consultations.jsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "@blueprintjs/core"; 2 | import { AxisBottom, AxisLeft } from "@vx/axis"; 3 | import { GlyphDot } from "@vx/glyph"; 4 | import { Group } from "@vx/group"; 5 | import { LegendItem, LegendLabel, LegendOrdinal } from "@vx/legend"; 6 | import { genDateValue } from "@vx/mock-data"; 7 | import { ParentSize } from "@vx/responsive"; 8 | import { scaleTime, scaleLinear, scaleOrdinal } from "@vx/scale"; 9 | import { LinePath } from "@vx/shape"; 10 | import React from "react"; 11 | import { extent, max } from "d3-array"; 12 | import { timeParse, timeFormat } from "d3-time-format"; 13 | 14 | const axisColor = Colors.DARK_GRAY1; 15 | 16 | const colors = [ 17 | "#669EFF", 18 | "#C274C2", 19 | "#62D96B", 20 | "#AD99FF", 21 | "#2EE6D6", 22 | "#D1F26D", 23 | "#FFC940", 24 | "#C99765", 25 | "#FF66A1", 26 | "#FF6E4A" 27 | ]; 28 | 29 | // const parseDate = timeParse("%a %m %d %Y"); 30 | const format = timeFormat("%d %b"); 31 | const formatDate = date => format(date); 32 | 33 | function genLines(num) { 34 | return new Array(num).fill(1).map((d, i) => { 35 | return Array(7) 36 | .fill(1) 37 | .map((d, i) => { 38 | return { 39 | date: new Date(Date.now() - i * 24 * 3600000), 40 | value: Math.max( 41 | (38 + Math.random() * 5) | 0, 42 | (Math.random() * 80) | 0 43 | ) 44 | }; 45 | }); 46 | }); 47 | } 48 | 49 | const series = genLines(3); 50 | const data = series.reduce((rec, d) => { 51 | return rec.concat(d); 52 | }, []); 53 | 54 | console.log(series); 55 | 56 | const x = d => d.date; 57 | const y = d => d.value; 58 | 59 | const keys = ["Médecine Générale", "Pédiatrie", "Radiologie"]; 60 | 61 | const colorScale = scaleOrdinal({ 62 | domain: keys, 63 | range: series.map((s, i) => colors[i]) 64 | }); 65 | 66 | export default () => { 67 | return ( 68 | 69 | {({ width: w, height: h }) => { 70 | // bounds 71 | const padding = { 72 | top: 40, 73 | bottom: 40, 74 | left: 40, 75 | right: 40 76 | }; 77 | 78 | const xMax = w - padding.left - padding.right; 79 | const yMax = h - padding.top - padding.bottom - 10; 80 | 81 | // scales 82 | const xScale = scaleTime({ 83 | range: [0, xMax], 84 | domain: extent(data, x) 85 | }); 86 | const yScale = scaleLinear({ 87 | range: [yMax, 0], 88 | domain: [0, max(data, y)] 89 | }); 90 | 91 | return ( 92 |
93 | 94 | {xMax > 8 && 95 | series.map((d, i) => { 96 | return ( 97 | 98 | xScale(x(d))} 101 | y={d => yScale(y(d))} 102 | stroke={colors[i]} 103 | strokeWidth={2} 104 | /> 105 | {d.map((p, j) => { 106 | const cx = xScale(x(p)); 107 | const cy = yScale(y(p)); 108 | return ( 109 | 110 | 116 | 117 | 118 | ); 119 | })} 120 | 121 | ); 122 | })} 123 | ({ 132 | fill: axisColor, 133 | fontSize: 11, 134 | textAnchor: "middle" 135 | })} 136 | tickValues={Array(7) 137 | .fill(1) 138 | .map((d, i) => { 139 | return new Date(Date.now() - i * 24 * 3600000); 140 | })} 141 | /> 142 | ({ 149 | dy: "0.4em", 150 | fill: axisColor, 151 | fontSize: 11, 152 | textAnchor: "end" 153 | })} 154 | /> 155 | 156 |
157 | `${label.toUpperCase()}`} 160 | > 161 | {labels => { 162 | return ( 163 |
164 | {labels.map((label, i) => { 165 | const size = 10; 166 | return ( 167 | { 171 | alert(`clicked: ${JSON.stringify(label)}`); 172 | }} 173 | > 174 | 175 | 180 | 181 | 182 | {label.text} 183 | 184 | 185 | ); 186 | })} 187 |
188 | ); 189 | }} 190 |
191 |
192 |
193 | ); 194 | }} 195 |
196 | ); 197 | }; 198 | -------------------------------------------------------------------------------- /src/views/main/components/Gestes.jsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "@blueprintjs/core"; 2 | import { GradientPinkBlue } from "@vx/gradient"; 3 | import { Group } from "@vx/group"; 4 | import { letterFrequency, browserUsage } from "@vx/mock-data"; 5 | import { ParentSize } from "@vx/responsive"; 6 | import { Pie } from "@vx/shape"; 7 | import React from "react"; 8 | 9 | const white = Colors.WHITE; 10 | const black = Colors.BLACK; 11 | 12 | const color1 = Colors.BLUE3; 13 | 14 | const colors = [ 15 | "#669EFF", 16 | "#C274C2", 17 | "#62D96B", 18 | "#AD99FF", 19 | "#2EE6D6", 20 | "#D1F26D", 21 | "#FFC940", 22 | "#C99765", 23 | "#FF66A1", 24 | "#FF6E4A" 25 | ]; 26 | 27 | const letters = letterFrequency.slice(0, 4); 28 | const browserNames = Object.keys(browserUsage[0]).filter(k => k !== "date"); 29 | const browsers = browserNames.map(k => ({ 30 | label: k, 31 | usage: browserUsage[0][k] 32 | })); 33 | 34 | let data = [ 35 | { acte: "Acte 1", Occurrences: 10, unitary_cost: 2000 }, 36 | { acte: "Acte 2", Occurrences: 5, unitary_cost: 400 }, 37 | { acte: "Acte 3", Occurrences: 35, unitary_cost: 120 }, 38 | { acte: "Acte 4", Occurrences: 14, unitary_cost: 330 }, 39 | { acte: "Acte 5", Occurrences: 1, unitary_cost: 8000 }, 40 | { acte: "Acte 6", Occurrences: 2, unitary_cost: 1200 }, 41 | { acte: "Acte 7", Occurrences: 4, unitary_cost: 80 }, 42 | { acte: "Acte 8", Occurrences: 2, unitary_cost: 170 } 43 | ]; 44 | 45 | data = data.map(acte => { 46 | return { 47 | ...acte, 48 | Revenu: (acte.Occurrences * acte.unitary_cost) / 1000 49 | }; 50 | }); 51 | 52 | const revenu = d => d.Revenu; 53 | 54 | export default () => { 55 | return ( 56 | 57 | {({ width: w, height: h }) => { 58 | const radius = Math.min(w, h) / 2; 59 | const centerY = h / 2; 60 | const centerX = w / 2; 61 | 62 | console.log(data); 63 | const totalRecettes = data.reduce((a, b) => a + b.Revenu, 0); 64 | console.log(totalRecettes); 65 | 66 | return ( 67 | 68 | 69 | Recettes par Acte 70 | 71 | 72 | 80 | {pie => { 81 | return pie.arcs.map((arc, i) => { 82 | const [centroidX, centroidY] = pie.path.centroid(arc); 83 | const { startAngle, endAngle } = arc; 84 | const hasSpaceForLabel = endAngle - startAngle >= 0.1; 85 | return ( 86 | 87 | 92 | {hasSpaceForLabel && ( 93 | 101 | {arc.data.acte} 102 | 103 | )} 104 | 105 | ); 106 | }); 107 | }} 108 | 109 | 110 | {Math.round(totalRecettes)}K 111 | 112 | 113 | 114 | ); 115 | }} 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /src/views/main/components/GestesRevenu.jsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "@blueprintjs/core"; 2 | import { AxisBottom } from "@vx/axis"; 3 | import { Group } from "@vx/group"; 4 | import { ParentSize } from "@vx/responsive"; 5 | import { scaleBand, scaleLinear, scaleOrdinal } from "@vx/scale"; 6 | import { BarGroup, LinePath } from "@vx/shape"; 7 | import { withTooltip, Tooltip } from "@vx/tooltip"; 8 | import { timeParse, timeFormat } from "d3-time-format"; 9 | import React from "react"; 10 | 11 | const black = Colors.BLACK; 12 | 13 | const color1 = Colors.BLUE2; 14 | const color12 = Colors.BLUE1; 15 | const color2 = Colors.BLUE4; 16 | const color22 = Colors.BLUE3; 17 | 18 | const axisColor = Colors.DARK_GRAY1; 19 | const tooltipColor = Colors.DARK_GRAY5; 20 | 21 | let data = [ 22 | { acte: "Acte 1", Occurrences: 10, unitary_cost: 2000 }, 23 | { acte: "Acte 2", Occurrences: 5, unitary_cost: 400 }, 24 | { acte: "Acte 3", Occurrences: 35, unitary_cost: 120 }, 25 | { acte: "Acte 4", Occurrences: 14, unitary_cost: 330 }, 26 | { acte: "Acte 5", Occurrences: 1, unitary_cost: 8000 }, 27 | { acte: "Acte 6", Occurrences: 2, unitary_cost: 1200 }, 28 | { acte: "Acte 7", Occurrences: 4, unitary_cost: 80 }, 29 | { acte: "Acte 8", Occurrences: 2, unitary_cost: 170 } 30 | ]; 31 | 32 | data = data.map(acte => { 33 | return { 34 | ...acte, 35 | Revenu: (acte.Occurrences * acte.unitary_cost) / 1000 36 | }; 37 | }); 38 | 39 | const keys = ["Occurrences", "Revenu"]; 40 | 41 | // accessors 42 | const x0 = d => d.acte; 43 | 44 | // scales 45 | const x0Scale = scaleBand({ 46 | domain: data.map(x0), 47 | padding: 0.2 48 | }); 49 | 50 | const x1Scale = scaleBand({ 51 | domain: keys, 52 | padding: 0.1 53 | }); 54 | 55 | const yScale = scaleLinear({ 56 | domain: [0, Math.max(...data.map(d => Math.max(...keys.map(key => d[key]))))] 57 | }); 58 | 59 | const color = scaleOrdinal({ 60 | domain: keys, 61 | range: [color1, color2] 62 | }); 63 | 64 | let tooltipTimeout; 65 | 66 | const TestComponent = withTooltip( 67 | ({ 68 | w, 69 | h, 70 | padding = { top: 40 }, 71 | tooltipOpen, 72 | tooltipLeft, 73 | tooltipTop, 74 | tooltipData, 75 | hideTooltip, 76 | showTooltip 77 | }) => { 78 | // bounds 79 | const xMax = w; 80 | const yMax = h - padding.top - 50; 81 | 82 | x0Scale.rangeRound([0, xMax]); 83 | x1Scale.rangeRound([0, x0Scale.bandwidth()]); 84 | yScale.range([yMax, 0]); 85 | 86 | return ( 87 |
88 | 89 | 96 | Actes Tarifés 97 | 98 | 108 | {barGroups => { 109 | return barGroups.map(barGroup => { 110 | return ( 111 | 115 | {barGroup.bars.map(bar => { 116 | return ( 117 | { 129 | const { key, value } = bar; 130 | alert(JSON.stringify({ key, value })); 131 | }} 132 | onMouseLeave={event => { 133 | tooltipTimeout = setTimeout(() => { 134 | hideTooltip(); 135 | }, 300); 136 | }} 137 | onMouseMove={event => { 138 | if (tooltipTimeout) clearTimeout(tooltipTimeout); 139 | const top = bar.y; 140 | const left = barGroup.x0 + bar.x; 141 | 142 | showTooltip({ 143 | tooltipData: { 144 | bar 145 | }, 146 | tooltipTop: top, 147 | tooltipLeft: left 148 | }); 149 | }} 150 | /> 151 | ); 152 | })} 153 | 154 | ); 155 | }); 156 | }} 157 | 158 | ({ 165 | fill: axisColor, 166 | fontSize: 11, 167 | textAnchor: "middle" 168 | })} 169 | /> 170 | 171 | 172 | {tooltipOpen && ( 173 | 182 |
183 | {tooltipData.bar.key} 184 |
185 |
186 | {tooltipData.bar.key == "Revenu" 187 | ? tooltipData.bar.value * 1000 188 | : tooltipData.bar.value} 189 |
190 |
191 | )} 192 |
193 | ); 194 | } 195 | ); 196 | 197 | export default ({ padding = { top: 40 } }) => { 198 | return ( 199 | 200 | {({ width: w, height: h }) => { 201 | return ; 202 | }} 203 | 204 | ); 205 | }; 206 | -------------------------------------------------------------------------------- /src/views/main/components/Resources/components/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Threshold from "../../Threshold"; 4 | 5 | interface IProps { 6 | warningIde: number; 7 | setWarningIde: any; 8 | notifyWarningIde: boolean; 9 | setNotifyWarningIde: any; 10 | dangerIde: number; 11 | setDangerIde: any; 12 | notifyDangerIde: boolean; 13 | setNotifyDangerIde: any; 14 | warningAides: number; 15 | setWarningAides: any; 16 | notifyWarningAides: boolean; 17 | setNotifyWarningAides: any; 18 | dangerAides: number; 19 | setDangerAides: any; 20 | notifyDangerAides: boolean; 21 | setNotifyDangerAides: any; 22 | visible: boolean; 23 | } 24 | 25 | const Component = ({ 26 | warningIde, 27 | setWarningIde, 28 | notifyWarningIde, 29 | setNotifyWarningIde, 30 | dangerIde, 31 | setDangerIde, 32 | notifyDangerIde, 33 | setNotifyDangerIde, 34 | warningAides, 35 | setWarningAides, 36 | notifyWarningAides, 37 | setNotifyWarningAides, 38 | dangerAides, 39 | setDangerAides, 40 | notifyDangerAides, 41 | setNotifyDangerAides, 42 | visible 43 | }: IProps) => { 44 | return ( 45 |
46 | 54 | 62 | 70 | 78 |
79 | ); 80 | }; 81 | 82 | export default Component; 83 | -------------------------------------------------------------------------------- /src/views/main/components/Resources/index.jsx: -------------------------------------------------------------------------------- 1 | import { ProgressBar } from "@blueprintjs/core"; 2 | import React from "react"; 3 | 4 | export default ({ ide, totalIde, aides, totalAides }) => { 5 | return ( 6 |
7 |

IDE

8 |
9 | Total: {ide}/{totalIde} 10 | 11 |
12 |
13 | Réanimation: 3/12 14 | 15 |
16 |
Ratio 1:5
17 |
18 | Soins intensifs: 2/4 19 | 20 |
21 |
Ratio 1:3
22 |

Aide soignant(e)s

23 |
24 | Total: {aides}/{totalAides} 25 | 30 |
31 |
32 | Réanimation: 9/31 33 | 34 |
35 |
Ratio 1:2
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/views/main/components/Threshold/index.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import Threshold from "./index"; 4 | 5 | describe("Threshold component", () => { 6 | it("Renders without crashing", () => { 7 | shallow( 8 | {}} 13 | notify={true} 14 | setNotify={() => {}} 15 | /> 16 | ); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/main/components/Threshold/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alignment, 3 | Icon, 4 | Label, 5 | NumericInput, 6 | Switch, 7 | Tag 8 | } from "@blueprintjs/core"; 9 | import * as React from "react"; 10 | 11 | import "./style.less"; 12 | 13 | interface IProps { 14 | danger?: boolean; 15 | warning?: boolean; 16 | label: string; 17 | threshold: number; 18 | setThreshold: any; 19 | notify: boolean; 20 | setNotify: any; 21 | } 22 | 23 | const Threshold = ({ 24 | danger, 25 | warning, 26 | label, 27 | threshold, 28 | setThreshold, 29 | notify, 30 | setNotify 31 | }: IProps) => { 32 | return ( 33 |
38 |
39 | 40 | { 43 | setThreshold(valueAsNumber); 44 | }} 45 | /> 46 |
47 |
48 | ) => { 53 | setNotify((currentNotify: boolean) => !currentNotify); 54 | }} 55 | > 56 | 57 | 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default Threshold; 64 | -------------------------------------------------------------------------------- /src/views/main/components/Threshold/style.less: -------------------------------------------------------------------------------- 1 | @import "~@blueprintjs/core/lib/css/blueprint.css"; 2 | @import "~@blueprintjs/core/lib/less/variables"; 3 | 4 | @padding: 20px; 5 | 6 | .threshold-group { 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: space-between; 10 | align-items: center; 11 | margin: @padding @padding; 12 | padding: @padding; 13 | background-color: @light-gray4; 14 | border-radius: 5px; 15 | box-shadow: 2px 2px 5px -1px @gray5; 16 | 17 | &.warning { 18 | border-left: 5px solid @orange4; 19 | } 20 | 21 | &.danger { 22 | border-left: 5px solid @red4; 23 | } 24 | 25 | .value { 26 | display: flex; 27 | flex-direction: row; 28 | align-items: center; 29 | 30 | .bp3-label { 31 | margin: 0px @padding 0px 0px; 32 | } 33 | } 34 | 35 | .notifications { 36 | display: flex; 37 | direction: row; 38 | align-items: center; 39 | background-color: @light-gray2; 40 | border-radius: 3px; 41 | padding: 10px; 42 | 43 | &.checked { 44 | .bp3-icon > svg { 45 | fill: @blue3; 46 | } 47 | } 48 | 49 | .bp3-icon > svg { 50 | fill: @gray1; 51 | } 52 | 53 | .bp3-control { 54 | margin-bottom: 0px; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/views/main/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alignment, 3 | Button, 4 | ControlGroup, 5 | Checkbox, 6 | Drawer, 7 | FormGroup, 8 | MenuItem, 9 | Navbar, 10 | NumericInput 11 | } from "@blueprintjs/core"; 12 | import { Select } from "@blueprintjs/select"; 13 | import * as React from "react"; 14 | 15 | import "./style.less"; 16 | 17 | import Bar from "../../components/bar"; 18 | import BarGroup from "../../components/barGroup"; 19 | import Lines from "../../components/Lines"; 20 | import ResponsiveLines from "../../components/responsiveLines"; 21 | 22 | import Admissions from "./components/Admissions"; 23 | import Beds from "./components/Beds"; 24 | import BedsDrawer from "./components/Beds/components/Drawer"; 25 | import Bloc from "./components/Bloc"; 26 | import Consultations from "./components/Consultations"; 27 | import Gestes from "./components/Gestes"; 28 | import GestesRevenu from "./components/GestesRevenu"; 29 | import Resources from "./components/Resources"; 30 | import ResourcesDrawer from "./components/Resources/components/Drawer"; 31 | 32 | const arkhnLogoWhite = require("../../assets/img/arkhn_logo_only_white.svg"); 33 | 34 | export interface IViewProps {} 35 | 36 | interface IState { 37 | drawerComponent: any; 38 | isOpen: boolean; 39 | service: string; 40 | } 41 | 42 | const MainView = () => { 43 | const [service, setService] = React.useState("Gériatrie"); 44 | const [drawerOpen, setDrawerOpen] = React.useState(false); 45 | const [drawerComponent, setDrawerComponent] = React.useState(null); 46 | 47 | const [beds, setBeds] = React.useState(445); 48 | const [totalBeds, setTotalBeds] = React.useState(500); 49 | const [warningBeds, setWarningBeds] = React.useState(400); 50 | const [dangerBeds, setDangerBeds] = React.useState(480); 51 | const [notifyWarningBeds, setNotifyWarningBeds] = React.useState(false); 52 | const [notifyDangerBeds, setNotifyDangerBeds] = React.useState(false); 53 | 54 | const [ide, setIde] = React.useState(102); 55 | const [totalIde, setTotalIde] = React.useState(108); 56 | const [warningIde, setWarningIde] = React.useState(80); 57 | const [notifyWarningIde, setNotifyWarningIde] = React.useState(false); 58 | const [dangerIde, setDangerIde] = React.useState(100); 59 | const [notifyDangerIde, setNotifyDangerIde] = React.useState(false); 60 | 61 | const [aides, setAides] = React.useState(87); 62 | const [totalAides, setTotalAides] = React.useState(113); 63 | const [warningAides, setWarningAides] = React.useState(90); 64 | const [notifyWarningAides, setNotifyWarningAides] = React.useState(false); 65 | const [dangerAides, setDangerAides] = React.useState(100); 66 | const [notifyDangerAides, setNotifyDangerAides] = React.useState(false); 67 | 68 | const handleOpen = (componentName: string) => { 69 | setDrawerComponent(componentName); 70 | setDrawerOpen(true); 71 | }; 72 | 73 | const ServiceSelect = Select.ofType(); 74 | 75 | return ( 76 |
77 | { 81 | setDrawerOpen(false); 82 | }} 83 | title="Configuration" 84 | > 85 | 96 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |

DASHBOARD

122 |
123 |
124 | 125 | ( 134 | 135 | )} 136 | onItemSelect={(item: string, event: any) => { 137 | setService(item); 138 | }} 139 | > 140 |
157 |
= warningBeds ? "warning" : "" 161 | } ${beds >= dangerBeds ? "danger" : ""}`} 162 | > 163 |
Occupation des lits
164 | 165 |
174 |
175 |
Bloc opératoire
176 | 177 |
186 |
187 |
Patients Hébergés Hors Service
188 |
2
189 |
198 |
199 |
Durée Moyenne de Séjour
200 |
5 jours 13 h
201 |
210 |
211 |
Consultations
212 | 213 |
222 |
= warningIde || aides >= warningAides ? "warning" : "" 226 | } ${ide >= dangerIde || aides >= dangerAides ? "danger" : ""}`} 227 | > 228 |
Ressources Humaines
229 | 235 |
244 | 245 | 246 | ); 247 | }; 248 | 249 | export default MainView; 250 | -------------------------------------------------------------------------------- /src/views/main/style.less: -------------------------------------------------------------------------------- 1 | @import "~@blueprintjs/core/lib/css/blueprint.css"; 2 | @import "~@blueprintjs/core/lib/less/variables"; 3 | 4 | @window-padding: 20px; 5 | @border-radius: 6px; 6 | 7 | body { 8 | background-color: @light-gray3; 9 | color: @black; 10 | } 11 | 12 | #navbar { 13 | background-color: @black; 14 | } 15 | 16 | .bp3-navbar-heading { 17 | display: flex; 18 | 19 | h2 { 20 | margin: 0px 0px 0px 9px; 21 | color: @white; 22 | } 23 | } 24 | 25 | #dashboard { 26 | display: grid; 27 | width: 100%; 28 | height: calc(100vh - 50px); 29 | padding: @window-padding; 30 | 31 | // Grid 32 | grid-template-rows: repeat(4, 1fr); 33 | grid-template-columns: repeat(3, 1fr); 34 | grid-column-gap: @window-padding; 35 | grid-row-gap: @window-padding; 36 | grid-template-areas: 37 | "rh admissions sejour" 38 | "rh admissions service" 39 | "beds consultations consultations" 40 | "bloc consultations consultations"; 41 | 42 | & > * { 43 | padding: calc(2 * @window-padding); 44 | } 45 | 46 | .admission-bar:hover { 47 | opacity: 0.8; 48 | } 49 | 50 | .comparison { 51 | fill-opacity: 0; 52 | stroke: @black; 53 | stroke-width: 2px; 54 | stroke-opacity: 0.4; 55 | stroke-dasharray: 6, 8; 56 | } 57 | 58 | .svg-dashboard-module { 59 | fill: @light-gray4; 60 | rx: 5; 61 | } 62 | 63 | .dashboard-module.warning { 64 | // background-color: @orange5; 65 | border-left: 15px solid @orange4; 66 | } 67 | 68 | .dashboard-module.danger { 69 | // background-color: @red5; 70 | border-left: 15px solid @red4; 71 | } 72 | 73 | .dashboard-module { 74 | background-color: @light-gray5; 75 | border-radius: 5px; 76 | box-shadow: 3px 3px 5px @light-gray1; 77 | padding: @window-padding; 78 | position: relative; 79 | border-left: 15px solid transparent; 80 | 81 | .requestButton { 82 | position: absolute; 83 | top: calc(@window-padding / 2); 84 | right: calc(@window-padding / 2); 85 | } 86 | 87 | .title { 88 | fill: @gray2; 89 | color: @gray2; 90 | font-size: 1.5em; 91 | font-weight: 300; 92 | text-transform: uppercase; 93 | } 94 | 95 | .right-align { 96 | fill: @gray1; 97 | color: @gray1; 98 | font-size: 1em; 99 | font-weight: 100; 100 | text-transform: uppercase; 101 | transform: translate(1, 1em); 102 | } 103 | 104 | svg .title { 105 | transform: translate(0, 1em); 106 | } 107 | 108 | .value { 109 | font-size: 4em; 110 | font-weight: 700; 111 | } 112 | } 113 | 114 | #admissions { 115 | grid-area: admissions; 116 | overflow: hidden; 117 | } 118 | 119 | #hospitalisation { 120 | grid-area: hospitalisation; 121 | } 122 | 123 | #beds { 124 | grid-area: beds; 125 | } 126 | 127 | #bloc { 128 | grid-area: bloc; 129 | } 130 | 131 | #consultations { 132 | grid-area: consultations; 133 | } 134 | 135 | #rh { 136 | grid-area: rh; 137 | } 138 | 139 | #attente { 140 | grid-area: attente; 141 | } 142 | 143 | #sejour { 144 | grid-area: sejour; 145 | } 146 | 147 | #service { 148 | grid-area: service; 149 | } 150 | 151 | #gestes { 152 | grid-area: gestes; 153 | overflow: hidden; 154 | } 155 | 156 | #gestesRevenu { 157 | grid-area: gestesRevenu; 158 | overflow: hidden; 159 | } 160 | 161 | .right-align { 162 | text-align: right; 163 | } 164 | 165 | .legend > * { 166 | justify-content: center; 167 | } 168 | } 169 | .modal { 170 | font-size: 12px; 171 | } 172 | .modal > .header { 173 | width: 100%; 174 | border-bottom: 1px solid gray; 175 | font-size: 18px; 176 | text-align: center; 177 | padding: 5px; 178 | } 179 | .modal > .content { 180 | width: 100%; 181 | padding: 10px 5px; 182 | } 183 | .modal > .actions { 184 | width: 100%; 185 | padding: 10px 5px; 186 | margin: auto; 187 | text-align: center; 188 | } 189 | .modal > .close { 190 | cursor: pointer; 191 | position: absolute; 192 | display: block; 193 | padding: 2px 5px; 194 | line-height: 20px; 195 | right: -10px; 196 | top: -10px; 197 | font-size: 24px; 198 | background: #ffffff; 199 | border-radius: 18px; 200 | border: 1px solid #cfcece; 201 | } 202 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "experimentalDecorators": true, 7 | "target": "es6", 8 | "allowJs": true, 9 | "lib": ["es6", "dom", "esnext"] 10 | }, 11 | "include": ["**/*.tsx"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"), 2 | htmlPlugin = require("html-webpack-plugin"); 3 | FaviconsWebpackPlugin = require("favicons-webpack-plugin"); 4 | 5 | var SRC_DIR = path.join(__dirname, "./src"); 6 | var DIST_DIR = path.join(__dirname, "./dist"); 7 | 8 | module.exports = { 9 | // Indicates where to start so as to build the module dependency graph 10 | context: SRC_DIR, 11 | entry: "./app.tsx", 12 | // Where bundles should be emitted 13 | output: { 14 | path: DIST_DIR, 15 | filename: "fhirball.bundle.js" 16 | }, 17 | // By default, webpack only handles js and json files. 18 | // In order to process other types of files, one should use 19 | // "loaders". 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(tsx|ts)?$/, 24 | use: ["awesome-typescript-loader"] 25 | }, 26 | { 27 | test: /\.(js|jsx)$/, 28 | exclude: /node_modules/, 29 | use: ["babel-loader"] 30 | }, 31 | { 32 | test: /\.less$/, 33 | use: ["style-loader", "css-loader", "less-loader"] 34 | }, 35 | { 36 | test: /\.svg$/, 37 | loader: "raw-loader" 38 | }, 39 | { 40 | // This is, among others, for files present in ./node_modules/graphql 41 | test: /\.mjs$/, 42 | include: /node_modules/, 43 | type: "javascript/auto" 44 | }, 45 | { 46 | test: /\.(graphql|gql)$/, 47 | exclude: /node_modules/, 48 | loader: "graphql-tag/loader" 49 | } 50 | ] 51 | }, 52 | // In this app, plugins are used to optimize emitted bundles and 53 | // set environment variables if need be. 54 | plugins: [ 55 | new htmlPlugin({ 56 | template: "index.html" 57 | }), 58 | new FaviconsWebpackPlugin({ logo: "./assets/img/logo.png" }) 59 | ], 60 | // Resolvers are used to locate modules using absolute paths. 61 | // This allows to write `import * from './module'` instead of 62 | // `import * from './module.tsx'` 63 | resolve: { 64 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json"] 65 | }, 66 | // Run optimisation scripts depending on the `mode` (dev or prod). 67 | // webpack minimises the code by default on prod 68 | optimization: { 69 | splitChunks: { 70 | chunks: "all" 71 | } 72 | }, 73 | devServer: { 74 | // Allows to handle routes with React instead of webpack 75 | historyApiFallback: true 76 | }, 77 | // Prevents source map erros in development in Firefox 78 | devtool: "source-map" 79 | }; 80 | --------------------------------------------------------------------------------