@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-maintenance-web-a",
3
- "version": "1.1.23",
3
+ "version": "1.1.24",
4
4
  "description": "Extension package to enable maintenance on Pluss Communities Platform",
5
5
  "main": "dist/index.cjs.js",
6
6
  "scripts": {
@@ -37,7 +37,7 @@
37
37
  "@babel/runtime": "^7.14.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "@plusscommunities/pluss-core-web": "^1.4.33",
40
+ "@plusscommunities/pluss-core-web": "1.6.6",
41
41
  "@fortawesome/fontawesome-svg-core": "^6.4.0",
42
42
  "@fortawesome/free-solid-svg-icons": "^6.4.0",
43
43
  "@fortawesome/react-fontawesome": "^0.2.0",
@@ -1,14 +1,10 @@
1
- import _, { rest } from 'lodash';
2
- import {
3
- JOBS_LOADED,
4
- JOBS_REMOVED,
5
- JOBS_LOADING,
6
- JOBS_TYPES_LOADED,
7
- //JOBS_TYPES_REMOVED, JOBS_PURGE, TICKETS_LOADED
8
- } from './types';
9
- import { maintenanceActions } from '../apis';
10
-
1
+ import _ from 'lodash';
2
+ import { JOBS_LOADED, JOBS_REMOVED, JOBS_LOADING, JOBS_TYPES_LOADED, JOBS_STATUSES_LOADED, JOBS_HIDE_SEEN } from './types';
3
+ import { maintenanceActions, stringActions } from '../apis';
4
+ import DefaultStatusTypes from '../maintenanceStatus.json';
11
5
  import { PlussCore } from '../feature.config';
6
+ import { values } from '../values.config';
7
+
12
8
  const { Helper } = PlussCore;
13
9
 
14
10
  export const jobsUpdate = (site, isdashboard) => {
@@ -64,6 +60,62 @@ export const jobTypesLoaded = (events) => {
64
60
  };
65
61
  };
66
62
 
63
+ export const jobStatusesUpdate = (site, callback = null) => {
64
+ return (dispatch) => {
65
+ stringActions
66
+ .getString(site, values.stringConfigJobStatus)
67
+ .then((res) => {
68
+ dispatch({
69
+ type: JOBS_STATUSES_LOADED,
70
+ payload: res.data,
71
+ });
72
+ if (callback) callback(res.data);
73
+ })
74
+ .catch((_error) => {
75
+ dispatch({
76
+ type: JOBS_STATUSES_LOADED,
77
+ payload: DefaultStatusTypes,
78
+ });
79
+ if (callback) callback(DefaultStatusTypes);
80
+ });
81
+ };
82
+ };
83
+
84
+ export const jobStatusesLoaded = (statuses) => {
85
+ return {
86
+ type: JOBS_STATUSES_LOADED,
87
+ payload: statuses,
88
+ };
89
+ };
90
+
91
+ export const jobHideSeenUpdate = (site, callback = null) => {
92
+ return (dispatch) => {
93
+ stringActions
94
+ .getString(site, values.stringConfigHideSeen)
95
+ .then((res) => {
96
+ dispatch({
97
+ type: JOBS_HIDE_SEEN,
98
+ payload: res.data,
99
+ });
100
+ if (callback) callback(res.data);
101
+ })
102
+ .catch((_error) => {
103
+ dispatch({
104
+ type: JOBS_HIDE_SEEN,
105
+ payload: false,
106
+ });
107
+ if (callback) callback(false);
108
+ });
109
+ };
110
+ };
111
+
112
+ export const jobHideSeenLoaded = (hide) => {
113
+ return {
114
+ type: JOBS_HIDE_SEEN,
115
+ payload: hide,
116
+ };
117
+ };
118
+
67
119
  // export const removeJobType = (id) => {
68
120
  // return {
69
121
  // type: JOBS_TYPES_REMOVED,
@@ -1,6 +1,8 @@
1
1
  import { values } from '../values.config';
2
2
 
3
3
  export const JOBS_LOADED = values.actionJobsLoaded;
4
+ export const JOBS_LOADING = values.actionJobsLoading;
4
5
  export const JOBS_REMOVED = values.actionJobsRemoved;
5
6
  export const JOBS_TYPES_LOADED = values.actionJobsTypesLoaded;
6
- export const JOBS_LOADING = values.actionJobsLoading;
7
+ export const JOBS_STATUSES_LOADED = values.actionJobsStatusesLoaded;
8
+ export const JOBS_HIDE_SEEN = values.actionJobsHideSeen;
package/src/apis/index.js CHANGED
@@ -4,6 +4,7 @@ const { Apis } = PlussCore;
4
4
 
5
5
  export const analyticsActions = Apis.analyticsActions;
6
6
  export const userActions = Apis.userActions;
7
+ export const stringActions = Apis.stringActions;
7
8
 
8
9
  export * from './maintenanceActions';
9
10
  export * from './reactionActions';
@@ -99,6 +99,13 @@ export const maintenanceActions = {
99
99
  data: { id, status },
100
100
  });
101
101
  },
102
+ editJobPriority: (id, priority) => {
103
+ return Session.authedFunction({
104
+ method: 'POST',
105
+ url: Helper.getUrl(values.serviceKey, 'update/priority'),
106
+ data: { id, priority },
107
+ });
108
+ },
102
109
  assignJob: (jobId, userId) => {
103
110
  return Session.authedFunction({
104
111
  method: 'POST',
@@ -115,7 +122,7 @@ export const maintenanceActions = {
115
122
  url: Helper.getUrl(values.serviceKey, 'get/assignees', { site }),
116
123
  });
117
124
  },
118
- addNote: (jobId, note, attachments) => {
125
+ addNote: (jobId, note, attachments, images) => {
119
126
  return Session.authedFunction({
120
127
  method: 'POST',
121
128
  url: Helper.getUrl(values.serviceKey, 'requests/note'),
@@ -123,11 +130,12 @@ export const maintenanceActions = {
123
130
  id: jobId,
124
131
  note,
125
132
  attachments,
133
+ images,
126
134
  action: 'AddNote',
127
135
  },
128
136
  });
129
137
  },
130
- editNote: (jobId, noteId, note, attachments) => {
138
+ editNote: (jobId, noteId, note, attachments, images) => {
131
139
  return Session.authedFunction({
132
140
  method: 'POST',
133
141
  url: Helper.getUrl(values.serviceKey, 'requests/note'),
@@ -135,6 +143,7 @@ export const maintenanceActions = {
135
143
  id: jobId,
136
144
  note,
137
145
  attachments,
146
+ images,
138
147
  noteId,
139
148
  action: 'EditNote',
140
149
  },
@@ -0,0 +1,392 @@
1
+ import React, { Component } from 'react';
2
+ import _ from 'lodash';
3
+ import FontAwesome from 'react-fontawesome';
4
+ import { connect } from 'react-redux';
5
+ import { withRouter } from 'react-router';
6
+
7
+ import { stringActions } from '../apis';
8
+ import { jobStatusesUpdate, jobStatusesLoaded, jobHideSeenUpdate, jobHideSeenLoaded } from '../actions';
9
+ import { STATUS_NOT_ACTIONED, STATUS_IN_PROGRESS, STATUS_COMPLETED } from '../helper';
10
+ import { PlussCore } from '../feature.config';
11
+ import { values } from '../values.config';
12
+
13
+ const { Session, Components, Colours } = PlussCore;
14
+
15
+ class Configuration extends Component {
16
+ constructor(props) {
17
+ super(props);
18
+ this.state = {
19
+ success: false,
20
+ submitting: false,
21
+ selectedStatusIndex: -1,
22
+ showStatusPopup: false,
23
+ statusLabel: '',
24
+ statusCategory: STATUS_NOT_ACTIONED,
25
+ statusColour: '',
26
+ fetchingStatusTypes: true,
27
+ statusTypes: this.props.statusTypes,
28
+ fetchingHideSeen: true,
29
+ hideSeen: this.props.hideSeen,
30
+ };
31
+ }
32
+
33
+ UNSAFE_componentWillMount() {
34
+ Session.checkLoggedIn(this);
35
+ }
36
+
37
+ componentDidMount() {
38
+ if (!Session.validateAccess(this.props.auth.site, 'featurePicker', this.props.auth, true)) {
39
+ this.props.history.push('/mastermenu');
40
+ } else {
41
+ this.props.jobStatusesUpdate(this.props.auth.site, (statusTypes) => this.setState({ statusTypes, fetchingStatusTypes: false }));
42
+ this.props.jobHideSeenUpdate(this.props.auth.site, (hideSeen) => this.setState({ hideSeen, fetchingHideSeen: false }));
43
+ }
44
+ }
45
+
46
+ isStatusValid = (statusLabel, statusCategory, statusColour) => {
47
+ if (_.isEmpty(statusLabel)) return false;
48
+ if (_.isEmpty(statusCategory)) return false;
49
+ if (_.isEmpty(statusColour)) return false;
50
+
51
+ return true;
52
+ };
53
+
54
+ validateForm() {
55
+ const { submitting, statusTypes } = this.state;
56
+ if (submitting) return false;
57
+
58
+ // Validate statuses
59
+ const statusesValid = statusTypes.map((status) => {
60
+ const { text, category, color } = status;
61
+ return this.isStatusValid(text, category, color);
62
+ });
63
+ if (!statusesValid.every((valid) => valid)) return false;
64
+
65
+ return true;
66
+ }
67
+
68
+ onMoveStatus = (index, up = true) => {
69
+ if (this.state.fetchingStatusTypes) return;
70
+
71
+ const statusTypes = _.cloneDeep(this.state.statusTypes);
72
+
73
+ if (up && index > 0) {
74
+ // Moving up
75
+ [statusTypes[index - 1], statusTypes[index]] = [statusTypes[index], statusTypes[index - 1]];
76
+ } else if (index < statusTypes.length - 1) {
77
+ // Moving down
78
+ [statusTypes[index], statusTypes[index + 1]] = [statusTypes[index + 1], statusTypes[index]];
79
+ }
80
+ // Reset order
81
+ statusTypes.forEach((status, index) => (status.order = index));
82
+
83
+ this.setState({ statusTypes });
84
+ };
85
+
86
+ onEditStatus = (index) => {
87
+ if (this.state.fetchingStatusTypes) return;
88
+
89
+ const status = this.state.statusTypes[index];
90
+
91
+ this.setState({
92
+ showWarnings: false,
93
+ showStatusPopup: true,
94
+ selectedStatusIndex: index,
95
+ statusLabel: status.text,
96
+ statusCategory: status.category,
97
+ statusColour: status.color,
98
+ });
99
+ };
100
+
101
+ onDeleteStatus = (index) => {
102
+ if (this.state.fetchingStatusTypes) return;
103
+
104
+ const statusTypes = _.cloneDeep(this.state.statusTypes);
105
+ statusTypes.splice(index, 1);
106
+
107
+ this.setState({ statusTypes });
108
+ };
109
+
110
+ onAddStatus = () => {
111
+ const defaultStatus = this.props.statusTypes.find((s) => s.category === STATUS_NOT_ACTIONED);
112
+ this.setState({
113
+ showWarnings: false,
114
+ showStatusPopup: true,
115
+ selectedStatusIndex: -1,
116
+ statusLabel: '',
117
+ statusCategory: STATUS_NOT_ACTIONED,
118
+ statusColour: defaultStatus.color,
119
+ });
120
+ };
121
+
122
+ onSaveStatus = () => {
123
+ const { selectedStatusIndex, statusLabel, statusCategory, statusColour } = this.state;
124
+ const statusTypes = _.cloneDeep(this.state.statusTypes);
125
+
126
+ if (selectedStatusIndex < 0) {
127
+ statusTypes.push({
128
+ text: statusLabel,
129
+ order: statusTypes.length,
130
+ color: statusColour,
131
+ category: statusCategory,
132
+ });
133
+ } else {
134
+ const status = statusTypes[selectedStatusIndex];
135
+ status.text = statusLabel;
136
+ status.color = statusColour;
137
+ status.category = statusCategory;
138
+ }
139
+
140
+ this.setState({ statusTypes, showStatusPopup: false });
141
+ };
142
+
143
+ onHideStatusPopup = () => {
144
+ if (this.state.submitting) return;
145
+
146
+ this.setState({ showStatusPopup: false });
147
+ };
148
+
149
+ onHandleChange = (event) => {
150
+ var stateChange = {};
151
+ stateChange[event.target.getAttribute('id')] = event.target.value;
152
+ this.setState(stateChange);
153
+ };
154
+
155
+ onToggleDisableSeen = () => {
156
+ if (this.state.fetchingHideSeen) return;
157
+
158
+ this.setState({ hideSeen: !this.state.hideSeen });
159
+ };
160
+
161
+ onSaveConfig = () => {
162
+ if (!this.validateForm()) return;
163
+
164
+ this.setState({ success: false, submitting: true }, async () => {
165
+ try {
166
+ const { statusTypes, hideSeen } = this.state;
167
+ await stringActions.setString(this.props.auth.site, values.stringConfigJobStatus, statusTypes);
168
+ await stringActions.setString(this.props.auth.site, values.stringConfigHideSeen, hideSeen);
169
+ this.props.jobStatusesLoaded(statusTypes);
170
+ this.props.jobHideSeenLoaded(hideSeen);
171
+
172
+ this.setState({ success: true });
173
+ } catch (error) {
174
+ console.log('onSaveConfig - error', error);
175
+ this.setState({ success: false });
176
+ } finally {
177
+ this.setState({ submitting: false });
178
+ }
179
+ });
180
+ };
181
+
182
+ renderStatuses() {
183
+ const { statusTypes } = this.state;
184
+ return (
185
+ <div>
186
+ <p className="fontMedium fontSize-36 text-dark">Statuses</p>
187
+ <div style={styles.statusCategoryHeading}>
188
+ <span className="fontMedium fontSize-16 text-bold">Status Category</span>
189
+ </div>
190
+ {statusTypes.map((status, index) => {
191
+ return (
192
+ <div key={`${status.text}_${index}`} style={styles.statusTypeContainer}>
193
+ <div key={status.text} className="statusLabel" style={{ ...styles.statusTextContainer, backgroundColor: status.color }}>
194
+ <span className="statusLabel_text">{status.text}</span>
195
+ </div>
196
+ <div style={styles.statusCategoryContainer}>
197
+ <span className="fontMedium fontSize-16 text-dark">{status.category}</span>
198
+ </div>
199
+ <FontAwesome
200
+ style={{ ...styles.statusIcon, visibility: index === 0 ? 'hidden' : 'visible' }}
201
+ name={'arrow-up'}
202
+ onClick={() => this.onMoveStatus(index, true)}
203
+ />
204
+ <FontAwesome
205
+ style={{ ...styles.statusIcon, visibility: index === statusTypes.length - 1 ? 'hidden' : 'visible' }}
206
+ name={'arrow-down'}
207
+ onClick={() => this.onMoveStatus(index, false)}
208
+ />
209
+ <FontAwesome style={{ ...styles.statusIcon }} name="pencil" onClick={() => this.onEditStatus(index)} />
210
+ <FontAwesome style={{ ...styles.statusIcon }} name="minus-circle" onClick={() => this.onDeleteStatus(index)} />
211
+ </div>
212
+ );
213
+ })}
214
+ <div onClick={this.onAddStatus} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginTop: 16 }}>
215
+ <Components.P60Icon className="addoption_plus" icon="add" style={{ fontSize: 12 }} />
216
+ <p className="addoption_text">Add another status</p>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ renderOtherOptions() {
223
+ return (
224
+ <div className="marginTop-55">
225
+ <p className="fontMedium fontSize-36 text-dark">Other options</p>
226
+ <Components.CheckBox
227
+ label={`Disable whether a request has been "Seen" and prevent that from being surfaced to the App user`}
228
+ isActive={this.state.hideSeen}
229
+ onChange={this.onToggleDisableSeen}
230
+ />
231
+ </div>
232
+ );
233
+ }
234
+
235
+ renderSubmit() {
236
+ if (this.state.submitting) {
237
+ return <Components.Button buttonType="secondary">Saving...</Components.Button>;
238
+ }
239
+ return (
240
+ <Components.Button inline buttonType="primary" onClick={this.onSaveConfig} isActive={this.validateForm()}>
241
+ Save
242
+ </Components.Button>
243
+ );
244
+ }
245
+
246
+ renderSuccess() {
247
+ if (!this.state.success) return null;
248
+ return <span style={{ ...styles.savedText, color: Colours.COLOUR_GREEN }}>Saved</span>;
249
+ }
250
+
251
+ renderNewStatusPopup() {
252
+ const { submitting, showStatusPopup, showWarnings, selectedStatusIndex, statusLabel, statusCategory, statusColour } = this.state;
253
+ if (!showStatusPopup) return null;
254
+
255
+ const canSave = !submitting && this.isStatusValid(statusLabel, statusCategory, statusColour);
256
+ return (
257
+ <Components.Popup
258
+ maxWidth={800}
259
+ minWidth={450}
260
+ hasPadding
261
+ buttons={[
262
+ {
263
+ type: 'primaryAction',
264
+ onClick: this.onSaveStatus,
265
+ isActive: canSave,
266
+ isActive: true,
267
+ text: submitting ? 'Saving...' : 'Save',
268
+ className: 'popupButton',
269
+ },
270
+ {
271
+ type: 'outlinedAction',
272
+ onClick: this.onHideStatusPopup,
273
+ isActive: !submitting,
274
+ text: 'Cancel',
275
+ className: 'popupButton',
276
+ },
277
+ ]}
278
+ onClose={this.onHideStatusPopup}
279
+ title={selectedStatusIndex < 0 ? 'New Status' : 'Edit Status'}
280
+ >
281
+ <Components.GenericInput
282
+ id="statusLabel"
283
+ type="text"
284
+ label="Label"
285
+ placeholder="Status label"
286
+ value={statusLabel}
287
+ onChange={this.onHandleChange}
288
+ isRequired
289
+ isValid={() => !_.isEmpty(statusLabel)}
290
+ showError={() => showWarnings && _.isEmpty(statusLabel)}
291
+ alwaysShowLabel
292
+ className="marginBottom-20"
293
+ />
294
+ <Components.RadioButton
295
+ label="Status Category"
296
+ subLabel={
297
+ <div className="marginBottom-16">
298
+ <div className="marginBottom-4">{'This is used to categorise statuses for filtering and analytics purposes'}</div>
299
+ <div className="text-bold">{'This status is considered'}</div>
300
+ </div>
301
+ }
302
+ className="marginBottom-20"
303
+ rowStyle={{ flexDirection: 'column' }}
304
+ buttonStyle={{ marginBottom: 8 }}
305
+ isActive={statusCategory || null}
306
+ options={[
307
+ {
308
+ Label: STATUS_NOT_ACTIONED,
309
+ Value: STATUS_NOT_ACTIONED,
310
+ onChange: () => this.setState({ statusCategory: STATUS_NOT_ACTIONED }),
311
+ },
312
+ { Label: STATUS_IN_PROGRESS, Value: STATUS_IN_PROGRESS, onChange: () => this.setState({ statusCategory: STATUS_IN_PROGRESS }) },
313
+ { Label: STATUS_COMPLETED, Value: STATUS_COMPLETED, onChange: () => this.setState({ statusCategory: STATUS_COMPLETED }) },
314
+ ]}
315
+ />
316
+
317
+ <Components.ColourOptions
318
+ options={['vibrant', 'picker']}
319
+ defaultTab="vibrant"
320
+ value={statusColour}
321
+ onColourSelected={(statusColour) => this.setState({ statusColour })}
322
+ />
323
+ <div className="marginTop-24">
324
+ <div className="fieldLabel marginBottom-4">Preview</div>
325
+ <Components.Tag text={this.state.statusLabel} style={{ backgroundColor: statusColour, borderColor: statusColour }} />
326
+ </div>
327
+ </Components.Popup>
328
+ );
329
+ }
330
+
331
+ render() {
332
+ return (
333
+ <div style={{ minWidth: '100%' }}>
334
+ {this.renderStatuses()}
335
+ {this.renderOtherOptions()}
336
+ <div style={{ paddingTop: 24, paddingBottom: 24 }}>
337
+ {this.renderSubmit()}
338
+ {this.renderSuccess()}
339
+ </div>
340
+ {this.renderNewStatusPopup()}
341
+ </div>
342
+ );
343
+ }
344
+ }
345
+
346
+ const styles = {
347
+ statusCategoryHeading: {
348
+ marginLeft: 130,
349
+ width: 160,
350
+ textAlign: 'center',
351
+ marginBottom: 12,
352
+ },
353
+ statusTypeContainer: {
354
+ display: 'flex',
355
+ flexDirection: 'row',
356
+ alignItems: 'center',
357
+ marginBottom: 20,
358
+ },
359
+ statusTextContainer: {
360
+ width: 130,
361
+ },
362
+ statusCategoryContainer: {
363
+ width: 160,
364
+ textAlign: 'center',
365
+ },
366
+ statusIcon: {
367
+ cursor: 'pointer',
368
+ fontSize: 20,
369
+ padding: 5,
370
+ marginLeft: 10,
371
+ color: Colours.COLOUR_BRANDING_ACTION,
372
+ visibility: 'visible',
373
+ },
374
+ savedText: {
375
+ fontSize: 14,
376
+ lineHeight: '33px',
377
+ marginLeft: 15,
378
+ },
379
+ };
380
+
381
+ const mapStateToProps = (state) => {
382
+ const { auth } = state;
383
+ return {
384
+ auth,
385
+ statusTypes: state[values.reducerKey].jobstatuses,
386
+ hideSeen: state[values.reducerKey].hideSeen,
387
+ };
388
+ };
389
+
390
+ export default connect(mapStateToProps, { jobStatusesUpdate, jobStatusesLoaded, jobHideSeenUpdate, jobHideSeenLoaded })(
391
+ withRouter(Configuration),
392
+ );