@plusscommunities/pluss-maintenance-web-a 1.1.23 → 1.1.24

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.
@@ -6,10 +6,17 @@ import { Link } from 'react-router-dom';
6
6
  import FontAwesome from 'react-fontawesome';
7
7
  import moment from 'moment';
8
8
  import _ from 'lodash';
9
- import { jobsLoaded, removeJob } from '../actions';
9
+ import { jobsLoaded, removeJob, jobStatusesUpdate } from '../actions';
10
10
  import { PlussCore } from '../feature.config';
11
11
  import { maintenanceActions } from '../apis';
12
- import StatusTypes from '../maintenanceStatus.json';
12
+ import {
13
+ STATUS_NOT_ACTIONED,
14
+ STATUS_COMPLETED,
15
+ STATUS_IMCOMPLETE,
16
+ jobPriorityOptions,
17
+ getDefaultPriority,
18
+ getJobPriority,
19
+ } from '../helper';
13
20
  import { Colours } from '@plusscommunities/pluss-core-web';
14
21
  import { values } from '../values.config';
15
22
 
@@ -24,6 +31,7 @@ class JobList extends Component {
24
31
  sortDesc: true,
25
32
  selectedTimeFilter: Analytics.getAnalyticsFilterOptions()[1],
26
33
  assignees: [],
34
+ requesters: [],
27
35
  };
28
36
 
29
37
  this.exportColumns = [
@@ -32,6 +40,7 @@ class JobList extends Component {
32
40
  { label: `${values.textEntityName} No.`, key: 'jobId' },
33
41
  { label: values.textJobType, key: 'type' },
34
42
  { label: 'Status', key: 'status' },
43
+ { label: 'Priority', key: 'priority' },
35
44
  { label: 'Title', key: 'title' },
36
45
  { label: 'Address', key: 'room' },
37
46
  { label: 'Description', key: 'description' },
@@ -51,15 +60,33 @@ class JobList extends Component {
51
60
  }
52
61
 
53
62
  componentDidMount() {
63
+ this.props.jobStatusesUpdate(this.props.auth.site);
54
64
  this.getJobs();
55
65
  this.getAssignees();
56
66
  }
57
67
 
68
+ setRequesters = (jobs) => {
69
+ const requesters = _.orderBy(
70
+ _.uniqBy(
71
+ jobs.map((j) => ({
72
+ id: j.userID,
73
+ displayName: j.userName,
74
+ profilePic: j.userProfilePic,
75
+ })),
76
+ 'id',
77
+ ),
78
+ 'displayName',
79
+ 'asc',
80
+ );
81
+ this.setState({ requesters });
82
+ };
83
+
58
84
  getJobs = async () => {
59
85
  const { auth } = this.props;
60
86
  try {
61
87
  const res = await maintenanceActions.getJobsRecursive(auth.site);
62
88
  if (!_.isEmpty(res) && res[0].site === auth.site) {
89
+ this.setRequesters(res);
63
90
  this.props.jobsLoaded(res);
64
91
  }
65
92
  } catch (error) {
@@ -117,6 +144,13 @@ class JobList extends Component {
117
144
  this.closeFilter();
118
145
  };
119
146
 
147
+ selectPriorityFilter = (filter) => {
148
+ this.setState({
149
+ selectedPriorityFilter: filter,
150
+ });
151
+ this.closeFilter();
152
+ };
153
+
120
154
  selectStatusFilter = (filter) => {
121
155
  this.setState({
122
156
  selectedStatusFilter: filter,
@@ -207,6 +241,32 @@ class JobList extends Component {
207
241
  this.closeFilter();
208
242
  };
209
243
 
244
+ onSelectRequester = (user) => {
245
+ this.setState({
246
+ selectedRequester: user,
247
+ });
248
+ };
249
+
250
+ removeRequesterFilter = () => {
251
+ this.setState({
252
+ selectedRequesterFilter: null,
253
+ selectedRequesterFilterText: null,
254
+ });
255
+ };
256
+
257
+ saveRequesterFilter = () => {
258
+ if (!this.state.selectedRequester) {
259
+ this.removeRequesterFilter();
260
+ } else {
261
+ this.setState({
262
+ selectedRequesterFilter: this.state.selectedRequester.id,
263
+ selectedRequesterFilterText: this.state.selectedRequester.displayName,
264
+ selectedRequester: null,
265
+ });
266
+ }
267
+ this.closeFilter();
268
+ };
269
+
210
270
  onHandleSearchChange = (event) => {
211
271
  const thisSearchTime = moment().valueOf();
212
272
  this.setState({ search: event.target.value, lastSearch: thisSearchTime });
@@ -238,14 +298,26 @@ class JobList extends Component {
238
298
  });
239
299
  }
240
300
 
301
+ // filter by priority
302
+ if (this.state.selectedPriorityFilter) {
303
+ const defaultPriority = getDefaultPriority().name;
304
+ source = _.filter(source, (r) => {
305
+ return (
306
+ r.priority === this.state.selectedPriorityFilter || (this.state.selectedPriorityFilter === defaultPriority && _.isNil(r.priority))
307
+ );
308
+ });
309
+ }
310
+
241
311
  // filter by status
242
312
  if (this.state.selectedStatusFilter) {
313
+ const { statusTypes } = this.props;
314
+ const defaultStatus = statusTypes.find((s) => s.category === STATUS_NOT_ACTIONED);
243
315
  source = _.filter(source, (r) => {
244
- const status = r.status && StatusTypes[r.status] ? r.status : 'Unassigned';
245
- if (this.state.selectedStatusFilter === 'incomplete') {
246
- return status !== 'Completed';
316
+ const status = statusTypes.find((s) => s.text === r.status) || defaultStatus;
317
+ if (this.state.selectedStatusFilter === STATUS_IMCOMPLETE) {
318
+ return status.category !== STATUS_COMPLETED;
247
319
  }
248
- return status === this.state.selectedStatusFilter;
320
+ return status.text === this.state.selectedStatusFilter;
249
321
  });
250
322
  }
251
323
 
@@ -255,6 +327,12 @@ class JobList extends Component {
255
327
  });
256
328
  }
257
329
 
330
+ if (this.state.selectedRequesterFilter) {
331
+ source = _.filter(source, (r) => {
332
+ return r.userID === this.state.selectedRequesterFilter;
333
+ });
334
+ }
335
+
258
336
  if (!_.isEmpty(this.state.searchTerm)) {
259
337
  source = _.filter(source, (r) => {
260
338
  if (r.jobId && r.jobId === this.state.searchTerm) {
@@ -319,6 +397,7 @@ class JobList extends Component {
319
397
 
320
398
  getExportSource = () => {
321
399
  const customColumns = [];
400
+ const defaultPriority = getDefaultPriority().name;
322
401
  const source = this.getSource().map((r) => {
323
402
  const history = r.history || [];
324
403
  const progressEntry = _.find(history, (e) => {
@@ -353,7 +432,8 @@ class JobList extends Component {
353
432
  notes += note.Note;
354
433
  });
355
434
  const customFieldValues = this.getCustomFields(r, customColumns);
356
- return { ...r, ...customFieldValues, notes, progressTime, completedTime, progressDuration, completedDuration };
435
+ const priority = r.priority || defaultPriority;
436
+ return { ...r, ...customFieldValues, notes, progressTime, completedTime, progressDuration, completedDuration, priority };
357
437
  });
358
438
 
359
439
  // Compose revised columns list with custom fields
@@ -397,19 +477,37 @@ class JobList extends Component {
397
477
  </Components.Popup>
398
478
  );
399
479
  }
480
+ if (this.state.filterOpen === 'priority') {
481
+ return (
482
+ <Components.Popup title="Select Priority" maxWidth={600} minWidth={400} hasPadding onClose={this.closeFilter}>
483
+ {jobPriorityOptions.map((p) => {
484
+ return (
485
+ <Components.Tag
486
+ key={p.name}
487
+ onClick={() => {
488
+ this.selectPriorityFilter(p.name);
489
+ }}
490
+ text={p.name}
491
+ className="marginRight-10"
492
+ />
493
+ );
494
+ })}
495
+ </Components.Popup>
496
+ );
497
+ }
400
498
  if (this.state.filterOpen === 'status') {
499
+ const { statusTypes } = this.props;
401
500
  return (
402
501
  <Components.Popup title="Select Status" maxWidth={600} minWidth={400} hasPadding onClose={this.closeFilter}>
403
- {['incomplete', ...Object.keys(StatusTypes)].map((sKey) => {
404
- const text = sKey === 'incomplete' ? 'All Incomplete' : StatusTypes[sKey].text;
502
+ {[STATUS_IMCOMPLETE, ...statusTypes.map((s) => s.text)].map((status) => {
405
503
  return (
406
504
  <Components.Tag
407
- key={sKey}
505
+ key={status}
408
506
  onClick={() => {
409
- this.selectStatusFilter(sKey);
507
+ this.selectStatusFilter(status);
410
508
  }}
411
- text={text}
412
- className="marginRight-10"
509
+ text={status}
510
+ className="marginRight-10 marginBottom-10"
413
511
  />
414
512
  );
415
513
  })}
@@ -515,15 +613,92 @@ class JobList extends Component {
515
613
  </Components.Popup>
516
614
  );
517
615
  }
616
+ if (this.state.filterOpen === 'requester') {
617
+ let userContent = null;
618
+ if (this.state.selectedRequester) {
619
+ userContent = (
620
+ <div>
621
+ <Components.UserListing
622
+ key={this.state.selectedRequester.id}
623
+ user={this.state.selectedRequester}
624
+ rightContent={
625
+ <Components.SVGIcon
626
+ className="removeIcon"
627
+ icon="close"
628
+ onClick={() => {
629
+ this.onSelectRequester();
630
+ }}
631
+ colour={Colours.COLOUR_DUSK}
632
+ />
633
+ }
634
+ />
635
+ </div>
636
+ );
637
+ } else {
638
+ userContent = (
639
+ <div>
640
+ <Components.GenericInput
641
+ id="requesterSearch"
642
+ type="text"
643
+ // label="Search"
644
+ placeholder="Search name"
645
+ value={this.state.requesterSearch}
646
+ onChange={(e) => this.onHandleChange(e)}
647
+ alwaysShowLabel
648
+ />
649
+ {_.sortBy(this.state.requesters, (u) => u.displayName.toUpperCase())
650
+ .filter((u) => {
651
+ if (_.isEmpty(this.state.requesterSearch)) return true;
652
+ return u.displayName.toUpperCase().indexOf(this.state.requesterSearch.toUpperCase()) > -1;
653
+ })
654
+ .map((user) => {
655
+ return (
656
+ <Components.UserListing
657
+ key={user.id}
658
+ user={user}
659
+ onClick={() => {
660
+ this.onSelectRequester(user);
661
+ }}
662
+ />
663
+ );
664
+ })}
665
+ </div>
666
+ );
667
+ }
668
+ return (
669
+ <Components.Popup
670
+ title="Select User"
671
+ maxWidth={600}
672
+ minWidth={400}
673
+ hasPadding
674
+ onClose={this.closeFilter}
675
+ buttons={[
676
+ {
677
+ type: 'primaryAction',
678
+ onClick: this.saveRequesterFilter,
679
+ text: 'Select',
680
+ isActive: true,
681
+ },
682
+ ]}
683
+ >
684
+ {userContent}
685
+ </Components.Popup>
686
+ );
687
+ }
518
688
  return null;
519
689
  }
520
690
 
521
691
  renderRequests() {
692
+ const { statusTypes } = this.props;
693
+ const defaultStatus = statusTypes.find((s) => s.category === STATUS_NOT_ACTIONED);
694
+
522
695
  return this.getSource().map((ev, index) => {
523
696
  if (!ev) {
524
697
  return null;
525
698
  }
526
- const status = ev.status && StatusTypes[ev.status] ? StatusTypes[ev.status] : StatusTypes['Unassigned'];
699
+ const status = (ev.status && statusTypes.find((s) => s.text === ev.status)) || defaultStatus;
700
+ const priority = getJobPriority(ev.priority);
701
+
527
702
  return (
528
703
  <tr key={index}>
529
704
  <td>{ev.jobId}</td>
@@ -535,6 +710,16 @@ class JobList extends Component {
535
710
  <td>{ev.type}</td>
536
711
  <td>{moment.utc(ev.createdTime).local().format('D MMM YY')}</td>
537
712
  <td>{ev.room}</td>
713
+ <td>
714
+ {ev.userName ? (
715
+ <Components.UserListing
716
+ user={{ id: ev.userID, displayName: ev.userName, profilePic: ev.userProfilePic }}
717
+ textClass="fontSize-13"
718
+ />
719
+ ) : (
720
+ 'Unknown'
721
+ )}
722
+ </td>
538
723
  <td>{ev.Assignee ? <Components.UserListing user={ev.Assignee} textClass="fontSize-13" /> : 'Unassigned'}</td>
539
724
  {/* <td>
540
725
  <div style={{ textAlign: 'center' }}>{ev.commentCount}</div>
@@ -548,10 +733,25 @@ class JobList extends Component {
548
733
  paddingTop: 2,
549
734
  paddingBottom: 2,
550
735
  color: '#fff',
551
- backgroundColor: status.color,
736
+ backgroundColor: status ? status.color : undefined,
737
+ }}
738
+ >
739
+ {status ? status.text : ''}
740
+ </div>
741
+ </td>
742
+ <td>
743
+ <div
744
+ style={{
745
+ textAlign: 'center',
746
+ borderRadius: 4,
747
+ width: 100,
748
+ paddingTop: 2,
749
+ paddingBottom: 2,
750
+ color: '#fff',
751
+ backgroundColor: priority.color,
552
752
  }}
553
753
  >
554
- {status.text}
754
+ {priority.name}
555
755
  </div>
556
756
  </td>
557
757
  <td className="table-options">
@@ -654,6 +854,15 @@ class JobList extends Component {
654
854
  >
655
855
  Address{this.renderSort('room')}
656
856
  </th>
857
+ <th
858
+ className={`${this.sortIsActive('userName')}`}
859
+ style={{ cursor: 'pointer', width: 150 }}
860
+ onClick={() => {
861
+ this.sortByCol('userName');
862
+ }}
863
+ >
864
+ Submitted By{this.renderSort('userName')}
865
+ </th>
657
866
  <th
658
867
  className={`${this.sortIsActive('assigned')}`}
659
868
  style={{ cursor: 'pointer', width: 150 }}
@@ -665,6 +874,7 @@ class JobList extends Component {
665
874
  </th>
666
875
  {/* <th style={{ width: 30 }}>Comments</th> */}
667
876
  <th style={{ width: 120 }}>Status</th>
877
+ <th style={{ width: 120 }}>Priority</th>
668
878
  <th style={{ width: 50 }} />
669
879
  </tr>
670
880
  </thead>
@@ -692,6 +902,15 @@ class JobList extends Component {
692
902
  text="Status"
693
903
  />
694
904
  );
905
+ let priorityFilter = (
906
+ <Components.Tag
907
+ className="marginRight-10"
908
+ onClick={() => {
909
+ this.openFilter('priority');
910
+ }}
911
+ text="Priority"
912
+ />
913
+ );
695
914
  let timeFilter = (
696
915
  <Components.Tag
697
916
  className="marginRight-10"
@@ -710,6 +929,15 @@ class JobList extends Component {
710
929
  text="Assigned To"
711
930
  />
712
931
  );
932
+ let requesterFilter = (
933
+ <Components.Tag
934
+ className="marginRight-10"
935
+ onClick={() => {
936
+ this.openFilter('requester');
937
+ }}
938
+ text="Submitted By"
939
+ />
940
+ );
713
941
 
714
942
  if (this.state.selectedTypeFilter) {
715
943
  typeFilter = (
@@ -727,6 +955,24 @@ class JobList extends Component {
727
955
  />
728
956
  );
729
957
  }
958
+
959
+ if (this.state.selectedPriorityFilter) {
960
+ priorityFilter = (
961
+ <Components.Tag
962
+ className="marginRight-10"
963
+ onClick={() => {
964
+ this.openFilter('priority');
965
+ }}
966
+ rightIcon="close"
967
+ rightClick={(e) => {
968
+ e.stopPropagation();
969
+ this.selectPriorityFilter();
970
+ }}
971
+ text={this.state.selectedPriorityFilter}
972
+ />
973
+ );
974
+ }
975
+
730
976
  if (this.state.selectedStatusFilter) {
731
977
  statusFilter = (
732
978
  <Components.Tag
@@ -739,7 +985,7 @@ class JobList extends Component {
739
985
  e.stopPropagation();
740
986
  this.selectStatusFilter();
741
987
  }}
742
- text={this.state.selectedStatusFilter === 'incomplete' ? 'All Incomplete' : StatusTypes[this.state.selectedStatusFilter].text}
988
+ text={this.state.selectedStatusFilter}
743
989
  />
744
990
  );
745
991
  }
@@ -775,6 +1021,22 @@ class JobList extends Component {
775
1021
  />
776
1022
  );
777
1023
  }
1024
+ if (this.state.selectedRequesterFilter) {
1025
+ requesterFilter = (
1026
+ <Components.Tag
1027
+ className="marginRight-10"
1028
+ onClick={() => {
1029
+ this.openFilter('requester');
1030
+ }}
1031
+ rightIcon="close"
1032
+ rightClick={(e) => {
1033
+ e.stopPropagation();
1034
+ this.removeRequesterFilter();
1035
+ }}
1036
+ text={this.state.selectedRequesterFilterText}
1037
+ />
1038
+ );
1039
+ }
778
1040
  return (
779
1041
  <div>
780
1042
  <div className="marginTop-20 flex flex-between flex-center">
@@ -784,8 +1046,10 @@ class JobList extends Component {
784
1046
  </Components.Text>
785
1047
  {typeFilter}
786
1048
  {statusFilter}
1049
+ {priorityFilter}
787
1050
  {timeFilter}
788
1051
  {userFilter}
1052
+ {requesterFilter}
789
1053
  </div>
790
1054
  <Components.Button
791
1055
  inline
@@ -837,7 +1101,8 @@ const mapStateToProps = (state) => {
837
1101
  jobs: state[values.reducerKey].jobs,
838
1102
  auth,
839
1103
  strings: (state.strings && state.strings.config) || {},
1104
+ statusTypes: state[values.reducerKey].jobstatuses,
840
1105
  };
841
1106
  };
842
1107
 
843
- export default connect(mapStateToProps, { jobsLoaded, removeJob })(withRouter(JobList));
1108
+ export default connect(mapStateToProps, { jobsLoaded, removeJob, jobStatusesUpdate })(withRouter(JobList));
@@ -0,0 +1,26 @@
1
+ import jobStatusOptions from '../maintenanceStatus.json';
2
+ import jobPriorityOptions from '../maintenancePriority.json';
3
+
4
+ const STATUS_IMCOMPLETE = 'All Incomplete';
5
+ const STATUS_NOT_ACTIONED = 'Not Actioned';
6
+ const STATUS_IN_PROGRESS = 'In Progress';
7
+ const STATUS_COMPLETED = 'Completed';
8
+
9
+ const getDefaultPriority = () => jobPriorityOptions.find((p) => p.default);
10
+
11
+ const getJobPriority = (priority) => {
12
+ let priorityOption = null;
13
+ if (priority) priorityOption = jobPriorityOptions.find((p) => p.name === priority);
14
+ return priorityOption || getDefaultPriority();
15
+ };
16
+
17
+ export {
18
+ STATUS_IMCOMPLETE,
19
+ STATUS_NOT_ACTIONED,
20
+ STATUS_IN_PROGRESS,
21
+ STATUS_COMPLETED,
22
+ jobStatusOptions,
23
+ jobPriorityOptions,
24
+ getDefaultPriority,
25
+ getJobPriority,
26
+ };
@@ -0,0 +1,5 @@
1
+ [
2
+ { "name": "Low", "color": "green", "default": true },
3
+ { "name": "Medium", "color": "orange" },
4
+ { "name": "High", "color": "red" }
5
+ ]
@@ -1,17 +1,20 @@
1
- {
2
- "Unassigned": {
1
+ [
2
+ {
3
3
  "text": "Open",
4
4
  "order": 0,
5
- "color": "#d5dde4"
5
+ "color": "#d5dde4",
6
+ "category": "Not Actioned"
6
7
  },
7
- "In Progress": {
8
+ {
8
9
  "text": "In Progress",
9
10
  "order": 1,
10
- "color": "#82d6e5"
11
+ "color": "#82d6e5",
12
+ "category": "In Progress"
11
13
  },
12
- "Completed": {
14
+ {
13
15
  "text": "Completed",
14
16
  "order": 2,
15
- "color": "#58dba4"
17
+ "color": "#58dba4",
18
+ "category": "Completed"
16
19
  }
17
- }
20
+ ]
@@ -1,20 +1,13 @@
1
1
  import _ from 'lodash';
2
-
3
- import {
4
- JOBS_LOADED,
5
- JOBS_REMOVED,
6
- JOBS_LOADING,
7
- JOBS_TYPES_LOADED,
8
- // JOBS_TYPES_REMOVED,
9
- // JOBS_PURGE,
10
- // TICKETS_LOADED,
11
- } from '../actions/types';
2
+ import { JOBS_LOADED, JOBS_REMOVED, JOBS_LOADING, JOBS_TYPES_LOADED, JOBS_STATUSES_LOADED, JOBS_HIDE_SEEN } from '../actions/types';
3
+ import { jobStatusOptions } from '../helper';
12
4
 
13
5
  const INITIAL_STATE = {
14
6
  jobs: [],
15
7
  jobtypes: [],
8
+ jobstatuses: jobStatusOptions,
16
9
  loading: false,
17
- // tickets: [],
10
+ hideSeen: false,
18
11
  };
19
12
 
20
13
  export default (state = INITIAL_STATE, action) => {
@@ -41,33 +34,15 @@ export default (state = INITIAL_STATE, action) => {
41
34
  }
42
35
  return state;
43
36
  case JOBS_TYPES_LOADED:
44
- const resultSub = _.unionWith(action.payload, state.jobtypes, (v1, v2) => {
37
+ const jobtypes = _.unionWith(action.payload, state.jobtypes, (v1, v2) => {
45
38
  return v1 != null && v2 != null && v1.id === v2.id;
46
39
  });
47
- return {
48
- ...state,
49
- jobtypes: resultSub,
50
- };
51
- // case JOBS_TYPES_REMOVED:
52
- // const indexSub = _.findIndex(state.jobtypes, (event) => {
53
- // return event != null && event.id === action.payload;
54
- // });
55
- // if (indexSub > -1) {
56
- // const newEventsSub = [...state.jobtypes];
57
- // newEventsSub.splice(indexSub, 1);
58
- // return { ...state, jobtypes: newEventsSub };
59
- // }
60
- // return state;
61
- // case TICKETS_LOADED:
62
- // const ticketResult = _.unionWith(action.payload, state.tickets, (v1, v2) => {
63
- // return v1 != null && v2 != null && v1.Id === v2.Id;
64
- // });
65
- // return {
66
- // ...state,
67
- // tickets: ticketResult,
68
- // };
69
- // case JOBS_PURGE:
70
- // return INITIAL_STATE;
40
+ return { ...state, jobtypes };
41
+ case JOBS_STATUSES_LOADED:
42
+ const jobstatuses = _.orderBy(action.payload, 'order', 'asc');
43
+ return { ...state, jobstatuses };
44
+ case JOBS_HIDE_SEEN:
45
+ return { ...state, hideSeen: action.payload };
71
46
  default:
72
47
  return state;
73
48
  }