Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • web-et-numerique/factory/llle_project/ecolyo
1 result
Show changes
Commits on Source (5)
Showing
with 707 additions and 43 deletions
......@@ -31,6 +31,12 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/default-param-last': 'error',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
// causes a build error and has a lot of effects on components
'@typescript-eslint/prefer-nullish-coalescing': 'off',
......
<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>
`;
......@@ -112,6 +112,8 @@ const FluidChart = ({ fluidType }: { fluidType: FluidType }) => {
case FluidType.WATER:
dispatch(setShowOfflineData(false))
break
default:
throw new Error('Unexpected fluid type')
}
}
......
......@@ -18,16 +18,21 @@ const Loader = ({ color = 'gold', fluidType, text }: LoaderProps) => {
const { t } = useI18n()
let variant = color
switch (fluidType) {
case FluidType.ELECTRICITY:
variant = 'elec'
break
case FluidType.GAS:
variant = 'gaz'
break
case FluidType.WATER:
variant = 'water'
break
if (fluidType !== undefined) {
switch (fluidType) {
case FluidType.ELECTRICITY:
variant = 'elec'
break
case FluidType.GAS:
variant = 'gaz'
break
case FluidType.WATER:
variant = 'water'
break
case FluidType.MULTIFLUID:
variant = 'gold'
break
}
}
return (
......
......@@ -7,6 +7,7 @@ import StyledIconButton from 'components/CommonKit/IconButton/StyledIconButton'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
import { FluidType } from 'enums'
import React from 'react'
import { getFluidLabel } from 'utils/utils'
import './partnerIssueModal.scss'
interface PartnerIssueModalProps {
......@@ -22,17 +23,6 @@ const PartnerIssueModal = ({
}: PartnerIssueModalProps) => {
const { t } = useI18n()
const getFluidTypeLabel = () => {
switch (issuedFluid) {
case FluidType.ELECTRICITY:
return 'elec'
case FluidType.WATER:
return 'water'
case FluidType.GAS:
return 'gaz'
}
}
return (
<Dialog
open={open}
......@@ -66,7 +56,9 @@ const PartnerIssueModal = ({
className="partner-issue-content text-16-normal"
dangerouslySetInnerHTML={{
__html: t(
`consumption.partner_issue_modal.error_connect_${getFluidTypeLabel()}`
`consumption.partner_issue_modal.error_connect_${getFluidLabel(
issuedFluid
)}`
),
}}
/>
......
......@@ -17,7 +17,8 @@
"hasReplacedHeater": "unknown",
"warmingFluid": 0,
"hotWaterFluid": 0,
"cookingFluid": 0
"cookingFluid": 0,
"equipments": []
}
}
]
......@@ -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",
......
......@@ -167,6 +167,8 @@ export async function migrate(
}
switch (result.type) {
case MIGRATION_RESULT_FAILED:
throw new Error('Migration failed')
case MIGRATION_RESULT_NOOP:
case MIGRATION_RESULT_COMPLETE:
await updateSchemaVersion(_client, migration.targetSchemaVersion)
......
import { QueryResult } from 'cozy-client'
import profileTypeDataJson from 'db/profileTypeData.json'
import { DateTime } from 'luxon'
import { ProfileType } from 'models'
import mockClient from 'tests/__mocks__/client.mock'
import { profileTypeData } from 'tests/__mocks__/profileType.mock'
......@@ -8,32 +10,53 @@ describe('UserProfileTypeEntity service', () => {
const pteService = new ProfileTypeEntityService(mockClient)
describe('getUserProfileType', () => {
const mockProfileQueryResult: QueryResult<ProfileType[]> = {
data: [profileTypeData],
bookmark: '',
next: false,
skip: 0,
}
const mockEmptyQueryResult: QueryResult<ProfileType[]> = {
data: [],
bookmark: '',
next: false,
skip: 0,
}
it('should return the closest profileType according to passed argument updateDate', async () => {
mockClient.query.mockResolvedValueOnce(mockProfileQueryResult)
const result = await pteService.getProfileType(DateTime.local(2022, 1, 1))
expect(result).toEqual(profileTypeData)
})
it('should not find a profile corresponding to the passed argument updateDate', async () => {
mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult)
mockClient.query.mockResolvedValueOnce(mockProfileQueryResult)
const result = await pteService.getProfileType(DateTime.local(2022, 1, 1))
expect(result).toEqual(profileTypeData)
})
it('should return default profileType if no profile found when date is given', async () => {
const date = DateTime.local(2022, 1, 1)
const defaultProfileType = {
...profileTypeDataJson[0].profileType,
updateDate: date,
} as ProfileType
mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult)
mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult)
const result = await pteService.getProfileType(date)
expect(result).toEqual(defaultProfileType)
})
it('should return the last profileType in base', async () => {
const mockQueryResult: QueryResult<ProfileType[]> = {
data: [profileTypeData],
bookmark: '',
next: false,
skip: 0,
}
mockClient.query.mockResolvedValueOnce(mockQueryResult)
mockClient.query.mockResolvedValueOnce(mockProfileQueryResult)
const result = await pteService.getProfileType()
expect(result).toEqual(profileTypeData)
})
// it('should return the closest profileType according to passed argument updateDate', async () => {})
it('should return null if no user profile found', async () => {
const mockQueryResult: QueryResult<ProfileType[]> = {
data: [],
bookmark: '',
next: false,
skip: 0,
}
mockClient.query.mockResolvedValueOnce(mockQueryResult)
mockClient.query.mockResolvedValueOnce(mockEmptyQueryResult)
const result = await pteService.getProfileType()
expect(result).toBeNull()
})
})
// describe('updateUserProfileType', () => {})
})