diff --git a/README.md b/README.md index 13d5863a8411a5a3b01002a9fcbd8447a554eff9..1f1c9290081990f5cffb7b016535b3e413a54128 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Ecolyo Agent Client -[check documentation here](https://doc-self-data.apps.grandlyon.com/ecolyo-agent/technical/getting_started/#local-usage) +[check documentation here](https://doc-self-data.apps.grandlyon.com/docs/ecolyo-agent/Technical/getting-started/#local-usage) diff --git a/src/API.ts b/src/API.ts index add94062679ebd8622c41b76e011e097088f9de4..38fcf1d70436a1b4600af2ea6f9ced041343811c 100644 --- a/src/API.ts +++ b/src/API.ts @@ -16,7 +16,7 @@ const endpoints = { animator: { partnersInfo: '/api/animator/partnersInfo', imageNames: '/api/animator/imageNames', - savePartnersInfo: '/api/animator/savePartnersInfo', + customPopup: '/api/animator/customPopup', }, } as const @@ -38,7 +38,7 @@ export const fetchEcogestureImages = async ( axiosHeaders: AxiosRequestConfig ) => { const { data: imageNames } = await axios.get<string[]>( - `/api/animator/imageNames`, + endpoints.animator.imageNames, axiosHeaders ) if (imageNames && imageNames !== null) { @@ -55,12 +55,12 @@ export const fetchPartnersIssue = async () => { return data } catch (error) { console.error('error partnersInfo', error) - toast.error('Accès refusé, veuillez vous connecter') + toast.error('Aucune information des partenaires trouvée') throw error } } -export const savePartnersInfo = async ( +export const putPartnersInfo = async ( partnersInfo: IPartnersInfo, axiosHeaders: AxiosRequestConfig ) => { @@ -88,7 +88,26 @@ export const fetchCustomPopup = async (): Promise<ICustomPopup> => { return data } catch (error) { console.error('error customPopup', error) - toast.error('Accès refusé, veuillez vous connecter') + toast.error('Aucune pop-up personnalisée trouvée') throw error } } + +export const putCustomPopup = async ( + customPopup: ICustomPopup, + axiosHeaders: AxiosRequestConfig +) => { + try { + await axios.put( + endpoints.animator.customPopup, + { + ...customPopup, + }, + axiosHeaders + ) + toast.success('Pop-up personnalisée enregistrée !') + } catch (e) { + toast.error('Erreur lors de l’enregistrement de la pop-up personnalisée') + console.error(e) + } +} diff --git a/src/components/Popups/CustomPopup.tsx b/src/components/Popups/CustomPopup.tsx index f8ed27fe7dde474cf918d077dd121489dde3cb72..2de48f4a4d19a72f2befe030b0c352021afc91e1 100644 --- a/src/components/Popups/CustomPopup.tsx +++ b/src/components/Popups/CustomPopup.tsx @@ -12,6 +12,7 @@ import React, { useEffect, useState } from 'react' import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' import { Link } from 'react-router-dom' import { getAxiosXSRFHeader } from '../../axios.config' +import { useCustomPopup } from '../../hooks/useCustomPopup' import { usePartnersIssue } from '../../hooks/usePartnersIssue' import { useWhoAmI } from '../../hooks/useWhoAmI' import { ICustomPopup, PopupDuration } from '../../models/customPopup.model' @@ -21,7 +22,6 @@ import { durationType, mapDuration, } from '../../models/durationOptions.model' -import { CustomPopupService } from '../../services/customPopup.service' import { convertStringToEditorState } from '../../utils/editorStateManagement' import { getFilenameFromPath } from '../../utils/imagesUrlsGetter' import ImagePicker from '../ImagePicker/ImagePicker' @@ -29,6 +29,7 @@ import Loader from '../Loader/Loader' import CustomEditor from '../Newsletter/CustomEditor' import { links } from '../Routes/Router' import './popups.scss' +import { PopupWrapper } from './Wrapper' const OPTIONS: Option[] = [ { @@ -46,37 +47,75 @@ const OPTIONS: Option[] = [ ] export const CustomPopup: React.FC = () => { - const { data: user } = useWhoAmI() const { partnersIssue } = usePartnersIssue() - const defaultIcon = '/assets/ecogesture/bullhorn.png' + const { customPopup } = useCustomPopup() + + const isPartnerNotificationOn = Boolean( + partnersIssue.data && + (partnersIssue.data.enedis_failure || + partnersIssue.data.egl_failure || + partnersIssue.data.grdf_failure) + ) + + if (isPartnerNotificationOn) { + return ( + <PopupWrapper> + <p className="singlePopupWarning"> + La pop-up personnalisée ne peut pas être activée en même temps que la{' '} + <Link to={links.partnersIssue.path}> + pop-up maintenance des partenaires + </Link> + </p> + </PopupWrapper> + ) + } + + if (customPopup.isLoading) { + return <Loader /> + } + + if (customPopup.error) { + return <p>Une erreur est survenue</p> + } + + if (customPopup.data) { + return ( + <PopupWrapper> + <CustomPopupForm initialData={customPopup.data} /> + </PopupWrapper> + ) + } +} + +export const CustomPopupForm = ({ + initialData, +}: { + initialData: ICustomPopup +}) => { + const { data: user } = useWhoAmI() + const { saveCustomPopup } = useCustomPopup() + + const isPopupOutdated = (date: string) => + DateTime.local() >= DateTime.fromISO(date) + + const isOutdated = isPopupOutdated(initialData.endDate) + const isEnabled = isOutdated ? false : initialData.popupEnabled - const [refreshData, setRefreshData] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [previousEndDate, setPreviousEndDate] = useState<string>() const [popupDuration, setPopupDuration] = useState<PopupDuration>({ type: durationEnum.infinite, duration: 5, }) + const [customPopup, setCustomPopup] = useState<ICustomPopup>({ - popupEnabled: false, - title: '', - image: '', - description: '', - endDate: DateTime.local().plus({ days: 365 }).toISO(), + popupEnabled: isEnabled, + title: initialData.title, + image: initialData.image || 'bullhorn', + description: initialData.description, + endDate: isOutdated + ? DateTime.local().plus({ days: 365 }).toISO() + : initialData.endDate, }) - const isPartnerNotificationOn = () => - Boolean( - partnersIssue.data && - (partnersIssue.data.enedis_failure || - partnersIssue.data.egl_failure || - partnersIssue.data.grdf_failure) - ) - - /** Only one type of popup can be enabled */ - const isPageValid = () => - !(isPartnerNotificationOn() && customPopup.popupEnabled) - const toggleCustomPopup = (value: boolean): void => { setCustomPopup(prev => ({ ...prev, @@ -94,57 +133,15 @@ export const CustomPopup: React.FC = () => { })) } - useEffect(() => { - let subscribed = true - setIsLoading(true) - - async function loadCustomPopup() { - if (user) { - const customPopupService = new CustomPopupService() - const previousPopup = await customPopupService.getCustomPopupInfo() - - if (previousPopup) { - const isOutdated = isPopupOutdated(previousPopup.endDate) - /** If outdated, set value to false, otherwise, set it to its value */ - const isEnabled = isOutdated ? false : previousPopup.popupEnabled - const popupImage = - previousPopup.image === '' - ? defaultIcon - : `/assets/ecogesture/${previousPopup.image}.png` - setCustomPopup({ - ...previousPopup, - image: popupImage, - endDate: customPopup.endDate, - popupEnabled: isEnabled, - }) - setPreviousEndDate( - isOutdated ? customPopup.endDate : previousPopup.endDate - ) - } - } - setIsLoading(false) - } - if (subscribed) { - loadCustomPopup() - } - return () => { - subscribed = false - setRefreshData(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user, refreshData, setCustomPopup]) - const handleSave = async (): Promise<void> => { if (user) { - const customPopupService = new CustomPopupService() - await customPopupService.saveCustomPopup( + saveCustomPopup( { ...customPopup, image: getFilenameFromPath(customPopup.image), }, getAxiosXSRFHeader(user.xsrftoken) ) - setPreviousEndDate(customPopup.endDate) } } @@ -158,30 +155,22 @@ export const CustomPopup: React.FC = () => { /** Handles duration change */ useEffect(() => { const now = DateTime.local() - let newDate: DateTime - if (popupDuration.type !== durationEnum.infinite) { - newDate = now.plus({ - [popupDuration.type]: popupDuration.duration, - }) - } else { - newDate = now.plus({ - years: 1, - }) - } + const newDate = + popupDuration.type !== durationEnum.infinite + ? now.plus({ [popupDuration.type]: popupDuration.duration }) + : now.plus({ years: 1 }) + setCustomPopup(prev => ({ ...prev, endDate: newDate.toISO() ?? '', })) }, [popupDuration]) - const isPopupOutdated = (date: string) => - DateTime.local() >= DateTime.fromISO(date) - /** * Returns "Popup expirée" OR "Temps restant : ..." */ const getRemainingDuration = (date: string) => { - if (isPopupOutdated(date)) { + if (isOutdated) { return <p className="endDate">Popup expirée</p> } return ( @@ -197,136 +186,105 @@ export const CustomPopup: React.FC = () => { return ( <> - <div className="header"> - <h1>Création de Pop-up</h1> - </div> + <div className="customPopup"> + <h3>Affichage de pop-up personnalisée</h3> + <FormGroup style={{ flexDirection: 'row' }}> + <FormControlLabel + label="Pop-up active" + labelPlacement="top" + control={ + <Switch + checked={customPopup.popupEnabled} + onChange={event => toggleCustomPopup(event.target.checked)} + /> + } + /> + {customPopup.popupEnabled && + initialData.endDate && + getRemainingDuration(initialData.endDate)} + </FormGroup> - <div className="content popups"> - {isLoading && <Loader />} - {!isLoading && ( - <> - <div className="customPopup"> - <h3>Affichage de pop-up personnalisée</h3> - <FormGroup style={{ flexDirection: 'row' }}> - <FormControlLabel - label="Pop-up active" - labelPlacement="top" - control={ - <Switch - disabled={isPartnerNotificationOn()} - checked={customPopup.popupEnabled} - onChange={event => - toggleCustomPopup(event.target.checked) - } - /> - } - /> - {customPopup.popupEnabled && - previousEndDate && - getRemainingDuration(previousEndDate)} - - {isPartnerNotificationOn() && ( - <p className="singlePopupWarning"> - La pop-up personnalisée ne peut pas être activée en même - temps que la{' '} - <Link to={links.partnersIssue.path}> - pop-up maintenance des partenaires - </Link> - </p> - )} - </FormGroup> - - <div className="popupEndDate"> - <h4>Durée</h4> - <div> - <FormControl style={{ flexDirection: 'row', gap: '1rem' }}> - <NativeSelect - inputProps={{ - name: 'age', - id: 'uncontrolled-native', - }} - onChange={event => handleSelectChange(event)} - value={popupDuration.type} - > - {OPTIONS.map(option => ( - <option key={option.value} value={option.value}> - {option.label} - </option> - ))} - </NativeSelect> - - {popupDuration.type !== 'infinite' && ( - <TextField - style={{ width: '6rem' }} - inputProps={{ - inputMode: 'numeric', - pattern: '[0-9]*', - }} - id="outlined-number" - type="number" - label={mapDuration[popupDuration.type]} - InputLabelProps={{ - shrink: true, - }} - value={popupDuration.duration} - onChange={e => - setPopupDuration(prev => ({ - ...prev, - duration: Number(e.target.value), - })) - } - /> - )} - </FormControl> - </div> - </div> - - <h4>Image</h4> - <div> - <ImagePicker - imageURL={customPopup.image} - handleChange={handlePopupChange} - defaultIcon={defaultIcon} - /> - </div> + <div className="popupEndDate"> + <h4>Durée</h4> + <div> + <FormControl style={{ flexDirection: 'row', gap: '1rem' }}> + <NativeSelect + inputProps={{ + name: 'age', + id: 'uncontrolled-native', + }} + onChange={event => handleSelectChange(event)} + value={popupDuration.type} + > + {OPTIONS.map(option => ( + <option key={option.value} value={option.value}> + {option.label} + </option> + ))} + </NativeSelect> - <h4>Contenu</h4> - <div className="popupTitle"> + {popupDuration.type !== 'infinite' && ( <TextField - type="text" - placeholder="Titre de la popup" - fullWidth - label="Titre" - value={customPopup.title} - onChange={event => - handlePopupChange(event.target.value, 'title') + style={{ width: '6rem' }} + inputProps={{ + inputMode: 'numeric', + pattern: '[0-9]*', + }} + id="outlined-number" + type="number" + label={mapDuration[popupDuration.type]} + InputLabelProps={{ + shrink: true, + }} + value={popupDuration.duration} + onChange={e => + setPopupDuration(prev => ({ + ...prev, + duration: Number(e.target.value), + })) } /> - </div> + )} + </FormControl> + </div> + </div> - <div className="popupDescription"> - <CustomEditor - baseState={convertStringToEditorState( - customPopup.description - )} - handleChange={value => - handlePopupChange(value, 'description') - } - type="custom_popup" - /> - </div> - </div> + <h4>Image</h4> + <div> + <ImagePicker + imageURL={`/assets/ecogesture/${customPopup.image}.png`} + handleChange={handlePopupChange} + /> + </div> + + <h4>Contenu</h4> + <div className="popupTitle"> + <TextField + type="text" + placeholder="Titre de la popup" + fullWidth + label="Titre" + value={customPopup.title} + onChange={event => handlePopupChange(event.target.value, 'title')} + /> + </div> + + <div className="popupDescription"> + <CustomEditor + baseState={convertStringToEditorState(customPopup.description)} + handleChange={value => handlePopupChange(value, 'description')} + type="custom_popup" + /> + </div> + </div> - <div className="buttons"> - <Button variant="outlined" onClick={() => setRefreshData(true)}> - Annuler - </Button> - <Button disabled={!isPageValid()} onClick={handleSave}> - Sauvegarder - </Button> - </div> - </> - )} + <div className="buttons"> + <Button variant="outlined" onClick={() => setCustomPopup(initialData)}> + Annuler + </Button> + <Button onClick={handleSave} disabled={!user}> + Sauvegarder + </Button> </div> </> ) diff --git a/src/components/Popups/PartnersIssue.tsx b/src/components/Popups/PartnersIssue.tsx index 94828928542782d6773aeeaf47e8b729aadc3273..bb2dce8002754faa0d0094fcaee0461dadd0764b 100644 --- a/src/components/Popups/PartnersIssue.tsx +++ b/src/components/Popups/PartnersIssue.tsx @@ -7,6 +7,7 @@ import { useWhoAmI } from '../../hooks/useWhoAmI' import Loader from '../Loader/Loader' import { links } from '../Routes/Router' import './popups.scss' +import { PopupWrapper } from './Wrapper' export const PartnersIssue = () => { const { data: user } = useWhoAmI() @@ -40,85 +41,70 @@ export const PartnersIssue = () => { } } - return ( - <> - <div className="header"> - <h1>Création de Pop-up</h1> - </div> - <div className="content popups"> - <div className="partnersInfo"> - <h3>Affichage des pop-up de panne</h3> - <div> - {partnersIssue.isPending && <Loader />} - {partnersIssue.isError && <p>Erreur</p>} - {partnersIssue.data && ( - <div> - <p>Services concernés</p> - <FormGroup style={{ flexDirection: 'row' }}> - <FormControlLabel - label={'Panne Enedis'} - labelPlacement="top" - control={ - <Switch - disabled={isCustomPopupEnabled} - checked={partnersIssue.data.enedis_failure} - onChange={event => { - handlePartnerIssue( - event.target.checked, - 'enedis_failure' - ) - }} - /> - } - /> - <FormControlLabel - label={'Panne EGL'} - labelPlacement="top" - control={ - <Switch - disabled={isCustomPopupEnabled} - checked={partnersIssue.data.egl_failure} - onChange={event => { - handlePartnerIssue( - event.target.checked, - 'egl_failure' - ) - }} - /> - } - /> - <FormControlLabel - label={'Panne GRDF'} - labelPlacement="top" - control={ - <Switch - disabled={isCustomPopupEnabled} - checked={partnersIssue.data.grdf_failure} - onChange={event => { - handlePartnerIssue( - event.target.checked, - 'grdf_failure' - ) - }} - /> - } - /> - </FormGroup> + if (partnersIssue.isLoading) { + return <Loader /> + } + + if (partnersIssue.error) { + return <p>Une erreur est survenue</p> + } - {isCustomPopupEnabled && ( - <p className="singlePopupWarning"> - La pop-up maintenance des partenaires ne peut pas être - activée en même temps que la{' '} - <Link to={links.customPopup.path}> - pop-up personnalisée - </Link> - </p> - )} - </div> - )} - </div> - </div> - </div> - </> - ) + if (partnersIssue.data) { + return ( + <PopupWrapper> + <h3>Affichage des pop-up de panne</h3> + + <p>Services concernés</p> + <FormGroup style={{ flexDirection: 'row' }}> + <FormControlLabel + label={'Panne Enedis'} + labelPlacement="top" + control={ + <Switch + disabled={isCustomPopupEnabled} + checked={partnersIssue.data.enedis_failure} + onChange={event => { + handlePartnerIssue(event.target.checked, 'enedis_failure') + }} + /> + } + /> + <FormControlLabel + label={'Panne EGL'} + labelPlacement="top" + control={ + <Switch + disabled={isCustomPopupEnabled} + checked={partnersIssue.data.egl_failure} + onChange={event => { + handlePartnerIssue(event.target.checked, 'egl_failure') + }} + /> + } + /> + <FormControlLabel + label={'Panne GRDF'} + labelPlacement="top" + control={ + <Switch + disabled={isCustomPopupEnabled} + checked={partnersIssue.data.grdf_failure} + onChange={event => { + handlePartnerIssue(event.target.checked, 'grdf_failure') + }} + /> + } + /> + </FormGroup> + + {isCustomPopupEnabled && ( + <p className="singlePopupWarning"> + La pop-up maintenance des partenaires ne peut pas être activée en + même temps que la{' '} + <Link to={links.customPopup.path}>pop-up personnalisée</Link> + </p> + )} + </PopupWrapper> + ) + } } diff --git a/src/components/Popups/Wrapper.tsx b/src/components/Popups/Wrapper.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9dd4b001bff0df41ca07e36f2e57d8ec566a64ae --- /dev/null +++ b/src/components/Popups/Wrapper.tsx @@ -0,0 +1,13 @@ +import './popups.scss' + +export const PopupWrapper = ({ children }: { children: React.ReactNode }) => { + return ( + <> + <div className="header"> + <h1>Création de Pop-up</h1> + </div> + + <div className="content popups">{children}</div> + </> + ) +} diff --git a/src/components/Popups/popups.scss b/src/components/Popups/popups.scss index aca1d5f23288e6d5fadab720fb0b4bccfd335bec..bda6a9afaf4b9570e1e4cbdab14a0150fe0875d9 100644 --- a/src/components/Popups/popups.scss +++ b/src/components/Popups/popups.scss @@ -1,10 +1,6 @@ @import '../../styles/config/colors'; .popups { - .partnersInfo { - margin-bottom: 2rem; - } - p.endDate { color: $gold-dark; font-weight: bold; diff --git a/src/hooks/useCustomPopup.tsx b/src/hooks/useCustomPopup.tsx index 6bdc99da450ee21da65aa90ba5d9676e4a4c7b1d..70f88d6265a70ad39f5ffeef13fe455043139a65 100644 --- a/src/hooks/useCustomPopup.tsx +++ b/src/hooks/useCustomPopup.tsx @@ -1,12 +1,34 @@ -import { useQuery } from '@tanstack/react-query' -import { fetchCustomPopup } from '../API' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { AxiosRequestConfig } from 'axios' +import { fetchCustomPopup, putCustomPopup } from '../API' +import { ICustomPopup } from '../models/customPopup.model' import { queryKeys } from './query-keys' export const useCustomPopup = () => { + const queryClient = useQueryClient() + const customPopup = useQuery({ queryKey: [queryKeys.customPopup], queryFn: fetchCustomPopup, }) - return { customPopup } + const mutation = useMutation< + void, + unknown, + { customPopup: ICustomPopup; axiosHeaders: AxiosRequestConfig } + >({ + mutationFn: ({ customPopup, axiosHeaders }) => + putCustomPopup(customPopup, axiosHeaders), + onSuccess: () => + queryClient.invalidateQueries({ queryKey: [queryKeys.customPopup] }), + }) + + const saveCustomPopup = ( + customPopup: ICustomPopup, + axiosHeaders: AxiosRequestConfig + ) => { + mutation.mutate({ customPopup, axiosHeaders }) + } + + return { customPopup, saveCustomPopup } } diff --git a/src/hooks/usePartnersIssue.tsx b/src/hooks/usePartnersIssue.tsx index fd517c66fa6ea8e58c027c16bced4c456253c329..2989dd45c58a9977dac5fb4634883f7decb909f7 100644 --- a/src/hooks/usePartnersIssue.tsx +++ b/src/hooks/usePartnersIssue.tsx @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { AxiosRequestConfig } from 'axios' -import { fetchPartnersIssue, savePartnersInfo } from '../API' +import { fetchPartnersIssue, putPartnersInfo } from '../API' import { IPartnersInfo } from '../models/partnersInfo.model' import { queryKeys } from './query-keys' @@ -18,10 +18,9 @@ export const usePartnersIssue = () => { { partnersInfo: IPartnersInfo; axiosHeaders: AxiosRequestConfig } >({ mutationFn: ({ partnersInfo, axiosHeaders }) => - savePartnersInfo(partnersInfo, axiosHeaders), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [queryKeys.partnersInfo] }) - }, + putPartnersInfo(partnersInfo, axiosHeaders), + onSuccess: () => + queryClient.invalidateQueries({ queryKey: [queryKeys.partnersInfo] }), }) const savePartnersIssue = ( diff --git a/src/models/customPopup.model.ts b/src/models/customPopup.model.ts index 74127e5c6d63f2eeea2a30c21e81e3086885dab7..ecebc5f4239e4d4622d3eed179d775ce705aa080 100644 --- a/src/models/customPopup.model.ts +++ b/src/models/customPopup.model.ts @@ -10,5 +10,6 @@ export interface ICustomPopup { export interface PopupDuration { type: durationType + /** number of days or hours */ duration: number } diff --git a/src/services/customPopup.service.ts b/src/services/customPopup.service.ts deleted file mode 100644 index 2f95978c387594a76708ed5e3177a5bc57a22ec2..0000000000000000000000000000000000000000 --- a/src/services/customPopup.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import axios, { AxiosRequestConfig } from 'axios' -import { toast } from 'react-toastify' -import { ICustomPopup } from '../models/customPopup.model' - -export class CustomPopupService { - /** - * Save the customPopup info - * @param customPopup - * @param axiosHeaders - */ - public saveCustomPopup = async ( - customPopup: ICustomPopup, - axiosHeaders: AxiosRequestConfig - ): Promise<void> => { - try { - await axios.put( - `/api/animator/customPopup`, - { - ...customPopup, - }, - axiosHeaders - ) - toast.success('Pop-up personnalisée enregistrée !') - } catch (e) { - toast.error('Erreur lors de l’enregistrement de la pop-up personnalisée') - console.error(e) - } - } - - /** - * Gets the custom pop-up information - */ - public getCustomPopupInfo = async (): Promise<ICustomPopup | null> => { - try { - const { data } = await axios.get<ICustomPopup>(`/api/common/customPopup`) - return data - } catch (e) { - console.error('error', e) - return null - } - } -}