Add article pack units with Redux-cached company unit list.

Edit amount/unit on the price tile (pc×1 = no pack unit), manage units in a dialog, and keep the default pc unit non-deletable.
This commit is contained in:
seb
2026-08-01 06:37:47 +02:00
parent 1b8652ceaf
commit 80f093649a
7 changed files with 841 additions and 25 deletions

98
src/store/unitSlice.js Normal file
View File

@@ -0,0 +1,98 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { unit as unitApi } from '../api';
import { requireArray, requireObject } from '../utils/apiShape';
import {
bootstrapSession,
createCompany,
loginWithGoogle,
logout,
selectCompany,
} from './authSlice';
function sortUnits(units) {
return [...units].sort((a, b) => {
const left = a.symbol ?? a.si ?? '';
const right = b.symbol ?? b.si ?? '';
return String(left).localeCompare(String(right), 'de');
});
}
export const loadUnits = createAsyncThunk('unit/load', async () => {
const res = await unitApi.list();
requireObject(res, 'GET /api/unit');
return requireArray(res.units, 'units');
});
export const upsertUnit = createAsyncThunk('unit/upsert', async (body) => {
const res = await unitApi.upsert(body);
requireObject(res, 'PUT /api/unit');
requireObject(res.unit, 'unit');
return res.unit;
});
export const deleteUnit = createAsyncThunk('unit/delete', async (si) => {
await unitApi.delete(si);
return si;
});
const initialState = {
units: [],
status: 'idle',
error: null,
};
const unitSlice = createSlice({
name: 'unit',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(loadUnits.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(loadUnits.fulfilled, (state, action) => {
state.status = 'succeeded';
state.units = sortUnits(action.payload);
})
.addCase(loadUnits.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
.addCase(upsertUnit.fulfilled, (state, action) => {
const row = action.payload;
state.units = sortUnits([
...state.units.filter((u) => u.si !== row.si),
row,
]);
})
.addCase(deleteUnit.fulfilled, (state, action) => {
state.units = state.units.filter((u) => u.si !== action.payload);
})
.addCase(selectCompany.fulfilled, () => initialState)
.addCase(createCompany.fulfilled, () => initialState)
.addCase(logout.fulfilled, () => initialState);
},
});
export const selectUnits = (state) => state.unit.units;
export const selectUnitsStatus = (state) => state.unit.status;
export const selectUnitsError = (state) => state.unit.error;
export function unitLoadListener(listenerMiddleware) {
listenerMiddleware.startListening({
matcher: (action) =>
bootstrapSession.fulfilled.match(action) ||
loginWithGoogle.fulfilled.match(action) ||
selectCompany.fulfilled.match(action) ||
createCompany.fulfilled.match(action),
effect: async (_action, api) => {
const companyId = api.getState().auth.user?.selectedCompanyId;
if (companyId) {
api.dispatch(loadUnits());
}
},
});
}
export default unitSlice.reducer;