@plusscommunities/pluss-maintenance-web-forms 1.1.20-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.babelrc +3 -0
  2. package/dist/index.cjs.js +6405 -0
  3. package/dist/index.esm.js +6353 -0
  4. package/dist/index.umd.js +6388 -0
  5. package/package.json +56 -0
  6. package/rollup.config.js +59 -0
  7. package/src/actions/JobsActions.js +98 -0
  8. package/src/actions/index.js +1 -0
  9. package/src/actions/types.js +6 -0
  10. package/src/apis/index.js +9 -0
  11. package/src/apis/maintenanceActions.js +181 -0
  12. package/src/apis/reactionActions.js +46 -0
  13. package/src/components/ActivityText.js +57 -0
  14. package/src/components/AnalyticsHub.js +167 -0
  15. package/src/components/JobList.js +842 -0
  16. package/src/components/JobTypes.js +198 -0
  17. package/src/components/PreviewFull.js +33 -0
  18. package/src/components/PreviewGrid.js +29 -0
  19. package/src/components/PreviewWidget.js +35 -0
  20. package/src/components/ViewFull.js +25 -0
  21. package/src/components/ViewWidget.js +23 -0
  22. package/src/feature.config.js +112 -0
  23. package/src/images/forms/full.png +0 -0
  24. package/src/images/forms/fullNoTitle.png +0 -0
  25. package/src/images/forms/previewWidget.png +0 -0
  26. package/src/images/forms/widget.png +0 -0
  27. package/src/images/full.png +0 -0
  28. package/src/images/fullNoTitle.png +0 -0
  29. package/src/images/previewWidget.png +0 -0
  30. package/src/images/widget.png +0 -0
  31. package/src/index.js +29 -0
  32. package/src/maintenanceStatus.json +17 -0
  33. package/src/reducers/MaintenanceReducer.js +74 -0
  34. package/src/screens/AddJob.js +859 -0
  35. package/src/screens/AddJobType.js +841 -0
  36. package/src/screens/Job.js +971 -0
  37. package/src/screens/RequestsHub.js +221 -0
  38. package/src/values.config.a.js +57 -0
  39. package/src/values.config.b.js +57 -0
  40. package/src/values.config.c.js +57 -0
  41. package/src/values.config.d.js +57 -0
  42. package/src/values.config.default.js +68 -0
  43. package/src/values.config.forms.js +67 -0
  44. package/src/values.config.js +67 -0
@@ -0,0 +1,971 @@
1
+ import React, { Component } from 'react';
2
+ import { withRouter } from 'react-router';
3
+ import { Link } from 'react-router-dom';
4
+ import moment from 'moment';
5
+ import _ from 'lodash';
6
+ import FontAwesome from 'react-fontawesome';
7
+ import Textarea from 'react-textarea-autosize';
8
+ import { connect } from 'react-redux';
9
+ import { jobsLoaded } from '../actions';
10
+ import Config, { PlussCore } from '../feature.config';
11
+ import { maintenanceActions, reactionActions } from '../apis';
12
+ import StatusTypes from '../maintenanceStatus.json';
13
+ import { values } from '../values.config';
14
+
15
+ const { Apis, Helper, Session, Colours, Components } = PlussCore;
16
+
17
+ class Job extends Component {
18
+ constructor(props) {
19
+ super(props);
20
+ this.state = {
21
+ jobId: Helper.safeReadParams(props, 'jobId') ? props.match.params.jobId : null,
22
+ job: null,
23
+ showingSelector: false,
24
+ updating: false,
25
+ comments: [],
26
+ commentInput: '',
27
+ loadingComments: false,
28
+ statusChangerOpen: false,
29
+ addNoteOpen: false,
30
+ noteAttachments: [],
31
+ noteInput: '',
32
+ assignees: [],
33
+ };
34
+ }
35
+
36
+ UNSAFE_componentWillReceiveProps(nextProps) {
37
+ Session.checkLoggedIn(this, this.props.auth);
38
+ }
39
+
40
+ componentDidMount() {
41
+ if (this.state.jobId) {
42
+ this.getJob();
43
+ this.getComments();
44
+ this.getAssignees();
45
+ }
46
+ }
47
+
48
+ getJob = async () => {
49
+ try {
50
+ const res = await maintenanceActions.getJob(this.props.auth.site, this.state.jobId);
51
+ this.setState({ updating: false });
52
+ res.data.location = res.data.site;
53
+ this.setJob(res.data);
54
+ } catch (error) {
55
+ console.error('getJob', error);
56
+ }
57
+ };
58
+
59
+ getAssignees = async () => {
60
+ try {
61
+ const res = await maintenanceActions.getAssignees(this.props.auth.site);
62
+ this.setState({ assignees: res.data.Users });
63
+ } catch (error) {
64
+ console.error('getAssignees', error);
65
+ }
66
+ };
67
+
68
+ getStatusType = (status) => {
69
+ let statusType = StatusTypes[status] || Object.values(StatusTypes).find((s) => s.text === status);
70
+ if (!statusType) statusType = { text: status, color: '#aaa' };
71
+ return statusType;
72
+ };
73
+
74
+ setJob = (job) => {
75
+ if (_.isEmpty(job.lastActivity)) {
76
+ job.lastActivity = '-- --';
77
+ job.noActivity = true;
78
+ }
79
+ if (_.isEmpty(job.status)) {
80
+ job.status = 'Unassigned';
81
+ job.notStatus = true;
82
+ }
83
+ if (_.isEmpty(job.audience)) {
84
+ job.audience = [{ displayName: 'Unassigned', isEmpty: true }];
85
+ }
86
+ let needToMarkSeen = false;
87
+ if (!job.seen) {
88
+ job.seen = true;
89
+ needToMarkSeen = true;
90
+ }
91
+ this.setState({ job }, () => {
92
+ if (needToMarkSeen) this.markSeen();
93
+ });
94
+ this.props.jobsLoaded([job]);
95
+ };
96
+
97
+ editJob = () => {};
98
+
99
+ isReadyToSaveNote = () => {
100
+ const { noteAttachments, noteInput } = this.state;
101
+ if (_.some(noteAttachments, (n) => n.Uploading)) return false;
102
+ return !_.isEmpty(noteInput) || !_.isEmpty(noteAttachments);
103
+ };
104
+
105
+ onOpenAddNote = () => {
106
+ this.setState({ addNoteOpen: true, editingNote: null });
107
+ };
108
+
109
+ onCloseAddNote = () => {
110
+ const newState = { addNoteOpen: false, editingNote: null };
111
+ if (!!this.state.editingNote) {
112
+ newState.noteInput = '';
113
+ newState.noteAttachments = [];
114
+ }
115
+ this.setState(newState);
116
+ };
117
+
118
+ onOpenNoteMenu = (index) => {
119
+ if (this.state.noteMenuOpen === index) {
120
+ this.setState({ noteMenuOpen: null });
121
+ } else {
122
+ this.setState({ noteMenuOpen: index });
123
+ }
124
+ };
125
+
126
+ onHandlePDFFileChange = (event) => {
127
+ const file = event.target.files[0];
128
+ if (!file || this.state.uploadingNoteAttachment) return;
129
+ const noteAttachments = [...this.state.noteAttachments];
130
+ const newAttachment = {
131
+ Uploading: true,
132
+ Title: file.name,
133
+ };
134
+ noteAttachments.push(newAttachment);
135
+ this.setState({
136
+ noteAttachments,
137
+ });
138
+ Apis.fileActions
139
+ .uploadMediaAsync(file, file.name)
140
+ .then((fileRes) => {
141
+ newAttachment.Source = fileRes;
142
+ newAttachment.Uploading = false;
143
+ this.setState({
144
+ noteAttachments: [...this.state.noteAttachments],
145
+ });
146
+ })
147
+ .catch((uploadErrorRes) => {
148
+ console.log(uploadErrorRes);
149
+ newAttachment.Uploading = false;
150
+ this.setState({
151
+ noteAttachments: [...this.state.noteAttachments],
152
+ });
153
+ });
154
+ event.target.value = '';
155
+ };
156
+
157
+ onRemoveAttachment = (a) => {
158
+ const index = this.state.noteAttachments.indexOf(a);
159
+ if (index > -1) {
160
+ const newAttachments = [...this.state.noteAttachments];
161
+ newAttachments.splice(index, 1);
162
+ this.setState({
163
+ noteAttachments: newAttachments,
164
+ });
165
+ }
166
+ };
167
+
168
+ onShowSelectAssignee = () => {
169
+ this.setState({
170
+ showingAssigneeSelector: true,
171
+ });
172
+ };
173
+
174
+ onCloseSelectAssignee = () => {
175
+ this.setState({
176
+ showingAssigneeSelector: false,
177
+ });
178
+ };
179
+
180
+ onSelectAssignee = (user) => {
181
+ this.setState({
182
+ selectedAssignee: user,
183
+ });
184
+ };
185
+
186
+ onConfirmAssignee = async () => {
187
+ this.setState({
188
+ confirmingAssignee: true,
189
+ });
190
+ try {
191
+ if (this.state.selectedAssignee) {
192
+ await this.onAssignUser(this.state.selectedAssignee.id);
193
+ }
194
+ this.onCloseSelectAssignee();
195
+ } catch (error) {
196
+ console.error('onConfirmAssignee', error);
197
+ }
198
+ this.setState({
199
+ confirmingAssignee: false,
200
+ });
201
+ };
202
+
203
+ // Method to handle user assignment
204
+ onAssignUser = async (userId) => {
205
+ try {
206
+ const res = await maintenanceActions.assignJob(this.state.jobId, userId);
207
+ this.getJob();
208
+ } catch (err) {
209
+ console.error('onAssignUser', err);
210
+ }
211
+ };
212
+
213
+ onConfirmAddNote = async () => {
214
+ if (!this.isReadyToSaveNote()) return;
215
+
216
+ try {
217
+ this.setState({ submittingNote: true });
218
+ const res = await (this.state.editingNote
219
+ ? maintenanceActions.editNote(
220
+ this.state.jobId,
221
+ this.state.editingNote,
222
+ this.state.noteInput,
223
+ this.state.noteAttachments.map((a) => {
224
+ return {
225
+ Title: a.Title,
226
+ Source: a.Source,
227
+ };
228
+ }),
229
+ )
230
+ : maintenanceActions.addNote(
231
+ this.state.jobId,
232
+ this.state.noteInput,
233
+ this.state.noteAttachments.map((a) => {
234
+ return {
235
+ Title: a.Title,
236
+ Source: a.Source,
237
+ };
238
+ }),
239
+ ));
240
+ this.setState(
241
+ {
242
+ job: res.data.job,
243
+ submittingNote: false,
244
+ addNoteOpen: false,
245
+ noteInput: '',
246
+ noteAttachments: [],
247
+ editingNote: null,
248
+ },
249
+ () => {
250
+ this.props.jobsLoaded([this.state.job]);
251
+ },
252
+ );
253
+ } catch (err) {
254
+ console.error('onConfirmAddNote', err);
255
+ }
256
+ };
257
+
258
+ onDeleteNote = (n) => {
259
+ if (!window.confirm(values.textAreYouSureYouWantToDeleteNote)) {
260
+ this.setState({ noteMenuOpen: null });
261
+ return;
262
+ }
263
+
264
+ maintenanceActions.deleteNote(this.state.jobId, n.Id);
265
+ const newNotes = _.filter(this.state.job.Notes, (note) => note.Id !== n.Id);
266
+ const newJob = { ...this.state.job };
267
+ newJob.Notes = newNotes;
268
+ this.setState({ job: newJob, noteMenuOpen: null });
269
+ };
270
+
271
+ onOpenEditNote = (n) => {
272
+ this.setState({
273
+ noteAttachments: n.Attachments || [],
274
+ noteInput: n.Note || '',
275
+ addNoteOpen: true,
276
+ editingNote: n.Id,
277
+ noteMenuOpen: null,
278
+ });
279
+ };
280
+
281
+ markSeen = () => {
282
+ const { job } = this.state;
283
+ const { auth } = this.props;
284
+ // Must have maintenance permission and not the requester
285
+ if (!Session.validateAccess(auth.site, values.permissionMaintenanceTracking, auth)) return;
286
+ if (auth.user.Id === job.userID) return;
287
+
288
+ this.setState({ updating: true }, async () => {
289
+ try {
290
+ const update = {
291
+ id: job.id,
292
+ seen: true,
293
+ status: job.status || 'Unassigned',
294
+ };
295
+
296
+ await maintenanceActions.editJob(update, auth.site);
297
+ } catch (error) {
298
+ this.setState({ updating: false });
299
+ console.log('markSeen error', error);
300
+ alert('Something went wrong with the request. Please try again.');
301
+ }
302
+ });
303
+ };
304
+
305
+ getComments = () => {
306
+ reactionActions.getComments(this.state.jobId, values.featureKey, 0).then((res) => {
307
+ this.setState({ comments: res.data });
308
+ });
309
+ };
310
+
311
+ onAddComment = async () => {
312
+ const { commentInput, jobId, job, comments } = this.state;
313
+ try {
314
+ this.setState({ commentInput: '' });
315
+ const res = await reactionActions.addComment(jobId, values.featureKey, job.title, job.site, commentInput);
316
+ this.setState({ comments: [...comments, res.data] });
317
+ } catch (error) {
318
+ console.error('onAddComment', error);
319
+ }
320
+ };
321
+
322
+ onHandleChange = (event) => {
323
+ var stateChange = {};
324
+ stateChange[event.target.getAttribute('id')] = event.target.value;
325
+ this.setState(stateChange);
326
+ };
327
+
328
+ onToggleStatusChanger = () => {
329
+ this.setState({ statusChangerOpen: !this.state.statusChangerOpen });
330
+ };
331
+
332
+ onSelectStatus = async (status) => {
333
+ this.setState({
334
+ job: {
335
+ ...this.state.job,
336
+ status: status,
337
+ },
338
+ statusChangerOpen: false,
339
+ });
340
+
341
+ try {
342
+ const res = await maintenanceActions.editJobStatus(this.state.job.id, status);
343
+ this.setState({ job: res.data.job }, () => {
344
+ this.props.jobsLoaded([this.state.job]);
345
+ });
346
+ } catch (error) {
347
+ console.error('onSelectStatus', error);
348
+ }
349
+ };
350
+
351
+ renderStatusLabel() {
352
+ if (!this.state.job.status) return null;
353
+ const statusType = this.getStatusType(this.state.job.status);
354
+ const { auth } = this.props;
355
+ if (Session.validateAccess(auth.site, values.permissionMaintenanceTracking, auth)) {
356
+ return (
357
+ <div className="statusLabel pointer" onClick={this.onToggleStatusChanger} style={{ backgroundColor: statusType.color }}>
358
+ <span className="statusLabel_text">{statusType.text}</span>
359
+ {this.renderStatusChanger()}
360
+ </div>
361
+ );
362
+ }
363
+ return (
364
+ <div className="statusLabel" style={{ backgroundColor: statusType.color }}>
365
+ <span className="statusLabel_text">{statusType.text}</span>
366
+ </div>
367
+ );
368
+ }
369
+
370
+ renderNotesButton() {
371
+ const { auth } = this.props;
372
+ if (!Session.validateAccess(auth.site, values.permissionMaintenanceTracking, auth)) return null;
373
+ return (
374
+ <div
375
+ className="statusLabel pointer"
376
+ onClick={this.onOpenAddNote}
377
+ style={{ backgroundColor: Config.env.colourBrandingMain, marginLeft: 8 }}
378
+ >
379
+ <span className="statusLabel_text">Add Note</span>
380
+ </div>
381
+ );
382
+ }
383
+
384
+ renderAssignButton() {
385
+ const { auth } = this.props;
386
+ if (!Session.validateAccess(auth.site, values.permissionMaintenanceTracking, auth)) return null;
387
+ return (
388
+ <div
389
+ className="statusLabel pointer"
390
+ onClick={this.onShowSelectAssignee}
391
+ style={{ backgroundColor: Config.env.colourBrandingMain, marginLeft: 8 }}
392
+ >
393
+ <span className="statusLabel_text">Assign</span>
394
+ </div>
395
+ );
396
+ }
397
+
398
+ renderStatusChanger() {
399
+ if (!this.state.statusChangerOpen) return null;
400
+ return (
401
+ <div className="statusChanger statusChanger-maintenance">
402
+ {Object.keys(StatusTypes).map((statusKey) => {
403
+ return (
404
+ <div
405
+ key={statusKey}
406
+ className="statusLabel"
407
+ onClick={() => this.onSelectStatus(statusKey)}
408
+ style={{ backgroundColor: StatusTypes[statusKey].color }}
409
+ >
410
+ <span className="statusLabel_text">{StatusTypes[statusKey].text}</span>
411
+ </div>
412
+ );
413
+ })}
414
+ </div>
415
+ );
416
+ }
417
+
418
+ renderComment(c) {
419
+ return <Components.Comment key={c.Id} comment={c} />;
420
+ return (
421
+ <div key={c.Id} className="comment">
422
+ <p className="comment_text">{Helper.toParagraphed(c.Comment)}</p>
423
+ <div className="comment_bottom">
424
+ <Components.ProfilePic className="comment_profilePic" size={25} image={c.User.profilePic} />
425
+ <p className="comment_name">{c.User.displayName}</p>
426
+ <p className="comment_time">{moment.utc(c.Timestamp).local().format('D MMM YYYY • h:mma')}</p>
427
+ </div>
428
+ </div>
429
+ );
430
+ }
431
+
432
+ renderCommentSection() {
433
+ if (this.state.loadingComments) return null;
434
+
435
+ return (
436
+ <div className="padding-60 paddingLeft-20">
437
+ <div className="newTopBar paddingLeft-40">
438
+ <Components.Text type="formTitleSmall" className="marginBottom-16">
439
+ Comments
440
+ </Components.Text>
441
+ <div className="commentSection">{this.state.comments.map((c) => this.renderComment(c))}</div>
442
+ <div className="commentReply">
443
+ <div
444
+ className={`commentReply_button${!_.isEmpty(this.state.commentInput) ? ' commentReply_button-active' : ''}`}
445
+ onClick={this.onAddComment}
446
+ >
447
+ <FontAwesome className="commentReply_icon" name="paper-plane-o" />
448
+ </div>
449
+ <Textarea
450
+ id="commentInput"
451
+ placeholder="Reply here..."
452
+ type="text"
453
+ className="commentReply_input"
454
+ value={this.state.commentInput}
455
+ onChange={(e) => this.onHandleChange(e)}
456
+ />
457
+ </div>
458
+ </div>
459
+ </div>
460
+ );
461
+ }
462
+
463
+ renderImageGrid(images) {
464
+ const imagesToUse = images && images.length > 0 ? images : [];
465
+ return (
466
+ <div className="imageGrid">
467
+ {imagesToUse.map((image, i) => {
468
+ return (
469
+ <a href={image} target="_blank" rel="noopener noreferrer" key={i}>
470
+ <div className="imageGrid_image" style={{ backgroundImage: `url('${Helper.get1400(image)}')` }}></div>
471
+ </a>
472
+ );
473
+ })}
474
+ </div>
475
+ );
476
+ }
477
+
478
+ renderImages() {
479
+ if (_.isEmpty(this.state.job.image) && _.isEmpty(this.state.job.images)) return null;
480
+
481
+ const imagesToUse = _.isEmpty(this.state.job.image) ? this.state.job.images : [this.state.job.image];
482
+ return (
483
+ <div className="padding-60 paddingVertical-40 bottomDivideBorder">
484
+ <Components.Text type="formTitleSmall" className="marginBottom-16">
485
+ Images
486
+ </Components.Text>
487
+ {this.renderImageGrid(imagesToUse)}
488
+ </div>
489
+ );
490
+ }
491
+
492
+ renderCustomFields() {
493
+ const { job } = this.state;
494
+ const { customFields } = job;
495
+
496
+ const labelClass = 'fieldLabel';
497
+ const answerClass = 'fontRegular fontSize-16 text-dark marginTop-5';
498
+
499
+ const renderAnswer = (field) => {
500
+ switch (field.type) {
501
+ case 'date':
502
+ return <div className={answerClass}>{field.answer ? moment(field.answer, 'YYYY-MM-DD').format('DD-MMM-YYYY') : ''}</div>;
503
+ case 'time':
504
+ return <div className={answerClass}>{field.answer ? moment(field.answer, 'HH:mm').format('h:mm a') : ''}</div>;
505
+ case 'yn':
506
+ return <div className={answerClass}>{field.answer ? 'Yes' : 'No'}</div>;
507
+ case 'checkbox':
508
+ return <div className={answerClass}>{field.answer && Array.isArray(field.answer) ? field.answer.join(', ') : ''}</div>;
509
+ case 'image':
510
+ return this.renderImageGrid(field.answer);
511
+ default:
512
+ return <div className={answerClass}>{field.answer}</div>;
513
+ }
514
+ };
515
+
516
+ return (
517
+ <div className="padding-60 paddingVertical-40 bottomDivideBorder">
518
+ {customFields.map((field, index) => {
519
+ if (['staticTitle', 'staticText'].includes(field.type)) return null;
520
+ if (_.isNil(field.answer) || field.answer === '') return null;
521
+ return (
522
+ <div key={index} className="marginTop-16">
523
+ <div className={labelClass}>{field.label}</div>
524
+ {renderAnswer(field)}
525
+ </div>
526
+ );
527
+ })}
528
+ </div>
529
+ );
530
+ }
531
+
532
+ renderInner() {
533
+ if (this.state.job == null) return null;
534
+ const { customFields } = this.state.job;
535
+ const hasCustomFields = customFields && customFields.length > 0;
536
+
537
+ return (
538
+ <div style={{ paddingBottom: 40 }}>
539
+ <div className="padding-60 paddingVertical-40 bottomDivideBorder relative">
540
+ <Components.Text type="formTitleLarge" className="marginBottom-8">
541
+ {this.state.job.title || values.textSingularName}
542
+ </Components.Text>
543
+ <Components.Text type="formTitleMedium" className="marginBottom-24">
544
+ {values.textEntityName} #{this.state.job.jobId}
545
+ </Components.Text>
546
+ <div className="marginTop-16">
547
+ <div className={'fieldLabel'}>Submission date</div>
548
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>
549
+ {moment.utc(this.state.job.createdTime).local().format('D MMM YY')}
550
+ </div>
551
+ </div>
552
+ <div className="marginTop-16">
553
+ <div className={'fieldLabel'}>Type</div>
554
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>{this.state.job.type}</div>
555
+ </div>
556
+ <div className="marginTop-16">
557
+ <div className={'fieldLabel'}>Address</div>
558
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>{this.state.job.room}</div>
559
+ </div>
560
+ {hasCustomFields ? null : (
561
+ <div className="marginTop-16">
562
+ <div className={'fieldLabel'}>Description {this.state.job.image ? '- (image supplied)' : ''}</div>
563
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>{this.state.job.description}</div>
564
+ </div>
565
+ )}
566
+ </div>
567
+ <div className="padding-60 paddingVertical-40 bottomDivideBorder">
568
+ <Components.Text type="formTitleSmall" className="marginBottom-16">
569
+ Contact Details
570
+ </Components.Text>
571
+ <div className="marginTop-16">
572
+ <div className={'fieldLabel'}>Name</div>
573
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>{this.state.job.userName}</div>
574
+ </div>
575
+ <div className="marginTop-16">
576
+ <div className={'fieldLabel'}>Contact number</div>
577
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>
578
+ {_.isEmpty(this.state.job.phone) ? 'No phone provided' : this.state.job.phone}
579
+ </div>
580
+ </div>
581
+ {hasCustomFields ? null : (
582
+ <div>
583
+ <div className="marginTop-16">
584
+ <div className={'fieldLabel'}>Should person be home?</div>
585
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>{this.state.job.isHome ? 'Yes' : 'No'}</div>
586
+ </div>
587
+ {this.state.job.isHome && this.state.job.homeText && (
588
+ <div className="marginTop-16">
589
+ <div className={'fieldLabel'}>When</div>
590
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>{this.state.job.homeText}</div>
591
+ </div>
592
+ )}
593
+ </div>
594
+ )}
595
+ </div>
596
+ {hasCustomFields ? null : this.renderImages()}
597
+ {hasCustomFields ? this.renderCustomFields() : null}
598
+ {this.renderCommentSection()}
599
+ </div>
600
+ );
601
+ }
602
+
603
+ renderHistoryEntry(e, i) {
604
+ const { job } = this.state;
605
+ const entryToUse = e || {
606
+ timestamp: job.createdTime,
607
+ status: 'Unassigned',
608
+ user: {
609
+ displayName: job.userName,
610
+ id: job.userID,
611
+ profilePic: job.userProfilePic,
612
+ },
613
+ };
614
+ const statusType = this.getStatusType(entryToUse.status);
615
+ return (
616
+ <div className="ticketHistoryEntry" key={i}>
617
+ <p className="ticketHistoryEntry_timestamp">{moment.utc(entryToUse.timestamp).local().format('D MMM YYYY h:mma')}</p>
618
+ <div className="statusLabel statusLabel-large statusLabel-full" style={{ backgroundColor: statusType.color }}>
619
+ <span className="statusLabel_text">{e ? `Marked as ${statusType.text}` : `${values.textEntityName} opened`}</span>
620
+ </div>
621
+ </div>
622
+ );
623
+ }
624
+
625
+ renderNote(note, index) {
626
+ return (
627
+ <div className="ticketHistoryEntry" key={index}>
628
+ <p className="ticketHistoryEntry_timestamp">{moment.utc(note.Timestamp).local().format('D MMM YYYY h:mma')}</p>
629
+ <div className="statusLabel statusLabel-large statusLabel-full" style={{ backgroundColor: '#6e79c5' }}>
630
+ <span className="statusLabel_text">Staff Notes</span>
631
+ </div>
632
+ <div className="maintenanceNote">
633
+ <div className="maintenanceNote_top">
634
+ {this.props.auth && this.props.auth.user && this.props.auth.user.Id === note.User.id && (
635
+ <Components.SVGIcon
636
+ colour={Colours.COLOUR_DUSK_LIGHT}
637
+ icon="more15"
638
+ className="maintenanceNote_moreIcon"
639
+ onClick={() => this.onOpenNoteMenu(index)}
640
+ />
641
+ )}
642
+ <p className="maintenanceNote_name">{note.User.displayName}</p>
643
+ {this.state.noteMenuOpen === index && (
644
+ <Components.MoreMenu
645
+ options={[
646
+ {
647
+ key: 'edit',
648
+ text: 'Edit',
649
+ onPress: () => this.onOpenEditNote(note),
650
+ },
651
+ {
652
+ key: 'delete',
653
+ text: 'Delete',
654
+ onPress: () => this.onDeleteNote(note),
655
+ },
656
+ ]}
657
+ />
658
+ )}
659
+ </div>
660
+ <p className="maintenanceNote_text">{Helper.toParagraphed(note.Note)}</p>
661
+ {note.Attachments.map((a, i) => this.renderAttachment(a, i))}
662
+ </div>
663
+ </div>
664
+ );
665
+ }
666
+
667
+ renderAssignment() {
668
+ const { job } = this.state;
669
+ if (!job) return null;
670
+
671
+ return (
672
+ <div className="padding-32 paddingVertical-40 bottomDivideBorder relative">
673
+ <div className="newTopBar clearfix flex flex-reverse">
674
+ {this.renderAssignButton()}
675
+ <Components.Text type="formTitleSmall" className="flex-1">
676
+ Assignment
677
+ </Components.Text>
678
+ </div>
679
+ <div>
680
+ <div className="marginTop-16">
681
+ <div className={'fieldLabel'}>Assigned to</div>
682
+ <div className={'fontRegular fontSize-16 text-dark marginTop-5'}>
683
+ {job.Assignee ? <Components.UserListing user={job.Assignee} /> : 'Unassigned'}
684
+ </div>
685
+ </div>
686
+ </div>
687
+ </div>
688
+ );
689
+ }
690
+
691
+ renderAssignmentEntry(e, i) {
692
+ return (
693
+ <div className="ticketHistoryEntry" key={i}>
694
+ <p className="ticketHistoryEntry_timestamp">{moment.utc(e.timestamp).local().format('D MMM YYYY h:mma')}</p>
695
+ <div className="statusLabel statusLabel-large statusLabel-full" style={{ backgroundColor: Colours.COLOUR_DUSK }}>
696
+ <span className="statusLabel_text">
697
+ {e.user.displayName} assigned the {values.textSingularName} to {e.assignedUser ? e.assignedUser.displayName : 'Unassigned'}
698
+ </span>
699
+ </div>
700
+ </div>
701
+ );
702
+ }
703
+
704
+ renderOverview() {
705
+ const { job } = this.state;
706
+ if (!job || !job.history) return null;
707
+
708
+ const source = _.sortBy(
709
+ [
710
+ ...job.history.map((e) => {
711
+ return { ...e, EntryType: e.EntryType || 'status' };
712
+ }),
713
+ ...(job.Notes || []).map((e) => {
714
+ return { ...e, timestamp: e.Timestamp, EntryType: 'note' };
715
+ }),
716
+ ],
717
+ 'timestamp',
718
+ );
719
+
720
+ return (
721
+ <div className="padding-32 paddingVertical-40 bottomDivideBorder relative">
722
+ <div className="newTopBar clearfix flex flex-reverse">
723
+ {this.renderNotesButton()}
724
+ {this.renderStatusLabel()}
725
+ <Components.Text type="formTitleSmall" className="flex-1">
726
+ Status History
727
+ </Components.Text>
728
+ </div>
729
+ {this.renderHistoryEntry(null, -1)}
730
+ {_.map(source, (e, i) => {
731
+ switch (e.EntryType) {
732
+ case 'status':
733
+ return this.renderHistoryEntry(e, i);
734
+ case 'note':
735
+ return this.renderNote(e, i);
736
+ case 'assignment':
737
+ return this.renderAssignmentEntry(e, i);
738
+ default:
739
+ break;
740
+ }
741
+ })}
742
+ </div>
743
+ );
744
+ }
745
+
746
+ renderButtons() {
747
+ return (
748
+ <div>
749
+ <Components.Button
750
+ inline
751
+ buttonType="tertiary"
752
+ onClick={() => {
753
+ window.history.back();
754
+ }}
755
+ isActive
756
+ style={{ marginRight: 16 }}
757
+ >
758
+ Back
759
+ </Components.Button>
760
+ {Session.validateAccess(this.props.auth.site, values.permissionMaintenanceTracking, this.props.auth) &&
761
+ !_.isEmpty(this.state.job) && (
762
+ <Link to={`${values.routeAddRequest}/${this.state.jobId}`}>
763
+ <Components.Button inline style={{ marginRight: 25 }} buttonType="outlined" isActive onClick={this.editJob}>
764
+ Edit Details
765
+ </Components.Button>
766
+ </Link>
767
+ )}
768
+ </div>
769
+ );
770
+ }
771
+
772
+ renderAttachment(attachment, index, onRemove) {
773
+ if (!attachment) return null;
774
+
775
+ return (
776
+ <Components.Attachment
777
+ key={index}
778
+ uploading={attachment.Uploading}
779
+ source={attachment.Source}
780
+ title={attachment.Title}
781
+ onRemove={onRemove ? () => onRemove(attachment) : undefined}
782
+ />
783
+ );
784
+ }
785
+
786
+ renderAddNotePopup() {
787
+ if (!this.state.addNoteOpen) return null;
788
+
789
+ if (this.state.submittingNote) {
790
+ return (
791
+ <Components.Popup title="Saving Note" maxWidth={600} hasPadding>
792
+ <div className="flex flex-center-row">
793
+ <FontAwesome className="spinner" name="spinner fa-pulse fa-fw" />
794
+ </div>
795
+ </Components.Popup>
796
+ );
797
+ }
798
+
799
+ return (
800
+ <Components.Popup
801
+ title={`${this.state.editingNote ? 'Edit' : 'Add'} Note`}
802
+ onClose={this.onCloseAddNote}
803
+ maxWidth={600}
804
+ hasPadding
805
+ buttons={[
806
+ {
807
+ type: 'primary',
808
+ onClick: this.onConfirmAddNote,
809
+ isActive: this.isReadyToSaveNote(),
810
+ text: 'Save',
811
+ },
812
+ {
813
+ type: 'tertiary',
814
+ onClick: this.onCloseAddNote,
815
+ isActive: true,
816
+ text: 'Cancel',
817
+ },
818
+ ]}
819
+ >
820
+ <Components.GenericInput
821
+ id="noteInput"
822
+ type="textarea"
823
+ componentClass="textarea"
824
+ value={this.state.noteInput}
825
+ placeholder="Enter note"
826
+ onChange={(e) => this.onHandleChange(e)}
827
+ inputStyle={{
828
+ width: 400,
829
+ }}
830
+ />
831
+ <Components.Text type="h5">Attachments</Components.Text>
832
+ {this.state.noteAttachments.map((a, i) => this.renderAttachment(a, i, this.onRemoveAttachment))}
833
+ <input
834
+ ref={(input) => (this.attachmentInput = input)}
835
+ id="attachmentInput"
836
+ type="file"
837
+ className="fileInput"
838
+ onChange={(e) => this.onHandlePDFFileChange(e)}
839
+ accept="application/pdf"
840
+ />
841
+ <div
842
+ className="iconTextButton"
843
+ onClick={() => {
844
+ this.attachmentInput.click();
845
+ }}
846
+ >
847
+ <FontAwesome className="iconTextButton_icon" name="paperclip" />
848
+ <p className="iconTextButton_text">Add Attachment</p>
849
+ </div>
850
+ </Components.Popup>
851
+ );
852
+ }
853
+
854
+ renderUsers() {
855
+ let content = null;
856
+ if (this.state.confirmingAssignee) {
857
+ content = (
858
+ <div className="flex flex-center-row">
859
+ <FontAwesome className="spinner" name="spinner fa-pulse fa-fw" />
860
+ </div>
861
+ );
862
+ } else if (this.state.selectedAssignee) {
863
+ content = (
864
+ <div>
865
+ <Components.UserListing
866
+ key={this.state.selectedAssignee.id}
867
+ user={this.state.selectedAssignee}
868
+ rightContent={
869
+ <Components.SVGIcon
870
+ className="removeIcon"
871
+ icon="close"
872
+ onClick={() => {
873
+ this.onSelectAssignee();
874
+ }}
875
+ colour={Colours.COLOUR_DUSK}
876
+ />
877
+ }
878
+ />
879
+ </div>
880
+ );
881
+ } else {
882
+ content = (
883
+ <div>
884
+ <Components.GenericInput
885
+ id="userSearch"
886
+ type="text"
887
+ // label="Search"
888
+ placeholder="Search name"
889
+ value={this.state.userSearch}
890
+ onChange={(e) => this.onHandleChange(e)}
891
+ alwaysShowLabel
892
+ />
893
+ {_.sortBy(this.state.assignees, (u) => u.displayName.toUpperCase())
894
+ .filter((u) => {
895
+ if (_.isEmpty(this.state.userSearch)) return true;
896
+ return u.displayName.toUpperCase().indexOf(this.state.userSearch.toUpperCase()) > -1;
897
+ })
898
+ .map((user) => {
899
+ return (
900
+ <Components.UserListing
901
+ key={user.id}
902
+ user={user}
903
+ onClick={() => {
904
+ this.onSelectAssignee(user);
905
+ }}
906
+ />
907
+ );
908
+ })}
909
+ </div>
910
+ );
911
+ }
912
+ return (
913
+ <div className="genericInputContainer">
914
+ <Components.Text type="formLabel">Select User</Components.Text>
915
+ {content}
916
+ </div>
917
+ );
918
+ }
919
+
920
+ renderUserSelectionPopup() {
921
+ if (!this.state.showingAssigneeSelector) return null;
922
+ return (
923
+ <Components.Popup
924
+ title="Assign Job"
925
+ onClose={this.onCloseSelectAssignee}
926
+ maxWidth={600}
927
+ hasPadding
928
+ buttons={[
929
+ {
930
+ type: 'primary',
931
+ onClick: this.onConfirmAssignee,
932
+ isActive: !!this.state.selectedAssignee,
933
+ text: 'Confirm',
934
+ },
935
+ {
936
+ type: 'tertiary',
937
+ onClick: this.onCloseSelectAssignee,
938
+ isActive: true,
939
+ text: 'Cancel',
940
+ },
941
+ ]}
942
+ >
943
+ {this.renderUsers()}
944
+ </Components.Popup>
945
+ );
946
+ }
947
+
948
+ render() {
949
+ return (
950
+ <Components.OverlayPage>
951
+ {this.renderAddNotePopup()}
952
+ {this.renderUserSelectionPopup()}
953
+ <Components.OverlayPageContents>
954
+ <Components.OverlayPageSection className="pageSectionWrapper--fixedPopupSize">{this.renderInner()}</Components.OverlayPageSection>
955
+ <Components.OverlayPageSection className="pageSectionWrapper--newPopupSide pageSectionWrapper--newPopupSide-fixedWidth">
956
+ {this.renderAssignment()}
957
+ {this.renderOverview()}
958
+ </Components.OverlayPageSection>
959
+ </Components.OverlayPageContents>
960
+ <Components.OverlayPageBottomButtons>{this.renderButtons()}</Components.OverlayPageBottomButtons>
961
+ </Components.OverlayPage>
962
+ );
963
+ }
964
+ }
965
+
966
+ const mapStateToProps = (state) => {
967
+ const { auth } = state;
968
+ return { auth };
969
+ };
970
+
971
+ export default connect(mapStateToProps, { jobsLoaded })(withRouter(Job));