@plusscommunities/pluss-maintenance-app 6.0.20 → 6.0.21-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/module/actions/JobActions.js +4 -4
  2. package/dist/module/actions/JobActions.js.map +1 -1
  3. package/dist/module/actions/index.js +1 -1
  4. package/dist/module/actions/index.js.map +1 -1
  5. package/dist/module/actions/types.js +1 -1
  6. package/dist/module/actions/types.js.map +1 -1
  7. package/dist/module/apis/index.js +3 -3
  8. package/dist/module/apis/index.js.map +1 -1
  9. package/dist/module/apis/maintenanceActions.js +36 -36
  10. package/dist/module/apis/maintenanceActions.js.map +1 -1
  11. package/dist/module/apis/userActions.js +5 -5
  12. package/dist/module/apis/userActions.js.map +1 -1
  13. package/dist/module/components/FilterPopupMenu.js +49 -49
  14. package/dist/module/components/FilterPopupMenu.js.map +1 -1
  15. package/dist/module/components/MaintenanceList.js +38 -38
  16. package/dist/module/components/MaintenanceList.js.map +1 -1
  17. package/dist/module/components/MaintenanceListItem.js +62 -62
  18. package/dist/module/components/MaintenanceListItem.js.map +1 -1
  19. package/dist/module/components/MaintenanceWidgetItem.js +27 -27
  20. package/dist/module/components/MaintenanceWidgetItem.js.map +1 -1
  21. package/dist/module/components/PrioritySelectorPopup.js +15 -15
  22. package/dist/module/components/PrioritySelectorPopup.js.map +1 -1
  23. package/dist/module/components/StatusSelectorPopup.js +16 -16
  24. package/dist/module/components/StatusSelectorPopup.js.map +1 -1
  25. package/dist/module/components/WidgetLarge.js +2 -2
  26. package/dist/module/components/WidgetLarge.js.map +1 -1
  27. package/dist/module/components/WidgetSmall.js +19 -19
  28. package/dist/module/components/WidgetSmall.js.map +1 -1
  29. package/dist/module/core.config.js +1 -1
  30. package/dist/module/core.config.js.map +1 -1
  31. package/dist/module/feature.config.js +17 -17
  32. package/dist/module/feature.config.js.map +1 -1
  33. package/dist/module/helper.js +10 -10
  34. package/dist/module/helper.js.map +1 -1
  35. package/dist/module/index.js +11 -11
  36. package/dist/module/index.js.map +1 -1
  37. package/dist/module/reducers/JobsReducer.js +13 -13
  38. package/dist/module/reducers/JobsReducer.js.map +1 -1
  39. package/dist/module/screens/JobTypePicker.js +17 -17
  40. package/dist/module/screens/JobTypePicker.js.map +1 -1
  41. package/dist/module/screens/MaintenancePage.js +10 -10
  42. package/dist/module/screens/MaintenancePage.js.map +1 -1
  43. package/dist/module/screens/MaintenanceUserPicker.js +129 -22
  44. package/dist/module/screens/MaintenanceUserPicker.js.map +1 -1
  45. package/dist/module/screens/RequestDetail.js +145 -145
  46. package/dist/module/screens/RequestDetail.js.map +1 -1
  47. package/dist/module/screens/RequestNotes.js +59 -59
  48. package/dist/module/screens/RequestNotes.js.map +1 -1
  49. package/dist/module/screens/ServiceRequest.js +187 -187
  50. package/dist/module/screens/ServiceRequest.js.map +1 -1
  51. package/dist/module/values.config.a.js +30 -30
  52. package/dist/module/values.config.a.js.map +1 -1
  53. package/dist/module/values.config.default.js +34 -34
  54. package/dist/module/values.config.default.js.map +1 -1
  55. package/dist/module/values.config.enquiry.js +34 -34
  56. package/dist/module/values.config.enquiry.js.map +1 -1
  57. package/dist/module/values.config.feedback.js +34 -34
  58. package/dist/module/values.config.feedback.js.map +1 -1
  59. package/dist/module/values.config.food.js +34 -34
  60. package/dist/module/values.config.food.js.map +1 -1
  61. package/dist/module/values.config.forms.js +34 -34
  62. package/dist/module/values.config.forms.js.map +1 -1
  63. package/dist/module/values.config.js +34 -34
  64. package/dist/module/values.config.js.map +1 -1
  65. package/package.json +51 -51
  66. package/src/actions/JobActions.js +67 -60
  67. package/src/actions/index.js +1 -1
  68. package/src/actions/types.js +1 -2
  69. package/src/apis/index.js +3 -3
  70. package/src/apis/maintenanceActions.js +189 -178
  71. package/src/apis/userActions.js +17 -17
  72. package/src/components/FilterPopupMenu.js +313 -256
  73. package/src/components/MaintenanceList.js +396 -317
  74. package/src/components/MaintenanceListItem.js +347 -288
  75. package/src/components/MaintenanceWidgetItem.js +145 -124
  76. package/src/components/PrioritySelectorPopup.js +81 -68
  77. package/src/components/StatusSelectorPopup.js +81 -70
  78. package/src/components/WidgetLarge.js +5 -5
  79. package/src/components/WidgetSmall.js +153 -133
  80. package/src/core.config.js +27 -3
  81. package/src/feature.config.js +62 -62
  82. package/src/helper.js +58 -53
  83. package/src/index.js +22 -22
  84. package/src/reducers/JobsReducer.js +85 -66
  85. package/src/screens/JobTypePicker.js +115 -92
  86. package/src/screens/MaintenancePage.js +89 -80
  87. package/src/screens/MaintenanceUserPicker.js +252 -100
  88. package/src/screens/RequestDetail.js +1348 -1125
  89. package/src/screens/RequestNotes.js +950 -806
  90. package/src/screens/ServiceRequest.js +1778 -1550
  91. package/src/values.config.a.js +33 -33
  92. package/src/values.config.default.js +39 -39
  93. package/src/values.config.enquiry.js +39 -39
  94. package/src/values.config.feedback.js +39 -39
  95. package/src/values.config.food.js +39 -39
  96. package/src/values.config.forms.js +39 -39
  97. package/src/values.config.js +39 -39
@@ -1,1564 +1,1792 @@
1
- import React, { Component } from 'react';
1
+ import React, { Component } from "react";
2
2
  import {
3
- Dimensions,
4
- Platform,
5
- KeyboardAvoidingView,
6
- ScrollView,
7
- Text,
8
- TouchableOpacity,
9
- View,
10
- Switch,
11
- FlatList,
12
- ImageBackground,
13
- Keyboard,
14
- } from 'react-native';
15
- import DateTimePicker from 'react-native-modal-datetime-picker';
16
- import { Icon } from 'react-native-elements';
17
- import _ from 'lodash';
18
- import moment from 'moment';
19
- import { connect } from 'react-redux';
20
- import { jobAdded } from '../actions';
21
- import { maintenanceActions, userActions } from '../apis';
22
- import { Services } from '../feature.config';
23
- import { Components, Colours, Helper, Config } from '../core.config';
24
- import { values } from '../values.config';
25
-
26
- const PHOTO_SIZE = (Dimensions.get('window').width - 64) / 3;
3
+ Dimensions,
4
+ Platform,
5
+ KeyboardAvoidingView,
6
+ ScrollView,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ Switch,
11
+ FlatList,
12
+ ImageBackground,
13
+ Keyboard,
14
+ } from "react-native";
15
+ import DateTimePicker from "react-native-modal-datetime-picker";
16
+ import { Icon } from "react-native-elements";
17
+ import _ from "lodash";
18
+ import moment from "moment";
19
+ import { connect } from "react-redux";
20
+ import { jobAdded } from "../actions";
21
+ import { maintenanceActions, userActions } from "../apis";
22
+ import { Services } from "../feature.config";
23
+ import { Components, Colours, Helper, Config } from "../core.config";
24
+ import { values } from "../values.config";
25
+
26
+ const PHOTO_SIZE = (Dimensions.get("window").width - 64) / 3;
27
27
 
28
28
  class MaintenanceRequest extends Component {
29
- constructor(props) {
30
- super(props);
31
- this.state = {
32
- submitting: false,
33
- success: false,
34
- fail: false,
35
- error: null,
36
- showError: false,
37
- loadingTypes: values.forceCustomFields,
38
-
39
- userName: '',
40
- roomNumber: '',
41
- phone: '',
42
- title: '',
43
- description: '',
44
- times: '',
45
-
46
- type: 'General',
47
-
48
- uploadingImage: false,
49
- images: [
50
- {
51
- add: true,
52
- },
53
- ],
54
- showFullscreenVideo: false,
55
- currentVideoUrl: '',
56
-
57
- isHome: false,
58
-
59
- types: [],
60
-
61
- confirmationToShow: false,
62
-
63
- customFields: [],
64
- customFieldImages: {},
65
- customFieldDocuments: {},
66
- isDateTimePickerVisible: false,
67
- popUpType: 'date',
68
- dateFieldId: null,
69
- imageFieldId: null,
70
-
71
- // PC-1255: On-behalf request fields
72
- canCreateOnBehalf: false,
73
- selectedUser: null,
74
- users: [],
75
- };
76
- this.checkThumb = null;
77
- this.keyboardTypes = {
78
- phone: 'phone-pad',
79
- email: 'email-address',
80
- text: 'default',
81
- };
82
- }
83
-
84
- componentDidMount() {
85
- this.checkUserPermissions();
86
- this.getJobTypes();
87
- }
88
-
89
- checkUserPermissions = async () => {
90
- // PC-1255: Check if user has userManagement permission
91
- const hasUserManagement = this.props.permissions?.includes('userManagement') || false;
92
-
93
- if (hasUserManagement) {
94
- this.setState({ canCreateOnBehalf: true });
95
- // Load site users for picker
96
- await this.loadSiteUsers();
97
- } else {
98
- // Default to logged-in user for non-staff or staff without permission
99
- this.setDefaultUser();
100
- }
101
- };
102
-
103
- loadSiteUsers = async () => {
104
- try {
105
- const response = await userActions.getSiteUsers(this.props.site);
106
- // PC-1255: Load all users (not just residents) to match web admin behavior
107
- const users = _.sortBy(response.data.Items || [], u => (u.displayName || '').toLowerCase());
108
- this.setState({ users });
109
- } catch (error) {
110
- console.log('Error loading site users:', error);
111
- // Fall back to default user if loading fails
112
- this.setDefaultUser();
113
- }
114
- };
115
-
116
- setDefaultUser = () => {
117
- if (this.props.userType !== 'KIOSK') {
118
- const defaultUser = {
119
- userId: this.props.uid,
120
- displayName: this.props.displayName,
121
- profilePic: this.props.profilePic,
122
- };
123
- this.setState({
124
- selectedUser: defaultUser,
125
- userName: this.props.displayName,
126
- roomNumber: this.props.unit,
127
- phone: !_.isEmpty(this.props.phoneNumber) ? this.props.phoneNumber : '',
128
- });
129
- }
130
- };
131
-
132
- componentWillUnmount() {
133
- clearInterval(this.checkThumb);
134
- }
135
-
136
- onChangeName = userName => {
137
- const update = { userName };
138
- if (!this.state.customFields || !_.some(this.state.customFields, 'isTitle')) {
139
- update.title = userName;
140
- }
141
- this.setState(update);
142
- };
143
-
144
- // PC-1255: Handle user picker navigation
145
- onPressUser = () => {
146
- const { users, selectedUser } = this.state;
147
- if (!users || users.length === 0) return;
148
-
149
- Services.navigation.navigate(values.screenUserPicker, {
150
- currentUser: selectedUser,
151
- users,
152
- onSelectUser: this.onSelectUser.bind(this),
153
- });
154
- };
155
-
156
- // PC-1255: Handle user selection and auto-populate contact details
157
- onSelectUser = async user => {
158
- const update = {
159
- selectedUser: user,
160
- userName: user.displayName,
161
- };
162
-
163
- // PC-1255: Try to fetch full user details for auto-population
164
- try {
165
- // getSiteUsers returns 'Id', but fetchUser expects 'userId'
166
- const userId = user.userId || user.Id;
167
- const response = await userActions.fetchUser(this.props.site, userId);
168
- if (response.data && response.data.user) {
169
- const userDetails = response.data.user;
170
- if (userDetails.phoneNumber) {
171
- update.phone = userDetails.phoneNumber;
172
- }
173
- if (userDetails.unit) {
174
- update.roomNumber = userDetails.unit;
175
- }
176
- if (!this.state.customFields || !_.some(this.state.customFields, 'isTitle')) {
177
- update.title = userDetails.displayName || null;
29
+ constructor(props) {
30
+ super(props);
31
+ this.state = {
32
+ submitting: false,
33
+ success: false,
34
+ fail: false,
35
+ error: null,
36
+ showError: false,
37
+ loadingTypes: values.forceCustomFields,
38
+
39
+ userName: "",
40
+ roomNumber: "",
41
+ phone: "",
42
+ title: "",
43
+ description: "",
44
+ times: "",
45
+
46
+ type: "General",
47
+
48
+ uploadingImage: false,
49
+ images: [
50
+ {
51
+ add: true,
52
+ },
53
+ ],
54
+ showFullscreenVideo: false,
55
+ currentVideoUrl: "",
56
+
57
+ isHome: false,
58
+
59
+ types: [],
60
+
61
+ confirmationToShow: false,
62
+
63
+ customFields: [],
64
+ customFieldImages: {},
65
+ customFieldDocuments: {},
66
+ isDateTimePickerVisible: false,
67
+ popUpType: "date",
68
+ dateFieldId: null,
69
+ imageFieldId: null,
70
+
71
+ // PC-1255: On-behalf request fields
72
+ canCreateOnBehalf: false,
73
+ selectedUser: null,
74
+ users: [],
75
+ };
76
+ this.checkThumb = null;
77
+ this.keyboardTypes = {
78
+ phone: "phone-pad",
79
+ email: "email-address",
80
+ text: "default",
81
+ };
178
82
  }
179
- }
180
- } catch (error) {
181
- // Permission denied (403) or other error - continue without auto-population
182
- // User can still manually enter contact details
183
- console.log('Could not fetch user details for auto-population:', error);
184
- } finally {
185
- // In any case, we still need a title. Inform the user to set one form field as a title.
186
- if (!state.title && !update.title) {
187
- update.title = "[Missing title - Set one of the form field as title in the community manager]"
83
+
84
+ componentDidMount() {
85
+ this.checkUserPermissions();
86
+ this.getJobTypes();
87
+ }
88
+
89
+ checkUserPermissions = async () => {
90
+ // PC-1255: Check if user has userManagement permission
91
+ const hasUserManagement =
92
+ this.props.permissions?.includes("userManagement") || false;
93
+
94
+ if (hasUserManagement) {
95
+ this.setState({ canCreateOnBehalf: true });
96
+ // Load site users for picker
97
+ await this.loadSiteUsers();
98
+ } else {
99
+ // Default to logged-in user for non-staff or staff without permission
100
+ this.setDefaultUser();
101
+ }
102
+ };
103
+
104
+ loadSiteUsers = async () => {
105
+ try {
106
+ const response = await userActions.getSiteUsers(this.props.site);
107
+ // PC-1255: Load all users (not just residents) to match web admin behavior
108
+ const users = _.sortBy(response.data.Items || [], (u) =>
109
+ (u.displayName || "").toLowerCase(),
110
+ );
111
+ this.setState({ users });
112
+ } catch (error) {
113
+ console.log("Error loading site users:", error);
114
+ // Fall back to default user if loading fails
115
+ this.setDefaultUser();
116
+ }
117
+ };
118
+
119
+ setDefaultUser = () => {
120
+ if (this.props.userType !== "KIOSK") {
121
+ const defaultUser = {
122
+ userId: this.props.uid,
123
+ displayName: this.props.displayName,
124
+ profilePic: this.props.profilePic,
125
+ };
126
+ this.setState({
127
+ selectedUser: defaultUser,
128
+ userName: this.props.displayName,
129
+ roomNumber: this.props.unit,
130
+ phone: !_.isEmpty(this.props.phoneNumber) ? this.props.phoneNumber : "",
131
+ });
132
+ }
133
+ };
134
+
135
+ componentWillUnmount() {
136
+ clearInterval(this.checkThumb);
137
+ }
138
+
139
+ onChangeName = (userName) => {
140
+ const update = { userName };
141
+ if (
142
+ !this.state.customFields ||
143
+ !_.some(this.state.customFields, "isTitle")
144
+ ) {
145
+ update.title = userName;
146
+ }
147
+ this.setState(update);
148
+ };
149
+
150
+ // PC-1255: Handle user picker navigation
151
+ onPressUser = () => {
152
+ const { users, selectedUser } = this.state;
153
+ if (!users || users.length === 0) return;
154
+
155
+ Services.navigation.navigate(values.screenUserPicker, {
156
+ currentUser: selectedUser,
157
+ users,
158
+ onSelectUser: this.onSelectUser.bind(this),
159
+ });
160
+ };
161
+
162
+ // PC-1255: Handle user selection and auto-populate contact details
163
+ onSelectUser = async (user) => {
164
+ const update = {
165
+ selectedUser: user,
166
+ userName: user.displayName,
167
+ };
168
+
169
+ // PC-1255: Try to fetch full user details for auto-population
170
+ try {
171
+ // getSiteUsers returns 'Id', but fetchUser expects 'userId'
172
+ const userId = user.userId || user.Id;
173
+ const response = await userActions.fetchUser(this.props.site, userId);
174
+ if (response.data && response.data.user) {
175
+ const userDetails = response.data.user;
176
+ if (userDetails.phoneNumber) {
177
+ update.phone = userDetails.phoneNumber;
178
+ }
179
+ if (userDetails.unit) {
180
+ update.roomNumber = userDetails.unit;
181
+ }
182
+ if (
183
+ !this.state.customFields ||
184
+ !_.some(this.state.customFields, "isTitle")
185
+ ) {
186
+ update.title = userDetails.displayName || null;
187
+ }
188
+ }
189
+ } catch (error) {
190
+ // Permission denied (403) or other error - continue without auto-population
191
+ // User can still manually enter contact details
192
+ console.log("Could not fetch user details for auto-population:", error);
193
+ } finally {
194
+ // In any case, we still need a title. Inform the user to set one form field as a title.
195
+ if (!state.title && !update.title) {
196
+ update.title =
197
+ "[Missing title - Set one of the form field as title in the community manager]";
198
+ }
199
+ }
200
+ this.setState(update);
201
+ };
202
+
203
+ onChangeAnswer = (fieldId, answer) => {
204
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
205
+ const field = update.customFields[fieldId];
206
+ field.answer = answer;
207
+ if (field.isTitle) update.title = field.answer;
208
+ this.setState(update);
209
+ };
210
+
211
+ onChangeToggleAnswer = (fieldId, answer) => {
212
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
213
+ const field = update.customFields[fieldId];
214
+ field.answer = field.answer === answer ? undefined : answer;
215
+ if (field.isTitle) update.title = field.answer;
216
+ this.setState(update);
217
+ };
218
+
219
+ onChangeCheckboxAnswer = (fieldId, answer) => {
220
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
221
+ const field = update.customFields[fieldId];
222
+ field.answer = _.xor(field.answer || [], [answer]);
223
+ if (field.isTitle) update.title = field.answer.join(", ");
224
+ this.setState(update);
225
+ };
226
+
227
+ onOpenDatePicker = (field, fieldId) => {
228
+ Keyboard.dismiss();
229
+ this.setState({
230
+ dateFieldId: fieldId,
231
+ popUpType: field.type,
232
+ isDateTimePickerVisible: true,
233
+ });
234
+ };
235
+
236
+ onClearDate = (fieldId) => {
237
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
238
+ const field = update.customFields[fieldId];
239
+ field.answer = undefined;
240
+ if (field.isTitle) update.title = field.answer;
241
+ this.setState(update);
242
+ };
243
+
244
+ onDateSelected = (date) => {
245
+ const { customFields, dateFieldId, popUpType } = this.state;
246
+ const update = {
247
+ customFields: _.cloneDeep(customFields),
248
+ isDateTimePickerVisible: false,
249
+ fieldId: null,
250
+ };
251
+ const field = update.customFields[dateFieldId];
252
+ const dateObj = moment(date);
253
+ if (popUpType === "date") {
254
+ field.answer = dateObj.format("YYYY-MM-DD");
255
+ if (field.isTitle) update.title = dateObj.format("DD MMM YYYY");
256
+ } else {
257
+ field.answer = dateObj.format("HH:mm");
258
+ if (field.isTitle) update.title = dateObj.format("h:mm a");
259
+ }
260
+ this.setState(update);
261
+ };
262
+
263
+ onUploadStartedImage = (uploadUri, imageUri) => {
264
+ const { imageFieldId } = this.state;
265
+ const imagesUpdate = this.getImages(imageFieldId);
266
+ imagesUpdate.splice(imagesUpdate.length - 1, 0, {
267
+ uploading: true,
268
+ uploadProgress: "0%",
269
+ uploadUri,
270
+ imageUri,
271
+ allowRetry: true,
272
+ });
273
+
274
+ this.setImages(imagesUpdate, imageFieldId);
275
+ };
276
+
277
+ onUploadProgressImage = (progress) => {
278
+ const { imageFieldId } = this.state;
279
+ const imagesUpdate = this.getImages(imageFieldId);
280
+ imagesUpdate.map((img) => {
281
+ if (img.uploadUri === progress.uri) {
282
+ img.uploadProgress = progress.percentage;
283
+ img.uploading = true;
284
+ img.allowRetry = true;
285
+ }
286
+ });
287
+
288
+ this.setImages(imagesUpdate, imageFieldId);
289
+ };
290
+
291
+ onUploadSuccessImage = async (uri, uploadUri) => {
292
+ const { imageFieldId } = this.state;
293
+ const imagesUpdate = this.getImages(imageFieldId);
294
+ imagesUpdate.map((img) => {
295
+ if (img.uploadUri === uploadUri && img.uploading) {
296
+ img.url = uri.replace("/general/", "/general1400/");
297
+ img.thumbNailExists = false;
298
+ img.thumbNailUrl = Helper.getThumb300(img.url);
299
+ img.allowRetry = true;
300
+ }
301
+ });
302
+
303
+ this.setImages(imagesUpdate, imageFieldId, () => this.waitForThumbnails());
304
+ };
305
+
306
+ onUploadFailedImage = (uploadUri) => {
307
+ const { imageFieldId } = this.state;
308
+ const imagesUpdate = this.getImages(imageFieldId);
309
+ imagesUpdate.map((img) => {
310
+ if (img.uploadUri === uploadUri) {
311
+ img.uploading = true; // Requried for retry
312
+ img.uploadProgress = "";
313
+ img.allowRetry = true;
314
+ }
315
+ });
316
+
317
+ this.setImages(imagesUpdate, imageFieldId);
318
+ };
319
+
320
+ onLibrarySelected = (uri) => {
321
+ const { imageFieldId } = this.state;
322
+ const imagesUpdate = this.getImages(imageFieldId);
323
+ imagesUpdate.splice(imagesUpdate.length - 1, 0, {
324
+ uploading: false,
325
+ allowRetry: false,
326
+ url: Helper.get1400(uri),
327
+ thumbNailExists: true,
328
+ thumbNailUrl: Helper.getThumb300(uri),
329
+ });
330
+
331
+ this.setImages(imagesUpdate, imageFieldId);
332
+ };
333
+
334
+ onUploadStartedDocument = (
335
+ uploadUri,
336
+ documentUri,
337
+ documentName,
338
+ documentExt,
339
+ documentFieldId,
340
+ ) => {
341
+ const documentsUpdate = this.getDocuments(documentFieldId);
342
+ documentsUpdate.splice(documentsUpdate.length - 1, 0, {
343
+ uploading: true,
344
+ uploadProgress: "0%",
345
+ uploadUri,
346
+ documentUri,
347
+ documentName,
348
+ documentExt,
349
+ allowRetry: true,
350
+ });
351
+
352
+ this.setDocuments(documentsUpdate, documentFieldId);
353
+ };
354
+
355
+ onUploadProgressDocument = (progress, documentFieldId) => {
356
+ const documentsUpdate = this.getDocuments(documentFieldId);
357
+ documentsUpdate.map((doc) => {
358
+ if (doc.uploadUri === progress.uri) {
359
+ doc.uploadProgress = progress.percentage;
360
+ doc.uploading = true;
361
+ doc.allowRetry = true;
362
+ }
363
+ });
364
+
365
+ this.setDocuments(documentsUpdate, documentFieldId);
366
+ };
367
+
368
+ onUploadSuccessDocument = async (uri, uploadUri, documentFieldId) => {
369
+ const documentsUpdate = this.getDocuments(documentFieldId);
370
+ documentsUpdate.map((doc) => {
371
+ if (doc.uploadUri === uploadUri && doc.uploading) {
372
+ doc.uploading = false;
373
+ doc.uploadProgress = "100%";
374
+ doc.url = uri;
375
+ doc.allowRetry = true;
376
+ }
377
+ });
378
+
379
+ this.setDocuments(documentsUpdate, documentFieldId);
380
+ };
381
+
382
+ onUploadFailedDocument = (uploadUri, documentFieldId) => {
383
+ const documentsUpdate = this.getDocuments(documentFieldId);
384
+ documentsUpdate.map((doc) => {
385
+ if (doc.uploadUri === uploadUri) {
386
+ doc.uploading = true; // Requried for retry
387
+ doc.uploadProgress = "";
388
+ doc.allowRetry = true;
389
+ }
390
+ });
391
+
392
+ this.setDocuments(documentsUpdate, documentFieldId);
393
+ };
394
+
395
+ onPressBack() {
396
+ Services.navigation.goBack();
397
+ }
398
+
399
+ onPressType() {
400
+ Services.navigation.navigate(values.screenJobTypePicker, {
401
+ currentType: this.state.type,
402
+ types: this.state.types,
403
+ onSelectType: this.pickType.bind(this),
404
+ });
405
+ }
406
+
407
+ onCloseConfirmationPopup() {
408
+ this.setState({
409
+ confirmationToShow: false,
410
+ });
411
+ Services.navigation.goBack();
412
+ this.props.onBack && this.props.onBack();
413
+ }
414
+
415
+ onConfirmationReset() {
416
+ this.setState(
417
+ {
418
+ confirmationToShow: false,
419
+ title: "",
420
+ description: "",
421
+ times: "",
422
+ isHome: false,
423
+ uploadingImage: false,
424
+ images: [
425
+ {
426
+ add: true,
427
+ },
428
+ ],
429
+ submitting: false,
430
+ success: false,
431
+ fail: false,
432
+ customFields: [],
433
+ customFieldImages: {},
434
+ customFieldDocuments: {},
435
+ isDateTimePickerVisible: false,
436
+ popUpType: "date",
437
+ dateFieldId: null,
438
+ imageFieldId: null,
439
+ },
440
+ () => this.pickType(this.state.type),
441
+ );
442
+ }
443
+
444
+ isFieldValid = (field, fieldId) => {
445
+ const { mandatory, type, answer } = field;
446
+ if (["staticTitle", "staticText"].includes(type)) return true;
447
+
448
+ const checkMandatory = () => {
449
+ if (!mandatory) return true;
450
+ switch (type) {
451
+ case "yn":
452
+ return _.isBoolean(answer);
453
+ case "image":
454
+ const imagesList = this.getImageUrls(fieldId);
455
+ return imagesList.length > 0;
456
+ case "document":
457
+ const documentsList = this.getDocumentAnswers(fieldId);
458
+ return documentsList.length > 0;
459
+ case "checkbox":
460
+ return _.isArray(answer) && answer.length > 0;
461
+ default:
462
+ return !_.isNil(answer) && !_.isEmpty(answer);
463
+ }
464
+ };
465
+ const checkFormat = () => {
466
+ if (_.isNil(answer) || _.isEmpty(answer)) return true;
467
+ switch (type) {
468
+ case "email":
469
+ return Helper.isEmail(answer);
470
+ case "date":
471
+ return moment(answer, "YYYY-MM-DD", true).isValid();
472
+ case "time":
473
+ return moment(answer, "HH:mm", true).isValid();
474
+ default:
475
+ return true;
476
+ }
477
+ };
478
+
479
+ const valid = checkMandatory() && checkFormat();
480
+ return valid;
481
+ };
482
+
483
+ getJobTypes() {
484
+ maintenanceActions
485
+ .getJobTypes(Helper.getSite(this.props.site))
486
+ .then((res) => {
487
+ this.setState({
488
+ types: res.data,
489
+ });
490
+ this.getDefaultJob();
491
+ })
492
+ .catch(() => {});
493
+ }
494
+
495
+ pickType(type) {
496
+ const { types } = this.state;
497
+ const selected = types.find((t) => t.typeName === type) || {};
498
+ if (values.forceCustomFields && !selected.hasCustomFields) {
499
+ this.setState({
500
+ type,
501
+ customFields: [],
502
+ noType: true,
503
+ });
504
+ return;
505
+ }
506
+ const update = {
507
+ type,
508
+ customFields:
509
+ selected.hasCustomFields && selected.customFields.length > 0
510
+ ? _.cloneDeep(selected.customFields)
511
+ : [],
512
+ loadingTypes: false,
513
+ };
514
+
515
+ if (
516
+ !_.isEmpty(update.customFields) &&
517
+ !_.some(update.customFields, "isTitle")
518
+ ) {
519
+ update.title = this.state.userName;
520
+ }
521
+
522
+ this.setState(update);
523
+ }
524
+
525
+ getDefaultJob() {
526
+ const { types, jobId } = this.state;
527
+ if (types.length !== 0 && jobId == null) {
528
+ const defaultType = types[0];
529
+ this.pickType(defaultType.typeName);
530
+ }
531
+ }
532
+
533
+ showUploadMenu = (fieldId) => {
534
+ Keyboard.dismiss();
535
+ if (this.state.uploadingImage || this.state.submitting) {
536
+ return;
537
+ }
538
+ if (fieldId) this.setState({ imageFieldId: fieldId });
539
+ this.imageUploader.showUploadMenu();
540
+ };
541
+
542
+ submit = async () => {
543
+ this.setState({ submitting: true });
544
+
545
+ let description = this.state.description;
546
+
547
+ if (this.state.isHome) {
548
+ description = description + `. -- Times Available: ${this.state.times}`;
549
+ }
550
+
551
+ setTimeout(() => {
552
+ this.scrollContainer.scrollTo({ y: 0 });
553
+ }, 100);
554
+
555
+ const images = this.getImageUrls();
556
+
557
+ // Fix custom images field answers
558
+ const customFields = _.cloneDeep(this.state.customFields);
559
+ const updatedCustomFields = customFields.map((field, fieldId) => {
560
+ if (field.type === "image") field.answer = this.getImageUrls(fieldId);
561
+ if (field.type === "document")
562
+ field.answer = this.getDocumentAnswers(fieldId);
563
+ return field;
564
+ });
565
+
566
+ maintenanceActions
567
+ .sendMaintenanceRequest(
568
+ this.props.uid,
569
+ this.state.userName,
570
+ this.state.phone,
571
+ this.state.roomNumber,
572
+ this.state.title,
573
+ description,
574
+ null,
575
+ this.state.type,
576
+ images,
577
+ Helper.getSite(this.props.site),
578
+ this.state.isHome,
579
+ this.state.times,
580
+ updatedCustomFields,
581
+ )
582
+ .then((res) => {
583
+ if (res.data.success) {
584
+ this.refreshRequest(res.data.searchResult);
585
+ if (this.props.onSubmissionSuccess)
586
+ this.props.onSubmissionSuccess(res.data);
587
+ this.setState({
588
+ submitting: false,
589
+ success: true,
590
+ confirmationToShow: true,
591
+ });
592
+ } else {
593
+ this.setState({
594
+ submitting: false,
595
+ });
596
+ }
597
+ })
598
+ .catch((err) => {
599
+ console.log("maintenance submission fail.");
600
+ console.log(err);
601
+ this.setState({
602
+ submitting: false,
603
+ });
604
+ });
605
+ };
606
+
607
+ refreshRequest = async (id) => {
608
+ try {
609
+ const job = await maintenanceActions.getJob(
610
+ Helper.getSite(this.props.site),
611
+ id,
612
+ );
613
+ this.props.jobAdded(job.data);
614
+ } catch (error) {
615
+ console.log("refreshRequest error", error);
616
+ }
617
+ };
618
+
619
+ validateCustomFields = () => {
620
+ const { customFields } = this.state;
621
+ if (!customFields || customFields.length === 0) return true;
622
+
623
+ return customFields.every((field, index) => {
624
+ const isValid = this.isFieldValid(field, index);
625
+ return isValid;
626
+ });
627
+ };
628
+
629
+ submitRequest() {
630
+ const {
631
+ customFields,
632
+ submitting,
633
+ uploadingImage,
634
+ title,
635
+ roomNumber,
636
+ isHome,
637
+ times,
638
+ } = this.state;
639
+ const hasCustomFields = customFields && customFields.length > 0;
640
+
641
+ if (submitting || !this.props.connected) {
642
+ if (!this.props.connected) {
643
+ this.setState({
644
+ error: { message: "No internet connection detected" },
645
+ });
646
+ }
647
+ return;
648
+ }
649
+ if (uploadingImage) return;
650
+
651
+ this.setState({ error: null, showError: false });
652
+
653
+ // PC-1255: Validate user selection for on-behalf requests
654
+ if (this.state.canCreateOnBehalf && !this.state.selectedUser) {
655
+ console.log("submitRequest - no user selected for on-behalf request");
656
+ this.setState({ showError: true });
657
+ return;
658
+ }
659
+
660
+ if (title.length === 0 || !roomNumber || roomNumber.length === 0) {
661
+ console.log("submitRequest - error", { title, roomNumber });
662
+ this.setState({ showError: true });
663
+ return;
664
+ }
665
+ if (hasCustomFields) {
666
+ if (!this.validateCustomFields()) {
667
+ console.log("submitRequest - custom fields error");
668
+ this.setState({ showError: true });
669
+ return;
670
+ }
671
+ } else {
672
+ if (isHome && times.length < 2) {
673
+ console.log("submitRequest - error", { isHome, times });
674
+ this.setState({ showError: true });
675
+ return;
676
+ }
677
+ }
678
+ this.submit();
679
+ }
680
+
681
+ getImages = (fieldId = null) => {
682
+ const { images, customFieldImages } = this.state;
683
+ const imagesList = _.cloneDeep(
684
+ fieldId ? customFieldImages[fieldId] : images,
685
+ );
686
+ if (!imagesList || !Array.isArray(imagesList) || imagesList.length === 0) {
687
+ return [{ add: true }];
688
+ }
689
+ return imagesList;
690
+ };
691
+
692
+ setImages = (imagesList, fieldId = null, callback = null) => {
693
+ let update = {};
694
+ if (fieldId) {
695
+ const customFieldImages = _.cloneDeep(this.state.customFieldImages);
696
+ customFieldImages[fieldId] = imagesList;
697
+ update = { customFieldImages };
698
+ } else {
699
+ update = { images: imagesList };
700
+ }
701
+ this.setState(update, callback);
702
+ };
703
+
704
+ getImageUrls = (fieldId = null) => {
705
+ const imagesList = this.getImages(fieldId);
706
+ return _.filter(imagesList, (img) => {
707
+ return !img.uploading && !img.add;
708
+ }).map((img) => {
709
+ return img.url;
710
+ });
711
+ };
712
+
713
+ waitForThumbnails = () => {
714
+ if (this.checkThumb) return;
715
+
716
+ this.checkThumb = setInterval(async () => {
717
+ const { imageFieldId } = this.state;
718
+ const imagesList = this.getImages(imageFieldId);
719
+ const imagesUpdate = [];
720
+ await Promise.all(
721
+ imagesList.map((image) => {
722
+ return new Promise(async (resolve) => {
723
+ const newImage = { ...image };
724
+ imagesUpdate.push(newImage);
725
+ if (newImage.url && !newImage.thumbNailExists) {
726
+ newImage.uploading = false;
727
+ newImage.allowRetry = false;
728
+ newImage.thumbNailExists = await Helper.imageExists(
729
+ newImage.thumbNailUrl,
730
+ );
731
+ resolve(newImage.thumbNailExists);
732
+ }
733
+ resolve(true);
734
+ });
735
+ }),
736
+ );
737
+ const thumbnailsExist = imagesUpdate.every(
738
+ (image) => !image.url || image.thumbNailExists,
739
+ );
740
+ if (thumbnailsExist) {
741
+ clearInterval(this.checkThumb);
742
+ this.checkThumb = null;
743
+ this.setImages(imagesUpdate, imageFieldId);
744
+ }
745
+ }, 2000);
746
+ };
747
+
748
+ removeImage = (index, fieldId) => {
749
+ const imagesUpdate = this.getImages(fieldId);
750
+ imagesUpdate.splice(index, 1);
751
+
752
+ this.setImages(imagesUpdate, fieldId);
753
+ };
754
+
755
+ getDocuments = (fieldId) => {
756
+ const { customFieldDocuments } = this.state;
757
+ const documentsList = _.cloneDeep(customFieldDocuments[fieldId]) || [];
758
+ return documentsList;
759
+ };
760
+
761
+ setDocuments = (documentsList, fieldId) => {
762
+ let update = {};
763
+ const customFieldDocuments = _.cloneDeep(this.state.customFieldDocuments);
764
+ customFieldDocuments[fieldId] = documentsList;
765
+ update = { customFieldDocuments };
766
+ this.setState(update);
767
+ };
768
+
769
+ getDocumentAnswers = (fieldId) => {
770
+ const documentsList = this.getDocuments(fieldId);
771
+ return _.filter(documentsList, (doc) => {
772
+ return !doc.uploading && doc.url;
773
+ }).map((doc) => {
774
+ return {
775
+ name: doc.documentName,
776
+ ext: doc.documentExt,
777
+ url: doc.url,
778
+ };
779
+ });
780
+ };
781
+
782
+ removeDocument = (index, fieldId) => {
783
+ const documentsUpdate = this.getDocuments(fieldId);
784
+ documentsUpdate.splice(index, 1);
785
+
786
+ this.setDocuments(documentsUpdate, fieldId);
787
+ };
788
+
789
+ toggleFullscreenVideo = (url) => {
790
+ if (typeof url !== "string") url = "";
791
+ this.setState({
792
+ showFullscreenVideo: url.length > 0,
793
+ currentVideoUrl: url,
794
+ });
795
+ };
796
+
797
+ renderImageUploader() {
798
+ return (
799
+ <Components.ImageUploader
800
+ ref={(ref) => (this.imageUploader = ref)}
801
+ onUploadStarted={this.onUploadStartedImage}
802
+ onUploadSuccess={this.onUploadSuccessImage}
803
+ onUploadFailed={this.onUploadFailedImage}
804
+ onUploadProgress={this.onUploadProgressImage}
805
+ onLibrarySelected={this.onLibrarySelectedImage}
806
+ size={{ width: 1400 }}
807
+ quality={0.8}
808
+ fileName={"serviceImage"}
809
+ popupTitle={"Upload Image"}
810
+ userId={this.props.uid}
811
+ allowsEditing={false}
812
+ multiple
813
+ hideLibrary
814
+ />
815
+ );
188
816
  }
189
817
 
190
- }
191
- this.setState(update);
192
- };
193
-
194
-
195
- onChangeAnswer = (fieldId, answer) => {
196
- const update = { customFields: _.cloneDeep(this.state.customFields) };
197
- const field = update.customFields[fieldId];
198
- field.answer = answer;
199
- if (field.isTitle) update.title = field.answer;
200
- this.setState(update);
201
- };
202
-
203
- onChangeToggleAnswer = (fieldId, answer) => {
204
- const update = { customFields: _.cloneDeep(this.state.customFields) };
205
- const field = update.customFields[fieldId];
206
- field.answer = field.answer === answer ? undefined : answer;
207
- if (field.isTitle) update.title = field.answer;
208
- this.setState(update);
209
- };
210
-
211
- onChangeCheckboxAnswer = (fieldId, answer) => {
212
- const update = { customFields: _.cloneDeep(this.state.customFields) };
213
- const field = update.customFields[fieldId];
214
- field.answer = _.xor(field.answer || [], [answer]);
215
- if (field.isTitle) update.title = field.answer.join(', ');
216
- this.setState(update);
217
- };
218
-
219
- onOpenDatePicker = (field, fieldId) => {
220
- Keyboard.dismiss();
221
- this.setState({ dateFieldId: fieldId, popUpType: field.type, isDateTimePickerVisible: true });
222
- };
223
-
224
- onClearDate = fieldId => {
225
- const update = { customFields: _.cloneDeep(this.state.customFields) };
226
- const field = update.customFields[fieldId];
227
- field.answer = undefined;
228
- if (field.isTitle) update.title = field.answer;
229
- this.setState(update);
230
- };
231
-
232
- onDateSelected = date => {
233
- const { customFields, dateFieldId, popUpType } = this.state;
234
- const update = { customFields: _.cloneDeep(customFields), isDateTimePickerVisible: false, fieldId: null };
235
- const field = update.customFields[dateFieldId];
236
- const dateObj = moment(date);
237
- if (popUpType === 'date') {
238
- field.answer = dateObj.format('YYYY-MM-DD');
239
- if (field.isTitle) update.title = dateObj.format('DD MMM YYYY');
240
- } else {
241
- field.answer = dateObj.format('HH:mm');
242
- if (field.isTitle) update.title = dateObj.format('h:mm a');
243
- }
244
- this.setState(update);
245
- };
246
-
247
- onUploadStartedImage = (uploadUri, imageUri) => {
248
- const { imageFieldId } = this.state;
249
- const imagesUpdate = this.getImages(imageFieldId);
250
- imagesUpdate.splice(imagesUpdate.length - 1, 0, {
251
- uploading: true,
252
- uploadProgress: '0%',
253
- uploadUri,
254
- imageUri,
255
- allowRetry: true,
256
- });
257
-
258
- this.setImages(imagesUpdate, imageFieldId);
259
- };
260
-
261
- onUploadProgressImage = progress => {
262
- const { imageFieldId } = this.state;
263
- const imagesUpdate = this.getImages(imageFieldId);
264
- imagesUpdate.map(img => {
265
- if (img.uploadUri === progress.uri) {
266
- img.uploadProgress = progress.percentage;
267
- img.uploading = true;
268
- img.allowRetry = true;
269
- }
270
- });
271
-
272
- this.setImages(imagesUpdate, imageFieldId);
273
- };
274
-
275
- onUploadSuccessImage = async (uri, uploadUri) => {
276
- const { imageFieldId } = this.state;
277
- const imagesUpdate = this.getImages(imageFieldId);
278
- imagesUpdate.map(img => {
279
- if (img.uploadUri === uploadUri && img.uploading) {
280
- img.url = uri.replace('/general/', '/general1400/');
281
- img.thumbNailExists = false;
282
- img.thumbNailUrl = Helper.getThumb300(img.url);
283
- img.allowRetry = true;
284
- }
285
- });
286
-
287
- this.setImages(imagesUpdate, imageFieldId, () => this.waitForThumbnails());
288
- };
289
-
290
- onUploadFailedImage = uploadUri => {
291
- const { imageFieldId } = this.state;
292
- const imagesUpdate = this.getImages(imageFieldId);
293
- imagesUpdate.map(img => {
294
- if (img.uploadUri === uploadUri) {
295
- img.uploading = true; // Requried for retry
296
- img.uploadProgress = '';
297
- img.allowRetry = true;
298
- }
299
- });
300
-
301
- this.setImages(imagesUpdate, imageFieldId);
302
- };
303
-
304
- onLibrarySelected = uri => {
305
- const { imageFieldId } = this.state;
306
- const imagesUpdate = this.getImages(imageFieldId);
307
- imagesUpdate.splice(imagesUpdate.length - 1, 0, {
308
- uploading: false,
309
- allowRetry: false,
310
- url: Helper.get1400(uri),
311
- thumbNailExists: true,
312
- thumbNailUrl: Helper.getThumb300(uri),
313
- });
314
-
315
- this.setImages(imagesUpdate, imageFieldId);
316
- };
317
-
318
- onUploadStartedDocument = (uploadUri, documentUri, documentName, documentExt, documentFieldId) => {
319
- const documentsUpdate = this.getDocuments(documentFieldId);
320
- documentsUpdate.splice(documentsUpdate.length - 1, 0, {
321
- uploading: true,
322
- uploadProgress: '0%',
323
- uploadUri,
324
- documentUri,
325
- documentName,
326
- documentExt,
327
- allowRetry: true,
328
- });
329
-
330
- this.setDocuments(documentsUpdate, documentFieldId);
331
- };
332
-
333
- onUploadProgressDocument = (progress, documentFieldId) => {
334
- const documentsUpdate = this.getDocuments(documentFieldId);
335
- documentsUpdate.map(doc => {
336
- if (doc.uploadUri === progress.uri) {
337
- doc.uploadProgress = progress.percentage;
338
- doc.uploading = true;
339
- doc.allowRetry = true;
340
- }
341
- });
342
-
343
- this.setDocuments(documentsUpdate, documentFieldId);
344
- };
345
-
346
- onUploadSuccessDocument = async (uri, uploadUri, documentFieldId) => {
347
- const documentsUpdate = this.getDocuments(documentFieldId);
348
- documentsUpdate.map(doc => {
349
- if (doc.uploadUri === uploadUri && doc.uploading) {
350
- doc.uploading = false;
351
- doc.uploadProgress = '100%';
352
- doc.url = uri;
353
- doc.allowRetry = true;
354
- }
355
- });
356
-
357
- this.setDocuments(documentsUpdate, documentFieldId);
358
- };
359
-
360
- onUploadFailedDocument = (uploadUri, documentFieldId) => {
361
- const documentsUpdate = this.getDocuments(documentFieldId);
362
- documentsUpdate.map(doc => {
363
- if (doc.uploadUri === uploadUri) {
364
- doc.uploading = true; // Requried for retry
365
- doc.uploadProgress = '';
366
- doc.allowRetry = true;
367
- }
368
- });
369
-
370
- this.setDocuments(documentsUpdate, documentFieldId);
371
- };
372
-
373
- onPressBack() {
374
- Services.navigation.goBack();
375
- }
376
-
377
- onPressType() {
378
- Services.navigation.navigate(values.screenJobTypePicker, {
379
- currentType: this.state.type,
380
- types: this.state.types,
381
- onSelectType: this.pickType.bind(this),
382
- });
383
- }
384
-
385
- onCloseConfirmationPopup() {
386
- this.setState({
387
- confirmationToShow: false,
388
- });
389
- Services.navigation.goBack();
390
- this.props.onBack && this.props.onBack();
391
- }
392
-
393
- onConfirmationReset() {
394
- this.setState(
395
- {
396
- confirmationToShow: false,
397
- title: '',
398
- description: '',
399
- times: '',
400
- isHome: false,
401
- uploadingImage: false,
402
- images: [
403
- {
404
- add: true,
405
- },
406
- ],
407
- submitting: false,
408
- success: false,
409
- fail: false,
410
- customFields: [],
411
- customFieldImages: {},
412
- customFieldDocuments: {},
413
- isDateTimePickerVisible: false,
414
- popUpType: 'date',
415
- dateFieldId: null,
416
- imageFieldId: null,
417
- },
418
- () => this.pickType(this.state.type),
419
- );
420
- }
421
-
422
- isFieldValid = (field, fieldId) => {
423
- const { mandatory, type, answer } = field;
424
- if (['staticTitle', 'staticText'].includes(type)) return true;
425
-
426
- const checkMandatory = () => {
427
- if (!mandatory) return true;
428
- switch (type) {
429
- case 'yn':
430
- return _.isBoolean(answer);
431
- case 'image':
432
- const imagesList = this.getImageUrls(fieldId);
433
- return imagesList.length > 0;
434
- case 'document':
435
- const documentsList = this.getDocumentAnswers(fieldId);
436
- return documentsList.length > 0;
437
- case 'checkbox':
438
- return _.isArray(answer) && answer.length > 0;
439
- default:
440
- return !_.isNil(answer) && !_.isEmpty(answer);
441
- }
442
- };
443
- const checkFormat = () => {
444
- if (_.isNil(answer) || _.isEmpty(answer)) return true;
445
- switch (type) {
446
- case 'email':
447
- return Helper.isEmail(answer);
448
- case 'date':
449
- return moment(answer, 'YYYY-MM-DD', true).isValid();
450
- case 'time':
451
- return moment(answer, 'HH:mm', true).isValid();
452
- default:
453
- return true;
454
- }
455
- };
456
-
457
- const valid = checkMandatory() && checkFormat();
458
- return valid;
459
- };
460
-
461
- getJobTypes() {
462
- maintenanceActions
463
- .getJobTypes(Helper.getSite(this.props.site))
464
- .then(res => {
465
- this.setState({
466
- types: res.data,
467
- });
468
- this.getDefaultJob();
469
- })
470
- .catch(() => {});
471
- }
472
-
473
- pickType(type) {
474
- const { types } = this.state;
475
- const selected = types.find(t => t.typeName === type) || {};
476
- if (values.forceCustomFields && !selected.hasCustomFields) {
477
- this.setState({
478
- type,
479
- customFields: [],
480
- noType: true,
481
- });
482
- return;
483
- }
484
- const update = {
485
- type,
486
- customFields: selected.hasCustomFields && selected.customFields.length > 0 ? _.cloneDeep(selected.customFields) : [],
487
- loadingTypes: false,
488
- };
489
-
490
- if (!_.isEmpty(update.customFields) && !_.some(update.customFields, 'isTitle')) {
491
- update.title = this.state.userName;
492
- }
493
-
494
- this.setState(update);
495
- }
496
-
497
- getDefaultJob() {
498
- const { types, jobId } = this.state;
499
- if (types.length !== 0 && jobId == null) {
500
- const defaultType = types[0];
501
- this.pickType(defaultType.typeName);
502
- }
503
- }
504
-
505
- showUploadMenu = fieldId => {
506
- Keyboard.dismiss();
507
- if (this.state.uploadingImage || this.state.submitting) {
508
- return;
509
- }
510
- if (fieldId) this.setState({ imageFieldId: fieldId });
511
- this.imageUploader.showUploadMenu();
512
- };
513
-
514
- submit = async () => {
515
- this.setState({ submitting: true });
516
-
517
- let description = this.state.description;
518
-
519
- if (this.state.isHome) {
520
- description = description + `. -- Times Available: ${this.state.times}`;
521
- }
522
-
523
- setTimeout(() => {
524
- this.scrollContainer.scrollTo({ y: 0 });
525
- }, 100);
526
-
527
- const images = this.getImageUrls();
528
-
529
- // Fix custom images field answers
530
- const customFields = _.cloneDeep(this.state.customFields);
531
- const updatedCustomFields = customFields.map((field, fieldId) => {
532
- if (field.type === 'image') field.answer = this.getImageUrls(fieldId);
533
- if (field.type === 'document') field.answer = this.getDocumentAnswers(fieldId);
534
- return field;
535
- });
536
-
537
- maintenanceActions
538
- .sendMaintenanceRequest(
539
- this.props.uid,
540
- this.state.userName,
541
- this.state.phone,
542
- this.state.roomNumber,
543
- this.state.title,
544
- description,
545
- null,
546
- this.state.type,
547
- images,
548
- Helper.getSite(this.props.site),
549
- this.state.isHome,
550
- this.state.times,
551
- updatedCustomFields,
552
- )
553
- .then(res => {
554
- if (res.data.success) {
555
- this.refreshRequest(res.data.searchResult);
556
- if (this.props.onSubmissionSuccess) this.props.onSubmissionSuccess(res.data);
557
- this.setState({
558
- submitting: false,
559
- success: true,
560
- confirmationToShow: true,
561
- });
562
- } else {
563
- this.setState({
564
- submitting: false,
565
- });
566
- }
567
- })
568
- .catch(err => {
569
- console.log('maintenance submission fail.');
570
- console.log(err);
571
- this.setState({
572
- submitting: false,
573
- });
574
- });
575
- };
576
-
577
- refreshRequest = async id => {
578
- try {
579
- const job = await maintenanceActions.getJob(Helper.getSite(this.props.site), id);
580
- this.props.jobAdded(job.data);
581
- } catch (error) {
582
- console.log('refreshRequest error', error);
583
- }
584
- };
585
-
586
- validateCustomFields = () => {
587
- const { customFields } = this.state;
588
- if (!customFields || customFields.length === 0) return true;
589
-
590
- return customFields.every((field, index) => {
591
- const isValid = this.isFieldValid(field, index);
592
- return isValid;
593
- });
594
- };
595
-
596
- submitRequest() {
597
- const { customFields, submitting, uploadingImage, title, roomNumber, isHome, times } = this.state;
598
- const hasCustomFields = customFields && customFields.length > 0;
599
-
600
- if (submitting || !this.props.connected) {
601
- if (!this.props.connected) {
602
- this.setState({ error: { message: 'No internet connection detected' } });
603
- }
604
- return;
605
- }
606
- if (uploadingImage) return;
607
-
608
- this.setState({ error: null, showError: false });
609
-
610
- // PC-1255: Validate user selection for on-behalf requests
611
- if (this.state.canCreateOnBehalf && !this.state.selectedUser) {
612
- console.log('submitRequest - no user selected for on-behalf request');
613
- this.setState({ showError: true });
614
- return;
615
- }
616
-
617
- if (title.length === 0 || !roomNumber || roomNumber.length === 0) {
618
- console.log('submitRequest - error', { title, roomNumber });
619
- this.setState({ showError: true });
620
- return;
621
- }
622
- if (hasCustomFields) {
623
- if (!this.validateCustomFields()) {
624
- console.log('submitRequest - custom fields error');
625
- this.setState({ showError: true });
626
- return;
627
- }
628
- } else {
629
- if (isHome && times.length < 2) {
630
- console.log('submitRequest - error', { isHome, times });
631
- this.setState({ showError: true });
632
- return;
633
- }
634
- }
635
- this.submit();
636
- }
637
-
638
- getImages = (fieldId = null) => {
639
- const { images, customFieldImages } = this.state;
640
- const imagesList = _.cloneDeep(fieldId ? customFieldImages[fieldId] : images);
641
- if (!imagesList || !Array.isArray(imagesList) || imagesList.length === 0) {
642
- return [{ add: true }];
643
- }
644
- return imagesList;
645
- };
646
-
647
- setImages = (imagesList, fieldId = null, callback = null) => {
648
- let update = {};
649
- if (fieldId) {
650
- const customFieldImages = _.cloneDeep(this.state.customFieldImages);
651
- customFieldImages[fieldId] = imagesList;
652
- update = { customFieldImages };
653
- } else {
654
- update = { images: imagesList };
655
- }
656
- this.setState(update, callback);
657
- };
658
-
659
- getImageUrls = (fieldId = null) => {
660
- const imagesList = this.getImages(fieldId);
661
- return _.filter(imagesList, img => {
662
- return !img.uploading && !img.add;
663
- }).map(img => {
664
- return img.url;
665
- });
666
- };
667
-
668
- waitForThumbnails = () => {
669
- if (this.checkThumb) return;
670
-
671
- this.checkThumb = setInterval(async () => {
672
- const { imageFieldId } = this.state;
673
- const imagesList = this.getImages(imageFieldId);
674
- const imagesUpdate = [];
675
- await Promise.all(
676
- imagesList.map(image => {
677
- return new Promise(async resolve => {
678
- const newImage = { ...image };
679
- imagesUpdate.push(newImage);
680
- if (newImage.url && !newImage.thumbNailExists) {
681
- newImage.uploading = false;
682
- newImage.allowRetry = false;
683
- newImage.thumbNailExists = await Helper.imageExists(newImage.thumbNailUrl);
684
- resolve(newImage.thumbNailExists);
685
- }
686
- resolve(true);
687
- });
688
- }),
689
- );
690
- const thumbnailsExist = imagesUpdate.every(image => !image.url || image.thumbNailExists);
691
- if (thumbnailsExist) {
692
- clearInterval(this.checkThumb);
693
- this.checkThumb = null;
694
- this.setImages(imagesUpdate, imageFieldId);
695
- }
696
- }, 2000);
697
- };
698
-
699
- removeImage = (index, fieldId) => {
700
- const imagesUpdate = this.getImages(fieldId);
701
- imagesUpdate.splice(index, 1);
702
-
703
- this.setImages(imagesUpdate, fieldId);
704
- };
705
-
706
- getDocuments = fieldId => {
707
- const { customFieldDocuments } = this.state;
708
- const documentsList = _.cloneDeep(customFieldDocuments[fieldId]) || [];
709
- return documentsList;
710
- };
711
-
712
- setDocuments = (documentsList, fieldId) => {
713
- let update = {};
714
- const customFieldDocuments = _.cloneDeep(this.state.customFieldDocuments);
715
- customFieldDocuments[fieldId] = documentsList;
716
- update = { customFieldDocuments };
717
- this.setState(update);
718
- };
719
-
720
- getDocumentAnswers = fieldId => {
721
- const documentsList = this.getDocuments(fieldId);
722
- return _.filter(documentsList, doc => {
723
- return !doc.uploading && doc.url;
724
- }).map(doc => {
725
- return {
726
- name: doc.documentName,
727
- ext: doc.documentExt,
728
- url: doc.url,
729
- };
730
- });
731
- };
732
-
733
- removeDocument = (index, fieldId) => {
734
- const documentsUpdate = this.getDocuments(fieldId);
735
- documentsUpdate.splice(index, 1);
736
-
737
- this.setDocuments(documentsUpdate, fieldId);
738
- };
739
-
740
- toggleFullscreenVideo = url => {
741
- if (typeof url !== 'string') url = '';
742
- this.setState({ showFullscreenVideo: url.length > 0, currentVideoUrl: url });
743
- };
744
-
745
- renderImageUploader() {
746
- return (
747
- <Components.ImageUploader
748
- ref={ref => (this.imageUploader = ref)}
749
- onUploadStarted={this.onUploadStartedImage}
750
- onUploadSuccess={this.onUploadSuccessImage}
751
- onUploadFailed={this.onUploadFailedImage}
752
- onUploadProgress={this.onUploadProgressImage}
753
- onLibrarySelected={this.onLibrarySelectedImage}
754
- size={{ width: 1400 }}
755
- quality={0.8}
756
- fileName={'serviceImage'}
757
- popupTitle={'Upload Image'}
758
- userId={this.props.uid}
759
- allowsEditing={false}
760
- multiple
761
- hideLibrary
762
- />
763
- );
764
- }
765
-
766
- renderImage(item, index, fieldId = null) {
767
- const isVideoUrl = Helper.isVideo(item.url);
768
- const imagesList = this.getImages(fieldId);
769
-
770
- if (item.add) {
771
- return (
772
- <TouchableOpacity activeOpacity={0.8} onPress={() => this.showUploadMenu(fieldId)}>
773
- <View
774
- style={[
775
- styles.imageContainer,
776
- imagesList.length > 1 && styles.imageContainerNotEmpty,
777
- index % 3 === 0 && { marginLeft: 0 },
778
- index > 2 && { marginTop: 8 },
779
- ]}
780
- >
781
- <View style={styles.imageCircle}>
782
- <Icon name="camera" type="font-awesome" iconStyle={styles.addImageIcon} />
783
- </View>
784
- </View>
785
- </TouchableOpacity>
786
- );
787
- }
788
- return (
789
- <View
790
- style={[
791
- styles.imageContainer,
792
- imagesList.length > 1 && styles.imageContainerNotEmpty,
793
- index % 3 === 0 && { marginLeft: 0 },
794
- index > 2 && { marginTop: 8 },
795
- ]}
796
- >
797
- {item.uploading ? (
798
- <Components.ImageUploadProgress uploader={this.imageUploader} image={item} color={this.props.colourBrandingMain} />
799
- ) : (
800
- <ImageBackground
801
- style={styles.imageBackground}
802
- source={Helper.getImageSource(item.thumbNailExists ? item.thumbNailUrl : item.url)}
803
- >
804
- {isVideoUrl && (
805
- <View style={styles.imagePlayContainer}>
806
- <TouchableOpacity onPress={this.toggleFullscreenVideo.bind(this, item.url)}>
807
- <Icon name="play" type="font-awesome" iconStyle={styles.imageControlIcon} />
808
- </TouchableOpacity>
809
- </View>
810
- )}
811
- <TouchableOpacity style={styles.removeImage} onPress={() => this.removeImage(index, fieldId)}>
812
- <Icon name="remove" type="font-awesome" iconStyle={styles.imageControlIcon} style={styles.removeImage} />
813
- </TouchableOpacity>
814
- </ImageBackground>
815
- )}
816
- </View>
817
- );
818
- }
819
-
820
- renderDocument(item, index, fieldId) {
821
- const { colourBrandingMain } = this.props;
822
- return (
823
- <View key={index} style={styles.documentContainer}>
824
- <View style={{ ...styles.documentTypeContainer, backgroundColor: colourBrandingMain }}>
825
- <Text style={styles.documentTypeText}>{item.documentExt}</Text>
826
- </View>
827
- <Text style={styles.documentText}>{`${item.documentName}${item.uploading ? ` - ${item.uploadProgress}` : ''}`}</Text>
828
- {!item.uploading && (
829
- <TouchableOpacity style={styles.removeDocumentButton} onPress={() => this.removeDocument(index, fieldId)}>
830
- <Icon name="remove" type="font-awesome" iconStyle={{ ...styles.removeDocumentIcon, color: colourBrandingMain }} />
831
- </TouchableOpacity>
832
- )}
833
- </View>
834
- );
835
- }
836
-
837
- renderSuccess() {
838
- return (
839
- <View style={{ padding: 16, flex: 1, backgroundColor: '#fff' }}>
840
- <Text style={styles.requestSuccess}>Your request has been submitted. Thank you.</Text>
841
- </View>
842
- );
843
- }
844
-
845
- renderImageList(fieldId = null) {
846
- const imagesList = this.getImages(fieldId);
847
- return (
848
- <View style={[styles.imageListContainer, imagesList.length < 2 && styles.imageListContainerEmpty]}>
849
- <FlatList
850
- keyboardShouldPersistTaps="always"
851
- enableEmptySections
852
- data={imagesList}
853
- renderItem={({ item, index }) => this.renderImage(item, index, fieldId)}
854
- keyExtractor={(item, index) => index}
855
- numColumns={3}
856
- />
857
- </View>
858
- );
859
- }
860
-
861
- renderDocumentList(fieldId) {
862
- const documentsList = this.getDocuments(fieldId);
863
- return (
864
- <View style={styles.documentListContainer}>
865
- {documentsList.length > 0 ? documentsList.map((document, index) => this.renderDocument(document, index, fieldId)) : null}
866
- <Components.DocumentUploader
867
- buttonTitle="Add Files"
868
- allowedTypes={['application/pdf']}
869
- onUploadStarted={(uploadUri, uri, name, ext) => this.onUploadStartedDocument(uploadUri, uri, name, ext, fieldId)}
870
- onUploadProgress={progress => this.onUploadProgressDocument(progress, fieldId)}
871
- onUploadSuccess={(uri, uploadUri) => this.onUploadSuccessDocument(uri, uploadUri, fieldId)}
872
- onUploadFailed={uploadUri => this.onUploadFailedDocument(uploadUri, fieldId)}
873
- fileName="serviceDocument"
874
- userId={this.props.uid}
875
- disabled={false}
876
- multiple
877
- />
878
- </View>
879
- );
880
- }
881
-
882
- renderDateField(field, fieldId, sectionStyle) {
883
- let displayText, placeHolder, icon, errorText;
884
- if (field.type === 'date') {
885
- displayText = field.answer ? moment(field.answer, 'YYYY-MM-DD').format('DD MMM YYYY') : '';
886
- placeHolder = 'dd mmm yyyy';
887
- icon = 'calendar';
888
- errorText = 'Not a valid date';
889
- } else {
890
- displayText = field.answer ? moment(field.answer, 'HH:mm').format('h:mm a') : '';
891
- placeHolder = '--:-- --';
892
- icon = 'clock-o';
893
- errorText = 'Not a valid time';
894
- }
895
-
896
- return (
897
- <Components.GenericInputSection
898
- key={fieldId}
899
- label={field.label}
900
- sectionStyle={sectionStyle}
901
- isValid={() => this.isFieldValid(field, fieldId)}
902
- required={field.mandatory}
903
- showError={this.state.showError}
904
- errorText={errorText}
905
- >
906
- <View style={styles.dateContainer}>
907
- <TouchableOpacity style={styles.dateFieldButton} onPress={() => this.onOpenDatePicker(field, fieldId)}>
908
- <View style={styles.dateFieldContainer}>
909
- <Text style={styles.dateText}>{displayText || placeHolder}</Text>
910
- <Icon type="font-awesome" name={icon} iconStyle={styles.dateIcon} />
911
- </View>
912
- </TouchableOpacity>
913
- {displayText ? (
914
- <TouchableOpacity style={styles.dateClearButton} onPress={() => this.onClearDate(fieldId)}>
915
- <Icon type="font-awesome" name="times" iconStyle={styles.removeIcon} />
916
- </TouchableOpacity>
917
- ) : null}
918
- </View>
919
- </Components.GenericInputSection>
920
- );
921
- }
922
-
923
- renderField(field, fieldId) {
924
- const sectionStyle = { marginTop: fieldId === 0 ? 24 : 0, marginBottom: 24 };
925
- switch (field.type) {
926
- case 'yn':
927
- return (
928
- <Components.GenericInputSection
929
- key={fieldId}
930
- label={field.label}
931
- sectionStyle={sectionStyle}
932
- inputType="toggle"
933
- value={field.answer}
934
- onChange={answer => this.onChangeToggleAnswer(fieldId, answer)}
935
- isValid={() => this.isFieldValid(field, fieldId)}
936
- showError={this.state.showError}
937
- required={field.mandatory}
938
- />
939
- );
940
- case 'multichoice':
941
- return (
942
- <Components.GenericInputSection
943
- key={fieldId}
944
- label={field.label}
945
- sectionStyle={sectionStyle}
946
- inputType="radio"
947
- value={field.answer}
948
- onChange={answer => this.onChangeToggleAnswer(fieldId, answer)}
949
- options={field.values.map(o => {
950
- return {
951
- Label: o,
952
- Value: o,
953
- };
954
- })}
955
- isValid={() => this.isFieldValid(field, fieldId)}
956
- showError={this.state.showError}
957
- required={field.mandatory}
958
- />
959
- );
960
- case 'checkbox':
961
- return (
962
- <Components.GenericInputSection
963
- key={fieldId}
964
- label={field.label}
965
- sectionStyle={sectionStyle}
966
- isValid={() => this.isFieldValid(field, fieldId)}
967
- showError={this.state.showError}
968
- required={field.mandatory}
969
- >
970
- {field.values.map((o, i) => {
971
- const isActive = field.answer && _.includes(field.answer, o);
972
- return (
973
- <TouchableOpacity
974
- onPress={() => this.onChangeCheckboxAnswer(fieldId, o)}
975
- key={i}
976
- style={styles.multiChoiceOption}
977
- hitSlop={{ top: 8, left: 8, bottom: 8, right: 8 }}
978
- >
979
- {isActive ? (
980
- <Components.TickIcon style={styles.tick} size={20} color={this.props.colourBrandingMain} />
981
- ) : (
982
- <View style={styles.unticked} />
983
- )}
984
- <Text style={styles.multiChoiceText}>{o}</Text>
985
- </TouchableOpacity>
986
- );
987
- })}
988
- </Components.GenericInputSection>
989
- );
990
- case 'text':
991
- case 'email':
992
- case 'phone':
993
- return (
994
- <Components.GenericInputSection
995
- key={fieldId}
996
- label={field.label}
997
- placeholder={field.placeHolder}
998
- value={field.answer}
999
- onChangeText={val => this.onChangeAnswer(fieldId, val)}
1000
- editable
1001
- squaredCorners
1002
- isValid={() => this.isFieldValid(field, fieldId)}
1003
- showError={this.state.showError}
1004
- errorText={field.type === 'email' ? 'Not a valid email' : undefined}
1005
- required={field.mandatory}
1006
- sectionStyle={sectionStyle}
1007
- autoCapitalize="sentences"
1008
- keyboardType={this.keyboardTypes[field.type]}
1009
- />
1010
- );
1011
- case 'staticTitle':
1012
- return (
1013
- <Text key={fieldId} style={[styles.staticTitle, { color: this.props.colourBrandingMain }, sectionStyle]}>
1014
- {field.label}
1015
- </Text>
1016
- );
1017
- case 'staticText':
1018
- return (
1019
- <View key={fieldId} style={[styles.staticText, sectionStyle]}>
1020
- {Helper.toParagraphed(field.label, styles.staticText)}
1021
- </View>
1022
- );
1023
- case 'date':
1024
- case 'time':
1025
- return this.renderDateField(field, fieldId, sectionStyle);
1026
- case 'image':
1027
- return (
1028
- <Components.GenericInputSection
1029
- key={fieldId}
1030
- label={field.label}
1031
- sectionStyle={sectionStyle}
1032
- isValid={() => this.isFieldValid(field, fieldId)}
1033
- required={field.mandatory}
1034
- showError={this.state.showError}
1035
- >
1036
- {this.renderImageList(fieldId)}
1037
- </Components.GenericInputSection>
1038
- );
1039
- case 'document':
1040
- return (
1041
- <Components.GenericInputSection
1042
- key={fieldId}
1043
- label={field.label}
1044
- sectionStyle={sectionStyle}
1045
- isValid={() => this.isFieldValid(field, fieldId)}
1046
- required={field.mandatory}
1047
- showError={this.state.showError}
1048
- >
1049
- {this.renderDocumentList(fieldId)}
1050
- </Components.GenericInputSection>
1051
- );
1052
- default:
1053
- return null;
1054
- }
1055
- }
1056
-
1057
- renderCustomFields() {
1058
- const { customFields } = this.state;
1059
- if (!customFields || customFields.length === 0) return null;
1060
-
1061
- return (
1062
- <Components.FormCard style={{ marginTop: 16 }}>{customFields.map((field, i) => this.renderField(field, i))}</Components.FormCard>
1063
- );
1064
- }
1065
-
1066
- renderForm() {
1067
- const { customFields, loadingTypes } = this.state;
1068
- const hasCustomFields = customFields && customFields.length > 0;
1069
-
1070
- if (loadingTypes) {
1071
- return <Components.Spinner />;
1072
- }
1073
-
1074
- return (
1075
- <View style={{ flex: 1 }}>
1076
- <ScrollView keyboardShouldPersistTaps="always" style={{ flex: 1 }} ref={ref => (this.scrollContainer = ref)}>
1077
- <View style={{ paddingBottom: 2 }}>
1078
- <Components.LoadingIndicator visible={this.state.submitting} />
1079
- {this.state.error && <Text style={styles.errorText}>{this.state.error}</Text>}
1080
- <Components.FormCard style={{ marginTop: 16 }}>
1081
- {/* PC-1255: User picker for on-behalf requests */}
1082
- {this.state.canCreateOnBehalf && (
1083
- <Components.FormCardSection
1084
- label={'Select User'}
1085
- textValue={this.state.selectedUser?.displayName}
1086
- hasContent
1087
- hasUnderline
1088
- required
1089
- errorText="Please select a user."
1090
- showError={this.state.showError && !this.state.selectedUser}
1091
- sectionStyle={{ borderBottomWidth: 1, borderBottomColor: Colours.LINEGREY }}
1092
- >
1093
- <TouchableOpacity onPress={this.onPressUser}>
1094
- <View style={styles.userPickerContainer}>
1095
- <View style={styles.profileContainer}>
1096
- <Components.ProfilePic ProfilePic={this.state.selectedUser?.profilePic} Diameter={30} />
1097
- <Text style={styles.nameText}>{this.state.selectedUser?.displayName || 'Select User'}</Text>
1098
- </View>
1099
- <Icon
1100
- name="angle-right"
1101
- type="font-awesome"
1102
- iconStyle={[styles.sectionTitle, { fontSize: 20, color: this.props.colourBrandingMain }]}
1103
- />
1104
- </View>
1105
- </TouchableOpacity>
1106
- </Components.FormCardSection>
1107
- )}
1108
- {/* PC-1255: Hide Name field when creating on behalf - name comes from selected user */}
1109
- {!this.state.canCreateOnBehalf && (
1110
- <Components.FormCardSection
1111
- label={'Name'}
1112
- placeholder={'Enter your name'}
1113
- textValue={this.state.userName}
1114
- onChangeText={userName => this.onChangeName(userName)}
1115
- editable={this.props.userType === 'KIOSK' && this.state.submitting === false}
1116
- isValid={() => {
1117
- return this.state.userName.length > 1;
1118
- }}
1119
- required
1120
- errorText="Please enter your name."
1121
- showError={this.state.showError && this.state.userName.length < 2}
1122
- hasUnderline
1123
- />
1124
- )}
1125
- <Components.FormCardSection
1126
- label={'Contact number'}
1127
- placeholder={'Enter phone number'}
1128
- textValue={this.state.phone}
1129
- onChangeText={phone => this.setState({ phone })}
1130
- editable={this.state.submitting === false}
1131
- hasUnderline
1132
- keyboardType={'phone-pad'}
1133
- />
1134
- <Components.FormCardSection
1135
- label={'Address'}
1136
- placeholder={'Enter your address'}
1137
- textValue={this.state.roomNumber}
1138
- onChangeText={roomNumber => this.setState({ roomNumber })}
1139
- editable={this.state.submitting === false}
1140
- hasUnderline
1141
- isValid={() => {
1142
- return this.state.roomNumber && this.state.roomNumber.length > 1;
1143
- }}
1144
- required
1145
- errorText="Please provide your address."
1146
- showError={this.state.showError && (!this.state.roomNumber || this.state.roomNumber.length < 2)}
1147
- />
1148
- </Components.FormCard>
1149
- <Components.FormCard
1150
- style={{ marginTop: 16, paddingHorizontal: 24, paddingVertical: 16, flexDirection: 'row', justifyContent: 'space-between' }}
1151
- >
1152
- <Text style={styles.sectionTitle}>{values.textJobType}</Text>
1153
- <TouchableOpacity onPress={this.onPressType.bind(this)}>
1154
- <View style={{ flexDirection: 'row' }}>
1155
- <Text style={[styles.sectionTitle, { color: this.props.colourBrandingMain, marginRight: 16 }]}>{this.state.type}</Text>
1156
- <Icon
1157
- name="angle-right"
1158
- type="font-awesome"
1159
- iconStyle={[styles.sectionTitle, { fontSize: 20, color: this.props.colourBrandingMain }]}
1160
- />
1161
- </View>
1162
- </TouchableOpacity>
1163
- </Components.FormCard>
1164
- {hasCustomFields ? (
1165
- this.renderCustomFields()
1166
- ) : (
1167
- <>
1168
- <Components.FormCard style={{ marginTop: 16 }}>
1169
- <Components.FormCardSection
1170
- label={'Title'}
1171
- placeholder={'Enter a title for your request'}
1172
- textValue={this.state.title}
1173
- onChangeText={title => this.setState({ title })}
1174
- editable={this.state.submitting === false}
1175
- hasUnderline
1176
- isValid={() => {
1177
- return this.state.title.length > 1;
1178
- }}
1179
- required
1180
- errorText="Please provide a title."
1181
- showError={this.state.showError && this.state.title.length < 2}
1182
- autoCorrect
1183
- multiline
1184
- autoGrow
1185
- />
1186
- <Components.FormCardSection
1187
- label={'Description'}
1188
- placeholder={'Describe your request here in detail'}
1189
- textValue={this.state.description}
1190
- onChangeText={description => this.setState({ description })}
1191
- editable={this.state.submitting === false}
1192
- hasUnderline
1193
- autoCorrect
1194
- multiline
1195
- autoGrow
1196
- />
1197
- </Components.FormCard>
1198
- <Components.FormCard style={{ marginTop: 16, paddingHorizontal: 24 }}>
1199
- <View
1200
- style={[
1201
- {
1202
- width: '100%',
1203
- paddingVertical: 16,
1204
- flexDirection: 'row',
1205
- justifyContent: 'space-between',
1206
- alignItems: 'center',
1207
- position: 'relative',
1208
- },
1209
- this.state.isHome && { borderBottomWidth: 1, borderBottomColor: Colours.LINEGREY },
1210
- ]}
1211
- >
1212
- <Text style={styles.sectionTitle}>{Config.env.strings.MAINTENANCE_HOME}</Text>
1213
- <Switch
1214
- value={this.state.isHome}
1215
- disabled={this.state.submitting}
1216
- onValueChange={value => this.setState({ isHome: value })}
1217
- trackColor={{ false: '#ddd', true: this.props.colourBrandingMain }}
1218
- thumbColor={Platform.OS === 'android' ? '#fff' : null}
1219
- />
1220
- </View>
1221
- {this.state.isHome && (
1222
- <Components.FormCardSection
1223
- label={'Available times'}
1224
- placeholder={'Describe your available times here in detail.'}
1225
- textValue={this.state.times}
1226
- onChangeText={times => this.setState({ times })}
1227
- editable={this.state.submitting === false}
1228
- hasUnderline
1229
- isValid={() => {
1230
- return this.state.times.length > 1;
1231
- }}
1232
- required
1233
- errorText="Please provide available times."
1234
- showError={this.state.showError && this.state.isHome && this.state.times.length < 2}
1235
- minHeight={40}
1236
- autoCorrect
1237
- multiline
1238
- autoGrow
1239
- />
1240
- )}
1241
- </Components.FormCard>
1242
- {this.renderImageList()}
1243
- </>
1244
- )}
1245
- </View>
1246
- </ScrollView>
1247
- </View>
1248
- );
1249
- }
1250
-
1251
- renderRegisterConfirmation() {
1252
- return (
1253
- <Components.ConfirmationPopup
1254
- confirmText={`${values.textEntityName} submitted`}
1255
- repeatText={'Submit another'}
1256
- visible={this.state.confirmationToShow}
1257
- onClose={this.onCloseConfirmationPopup.bind(this)}
1258
- onPressAction={this.onConfirmationReset.bind(this)}
1259
- />
1260
- );
1261
- }
1262
-
1263
- renderVideoPlayerPopup() {
1264
- const { showFullscreenVideo, currentVideoUrl } = this.state;
1265
- if (!currentVideoUrl) return;
1266
-
1267
- return (
1268
- <Components.VideoPopup
1269
- uri={currentVideoUrl}
1270
- visible={showFullscreenVideo}
1271
- showFullscreenButton={false}
1272
- onClose={this.toggleFullscreenVideo}
1273
- />
1274
- );
1275
- }
1276
-
1277
- renderNoType() {
1278
- if (!this.state.noType) {
1279
- return null;
1280
- }
1281
- return (
1282
- <Components.WarningPopup
1283
- confirmText={'No forms are available'}
1284
- infoText={'Check back later for forms.'}
1285
- visible={this.state.noType}
1286
- onClose={this.onPressBack.bind(this)}
1287
- padHorizontal
1288
- />
1289
- );
1290
- }
1291
-
1292
- render() {
1293
- const { submitting, success, isDateTimePickerVisible, popUpType } = this.state;
1294
-
1295
- return (
1296
- <KeyboardAvoidingView behavior={Platform.OS === 'ios' && 'padding'} style={styles.viewContainer}>
1297
- {this.renderImageUploader()}
1298
- <View style={styles.container}>
1299
- <Components.Header
1300
- leftIcon="angle-left"
1301
- onPressLeft={this.onPressBack.bind(this)}
1302
- text={this.props.strings[`${values.featureKey}_textFeatureTitle`] || values.textFeatureTitle}
1303
- rightText={submitting || success ? null : 'Done'}
1304
- onPressRight={this.submitRequest.bind(this)}
1305
- absoluteRight
1306
- />
1307
- {this.renderForm()}
1308
- </View>
1309
- {this.renderRegisterConfirmation()}
1310
- {this.renderVideoPlayerPopup()}
1311
- {this.renderNoType()}
1312
- <DateTimePicker
1313
- isVisible={isDateTimePickerVisible}
1314
- onConfirm={this.onDateSelected}
1315
- onCancel={() => this.setState({ isDateTimePickerVisible: false })}
1316
- mode={popUpType}
1317
- headerTextIOS={`Pick a ${popUpType}`}
1318
- />
1319
- </KeyboardAvoidingView>
1320
- );
1321
- }
818
+ renderImage(item, index, fieldId = null) {
819
+ const isVideoUrl = Helper.isVideo(item.url);
820
+ const imagesList = this.getImages(fieldId);
821
+
822
+ if (item.add) {
823
+ return (
824
+ <TouchableOpacity
825
+ activeOpacity={0.8}
826
+ onPress={() => this.showUploadMenu(fieldId)}
827
+ >
828
+ <View
829
+ style={[
830
+ styles.imageContainer,
831
+ imagesList.length > 1 && styles.imageContainerNotEmpty,
832
+ index % 3 === 0 && { marginLeft: 0 },
833
+ index > 2 && { marginTop: 8 },
834
+ ]}
835
+ >
836
+ <View style={styles.imageCircle}>
837
+ <Icon
838
+ name="camera"
839
+ type="font-awesome"
840
+ iconStyle={styles.addImageIcon}
841
+ />
842
+ </View>
843
+ </View>
844
+ </TouchableOpacity>
845
+ );
846
+ }
847
+ return (
848
+ <View
849
+ style={[
850
+ styles.imageContainer,
851
+ imagesList.length > 1 && styles.imageContainerNotEmpty,
852
+ index % 3 === 0 && { marginLeft: 0 },
853
+ index > 2 && { marginTop: 8 },
854
+ ]}
855
+ >
856
+ {item.uploading ? (
857
+ <Components.ImageUploadProgress
858
+ uploader={this.imageUploader}
859
+ image={item}
860
+ color={this.props.colourBrandingMain}
861
+ />
862
+ ) : (
863
+ <ImageBackground
864
+ style={styles.imageBackground}
865
+ source={Helper.getImageSource(
866
+ item.thumbNailExists ? item.thumbNailUrl : item.url,
867
+ )}
868
+ >
869
+ {isVideoUrl && (
870
+ <View style={styles.imagePlayContainer}>
871
+ <TouchableOpacity
872
+ onPress={this.toggleFullscreenVideo.bind(this, item.url)}
873
+ >
874
+ <Icon
875
+ name="play"
876
+ type="font-awesome"
877
+ iconStyle={styles.imageControlIcon}
878
+ />
879
+ </TouchableOpacity>
880
+ </View>
881
+ )}
882
+ <TouchableOpacity
883
+ style={styles.removeImage}
884
+ onPress={() => this.removeImage(index, fieldId)}
885
+ >
886
+ <Icon
887
+ name="remove"
888
+ type="font-awesome"
889
+ iconStyle={styles.imageControlIcon}
890
+ style={styles.removeImage}
891
+ />
892
+ </TouchableOpacity>
893
+ </ImageBackground>
894
+ )}
895
+ </View>
896
+ );
897
+ }
898
+
899
+ renderDocument(item, index, fieldId) {
900
+ const { colourBrandingMain } = this.props;
901
+ return (
902
+ <View key={index} style={styles.documentContainer}>
903
+ <View
904
+ style={{
905
+ ...styles.documentTypeContainer,
906
+ backgroundColor: colourBrandingMain,
907
+ }}
908
+ >
909
+ <Text style={styles.documentTypeText}>{item.documentExt}</Text>
910
+ </View>
911
+ <Text
912
+ style={styles.documentText}
913
+ >{`${item.documentName}${item.uploading ? ` - ${item.uploadProgress}` : ""}`}</Text>
914
+ {!item.uploading && (
915
+ <TouchableOpacity
916
+ style={styles.removeDocumentButton}
917
+ onPress={() => this.removeDocument(index, fieldId)}
918
+ >
919
+ <Icon
920
+ name="remove"
921
+ type="font-awesome"
922
+ iconStyle={{
923
+ ...styles.removeDocumentIcon,
924
+ color: colourBrandingMain,
925
+ }}
926
+ />
927
+ </TouchableOpacity>
928
+ )}
929
+ </View>
930
+ );
931
+ }
932
+
933
+ renderSuccess() {
934
+ return (
935
+ <View style={{ padding: 16, flex: 1, backgroundColor: "#fff" }}>
936
+ <Text style={styles.requestSuccess}>
937
+ Your request has been submitted. Thank you.
938
+ </Text>
939
+ </View>
940
+ );
941
+ }
942
+
943
+ renderImageList(fieldId = null) {
944
+ const imagesList = this.getImages(fieldId);
945
+ return (
946
+ <View
947
+ style={[
948
+ styles.imageListContainer,
949
+ imagesList.length < 2 && styles.imageListContainerEmpty,
950
+ ]}
951
+ >
952
+ <FlatList
953
+ keyboardShouldPersistTaps="always"
954
+ enableEmptySections
955
+ data={imagesList}
956
+ renderItem={({ item, index }) =>
957
+ this.renderImage(item, index, fieldId)
958
+ }
959
+ keyExtractor={(item, index) => index}
960
+ numColumns={3}
961
+ />
962
+ </View>
963
+ );
964
+ }
965
+
966
+ renderDocumentList(fieldId) {
967
+ const documentsList = this.getDocuments(fieldId);
968
+ return (
969
+ <View style={styles.documentListContainer}>
970
+ {documentsList.length > 0
971
+ ? documentsList.map((document, index) =>
972
+ this.renderDocument(document, index, fieldId),
973
+ )
974
+ : null}
975
+ <Components.DocumentUploader
976
+ buttonTitle="Add Files"
977
+ allowedTypes={["application/pdf"]}
978
+ onUploadStarted={(uploadUri, uri, name, ext) =>
979
+ this.onUploadStartedDocument(uploadUri, uri, name, ext, fieldId)
980
+ }
981
+ onUploadProgress={(progress) =>
982
+ this.onUploadProgressDocument(progress, fieldId)
983
+ }
984
+ onUploadSuccess={(uri, uploadUri) =>
985
+ this.onUploadSuccessDocument(uri, uploadUri, fieldId)
986
+ }
987
+ onUploadFailed={(uploadUri) =>
988
+ this.onUploadFailedDocument(uploadUri, fieldId)
989
+ }
990
+ fileName="serviceDocument"
991
+ userId={this.props.uid}
992
+ disabled={false}
993
+ multiple
994
+ />
995
+ </View>
996
+ );
997
+ }
998
+
999
+ renderDateField(field, fieldId, sectionStyle) {
1000
+ let displayText, placeHolder, icon, errorText;
1001
+ if (field.type === "date") {
1002
+ displayText = field.answer
1003
+ ? moment(field.answer, "YYYY-MM-DD").format("DD MMM YYYY")
1004
+ : "";
1005
+ placeHolder = "dd mmm yyyy";
1006
+ icon = "calendar";
1007
+ errorText = "Not a valid date";
1008
+ } else {
1009
+ displayText = field.answer
1010
+ ? moment(field.answer, "HH:mm").format("h:mm a")
1011
+ : "";
1012
+ placeHolder = "--:-- --";
1013
+ icon = "clock-o";
1014
+ errorText = "Not a valid time";
1015
+ }
1016
+
1017
+ return (
1018
+ <Components.GenericInputSection
1019
+ key={fieldId}
1020
+ label={field.label}
1021
+ sectionStyle={sectionStyle}
1022
+ isValid={() => this.isFieldValid(field, fieldId)}
1023
+ required={field.mandatory}
1024
+ showError={this.state.showError}
1025
+ errorText={errorText}
1026
+ >
1027
+ <View style={styles.dateContainer}>
1028
+ <TouchableOpacity
1029
+ style={styles.dateFieldButton}
1030
+ onPress={() => this.onOpenDatePicker(field, fieldId)}
1031
+ >
1032
+ <View style={styles.dateFieldContainer}>
1033
+ <Text style={styles.dateText}>{displayText || placeHolder}</Text>
1034
+ <Icon
1035
+ type="font-awesome"
1036
+ name={icon}
1037
+ iconStyle={styles.dateIcon}
1038
+ />
1039
+ </View>
1040
+ </TouchableOpacity>
1041
+ {displayText ? (
1042
+ <TouchableOpacity
1043
+ style={styles.dateClearButton}
1044
+ onPress={() => this.onClearDate(fieldId)}
1045
+ >
1046
+ <Icon
1047
+ type="font-awesome"
1048
+ name="times"
1049
+ iconStyle={styles.removeIcon}
1050
+ />
1051
+ </TouchableOpacity>
1052
+ ) : null}
1053
+ </View>
1054
+ </Components.GenericInputSection>
1055
+ );
1056
+ }
1057
+
1058
+ renderField(field, fieldId) {
1059
+ const sectionStyle = {
1060
+ marginTop: fieldId === 0 ? 24 : 0,
1061
+ marginBottom: 24,
1062
+ };
1063
+ switch (field.type) {
1064
+ case "yn":
1065
+ return (
1066
+ <Components.GenericInputSection
1067
+ key={fieldId}
1068
+ label={field.label}
1069
+ sectionStyle={sectionStyle}
1070
+ inputType="toggle"
1071
+ value={field.answer}
1072
+ onChange={(answer) => this.onChangeToggleAnswer(fieldId, answer)}
1073
+ isValid={() => this.isFieldValid(field, fieldId)}
1074
+ showError={this.state.showError}
1075
+ required={field.mandatory}
1076
+ />
1077
+ );
1078
+ case "multichoice":
1079
+ return (
1080
+ <Components.GenericInputSection
1081
+ key={fieldId}
1082
+ label={field.label}
1083
+ sectionStyle={sectionStyle}
1084
+ inputType="radio"
1085
+ value={field.answer}
1086
+ onChange={(answer) => this.onChangeToggleAnswer(fieldId, answer)}
1087
+ options={field.values.map((o) => {
1088
+ return {
1089
+ Label: o,
1090
+ Value: o,
1091
+ };
1092
+ })}
1093
+ isValid={() => this.isFieldValid(field, fieldId)}
1094
+ showError={this.state.showError}
1095
+ required={field.mandatory}
1096
+ />
1097
+ );
1098
+ case "checkbox":
1099
+ return (
1100
+ <Components.GenericInputSection
1101
+ key={fieldId}
1102
+ label={field.label}
1103
+ sectionStyle={sectionStyle}
1104
+ isValid={() => this.isFieldValid(field, fieldId)}
1105
+ showError={this.state.showError}
1106
+ required={field.mandatory}
1107
+ >
1108
+ {field.values.map((o, i) => {
1109
+ const isActive = field.answer && _.includes(field.answer, o);
1110
+ return (
1111
+ <TouchableOpacity
1112
+ onPress={() => this.onChangeCheckboxAnswer(fieldId, o)}
1113
+ key={i}
1114
+ style={styles.multiChoiceOption}
1115
+ hitSlop={{ top: 8, left: 8, bottom: 8, right: 8 }}
1116
+ >
1117
+ {isActive ? (
1118
+ <Components.TickIcon
1119
+ style={styles.tick}
1120
+ size={20}
1121
+ color={this.props.colourBrandingMain}
1122
+ />
1123
+ ) : (
1124
+ <View style={styles.unticked} />
1125
+ )}
1126
+ <Text style={styles.multiChoiceText}>{o}</Text>
1127
+ </TouchableOpacity>
1128
+ );
1129
+ })}
1130
+ </Components.GenericInputSection>
1131
+ );
1132
+ case "text":
1133
+ case "email":
1134
+ case "phone":
1135
+ return (
1136
+ <Components.GenericInputSection
1137
+ key={fieldId}
1138
+ label={field.label}
1139
+ placeholder={field.placeHolder}
1140
+ value={field.answer}
1141
+ onChangeText={(val) => this.onChangeAnswer(fieldId, val)}
1142
+ editable
1143
+ squaredCorners
1144
+ isValid={() => this.isFieldValid(field, fieldId)}
1145
+ showError={this.state.showError}
1146
+ errorText={field.type === "email" ? "Not a valid email" : undefined}
1147
+ required={field.mandatory}
1148
+ sectionStyle={sectionStyle}
1149
+ autoCapitalize="sentences"
1150
+ keyboardType={this.keyboardTypes[field.type]}
1151
+ />
1152
+ );
1153
+ case "staticTitle":
1154
+ return (
1155
+ <Text
1156
+ key={fieldId}
1157
+ style={[
1158
+ styles.staticTitle,
1159
+ { color: this.props.colourBrandingMain },
1160
+ sectionStyle,
1161
+ ]}
1162
+ >
1163
+ {field.label}
1164
+ </Text>
1165
+ );
1166
+ case "staticText":
1167
+ return (
1168
+ <View key={fieldId} style={[styles.staticText, sectionStyle]}>
1169
+ {Helper.toParagraphed(field.label, styles.staticText)}
1170
+ </View>
1171
+ );
1172
+ case "date":
1173
+ case "time":
1174
+ return this.renderDateField(field, fieldId, sectionStyle);
1175
+ case "image":
1176
+ return (
1177
+ <Components.GenericInputSection
1178
+ key={fieldId}
1179
+ label={field.label}
1180
+ sectionStyle={sectionStyle}
1181
+ isValid={() => this.isFieldValid(field, fieldId)}
1182
+ required={field.mandatory}
1183
+ showError={this.state.showError}
1184
+ >
1185
+ {this.renderImageList(fieldId)}
1186
+ </Components.GenericInputSection>
1187
+ );
1188
+ case "document":
1189
+ return (
1190
+ <Components.GenericInputSection
1191
+ key={fieldId}
1192
+ label={field.label}
1193
+ sectionStyle={sectionStyle}
1194
+ isValid={() => this.isFieldValid(field, fieldId)}
1195
+ required={field.mandatory}
1196
+ showError={this.state.showError}
1197
+ >
1198
+ {this.renderDocumentList(fieldId)}
1199
+ </Components.GenericInputSection>
1200
+ );
1201
+ default:
1202
+ return null;
1203
+ }
1204
+ }
1205
+
1206
+ renderCustomFields() {
1207
+ const { customFields } = this.state;
1208
+ if (!customFields || customFields.length === 0) return null;
1209
+
1210
+ return (
1211
+ <Components.FormCard style={{ marginTop: 16 }}>
1212
+ {customFields.map((field, i) => this.renderField(field, i))}
1213
+ </Components.FormCard>
1214
+ );
1215
+ }
1216
+
1217
+ renderForm() {
1218
+ const { customFields, loadingTypes } = this.state;
1219
+ const hasCustomFields = customFields && customFields.length > 0;
1220
+
1221
+ if (loadingTypes) {
1222
+ return <Components.Spinner />;
1223
+ }
1224
+
1225
+ return (
1226
+ <View style={{ flex: 1 }}>
1227
+ <ScrollView
1228
+ keyboardShouldPersistTaps="always"
1229
+ style={{ flex: 1 }}
1230
+ ref={(ref) => (this.scrollContainer = ref)}
1231
+ >
1232
+ <View style={{ paddingBottom: 2 }}>
1233
+ <Components.LoadingIndicator visible={this.state.submitting} />
1234
+ {this.state.error && (
1235
+ <Text style={styles.errorText}>{this.state.error}</Text>
1236
+ )}
1237
+ <Components.FormCard style={{ marginTop: 16 }}>
1238
+ {/* PC-1255: User picker for on-behalf requests */}
1239
+ {this.state.canCreateOnBehalf && (
1240
+ <Components.FormCardSection
1241
+ label={"Select User"}
1242
+ textValue={this.state.selectedUser?.displayName}
1243
+ hasContent
1244
+ hasUnderline
1245
+ required
1246
+ errorText="Please select a user."
1247
+ showError={this.state.showError && !this.state.selectedUser}
1248
+ sectionStyle={{
1249
+ borderBottomWidth: 1,
1250
+ borderBottomColor: Colours.LINEGREY,
1251
+ }}
1252
+ >
1253
+ <TouchableOpacity onPress={this.onPressUser}>
1254
+ <View style={styles.userPickerContainer}>
1255
+ <View style={styles.profileContainer}>
1256
+ <Components.ProfilePic
1257
+ ProfilePic={this.state.selectedUser?.profilePic}
1258
+ Diameter={30}
1259
+ />
1260
+ <Text style={styles.nameText}>
1261
+ {this.state.selectedUser?.displayName ||
1262
+ "Select User"}
1263
+ </Text>
1264
+ </View>
1265
+ <Icon
1266
+ name="angle-right"
1267
+ type="font-awesome"
1268
+ iconStyle={[
1269
+ styles.sectionTitle,
1270
+ {
1271
+ fontSize: 20,
1272
+ color: this.props.colourBrandingMain,
1273
+ },
1274
+ ]}
1275
+ />
1276
+ </View>
1277
+ </TouchableOpacity>
1278
+ </Components.FormCardSection>
1279
+ )}
1280
+ {/* PC-1255: Hide Name field when creating on behalf - name comes from selected user */}
1281
+ {!this.state.canCreateOnBehalf && (
1282
+ <Components.FormCardSection
1283
+ label={"Name"}
1284
+ placeholder={"Enter your name"}
1285
+ textValue={this.state.userName}
1286
+ onChangeText={(userName) => this.onChangeName(userName)}
1287
+ editable={
1288
+ this.props.userType === "KIOSK" &&
1289
+ this.state.submitting === false
1290
+ }
1291
+ isValid={() => {
1292
+ return this.state.userName.length > 1;
1293
+ }}
1294
+ required
1295
+ errorText="Please enter your name."
1296
+ showError={
1297
+ this.state.showError && this.state.userName.length < 2
1298
+ }
1299
+ hasUnderline
1300
+ />
1301
+ )}
1302
+ <Components.FormCardSection
1303
+ label={"Contact number"}
1304
+ placeholder={"Enter phone number"}
1305
+ textValue={this.state.phone}
1306
+ onChangeText={(phone) => this.setState({ phone })}
1307
+ editable={this.state.submitting === false}
1308
+ hasUnderline
1309
+ keyboardType={"phone-pad"}
1310
+ />
1311
+ <Components.FormCardSection
1312
+ label={"Address"}
1313
+ placeholder={"Enter your address"}
1314
+ textValue={this.state.roomNumber}
1315
+ onChangeText={(roomNumber) => this.setState({ roomNumber })}
1316
+ editable={this.state.submitting === false}
1317
+ hasUnderline
1318
+ isValid={() => {
1319
+ return (
1320
+ this.state.roomNumber && this.state.roomNumber.length > 1
1321
+ );
1322
+ }}
1323
+ required
1324
+ errorText="Please provide your address."
1325
+ showError={
1326
+ this.state.showError &&
1327
+ (!this.state.roomNumber || this.state.roomNumber.length < 2)
1328
+ }
1329
+ />
1330
+ </Components.FormCard>
1331
+ <Components.FormCard
1332
+ style={{
1333
+ marginTop: 16,
1334
+ paddingHorizontal: 24,
1335
+ paddingVertical: 16,
1336
+ flexDirection: "row",
1337
+ justifyContent: "space-between",
1338
+ }}
1339
+ >
1340
+ <Text style={styles.sectionTitle}>{values.textJobType}</Text>
1341
+ <TouchableOpacity onPress={this.onPressType.bind(this)}>
1342
+ <View style={{ flexDirection: "row" }}>
1343
+ <Text
1344
+ style={[
1345
+ styles.sectionTitle,
1346
+ { color: this.props.colourBrandingMain, marginRight: 16 },
1347
+ ]}
1348
+ >
1349
+ {this.state.type}
1350
+ </Text>
1351
+ <Icon
1352
+ name="angle-right"
1353
+ type="font-awesome"
1354
+ iconStyle={[
1355
+ styles.sectionTitle,
1356
+ { fontSize: 20, color: this.props.colourBrandingMain },
1357
+ ]}
1358
+ />
1359
+ </View>
1360
+ </TouchableOpacity>
1361
+ </Components.FormCard>
1362
+ {hasCustomFields ? (
1363
+ this.renderCustomFields()
1364
+ ) : (
1365
+ <>
1366
+ <Components.FormCard style={{ marginTop: 16 }}>
1367
+ <Components.FormCardSection
1368
+ label={"Title"}
1369
+ placeholder={"Enter a title for your request"}
1370
+ textValue={this.state.title}
1371
+ onChangeText={(title) => this.setState({ title })}
1372
+ editable={this.state.submitting === false}
1373
+ hasUnderline
1374
+ isValid={() => {
1375
+ return this.state.title.length > 1;
1376
+ }}
1377
+ required
1378
+ errorText="Please provide a title."
1379
+ showError={
1380
+ this.state.showError && this.state.title.length < 2
1381
+ }
1382
+ autoCorrect
1383
+ multiline
1384
+ autoGrow
1385
+ />
1386
+ <Components.FormCardSection
1387
+ label={"Description"}
1388
+ placeholder={"Describe your request here in detail"}
1389
+ textValue={this.state.description}
1390
+ onChangeText={(description) =>
1391
+ this.setState({ description })
1392
+ }
1393
+ editable={this.state.submitting === false}
1394
+ hasUnderline
1395
+ autoCorrect
1396
+ multiline
1397
+ autoGrow
1398
+ />
1399
+ </Components.FormCard>
1400
+ <Components.FormCard
1401
+ style={{ marginTop: 16, paddingHorizontal: 24 }}
1402
+ >
1403
+ <View
1404
+ style={[
1405
+ {
1406
+ width: "100%",
1407
+ paddingVertical: 16,
1408
+ flexDirection: "row",
1409
+ justifyContent: "space-between",
1410
+ alignItems: "center",
1411
+ position: "relative",
1412
+ },
1413
+ this.state.isHome && {
1414
+ borderBottomWidth: 1,
1415
+ borderBottomColor: Colours.LINEGREY,
1416
+ },
1417
+ ]}
1418
+ >
1419
+ <Text style={styles.sectionTitle}>
1420
+ {Config.env.strings.MAINTENANCE_HOME}
1421
+ </Text>
1422
+ <Switch
1423
+ value={this.state.isHome}
1424
+ disabled={this.state.submitting}
1425
+ onValueChange={(value) =>
1426
+ this.setState({ isHome: value })
1427
+ }
1428
+ trackColor={{
1429
+ false: "#ddd",
1430
+ true: this.props.colourBrandingMain,
1431
+ }}
1432
+ thumbColor={Platform.OS === "android" ? "#fff" : null}
1433
+ />
1434
+ </View>
1435
+ {this.state.isHome && (
1436
+ <Components.FormCardSection
1437
+ label={"Available times"}
1438
+ placeholder={
1439
+ "Describe your available times here in detail."
1440
+ }
1441
+ textValue={this.state.times}
1442
+ onChangeText={(times) => this.setState({ times })}
1443
+ editable={this.state.submitting === false}
1444
+ hasUnderline
1445
+ isValid={() => {
1446
+ return this.state.times.length > 1;
1447
+ }}
1448
+ required
1449
+ errorText="Please provide available times."
1450
+ showError={
1451
+ this.state.showError &&
1452
+ this.state.isHome &&
1453
+ this.state.times.length < 2
1454
+ }
1455
+ minHeight={40}
1456
+ autoCorrect
1457
+ multiline
1458
+ autoGrow
1459
+ />
1460
+ )}
1461
+ </Components.FormCard>
1462
+ {this.renderImageList()}
1463
+ </>
1464
+ )}
1465
+ </View>
1466
+ </ScrollView>
1467
+ </View>
1468
+ );
1469
+ }
1470
+
1471
+ renderRegisterConfirmation() {
1472
+ return (
1473
+ <Components.ConfirmationPopup
1474
+ confirmText={`${values.textEntityName} submitted`}
1475
+ repeatText={"Submit another"}
1476
+ visible={this.state.confirmationToShow}
1477
+ onClose={this.onCloseConfirmationPopup.bind(this)}
1478
+ onPressAction={this.onConfirmationReset.bind(this)}
1479
+ />
1480
+ );
1481
+ }
1482
+
1483
+ renderVideoPlayerPopup() {
1484
+ const { showFullscreenVideo, currentVideoUrl } = this.state;
1485
+ if (!currentVideoUrl) return;
1486
+
1487
+ return (
1488
+ <Components.VideoPopup
1489
+ uri={currentVideoUrl}
1490
+ visible={showFullscreenVideo}
1491
+ showFullscreenButton={false}
1492
+ onClose={this.toggleFullscreenVideo}
1493
+ />
1494
+ );
1495
+ }
1496
+
1497
+ renderNoType() {
1498
+ if (!this.state.noType) {
1499
+ return null;
1500
+ }
1501
+ return (
1502
+ <Components.WarningPopup
1503
+ confirmText={"No forms are available"}
1504
+ infoText={"Check back later for forms."}
1505
+ visible={this.state.noType}
1506
+ onClose={this.onPressBack.bind(this)}
1507
+ padHorizontal
1508
+ />
1509
+ );
1510
+ }
1511
+
1512
+ render() {
1513
+ const { submitting, success, isDateTimePickerVisible, popUpType } =
1514
+ this.state;
1515
+
1516
+ return (
1517
+ <KeyboardAvoidingView
1518
+ behavior={Platform.OS === "ios" && "padding"}
1519
+ style={styles.viewContainer}
1520
+ >
1521
+ {this.renderImageUploader()}
1522
+ <View style={styles.container}>
1523
+ <Components.Header
1524
+ leftIcon="angle-left"
1525
+ onPressLeft={this.onPressBack.bind(this)}
1526
+ text={
1527
+ this.props.strings[`${values.featureKey}_textFeatureTitle`] ||
1528
+ values.textFeatureTitle
1529
+ }
1530
+ rightText={submitting || success ? null : "Done"}
1531
+ onPressRight={this.submitRequest.bind(this)}
1532
+ absoluteRight
1533
+ />
1534
+ {this.renderForm()}
1535
+ </View>
1536
+ {this.renderRegisterConfirmation()}
1537
+ {this.renderVideoPlayerPopup()}
1538
+ {this.renderNoType()}
1539
+ <DateTimePicker
1540
+ isVisible={isDateTimePickerVisible}
1541
+ onConfirm={this.onDateSelected}
1542
+ onCancel={() => this.setState({ isDateTimePickerVisible: false })}
1543
+ mode={popUpType}
1544
+ headerTextIOS={`Pick a ${popUpType}`}
1545
+ />
1546
+ </KeyboardAvoidingView>
1547
+ );
1548
+ }
1322
1549
  }
1323
1550
 
1324
1551
  const styles = {
1325
- viewContainer: {
1326
- flex: 1,
1327
- backgroundColor: '#fff',
1328
- },
1329
- container: {
1330
- flex: 1,
1331
- position: 'relative',
1332
- backgroundColor: '#f0f0f5',
1333
- },
1334
- errorText: {
1335
- fontFamily: 'sf-regular',
1336
- color: Colours.COLOUR_TANGERINE,
1337
- fontSize: 16,
1338
- },
1339
- requestSuccess: {
1340
- fontFamily: 'sf-regular',
1341
- color: Colours.TEXT_DARK,
1342
- fontSize: 17,
1343
- textAlign: 'center',
1344
- },
1345
- sectionTitle: {
1346
- fontFamily: 'sf-regular',
1347
- fontSize: 17,
1348
- color: Colours.TEXT_DARK,
1349
- },
1350
- imageListContainer: {
1351
- marginTop: 8,
1352
- padding: 8,
1353
- flexDirection: 'row',
1354
- backgroundColor: Colours.BOXGREY,
1355
- minHeight: 106,
1356
- },
1357
- imageListContainerEmpty: {
1358
- justifyContent: 'center',
1359
- alignItems: 'center',
1360
- flexDirection: 'column',
1361
- },
1362
- documentListContainer: {
1363
- marginTop: 8,
1364
- flexDirection: 'column',
1365
- alignItems: 'flex-start',
1366
- },
1367
- imageContainer: {
1368
- width: PHOTO_SIZE,
1369
- height: PHOTO_SIZE,
1370
- borderStyle: 'dashed',
1371
- justifyContent: 'center',
1372
- alignItems: 'center',
1373
- borderWidth: 1,
1374
- borderColor: Colours.LINEGREY,
1375
- borderRadius: 4,
1376
- marginLeft: 8,
1377
- },
1378
- imageContainerNotEmpty: {
1379
- borderWidth: 1,
1380
- borderColor: Colours.LINEGREY,
1381
- backgroundColor: '#fff',
1382
- borderStyle: 'dashed',
1383
- },
1384
- imageBackground: {
1385
- flex: 1,
1386
- height: '100%',
1387
- width: '100%',
1388
- borderRadius: 4,
1389
- },
1390
- imageCircle: {
1391
- width: 90,
1392
- height: 90,
1393
- borderRadius: 45,
1394
- backgroundColor: Colours.PINKISH_GREY,
1395
- justifyContent: 'center',
1396
- },
1397
- addImageIcon: {
1398
- color: '#fff',
1399
- fontSize: 32,
1400
- },
1401
- imagePlayContainer: {
1402
- position: 'absolute',
1403
- top: 0,
1404
- left: 0,
1405
- right: 0,
1406
- bottom: 0,
1407
- alignItems: 'center',
1408
- justifyContent: 'center',
1409
- },
1410
- imageControlIcon: {
1411
- color: '#fff',
1412
- fontSize: 20,
1413
- textShadowColor: 'rgba(0,0,0,0.3)',
1414
- textShadowOffset: { width: 2, height: 2 },
1415
- },
1416
- removeImage: {
1417
- position: 'absolute',
1418
- top: 0,
1419
- right: 0,
1420
- padding: 4,
1421
- width: 40,
1422
- height: 40,
1423
- alignItems: 'center',
1424
- justifyContent: 'center',
1425
- },
1426
- staticTitle: {
1427
- fontSize: 20,
1428
- fontFamily: 'sf-semibold',
1429
- color: Colours.TEXT_DARKEST,
1430
- },
1431
- staticText: {
1432
- fontSize: 17,
1433
- fontFamily: 'sf-regular',
1434
- color: Colours.TEXT_DARKEST,
1435
- lineHeight: 24,
1436
- },
1437
- multiChoiceOption: {
1438
- marginTop: 16,
1439
- flexDirection: 'row',
1440
- alignItems: 'center',
1441
- minHeight: 20,
1442
- },
1443
- multiChoiceText: {
1444
- flex: 1,
1445
- fontFamily: 'sf-medium',
1446
- fontSize: 14,
1447
- color: Colours.TEXT_DARK,
1448
- },
1449
- tick: {
1450
- marginRight: 10,
1451
- borderRadius: 4,
1452
- },
1453
- unticked: {
1454
- marginRight: 10,
1455
- width: 20,
1456
- height: 20,
1457
- borderColor: Colours.LINEGREY,
1458
- borderWidth: 1,
1459
- borderRadius: 4,
1460
- },
1461
- dateContainer: {
1462
- flexDirection: 'row',
1463
- alignItems: 'center',
1464
- },
1465
- dateFieldButton: {
1466
- flex: 1,
1467
- },
1468
- dateFieldContainer: {
1469
- flexDirection: 'row',
1470
- borderRadius: 2,
1471
- backgroundColor: '#ebeff2',
1472
- padding: 8,
1473
- marginTop: 8,
1474
- },
1475
- dateText: {
1476
- flex: 1,
1477
- fontFamily: 'sf-regular',
1478
- fontSize: 16,
1479
- color: '#65686D',
1480
- },
1481
- dateIcon: {
1482
- fontSize: 18,
1483
- color: Colours.TEXT_BLUEGREY,
1484
- },
1485
- dateClearButton: {
1486
- paddingLeft: 12,
1487
- },
1488
- removeIcon: {
1489
- fontSize: 26,
1490
- color: Colours.TEXT_BLUEGREY,
1491
- },
1492
- documentContainer: {
1493
- flexDirection: 'row',
1494
- alignItems: 'center',
1495
- justifyContent: 'space-between',
1496
- paddingVertical: 4,
1497
- },
1498
- documentTypeContainer: {
1499
- width: 50,
1500
- height: 60,
1501
- justifyContent: 'center',
1502
- alignItems: 'center',
1503
- borderRadius: 5,
1504
- marginRight: 8,
1505
- },
1506
- documentTypeText: {
1507
- color: '#fff',
1508
- fontFamily: 'sf-semibold',
1509
- textAlign: 'center',
1510
- },
1511
- documentText: {
1512
- flex: 1,
1513
- fontFamily: 'sf-semibold',
1514
- fontSize: 16,
1515
- color: '#65686D',
1516
- },
1517
- removeDocumentButton: {
1518
- padding: 4,
1519
- width: 40,
1520
- height: 40,
1521
- alignItems: 'center',
1522
- justifyContent: 'center',
1523
- marginLeft: 8,
1524
- },
1525
- removeDocumentIcon: {
1526
- fontSize: 24,
1527
- },
1528
- // PC-1255: User picker styles
1529
- userPickerContainer: {
1530
- flexDirection: 'row',
1531
- justifyContent: 'space-between',
1532
- alignItems: 'center',
1533
- },
1534
- profileContainer: {
1535
- flexDirection: 'row',
1536
- alignItems: 'center',
1537
- },
1538
- nameText: {
1539
- fontFamily: 'sf-medium',
1540
- fontSize: 16,
1541
- color: Colours.TEXT_DARK,
1542
- marginLeft: 8,
1543
- },
1552
+ viewContainer: {
1553
+ flex: 1,
1554
+ backgroundColor: "#fff",
1555
+ },
1556
+ container: {
1557
+ flex: 1,
1558
+ position: "relative",
1559
+ backgroundColor: "#f0f0f5",
1560
+ },
1561
+ errorText: {
1562
+ fontFamily: "sf-regular",
1563
+ color: Colours.COLOUR_TANGERINE,
1564
+ fontSize: 16,
1565
+ },
1566
+ requestSuccess: {
1567
+ fontFamily: "sf-regular",
1568
+ color: Colours.TEXT_DARK,
1569
+ fontSize: 17,
1570
+ textAlign: "center",
1571
+ },
1572
+ sectionTitle: {
1573
+ fontFamily: "sf-regular",
1574
+ fontSize: 17,
1575
+ color: Colours.TEXT_DARK,
1576
+ },
1577
+ imageListContainer: {
1578
+ marginTop: 8,
1579
+ padding: 8,
1580
+ flexDirection: "row",
1581
+ backgroundColor: Colours.BOXGREY,
1582
+ minHeight: 106,
1583
+ },
1584
+ imageListContainerEmpty: {
1585
+ justifyContent: "center",
1586
+ alignItems: "center",
1587
+ flexDirection: "column",
1588
+ },
1589
+ documentListContainer: {
1590
+ marginTop: 8,
1591
+ flexDirection: "column",
1592
+ alignItems: "flex-start",
1593
+ },
1594
+ imageContainer: {
1595
+ width: PHOTO_SIZE,
1596
+ height: PHOTO_SIZE,
1597
+ borderStyle: "dashed",
1598
+ justifyContent: "center",
1599
+ alignItems: "center",
1600
+ borderWidth: 1,
1601
+ borderColor: Colours.LINEGREY,
1602
+ borderRadius: 4,
1603
+ marginLeft: 8,
1604
+ },
1605
+ imageContainerNotEmpty: {
1606
+ borderWidth: 1,
1607
+ borderColor: Colours.LINEGREY,
1608
+ backgroundColor: "#fff",
1609
+ borderStyle: "dashed",
1610
+ },
1611
+ imageBackground: {
1612
+ flex: 1,
1613
+ height: "100%",
1614
+ width: "100%",
1615
+ borderRadius: 4,
1616
+ },
1617
+ imageCircle: {
1618
+ width: 90,
1619
+ height: 90,
1620
+ borderRadius: 45,
1621
+ backgroundColor: Colours.PINKISH_GREY,
1622
+ justifyContent: "center",
1623
+ },
1624
+ addImageIcon: {
1625
+ color: "#fff",
1626
+ fontSize: 32,
1627
+ },
1628
+ imagePlayContainer: {
1629
+ position: "absolute",
1630
+ top: 0,
1631
+ left: 0,
1632
+ right: 0,
1633
+ bottom: 0,
1634
+ alignItems: "center",
1635
+ justifyContent: "center",
1636
+ },
1637
+ imageControlIcon: {
1638
+ color: "#fff",
1639
+ fontSize: 20,
1640
+ textShadowColor: "rgba(0,0,0,0.3)",
1641
+ textShadowOffset: { width: 2, height: 2 },
1642
+ },
1643
+ removeImage: {
1644
+ position: "absolute",
1645
+ top: 0,
1646
+ right: 0,
1647
+ padding: 4,
1648
+ width: 40,
1649
+ height: 40,
1650
+ alignItems: "center",
1651
+ justifyContent: "center",
1652
+ },
1653
+ staticTitle: {
1654
+ fontSize: 20,
1655
+ fontFamily: "sf-semibold",
1656
+ color: Colours.TEXT_DARKEST,
1657
+ },
1658
+ staticText: {
1659
+ fontSize: 17,
1660
+ fontFamily: "sf-regular",
1661
+ color: Colours.TEXT_DARKEST,
1662
+ lineHeight: 24,
1663
+ },
1664
+ multiChoiceOption: {
1665
+ marginTop: 16,
1666
+ flexDirection: "row",
1667
+ alignItems: "center",
1668
+ minHeight: 20,
1669
+ },
1670
+ multiChoiceText: {
1671
+ flex: 1,
1672
+ fontFamily: "sf-medium",
1673
+ fontSize: 14,
1674
+ color: Colours.TEXT_DARK,
1675
+ },
1676
+ tick: {
1677
+ marginRight: 10,
1678
+ borderRadius: 4,
1679
+ },
1680
+ unticked: {
1681
+ marginRight: 10,
1682
+ width: 20,
1683
+ height: 20,
1684
+ borderColor: Colours.LINEGREY,
1685
+ borderWidth: 1,
1686
+ borderRadius: 4,
1687
+ },
1688
+ dateContainer: {
1689
+ flexDirection: "row",
1690
+ alignItems: "center",
1691
+ },
1692
+ dateFieldButton: {
1693
+ flex: 1,
1694
+ },
1695
+ dateFieldContainer: {
1696
+ flexDirection: "row",
1697
+ borderRadius: 2,
1698
+ backgroundColor: "#ebeff2",
1699
+ padding: 8,
1700
+ marginTop: 8,
1701
+ },
1702
+ dateText: {
1703
+ flex: 1,
1704
+ fontFamily: "sf-regular",
1705
+ fontSize: 16,
1706
+ color: "#65686D",
1707
+ },
1708
+ dateIcon: {
1709
+ fontSize: 18,
1710
+ color: Colours.TEXT_BLUEGREY,
1711
+ },
1712
+ dateClearButton: {
1713
+ paddingLeft: 12,
1714
+ },
1715
+ removeIcon: {
1716
+ fontSize: 26,
1717
+ color: Colours.TEXT_BLUEGREY,
1718
+ },
1719
+ documentContainer: {
1720
+ flexDirection: "row",
1721
+ alignItems: "center",
1722
+ justifyContent: "space-between",
1723
+ paddingVertical: 4,
1724
+ },
1725
+ documentTypeContainer: {
1726
+ width: 50,
1727
+ height: 60,
1728
+ justifyContent: "center",
1729
+ alignItems: "center",
1730
+ borderRadius: 5,
1731
+ marginRight: 8,
1732
+ },
1733
+ documentTypeText: {
1734
+ color: "#fff",
1735
+ fontFamily: "sf-semibold",
1736
+ textAlign: "center",
1737
+ },
1738
+ documentText: {
1739
+ flex: 1,
1740
+ fontFamily: "sf-semibold",
1741
+ fontSize: 16,
1742
+ color: "#65686D",
1743
+ },
1744
+ removeDocumentButton: {
1745
+ padding: 4,
1746
+ width: 40,
1747
+ height: 40,
1748
+ alignItems: "center",
1749
+ justifyContent: "center",
1750
+ marginLeft: 8,
1751
+ },
1752
+ removeDocumentIcon: {
1753
+ fontSize: 24,
1754
+ },
1755
+ // PC-1255: User picker styles
1756
+ userPickerContainer: {
1757
+ flexDirection: "row",
1758
+ justifyContent: "space-between",
1759
+ alignItems: "center",
1760
+ },
1761
+ profileContainer: {
1762
+ flexDirection: "row",
1763
+ alignItems: "center",
1764
+ },
1765
+ nameText: {
1766
+ fontFamily: "sf-medium",
1767
+ fontSize: 16,
1768
+ color: Colours.TEXT_DARK,
1769
+ marginLeft: 8,
1770
+ },
1544
1771
  };
1545
1772
 
1546
- const mapStateToProps = state => {
1547
- const { user, connection } = state;
1548
- const { displayName, profilePic, uid, site, unit, phoneNumber, permissions } = user;
1549
- return {
1550
- connected: connection.connected,
1551
- userType: user.type,
1552
- displayName,
1553
- profilePic,
1554
- uid,
1555
- site,
1556
- unit,
1557
- phoneNumber,
1558
- permissions,
1559
- colourBrandingMain: Colours.getMainBrandingColourFromState(state),
1560
- strings: state.strings?.config || {},
1561
- };
1773
+ const mapStateToProps = (state) => {
1774
+ const { user, connection } = state;
1775
+ const { displayName, profilePic, uid, site, unit, phoneNumber, permissions } =
1776
+ user;
1777
+ return {
1778
+ connected: connection.connected,
1779
+ userType: user.type,
1780
+ displayName,
1781
+ profilePic,
1782
+ uid,
1783
+ site,
1784
+ unit,
1785
+ phoneNumber,
1786
+ permissions,
1787
+ colourBrandingMain: Colours.getMainBrandingColourFromState(state),
1788
+ strings: state.strings?.config || {},
1789
+ };
1562
1790
  };
1563
1791
 
1564
1792
  export default connect(mapStateToProps, { jobAdded })(MaintenanceRequest);