@plusscommunities/pluss-maintenance-app-forms 8.0.5 → 8.0.6

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