From 451c98bc5c1f25bc5439c51d297774e899a17c6e Mon Sep 17 00:00:00 2001 From: Bastien DUMONT <bdumont@grandlyon.com> Date: Tue, 8 Oct 2024 11:23:57 +0200 Subject: [PATCH] Reapply "feat(water): solidarity pricing" This reverts commit fca2f831e5b8af04120ad360b26e4fd703682091. --- src/assets/icons/ico/euro-crossed.svg | 7 + .../ico/{euro-icon.svg => euro-gold.svg} | 0 src/assets/icons/ico/euro.svg | 6 + .../ProfileComparatorRow.tsx | 2 +- .../Consumption/ConsumptionView.spec.tsx | 3 + .../Consumption/ConsumptionView.tsx | 4 + .../WaterPricing/WaterPricing.scss | 192 ++++++++++++++++++ .../WaterPricing/WaterPricing.spec.tsx | 29 +++ .../Consumption/WaterPricing/WaterPricing.tsx | 164 +++++++++++++++ .../WaterPricing/WaterPricingModal.scss | 13 ++ .../WaterPricing/WaterPricingModal.tsx | 49 +++++ .../__snapshots__/WaterPricing.spec.tsx.snap | 153 ++++++++++++++ src/locales/fr.json | 15 +- 13 files changed, 635 insertions(+), 2 deletions(-) create mode 100644 src/assets/icons/ico/euro-crossed.svg rename src/assets/icons/ico/{euro-icon.svg => euro-gold.svg} (100%) create mode 100644 src/assets/icons/ico/euro.svg create mode 100644 src/components/Consumption/WaterPricing/WaterPricing.scss create mode 100644 src/components/Consumption/WaterPricing/WaterPricing.spec.tsx create mode 100644 src/components/Consumption/WaterPricing/WaterPricing.tsx create mode 100644 src/components/Consumption/WaterPricing/WaterPricingModal.scss create mode 100644 src/components/Consumption/WaterPricing/WaterPricingModal.tsx create mode 100644 src/components/Consumption/WaterPricing/__snapshots__/WaterPricing.spec.tsx.snap diff --git a/src/assets/icons/ico/euro-crossed.svg b/src/assets/icons/ico/euro-crossed.svg new file mode 100644 index 000000000..1118c616a --- /dev/null +++ b/src/assets/icons/ico/euro-crossed.svg @@ -0,0 +1,7 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect x="0.5" y="0.5" width="11" height="11" rx="5.5" stroke="#1B1C22" /> + <path d="M9.5 8.5L4 3" stroke="#1B1C22" stroke-linecap="round" /> + <path fill-rule="evenodd" clip-rule="evenodd" + d="M4.29445 4.70867C4.2492 4.80317 4.20873 4.90039 4.17333 5H3.33333C3.15 5 3 5.15 3 5.33333C3 5.51667 3.15 5.66667 3.33333 5.66667H4.02C4.00667 5.77667 4 5.88667 4 6C4 6.11333 4.00667 6.22333 4.02 6.33333H3.33333C3.15 6.33333 3 6.48333 3 6.66667C3 6.85 3.15 7 3.33333 7H4.17333C4.58667 8.16333 5.69333 9 7 9C7.46345 9 7.90137 8.89571 8.29333 8.70755L7.65291 8.06712C7.44768 8.13205 7.22878 8.16667 7 8.16667C6.16333 8.16667 5.44 7.69333 5.08 7H6.58579L5.91912 6.33333H4.86C4.84333 6.22333 4.83333 6.11333 4.83333 6C4.83333 5.88667 4.84333 5.77667 4.86 5.66667H5.25245L4.29445 4.70867ZM6.34784 3.93363C6.5538 3.86847 6.77294 3.83333 7 3.83333C7.41667 3.83333 7.80667 3.95333 8.14 4.15667C8.30667 4.26 8.52333 4.24333 8.66333 4.10333C8.85667 3.91 8.81333 3.59333 8.58 3.45C8.12 3.16667 7.57667 3 7 3C6.53657 3 6.0983 3.10524 5.70719 3.29298L6.34784 3.93363Z" + fill="#1B1C22" /> +</svg> \ No newline at end of file diff --git a/src/assets/icons/ico/euro-icon.svg b/src/assets/icons/ico/euro-gold.svg similarity index 100% rename from src/assets/icons/ico/euro-icon.svg rename to src/assets/icons/ico/euro-gold.svg diff --git a/src/assets/icons/ico/euro.svg b/src/assets/icons/ico/euro.svg new file mode 100644 index 000000000..e09b58243 --- /dev/null +++ b/src/assets/icons/ico/euro.svg @@ -0,0 +1,6 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect x="0.5" y="0.5" width="11" height="11" rx="5.5" stroke="currentColor" /> + <path + d="M7 8.16667C6.16333 8.16667 5.44 7.69333 5.08 7H6.66667C6.85 7 7 6.85 7 6.66667C7 6.48333 6.85 6.33333 6.66667 6.33333H4.86C4.84333 6.22333 4.83333 6.11333 4.83333 6C4.83333 5.88667 4.84333 5.77667 4.86 5.66667H6.66667C6.85 5.66667 7 5.51667 7 5.33333C7 5.15 6.85 5 6.66667 5H5.08C5.44 4.30667 6.16667 3.83333 7 3.83333C7.41667 3.83333 7.80667 3.95333 8.14 4.15667C8.30667 4.26 8.52333 4.24333 8.66333 4.10333C8.85667 3.91 8.81333 3.59333 8.58 3.45C8.12 3.16667 7.57667 3 7 3C5.69333 3 4.58667 3.83667 4.17333 5H3.33333C3.15 5 3 5.15 3 5.33333C3 5.51667 3.15 5.66667 3.33333 5.66667H4.02C4.00667 5.77667 4 5.88667 4 6C4 6.11333 4.00667 6.22333 4.02 6.33333H3.33333C3.15 6.33333 3 6.48333 3 6.66667C3 6.85 3.15 7 3.33333 7H4.17333C4.58667 8.16333 5.69333 9 7 9C7.58 9 8.12 8.83667 8.58 8.55C8.81 8.40667 8.85333 8.08667 8.66 7.89333C8.52 7.75333 8.30333 7.73667 8.13667 7.84333C7.80667 8.05 7.42 8.16667 7 8.16667Z" + fill="currentColor" /> +</svg> \ No newline at end of file diff --git a/src/components/Analysis/ProfileComparator/ProfileComparatorRow.tsx b/src/components/Analysis/ProfileComparator/ProfileComparatorRow.tsx index a4bc59be3..7b47c580b 100644 --- a/src/components/Analysis/ProfileComparator/ProfileComparatorRow.tsx +++ b/src/components/Analysis/ProfileComparator/ProfileComparatorRow.tsx @@ -1,4 +1,4 @@ -import EuroIcon from 'assets/icons/ico/euro-icon.svg' +import EuroIcon from 'assets/icons/ico/euro-gold.svg' import classNames from 'classnames' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' diff --git a/src/components/Consumption/ConsumptionView.spec.tsx b/src/components/Consumption/ConsumptionView.spec.tsx index df06f2717..ba2d74b7e 100644 --- a/src/components/Consumption/ConsumptionView.spec.tsx +++ b/src/components/Consumption/ConsumptionView.spec.tsx @@ -56,6 +56,9 @@ mockFluidStatus[FluidType.ELECTRICITY].status = FluidState.DONE const mockChartStateShowOffline = { ...mockChartState, showOfflineData: true } describe('ConsumptionView component', () => { + beforeEach(() => { + jest.clearAllMocks() + }) it('should be rendered correctly', async () => { const store = createMockEcolyoStore({ chart: { diff --git a/src/components/Consumption/ConsumptionView.tsx b/src/components/Consumption/ConsumptionView.tsx index 816cbc96c..3e3cbe47d 100644 --- a/src/components/Consumption/ConsumptionView.tsx +++ b/src/components/Consumption/ConsumptionView.tsx @@ -33,6 +33,7 @@ import { } from 'utils/utils' import ConsumptionDetails from './ConsumptionDetails/ConsumptionDetails' import FluidButtons from './FluidButtons/FluidButtons' +import { WaterPricing } from './WaterPricing/WaterPricing' /** * http://ecolyo.cozy.tools:8080/#/consumption @@ -210,6 +211,9 @@ const ConsumptionView = ({ fluidType }: { fluidType: FluidType }) => { <ConsumptionDetails fluidType={fluidType} /> </> )} + + {fluidType === FluidType.WATER && showOfflineData && <WaterPricing />} + {!isMulti && <KonnectorViewerCard fluidType={fluidType} />} {isMulti && !showOfflineData && <KonnectorViewerList />} diff --git a/src/components/Consumption/WaterPricing/WaterPricing.scss b/src/components/Consumption/WaterPricing/WaterPricing.scss new file mode 100644 index 000000000..769c7e523 --- /dev/null +++ b/src/components/Consumption/WaterPricing/WaterPricing.scss @@ -0,0 +1,192 @@ +@import 'src/styles/base/color'; +@import 'src/styles/base/breakpoint'; + +$price-free: #99cfff; +$price-regular: #3a98ec; +$price-double: #3793ff; +$price-background: #383941; + +.pricing-root { + margin: 0 auto; + margin-bottom: 1rem; + max-width: 45.75rem; + width: 100%; + box-sizing: border-box; + @media #{$large-phone} { + padding-left: 1rem; + padding-right: 1rem; + } + + .pricing-container { + background: $grey-linear-gradient-background; + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 16px; + padding: 1rem; + p { + margin: 0; + } + + .row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + .year { + color: $white; + } + } + + .gauges { + display: grid; + grid-template-columns: 1fr auto 3fr auto 1fr; + gap: 4px; + + .separator { + border-right: 1px solid $soft-grey; + height: 130%; + } + } + + .limit-container { + margin-top: 4px; + display: grid; + grid-template-columns: 1fr auto 3fr auto 1fr; + span { + color: $soft-grey; + } + .limit12 { + grid-column: 2; + } + .limit180 { + grid-column: 4; + } + } + + // Colors + .gauge-container.free { + .gauge-border { + border-color: $price-free; + background-image: linear-gradient( + 45deg, + $price-free 11.11%, + $price-background 11.11%, + $price-background 50%, + $price-free 50%, + $price-free 61.11%, + $price-background 61.11%, + $price-background 100% + ); + } + .gauge-content { + background-color: $price-free; + } + } + .gauge-container.regular { + .gauge-border { + border-radius: 0; + border-color: $price-regular; + background-image: linear-gradient( + 45deg, + $price-regular 11.11%, + $price-background 11.11%, + $price-background 50%, + $price-regular 50%, + $price-regular 61.11%, + $price-background 61.11%, + $price-background 100% + ); + } + .gauge-content { + background-color: $price-regular; + } + } + + .gauge-container.double { + .gauge-border { + border-radius: 0 20px 20px 0; + border-color: $price-double; + background-image: linear-gradient( + 45deg, + $price-double 11.11%, + $price-background 11.11%, + $price-background 50%, + $price-double 50%, + $price-double 61.11%, + $price-background 61.11%, + $price-background 100% + ); + } + .gauge-content { + background-color: $price-double; + } + } + + .gauge-container.no-color { + .gauge-border { + border-color: transparent; + background-color: $price-background; + background-image: none; + } + .gauge-content { + background-color: transparent; + } + } + + .gauge-container { + .gauge-border { + height: 16px; + box-sizing: border-box; + border: 1px solid; + background-size: 9px 9px; + border-radius: 20px 0 0 20px; + overflow: hidden; + position: relative; + .gauge-content { + position: absolute; + transition: all 0.5s ease; + height: 17px; + width: 100%; + &.rounded { + border-radius: 0 20px 20px 0; + } + } + } + } + + .iconFree, + .iconRegular, + .iconDouble { + z-index: 10; + position: absolute; + top: 1px; + left: 1px; + &.filled { + color: #1b1c22; + } + } + .iconDouble:nth-of-type(2) { + left: 15px; + } + + .pricing { + margin-bottom: 4px; + &.free { + color: $price-free; + } + &.regular { + color: $price-regular; + } + &.double { + color: $price-double; + } + } + + .consumption span { + color: $white; + font-weight: 700; + } + } +} diff --git a/src/components/Consumption/WaterPricing/WaterPricing.spec.tsx b/src/components/Consumption/WaterPricing/WaterPricing.spec.tsx new file mode 100644 index 000000000..14ac6b98f --- /dev/null +++ b/src/components/Consumption/WaterPricing/WaterPricing.spec.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import React from 'react' +import { Provider } from 'react-redux' +import { createMockEcolyoStore } from 'tests/__mocks__/store' +import { WaterPricing } from './WaterPricing' + +const store = createMockEcolyoStore() + +describe('WaterPricing component', () => { + it('should be rendered correctly', () => { + const { container } = render( + <Provider store={store}> + <WaterPricing /> + </Provider> + ) + expect(container).toMatchSnapshot() + }) + + it('should open modal when click on button', async () => { + render( + <Provider store={store}> + <WaterPricing /> + </Provider> + ) + userEvent.click(screen.getByRole('button')) + expect(await screen.findByRole('dialog')).toBeInTheDocument() + }) +}) diff --git a/src/components/Consumption/WaterPricing/WaterPricing.tsx b/src/components/Consumption/WaterPricing/WaterPricing.tsx new file mode 100644 index 000000000..91107bbfc --- /dev/null +++ b/src/components/Consumption/WaterPricing/WaterPricing.tsx @@ -0,0 +1,164 @@ +import { Button } from '@material-ui/core' +import euroCrossedIcon from 'assets/icons/ico/euro-crossed.svg' +import euroIcon from 'assets/icons/ico/euro.svg' +import classNames from 'classnames' +import StyledIcon from 'components/CommonKit/Icon/StyledIcon' +import { useClient } from 'cozy-client' +import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' +import { FluidType, TimeStep } from 'enums' +import { DateTime } from 'luxon' +import React, { useEffect, useState } from 'react' +import ConsumptionService from 'services/consumption.service' +import { useAppSelector } from 'store/hooks' +import './WaterPricing.scss' +import { WaterPricingModal } from './WaterPricingModal' + +// In m³ +const MAX_FREE = 12 +const MAX_REGULAR = 180 + +export const WaterPricing = () => { + const { t } = useI18n() + const client = useClient() + const { selectedDate } = useAppSelector(state => state.ecolyo.chart) + const [showModal, setShowModal] = useState(false) + const [consumption, setConsumption] = useState(0) + + const pricing = + consumption > 180 ? 'double' : consumption > 12 ? 'regular' : 'free' + const isFreeExceeded = consumption >= MAX_FREE + const isRegularExceeded = consumption >= MAX_REGULAR + + const freePercentage = Math.min((consumption / MAX_FREE) * 100, 100) + // threshold of 30% to display icon + const freeWithThreshold = Math.max(30, freePercentage) + + const regularPercentage = Math.min((consumption / MAX_REGULAR) * 100, 100) + const regularWithThreshold = Math.max(10, regularPercentage) + + const year = Number(selectedDate.toFormat('y')) + + useEffect(() => { + async function fetchData() { + const cs = new ConsumptionService(client) + const startDate = DateTime.local(year, 1, 1) + const endDate = DateTime.local(year, 12, 31) + + const dataLoad = await cs.getGraphData({ + fluidTypes: [FluidType.WATER], + timeStep: TimeStep.YEAR, + timePeriod: { startDate, endDate }, + }) + + if (!dataLoad?.actualData) return null + + const rounded = Math.ceil(dataLoad.actualData[0].value / 100) / 10 + setConsumption(rounded) + } + fetchData() + }, [client, year]) + + return ( + <div className="pricing-root"> + <div className="pricing-container"> + <div className="row"> + <span className="year text-16-bold"> + {t('consumption.water_pricing.year', { year })} + </span> + <Button className="btnText" onClick={() => setShowModal(true)}> + {t('consumption.water_pricing.more')} + </Button> + </div> + + <div> + <div className="gauges"> + <div className="gauge-container free"> + <div className="gauge-border"> + <StyledIcon + className="iconFree" + icon={euroCrossedIcon} + size={12} + /> + <div + className={classNames('gauge-content', { + rounded: !isFreeExceeded, + })} + style={{ right: `${100 - freeWithThreshold}%` }} + /> + </div> + </div> + + <div className="separator" /> + + <div + className={classNames('gauge-container regular', { + 'no-color': !isFreeExceeded, + })} + > + <div className="gauge-border"> + <StyledIcon + className={`iconRegular ${!isFreeExceeded ? '' : 'filled'}`} + icon={euroIcon} + size={12} + /> + <div + className={classNames('gauge-content', { + rounded: !isRegularExceeded, + })} + style={{ right: `${100 - regularWithThreshold}%` }} + /> + </div> + </div> + + <div className="separator" /> + + <div + className={classNames('gauge-container double', { + 'no-color': !isRegularExceeded, + })} + > + <div className="gauge-border"> + <StyledIcon + className={`iconDouble ${!isRegularExceeded ? '' : 'filled'}`} + icon={euroIcon} + size={12} + /> + <StyledIcon + className={`iconDouble ${!isRegularExceeded ? '' : 'filled'}`} + icon={euroIcon} + size={12} + /> + <div + className="gauge-content rounded" + style={{ right: `${40}%` }} + /> + </div> + </div> + </div> + + <div className="limit-container"> + <span className="limit12">{MAX_FREE}m³</span> + <span className="limit180">{MAX_REGULAR}m³</span> + </div> + </div> + + <div> + <p className={`pricing ${pricing} text-14`}> + {t(`consumption.water_pricing.${pricing}`)} + </p> + <p + className="consumption text-14" + dangerouslySetInnerHTML={{ + __html: t('consumption.water_pricing.consumption', { + consumption, + }), + }} + /> + </div> + {showModal && ( + <WaterPricingModal handleCloseClick={() => setShowModal(false)} /> + )} + </div> + </div> + ) +} diff --git a/src/components/Consumption/WaterPricing/WaterPricingModal.scss b/src/components/Consumption/WaterPricing/WaterPricingModal.scss new file mode 100644 index 000000000..71c4d0c34 --- /dev/null +++ b/src/components/Consumption/WaterPricing/WaterPricingModal.scss @@ -0,0 +1,13 @@ +@import 'src/styles/base/color'; + +.waterPricingModal { + text-align: center; + + h1 { + color: $water-color; + } + + p { + color: $grey-bright; + } +} diff --git a/src/components/Consumption/WaterPricing/WaterPricingModal.tsx b/src/components/Consumption/WaterPricing/WaterPricingModal.tsx new file mode 100644 index 000000000..1caad1470 --- /dev/null +++ b/src/components/Consumption/WaterPricing/WaterPricingModal.tsx @@ -0,0 +1,49 @@ +import { Dialog } from '@material-ui/core' +import Button from '@material-ui/core/Button' +import CloseIcon from 'assets/icons/ico/close.svg' +import StyledIconButton from 'components/CommonKit/IconButton/StyledIconButton' +import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' +import React from 'react' +import './WaterPricingModal.scss' + +export const WaterPricingModal = ({ + handleCloseClick, +}: { + handleCloseClick: () => void +}) => { + const { t } = useI18n() + return ( + <Dialog + open={true} + disableEscapeKeyDown + onClose={(event, reason): void => { + event && reason !== 'backdropClick' && handleCloseClick() + }} + classes={{ + root: 'modal-root', + paper: 'modal-paper', + }} + > + <StyledIconButton + icon={CloseIcon} + onClick={handleCloseClick} + aria-label={t('feedback.accessibility.button_close')} + className="modal-paper-close-button" + /> + <div className="waterPricingModal"> + <h1 className="text-20-bold"> + {t('consumption.water_pricing.modal.title')} + </h1> + <p + dangerouslySetInnerHTML={{ + __html: t('consumption.water_pricing.modal.details'), + }} + /> + + <Button onClick={handleCloseClick} className="btnPrimary"> + {t('consumption.water_pricing.modal.understood')} + </Button> + </div> + </Dialog> + ) +} diff --git a/src/components/Consumption/WaterPricing/__snapshots__/WaterPricing.spec.tsx.snap b/src/components/Consumption/WaterPricing/__snapshots__/WaterPricing.spec.tsx.snap new file mode 100644 index 000000000..d36d9c0a7 --- /dev/null +++ b/src/components/Consumption/WaterPricing/__snapshots__/WaterPricing.spec.tsx.snap @@ -0,0 +1,153 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WaterPricing component should be rendered correctly 1`] = ` +<div> + <div + class="pricing-root" + > + <div + class="pricing-container" + > + <div + class="row" + > + <span + class="year text-16-bold" + > + consumption.water_pricing.year + </span> + <button + class="MuiButtonBase-root MuiButton-root MuiButton-text btnText" + tabindex="0" + type="button" + > + <span + class="MuiButton-label" + > + consumption.water_pricing.more + </span> + <span + class="MuiTouchRipple-root" + /> + </button> + </div> + <div> + <div + class="gauges" + > + <div + class="gauge-container free" + > + <div + class="gauge-border" + > + <svg + aria-hidden="true" + class="iconFree styles__icon___23x3R" + height="12" + width="12" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <div + class="gauge-content rounded" + style="right: 70%;" + /> + </div> + </div> + <div + class="separator" + /> + <div + class="gauge-container regular no-color" + > + <div + class="gauge-border" + > + <svg + aria-hidden="true" + class="iconRegular styles__icon___23x3R" + height="12" + width="12" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <div + class="gauge-content rounded" + style="right: 90%;" + /> + </div> + </div> + <div + class="separator" + /> + <div + class="gauge-container double no-color" + > + <div + class="gauge-border" + > + <svg + aria-hidden="true" + class="iconDouble styles__icon___23x3R" + height="12" + width="12" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <svg + aria-hidden="true" + class="iconDouble styles__icon___23x3R" + height="12" + width="12" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <div + class="gauge-content rounded" + style="right: 40%;" + /> + </div> + </div> + </div> + <div + class="limit-container" + > + <span + class="limit12" + > + 12 + m³ + </span> + <span + class="limit180" + > + 180 + m³ + </span> + </div> + </div> + <div> + <p + class="pricing free text-14" + > + consumption.water_pricing.free + </p> + <p + class="consumption text-14" + > + consumption.water_pricing.consumption + </p> + </div> + </div> + </div> +</div> +`; diff --git a/src/locales/fr.json b/src/locales/fr.json index 3339a628e..5748f6503 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -368,7 +368,20 @@ "additional_text": "La visualisation et/ou la connexion à vos données de consommation peut s'en trouver affectée.<br /><br /><i>Merci pour votre patience en attendant un retour à la normale :)</i>", "ok": "Ok" }, - "compared": "Comparé" + "compared": "Comparé", + "water_pricing": { + "year": "Année %{year}", + "consumption": "Consommation : <span>%{consumption}m³</span>", + "free": "Gratuit", + "regular": "Tarif normal", + "double": "Tarif double", + "more": "En savoir plus", + "modal": { + "title": "A partir du 1er janvier 2025, une tarification solidaire et environnementale de l’eau est mise en place.", + "details": "Cette jauge vous permet de garder un œil tout au long de l’année sur votre consommation d’eau afin de voir dans quelle tranche vous vous situez en tant que particulier.<br><br> Cette information vous est donnée à titre informatif, l'application définitive des tranches sera assurée par Eau Publique du Grand Lyon sur vos factures d'eau à compter du 01/01/2025 en fonction de vos consommations.", + "understood": "J'ai compris" + } + } }, "consumption_details": { "detail": "Détail par fluide", -- GitLab