@eohjsc/react-native-smart-city 0.2.78 → 0.2.82

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.78",
4
+ "version": "0.2.82",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -97,10 +97,10 @@
97
97
  "dependencies": {
98
98
  "@ant-design/icons-react-native": "^2.2.1",
99
99
  "@ant-design/react-native": "^4.0.5",
100
+ "@eohjsc/highcharts": "^1.0.3",
100
101
  "@formatjs/intl-getcanonicallocales": "^1.4.5",
101
102
  "@formatjs/intl-numberformat": "^5.6.2",
102
103
  "@formatjs/intl-pluralrules": "^3.4.7",
103
- "@highcharts/highcharts-react-native": "highcharts/highcharts-react-native#107/head",
104
104
  "@invertase/react-native-apple-authentication": "^1.1.2",
105
105
  "@messageformat/core": "^3.0.0",
106
106
  "@react-native-community/async-storage": "^1.12.1",
@@ -1,6 +1,6 @@
1
1
  import React, { memo, useEffect, useMemo, useState } from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
- import HighchartsReactNative from '@highcharts/highcharts-react-native/src/HighchartsReactNative';
3
+ import HighchartsReactNative from '@eohjsc/highcharts';
4
4
  import { isEmpty } from 'lodash';
5
5
 
6
6
  import { Colors } from '../../configs';
@@ -1,6 +1,6 @@
1
1
  import React, { memo, useState, useEffect, useCallback } from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
- import HighchartsReactNative from '@highcharts/highcharts-react-native';
3
+ import HighchartsReactNative from '@eohjsc/highcharts';
4
4
  import moment from 'moment';
5
5
  import { Colors } from '../../configs';
6
6
 
@@ -41,9 +41,9 @@ describe('Test ImagePicker', () => {
41
41
  Platform.OS = 'android';
42
42
  const options = {
43
43
  mediaType: 'photo',
44
- maxWidth: 1024,
45
- quality: 0.8,
46
- saveToPhotos: true,
44
+ compressImageMaxWidth: 1280,
45
+ compressImageMaxHeight: 720,
46
+ compressImageQuality: 0.8,
47
47
  };
48
48
  act(() => {
49
49
  tree = renderer.create(wrapComponent(options));
@@ -51,5 +51,26 @@ describe('Test ImagePicker', () => {
51
51
  const instance = tree.root;
52
52
  const textInputs = instance.findAllByType(ButtonPopup);
53
53
  expect(textInputs.length).toBe(1);
54
+ act(() => {
55
+ textInputs[0].props.onPressMain();
56
+ });
57
+ });
58
+ test('create ImagePicker onChooseFile', () => {
59
+ Platform.OS = 'android';
60
+ const options = {
61
+ mediaType: 'photo',
62
+ compressImageMaxWidth: 1280,
63
+ compressImageMaxHeight: 720,
64
+ compressImageQuality: 0.8,
65
+ };
66
+ act(() => {
67
+ tree = renderer.create(wrapComponent(options));
68
+ });
69
+ const instance = tree.root;
70
+ const textInputs = instance.findAllByType(ButtonPopup);
71
+ expect(textInputs.length).toBe(1);
72
+ act(() => {
73
+ textInputs[0].props.onPressSecondary();
74
+ });
54
75
  });
55
76
  });
@@ -164,6 +164,7 @@ const MediaPlayerDetail = ({
164
164
  activeOpacity={1}
165
165
  style={styles.videoBtn}
166
166
  onPress={onTapGoDetail}
167
+ testID={TESTID.SUB_UNIT_GO_DETAIL}
167
168
  >
168
169
  {paused ? (
169
170
  <View style={[styles.player, style]}>
@@ -31,9 +31,12 @@ const SubUnitFavorites = ({
31
31
  favorites.devices.forEach((sensor) => {
32
32
  params.append('sensors', sensor.id);
33
33
  });
34
- const { success, data } = await axiosGet(API.SENSOR.STATUS(), {
35
- params: params,
36
- });
34
+ const { success, data } = await axiosGet(
35
+ API.UNIT.SENSORS_STATUS(unit.id),
36
+ {
37
+ params: params,
38
+ }
39
+ );
37
40
  if (success) {
38
41
  setSensorsStatus(data);
39
42
  }
@@ -45,7 +48,7 @@ const SubUnitFavorites = ({
45
48
  } else {
46
49
  clearInterval(intervalSensorStatus.current);
47
50
  }
48
- }, [isFocused, favorites?.devices]);
51
+ }, [isFocused, favorites?.devices, unit.id]);
49
52
 
50
53
  const handleOnAddNew = () => {
51
54
  alert(t('feature_under_development'));
@@ -35,9 +35,12 @@ const ShortDetailSubUnit = ({ unit, station, isGGHomeConnected }) => {
35
35
  station.sensors.forEach((sensor) => {
36
36
  params.append('sensors', sensor.id);
37
37
  });
38
- const { success, data } = await axiosGet(API.SENSOR.STATUS(), {
39
- params: params,
40
- });
38
+ const { success, data } = await axiosGet(
39
+ API.UNIT.SENSORS_STATUS(unit.id),
40
+ {
41
+ params: params,
42
+ }
43
+ );
41
44
  if (success) {
42
45
  setSensorsStatus(data);
43
46
  }
@@ -49,7 +52,7 @@ const ShortDetailSubUnit = ({ unit, station, isGGHomeConnected }) => {
49
52
  } else {
50
53
  clearInterval(intervalSensorStatus.current);
51
54
  }
52
- }, [isFocused, station?.sensors]);
55
+ }, [isFocused, station?.sensors, unit.id]);
53
56
 
54
57
  const renderCamera = () => {
55
58
  if (station?.camera) {
@@ -35,6 +35,8 @@ const API = {
35
35
  SCConfig.apiRoot + `/property_manager/units/${id}/device_sensor/`,
36
36
  ADD_GATEWAY: (id) =>
37
37
  SCConfig.apiRoot + `/property_manager/units/${id}/add_gateway/`,
38
+ SENSORS_STATUS: (id) =>
39
+ SCConfig.apiRoot + `/property_manager/units/${id}/sensors_status/`,
38
40
  },
39
41
  SUB_UNIT: {
40
42
  REMOVE_SUB_UNIT: (unitId, id) =>
@@ -87,7 +89,6 @@ const API = {
87
89
  CHANGE_SUB_UNIT: (unit_id, station_id, id) =>
88
90
  SCConfig.apiRoot +
89
91
  `/property_manager/${unit_id}/sub_units/${station_id}/devices/${id}/change_sub_unit/`,
90
- STATUS: () => SCConfig.apiRoot + '/property_manager/sensors/status/',
91
92
  },
92
93
  SHARED_SENSOR: {
93
94
  ACCESS: (id) =>
@@ -210,6 +210,7 @@ export const TESTID = {
210
210
  SUB_UNIT_STATION: 'SUB_UNIT_STATION',
211
211
  SUB_UNIT_SELECT_AUTOMATE_TYPE: 'SUB_UNIT_SELECT_AUTOMATE_TYPE',
212
212
  SUB_UNIT_TEXT_DROPDOWN: 'SUB_UNIT_TEXT_DROPDOWN',
213
+ SUB_UNIT_GO_DETAIL: 'SUB_UNIT_GO_DETAIL',
213
214
 
214
215
  // NavBar
215
216
  NAVBAR_ICON_BARS: 'NAVBAR_ICON_BARS',
@@ -436,6 +437,7 @@ export const TESTID = {
436
437
  MANAGE_UNIT_LIST_EMERGENCY_CONTACT: 'MANAGE_UNIT_LIST_EMERGENCY_CONTACT',
437
438
  MANAGE_UNIT_DETAIL_LIST_CONTACT_BUTTON:
438
439
  'MANAGE_UNIT_DETAIL_LIST_CONTACT_BUTTON',
440
+ MANAGE_UNIT_GO_TO_SUBUNIT: 'MANAGE_UNIT_GO_TO_SUBUNIT',
439
441
 
440
442
  // Add Vehicle
441
443
  ADD_VEHICLE_TAKE_PHOTO: 'ADD_VEHICLE_TAKE_PHOTO',
@@ -635,6 +637,7 @@ export const NOTIFICATION_TYPES = {
635
637
  NOTIFY_REMOVE_UNIT: 'NOTIFY_REMOVE_UNIT',
636
638
  NOTIFY_REMOVE_MEMBER: 'NOTIFY_REMOVE_MEMBER',
637
639
  NOTIFY_MEMBER_LEAVE_UNIT: 'NOTIFY_MEMBER_LEAVE_UNIT',
640
+ NOTIFY_RENAME_UNIT: 'NOTIFY_RENAME_UNIT',
638
641
  };
639
642
 
640
643
  export const ACTIVITY_LOG_TYPES = {
@@ -24,6 +24,8 @@ const AddCommonSelectSubUnit = ({ route }) => {
24
24
  listSelectDevice,
25
25
  smart_account_id,
26
26
  smart_account_id_from_backend,
27
+ username,
28
+ brand,
27
29
  } = route.params;
28
30
  const [selectedIndex, setSelectedIndex] = useState(-1);
29
31
  const [unit, setUnit] = useState([]);
@@ -86,6 +88,8 @@ const AddCommonSelectSubUnit = ({ route }) => {
86
88
  smart_account_id: smart_account_id,
87
89
  unit_id: unit_id,
88
90
  smart_account_id_from_backend: smart_account_id_from_backend,
91
+ username,
92
+ brand,
89
93
  });
90
94
  break;
91
95
  case 'AddVconnexDevice':
@@ -115,6 +119,8 @@ const AddCommonSelectSubUnit = ({ route }) => {
115
119
  smart_account_id,
116
120
  unit_id,
117
121
  smart_account_id_from_backend,
122
+ username,
123
+ brand,
118
124
  ]);
119
125
 
120
126
  const handleSelectIndex = (index) => {
@@ -7,6 +7,9 @@ import { SCProvider } from '../../../context';
7
7
  import { mockSCStore } from '../../../context/mockStore';
8
8
  import SetupGatewayWifi from '../SetupGatewayWifi';
9
9
  import DisplayChecking from '../../../commons/DisplayChecking';
10
+ import { AlertAction } from '../../../commons';
11
+ import _TextInputPassword from '../../../commons/Form/TextInputPassword';
12
+ import _TextInput from '../../../commons/Form/TextInput';
10
13
 
11
14
  const wrapComponent = (route) => (
12
15
  <SCProvider initState={mockSCStore({})}>
@@ -87,4 +90,53 @@ describe('Test SetupGatewayWifi', () => {
87
90
  const displayLoadingConnect = instance.findByType(DisplayChecking);
88
91
  expect(displayLoadingConnect.props.visible).toBeTruthy();
89
92
  });
93
+
94
+ test('test render AlertAction', async () => {
95
+ await act(async () => {
96
+ tree = await create(wrapComponent(route));
97
+ });
98
+ const instance = tree.root;
99
+ const alertAction = instance.findByType(AlertAction);
100
+ await act(async () => {
101
+ alertAction.props.hideModal();
102
+ alertAction.props.leftButtonClick();
103
+ });
104
+ expect(alertAction).toBeDefined();
105
+ });
106
+
107
+ test('test render _TextInputPassword', async () => {
108
+ await act(async () => {
109
+ tree = await create(wrapComponent(route));
110
+ });
111
+ const instance = tree.root;
112
+ const textInputPassword = instance.findByType(_TextInputPassword);
113
+ await act(async () => {
114
+ textInputPassword.props.onChange('new_name');
115
+ });
116
+ expect(textInputPassword.props.value).toEqual('new_name');
117
+ });
118
+
119
+ test('test render TextInput', async () => {
120
+ await act(async () => {
121
+ tree = await create(wrapComponent(route));
122
+ });
123
+ const instance = tree.root;
124
+ const textInput = instance.findByType(_TextInput);
125
+ await act(async () => {
126
+ textInput.props.onChange('new_text');
127
+ });
128
+ expect(textInput.props.value).toEqual('new_text');
129
+ });
130
+
131
+ test('test render DisplayChecking', async () => {
132
+ await act(async () => {
133
+ tree = await create(wrapComponent(route));
134
+ });
135
+ const instance = tree.root;
136
+ const displayChecking = instance.findByType(DisplayChecking);
137
+ await act(async () => {
138
+ displayChecking.props.onClose();
139
+ });
140
+ expect(displayChecking).toBeDefined();
141
+ });
90
142
  });
@@ -1,4 +1,10 @@
1
- import React, { useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import { View, Text, TouchableOpacity, Platform, Image } from 'react-native';
3
9
  import { useRoute, useNavigation } from '@react-navigation/native';
4
10
  import { chunk } from 'lodash';
@@ -28,36 +34,51 @@ const AllCamera = () => {
28
34
  const [isFullScreen, setIsFullScreen] = useState(false);
29
35
  const [dataFullScreen, setDataFullScreen] = useState();
30
36
 
31
- const handleFullScreen = (data) => {
32
- setIsFullScreen(!isFullScreen);
33
- setDataFullScreen(data);
34
- };
37
+ const handleFullScreen = useCallback(
38
+ (data) => {
39
+ setIsFullScreen(!isFullScreen);
40
+ setDataFullScreen(data);
41
+ },
42
+ [isFullScreen]
43
+ );
35
44
 
36
- const onPressAmount = (value) => () => {
37
- setAmount(value);
38
- value === 1 && setActiveCamera(arrCameras[0]);
39
- const dataTemp = [...arrCameras];
40
- setData(value === 1 ? dataTemp : chunk(dataTemp, value));
41
- };
45
+ const onPressAmount = useCallback(
46
+ (value) => () => {
47
+ setAmount(value);
48
+ value === 1 && setActiveCamera(arrCameras[0]);
49
+ const dataTemp = [...arrCameras];
50
+ setData(value === 1 ? dataTemp : chunk(dataTemp, value));
51
+ setCurrentPage(1);
52
+ },
53
+ [arrCameras]
54
+ );
42
55
 
43
- const onPressNext = () =>
56
+ const onPressNext = useCallback(() => {
44
57
  currentPage !== data.length && carouselRef?.current?.snapToNext();
58
+ }, [currentPage, data.length]);
45
59
 
46
- const onPressPrev = () =>
60
+ const onPressPrev = useCallback(() => {
47
61
  currentPage !== 1 && carouselRef?.current?.snapToPrev();
62
+ }, [currentPage]);
48
63
 
49
- const onSnapToItem = (index) => {
50
- setActiveCamera(data[index]);
51
- setCurrentPage(index + 1);
52
- };
64
+ const onSnapToItem = useCallback(
65
+ (index) => {
66
+ setActiveCamera(data[index]);
67
+ setCurrentPage(index + 1);
68
+ },
69
+ [data]
70
+ );
53
71
 
54
72
  const onClose = () => {
55
73
  setIsFullScreen(false);
56
74
  };
57
75
 
58
- const goToPlayBack = (item, thumbnail) => () => {
59
- navigate(Routes.PlaybackCamera, { item, thumbnail });
60
- };
76
+ const goToPlayBack = useCallback(
77
+ (item, thumbnail) => () => {
78
+ navigate(Routes.PlaybackCamera, { item, thumbnail });
79
+ },
80
+ [navigate]
81
+ );
61
82
 
62
83
  const CameraItem = ({ item, width, height }) => {
63
84
  return (
@@ -82,7 +103,7 @@ const AllCamera = () => {
82
103
  );
83
104
  };
84
105
 
85
- const renderItem = ({ item, index }) => {
106
+ const renderItem = ({ item }) => {
86
107
  return (
87
108
  <View style={styles.wrapItem}>
88
109
  {amount === 1 ? (
@@ -93,13 +114,13 @@ const AllCamera = () => {
93
114
  <CameraItem
94
115
  key={item?.id}
95
116
  item={item[0]}
96
- width={188}
117
+ width={Constants.width / 2}
97
118
  height={112}
98
119
  />
99
120
  <CameraItem
100
121
  key={item?.id}
101
122
  item={item[1]}
102
- width={188}
123
+ width={Constants.width / 2}
103
124
  height={112}
104
125
  />
105
126
  </View>
@@ -107,13 +128,13 @@ const AllCamera = () => {
107
128
  <CameraItem
108
129
  key={item?.id}
109
130
  item={item[2]}
110
- width={188}
131
+ width={Constants.width / 2}
111
132
  height={112}
112
133
  />
113
134
  <CameraItem
114
135
  key={item?.id}
115
136
  item={item[3]}
116
- width={188}
137
+ width={Constants.width / 2}
117
138
  height={112}
118
139
  />
119
140
  </View>
@@ -138,19 +159,23 @@ const AllCamera = () => {
138
159
 
139
160
  const renderCarousel = useMemo(() => {
140
161
  return (
141
- <Carousel
142
- ref={carouselRef}
143
- data={data}
144
- renderItem={renderItem}
145
- sliderWidth={Constants.width}
146
- itemWidth={Constants.width}
147
- inactiveSlideScale={1}
148
- onSnapToItem={onSnapToItem}
149
- inactiveSlideOpacity={1}
150
- extraData={data}
151
- loop={false}
152
- isForceIndex={Platform.OS === 'android'}
153
- />
162
+ <View style={styles.wrapCarousel}>
163
+ <Carousel
164
+ ref={carouselRef}
165
+ data={data}
166
+ renderItem={renderItem}
167
+ sliderWidth={Constants.width}
168
+ itemWidth={Constants.width}
169
+ inactiveSlideScale={1}
170
+ onSnapToItem={onSnapToItem}
171
+ inactiveSlideOpacity={1}
172
+ extraData={data}
173
+ loop={false}
174
+ lockScrollWhileSnapping
175
+ isForceIndex={Platform.OS === 'android'}
176
+ removeClippedSubviews={false}
177
+ />
178
+ </View>
154
179
  );
155
180
  // eslint-disable-next-line react-hooks/exhaustive-deps
156
181
  }, [data]);
@@ -182,6 +207,17 @@ const AllCamera = () => {
182
207
  // eslint-disable-next-line react-hooks/exhaustive-deps
183
208
  }, [amount]);
184
209
 
210
+ const renderName = useMemo(
211
+ () => (
212
+ <Text style={styles.cameraName}>
213
+ {amount === 1
214
+ ? activeCamera?.configuration?.name
215
+ : `${currentPage}/${data.length}`}
216
+ </Text>
217
+ ),
218
+ [activeCamera?.configuration?.name, amount, currentPage, data.length]
219
+ );
220
+
185
221
  useEffect(() => {
186
222
  const to = setTimeout(() => {
187
223
  carouselRef?.current?.snapToItem();
@@ -203,16 +239,12 @@ const AllCamera = () => {
203
239
  <TouchableOpacity style={styles.buttonNext} onPress={onPressPrev}>
204
240
  <Image source={Images.arrowLeft} />
205
241
  </TouchableOpacity>
206
- <Text numberOfLines={1} style={styles.cameraName}>
207
- {amount === 1
208
- ? activeCamera?.configuration?.name
209
- : `${currentPage}/${data.length}`}
210
- </Text>
242
+ {renderName}
211
243
  <TouchableOpacity style={styles.buttonNext} onPress={onPressNext}>
212
244
  <Image source={Images.arrowLeft} style={styles.arrowRight} />
213
245
  </TouchableOpacity>
214
246
  </View>
215
- <View style={styles.wrapCarousel}>{renderCarousel}</View>
247
+ {renderCarousel}
216
248
  {renderAmount}
217
249
  </View>
218
250
  </View>
@@ -48,9 +48,9 @@ const NotificationItem = memo(({ item }) => {
48
48
  const renderItem = useCallback(() => {
49
49
  const paramsJSON = JSON.parse(params.replace(regex, '"'));
50
50
  const booking_id = paramsJSON.booking_id && paramsJSON.booking_id;
51
+ const unitId = paramsJSON?.unit_id;
51
52
  switch (content_code) {
52
53
  case NOTIFICATION_TYPES.NOTIFY_INVITE_MEMBER:
53
- const unitId = paramsJSON?.unit_id;
54
54
  return {
55
55
  content: customColorText(
56
56
  t('text_notification_content_invite_member'),
@@ -254,6 +254,24 @@ const NotificationItem = memo(({ item }) => {
254
254
  <IconComponent icon={'home'} style={styles.backgroundSummer} />
255
255
  ),
256
256
  };
257
+ case NOTIFICATION_TYPES.NOTIFY_RENAME_UNIT:
258
+ return {
259
+ content: customColorText(
260
+ t('text_notification_content_rename_unit'),
261
+ arrParams
262
+ ),
263
+ redirect: () => {
264
+ navigation.navigate(Routes.UnitStack, {
265
+ screen: Routes.UnitDetail,
266
+ params: {
267
+ unitId,
268
+ },
269
+ });
270
+ },
271
+ iconContent: (
272
+ <IconComponent icon={'home'} style={styles.backgroundSummer} />
273
+ ),
274
+ };
257
275
  default:
258
276
  return {
259
277
  content: customColorText(t('this_notification_will_be_updated_soon')),
@@ -8,7 +8,10 @@ import { axiosGet } from '../../utils/Apis/axios';
8
8
  import { useTranslations } from '../../hooks/Common/useTranslations';
9
9
  import NotificationItem from './components/NotificationItem';
10
10
  import WrapHeaderScrollable from '../../commons/Sharing/WrapHeaderScrollable';
11
- import { watchNotificationData, unwatchNotificationData } from './Monitor';
11
+ import {
12
+ watchNotificationData,
13
+ unwatchNotificationData,
14
+ } from '../../utils/Monitor';
12
15
  import { useSCContextSelector } from '../../context';
13
16
 
14
17
  let page = 1;
@@ -48,16 +48,23 @@ const SelectUser = ({ route }) => {
48
48
  if (success) {
49
49
  setUsers([...users, data.user]);
50
50
  } else {
51
- const textTemp = t(
52
- data?.phone
53
- ? 'text_phone_share_permission'
54
- : data?.email
55
- ? 'text_email_share_permission'
56
- : ''
57
- );
58
- ToastBottomHelper.error(
59
- t('error_share_permission', { data: phone || email, text: textTemp })
60
- );
51
+ if (data.non_field_errors) {
52
+ ToastBottomHelper.error(data.non_field_errors);
53
+ } else {
54
+ const textTemp = t(
55
+ data?.phone
56
+ ? 'text_phone_share_permission'
57
+ : data?.email
58
+ ? 'text_email_share_permission'
59
+ : ''
60
+ );
61
+ ToastBottomHelper.error(
62
+ t('error_share_permission', {
63
+ data: phone || email,
64
+ text: textTemp,
65
+ })
66
+ );
67
+ }
61
68
  }
62
69
  },
63
70
  [users, unit.id, permissions, t]
@@ -11,6 +11,7 @@ import { API } from '../../../configs';
11
11
  import { getTranslate } from '../../../utils/I18n';
12
12
  import { SCProvider } from '../../../context';
13
13
  import { mockSCStore } from '../../../context/mockStore';
14
+ import { ToastBottomHelper } from '../../../utils/Utils';
14
15
 
15
16
  const wrapComponent = (route) => (
16
17
  <SCProvider initState={mockSCStore({})}>
@@ -191,4 +192,76 @@ describe('test SelectUser container', () => {
191
192
  accountList = instance.findAllByType(AccountList);
192
193
  expect(accountList).toHaveLength(0);
193
194
  });
195
+
196
+ test('_TextInput onChange unitOwner phone, validate and call api sharedPermission', async () => {
197
+ const response = {
198
+ status: 400,
199
+ data: { non_field_errors: 'You cannot invite yourself' },
200
+ };
201
+ mockAxiosPost(response);
202
+
203
+ const spyToastError = jest.spyOn(ToastBottomHelper, 'error');
204
+
205
+ await act(async () => {
206
+ tree = create(wrapComponent(route));
207
+ });
208
+ const instance = tree.root;
209
+
210
+ const textInput = instance.findByType(_TextInput);
211
+ const button = instance.findByType(Button);
212
+
213
+ await act(async () => {
214
+ await textInput.props.onChange('0909123456');
215
+ });
216
+
217
+ await act(async () => {
218
+ await button.props.onPress();
219
+ });
220
+
221
+ expect(textInput.props.errorText).toEqual('');
222
+ expect(axios.post).toHaveBeenCalledWith(API.SHARE.SHARE(), {
223
+ phone: '0909123456',
224
+ email: '',
225
+ unit: 1,
226
+ permissions: { controlPermissions: {}, readPermissions: {} },
227
+ });
228
+
229
+ expect(spyToastError).toBeCalled();
230
+ });
231
+
232
+ test('_TextInput onChange phone, validate and call api not sharedPermission', async () => {
233
+ const response = {
234
+ status: 400,
235
+ data: { phone: '0909123456' },
236
+ };
237
+ mockAxiosPost(response);
238
+
239
+ const spyToastError = jest.spyOn(ToastBottomHelper, 'error');
240
+
241
+ await act(async () => {
242
+ tree = create(wrapComponent(route));
243
+ });
244
+ const instance = tree.root;
245
+
246
+ const textInput = instance.findByType(_TextInput);
247
+ const button = instance.findByType(Button);
248
+
249
+ await act(async () => {
250
+ await textInput.props.onChange('0909123456');
251
+ });
252
+
253
+ await act(async () => {
254
+ await button.props.onPress();
255
+ });
256
+
257
+ expect(textInput.props.errorText).toEqual('');
258
+ expect(axios.post).toHaveBeenCalledWith(API.SHARE.SHARE(), {
259
+ phone: '0909123456',
260
+ email: '',
261
+ unit: 1,
262
+ permissions: { controlPermissions: {}, readPermissions: {} },
263
+ });
264
+
265
+ expect(spyToastError).toBeCalled();
266
+ });
194
267
  });
@@ -26,7 +26,11 @@ import { useNavigation } from '@react-navigation/native';
26
26
  import Routes from '../../utils/Route';
27
27
  import SubUnitAutomate from '../../commons/SubUnit/OneTap';
28
28
  import SubUnitFavorites from '../../commons/SubUnit/Favorites';
29
- import { AUTOMATE_TYPE } from '../../configs/Constants';
29
+ import { AUTOMATE_TYPE, NOTIFICATION_TYPES } from '../../configs/Constants';
30
+ import {
31
+ watchNotificationData,
32
+ unwatchNotificationData,
33
+ } from '../../utils/Monitor';
30
34
 
31
35
  const UnitDetail = ({ route }) => {
32
36
  const t = useTranslations();
@@ -43,6 +47,7 @@ const UnitDetail = ({ route }) => {
43
47
  const isFocused = useIsFocused();
44
48
  const { stateData, setAction } = useContext(SCContext);
45
49
  const { navigate } = useNavigation();
50
+ const user = useSCContextSelector((state) => state?.auth?.account?.user);
46
51
  const isLavidaSource = useSCContextSelector(
47
52
  (state) => state.app.isLavidaSource
48
53
  );
@@ -144,9 +149,13 @@ const UnitDetail = ({ route }) => {
144
149
  );
145
150
  }, [t, unitId]);
146
151
 
147
- const onRefresh = useCallback(() => {
148
- fetchDetails();
149
- }, [fetchDetails]);
152
+ const onRefresh = useCallback(
153
+ (data = { content_code: NOTIFICATION_TYPES.NOTIFY_RENAME_UNIT }) => {
154
+ data.content_code === NOTIFICATION_TYPES.NOTIFY_RENAME_UNIT &&
155
+ fetchDetails();
156
+ },
157
+ [fetchDetails]
158
+ );
150
159
 
151
160
  const handleAppStateChange = useCallback(
152
161
  (nextAppState) => {
@@ -312,6 +321,11 @@ const UnitDetail = ({ route }) => {
312
321
  navigate(isLavidaSource ? Routes.SmartHomeDashboard : Routes.Dashboard);
313
322
  }, [isLavidaSource, navigate]);
314
323
 
324
+ useEffect(() => {
325
+ watchNotificationData(user, onRefresh);
326
+ return () => unwatchNotificationData(user);
327
+ }, [user, onRefresh]);
328
+
315
329
  return (
316
330
  <WrapParallaxScrollView
317
331
  uriImg={unit.background}
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback, useEffect } from 'react';
1
+ import React, { useState, useCallback, useEffect, useMemo } from 'react';
2
2
  import { View, TouchableOpacity, Image, Platform } from 'react-native';
3
3
  import Animated from 'react-native-reanimated';
4
4
  import { useTranslations } from '../../hooks/Common/useTranslations';
@@ -21,6 +21,7 @@ import { IconOutline } from '@ant-design/icons-react-native';
21
21
  import styles from './ManageUnitStyles';
22
22
  import { useNavigation } from '@react-navigation/native';
23
23
  import { ModalCustom } from '../../commons/Modal';
24
+ import { Icon } from '@ant-design/react-native';
24
25
 
25
26
  const ButtonWrapper = ({
26
27
  onPress,
@@ -29,17 +30,26 @@ const ButtonWrapper = ({
29
30
  value,
30
31
  valueColor,
31
32
  children,
33
+ icon,
32
34
  }) => {
33
35
  return (
34
36
  <TouchableOpacity
35
37
  onPress={onPress}
36
38
  testID={testId}
37
- style={styles.buttonWrapper}
39
+ style={!icon ? styles.buttonWrapper : styles.buttonWrapperAvatar}
38
40
  >
39
41
  <View style={styles.buttonInfo}>
40
- <Text type="H4" semibold>
41
- {title}
42
- </Text>
42
+ {!icon ? (
43
+ <Text type="H4" semibold>
44
+ {title}
45
+ </Text>
46
+ ) : (
47
+ (icon && <Image source={{ uri: icon }} style={styles.avatar} />) || (
48
+ <View style={styles.avatar}>
49
+ <Icon name={'user'} size={27} />
50
+ </View>
51
+ )
52
+ )}
43
53
  <View style={styles.buttonValue}>
44
54
  <Text
45
55
  type="Body"
@@ -66,15 +76,29 @@ const ManageUnit = ({ route }) => {
66
76
  name: unit.name,
67
77
  address: unit.address,
68
78
  background: unit.background,
79
+ icon: unit.icon,
69
80
  });
70
81
 
71
82
  const [unitName, setUnitName] = useState(unit.name);
72
- const [imageUrl, setImageUrl] = useState('');
83
+ const [imageUrlBackground, setImageUrlBackground] = useState('');
84
+ const [imageUrlIcon, setImageUrlIcon] = useState('');
85
+ const [checkSelectImage, setCheckSelectImage] = useState('');
73
86
  const [showImagePicker, setShowImagePicker] = useState(false);
74
87
 
88
+ const onUpdateImage = useCallback(
89
+ async (data) => {
90
+ if (checkSelectImage === 'background') {
91
+ setImageUrlBackground(data);
92
+ } else {
93
+ setImageUrlIcon(data);
94
+ }
95
+ },
96
+ [checkSelectImage]
97
+ );
98
+
75
99
  const updateUnit = useCallback(
76
- async (bodyData, headers) => {
77
- const formData = createFormData(bodyData, ['background']);
100
+ async (bodyData, headers, dataInput) => {
101
+ const formData = createFormData(bodyData, [dataInput]);
78
102
 
79
103
  const { success, data } = await axiosPatch(
80
104
  API.UNIT.MANAGE_UNIT(unit.id),
@@ -110,20 +134,37 @@ const ManageUnit = ({ route }) => {
110
134
  setHideEdit(true);
111
135
  }, [unitName, setHideEdit, updateUnit]);
112
136
 
113
- const handleChoosePhoto = useCallback(() => {
114
- setShowImagePicker(true);
115
- }, [setShowImagePicker]);
137
+ const handleChoosePhoto = useCallback(
138
+ (text) => {
139
+ setCheckSelectImage(text);
140
+ setShowImagePicker(true);
141
+ },
142
+ [setShowImagePicker]
143
+ );
116
144
 
117
145
  useEffect(() => {
118
- if (imageUrl) {
146
+ if (imageUrlBackground) {
119
147
  updateUnit(
120
- { background: imageUrl },
148
+ { background: imageUrlBackground },
121
149
  {
122
150
  headers: { 'Content-Type': 'multipart/form-data' },
123
- }
151
+ },
152
+ 'background'
124
153
  );
125
154
  }
126
- }, [imageUrl, updateUnit]);
155
+ }, [imageUrlBackground, updateUnit]);
156
+
157
+ useEffect(() => {
158
+ if (imageUrlIcon) {
159
+ updateUnit(
160
+ { icon: imageUrlIcon },
161
+ {
162
+ headers: { 'Content-Type': 'multipart/form-data' },
163
+ },
164
+ 'icon'
165
+ );
166
+ }
167
+ }, [imageUrlIcon, updateUnit]);
127
168
 
128
169
  const [showRemove, setshowRemove, setHideRemove] = useBoolean();
129
170
  const goRemove = useCallback(async () => {
@@ -160,6 +201,12 @@ const ManageUnit = ({ route }) => {
160
201
  compressImageQuality: 0.8,
161
202
  };
162
203
 
204
+ const SensorNumbers = useMemo(() => {
205
+ const stations = unit?.stations || [];
206
+ return stations.filter((item) => !item.isFavorites && !item.isOneTap)
207
+ .length;
208
+ }, [unit.stations]);
209
+
163
210
  return (
164
211
  <>
165
212
  <WrapHeaderScrollable
@@ -169,6 +216,12 @@ const ManageUnit = ({ route }) => {
169
216
  <View style={styles.wraper}>
170
217
  {isOwner && (
171
218
  <>
219
+ <ButtonWrapper
220
+ onPress={() => handleChoosePhoto('avatar')}
221
+ value={t('icon_unit')}
222
+ icon={unitData.icon}
223
+ testID={TESTID.MANAGE_UNIT_CHANGE_PHOTO}
224
+ />
172
225
  <ButtonWrapper
173
226
  onPress={setshowEdit}
174
227
  testID={TESTID.MANAGE_UNIT_CHANGE_NAME}
@@ -187,10 +240,11 @@ const ManageUnit = ({ route }) => {
187
240
  <ButtonWrapper
188
241
  onPress={goToManageSubUnit}
189
242
  title={t('manage_sub_units')}
190
- value={`${unit?.stations?.length} sub-units`}
243
+ value={`${SensorNumbers} sub-units`}
244
+ testID={TESTID.MANAGE_UNIT_GO_TO_SUBUNIT}
191
245
  />
192
246
  <ButtonWrapper
193
- onPress={handleChoosePhoto}
247
+ onPress={() => handleChoosePhoto('background')}
194
248
  title={t('unit_wallpaper')}
195
249
  value={t('tap_to_change')}
196
250
  valueColor={Colors.Orange}
@@ -213,7 +267,7 @@ const ManageUnit = ({ route }) => {
213
267
  <ImagePicker
214
268
  showImagePicker={showImagePicker}
215
269
  setShowImagePicker={setShowImagePicker}
216
- setImageUrl={setImageUrl}
270
+ setImageUrl={onUpdateImage}
217
271
  optionsCapture={options}
218
272
  testID={TESTID.MANAGE_UNIT_IMAGE_PICKER}
219
273
  />
@@ -221,20 +275,22 @@ const ManageUnit = ({ route }) => {
221
275
  )}
222
276
  </View>
223
277
  </WrapHeaderScrollable>
224
- <TouchableOpacity
225
- style={styles.removeButton}
226
- onPress={setshowRemove}
227
- testID={TESTID.MANAGE_UNIT_SHOW_REMOVE}
228
- >
229
- <Text
230
- type={'H4'}
231
- semibold
232
- color={Colors.Red}
233
- style={styles.removeBorderBottom}
278
+ {!showEdit && (
279
+ <TouchableOpacity
280
+ style={styles.removeButton}
281
+ onPress={setshowRemove}
282
+ testID={TESTID.MANAGE_UNIT_SHOW_REMOVE}
234
283
  >
235
- {t('remove_unit')}
236
- </Text>
237
- </TouchableOpacity>
284
+ <Text
285
+ type={'H4'}
286
+ semibold
287
+ color={Colors.Red}
288
+ style={styles.removeBorderBottom}
289
+ >
290
+ {t('remove_unit')}
291
+ </Text>
292
+ </TouchableOpacity>
293
+ )}
238
294
  <ModalCustom
239
295
  isVisible={showEdit}
240
296
  onBackButtonPress={setHideEdit}
@@ -250,7 +306,7 @@ const ManageUnit = ({ route }) => {
250
306
  </Text>
251
307
  </View>
252
308
  <_TextInput
253
- defaultValue={unitName}
309
+ defaultValue={unitData?.name}
254
310
  onChange={(value) => setUnitName(value)}
255
311
  textInputStyle={styles.textInputStyle}
256
312
  wrapStyle={styles.textInputWrapStyle}
@@ -16,6 +16,12 @@ export default StyleSheet.create({
16
16
  borderBottomWidth: 0.5,
17
17
  borderBottomColor: Colors.Gray6,
18
18
  },
19
+ buttonWrapperAvatar: {
20
+ paddingTop: 14,
21
+ paddingBottom: 24,
22
+ borderBottomWidth: 0.5,
23
+ borderBottomColor: Colors.Gray7,
24
+ },
19
25
  buttonInfo: {
20
26
  flex: 1,
21
27
  flexDirection: 'row',
@@ -29,6 +35,7 @@ export default StyleSheet.create({
29
35
  },
30
36
  value: {
31
37
  marginRight: 20,
38
+ fontSize: 14,
32
39
  },
33
40
  location: {
34
41
  marginTop: 16,
@@ -104,4 +111,17 @@ export default StyleSheet.create({
104
111
  textInputWrapStyle: {
105
112
  marginTop: 0,
106
113
  },
114
+ avatar: {
115
+ height: 50,
116
+ width: 50,
117
+ borderRadius: 25,
118
+ borderWidth: 0.5,
119
+ borderColor: Colors.Gray5,
120
+ justifyContent: 'center',
121
+ alignItems: 'center',
122
+ },
123
+ wrapAvatar: {
124
+ flexDirection: 'row',
125
+ justifyContent: 'space-between',
126
+ },
107
127
  });
@@ -18,6 +18,7 @@ import { usePopover, useBoolean } from '../../hooks/Common';
18
18
  import { MenuActionMore, AlertAction } from '../../commons';
19
19
  import { useTranslations } from '../../hooks/Common/useTranslations';
20
20
  import { useStateAlertRemove } from '../Unit/hook/useStateAlertRemove';
21
+ import { ToastBottomHelper } from '../../utils/Utils';
21
22
 
22
23
  const ListSmartAccount = ({ route }) => {
23
24
  const { unitId } = route?.params || {};
@@ -76,8 +77,11 @@ const ListSmartAccount = ({ route }) => {
76
77
  const { success } = await axiosDelete(
77
78
  API.SMART_ACCOUNT.REMOVE_SMART_ACCOUNT(id)
78
79
  );
79
- success && getAllSmartAccounts();
80
- }, [getAllSmartAccounts, hideAlertAction]);
80
+ if (success) {
81
+ ToastBottomHelper.success(t('removed_successfully'));
82
+ getAllSmartAccounts();
83
+ }
84
+ }, [getAllSmartAccounts, hideAlertAction, t]);
81
85
 
82
86
  const listMenuItem = useMemo(() => {
83
87
  return [{ action: 'remove', text: t('remove_account') }];
@@ -74,7 +74,7 @@ describe('Test UnitDetail', () => {
74
74
 
75
75
  const detailUnitApiUrl = API.UNIT.UNIT_DETAIL(1);
76
76
  const summaryUnitApiUrl = API.UNIT.UNIT_SUMMARY(1);
77
- const sensorStatusApiUrl = API.SENSOR.STATUS();
77
+ const sensorStatusApiUrl = API.UNIT.SENSORS_STATUS(1);
78
78
 
79
79
  let tree;
80
80
 
@@ -452,7 +452,50 @@ describe('Test UnitDetail', () => {
452
452
  expect(fullView).toHaveLength(1);
453
453
  expect(fullView[0].props.isVisible).toEqual(false);
454
454
  });
455
+ test('onPress subunit camera devices', async () => {
456
+ const unitData = {
457
+ stations: [
458
+ {
459
+ camera_devices: [
460
+ {
461
+ configuration: {
462
+ id: 4,
463
+ name: 'Camera cửa nhà xe',
464
+ uri: 'rtsp://admin:hd543211@/ISAPI/Streaming/Channels/101/',
465
+ preview_uri:
466
+ 'rtsp://admin:hd543211@/ISAPI/Streaming/Channels/101/',
467
+ playback: 'rtsp://admin:hd543211@/Streaming/tracks/101/',
468
+ },
469
+ id: 41,
470
+ order: 1,
471
+ template: 'camera',
472
+ type: 'camera',
473
+ },
474
+ ],
475
+ },
476
+ ],
477
+ };
478
+
479
+ await act(async () => {
480
+ tree = await renderer.create(
481
+ wrapComponent({ params: { ...route.params, unitData } }, account)
482
+ );
483
+ });
484
+ const instance = tree.root;
485
+ const CameraDeviceViews = instance.findAllByType(CameraDevice);
486
+ expect(CameraDeviceViews).toHaveLength(1);
487
+ const goDetailButton = tree.root.findAll(
488
+ (el) =>
489
+ el.props.testID === TESTID.SUB_UNIT_GO_DETAIL &&
490
+ el.type === TouchableOpacity
491
+ );
455
492
 
493
+ expect(goDetailButton).toHaveLength(2);
494
+ await act(async () => {
495
+ await goDetailButton[0].props.onPress();
496
+ });
497
+ expect(mockedNavigate).toHaveBeenCalled();
498
+ });
456
499
  test('render subunit favorites', async () => {
457
500
  const unitData = {
458
501
  stations: [
@@ -11,6 +11,17 @@ import { mockSCStore } from '../../../context/mockStore';
11
11
 
12
12
  jest.mock('axios');
13
13
 
14
+ const mockNavigate = jest.fn();
15
+ jest.mock('@react-navigation/native', () => {
16
+ return {
17
+ ...jest.requireActual('@react-navigation/native'),
18
+ useRoute: jest.fn(),
19
+ useNavigation: () => ({
20
+ navigate: mockNavigate,
21
+ }),
22
+ };
23
+ });
24
+
14
25
  const mockedDispatch = jest.fn();
15
26
  jest.mock('react-redux', () => ({
16
27
  useSelector: jest.fn(),
@@ -42,6 +53,11 @@ describe('Test Manage Unit', () => {
42
53
  },
43
54
  };
44
55
 
56
+ beforeEach(() => {
57
+ mockNavigate.mockClear();
58
+ axios.get.mockClear();
59
+ });
60
+
45
61
  const getElement = (instance) => {
46
62
  const changeName = instance.findAll(
47
63
  (item) => item.props.testID === TESTID.MANAGE_UNIT_CHANGE_NAME
@@ -207,4 +223,57 @@ describe('Test Manage Unit', () => {
207
223
  spyToast.mockReset();
208
224
  spyToast.mockRestore();
209
225
  });
226
+ test('test onPress onUpdateImage', async () => {
227
+ await act(async () => {
228
+ tree = create(wrapComponent(route));
229
+ });
230
+ const instance = tree.root;
231
+
232
+ const imagePicker = instance.find(
233
+ (item) => item.props.testID === TESTID.MANAGE_UNIT_IMAGE_PICKER
234
+ );
235
+
236
+ const handleChoosePhoto = instance.findAll(
237
+ (item) => item.props.testID === TESTID.MANAGE_UNIT_CHANGE_PHOTO
238
+ );
239
+
240
+ act(() => {
241
+ handleChoosePhoto[0].props.onPress();
242
+ handleChoosePhoto[1].props.onPress();
243
+ imagePicker.props.setImageUrl('abc');
244
+ });
245
+
246
+ expect(axios.patch).toBeCalled();
247
+ expect(imagePicker.props.showImagePicker).toBeTruthy();
248
+ });
249
+ test('test onPress goSelectLocation', async () => {
250
+ await act(async () => {
251
+ tree = create(wrapComponent(route));
252
+ });
253
+ const instance = tree.root;
254
+
255
+ const buttonWrapper = instance.find(
256
+ (item) => item.props.testID === TESTID.MANAGE_UNIT_CHANGE_LOCATION
257
+ );
258
+
259
+ act(() => {
260
+ buttonWrapper.props.onPress();
261
+ });
262
+ expect(mockNavigate).toBeCalled();
263
+ });
264
+ test('test onPress goToManageSubUnit', async () => {
265
+ await act(async () => {
266
+ tree = create(wrapComponent(route));
267
+ });
268
+ const instance = tree.root;
269
+
270
+ const buttonWrapper = instance.find(
271
+ (item) => item.props.testID === TESTID.MANAGE_UNIT_GO_TO_SUBUNIT
272
+ );
273
+
274
+ act(() => {
275
+ buttonWrapper.props.onPress();
276
+ });
277
+ expect(mockNavigate).toBeCalled();
278
+ });
210
279
  });
@@ -6,21 +6,22 @@ import { SCProvider } from '../../../context';
6
6
  import { mockSCStore } from '../../../context/mockStore';
7
7
  import ListSmartAccount from '../SmartAccount';
8
8
  import { SmartAccountItem } from '../SmartAccountItem';
9
- import { MenuActionMore } from '../../../commons';
9
+ import { AlertAction, MenuActionMore } from '../../../commons';
10
+ import Routes from '../../../utils/Route';
10
11
 
11
12
  const wrapComponent = (route, navigation) => (
12
13
  <SCProvider initState={mockSCStore({})}>
13
- <ListSmartAccount />
14
+ <ListSmartAccount route={route} />
14
15
  </SCProvider>
15
16
  );
16
17
 
17
- const mockedNavigate = jest.fn();
18
-
18
+ const mockNavigate = jest.fn();
19
19
  jest.mock('@react-navigation/native', () => {
20
20
  return {
21
21
  ...jest.requireActual('@react-navigation/native'),
22
+ useRoute: jest.fn(),
22
23
  useNavigation: () => ({
23
- goBack: mockedNavigate,
24
+ navigate: mockNavigate,
24
25
  }),
25
26
  };
26
27
  });
@@ -35,8 +36,13 @@ jest.mock('react', () => {
35
36
  jest.mock('axios');
36
37
  describe('Test SmartAccount', () => {
37
38
  let tree;
39
+ let route = {
40
+ params: {
41
+ unitId: 1,
42
+ },
43
+ };
38
44
  test('test render SmartAccount', async () => {
39
- axios.get.mockImplementationOnce(() => ({
45
+ const reponse = {
40
46
  status: 200,
41
47
  data: [
42
48
  {
@@ -58,14 +64,25 @@ describe('Test SmartAccount', () => {
58
64
  username: 'loitest3',
59
65
  },
60
66
  ],
61
- }));
67
+ };
68
+ axios.get.mockImplementationOnce(() => reponse);
62
69
  await act(async () => {
63
- tree = await renderer.create(wrapComponent());
70
+ tree = await renderer.create(wrapComponent(route));
64
71
  });
65
72
  const instance = tree.root;
66
73
 
67
74
  const smartAccountItem = instance.findAllByType(SmartAccountItem);
68
75
  expect(smartAccountItem.length).toEqual(3);
76
+ act(() => {
77
+ smartAccountItem[0].props.gotoSmartAccountDetail(reponse.data[0]);
78
+ smartAccountItem[0].props.onShowMenuMore(reponse.data[0]);
79
+ });
80
+ expect(mockNavigate).toBeCalledWith(Routes.ListDeviceSmartAccount, {
81
+ username: reponse.data[0].username,
82
+ brand: reponse.data[0].brand,
83
+ smart_account_id: reponse.data[0].id,
84
+ unit_id: route.params.unitId,
85
+ });
69
86
  });
70
87
 
71
88
  test('test render SmartAccountItem', async () => {
@@ -79,4 +96,16 @@ describe('Test SmartAccount', () => {
79
96
  });
80
97
  expect(menuActionMore.props.isVisible).toEqual(false);
81
98
  });
99
+
100
+ test('test render AlertAction', async () => {
101
+ await act(async () => {
102
+ tree = await renderer.create(wrapComponent());
103
+ });
104
+ const instance = tree.root;
105
+ const alertAction = instance.findByType(AlertAction);
106
+ await act(async () => {
107
+ alertAction.props.leftButtonClick();
108
+ });
109
+ expect(alertAction).toBeDefined();
110
+ });
82
111
  });
@@ -7,7 +7,7 @@ export const useStateAlertRemove = () => {
7
7
  visible: false,
8
8
  title: t('remove_account'),
9
9
  message: '',
10
- leftButton: t('yes_remove'),
10
+ leftButton: t('remove'),
11
11
  rightButton: t('cancel'),
12
12
  });
13
13
  const hideAlertAction = useCallback(() => {
@@ -151,7 +151,10 @@ export async function axiosPatch(...options) {
151
151
  export async function axiosDelete(...options) {
152
152
  return await axiosCall('delete', ...options);
153
153
  }
154
-
154
+ const convertFilenameImage = (filename) => {
155
+ const filenameConverted = filename?.replace(/HEIC/g, 'jpg');
156
+ return filename || filenameConverted;
157
+ };
155
158
  export function createFormData(data, list_file_field) {
156
159
  const formData = new FormData();
157
160
 
@@ -165,7 +168,9 @@ export function createFormData(data, list_file_field) {
165
168
  }
166
169
 
167
170
  formData.append(key, {
168
- name: item.filename || item.path?.split('/').pop(),
171
+ name: convertFilenameImage(
172
+ item.filename || item.path?.split('/').pop()
173
+ ),
169
174
  type: item.mime,
170
175
  uri: item.path,
171
176
  });
@@ -672,6 +672,7 @@
672
672
  "text_notification_content_remove_unit_to_member": "Unit **%{unit_name}** has been removed by **%{unit_owner_name}**. You cannot access to this unit anymore.",
673
673
  "text_notification_content_remove_member": "You were remove from **%{unit_name}** by **%{unit_owner_name}**. You cannot access to this unit anymore.",
674
674
  "text_notification_content_member_leave_unit": "**%{member_name}** has left **%{unit_name}**.",
675
+ "text_notification_content_rename_unit": "Unit **%{old_unit_name}** has been renamed to **%{new_unit_name}** by **%{owner_name}**.",
675
676
  "this_spot_does_not_exsit": "This spot does not exist",
676
677
  "please_scan_again_or_contact_the_parking_manager": "Please scan again or contact the parking manager",
677
678
  "this_spot_does_not_support_to_scan": "This spot does not support to scan",
@@ -894,8 +895,9 @@
894
895
  "location_permission_required_wifi_message": "This app needs location permission as this is required to scan for wifi networks.",
895
896
  "deny": "DENY",
896
897
  "allow": "ALLOW",
898
+ "avatar" : "Avatar",
897
899
  "error_share_permission": "{text} {data} does not exist!",
898
900
  "text_phone_share_permission": "The phone",
899
901
  "text_email_share_permission": "Email",
900
- "yes_remove": "Yes,remove"
902
+ "icon_unit": "Icon unit"
901
903
  }
@@ -680,6 +680,7 @@
680
680
  "text_notification_content_remove_unit_to_member": "Địa điểm **%{unit_name}** vừa được xoá bởi **%{unit_owner_name}**. Bạn không thể truy cập vào địa điểm này được nữa.",
681
681
  "text_notification_content_remove_member": "Bạn vừa được xoá khỏi **%{unit_name}** bởi **%{unit_owner_name}**. Bạn không thể truy cập vào địa điểm này được nữa.",
682
682
  "text_notification_content_member_leave_unit": "**%{member_name}** vừa rời khỏi **%{unit_name}**.",
683
+ "text_notification_content_rename_unit": "Địa điểm **%{old_unit_name}** vừa được đổi tên thành **%{new_unit_name}** bởi **%{owner_name}**.",
683
684
  "this_spot_does_not_exsit": "Vị trí đỗ này không tồn tại",
684
685
  "please_scan_again_or_contact_the_parking_manager": "Vui lòng quét lại hoặc liên hệ với người quản lý bãi đậu xe",
685
686
  "this_spot_does_not_support_to_scan": "Vị trí đỗ này không hỗ trợ quét",
@@ -897,8 +898,9 @@
897
898
  "location_permission_required_wifi_message": "Ứng dụng này cần có quyền định vị vì ứng dụng này được yêu cầu để quét các mạng Wi-Fi.",
898
899
  "deny": "Từ chối",
899
900
  "allow": "Cho phép",
901
+ "avatar" : "Ảnh đại diện",
900
902
  "error_share_permission": "{text} {data} không tồn tại!",
901
903
  "text_phone_share_permission": "Số điện thoại",
902
904
  "text_email_share_permission": "Email",
903
- "yes_remove": "Có,loại bỏ"
905
+ "icon_unit": "Ảnh đại diện địa điểm"
904
906
  }
@@ -1,4 +1,4 @@
1
- import { getPusher, destroyPusher } from '../../utils/Pusher';
1
+ import { getPusher, destroyPusher } from './Pusher';
2
2
 
3
3
  // eslint-disable-next-line promise/prefer-await-to-callbacks
4
4
  export const watchNotificationData = (user, callback) => {