@widergy/utilitygo-smart-bill-mobile 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -1
  3. package/src/lib/components/SmartBillSummary/constants.js +2 -4
  4. package/src/lib/components/SmartBillSummary/index.js +20 -4
  5. package/src/lib/components/SmartBillSummary/tabs/Billing/components/DebtStatusLabel/constants.js +3 -0
  6. package/src/lib/components/SmartBillSummary/tabs/Billing/components/DebtStatusLabel/index.js +33 -0
  7. package/src/lib/components/SmartBillSummary/tabs/Billing/components/DebtStatusLabel/styles.js +20 -0
  8. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/components/ProgressBar/components/LinearProgress/index.js +57 -0
  9. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/components/ProgressBar/components/LinearProgress/styles.js +22 -0
  10. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/components/ProgressBar/constants.js +3 -0
  11. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/components/ProgressBar/index.js +163 -0
  12. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/components/ProgressBar/styles.js +75 -0
  13. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/components/ProgressBar/utils.js +89 -0
  14. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/index.js +163 -0
  15. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/styles.js +62 -0
  16. package/src/lib/components/SmartBillSummary/tabs/Billing/components/RateCard/utils.js +5 -0
  17. package/src/lib/components/SmartBillSummary/tabs/Billing/index.js +271 -0
  18. package/src/lib/components/SmartBillSummary/tabs/Billing/styles.js +73 -0
  19. package/src/lib/components/SmartBillSummary/tabs/Billing/utils.js +7 -0
  20. package/src/lib/components/SmartBillSummary/tabs/Consumptions/components/CurrentConsumption/components/ConsumptionComparison/utils.js +1 -1
  21. package/src/lib/components/SmartBillSummary/tabs/Consumptions/components/CurrentConsumption/utils.js +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.2.0](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/compare/v2.1.0...v2.2.0) (2025-04-21)
2
+
3
+
4
+ ### Features
5
+
6
+ * [CX-376] billing tab smartbill ([#31](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/issues/31)) ([37bf8d3](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/commit/37bf8d3dca5d23375e22e50997826b2f559a236e))
7
+
1
8
  # [2.1.0](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/compare/v2.0.0...v2.1.0) (2025-04-07)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widergy/utilitygo-smart-bill-mobile",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "UtilityGO SmartBill Mobile",
5
5
  "license": "MIT",
6
6
  "main": "src/lib/index.js",
@@ -74,6 +74,7 @@
74
74
  "react-native-collapsible": "^1.6.1",
75
75
  "react-native-document-picker": "^9.0.1",
76
76
  "react-native-image-picker": "^5.0.0",
77
+ "react-native-linear-gradient": "^2.8.3",
77
78
  "@bam.tech/react-native-image-resizer": "^3.0.7",
78
79
  "react-native-svg": "^13.0.0",
79
80
  "react-native-vector-icons": "^10.0.0",
@@ -5,6 +5,7 @@ import { UTLabel } from '@widergy/mobile-ui';
5
5
  import { SMARTBILL_TABS } from '../../constants/tabs';
6
6
 
7
7
  import Consumptions from './tabs/Consumptions';
8
+ import Billing from './tabs/Billing';
8
9
 
9
10
  export const BIMESTRAL_PERIODICITY = 'BIMESTRAL';
10
11
 
@@ -15,10 +16,7 @@ const renderComponentWithProps = (Component, tab) => props => (
15
16
 
16
17
  export const TAB_COMPONENT_MAPPER = {
17
18
  [SMARTBILL_TABS.CONSUMPTIONS]: renderComponentWithProps(Consumptions, SMARTBILL_TABS.CONSUMPTIONS),
18
- [SMARTBILL_TABS.BILLING]: renderComponentWithProps(
19
- () => <UTLabel>Facturación</UTLabel>,
20
- SMARTBILL_TABS.BILLING
21
- ),
19
+ [SMARTBILL_TABS.BILLING]: renderComponentWithProps(Billing, SMARTBILL_TABS.BILLING),
22
20
  [SMARTBILL_TABS.GLOSSARY]: renderComponentWithProps(
23
21
  () => <UTLabel>Glosario</UTLabel>,
24
22
  SMARTBILL_TABS.GLOSSARY
@@ -2,13 +2,23 @@
2
2
  import React, { useEffect, useState } from 'react';
3
3
  import { UTLoading, UTTabs } from '@widergy/mobile-ui';
4
4
  import { View } from 'react-native';
5
- import { arrayOf, bool, object, shape, string } from 'prop-types';
5
+ import { arrayOf, bool, func, object, shape, string } from 'prop-types';
6
6
 
7
7
  import { TAB_COMPONENT_MAPPER } from './constants';
8
8
  import { getDefaultCurrentTab, getTabOptions } from './utils';
9
9
  import styles from './styles';
10
10
 
11
- const SmartBillSummary = ({ assets, colors, loading, smartBill, tabOptions, translations }) => {
11
+ const SmartBillSummary = ({
12
+ assets,
13
+ colors,
14
+ constants,
15
+ handlers,
16
+ loading,
17
+ smartBill,
18
+ tabOptions,
19
+ translations,
20
+ utils
21
+ }) => {
12
22
  const filteredTabOptions = getTabOptions(tabOptions, smartBill) || [];
13
23
  const defaultCurrentTab = getDefaultCurrentTab(filteredTabOptions);
14
24
  const [currentTab, setCurrentTab] = useState(defaultCurrentTab);
@@ -31,10 +41,13 @@ const SmartBillSummary = ({ assets, colors, loading, smartBill, tabOptions, tran
31
41
  {TAB_COMPONENT_MAPPER?.[currentTab]?.({
32
42
  assets,
33
43
  colors,
44
+ constants,
45
+ handlers,
34
46
  loading,
35
47
  smartBill,
36
48
  tabOptions,
37
- translations
49
+ translations,
50
+ utils
38
51
  })}
39
52
  </View>
40
53
  </UTLoading>
@@ -44,6 +57,8 @@ const SmartBillSummary = ({ assets, colors, loading, smartBill, tabOptions, tran
44
57
  SmartBillSummary.propTypes = {
45
58
  assets: object,
46
59
  colors: object,
60
+ constants: object,
61
+ handlers: shape({ [string]: func }),
47
62
  loading: bool,
48
63
  smartBill: object,
49
64
  tabOptions: arrayOf(
@@ -55,7 +70,8 @@ SmartBillSummary.propTypes = {
55
70
  value: string
56
71
  })
57
72
  ),
58
- translations: object
73
+ translations: object,
74
+ utils: object
59
75
  };
60
76
 
61
77
  export default SmartBillSummary;
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_WARNING_ICON = 'IconAlertTriangle';
2
+ export const DEFAULT_SUCCESS_ICON = 'IconCheck';
3
+ export const DEFAULT_ICON_SIZE = 16;
@@ -0,0 +1,33 @@
1
+ /* eslint-disable react/forbid-prop-types */
2
+ import React from 'react';
3
+ import { UTIcon, UTLabel } from '@widergy/mobile-ui';
4
+ import { bool, object, string } from 'prop-types';
5
+ import { View } from 'react-native';
6
+
7
+ import { DEFAULT_ICON_SIZE, DEFAULT_SUCCESS_ICON, DEFAULT_WARNING_ICON } from './constants';
8
+ import { createStyles } from './styles';
9
+
10
+ const DebtStatusLabel = ({ colors, debtStatusLabel, isWarning }) => {
11
+ const styles = createStyles(colors);
12
+
13
+ return (
14
+ <View style={[styles.container, isWarning ? styles.withDebtsContainer : styles.adheredToAutomaticDebit]}>
15
+ <UTIcon
16
+ colorTheme={isWarning ? 'warning' : 'success'}
17
+ name={isWarning ? DEFAULT_WARNING_ICON : DEFAULT_SUCCESS_ICON}
18
+ size={DEFAULT_ICON_SIZE}
19
+ />
20
+ <UTLabel colorTheme={isWarning ? 'warning' : 'success'} variant="small" weight="medium">
21
+ {debtStatusLabel}
22
+ </UTLabel>
23
+ </View>
24
+ );
25
+ };
26
+
27
+ DebtStatusLabel.propTypes = {
28
+ colors: object,
29
+ debtStatusLabel: string,
30
+ isWarning: bool
31
+ };
32
+
33
+ export default DebtStatusLabel;
@@ -0,0 +1,20 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const createStyles = colors =>
4
+ StyleSheet.create({
5
+ container: {
6
+ alignItems: 'center',
7
+ borderRadius: 4,
8
+ flexDirection: 'row',
9
+ gap: 8,
10
+ justifyContent: 'space-between',
11
+ paddingHorizontal: 8,
12
+ paddingVertical: 4
13
+ },
14
+ withDebtsContainer: {
15
+ backgroundColor: colors.semanticWarning01
16
+ },
17
+ adheredToAutomaticDebit: {
18
+ backgroundColor: colors.semanticSuccess01
19
+ }
20
+ });
@@ -0,0 +1,57 @@
1
+ /* eslint-disable react/forbid-prop-types */
2
+ import React from 'react';
3
+ import { View } from 'react-native';
4
+ import { bool, number, object } from 'prop-types';
5
+ import LinearGradient from 'react-native-linear-gradient';
6
+
7
+ import { createStyles } from './styles';
8
+
9
+ const LinearProgress = ({
10
+ colors,
11
+ consumptionPercentage = 0,
12
+ contrastValue = null,
13
+ currentValue = 0,
14
+ hiredValue = 0,
15
+ isOverLimit = false
16
+ }) => {
17
+ const styles = createStyles(colors);
18
+ const shouldShowGradient = isOverLimit || (currentValue > hiredValue && !contrastValue);
19
+
20
+ const backgroundStyle = [styles.backgroundBar, shouldShowGradient && styles.overHiredBackgroundBar];
21
+
22
+ const foregroundStyle = [
23
+ styles.foregroundBar,
24
+ shouldShowGradient && styles.overHiredForegroundBar,
25
+ { width: `${consumptionPercentage}%` }
26
+ ];
27
+
28
+ if (!consumptionPercentage) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <View style={backgroundStyle}>
34
+ {shouldShowGradient ? (
35
+ <LinearGradient
36
+ colors={[colors.chartGradient1start, colors.chartGradient1end]}
37
+ start={{ x: 0, y: 0 }}
38
+ end={{ x: 1, y: 0 }}
39
+ style={foregroundStyle}
40
+ />
41
+ ) : (
42
+ <View style={foregroundStyle} />
43
+ )}
44
+ </View>
45
+ );
46
+ };
47
+
48
+ LinearProgress.propTypes = {
49
+ colors: object,
50
+ consumptionPercentage: number,
51
+ contrastValue: number,
52
+ currentValue: number,
53
+ hiredValue: number,
54
+ isOverLimit: bool
55
+ };
56
+
57
+ export default LinearProgress;
@@ -0,0 +1,22 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const createStyles = colors =>
4
+ StyleSheet.create({
5
+ backgroundBar: {
6
+ height: 9,
7
+ borderRadius: 100,
8
+ backgroundColor: colors.light02,
9
+ overflow: 'hidden'
10
+ },
11
+ foregroundBar: {
12
+ height: '100%',
13
+ borderRadius: 100,
14
+ backgroundColor: colors.chartSolidVariant10
15
+ },
16
+ overHiredBackgroundBar: {
17
+ backgroundColor: colors.chartSolidVariant7
18
+ },
19
+ overHiredForegroundBar: {
20
+ backgroundColor: 'transparent'
21
+ }
22
+ });
@@ -0,0 +1,3 @@
1
+ export const MIN_VALUE_PROGRESS = 0;
2
+ export const MAX_VALUE_PROGRESS = 100;
3
+ export const DEFAULT_GRADIENT_COLORS = ['#30e3a2', '#efc73c', '#ec574d'];
@@ -0,0 +1,163 @@
1
+ /* eslint-disable react/forbid-prop-types */
2
+ /* eslint-disable react-native/no-inline-styles */
3
+ import React, { useState } from 'react';
4
+ import { arrayOf, bool, number, object, oneOfType, shape, string } from 'prop-types';
5
+ import { UTIcon, UTLabel, UTTooltip } from '@widergy/mobile-ui';
6
+ import { View } from 'react-native';
7
+ import LinearGradient from 'react-native-linear-gradient';
8
+
9
+ import { formatLabels, getConsumptionPercentage, getDynamicPosition, getProgressOverlayValue } from './utils';
10
+ import LinearProgress from './components/LinearProgress';
11
+ import { DEFAULT_GRADIENT_COLORS, MAX_VALUE_PROGRESS } from './constants';
12
+ import { createStyles } from './styles';
13
+
14
+ const ProgressBar = ({
15
+ colors,
16
+ currentPeriod,
17
+ data,
18
+ isT1,
19
+ limitValue,
20
+ specificRate,
21
+ tooltipContent,
22
+ translations,
23
+ value
24
+ }) => {
25
+ const styles = createStyles(colors);
26
+ const lastRange = data[data.length - 1]?.range[1];
27
+ const totalRange = lastRange === 'Infinity' ? (data[data.length - 2]?.range[1] ?? 0) * 1.5 : lastRange;
28
+ const progressOverlayValue = getProgressOverlayValue(specificRate, totalRange, value);
29
+
30
+ const isOverLimit = value > limitValue;
31
+ const { initialLabel, dynamicLabel, finalLabel, currentValueLabel, contrastValueLabel } =
32
+ formatLabels({
33
+ currentPeriod,
34
+ isOverLimit,
35
+ limitValue,
36
+ translations,
37
+ value
38
+ }) || {};
39
+ const consumptionPercentage =
40
+ getConsumptionPercentage(value, limitValue, isOverLimit) || MAX_VALUE_PROGRESS;
41
+ const shouldRenderDynamicLabel = value > 0 && limitValue > 0;
42
+
43
+ const [barWidth, setBarWidth] = useState(0);
44
+
45
+ const handleLayout = event => {
46
+ setBarWidth(event.nativeEvent.layout.width);
47
+ };
48
+
49
+ return isT1 ? (
50
+ <View style={styles.progressBar} onLayout={handleLayout}>
51
+ <LinearGradient
52
+ colors={DEFAULT_GRADIENT_COLORS}
53
+ start={{ x: 0, y: 0 }}
54
+ end={{ x: 1, y: 0 }}
55
+ style={styles.progressBackground}>
56
+ {data.map(({ range, name }) => {
57
+ const isLast = range[1] === 'Infinity';
58
+ const width = isLast ? '20%' : `${(range[1] / totalRange) * 100}%`;
59
+
60
+ return (
61
+ <View
62
+ key={name}
63
+ style={[
64
+ styles.progressSegment,
65
+ {
66
+ width,
67
+ flexGrow: isLast ? 1 : 0
68
+ }
69
+ ]}>
70
+ <UTLabel style={styles.segmentName} colorTheme="gray" variant="xsmall">
71
+ {name}
72
+ </UTLabel>
73
+ </View>
74
+ );
75
+ })}
76
+ </LinearGradient>
77
+
78
+ <View
79
+ style={[
80
+ styles.progressOverlay,
81
+ {
82
+ left: (progressOverlayValue / 100) * barWidth,
83
+ width: barWidth - ((progressOverlayValue - 0.5) / 100) * barWidth
84
+ }
85
+ ]}
86
+ />
87
+ </View>
88
+ ) : (
89
+ <View>
90
+ <View style={[styles.powerAndDatesContainer, isOverLimit && styles.overHiredContainer]}>
91
+ <View style={styles.consumptionDetailsContainer}>
92
+ <View style={styles.consumptionComparison}>
93
+ {value >= 0 && <UTLabel variant="subtitle1">{currentValueLabel}</UTLabel>}
94
+
95
+ {value >= 0 && limitValue > 0 && (
96
+ <UTLabel colorTheme="gray" variant="small">
97
+ {contrastValueLabel}
98
+ </UTLabel>
99
+ )}
100
+ </View>
101
+
102
+ {tooltipContent && (
103
+ <UTTooltip content={tooltipContent}>
104
+ <View>
105
+ <UTIcon colorTheme="neutral" name="EnergyIconQuestionFilled" size={20} />
106
+ </View>
107
+ </UTTooltip>
108
+ )}
109
+ </View>
110
+ </View>
111
+
112
+ <View style={styles.progressContainer} onLayout={handleLayout}>
113
+ <LinearProgress
114
+ colors={colors}
115
+ consumptionPercentage={consumptionPercentage}
116
+ contrastValue={limitValue}
117
+ currentValue={value}
118
+ hiredValue={limitValue}
119
+ isOverLimit={isOverLimit}
120
+ />
121
+
122
+ <View style={styles.progressLabels}>
123
+ <UTLabel colorTheme="gray" variant="xsmall">
124
+ {initialLabel}
125
+ </UTLabel>
126
+
127
+ {shouldRenderDynamicLabel && (
128
+ <View
129
+ style={[styles.dinamicLabel, { left: getDynamicPosition(consumptionPercentage, barWidth) }]}>
130
+ <UTLabel colorTheme="gray" variant="xsmall">
131
+ {dynamicLabel}
132
+ </UTLabel>
133
+ </View>
134
+ )}
135
+
136
+ <UTLabel colorTheme="gray" variant="xsmall">
137
+ {finalLabel}
138
+ </UTLabel>
139
+ </View>
140
+ </View>
141
+ </View>
142
+ );
143
+ };
144
+
145
+ ProgressBar.propTypes = {
146
+ colors: object,
147
+ currentPeriod: object,
148
+ data: arrayOf(
149
+ shape({
150
+ name: string,
151
+ color: string,
152
+ range: arrayOf(oneOfType([number, string]))
153
+ })
154
+ ),
155
+ isT1: bool,
156
+ limitValue: number,
157
+ specificRate: string,
158
+ tooltipContent: string,
159
+ translations: object,
160
+ value: number
161
+ };
162
+
163
+ export default ProgressBar;
@@ -0,0 +1,75 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const createStyles = colors =>
4
+ StyleSheet.create({
5
+ progressBar: {
6
+ height: 9,
7
+ marginHorizontal: 24,
8
+ position: 'relative'
9
+ },
10
+ progressBackground: {
11
+ position: 'absolute',
12
+ top: 0,
13
+ left: 0,
14
+ height: '100%',
15
+ width: '100%',
16
+ flexDirection: 'row',
17
+ borderRadius: 100
18
+ },
19
+ progressSegment: {
20
+ height: '100%',
21
+ position: 'relative',
22
+ borderRightColor: colors.light01,
23
+ borderRightWidth: 2
24
+ },
25
+ segmentName: {
26
+ position: 'absolute',
27
+ top: 18
28
+ },
29
+ progressOverlay: {
30
+ backgroundColor: colors.light04,
31
+ borderBottomEndRadius: 100,
32
+ borderTopEndRadius: 100,
33
+ height: '100%',
34
+ position: 'absolute',
35
+ top: 0,
36
+ zIndex: 3
37
+ },
38
+ powerAndDatesContainer: {
39
+ flexDirection: 'row',
40
+ justifyContent: 'space-between',
41
+ marginHorizontal: 24,
42
+ position: 'relative'
43
+ },
44
+ overHiredContainer: {
45
+ flexDirection: 'row-reverse',
46
+ marginBottom: 8
47
+ },
48
+ consumptionDetailsContainer: {
49
+ flex: 1,
50
+ flexDirection: 'row',
51
+ justifyContent: 'space-between',
52
+ gap: 4
53
+ },
54
+ consumptionComparison: {
55
+ flexDirection: 'row',
56
+ gap: 4,
57
+ marginBottom: 8
58
+ },
59
+ progressContainer: {
60
+ marginHorizontal: 24,
61
+ position: 'relative'
62
+ },
63
+ progressLabels: {
64
+ flexDirection: 'row',
65
+ justifyContent: 'space-between',
66
+ marginTop: 8,
67
+ marginBottom: -24,
68
+ position: 'relative'
69
+ },
70
+ dinamicLabel: {
71
+ position: 'absolute',
72
+ top: 0,
73
+ transform: [{ translateX: -0.5 }]
74
+ }
75
+ });
@@ -0,0 +1,89 @@
1
+ import dayjs from 'dayjs';
2
+ import numeral from 'numeral';
3
+ import isBetween from 'dayjs/plugin/isBetween';
4
+
5
+ import { MIN_VALUE_PROGRESS } from './constants';
6
+
7
+ export const getConsumptionPercentage = (currentValue, contrastValue, isOverLimit) =>
8
+ (currentValue > contrastValue
9
+ ? isOverLimit
10
+ ? contrastValue / currentValue
11
+ : 1
12
+ : currentValue / contrastValue) * 100;
13
+
14
+ export const formatMaxPowerRegisteredDate = maxConsumptionDay =>
15
+ dayjs(maxConsumptionDay).tz().format('D MMM');
16
+
17
+ export const getDynamicPosition = (percentage, barWidth, labelWidth = 60) => {
18
+ const min = 16;
19
+ const max = barWidth - labelWidth - 28;
20
+ const calculated = (percentage / 100) * barWidth - 10;
21
+ return Math.min(Math.max(min, calculated), max);
22
+ };
23
+
24
+ dayjs.extend(isBetween);
25
+
26
+ export const formatLabels = ({ currentPeriod, isOverLimit, limitValue, translations = {}, value }) => {
27
+ const currentValueFormatted = numeral(value).format('0,0.[00]');
28
+ const contrastValueFormatted = numeral(limitValue).format('0,0.[00]') || 0;
29
+ const maxConsumptionDay = dayjs(`${currentPeriod?.year}-${currentPeriod?.number}-01`);
30
+ const { currentValueWithUnitLabel, maxValueWithUnitLabel } = translations;
31
+
32
+ return {
33
+ initialLabel: MIN_VALUE_PROGRESS.toString(),
34
+ dynamicLabel: isOverLimit ? contrastValueFormatted : currentValueFormatted,
35
+ finalLabel: isOverLimit || !limitValue ? currentValueFormatted : contrastValueFormatted,
36
+ currentValueLabel: currentValueWithUnitLabel?.({
37
+ value: currentValueFormatted,
38
+ day: formatMaxPowerRegisteredDate(maxConsumptionDay)
39
+ }),
40
+ contrastValueLabel: maxValueWithUnitLabel?.({
41
+ detailLabel: 'contratada',
42
+ value: contrastValueFormatted
43
+ })
44
+ };
45
+ };
46
+
47
+ export const getProgressBarClasses = ({
48
+ consumptionPercentage,
49
+ contrastValue,
50
+ currentValue,
51
+ hiredValue,
52
+ isOverLimit,
53
+ styles
54
+ }) => ({
55
+ root: `${isOverLimit && styles.overHiredBackgroundBar} ${styles.backgroundBar}`,
56
+ bar: `
57
+ ${!isOverLimit ? styles.foregroundBar : styles.overHiredForegroundBar}
58
+ ${currentValue > hiredValue && !contrastValue && styles.overHiredBackgroundBar}
59
+ ${!consumptionPercentage && styles.hidden}
60
+ `
61
+ });
62
+
63
+ export const getProgressOverlayValue = (rate, totalRange, value) => {
64
+ const rateName = rate?.name;
65
+ const defaultProgress = (value / totalRange) * 100;
66
+
67
+ switch (rateName) {
68
+ case 'R1':
69
+ return defaultProgress * 0.4;
70
+
71
+ case 'R2':
72
+ return defaultProgress * 0.55;
73
+
74
+ case 'R3':
75
+ return defaultProgress * 0.85;
76
+
77
+ case 'R4':
78
+ return defaultProgress * 1.13;
79
+
80
+ case 'R5':
81
+ return defaultProgress * 1.368;
82
+
83
+ case 'R6':
84
+ return defaultProgress * 1.37;
85
+
86
+ default:
87
+ return defaultProgress;
88
+ }
89
+ };
@@ -0,0 +1,163 @@
1
+ /* eslint-disable react/forbid-prop-types */
2
+ import React from 'react';
3
+ import { Linking, View } from 'react-native';
4
+ import { Surface, UTButton, UTLabel } from '@widergy/mobile-ui';
5
+ import { object, string, func, objectOf, array } from 'prop-types';
6
+
7
+ import {
8
+ getConsumptionLabels,
9
+ getConsumptionPeriodsToCompare
10
+ } from '../../../Consumptions/components/CurrentConsumption/utils';
11
+
12
+ import ProgressBar from './components/ProgressBar';
13
+ import { getCurrentRate } from './utils';
14
+ import { createStyles } from './styles';
15
+
16
+ const RateCard = ({
17
+ colors,
18
+ consumptionLevels,
19
+ currentPeriod,
20
+ formatAmount,
21
+ normalizedRate = '',
22
+ rateCardTranslations = {},
23
+ ratesTableLink,
24
+ smartBill = {},
25
+ subsidy = '',
26
+ subsidyLevels
27
+ }) => {
28
+ const styles = createStyles(colors);
29
+ const {
30
+ helpText,
31
+ limitRateValueLabel,
32
+ seeRatesTable,
33
+ startRateValueLabel,
34
+ title,
35
+ tooltipContent,
36
+ unit,
37
+ yourConsumption,
38
+ yourRate
39
+ } = rateCardTranslations;
40
+ const { client, detail, periods } = smartBill;
41
+ const purchasedRate = client?.rate?.purchased || '';
42
+ const periodsToCompare = getConsumptionPeriodsToCompare(periods);
43
+ const { totalConsumption } = getConsumptionLabels(periodsToCompare) || {};
44
+ const { levels = [] } =
45
+ consumptionLevels.find(({ rate }) => rate === normalizedRate) || consumptionLevels[0] || {};
46
+ const { rates = [] } =
47
+ levels.find(({ name }) => name && name === subsidyLevels[subsidy]) || levels[0] || {};
48
+ const currentIndex = rates.findIndex(({ range }) => range[1] >= totalConsumption);
49
+ const currentRate = rates[currentIndex] ?? {};
50
+ const startRateValue = Array.isArray(currentRate?.range) && currentRate.range[0];
51
+ const limitRateValue =
52
+ Array.isArray(currentRate?.range) &&
53
+ currentRate.range.length > 0 &&
54
+ currentRate.range[currentRate.range.length - 1];
55
+ const concepts = detail?.concepts?.[0]?.lines;
56
+ const fixedCharges = concepts?.[0];
57
+ const variableCharges = concepts?.[1];
58
+ const { title: fixedChargeTitle, amount: fixedChargeValue } = fixedCharges || {};
59
+ const { title: variableChargeTitle, amount: variableChargeValue } = variableCharges || {};
60
+ const isT1 = purchasedRate.includes('T1');
61
+
62
+ const openExternalLink = () => ratesTableLink && Linking.openURL(ratesTableLink);
63
+
64
+ return (
65
+ <Surface elevation={1} style={styles.container}>
66
+ <View style={styles.headerTitles}>
67
+ <UTLabel variant="title2" weight="medium">
68
+ {title}
69
+ </UTLabel>
70
+ <UTLabel colorTheme="gray">{helpText}</UTLabel>
71
+ </View>
72
+
73
+ <ProgressBar
74
+ colors={colors}
75
+ currentPeriod={currentPeriod}
76
+ data={rates}
77
+ isT1={isT1}
78
+ limitValue={limitRateValue}
79
+ specificRate={currentRate}
80
+ tooltipContent={tooltipContent}
81
+ translations={rateCardTranslations}
82
+ value={Number(totalConsumption) || 0}
83
+ />
84
+
85
+ <View style={styles.consumptionDataContainer}>
86
+ <View style={styles.consumptionContent}>
87
+ <UTLabel variant="title3" weight="medium">
88
+ {getCurrentRate(purchasedRate, currentRate)}
89
+ </UTLabel>
90
+ <UTLabel colorTheme="gray" variant="small">
91
+ {!isT1
92
+ ? yourRate
93
+ : limitRateValue === 'Infinity'
94
+ ? startRateValueLabel?.(startRateValue)
95
+ : limitRateValueLabel?.(limitRateValue)}
96
+ </UTLabel>
97
+ </View>
98
+
99
+ <View style={styles.consumptionContent}>
100
+ <UTLabel variant="title3" weight="medium">
101
+ {totalConsumption} {unit}
102
+ </UTLabel>
103
+ <UTLabel colorTheme="gray" variant="small">
104
+ {yourConsumption}
105
+ </UTLabel>
106
+ </View>
107
+ </View>
108
+
109
+ <View style={styles.footer}>
110
+ <View style={styles.detail}>
111
+ <UTLabel style={styles.detailTitle} weight="medium">
112
+ {fixedChargeTitle}
113
+ </UTLabel>
114
+ <UTLabel variant="subtitle1" weight="medium">
115
+ {formatAmount?.(fixedChargeValue)}
116
+ </UTLabel>
117
+ </View>
118
+
119
+ <View style={styles.detail}>
120
+ <UTLabel style={styles.detailTitle} weight="medium">
121
+ {variableChargeTitle}
122
+ </UTLabel>
123
+ <UTLabel variant="subtitle1" weight="medium">
124
+ {formatAmount?.(variableChargeValue)}
125
+ </UTLabel>
126
+ </View>
127
+
128
+ <View style={styles.footerAction}>
129
+ <UTButton
130
+ colorTheme="secondary"
131
+ Icon="IconChevronRight"
132
+ iconPlacement="right"
133
+ onPress={openExternalLink}
134
+ style={{
135
+ root: styles.footerActionRoot,
136
+ childrenContainer: styles.footerActionChildren,
137
+ icon: styles.icon
138
+ }}
139
+ variant="text">
140
+ <UTLabel colorTheme="neutral" weight="medium">
141
+ {seeRatesTable}
142
+ </UTLabel>
143
+ </UTButton>
144
+ </View>
145
+ </View>
146
+ </Surface>
147
+ );
148
+ };
149
+
150
+ RateCard.propTypes = {
151
+ colors: object,
152
+ consumptionLevels: array,
153
+ currentPeriod: object,
154
+ formatAmount: func,
155
+ normalizedRate: string,
156
+ rateCardTranslations: object,
157
+ ratesTableLink: string,
158
+ smartBill: object,
159
+ subsidy: string,
160
+ subsidyLevels: objectOf(string)
161
+ };
162
+
163
+ export default RateCard;
@@ -0,0 +1,62 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const createStyles = colors =>
4
+ StyleSheet.create({
5
+ container: {
6
+ backgroundColor: colors.light01,
7
+ borderRadius: 8,
8
+ flexDirection: 'column',
9
+ gap: 24
10
+ },
11
+ headerTitles: {
12
+ flexDirection: 'column',
13
+ gap: 8,
14
+ paddingTop: 24,
15
+ paddingHorizontal: 24
16
+ },
17
+ consumptionDataContainer: {
18
+ alignItems: 'center',
19
+ flexDirection: 'row',
20
+ justifyContent: 'space-between',
21
+ gap: 24,
22
+ paddingTop: 24,
23
+ paddingHorizontal: 24
24
+ },
25
+ consumptionContent: {
26
+ flex: 1
27
+ },
28
+ footer: {
29
+ width: '100%'
30
+ },
31
+ footerAction: {
32
+ alignItems: 'center',
33
+ backgroundColor: 'transparent',
34
+ borderTopWidth: 1,
35
+ borderTopColor: colors.light04,
36
+ flexDirection: 'row',
37
+ justifyContent: 'space-between',
38
+ gap: 8
39
+ },
40
+ detail: {
41
+ alignItems: 'center',
42
+ borderTopWidth: 1,
43
+ borderTopColor: colors.light04,
44
+ flexDirection: 'row',
45
+ justifyContent: 'space-between',
46
+ gap: 16,
47
+ padding: 16
48
+ },
49
+ detailTitle: {
50
+ flex: 1
51
+ },
52
+ footerActionRoot: {
53
+ padding: 0,
54
+ margin: 0,
55
+ width: '100%'
56
+ },
57
+ footerActionChildren: {
58
+ alignItems: 'center',
59
+ justifyContent: 'space-between',
60
+ paddingVertical: 8
61
+ }
62
+ });
@@ -0,0 +1,5 @@
1
+ export const getCurrentRate = (baseRate = '', specificRate = '') => {
2
+ const isT1 = baseRate.includes('T1');
3
+ const baseRateParts = baseRate.split('-');
4
+ return isT1 ? `${baseRateParts[0]}-${specificRate?.name || baseRateParts[1]}` : baseRate;
5
+ };
@@ -0,0 +1,271 @@
1
+ /* eslint-disable react/forbid-prop-types */
2
+ import React, { useEffect } from 'react';
3
+ import { Image, ScrollView, View } from 'react-native';
4
+ import { Surface, UTActionCard, UTLabel } from '@widergy/mobile-ui';
5
+ import { func, object, shape, string } from 'prop-types';
6
+ import isEmpty from 'lodash/isEmpty';
7
+
8
+ import { getCurrentPeriod, getFormattedDate } from './utils';
9
+ import { createStyles } from './styles';
10
+ import DebtStatusLabel from './components/DebtStatusLabel';
11
+ import RateCard from './components/RateCard';
12
+
13
+ const Billing = ({
14
+ assets,
15
+ colors,
16
+ constants,
17
+ currentAccount = {},
18
+ handlers = {},
19
+ smartBill = {},
20
+ translations,
21
+ utils = {}
22
+ }) => {
23
+ const styles = createStyles(colors);
24
+ const { UtilityLogo } = assets;
25
+ const { adherenceStatus, consumptionLevels = [], subsidyLevels } = constants;
26
+ const { formatters = {} } = utils;
27
+ const { clientNumberFormatter, formatAmount } = formatters;
28
+ const {
29
+ bill_number: billNumber,
30
+ client,
31
+ debt_status_label: debtStatusLabel,
32
+ detail,
33
+ expirations
34
+ } = smartBill;
35
+ const {
36
+ account_address: accountAddress,
37
+ client_number: clientNumber = '',
38
+ holder_name: titularName,
39
+ postal_address: postalAddress,
40
+ iva_condition: ivaCondition
41
+ } = client || {};
42
+ const {
43
+ adhered,
44
+ automaticDebit = {},
45
+ digitalBill = {},
46
+ downloadBill = {},
47
+ header = {},
48
+ notAdhered,
49
+ rateCard: rateCardTranslations,
50
+ ratesTableLink,
51
+ titularCard = {},
52
+ totalAmountLabel,
53
+ totalCard = {}
54
+ } = translations;
55
+ const {
56
+ title: titularCardTitle,
57
+ addressHelpText,
58
+ titularHelpText,
59
+ clientNumberHelpText,
60
+ ivaConditionHelpText
61
+ } = titularCard;
62
+ const { firstExpiration } = totalCard;
63
+ const { billIssueDate, billTypeHelpText } = header;
64
+ const currentPeriod = getCurrentPeriod(smartBill);
65
+ const issuedOnDate = getFormattedDate(currentPeriod?.settlements?.current?.issued_on);
66
+ const debtStatusValidation = debtStatusLabel !== null;
67
+ const firstExpirationDate = getFormattedDate(expirations?.[0]?.date);
68
+ const { total_amount: totalAmount } = detail || {};
69
+ const {
70
+ handleAutomaticDebitAdherence = () => {},
71
+ handleDownloadBill = () => {},
72
+ handleGoToDigitalBill = () => {},
73
+ getConsumptionLevels = () => {}
74
+ } = handlers;
75
+ const {
76
+ adherence_to_automatic_debit: adherenceToAutomaticDebit,
77
+ normalized_rate: normalizedRate,
78
+ subsidy_level: subsidy
79
+ } = currentAccount;
80
+ const { title: digitalBillTitle } = digitalBill;
81
+ const { title: downloadBillTitle } = downloadBill;
82
+ const { title: automaticDebitTitle } = automaticDebit;
83
+ const isAdheredToDigitalBill = client?.adherence_to_digital_bill;
84
+ const isAdheredToAutomaticDebit = adherenceToAutomaticDebit === adherenceStatus?.subscribed;
85
+
86
+ const actionCards = [
87
+ {
88
+ isAdhered: isAdheredToDigitalBill,
89
+ onPress: !isAdheredToDigitalBill && handleGoToDigitalBill,
90
+ showStatus: true,
91
+ title: digitalBillTitle
92
+ },
93
+ {
94
+ isAdhered: isAdheredToAutomaticDebit,
95
+ onPress: !isAdheredToAutomaticDebit && handleAutomaticDebitAdherence,
96
+ showStatus: true,
97
+ title: automaticDebitTitle
98
+ },
99
+ {
100
+ onPress: handleDownloadBill,
101
+ Icon: 'IconDownload',
102
+ title: downloadBillTitle
103
+ }
104
+ ].map(card => {
105
+ if (!card.showStatus) return card;
106
+ const { isAdhered } = card || {};
107
+ return {
108
+ ...card,
109
+ statusAttributes: {
110
+ status: isAdhered ? 'active' : 'pending',
111
+ statusLabel: isAdhered ? adhered : notAdhered,
112
+ statusAlignment: 'center',
113
+ statusProps: {
114
+ backgroundColor: isAdhered ? colors.semanticSuccess01 : colors.light04,
115
+ colorTheme: isAdhered ? 'success' : 'gray'
116
+ }
117
+ }
118
+ };
119
+ });
120
+
121
+ useEffect(() => {
122
+ if (isEmpty(consumptionLevels)) getConsumptionLevels();
123
+ // eslint-disable-next-line react-hooks/exhaustive-deps
124
+ }, []);
125
+
126
+ return (
127
+ <ScrollView>
128
+ <View style={styles.generalContainer}>
129
+ <Surface elevation={1} style={styles.billHeader}>
130
+ {UtilityLogo && <Image resizeMode="contain" source={UtilityLogo} height={35} />}
131
+ <View style={styles.billHeaderLabels}>
132
+ <View style={styles.valueAndHelpText}>
133
+ <UTLabel variant="small">{billNumber}</UTLabel>
134
+ <UTLabel colorTheme="gray" variant="small">
135
+ {billTypeHelpText}
136
+ </UTLabel>
137
+ </View>
138
+
139
+ <View style={styles.valueAndHelpText}>
140
+ <UTLabel variant="small">{issuedOnDate}</UTLabel>
141
+ <UTLabel colorTheme="gray" variant="small">
142
+ {billIssueDate}
143
+ </UTLabel>
144
+ </View>
145
+
146
+ {debtStatusValidation && (
147
+ <DebtStatusLabel
148
+ colors={colors}
149
+ debtStatusLabel={debtStatusLabel}
150
+ isWarning={smartBill?.warning}
151
+ />
152
+ )}
153
+ </View>
154
+ </Surface>
155
+
156
+ <Surface elevation={1} style={styles.totalCard}>
157
+ <View style={styles.total}>
158
+ <UTLabel variant="subtitle1" weight="medium">
159
+ {totalAmountLabel}
160
+ </UTLabel>
161
+ <UTLabel colorTheme="accent" variant="title2" weight="medium">
162
+ {formatAmount?.(totalAmount, true)}
163
+ </UTLabel>
164
+ </View>
165
+
166
+ <UTLabel colorTheme="gray" variant="small" weight="medium">
167
+ {firstExpiration?.(firstExpirationDate)}
168
+ </UTLabel>
169
+ </Surface>
170
+
171
+ {!isEmpty(consumptionLevels) && (
172
+ <RateCard
173
+ {...{
174
+ colors,
175
+ consumptionLevels,
176
+ currentPeriod,
177
+ formatAmount,
178
+ normalizedRate,
179
+ rateCardTranslations,
180
+ ratesTableLink,
181
+ smartBill,
182
+ subsidy,
183
+ subsidyLevels,
184
+ translations
185
+ }}
186
+ />
187
+ )}
188
+
189
+ {actionCards?.map(({ onPress, Icon = 'IconChevronRight', statusAttributes = {}, title }) => (
190
+ <UTActionCard
191
+ classNames={{ headerTitles: styles.headerAndChildrenContainer }}
192
+ disabled={!onPress}
193
+ headerActions={
194
+ onPress
195
+ ? [
196
+ {
197
+ colorTheme: 'accent',
198
+ Icon,
199
+ isPrimary: true
200
+ }
201
+ ]
202
+ : []
203
+ }
204
+ key={title}
205
+ mainAction={onPress}
206
+ title={title}
207
+ titleProps={{ style: styles.titleActionCard }}
208
+ {...statusAttributes}
209
+ />
210
+ ))}
211
+
212
+ <Surface elevation={1} style={styles.titularCard}>
213
+ <UTLabel variant="title3" weight="medium">
214
+ {titularCardTitle}
215
+ </UTLabel>
216
+
217
+ <View style={styles.titularDataContainer}>
218
+ <View style={styles.titularData}>
219
+ <UTLabel style={styles.titularDataTitle} weight="bold">
220
+ {accountAddress}
221
+ </UTLabel>
222
+
223
+ {accountAddress !== postalAddress && <UTLabel>{postalAddress}</UTLabel>}
224
+
225
+ <UTLabel colorTheme="gray">{addressHelpText}</UTLabel>
226
+ </View>
227
+
228
+ <View style={styles.titularData}>
229
+ <UTLabel style={styles.titularDataTitle} weight="bold">
230
+ {titularName}
231
+ </UTLabel>
232
+
233
+ <UTLabel colorTheme="gray">{titularHelpText}</UTLabel>
234
+ </View>
235
+
236
+ <View style={styles.titularFooter}>
237
+ <View style={styles.titularData}>
238
+ <UTLabel style={styles.titularDataTitle} weight="bold">
239
+ {clientNumberFormatter?.(clientNumber)}
240
+ </UTLabel>
241
+
242
+ <UTLabel colorTheme="gray">{clientNumberHelpText}</UTLabel>
243
+ </View>
244
+
245
+ <View style={styles.titularData}>
246
+ <UTLabel style={styles.titularDataTitle} weight="bold">
247
+ {ivaCondition}
248
+ </UTLabel>
249
+
250
+ <UTLabel colorTheme="gray">{ivaConditionHelpText}</UTLabel>
251
+ </View>
252
+ </View>
253
+ </View>
254
+ </Surface>
255
+ </View>
256
+ </ScrollView>
257
+ );
258
+ };
259
+
260
+ Billing.propTypes = {
261
+ assets: object,
262
+ colors: object,
263
+ constants: object,
264
+ currentAccount: object,
265
+ handlers: shape({ [string]: func }),
266
+ smartBill: object,
267
+ translations: object,
268
+ utils: object
269
+ };
270
+
271
+ export default Billing;
@@ -0,0 +1,73 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export const createStyles = colors =>
4
+ StyleSheet.create({
5
+ generalContainer: {
6
+ flexDirection: 'column',
7
+ gap: 32,
8
+ paddingVertical: 24,
9
+ paddingHorizontal: 16,
10
+ marginBottom: 48
11
+ },
12
+ billHeader: {
13
+ backgroundColor: colors.light01,
14
+ borderRadius: 8,
15
+ flexDirection: 'column',
16
+ gap: 12,
17
+ justifyContent: 'space-between',
18
+ padding: 16
19
+ },
20
+ billHeaderLabels: {
21
+ alignItems: 'center',
22
+ columnGap: 32,
23
+ flexDirection: 'row',
24
+ justifyContent: 'space-between',
25
+ paddingTop: 16,
26
+ rowGap: 16,
27
+ flexWrap: 'wrap'
28
+ },
29
+ valueAndHelpText: {
30
+ flexDirection: 'column',
31
+ gap: 4
32
+ },
33
+ totalCard: {
34
+ backgroundColor: colors.light01,
35
+ borderRadius: 4,
36
+ flexDirection: 'column',
37
+ padding: 16,
38
+ gap: 24
39
+ },
40
+ total: {
41
+ flexDirection: 'row',
42
+ justifyContent: 'space-between',
43
+ gap: 8
44
+ },
45
+ titularCard: {
46
+ backgroundColor: colors.light01,
47
+ borderRadius: 8,
48
+ padding: 24,
49
+ flexDirection: 'column',
50
+ gap: 24
51
+ },
52
+ titularDataContainer: {
53
+ flexDirection: 'column'
54
+ },
55
+ titularData: {
56
+ flex: 1,
57
+ flexDirection: 'column',
58
+ gap: 8,
59
+ paddingVertical: 12
60
+ },
61
+ titularDataTitle: {
62
+ paddingVertical: 4
63
+ },
64
+ titularFooter: {
65
+ flexDirection: 'row',
66
+ justifyContent: 'space-between',
67
+ gap: 16
68
+ },
69
+ titleActionCard: {
70
+ marginBottom: 'auto',
71
+ marginTop: 'auto'
72
+ }
73
+ });
@@ -0,0 +1,7 @@
1
+ import dayjs from 'dayjs';
2
+ import { getIn } from 'seamless-immutable';
3
+
4
+ export const getCurrentPeriod = billData =>
5
+ getIn(billData, ['periods'], []).find(period => period.current) || {};
6
+
7
+ export const getFormattedDate = date => dayjs(date, 'YYYY-MM-DD').format('DD/MM/YYYY');
@@ -8,7 +8,7 @@ import { MAX_HEIGHT, MIN_HEIGHT, SCALE_FACTOR } from './constants';
8
8
  dayjs.locale('es');
9
9
 
10
10
  export const getMaxValue = consumptionData =>
11
- Math.max(...consumptionData.map(d => d.consumptions?.[0]?.value || 0));
11
+ Math.max(...consumptionData.map(d => d?.consumptions?.[0]?.value || 0));
12
12
 
13
13
  export const getBarHeight = (maxValue, value) => {
14
14
  const adjustedValue = value ** SCALE_FACTOR;
@@ -14,7 +14,7 @@ export const getConsumptionPeriodsToCompare = (periods = []) => {
14
14
  parseInt(period.year, 10) === parseInt(currentPeriod.year, 10) - 1
15
15
  );
16
16
 
17
- return [currentPeriod, previousYearPeriod];
17
+ return [currentPeriod, previousYearPeriod].filter(Boolean);
18
18
  }
19
19
  return [];
20
20
  };