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