@plusscommunities/pluss-maintenance-app-forms 6.0.10 → 6.0.11-auth.0

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