@plusscommunities/pluss-maintenance-app 6.0.5 → 6.0.6-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/module/actions/types.js +4 -3
  2. package/dist/module/actions/types.js.map +1 -1
  3. package/dist/module/apis/maintenanceActions.js +17 -15
  4. package/dist/module/apis/maintenanceActions.js.map +1 -1
  5. package/dist/module/components/MaintenanceList.js +4 -3
  6. package/dist/module/components/MaintenanceList.js.map +1 -1
  7. package/dist/module/components/MaintenanceListItem.js +3 -2
  8. package/dist/module/components/MaintenanceListItem.js.map +1 -1
  9. package/dist/module/components/MaintenanceWidgetItem.js +2 -1
  10. package/dist/module/components/MaintenanceWidgetItem.js.map +1 -1
  11. package/dist/module/components/WidgetSmall.js +6 -3
  12. package/dist/module/components/WidgetSmall.js.map +1 -1
  13. package/dist/module/feature.config.js +18 -33
  14. package/dist/module/feature.config.js.map +1 -1
  15. package/dist/module/index.js +15 -10
  16. package/dist/module/index.js.map +1 -1
  17. package/dist/module/screens/MaintenancePage.js +7 -3
  18. package/dist/module/screens/MaintenancePage.js.map +1 -1
  19. package/dist/module/screens/RequestDetail.js +105 -30
  20. package/dist/module/screens/RequestDetail.js.map +1 -1
  21. package/dist/module/screens/RequestNotes.js +2 -1
  22. package/dist/module/screens/RequestNotes.js.map +1 -1
  23. package/dist/module/screens/ServiceRequest.js +552 -84
  24. package/dist/module/screens/ServiceRequest.js.map +1 -1
  25. package/dist/module/values.config.a.js +30 -0
  26. package/dist/module/values.config.a.js.map +1 -0
  27. package/dist/module/values.config.b.js +30 -0
  28. package/dist/module/values.config.b.js.map +1 -0
  29. package/dist/module/values.config.c.js +30 -0
  30. package/dist/module/values.config.c.js.map +1 -0
  31. package/dist/module/values.config.d.js +30 -0
  32. package/dist/module/values.config.d.js.map +1 -0
  33. package/dist/module/values.config.default.js +30 -0
  34. package/dist/module/values.config.default.js.map +1 -0
  35. package/dist/module/values.config.js +30 -0
  36. package/dist/module/values.config.js.map +1 -0
  37. package/package.json +9 -3
  38. package/src/actions/types.js +5 -3
  39. package/src/apis/maintenanceActions.js +30 -14
  40. package/src/components/MaintenanceList.js +5 -3
  41. package/src/components/MaintenanceListItem.js +7 -2
  42. package/src/components/MaintenanceWidgetItem.js +2 -1
  43. package/src/components/WidgetSmall.js +6 -3
  44. package/src/feature.config.js +18 -30
  45. package/src/index.js +15 -8
  46. package/src/screens/MaintenancePage.js +5 -4
  47. package/src/screens/RequestDetail.js +100 -32
  48. package/src/screens/RequestNotes.js +2 -1
  49. package/src/screens/ServiceRequest.js +591 -149
  50. package/src/values.config.a.js +30 -0
  51. package/src/values.config.b.js +30 -0
  52. package/src/values.config.c.js +30 -0
  53. package/src/values.config.d.js +30 -0
  54. package/src/values.config.default.js +30 -0
  55. package/src/values.config.js +30 -0
@@ -12,13 +12,16 @@ import {
12
12
  ImageBackground,
13
13
  Keyboard,
14
14
  } from 'react-native';
15
- import _ from 'lodash';
15
+ import DateTimePicker from 'react-native-modal-datetime-picker';
16
16
  import { Icon } from 'react-native-elements';
17
+ import _ from 'lodash';
18
+ import moment from 'moment';
17
19
  import { connect } from 'react-redux';
18
20
  import { jobAdded } from '../actions';
19
21
  import { maintenanceActions } from '../apis';
20
22
  import { Services } from '../feature.config';
21
23
  import { Components, Colours, Helper, Config } from '../core.config';
24
+ import { values } from '../values.config';
22
25
 
23
26
  const PHOTO_SIZE = (Dimensions.get('window').width - 64) / 3;
24
27
 
@@ -55,8 +58,20 @@ class MaintenanceRequest extends Component {
55
58
  types: [],
56
59
 
57
60
  confirmationToShow: false,
61
+
62
+ customFields: [],
63
+ customFieldImages: {},
64
+ isDateTimePickerVisible: false,
65
+ popUpType: 'date',
66
+ dateFieldId: null,
67
+ imageFieldId: null,
58
68
  };
59
69
  this.checkThumb = null;
70
+ this.keyboardTypes = {
71
+ phone: 'phone-pad',
72
+ email: 'email-address',
73
+ text: 'default',
74
+ };
60
75
  }
61
76
 
62
77
  componentDidMount() {
@@ -74,9 +89,62 @@ class MaintenanceRequest extends Component {
74
89
  clearInterval(this.checkThumb);
75
90
  }
76
91
 
92
+ onChangeAnswer = (fieldId, answer) => {
93
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
94
+ const field = update.customFields[fieldId];
95
+ field.answer = answer;
96
+ if (field.isTitle) update.title = field.answer;
97
+ this.setState(update);
98
+ };
99
+
100
+ onChangeToggleAnswer = (fieldId, answer) => {
101
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
102
+ const field = update.customFields[fieldId];
103
+ field.answer = field.answer === answer ? undefined : answer;
104
+ if (field.isTitle) update.title = field.answer;
105
+ this.setState(update);
106
+ };
107
+
108
+ onChangeCheckboxAnswer = (fieldId, answer) => {
109
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
110
+ const field = update.customFields[fieldId];
111
+ field.answer = _.xor(field.answer || [], [answer]);
112
+ if (field.isTitle) update.title = field.answer.join(', ');
113
+ this.setState(update);
114
+ };
115
+
116
+ onOpenDatePicker = (field, fieldId) => {
117
+ Keyboard.dismiss();
118
+ this.setState({ dateFieldId: fieldId, popUpType: field.type, isDateTimePickerVisible: true });
119
+ };
120
+
121
+ onClearDate = fieldId => {
122
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
123
+ const field = update.customFields[fieldId];
124
+ field.answer = undefined;
125
+ if (field.isTitle) update.title = field.answer;
126
+ this.setState(update);
127
+ };
128
+
129
+ onDateSelected = date => {
130
+ const { customFields, dateFieldId, popUpType } = this.state;
131
+ const update = { customFields: _.cloneDeep(customFields), isDateTimePickerVisible: false, fieldId: null };
132
+ const field = update.customFields[dateFieldId];
133
+ const dateObj = moment(date);
134
+ if (popUpType === 'date') {
135
+ field.answer = dateObj.format('YYYY-MM-DD');
136
+ if (field.isTitle) update.title = dateObj.format('DD MMM YYYY');
137
+ } else {
138
+ field.answer = dateObj.format('HH:mm');
139
+ if (field.isTitle) update.title = dateObj.format('h:mm a');
140
+ }
141
+ this.setState(update);
142
+ };
143
+
77
144
  onUploadStarted = (uploadUri, imageUri) => {
78
- const images = [...this.state.images];
79
- images.splice(images.length - 1, 0, {
145
+ const { imageFieldId } = this.state;
146
+ const imagesUpdate = this.getImages(imageFieldId);
147
+ imagesUpdate.splice(imagesUpdate.length - 1, 0, {
80
148
  uploading: true,
81
149
  uploadProgress: '0%',
82
150
  uploadUri,
@@ -84,24 +152,27 @@ class MaintenanceRequest extends Component {
84
152
  allowRetry: true,
85
153
  });
86
154
 
87
- this.setState({ images });
155
+ this.setImages(imagesUpdate, imageFieldId);
88
156
  };
89
157
 
90
158
  onUploadProgress = progress => {
91
- const images = [...this.state.images];
92
- images.map(img => {
159
+ const { imageFieldId } = this.state;
160
+ const imagesUpdate = this.getImages(imageFieldId);
161
+ imagesUpdate.map(img => {
93
162
  if (img.uploadUri === progress.uri) {
94
163
  img.uploadProgress = progress.percentage;
95
164
  img.uploading = true;
96
165
  img.allowRetry = true;
97
166
  }
98
167
  });
99
- this.setState({ images });
168
+
169
+ this.setImages(imagesUpdate, imageFieldId);
100
170
  };
101
171
 
102
172
  onUploadSuccess = async (uri, uploadUri) => {
103
- const images = [...this.state.images];
104
- images.map(img => {
173
+ const { imageFieldId } = this.state;
174
+ const imagesUpdate = this.getImages(imageFieldId);
175
+ imagesUpdate.map(img => {
105
176
  if (img.uploadUri === uploadUri && img.uploading) {
106
177
  img.url = uri.replace('/general/', '/general1400/');
107
178
  img.thumbNailExists = false;
@@ -109,12 +180,14 @@ class MaintenanceRequest extends Component {
109
180
  img.allowRetry = true;
110
181
  }
111
182
  });
112
- this.setState({ images }, () => this.waitForThumbnails());
183
+
184
+ this.setImages(imagesUpdate, imageFieldId, () => this.waitForThumbnails());
113
185
  };
114
186
 
115
187
  onUploadFailed = uploadUri => {
116
- const images = [...this.state.images];
117
- images.map(img => {
188
+ const { imageFieldId } = this.state;
189
+ const imagesUpdate = this.getImages(imageFieldId);
190
+ imagesUpdate.map(img => {
118
191
  if (img.uploadUri === uploadUri) {
119
192
  img.uploading = true; // Requried for retry
120
193
  img.uploadProgress = '';
@@ -122,12 +195,13 @@ class MaintenanceRequest extends Component {
122
195
  }
123
196
  });
124
197
 
125
- this.setState({ images });
198
+ this.setImages(imagesUpdate, imageFieldId);
126
199
  };
127
200
 
128
201
  onLibrarySelected = uri => {
129
- const images = [...this.state.images];
130
- images.splice(images.length - 1, 0, {
202
+ const { imageFieldId } = this.state;
203
+ const imagesUpdate = this.getImages(imageFieldId);
204
+ imagesUpdate.splice(imagesUpdate.length - 1, 0, {
131
205
  uploading: false,
132
206
  allowRetry: false,
133
207
  url: Helper.get1400(uri),
@@ -135,7 +209,7 @@ class MaintenanceRequest extends Component {
135
209
  thumbNailUrl: Helper.getThumb300(uri),
136
210
  });
137
211
 
138
- this.setState({ images });
212
+ this.setImages(imagesUpdate, imageFieldId);
139
213
  };
140
214
 
141
215
  onPressBack() {
@@ -143,7 +217,7 @@ class MaintenanceRequest extends Component {
143
217
  }
144
218
 
145
219
  onPressType() {
146
- Services.navigation.navigate('jobTypePicker', {
220
+ Services.navigation.navigate(values.screenJobTypePicker, {
147
221
  currentType: this.state.type,
148
222
  types: this.state.types,
149
223
  onSelectType: this.pickType.bind(this),
@@ -159,24 +233,69 @@ class MaintenanceRequest extends Component {
159
233
  }
160
234
 
161
235
  onConfirmationReset() {
162
- this.setState({
163
- confirmationToShow: false,
164
- title: '',
165
- description: '',
166
- times: '',
167
- isHome: false,
168
- uploadingImage: false,
169
- images: [
170
- {
171
- add: true,
172
- },
173
- ],
174
- submitting: false,
175
- success: false,
176
- fail: false,
177
- });
236
+ this.setState(
237
+ {
238
+ confirmationToShow: false,
239
+ title: '',
240
+ description: '',
241
+ times: '',
242
+ isHome: false,
243
+ uploadingImage: false,
244
+ images: [
245
+ {
246
+ add: true,
247
+ },
248
+ ],
249
+ submitting: false,
250
+ success: false,
251
+ fail: false,
252
+ customFields: [],
253
+ customFieldImages: {},
254
+ isDateTimePickerVisible: false,
255
+ popUpType: 'date',
256
+ dateFieldId: null,
257
+ imageFieldId: null,
258
+ },
259
+ () => this.pickType(this.state.type),
260
+ );
178
261
  }
179
262
 
263
+ isFieldValid = (field, fieldId) => {
264
+ const { mandatory, type, answer } = field;
265
+ if (['staticTitle', 'staticText'].includes(type)) return true;
266
+
267
+ const imagesList = type === 'image' ? this.getImageUrls(fieldId) : [];
268
+ const checkMandatory = () => {
269
+ if (!mandatory) return true;
270
+ switch (type) {
271
+ case 'yn':
272
+ return _.isBoolean(answer);
273
+ case 'image':
274
+ return imagesList.length > 0;
275
+ case 'checkbox':
276
+ return _.isArray(answer) && answer.length > 0;
277
+ default:
278
+ return !_.isNil(answer) && !_.isEmpty(answer);
279
+ }
280
+ };
281
+ const checkFormat = () => {
282
+ if (_.isNil(answer) || _.isEmpty(answer)) return true;
283
+ switch (type) {
284
+ case 'email':
285
+ return Helper.isEmail(answer);
286
+ case 'date':
287
+ return moment(answer, 'YYYY-MM-DD', true).isValid();
288
+ case 'time':
289
+ return moment(answer, 'HH:mm', true).isValid();
290
+ default:
291
+ return true;
292
+ }
293
+ };
294
+
295
+ const valid = checkMandatory() && checkFormat();
296
+ return valid;
297
+ };
298
+
180
299
  getJobTypes() {
181
300
  const self = this;
182
301
  maintenanceActions
@@ -190,21 +309,29 @@ class MaintenanceRequest extends Component {
190
309
  .catch(() => {});
191
310
  }
192
311
 
193
- getDefaultJob() {
194
- if (this.state.types.length !== 0 && this.state.jobId == null) {
195
- this.setState({ type: this.state.types[0].typeName });
196
- }
312
+ pickType(type) {
313
+ const { types } = this.state;
314
+ const selected = types.find(t => t.typeName === type) || {};
315
+ this.setState({
316
+ type,
317
+ customFields: selected.hasCustomFields && selected.customFields.length > 0 ? _.cloneDeep(selected.customFields) : [],
318
+ });
197
319
  }
198
320
 
199
- pickType(type) {
200
- this.setState({ type });
321
+ getDefaultJob() {
322
+ const { types, jobId } = this.state;
323
+ if (types.length !== 0 && jobId == null) {
324
+ const defaultType = types[0];
325
+ this.pickType(defaultType.typeName);
326
+ }
201
327
  }
202
328
 
203
- showUploadMenu = () => {
329
+ showUploadMenu = fieldId => {
204
330
  Keyboard.dismiss();
205
331
  if (this.state.uploadingImage || this.state.submitting) {
206
332
  return;
207
333
  }
334
+ if (fieldId) this.setState({ imageFieldId: fieldId });
208
335
  this.imageUploader.showUploadMenu();
209
336
  };
210
337
 
@@ -221,10 +348,13 @@ class MaintenanceRequest extends Component {
221
348
  this.scrollContainer.scrollTo({ y: 0 });
222
349
  }, 100);
223
350
 
224
- const images = _.filter(this.state.images, img => {
225
- return !img.uploading && !img.add;
226
- }).map(img => {
227
- return img.url;
351
+ const images = this.getImageUrls();
352
+
353
+ // Fix custom images field answers
354
+ const customFields = _.cloneDeep(this.state.customFields);
355
+ const updatedCustomFields = customFields.map((field, fieldId) => {
356
+ if (field.type === 'image') field.answer = this.getImageUrls(fieldId);
357
+ return field;
228
358
  });
229
359
 
230
360
  maintenanceActions
@@ -241,6 +371,7 @@ class MaintenanceRequest extends Component {
241
371
  Helper.getSite(this.props.site),
242
372
  this.state.isHome,
243
373
  this.state.times,
374
+ updatedCustomFields,
244
375
  )
245
376
  .then(res => {
246
377
  if (res.data.success) {
@@ -269,45 +400,98 @@ class MaintenanceRequest extends Component {
269
400
  refreshRequest = async id => {
270
401
  try {
271
402
  const job = await maintenanceActions.getJob(Helper.getSite(this.props.site), id);
272
- // console.log('refreshRequest', job?.data);
273
403
  this.props.jobAdded(job.data);
274
404
  } catch (error) {
275
405
  console.log('refreshRequest error', error);
276
406
  }
277
407
  };
278
408
 
409
+ validateCustomFields = () => {
410
+ const { customFields } = this.state;
411
+ if (!customFields || customFields.length === 0) return true;
412
+
413
+ return customFields.every((field, index) => {
414
+ const isValid = this.isFieldValid(field, index);
415
+ return isValid;
416
+ });
417
+ };
418
+
279
419
  submitRequest() {
280
- if (this.state.submitting || !this.props.connected) {
420
+ const { customFields, submitting, uploadingImage, title, roomNumber, isHome, times } = this.state;
421
+ const hasCustomFields = customFields && customFields.length > 0;
422
+
423
+ if (submitting || !this.props.connected) {
281
424
  if (!this.props.connected) {
282
425
  this.setState({ error: { message: 'No internet connection detected' } });
283
426
  }
284
427
  return;
285
428
  }
286
- if (this.state.uploadingImage) {
287
- return;
288
- }
429
+ if (uploadingImage) return;
430
+
289
431
  this.setState({ error: null, showError: false });
290
- if (this.state.title.length === 0 || !this.state.roomNumber || this.state.roomNumber.length === 0) {
432
+ if (title.length === 0 || !roomNumber || roomNumber.length === 0) {
433
+ console.log('submitRequest - error', { title, roomNumber });
291
434
  this.setState({ showError: true });
292
435
  return;
293
436
  }
294
- if (this.state.isHome && this.state.times.length < 2) {
295
- this.setState({ showError: true });
296
- return;
437
+ if (hasCustomFields) {
438
+ if (!this.validateCustomFields()) {
439
+ console.log('submitRequest - custom fields error');
440
+ this.setState({ showError: true });
441
+ return;
442
+ }
443
+ } else {
444
+ if (isHome && times.length < 2) {
445
+ console.log('submitRequest - error', { isHome, times });
446
+ this.setState({ showError: true });
447
+ return;
448
+ }
297
449
  }
298
450
  this.submit();
299
451
  }
300
452
 
453
+ getImages = (fieldId = null) => {
454
+ const { images, customFieldImages } = this.state;
455
+ const imagesList = _.cloneDeep(fieldId ? customFieldImages[fieldId] : images);
456
+ if (!imagesList || !Array.isArray(imagesList) || imagesList.length === 0) {
457
+ return [{ add: true }];
458
+ }
459
+ return imagesList;
460
+ };
461
+
462
+ setImages = (imagesList, fieldId = null, callback = null) => {
463
+ let update = {};
464
+ if (fieldId) {
465
+ const customFieldImages = _.cloneDeep(this.state.customFieldImages);
466
+ customFieldImages[fieldId] = imagesList;
467
+ update = { customFieldImages };
468
+ } else {
469
+ update = { images: imagesList };
470
+ }
471
+ this.setState(update, callback);
472
+ };
473
+
474
+ getImageUrls = (fieldId = null) => {
475
+ const imagesList = this.getImages(fieldId);
476
+ return _.filter(imagesList, img => {
477
+ return !img.uploading && !img.add;
478
+ }).map(img => {
479
+ return img.url;
480
+ });
481
+ };
482
+
301
483
  waitForThumbnails = () => {
302
484
  if (this.checkThumb) return;
303
485
 
304
486
  this.checkThumb = setInterval(async () => {
305
- const images = [];
487
+ const { imageFieldId } = this.state;
488
+ const imagesList = this.getImages(imageFieldId);
489
+ const imagesUpdate = [];
306
490
  await Promise.all(
307
- this.state.images.map(image => {
491
+ imagesList.map(image => {
308
492
  return new Promise(async resolve => {
309
493
  const newImage = { ...image };
310
- images.push(newImage);
494
+ imagesUpdate.push(newImage);
311
495
  if (newImage.url && !newImage.thumbNailExists) {
312
496
  newImage.uploading = false;
313
497
  newImage.allowRetry = false;
@@ -318,20 +502,20 @@ class MaintenanceRequest extends Component {
318
502
  });
319
503
  }),
320
504
  );
321
- const thumbnailsExist = images.every(image => !image.url || image.thumbNailExists);
505
+ const thumbnailsExist = imagesUpdate.every(image => !image.url || image.thumbNailExists);
322
506
  if (thumbnailsExist) {
323
507
  clearInterval(this.checkThumb);
324
508
  this.checkThumb = null;
325
- this.setState({ images });
509
+ this.setImages(imagesUpdate, imageFieldId);
326
510
  }
327
511
  }, 2000);
328
512
  };
329
513
 
330
- removeImage = index => {
331
- const images = [...this.state.images];
332
- images.splice(index, 1);
514
+ removeImage = (index, fieldId) => {
515
+ const imagesUpdate = this.getImages(fieldId);
516
+ imagesUpdate.splice(index, 1);
333
517
 
334
- this.setState({ images });
518
+ this.setImages(imagesUpdate, fieldId);
335
519
  };
336
520
 
337
521
  toggleFullscreenVideo = url => {
@@ -360,16 +544,17 @@ class MaintenanceRequest extends Component {
360
544
  );
361
545
  }
362
546
 
363
- renderImage(item, index) {
547
+ renderImage(item, index, fieldId = null) {
364
548
  const isVideoUrl = Helper.isVideo(item.url);
549
+ const imagesList = this.getImages(fieldId);
365
550
 
366
551
  if (item.add) {
367
552
  return (
368
- <TouchableOpacity activeOpacity={0.8} onPress={this.showUploadMenu}>
553
+ <TouchableOpacity activeOpacity={0.8} onPress={() => this.showUploadMenu(fieldId)}>
369
554
  <View
370
555
  style={[
371
556
  styles.imageContainer,
372
- this.state.images.length > 1 && styles.imageContainerNotEmpty,
557
+ imagesList.length > 1 && styles.imageContainerNotEmpty,
373
558
  index % 3 === 0 && { marginLeft: 0 },
374
559
  index > 2 && { marginTop: 8 },
375
560
  ]}
@@ -385,7 +570,7 @@ class MaintenanceRequest extends Component {
385
570
  <View
386
571
  style={[
387
572
  styles.imageContainer,
388
- this.state.images.length > 1 && styles.imageContainerNotEmpty,
573
+ imagesList.length > 1 && styles.imageContainerNotEmpty,
389
574
  index % 3 === 0 && { marginLeft: 0 },
390
575
  index > 2 && { marginTop: 8 },
391
576
  ]}
@@ -404,7 +589,7 @@ class MaintenanceRequest extends Component {
404
589
  </TouchableOpacity>
405
590
  </View>
406
591
  )}
407
- <TouchableOpacity style={styles.removeImage} onPress={this.removeImage.bind(this, index)}>
592
+ <TouchableOpacity style={styles.removeImage} onPress={() => this.removeImage(index, fieldId)}>
408
593
  <Icon name="remove" type="font-awesome" iconStyle={styles.imageControlIcon} style={styles.removeImage} />
409
594
  </TouchableOpacity>
410
595
  </ImageBackground>
@@ -421,15 +606,16 @@ class MaintenanceRequest extends Component {
421
606
  );
422
607
  }
423
608
 
424
- renderImageList() {
609
+ renderImageList(fieldId = null) {
610
+ const imagesList = this.getImages(fieldId);
425
611
  return (
426
612
  <View style={styles.imageSection}>
427
- <View style={[styles.imageListContainer, this.state.images.length < 2 && styles.imageListContainerEmpty]}>
613
+ <View style={[styles.imageListContainer, imagesList.length < 2 && styles.imageListContainerEmpty]}>
428
614
  <FlatList
429
615
  keyboardShouldPersistTaps="always"
430
616
  enableEmptySections
431
- data={this.state.images}
432
- renderItem={({ item, index }) => this.renderImage(item, index)}
617
+ data={imagesList}
618
+ renderItem={({ item, index }) => this.renderImage(item, index, fieldId)}
433
619
  keyExtractor={(item, index) => index}
434
620
  numColumns={3}
435
621
  />
@@ -438,7 +624,181 @@ class MaintenanceRequest extends Component {
438
624
  );
439
625
  }
440
626
 
627
+ renderDateField(field, fieldId, sectionStyle) {
628
+ let displayText, placeHolder, icon, errorText;
629
+ if (field.type === 'date') {
630
+ displayText = field.answer ? moment(field.answer, 'YYYY-MM-DD').format('DD MMM YYYY') : '';
631
+ placeHolder = 'dd mmm yyyy';
632
+ icon = 'calendar';
633
+ errorText = 'Not a valid date';
634
+ } else {
635
+ displayText = field.answer ? moment(field.answer, 'HH:mm').format('h:mm a') : '';
636
+ placeHolder = '--:-- --';
637
+ icon = 'clock-o';
638
+ errorText = 'Not a valid time';
639
+ }
640
+
641
+ return (
642
+ <Components.GenericInputSection
643
+ key={fieldId}
644
+ label={field.label}
645
+ sectionStyle={sectionStyle}
646
+ isValid={() => this.isFieldValid(field, fieldId)}
647
+ required={field.mandatory}
648
+ showError={this.state.showError}
649
+ errorText={errorText}
650
+ >
651
+ <View style={styles.dateContainer}>
652
+ <TouchableOpacity style={styles.dateFieldButton} onPress={() => this.onOpenDatePicker(field, fieldId)}>
653
+ <View style={styles.dateFieldContainer}>
654
+ <Text style={styles.dateText}>{displayText || placeHolder}</Text>
655
+ <Icon type="font-awesome" name={icon} iconStyle={styles.dateIcon} />
656
+ </View>
657
+ </TouchableOpacity>
658
+ {displayText ? (
659
+ <TouchableOpacity style={styles.dateClearButton} onPress={() => this.onClearDate(fieldId)}>
660
+ <Icon type="font-awesome" name="times" iconStyle={styles.removeIcon} />
661
+ </TouchableOpacity>
662
+ ) : null}
663
+ </View>
664
+ </Components.GenericInputSection>
665
+ );
666
+ }
667
+
668
+ renderField(field, fieldId) {
669
+ const sectionStyle = { marginTop: fieldId === 0 ? 24 : 0, marginBottom: 24 };
670
+ switch (field.type) {
671
+ case 'yn':
672
+ return (
673
+ <Components.GenericInputSection
674
+ key={fieldId}
675
+ label={field.label}
676
+ sectionStyle={sectionStyle}
677
+ inputType="toggle"
678
+ value={field.answer}
679
+ onChange={answer => this.onChangeToggleAnswer(fieldId, answer)}
680
+ isValid={() => this.isFieldValid(field, fieldId)}
681
+ showError={this.state.showError}
682
+ required={field.mandatory}
683
+ />
684
+ );
685
+ case 'multichoice':
686
+ return (
687
+ <Components.GenericInputSection
688
+ key={fieldId}
689
+ label={field.label}
690
+ sectionStyle={sectionStyle}
691
+ inputType="radio"
692
+ value={field.answer}
693
+ onChange={answer => this.onChangeToggleAnswer(fieldId, answer)}
694
+ options={field.values.map(o => {
695
+ return {
696
+ Label: o,
697
+ Value: o,
698
+ };
699
+ })}
700
+ isValid={() => this.isFieldValid(field, fieldId)}
701
+ showError={this.state.showError}
702
+ required={field.mandatory}
703
+ />
704
+ );
705
+ case 'checkbox':
706
+ return (
707
+ <Components.GenericInputSection
708
+ key={fieldId}
709
+ label={field.label}
710
+ sectionStyle={sectionStyle}
711
+ isValid={() => this.isFieldValid(field, fieldId)}
712
+ showError={this.state.showError}
713
+ required={field.mandatory}
714
+ >
715
+ {field.values.map((o, i) => {
716
+ const isActive = field.answer && _.includes(field.answer, o);
717
+ return (
718
+ <TouchableOpacity
719
+ onPress={() => this.onChangeCheckboxAnswer(fieldId, o)}
720
+ key={i}
721
+ style={styles.multiChoiceOption}
722
+ hitSlop={{ top: 8, left: 8, bottom: 8, right: 8 }}
723
+ >
724
+ {isActive ? (
725
+ <Components.TickIcon style={styles.tick} size={20} color={this.props.colourBrandingMain} />
726
+ ) : (
727
+ <View style={styles.unticked} />
728
+ )}
729
+ <Text style={styles.multiChoiceText}>{o}</Text>
730
+ </TouchableOpacity>
731
+ );
732
+ })}
733
+ </Components.GenericInputSection>
734
+ );
735
+ case 'text':
736
+ case 'email':
737
+ case 'phone':
738
+ return (
739
+ <Components.GenericInputSection
740
+ key={fieldId}
741
+ label={field.label}
742
+ placeholder={field.placeHolder}
743
+ value={field.answer}
744
+ onChangeText={val => this.onChangeAnswer(fieldId, val)}
745
+ editable
746
+ squaredCorners
747
+ isValid={() => this.isFieldValid(field, fieldId)}
748
+ showError={this.state.showError}
749
+ errorText={field.type === 'email' ? 'Not a valid email' : undefined}
750
+ required={field.mandatory}
751
+ sectionStyle={sectionStyle}
752
+ autoCapitalize="sentences"
753
+ keyboardType={this.keyboardTypes[field.type]}
754
+ />
755
+ );
756
+ case 'staticTitle':
757
+ return (
758
+ <Text key={fieldId} style={[styles.staticTitle, { color: this.props.colourBrandingMain }, sectionStyle]}>
759
+ {field.label}
760
+ </Text>
761
+ );
762
+ case 'staticText':
763
+ return (
764
+ <View key={fieldId} style={[styles.staticText, sectionStyle]}>
765
+ {Helper.toParagraphed(field.label, styles.staticText)}
766
+ </View>
767
+ );
768
+ case 'date':
769
+ case 'time':
770
+ return this.renderDateField(field, fieldId, sectionStyle);
771
+ case 'image':
772
+ return (
773
+ <Components.GenericInputSection
774
+ key={fieldId}
775
+ label={field.label}
776
+ sectionStyle={sectionStyle}
777
+ isValid={() => this.isFieldValid(field, fieldId)}
778
+ required={field.mandatory}
779
+ showError={this.state.showError}
780
+ >
781
+ {this.renderImageList(fieldId)}
782
+ </Components.GenericInputSection>
783
+ );
784
+ default:
785
+ return null;
786
+ }
787
+ }
788
+
789
+ renderCustomFields() {
790
+ const { customFields } = this.state;
791
+ if (!customFields || customFields.length === 0) return null;
792
+
793
+ return (
794
+ <Components.FormCard style={{ marginTop: 16 }}>{customFields.map((field, i) => this.renderField(field, i))}</Components.FormCard>
795
+ );
796
+ }
797
+
441
798
  renderForm() {
799
+ const { customFields } = this.state;
800
+ const hasCustomFields = customFields && customFields.length > 0;
801
+
442
802
  return (
443
803
  <View style={{ flex: 1 }}>
444
804
  <ScrollView keyboardShouldPersistTaps="always" style={{ flex: 1 }} ref={ref => (this.scrollContainer = ref)}>
@@ -481,7 +841,7 @@ class MaintenanceRequest extends Component {
481
841
  }}
482
842
  required
483
843
  errorText="Please provide your address."
484
- showError={this.state.showError && this.state.roomNumber && this.state.roomNumber.length < 2}
844
+ showError={this.state.showError && (!this.state.roomNumber || this.state.roomNumber.length < 2)}
485
845
  />
486
846
  </Components.FormCard>
487
847
  <Components.FormCard
@@ -499,81 +859,87 @@ class MaintenanceRequest extends Component {
499
859
  </View>
500
860
  </TouchableOpacity>
501
861
  </Components.FormCard>
502
- <Components.FormCard style={{ marginTop: 16 }}>
503
- <Components.FormCardSection
504
- label={'Title'}
505
- placeholder={'Enter a title for your request'}
506
- textValue={this.state.title}
507
- onChangeText={title => this.setState({ title })}
508
- editable={this.state.submitting === false}
509
- hasUnderline
510
- isValid={() => {
511
- return this.state.title.length > 1;
512
- }}
513
- required
514
- errorText="Please provide a title."
515
- showError={this.state.showError && this.state.title.length < 2}
516
- autoCorrect
517
- multiline
518
- autoGrow
519
- />
520
- <Components.FormCardSection
521
- label={'Description'}
522
- placeholder={'Describe your request here in detail'}
523
- textValue={this.state.description}
524
- onChangeText={description => this.setState({ description })}
525
- editable={this.state.submitting === false}
526
- hasUnderline
527
- autoCorrect
528
- multiline
529
- autoGrow
530
- />
531
- </Components.FormCard>
532
- <Components.FormCard style={{ marginTop: 16, paddingHorizontal: 24 }}>
533
- <View
534
- style={[
535
- {
536
- width: '100%',
537
- paddingVertical: 16,
538
- flexDirection: 'row',
539
- justifyContent: 'space-between',
540
- alignItems: 'center',
541
- position: 'relative',
542
- },
543
- this.state.isHome && { borderBottomWidth: 1, borderBottomColor: Colours.LINEGREY },
544
- ]}
545
- >
546
- <Text style={styles.sectionTitle}>{Config.env.strings.MAINTENANCE_HOME}</Text>
547
- <Switch
548
- value={this.state.isHome}
549
- disabled={this.state.submitting}
550
- onValueChange={value => this.setState({ isHome: value })}
551
- trackColor={{ false: '#ddd', true: this.props.colourBrandingMain }}
552
- thumbColor={Platform.OS === 'android' ? '#fff' : null}
553
- />
554
- </View>
555
- {this.state.isHome && (
556
- <Components.FormCardSection
557
- label={'Available times'}
558
- placeholder={'Describe your available times here in detail.'}
559
- textValue={this.state.times}
560
- onChangeText={times => this.setState({ times })}
561
- editable={this.state.submitting === false}
562
- hasUnderline
563
- isValid={() => {
564
- return this.state.times.length > 1;
565
- }}
566
- required
567
- errorText="Please provide available times."
568
- showError={this.state.showError && this.state.isHome && this.state.times.length < 2}
569
- minHeight={40}
570
- autoCorrect
571
- multiline
572
- autoGrow
573
- />
574
- )}
575
- </Components.FormCard>
576
- {this.renderImageList()}
862
+ {hasCustomFields ? (
863
+ this.renderCustomFields()
864
+ ) : (
865
+ <>
866
+ <Components.FormCard style={{ marginTop: 16 }}>
867
+ <Components.FormCardSection
868
+ label={'Title'}
869
+ placeholder={'Enter a title for your request'}
870
+ textValue={this.state.title}
871
+ onChangeText={title => this.setState({ title })}
872
+ editable={this.state.submitting === false}
873
+ hasUnderline
874
+ isValid={() => {
875
+ return this.state.title.length > 1;
876
+ }}
877
+ required
878
+ errorText="Please provide a title."
879
+ showError={this.state.showError && this.state.title.length < 2}
880
+ autoCorrect
881
+ multiline
882
+ autoGrow
883
+ />
884
+ <Components.FormCardSection
885
+ label={'Description'}
886
+ placeholder={'Describe your request here in detail'}
887
+ textValue={this.state.description}
888
+ onChangeText={description => this.setState({ description })}
889
+ editable={this.state.submitting === false}
890
+ hasUnderline
891
+ autoCorrect
892
+ multiline
893
+ autoGrow
894
+ />
895
+ </Components.FormCard>
896
+ <Components.FormCard style={{ marginTop: 16, paddingHorizontal: 24 }}>
897
+ <View
898
+ style={[
899
+ {
900
+ width: '100%',
901
+ paddingVertical: 16,
902
+ flexDirection: 'row',
903
+ justifyContent: 'space-between',
904
+ alignItems: 'center',
905
+ position: 'relative',
906
+ },
907
+ this.state.isHome && { borderBottomWidth: 1, borderBottomColor: Colours.LINEGREY },
908
+ ]}
909
+ >
910
+ <Text style={styles.sectionTitle}>{Config.env.strings.MAINTENANCE_HOME}</Text>
911
+ <Switch
912
+ value={this.state.isHome}
913
+ disabled={this.state.submitting}
914
+ onValueChange={value => this.setState({ isHome: value })}
915
+ trackColor={{ false: '#ddd', true: this.props.colourBrandingMain }}
916
+ thumbColor={Platform.OS === 'android' ? '#fff' : null}
917
+ />
918
+ </View>
919
+ {this.state.isHome && (
920
+ <Components.FormCardSection
921
+ label={'Available times'}
922
+ placeholder={'Describe your available times here in detail.'}
923
+ textValue={this.state.times}
924
+ onChangeText={times => this.setState({ times })}
925
+ editable={this.state.submitting === false}
926
+ hasUnderline
927
+ isValid={() => {
928
+ return this.state.times.length > 1;
929
+ }}
930
+ required
931
+ errorText="Please provide available times."
932
+ showError={this.state.showError && this.state.isHome && this.state.times.length < 2}
933
+ minHeight={40}
934
+ autoCorrect
935
+ multiline
936
+ autoGrow
937
+ />
938
+ )}
939
+ </Components.FormCard>
940
+ {this.renderImageList()}
941
+ </>
942
+ )}
577
943
  </View>
578
944
  </ScrollView>
579
945
  </View>
@@ -607,6 +973,8 @@ class MaintenanceRequest extends Component {
607
973
  }
608
974
 
609
975
  render() {
976
+ const { submitting, success, isDateTimePickerVisible, popUpType } = this.state;
977
+
610
978
  return (
611
979
  <KeyboardAvoidingView behavior={Platform.OS === 'ios' && 'padding'} style={styles.viewContainer}>
612
980
  {this.renderUploadMenu()}
@@ -614,8 +982,8 @@ class MaintenanceRequest extends Component {
614
982
  <Components.Header
615
983
  leftIcon="angle-left"
616
984
  onPressLeft={this.onPressBack.bind(this)}
617
- text={Config.env.strings.MAINTENANCE}
618
- rightText={this.state.submitting || this.state.success ? null : 'Done'}
985
+ text={this.props.strings[`${values.featureKey}_textFeatureTitle`] || values.textFeatureTitle}
986
+ rightText={submitting || success ? null : 'Done'}
619
987
  onPressRight={this.submitRequest.bind(this)}
620
988
  absoluteRight
621
989
  />
@@ -623,6 +991,13 @@ class MaintenanceRequest extends Component {
623
991
  </View>
624
992
  {this.renderRegisterConfirmation()}
625
993
  {this.renderVideoPlayerPopup()}
994
+ <DateTimePicker
995
+ isVisible={isDateTimePickerVisible}
996
+ onConfirm={this.onDateSelected}
997
+ onCancel={() => this.setState({ isDateTimePickerVisible: false })}
998
+ mode={popUpType}
999
+ headerTextIOS={`Pick a ${popUpType}`}
1000
+ />
626
1001
  </KeyboardAvoidingView>
627
1002
  );
628
1003
  }
@@ -725,6 +1100,72 @@ const styles = {
725
1100
  alignItems: 'center',
726
1101
  justifyContent: 'center',
727
1102
  },
1103
+ staticTitle: {
1104
+ fontSize: 20,
1105
+ fontFamily: 'sf-semibold',
1106
+ color: Colours.TEXT_DARKEST,
1107
+ },
1108
+ staticText: {
1109
+ fontSize: 17,
1110
+ fontFamily: 'sf-regular',
1111
+ color: Colours.TEXT_DARKEST,
1112
+ lineHeight: 24,
1113
+ },
1114
+ multiChoiceOption: {
1115
+ marginTop: 16,
1116
+ flexDirection: 'row',
1117
+ alignItems: 'center',
1118
+ minHeight: 20,
1119
+ },
1120
+ multiChoiceText: {
1121
+ flex: 1,
1122
+ fontFamily: 'sf-medium',
1123
+ fontSize: 14,
1124
+ color: Colours.TEXT_DARK,
1125
+ },
1126
+ tick: {
1127
+ marginRight: 10,
1128
+ borderRadius: 4,
1129
+ },
1130
+ unticked: {
1131
+ marginRight: 10,
1132
+ width: 20,
1133
+ height: 20,
1134
+ borderColor: Colours.LINEGREY,
1135
+ borderWidth: 1,
1136
+ borderRadius: 4,
1137
+ },
1138
+ dateContainer: {
1139
+ flexDirection: 'row',
1140
+ alignItems: 'center',
1141
+ },
1142
+ dateFieldButton: {
1143
+ flex: 1,
1144
+ },
1145
+ dateFieldContainer: {
1146
+ flexDirection: 'row',
1147
+ borderRadius: 2,
1148
+ backgroundColor: '#ebeff2',
1149
+ padding: 8,
1150
+ marginTop: 8,
1151
+ },
1152
+ dateText: {
1153
+ flex: 1,
1154
+ fontFamily: 'sf-regular',
1155
+ fontSize: 16,
1156
+ color: '#65686D',
1157
+ },
1158
+ dateIcon: {
1159
+ fontSize: 18,
1160
+ color: Colours.TEXT_BLUEGREY,
1161
+ },
1162
+ dateClearButton: {
1163
+ paddingLeft: 12,
1164
+ },
1165
+ removeIcon: {
1166
+ fontSize: 26,
1167
+ color: Colours.TEXT_BLUEGREY,
1168
+ },
728
1169
  };
729
1170
 
730
1171
  const mapStateToProps = state => {
@@ -740,6 +1181,7 @@ const mapStateToProps = state => {
740
1181
  unit,
741
1182
  phoneNumber,
742
1183
  colourBrandingMain: Colours.getMainBrandingColourFromState(state),
1184
+ strings: state.strings?.config || {},
743
1185
  };
744
1186
  };
745
1187