@eohjsc/react-native-smart-city 0.3.63 → 0.3.65

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 (26) hide show
  1. package/package.json +2 -2
  2. package/src/commons/Action/ItemQuickAction.js +1 -2
  3. package/src/commons/ActionGroup/OptionsDropdownActionTemplate.js +1 -2
  4. package/src/commons/ActionGroup/StatesGridActionTemplate.js +1 -2
  5. package/src/commons/ActionGroup/ThreeButtonTemplate.js +5 -3
  6. package/src/commons/DevMode/Item.js +1 -1
  7. package/src/commons/Device/ItemDevice.js +1 -1
  8. package/src/commons/Device/WindSpeed/Anemometer/index.js +13 -29
  9. package/src/commons/FourButtonFilterHistory/index.js +1 -1
  10. package/src/commons/IconComponent/index.js +17 -7
  11. package/src/hooks/IoT/useValueEvaluation.js +1 -3
  12. package/src/iot/RemoteControl/Bluetooth.js +3 -2
  13. package/src/screens/AddNewAction/Device/index.js +1 -2
  14. package/src/screens/AddNewGateway/RenameNewDevices.js +1 -1
  15. package/src/screens/AllGateway/GatewayDetail/index.js +2 -2
  16. package/src/screens/AllGateway/__test__/index.test.js +8 -2
  17. package/src/screens/AllGateway/components/GatewayItem/index.js +1 -1
  18. package/src/screens/AllGateway/hooks/useGateway.js +39 -4
  19. package/src/screens/AllGateway/index.js +25 -7
  20. package/src/screens/Device/__test__/DetailHistoryChart.test.js +25 -10
  21. package/src/screens/Device/components/DetailHistoryChart.js +74 -22
  22. package/src/screens/Notification/components/NotificationItem.js +2 -2
  23. package/src/screens/WaterQualityGuide/__test__/index.test.js +2 -2
  24. package/src/screens/WaterQualityGuide/index.js +18 -9
  25. package/src/utils/I18n/translations/en.json +7 -8
  26. package/src/utils/I18n/translations/vi.json +7 -5
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.3.63",
4
+ "version": "0.3.65",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -102,7 +102,7 @@
102
102
  "@ant-design/icons-react-native": "^2.2.1",
103
103
  "@ant-design/react-native": "^4.0.5",
104
104
  "@babel/helper-environment-visitor": "^7.16.7",
105
- "@eohjsc/highcharts": "^1.0.9",
105
+ "@eohjsc/highcharts": "^1.1.0",
106
106
  "@eohjsc/react-native-keyboard-aware-scroll-view": "^0.9.5",
107
107
  "@formatjs/intl-getcanonicallocales": "^1.4.5",
108
108
  "@formatjs/intl-numberformat": "^5.6.2",
@@ -83,8 +83,7 @@ const ItemQuickAction = memo(({ sensor, wrapperStyle, setStatus }) => {
83
83
  >
84
84
  <View style={wrapperStyle}>
85
85
  <IconComponent
86
- icon={action.icon}
87
- iconKit={action.icon_kit}
86
+ icon={action?.icon_kit || action?.icon}
88
87
  isSendingCommand={isSendingCommand}
89
88
  icon_outlined={'poweroff'}
90
89
  />
@@ -130,8 +130,7 @@ const OptionsDropdownActionTemplate = ({
130
130
  <View style={styles.iconAndText}>
131
131
  {!checkIcon && (
132
132
  <IconComponent
133
- icon={icon}
134
- iconKit={iconKit}
133
+ icon={iconKit || icon}
135
134
  icon_outlined={icon_outlined}
136
135
  isSendingCommand={false}
137
136
  size={17}
@@ -92,8 +92,7 @@ const GridItem = ({ item, index, length, doAction, sensor, title }) => {
92
92
  >
93
93
  {!checkIcon && (
94
94
  <IconComponent
95
- icon={icon}
96
- iconKit={iconKit}
95
+ icon={iconKit || icon}
97
96
  icon_outlined={icon_outlined}
98
97
  isSendingCommand={false}
99
98
  size={24}
@@ -5,6 +5,7 @@ import styles from './ThreeButtonTemplateStyle';
5
5
  import Text from '../Text';
6
6
  import { AccessibilityLabel } from '../../configs/Constants';
7
7
  import { Colors } from '../../configs';
8
+ import IconComponent from '../IconComponent';
8
9
 
9
10
  const ThreeButtonTemplate = memo(({ actionGroup, doAction }) => {
10
11
  const { configuration } = actionGroup;
@@ -28,7 +29,7 @@ const ThreeButtonTemplate = memo(({ actionGroup, doAction }) => {
28
29
  return icon === 'stop' ? (
29
30
  <View style={styles.squareStop} />
30
31
  ) : (
31
- <Icon name={icon} size={30} color={Colors.Primary} />
32
+ <IconComponent icon={icon2} iconSize={30} />
32
33
  );
33
34
  };
34
35
  const onButton1Press = useCallback(() => {
@@ -75,6 +76,7 @@ const ThreeButtonTemplate = memo(({ actionGroup, doAction }) => {
75
76
  </>
76
77
  );
77
78
  };
79
+
78
80
  return (
79
81
  <>
80
82
  <View style={styles.wrap}>
@@ -85,7 +87,7 @@ const ThreeButtonTemplate = memo(({ actionGroup, doAction }) => {
85
87
  underlayColor={Colors.Gray2}
86
88
  >
87
89
  <View style={styles.imageButton}>
88
- <Icon name={icon1} size={30} color={Colors.Primary} />
90
+ <IconComponent icon={icon1} iconSize={30} />
89
91
  </View>
90
92
  <Text style={styles.text}>{text1}</Text>
91
93
  </TouchableOpacity>
@@ -107,7 +109,7 @@ const ThreeButtonTemplate = memo(({ actionGroup, doAction }) => {
107
109
  underlayColor={Colors.Gray2}
108
110
  >
109
111
  <View style={styles.imageButton}>
110
- <Icon name={icon3} size={30} color={Colors.Primary} />
112
+ <IconComponent icon={icon3} iconSize={30} />
111
113
  </View>
112
114
  <Text style={styles.text}>{text3}</Text>
113
115
  </TouchableOpacity>
@@ -10,7 +10,7 @@ const Item = ({ item, index, onPress }) => {
10
10
  onPress={onPress}
11
11
  style={[styles.item, index % 2 === 0 && styles.oddItem]}
12
12
  >
13
- <IconComponent iconKit={item?.icon} />
13
+ <IconComponent icon={item?.icon} />
14
14
  <Text style={styles.nameItem}>{item?.name}</Text>
15
15
  </TouchableOpacity>
16
16
  );
@@ -133,7 +133,7 @@ const ItemDevice = memo(
133
133
  >
134
134
  <View style={styles.boxIcon}>
135
135
  <TouchableOpacity onPress={goToSensorDisplay}>
136
- <IconComponent icon={sensor.icon} iconKit={sensor.icon_kit} />
136
+ <IconComponent icon={sensor?.icon_kit || sensor?.icon} />
137
137
  </TouchableOpacity>
138
138
  {canRenderQuickAction && (
139
139
  <ItemQuickAction sensor={sensor} unit={unit} />
@@ -1,5 +1,5 @@
1
1
  import React, { memo, useCallback } from 'react';
2
- import Svg, { Path, Text, G, Circle } from 'react-native-svg';
2
+ import Svg, { Path, Text, Circle } from 'react-native-svg';
3
3
  import { View, StyleSheet } from 'react-native';
4
4
 
5
5
  import {
@@ -28,6 +28,8 @@ const Anemometer = memo(
28
28
  };
29
29
 
30
30
  const value = data.length ? data[0].value : 0;
31
+ const measure = data.length ? data[0].measure : 'm/s';
32
+
31
33
  const radius = (size - strokeWidth) / 2;
32
34
  const viewBox = `0 0 ${width} ${width}`;
33
35
  const d = drawArc(center.x, center.y, radius, startAngle, endAngle);
@@ -35,8 +37,6 @@ const Anemometer = memo(
35
37
  const strokeAngle = (endAngle - startAngle) / numberOfSection;
36
38
  const strokeLength = (strokeAngle * circumference) / 360 - 1;
37
39
  const strokeDasharrayBg = `${strokeLength} 1`;
38
- const totalAngle = (3 * PI) / 2;
39
- const alpha = (value * totalAngle) / maxValue;
40
40
  const arc = circumference * 0.75;
41
41
  const offset = arc - (value / maxValue) * arc;
42
42
 
@@ -54,7 +54,7 @@ const Anemometer = memo(
54
54
  arr.push(
55
55
  <Text
56
56
  fill={txtColor}
57
- fontSize="10"
57
+ fontSize={18}
58
58
  x={textPosition.x}
59
59
  y={textPosition.y}
60
60
  textAnchor="middle"
@@ -75,7 +75,7 @@ const Anemometer = memo(
75
75
  arr.push(
76
76
  <Text
77
77
  fill={txtColor}
78
- fontSize="10"
78
+ fontSize={18}
79
79
  x={textPosition.x}
80
80
  y={textPosition.y}
81
81
  textAnchor="middle"
@@ -95,7 +95,7 @@ const Anemometer = memo(
95
95
  arr.push(
96
96
  <Text
97
97
  fill={txtColor}
98
- fontSize="10"
98
+ fontSize={18}
99
99
  x={textPosition.x}
100
100
  y={textPosition.y}
101
101
  textAnchor="middle"
@@ -116,7 +116,6 @@ const Anemometer = memo(
116
116
  maxValue,
117
117
  txtColor,
118
118
  ]);
119
- const needleAngle = -21 + (alpha / PI) * 180;
120
119
 
121
120
  return (
122
121
  <View style={styles.standard}>
@@ -145,10 +144,11 @@ const Anemometer = memo(
145
144
  />
146
145
  {textAngles()}
147
146
  <Text
148
- fill={Colors.Gray8}
149
- fontSize="24"
147
+ fill={Colors.Lime6}
148
+ fontSize={48}
149
+ fontWeight="bold"
150
150
  x={width / 2}
151
- y={width / 2 + 64}
151
+ y={width / 2 + 16}
152
152
  textAnchor="middle"
153
153
  fontFamily={Fonts.Regular}
154
154
  >
@@ -156,30 +156,14 @@ const Anemometer = memo(
156
156
  </Text>
157
157
  <Text
158
158
  fill={Colors.Gray8}
159
- fontSize="16"
159
+ fontSize={16}
160
160
  x={width / 2}
161
- y={width / 2 + 88}
161
+ y={width / 2 + 64}
162
162
  textAnchor="middle"
163
163
  fontFamily={Fonts.Regular}
164
164
  >
165
- m/s
165
+ {measure}
166
166
  </Text>
167
-
168
- <G
169
- x={width / 2 - 50}
170
- y={width / 2 - 12}
171
- rotation={needleAngle}
172
- origin="50,10."
173
- >
174
- <Path
175
- fillRule="evenodd"
176
- clipRule="evenodd"
177
- // eslint-disable-next-line max-len
178
- d="M6.75518 30.5986L46.6732 5.59447C49.7242 3.68338 53.7596 4.87054 55.2897 8.12934C56.8321 11.4146 55.1275 15.3065 51.667 16.4005L6.75518 30.5986Z"
179
- fill="#595959"
180
- />
181
- <Circle cx={49.5} cy={10.5762} r={3.5} fill="white" />
182
- </G>
183
167
  </Svg>
184
168
  </View>
185
169
  );
@@ -47,7 +47,7 @@ const FourButtonFilterHistory = memo(({ groupBy, setGroupBy }) => {
47
47
 
48
48
  const onPressButton = useCallback(
49
49
  (data) => () => {
50
- setGroupBy(data);
50
+ setGroupBy && setGroupBy(data);
51
51
  },
52
52
  [setGroupBy]
53
53
  );
@@ -1,29 +1,39 @@
1
1
  import { IconOutline, IconFill } from '@ant-design/icons-react-native';
2
- import React, { memo } from 'react';
2
+ import React, { memo, useMemo } from 'react';
3
3
  import { StyleSheet } from 'react-native';
4
4
  import { Colors } from '../../configs';
5
5
  import FImage from '../FImage';
6
6
  import Text from '../Text';
7
7
 
8
- // Priority: iconKit - icon - icon_outlined
8
+ // Priority: icon - icon_outlined
9
9
  const IconComponent = memo(
10
10
  ({
11
11
  icon_outlined,
12
12
  icon,
13
- iconKit,
14
13
  isSendingCommand = false,
15
14
  size = 40,
15
+ iconSize = 24,
16
16
  style,
17
17
  }) => {
18
18
  let extraStyle = {
19
19
  width: size,
20
20
  height: size,
21
21
  };
22
+ const isUrlImage = useMemo(() => {
23
+ if (!icon) {
24
+ return false;
25
+ }
26
+ const isUrl = icon?.match(
27
+ // eslint-disable-next-line no-useless-escape
28
+ /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g
29
+ );
30
+ return isUrl !== null;
31
+ }, [icon]);
22
32
 
23
- if (iconKit) {
33
+ if (isUrlImage) {
24
34
  return (
25
35
  <FImage
26
- source={{ uri: iconKit }}
36
+ source={{ uri: icon }}
27
37
  style={[styles.iconAction, extraStyle, style]}
28
38
  />
29
39
  );
@@ -32,7 +42,7 @@ const IconComponent = memo(
32
42
  <IconFill
33
43
  name={icon}
34
44
  color={isSendingCommand ? Colors.TextGray : Colors.Green7}
35
- size={24}
45
+ size={iconSize}
36
46
  style={style}
37
47
  />
38
48
  );
@@ -41,7 +51,7 @@ const IconComponent = memo(
41
51
  <IconOutline
42
52
  name={icon_outlined}
43
53
  color={isSendingCommand ? Colors.TextGray : Colors.Green7}
44
- size={24}
54
+ size={iconSize}
45
55
  style={style}
46
56
  />
47
57
  );
@@ -15,9 +15,7 @@ const useValueEvaluations = (unitId) => {
15
15
  if (!unitId || fetchedValueEvaluationUnits.indexOf(unitId) !== -1) {
16
16
  return;
17
17
  }
18
- const params = new URLSearchParams();
19
- params.append('config__end_device__station__unit', unitId);
20
- params.append('page', page);
18
+ const params = { configs__end_device__station__unit: unitId, page: page };
21
19
  const { success, data } = await axiosGet(API.VALUE_EVALUATIONS(), {
22
20
  params,
23
21
  });
@@ -71,12 +71,13 @@ export const realScanBluetoothDevices = async (onDeviceFound) => {
71
71
  }
72
72
  });
73
73
 
74
- setTimeout(() => {
74
+ const to = setTimeout(() => {
75
75
  try {
76
76
  bleManager.stopDeviceScan();
77
+ clearTimeout(to);
77
78
  // eslint-disable-next-line no-empty
78
79
  } catch {}
79
- }, 15000);
80
+ }, 30000);
80
81
  };
81
82
 
82
83
  export const sendCommandOverBluetooth = async (
@@ -21,8 +21,7 @@ const Device = memo(({ svgMain, sensor, title, isSelectDevice, onPress }) => {
21
21
  <View style={[styles.container, isActive]}>
22
22
  <View style={styles.boxIcon}>
23
23
  <IconComponent
24
- icon={svgMain}
25
- iconKit={sensor.icon_kit}
24
+ icon={sensor?.icon_kit || svgMain}
26
25
  style={styles.iconSensor}
27
26
  />
28
27
  </View>
@@ -143,7 +143,7 @@ const RenameNewDevices = memo(({ route }) => {
143
143
  {isGateway ? (
144
144
  <GatewayIcon width={43} height={43} />
145
145
  ) : (
146
- <IconComponent icon={item?.icon} iconKit={item?.icon_kit} />
146
+ <IconComponent icon={item?.icon_kit || item?.icon} />
147
147
  )}
148
148
  <View style={styles.viewTextInput}>
149
149
  {isCanRenameItem ? (
@@ -112,13 +112,13 @@ const GatewayDetail = () => {
112
112
  };
113
113
  const RouteRebootGateway = {
114
114
  id: 2,
115
- text: t('reboot'),
115
+ text: `${t('reboot')} ${t('gateway')?.toLowerCase()}`,
116
116
  textStyle: styles.textColorRed,
117
117
  doAction: handleRebootGateway,
118
118
  };
119
119
  const ListDeleteGateway = {
120
120
  id: 3,
121
- text: t('delete'),
121
+ text: `${t('delete')} ${t('gateway')?.toLowerCase()}`,
122
122
  textStyle: styles.textColorRed,
123
123
  doAction: handleDeleteGateway,
124
124
  };
@@ -25,9 +25,15 @@ jest.mock('@react-navigation/native', () => {
25
25
  };
26
26
  });
27
27
 
28
- const wrapComponent = (route) => (
28
+ const wrapComponent = (
29
+ route = {
30
+ params: {
31
+ unitId: 1,
32
+ },
33
+ }
34
+ ) => (
29
35
  <SCProvider initState={mockSCStore({})}>
30
- <Gateway />
36
+ <Gateway route={route} />
31
37
  </SCProvider>
32
38
  );
33
39
 
@@ -15,7 +15,7 @@ const GatewayItem = ({ item, onPress }) => {
15
15
  </View>
16
16
  <View style={styles.viewValue}>
17
17
  <Text type="Body" color={Colors.Gray20} style={styles.textValue}>
18
- {`${item?.last_healthy || '--'} dBm`}
18
+ {`${item?.last_healthy?.signal || '--'} dBm`}
19
19
  </Text>
20
20
  </View>
21
21
  <StatusBox status={item?.is_connected} />
@@ -23,6 +23,9 @@ export const useGateway = () => {
23
23
  const [detailDeviceModbus, setDetailDeviceModbus] = useState({});
24
24
  const [detailDeviceInternal, setDetailDeviceInternal] = useState({});
25
25
  const [dataModalPopupCT, setDataModalPopupCT] = useState({});
26
+ const [pages, setPages] = useState(1);
27
+ const [loadingMore, setLoadingMore] = useState(false);
28
+ const [canLoadMore, setCanLoadMore] = useState(true);
26
29
 
27
30
  const hideModalPopupCT = useCallback(() => {
28
31
  setDataModalPopupCT((prevData) => ({
@@ -191,10 +194,39 @@ export const useGateway = () => {
191
194
  []
192
195
  );
193
196
 
194
- const fetchDataGateways = useCallback(async () => {
195
- const { success, data } = await axiosGet(API.DEV_MODE.GATEWAY.LIST());
196
- success && setGateways(data?.results || []);
197
- }, []);
197
+ const fetchDataGateways = useCallback(
198
+ async (selectedPage, unitId) => {
199
+ if (!canLoadMore) {
200
+ return;
201
+ }
202
+ setPages(selectedPage);
203
+ if (selectedPage === 1) {
204
+ setRefresh(true);
205
+ } else {
206
+ setLoadingMore(true);
207
+ }
208
+ const params = new URLSearchParams();
209
+ params.append('page', selectedPage);
210
+ params.append('unit', unitId);
211
+ const { success, data } = await axiosGet(API.DEV_MODE.GATEWAY.LIST(), {
212
+ params,
213
+ });
214
+ if (success) {
215
+ if (selectedPage === 1) {
216
+ setGateways(data?.results || []);
217
+ } else {
218
+ setGateways((prevData) => prevData.concat(data?.results));
219
+ setCanLoadMore(!(data?.results?.length < 10));
220
+ }
221
+ }
222
+ if (selectedPage === 1) {
223
+ setRefresh(false);
224
+ } else {
225
+ setLoadingMore(false);
226
+ }
227
+ },
228
+ [canLoadMore]
229
+ );
198
230
 
199
231
  return {
200
232
  gateways,
@@ -225,5 +257,8 @@ export const useGateway = () => {
225
257
  hideModalPopupCT,
226
258
  fetchConfigActionInterval,
227
259
  detailDeviceInternal,
260
+ loadingMore,
261
+ pages,
262
+ setCanLoadMore,
228
263
  };
229
264
  };
@@ -11,17 +11,23 @@ import Routes from '../../utils/Route';
11
11
  import styles from './styles';
12
12
  import { HeaderCustom } from '../../commons/Header';
13
13
 
14
+ let onEndReachedCalledDuringMomentum = false;
15
+
14
16
  const AllGateway = ({ route }) => {
15
17
  const t = useTranslations();
16
18
  const isFocused = useIsFocused();
17
19
  const { navigate } = useNavigation();
20
+ const { unitId } = route?.params;
21
+
18
22
  const {
19
23
  gateways,
20
24
  fetchDataGateways,
21
25
  setNameSearch,
22
26
  nameSearch,
23
27
  refresh,
24
- setRefresh,
28
+ pages,
29
+ loadingMore,
30
+ setCanLoadMore,
25
31
  } = useGateway();
26
32
 
27
33
  const goGoDetail = useCallback(
@@ -60,14 +66,23 @@ const AllGateway = ({ route }) => {
60
66
  );
61
67
 
62
68
  const onRefresh = useCallback(() => {
63
- setRefresh(true);
64
- fetchDataGateways();
65
- setRefresh(false);
66
- }, [fetchDataGateways, setRefresh]);
69
+ setCanLoadMore(true);
70
+ fetchDataGateways(1, unitId);
71
+ }, [fetchDataGateways, setCanLoadMore, unitId]);
67
72
 
68
73
  useEffect(() => {
69
- isFocused && fetchDataGateways();
70
- }, [isFocused, fetchDataGateways]);
74
+ isFocused && fetchDataGateways(1, unitId);
75
+ }, [isFocused, fetchDataGateways, unitId]);
76
+
77
+ const onMomentumScrollBegin = () =>
78
+ (onEndReachedCalledDuringMomentum = false);
79
+
80
+ const onLoadMore = useCallback(() => {
81
+ if (!onEndReachedCalledDuringMomentum) {
82
+ onEndReachedCalledDuringMomentum = true;
83
+ fetchDataGateways(pages + 1, unitId);
84
+ }
85
+ }, [fetchDataGateways, pages, unitId]);
71
86
 
72
87
  return (
73
88
  <>
@@ -86,6 +101,7 @@ const AllGateway = ({ route }) => {
86
101
  <FlatList
87
102
  onRefresh={onRefresh}
88
103
  refreshing={refresh}
104
+ isLoadMore={loadingMore}
89
105
  columnWrapperStyle={styles.spaceBetween}
90
106
  showsVerticalScrollIndicator={false}
91
107
  showsHorizontalScrollIndicator={false}
@@ -99,6 +115,8 @@ const AllGateway = ({ route }) => {
99
115
  extraData={gatewaysSearched}
100
116
  ListEmptyComponent={renderListEmptyComponent}
101
117
  numColumns={2}
118
+ onMomentumScrollBegin={onMomentumScrollBegin}
119
+ onEndReached={onLoadMore}
102
120
  />
103
121
  </View>
104
122
  </>
@@ -1,17 +1,16 @@
1
1
  import React from 'react';
2
2
  import renderer, { act } from 'react-test-renderer';
3
+ import MockAdapter from 'axios-mock-adapter';
4
+
3
5
  import { DetailHistoryChart } from '../components/DetailHistoryChart';
4
6
  import { SCProvider } from '../../../context';
5
7
  import { mockSCStore } from '../../../context/mockStore';
6
8
  import HistoryChart from '../../../commons/Device/HistoryChart';
7
- const mockSetState = jest.fn();
8
- jest.mock('react', () => {
9
- return {
10
- ...jest.requireActual('react'),
11
- memo: (x) => x,
12
- useState: jest.fn((init) => [init, mockSetState]),
13
- };
14
- });
9
+ import api from '../../../utils/Apis/axios';
10
+ import { API } from '../../../configs';
11
+
12
+ const mock = new MockAdapter(api.axiosInstance);
13
+
15
14
  const wrapComponent = (item, sensor) => (
16
15
  <SCProvider initState={mockSCStore({})}>
17
16
  <DetailHistoryChart item={item} sensor={sensor} />
@@ -20,12 +19,28 @@ const wrapComponent = (item, sensor) => (
20
19
  describe('Test DetailHistoryChart', () => {
21
20
  let tree;
22
21
  it('create DetailHistoryChart', async () => {
22
+ mock
23
+ .onGet(API.VALUE_CONSUME.DISPLAY_HISTORY())
24
+ .reply(200, [{ data: [1, 2, 3] }]);
23
25
  const item = {
26
+ id: 10452,
27
+ order: 0,
28
+ template: 'history',
29
+ type: 'history',
24
30
  configuration: {
25
- configs: [0],
26
- icon: 'slack',
31
+ type: 'horizontal_bar_chart',
32
+ date_format: 'DD.MM',
33
+ configs: [
34
+ {
35
+ id: 9848,
36
+ title: 'horizontal',
37
+ color: 'blue',
38
+ },
39
+ ],
27
40
  },
41
+ is_configuration_ready: true,
28
42
  };
43
+
29
44
  const sensor = {
30
45
  name: 'Sensor name',
31
46
  is_managed_by_backend: false,
@@ -1,5 +1,5 @@
1
1
  import moment from 'moment';
2
- import React, { useEffect, useState } from 'react';
2
+ import React, { useCallback, useEffect, useState } from 'react';
3
3
  import { StyleSheet } from 'react-native';
4
4
  import HistoryChart from '../../../commons/Device/HistoryChart';
5
5
  import { API } from '../../../configs';
@@ -7,20 +7,40 @@ import { axiosGet } from '../../../utils/Apis/axios';
7
7
 
8
8
  export const DetailHistoryChart = ({ item = {}, sensor = {} }) => {
9
9
  const { configuration = {} } = item;
10
- const { configs = [] } = configuration;
11
- const [chartData, setChartData] = useState(configs);
10
+ const { configs = [], type = '' } = configuration;
11
+ const [lineChartData, setLineChartData] = useState(configs);
12
+ const [horizontalChartData, setHorizontalChartData] = useState([]);
13
+ const [groupBy, setGroupBy] = useState('date');
12
14
  const [startDate, setStartDate] = useState(
13
- moment().subtract(1, 'days').valueOf()
15
+ type === 'line_chart'
16
+ ? moment().subtract(1, 'days').valueOf()
17
+ : moment().subtract(6, 'days')
14
18
  );
15
- const [endDate, setEndDate] = useState(moment().valueOf());
16
- useEffect(() => {
17
- const fetchData = async () => {
18
- let params = new URLSearchParams();
19
- configs.map((config) => {
20
- params.append('config', config.id);
21
- });
19
+ const [endDate, setEndDate] = useState(
20
+ type === 'line_chart' ? moment().valueOf() : moment()
21
+ );
22
+ const [chartConfig, setChartConfig] = useState({
23
+ unit: '',
24
+ });
25
+ const [isLoading, setIsLoading] = useState(false);
26
+
27
+ const fetchData = useCallback(async () => {
28
+ let params = new URLSearchParams();
29
+ configs.map((config) => {
30
+ params.append('config', config.id);
31
+ });
32
+
33
+ if (type !== 'line_chart') {
34
+ params.append('group_by', groupBy);
35
+ if (groupBy === 'date') {
36
+ params.append('date_from', moment(startDate).format('YYYY-MM-DD'));
37
+ params.append('date_to', moment(endDate).format('YYYY-MM-DD'));
38
+ }
39
+ } else {
22
40
  params.append('date_from', startDate / 1000);
23
41
  params.append('date_to', endDate / 1000);
42
+ }
43
+ if (type === 'line_chart') {
24
44
  const { success, data } = await axiosGet(
25
45
  API.DEVICE.DISPLAY_HISTORY(sensor.id),
26
46
  { params }
@@ -38,23 +58,55 @@ export const DetailHistoryChart = ({ item = {}, sensor = {} }) => {
38
58
  };
39
59
  return { ...config, data: dataChart.data };
40
60
  });
41
- setChartData(formatData);
61
+ setLineChartData(formatData);
62
+ setIsLoading(true);
63
+ }
64
+ }
65
+ if (type === 'horizontal_bar_chart') {
66
+ const { success, data } = await axiosGet(
67
+ API.VALUE_CONSUME.DISPLAY_HISTORY(),
68
+ {
69
+ params,
70
+ }
71
+ );
72
+ if (success) {
73
+ if (data[0]?.data) {
74
+ setHorizontalChartData(data);
75
+ setIsLoading(true);
76
+ } else {
77
+ setIsLoading(false);
78
+ }
42
79
  }
43
- };
80
+ }
81
+ }, [configs, endDate, groupBy, sensor.id, startDate, type]);
82
+
83
+ useEffect(() => {
44
84
  fetchData();
45
- }, [startDate, endDate, sensor, configs]);
46
- if (!chartData.length) {
85
+ }, [fetchData]);
86
+
87
+ if (!lineChartData?.length) {
47
88
  return false;
48
89
  }
49
90
 
50
91
  return (
51
- <HistoryChart
52
- configuration={item.configuration}
53
- datas={chartData}
54
- style={styles.chartStyle}
55
- setStartDate={setStartDate}
56
- setEndDate={setEndDate}
57
- />
92
+ <>
93
+ {isLoading && (
94
+ <HistoryChart
95
+ configuration={item.configuration}
96
+ datas={type === 'line_chart' ? lineChartData : horizontalChartData}
97
+ setStartDate={setStartDate}
98
+ startDate={startDate}
99
+ setEndDate={setEndDate}
100
+ endDate={endDate}
101
+ groupBy={groupBy}
102
+ setGroupBy={setGroupBy}
103
+ chartConfig={chartConfig}
104
+ setChartConfig={setChartConfig}
105
+ formatType={type !== 'line_chart' && 'date'}
106
+ style={styles.chartStyle}
107
+ />
108
+ )}
109
+ </>
58
110
  );
59
111
  };
60
112
 
@@ -199,7 +199,7 @@ const NotificationItem = memo(({ item }) => {
199
199
  },
200
200
  }),
201
201
  iconContent: (
202
- <IconComponent iconKit={icon} style={styles.iconNotification} />
202
+ <IconComponent icon={icon} style={styles.iconNotification} />
203
203
  ),
204
204
  };
205
205
  case NOTIFICATION_TYPES.NOTIFY_REMOVE_UNIT:
@@ -378,7 +378,7 @@ const NotificationItem = memo(({ item }) => {
378
378
  <View style={[styles.container, !isRead && styles.backgroundGray]}>
379
379
  <View style={styles.wrapIcon}>
380
380
  {iconContent || (
381
- <IconComponent iconKit={icon} style={styles.iconNotification} />
381
+ <IconComponent icon={icon} style={styles.iconNotification} />
382
382
  )}
383
383
  </View>
384
384
 
@@ -179,8 +179,8 @@ describe('Test WaterQualityGuide', () => {
179
179
 
180
180
  expect(textTitle).toHaveLength(2);
181
181
  expect(textDescription).toHaveLength(2);
182
- expect(textTitleLevel).toHaveLength(2);
183
- expect(textDescriptionLevel).toHaveLength(2);
182
+ expect(textTitleLevel).toHaveLength(3);
183
+ expect(textDescriptionLevel).toHaveLength(3);
184
184
 
185
185
  expect(textTitle1).toHaveLength(0);
186
186
  expect(textDescription1).toHaveLength(0);
@@ -29,23 +29,30 @@ const WaterQualityGuide = memo(({ route }) => {
29
29
  {t('text_recommended_clo')}{' '}
30
30
  <Text bold color={Colors.Gray8}>
31
31
  {t('text_recommended_clo_range')}
32
- </Text>
32
+ </Text>{' '}
33
+ {t('text_recommend_ref')}
33
34
  </>
34
35
  ),
35
36
  },
36
37
  ],
37
38
  level: [
38
39
  {
39
- type: 'Very Good',
40
- title: t('text_very_good_level'),
40
+ type: 'Low',
41
+ title: t('text_low_level'),
42
+ color: Colors.Yellow6,
43
+ des: t('text_clo_low_range'),
44
+ },
45
+ {
46
+ type: 'Good',
47
+ title: t('text_good_level'),
41
48
  color: Colors.Green6,
42
- des: t('text_clo_very_good_range'),
49
+ des: t('text_clo_good_range'),
43
50
  },
44
51
  {
45
- type: 'Poor',
46
- title: t('text_poor_level'),
52
+ type: 'High',
53
+ title: t('text_high_level'),
47
54
  color: Colors.Red6,
48
- des: t('text_clo_poor_range'),
55
+ des: t('text_clo_high_range'),
49
56
  },
50
57
  ],
51
58
  titles1: [],
@@ -70,7 +77,8 @@ const WaterQualityGuide = memo(({ route }) => {
70
77
  {t('text_turbidity_recommend')}{' '}
71
78
  <Text bold color={Colors.Gray8}>
72
79
  {t('text_turbidity_recommend_range')}
73
- </Text>
80
+ </Text>{' '}
81
+ {t('text_recommend_ref')}
74
82
  </>
75
83
  ),
76
84
  },
@@ -111,7 +119,8 @@ const WaterQualityGuide = memo(({ route }) => {
111
119
  {t('text_ph_recommend')}{' '}
112
120
  <Text bold color={Colors.Gray8}>
113
121
  {t('text_ph_recommend_range')}
114
- </Text>
122
+ </Text>{' '}
123
+ {t('text_recommend_ref')}
115
124
  </>
116
125
  ),
117
126
  },
@@ -180,9 +180,10 @@
180
180
  "text_what_clo": "Residual chlorine is the low level amount of chlorine remaining in the water after a certain period of contact time after its initial application. It constitutes an important safeguard against the risk of subsequent microbial contamination after treatment—a unique and significant benefit for public health.\nTesting for residual chlorine is one of the most common tests used by water treatment plants. Through the residual chlorine test, the remaining chlorine amount is determined in water that has finished testing and is ready to be released in the distribution system.",
181
181
  "recommended_clo_level": "Recommended Chlorine residual levels",
182
182
  "text_recommended_clo": "For normal domestic use, residual chlorine levels at the point where the consumer collects water should be",
183
- "text_recommended_clo_range": "between 0.2 and 1 mg/l.",
184
- "text_clo_very_good_range": "0.2 - 1 mg/l",
185
- "text_clo_poor_range": "< 0.2 mg/l\n> 1 mg/l",
183
+ "text_recommended_clo_range": "between 0.2 and 1 mg/l",
184
+ "text_clo_good_range": "0.2 - 1 mg/l",
185
+ "text_clo_high_range": "> 1 mg/l",
186
+ "text_clo_low_range": "< 0.2 mg/l",
186
187
  "text_low_level": "Low",
187
188
  "text_high_level": "High",
188
189
  "launch_one_tap": "Launch One-Tap",
@@ -199,15 +200,12 @@
199
200
  "text_moderate_level": "Moderate",
200
201
  "text_poor_level": "Poor",
201
202
  "text_very_poor_level": "Very poor",
202
- "text_clo_low_range": "< 0.2 mg/l",
203
- "text_clo_good_range": "0.2 - 0.5 mg/l",
204
- "text_clo_high_range": "> 0.5 mg/l",
205
203
  "turbidity_guide": "Water Turbidity Guide",
206
204
  "what_is_turbidity": "What is Water Turbidity?",
207
205
  "text_what_is_turbidity": "Turbidity is the amount of cloudiness in the water. This can vary from a river full of mud and silt where it would be impossible to see through the water (high turbidity), to a spring water which appears to be completely clear (low turbidity). Turbidity can be caused by :\n- silt, sand and mud ;\n- bacteria and other germs ;\n- chemical precipitates.",
208
206
  "turbidity_recommend": "Recommended Turbidity levels",
209
207
  "text_turbidity_recommend": "For normal domestic use, residual chlorine levels at the point where the consumer collects water should be",
210
- "text_turbidity_recommend_range": "less than 2 NTU.",
208
+ "text_turbidity_recommend_range": "less than 2 NTU",
211
209
  "text_turbidity_very_good_range": "0 - 2 NTU",
212
210
  "text_turbidity_poor_range": "> 2 NTU",
213
211
  "ph_guide": "pH Guide",
@@ -215,7 +213,8 @@
215
213
  "text_what_ph": "pH is a measurement of electrically charged particles in a substance. It indicates how acidic or alkaline (basic) that substance is.",
216
214
  "ph_recommend": "Recommended pH levels",
217
215
  "text_ph_recommend": "For normal domestic use, pH levels at the point where the consumer collects water should be",
218
- "text_ph_recommend_range": "between 6.0 and 8.5.",
216
+ "text_ph_recommend_range": "between 6.0 and 8.5",
217
+ "text_recommend_ref": "(The National technical regulation on domestic water quality QCVN 01-1:2018/BYT)",
219
218
  "text_ph_very_good_range": "pH 6.0 - 8.5",
220
219
  "text_ph_poor_range": "pH < 6.0\npH > 8.5",
221
220
  "ph_scale": "The pH scale",
@@ -225,7 +225,7 @@
225
225
  "text_what_clo": "Clo dư là lượng clo ở mức thấp vẫn còn trong nước sau một thời gian nhất định hoặc tiếp xúc thời gian sau khi ứng dụng ban đầu của nó. Nó tạo nên một lớp bảo vệ chống lại nguy cơ vi sinh vật tiếp theo gây ô nhiễm sau khi xử lý - một lợi ích độc đáo cho sức khỏe cộng đồng.\nThử nghiệm clo dư là một trong những thử nghiệm phổ biến nhất được các nhà máy xử lý nước sử dụng. Qua thử nghiệm clo dư, lượng clo còn lại được xác định trong nước đã kết thúc thử nghiệm và sẵn sàng đưa ra hệ thống phân phối.",
226
226
  "recommended_clo_level": "Mức độ Clo dư khuyến nghị",
227
227
  "text_recommended_clo": "Đối với mục đích sử dụng bình thường trong sinh hoạt, mức độ Clo tại điểm người tiêu dùng lấy nước phải nằm trong khoảng",
228
- "text_recommended_clo_range": "từ 0.2 đến 1 mg/l.",
228
+ "text_recommended_clo_range": "từ 0.2 đến 1 mg/l",
229
229
  "text_low_level": "Thấp",
230
230
  "text_high_level": "Cao",
231
231
  "text_very_good_level": "Rất tốt",
@@ -233,14 +233,15 @@
233
233
  "text_moderate_level": "Trung bình",
234
234
  "text_poor_level": "Kém",
235
235
  "text_very_poor_level": "Rất kém",
236
- "text_clo_very_good_range": "0.2 - 1mg/l",
237
- "text_clo_poor_range": "< 0.2mg/l\n> 1mg/l",
236
+ "text_clo_good_range": "0.2 - 1mg/l",
237
+ "text_clo_high_range": "> 1 mg/l",
238
+ "text_clo_low_range": "< 0.2 mg/l",
238
239
  "turbidity_guide": "Độ đục",
239
240
  "what_is_turbidity": "Độ đục của nước là gì?",
240
241
  "text_what_is_turbidity": "Độ đục của nước là thước đo độ trong tương đối của nước. Độ đục là một đặc tính quang học của nước và là một biểu hiện của lượng ánh sáng được phân tán bởi vật liệu trong nước khi ánh sáng được chiếu xuyên qua mẫu nước. Cường độ ánh sáng càng cao thì độ đục càng cao. Độ đục có thể đến từ các hạt vật chất lơ lửng như bùn, đất sét, vật liêu vô cơ hoặc các chất hữu cơ như tảo, sinh vật phù du, vật liệu phân rã.",
241
242
  "turbidity_recommend": "Mức độ đục của nước khuyến nghị",
242
243
  "text_turbidity_recommend": "Đối với mục đích sử dụng bình thường trong sinh hoạt, mức độ pH tại điểm người tiêu dùng lấy nước phải",
243
- "text_turbidity_recommend_range": "thấp hơn 2 NTU.",
244
+ "text_turbidity_recommend_range": "thấp hơn 2 NTU",
244
245
  "text_turbidity_very_good_range": "0 - 2 NTU",
245
246
  "text_turbidity_poor_range": "> 2 NTU",
246
247
  "ph_guide": "Độ pH",
@@ -248,7 +249,8 @@
248
249
  "text_what_ph": "pH là một chỉ số xác định tính chất hoá học của nước. Nó cho biết chất đó có tính axit hoặc kiềm (bazơ) như thế nào.",
249
250
  "ph_recommend": "Mức độ pH khuyến nghị",
250
251
  "text_ph_recommend": "Đối với mục đích sử dụng bình thường trong sinh hoạt, mức độ pH tại điểm người tiêu dùng lấy nước phải nằm trong khoảng",
251
- "text_ph_recommend_range": "từ 6.0 đến 8.5.",
252
+ "text_ph_recommend_range": "từ 6.0 đến 8.5",
253
+ "text_recommend_ref": "(Quy chuẩn kỹ thuật quốc gia về chất lượng nước sinh hoạt QCVN 01-1:2018/BYT)",
252
254
  "text_ph_very_good_range": "pH 6.0 - 8.5",
253
255
  "text_ph_poor_range": "pH < 6.0\npH > 8.5",
254
256
  "ph_scale": "Thang đo pH",