diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 52536bf2a..f29485670 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,63 +1,63 @@ -name: Playwright Tests -on: - push: - branches: [main] - tags-ignore: [v*] - pull_request: - branches: [main] -jobs: - test: - timeout-minutes: 30 - runs-on: ubuntu-latest +# name: Playwright Tests +# on: +# push: +# branches: [main] +# tags-ignore: [v*] +# pull_request: +# branches: [main] +# jobs: +# test: +# timeout-minutes: 30 +# runs-on: ubuntu-latest - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 +# services: +# postgres: +# image: postgres +# env: +# POSTGRES_PASSWORD: postgres +# options: >- +# --health-cmd pg_isready +# --health-interval 10s +# --health-timeout 5s +# --health-retries 5 +# ports: +# - 5432:5432 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 - - name: Install dependencies - run: yarn - - name: Install Playwright Browsers - run: npx playwright install --with-deps +# steps: +# - uses: actions/checkout@v3 +# - uses: actions/setup-node@v3 +# with: +# node-version: 16 +# - name: Install dependencies +# run: yarn +# - name: Install Playwright Browsers +# run: npx playwright install --with-deps - - name: Install dashboard dependencies - run: yarn --cwd ./dashboard +# - name: Install dashboard dependencies +# run: yarn --cwd ./dashboard - - name: Install api dependencies - run: yarn --cwd ./api +# - name: Install api dependencies +# run: yarn --cwd ./api - - name: Init DB - run: yarn test:init-db - env: - PGDATABASE: manotest - PGBASEURL: postgres://postgres:postgres@localhost:5432 +# - name: Init DB +# run: yarn test:init-db +# env: +# PGDATABASE: manotest +# PGBASEURL: postgres://postgres:postgres@localhost:5432 - - name: Run Playwright tests - run: yarn playwright test - env: - PGBASEURL: postgres://postgres:postgres@localhost:5432 - PGHOST: localhost - PGDATABASE: manotest - PGPORT: 5432 - PGUSER: postgres - PGPASSWORD: postgres +# - name: Run Playwright tests +# run: yarn playwright test +# env: +# PGBASEURL: postgres://postgres:postgres@localhost:5432 +# PGHOST: localhost +# PGDATABASE: manotest +# PGPORT: 5432 +# PGUSER: postgres +# PGPASSWORD: postgres - - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 +# - uses: actions/upload-artifact@v3 +# if: always() +# with: +# name: playwright-report +# path: playwright-report/ +# retention-days: 30 diff --git a/app/src/Navigators.js b/app/src/Navigators.js index 15b258bdf..dd9d53693 100644 --- a/app/src/Navigators.js +++ b/app/src/Navigators.js @@ -6,7 +6,7 @@ import { NavigationContainer, useNavigationContainerRef } from '@react-navigatio import { createStackNavigator } from '@react-navigation/stack'; import { useMMKVNumber } from 'react-native-mmkv'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { AgendaIcon, PersonIcon, TerritoryIcon } from './icons'; +import { AgendaIcon, PersonIcon, StructuresIcon, TerritoryIcon } from './icons'; import { RecoilRoot, useRecoilValue, useResetRecoilState } from 'recoil'; import logEvents from './services/logEvents'; import Login from './scenes/Login/Login'; @@ -59,12 +59,15 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { currentTeamState, organisationState, teamsState, userState } from './recoil/auth'; import { appCurrentCacheKey, clearCache } from './services/dataManagement'; import useResetAllCachedDataRecoilStates from './recoil/reset'; +import MenuIcon from './icons/MenuIcon'; +import { TODO } from './recoil/actions'; +import ActionsList from './scenes/Actions/ActionsList'; const ActionsStack = createStackNavigator(); const ActionsNavigator = () => { return ( - + @@ -209,41 +212,28 @@ const TabNavigator = () => { name="Agenda" component={ActionsNavigator} options={{ - tabBarIcon: AgendaIcon, + tabBarIcon: ({ size, color }) => , tabBarLabel: 'AGENDA', - tabBarTestID: 'tab-bar-actions', }} /> {!!organisation?.territoriesEnabled && ( , + tabBarLabel: 'USAGERS', }} /> )} - , + tabBarLabel: 'STRUCTURES', }} /> { name="MenuTab" component={MenuNavigator} options={{ - tabBarIcon: DotsIcon, - tabBarLabel: 'MENU', - tabBarTestID: 'tab-bar-profil', + tabBarIcon: ({ size, color }) => , + tabBarLabel: 'PROFIL', }} /> diff --git a/app/src/components/Button.js b/app/src/components/Button.js index 10919ba3c..72315f34b 100644 --- a/app/src/components/Button.js +++ b/app/src/components/Button.js @@ -1,87 +1,42 @@ import React from 'react'; import styled from 'styled-components'; import { TouchableOpacity, ActivityIndicator, TouchableWithoutFeedback, Dimensions } from 'react-native'; -import { MyText } from './MyText'; -import colors from '../utils/colors'; -import Spacer from './Spacer'; -const hitSlop = { - top: 20, - left: 20, - right: 20, - bottom: 20, -}; - -const Button = ({ - caption, - onPress, - disabled, - outlined, - borderColor, - backgroundColor = null, - color = colors.app.color, - loading, - fullWidth, - Icon, - noBorder = false, - buttonSize = 40, - testID = '', -}) => { +const Button = ({ caption, onPress, disabled, outlined, borderColor, backgroundColor = 'green', color = 'white', loading, style = {} }) => { const Root = loading !== undefined ? TouchableWithoutFeedback : TouchableOpacity; return ( - - + + {loading ? ( ) : ( - <> - {!!Icon && } - {!!Icon && !!caption && } - {!!caption && ( - - {caption} - - )} - + + {caption} + )} ); }; +const buttonSize = 40; const ButtonContainer = styled.View` - /* background-color: ${(props) => (props.outlined ? 'white' : props.backgroundColor)}; - border-color: ${(props) => props.borderColor || props.backgroundColor}; */ - ${(props) => props.backgroundColor && `background-color: ${props.backgroundColor};`} - border: 1px solid rgba(30, 36, 55, 0.1); - border-radius: 16px; - padding-horizontal: 20px; - padding-vertical: ${(props) => props.buttonSize / 2}px; + background-color: ${(props) => (props.outlined ? 'white' : props.backgroundColor)}; + border-color: ${(props) => props.borderColor || props.backgroundColor}; + border-width: 1px; + height: ${buttonSize}px; + border-radius: ${buttonSize}px; + padding-horizontal: ${buttonSize / 2}px; align-self: center; justify-content: center; - align-items: center; - justify-content: center; min-width: ${Math.min(Dimensions.get('window').width * 0.3, 140)}px; - flex-direction: row; /* min-width: 140px; */ ${(props) => props.disabled && 'opacity: 0.5;'} - ${(props) => props.fullWidth && 'width: 100%;'} - ${(props) => props.noBorder && 'border-width: 0;'} `; -const Caption = styled(MyText)` - color: ${(props) => props.color}; +const Caption = styled.Text` + font-weight: bold; + color: ${(props) => (props.outlined ? props.backgroundColor : props.color)}; align-items: center; justify-content: center; text-align: center; diff --git a/app/src/components/ButtonDelete.js b/app/src/components/ButtonDelete.js index 660ee405e..9961d1006 100644 --- a/app/src/components/ButtonDelete.js +++ b/app/src/components/ButtonDelete.js @@ -3,7 +3,14 @@ import Button from './Button'; import colors from '../utils/colors'; const ButtonDelete = ({ onPress, caption = 'Supprimer', deleting }) => ( - ); @@ -28,4 +27,10 @@ const Button = styled.View` align-items: center; `; +const Action = styled.Text` + color: #fff; + font-size: 25px; + font-weight: bold; +`; + export default FloatAddButton; diff --git a/app/src/components/InputLabelled.js b/app/src/components/InputLabelled.js index 57652e97b..9dce3012e 100644 --- a/app/src/components/InputLabelled.js +++ b/app/src/components/InputLabelled.js @@ -1,95 +1,33 @@ import React from 'react'; import styled from 'styled-components'; -import { TouchableWithoutFeedback } from 'react-native'; import Label from './Label'; import InputMultilineAutoAdjust from './InputMultilineAutoAdjust'; -import { MyText, MyTextInput } from './MyText'; -import colors from '../utils/colors'; -import Spacer from './Spacer'; -const InputLabelled = React.forwardRef(({ error, label, multiline, editable = true, onClear, noMargin, EndIcon, onEndIconPress, ...props }, ref) => { - if (!editable) { - const value = String(props.value || '') - .split('\\n') - .join('\u000A'); - return ( - - {!!label && {`${label} : `}} - - {value} - - - - ); - } - return ( - - {label && - ); -}); - -const FieldContainer = styled.View` - flex-grow: 1; - margin-bottom: ${(props) => (props.noMargin ? 0 : 25)}px; -`; +const InputLabelled = React.forwardRef(({ error, label, multiline, ...props }, ref) => ( + + {label && +)); const InputContainer = styled.View` - margin-bottom: 30px; + margin-bottom: 15px; flex-grow: 1; `; -const Error = styled(MyText)` +const Error = styled.Text` margin-left: 5px; font-size: 14px; color: red; height: 18px; `; -const InlineLabel = styled(MyText)` - font-size: 15px; - color: ${colors.app.color}; - margin-bottom: 15px; -`; - -const Content = styled(MyText)` - font-size: 17px; - line-height: 20px; -`; - -const Input = styled(MyTextInput)` - border: 1px solid rgba(30, 36, 55, 0.1); - border-radius: 12px; - padding-horizontal: 12px; - padding-vertical: 15px; -`; - -const IconWrapper = styled.View` - position: absolute; - right: 12px; - top: 0; - bottom: 0; - justify-content: center; - padding-top: 22px; -`; - -const Row = styled.View` - flex-direction: row; - align-items: center; +const Input = styled.TextInput` + border: 1px solid #666; + border-radius: 8px; + padding-horizontal: 15px; + padding-vertical: 10px; `; export default InputLabelled; diff --git a/app/src/components/InputMultilineAutoAdjust.js b/app/src/components/InputMultilineAutoAdjust.js index 9edc672c8..15dc12fbb 100644 --- a/app/src/components/InputMultilineAutoAdjust.js +++ b/app/src/components/InputMultilineAutoAdjust.js @@ -1,6 +1,5 @@ import React from 'react'; import styled from 'styled-components'; -import { MyTextInput } from './MyText'; const InputMultilineAutoAdjust = React.forwardRef((props, ref) => { return ( @@ -13,15 +12,14 @@ const InputMultilineAutoAdjust = React.forwardRef((props, ref) => { const InputContainer = styled.View` flex-grow: 1; flex-shrink: 1; - border: 1px solid rgba(30, 36, 55, 0.1); - border-radius: 12px; - padding-horizontal: 12px; - padding-top: 10px; - padding-bottom: 15px; `; -const Input = styled(MyTextInput)` +const Input = styled.TextInput` flex-grow: 1; + border: 1px solid #666; + border-radius: 8px; + padding-horizontal: 15px; + padding-vertical: 10px; align-items: flex-start; text-align-vertical: top; `; diff --git a/app/src/components/Label.js b/app/src/components/Label.js index 9cb78a17c..9db6fe854 100644 --- a/app/src/components/Label.js +++ b/app/src/components/Label.js @@ -1,17 +1,11 @@ import React from 'react'; import styled from 'styled-components'; -import { MyText } from './MyText'; -const Label = ({ label, big }) => ( - - {label} - -); +const Label = ({ label }) => {label}; -const LabelStyled = styled(MyText)` +const LabelStyled = styled.Text` margin-bottom: 10px; font-weight: bold; - ${(props) => props.big && 'font-size: 17px;'} `; export default Label; diff --git a/app/src/components/Loader.js b/app/src/components/Loader.js index f2fb195b0..46d8a42fd 100644 --- a/app/src/components/Loader.js +++ b/app/src/components/Loader.js @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react'; import styled from 'styled-components'; -import { Dimensions } from 'react-native'; +import { Dimensions, ActivityIndicator } from 'react-native'; import API from '../services/api'; import { MyText } from './MyText'; import colors from '../utils/colors'; @@ -412,6 +412,12 @@ export const LoaderProgress = () => { if (!loading) return null; + return ( + + + + ); + return ( {!!fullScreen && } @@ -425,7 +431,7 @@ export const LoaderProgress = () => { const Container = styled.SafeAreaView` width: 100%; - background-color: ${colors.app.color}; + // background-color: ${colors.app.color}; ${(p) => !p.fullScreen && 'position: absolute;'} ${(p) => !p.fullScreen && 'top: 0;'} ${(p) => p.fullScreen && 'height: 100%;'} diff --git a/app/src/components/RowContainer.js b/app/src/components/RowContainer.js index 363fac930..aab0b6b08 100644 --- a/app/src/components/RowContainer.js +++ b/app/src/components/RowContainer.js @@ -1,18 +1,41 @@ import React from 'react'; -import { TouchableOpacity, View } from 'react-native'; +import { TouchableOpacity, StyleSheet, View } from 'react-native'; -const RowContainer = ({ Component = TouchableOpacity, onPress, disabled, noPadding, children, center, testID = '', styles: stylesProps = {} }) => { +const RowContainer = ({ onPress, noPadding, children }) => { return ( - - - - {children} - + + + {children} - + ); }; +// seems to be a problem with the nested shadow-offset and styled-components +// switching temporarily to stylesheet for this +const styles = StyleSheet.create({ + container: { + overflow: 'hidden', + borderRadius: 10, + backgroundColor: 'white', + margin: 10, + shadowColor: '#000', + shadowOffset: { width: 0, height: 3 }, + shadowRadius: 5, + shadowOpacity: 0.55, + elevation: 10, + }, + subContainer: { + padding: 15, + alignItems: 'center', + borderBottomColor: '#ddd', + borderBottomWidth: 1, + flexDirection: 'row', + width: '100%', + }, + noPadding: { + padding: 0, + }, +}); + export default RowContainer; diff --git a/app/src/components/SceneContainer.js b/app/src/components/SceneContainer.js index cb5f415df..4873d791e 100644 --- a/app/src/components/SceneContainer.js +++ b/app/src/components/SceneContainer.js @@ -1,30 +1,12 @@ import React from 'react'; import styled from 'styled-components'; -import { Platform } from 'react-native'; -import colors from '../utils/colors'; -const SceneContainer = ({ children, debug, enabled = true, backgroundColor = colors.app.color, testID = '' }) => ( - - - {children} - - -); +const SceneContainer = ({ children, debug }) => {children}; const Container = styled.View` flex: 1; - background-color: ${(props) => props.backgroundColor}; + background-color: #f5f6f6; ${(props) => props.debug && 'border: 3px solid #000;'} `; -const KeyboardAvoidingView = styled.KeyboardAvoidingView` - flex: 1; - background-color: #fff; - ${(props) => props.debug && 'border: 3px solid #f00;'} -`; - export default SceneContainer; diff --git a/app/src/components/ScreenTitle.js b/app/src/components/ScreenTitle.js index eb4c78c9a..a812ba02f 100644 --- a/app/src/components/ScreenTitle.js +++ b/app/src/components/ScreenTitle.js @@ -1,178 +1,132 @@ -import React from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; -import { ActivityIndicator, Animated, StatusBar, StyleSheet, TouchableOpacity, View, SafeAreaView } from 'react-native'; -import { MyText } from './MyText'; -import colors from '../utils/colors'; -import ArrowLeftExtended from '../icons/ArrowLeftExtended'; +import { Animated, Dimensions, StatusBar, TouchableOpacity } from 'react-native'; +import ButtonRight from './ButtonRight'; +import Svg, { Circle } from 'react-native-svg'; -const hitSlop = { - top: 20, - left: 20, - bottom: 20, - right: 20, -}; +const AnimatedSvg = Animated.createAnimatedComponent(Svg); +const AnimatedCircle = Animated.createAnimatedComponent(Circle); + +const ScreenTitle = ({ title, onBack, onAdd, backgroundColor = '#fff', color = '#000', offset, onLayout }) => { + const showRightButton = Boolean(onAdd); + const [titleHeight, setTitleHeight] = useState(0); + // const [_offset, set_offset] = useState(0); + // useEffect(() => { + // return () => { + // set_offset(getCurveOptions()); + // }; + // }, [offset]); -const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView); + let animatedOffset, animatedRadius; -const ScreenTitle = ({ - title, - onBack, - onAdd, - onEdit, - onSave, - onPressRight, - customRight, - backgroundColor = colors.app.color, - color = '#FFF', - saving, - children, - parentScroll, - testID = '', - forceTop = false, -}) => { - const showRightButton = Boolean(onAdd) || Boolean(onEdit) || Boolean(onSave); - const showLeftButton = showRightButton || Boolean(onBack); + if (offset) { + if (!(offset instanceof Animated.Value)) { + animatedOffset = -1000 + titleHeight + 25; + animatedRadius = 1000; + } else { + animatedOffset = offset.interpolate({ + inputRange: [0, 100], + outputRange: [-1000 + titleHeight + 25, -3000 + titleHeight], + extrapolate: 'clamp', + }); + animatedRadius = offset.interpolate({ + inputRange: [0, 100], + outputRange: [1000, 3000], + extrapolate: 'clamp', + }); + } + } return ( <> - + { + setTitleHeight(e.nativeEvent.layout.height); + if (onLayout) { + onLayout(e); + } + }}> - - - {!forceTop && } - - - {title} - - {Boolean(onPressRight) && ( - - {customRight} - - )} - - - - {!!showLeftButton && ( - - - - - - )} - {!!showLeftButton && ( - - {Boolean(onAdd) && ( - - Créer - - )} - {Boolean(onEdit) && - (saving ? ( - - ) : ( - - Modifier - - ))} - {Boolean(onSave) && - (saving ? ( - - ) : ( - - Enregistrer - - ))} - - )} - - - {children} - + + + + + {'<'} + + + + + {title} + + + {Boolean(onAdd) && } + + + + {offset && ( + + + + )} ); }; -const Title = styled(MyText)` - font-size: 25px; +const Container = styled.SafeAreaView` + background-color: ${(props) => props.backgroundColor}; + overflow: visible; + z-index: 100; +`; + +const TitleContainer = styled.View` + padding-horizontal: 15px; + padding-top: 5px; + padding-bottom: 10px; + align-items: center; + flex-direction: row; + background-color: ${(props) => props.backgroundColor}; +`; + +const Title = styled.Text` + font-size: 20px; + flex-grow: 1; + flex-shrink: 1; + text-align: center; color: ${(props) => props.color}; `; -const ButtonText = styled(MyText)` - color: #ffffff; +const ButtonContainer = styled.View` + ${(props) => !props.show && 'opacity: 0;'} + min-width: 30px; `; -const styles = StyleSheet.create({ - wrapper: (parentScroll, forceTop) => ({ - overflow: 'visible', - zIndex: 100, - marginTop: forceTop ? 0 : parentScroll?.interpolate ? -90 : 0, - transform: [ - { - translateY: forceTop - ? 0 - : parentScroll?.interpolate - ? parentScroll.interpolate({ - inputRange: [0, 100], - outputRange: [90, 0], - extrapolate: 'clamp', - }) - : 0, - }, - ], - }), - titleContainer: (parentScroll, forceTop) => ({ - justifyContent: 'flex-start', - alignItems: 'flex-start', - transform: [ - { - translateY: forceTop - ? 0 - : parentScroll?.interpolate - ? parentScroll.interpolate({ - inputRange: [0, 100], - outputRange: [0, -90], - extrapolate: 'clamp', - }) - : 0, - }, - ], - }), - color: (backgroundColor) => ({ - backgroundColor, - }), - container: (forceTop) => ({ - paddingHorizontal: 15, - paddingTop: forceTop ? 0 : '5%', - paddingBottom: forceTop ? 0 : '5%', - }), - buttonsContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - height: 30, - width: '100%', - }, - buttonsContainerFixed: { - position: 'absolute', - top: 15, - left: 15, - right: 15, - borderWidth: 0, - }, +const iconSize = 30; +const Icon = styled.View` + height: ${iconSize}px; + width: ${iconSize}px; + margin-right: 15px; +`; - buttonContainer: (show) => ({ - minWidth: 30, - opacity: show ? 1 : 0, - }), +const Back = styled.Text` + align-self: center; + font-size: ${iconSize - 4}px; + line-height: ${iconSize - 1}px; + color: ${(props) => props.color}; + text-align: center; +`; - titleCaptionContainer: { - flexShrink: 1, - alignItems: 'center', - justifyContent: 'space-between', - flexGrow: 0, - width: '100%', - flexDirection: 'row', - }, -}); +const CurveContainer = styled(AnimatedSvg)` + position: absolute; + z-index: 99; +`; export default ScreenTitle; diff --git a/app/src/components/ScrollContainer.js b/app/src/components/ScrollContainer.js index cc387b1e9..f40f4fcfe 100644 --- a/app/src/components/ScrollContainer.js +++ b/app/src/components/ScrollContainer.js @@ -1,30 +1,21 @@ import React from 'react'; import styled from 'styled-components'; -import colors from '../utils/colors'; -const ScrollContainer = React.forwardRef(({ children, ...props }, ref) => ( - +const ScrollContainer = React.forwardRef(({ children, debug, noPadding, ...props }, ref) => ( + {children} )); -const Container = styled.ScrollView.attrs(({ debug, noPadding, testID, contentContainerStyle = {} }) => ({ +const Container = styled.ScrollView.attrs(({ debug, noPadding }) => ({ contentContainerStyle: { borderWidth: debug ? 2 : 0, borderColor: 'red', padding: noPadding ? 0 : 30, - backgroundColor: '#fff', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - flexGrow: 1, - ...contentContainerStyle, }, - testID, }))` flex: 1; - background-color: ${(props) => props.backgroundColor || colors.app.color}; ${(props) => props.debug && 'border: 3px solid #000;'} - ${(props) => props.flexGrow && `flex-grow: ${props.flexGrow};`} `; export default ScrollContainer; diff --git a/app/src/scenes/Actions/Action.js b/app/src/scenes/Actions/Action.js index 9df21c7f1..49eb11f48 100644 --- a/app/src/scenes/Actions/Action.js +++ b/app/src/scenes/Actions/Action.js @@ -32,6 +32,7 @@ import useCreateReportAtDateIfNotExist from '../../utils/useCreateReportAtDateIf import { groupsState } from '../../recoil/groups'; import { useFocusEffect } from '@react-navigation/native'; import { itemsGroupedByPersonSelector } from '../../recoil/selectors'; +import colors from '../../utils/colors'; const castToAction = (action) => { if (!action) action = {}; @@ -414,6 +415,8 @@ const Action = ({ navigation, route }) => { onSave={!editable || isUpdateDisabled ? null : onUpdateRequest} saving={updating} testID="action" + backgroundColor={colors.action.backgroundColor} + color={colors.action.color} /> {!!action.user && } @@ -472,30 +475,14 @@ const Action = ({ navigation, route }) => { ref={descriptionRef} onFocus={() => _scrollToInput(descriptionRef)} /> - setAction((a) => ({ ...a, categories }))} values={categories} editable={editable} /> - {editable ? ( - setAction((a) => ({ ...a, urgent: !a.urgent }))} - value={urgent} - /> - ) : null} - {editable && !!canToggleGroupCheck ? ( - setAction((a) => ({ ...a, group: !a.group }))} - value={group} - /> - ) : null} - {!editable && } - - + + + ); -}; +} + +const Profile = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 0; +`; + +const Sidebar = styled.div` + background-color: ${theme.white}; + height: 100%; + max-width: 260px; + width: 100%; + z-index: 10; + position: fixed; + left: 0; + top: 0; + padding: 32px 18px; +`; + +const Name = styled.div` + font-weight: bold; + font-size: 20px; + line-height: 28px; + + text-align: center; + color: ${theme.main}; +`; + +const Team = styled.div` + font-weight: 600; + font-size: 14px; + line-height: 22px; + text-align: center; + letter-spacing: -0.01em; +`; +const Organisation = styled.div` + font-weight: 600; + font-size: 14px; + line-height: 22px; + text-align: center; + letter-spacing: -0.01em; +`; -export default Drawer; +const Nav = styled.div` + a { + text-decoration: none; + padding: 16px; + display: block; + border-radius: 8px; + color: ${theme.black75}; + font-style: normal; + font-weight: 600; + font-size: 14px; + line-height: 24px; + margin: 2px 0; + } + a.active, + a:hover { + background-color: ${theme.mainLight}; + color: ${theme.main}; + } + a:hover { + opacity: 0.6; + } + li { + list-style-type: none; + } +`; diff --git a/dashboard/src/components/filters/Filter.js b/dashboard/src/components/filters/Filter.js new file mode 100644 index 000000000..6c39471b5 --- /dev/null +++ b/dashboard/src/components/filters/Filter.js @@ -0,0 +1,25 @@ +import styled from "styled-components"; +import {theme} from "../../config"; + +export const FilterTitle = styled.div` + font-weight: 800; + font-size: 12px; + line-height: 22px; + + letter-spacing: 0.02em; + text-transform: uppercase; + + color: ${theme.black}; +` + +export const filterStyles = { + control: styles => ({ + ...styles, + borderWidth: 0, + }), + indicatorSeparator: styles => ({ + ...styles, + borderWidth: 0, + backgroundColor: "transparent", + }), +} \ No newline at end of file diff --git a/dashboard/src/components/loadingButton.js b/dashboard/src/components/loadingButton.js new file mode 100644 index 000000000..e3b6c1b50 --- /dev/null +++ b/dashboard/src/components/loadingButton.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { Spinner, Button } from 'reactstrap'; + +export default function LoadingButton({ loading, children, disabled, ...rest }) { + return ( + + ); +} diff --git a/dashboard/src/components/tabButton.js b/dashboard/src/components/tabButton.js new file mode 100644 index 000000000..db354e9d2 --- /dev/null +++ b/dashboard/src/components/tabButton.js @@ -0,0 +1,13 @@ +import styled from 'styled-components'; +import { NavLink } from 'reactstrap'; + +const TabButton = styled(NavLink)` + border: none !important; + border-bottom: ${({ active }) => (active ? '1px solid #0046FE !important' : '1px solid #e9e9e9 !important')}; + color: ${({ active }) => (active ? '#0046FE !important' : '#1D2021 !important')}; + cursor: pointer; + font-weight: 600; + font-size: 14px; +`; + +export default TabButton; diff --git a/dashboard/src/components/table.js b/dashboard/src/components/table.js index 1424bf0fd..254ab2a51 100644 --- a/dashboard/src/components/table.js +++ b/dashboard/src/components/table.js @@ -1,136 +1,81 @@ -import React, { useCallback, useEffect, useRef } from 'react'; -import Sortable from 'sortablejs'; +import React from 'react'; +import styled from 'styled-components'; +import { theme } from '../config'; -const Table = ({ - columns = [], - data = [], - rowKey, - dataTestId = null, - onRowClick, - rowDisabled = () => false, - nullDisplay = '', - className, - title, - noData, - isSortable, - onSort, -}) => { - const gridRef = useRef(null); - const sortableJsRef = useRef(null); - - const onListChange = useCallback(() => { - if (!isSortable) return; - const newOrder = [...gridRef.current.children].map((i) => i.dataset.key); - onSort(newOrder, data); - }, [onSort, data, isSortable]); - - useEffect(() => { - if (!!isSortable && !!data.length) { - sortableJsRef.current = new Sortable(gridRef.current, { - animation: 150, - onEnd: onListChange, - }); - } - }, [onListChange, isSortable, data.length]); - - if (!data.length && noData) { - return ( - +const Table = ({ columns = [], data = [], rowKey, onRowClick, nullDisplay = '' }) => { + return ( + +
- {!!title && ( - - - - )} - + {columns.map((column) => ( + + ))} -
- {title} -
{noData}{column.title}
- ); - } - return ( - - - {!!title && ( - - - - )} - - {columns.map((column) => { - const { onSortBy, onSortOrder, sortBy, sortOrder, sortableKey, dataKey } = column; - const onNameClick = () => { - if (sortBy === sortableKey || sortBy === dataKey) { - onSortOrder(sortOrder === 'ASC' ? 'DESC' : 'ASC'); - return; - } - onSortBy(sortableKey || dataKey); - }; - return ( - - ); - })} - - - - {data - .filter((e) => e) - .map((item) => { + + {data.map((i) => { return ( - (!rowDisabled(item) && onRowClick ? onRowClick(item) : null)} - onKeyUp={(event) => { - if (event.key === 'Enter') - if (!rowDisabled(item) && onRowClick) { - onRowClick(item); - } - }} - key={item[rowKey] || item._id} - data-key={item[rowKey] || item._id} - data-test-id={item[dataTestId] || item[rowKey] || item._id} - tabIndex={0} - className={[ - rowDisabled(item) - ? 'tw-cursor-not-allowed' - : isSortable - ? 'tw-cursor-move' - : Boolean(onRowClick) - ? 'tw-cursor-pointer' - : 'tw-cursor-auto', - ].join(' ')} - style={item.style || {}}> - {columns.map((column) => { - return ( - - ); - })} + onRowClick(i)} key={i[rowKey]}> + {columns.map((column) => ( + + ))} ); })} - -
- {title} -
- - {column.help && <>{column.help}} - {!!onSortBy && (sortBy === sortableKey || sortBy === dataKey) && ( - - )} -
- {column.render ? column.render(item) : item[column.dataKey] || nullDisplay} -
{column.render ? column.render(i) : i[column.dataKey] || nullDisplay}
+ + + ); }; +const TableWrapper = styled.div` + width: 100%; + padding: 16px; + background: ${theme.white}; + box-shadow: 0px 4px 8px rgba(29, 32, 33, 0.02); + border-radius: 8px; + + table { + width: 100%; + + tr { + height: 56px; + border-radius: 4px; + } + + tbody > tr:nth-child(odd) { + background-color: ${theme.black05}; + } + + tbody > tr:hover { + background-color: ${theme.black25}; + } + + td { + padding: 5px 0; + padding-left: 20px; + font-size: 14px; + } + + td:first-child { + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + } + td:last-child { + border-bottom-right-radius: 10px; + border-top-right-radius: 10px; + } + + thead td { + color: ${theme.main}; + + font-weight: 800; + font-size: 12px; + letter-spacing: 0.02em; + text-transform: uppercase; + } + } +`; + export default Table; diff --git a/dashboard/src/config.js b/dashboard/src/config.js index 7b392033e..150ff89a0 100644 --- a/dashboard/src/config.js +++ b/dashboard/src/config.js @@ -3,20 +3,16 @@ import packageInfo from '../package.json'; // https://www.gouvernement.fr/charte/charte-graphique-les-fondamentaux/les-couleurs // Menthe const theme = { - main: '#008e7f', // higher contrast - main75: '#49c3a6', - main50: '#94c7bf', - main25: '#c7e1dd', + main: '#0046FE', + mainLight: '#F2F6FF', black: '#1D2021', - black75: '#3b3b3b', - black50: '#777777', - black25: '#b9b9b9', + black75: '#565A5B', + black50: '#8C9294', + black25: '#CBD3D6', black05: '#F7F9FA', white: '#FFFFFF', redDark: '#F5222D', redLight: '#FBE4E4', - orangeLight: '#FEF3C7', - orangeDark: '#D97706', }; const getHost = () => { diff --git a/dashboard/src/index.scss b/dashboard/src/index.scss index b7e17a37f..dd2978cc9 100644 --- a/dashboard/src/index.scss +++ b/dashboard/src/index.scss @@ -47,19 +47,19 @@ div.tailwindui, input.tailwindui, textarea.tailwindui { - @apply tw-relative tw-mt-1 tw-block tw-w-full tw-appearance-none tw-rounded tw-border tw-border-gray-300 tw-py-2 tw-px-2 tw-text-base tw-shadow-sm focus:tw-border-indigo-500 focus:tw-ring-indigo-500 sm:tw-text-sm; + // @apply tw-relative tw-mt-1 tw-block tw-w-full tw-appearance-none tw-rounded tw-border tw-border-gray-300 tw-py-2 tw-px-2 tw-text-base tw-shadow-sm focus:tw-border-indigo-500 focus:tw-ring-indigo-500 sm:tw-text-sm; } label.tailwindui { @apply tw-mb-2 tw-block tw-text-gray-700; } .input-focus-helper { - outline: 1px dashed #eee !important; - min-width: 50px !important; + // outline: 1px dashed #eee !important; + // min-width: 50px !important; } .input-focus-helper:focus, .input-focus-helper:active { - outline: auto !important; + // outline: auto !important; } /* table selego */ @@ -348,12 +348,12 @@ input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active { - -webkit-box-shadow: 0 0 0 30px rgba(222, 243, 237, 1) inset !important; + // -webkit-box-shadow: 0 0 0 30px rgba(222, 243, 237, 1) inset !important; } -$theme-colors: ( - 'primary': #49c3a6, -); +// $theme-colors: ( +// 'primary': #49c3a6, +// ); .rrt-success { background-color: #49c3a6 !important; @@ -374,3 +374,96 @@ $theme-colors: ( } @import '~bootstrap/scss/bootstrap.scss'; + +@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap'); +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +body { + font-family: Lato, serif; + background-color: #f7f9fa; + color: #1d2021; +} + +ul { + list-style: inside; + position: 0; + margin: 0; +} + +#root { + display: flex; + flex-direction: column; + height: 100vh; +} + +.main { + flex: 1; + height: 100%; + flex-direction: column; + display: flex; + height: 100%; + overflow-y: auto; +} + +input:focus, +textarea:focus, +select:focus { + outline: none; +} +table { + background-color: white; + padding: 24px !important; +} +table tr { + cursor: pointer; +} +a { + text-decoration: none; +} + +.image-input { + height: 100px; + min-width: 150px; + background: #f3f3f3 url('./assets/image.svg') center no-repeat; + background-size: 50%; + border: 2px solid #eee; + margin-top: 10px; + cursor: pointer; +} + +.time-range { + background-color: white; + border-radius: 5px; + border-color: lightgray; + border-style: solid; + border-width: 1px; + padding: 4.5px 4px; +} + +.time-range { + border: none !important; + width: 100%; + .react-daterange-picker__wrapper { + border: none !important; + } + + .react-daterange-picker__clear-button { + border-radius: 100px; + border: none; + padding: 2px; + width: 14px; + height: 14px; + background-color: #cbd3d6; + } + + .react-daterange-picker__clear-button__icon { + width: 10px; + height: 10px; + color: white; + stroke: white; + } +} diff --git a/dashboard/src/scenes/action/index.js b/dashboard/src/scenes/action/index.js index 50a091d73..840ac0496 100644 --- a/dashboard/src/scenes/action/index.js +++ b/dashboard/src/scenes/action/index.js @@ -3,10 +3,12 @@ import { Switch } from 'react-router-dom'; import SentryRoute from '../../components/Sentryroute'; import List from './list'; +import View from './view'; const Router = () => { return ( + ); diff --git a/dashboard/src/scenes/action/list.js b/dashboard/src/scenes/action/list.js index 3a2a64fac..3426f1e62 100644 --- a/dashboard/src/scenes/action/list.js +++ b/dashboard/src/scenes/action/list.js @@ -1,355 +1,38 @@ -import { useMemo, useState } from 'react'; -import { selectorFamily, useRecoilValue } from 'recoil'; +import { Badge, Container } from 'reactstrap'; import { useHistory } from 'react-router-dom'; -import { BottomSheet } from 'react-spring-bottom-sheet'; -import { SmallHeader } from '../../components/header'; -import Search from '../../components/search'; -import ActionsCalendar from '../../components/ActionsCalendar'; -import ActionsWeekly from '../../components/ActionsWeekly'; -import SelectCustom from '../../components/SelectCustom'; -import { mappedIdsToLabels, TODO } from '../../recoil/actions'; -import { currentTeamState, teamsState, userState } from '../../recoil/auth'; -import { arrayOfitemsGroupedByActionSelector, arrayOfitemsGroupedByConsultationSelector } from '../../recoil/selectors'; -import { filterBySearch } from '../search/utils'; -import useTitle from '../../services/useTitle'; -import useSearchParamState from '../../services/useSearchParamState'; -import ButtonCustom from '../../components/ButtonCustom'; -import agendaIcon from '../../assets/icons/agenda-icon.svg'; -import ActionsCategorySelect from '../../components/tailwind/ActionsCategorySelect'; -import { useLocalStorage } from '../../services/useLocalStorage'; -import SelectTeamMultiple from '../../components/SelectTeamMultiple'; -import ActionsSortableList from '../../components/ActionsSortableList'; -import { dayjsInstance } from '../../services/date'; -import useMinimumWidth from '../../services/useMinimumWidth'; -const showAsOptions = ['Calendrier', 'Liste', 'Hebdomadaire']; - -const actionsByTeamAndStatusSelector = selectorFamily({ - key: 'actionsByTeamAndStatusSelector', - get: - ({ statuses, categories, teamIds, viewAllOrganisationData, viewNoTeamData, actionsWithNoCategory }) => - ({ get }) => { - const actions = get(arrayOfitemsGroupedByActionSelector); - - const actionsByTeamAndStatus = actions.filter((action) => { - if (!viewAllOrganisationData) { - if (teamIds.length) { - if (Array.isArray(action.teams)) { - if (!teamIds.some((t) => action.teams.includes(t))) return false; - } else { - if (!teamIds.includes(action.team)) return false; - } - } - } - if (viewNoTeamData) { - if (Array.isArray(action.teams)) { - if (action.teams.length) return false; - } else { - if (action.team) return false; - } - } - if (statuses.length) { - if (!statuses.includes(action.status)) return false; - } - if (actionsWithNoCategory) { - if (action.categories?.length) return false; - } - if (categories.length) { - if (!categories.some((c) => action.categories?.includes(c))) { - return false; - } - } - return true; - }); - return actionsByTeamAndStatus; - }, -}); - -const consultationsByStatusSelector = selectorFamily({ - key: 'consultationsByStatusSelector', - get: - ({ statuses, teamIds, viewAllOrganisationData, viewNoTeamData }) => - ({ get }) => { - const consultations = get(arrayOfitemsGroupedByConsultationSelector); - const consultationsByStatus = consultations.filter((consultation) => { - if (!viewAllOrganisationData) { - if (teamIds.length) { - if (consultation.teams?.length && !teamIds.some((t) => consultation.teams.includes(t))) return false; - } - } - if (viewNoTeamData) { - if (consultation.teams?.length) return false; - } - if (statuses.length) { - if (!statuses.includes(consultation.status)) return false; - } - return true; - }); - return consultationsByStatus; - }, -}); - -const dataFilteredBySearchSelector = selectorFamily({ - key: 'dataFilteredBySearchSelector', - get: - ({ search, statuses, categories, teamIds, viewAllOrganisationData, viewNoTeamData, actionsWithNoCategory }) => - ({ get }) => { - const actions = get( - actionsByTeamAndStatusSelector({ statuses, categories, teamIds, viewNoTeamData, viewAllOrganisationData, actionsWithNoCategory }) - ); - // When we filter by category, we don't want to see all consultations. - const consultations = categories?.length - ? [] - : get(consultationsByStatusSelector({ statuses, teamIds, viewNoTeamData, viewAllOrganisationData })); - - if (!search) { - return [...actions, ...consultations]; - } - const actionsFiltered = filterBySearch(search, actions); - const consultationsFiltered = filterBySearch(search, consultations); - return [...actionsFiltered, ...consultationsFiltered]; - }, -}); - -const List = () => { - useTitle('Agenda'); - const currentTeam = useRecoilValue(currentTeamState); - const user = useRecoilValue(userState); - const teams = useRecoilValue(teamsState); +import Header from '../../components/header'; +import Loading from '../../components/loading'; +import Table from '../../components/table'; +import { useRecoilValue } from 'recoil'; +import { actionsState } from '../../recoil/actions'; +export default function ActionList() { + const action = useRecoilValue(actionsState); const history = useHistory(); - const [search, setSearch] = useSearchParamState('search', ''); - const [categories, setCategories] = useLocalStorage('action-categories', []); - const [statuses, setStatuses] = useLocalStorage('action-statuses', [TODO]); - const [selectedTeamIds, setSelectedTeamIds] = useLocalStorage('action-teams', [currentTeam._id]); - const [viewAllOrganisationData, setViewAllOrganisationData] = useLocalStorage('action-allOrg', false); - const [viewNoTeamData, setViewNoTeamData] = useLocalStorage('action-noTeam', false); - const [actionsWithNoCategory, setActionsWithNoCategory] = useLocalStorage('action-noCategory', false); - const [mobileBottomSheetOpened, setMobileBottomSheetOpened] = useState(false); - - const [showAs, setShowAs] = useLocalStorage('action-showAs', showAsOptions[0]); // calendar, list - const dataConsolidated = useRecoilValue( - dataFilteredBySearchSelector({ - search, - statuses, - categories, - teamIds: selectedTeamIds, - viewAllOrganisationData, - viewNoTeamData, - actionsWithNoCategory, - }) - ); - const isDesktop = useMinimumWidth('sm'); - - const selectedTeams = useMemo(() => { - if (viewAllOrganisationData) return teams; - if (!selectedTeamIds.length) return teams; - return teams.filter((t) => selectedTeamIds.includes(t._id)); - }, [selectedTeamIds, viewAllOrganisationData, teams]); - const allSelectedTeamsAreNightSession = useMemo(() => { - for (const team of selectedTeams) { - if (!team.nightSession) return false; - } - return true; - }, [selectedTeams]); + if (!action) return ; return ( - <> - - Agenda{' '} - {viewAllOrganisationData ? ( - <>de toute l'organisation - ) : ( - <> - {selectedTeamIds.length > 1 ? 'des équipes' : "de l'équipe"}{' '} - - {teams - .filter((t) => selectedTeamIds.includes(t._id)) - .map((e) => e?.name) - .join(', ')} - - - )} - - } + +
+ history.push(`/action/${i._id}`)} + columns={[ + { title: 'Nom', dataKey: 'name' }, + { title: 'Créée le', dataKey: 'createdAt', render: (i) => (i.createdAt || '').slice(0, 10) }, + { title: 'À faire le', dataKey: 'dueAt', render: (i) => (i.createdAt || '').slice(0, 10) }, + { title: 'Status', dataKey: 'status', render: (i) => }, + ]} /> - {isDesktop ? ( -
-
- { - const searchParams = new URLSearchParams(history.location.search); - searchParams.set('dueAt', dayjsInstance().toISOString()); - searchParams.set('newAction', true); - history.push(`?${searchParams.toString()}`); - }} - color="primary" - title="Créer une nouvelle action" - padding={'12px 24px'} - /> - {Boolean(user.healthcareProfessional) && ( - { - const searchParams = new URLSearchParams(history.location.search); - searchParams.set('dueAt', dayjsInstance().toISOString()); - searchParams.set('newConsultation', true); - history.push(`?${searchParams.toString()}`); - }} - color="primary" - title="Créer une nouvelle consultation" - padding={'12px 24px'} - /> - )} -
-
- ) : ( - <> - - My awesome content here - - )} - {isDesktop && ( -
-
- -
- setShowAs(value)} - value={{ value: showAs, label: showAs }} - options={showAsOptions.map((_option) => ({ value: _option, label: _option }))} - isClearable={false} - isMulti={false} - inputId="actions-show-as" - getOptionValue={(o) => o.value} - getOptionLabel={(o) => o.label} - /> -
-
-
- - -
-
- -
- setCategories(c)} - values={categories} - isDisabled={!!actionsWithNoCategory} - /> -
- -
-
- -
- { - setSelectedTeamIds(teamIds); - }} - value={selectedTeamIds} - colored - isDisabled={viewAllOrganisationData || viewNoTeamData} - /> - {teams.length > 1 && ( - - )} - {teams.length > 1 && ( - - )} -
-
-
- -
- s._id} - getOptionLabel={(s) => s.name} - name="statuses" - onChange={(s) => setStatuses(s.map((s) => s._id))} - isClearable - isMulti - value={mappedIdsToLabels.filter((s) => statuses.includes(s._id))} - /> -
-
-
- )} - {showAs === showAsOptions[0] && ( -
- -
- )} - {showAs === showAsOptions[1] && ( -
- -
- )} - {showAs === showAsOptions[2] && ( -
- { - const searchParams = new URLSearchParams(history.location.search); - searchParams.set('dueAt', dayjsInstance(date).toISOString()); - searchParams.set('newAction', true); - history.push(`?${searchParams.toString()}`); // Update the URL with the new search parameters. - }} - /> -
- )} - + ); -}; +} -export default List; +const Status = ({ status }) => { + if (status === 'A FAIRE') return {status}; + if (status === 'FAIT') return {status}; + return
; +}; diff --git a/dashboard/src/scenes/action/view.js b/dashboard/src/scenes/action/view.js new file mode 100644 index 000000000..1450bd399 --- /dev/null +++ b/dashboard/src/scenes/action/view.js @@ -0,0 +1,149 @@ +import React, { useEffect, useState } from 'react'; +import { Container, Nav, NavItem, TabContent, TabPane, Row, Col } from 'reactstrap'; +import styled from 'styled-components'; +import { useParams, Link } from 'react-router-dom'; + +import api from '../../services/api'; +import Header from '../../components/header'; +import TabButton from '../../components/tabButton'; +import BackButton from '../../components/backButton'; +import Box from '../../components/Box'; +import { useRecoilValue } from 'recoil'; +import { actionsState } from '../../recoil/actions'; + +export default () => { + const [activeTab, setActiveTab] = useState('1'); + const { actionId } = useParams(); + const action = useRecoilValue(actionsState).find((e) => e._id === actionId); + + return ( + +
} /> + + + + + +
+
{action.name}
+
{action.description}
+
{`Status : ${action.status}`}
+
{`Crée le : ${action.createdAt.slice(0, 10)}`}
+ + + + + + + + + + +
+              {Object.keys(action).map((e) => (
+                
+ {e}: {JSON.stringify(action[e])} +
+ ))} +
+
+ + + + ); +}; + +const Organisation = ({ id }) => { + const [organisation, setOrganisation] = useState(null); + useEffect(() => { + (async () => { + const { data } = await api.get(`/organisation/${id}`); + setOrganisation(data); + })(); + }, []); + if (!organisation) return
; + return ( + +

Organisation

+ {organisation.name} +
+ ); +}; + +const Person = ({ id }) => { + const [person, setPerson] = useState(null); + useEffect(() => { + (async () => { + const { data } = await api.get(`/person/${id}`); + setPerson(data); + })(); + }, []); + if (!person) return
; + return ( + +

Usager

+ {person.name} +
+ ); +}; + +const Team = ({ id }) => { + const [team, setTeam] = useState(null); + useEffect(() => { + (async () => { + const { data } = await api.get(`/team/${id}`); + setTeam(data); + })(); + }, []); + if (!team) return
; + return ( + +

Équipe

+ {team.name} +
+ ); +}; + +const User = ({ id }) => { + const [data, setData] = useState(null); + useEffect(() => { + (async () => { + const { data: d } = await api.get(`/user/${id}`); + setData(d); + })(); + }, []); + + if (!data) return
; + + return ( + +

Utilisateur

+ {data.name} +
+ ); +}; + +const Card = styled.div` + background: white; + padding: 10px; + margin: 5px; + border-style: solid; + border-width: 1px; + border-radius: 10px; + border-color: #ddd; + h1 { + font-size: 14px; + font-weight: bold; + } +`; diff --git a/dashboard/src/scenes/auth/signin.js b/dashboard/src/scenes/auth/signin.js index 5f6087614..b1749fff9 100644 --- a/dashboard/src/scenes/auth/signin.js +++ b/dashboard/src/scenes/auth/signin.js @@ -1,10 +1,11 @@ import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { FormGroup } from 'reactstrap'; import validator from 'validator'; import { Link, useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { detect } from 'detect-browser'; -import packageInfo from '../../../package.json'; import ButtonCustom from '../../components/ButtonCustom'; import { DEFAULT_ORGANISATION_KEY } from '../../config'; import PasswordInput from '../../components/PasswordInput'; @@ -12,6 +13,7 @@ import { currentTeamState, organisationState, sessionInitialDateTimestamp, teams import API, { setOrgEncryptionKey, authTokenState } from '../../services/api'; import { useDataLoader } from '../../components/DataLoader'; import useMinimumWidth from '../../services/useMinimumWidth'; +import LoadingButton from '../../components/loadingButton'; const SignIn = () => { const [organisation, setOrganisation] = useRecoilState(organisationState); @@ -22,7 +24,7 @@ const SignIn = () => { const [user, setUser] = useRecoilState(userState); const history = useHistory(); const [showErrors, setShowErrors] = useState(false); - const [userName, setUserName] = useState(false); + // const [userName, setUserName] = useState(false); const [showSelectTeam, setShowSelectTeam] = useState(false); const [showEncryption, setShowEncryption] = useState(false); const [showPassword, setShowPassword] = useState(false); @@ -50,7 +52,7 @@ const SignIn = () => { const onLogout = async () => { await API.logout(); setShowErrors(false); - setUserName(''); + // setUserName(''); setShowSelectTeam(false); setShowEncryption(false); setShowPassword(false); @@ -68,7 +70,7 @@ const SignIn = () => { } window.localStorage.setItem('mano-organisationId', organisation._id); setOrganisation(organisation); - setUserName(user.name); + // setUserName(user.name); if (!!organisation.encryptionEnabled && !['superadmin'].includes(user.role)) setShowEncryption(true); } @@ -79,6 +81,7 @@ const SignIn = () => { const handleSubmit = async (e) => { try { + console.log('SLASH'); e.preventDefault(); const emailError = !authViaCookie && !validator.isEmail(signinForm.email) ? 'Adresse email invalide' : ''; const passwordError = !authViaCookie && validator.isEmpty(signinForm.password) ? 'Ce champ est obligatoire' : ''; @@ -144,12 +147,12 @@ const SignIn = () => { return; } // basic login - if (user.teams.length === 1 || (process.env.NODE_ENV === 'development' && process.env.REACT_APP_SKIP_TEAMS === 'true')) { - setCurrentTeam(user.teams[0]); - onSigninValidated(); - return; - } - setShowSelectTeam(true); + // if (user.teams.length === 1 || (process.env.NODE_ENV === 'development' && process.env.REACT_APP_SKIP_TEAMS === 'true')) { + setCurrentTeam(user.teams[0]); + onSigninValidated(); + return; + // } + // setShowSelectTeam(true); } catch (signinError) { console.log('error signin', signinError); toast.error('Mauvais identifiants', signinError.message); @@ -183,92 +186,148 @@ const SignIn = () => { } return ( -
-

- {userName ? `Bienvenue ${userName?.split(' ')?.[0]}\u00a0!` : 'Bienvenue\u00a0!'} -

-
- {!authViaCookie && ( - <> -
-
- - -
- {!!showErrors &&

{signinFormErrors.email}

} -
-
-
- - -
- {!!showErrors &&

{signinFormErrors.password}

} -
-
- Mot de passe oublié ? + + Se connecter + {!authViaCookie && ( + <> + +
+ !validator.isEmail(v) && 'Adresse email invalide'} + name="email" + type="email" + id="email" + value={signinForm.email} + onChange={handleChangeRequest} + /> +
- - )} - {!!showEncryption && ( + {!!showErrors &&

{signinFormErrors.email}

} +
- +
- {!!showErrors &&

{signinFormErrors.orgEncryptionKey}

} + {!!showErrors &&

{signinFormErrors.password}

}
- )} +
+ Mot de passe oublié ? +
+ + )} + {!!showEncryption && ( +
+
+ + +
+ {!!showErrors &&

{signinFormErrors.orgEncryptionKey}

} +
+ )} + + Se connecter + + {!!authViaCookie && ( - {!!authViaCookie && ( - - )} -

Version: {packageInfo.version}

- -
+ )} +

Version: 14 janvier 2021

+ ); }; export default SignIn; + +const AuthWrapper = styled.form` + max-width: 500px; + width: calc(100% - 40px); + padding: 40px 30px 30px; + border-radius: 0.5em; + background-color: #fff; + font-family: Nista, Helvetica; + color: #252b2f; + margin: 5em auto; + overflow: hidden; + -webkit-box-shadow: 0 0 1.25rem 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 0 1.25rem 0 rgba(0, 0, 0, 0.2); +`; + +const Title = styled.div` + font-family: Helvetica; + text-align: center; + font-size: 32px; + font-weight: 600; + margin-bottom: 15px; +`; + +const Submit = styled(LoadingButton)` + font-family: Helvetica; + width: 220px; + border-radius: 30px; + margin: auto; + display: block; + font-size: 16px; + padding: 8px; + min-height: 42px; +`; + +const InputField = styled.input` + background-color: transparent; + outline: 0; + display: block; + width: 100%; + padding: 0.625rem; + margin-bottom: 0.375rem; + border-radius: 4px; + border: 1px solid #a7b0b7; + color: #252b2f; + -webkit-transition: border 0.2s ease; + transition: border 0.2s ease; + line-height: 1.2; + &:focus { + outline: none; + border: 1px solid #116eee; + & + label { + color: #116eee; + } + } +`; +// const Account = styled.div` +// text-align: center; +// padding-top: 25px; +// font-size: 14px; +// `; +const StyledFormGroup = styled(FormGroup)` + margin-bottom: 25px; + div { + display: flex; + flex-direction: column-reverse; + } +`; diff --git a/dashboard/src/scenes/home/index.js b/dashboard/src/scenes/home/index.js new file mode 100644 index 000000000..6c2473cfe --- /dev/null +++ b/dashboard/src/scenes/home/index.js @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { Col, Container, Row } from 'reactstrap'; +import Header from '../../components/header'; + +import { theme } from '../../config'; +import { useRecoilValue } from 'recoil'; +import { personsState } from '../../recoil/persons'; +import { actionsState } from '../../recoil/actions'; +import API from '../../services/api'; + +export default function Home() { + return ( + <> + +
+ + + + + + + + + ); +} + +const BlockPersons = ({ filters }) => { + const persons = useRecoilValue(personsState); + const count = persons.length; + + return ( +
+ + + ); +}; + +const BlockActions = ({ filters }) => { + const actions = useRecoilValue(actionsState); + const count = actions.length; + + return ( + + + + ); +}; + +const BlockStructure = ({ filters }) => { + const [count, setCount] = useState(0); + + useEffect(() => { + getCount(); + }, []); + + const getCount = async () => { + const response = await API.get({ path: '/structure' }); + if (!response.ok) return; + setCount(response.data.length); + }; + return ( + + + + ); +}; + +const Card = ({ title, count }) => ( + + {title} + {count} + +); + +const CardWrapper = styled.div` + background: ${theme.white}; + padding: 24px 0 40px; + border-radius: 20px; + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: column; + font-weight: bold; + height: 184px; + margin-bottom: 10px; +`; + +const CardTitle = styled.div` + font-size: 16px; + line-height: 24px; + text-align: center; + color: ${theme.black}; +`; + +const CardCount = styled.div` + font-size: 56px; + line-height: 64px; + color: ${theme.main}; +`; diff --git a/dashboard/src/scenes/person/list.js b/dashboard/src/scenes/person/list.js index c9fb796e5..198a75e7f 100644 --- a/dashboard/src/scenes/person/list.js +++ b/dashboard/src/scenes/person/list.js @@ -1,319 +1,31 @@ -import React, { useMemo } from 'react'; +import { Container } from 'reactstrap'; import { useHistory } from 'react-router-dom'; -import { selector, selectorFamily, useRecoilValue } from 'recoil'; -import { useLocalStorage } from '../../services/useLocalStorage'; -import { SmallHeader } from '../../components/header'; -import Page from '../../components/pagination'; -import Search from '../../components/search'; + +import Header from '../../components/header'; import Loading from '../../components/loading'; import Table from '../../components/table'; -import CreatePerson from './CreatePerson'; -import { - fieldsPersonsCustomizableOptionsSelector, - filterPersonsBaseSelector, - flattenedCustomFieldsPersonsSelector, - sortPersons, -} from '../../recoil/persons'; -import TagTeam from '../../components/TagTeam'; -import Filters, { filterData } from '../../components/Filters'; -import { dayjsInstance, formatDateWithFullMonth } from '../../services/date'; -import { personsWithMedicalFileMergedSelector } from '../../recoil/selectors'; -import { currentTeamState, organisationState, userState } from '../../recoil/auth'; -import { placesState } from '../../recoil/places'; -import { filterBySearch } from '../search/utils'; -import useTitle from '../../services/useTitle'; -import useSearchParamState from '../../services/useSearchParamState'; -import { useDataLoader } from '../../components/DataLoader'; -import ExclamationMarkButton from '../../components/tailwind/ExclamationMarkButton'; -import { customFieldsMedicalFileSelector } from '../../recoil/medicalFiles'; - -const limit = 20; - -const personsFilteredSelector = selectorFamily({ - key: 'personsFilteredSelector', - get: - ({ viewAllOrganisationData, filters, alertness }) => - ({ get }) => { - const personWithBirthDate = get(personsWithMedicalFileMergedSelector); - const currentTeam = get(currentTeamState); - let pFiltered = personWithBirthDate; - if (!!filters?.filter((f) => Boolean(f?.value)).length) pFiltered = filterData(pFiltered, filters); - if (!!alertness) pFiltered = pFiltered.filter((p) => !!p.alertness); - if (!!viewAllOrganisationData) return pFiltered; - return pFiltered.filter((p) => p.assignedTeams?.includes(currentTeam._id)); - }, -}); - -const personsFilteredBySearchSelector = selectorFamily({ - key: 'personsFilteredBySearchSelector', - get: - ({ viewAllOrganisationData, filters, alertness, search, sortBy, sortOrder }) => - ({ get }) => { - const personsFiltered = get(personsFilteredSelector({ viewAllOrganisationData, filters, alertness })); - const personsSorted = [...personsFiltered].sort(sortPersons(sortBy, sortOrder)); - - if (!search?.length) { - return personsSorted; - } - - const personsfilteredBySearch = filterBySearch(search, personsSorted); - - return personsfilteredBySearch; - }, -}); - -const filterPersonsWithAllFieldsSelector = selector({ - key: 'filterPersonsWithAllFieldsSelector', - get: ({ get }) => { - const places = get(placesState); - const user = get(userState); - const team = get(currentTeamState); - const fieldsPersonsCustomizableOptions = get(fieldsPersonsCustomizableOptionsSelector); - const flattenedCustomFieldsPersons = get(flattenedCustomFieldsPersonsSelector); - const customFieldsMedicalFile = get(customFieldsMedicalFileSelector); - const filterPersonsBase = get(filterPersonsBaseSelector); - return [ - ...filterPersonsBase, - ...fieldsPersonsCustomizableOptions.filter((a) => a.enabled || a.enabledTeams?.includes(team._id)).map((a) => ({ field: a.name, ...a })), - ...flattenedCustomFieldsPersons.filter((a) => a.enabled || a.enabledTeams?.includes(team._id)).map((a) => ({ field: a.name, ...a })), - ...(user.healthcareProfessional - ? customFieldsMedicalFile.filter((a) => a.enabled || a.enabledTeams?.includes(team._id)).map((a) => ({ field: a.name, ...a })) - : []), - { - label: 'Lieux fréquentés', - field: 'places', - options: [...new Set(places.map((place) => place.name))], - }, - ]; - }, -}); +import { useRecoilValue } from 'recoil'; +import { personsState } from '../../recoil/persons'; -const List = () => { - useTitle('Personnes'); - useDataLoader({ refreshOnMount: true }); - const filterPersonsWithAllFields = useRecoilValue(filterPersonsWithAllFieldsSelector); - - const [search, setSearch] = useSearchParamState('search', ''); - const [alertness, setFilterAlertness] = useLocalStorage('person-alertness', false); - const [viewAllOrganisationData, setViewAllOrganisationData] = useLocalStorage('person-allOrg', true); - const [sortBy, setSortBy] = useLocalStorage('person-sortBy', 'name'); - const [sortOrder, setSortOrder] = useLocalStorage('person-sortOrder', 'ASC'); - const [filters, setFilters] = useLocalStorage('person-filters', []); - const [page, setPage] = useSearchParamState('page', 0); - const currentTeam = useRecoilValue(currentTeamState); - - const personsFilteredBySearch = useRecoilValue( - personsFilteredBySearchSelector({ search, viewAllOrganisationData, filters, alertness, sortBy, sortOrder }) - ); - - const data = useMemo(() => { - return personsFilteredBySearch.filter((_, index) => index < (page + 1) * limit && index >= page * limit); - }, [personsFilteredBySearch, page]); - const total = useMemo(() => personsFilteredBySearch.length, [personsFilteredBySearch]); - - const organisation = useRecoilValue(organisationState); +export default function PersonsList() { + const people = useRecoilValue(personsState); const history = useHistory(); - if (!personsFilteredBySearch) return ; + if (!people) return ; return ( - <> - - Personnes suivies par{' '} - {viewAllOrganisationData ? ( - <> - l'organisation {organisation.name} - - ) : ( - <> - l'équipe {currentTeam?.name || ''} - - )} - - } - /> -
-
-
- -
-
-
-
-
- -
- { - if (page) { - setPage(0); - setSearch(value, { sideEffect: ['page', 0] }); - } else { - setSearch(value); - } - }} - /> -
- -
-
- -
-
-
-
- + +
history.push(`/person/${p._id}`)} + onRowClick={(i) => history.push(`/person/${i._id}`)} columns={[ - { - title: '', - dataKey: 'group', - small: true, - onSortOrder: setSortOrder, - onSortBy: setSortBy, - sortOrder, - sortBy, - render: (person) => { - if (!person.group) return null; - return ( -
- - 👪 - -
- ); - }, - }, - { - title: 'Nom', - dataKey: 'name', - onSortOrder: setSortOrder, - onSortBy: setSortBy, - sortOrder, - sortBy, - render: (p) => { - if (p.outOfActiveList) - return ( -
-
{p.name}
-
Sortie de file active : {p.outOfActiveListReasons?.join(', ')}
-
- ); - return
{p.name}
; - }, - }, - { - title: 'Date de naissance', - dataKey: 'formattedBirthDate', - onSortOrder: setSortOrder, - onSortBy: setSortBy, - sortOrder, - sortBy, - render: (p) => { - if (!p.birthdate) return ''; - else if (p.outOfActiveList) return {p.formattedBirthDate}; - return ( - - {p.formattedBirthDate} - - ); - }, - }, - { - title: 'Vigilance', - dataKey: 'alertness', - onSortOrder: setSortOrder, - onSortBy: setSortBy, - sortOrder, - sortBy, - render: (p) => { - return p.alertness ? ( - - ) : null; - }, - }, - { - title: 'Équipe(s) en charge', - dataKey: 'assignedTeams', - render: (person) => , - }, - { - title: 'Suivi(e) depuis le', - dataKey: 'followedSince', - onSortOrder: setSortOrder, - onSortBy: setSortBy, - sortOrder, - sortBy, - render: (p) => { - if (p.outOfActiveList) return
{formatDateWithFullMonth(p.followedSince || p.createdAt || '')}
; - return formatDateWithFullMonth(p.followedSince || p.createdAt || ''); - }, - }, - { - title: 'Dernière interaction', - dataKey: 'lastUpdateCheckForGDPR', - onSortOrder: setSortOrder, - onSortBy: setSortBy, - sortOrder, - sortBy, - render: (p) => { - return ( -
- {formatDateWithFullMonth(p.lastUpdateCheckForGDPR)} -
- ); - }, - }, - ].filter((c) => organisation.groupsEnabled || c.dataKey !== 'group')} + { title: 'Nom', dataKey: 'name' }, + { title: 'Créée le', dataKey: 'createdAt', render: (i) => (i.createdAt || '').slice(0, 10) }, + ]} /> - setPage(page, true)} /> - + {/* */} + ); -}; - -const Teams = ({ person: { _id, assignedTeams } }) => ( -
- {assignedTeams?.map((teamId) => ( - - ))} -
-); - -export default List; +} diff --git a/dashboard/src/scenes/person/view.js b/dashboard/src/scenes/person/view.js index 7dad0ae86..e3fdec022 100644 --- a/dashboard/src/scenes/person/view.js +++ b/dashboard/src/scenes/person/view.js @@ -1,140 +1,83 @@ -import { useHistory, useLocation, useParams } from 'react-router-dom'; -import { Alert } from 'reactstrap'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; -import Places from './Places'; -import { itemsGroupedByPersonSelector } from '../../recoil/selectors'; -import API from '../../services/api'; -import { formatDateWithFullMonth } from '../../services/date'; -import History from './components/History'; -import MedicalFile from './components/MedicalFile'; -import Summary from './components/Summary'; +import React, { useState } from 'react'; +import { Container, Nav, NavItem, TabContent, TabPane, FormGroup, Input, Label, Row, Col } from 'reactstrap'; + +import { useParams } from 'react-router-dom'; +import { Formik } from 'formik'; + +import Header from '../../components/header'; +import Loading from '../../components/loading'; +import Button from '../../components/Button'; import BackButton from '../../components/backButton'; -import UserName from '../../components/UserName'; -import { personsState, usePreparePersonForEncryption } from '../../recoil/persons'; -import { toast } from 'react-toastify'; -import { organisationState, userState } from '../../recoil/auth'; -import PersonFamily from './PersonFamily'; -import { groupSelector } from '../../recoil/groups'; +import Box from '../../components/Box'; +import TabButton from '../../components/tabButton'; +import { personsObjectSelector } from '../../recoil/selectors'; +import { useRecoilValue } from 'recoil'; -export default function View() { +export default function PersonView() { + const [activeTab, setActiveTab] = useState('1'); const { personId } = useParams(); - const history = useHistory(); - const location = useLocation(); + const person = useRecoilValue(personsObjectSelector)[personId]; - const organisation = useRecoilValue(organisationState); - const person = useRecoilValue(itemsGroupedByPersonSelector)[personId]; - const personGroup = useRecoilValue(groupSelector({ personId })); - const setPersons = useSetRecoilState(personsState); - const user = useRecoilValue(userState); - const searchParams = new URLSearchParams(location.search); - const currentTab = searchParams.get('tab') || 'Résumé'; - const setCurrentTab = (tab) => { - searchParams.set('tab', tab); - history.push(`?${searchParams.toString()}`); - }; - - const preparePersonForEncryption = usePreparePersonForEncryption(); + if (!person) return ; return ( -
-
-
- -
-
- 'Créée par '} - canAddUser - handleChange={async (newUser) => { - const response = await API.put({ - path: `/person/${person._id}`, - body: preparePersonForEncryption({ ...person, user: newUser }), - }); - if (response.ok) { - toast.success('Personne mise à jour (créée par)'); - const newPerson = response.decryptedData; - setPersons((persons) => - persons.map((p) => { - if (p._id === person._id) return newPerson; - return p; - }) - ); - } else { - toast.error('Impossible de mettre à jour la personne'); - } - }} - /> -
-
-
-
- {!['restricted-access'].includes(user.role) && ( -
    -
  • - -
  • - {Boolean(user.healthcareProfessional) && ( -
  • - -
  • - )} -
  • - -
  • -
  • - -
  • - {Boolean(organisation.groupsEnabled) && ( -
  • - -
  • + +
    } /> + + + + + { + // try { + // await api.put(`/person?organisation_id=${organisation._id}`, values); + // toastr.success("Mis à jour !"); + // } catch (e) { + // console.log(e); + // toastr.error("Erreur!"); + // } + }}> + {({ values, handleChange, handleSubmit, isSubmitting }) => ( + + +
+ + + + + + +
+
+ )} - - )} - - -
- {person.outOfActiveList && ( - - {person?.name} est en dehors de la file active - {person.outOfActiveListReasons?.length ? ( - <> - , pour {person.outOfActiveListReasons.length > 1 ? 'les motifs suivants' : 'le motif suivant'} :{' '} - {person.outOfActiveListReasons.join(', ')} - - ) : ( - '' - )}{' '} - {person.outOfActiveListDate && ` depuis le ${formatDateWithFullMonth(person.outOfActiveListDate)}`} - - )} - {currentTab === 'Résumé' && } - {!['restricted-access'].includes(user.role) && ( - <> - {currentTab === 'Dossier Médical' && user.healthcareProfessional && } - {currentTab === 'Lieux fréquentés' && } - {currentTab === 'Historique' && } - {currentTab === 'Liens familiaux' && } - - )} -
- + + + +
+              {Object.keys(person).map((e) => (
+                
+ {e}: {JSON.stringify(person[e])} +
+ ))} +
+
+ + + ); }