Skip to content

Commit b1a47f7

Browse files
committed
feat: save wo filters to localStorage
1 parent d004d63 commit b1a47f7

8 files changed

Lines changed: 129 additions & 64 deletions

File tree

frontend/src/content/own/WorkOrders/Filters/MoreFilters.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import Form from '../../components/form';
44
import { IField } from '../../type';
55
import { useTranslation } from 'react-i18next';
66
import { Grid, Typography } from '@mui/material';
7-
import { useSelector } from '../../../../store';
7+
import { useDispatch, useSelector } from '../../../../store';
88
import { UserMiniDTO } from '../../../../models/user';
99
import {
1010
FilterFieldType,
1111
filterSingleField,
1212
getDateValue,
1313
getLabelAndValue
1414
} from '../../../../utils/filter';
15+
import { useEffect } from 'react';
16+
import { getAssetsMini } from '../../../../slices/asset';
17+
import { getCustomersMini } from '../../../../slices/customer';
18+
import { getTeamsMini } from '../../../../slices/team';
19+
import { getLocationsMini } from '../../../../slices/location';
20+
import { getCategories } from '../../../../slices/category';
21+
import { getUsersMini } from '../../../../slices/user';
1522

1623
interface OwnProps {
1724
onFilterChange: (filterFields: FilterField[]) => void;
@@ -27,6 +34,7 @@ function MoreFilters({ filterFields, onFilterChange, onClose }: OwnProps) {
2734
const { usersMini } = useSelector((state) => state.users);
2835
const { assetsMini } = useSelector((state) => state.assets);
2936
const { teamsMini } = useSelector((state) => state.teams);
37+
const dispatch = useDispatch();
3038

3139
const filtersConfig: {
3240
accessor: string;
@@ -43,7 +51,7 @@ function MoreFilters({ filterFields, onFilterChange, onClose }: OwnProps) {
4351
{ accessor: 'completedBy', fieldName: 'completedBy', type: 'array' },
4452
{
4553
accessor: 'customers',
46-
fieldName: 'customer',
54+
fieldName: 'customers',
4755
operator: 'inm',
4856
type: 'array'
4957
},
@@ -181,12 +189,12 @@ function MoreFilters({ filterFields, onFilterChange, onClose }: OwnProps) {
181189
break;
182190
}
183191
};
184-
const getValuesFromfilterFields = (): {
192+
const getValuesFromFilterFields = (): {
185193
[key: string]:
186194
| { label: string; value: string }
187195
| { label: string; value: number }[]
188196
| boolean
189-
| [string, string];
197+
| [Date | null, Date | null];
190198
} => {
191199
const typeValue = filterFields.find(
192200
(filterField) => filterField.field === 'parentPreventiveMaintenance'
@@ -239,7 +247,7 @@ function MoreFilters({ filterFields, onFilterChange, onClose }: OwnProps) {
239247
customers: getLabelAndValue(
240248
filterFields,
241249
customersMini,
242-
'customer',
250+
'customers',
243251
'name'
244252
),
245253
createdBy: getLabelAndValue(
@@ -255,6 +263,27 @@ function MoreFilters({ filterFields, onFilterChange, onClose }: OwnProps) {
255263
};
256264
};
257265
const shape = {};
266+
267+
const USER_FIELDS = ['primaryUser', 'completedBy', 'createdBy', 'assignedTo'];
268+
269+
useEffect(() => {
270+
const fieldsInUse = new Set(filterFields.map((f) => f.field));
271+
272+
if (fieldsInUse.has('asset')) dispatch(getAssetsMini());
273+
if (fieldsInUse.has('customers') && !customersMini.length)
274+
dispatch(getCustomersMini());
275+
if (fieldsInUse.has('team') && !teamsMini.length) dispatch(getTeamsMini());
276+
if (fieldsInUse.has('location') && !locationsMini.length)
277+
dispatch(getLocationsMini());
278+
if (
279+
fieldsInUse.has('category') &&
280+
!categories['work-order-categories']?.length
281+
)
282+
dispatch(getCategories('work-order-categories'));
283+
if (USER_FIELDS.some((f) => fieldsInUse.has(f)) && !usersMini.length)
284+
dispatch(getUsersMini());
285+
}, [filterFields.length]);
286+
258287
return (
259288
<Grid
260289
container
@@ -268,10 +297,12 @@ function MoreFilters({ filterFields, onFilterChange, onClose }: OwnProps) {
268297
</Grid>
269298
<Grid item xs={12}>
270299
<Form
300+
key={`${assetsMini.length}-${teamsMini.length}-${customersMini.length}-${locationsMini.length}-${usersMini.length}-${categories['work-order-categories']?.length}`}
271301
fields={fields}
272302
validation={Yup.object().shape(shape)}
273303
submitText={t('save')}
274-
values={getValuesFromfilterFields()}
304+
values={getValuesFromFilterFields()}
305+
enableReinitialize
275306
onChange={({ field, e }) => {}}
276307
onSubmit={async (values) => {
277308
let newFilters = [...filterFields];

frontend/src/content/own/WorkOrders/index.tsx

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,56 @@ const fieldMapping: Record<string, string> = {
107107
createdAt: 'createdAt',
108108
dueDate: 'dueDate'
109109
};
110+
const normalizeFields = (fields: FilterField[]) =>
111+
[...fields]
112+
.sort((a, b) => a.field.localeCompare(b.field))
113+
.map((f) => ({ ...f, values: f.values ? [...f.values].sort() : f.values }));
114+
115+
const FILTERS_STORAGE_KEY = 'workOrder_filters';
116+
const DEFAULT_FILTER_FIELDS: FilterField[] = [
117+
{ field: 'archived', operation: 'eq', value: false },
118+
{
119+
field: 'priority',
120+
operation: 'in',
121+
values: ['NONE', 'LOW', 'MEDIUM', 'HIGH'],
122+
value: '',
123+
enumName: 'PRIORITY'
124+
},
125+
{
126+
field: 'status',
127+
operation: 'in',
128+
values: ['OPEN', 'IN_PROGRESS', 'ON_HOLD'],
129+
value: '',
130+
enumName: 'STATUS'
131+
}
132+
];
133+
134+
const QUERY_SEARCH_FIELDS = new Set([
135+
'title',
136+
'description',
137+
'feedback',
138+
'customId'
139+
]);
140+
141+
const getInitialFilterFields = (): FilterField[] => {
142+
try {
143+
const saved = localStorage.getItem(FILTERS_STORAGE_KEY);
144+
if (saved) {
145+
const parsed: FilterField[] = JSON.parse(saved);
146+
const cleaned = parsed.filter((f) => !QUERY_SEARCH_FIELDS.has(f.field));
147+
const fields = new Set(cleaned.map((f) => f.field));
148+
149+
for (const defaults of DEFAULT_FILTER_FIELDS) {
150+
if (!fields.has(defaults.field)) cleaned.push(defaults);
151+
}
152+
153+
return cleaned;
154+
}
155+
} catch {
156+
/* ignore invalid JSON */
157+
}
158+
return DEFAULT_FILTER_FIELDS;
159+
};
110160
function WorkOrders() {
111161
const { t }: { t: any } = useTranslation();
112162
const [currentTab, setCurrentTab] = useState<string>('list');
@@ -175,27 +225,7 @@ function WorkOrders() {
175225

176226
// Use the table state hook for TanStack Table
177227
const initialCriteria: SearchCriteria = {
178-
filterFields: [
179-
{
180-
field: 'priority',
181-
operation: 'in',
182-
values: ['NONE', 'LOW', 'MEDIUM', 'HIGH'],
183-
value: '',
184-
enumName: 'PRIORITY'
185-
},
186-
{
187-
field: 'status',
188-
operation: 'in',
189-
values: ['OPEN', 'IN_PROGRESS', 'ON_HOLD'],
190-
value: '',
191-
enumName: 'STATUS'
192-
},
193-
{
194-
field: 'archived',
195-
operation: 'eq',
196-
value: false
197-
}
198-
],
228+
filterFields: getInitialFilterFields(),
199229
pageSize: 10,
200230
pageNum: 0,
201231
direction: 'DESC'
@@ -205,6 +235,7 @@ function WorkOrders() {
205235
sortField: 'updatedAt',
206236
direction: 'DESC'
207237
});
238+
208239
const {
209240
sorting,
210241
setSorting,
@@ -296,6 +327,10 @@ function WorkOrders() {
296327
const newCriteria = { ...criteria };
297328
newCriteria.filterFields = newFilters;
298329
setCriteria(newCriteria);
330+
const toSave = newFilters.filter(
331+
(f) => !QUERY_SEARCH_FIELDS.includes(f.field)
332+
);
333+
localStorage.setItem(FILTERS_STORAGE_KEY, JSON.stringify(toSave));
299334
};
300335
useEffect(() => {
301336
if (workOrderId && isNumeric(workOrderId)) {
@@ -393,12 +428,12 @@ function WorkOrders() {
393428
showSnackBar(t('wo_delete_failure'), 'error');
394429

395430
const onQueryChange = (event) => {
396-
onSearchQueryChange<WorkOrder>(event, criteria, setCriteria, [
397-
'title',
398-
'description',
399-
'feedback',
400-
'customId'
401-
]);
431+
onSearchQueryChange<WorkOrder>(
432+
event,
433+
criteria,
434+
setCriteria,
435+
QUERY_SEARCH_FIELDS.values()
436+
);
402437
};
403438
const debouncedQueryChange = useMemo(() => debounce(onQueryChange, 1300), []);
404439
useEffect(() => {
@@ -959,7 +994,10 @@ function WorkOrders() {
959994
minWidth: 0
960995
}}
961996
variant={
962-
_.isEqual(criteria.filterFields, initialCriteria.filterFields)
997+
_.isEqual(
998+
normalizeFields(criteria.filterFields),
999+
normalizeFields(DEFAULT_FILTER_FIELDS)
1000+
)
9631001
? 'outlined'
9641002
: 'contained'
9651003
}

frontend/src/content/own/components/form/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface PropsType {
3535
validation?: ObjectSchema<any>;
3636
isLoading?: boolean;
3737
isButtonEnabled?: (values: IHash<any>, ...props: any[]) => boolean;
38+
enableReinitialize?: boolean;
3839
}
3940

4041
export default (props: PropsType) => {
@@ -106,6 +107,7 @@ export default (props: PropsType) => {
106107
validateOnChange={false}
107108
validateOnBlur={false}
108109
initialValues={props.values || {}}
110+
enableReinitialize={props.enableReinitialize}
109111
onSubmit={(
110112
values,
111113
{ resetForm, setErrors, setStatus, setSubmitting }

frontend/src/slices/asset.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export const reducer = slice.reducer;
178178
export const getAssets =
179179
(criteria: SearchCriteria): AppThunk =>
180180
async (dispatch) => {
181-
const { signal } = createCancellableRequest();
181+
const { signal } = createCancellableRequest('getAssets');
182182
try {
183183
dispatch(slice.actions.setLoadingGet({ loading: true }));
184184
const assets = await api.post<Page<AssetDTO>>(
@@ -197,7 +197,7 @@ export const getAssets =
197197
export const getAssetsMini =
198198
(locationId?: number): AppThunk =>
199199
async (dispatch) => {
200-
const { signal } = createCancellableRequest();
200+
const { signal } = createCancellableRequest('getAssetsMini');
201201
try {
202202
dispatch(slice.actions.setLoadingGet({ loading: true }));
203203
const assets = await api.get<AssetMiniDTO[]>(

frontend/src/slices/category.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const slice = createSlice({
7070
export const reducer = slice.reducer;
7171

7272
export const getCategories =
73-
(basePath): AppThunk =>
73+
(basePath: string): AppThunk =>
7474
async (dispatch) => {
7575
try {
7676
dispatch(slice.actions.loading({ basePath, loading: true }));

frontend/src/slices/location.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const reducer = slice.reducer;
142142
export const getLocations =
143143
(criteria: SearchCriteria): AppThunk =>
144144
async (dispatch) => {
145-
const { signal } = createCancellableRequest();
145+
const { signal } = createCancellableRequest('getLocations');
146146
try {
147147
dispatch(slice.actions.setLoadingGet({ loading: true }));
148148
const locations = await api.post<Page<Location>>(
@@ -159,7 +159,7 @@ export const getLocations =
159159
}
160160
};
161161
export const getLocationsMini = (): AppThunk => async (dispatch) => {
162-
const { signal } = createCancellableRequest();
162+
const { signal } = createCancellableRequest('getLocationsMini');
163163
try {
164164
dispatch(slice.actions.setLoadingGet({ loading: true }));
165165
const locations = await api.get<LocationMiniDTO[]>('locations/mini', {

frontend/src/utils/cancellableRequest.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,23 @@ interface CancellableRequest {
88
signal: AbortSignal | null;
99
}
1010

11-
let currentController: AbortController | null = null;
12-
1311
/**
1412
* Creates a new cancellable request, aborting any previous request.
1513
* @returns CancellableRequest object with abort function and signal
1614
*/
17-
export function createCancellableRequest(): CancellableRequest {
18-
// Abort previous request if it exists
19-
if (currentController) {
20-
currentController.abort();
21-
}
15+
const controllers = new Map<string, AbortController>();
16+
17+
export function createCancellableRequest(key: string): CancellableRequest {
18+
controllers.get(key)?.abort();
2219

23-
// Create new controller
24-
currentController = new AbortController();
20+
const controller = new AbortController();
21+
controllers.set(key, controller);
2522

2623
return {
27-
abort: () => currentController?.abort(),
28-
signal: currentController.signal
24+
abort: () => controller.abort(),
25+
signal: controller.signal
2926
};
3027
}
31-
3228
/**
3329
* Checks if an error is an AbortError (request was cancelled).
3430
*/

frontend/src/utils/filter.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,24 @@ export const getLabelAndValue = <T extends { id: number }>(
1414
.find((filterField) => filterField.field === fieldName)
1515
?.values?.map((id) => ({
1616
label: formatter
17-
? formatter(minis.find((mini) => mini.id === id))
18-
: minis.find((mini) => mini.id === id)[labelAccessor].toString(),
17+
? formatter(minis?.find((mini) => mini.id === id) || ({} as T))
18+
: minis?.find((mini) => mini.id === id)?.[labelAccessor].toString(),
1919
value: id
2020
})) ?? null
2121
);
2222
};
2323
export const getDateValue = (
2424
filterFields: FilterField[],
2525
fieldName: string
26-
): [string, string] => {
27-
return [
28-
filterFields.find(
29-
(filterField) =>
30-
filterField.field === fieldName && filterField.operation === 'ge'
31-
)?.value ?? null,
32-
filterFields.find(
33-
(filterField) =>
34-
filterField.field === fieldName && filterField.operation === 'le'
35-
)?.value ?? null
36-
];
26+
): [Date | null, Date | null] => {
27+
const find = (operation: string) => {
28+
const value = filterFields.find(
29+
(f) => f.field === fieldName && f.operation === operation
30+
)?.value;
31+
return value ? new Date(value as string) : null;
32+
};
33+
34+
return [find('ge'), find('le')];
3735
};
3836

3937
export const filterSingleField = (

0 commit comments

Comments
 (0)