import { captureException } from '@sentry/react' import { FluidSlugType, FluidState, FluidType, KonnectorUpdate, Season, } from 'enums' import get from 'lodash/get' import { DateTime, Interval } from 'luxon' import { FluidStatus, Relation } from 'models' import challengeData from '../db/challengeEntity.json' /** Array of elec, water & gas */ export const allFluids = [FluidType.ELECTRICITY, FluidType.WATER, FluidType.GAS] export function getKonnectorSlug( fluidType: Exclude<FluidType, FluidType.MULTIFLUID> ) { switch (fluidType) { case FluidType.ELECTRICITY: return FluidSlugType.ELECTRICITY case FluidType.WATER: return FluidSlugType.WATER case FluidType.GAS: return FluidSlugType.GAS default: throw new Error('unknown fluidtype') } } /** * Return lowercase fluidtype * @example FluidType.ELECTRICITY => 'electricity' */ export function getFluidName(fluidType: FluidType) { return FluidType[fluidType].toLowerCase() } export const getFluidTypeTranslation = ( fluidType: Exclude<FluidType, FluidType.MULTIFLUID> ) => { switch (fluidType) { case FluidType.GAS: return 'de gaz' case FluidType.ELECTRICITY: return "d'électricité" case FluidType.WATER: return "d'eau" default: throw new Error('unexpected fluidtype') } } export const getPartnerKey = (fluidType: FluidType) => { switch (fluidType) { case FluidType.ELECTRICITY: return 'enedis' case FluidType.WATER: return 'egl' case FluidType.GAS: return 'grdf' default: throw new Error('unknown fluidtype') } } export function getKonnectorUpdateError(type: string) { switch (type.toUpperCase()) { case 'USER_ACTION_NEEDED.OAUTH_OUTDATED': case 'USER_ACTION_NEEDED.SCA_REQUIRED': return KonnectorUpdate.ERROR_UPDATE_OAUTH case 'LOGIN_FAILED': return KonnectorUpdate.LOGIN_FAILED default: return KonnectorUpdate.ERROR_UPDATE } } export function isKonnectorActive( fluidStatus: FluidStatus[], fluidType: FluidType ): boolean { if (fluidType === FluidType.MULTIFLUID) { if ( fluidStatus.filter( fluid => fluid.status === FluidState.NOT_CONNECTED || fluid.status === FluidState.KONNECTOR_NOT_FOUND ).length === 3 ) { return false } else { return true } } if ( fluidStatus[fluidType].status === FluidState.NOT_CONNECTED || fluidStatus[fluidType].status === FluidState.KONNECTOR_NOT_FOUND ) { return false } else return true } export function formatNumberValues( value: number | null, fluidStyle?: string, toBeCompared = false ) { if (value || value === 0) { const localeValue = value.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2, }) const noSpaceValue = parseInt(localeValue.replace(/\s/g, '')) if (toBeCompared) return noSpaceValue if (fluidStyle && noSpaceValue >= 1000) { const convertedValue = (noSpaceValue / 1000).toFixed(2).replace('.', ',') return convertedValue } else return localeValue } else { return '--,--' } } /** * Get one relation in doc * @param {object} doc - DocumentEntity * @param {string} relName - Name of the relation */ export function getRelationship<D>(doc: D, relName: string): Relation { return get(doc, `relationships.${relName}.data`, []) } /** * Get array of items in one relation in doc * @param {object} doc - DocumentEntity * @param {string} relName - Name of the relation */ export function getRelationshipHasMany<D>(doc: D, relName: string): Relation[] { return get(doc, `relationships.${relName}.data`, []) } /** * Import a svg file with format : id.svg */ export const importIconById = async ( id: string, pathType: string ): Promise<string | undefined> => { let importedChallengeIcon try { importedChallengeIcon = await import( /* webpackMode: "eager" */ `assets/icons/visu/${pathType}/${id}.svg` ) if (importedChallengeIcon) { return importedChallengeIcon.default } } catch (e) { console.error(`Could not import icon ${pathType}/${id}`) captureException(e) } } export const getMonthFullName = (month: number) => { const monthNames = [ 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre', ] as const if (month < 1 || month > 12) throw new Error('Invalid month') return monthNames[month - 1] } /** * Return month string according to month index * @variation Equivalent to date.monthLong * @param date - DateTime * @returns month in french */ export const getMonthName = (date: DateTime) => { const monthNames = [ 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre', ] as const return monthNames[date.month - 1] } /** * Return month string according to month index * @param date - DateTime * @returns "de" month in french */ export const getMonthNameWithPrep = (date: DateTime) => { const monthNames = [ 'de janvier', 'de février', 'de mars', `d’avril`, 'de mai', 'de juin', 'de juillet', `d’août`, 'de septembre', `d’octobre`, 'de novembre', 'de décembre', ] as const return monthNames[date.month - 1] } /** * Return season according to following rules * - Winter is : 1/11/XXXX => 1/3/XXXX * - Summer is : 1/6/XXXX => 1/9/XXXX * @returns Season */ export const getSeason = (): Season => { const currentDate: DateTime = DateTime.local().setZone('utc', { keepLocalTime: true, }) const currentYear: number = currentDate.year const winterStart: DateTime = DateTime.local(currentYear, 11, 1).setZone( 'utc', { keepLocalTime: true, } ) const winterEnd: DateTime = DateTime.local(currentYear + 1, 3, 1).setZone( 'utc', { keepLocalTime: true, } ) const summerStart: DateTime = DateTime.local(currentYear, 6, 1).setZone( 'utc', { keepLocalTime: true, } ) const summerEnd: DateTime = DateTime.local(currentYear, 9, 1).setZone('utc', { keepLocalTime: true, }) const summerInterval: Interval = Interval.fromDateTimes( summerStart, summerEnd ) const winterInterval: Interval = Interval.fromDateTimes( winterStart, winterEnd ) if (summerInterval.contains(currentDate)) { return Season.SUMMER } else if (winterInterval.contains(currentDate)) { return Season.WINTER } else { return Season.NONE } } /** * Returns the challenge title with line return ( \n ). The result is coming from challengeEntity.json * @param userChallengeId EXPLORATION001 * @returns Simone\nVEILLE */ export const getChallengeTitleWithLineReturn = (userChallengeId: string) => { for (const challenge of challengeData) { if (challenge._id === userChallengeId) { return challenge.title_line_return } } } /** * Returns today's date, example: 2022-09-28T00:00:00.000Z * @returns DateTime */ export const getTodayDate = () => DateTime.local() .setZone('utc', { keepLocalTime: true, }) .startOf('day') /** * Formats an array of strings into a list with commas and an "et" (and) before the last element. * @param {string[]} array - The array of strings to be formatted. * @returns {string} The formatted list string. * * If the array is empty, an empty string is returned. * If the array has only one element, that element is returned as is. * If the array has two elements, they are joined with " et " (and). * If the array has more than two elements, all but the last element are joined with commas, * and " et " (and) is placed before the last element. * @example * // Returns "pomme, banane et cerise" * formatListWithAnd(['pomme', 'banane', 'cerise']); */ export const formatListWithAnd = (array: string[]) => { if (array.length === 0) { return '' } else if (array.length === 1) { return array[0] } else if (array.length === 2) { return array.join(' et ') } else { const lastElement = array.pop() return array.join(', ') + ' et ' + lastElement } } export type OffPeakHours = { start: { hour: number; minute: number } end: { hour: number; minute: number } } /** * Check if a string is a valid off-peak hour format * @example * isValidOffPeakHours("6H15-14H15") => true * isValidOffPeakHours("68H78_12Hab") => false */ export const isValidOffPeakHours = (range: string) => { const offPeakHoursRegex = /^(0?\d|1\d|2[0-3])H[0-5]?\d-(0?\d|1\d|2[0-3])H[0-5]?\d$/ return offPeakHoursRegex.test(range) } /** * Parse the string representation of off-peak hours from Enedis to an array of time ranges object */ export const parseOffPeakHours = (timeString: string): OffPeakHours[] => { const timeRanges = timeString.split(';') if (!timeRanges.every(range => isValidOffPeakHours(range))) { console.error(`Error parsing time range "${timeString}"`) return [] } const intervals: OffPeakHours[] = [] for (const range of timeRanges) { const [startStr, endStr] = range.split('-') const startTime = DateTime.fromFormat(startStr, "H'H'mm") const endTime = DateTime.fromFormat(endStr, "H'H'mm") intervals.push({ start: { hour: startTime.hour, minute: startTime.minute }, end: { hour: endTime.hour, minute: endTime.minute }, }) } return intervals } /** * Format a number into a 2-digits string, padded with 0 * @example formatTwoDigits(5) returns "05" */ export const formatTwoDigits = (num: number): string => { return num.toString().padStart(2, '0') } /** * Format off-peak hours object into a human-readable string * @example formatOffPeakHours({ start: { hour: 2, minute: 0 }, end: { hour: 10, minute: 0 }}) returns "02H00-10H00" */ export const formatOffPeakHours = (offPeakHours: OffPeakHours): string => { const { start, end } = offPeakHours const startTime = `${formatTwoDigits(start.hour)}H${formatTwoDigits( start.minute )}` const endTime = `${formatTwoDigits(end.hour)}H${formatTwoDigits(end.minute)}` return `${startTime}-${endTime}` } /** * Split off-peak hours that cross midnight * @example The range "22H00-6H00" becomes "22H00-23H59" and "0H00-6H00" */ export const splitOffPeakHours = ( offPeakHours: OffPeakHours[] ): OffPeakHours[] => { return offPeakHours.reduce((acc: OffPeakHours[], offPeakHour) => { if (offPeakHour.start.hour > offPeakHour.end.hour) { acc.push({ start: { hour: offPeakHour.start.hour, minute: offPeakHour.start.minute, }, end: { hour: 23, minute: 59, }, }) acc.push({ start: { hour: 0, minute: 0, }, end: { hour: offPeakHour.end.hour, minute: offPeakHour.end.minute, }, }) } else { acc.push(offPeakHour) } return acc }, []) } export const roundToNearestHalfHour = ( hour: number, minute: number, isEnd: boolean ): { hour: number; minute: number } => { let roundedMinute = Math.round(minute / 30) * 30 // Round to the nearest half-hour let roundedHour = hour // If rounding to the next hour (except for midnight), adjust the hour and reset the minute if (roundedMinute === 60 && roundedHour !== 23) { roundedHour += 1 roundedMinute = 0 } // Don't round to midnight for the off-peak hours end, instead round to 23:59 if ( (roundedMinute === 60 && roundedHour === 23) || (roundedMinute === 0 && roundedHour === 0) ) { if (isEnd) { roundedHour = 23 roundedMinute = 59 } else { roundedHour = 0 roundedMinute = 0 } } return { hour: roundedHour, minute: roundedMinute } } /** * Round off-peak hours to the nearest half-hour * @example "6H50-14H50" becomes "7H00-15H00" */ export const roundOffPeakHours = ( offPeakHours: OffPeakHours[] ): OffPeakHours[] => { return offPeakHours.map(({ start, end }) => ({ start: roundToNearestHalfHour(start.hour, start.minute, false), end: roundToNearestHalfHour(end.hour, end.minute, true), })) }