@plusscommunities/pluss-maintenance-app 6.0.7-auth.0 → 6.0.8-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 (61) 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 +5 -4
  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 +8 -5
  12. package/dist/module/components/WidgetSmall.js.map +1 -1
  13. package/dist/module/feature.config.js +21 -36
  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/reducers/JobsReducer.js +2 -1
  18. package/dist/module/reducers/JobsReducer.js.map +1 -1
  19. package/dist/module/screens/MaintenancePage.js +7 -3
  20. package/dist/module/screens/MaintenancePage.js.map +1 -1
  21. package/dist/module/screens/RequestDetail.js +106 -31
  22. package/dist/module/screens/RequestDetail.js.map +1 -1
  23. package/dist/module/screens/RequestNotes.js +2 -1
  24. package/dist/module/screens/RequestNotes.js.map +1 -1
  25. package/dist/module/screens/ServiceRequest.js +581 -86
  26. package/dist/module/screens/ServiceRequest.js.map +1 -1
  27. package/dist/module/values.config.a.js +30 -0
  28. package/dist/module/values.config.a.js.map +1 -0
  29. package/dist/module/values.config.b.js +30 -0
  30. package/dist/module/values.config.b.js.map +1 -0
  31. package/dist/module/values.config.c.js +30 -0
  32. package/dist/module/values.config.c.js.map +1 -0
  33. package/dist/module/values.config.d.js +30 -0
  34. package/dist/module/values.config.d.js.map +1 -0
  35. package/dist/module/values.config.default.js +35 -0
  36. package/dist/module/values.config.default.js.map +1 -0
  37. package/dist/module/values.config.forms.js +35 -0
  38. package/dist/module/values.config.forms.js.map +1 -0
  39. package/dist/module/values.config.js +35 -0
  40. package/dist/module/values.config.js.map +1 -0
  41. package/package.json +9 -6
  42. package/src/actions/types.js +5 -3
  43. package/src/apis/maintenanceActions.js +30 -14
  44. package/src/components/MaintenanceList.js +6 -4
  45. package/src/components/MaintenanceListItem.js +7 -2
  46. package/src/components/MaintenanceWidgetItem.js +2 -1
  47. package/src/components/WidgetSmall.js +8 -5
  48. package/src/feature.config.js +30 -40
  49. package/src/index.js +15 -8
  50. package/src/reducers/JobsReducer.js +2 -1
  51. package/src/screens/MaintenancePage.js +5 -4
  52. package/src/screens/RequestDetail.js +101 -33
  53. package/src/screens/RequestNotes.js +2 -1
  54. package/src/screens/ServiceRequest.js +625 -152
  55. package/src/values.config.a.js +30 -0
  56. package/src/values.config.b.js +30 -0
  57. package/src/values.config.c.js +30 -0
  58. package/src/values.config.d.js +30 -0
  59. package/src/values.config.default.js +35 -0
  60. package/src/values.config.forms.js +35 -0
  61. package/src/values.config.js +35 -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
 
@@ -31,6 +34,7 @@ class MaintenanceRequest extends Component {
31
34
  fail: false,
32
35
  error: null,
33
36
  showError: false,
37
+ loadingTypes: values.forceCustomFields,
34
38
 
35
39
  userName: '',
36
40
  roomNumber: '',
@@ -55,8 +59,20 @@ class MaintenanceRequest extends Component {
55
59
  types: [],
56
60
 
57
61
  confirmationToShow: false,
62
+
63
+ customFields: [],
64
+ customFieldImages: {},
65
+ isDateTimePickerVisible: false,
66
+ popUpType: 'date',
67
+ dateFieldId: null,
68
+ imageFieldId: null,
58
69
  };
59
70
  this.checkThumb = null;
71
+ this.keyboardTypes = {
72
+ phone: 'phone-pad',
73
+ email: 'email-address',
74
+ text: 'default',
75
+ };
60
76
  }
61
77
 
62
78
  componentDidMount() {
@@ -74,9 +90,62 @@ class MaintenanceRequest extends Component {
74
90
  clearInterval(this.checkThumb);
75
91
  }
76
92
 
93
+ onChangeAnswer = (fieldId, answer) => {
94
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
95
+ const field = update.customFields[fieldId];
96
+ field.answer = answer;
97
+ if (field.isTitle) update.title = field.answer;
98
+ this.setState(update);
99
+ };
100
+
101
+ onChangeToggleAnswer = (fieldId, answer) => {
102
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
103
+ const field = update.customFields[fieldId];
104
+ field.answer = field.answer === answer ? undefined : answer;
105
+ if (field.isTitle) update.title = field.answer;
106
+ this.setState(update);
107
+ };
108
+
109
+ onChangeCheckboxAnswer = (fieldId, answer) => {
110
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
111
+ const field = update.customFields[fieldId];
112
+ field.answer = _.xor(field.answer || [], [answer]);
113
+ if (field.isTitle) update.title = field.answer.join(', ');
114
+ this.setState(update);
115
+ };
116
+
117
+ onOpenDatePicker = (field, fieldId) => {
118
+ Keyboard.dismiss();
119
+ this.setState({ dateFieldId: fieldId, popUpType: field.type, isDateTimePickerVisible: true });
120
+ };
121
+
122
+ onClearDate = fieldId => {
123
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
124
+ const field = update.customFields[fieldId];
125
+ field.answer = undefined;
126
+ if (field.isTitle) update.title = field.answer;
127
+ this.setState(update);
128
+ };
129
+
130
+ onDateSelected = date => {
131
+ const { customFields, dateFieldId, popUpType } = this.state;
132
+ const update = { customFields: _.cloneDeep(customFields), isDateTimePickerVisible: false, fieldId: null };
133
+ const field = update.customFields[dateFieldId];
134
+ const dateObj = moment(date);
135
+ if (popUpType === 'date') {
136
+ field.answer = dateObj.format('YYYY-MM-DD');
137
+ if (field.isTitle) update.title = dateObj.format('DD MMM YYYY');
138
+ } else {
139
+ field.answer = dateObj.format('HH:mm');
140
+ if (field.isTitle) update.title = dateObj.format('h:mm a');
141
+ }
142
+ this.setState(update);
143
+ };
144
+
77
145
  onUploadStarted = (uploadUri, imageUri) => {
78
- const images = [...this.state.images];
79
- images.splice(images.length - 1, 0, {
146
+ const { imageFieldId } = this.state;
147
+ const imagesUpdate = this.getImages(imageFieldId);
148
+ imagesUpdate.splice(imagesUpdate.length - 1, 0, {
80
149
  uploading: true,
81
150
  uploadProgress: '0%',
82
151
  uploadUri,
@@ -84,24 +153,27 @@ class MaintenanceRequest extends Component {
84
153
  allowRetry: true,
85
154
  });
86
155
 
87
- this.setState({ images });
156
+ this.setImages(imagesUpdate, imageFieldId);
88
157
  };
89
158
 
90
159
  onUploadProgress = progress => {
91
- const images = [...this.state.images];
92
- images.map(img => {
160
+ const { imageFieldId } = this.state;
161
+ const imagesUpdate = this.getImages(imageFieldId);
162
+ imagesUpdate.map(img => {
93
163
  if (img.uploadUri === progress.uri) {
94
164
  img.uploadProgress = progress.percentage;
95
165
  img.uploading = true;
96
166
  img.allowRetry = true;
97
167
  }
98
168
  });
99
- this.setState({ images });
169
+
170
+ this.setImages(imagesUpdate, imageFieldId);
100
171
  };
101
172
 
102
173
  onUploadSuccess = async (uri, uploadUri) => {
103
- const images = [...this.state.images];
104
- images.map(img => {
174
+ const { imageFieldId } = this.state;
175
+ const imagesUpdate = this.getImages(imageFieldId);
176
+ imagesUpdate.map(img => {
105
177
  if (img.uploadUri === uploadUri && img.uploading) {
106
178
  img.url = uri.replace('/general/', '/general1400/');
107
179
  img.thumbNailExists = false;
@@ -109,12 +181,14 @@ class MaintenanceRequest extends Component {
109
181
  img.allowRetry = true;
110
182
  }
111
183
  });
112
- this.setState({ images }, () => this.waitForThumbnails());
184
+
185
+ this.setImages(imagesUpdate, imageFieldId, () => this.waitForThumbnails());
113
186
  };
114
187
 
115
188
  onUploadFailed = uploadUri => {
116
- const images = [...this.state.images];
117
- images.map(img => {
189
+ const { imageFieldId } = this.state;
190
+ const imagesUpdate = this.getImages(imageFieldId);
191
+ imagesUpdate.map(img => {
118
192
  if (img.uploadUri === uploadUri) {
119
193
  img.uploading = true; // Requried for retry
120
194
  img.uploadProgress = '';
@@ -122,12 +196,13 @@ class MaintenanceRequest extends Component {
122
196
  }
123
197
  });
124
198
 
125
- this.setState({ images });
199
+ this.setImages(imagesUpdate, imageFieldId);
126
200
  };
127
201
 
128
202
  onLibrarySelected = uri => {
129
- const images = [...this.state.images];
130
- images.splice(images.length - 1, 0, {
203
+ const { imageFieldId } = this.state;
204
+ const imagesUpdate = this.getImages(imageFieldId);
205
+ imagesUpdate.splice(imagesUpdate.length - 1, 0, {
131
206
  uploading: false,
132
207
  allowRetry: false,
133
208
  url: Helper.get1400(uri),
@@ -135,7 +210,7 @@ class MaintenanceRequest extends Component {
135
210
  thumbNailUrl: Helper.getThumb300(uri),
136
211
  });
137
212
 
138
- this.setState({ images });
213
+ this.setImages(imagesUpdate, imageFieldId);
139
214
  };
140
215
 
141
216
  onPressBack() {
@@ -143,7 +218,7 @@ class MaintenanceRequest extends Component {
143
218
  }
144
219
 
145
220
  onPressType() {
146
- Services.navigation.navigate('jobTypePicker', {
221
+ Services.navigation.navigate(values.screenJobTypePicker, {
147
222
  currentType: this.state.type,
148
223
  types: this.state.types,
149
224
  onSelectType: this.pickType.bind(this),
@@ -159,52 +234,115 @@ class MaintenanceRequest extends Component {
159
234
  }
160
235
 
161
236
  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
- });
237
+ this.setState(
238
+ {
239
+ confirmationToShow: false,
240
+ title: '',
241
+ description: '',
242
+ times: '',
243
+ isHome: false,
244
+ uploadingImage: false,
245
+ images: [
246
+ {
247
+ add: true,
248
+ },
249
+ ],
250
+ submitting: false,
251
+ success: false,
252
+ fail: false,
253
+ customFields: [],
254
+ customFieldImages: {},
255
+ isDateTimePickerVisible: false,
256
+ popUpType: 'date',
257
+ dateFieldId: null,
258
+ imageFieldId: null,
259
+ },
260
+ () => this.pickType(this.state.type),
261
+ );
178
262
  }
179
263
 
264
+ isFieldValid = (field, fieldId) => {
265
+ const { mandatory, type, answer } = field;
266
+ if (['staticTitle', 'staticText'].includes(type)) return true;
267
+
268
+ const imagesList = type === 'image' ? this.getImageUrls(fieldId) : [];
269
+ const checkMandatory = () => {
270
+ if (!mandatory) return true;
271
+ switch (type) {
272
+ case 'yn':
273
+ return _.isBoolean(answer);
274
+ case 'image':
275
+ return imagesList.length > 0;
276
+ case 'checkbox':
277
+ return _.isArray(answer) && answer.length > 0;
278
+ default:
279
+ return !_.isNil(answer) && !_.isEmpty(answer);
280
+ }
281
+ };
282
+ const checkFormat = () => {
283
+ if (_.isNil(answer) || _.isEmpty(answer)) return true;
284
+ switch (type) {
285
+ case 'email':
286
+ return Helper.isEmail(answer);
287
+ case 'date':
288
+ return moment(answer, 'YYYY-MM-DD', true).isValid();
289
+ case 'time':
290
+ return moment(answer, 'HH:mm', true).isValid();
291
+ default:
292
+ return true;
293
+ }
294
+ };
295
+
296
+ const valid = checkMandatory() && checkFormat();
297
+ return valid;
298
+ };
299
+
180
300
  getJobTypes() {
181
- const self = this;
182
301
  maintenanceActions
183
302
  .getJobTypes(Helper.getSite(this.props.site))
184
303
  .then(res => {
185
- self.setState({
304
+ this.setState({
186
305
  types: res.data,
187
306
  });
188
- self.getDefaultJob();
307
+ console.log(res.data);
308
+ this.getDefaultJob();
189
309
  })
190
310
  .catch(() => {});
191
311
  }
192
312
 
193
- getDefaultJob() {
194
- if (this.state.types.length !== 0 && this.state.jobId == null) {
195
- this.setState({ type: this.state.types[0].typeName });
313
+ pickType(type) {
314
+ const { types } = this.state;
315
+ const selected = types.find(t => t.typeName === type) || {};
316
+ if (values.forceCustomFields && !selected.hasCustomFields) {
317
+ console.log(selected);
318
+ this.setState({
319
+ type,
320
+ customFields: [],
321
+ noType: true,
322
+ });
323
+ return;
196
324
  }
325
+ this.setState({
326
+ type,
327
+ customFields: selected.hasCustomFields && selected.customFields.length > 0 ? _.cloneDeep(selected.customFields) : [],
328
+ loadingTypes: false,
329
+ });
197
330
  }
198
331
 
199
- pickType(type) {
200
- this.setState({ type });
332
+ getDefaultJob() {
333
+ const { types, jobId } = this.state;
334
+ if (types.length !== 0 && jobId == null) {
335
+ const defaultType = types[0];
336
+ this.pickType(defaultType.typeName);
337
+ }
201
338
  }
202
339
 
203
- showUploadMenu = () => {
340
+ showUploadMenu = fieldId => {
204
341
  Keyboard.dismiss();
205
342
  if (this.state.uploadingImage || this.state.submitting) {
206
343
  return;
207
344
  }
345
+ if (fieldId) this.setState({ imageFieldId: fieldId });
208
346
  this.imageUploader.showUploadMenu();
209
347
  };
210
348
 
@@ -221,10 +359,13 @@ class MaintenanceRequest extends Component {
221
359
  this.scrollContainer.scrollTo({ y: 0 });
222
360
  }, 100);
223
361
 
224
- const images = _.filter(this.state.images, img => {
225
- return !img.uploading && !img.add;
226
- }).map(img => {
227
- return img.url;
362
+ const images = this.getImageUrls();
363
+
364
+ // Fix custom images field answers
365
+ const customFields = _.cloneDeep(this.state.customFields);
366
+ const updatedCustomFields = customFields.map((field, fieldId) => {
367
+ if (field.type === 'image') field.answer = this.getImageUrls(fieldId);
368
+ return field;
228
369
  });
229
370
 
230
371
  maintenanceActions
@@ -241,6 +382,7 @@ class MaintenanceRequest extends Component {
241
382
  Helper.getSite(this.props.site),
242
383
  this.state.isHome,
243
384
  this.state.times,
385
+ updatedCustomFields,
244
386
  )
245
387
  .then(res => {
246
388
  if (res.data.success) {
@@ -269,45 +411,98 @@ class MaintenanceRequest extends Component {
269
411
  refreshRequest = async id => {
270
412
  try {
271
413
  const job = await maintenanceActions.getJob(Helper.getSite(this.props.site), id);
272
- // console.log('refreshRequest', job?.data);
273
414
  this.props.jobAdded(job.data);
274
415
  } catch (error) {
275
416
  console.log('refreshRequest error', error);
276
417
  }
277
418
  };
278
419
 
420
+ validateCustomFields = () => {
421
+ const { customFields } = this.state;
422
+ if (!customFields || customFields.length === 0) return true;
423
+
424
+ return customFields.every((field, index) => {
425
+ const isValid = this.isFieldValid(field, index);
426
+ return isValid;
427
+ });
428
+ };
429
+
279
430
  submitRequest() {
280
- if (this.state.submitting || !this.props.connected) {
431
+ const { customFields, submitting, uploadingImage, title, roomNumber, isHome, times } = this.state;
432
+ const hasCustomFields = customFields && customFields.length > 0;
433
+
434
+ if (submitting || !this.props.connected) {
281
435
  if (!this.props.connected) {
282
436
  this.setState({ error: { message: 'No internet connection detected' } });
283
437
  }
284
438
  return;
285
439
  }
286
- if (this.state.uploadingImage) {
287
- return;
288
- }
440
+ if (uploadingImage) return;
441
+
289
442
  this.setState({ error: null, showError: false });
290
- if (this.state.title.length === 0 || !this.state.roomNumber || this.state.roomNumber.length === 0) {
443
+ if (title.length === 0 || !roomNumber || roomNumber.length === 0) {
444
+ console.log('submitRequest - error', { title, roomNumber });
291
445
  this.setState({ showError: true });
292
446
  return;
293
447
  }
294
- if (this.state.isHome && this.state.times.length < 2) {
295
- this.setState({ showError: true });
296
- return;
448
+ if (hasCustomFields) {
449
+ if (!this.validateCustomFields()) {
450
+ console.log('submitRequest - custom fields error');
451
+ this.setState({ showError: true });
452
+ return;
453
+ }
454
+ } else {
455
+ if (isHome && times.length < 2) {
456
+ console.log('submitRequest - error', { isHome, times });
457
+ this.setState({ showError: true });
458
+ return;
459
+ }
297
460
  }
298
461
  this.submit();
299
462
  }
300
463
 
464
+ getImages = (fieldId = null) => {
465
+ const { images, customFieldImages } = this.state;
466
+ const imagesList = _.cloneDeep(fieldId ? customFieldImages[fieldId] : images);
467
+ if (!imagesList || !Array.isArray(imagesList) || imagesList.length === 0) {
468
+ return [{ add: true }];
469
+ }
470
+ return imagesList;
471
+ };
472
+
473
+ setImages = (imagesList, fieldId = null, callback = null) => {
474
+ let update = {};
475
+ if (fieldId) {
476
+ const customFieldImages = _.cloneDeep(this.state.customFieldImages);
477
+ customFieldImages[fieldId] = imagesList;
478
+ update = { customFieldImages };
479
+ } else {
480
+ update = { images: imagesList };
481
+ }
482
+ this.setState(update, callback);
483
+ };
484
+
485
+ getImageUrls = (fieldId = null) => {
486
+ const imagesList = this.getImages(fieldId);
487
+ return _.filter(imagesList, img => {
488
+ return !img.uploading && !img.add;
489
+ }).map(img => {
490
+ return img.url;
491
+ });
492
+ };
493
+
301
494
  waitForThumbnails = () => {
302
495
  if (this.checkThumb) return;
303
496
 
304
497
  this.checkThumb = setInterval(async () => {
305
- const images = [];
498
+ const { imageFieldId } = this.state;
499
+ const imagesList = this.getImages(imageFieldId);
500
+ const imagesUpdate = [];
306
501
  await Promise.all(
307
- this.state.images.map(image => {
502
+ imagesList.map(image => {
308
503
  return new Promise(async resolve => {
309
504
  const newImage = { ...image };
310
- images.push(newImage);
505
+ imagesUpdate.push(newImage);
311
506
  if (newImage.url && !newImage.thumbNailExists) {
312
507
  newImage.uploading = false;
313
508
  newImage.allowRetry = false;
@@ -318,20 +513,20 @@ class MaintenanceRequest extends Component {
318
513
  });
319
514
  }),
320
515
  );
321
- const thumbnailsExist = images.every(image => !image.url || image.thumbNailExists);
516
+ const thumbnailsExist = imagesUpdate.every(image => !image.url || image.thumbNailExists);
322
517
  if (thumbnailsExist) {
323
518
  clearInterval(this.checkThumb);
324
519
  this.checkThumb = null;
325
- this.setState({ images });
520
+ this.setImages(imagesUpdate, imageFieldId);
326
521
  }
327
522
  }, 2000);
328
523
  };
329
524
 
330
- removeImage = index => {
331
- const images = [...this.state.images];
332
- images.splice(index, 1);
525
+ removeImage = (index, fieldId) => {
526
+ const imagesUpdate = this.getImages(fieldId);
527
+ imagesUpdate.splice(index, 1);
333
528
 
334
- this.setState({ images });
529
+ this.setImages(imagesUpdate, fieldId);
335
530
  };
336
531
 
337
532
  toggleFullscreenVideo = url => {
@@ -360,16 +555,17 @@ class MaintenanceRequest extends Component {
360
555
  );
361
556
  }
362
557
 
363
- renderImage(item, index) {
558
+ renderImage(item, index, fieldId = null) {
364
559
  const isVideoUrl = Helper.isVideo(item.url);
560
+ const imagesList = this.getImages(fieldId);
365
561
 
366
562
  if (item.add) {
367
563
  return (
368
- <TouchableOpacity activeOpacity={0.8} onPress={this.showUploadMenu}>
564
+ <TouchableOpacity activeOpacity={0.8} onPress={() => this.showUploadMenu(fieldId)}>
369
565
  <View
370
566
  style={[
371
567
  styles.imageContainer,
372
- this.state.images.length > 1 && styles.imageContainerNotEmpty,
568
+ imagesList.length > 1 && styles.imageContainerNotEmpty,
373
569
  index % 3 === 0 && { marginLeft: 0 },
374
570
  index > 2 && { marginTop: 8 },
375
571
  ]}
@@ -385,7 +581,7 @@ class MaintenanceRequest extends Component {
385
581
  <View
386
582
  style={[
387
583
  styles.imageContainer,
388
- this.state.images.length > 1 && styles.imageContainerNotEmpty,
584
+ imagesList.length > 1 && styles.imageContainerNotEmpty,
389
585
  index % 3 === 0 && { marginLeft: 0 },
390
586
  index > 2 && { marginTop: 8 },
391
587
  ]}
@@ -404,7 +600,7 @@ class MaintenanceRequest extends Component {
404
600
  </TouchableOpacity>
405
601
  </View>
406
602
  )}
407
- <TouchableOpacity style={styles.removeImage} onPress={this.removeImage.bind(this, index)}>
603
+ <TouchableOpacity style={styles.removeImage} onPress={() => this.removeImage(index, fieldId)}>
408
604
  <Icon name="remove" type="font-awesome" iconStyle={styles.imageControlIcon} style={styles.removeImage} />
409
605
  </TouchableOpacity>
410
606
  </ImageBackground>
@@ -421,15 +617,16 @@ class MaintenanceRequest extends Component {
421
617
  );
422
618
  }
423
619
 
424
- renderImageList() {
620
+ renderImageList(fieldId = null) {
621
+ const imagesList = this.getImages(fieldId);
425
622
  return (
426
623
  <View style={styles.imageSection}>
427
- <View style={[styles.imageListContainer, this.state.images.length < 2 && styles.imageListContainerEmpty]}>
624
+ <View style={[styles.imageListContainer, imagesList.length < 2 && styles.imageListContainerEmpty]}>
428
625
  <FlatList
429
626
  keyboardShouldPersistTaps="always"
430
627
  enableEmptySections
431
- data={this.state.images}
432
- renderItem={({ item, index }) => this.renderImage(item, index)}
628
+ data={imagesList}
629
+ renderItem={({ item, index }) => this.renderImage(item, index, fieldId)}
433
630
  keyExtractor={(item, index) => index}
434
631
  numColumns={3}
435
632
  />
@@ -438,7 +635,185 @@ class MaintenanceRequest extends Component {
438
635
  );
439
636
  }
440
637
 
638
+ renderDateField(field, fieldId, sectionStyle) {
639
+ let displayText, placeHolder, icon, errorText;
640
+ if (field.type === 'date') {
641
+ displayText = field.answer ? moment(field.answer, 'YYYY-MM-DD').format('DD MMM YYYY') : '';
642
+ placeHolder = 'dd mmm yyyy';
643
+ icon = 'calendar';
644
+ errorText = 'Not a valid date';
645
+ } else {
646
+ displayText = field.answer ? moment(field.answer, 'HH:mm').format('h:mm a') : '';
647
+ placeHolder = '--:-- --';
648
+ icon = 'clock-o';
649
+ errorText = 'Not a valid time';
650
+ }
651
+
652
+ return (
653
+ <Components.GenericInputSection
654
+ key={fieldId}
655
+ label={field.label}
656
+ sectionStyle={sectionStyle}
657
+ isValid={() => this.isFieldValid(field, fieldId)}
658
+ required={field.mandatory}
659
+ showError={this.state.showError}
660
+ errorText={errorText}
661
+ >
662
+ <View style={styles.dateContainer}>
663
+ <TouchableOpacity style={styles.dateFieldButton} onPress={() => this.onOpenDatePicker(field, fieldId)}>
664
+ <View style={styles.dateFieldContainer}>
665
+ <Text style={styles.dateText}>{displayText || placeHolder}</Text>
666
+ <Icon type="font-awesome" name={icon} iconStyle={styles.dateIcon} />
667
+ </View>
668
+ </TouchableOpacity>
669
+ {displayText ? (
670
+ <TouchableOpacity style={styles.dateClearButton} onPress={() => this.onClearDate(fieldId)}>
671
+ <Icon type="font-awesome" name="times" iconStyle={styles.removeIcon} />
672
+ </TouchableOpacity>
673
+ ) : null}
674
+ </View>
675
+ </Components.GenericInputSection>
676
+ );
677
+ }
678
+
679
+ renderField(field, fieldId) {
680
+ const sectionStyle = { marginTop: fieldId === 0 ? 24 : 0, marginBottom: 24 };
681
+ switch (field.type) {
682
+ case 'yn':
683
+ return (
684
+ <Components.GenericInputSection
685
+ key={fieldId}
686
+ label={field.label}
687
+ sectionStyle={sectionStyle}
688
+ inputType="toggle"
689
+ value={field.answer}
690
+ onChange={answer => this.onChangeToggleAnswer(fieldId, answer)}
691
+ isValid={() => this.isFieldValid(field, fieldId)}
692
+ showError={this.state.showError}
693
+ required={field.mandatory}
694
+ />
695
+ );
696
+ case 'multichoice':
697
+ return (
698
+ <Components.GenericInputSection
699
+ key={fieldId}
700
+ label={field.label}
701
+ sectionStyle={sectionStyle}
702
+ inputType="radio"
703
+ value={field.answer}
704
+ onChange={answer => this.onChangeToggleAnswer(fieldId, answer)}
705
+ options={field.values.map(o => {
706
+ return {
707
+ Label: o,
708
+ Value: o,
709
+ };
710
+ })}
711
+ isValid={() => this.isFieldValid(field, fieldId)}
712
+ showError={this.state.showError}
713
+ required={field.mandatory}
714
+ />
715
+ );
716
+ case 'checkbox':
717
+ return (
718
+ <Components.GenericInputSection
719
+ key={fieldId}
720
+ label={field.label}
721
+ sectionStyle={sectionStyle}
722
+ isValid={() => this.isFieldValid(field, fieldId)}
723
+ showError={this.state.showError}
724
+ required={field.mandatory}
725
+ >
726
+ {field.values.map((o, i) => {
727
+ const isActive = field.answer && _.includes(field.answer, o);
728
+ return (
729
+ <TouchableOpacity
730
+ onPress={() => this.onChangeCheckboxAnswer(fieldId, o)}
731
+ key={i}
732
+ style={styles.multiChoiceOption}
733
+ hitSlop={{ top: 8, left: 8, bottom: 8, right: 8 }}
734
+ >
735
+ {isActive ? (
736
+ <Components.TickIcon style={styles.tick} size={20} color={this.props.colourBrandingMain} />
737
+ ) : (
738
+ <View style={styles.unticked} />
739
+ )}
740
+ <Text style={styles.multiChoiceText}>{o}</Text>
741
+ </TouchableOpacity>
742
+ );
743
+ })}
744
+ </Components.GenericInputSection>
745
+ );
746
+ case 'text':
747
+ case 'email':
748
+ case 'phone':
749
+ return (
750
+ <Components.GenericInputSection
751
+ key={fieldId}
752
+ label={field.label}
753
+ placeholder={field.placeHolder}
754
+ value={field.answer}
755
+ onChangeText={val => this.onChangeAnswer(fieldId, val)}
756
+ editable
757
+ squaredCorners
758
+ isValid={() => this.isFieldValid(field, fieldId)}
759
+ showError={this.state.showError}
760
+ errorText={field.type === 'email' ? 'Not a valid email' : undefined}
761
+ required={field.mandatory}
762
+ sectionStyle={sectionStyle}
763
+ autoCapitalize="sentences"
764
+ keyboardType={this.keyboardTypes[field.type]}
765
+ />
766
+ );
767
+ case 'staticTitle':
768
+ return (
769
+ <Text key={fieldId} style={[styles.staticTitle, { color: this.props.colourBrandingMain }, sectionStyle]}>
770
+ {field.label}
771
+ </Text>
772
+ );
773
+ case 'staticText':
774
+ return (
775
+ <View key={fieldId} style={[styles.staticText, sectionStyle]}>
776
+ {Helper.toParagraphed(field.label, styles.staticText)}
777
+ </View>
778
+ );
779
+ case 'date':
780
+ case 'time':
781
+ return this.renderDateField(field, fieldId, sectionStyle);
782
+ case 'image':
783
+ return (
784
+ <Components.GenericInputSection
785
+ key={fieldId}
786
+ label={field.label}
787
+ sectionStyle={sectionStyle}
788
+ isValid={() => this.isFieldValid(field, fieldId)}
789
+ required={field.mandatory}
790
+ showError={this.state.showError}
791
+ >
792
+ {this.renderImageList(fieldId)}
793
+ </Components.GenericInputSection>
794
+ );
795
+ default:
796
+ return null;
797
+ }
798
+ }
799
+
800
+ renderCustomFields() {
801
+ const { customFields } = this.state;
802
+ if (!customFields || customFields.length === 0) return null;
803
+
804
+ return (
805
+ <Components.FormCard style={{ marginTop: 16 }}>{customFields.map((field, i) => this.renderField(field, i))}</Components.FormCard>
806
+ );
807
+ }
808
+
441
809
  renderForm() {
810
+ const { customFields, loadingTypes } = this.state;
811
+ const hasCustomFields = customFields && customFields.length > 0;
812
+
813
+ if (loadingTypes) {
814
+ return <Components.Spinner />;
815
+ }
816
+
442
817
  return (
443
818
  <View style={{ flex: 1 }}>
444
819
  <ScrollView keyboardShouldPersistTaps="always" style={{ flex: 1 }} ref={ref => (this.scrollContainer = ref)}>
@@ -481,7 +856,7 @@ class MaintenanceRequest extends Component {
481
856
  }}
482
857
  required
483
858
  errorText="Please provide your address."
484
- showError={this.state.showError && this.state.roomNumber && this.state.roomNumber.length < 2}
859
+ showError={this.state.showError && (!this.state.roomNumber || this.state.roomNumber.length < 2)}
485
860
  />
486
861
  </Components.FormCard>
487
862
  <Components.FormCard
@@ -499,81 +874,87 @@ class MaintenanceRequest extends Component {
499
874
  </View>
500
875
  </TouchableOpacity>
501
876
  </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()}
877
+ {hasCustomFields ? (
878
+ this.renderCustomFields()
879
+ ) : (
880
+ <>
881
+ <Components.FormCard style={{ marginTop: 16 }}>
882
+ <Components.FormCardSection
883
+ label={'Title'}
884
+ placeholder={'Enter a title for your request'}
885
+ textValue={this.state.title}
886
+ onChangeText={title => this.setState({ title })}
887
+ editable={this.state.submitting === false}
888
+ hasUnderline
889
+ isValid={() => {
890
+ return this.state.title.length > 1;
891
+ }}
892
+ required
893
+ errorText="Please provide a title."
894
+ showError={this.state.showError && this.state.title.length < 2}
895
+ autoCorrect
896
+ multiline
897
+ autoGrow
898
+ />
899
+ <Components.FormCardSection
900
+ label={'Description'}
901
+ placeholder={'Describe your request here in detail'}
902
+ textValue={this.state.description}
903
+ onChangeText={description => this.setState({ description })}
904
+ editable={this.state.submitting === false}
905
+ hasUnderline
906
+ autoCorrect
907
+ multiline
908
+ autoGrow
909
+ />
910
+ </Components.FormCard>
911
+ <Components.FormCard style={{ marginTop: 16, paddingHorizontal: 24 }}>
912
+ <View
913
+ style={[
914
+ {
915
+ width: '100%',
916
+ paddingVertical: 16,
917
+ flexDirection: 'row',
918
+ justifyContent: 'space-between',
919
+ alignItems: 'center',
920
+ position: 'relative',
921
+ },
922
+ this.state.isHome && { borderBottomWidth: 1, borderBottomColor: Colours.LINEGREY },
923
+ ]}
924
+ >
925
+ <Text style={styles.sectionTitle}>{Config.env.strings.MAINTENANCE_HOME}</Text>
926
+ <Switch
927
+ value={this.state.isHome}
928
+ disabled={this.state.submitting}
929
+ onValueChange={value => this.setState({ isHome: value })}
930
+ trackColor={{ false: '#ddd', true: this.props.colourBrandingMain }}
931
+ thumbColor={Platform.OS === 'android' ? '#fff' : null}
932
+ />
933
+ </View>
934
+ {this.state.isHome && (
935
+ <Components.FormCardSection
936
+ label={'Available times'}
937
+ placeholder={'Describe your available times here in detail.'}
938
+ textValue={this.state.times}
939
+ onChangeText={times => this.setState({ times })}
940
+ editable={this.state.submitting === false}
941
+ hasUnderline
942
+ isValid={() => {
943
+ return this.state.times.length > 1;
944
+ }}
945
+ required
946
+ errorText="Please provide available times."
947
+ showError={this.state.showError && this.state.isHome && this.state.times.length < 2}
948
+ minHeight={40}
949
+ autoCorrect
950
+ multiline
951
+ autoGrow
952
+ />
953
+ )}
954
+ </Components.FormCard>
955
+ {this.renderImageList()}
956
+ </>
957
+ )}
577
958
  </View>
578
959
  </ScrollView>
579
960
  </View>
@@ -583,7 +964,7 @@ class MaintenanceRequest extends Component {
583
964
  renderRegisterConfirmation() {
584
965
  return (
585
966
  <Components.ConfirmationPopup
586
- confirmText={'Request submitted'}
967
+ confirmText={`${values.textEntityName} submitted`}
587
968
  repeatText={'Submit another'}
588
969
  visible={this.state.confirmationToShow}
589
970
  onClose={this.onCloseConfirmationPopup.bind(this)}
@@ -606,7 +987,24 @@ class MaintenanceRequest extends Component {
606
987
  );
607
988
  }
608
989
 
990
+ renderNoType() {
991
+ if (!this.state.noType) {
992
+ return null;
993
+ }
994
+ return (
995
+ <Components.WarningPopup
996
+ confirmText={'No forms are available'}
997
+ infoText={'Check back later for forms.'}
998
+ visible={this.state.noType}
999
+ onClose={this.onPressBack.bind(this)}
1000
+ padHorizontal
1001
+ />
1002
+ );
1003
+ }
1004
+
609
1005
  render() {
1006
+ const { submitting, success, isDateTimePickerVisible, popUpType } = this.state;
1007
+
610
1008
  return (
611
1009
  <KeyboardAvoidingView behavior={Platform.OS === 'ios' && 'padding'} style={styles.viewContainer}>
612
1010
  {this.renderUploadMenu()}
@@ -614,8 +1012,8 @@ class MaintenanceRequest extends Component {
614
1012
  <Components.Header
615
1013
  leftIcon="angle-left"
616
1014
  onPressLeft={this.onPressBack.bind(this)}
617
- text={Config.env.strings.MAINTENANCE}
618
- rightText={this.state.submitting || this.state.success ? null : 'Done'}
1015
+ text={this.props.strings[`${values.featureKey}_textFeatureTitle`] || values.textFeatureTitle}
1016
+ rightText={submitting || success ? null : 'Done'}
619
1017
  onPressRight={this.submitRequest.bind(this)}
620
1018
  absoluteRight
621
1019
  />
@@ -623,6 +1021,14 @@ class MaintenanceRequest extends Component {
623
1021
  </View>
624
1022
  {this.renderRegisterConfirmation()}
625
1023
  {this.renderVideoPlayerPopup()}
1024
+ {this.renderNoType()}
1025
+ <DateTimePicker
1026
+ isVisible={isDateTimePickerVisible}
1027
+ onConfirm={this.onDateSelected}
1028
+ onCancel={() => this.setState({ isDateTimePickerVisible: false })}
1029
+ mode={popUpType}
1030
+ headerTextIOS={`Pick a ${popUpType}`}
1031
+ />
626
1032
  </KeyboardAvoidingView>
627
1033
  );
628
1034
  }
@@ -725,6 +1131,72 @@ const styles = {
725
1131
  alignItems: 'center',
726
1132
  justifyContent: 'center',
727
1133
  },
1134
+ staticTitle: {
1135
+ fontSize: 20,
1136
+ fontFamily: 'sf-semibold',
1137
+ color: Colours.TEXT_DARKEST,
1138
+ },
1139
+ staticText: {
1140
+ fontSize: 17,
1141
+ fontFamily: 'sf-regular',
1142
+ color: Colours.TEXT_DARKEST,
1143
+ lineHeight: 24,
1144
+ },
1145
+ multiChoiceOption: {
1146
+ marginTop: 16,
1147
+ flexDirection: 'row',
1148
+ alignItems: 'center',
1149
+ minHeight: 20,
1150
+ },
1151
+ multiChoiceText: {
1152
+ flex: 1,
1153
+ fontFamily: 'sf-medium',
1154
+ fontSize: 14,
1155
+ color: Colours.TEXT_DARK,
1156
+ },
1157
+ tick: {
1158
+ marginRight: 10,
1159
+ borderRadius: 4,
1160
+ },
1161
+ unticked: {
1162
+ marginRight: 10,
1163
+ width: 20,
1164
+ height: 20,
1165
+ borderColor: Colours.LINEGREY,
1166
+ borderWidth: 1,
1167
+ borderRadius: 4,
1168
+ },
1169
+ dateContainer: {
1170
+ flexDirection: 'row',
1171
+ alignItems: 'center',
1172
+ },
1173
+ dateFieldButton: {
1174
+ flex: 1,
1175
+ },
1176
+ dateFieldContainer: {
1177
+ flexDirection: 'row',
1178
+ borderRadius: 2,
1179
+ backgroundColor: '#ebeff2',
1180
+ padding: 8,
1181
+ marginTop: 8,
1182
+ },
1183
+ dateText: {
1184
+ flex: 1,
1185
+ fontFamily: 'sf-regular',
1186
+ fontSize: 16,
1187
+ color: '#65686D',
1188
+ },
1189
+ dateIcon: {
1190
+ fontSize: 18,
1191
+ color: Colours.TEXT_BLUEGREY,
1192
+ },
1193
+ dateClearButton: {
1194
+ paddingLeft: 12,
1195
+ },
1196
+ removeIcon: {
1197
+ fontSize: 26,
1198
+ color: Colours.TEXT_BLUEGREY,
1199
+ },
728
1200
  };
729
1201
 
730
1202
  const mapStateToProps = state => {
@@ -740,6 +1212,7 @@ const mapStateToProps = state => {
740
1212
  unit,
741
1213
  phoneNumber,
742
1214
  colourBrandingMain: Colours.getMainBrandingColourFromState(state),
1215
+ strings: state.strings?.config || {},
743
1216
  };
744
1217
  };
745
1218