@plusscommunities/pluss-maintenance-app 8.0.8 → 8.0.9-beta.0

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