@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.
- package/package.json +2 -2
- package/src/commons/Action/ItemQuickAction.js +1 -2
- package/src/commons/ActionGroup/OptionsDropdownActionTemplate.js +1 -2
- package/src/commons/ActionGroup/StatesGridActionTemplate.js +1 -2
- package/src/commons/ActionGroup/ThreeButtonTemplate.js +5 -3
- package/src/commons/DevMode/Item.js +1 -1
- package/src/commons/Device/ItemDevice.js +1 -1
- package/src/commons/Device/WindSpeed/Anemometer/index.js +13 -29
- package/src/commons/FourButtonFilterHistory/index.js +1 -1
- package/src/commons/IconComponent/index.js +17 -7
- package/src/hooks/IoT/useValueEvaluation.js +1 -3
- package/src/iot/RemoteControl/Bluetooth.js +3 -2
- package/src/screens/AddNewAction/Device/index.js +1 -2
- package/src/screens/AddNewGateway/RenameNewDevices.js +1 -1
- package/src/screens/AllGateway/GatewayDetail/index.js +2 -2
- package/src/screens/AllGateway/__test__/index.test.js +8 -2
- package/src/screens/AllGateway/components/GatewayItem/index.js +1 -1
- package/src/screens/AllGateway/hooks/useGateway.js +39 -4
- package/src/screens/AllGateway/index.js +25 -7
- package/src/screens/Device/__test__/DetailHistoryChart.test.js +25 -10
- package/src/screens/Device/components/DetailHistoryChart.js +74 -22
- package/src/screens/Notification/components/NotificationItem.js +2 -2
- package/src/screens/WaterQualityGuide/__test__/index.test.js +2 -2
- package/src/screens/WaterQualityGuide/index.js +18 -9
- package/src/utils/I18n/translations/en.json +7 -8
- 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.
|
|
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
|
|
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
|
|
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}
|
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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,
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
149
|
-
fontSize=
|
|
147
|
+
fill={Colors.Lime6}
|
|
148
|
+
fontSize={48}
|
|
149
|
+
fontWeight="bold"
|
|
150
150
|
x={width / 2}
|
|
151
|
-
y={width / 2 +
|
|
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=
|
|
159
|
+
fontSize={16}
|
|
160
160
|
x={width / 2}
|
|
161
|
-
y={width / 2 +
|
|
161
|
+
y={width / 2 + 64}
|
|
162
162
|
textAnchor="middle"
|
|
163
163
|
fontFamily={Fonts.Regular}
|
|
164
164
|
>
|
|
165
|
-
|
|
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
|
);
|
|
@@ -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:
|
|
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 (
|
|
33
|
+
if (isUrlImage) {
|
|
24
34
|
return (
|
|
25
35
|
<FImage
|
|
26
|
-
source={{ uri:
|
|
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={
|
|
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={
|
|
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 =
|
|
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
|
-
},
|
|
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?.
|
|
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 = (
|
|
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(
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
fetchDataGateways();
|
|
65
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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 [
|
|
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
|
-
|
|
15
|
+
type === 'line_chart'
|
|
16
|
+
? moment().subtract(1, 'days').valueOf()
|
|
17
|
+
: moment().subtract(6, 'days')
|
|
14
18
|
);
|
|
15
|
-
const [endDate, setEndDate] = useState(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
46
|
-
|
|
85
|
+
}, [fetchData]);
|
|
86
|
+
|
|
87
|
+
if (!lineChartData?.length) {
|
|
47
88
|
return false;
|
|
48
89
|
}
|
|
49
90
|
|
|
50
91
|
return (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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(
|
|
183
|
-
expect(textDescriptionLevel).toHaveLength(
|
|
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: '
|
|
40
|
-
title: t('
|
|
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('
|
|
49
|
+
des: t('text_clo_good_range'),
|
|
43
50
|
},
|
|
44
51
|
{
|
|
45
|
-
type: '
|
|
46
|
-
title: t('
|
|
52
|
+
type: 'High',
|
|
53
|
+
title: t('text_high_level'),
|
|
47
54
|
color: Colors.Red6,
|
|
48
|
-
des: t('
|
|
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
|
-
"
|
|
185
|
-
"
|
|
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
|
-
"
|
|
237
|
-
"
|
|
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",
|