@plusscommunities/pluss-maintenance-app-a 8.0.26 → 8.0.27

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,22 +1,22 @@
1
1
  import React, { Component } from "react";
2
2
  import { Text } from "@plusscommunities/pluss-core-app/components";
3
3
  import {
4
- View,
5
- StyleSheet,
6
- FlatList,
7
- TouchableOpacity,
8
- Animated,
4
+ View,
5
+ StyleSheet,
6
+ FlatList,
7
+ TouchableOpacity,
8
+ Animated,
9
9
  } from "react-native";
10
10
  import _ from "lodash";
11
11
  import { connect } from "react-redux";
12
12
  import { maintenanceActions } from "../apis";
13
13
  import {
14
- jobsLoaded,
15
- jobAdded,
16
- jobsAdded,
17
- jobStatusesUpdate,
18
- jobHideSeenUpdate,
19
- jobsFilterLoaded,
14
+ jobsLoaded,
15
+ jobAdded,
16
+ jobsAdded,
17
+ jobStatusesUpdate,
18
+ jobHideSeenUpdate,
19
+ jobsFilterLoaded,
20
20
  } from "../actions";
21
21
  import MaintenanceListItem from "../components/MaintenanceListItem";
22
22
  import FilterPopupMenu from "./FilterPopupMenu";
@@ -24,597 +24,590 @@ import { Components, Colours, Helper } from "../core.config";
24
24
  import { values } from "../values.config";
25
25
 
26
26
  class MaintenanceList extends Component {
27
- _refreshCounter = 0;
28
- _filterApplying = false;
29
- _filterDataReady = false;
30
- _overlayOpacity = new Animated.Value(0);
31
-
32
- constructor(props) {
33
- super(props);
34
-
35
- this.state = {
36
- types: [],
37
- filteredList: props.jobs,
38
- loading: false,
39
- loadingMore: false,
40
- hasMore: true,
41
- lastKey: null,
42
- searchText: "",
43
- showFilterPopup: false,
44
- };
45
- }
46
-
47
- componentDidMount() {
48
- this.props.jobStatusesUpdate(this.props.site);
49
- this.props.jobHideSeenUpdate(this.props.site);
50
- this.refresh();
51
- this.refreshTypes();
52
- this.resetDataSource();
53
- }
54
-
55
- componentDidUpdate(prevProps) {
56
- if (!prevProps.dataUpdated && this.props.dataUpdated) this.refresh();
57
- if (
58
- !_.isEqual(prevProps.jobs, this.props.jobs) ||
59
- !_.isEqual(prevProps.jobfilters, this.props.jobfilters)
60
- )
61
- this.resetDataSource();
62
- }
63
-
64
- expandStatus = (status) => {
65
- if (status !== "Incomplete") return status;
66
- const { statusTypes } = this.props;
67
- if (!statusTypes || !statusTypes.length) return status;
68
- const incompleteStatuses = statusTypes
69
- .filter((s) => s.category !== "Completed")
70
- .map((s) => s.text);
71
- return incompleteStatuses.length > 0
72
- ? incompleteStatuses.join(",")
73
- : status;
74
- };
75
-
76
- refresh = (overrideFilters) => {
77
- const currentCounter = ++this._refreshCounter;
78
- this.onLoadingChanged(true, async () => {
79
- try {
80
- const jobfilters = overrideFilters || this.props.jobfilters;
81
- const res = await maintenanceActions.getJobs2(
82
- this.props.site,
83
- this.expandStatus(jobfilters.status),
84
- jobfilters.priority,
85
- jobfilters.type,
86
- null, // No lastKey = first page
87
- jobfilters.assignee,
88
- );
89
-
90
- // Discard stale responses from earlier filter changes
91
- if (currentCounter !== this._refreshCounter) return;
92
-
93
- const jobs = res.data.Items;
94
- const lastKey = res.data.LastKey;
95
-
96
- // Signal that the API response is ready before dispatching to Redux
97
- this._filterDataReady = true;
98
-
99
- // Always replace jobs on refresh (filters are server-side)
100
- this.props.jobsLoaded(jobs);
101
-
102
- // Reset filter flags directly in case resetDataSource never fires
103
- // (e.g. when the list was already empty and jobsLoaded([]) doesn't
104
- // trigger componentDidUpdate because _.isEqual([], []) is true)
105
- if (this._filterApplying) {
106
- this._filterApplying = false;
107
- this._filterDataReady = false;
108
- this._showOverlay(false);
109
- }
110
-
111
- this.setState({
112
- lastKey,
113
- hasMore: !!lastKey,
114
- });
115
- } catch (error) {
116
- if (currentCounter !== this._refreshCounter) return;
117
- console.log("refresh error", error);
118
- this._filterApplying = false;
119
- this._filterDataReady = false;
120
- this._showOverlay(false);
121
- } finally {
122
- if (currentCounter === this._refreshCounter) {
123
- this.onLoadingChanged(false);
124
- }
125
- }
126
- });
127
- };
128
-
129
- loadMore = () => {
130
- const { loading, loadingMore, hasMore, lastKey, searchText } = this.state;
131
-
132
- // Don't load if already loading, no more pages, or during search
133
- if (loading || loadingMore || !hasMore || !lastKey || searchText) return;
134
-
135
- this.setState({ loadingMore: true }, async () => {
136
- try {
137
- const { jobfilters } = this.props;
138
- const res = await maintenanceActions.getJobs2(
139
- this.props.site,
140
- this.expandStatus(jobfilters.status),
141
- jobfilters.priority,
142
- jobfilters.type,
143
- lastKey,
144
- jobfilters.assignee,
145
- );
146
-
147
- const newJobs = res.data.Items;
148
- const newLastKey = res.data.LastKey;
149
-
150
- // Append to existing jobs
151
- this.props.jobsAdded(newJobs);
152
-
153
- this.setState({
154
- lastKey: newLastKey,
155
- hasMore: !!newLastKey,
156
- });
157
- } catch (error) {
158
- console.log("loadMore error", error);
159
- } finally {
160
- this.setState({ loadingMore: false });
161
- }
162
- });
163
- };
164
-
165
- refreshTypes = async () => {
166
- const { data } = await maintenanceActions.getJobTypes(
167
- Helper.getSite(this.props.site),
168
- );
169
- const types = data.map((t) => {
170
- return { label: t.typeName, value: t.typeName };
171
- });
172
- types.splice(0, 0, { label: "All", value: "" });
173
- // console.log('refreshTypes', types);
174
- this.setState({ types });
175
- };
176
-
177
- fetchJob = (jobId) => {
178
- if (this.state.loading) return;
179
-
180
- this.onLoadingChanged(true, async () => {
181
- try {
182
- const job = await maintenanceActions.getJobByJobId(
183
- this.props.site,
184
- jobId,
185
- );
186
- // console.log('fetchJob', job?.data);
187
- this.props.jobAdded(job.data);
188
- } catch (error) {
189
- console.log("fetchJob error", error);
190
- } finally {
191
- this.onLoadingChanged(false);
192
- }
193
- });
194
- };
195
-
196
- getEmptyStateText() {
197
- if (this.props.options && !_.isEmpty(this.props.options.EmptyText)) {
198
- return this.props.options.EmptyText;
199
- }
200
- return this.props.userCategory === "staff"
201
- ? values.emptyRequestsStaff
202
- : values.emptyRequestsUser;
203
- }
204
-
205
- resetDataSource = (source = "") => {
206
- const { searchText } = this.state;
207
- const { jobs, jobfilters } = this.props;
208
-
209
- let filteredList = jobs;
210
- let jobIdMatch = null;
211
-
212
- if (jobfilters.status) {
213
- const expandedStatuses = this.expandStatus(jobfilters.status).split(",");
214
- filteredList = filteredList.filter(
215
- (j) => !j.status || expandedStatuses.includes(j.status),
216
- );
217
- }
218
-
219
- // Client-side search filtering (search is not supported server-side)
220
- if (searchText) {
221
- jobIdMatch = _.find(jobs, (j) => j.jobId === searchText);
222
- filteredList = jobs.filter((j) => {
223
- if (
224
- j.room &&
225
- j.room.toLowerCase().indexOf(searchText.toLowerCase()) > -1
226
- ) {
227
- return true;
228
- }
229
- return false;
230
- });
231
- if (!jobIdMatch) this.fetchJob(searchText);
232
- }
233
-
234
- // Note: status, priority, type, and assignee filters are applied server-side
235
- // via getJobs2, so no client-side filtering needed for those
236
-
237
- if (jobIdMatch) {
238
- const jobIndex = filteredList.indexOf(jobIdMatch);
239
- if (jobIndex > -1) {
240
- filteredList.splice(jobIndex, 1);
241
- }
242
- filteredList.unshift(jobIdMatch);
243
- }
244
-
245
- // During a filter transition, don't clear the list until the API
246
- // response has arrived and been processed. This prevents the empty
247
- // state from flashing when jobs transiently drop to [].
248
- if (
249
- this._filterApplying &&
250
- !this._filterDataReady &&
251
- filteredList.length === 0
252
- ) {
253
- return;
254
- }
255
-
256
- this.setState({ filteredList }, () => {
257
- if (this._filterDataReady) {
258
- this._filterApplying = false;
259
- this._filterDataReady = false;
260
- this._showOverlay(false);
261
- }
262
- });
263
- };
264
-
265
- onLoadingChanged = (loading, callback) => {
266
- this.setState({ loading }, () => {
267
- if (this.props.onLoadingChanged)
268
- this.props.onLoadingChanged(this.state.loading);
269
- if (callback) callback();
270
- });
271
- };
272
-
273
- onSearchText = (value) => {
274
- this.setState({ searchText: value }, () => {
275
- if (_.isEmpty(this.state.searchText)) this.resetDataSource("search");
276
- });
277
- };
278
-
279
- onSearchSubmit = () => {
280
- this.resetDataSource("search");
281
- };
282
-
283
- onToggleFilter = () => {
284
- this.setState({ showFilterPopup: !this.state.showFilterPopup });
285
- };
286
-
287
- onSelectFilter = (selected) => {
288
- this._filterApplying = true;
289
- this._filterDataReady = false;
290
- this._showOverlay(true);
291
- this.props.jobsFilterLoaded(selected);
292
- this.onToggleFilter();
293
- // Reset pagination but keep old list visible while loading
294
- // Pass filters directly to avoid stale props from async Redux update
295
- this.setState({ lastKey: null, hasMore: true }, () => {
296
- this.refresh(selected);
297
- });
298
- };
299
-
300
- getFilterButtonText = () => {
301
- const { jobfilters } = this.props;
302
- const filterTexts = [];
303
- if (!_.isEmpty(jobfilters.status)) {
304
- filterTexts.push(jobfilters.statusText);
305
- }
306
- if (!_.isEmpty(jobfilters.priority)) {
307
- filterTexts.push(jobfilters.priorityText);
308
- }
309
- if (!_.isEmpty(jobfilters.type)) {
310
- filterTexts.push(jobfilters.type);
311
- }
312
- if (!_.isEmpty(jobfilters.assignee)) {
313
- filterTexts.push(jobfilters.assigneeName);
314
- }
315
- if (_.isEmpty(filterTexts)) {
316
- return "Filter";
317
- }
318
- return `Filtering by ${filterTexts.join(", ")}`;
319
- };
320
-
321
- hasFiltersActive() {
322
- const { jobfilters } = this.props;
323
- return (
324
- !_.isEmpty(jobfilters.status) ||
325
- !_.isEmpty(jobfilters.priority) ||
326
- !_.isEmpty(jobfilters.type) ||
327
- !_.isEmpty(jobfilters.assignee)
328
- );
329
- }
330
-
331
- clearFilters = () => {
332
- const emptyFilters = {
333
- status: "",
334
- statusText: "",
335
- priority: "",
336
- priorityText: "",
337
- type: "",
338
- assignee: "",
339
- assigneeName: "",
340
- };
341
- this.onSelectFilter(emptyFilters);
342
- };
343
-
344
- renderEmptyList() {
345
- if (this.state.loading || this._filterApplying) return null;
346
-
347
- const hasFilters = this.hasFiltersActive();
348
-
349
- return (
350
- <View style={{ marginHorizontal: 16 }}>
351
- <Components.EmptyStateMain
352
- title={
353
- hasFilters
354
- ? "No requests match your filters"
355
- : this.getEmptyStateText()
356
- }
357
- />
358
- {hasFilters && (
359
- <TouchableOpacity
360
- onPress={this.clearFilters}
361
- style={[styles.clearFiltersButton, { alignSelf: "center" }]}
362
- >
363
- <Text
364
- style={[
365
- styles.clearFiltersText,
366
- { color: this.props.colourBrandingMain },
367
- ]}
368
- >
369
- Clear filters
370
- </Text>
371
- </TouchableOpacity>
372
- )}
373
- </View>
374
- );
375
- }
376
-
377
- renderFilterButton() {
378
- return (
379
- <TouchableOpacity onPress={this.onToggleFilter}>
380
- <View style={styles.filterButton}>
381
- <Text
382
- style={[
383
- styles.filterButtonText,
384
- { color: this.props.colourBrandingMain },
385
- ]}
386
- >
387
- {this.getFilterButtonText()}
388
- </Text>
389
- </View>
390
- </TouchableOpacity>
391
- );
392
- }
393
-
394
- renderSearch() {
395
- if (!this.props.hasPermission) return null;
396
-
397
- return (
398
- <View style={styles.searchContainer}>
399
- <Components.GenericInput
400
- placeholder={`Search by ${values.textEntityName} ID or Address`}
401
- value={this.state.searchText}
402
- onChangeText={this.onSearchText}
403
- onSubmitEditing={this.onSearchSubmit}
404
- squaredCorners
405
- hasClear
406
- // keyboardType={'numeric'}
407
- returnKeyType={"done"}
408
- />
409
- </View>
410
- );
411
- }
412
-
413
- renderListHeader() {
414
- const { ListHeaderComponent } = this.props;
415
- return (
416
- <View>
417
- {ListHeaderComponent ? (
418
- ListHeaderComponent
419
- ) : (
420
- <View style={{ height: 8 }} />
421
- )}
422
- {this.renderFilterButton()}
423
- {this.renderSearch()}
424
- </View>
425
- );
426
- }
427
-
428
- renderList() {
429
- const { filteredList, loading } = this.state;
430
-
431
- return (
432
- <FlatList
433
- keyboardShouldPersistTaps="always"
434
- style={{ flex: 1 }}
435
- contentContainerStyle={{ paddingBottom: 16 }}
436
- data={filteredList}
437
- keyExtractor={(item) => item.id}
438
- renderItem={({ item }) => (
439
- <MaintenanceListItem style={this.props.itemStyle} job={item} />
440
- )}
441
- ListEmptyComponent={this.renderEmptyList()}
442
- ListHeaderComponent={this.renderListHeader()}
443
- ListFooterComponent={this.renderListFooter()}
444
- onEndReached={this.loadMore}
445
- onEndReachedThreshold={0.5}
446
- onRefresh={this.refresh}
447
- refreshing={loading}
448
- />
449
- );
450
- }
451
-
452
- renderListFooter = () => {
453
- if (!this.state.loadingMore) return null;
454
-
455
- return (
456
- <View style={styles.loadingMore}>
457
- <Components.LoadingIndicator visible={true} />
458
- </View>
459
- );
460
- };
461
-
462
- _showOverlay = (visible) => {
463
- Animated.timing(this._overlayOpacity, {
464
- toValue: visible ? 1 : 0,
465
- duration: 200,
466
- useNativeDriver: true,
467
- }).start();
468
- };
469
-
470
- renderFilterOverlay() {
471
- if (!this._filterApplying) return null;
472
-
473
- return (
474
- <Animated.View
475
- style={[styles.filterOverlay, { opacity: this._overlayOpacity }]}
476
- pointerEvents="none"
477
- >
478
- <View style={styles.filterOverlayInner}>
479
- <Components.LoadingCircles />
480
- </View>
481
- </Animated.View>
482
- );
483
- }
484
-
485
- renderFilterPopup() {
486
- const { jobfilters } = this.props;
487
- const { showFilterPopup, types } = this.state;
488
- if (!showFilterPopup) return null;
489
-
490
- return (
491
- <FilterPopupMenu
492
- site={this.props.site}
493
- types={types}
494
- status={jobfilters.status}
495
- priority={jobfilters.priority}
496
- assignee={jobfilters.assignee}
497
- type={jobfilters.type}
498
- onClose={this.onSelectFilter}
499
- />
500
- );
501
- }
502
-
503
- render() {
504
- return (
505
- <View style={[styles.container, this.props.style]}>
506
- {this.renderList()}
507
- {this.renderFilterOverlay()}
508
- {this.renderFilterPopup()}
509
- </View>
510
- );
511
- }
27
+ _refreshCounter = 0;
28
+ _filterApplying = false;
29
+ _filterDataReady = false;
30
+ _overlayOpacity = new Animated.Value(0);
31
+
32
+ constructor(props) {
33
+ super(props);
34
+
35
+ this.state = {
36
+ types: [],
37
+ filteredList: props.jobs,
38
+ loading: false,
39
+ loadingMore: false,
40
+ hasMore: true,
41
+ lastKey: null,
42
+ searchText: "",
43
+ showFilterPopup: false,
44
+ };
45
+ }
46
+
47
+ componentDidMount() {
48
+ this.props.jobStatusesUpdate(this.props.site);
49
+ this.props.jobHideSeenUpdate(this.props.site);
50
+ this.refresh();
51
+ this.refreshTypes();
52
+ this.resetDataSource();
53
+ }
54
+
55
+ componentDidUpdate(prevProps) {
56
+ if (!prevProps.dataUpdated && this.props.dataUpdated) this.refresh();
57
+ if (
58
+ !_.isEqual(prevProps.jobs, this.props.jobs) ||
59
+ !_.isEqual(prevProps.jobfilters, this.props.jobfilters)
60
+ )
61
+ this.resetDataSource();
62
+ }
63
+
64
+ expandStatus = (status) => {
65
+ if (status !== "Incomplete") return status;
66
+ const { statusTypes } = this.props;
67
+ if (!statusTypes || !statusTypes.length) return status;
68
+ const incompleteStatuses = statusTypes
69
+ .filter((s) => s.category !== "Completed")
70
+ .map((s) => s.text);
71
+ return incompleteStatuses.length > 0
72
+ ? incompleteStatuses.join(",")
73
+ : status;
74
+ };
75
+
76
+ refresh = (overrideFilters) => {
77
+ const currentCounter = ++this._refreshCounter;
78
+ this.onLoadingChanged(true, async () => {
79
+ try {
80
+ const jobfilters = overrideFilters || this.props.jobfilters;
81
+ const res = await maintenanceActions.getJobs2(
82
+ this.props.site,
83
+ this.expandStatus(jobfilters.status),
84
+ jobfilters.priority,
85
+ jobfilters.type,
86
+ null, // No lastKey = first page
87
+ jobfilters.assignee,
88
+ );
89
+
90
+ // Discard stale responses from earlier filter changes
91
+ if (currentCounter !== this._refreshCounter) return;
92
+
93
+ const jobs = res.data.Items;
94
+ const lastKey = res.data.LastKey;
95
+
96
+ // Signal that the API response is ready before dispatching to Redux
97
+ this._filterDataReady = true;
98
+
99
+ // Always replace jobs on refresh (filters are server-side)
100
+ this.props.jobsLoaded(jobs);
101
+
102
+ // Reset filter flags directly in case resetDataSource never fires
103
+ // (e.g. when the list was already empty and jobsLoaded([]) doesn't
104
+ // trigger componentDidUpdate because _.isEqual([], []) is true)
105
+ if (this._filterApplying) {
106
+ this._filterApplying = false;
107
+ this._filterDataReady = false;
108
+ this._showOverlay(false);
109
+ }
110
+
111
+ this.setState({
112
+ lastKey,
113
+ hasMore: !!lastKey,
114
+ });
115
+ } catch (error) {
116
+ if (currentCounter !== this._refreshCounter) return;
117
+ console.log("refresh error", error);
118
+ this._filterApplying = false;
119
+ this._filterDataReady = false;
120
+ this._showOverlay(false);
121
+ } finally {
122
+ if (currentCounter === this._refreshCounter) {
123
+ this.onLoadingChanged(false);
124
+ }
125
+ }
126
+ });
127
+ };
128
+
129
+ loadMore = () => {
130
+ const { loading, loadingMore, hasMore, lastKey, searchText } = this.state;
131
+
132
+ // Don't load if already loading, no more pages, or during search
133
+ if (loading || loadingMore || !hasMore || !lastKey || searchText) return;
134
+
135
+ this.setState({ loadingMore: true }, async () => {
136
+ try {
137
+ const { jobfilters } = this.props;
138
+ const res = await maintenanceActions.getJobs2(
139
+ this.props.site,
140
+ this.expandStatus(jobfilters.status),
141
+ jobfilters.priority,
142
+ jobfilters.type,
143
+ lastKey,
144
+ jobfilters.assignee,
145
+ );
146
+
147
+ const newJobs = res.data.Items;
148
+ const newLastKey = res.data.LastKey;
149
+
150
+ // Append to existing jobs
151
+ this.props.jobsAdded(newJobs);
152
+
153
+ this.setState({
154
+ lastKey: newLastKey,
155
+ hasMore: !!newLastKey,
156
+ });
157
+ } catch (error) {
158
+ console.log("loadMore error", error);
159
+ } finally {
160
+ this.setState({ loadingMore: false });
161
+ }
162
+ });
163
+ };
164
+
165
+ refreshTypes = async () => {
166
+ const { data } = await maintenanceActions.getJobTypes(
167
+ Helper.getSite(this.props.site),
168
+ );
169
+ const types = data.map((t) => {
170
+ return { label: t.typeName, value: t.typeName };
171
+ });
172
+ types.splice(0, 0, { label: "All", value: "" });
173
+ // console.log('refreshTypes', types);
174
+ this.setState({ types });
175
+ };
176
+
177
+ fetchJob = (jobId) => {
178
+ if (this.state.loading) return;
179
+
180
+ this.onLoadingChanged(true, async () => {
181
+ try {
182
+ const job = await maintenanceActions.getJobByJobId(
183
+ this.props.site,
184
+ jobId,
185
+ );
186
+ // console.log('fetchJob', job?.data);
187
+ this.props.jobAdded(job.data);
188
+ } catch (error) {
189
+ console.log("fetchJob error", error);
190
+ } finally {
191
+ this.onLoadingChanged(false);
192
+ }
193
+ });
194
+ };
195
+
196
+ getEmptyStateText() {
197
+ if (this.props.options && !_.isEmpty(this.props.options.EmptyText)) {
198
+ return this.props.options.EmptyText;
199
+ }
200
+ return this.props.userCategory === "staff"
201
+ ? values.emptyRequestsStaff
202
+ : values.emptyRequestsUser;
203
+ }
204
+
205
+ resetDataSource = (source = "") => {
206
+ const { searchText } = this.state;
207
+ const { jobs, jobfilters } = this.props;
208
+
209
+ let filteredList = jobs;
210
+ let jobIdMatch = null;
211
+
212
+ // Note: status, priority, type, and assignee filters are applied server-side
213
+ // via getJobs2, so no client-side filtering needed for those
214
+
215
+ // Client-side search filtering (search is not supported server-side)
216
+ if (searchText) {
217
+ jobIdMatch = _.find(jobs, (j) => j.jobId === searchText);
218
+ filteredList = jobs.filter((j) => {
219
+ if (
220
+ j.room &&
221
+ j.room.toLowerCase().indexOf(searchText.toLowerCase()) > -1
222
+ ) {
223
+ return true;
224
+ }
225
+ return false;
226
+ });
227
+ if (!jobIdMatch) this.fetchJob(searchText);
228
+ }
229
+
230
+ if (jobIdMatch) {
231
+ const jobIndex = filteredList.indexOf(jobIdMatch);
232
+ if (jobIndex > -1) {
233
+ filteredList.splice(jobIndex, 1);
234
+ }
235
+ filteredList.unshift(jobIdMatch);
236
+ }
237
+
238
+ // During a filter transition, don't clear the list until the API
239
+ // response has arrived and been processed. This prevents the empty
240
+ // state from flashing when jobs transiently drop to [].
241
+ if (
242
+ this._filterApplying &&
243
+ !this._filterDataReady &&
244
+ filteredList.length === 0
245
+ ) {
246
+ return;
247
+ }
248
+
249
+ this.setState({ filteredList }, () => {
250
+ if (this._filterDataReady) {
251
+ this._filterApplying = false;
252
+ this._filterDataReady = false;
253
+ this._showOverlay(false);
254
+ }
255
+ });
256
+ };
257
+
258
+ onLoadingChanged = (loading, callback) => {
259
+ this.setState({ loading }, () => {
260
+ if (this.props.onLoadingChanged)
261
+ this.props.onLoadingChanged(this.state.loading);
262
+ if (callback) callback();
263
+ });
264
+ };
265
+
266
+ onSearchText = (value) => {
267
+ this.setState({ searchText: value }, () => {
268
+ if (_.isEmpty(this.state.searchText)) this.resetDataSource("search");
269
+ });
270
+ };
271
+
272
+ onSearchSubmit = () => {
273
+ this.resetDataSource("search");
274
+ };
275
+
276
+ onToggleFilter = () => {
277
+ this.setState({ showFilterPopup: !this.state.showFilterPopup });
278
+ };
279
+
280
+ onSelectFilter = (selected) => {
281
+ this._filterApplying = true;
282
+ this._filterDataReady = false;
283
+ this._showOverlay(true);
284
+ this.props.jobsFilterLoaded(selected);
285
+ this.onToggleFilter();
286
+ // Reset pagination but keep old list visible while loading
287
+ // Pass filters directly to avoid stale props from async Redux update
288
+ this.setState({ lastKey: null, hasMore: true }, () => {
289
+ this.refresh(selected);
290
+ });
291
+ };
292
+
293
+ getFilterButtonText = () => {
294
+ const { jobfilters } = this.props;
295
+ const filterTexts = [];
296
+ if (!_.isEmpty(jobfilters.status)) {
297
+ filterTexts.push(jobfilters.statusText);
298
+ }
299
+ if (!_.isEmpty(jobfilters.priority)) {
300
+ filterTexts.push(jobfilters.priorityText);
301
+ }
302
+ if (!_.isEmpty(jobfilters.type)) {
303
+ filterTexts.push(jobfilters.type);
304
+ }
305
+ if (!_.isEmpty(jobfilters.assignee)) {
306
+ filterTexts.push(jobfilters.assigneeName);
307
+ }
308
+ if (_.isEmpty(filterTexts)) {
309
+ return "Filter";
310
+ }
311
+ return `Filtering by ${filterTexts.join(", ")}`;
312
+ };
313
+
314
+ hasFiltersActive() {
315
+ const { jobfilters } = this.props;
316
+ return (
317
+ !_.isEmpty(jobfilters.status) ||
318
+ !_.isEmpty(jobfilters.priority) ||
319
+ !_.isEmpty(jobfilters.type) ||
320
+ !_.isEmpty(jobfilters.assignee)
321
+ );
322
+ }
323
+
324
+ clearFilters = () => {
325
+ const emptyFilters = {
326
+ status: "",
327
+ statusText: "",
328
+ priority: "",
329
+ priorityText: "",
330
+ type: "",
331
+ assignee: "",
332
+ assigneeName: "",
333
+ };
334
+ this.onSelectFilter(emptyFilters);
335
+ };
336
+
337
+ renderEmptyList() {
338
+ if (this.state.loading || this._filterApplying) return null;
339
+
340
+ const hasFilters = this.hasFiltersActive();
341
+
342
+ return (
343
+ <View style={{ marginHorizontal: 16 }}>
344
+ <Components.EmptyStateMain
345
+ title={
346
+ hasFilters
347
+ ? "No requests match your filters"
348
+ : this.getEmptyStateText()
349
+ }
350
+ />
351
+ {hasFilters && (
352
+ <TouchableOpacity
353
+ onPress={this.clearFilters}
354
+ style={[styles.clearFiltersButton, { alignSelf: "center" }]}
355
+ >
356
+ <Text
357
+ style={[
358
+ styles.clearFiltersText,
359
+ { color: this.props.colourBrandingMain },
360
+ ]}
361
+ >
362
+ Clear filters
363
+ </Text>
364
+ </TouchableOpacity>
365
+ )}
366
+ </View>
367
+ );
368
+ }
369
+
370
+ renderFilterButton() {
371
+ return (
372
+ <TouchableOpacity onPress={this.onToggleFilter}>
373
+ <View style={styles.filterButton}>
374
+ <Text
375
+ style={[
376
+ styles.filterButtonText,
377
+ { color: this.props.colourBrandingMain },
378
+ ]}
379
+ >
380
+ {this.getFilterButtonText()}
381
+ </Text>
382
+ </View>
383
+ </TouchableOpacity>
384
+ );
385
+ }
386
+
387
+ renderSearch() {
388
+ if (!this.props.hasPermission) return null;
389
+
390
+ return (
391
+ <View style={styles.searchContainer}>
392
+ <Components.GenericInput
393
+ placeholder={`Search by ${values.textEntityName} ID or Address`}
394
+ value={this.state.searchText}
395
+ onChangeText={this.onSearchText}
396
+ onSubmitEditing={this.onSearchSubmit}
397
+ squaredCorners
398
+ hasClear
399
+ // keyboardType={'numeric'}
400
+ returnKeyType={"done"}
401
+ />
402
+ </View>
403
+ );
404
+ }
405
+
406
+ renderListHeader() {
407
+ const { ListHeaderComponent } = this.props;
408
+ return (
409
+ <View>
410
+ {ListHeaderComponent ? (
411
+ ListHeaderComponent
412
+ ) : (
413
+ <View style={{ height: 8 }} />
414
+ )}
415
+ {this.renderFilterButton()}
416
+ {this.renderSearch()}
417
+ </View>
418
+ );
419
+ }
420
+
421
+ renderList() {
422
+ const { filteredList, loading } = this.state;
423
+
424
+ return (
425
+ <FlatList
426
+ keyboardShouldPersistTaps="always"
427
+ style={{ flex: 1 }}
428
+ contentContainerStyle={{ paddingBottom: 16 }}
429
+ data={filteredList}
430
+ keyExtractor={(item) => item.id}
431
+ renderItem={({ item }) => (
432
+ <MaintenanceListItem style={this.props.itemStyle} job={item} />
433
+ )}
434
+ ListEmptyComponent={this.renderEmptyList()}
435
+ ListHeaderComponent={this.renderListHeader()}
436
+ ListFooterComponent={this.renderListFooter()}
437
+ onEndReached={this.loadMore}
438
+ onEndReachedThreshold={0.5}
439
+ onRefresh={this.refresh}
440
+ refreshing={loading}
441
+ />
442
+ );
443
+ }
444
+
445
+ renderListFooter = () => {
446
+ if (!this.state.loadingMore) return null;
447
+
448
+ return (
449
+ <View style={styles.loadingMore}>
450
+ <Components.LoadingIndicator visible={true} />
451
+ </View>
452
+ );
453
+ };
454
+
455
+ _showOverlay = (visible) => {
456
+ Animated.timing(this._overlayOpacity, {
457
+ toValue: visible ? 1 : 0,
458
+ duration: 200,
459
+ useNativeDriver: true,
460
+ }).start();
461
+ };
462
+
463
+ renderFilterOverlay() {
464
+ if (!this._filterApplying) return null;
465
+
466
+ return (
467
+ <Animated.View
468
+ style={[styles.filterOverlay, { opacity: this._overlayOpacity }]}
469
+ pointerEvents="none"
470
+ >
471
+ <View style={styles.filterOverlayInner}>
472
+ <Components.LoadingCircles />
473
+ </View>
474
+ </Animated.View>
475
+ );
476
+ }
477
+
478
+ renderFilterPopup() {
479
+ const { jobfilters } = this.props;
480
+ const { showFilterPopup, types } = this.state;
481
+ if (!showFilterPopup) return null;
482
+
483
+ return (
484
+ <FilterPopupMenu
485
+ site={this.props.site}
486
+ types={types}
487
+ status={jobfilters.status}
488
+ priority={jobfilters.priority}
489
+ assignee={jobfilters.assignee}
490
+ type={jobfilters.type}
491
+ onClose={this.onSelectFilter}
492
+ />
493
+ );
494
+ }
495
+
496
+ render() {
497
+ return (
498
+ <View style={[styles.container, this.props.style]}>
499
+ {this.renderList()}
500
+ {this.renderFilterOverlay()}
501
+ {this.renderFilterPopup()}
502
+ </View>
503
+ );
504
+ }
512
505
  }
513
506
 
514
507
  const styles = StyleSheet.create({
515
- container: {
516
- flex: 1,
517
- backgroundColor: "#fff",
518
- },
519
- filterContainerOuter: {
520
- flexDirection: "row",
521
- justifyContent: "space-between",
522
- alignItems: "center",
523
- paddingHorizontal: 16,
524
- paddingVertical: 16,
525
- },
526
- filterTitle: {
527
- fontFamily: "sf-bold",
528
- fontSize: 11,
529
- letterSpacing: 0.8,
530
- color: "#4d4d4d",
531
- },
532
- filterContainer: {
533
- flexDirection: "row",
534
- alignItems: "center",
535
- },
536
- filterText: {
537
- fontFamily: "sf-semibold",
538
- fontSize: 16,
539
- marginRight: 6,
540
- },
541
- filterIcon: {
542
- fontSize: 20,
543
- },
544
- searchContainer: {
545
- flexDirection: "row",
546
- alignItems: "center",
547
- paddingBottom: 8,
548
- paddingHorizontal: 16,
549
- },
550
- filterButton: {
551
- paddingBottom: 8,
552
- paddingHorizontal: 16,
553
- flexDirection: "row-reverse",
554
- },
555
- filterButtonText: {
556
- fontFamily: "sf-semibold",
557
- fontSize: 16,
558
- },
559
- loadingMore: {
560
- paddingVertical: 20,
561
- alignItems: "center",
562
- },
563
- clearFiltersButton: {
564
- marginTop: 12,
565
- paddingVertical: 10,
566
- paddingHorizontal: 24,
567
- borderRadius: 8,
568
- borderWidth: 1,
569
- borderColor: "#ccc",
570
- backgroundColor: "#f9f9f9",
571
- },
572
- clearFiltersText: {
573
- fontFamily: "sf-semibold",
574
- fontSize: 15,
575
- },
576
- filterOverlay: {
577
- ...StyleSheet.absoluteFillObject,
578
- justifyContent: "center",
579
- alignItems: "center",
580
- backgroundColor: "rgba(255, 255, 255, 0.7)",
581
- },
582
- filterOverlayInner: {
583
- padding: 20,
584
- borderRadius: 12,
585
- },
508
+ container: {
509
+ flex: 1,
510
+ backgroundColor: "#fff",
511
+ },
512
+ filterContainerOuter: {
513
+ flexDirection: "row",
514
+ justifyContent: "space-between",
515
+ alignItems: "center",
516
+ paddingHorizontal: 16,
517
+ paddingVertical: 16,
518
+ },
519
+ filterTitle: {
520
+ fontFamily: "sf-bold",
521
+ fontSize: 11,
522
+ letterSpacing: 0.8,
523
+ color: "#4d4d4d",
524
+ },
525
+ filterContainer: {
526
+ flexDirection: "row",
527
+ alignItems: "center",
528
+ },
529
+ filterText: {
530
+ fontFamily: "sf-semibold",
531
+ fontSize: 16,
532
+ marginRight: 6,
533
+ },
534
+ filterIcon: {
535
+ fontSize: 20,
536
+ },
537
+ searchContainer: {
538
+ flexDirection: "row",
539
+ alignItems: "center",
540
+ paddingBottom: 8,
541
+ paddingHorizontal: 16,
542
+ },
543
+ filterButton: {
544
+ paddingBottom: 8,
545
+ paddingHorizontal: 16,
546
+ flexDirection: "row-reverse",
547
+ },
548
+ filterButtonText: {
549
+ fontFamily: "sf-semibold",
550
+ fontSize: 16,
551
+ },
552
+ loadingMore: {
553
+ paddingVertical: 20,
554
+ alignItems: "center",
555
+ },
556
+ clearFiltersButton: {
557
+ marginTop: 12,
558
+ paddingVertical: 10,
559
+ paddingHorizontal: 24,
560
+ borderRadius: 8,
561
+ borderWidth: 1,
562
+ borderColor: "#ccc",
563
+ backgroundColor: "#f9f9f9",
564
+ },
565
+ clearFiltersText: {
566
+ fontFamily: "sf-semibold",
567
+ fontSize: 15,
568
+ },
569
+ filterOverlay: {
570
+ ...StyleSheet.absoluteFillObject,
571
+ justifyContent: "center",
572
+ alignItems: "center",
573
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
574
+ },
575
+ filterOverlayInner: {
576
+ padding: 20,
577
+ borderRadius: 12,
578
+ },
586
579
  });
587
580
 
588
581
  const mapStateToProps = (state) => {
589
- const { user, notifications } = state;
590
- const jobs = state[values.reducerKey];
591
- const jobsOrdered = _.orderBy(jobs.jobs, ["createdUnix"], ["desc"]);
592
- const hasPermission = _.includes(user.permissions, "maintenanceTracking");
593
-
594
- return {
595
- hasPermission,
596
- jobs: jobsOrdered,
597
- site: user.site,
598
- userCategory: user.category,
599
- colourBrandingMain: Colours.getMainBrandingColourFromState(state),
600
- dataUpdated: notifications.dataUpdated[values.updateKey],
601
- statusTypes: state[values.reducerKey].jobstatuses,
602
- jobfilters: state[values.reducerKey].jobfilters || {},
603
- };
582
+ const { user, notifications } = state;
583
+ const jobs = state[values.reducerKey];
584
+ const jobsOrdered = _.orderBy(jobs.jobs, ["createdUnix"], ["desc"]);
585
+ const hasPermission = _.includes(user.permissions, "maintenanceTracking");
586
+
587
+ return {
588
+ hasPermission,
589
+ jobs: jobsOrdered,
590
+ site: user.site,
591
+ userCategory: user.category,
592
+ colourBrandingMain: Colours.getMainBrandingColourFromState(state),
593
+ dataUpdated: notifications.dataUpdated[values.updateKey],
594
+ statusTypes: state[values.reducerKey].jobstatuses,
595
+ jobfilters: state[values.reducerKey].jobfilters || {},
596
+ };
604
597
  };
605
598
 
606
599
  export default connect(
607
- mapStateToProps,
608
- {
609
- jobsLoaded,
610
- jobAdded,
611
- jobsAdded,
612
- jobStatusesUpdate,
613
- jobHideSeenUpdate,
614
- jobsFilterLoaded,
615
- },
616
- null,
617
- {
618
- forwardRef: true,
619
- },
600
+ mapStateToProps,
601
+ {
602
+ jobsLoaded,
603
+ jobAdded,
604
+ jobsAdded,
605
+ jobStatusesUpdate,
606
+ jobHideSeenUpdate,
607
+ jobsFilterLoaded,
608
+ },
609
+ null,
610
+ {
611
+ forwardRef: true,
612
+ },
620
613
  )(MaintenanceList);