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