Skip to content
Snippets Groups Projects
Commit 451c98bc authored by Bastien DUMONT's avatar Bastien DUMONT :angel:
Browse files

Reapply "feat(water): solidarity pricing"

This reverts commit fca2f831.
parent 1fca8231
No related branches found
No related tags found
No related merge requests found
Showing
with 635 additions and 2 deletions
<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
<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
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'
......
......@@ -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: {
......
......@@ -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 />}
......
@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;
}
}
}
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()
})
})
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}</span>
<span className="limit180">{MAX_REGULAR}</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>
)
}
@import 'src/styles/base/color';
.waterPricingModal {
text-align: center;
h1 {
color: $water-color;
}
p {
color: $grey-bright;
}
}
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>
)
}
// 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
</span>
<span
class="limit180"
>
180
</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>
`;
......@@ -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",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment