@plusscommunities/pluss-maintenance-app 2.1.4 → 2.2.1-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,2 +1,3 @@
1
1
  export const JOBS_LOADED = 'JOBS_LOADED';
2
2
  export const JOB_ADDED = 'JOB_ADDED';
3
+ export const JOBS_ADDED = 'JOBS_ADDED';
@@ -4,18 +4,25 @@
4
4
  import { Helper, Session } from '../core.config';
5
5
 
6
6
  export const generalActions = {
7
- getJob: (site, jobId) => {
7
+ getJob: (site, id) => {
8
+ return Session.authedFunction({
9
+ method: 'POST',
10
+ url: Helper.getUrl('maintenance', 'getJob'),
11
+ data: { site, id },
12
+ });
13
+ },
14
+ getJobByJobId: (site, jobId) => {
8
15
  return Session.authedFunction({
9
16
  method: 'POST',
10
17
  url: Helper.getUrl('maintenance', 'getJob'),
11
18
  data: { site, jobId },
12
19
  });
13
20
  },
14
- getJobs: site => {
21
+ getJobs: (site, status = '', type = '') => {
15
22
  return Session.authedFunction({
16
23
  method: 'POST',
17
24
  url: Helper.getUrl('maintenance', 'getJobs'),
18
- data: { site },
25
+ data: { site, status, type },
19
26
  });
20
27
  },
21
28
  sendMaintenanceRequest: (userID, userName, phone, room, title, description, date, type, images, location, isHome, homeText) => {
@@ -0,0 +1,224 @@
1
+ import React, { Component } from 'react';
2
+ import { View, Text, TouchableOpacity, Modal } from 'react-native';
3
+ import { connect } from 'react-redux';
4
+ import _ from 'lodash';
5
+ import { generalActions } from '../apis';
6
+ import { Colours, Helper } from '../core.config';
7
+
8
+ class FilterPopupMenu extends Component {
9
+ constructor(props) {
10
+ super(props);
11
+
12
+ this.state = {
13
+ types: props.types || [],
14
+ selectedStatus: props.status || '',
15
+ selectedType: props.type || '',
16
+ };
17
+ this.statusOptions = [
18
+ {
19
+ label: 'All',
20
+ value: '',
21
+ },
22
+ {
23
+ label: 'Incomplete',
24
+ value: 'Unassigned|In Progress',
25
+ },
26
+ {
27
+ label: 'Unassigned',
28
+ value: 'Unassigned',
29
+ },
30
+ {
31
+ label: 'In Progress',
32
+ value: 'In Progress',
33
+ },
34
+ {
35
+ label: 'Completed',
36
+ value: 'Completed',
37
+ },
38
+ ];
39
+ }
40
+
41
+ componentDidMount() {
42
+ if (_.isEmpty(this.state.types)) this.refreshTypes();
43
+ }
44
+
45
+ componentDidUpdate(prevProps) {
46
+ if (prevProps.site !== this.props.site) {
47
+ this.refreshTypes();
48
+ }
49
+ }
50
+
51
+ refreshTypes = async () => {
52
+ const { data } = await generalActions.getJobTypes(Helper.getSite(this.props.site));
53
+ const types = data.map(t => {
54
+ return { label: t.typeName, value: t.typeName };
55
+ });
56
+ types.splice(0, 0, { label: 'All', value: '' });
57
+ // console.log('refreshTypes', types);
58
+ this.setState({ types });
59
+ };
60
+
61
+ onSelectOption = (key, value) => {
62
+ const newState = {};
63
+ newState[key] = value;
64
+ this.setState(newState);
65
+ };
66
+
67
+ onDone = () => {
68
+ const { onClose } = this.props;
69
+ const { selectedStatus, selectedType } = this.state;
70
+ if (onClose)
71
+ onClose({
72
+ status: selectedStatus,
73
+ type: selectedType,
74
+ });
75
+ };
76
+
77
+ renderTitle() {
78
+ const { title } = this.props;
79
+ return (
80
+ <View style={styles.titleContainer}>
81
+ <Text style={styles.titleText}>{title || 'Filter By'}</Text>
82
+ </View>
83
+ );
84
+ }
85
+
86
+ renderCancel() {
87
+ const { colourBrandingMain, cancelText } = this.props;
88
+ return (
89
+ <TouchableOpacity onPress={this.onDone}>
90
+ <View style={styles.cancelContainer}>
91
+ <Text style={[styles.cancelText, { color: colourBrandingMain }]}>{cancelText || 'Done'}</Text>
92
+ </View>
93
+ </TouchableOpacity>
94
+ );
95
+ }
96
+
97
+ renderOptions(title, options, selectedKey) {
98
+ const { colourBrandingMain } = this.props;
99
+ return (
100
+ <View style={styles.optionsContainer}>
101
+ <Text style={styles.optionsTitle}>{title}</Text>
102
+ <View style={styles.options}>
103
+ {options.map(o => {
104
+ const selected = this.state[selectedKey];
105
+ const backgroundColor = o.value === selected ? colourBrandingMain : '#fff';
106
+ const color = o.value === selected ? '#fff' : colourBrandingMain;
107
+ return (
108
+ <TouchableOpacity key={o.label} onPress={() => this.onSelectOption(selectedKey, o.value)}>
109
+ <View style={[styles.optionContainer, { backgroundColor, borderColor: colourBrandingMain }]}>
110
+ <Text style={[styles.optionText, { color }]}>{o.label}</Text>
111
+ </View>
112
+ </TouchableOpacity>
113
+ );
114
+ })}
115
+ </View>
116
+ </View>
117
+ );
118
+ }
119
+
120
+ render() {
121
+ return (
122
+ <Modal visible transparent animationType="slide" onRequestClose={this.onDone}>
123
+ <View style={styles.container}>
124
+ <View style={styles.menu}>
125
+ {this.renderTitle()}
126
+ <View style={styles.optionContent}>
127
+ {this.renderOptions('Status', this.statusOptions, 'selectedStatus')}
128
+ {this.renderOptions('Type', this.state.types, 'selectedType')}
129
+ </View>
130
+ {this.renderCancel()}
131
+ </View>
132
+ </View>
133
+ </Modal>
134
+ );
135
+ }
136
+ }
137
+
138
+ const styles = {
139
+ container: {
140
+ position: 'absolute',
141
+ bottom: 0,
142
+ left: 0,
143
+ right: 0,
144
+ top: 0,
145
+ backgroundColor: 'rgba(0,0,0,0.5)',
146
+ zIndex: 1000,
147
+ },
148
+ menu: {
149
+ position: 'absolute',
150
+ bottom: 0,
151
+ left: 0,
152
+ right: 0,
153
+ backgroundColor: '#fff',
154
+ borderTopLeftRadius: 12,
155
+ borderTopRightRadius: 12,
156
+ },
157
+ cancelContainer: {
158
+ paddingVertical: 16,
159
+ borderColor: Colours.LINEGREY,
160
+ borderTopWidth: 1,
161
+ paddingHorizontal: 16,
162
+ },
163
+ cancelText: {
164
+ fontFamily: 'sf-medium',
165
+ fontSize: 16,
166
+ textAlign: 'center',
167
+ color: Colours.TEXT_DARK,
168
+ },
169
+ titleContainer: {
170
+ padding: 16,
171
+ borderColor: Colours.LINEGREY,
172
+ borderBottomWidth: 1,
173
+ },
174
+ titleText: {
175
+ fontFamily: 'sf-semibold',
176
+ fontSize: 16,
177
+ textAlign: 'center',
178
+ color: Colours.TEXT_DARK,
179
+ },
180
+ optionContent: {
181
+ paddingVertical: 10,
182
+ },
183
+ optionsContainer: {
184
+ marginHorizontal: 14,
185
+ },
186
+ optionsTitle: {
187
+ fontFamily: 'sf-semibold',
188
+ marginBottom: 4,
189
+ },
190
+ options: {
191
+ flex: 1,
192
+ flexDirection: 'row',
193
+ flexWrap: 'wrap',
194
+ },
195
+ optionContainer: {
196
+ flexDirection: 'row',
197
+ alignItems: 'center',
198
+ justifyContent: 'center',
199
+ minWidth: 50,
200
+ height: 30,
201
+ paddingHorizontal: 8,
202
+ borderRadius: 4,
203
+ borderWidth: 1,
204
+ marginRight: 8,
205
+ marginBottom: 8,
206
+ },
207
+ optionText: {
208
+ color: '#fff',
209
+ textAlign: 'center',
210
+ fontFamily: 'sf-semibold',
211
+ fontSize: 13,
212
+ },
213
+ };
214
+
215
+ const mapStateToProps = state => {
216
+ const { user } = state;
217
+
218
+ return {
219
+ site: user.site,
220
+ colourBrandingMain: Colours.getMainBrandingColourFromState(state),
221
+ };
222
+ };
223
+
224
+ export default connect(mapStateToProps, {})(FilterPopupMenu);
@@ -1,44 +1,52 @@
1
1
  import React, { Component } from 'react';
2
2
  import { View, StyleSheet, FlatList, TouchableOpacity, Text } from 'react-native';
3
- import { Icon } from 'react-native-elements';
4
3
  import _ from 'lodash';
5
4
  import { connect } from 'react-redux';
6
5
  import { generalActions } from '../apis';
7
- import { jobsLoaded } from '../actions';
6
+ import { jobsLoaded, jobAdded, jobsAdded } from '../actions';
8
7
  import MaintenanceListItem from '../components/MaintenanceListItem';
9
- import StatusSelectorPopup from './StatusSelectorPopup';
10
- import { Components, Colours, Config } from '../core.config';
11
-
12
- const SHOW_ALL_STATUS = 'Show All';
8
+ import FilterPopupMenu from './FilterPopupMenu';
9
+ import { Components, Colours, Config, Helper } from '../core.config';
13
10
 
14
11
  class MaintenanceList extends Component {
15
12
  constructor(props) {
16
13
  super(props);
17
14
 
18
15
  this.state = {
16
+ types: [],
17
+ filteredList: props.jobs,
19
18
  loading: false,
20
- selectedStatus: SHOW_ALL_STATUS,
21
- showStatusPopup: false,
19
+ searchText: '',
20
+ showFilterPopup: false,
21
+ selectedStatus: props.hasPermission ? 'Unassigned|In Progress' : '',
22
+ selectedType: '',
22
23
  };
23
24
  }
24
25
 
25
26
  componentDidMount() {
26
27
  this.refresh();
28
+ this.refreshTypes();
29
+
30
+ this.resetDataSource();
27
31
  }
28
32
 
29
33
  componentDidUpdate(prevProps) {
30
- if (!_.isEqual(prevProps.statuses, this.props.statuses) && !this.props.statuses.includes(this.state.selectedStatus)) {
31
- // Reset selected status if not exists
32
- this.setState({ selectedStatus: SHOW_ALL_STATUS });
33
- }
34
34
  if (!prevProps.dataUpdated && this.props.dataUpdated) this.refresh();
35
+ if (!_.isEqual(prevProps.jobs, this.props.jobs)) this.resetDataSource();
35
36
  }
36
37
 
37
38
  refresh = () => {
38
39
  this.onLoadingChanged(true, async () => {
39
40
  try {
40
- const res = await generalActions.getJobs(this.props.site);
41
- this.props.jobsLoaded(res.data);
41
+ const { selectedStatus, selectedType } = this.state;
42
+ // console.log('filters', { selectedStatus, selectedType });
43
+ const res = await generalActions.getJobs(this.props.site, selectedStatus, selectedType);
44
+ // console.log('refresh', res?.data);
45
+ if (selectedStatus || selectedType) {
46
+ this.props.jobsAdded(res.data);
47
+ } else {
48
+ this.props.jobsLoaded(res.data);
49
+ }
42
50
  } catch (error) {
43
51
  console.log('refresh error', error);
44
52
  } finally {
@@ -47,6 +55,32 @@ class MaintenanceList extends Component {
47
55
  });
48
56
  };
49
57
 
58
+ refreshTypes = async () => {
59
+ const { data } = await generalActions.getJobTypes(Helper.getSite(this.props.site));
60
+ const types = data.map(t => {
61
+ return { label: t.typeName, value: t.typeName };
62
+ });
63
+ types.splice(0, 0, { label: 'All', value: '' });
64
+ // console.log('refreshTypes', types);
65
+ this.setState({ types });
66
+ };
67
+
68
+ fetchJob = jobId => {
69
+ if (this.state.loading) return;
70
+
71
+ this.onLoadingChanged(true, async () => {
72
+ try {
73
+ const job = await generalActions.getJobByJobId(this.props.site, jobId);
74
+ // console.log('fetchJob', job?.data);
75
+ this.props.jobAdded(job.data);
76
+ } catch (error) {
77
+ console.log('fetchJob error', error);
78
+ } finally {
79
+ this.onLoadingChanged(false);
80
+ }
81
+ });
82
+ };
83
+
50
84
  getEmptyStateText() {
51
85
  if (this.props.options && !_.isEmpty(this.props.options.EmptyText)) {
52
86
  return this.props.options.EmptyText;
@@ -54,6 +88,23 @@ class MaintenanceList extends Component {
54
88
  return this.props.userCategory === 'staff' ? Config.env.strings.EMPTY_REQUESTS_STAFF : Config.env.strings.EMPTY_REQUESTS_USER;
55
89
  }
56
90
 
91
+ resetDataSource = (source = '') => {
92
+ const { searchText, selectedStatus, selectedType } = this.state;
93
+ const { jobs } = this.props;
94
+
95
+ let filteredList = jobs;
96
+ if (searchText) {
97
+ filteredList = jobs.filter(j => j.jobId === searchText);
98
+ if (filteredList.length === 0) this.fetchJob(searchText);
99
+ } else if (source !== 'search') {
100
+ if (selectedStatus) filteredList = filteredList.filter(j => selectedStatus.includes(j.status));
101
+ if (selectedType) filteredList = filteredList.filter(j => selectedType.includes(j.type));
102
+ this.refresh();
103
+ }
104
+
105
+ this.setState({ filteredList });
106
+ };
107
+
57
108
  onLoadingChanged = (loading, callback) => {
58
109
  this.setState({ loading }, () => {
59
110
  if (this.props.onLoadingChanged) this.props.onLoadingChanged(this.state.loading);
@@ -61,36 +112,56 @@ class MaintenanceList extends Component {
61
112
  });
62
113
  };
63
114
 
64
- onPressFilter = () => {
65
- this.setState({ showStatusPopup: true });
115
+ onSearchText = value => {
116
+ this.setState({ searchText: value }, () => {
117
+ if (_.isEmpty(this.state.searchText)) this.resetDataSource('search');
118
+ });
66
119
  };
67
120
 
68
- onCloseFilter = () => {
69
- this.setState({ showStatusPopup: false });
121
+ onSearchSubmit = () => {
122
+ this.resetDataSource('search');
70
123
  };
71
124
 
72
- onSelectStatus = selectedStatus => {
73
- this.setState({ showStatusPopup: false, selectedStatus });
125
+ onToggleFilter = () => {
126
+ this.setState({ showFilterPopup: !this.state.showFilterPopup });
127
+ };
128
+
129
+ onSelectFilter = selected => {
130
+ this.setState({ selectedStatus: selected.status, selectedType: selected.type }, () => {
131
+ this.resetDataSource();
132
+ this.onToggleFilter();
133
+ });
74
134
  };
75
135
 
76
136
  renderEmptyList() {
77
137
  return this.state.loading ? null : <Components.EmptyStateMain title={this.getEmptyStateText()} style={{ marginHorizontal: 16 }} />;
78
138
  }
79
139
 
80
- renderFilter() {
81
- if (this.props.statuses?.length <= 1) return;
140
+ renderFilterButton() {
141
+ return (
142
+ <TouchableOpacity onPress={this.onToggleFilter}>
143
+ <View style={styles.filterButton}>
144
+ <Text style={[styles.filterButtonText, { color: this.props.colourBrandingMain }]}>Filter</Text>
145
+ </View>
146
+ </TouchableOpacity>
147
+ );
148
+ }
149
+
150
+ renderSearch() {
151
+ if (!this.props.hasPermission) return null;
82
152
 
83
153
  return (
84
- <View style={styles.filterContainerOuter}>
85
- <Text style={styles.filterTitle}>FILTER</Text>
86
- <TouchableOpacity onPress={this.onPressFilter}>
87
- <View style={styles.filterContainer}>
88
- <Text style={[styles.filterText, { color: this.props.colourBrandingMain }]}>
89
- {this.state.selectedStatus || SHOW_ALL_STATUS}
90
- </Text>
91
- <Icon name="angle-down" type="font-awesome" iconStyle={[styles.filterIcon, { color: this.props.colourBrandingMain }]} />
92
- </View>
93
- </TouchableOpacity>
154
+ <View style={styles.searchContainer}>
155
+ <Components.GenericInput
156
+ placeholder="Search by Job ID"
157
+ value={this.state.searchText}
158
+ onChangeText={this.onSearchText}
159
+ onSubmitEditing={this.onSearchSubmit}
160
+ squaredCorners
161
+ hasClear
162
+ keyboardType={'numeric'}
163
+ returnKeyType={'done'}
164
+ />
94
165
  </View>
95
166
  );
96
167
  }
@@ -99,15 +170,14 @@ class MaintenanceList extends Component {
99
170
  return (
100
171
  <View>
101
172
  {this.props.ListHeaderComponent}
102
- {this.renderFilter()}
173
+ {this.renderFilterButton()}
174
+ {this.renderSearch()}
103
175
  </View>
104
176
  );
105
177
  }
106
178
 
107
179
  renderList() {
108
- const { selectedStatus } = this.state;
109
- const { jobs } = this.props;
110
- const filteredList = selectedStatus === SHOW_ALL_STATUS ? jobs : jobs.filter(job => job.status === selectedStatus);
180
+ const { filteredList } = this.state;
111
181
 
112
182
  return (
113
183
  <FlatList
@@ -123,21 +193,12 @@ class MaintenanceList extends Component {
123
193
  );
124
194
  }
125
195
 
126
- // renderAddButton() {
127
- // return this.props.enableAdd ? <Components.AddContentButton /> : null;
128
- // }
129
-
130
- renderStatusPopup() {
131
- if (!this.state.showStatusPopup) return null;
196
+ renderFilterPopup() {
197
+ const { showFilterPopup, types, selectedStatus, selectedType } = this.state;
198
+ if (!showFilterPopup) return null;
132
199
 
133
200
  return (
134
- <StatusSelectorPopup
135
- filter={this.props.statuses}
136
- includeAll
137
- allText={SHOW_ALL_STATUS}
138
- onClose={this.onCloseFilter}
139
- onSelect={this.onSelectStatus}
140
- />
201
+ <FilterPopupMenu site={this.props.site} types={types} status={selectedStatus} type={selectedType} onClose={this.onSelectFilter} />
141
202
  );
142
203
  }
143
204
 
@@ -145,8 +206,7 @@ class MaintenanceList extends Component {
145
206
  return (
146
207
  <View style={[styles.container, this.props.style]}>
147
208
  {this.renderList()}
148
- {/* {this.renderAddButton()} */}
149
- {/* {this.renderStatusPopup()} */}
209
+ {this.renderFilterPopup()}
150
210
  </View>
151
211
  );
152
212
  }
@@ -182,16 +242,31 @@ const styles = StyleSheet.create({
182
242
  filterIcon: {
183
243
  fontSize: 20,
184
244
  },
245
+ searchContainer: {
246
+ flexDirection: 'row',
247
+ alignItems: 'center',
248
+ paddingBottom: 8,
249
+ paddingHorizontal: 16,
250
+ },
251
+ filterButton: {
252
+ position: 'absolute',
253
+ right: 20,
254
+ top: -32,
255
+ },
256
+ filterButtonText: {
257
+ fontFamily: 'sf-semibold',
258
+ fontSize: 16,
259
+ },
185
260
  });
186
261
 
187
262
  const mapStateToProps = state => {
188
263
  const { user, jobs, notifications } = state;
189
264
  const jobsOrdered = _.orderBy(jobs.jobs, ['createdUnix'], ['desc']);
190
- const statuses = _.uniq(jobsOrdered.map(job => job.status));
265
+ const hasPermission = _.includes(user.permissions, 'maintenanceTracking');
191
266
 
192
267
  return {
268
+ hasPermission,
193
269
  jobs: jobsOrdered,
194
- statuses,
195
270
  site: user.site,
196
271
  userCategory: user.category,
197
272
  colourBrandingMain: Colours.getMainBrandingColourFromState(state),
@@ -199,4 +274,4 @@ const mapStateToProps = state => {
199
274
  };
200
275
  };
201
276
 
202
- export default connect(mapStateToProps, { jobsLoaded }, null, { forwardRef: true })(MaintenanceList);
277
+ export default connect(mapStateToProps, { jobsLoaded, jobAdded, jobsAdded }, null, { forwardRef: true })(MaintenanceList);
@@ -72,7 +72,9 @@ class MaintenanceListItem extends Component {
72
72
  <View style={styles.jobInnerContainer}>
73
73
  <View style={styles.jobTopSection}>
74
74
  <View style={styles.jobTopLeft}>
75
+ {job.jobId ? <Text style={[styles.jobIdText, { color: this.props.colourBrandingMain }]}>{`Job #${job.jobId}`}</Text> : null}
75
76
  <Text style={styles.jobTitleText}>{job.title}</Text>
77
+ {job.room ? <Text style={styles.jobLocationText}>{job.room}</Text> : null}
76
78
  <View style={styles.jobTypeSeenContainer}>
77
79
  <View style={[styles.jobTypeContainer, { backgroundColor: Colours.hexToRGBAstring(this.props.colourBrandingMain, 0.2) }]}>
78
80
  <Text style={[styles.jobTypeText, { color: this.props.colourBrandingMain }]}>{job.type}</Text>
@@ -117,22 +119,34 @@ const styles = StyleSheet.create({
117
119
  },
118
120
  jobTopSection: {
119
121
  flexDirection: 'row',
120
- alignItems: 'center',
122
+ alignItems: 'flex-end',
121
123
  paddingHorizontal: 10,
122
- paddingVertical: 16,
124
+ paddingVertical: 10,
123
125
  borderBottomWidth: 1,
124
126
  borderBottomColor: Colours.LINEGREY,
125
127
  },
126
128
  jobTopLeft: {
127
129
  flex: 1,
128
130
  },
131
+ jobIdText: {
132
+ fontFamily: 'sf-medium',
133
+ fontSize: 12,
134
+ marginBottom: 4,
135
+ },
129
136
  jobTitleText: {
130
137
  fontFamily: 'sf-semibold',
131
138
  fontSize: 18,
132
139
  color: Colours.TEXT_DARK,
133
- marginBottom: 8,
140
+ marginBottom: 4,
141
+ },
142
+ jobLocationText: {
143
+ fontFamily: 'sf-medium',
144
+ fontSize: 12,
145
+ color: Colours.TEXT_LIGHT,
146
+ marginBottom: 4,
134
147
  },
135
148
  jobTypeSeenContainer: {
149
+ marginTop: 4,
136
150
  flexDirection: 'row',
137
151
  alignItems: 'center',
138
152
  },
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-param-reassign */
2
+ import _ from 'lodash';
2
3
  import { REHYDRATE } from 'redux-persist';
3
- import { JOBS_LOADED, JOB_ADDED } from '../actions/types';
4
+ import { JOBS_LOADED, JOB_ADDED, JOBS_ADDED } from '../actions/types';
4
5
  import { ActionTypes } from '../core.config';
5
6
 
6
7
  const REDUCER_KEY = 'jobs';
@@ -19,6 +20,12 @@ export default (state = INITIAL_STATE, action) => {
19
20
  return INITIAL_STATE;
20
21
  case JOBS_LOADED:
21
22
  return { ...state, jobs: action.payload.map(job => ({ title: job.title || job.description, ...job })) };
23
+ case JOBS_ADDED:
24
+ updateJobs = action.payload.map(job => ({ title: job.title || job.description, ...job }));
25
+ updateJobs = _.unionWith(updateJobs, state.jobs, (j1, j2) => {
26
+ return j1.id === j2.id;
27
+ });
28
+ return { ...state, jobs: updateJobs };
22
29
  case JOB_ADDED:
23
30
  updateJobs = [...state.jobs];
24
31
  index = updateJobs.findIndex(item => item.id === action.payload.id);