@eohjsc/react-native-smart-city 0.2.50 → 0.2.51

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eohjsc/react-native-smart-city",
3
3
  "title": "React Native Smart Home",
4
- "version": "0.2.50",
4
+ "version": "0.2.51",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -39,6 +39,7 @@
39
39
  "reset-cache": "react-native start --reset-cache",
40
40
  "merge_conflict": "yarn jest && yarn update_coverage_result && yarn check_coverage_config",
41
41
  "jest": "jest --detectOpenHandles",
42
+ "postinstall": "npx husky install",
42
43
  "example": "yarn --cwd example",
43
44
  "pods": "cd example && pod-install --quiet",
44
45
  "bootstrap": "yarn example && yarn && yarn pods",
@@ -85,7 +86,7 @@
85
86
  "eslint-plugin-react": "^7.21.5",
86
87
  "eslint-plugin-react-native": "^3.10.0",
87
88
  "factory-girl": "^5.0.4",
88
- "husky": "^2.4.1",
89
+ "husky": "^2.7.0",
89
90
  "jest": "^26.6.3",
90
91
  "jest-circus": "^26.6.3",
91
92
  "jetifier": "^1.6.6",
@@ -1,14 +1,18 @@
1
- import React, { memo, useCallback, useState } from 'react';
1
+ import React, { memo, useCallback, useState, useMemo } from 'react';
2
2
  import { StyleSheet, View, Dimensions } from 'react-native';
3
3
  import moment from 'moment';
4
4
  import { useTranslations } from '../../hooks/Common/useTranslations';
5
5
 
6
6
  import { Colors } from '../../configs';
7
7
  import Text from '../../commons/Text';
8
+ import TextInput from '../../commons/Form/TextInput';
9
+ import Button from '../../commons/Button';
8
10
  import DateTimeRangeChange from '../DateTimeRangeChange';
9
11
  import HorizontalBarChart from './HorizontalBarChart';
10
12
  import DateTimePickerModal from 'react-native-modal-datetime-picker';
11
13
  import LinearChart from './LinearChart';
14
+ import ThreeButtonHistory from '../ThreeButtonHistory';
15
+ import { formatMoney } from '../../utils/Utils';
12
16
 
13
17
  export const dateTimeType = {
14
18
  date: 'date',
@@ -21,12 +25,15 @@ const { width } = Dimensions.get('window');
21
25
  const HistoryChart = memo(
22
26
  ({
23
27
  datas,
24
- unit,
28
+ chartConfig,
29
+ setChartConfig,
25
30
  style,
26
31
  formatType,
27
32
  startDate,
33
+ endDate,
28
34
  setEndDate,
29
35
  setStartDate,
36
+ setGroupBy,
30
37
  configuration,
31
38
  }) => {
32
39
  const t = useTranslations();
@@ -42,6 +49,9 @@ const HistoryChart = memo(
42
49
  startTime: startDate ? startDate : moment().subtract(1, 'day').valueOf(),
43
50
  endTime: dateNow,
44
51
  });
52
+ const [price, setPrice] = useState('');
53
+ const [priceDisplay, setPriceDisplay] = useState('');
54
+
45
55
  const onStart = useCallback(() => {
46
56
  setEventPicker({
47
57
  ...eventPicker,
@@ -126,6 +136,40 @@ const HistoryChart = memo(
126
136
  },
127
137
  [chartOptions]
128
138
  );
139
+
140
+ const onChangePrice = useCallback(
141
+ (input) => {
142
+ const validInput = input.replace(/\D/g, '');
143
+ setPrice(validInput);
144
+ setPriceDisplay(validInput);
145
+ },
146
+ [setPrice, setPriceDisplay]
147
+ );
148
+ const onPriceFocus = useCallback(() => {
149
+ setPriceDisplay(price);
150
+ }, [price]);
151
+ const onPriceBlur = useCallback(() => {
152
+ setPriceDisplay(price !== '' ? formatMoney(price) : '');
153
+ }, [price]);
154
+ const onCalculateCost = useCallback(() => {
155
+ setChartConfig((config) => ({
156
+ ...config,
157
+ price,
158
+ }));
159
+ }, [setChartConfig, price]);
160
+
161
+ const totalPrice = useMemo(() => {
162
+ if (configuration.config !== 'power_consumption') {
163
+ return null;
164
+ }
165
+ const { price } = chartConfig;
166
+ if (price === '' || isNaN(price)) {
167
+ return null;
168
+ }
169
+ const sum = datas[0].data.reduce((a, b) => a + b.y, 0);
170
+ return sum * price;
171
+ }, [configuration, datas, chartConfig]);
172
+
129
173
  const renderChart = useCallback(() => {
130
174
  if (configuration.type === 'line_chart') {
131
175
  return (
@@ -137,7 +181,7 @@ const HistoryChart = memo(
137
181
  );
138
182
  }
139
183
  if (configuration.type === 'horizontal_bar_chart') {
140
- return <HorizontalBarChart unit={unit} datas={datas} />;
184
+ return <HorizontalBarChart datas={datas} config={chartConfig} />;
141
185
  }
142
186
  // eslint-disable-next-line react-hooks/exhaustive-deps
143
187
  }, [configuration, datas, chartOptions, onShowOneChart]);
@@ -145,19 +189,66 @@ const HistoryChart = memo(
145
189
  return (
146
190
  <View style={style}>
147
191
  <View style={styles.historyView}>
148
- <Text size={20} semibold color={Colors.Gray9}>
149
- {t('history')}
150
- </Text>
151
- <DateTimeRangeChange
152
- startTime={eventPicker.startTime}
153
- onStart={onStart}
154
- onEnd={onEnd}
155
- endTime={eventPicker.endTime}
156
- date={dateNow}
157
- formatType={formatType}
158
- />
192
+ <View style={styles.titleHistory}>
193
+ <Text size={20} semibold color={Colors.Gray9}>
194
+ {t('history')}
195
+ </Text>
196
+ {configuration.type === 'horizontal_bar_chart' && (
197
+ <ThreeButtonHistory
198
+ setStartDate={setStartDate}
199
+ setEndDate={setEndDate}
200
+ setGroupBy={setGroupBy}
201
+ startDate={startDate}
202
+ endDate={endDate}
203
+ />
204
+ )}
205
+ </View>
206
+ {configuration.type !== 'horizontal_bar_chart' && (
207
+ <DateTimeRangeChange
208
+ startTime={eventPicker.startTime}
209
+ onStart={onStart}
210
+ onEnd={onEnd}
211
+ endTime={eventPicker.endTime}
212
+ date={dateNow}
213
+ formatType={formatType}
214
+ inline={false}
215
+ />
216
+ )}
159
217
  </View>
218
+ {configuration.config === 'power_consumption' && (
219
+ <View style={styles.wrapCalculateCost}>
220
+ <Text type="H4">
221
+ {t('input_price_to_calculate_electricity_cost')}
222
+ </Text>
223
+ <View style={styles.row}>
224
+ <TextInput
225
+ onFocus={onPriceFocus}
226
+ onBlur={onPriceBlur}
227
+ value={priceDisplay}
228
+ onChange={onChangePrice}
229
+ onSubmitEditing={onCalculateCost}
230
+ placeholder={'0đ'}
231
+ keyboardType="numeric"
232
+ textInputStyle={styles.textInput}
233
+ wrapStyle={styles.wrapTextInput}
234
+ borderBottomOnly
235
+ />
236
+ <Button
237
+ type="primary"
238
+ title={t('calculate_cost')}
239
+ onPress={onCalculateCost}
240
+ style={styles.buttonCalculate}
241
+ textSemiBold={false}
242
+ />
243
+ </View>
244
+ </View>
245
+ )}
160
246
  {renderChart()}
247
+ {configuration.config === 'power_consumption' && !!chartConfig.price && (
248
+ <Text type="H4">
249
+ {t('total_power_price')} <Text bold>{formatMoney(totalPrice)}</Text>
250
+ </Text>
251
+ )}
161
252
  <DateTimePickerModal
162
253
  isVisible={eventPicker.showModalStart}
163
254
  date={eventPicker.startTime}
@@ -185,6 +276,11 @@ const styles = StyleSheet.create({
185
276
  historyView: {
186
277
  paddingTop: 16,
187
278
  },
279
+ titleHistory: {
280
+ flexDirection: 'row',
281
+ alignItems: 'center',
282
+ justifyContent: 'space-between',
283
+ },
188
284
  chartInfo: {
189
285
  flexDirection: 'row',
190
286
  justifyContent: 'center',
@@ -196,4 +292,27 @@ const styles = StyleSheet.create({
196
292
  chart: {
197
293
  height: 300,
198
294
  },
295
+ wrapCalculateCost: {
296
+ marginTop: 12,
297
+ },
298
+ row: {
299
+ flexDirection: 'row',
300
+ alignItems: 'center',
301
+ },
302
+ wrapTextInput: {
303
+ flex: 1,
304
+ marginRight: 16,
305
+ },
306
+ textInput: {
307
+ marginTop: 0,
308
+ paddingTop: 0,
309
+ paddingLeft: 0,
310
+ fontSize: 18,
311
+ lineHeight: 24,
312
+ },
313
+ buttonCalculate: {
314
+ width: null,
315
+ flex: null,
316
+ paddingHorizontal: 12,
317
+ },
199
318
  });
@@ -5,7 +5,7 @@ import HighchartsReactNative from '@highcharts/highcharts-react-native/src/Highc
5
5
  import { Colors } from '../../configs';
6
6
  import { getMaxValueIndex } from '../../utils/chartHelper/getMaxValueIndex';
7
7
 
8
- const HorizontalBarChart = memo(({ datas, unit }) => {
8
+ const HorizontalBarChart = memo(({ datas, config }) => {
9
9
  const dataY = datas[0].data.map((item, index) => {
10
10
  return {
11
11
  color: index % 2 === 0 ? Colors.Primary + '20' : Colors.Primary + '16',
@@ -18,6 +18,7 @@ const HorizontalBarChart = memo(({ datas, unit }) => {
18
18
  }, [dataX]);
19
19
  const maxY = getMaxValueIndex(dataY);
20
20
  dataY.splice(maxY._index, 1, { ...maxY.max, color: Colors.Primary });
21
+
21
22
  // eslint-disable-next-line no-unused-vars
22
23
  const chartOptions = {
23
24
  chart: {
@@ -48,7 +49,7 @@ const HorizontalBarChart = memo(({ datas, unit }) => {
48
49
  {
49
50
  marker: { enabled: true },
50
51
  color: Colors.Primary + '50',
51
- name: datas[0].measure,
52
+ name: JSON.stringify(config),
52
53
  data: dataY,
53
54
  },
54
55
  ],
@@ -76,7 +77,33 @@ const HorizontalBarChart = memo(({ datas, unit }) => {
76
77
  bar: {
77
78
  dataLabels: {
78
79
  enabled: true,
79
- format: `{point.y} ${unit}`,
80
+ useHTML: true,
81
+ formatter: function () {
82
+ const { unit, price } = JSON.parse(this.series.name);
83
+ const textColor = this.color === '#00979D' ? '#FFFFFF' : '#262626';
84
+ const valueStyle = `color:${textColor};font-weight:normal;font-size:12px;`;
85
+ const costStyle = valueStyle + 'font-weight:bold;';
86
+
87
+ let label = `<span style="${valueStyle}">` + `${this.y}${unit}`;
88
+ if (price === '' || isNaN(price)) {
89
+ return label + '</span>';
90
+ }
91
+
92
+ const formatMoney = (number) => {
93
+ return (
94
+ parseInt(number, 10)
95
+ .toFixed(0)
96
+ .toString()
97
+ .replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1.') + 'đ'
98
+ );
99
+ };
100
+ return (
101
+ label +
102
+ `/<span style="${costStyle}">` +
103
+ formatMoney(this.y * price) +
104
+ '</span></span>'
105
+ );
106
+ },
80
107
  align: 'right',
81
108
  },
82
109
  labels: {
@@ -51,15 +51,10 @@ const chartOptions = {
51
51
  const time = new Date(this.value || value);
52
52
  let date = ('0' + time.getDate()).slice(-2);
53
53
  let month = ('0' + (time.getMonth() + 1)).slice(-2);
54
- let hours = ('0' + time.getHours()).slice(-2);
55
- let minutes = ('0' + time.getMinutes()).slice(-2);
56
- if (hours === '00' && minutes === '00') {
57
- return `${date}.${month}`;
58
- }
59
- return `${hours}:${minutes}`;
54
+ return `${date}.${month}`;
60
55
  },
61
56
  },
62
- minRange: 3600 * 1000,
57
+ minRange: 3600 * 24 * 1000,
63
58
  },
64
59
  plotOptions: {
65
60
  series: {
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ TouchableOpacity,
5
+ TouchableWithoutFeedback,
6
+ Image,
7
+ } from 'react-native';
8
+ import Text from '../Text';
9
+ import { Images } from '../../configs';
10
+ import styles from './CalendarHeaderStyles';
11
+
12
+ const CalendarHeader = ({
13
+ currentMonth,
14
+ onAddMonth,
15
+ onSubtractMonth,
16
+ onPressTitle,
17
+ }) => {
18
+ return (
19
+ <View style={styles.wrap}>
20
+ <TouchableOpacity onPress={onSubtractMonth} style={styles.button}>
21
+ <Image source={Images.arrowLeft} />
22
+ </TouchableOpacity>
23
+ <TouchableWithoutFeedback onPress={onPressTitle}>
24
+ <Text type="H4" bold>
25
+ {currentMonth.format('MMMM YYYY')}
26
+ </Text>
27
+ </TouchableWithoutFeedback>
28
+ <TouchableOpacity onPress={onAddMonth} style={styles.button}>
29
+ <Image source={Images.arrowLeft} style={styles.arrow} />
30
+ </TouchableOpacity>
31
+ </View>
32
+ );
33
+ };
34
+
35
+ export default CalendarHeader;
@@ -0,0 +1,17 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export default StyleSheet.create({
4
+ wrap: {
5
+ flexDirection: 'row',
6
+ alignItems: 'center',
7
+ justifyContent: 'space-between',
8
+ paddingVertical: 16,
9
+ paddingHorizontal: 24,
10
+ },
11
+ arrow: {
12
+ transform: [{ rotate: '180deg' }],
13
+ },
14
+ button: {
15
+ padding: 8,
16
+ },
17
+ });
@@ -0,0 +1,53 @@
1
+ import React, { memo, useMemo, useCallback } from 'react';
2
+ import { View, TouchableOpacity } from 'react-native';
3
+ import moment from 'moment';
4
+
5
+ import Text from '../Text';
6
+ import styles from './SelectMonthStyles';
7
+ import { Colors } from '../../configs';
8
+
9
+ const SelectMonth = memo(({ selectedMonth, setSelectedMonth }) => {
10
+ const months = useMemo(() => {
11
+ return Array.from({ length: 12 }, (_, i) => moment(selectedMonth).month(i));
12
+ }, [selectedMonth]);
13
+
14
+ const MonthItem = useCallback(
15
+ ({ month }) => {
16
+ const isSelected = month.isSame(selectedMonth, 'month');
17
+ const isDisabled = month.isAfter(moment(), 'month');
18
+ const textColor = isSelected
19
+ ? Colors.White
20
+ : isDisabled
21
+ ? Colors.Gray6
22
+ : Colors.Gray9;
23
+ const backgroundColor = isSelected ? Colors.Primary : Colors.Transparent;
24
+
25
+ const onPress = () => setSelectedMonth(month);
26
+
27
+ return (
28
+ <View style={styles.wrapItem}>
29
+ <TouchableOpacity
30
+ onPress={onPress}
31
+ disabled={isDisabled}
32
+ style={[styles.item, { backgroundColor }]}
33
+ >
34
+ <Text color={textColor} semibold>
35
+ {month.format('MMM')}
36
+ </Text>
37
+ </TouchableOpacity>
38
+ </View>
39
+ );
40
+ },
41
+ [selectedMonth, setSelectedMonth]
42
+ );
43
+
44
+ return (
45
+ <View style={styles.wrap}>
46
+ {months.map((month, index) => (
47
+ <MonthItem key={index} month={month} />
48
+ ))}
49
+ </View>
50
+ );
51
+ });
52
+
53
+ export default SelectMonth;
@@ -0,0 +1,29 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { Constants } from '../../configs';
3
+
4
+ const marginItem = 40;
5
+ const marginHorizontal = 24;
6
+ const widthItem = (Constants.width - marginHorizontal * 2 - marginItem * 2) / 3;
7
+ const heightItem = (widthItem / 36) * 16;
8
+
9
+ export default StyleSheet.create({
10
+ wrap: {
11
+ marginHorizontal: 24,
12
+ flexDirection: 'row',
13
+ justifyContent: 'space-between',
14
+ flexWrap: 'wrap',
15
+ },
16
+ wrapItem: {
17
+ width: widthItem,
18
+ height: heightItem,
19
+ marginBottom: 4,
20
+ paddingVertical: 4,
21
+ paddingHorizontal: 8,
22
+ },
23
+ item: {
24
+ flex: 1,
25
+ justifyContent: 'center',
26
+ alignItems: 'center',
27
+ borderRadius: 4,
28
+ },
29
+ });
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { act, create } from 'react-test-renderer';
3
+ import { TouchableOpacity } from 'react-native';
4
+ import SelectMonth from '../SelectMonth';
5
+ import moment from 'moment';
6
+
7
+ jest.mock('react', () => {
8
+ return {
9
+ ...jest.requireActual('react'),
10
+ memo: (x) => x,
11
+ };
12
+ });
13
+
14
+ test('test SelectMonth', () => {
15
+ let tree;
16
+ const mockSetSelectedMonth = jest.fn();
17
+ const currentMonth = moment();
18
+
19
+ act(() => {
20
+ tree = create(
21
+ <SelectMonth
22
+ selectedMonth={currentMonth}
23
+ setSelectedMonth={mockSetSelectedMonth}
24
+ />
25
+ );
26
+ });
27
+ const instance = tree.root;
28
+ const monthItems = instance.findAllByType(TouchableOpacity);
29
+ expect(monthItems).toHaveLength(12);
30
+
31
+ for (let i = 0; i < 12; i++) {
32
+ act(() => {
33
+ monthItems[i].props.onPress();
34
+ });
35
+ expect(mockSetSelectedMonth).toBeCalledWith(moment(currentMonth).month(i));
36
+ }
37
+ });
@@ -0,0 +1,231 @@
1
+ import React from 'react';
2
+ import { act, create } from 'react-test-renderer';
3
+ import { SCProvider } from '../../../context';
4
+ import { mockSCStore } from '../../../context/mockStore';
5
+ import ThreeButtonHistory from '../index';
6
+ import moment from 'moment';
7
+ import SelectMonth from '../SelectMonth';
8
+ import { TESTID } from '../../../configs/Constants';
9
+ import { TouchableOpacity } from 'react-native';
10
+ import CalendarHeader from '../CalendarHeader';
11
+ import ViewButtonBottom from '../../ViewButtonBottom';
12
+ import BottomSheet from '../../BottomSheet';
13
+ import styles from '../styles';
14
+
15
+ const wrapComponent = (props) => (
16
+ <SCProvider initState={mockSCStore({})}>
17
+ <ThreeButtonHistory {...props} />
18
+ </SCProvider>
19
+ );
20
+
21
+ jest.mock('react', () => {
22
+ return {
23
+ ...jest.requireActual('react'),
24
+ memo: (x) => x,
25
+ };
26
+ });
27
+
28
+ describe('test ThreeButtonHistory', () => {
29
+ Date.now = jest.fn(() => new Date('2021-09-09T10:00:00.000Z'));
30
+ let tree;
31
+ let props;
32
+ const startDate = moment().add(-7, 'days').valueOf();
33
+ const endDate = moment().valueOf();
34
+ const mockSetStartDate = jest.fn();
35
+ const mockSetEndDate = jest.fn();
36
+ const mockSetGroupBy = jest.fn();
37
+
38
+ beforeEach(() => {
39
+ mockSetStartDate.mockClear();
40
+ mockSetEndDate.mockClear();
41
+ mockSetGroupBy.mockClear();
42
+ props = {
43
+ startDate,
44
+ endDate,
45
+ setStartDate: mockSetStartDate,
46
+ setEndDate: mockSetEndDate,
47
+ setGroupBy: mockSetGroupBy,
48
+ };
49
+ });
50
+
51
+ const openCalendar = async (instance) => {
52
+ const historyButtons = instance.findAll(
53
+ (el) =>
54
+ el.props.testID === TESTID.HISTORY_BUTTON &&
55
+ el.type === TouchableOpacity
56
+ );
57
+ expect(historyButtons).toHaveLength(3);
58
+ await act(async () => {
59
+ await historyButtons[2].props.onPress();
60
+ });
61
+ };
62
+
63
+ const testSelectGroupBy = async (index, groupBy) => {
64
+ await act(async () => {
65
+ tree = await create(wrapComponent(props));
66
+ });
67
+ const instance = tree.root;
68
+ const historyButtons = instance.findAll(
69
+ (el) =>
70
+ el.props.testID === TESTID.HISTORY_BUTTON &&
71
+ el.type === TouchableOpacity
72
+ );
73
+ await act(async () => {
74
+ await historyButtons[index].props.onPress();
75
+ });
76
+ expect(mockSetGroupBy).toBeCalledWith(groupBy);
77
+ };
78
+
79
+ test('test group by date', async () => {
80
+ await testSelectGroupBy(2, 'date');
81
+ });
82
+
83
+ test('test group by week', async () => {
84
+ await testSelectGroupBy(0, 'week');
85
+ });
86
+
87
+ test('test group by month', async () => {
88
+ await testSelectGroupBy(1, 'month');
89
+ });
90
+
91
+ test('test calendar change month', async () => {
92
+ await act(async () => {
93
+ tree = await create(wrapComponent(props));
94
+ });
95
+ const instance = tree.root;
96
+
97
+ await openCalendar(instance);
98
+ const calendar = instance.find(
99
+ (el) => el.props.testID === TESTID.HISTORY_CALENDAR
100
+ );
101
+ const calendarHeader = instance.findByType(CalendarHeader);
102
+ expect(calendarHeader.props.currentMonth.toString()).toBe(
103
+ moment().toString()
104
+ );
105
+ // Click calendar title
106
+ await act(async () => {
107
+ await calendarHeader.props.onPressTitle();
108
+ });
109
+ const selectMonth = instance.findByType(SelectMonth);
110
+ expect(selectMonth).toBeDefined();
111
+ expect(calendar.props.style).toBe(styles.displayNone);
112
+ // Click arrow left
113
+ await act(async () => {
114
+ await calendarHeader.props.onSubtractMonth();
115
+ });
116
+ expect(calendarHeader.props.currentMonth.toString()).toBe(
117
+ moment().add(-1, 'month').toString()
118
+ );
119
+ // Click arrow right
120
+ await act(async () => {
121
+ await calendarHeader.props.onAddMonth();
122
+ });
123
+ expect(calendarHeader.props.currentMonth.toString()).toBe(
124
+ moment().toString()
125
+ );
126
+ // Click arrow right
127
+ await act(async () => {
128
+ await calendarHeader.props.onAddMonth();
129
+ });
130
+ expect(calendarHeader.props.currentMonth.toString()).toBe(
131
+ moment().toString()
132
+ ); // exceeded limit
133
+ });
134
+
135
+ test('test press Done', async () => {
136
+ await act(async () => {
137
+ tree = await create(wrapComponent(props));
138
+ });
139
+ const instance = tree.root;
140
+ const modal = instance.findByType(BottomSheet);
141
+
142
+ await openCalendar(instance);
143
+ expect(modal.props.isVisible).toBeTruthy();
144
+
145
+ const calendar = instance.find(
146
+ (el) => el.props.testID === TESTID.HISTORY_CALENDAR
147
+ );
148
+ const calendarHeader = instance.findByType(CalendarHeader);
149
+ const viewButtonBottom = instance.findByType(ViewButtonBottom);
150
+ // Click calendar title
151
+ await act(async () => {
152
+ await calendarHeader.props.onPressTitle();
153
+ });
154
+ expect(calendar.props.style).toBe(styles.displayNone);
155
+ // Click Done
156
+ await act(async () => {
157
+ await viewButtonBottom.props.onRightClick();
158
+ });
159
+ expect(calendar.props.style).not.toBe(styles.displayNone);
160
+ // Click Done
161
+ await act(async () => {
162
+ await viewButtonBottom.props.onRightClick();
163
+ });
164
+ expect(mockSetStartDate).toBeCalled();
165
+ expect(mockSetEndDate).toBeCalled();
166
+ expect(modal.props.isVisible).toBeFalsy();
167
+ });
168
+
169
+ test('test press Cancel', async () => {
170
+ await act(async () => {
171
+ tree = await create(wrapComponent(props));
172
+ });
173
+ const instance = tree.root;
174
+ const modal = instance.findByType(BottomSheet);
175
+
176
+ await openCalendar(instance);
177
+ expect(modal.props.isVisible).toBeTruthy();
178
+
179
+ const calendar = instance.find(
180
+ (el) => el.props.testID === TESTID.HISTORY_CALENDAR
181
+ );
182
+ const calendarHeader = instance.findByType(CalendarHeader);
183
+ const viewButtonBottom = instance.findByType(ViewButtonBottom);
184
+ // Click calendar title
185
+ await act(async () => {
186
+ await calendarHeader.props.onPressTitle();
187
+ });
188
+ expect(calendar.props.style).toBe(styles.displayNone);
189
+ // Click Cancel
190
+ await act(async () => {
191
+ await viewButtonBottom.props.onLeftClick();
192
+ });
193
+ expect(calendar.props.style).not.toBe(styles.displayNone);
194
+ // Click Cancel
195
+ await act(async () => {
196
+ await viewButtonBottom.props.onLeftClick();
197
+ });
198
+ expect(modal.props.isVisible).toBeFalsy();
199
+ });
200
+
201
+ test('test calendar select date range', async () => {
202
+ await act(async () => {
203
+ tree = await create(wrapComponent(props));
204
+ });
205
+ const instance = tree.root;
206
+
207
+ await openCalendar(instance);
208
+ const calendar = instance.find(
209
+ (el) => el.props.testID === TESTID.HISTORY_CALENDAR
210
+ );
211
+
212
+ const selectDate = async (date) => {
213
+ await act(async () => {
214
+ await calendar.props.onDayPress({ dateString: date });
215
+ });
216
+ };
217
+ await selectDate('2021-09-20');
218
+ await selectDate('2021-09-02');
219
+ await selectDate('2021-09-09');
220
+ await selectDate('2021-09-10');
221
+ await selectDate('2021-09-05');
222
+ await selectDate('2021-09-05');
223
+ await selectDate('2021-09-20');
224
+ const viewButtonBottom = instance.findByType(ViewButtonBottom);
225
+ await act(async () => {
226
+ await viewButtonBottom.props.onRightClick();
227
+ });
228
+ expect(mockSetStartDate).toBeCalledWith(moment('2021-09-10').valueOf());
229
+ expect(mockSetEndDate).toBeCalledWith(moment('2021-09-20').valueOf());
230
+ });
231
+ });
@@ -0,0 +1,281 @@
1
+ import React, { memo, useCallback, useState, useMemo, useRef } from 'react';
2
+ import { View, TouchableOpacity } from 'react-native';
3
+ import { IconOutline } from '@ant-design/icons-react-native';
4
+ import { Calendar } from 'react-native-calendars';
5
+ import moment from 'moment';
6
+
7
+ import Text from '../../commons/Text';
8
+ import BottomSheet from '../../commons/BottomSheet';
9
+ import SelectMonth from './SelectMonth';
10
+ import CalendarHeader from './CalendarHeader';
11
+ import ViewButtonBottom from '../ViewButtonBottom';
12
+ import { useTranslations } from '../../hooks/Common/useTranslations';
13
+
14
+ import { Colors } from '../../configs';
15
+ import { TESTID } from '../../configs/Constants';
16
+ import styles from './styles';
17
+
18
+ const ThreeButtonHistory = memo(
19
+ ({ startDate, endDate, setEndDate, setStartDate, setGroupBy }) => {
20
+ const t = useTranslations();
21
+ const calendarRef = useRef();
22
+ const [selectedIndex, setSelectedIndex] = useState(2);
23
+ const [isShowDate, setIsShowDate] = useState(false);
24
+ const [selectedStart, setSelectedStart] = useState(
25
+ moment(startDate).format('YYYY-MM-DD')
26
+ );
27
+ const [selectedEnd, setSelectedEnd] = useState(
28
+ moment(endDate).format('YYYY-MM-DD')
29
+ );
30
+
31
+ const [showSelectMonth, setShowSelectMonth] = useState(false);
32
+ const [currentMonth, setCurrentMonth] = useState(moment());
33
+
34
+ const listItem = useMemo(
35
+ () => [
36
+ {
37
+ id: 1,
38
+ dateTitle: '7D',
39
+ },
40
+ {
41
+ id: 2,
42
+ dateTitle: '1M',
43
+ },
44
+ {
45
+ id: 3,
46
+ dateTitle: (
47
+ <View style={styles.paddingTop2}>
48
+ <IconOutline
49
+ name="calendar"
50
+ color={selectedIndex === 2 ? Colors.Primary : Colors.Gray8}
51
+ size={21}
52
+ />
53
+ </View>
54
+ ),
55
+ },
56
+ ],
57
+ [selectedIndex]
58
+ );
59
+
60
+ const onPressCancel = useCallback(() => {
61
+ if (showSelectMonth) {
62
+ setShowSelectMonth(false);
63
+ return;
64
+ }
65
+ setIsShowDate(false);
66
+ }, [showSelectMonth, setShowSelectMonth]);
67
+
68
+ const onPressDone = useCallback(() => {
69
+ if (showSelectMonth) {
70
+ setShowSelectMonth(false);
71
+ return;
72
+ }
73
+ if (selectedStart === null || selectedEnd === null) {
74
+ return;
75
+ }
76
+ setIsShowDate(false);
77
+ setStartDate(moment(selectedStart).valueOf());
78
+ setEndDate(moment(selectedEnd).valueOf());
79
+ }, [
80
+ setStartDate,
81
+ selectedStart,
82
+ setEndDate,
83
+ selectedEnd,
84
+ showSelectMonth,
85
+ setShowSelectMonth,
86
+ ]);
87
+
88
+ const onDayPress = useCallback(
89
+ (date) => {
90
+ const selectedDate = date.dateString;
91
+ if (selectedDate === selectedStart) {
92
+ setSelectedStart(null);
93
+ return;
94
+ }
95
+ if (selectedDate === selectedEnd) {
96
+ setSelectedEnd(null);
97
+ return;
98
+ }
99
+ if (selectedStart !== null && selectedEnd !== null) {
100
+ return;
101
+ }
102
+ if (selectedEnd === null) {
103
+ setSelectedEnd(selectedDate);
104
+ } else {
105
+ if (moment(selectedDate).isAfter(selectedEnd, 'date')) {
106
+ setSelectedStart(selectedEnd);
107
+ setSelectedEnd(selectedDate);
108
+ } else {
109
+ setSelectedStart(selectedDate);
110
+ }
111
+ }
112
+ },
113
+ [selectedStart, selectedEnd]
114
+ );
115
+
116
+ const onPressItemButton = useCallback(
117
+ (index) => {
118
+ switch (index) {
119
+ case 0:
120
+ setGroupBy('week');
121
+ break;
122
+ case 1:
123
+ setGroupBy('month');
124
+ break;
125
+ case 2:
126
+ setGroupBy('date');
127
+ if (selectedIndex === 2) {
128
+ setIsShowDate(true);
129
+ }
130
+ }
131
+ setSelectedIndex(index);
132
+ },
133
+ [selectedIndex, setGroupBy]
134
+ );
135
+
136
+ const ItemButton = memo(
137
+ ({ dateTitle, onPress, isSelected }) => {
138
+ return (
139
+ <TouchableOpacity
140
+ onPress={onPress}
141
+ style={[styles.buttonDate, isSelected && styles.buttonDateActive]}
142
+ testID={TESTID.HISTORY_BUTTON}
143
+ >
144
+ <View>
145
+ <Text semibold color={isSelected && Colors.Primary}>
146
+ {dateTitle}
147
+ </Text>
148
+ </View>
149
+ </TouchableOpacity>
150
+ );
151
+ },
152
+ [selectedIndex]
153
+ );
154
+
155
+ const onPressCalendarTitle = useCallback(() => {
156
+ setShowSelectMonth((state) => !state);
157
+ }, [setShowSelectMonth]);
158
+
159
+ const onAddMonth = useCallback(() => {
160
+ setCurrentMonth((oldMonth) => {
161
+ const newMonth = moment(oldMonth).add(1, 'months');
162
+ if (newMonth.isAfter(moment(), 'month')) {
163
+ return oldMonth;
164
+ }
165
+ calendarRef?.current?.addMonth(1);
166
+ return newMonth;
167
+ });
168
+ }, []);
169
+
170
+ const onSubtractMonth = useCallback(() => {
171
+ setCurrentMonth((oldMonth) => moment(oldMonth).add(-1, 'months'));
172
+ calendarRef?.current?.addMonth(-1);
173
+ }, []);
174
+
175
+ const onMonthSelected = useCallback(
176
+ (month) => {
177
+ setCurrentMonth((oldMonth) => {
178
+ const monthDiff = month.month() - oldMonth.month();
179
+ calendarRef?.current?.addMonth(monthDiff);
180
+ return month;
181
+ });
182
+ },
183
+ [setCurrentMonth]
184
+ );
185
+
186
+ const markedDates = useMemo(() => {
187
+ const dates = {
188
+ [selectedStart]: {
189
+ selected: true,
190
+ selectedColor: Colors.Primary,
191
+ selectedTextColor: Colors.White,
192
+ },
193
+ [selectedEnd]: {
194
+ selected: true,
195
+ selectedColor: Colors.Primary,
196
+ selectedTextColor: Colors.White,
197
+ },
198
+ };
199
+ const currDate = moment(selectedStart).startOf('day');
200
+ const lastDate = moment(selectedEnd).startOf('day');
201
+
202
+ while (currDate.add(1, 'days').diff(lastDate) < 0) {
203
+ dates[[moment(currDate).format('YYYY-MM-DD')]] = {
204
+ customStyles: {
205
+ container: {
206
+ borderRadius: 0,
207
+ width: 60,
208
+ backgroundColor: Colors.Gray4,
209
+ },
210
+ text: {
211
+ color: Colors.Black,
212
+ },
213
+ },
214
+ selected: true,
215
+ disableTouchEvent: true,
216
+ color: Colors.Gray4,
217
+ };
218
+ }
219
+ return dates;
220
+ }, [selectedStart, selectedEnd]);
221
+
222
+ return (
223
+ <>
224
+ <View style={styles.historyTitleRight}>
225
+ {listItem &&
226
+ listItem.map((item, index) => {
227
+ return (
228
+ <ItemButton
229
+ key={index}
230
+ dateTitle={item.dateTitle}
231
+ onPress={() => onPressItemButton(index)}
232
+ isSelected={selectedIndex === index}
233
+ />
234
+ );
235
+ })}
236
+ </View>
237
+ <BottomSheet
238
+ isVisible={isShowDate}
239
+ onBackdropPress={onPressCancel}
240
+ style={styles.modal}
241
+ >
242
+ <View style={styles.calendar}>
243
+ <CalendarHeader
244
+ currentMonth={currentMonth}
245
+ onAddMonth={onAddMonth}
246
+ onSubtractMonth={onSubtractMonth}
247
+ onPressTitle={onPressCalendarTitle}
248
+ />
249
+ {showSelectMonth && (
250
+ <SelectMonth
251
+ selectedMonth={currentMonth}
252
+ setSelectedMonth={onMonthSelected}
253
+ />
254
+ )}
255
+ <Calendar
256
+ ref={calendarRef}
257
+ current={currentMonth.format('YYYY-MM-DD')}
258
+ style={showSelectMonth && styles.displayNone}
259
+ markingType={'custom'}
260
+ onDayPress={onDayPress}
261
+ maxDate={moment().format('YYYY-MM-DD')}
262
+ markedDates={markedDates}
263
+ hideArrows={true}
264
+ renderHeader={() => <></>}
265
+ testID={TESTID.HISTORY_CALENDAR}
266
+ />
267
+ <View style={styles.separator} />
268
+ <ViewButtonBottom
269
+ leftTitle={t('cancel')}
270
+ onLeftClick={onPressCancel}
271
+ rightTitle={t('done')}
272
+ onRightClick={onPressDone}
273
+ />
274
+ </View>
275
+ </BottomSheet>
276
+ </>
277
+ );
278
+ }
279
+ );
280
+
281
+ export default ThreeButtonHistory;
@@ -0,0 +1,65 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { isIphoneX } from 'react-native-iphone-x-helper';
3
+
4
+ import { Colors } from '../../configs';
5
+
6
+ export default StyleSheet.create({
7
+ historyTitle: {
8
+ flex: 1,
9
+ flexDirection: 'row',
10
+ justifyContent: 'space-between',
11
+ },
12
+ historyTitleRight: {
13
+ flexDirection: 'row',
14
+ backgroundColor: Colors.Gray4,
15
+ borderRadius: 5,
16
+ paddingHorizontal: 2,
17
+ },
18
+ buttonDate: {
19
+ alignItems: 'center',
20
+ marginVertical: 2,
21
+ borderRadius: 5,
22
+ paddingHorizontal: 8,
23
+ },
24
+ buttonDateActive: {
25
+ backgroundColor: Colors.White,
26
+ },
27
+ modal: {
28
+ margin: 0,
29
+ padding: 0,
30
+ },
31
+ calendar: {
32
+ position: 'absolute',
33
+ bottom: 0,
34
+ left: 0,
35
+ right: 0,
36
+ borderTopLeftRadius: 10,
37
+ borderTopRightRadius: 10,
38
+ overflow: 'hidden',
39
+ backgroundColor: Colors.White,
40
+ },
41
+ separator: {
42
+ height: 1,
43
+ backgroundColor: Colors.Gray3,
44
+ marginTop: 5,
45
+ },
46
+ wrapBottomButton: {
47
+ height: 56 + (isIphoneX() ? 10 : 0),
48
+ justifyContent: 'space-between',
49
+ alignItems: 'center',
50
+ paddingHorizontal: 80,
51
+ flexDirection: 'row',
52
+ marginTop: -10,
53
+ },
54
+ button: {
55
+ height: 40,
56
+ justifyContent: 'center',
57
+ alignItems: 'center',
58
+ },
59
+ paddingTop2: {
60
+ paddingTop: 2,
61
+ },
62
+ displayNone: {
63
+ display: 'none',
64
+ },
65
+ });
@@ -568,6 +568,10 @@ export const TESTID = {
568
568
  // ActivityLog
569
569
  FILTER_BUTTON: 'FILTER_BUTTON',
570
570
  ITEM_USER_FILTER: 'ITEM_USER_FILTER',
571
+
572
+ // ThreeButtonHistory
573
+ HISTORY_BUTTON: 'HISTORY_BUTTON',
574
+ HISTORY_CALENDAR: 'HISTORY_CALENDAR',
571
575
  };
572
576
 
573
577
  export const NOTIFICATION_TYPES = {
@@ -108,7 +108,16 @@ describe('test AddNewOneTap', () => {
108
108
  id: 1,
109
109
  },
110
110
  isCreateScriptSuccess: true,
111
- automate: {},
111
+ automate: {
112
+ id: 1,
113
+ script: {
114
+ id: 1,
115
+ name: 'William Miller',
116
+ },
117
+ type: 'one_tap',
118
+ unit: 1,
119
+ weekday_repeat: [],
120
+ },
112
121
  isAutomateTab: false,
113
122
  isMultiUnits: false,
114
123
  });
@@ -46,10 +46,10 @@ const AddNewOneTap = memo(({ route }) => {
46
46
  id: data.id,
47
47
  name: name,
48
48
  type: type,
49
- automate,
49
+ automate: { ...automate, ...data },
50
50
  havePermission: true,
51
51
  isCreateScriptSuccess: true,
52
- isAutomateTab: automateId ? false : isAutomateTab,
52
+ isAutomateTab: isAutomateTab,
53
53
  isMultiUnits,
54
54
  });
55
55
  }
@@ -59,8 +59,8 @@ const AddNewOneTap = memo(({ route }) => {
59
59
  type,
60
60
  name,
61
61
  automateData,
62
- automateId,
63
62
  automate,
63
+ automateId,
64
64
  navigate,
65
65
  isAutomateTab,
66
66
  ]);
@@ -141,13 +141,20 @@ const ThreePhasePowerConsumption = memo(({ unit, summary, summaryDetail }) => {
141
141
  moment().subtract(7, 'days').valueOf()
142
142
  );
143
143
  const [endDate, setEndDate] = useState(moment().valueOf());
144
+ const [groupBy, setGroupBy] = useState('date');
144
145
  const [getData, setData] = useState({});
146
+ const [chartConfig, setChartConfig] = useState({
147
+ unit: 'kWh',
148
+ price: '',
149
+ });
150
+
145
151
  useEffect(() => {
146
152
  const fetchData = async () => {
147
153
  let params = new URLSearchParams();
148
154
  params.append('config', listConfigs.total_power);
149
- params.append('date_from', new Date(startDate).setHours(0, 0) / 1000);
150
- params.append('date_to', new Date(endDate).setHours(23, 59) / 1000);
155
+ params.append('group_by', groupBy);
156
+ params.append('date_from', moment(startDate).format('YYYY-MM-DD'));
157
+ params.append('date_to', moment(endDate).format('YYYY-MM-DD'));
151
158
  const { success, data } = await axiosGet(
152
159
  API.POWER_CONSUME.DISPLAY_HISTORY,
153
160
  {
@@ -161,7 +168,7 @@ const ThreePhasePowerConsumption = memo(({ unit, summary, summaryDetail }) => {
161
168
  if (listConfigs?.total_power) {
162
169
  fetchData();
163
170
  }
164
- }, [startDate, endDate, listConfigs]);
171
+ }, [startDate, endDate, listConfigs, groupBy]);
165
172
  return (
166
173
  <>
167
174
  <Section type={'border'}>
@@ -239,13 +246,18 @@ const ThreePhasePowerConsumption = memo(({ unit, summary, summaryDetail }) => {
239
246
  <PMSensorIndicatior data={dataTotal} style={styles.styleTotalPower} />
240
247
  {!!getData?.length && (
241
248
  <HistoryChart
242
- unit={'kWh'}
243
249
  datas={getData}
250
+ chartConfig={chartConfig}
251
+ setChartConfig={setChartConfig}
244
252
  formatType={'date'}
245
253
  startDate={startDate}
246
254
  setEndDate={setEndDate}
247
255
  setStartDate={setStartDate}
248
- configuration={{ type: 'horizontal_bar_chart' }}
256
+ setGroupBy={setGroupBy}
257
+ configuration={{
258
+ type: 'horizontal_bar_chart',
259
+ config: 'power_consumption',
260
+ }}
249
261
  />
250
262
  )}
251
263
  </Section>
@@ -81,13 +81,20 @@ const PowerConsumption = memo(({ summaryDetail }) => {
81
81
  moment().subtract(7, 'days').valueOf()
82
82
  );
83
83
  const [endDate, setEndDate] = useState(moment().valueOf());
84
- const [getData, setData] = useState({});
84
+ const [groupBy, setGroupBy] = useState('date');
85
+ const [getData, setData] = useState([]);
86
+ const [chartConfig, setChartConfig] = useState({
87
+ unit: 'kWh',
88
+ price: '',
89
+ });
90
+
85
91
  useEffect(() => {
86
92
  const fetchData = async () => {
87
93
  let params = new URLSearchParams();
88
94
  params.append('config', listConfigs.total_power);
89
- params.append('date_from', new Date(startDate).setHours(0, 0) / 1000);
90
- params.append('date_to', new Date(endDate).setHours(23, 59) / 1000);
95
+ params.append('group_by', groupBy);
96
+ params.append('date_from', moment(startDate).format('YYYY-MM-DD'));
97
+ params.append('date_to', moment(endDate).format('YYYY-MM-DD'));
91
98
  const { success, data } = await axiosGet(
92
99
  API.POWER_CONSUME.DISPLAY_HISTORY(),
93
100
  {
@@ -101,7 +108,7 @@ const PowerConsumption = memo(({ summaryDetail }) => {
101
108
  if (listConfigs?.total_power) {
102
109
  fetchData();
103
110
  }
104
- }, [startDate, endDate, listConfigs]);
111
+ }, [startDate, endDate, listConfigs, groupBy]);
105
112
  return (
106
113
  <>
107
114
  <Section type={'border'}>
@@ -145,13 +152,19 @@ const PowerConsumption = memo(({ summaryDetail }) => {
145
152
  <PMSensorIndicatior data={dataTotal} style={styles.styleTotalPower} />
146
153
  {!!getData?.length && (
147
154
  <HistoryChart
148
- unit={'kWh'}
149
155
  datas={getData}
156
+ chartConfig={chartConfig}
157
+ setChartConfig={setChartConfig}
150
158
  formatType={'date'}
151
159
  startDate={startDate}
160
+ endDate={endDate}
152
161
  setEndDate={setEndDate}
153
162
  setStartDate={setStartDate}
154
- configuration={{ type: 'horizontal_bar_chart' }}
163
+ setGroupBy={setGroupBy}
164
+ configuration={{
165
+ type: 'horizontal_bar_chart',
166
+ config: 'power_consumption',
167
+ }}
155
168
  />
156
169
  )}
157
170
  </Section>
@@ -133,14 +133,10 @@ const UnitSummary = memo(({ route }) => {
133
133
  export default UnitSummary;
134
134
 
135
135
  const styles = StyleSheet.create({
136
- scrollView: {
137
- flex: 1,
138
- backgroundColor: Colors.Gray2,
139
- paddingTop: 50,
140
- },
141
136
  container: {
142
137
  flex: 1,
143
138
  backgroundColor: Colors.Gray2,
139
+ paddingBottom: 60,
144
140
  },
145
141
  textHeader: {
146
142
  color: Colors.Gray9,
@@ -379,6 +379,9 @@
379
379
  "saved_vehicle": "Saved Vehicle",
380
380
  "set_a_number": "Set a number",
381
381
  "text_total_power_consumption": "Total Power Consumption",
382
+ "input_price_to_calculate_electricity_cost": "Input price to calculate electricity cost",
383
+ "calculate_cost": "Calculate cost",
384
+ "total_power_price": "Total power price:",
382
385
  "smart_parking": "Smart Parking",
383
386
  "smart_parking_add_destination": "I would like to park near...",
384
387
  "see_nearby_parking": "See nearby parking areas",
@@ -412,6 +412,9 @@
412
412
  "save_this_vehicle": "Lưu và đặt phương tiện mặc định",
413
413
  "saved_vehicle": "Phương tiện đã lưu",
414
414
  "text_total_power_consumption": "Tổng điện năng tiêu thụ",
415
+ "input_price_to_calculate_electricity_cost": "Nhập số tiền để tính giá tiền điện đã sử dụng",
416
+ "calculate_cost": "Tính giá điện",
417
+ "total_power_price": "Tổng giá điện:",
415
418
  "smart_parking": "Smart Parking",
416
419
  "smart_parking_add_destination": "Tôi muốn đỗ xe gần...",
417
420
  "add_card": "Thêm thẻ",