@plusscommunities/pluss-maintenance-app-forms 7.0.20-auth.0 → 7.0.21

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