@plusscommunities/pluss-maintenance-web-forms 1.1.27 → 1.1.28-auth.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.
@@ -1,975 +1,1138 @@
1
- import _ from 'lodash';
2
- import moment from 'moment';
3
- import React, { Component } from 'react';
4
- import { DropdownButton, MenuItem } from 'react-bootstrap';
5
- import FontAwesome from 'react-fontawesome';
6
- import { withRouter } from 'react-router';
7
- import { connect } from 'react-redux';
8
- import { jobsLoaded, jobsUpdate } from '../actions';
9
- import { PlussCore } from '../feature.config';
10
- import { maintenanceActions, userActions } from '../apis';
11
- import { values } from '../values.config';
1
+ import _ from "lodash";
2
+ import moment from "moment";
3
+ import React, { Component } from "react";
4
+ import { DropdownButton, MenuItem } from "react-bootstrap";
5
+ import FontAwesome from "react-fontawesome";
6
+ import { withRouter } from "react-router";
7
+ import { connect } from "react-redux";
8
+ import { jobsLoaded, jobsUpdate } from "../actions";
9
+ import { PlussCore } from "../feature.config";
10
+ import { maintenanceActions, userActions } from "../apis";
11
+ import { values } from "../values.config";
12
12
 
13
13
  const { Actions, Components, Helper, Session, Colours, Apis } = PlussCore;
14
14
 
15
15
  class AddJob extends Component {
16
- constructor(props) {
17
- super(props);
18
- this.imageInput = null;
19
- this.customImageInputs = {};
20
- this.customDocumentInputs = {};
21
- this.state = {
22
- jobId: Helper.safeReadParams(this.props, 'jobId') ? this.props.match.params.jobId : null,
23
- job: null,
24
- showingSelector: false,
25
- updating: false,
26
- connected: false,
27
- types: [],
28
- users: [],
29
- images: [],
30
- userSearch: '',
31
- userFilterOpen: false,
32
- selectedUser: null,
33
- id: null,
34
- userID: '',
35
- userName: '',
36
- room: '',
37
- phone: '',
38
- location: this.props.auth.site,
39
- title: '',
40
- description: '',
41
- isHome: false,
42
- homeText: '',
43
- prevType: 'General',
44
- type: 'General',
45
- image: null,
46
- thumbnail: null,
47
- showWarnings: false,
48
- success: false,
49
- prevCustomFileds: [],
50
- customFields: [],
51
- showDate: {},
52
- };
53
- }
54
-
55
- UNSAFE_componentWillMount() {
56
- Session.checkLoggedIn(this, this.props.auth);
57
- }
58
-
59
- componentDidMount() {
60
- this.getJobTypes();
61
- this.getUsers();
62
- if (this.state.jobId) this.getJob();
63
- this.props.addRecentlyCreated(values.featureKey);
64
- }
65
-
66
- getJob = async () => {
67
- try {
68
- const res = await maintenanceActions.getJob(this.props.auth.site, this.state.jobId);
69
- res.data.location = res.data.site;
70
- const { userID, userName, userProfilePic, type, customFields } = res.data;
71
- this.setState({
72
- ...res.data,
73
- prevType: type,
74
- prevCustomFileds: customFields,
75
- type,
76
- customFields,
77
- selectedUser: {
78
- userId: userID,
79
- displayName: userName,
80
- profilePic: userProfilePic,
81
- },
82
- });
83
- this.checkSetImages(this.imageInput, res.data.images);
84
- if (customFields) {
85
- customFields.forEach((field, index) => {
86
- if (field.type === 'image' && field.answer) {
87
- this.checkSetImages(this.customImageInputs[index], field.answer);
88
- }
89
- });
90
- }
91
- } catch (error) {
92
- console.error('getJob', error);
93
- }
94
- };
95
-
96
- checkSetImages(imageRef, images) {
97
- if (imageRef) {
98
- if (!_.isEmpty(images)) {
99
- imageRef.getWrappedInstance().setValue(images);
100
- }
101
- } else {
102
- setTimeout(() => {
103
- this.checkSetImages(images);
104
- }, 100);
105
- }
106
- }
107
-
108
- getJobTypes = async () => {
109
- try {
110
- const res = await maintenanceActions.getJobTypes(this.props.auth.site);
111
- this.setState({ types: res.data });
112
- this.getDefaultJob();
113
- } catch (error) {
114
- console.error('getJobTypes', error);
115
- }
116
- };
117
-
118
- getUsers = async () => {
119
- try {
120
- const res = await userActions.fetchUsers(this.props.auth.site);
121
- if (res.userFetchFail) return;
122
- if (res.data != null && !_.isEmpty(res.data.results.Items)) {
123
- const items = res.data.results.Items;
124
- this.setState({
125
- users: _.sortBy(items, (u) => {
126
- return (u.displayName || '').toLowerCase();
127
- }),
128
- });
129
- }
130
- } catch (error) {
131
- console.error('getUsers', error);
132
- }
133
- };
134
-
135
- getDefaultJob = () => {
136
- const { types, jobId } = this.state;
137
- if (jobId == null) {
138
- if (types.length !== 0) {
139
- const defaultType = types[0];
140
- this.setState({
141
- type: defaultType.typeName,
142
- customFields: defaultType.hasCustomFields && defaultType.customFields.length > 0 ? defaultType.customFields : [],
143
- });
144
- } else if (values.forceCustomFields) {
145
- this.setState({ noTypes: true });
146
- } else {
147
- this.setState({ type: 'General' });
148
- }
149
- }
150
- };
151
-
152
- onSelectType = (key, e) => {
153
- const { types, prevType, prevCustomFileds } = this.state;
154
- const selectedType = types.find((t) => t.typeName === key);
155
- // If selected type had previously saved custom fields, use the previous version
156
- const hasPrevCustomFields = prevType === selectedType.typeName && prevCustomFileds && prevCustomFileds.length > 0;
157
- const update = {
158
- type: selectedType.typeName,
159
- customFields: hasPrevCustomFields ? prevCustomFileds : selectedType.hasCustomFields ? selectedType.customFields : [],
160
- };
161
-
162
- if (!_.isEmpty(update.customFields) && !_.some(update.customFields, 'isTitle')) {
163
- update.title = this.state.selectedUser ? this.state.selectedUser.displayName : '';
164
- }
165
-
166
- this.setState(update);
167
- };
168
-
169
- renderTypeOptions() {
170
- const { types, type } = this.state;
171
- return types.map((ev) => {
172
- if (ev != null) {
173
- return (
174
- <MenuItem key={ev.typeName} eventKey={ev.typeName} active={type === ev.typeName}>
175
- {ev.typeName}
176
- </MenuItem>
177
- );
178
- }
179
- return null;
180
- });
181
- }
182
-
183
- onHandleChange = (event) => {
184
- var stateChange = {};
185
- stateChange[event.target.getAttribute('id')] = event.target.value;
186
- this.setState(stateChange);
187
- };
188
-
189
- onOpenUserSelector = () => {
190
- this.setState({ userFilterOpen: true });
191
- };
192
-
193
- onCloseUserSelector = () => {
194
- this.setState({ userFilterOpen: false });
195
- };
196
-
197
- onSelectUser = (user) => {
198
- const update = { selectedUser: user, userID: user.userId, userName: user.displayName, userFilterOpen: false };
199
- if (!_.isEmpty(this.state.customFields) && !_.some(this.state.customFields, 'isTitle')) {
200
- update.title = user.displayName;
201
- }
202
-
203
- // Update UI immediately (non-blocking)
204
- this.setState(update);
205
-
206
- // PC-1255: Auto-populate contact details when staff create requests on behalf of residents
207
- // Requires userManagement permission - gracefully falls back to manual entry if denied
208
- // Fetch in background to avoid blocking UI
209
- userActions
210
- .fetchUser(this.props.auth.site, user.userId)
211
- .then((response) => {
212
- if (response.data && response.data.user) {
213
- const contactUpdate = {};
214
- // Auto-populate phone and room from user profile
215
- if (response.data.user.phoneNumber) {
216
- contactUpdate.phone = response.data.user.phoneNumber;
217
- }
218
- if (response.data.user.unit) {
219
- contactUpdate.room = response.data.user.unit;
220
- }
221
- // Update contact fields when data arrives
222
- if (Object.keys(contactUpdate).length > 0) {
223
- this.setState(contactUpdate);
224
- }
225
- }
226
- })
227
- .catch((error) => {
228
- // Permission denied (403) or other error - continue without auto-population
229
- // Staff can still create the request, just need to enter contact details manually
230
- console.log('Could not fetch user details for auto-population:', error);
231
- });
232
- };
233
-
234
- onUnselectUser = () => {
235
- const update = { selectedUser: null, userID: '', userName: '', phone: '', room: '' };
236
- if (!_.isEmpty(this.state.customFields) && !_.some(this.state.customFields, 'isTitle')) {
237
- update.title = '';
238
- }
239
- this.setState(update);
240
- };
241
-
242
- onChangeAnswer = (qId, answer) => {
243
- const update = { customFields: _.cloneDeep(this.state.customFields) };
244
- const field = update.customFields[qId];
245
- field.answer = answer;
246
- if (field.isTitle) update.title = field.answer;
247
- this.setState(update);
248
- };
249
-
250
- onChangeToggleAnswer = (qId, answer) => {
251
- const update = { customFields: _.cloneDeep(this.state.customFields) };
252
- const field = update.customFields[qId];
253
- field.answer = field.answer === answer ? undefined : answer;
254
- if (field.isTitle) update.title = field.answer;
255
- this.setState(update);
256
- };
257
-
258
- onChangeCheckboxAnswer = (qId, answer) => {
259
- const update = { customFields: _.cloneDeep(this.state.customFields) };
260
- const field = update.customFields[qId];
261
- field.answer = _.xor(field.answer || [], [answer]);
262
- if (field.isTitle) update.title = field.answer.join(', ');
263
- this.setState(update);
264
- };
265
-
266
- onChangeDateAnswer = (qId, answer, togglePicker = true) => {
267
- const update = { customFields: _.cloneDeep(this.state.customFields) };
268
- const field = update.customFields[qId];
269
- field.answer = answer;
270
- if (field.isTitle) update.title = moment(field.answer, 'YYYY-MM-DD').format('DD-MMM-YYYY');
271
- this.setState(update);
272
-
273
- if (togglePicker) this.onToggleDatePicker(qId);
274
- };
275
-
276
- onChangeTimeAnswer = (qId, answer) => {
277
- const update = { customFields: _.cloneDeep(this.state.customFields) };
278
- const field = update.customFields[qId];
279
- field.answer = answer;
280
- if (field.isTitle) update.title = moment(field.answer, 'HH:mm').format('h:mm a');
281
- this.setState(update);
282
- };
283
-
284
- onChangeImageAnswer = (qId, answer) => {
285
- const update = { customFields: _.cloneDeep(this.state.customFields) };
286
- const field = update.customFields[qId];
287
- field.answer = answer;
288
- this.setState(update);
289
- };
290
-
291
- onRemoveDocumentAnswer = (qId, document) => {
292
- const update = { customFields: _.cloneDeep(this.state.customFields) };
293
- const field = update.customFields[qId];
294
- field.answer = _.filter(field.answer, (d) => d.url !== document.url);
295
- this.setState(update);
296
- };
297
-
298
- onHandlePDFFileChange = (event, qId) => {
299
- const file = event.target.files[0];
300
- if (!file) return;
301
-
302
- const update = { customFields: _.cloneDeep(this.state.customFields) };
303
- const field = update.customFields[qId];
304
- const attachments = field.answer || [];
305
- const [name, ext] = file.name.split('.');
306
- const newAttachment = {
307
- uploading: true,
308
- name,
309
- ext: ext.toLowerCase(),
310
- };
311
- attachments.push(newAttachment);
312
- field.answer = attachments;
313
- this.setState(update);
314
-
315
- Apis.fileActions
316
- .uploadMediaAsync(file, file.name)
317
- .then((fileRes) => {
318
- newAttachment.url = fileRes;
319
- delete newAttachment.uploading;
320
- this.setState(update);
321
- })
322
- .catch((uploadErrorRes) => {
323
- console.log(uploadErrorRes);
324
- delete newAttachment.uploading;
325
- this.setState(update);
326
- });
327
- event.target.value = '';
328
- };
329
-
330
- onToggleDatePicker = (qId) => {
331
- const showDate = { ...this.state.showDate };
332
- showDate[qId] = !showDate[qId];
333
- this.setState({ showDate });
334
- };
335
-
336
- onSave = () => {
337
- this.setState({ showWarnings: false });
338
- if (!this.validateForm()) {
339
- this.setState({ showWarnings: true });
340
- return;
341
- }
342
- if (this.state.updating) return;
343
- this.setState({ updating: true });
344
-
345
- const job = {
346
- id: this.state.id,
347
- userID: this.state.userID,
348
- userName: this.state.userName,
349
- room: this.state.room,
350
- phone: this.state.phone,
351
- location: this.state.location,
352
- title: this.state.title,
353
- description: this.state.description,
354
- isHome: this.state.isHome,
355
- homeText: this.state.homeText,
356
- type: this.state.type,
357
- date: null,
358
- images: this.state.images,
359
- customFields: this.state.customFields,
360
- };
361
-
362
- if (this.state.id != null) {
363
- maintenanceActions
364
- .editJob(job, this.props.auth.site)
365
- .then((res) => {
366
- this.setState({
367
- success: true,
368
- updating: false,
369
- });
370
- this.props.jobsLoaded([job]);
371
- })
372
- .catch((res) => {
373
- this.setState({ updating: false });
374
- alert('Something went wrong with the request. Please try again.');
375
- });
376
- } else {
377
- // Create New Job
378
- maintenanceActions
379
- .createJob(job)
380
- .then((res) => {
381
- this.setState({
382
- success: true,
383
- updating: false,
384
- });
385
- this.props.jobsUpdate(this.props.auth.site);
386
- })
387
- .catch((res) => {
388
- this.setState({ updating: false });
389
- alert('Something went wrong with the request. Please try again.');
390
- });
391
- }
392
- };
393
-
394
- renderSuccess() {
395
- if (!this.state.success) return null;
396
-
397
- const title = this.props.strings[`${values.featureKey}_textTitleRequests`] || values.textTitleRequests;
398
- return (
399
- <Components.SuccessPopup
400
- text={`${values.textEntityName} has been ${this.state.id != null ? 'edited' : 'added'}`}
401
- buttons={[
402
- {
403
- type: 'outlined',
404
- onClick: () => {
405
- window.history.back();
406
- },
407
- text: `Back to ${title}`,
408
- },
409
- ]}
410
- />
411
- );
412
- }
413
-
414
- isFieldValid = (field) => {
415
- const { mandatory, type, answer } = field;
416
- if (['staticTitle', 'staticText'].includes(type)) return true;
417
-
418
- const checkMandatory = () => {
419
- if (!mandatory) return true;
420
- switch (type) {
421
- case 'yn':
422
- return _.isBoolean(answer);
423
- case 'image':
424
- case 'document':
425
- case 'checkbox':
426
- return _.isArray(answer) && answer.length > 0;
427
- default:
428
- return !_.isNil(answer) && !_.isEmpty(answer);
429
- }
430
- };
431
- const checkFormat = () => {
432
- if (_.isNil(answer) || _.isEmpty(answer)) return true;
433
- switch (type) {
434
- case 'email':
435
- return Helper.isEmail(answer);
436
- case 'date':
437
- return moment(answer, 'YYYY-MM-DD', true).isValid();
438
- case 'time':
439
- return moment(answer, 'HH:mm', true).isValid();
440
- default:
441
- return true;
442
- }
443
- };
444
-
445
- const valid = checkMandatory() && checkFormat();
446
- return valid;
447
- };
448
-
449
- validateCustomFields() {
450
- const { customFields } = this.state;
451
- if (!customFields || customFields.length === 0) return true;
452
-
453
- return customFields.every((field) => {
454
- const isValid = this.isFieldValid(field);
455
- return isValid;
456
- });
457
- }
458
-
459
- validateForm() {
460
- if (_.isEmpty(this.state.userID)) return false;
461
- if (_.isEmpty(this.state.userName)) return false;
462
- if (_.isEmpty(this.state.room)) return false;
463
- if (_.isEmpty(this.state.title)) return false;
464
- if (this.state.isHome && _.isEmpty(this.state.homeText)) return false;
465
- if (!this.validateCustomFields()) return false;
466
- return true;
467
- }
468
-
469
- getFieldContainerClass = (isValid = true) => {
470
- const showError = this.state.showWarnings && !isValid;
471
- return `genericInputContainer ${isValid ? 'genericInput-valid' : ''} ${showError ? 'genericInput-error' : ''}`.trim();
472
- };
473
-
474
- renderSubmit() {
475
- if (this.state.updating) {
476
- return <Components.Button buttonType="secondary">Saving...</Components.Button>;
477
- }
478
-
479
- return (
480
- <div>
481
- <Components.Button
482
- inline
483
- buttonType="tertiary"
484
- onClick={() => this.props.history.push(values.routeRequestsHub)}
485
- isActive
486
- style={{ marginRight: 16 }}
487
- >
488
- Cancel
489
- </Components.Button>
490
- <Components.Button inline buttonType="primary" onClick={this.onSave} isActive={this.validateForm()}>
491
- Save
492
- </Components.Button>
493
- </div>
494
- );
495
- }
496
-
497
- renderSelectUser() {
498
- const { showWarnings, selectedUser } = this.state;
499
- const isValid = !_.isNil(selectedUser);
500
- return (
501
- <div className={this.getFieldContainerClass(isValid)}>
502
- <div style={styles.userLabelContainer}>
503
- <div className="fieldLabel">User</div>
504
- {showWarnings && !isValid ? <div className="fieldLabel fieldLabel-warning">Required</div> : null}
505
- </div>
506
- <div style={styles.fieldContainer}>
507
- <div className="inputRequired " />
508
- {selectedUser ? (
509
- <Components.Tag className="marginRight-10" rightIcon="close" rightClick={this.onUnselectUser}>
510
- <Components.UserListing size={15} user={selectedUser} textClass="tag_text" />
511
- </Components.Tag>
512
- ) : (
513
- <Components.Tag onClick={this.onOpenUserSelector} text="Select User" />
514
- )}
515
- </div>
516
- </div>
517
- );
518
- }
519
-
520
- renderDefaultFields() {
521
- return (
522
- <div>
523
- <Components.GenericInput
524
- id="title"
525
- label="Title for the work required"
526
- type="textarea"
527
- placeholder="Title for the work required"
528
- value={this.state.title}
529
- onChange={(e) => this.onHandleChange(e)}
530
- inputStyle={{
531
- height: 80,
532
- }}
533
- isRequired
534
- isValid={() => {
535
- return !_.isEmpty(this.state.title);
536
- }}
537
- showError={() => {
538
- return this.state.showWarnings && _.isEmpty(this.state.title);
539
- }}
540
- alwaysShowLabel
541
- />
542
- <Components.GenericInput
543
- id="description"
544
- label="Description of work required"
545
- type="textarea"
546
- placeholder="Description of work required"
547
- value={this.state.description}
548
- onChange={(e) => this.onHandleChange(e)}
549
- inputStyle={{
550
- height: 80,
551
- }}
552
- alwaysShowLabel
553
- />
554
- <div className="marginBottom-16">
555
- <Components.Text type="formLabel" className="marginBottom-4">
556
- Images
557
- </Components.Text>
558
- <Components.ImageInput
559
- ref={(ref) => {
560
- this.imageInput = ref;
561
- }}
562
- multiple
563
- refreshCallback={(images) => {
564
- this.setState({ images });
565
- }}
566
- />
567
- </div>
568
- <Components.RadioButton
569
- label="Person must be home during work?"
570
- isActive={this.state.isHome}
571
- options={[
572
- {
573
- Label: 'No',
574
- Value: false,
575
- onChange: () => this.setState({ isHome: false }),
576
- },
577
- {
578
- Label: 'Yes',
579
- Value: true,
580
- onChange: () => this.setState({ isHome: true }),
581
- },
582
- ]}
583
- />
584
- {this.state.isHome && (
585
- <Components.GenericInput
586
- style={{ marginTop: 16 }}
587
- label="Description of person's available times"
588
- id="homeText"
589
- type="textarea"
590
- placeholder="Description of person's available times"
591
- value={this.state.homeText}
592
- onChange={(e) => this.onHandleChange(e)}
593
- inputStyle={{
594
- height: 80,
595
- }}
596
- isRequired
597
- isValid={() => {
598
- return !_.isEmpty(this.state.homeText);
599
- }}
600
- showError={() => {
601
- return this.state.showWarnings && _.isEmpty(this.state.homeText);
602
- }}
603
- alwaysShowLabel
604
- />
605
- )}
606
- </div>
607
- );
608
- }
609
-
610
- renderField(field, fieldId) {
611
- switch (field.type) {
612
- case 'yn':
613
- return (
614
- <div
615
- key={fieldId}
616
- className={`visitorSignIn_question ${this.getFieldContainerClass(this.isFieldValid(field))}`}
617
- style={styles.fieldContainer}
618
- >
619
- {field.mandatory ? <div className="inputRequired " /> : null}
620
- <Components.RadioButton
621
- label={field.label}
622
- labelStyle={{ color: this.props.colour }}
623
- isActive={field.answer}
624
- noHoverHighlight
625
- highlightColour={this.props.colour}
626
- options={[
627
- { Label: 'Yes', Value: true, onChange: this.onChangeToggleAnswer.bind(this, fieldId, true) },
628
- { Label: 'No', Value: false, onChange: this.onChangeToggleAnswer.bind(this, fieldId, false) },
629
- ]}
630
- />
631
- </div>
632
- );
633
- case 'multichoice':
634
- return (
635
- <div
636
- key={fieldId}
637
- className={`visitorSignIn_question ${this.getFieldContainerClass(this.isFieldValid(field))}`}
638
- style={styles.fieldContainer}
639
- >
640
- {field.mandatory ? <div className="inputRequired " /> : null}
641
- <Components.RadioButton
642
- label={field.label}
643
- labelStyle={{ color: this.props.colour }}
644
- isActive={field.answer}
645
- noHoverHighlight
646
- highlightColour={this.props.colour}
647
- options={field.values.map((o) => {
648
- return { Label: o, Value: o, onChange: this.onChangeToggleAnswer.bind(this, fieldId, o) };
649
- })}
650
- rowStyle={{ flexDirection: 'column' }}
651
- buttonStyle={{ marginTop: '5px', marginBottom: '5px' }}
652
- />
653
- </div>
654
- );
655
- case 'checkbox':
656
- return (
657
- <div key={fieldId} className={this.getFieldContainerClass(this.isFieldValid(field))} style={styles.fieldContainer}>
658
- {field.mandatory ? <div className="inputRequired " /> : null}
659
- <div className="visitorSignIn_question" style={{ flex: 1 }}>
660
- <div className="fieldLabel" style={{ marginBottom: '5px', color: this.props.colour }}>
661
- {field.label}
662
- </div>
663
- {field.values.map((option, optionIndex) => {
664
- return (
665
- <Components.CheckBox
666
- key={optionIndex}
667
- label={option}
668
- isActive={field.answer && field.answer.includes(option)}
669
- highlightColour={this.props.colour}
670
- noHoverHighlight
671
- onChange={() => this.onChangeCheckboxAnswer(fieldId, option)}
672
- />
673
- );
674
- })}
675
- </div>
676
- </div>
677
- );
678
- case 'text':
679
- case 'email':
680
- case 'phone':
681
- return (
682
- <div key={fieldId}>
683
- <Components.GenericInput
684
- id={fieldId}
685
- type="text"
686
- label={field.label}
687
- placeholder={field.placeHolder}
688
- value={field.answer}
689
- onChange={(e) => this.onChangeAnswer(fieldId, e.target.value)}
690
- isRequired={field.mandatory}
691
- isValid={() => this.isFieldValid(field)}
692
- showError={() => this.state.showWarnings && !this.isFieldValid(field)}
693
- errorMessage={field.type === 'email' ? 'Not a valid email' : undefined}
694
- alwaysShowLabel
695
- />
696
- </div>
697
- );
698
- case 'staticTitle':
699
- return (
700
- <p className="visitorSignIn_text-staticTitle" style={{ color: this.props.colour }} key={fieldId}>
701
- {field.label}
702
- </p>
703
- );
704
- case 'staticText':
705
- return (
706
- <p className="visitorSignIn_text-staticText" key={fieldId}>
707
- {Helper.toParagraphed(field.label, { marginTop: 10 })}
708
- </p>
709
- );
710
- case 'date':
711
- return (
712
- <div key={fieldId}>
713
- <Components.GenericInput
714
- id={fieldId}
715
- label={field.label}
716
- placeholder={'DD-MMM-YYYY'}
717
- value={field.answer ? moment(field.answer, 'YYYY-MM-DD').format('DD-MMM-YYYY') : ''}
718
- onClick={(e) => this.onToggleDatePicker(fieldId)}
719
- isRequired={field.mandatory}
720
- isValid={() => this.isFieldValid(field)}
721
- showError={() => this.state.showWarnings && !this.isFieldValid(field)}
722
- errorMessage="Not a valid date"
723
- alwaysShowLabel
724
- readOnly
725
- rightContent={
726
- !_.isEmpty(field.answer) && (
727
- <Components.SVGIcon
728
- colour={Colours.COLOUR_DUSK_LIGHT}
729
- icon="close"
730
- className="timepicker_clear"
731
- onClick={() => this.onChangeDateAnswer(fieldId, undefined, false)}
732
- />
733
- )
734
- }
735
- />
736
- {this.state.showDate[fieldId] && (
737
- <Components.DatePicker selectedDate={field.answer} selectDate={(date) => this.onChangeDateAnswer(fieldId, date)} />
738
- )}
739
- </div>
740
- );
741
- case 'time':
742
- return (
743
- <div key={fieldId}>
744
- <Components.GenericInput
745
- id={fieldId}
746
- label={field.label}
747
- placeholder={'--:-- --'}
748
- value={field.answer ? moment(field.answer, 'HH:mm').format('h:mm a') : ''}
749
- type="time"
750
- isRequired={field.mandatory}
751
- isValid={() => this.isFieldValid(field)}
752
- showError={() => this.state.showWarnings && !this.isFieldValid(field)}
753
- errorMessage="Not a valid time"
754
- alwaysShowLabel
755
- inputComponent={
756
- <Components.TimePicker
757
- selectedTime={field.answer}
758
- selectTime={(time) => this.onChangeTimeAnswer(fieldId, time)}
759
- className="timepicker-condensed"
760
- callbackFormat="HH:mm"
761
- style={{ width: '100%' }}
762
- />
763
- }
764
- rightContent={
765
- !_.isEmpty(field.answer) && (
766
- <Components.SVGIcon
767
- colour={Colours.COLOUR_DUSK_LIGHT}
768
- icon="close"
769
- className="timepicker_clear"
770
- onClick={() => this.onChangeTimeAnswer(fieldId, undefined)}
771
- />
772
- )
773
- }
774
- />
775
- </div>
776
- );
777
- case 'image':
778
- return (
779
- <div key={fieldId} className={this.getFieldContainerClass(this.isFieldValid(field))} style={styles.fieldContainer}>
780
- {field.mandatory ? <div className="inputRequired " /> : null}
781
- <div className="visitorSignIn_question" style={{ flex: 1 }}>
782
- <Components.Text type="formLabel" className="marginBottom-4">
783
- {field.label}
784
- </Components.Text>
785
- <Components.ImageInput
786
- ref={(ref) => (this.customImageInputs[fieldId] = ref)}
787
- multiple
788
- refreshCallback={(images) => this.onChangeImageAnswer(fieldId, images)}
789
- />
790
- </div>
791
- </div>
792
- );
793
- case 'document':
794
- const documents = field.answer || [];
795
- return (
796
- <div key={fieldId} className={this.getFieldContainerClass(this.isFieldValid(field))} style={styles.fieldContainer}>
797
- {field.mandatory ? <div className="inputRequired " /> : null}
798
- <div className="visitorSignIn_question" style={{ flex: 1 }}>
799
- <Components.Text type="formLabel" className="marginBottom-4">
800
- {field.label}
801
- </Components.Text>
802
- {documents.map((doc, index) => (
803
- <Components.Attachment
804
- key={index}
805
- uploading={doc.uploading}
806
- source={doc.url}
807
- title={doc.name}
808
- onRemove={() => this.onRemoveDocumentAnswer(fieldId, doc)}
809
- />
810
- ))}
811
- <input
812
- ref={(input) => (this.customDocumentInputs[fieldId] = input)}
813
- id={`documentInput-${fieldId}`}
814
- type="file"
815
- className="fileInput"
816
- onChange={(e) => this.onHandlePDFFileChange(e, fieldId)}
817
- accept="application/pdf"
818
- />
819
- <div className="iconTextButton marginBottom-16" onClick={() => this.customDocumentInputs[fieldId].click()}>
820
- <FontAwesome className="iconTextButton_icon" name="paperclip" />
821
- <p className="iconTextButton_text">Add Attachment</p>
822
- </div>
823
- </div>
824
- </div>
825
- );
826
- default:
827
- return null;
828
- }
829
- }
830
-
831
- renderCustomFields() {
832
- const { customFields } = this.state;
833
- if (!customFields || customFields.length === 0) return null;
834
-
835
- return (
836
- <div>
837
- {customFields.map((field, i) => {
838
- return this.renderField(field, i);
839
- })}
840
- </div>
841
- );
842
- }
843
-
844
- renderMain() {
845
- const { customFields } = this.state;
846
-
847
- return (
848
- <div style={{ marginBottom: 15 }}>
849
- <div className="padding-60 paddingVertical-40 bottomDivideBorder">
850
- <Components.Text type="formTitleLarge" className="marginBottom-24">
851
- {this.state.infoId == null ? 'New' : 'Edit'} {values.textSingularName}
852
- </Components.Text>
853
- {/* Resident Information */}
854
- {this.renderSelectUser()}
855
- <Components.GenericInput
856
- id="phone"
857
- type="text"
858
- label="Contact number"
859
- placeholder="04XX XXX XXX"
860
- value={this.state.phone}
861
- // showError={this.state.showWarnings && !this.validateImage()}
862
- onChange={(e) => this.onHandleChange(e)}
863
- alwaysShowLabel
864
- />
865
- <Components.GenericInput
866
- id="room"
867
- type="text"
868
- label="Address"
869
- placeholder="Insert address here"
870
- value={this.state.room}
871
- onChange={(e) => this.onHandleChange(e)}
872
- isRequired
873
- alwaysShowLabel
874
- isValid={() => {
875
- return !_.isEmpty(this.state.room);
876
- }}
877
- showError={() => {
878
- return this.state.showWarnings && _.isEmpty(this.state.room);
879
- }}
880
- />
881
- <div style={{ marginBottom: 15 }}>
882
- <Components.Text type="formLabel">{values.textJobType}</Components.Text>
883
- <DropdownButton style={{ minWidth: 80 }} bsStyle="default" title={this.state.type} id="typeSelect" onSelect={this.onSelectType}>
884
- {this.renderTypeOptions()}
885
- </DropdownButton>
886
- </div>
887
- {!_.isEmpty(customFields) || values.forceCustomFields ? this.renderCustomFields() : this.renderDefaultFields()}
888
- </div>
889
- </div>
890
- );
891
- }
892
-
893
- renderUserFilterPopup() {
894
- const { userFilterOpen, userSearch, users } = this.state;
895
- if (!userFilterOpen) return null;
896
- return (
897
- <Components.Popup
898
- title="Select Requestor"
899
- maxWidth={600}
900
- minWidth={400}
901
- maxHeight={600}
902
- minHeight={600}
903
- hasPadding
904
- onClose={this.onCloseUserSelector}
905
- buttons={[
906
- {
907
- type: 'tertiary',
908
- onClick: this.onCloseUserSelector,
909
- isActive: true,
910
- text: 'Cancel',
911
- },
912
- ]}
913
- >
914
- <Components.GenericInput
915
- id="userSearch"
916
- type="text"
917
- label="Search User"
918
- placeholder="Search name"
919
- value={userSearch}
920
- onChange={(e) => this.onHandleChange(e)}
921
- alwaysShowLabel
922
- />
923
- {_.sortBy(users, (u) => u.displayName.toUpperCase())
924
- .filter((u) => {
925
- if (_.isEmpty(userSearch)) return true;
926
- return u.displayName.toUpperCase().indexOf(userSearch.toUpperCase()) > -1;
927
- })
928
- .map((user) => {
929
- return <Components.UserListing key={user.userId} user={user} onClick={() => this.onSelectUser(user)} />;
930
- })}
931
- </Components.Popup>
932
- );
933
- }
934
-
935
- render() {
936
- const { success } = this.state;
937
-
938
- return (
939
- <Components.OverlayPage>
940
- <Components.OverlayPageContents noBottomButtons={success}>
941
- <Components.OverlayPageSection className="pageSectionWrapper--newPopup">
942
- <div>
943
- {this.renderSuccess()}
944
- {!success && this.renderMain()}
945
- </div>
946
- {this.renderUserFilterPopup()}
947
- </Components.OverlayPageSection>
948
- </Components.OverlayPageContents>
949
- <Components.OverlayPageBottomButtons>{this.renderSubmit()}</Components.OverlayPageBottomButtons>
950
- </Components.OverlayPage>
951
- );
952
- }
16
+ constructor(props) {
17
+ super(props);
18
+ this.imageInput = null;
19
+ this.customImageInputs = {};
20
+ this.customDocumentInputs = {};
21
+ this.state = {
22
+ jobId: Helper.safeReadParams(this.props, "jobId")
23
+ ? this.props.match.params.jobId
24
+ : null,
25
+ job: null,
26
+ showingSelector: false,
27
+ updating: false,
28
+ connected: false,
29
+ types: [],
30
+ users: [],
31
+ images: [],
32
+ userSearch: "",
33
+ userFilterOpen: false,
34
+ selectedUser: null,
35
+ id: null,
36
+ userID: "",
37
+ userName: "",
38
+ room: "",
39
+ phone: "",
40
+ location: this.props.auth.site,
41
+ title: "",
42
+ description: "",
43
+ isHome: false,
44
+ homeText: "",
45
+ prevType: "General",
46
+ type: "General",
47
+ image: null,
48
+ thumbnail: null,
49
+ showWarnings: false,
50
+ success: false,
51
+ prevCustomFileds: [],
52
+ customFields: [],
53
+ showDate: {},
54
+ };
55
+ }
56
+
57
+ UNSAFE_componentWillMount() {
58
+ Session.checkLoggedIn(this, this.props.auth);
59
+ }
60
+
61
+ componentDidMount() {
62
+ this.getJobTypes();
63
+ this.getUsers();
64
+ if (this.state.jobId) this.getJob();
65
+ this.props.addRecentlyCreated(values.featureKey);
66
+ }
67
+
68
+ getJob = async () => {
69
+ try {
70
+ const res = await maintenanceActions.getJob(
71
+ this.props.auth.site,
72
+ this.state.jobId,
73
+ );
74
+ res.data.location = res.data.site;
75
+ const { userID, userName, userProfilePic, type, customFields } = res.data;
76
+ this.setState({
77
+ ...res.data,
78
+ prevType: type,
79
+ prevCustomFileds: customFields,
80
+ type,
81
+ customFields,
82
+ selectedUser: {
83
+ userId: userID,
84
+ displayName: userName,
85
+ profilePic: userProfilePic,
86
+ },
87
+ });
88
+ this.checkSetImages(this.imageInput, res.data.images);
89
+ if (customFields) {
90
+ customFields.forEach((field, index) => {
91
+ if (field.type === "image" && field.answer) {
92
+ this.checkSetImages(this.customImageInputs[index], field.answer);
93
+ }
94
+ });
95
+ }
96
+ } catch (error) {
97
+ console.error("getJob", error);
98
+ }
99
+ };
100
+
101
+ checkSetImages(imageRef, images) {
102
+ if (imageRef) {
103
+ if (!_.isEmpty(images)) {
104
+ imageRef.setValue(images);
105
+ }
106
+ } else {
107
+ setTimeout(() => {
108
+ this.checkSetImages(images);
109
+ }, 100);
110
+ }
111
+ }
112
+
113
+ getJobTypes = async () => {
114
+ try {
115
+ const res = await maintenanceActions.getJobTypes(this.props.auth.site);
116
+ this.setState({ types: res.data });
117
+ this.getDefaultJob();
118
+ } catch (error) {
119
+ console.error("getJobTypes", error);
120
+ }
121
+ };
122
+
123
+ getUsers = async () => {
124
+ try {
125
+ const res = await userActions.fetchUsers(this.props.auth.site);
126
+ if (res.userFetchFail) return;
127
+ if (res.data != null && !_.isEmpty(res.data.results.Items)) {
128
+ let items = res.data.results.Items;
129
+ if (this.props.optionOnlyForResidents) {
130
+ items = _.filter(items, (u) => u.category === "resident");
131
+ }
132
+ this.setState({
133
+ users: _.sortBy(items, (u) => {
134
+ return (u.displayName || "").toLowerCase();
135
+ }),
136
+ });
137
+ }
138
+ } catch (error) {
139
+ console.error("getUsers", error);
140
+ }
141
+ };
142
+
143
+ getDefaultJob = () => {
144
+ const { types, jobId } = this.state;
145
+ if (jobId == null) {
146
+ if (types.length !== 0) {
147
+ const defaultType = types[0];
148
+ this.setState({
149
+ type: defaultType.typeName,
150
+ customFields:
151
+ defaultType.hasCustomFields && defaultType.customFields.length > 0
152
+ ? defaultType.customFields
153
+ : [],
154
+ });
155
+ } else if (values.forceCustomFields) {
156
+ this.setState({ noTypes: true });
157
+ } else {
158
+ this.setState({ type: "General" });
159
+ }
160
+ }
161
+ };
162
+
163
+ onSelectType = (key, e) => {
164
+ const { types, prevType, prevCustomFileds } = this.state;
165
+ const selectedType = types.find((t) => t.typeName === key);
166
+ // If selected type had previously saved custom fields, use the previous version
167
+ const hasPrevCustomFields =
168
+ prevType === selectedType.typeName &&
169
+ prevCustomFileds &&
170
+ prevCustomFileds.length > 0;
171
+ const update = {
172
+ type: selectedType.typeName,
173
+ customFields: hasPrevCustomFields
174
+ ? prevCustomFileds
175
+ : selectedType.hasCustomFields
176
+ ? selectedType.customFields
177
+ : [],
178
+ };
179
+
180
+ if (
181
+ !_.isEmpty(update.customFields) &&
182
+ !_.some(update.customFields, "isTitle")
183
+ ) {
184
+ update.title = this.state.selectedUser
185
+ ? this.state.selectedUser.displayName
186
+ : "";
187
+ }
188
+
189
+ this.setState(update);
190
+ };
191
+
192
+ renderTypeOptions() {
193
+ const { types, type } = this.state;
194
+ return types.map((ev) => {
195
+ if (ev != null) {
196
+ return (
197
+ <MenuItem
198
+ key={ev.typeName}
199
+ eventKey={ev.typeName}
200
+ active={type === ev.typeName}
201
+ >
202
+ {ev.typeName}
203
+ </MenuItem>
204
+ );
205
+ }
206
+ return null;
207
+ });
208
+ }
209
+
210
+ onHandleChange = (event) => {
211
+ var stateChange = {};
212
+ stateChange[event.target.getAttribute("id")] = event.target.value;
213
+ this.setState(stateChange);
214
+ };
215
+
216
+ onOpenUserSelector = () => {
217
+ this.setState({ userFilterOpen: true });
218
+ };
219
+
220
+ onCloseUserSelector = () => {
221
+ this.setState({ userFilterOpen: false });
222
+ };
223
+
224
+ onSelectUser = (user) => {
225
+ const update = {
226
+ selectedUser: user,
227
+ userID: user.userId,
228
+ userName: user.displayName,
229
+ userFilterOpen: false,
230
+ };
231
+ if (
232
+ !_.isEmpty(this.state.customFields) &&
233
+ !_.some(this.state.customFields, "isTitle")
234
+ ) {
235
+ update.title = user.displayName;
236
+ }
237
+
238
+ // Update UI immediately (non-blocking)
239
+ this.setState(update);
240
+
241
+ // PC-1255: Auto-populate contact details when staff create requests on behalf of residents
242
+ // Requires userManagement permission - gracefully falls back to manual entry if denied
243
+ // Fetch in background to avoid blocking UI
244
+ userActions
245
+ .fetchUser(this.props.auth.site, user.userId)
246
+ .then((response) => {
247
+ if (response.data && response.data.user) {
248
+ const contactUpdate = {};
249
+ // Auto-populate phone and room from user profile
250
+ if (response.data.user.phoneNumber) {
251
+ contactUpdate.phone = response.data.user.phoneNumber;
252
+ }
253
+ if (response.data.user.unit) {
254
+ contactUpdate.room = response.data.user.unit;
255
+ }
256
+ // Update contact fields when data arrives
257
+ if (Object.keys(contactUpdate).length > 0) {
258
+ this.setState(contactUpdate);
259
+ }
260
+ }
261
+ })
262
+ .catch((error) => {
263
+ // Permission denied (403) or other error - continue without auto-population
264
+ // Staff can still create the request, just need to enter contact details manually
265
+ console.log("Could not fetch user details for auto-population:", error);
266
+ });
267
+ };
268
+
269
+ onUnselectUser = () => {
270
+ const update = {
271
+ selectedUser: null,
272
+ userID: "",
273
+ userName: "",
274
+ phone: "",
275
+ room: "",
276
+ };
277
+ if (
278
+ !_.isEmpty(this.state.customFields) &&
279
+ !_.some(this.state.customFields, "isTitle")
280
+ ) {
281
+ update.title = "";
282
+ }
283
+ this.setState(update);
284
+ };
285
+
286
+ onChangeAnswer = (qId, answer) => {
287
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
288
+ const field = update.customFields[qId];
289
+ field.answer = answer;
290
+ if (field.isTitle) update.title = field.answer;
291
+ this.setState(update);
292
+ };
293
+
294
+ onChangeToggleAnswer = (qId, answer) => {
295
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
296
+ const field = update.customFields[qId];
297
+ field.answer = field.answer === answer ? undefined : answer;
298
+ if (field.isTitle) update.title = field.answer;
299
+ this.setState(update);
300
+ };
301
+
302
+ onChangeCheckboxAnswer = (qId, answer) => {
303
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
304
+ const field = update.customFields[qId];
305
+ field.answer = _.xor(field.answer || [], [answer]);
306
+ if (field.isTitle) update.title = field.answer.join(", ");
307
+ this.setState(update);
308
+ };
309
+
310
+ onChangeDateAnswer = (qId, answer, togglePicker = true) => {
311
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
312
+ const field = update.customFields[qId];
313
+ field.answer = answer;
314
+ if (field.isTitle)
315
+ update.title = moment(field.answer, "YYYY-MM-DD").format("DD-MMM-YYYY");
316
+ this.setState(update);
317
+
318
+ if (togglePicker) this.onToggleDatePicker(qId);
319
+ };
320
+
321
+ onChangeTimeAnswer = (qId, answer) => {
322
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
323
+ const field = update.customFields[qId];
324
+ field.answer = answer;
325
+ if (field.isTitle)
326
+ update.title = moment(field.answer, "HH:mm").format("h:mm a");
327
+ this.setState(update);
328
+ };
329
+
330
+ onChangeImageAnswer = (qId, answer) => {
331
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
332
+ const field = update.customFields[qId];
333
+ field.answer = answer;
334
+ this.setState(update);
335
+ };
336
+
337
+ onRemoveDocumentAnswer = (qId, document) => {
338
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
339
+ const field = update.customFields[qId];
340
+ field.answer = _.filter(field.answer, (d) => d.url !== document.url);
341
+ this.setState(update);
342
+ };
343
+
344
+ onHandlePDFFileChange = (event, qId) => {
345
+ const file = event.target.files[0];
346
+ if (!file) return;
347
+
348
+ const update = { customFields: _.cloneDeep(this.state.customFields) };
349
+ const field = update.customFields[qId];
350
+ const attachments = field.answer || [];
351
+ const [name, ext] = file.name.split(".");
352
+ const newAttachment = {
353
+ uploading: true,
354
+ name,
355
+ ext: ext.toLowerCase(),
356
+ };
357
+ attachments.push(newAttachment);
358
+ field.answer = attachments;
359
+ this.setState(update);
360
+
361
+ Apis.fileActions
362
+ .uploadMediaAsync(file, file.name)
363
+ .then((fileRes) => {
364
+ newAttachment.url = fileRes;
365
+ delete newAttachment.uploading;
366
+ this.setState(update);
367
+ })
368
+ .catch((uploadErrorRes) => {
369
+ console.log(uploadErrorRes);
370
+ delete newAttachment.uploading;
371
+ this.setState(update);
372
+ });
373
+ event.target.value = "";
374
+ };
375
+
376
+ onToggleDatePicker = (qId) => {
377
+ const showDate = { ...this.state.showDate };
378
+ showDate[qId] = !showDate[qId];
379
+ this.setState({ showDate });
380
+ };
381
+
382
+ onSave = () => {
383
+ this.setState({ showWarnings: false });
384
+ if (!this.validateForm()) {
385
+ this.setState({ showWarnings: true });
386
+ return;
387
+ }
388
+ if (this.state.updating) return;
389
+ this.setState({ updating: true });
390
+
391
+ const job = {
392
+ id: this.state.id,
393
+ userID: this.state.userID,
394
+ userName: this.state.userName,
395
+ room: this.state.room,
396
+ phone: this.state.phone,
397
+ location: this.state.location,
398
+ title: this.state.title,
399
+ description: this.state.description,
400
+ isHome: this.state.isHome,
401
+ homeText: this.state.homeText,
402
+ type: this.state.type,
403
+ date: null,
404
+ images: this.state.images,
405
+ customFields: this.state.customFields,
406
+ };
407
+
408
+ if (this.state.id != null) {
409
+ maintenanceActions
410
+ .editJob(job, this.props.auth.site)
411
+ .then((res) => {
412
+ this.setState({
413
+ success: true,
414
+ updating: false,
415
+ });
416
+ this.props.jobsLoaded([job]);
417
+ })
418
+ .catch((res) => {
419
+ this.setState({ updating: false });
420
+ alert("Something went wrong with the request. Please try again.");
421
+ });
422
+ } else {
423
+ // Create New Job
424
+ maintenanceActions
425
+ .createJob(job)
426
+ .then((res) => {
427
+ this.setState({
428
+ success: true,
429
+ updating: false,
430
+ });
431
+ this.props.jobsUpdate(this.props.auth.site);
432
+ })
433
+ .catch((res) => {
434
+ this.setState({ updating: false });
435
+ alert("Something went wrong with the request. Please try again.");
436
+ });
437
+ }
438
+ };
439
+
440
+ renderSuccess() {
441
+ if (!this.state.success) return null;
442
+
443
+ const title =
444
+ this.props.strings[`${values.featureKey}_textTitleRequests`] ||
445
+ values.textTitleRequests;
446
+ return (
447
+ <Components.SuccessPopup
448
+ text={`${values.textEntityName} has been ${this.state.id != null ? "edited" : "added"}`}
449
+ buttons={[
450
+ {
451
+ type: "outlined",
452
+ onClick: () => {
453
+ window.history.back();
454
+ },
455
+ text: `Back to ${title}`,
456
+ },
457
+ ]}
458
+ />
459
+ );
460
+ }
461
+
462
+ isFieldValid = (field) => {
463
+ const { mandatory, type, answer } = field;
464
+ if (["staticTitle", "staticText"].includes(type)) return true;
465
+
466
+ const checkMandatory = () => {
467
+ if (!mandatory) return true;
468
+ switch (type) {
469
+ case "yn":
470
+ return _.isBoolean(answer);
471
+ case "image":
472
+ case "document":
473
+ case "checkbox":
474
+ return _.isArray(answer) && answer.length > 0;
475
+ default:
476
+ return !_.isNil(answer) && !_.isEmpty(answer);
477
+ }
478
+ };
479
+ const checkFormat = () => {
480
+ if (_.isNil(answer) || _.isEmpty(answer)) return true;
481
+ switch (type) {
482
+ case "email":
483
+ return Helper.isEmail(answer);
484
+ case "date":
485
+ return moment(answer, "YYYY-MM-DD", true).isValid();
486
+ case "time":
487
+ return moment(answer, "HH:mm", true).isValid();
488
+ default:
489
+ return true;
490
+ }
491
+ };
492
+
493
+ const valid = checkMandatory() && checkFormat();
494
+ return valid;
495
+ };
496
+
497
+ validateCustomFields() {
498
+ const { customFields } = this.state;
499
+ if (!customFields || customFields.length === 0) return true;
500
+
501
+ return customFields.every((field) => {
502
+ const isValid = this.isFieldValid(field);
503
+ return isValid;
504
+ });
505
+ }
506
+
507
+ validateForm() {
508
+ if (_.isEmpty(this.state.userID)) return false;
509
+ if (_.isEmpty(this.state.userName)) return false;
510
+ if (_.isEmpty(this.state.room)) return false;
511
+ if (_.isEmpty(this.state.title)) return false;
512
+ if (this.state.isHome && _.isEmpty(this.state.homeText)) return false;
513
+ if (!this.validateCustomFields()) return false;
514
+ return true;
515
+ }
516
+
517
+ getFieldContainerClass = (isValid = true) => {
518
+ const showError = this.state.showWarnings && !isValid;
519
+ return `genericInputContainer ${isValid ? "genericInput-valid" : ""} ${showError ? "genericInput-error" : ""}`.trim();
520
+ };
521
+
522
+ renderSubmit() {
523
+ if (this.state.updating) {
524
+ return (
525
+ <Components.Button buttonType="secondary">Saving...</Components.Button>
526
+ );
527
+ }
528
+
529
+ return (
530
+ <div>
531
+ <Components.Button
532
+ inline
533
+ buttonType="tertiary"
534
+ onClick={() => this.props.history.push(values.routeRequestsHub)}
535
+ isActive
536
+ style={{ marginRight: 16 }}
537
+ >
538
+ Cancel
539
+ </Components.Button>
540
+ <Components.Button
541
+ inline
542
+ buttonType="primary"
543
+ onClick={this.onSave}
544
+ isActive={this.validateForm()}
545
+ >
546
+ Save
547
+ </Components.Button>
548
+ </div>
549
+ );
550
+ }
551
+
552
+ renderSelectUser() {
553
+ const { showWarnings, selectedUser } = this.state;
554
+ const isValid = !_.isNil(selectedUser);
555
+ return (
556
+ <div className={this.getFieldContainerClass(isValid)}>
557
+ <div style={styles.userLabelContainer}>
558
+ <div className="fieldLabel">User</div>
559
+ {showWarnings && !isValid ? (
560
+ <div className="fieldLabel fieldLabel-warning">Required</div>
561
+ ) : null}
562
+ </div>
563
+ <div style={styles.fieldContainer}>
564
+ <div className="inputRequired " />
565
+ {selectedUser ? (
566
+ <Components.Tag
567
+ className="marginRight-10"
568
+ rightIcon="close"
569
+ rightClick={this.onUnselectUser}
570
+ >
571
+ <Components.UserListing
572
+ size={15}
573
+ user={selectedUser}
574
+ textClass="tag_text"
575
+ />
576
+ </Components.Tag>
577
+ ) : (
578
+ <Components.Tag
579
+ onClick={this.onOpenUserSelector}
580
+ text="Select User"
581
+ />
582
+ )}
583
+ </div>
584
+ </div>
585
+ );
586
+ }
587
+
588
+ renderDefaultFields() {
589
+ return (
590
+ <div>
591
+ <Components.GenericInput
592
+ id="title"
593
+ label="Title for the work required"
594
+ type="textarea"
595
+ placeholder="Title for the work required"
596
+ value={this.state.title}
597
+ onChange={(e) => this.onHandleChange(e)}
598
+ inputStyle={{
599
+ height: 80,
600
+ }}
601
+ isRequired
602
+ isValid={() => {
603
+ return !_.isEmpty(this.state.title);
604
+ }}
605
+ showError={() => {
606
+ return this.state.showWarnings && _.isEmpty(this.state.title);
607
+ }}
608
+ alwaysShowLabel
609
+ />
610
+ <Components.GenericInput
611
+ id="description"
612
+ label="Description of work required"
613
+ type="textarea"
614
+ placeholder="Description of work required"
615
+ value={this.state.description}
616
+ onChange={(e) => this.onHandleChange(e)}
617
+ inputStyle={{
618
+ height: 80,
619
+ }}
620
+ alwaysShowLabel
621
+ />
622
+ <div className="marginBottom-16">
623
+ <Components.Text type="formLabel" className="marginBottom-4">
624
+ Images
625
+ </Components.Text>
626
+ <Components.ImageInput
627
+ ref={(ref) => {
628
+ this.imageInput = ref;
629
+ }}
630
+ multiple
631
+ refreshCallback={(images) => {
632
+ this.setState({ images });
633
+ }}
634
+ />
635
+ </div>
636
+ <Components.RadioButton
637
+ label="Person must be home during work?"
638
+ isActive={this.state.isHome}
639
+ options={[
640
+ {
641
+ Label: "No",
642
+ Value: false,
643
+ onChange: () => this.setState({ isHome: false }),
644
+ },
645
+ {
646
+ Label: "Yes",
647
+ Value: true,
648
+ onChange: () => this.setState({ isHome: true }),
649
+ },
650
+ ]}
651
+ />
652
+ {this.state.isHome && (
653
+ <Components.GenericInput
654
+ style={{ marginTop: 16 }}
655
+ label="Description of person's available times"
656
+ id="homeText"
657
+ type="textarea"
658
+ placeholder="Description of person's available times"
659
+ value={this.state.homeText}
660
+ onChange={(e) => this.onHandleChange(e)}
661
+ inputStyle={{
662
+ height: 80,
663
+ }}
664
+ isRequired
665
+ isValid={() => {
666
+ return !_.isEmpty(this.state.homeText);
667
+ }}
668
+ showError={() => {
669
+ return this.state.showWarnings && _.isEmpty(this.state.homeText);
670
+ }}
671
+ alwaysShowLabel
672
+ />
673
+ )}
674
+ </div>
675
+ );
676
+ }
677
+
678
+ renderField(field, fieldId) {
679
+ switch (field.type) {
680
+ case "yn":
681
+ return (
682
+ <div
683
+ key={fieldId}
684
+ className={`visitorSignIn_question ${this.getFieldContainerClass(this.isFieldValid(field))}`}
685
+ style={styles.fieldContainer}
686
+ >
687
+ {field.mandatory ? <div className="inputRequired " /> : null}
688
+ <Components.RadioButton
689
+ label={field.label}
690
+ labelStyle={{ color: this.props.colour }}
691
+ isActive={field.answer}
692
+ noHoverHighlight
693
+ highlightColour={this.props.colour}
694
+ options={[
695
+ {
696
+ Label: "Yes",
697
+ Value: true,
698
+ onChange: this.onChangeToggleAnswer.bind(this, fieldId, true),
699
+ },
700
+ {
701
+ Label: "No",
702
+ Value: false,
703
+ onChange: this.onChangeToggleAnswer.bind(
704
+ this,
705
+ fieldId,
706
+ false,
707
+ ),
708
+ },
709
+ ]}
710
+ />
711
+ </div>
712
+ );
713
+ case "multichoice":
714
+ return (
715
+ <div
716
+ key={fieldId}
717
+ className={`visitorSignIn_question ${this.getFieldContainerClass(this.isFieldValid(field))}`}
718
+ style={styles.fieldContainer}
719
+ >
720
+ {field.mandatory ? <div className="inputRequired " /> : null}
721
+ <Components.RadioButton
722
+ label={field.label}
723
+ labelStyle={{ color: this.props.colour }}
724
+ isActive={field.answer}
725
+ noHoverHighlight
726
+ highlightColour={this.props.colour}
727
+ options={field.values.map((o) => {
728
+ return {
729
+ Label: o,
730
+ Value: o,
731
+ onChange: this.onChangeToggleAnswer.bind(this, fieldId, o),
732
+ };
733
+ })}
734
+ rowStyle={{ flexDirection: "column" }}
735
+ buttonStyle={{ marginTop: "5px", marginBottom: "5px" }}
736
+ />
737
+ </div>
738
+ );
739
+ case "checkbox":
740
+ return (
741
+ <div
742
+ key={fieldId}
743
+ className={this.getFieldContainerClass(this.isFieldValid(field))}
744
+ style={styles.fieldContainer}
745
+ >
746
+ {field.mandatory ? <div className="inputRequired " /> : null}
747
+ <div className="visitorSignIn_question" style={{ flex: 1 }}>
748
+ <div
749
+ className="fieldLabel"
750
+ style={{ marginBottom: "5px", color: this.props.colour }}
751
+ >
752
+ {field.label}
753
+ </div>
754
+ {field.values.map((option, optionIndex) => {
755
+ return (
756
+ <Components.CheckBox
757
+ key={optionIndex}
758
+ label={option}
759
+ isActive={field.answer && field.answer.includes(option)}
760
+ highlightColour={this.props.colour}
761
+ noHoverHighlight
762
+ onChange={() =>
763
+ this.onChangeCheckboxAnswer(fieldId, option)
764
+ }
765
+ />
766
+ );
767
+ })}
768
+ </div>
769
+ </div>
770
+ );
771
+ case "text":
772
+ case "email":
773
+ case "phone":
774
+ return (
775
+ <div key={fieldId}>
776
+ <Components.GenericInput
777
+ id={fieldId}
778
+ type="text"
779
+ label={field.label}
780
+ placeholder={field.placeHolder}
781
+ value={field.answer}
782
+ onChange={(e) => this.onChangeAnswer(fieldId, e.target.value)}
783
+ isRequired={field.mandatory}
784
+ isValid={() => this.isFieldValid(field)}
785
+ showError={() =>
786
+ this.state.showWarnings && !this.isFieldValid(field)
787
+ }
788
+ errorMessage={
789
+ field.type === "email" ? "Not a valid email" : undefined
790
+ }
791
+ alwaysShowLabel
792
+ />
793
+ </div>
794
+ );
795
+ case "staticTitle":
796
+ return (
797
+ <p
798
+ className="visitorSignIn_text-staticTitle"
799
+ style={{ color: this.props.colour }}
800
+ key={fieldId}
801
+ >
802
+ {field.label}
803
+ </p>
804
+ );
805
+ case "staticText":
806
+ return (
807
+ <p className="visitorSignIn_text-staticText" key={fieldId}>
808
+ {Helper.toParagraphed(field.label, { marginTop: 10 })}
809
+ </p>
810
+ );
811
+ case "date":
812
+ return (
813
+ <div key={fieldId}>
814
+ <Components.GenericInput
815
+ id={fieldId}
816
+ label={field.label}
817
+ placeholder={"DD-MMM-YYYY"}
818
+ value={
819
+ field.answer
820
+ ? moment(field.answer, "YYYY-MM-DD").format("DD-MMM-YYYY")
821
+ : ""
822
+ }
823
+ onClick={(e) => this.onToggleDatePicker(fieldId)}
824
+ isRequired={field.mandatory}
825
+ isValid={() => this.isFieldValid(field)}
826
+ showError={() =>
827
+ this.state.showWarnings && !this.isFieldValid(field)
828
+ }
829
+ errorMessage="Not a valid date"
830
+ alwaysShowLabel
831
+ readOnly
832
+ rightContent={
833
+ !_.isEmpty(field.answer) && (
834
+ <Components.SVGIcon
835
+ colour={Colours.COLOUR_DUSK_LIGHT}
836
+ icon="close"
837
+ className="timepicker_clear"
838
+ onClick={() =>
839
+ this.onChangeDateAnswer(fieldId, undefined, false)
840
+ }
841
+ />
842
+ )
843
+ }
844
+ />
845
+ {this.state.showDate[fieldId] && (
846
+ <Components.DatePicker
847
+ selectedDate={field.answer}
848
+ selectDate={(date) => this.onChangeDateAnswer(fieldId, date)}
849
+ />
850
+ )}
851
+ </div>
852
+ );
853
+ case "time":
854
+ return (
855
+ <div key={fieldId}>
856
+ <Components.GenericInput
857
+ id={fieldId}
858
+ label={field.label}
859
+ placeholder={"--:-- --"}
860
+ value={
861
+ field.answer
862
+ ? moment(field.answer, "HH:mm").format("h:mm a")
863
+ : ""
864
+ }
865
+ type="time"
866
+ isRequired={field.mandatory}
867
+ isValid={() => this.isFieldValid(field)}
868
+ showError={() =>
869
+ this.state.showWarnings && !this.isFieldValid(field)
870
+ }
871
+ errorMessage="Not a valid time"
872
+ alwaysShowLabel
873
+ inputComponent={
874
+ <Components.TimePicker
875
+ selectedTime={field.answer}
876
+ selectTime={(time) => this.onChangeTimeAnswer(fieldId, time)}
877
+ className="timepicker-condensed"
878
+ callbackFormat="HH:mm"
879
+ style={{ width: "100%" }}
880
+ />
881
+ }
882
+ rightContent={
883
+ !_.isEmpty(field.answer) && (
884
+ <Components.SVGIcon
885
+ colour={Colours.COLOUR_DUSK_LIGHT}
886
+ icon="close"
887
+ className="timepicker_clear"
888
+ onClick={() => this.onChangeTimeAnswer(fieldId, undefined)}
889
+ />
890
+ )
891
+ }
892
+ />
893
+ </div>
894
+ );
895
+ case "image":
896
+ return (
897
+ <div
898
+ key={fieldId}
899
+ className={this.getFieldContainerClass(this.isFieldValid(field))}
900
+ style={styles.fieldContainer}
901
+ >
902
+ {field.mandatory ? <div className="inputRequired " /> : null}
903
+ <div className="visitorSignIn_question" style={{ flex: 1 }}>
904
+ <Components.Text type="formLabel" className="marginBottom-4">
905
+ {field.label}
906
+ </Components.Text>
907
+ <Components.ImageInput
908
+ ref={(ref) => (this.customImageInputs[fieldId] = ref)}
909
+ multiple
910
+ refreshCallback={(images) =>
911
+ this.onChangeImageAnswer(fieldId, images)
912
+ }
913
+ />
914
+ </div>
915
+ </div>
916
+ );
917
+ case "document":
918
+ const documents = field.answer || [];
919
+ return (
920
+ <div
921
+ key={fieldId}
922
+ className={this.getFieldContainerClass(this.isFieldValid(field))}
923
+ style={styles.fieldContainer}
924
+ >
925
+ {field.mandatory ? <div className="inputRequired " /> : null}
926
+ <div className="visitorSignIn_question" style={{ flex: 1 }}>
927
+ <Components.Text type="formLabel" className="marginBottom-4">
928
+ {field.label}
929
+ </Components.Text>
930
+ {documents.map((doc, index) => (
931
+ <Components.Attachment
932
+ key={index}
933
+ uploading={doc.uploading}
934
+ source={doc.url}
935
+ title={doc.name}
936
+ onRemove={() => this.onRemoveDocumentAnswer(fieldId, doc)}
937
+ />
938
+ ))}
939
+ <input
940
+ ref={(input) => (this.customDocumentInputs[fieldId] = input)}
941
+ id={`documentInput-${fieldId}`}
942
+ type="file"
943
+ className="fileInput"
944
+ onChange={(e) => this.onHandlePDFFileChange(e, fieldId)}
945
+ accept="application/pdf"
946
+ />
947
+ <div
948
+ className="iconTextButton marginBottom-16"
949
+ onClick={() => this.customDocumentInputs[fieldId].click()}
950
+ >
951
+ <FontAwesome className="iconTextButton_icon" name="paperclip" />
952
+ <p className="iconTextButton_text">Add Attachment</p>
953
+ </div>
954
+ </div>
955
+ </div>
956
+ );
957
+ default:
958
+ return null;
959
+ }
960
+ }
961
+
962
+ renderCustomFields() {
963
+ const { customFields } = this.state;
964
+ if (!customFields || customFields.length === 0) return null;
965
+
966
+ return (
967
+ <div>
968
+ {customFields.map((field, i) => {
969
+ return this.renderField(field, i);
970
+ })}
971
+ </div>
972
+ );
973
+ }
974
+
975
+ renderMain() {
976
+ const { customFields } = this.state;
977
+
978
+ return (
979
+ <div style={{ marginBottom: 15 }}>
980
+ <div className="padding-60 paddingVertical-40 bottomDivideBorder">
981
+ <Components.Text type="formTitleLarge" className="marginBottom-24">
982
+ {this.state.infoId == null ? "New" : "Edit"}{" "}
983
+ {values.textSingularName}
984
+ </Components.Text>
985
+ {/* Resident Information */}
986
+ {this.renderSelectUser()}
987
+ <Components.GenericInput
988
+ id="phone"
989
+ type="text"
990
+ label="Contact number"
991
+ placeholder="04XX XXX XXX"
992
+ value={this.state.phone}
993
+ // showError={this.state.showWarnings && !this.validateImage()}
994
+ onChange={(e) => this.onHandleChange(e)}
995
+ alwaysShowLabel
996
+ />
997
+ <Components.GenericInput
998
+ id="room"
999
+ type="text"
1000
+ label="Address"
1001
+ placeholder="Insert address here"
1002
+ value={this.state.room}
1003
+ onChange={(e) => this.onHandleChange(e)}
1004
+ isRequired
1005
+ alwaysShowLabel
1006
+ isValid={() => {
1007
+ return !_.isEmpty(this.state.room);
1008
+ }}
1009
+ showError={() => {
1010
+ return this.state.showWarnings && _.isEmpty(this.state.room);
1011
+ }}
1012
+ />
1013
+ <div style={{ marginBottom: 15 }}>
1014
+ <Components.Text type="formLabel">
1015
+ {values.textJobType}
1016
+ </Components.Text>
1017
+ <DropdownButton
1018
+ style={{ minWidth: 80 }}
1019
+ bsStyle="default"
1020
+ title={this.state.type}
1021
+ id="typeSelect"
1022
+ onSelect={this.onSelectType}
1023
+ >
1024
+ {this.renderTypeOptions()}
1025
+ </DropdownButton>
1026
+ </div>
1027
+ {!_.isEmpty(customFields) || values.forceCustomFields
1028
+ ? this.renderCustomFields()
1029
+ : this.renderDefaultFields()}
1030
+ </div>
1031
+ </div>
1032
+ );
1033
+ }
1034
+
1035
+ renderUserFilterPopup() {
1036
+ const { userFilterOpen, userSearch, users } = this.state;
1037
+ if (!userFilterOpen) return null;
1038
+ return (
1039
+ <Components.Popup
1040
+ title="Select Requestor"
1041
+ maxWidth={600}
1042
+ minWidth={400}
1043
+ maxHeight={600}
1044
+ minHeight={600}
1045
+ hasPadding
1046
+ onClose={this.onCloseUserSelector}
1047
+ buttons={[
1048
+ {
1049
+ type: "tertiary",
1050
+ onClick: this.onCloseUserSelector,
1051
+ isActive: true,
1052
+ text: "Cancel",
1053
+ },
1054
+ ]}
1055
+ >
1056
+ <Components.GenericInput
1057
+ id="userSearch"
1058
+ type="text"
1059
+ label="Search User"
1060
+ placeholder="Search name"
1061
+ value={userSearch}
1062
+ onChange={(e) => this.onHandleChange(e)}
1063
+ alwaysShowLabel
1064
+ />
1065
+ {_.sortBy(users, (u) => u.displayName.toUpperCase())
1066
+ .filter((u) => {
1067
+ if (_.isEmpty(userSearch)) return true;
1068
+ return (
1069
+ u.displayName.toUpperCase().indexOf(userSearch.toUpperCase()) > -1
1070
+ );
1071
+ })
1072
+ .map((user) => {
1073
+ return (
1074
+ <Components.UserListing
1075
+ key={user.userId}
1076
+ user={user}
1077
+ onClick={() => this.onSelectUser(user)}
1078
+ />
1079
+ );
1080
+ })}
1081
+ </Components.Popup>
1082
+ );
1083
+ }
1084
+
1085
+ render() {
1086
+ const { success } = this.state;
1087
+
1088
+ return (
1089
+ <Components.OverlayPage>
1090
+ <Components.OverlayPageContents noBottomButtons={success}>
1091
+ <Components.OverlayPageSection className="pageSectionWrapper--newPopup">
1092
+ <div>
1093
+ {this.renderSuccess()}
1094
+ {!success && this.renderMain()}
1095
+ </div>
1096
+ {this.renderUserFilterPopup()}
1097
+ </Components.OverlayPageSection>
1098
+ </Components.OverlayPageContents>
1099
+ <Components.OverlayPageBottomButtons>
1100
+ {this.renderSubmit()}
1101
+ </Components.OverlayPageBottomButtons>
1102
+ </Components.OverlayPage>
1103
+ );
1104
+ }
953
1105
  }
954
1106
 
955
1107
  const styles = {
956
- userLabelContainer: {
957
- display: 'flex',
958
- flexDirection: 'row',
959
- alignItems: 'center',
960
- marginBottom: 0,
961
- justifyContent: 'space-between',
962
- },
963
- fieldContainer: {
964
- display: 'flex',
965
- flexDirection: 'row',
966
- alignItems: 'center',
967
- },
1108
+ userLabelContainer: {
1109
+ display: "flex",
1110
+ flexDirection: "row",
1111
+ alignItems: "center",
1112
+ marginBottom: 0,
1113
+ justifyContent: "space-between",
1114
+ },
1115
+ fieldContainer: {
1116
+ display: "flex",
1117
+ flexDirection: "row",
1118
+ alignItems: "center",
1119
+ },
968
1120
  };
969
1121
 
970
1122
  const mapStateToProps = (state) => {
971
- const { auth } = state;
972
- return { auth, strings: (state.strings && state.strings.config) || {} };
1123
+ const { auth } = state;
1124
+ return {
1125
+ auth,
1126
+ strings: (state.strings && state.strings.config) || {},
1127
+ optionOnlyForResidents: Helper.getSiteSettingFromState(
1128
+ state,
1129
+ values.optionOnlyForResidents,
1130
+ ),
1131
+ };
973
1132
  };
974
1133
 
975
- export default connect(mapStateToProps, { jobsUpdate, jobsLoaded, addRecentlyCreated: Actions.addRecentlyCreated })(withRouter(AddJob));
1134
+ export default connect(mapStateToProps, {
1135
+ jobsUpdate,
1136
+ jobsLoaded,
1137
+ addRecentlyCreated: Actions.addRecentlyCreated,
1138
+ })(withRouter(AddJob));