u
This commit is contained in:
53
TODO.md
Normal file
53
TODO.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# TODO — nächster Schritt (Brands / Artikelfelder)
|
||||||
|
|
||||||
|
**dashback** hat die HTTP-Routen geliefert (siehe `../dashback/TODO.md`). Alle Endpoints stehen. `companyId` kommt **immer aus der Auth-Session** (nicht aus `?companyId=`), gesteuert über `google_auth.setSelectedCompany`. Hier die UI bauen.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| UI-Bedarf | HTTP | Request | Response |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Brand-Liste | `GET /api/brand` | – | `{ brands: Brand[] }` |
|
||||||
|
| Brand anlegen / updaten | `PUT /api/brand` | `{ name, gsprpinfo? }` | `{ brand: Brand }` |
|
||||||
|
| Brand löschen | `DELETE /api/brand/:id` | – | `204` |
|
||||||
|
| EANs setzen (replace-all) | `PUT /api/article/:sku/eans` | `{ eans: string[] }` | `{ article }` (Detail) |
|
||||||
|
| Brand setzen / entfernen | `PUT /api/article/:sku/brand` | `{ brandId: number \| null }` | `{ article }` (Detail) |
|
||||||
|
| Gewicht setzen | `PUT /api/article/:sku/weight` | `{ weight: number }` | `{ article }` (Detail) |
|
||||||
|
| Kategorien setzen (replace-all) | `PUT /api/article/:sku/categories` | `{ categoryIds: number[] }` | `{ article }` (Detail) |
|
||||||
|
| Detail liefert neue Felder | `GET /api/article/:sku` | – | `{ article }` mit `eans`, `categoryIds`, `weight`, `brand` |
|
||||||
|
|
||||||
|
`Brand` = `{ id, name, gsprpinfo \| null }`.
|
||||||
|
|
||||||
|
Alle PUT/PATCH-Endpoints der Artikel liefern dasselbe Shape wie `GET /api/article/:sku`.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
### API-Client (`src/api/index.js`)
|
||||||
|
- Alle Calls über den bestehenden Client (`credentials: 'include'`) — Frontend spricht nur HTTP, nie direkt den Store.
|
||||||
|
- Neuen Methoden für: `brand.list` / `brand.upsert` / `brand.delete` und `article.setEansBySku` / `article.setBrandIdBySku` / `article.setWeightBySku` / `article.setCategoryIdsBySku`.
|
||||||
|
- **Kein `companyId` in Request/Pfad/Query senden** — die Company kommt aus der Session.
|
||||||
|
|
||||||
|
### Brand-UI
|
||||||
|
- **Liste**: lädt `GET /api/brand` und zeigt `name` (+ optional `gsprpinfo`).
|
||||||
|
- **Anlegen/Updaten**: ein Formular (Name + optional GSPRP-Info). Upsert matcht per `name` — gleicher Name ⇒ Update, sonst Create. Leeres/fehlendes `name` ⇒ API `400 Invalid brand`.
|
||||||
|
- **Löschen**: Bestätigung; `409 Brand in use` als Fehler anzeigen (Brand ist noch an Artikeln).
|
||||||
|
- Hängt an `selectedCompanyId`: bei Company-Wechsel Liste neu laden / leeren.
|
||||||
|
|
||||||
|
### Artikel-Detail-Felder
|
||||||
|
- **EANs**: Mehrfach-Eingabe (Liste). Replace-all: leere Liste löscht alle. `400 Invalid eans` bei leeren/duplizierten Einträgen.
|
||||||
|
- **Brand**: Dropdown aus `GET /api/brand`; „Keine Brand“ ⇒ `brandId: null`. Deal nur mit Brands derselben Company (sonst `404 Brand not found`).
|
||||||
|
- **Gewicht**: Zahlenfeld, `>= 0`. `400 Invalid weight` sonst.
|
||||||
|
- **Kategorien**: Mehrfachauswahl aus der Category-Tree; nur Kategorien derselben Company (sonst `400 Invalid categoryIds`). Leere Liste entfernt alle.
|
||||||
|
- Nach jedem Save das zurückgegebene Detail-Objekt in den Store übernehmen.
|
||||||
|
|
||||||
|
### Fehlerbehandlung
|
||||||
|
- Fehler als Toast/Formular-Fehler abbilden:
|
||||||
|
- `Brand in use` (409), `Invalid eans` / `Invalid weight` / `Invalid categoryIds` / `Invalid brand` (400)
|
||||||
|
- `Brand not found` / `Article not found` (404)
|
||||||
|
- `No company selected` (400) → Company-Wechsel-Flow
|
||||||
|
- `Not authorized` (403)
|
||||||
|
|
||||||
|
## Referenz
|
||||||
|
- API-Doku: `../dashback/docs/api/README.md` und `../dashback/docs/api/routes/`
|
||||||
|
- `brand/list.md`, `brand/upsert.md`, `brand/delete.md`
|
||||||
|
- `article/set-eans.md`, `article/set-brand.md`, `article/set-weight.md`, `article/set-categories.md`
|
||||||
|
- Company-Scope: `google_auth.setSelectedCompany` (Store) — siehe `../dashdbschema/README.md`.
|
||||||
@@ -235,6 +235,32 @@ export const article = {
|
|||||||
body: { prices },
|
body: { prices },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/article/:sku/brand
|
||||||
|
* Body: { brandId: number | null } — null clears the brand.
|
||||||
|
* brandId must belong to the article's company → 404 Article or brand not found.
|
||||||
|
* → 200 { article } (same shape as GET by SKU)
|
||||||
|
*/
|
||||||
|
setBrandIdBySku(sku, brandId) {
|
||||||
|
return http(`/api/article/${encodeURIComponent(sku)}/brand`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { brandId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/article/:sku/eans
|
||||||
|
* Body: { eans: string[] } — replace-all. Empty array clears all.
|
||||||
|
* EANs must be non-empty unique strings → 400 Invalid eans.
|
||||||
|
* → 200 { article } (same shape as GET by SKU)
|
||||||
|
*/
|
||||||
|
setEansBySku(sku, eans) {
|
||||||
|
return http(`/api/article/${encodeURIComponent(sku)}/eans`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { eans },
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,6 +334,27 @@ export const category = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Company-scoped brands (selectedCompanyId from session).
|
||||||
|
* Shape: Brand = { id, name, gsprpinfo | null }
|
||||||
|
*/
|
||||||
|
export const brand = {
|
||||||
|
/** GET /api/brand → { brands: Brand[] } · 400 No company selected */
|
||||||
|
list() {
|
||||||
|
return http('/api/brand');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/brand
|
||||||
|
* Body: { name, gsprpinfo? } — upsert by name for the selected company.
|
||||||
|
* Same name ⇒ update, otherwise create. Empty name ⇒ 400 Invalid brand.
|
||||||
|
* → 200 { brand: Brand }
|
||||||
|
*/
|
||||||
|
upsert(body) {
|
||||||
|
return http('/api/brand', { method: 'PUT', body });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const company = {
|
export const company = {
|
||||||
async list() {
|
async list() {
|
||||||
try {
|
try {
|
||||||
@@ -402,4 +449,4 @@ export const auth = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { auth, article, category, company, thumb, unit };
|
export default { auth, article, brand, category, company, thumb, unit };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
|
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
|
||||||
import StraightenOutlinedIcon from '@mui/icons-material/StraightenOutlined';
|
import StraightenOutlinedIcon from '@mui/icons-material/StraightenOutlined';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
@@ -23,10 +24,10 @@ import ArticleUnitEditorDialog from './ArticleUnitEditorDialog';
|
|||||||
import { article } from '../../api';
|
import { article } from '../../api';
|
||||||
import { EditablePanelTile, PanelTile, PanelTileGrid } from '../panels';
|
import { EditablePanelTile, PanelTile, PanelTileGrid } from '../panels';
|
||||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
|
import { loadBrands, selectBrands, selectBrandsStatus, upsertBrand } from '../../store/brandSlice';
|
||||||
import { loadUnits, selectUnits, selectUnitsStatus } from '../../store/unitSlice';
|
import { loadUnits, selectUnits, selectUnitsStatus } from '../../store/unitSlice';
|
||||||
import {
|
import {
|
||||||
articleEans,
|
articleEans,
|
||||||
brandName,
|
|
||||||
buildPosPricesPutBody,
|
buildPosPricesPutBody,
|
||||||
companyTaxRateOptions,
|
companyTaxRateOptions,
|
||||||
priceBrutto,
|
priceBrutto,
|
||||||
@@ -123,6 +124,32 @@ function articleHasPackUnit(article) {
|
|||||||
return !isNoPackUnit(si, amount);
|
return !isNoPackUnit(si, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function brandDraftValue(article) {
|
||||||
|
return { name: article?.brand?.name ?? '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function brandDraftDirty(draft, saved) {
|
||||||
|
return String(draft.name ?? '').trim() !== String(saved.name ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function brandDraftValid(draft) {
|
||||||
|
return String(draft.name ?? '').trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eansDraftValue(article) {
|
||||||
|
return { eans: articleEans(article) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function eansDraftDirty(draft, saved) {
|
||||||
|
return JSON.stringify(draft.eans ?? []) !== JSON.stringify(saved.eans ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eansDraftValid(draft) {
|
||||||
|
return (draft.eans ?? []).every(
|
||||||
|
(ean) => typeof ean === 'string' && ean.trim().length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function priceDraftValue(article, company) {
|
function priceDraftValue(article, company) {
|
||||||
const basePrice = primaryPrice(article);
|
const basePrice = primaryPrice(article);
|
||||||
const gross = priceBrutto(basePrice);
|
const gross = priceBrutto(basePrice);
|
||||||
@@ -567,8 +594,9 @@ export default function ArticleView({
|
|||||||
const company = useSelector(selectCurrentCompany);
|
const company = useSelector(selectCurrentCompany);
|
||||||
const units = useSelector(selectUnits);
|
const units = useSelector(selectUnits);
|
||||||
const unitsStatus = useSelector(selectUnitsStatus);
|
const unitsStatus = useSelector(selectUnitsStatus);
|
||||||
|
const brands = useSelector(selectBrands);
|
||||||
|
const brandsStatus = useSelector(selectBrandsStatus);
|
||||||
const basePrice = primaryPrice(articleData);
|
const basePrice = primaryPrice(articleData);
|
||||||
const eans = articleEans(articleData);
|
|
||||||
const taxRates = companyTaxRateOptions(company, basePrice?.taxRate);
|
const taxRates = companyTaxRateOptions(company, basePrice?.taxRate);
|
||||||
const priceValue = priceDraftValue(articleData, company);
|
const priceValue = priceDraftValue(articleData, company);
|
||||||
const unitOptions =
|
const unitOptions =
|
||||||
@@ -581,6 +609,11 @@ export default function ArticleView({
|
|||||||
void dispatch(loadUnits()).unwrap().catch((err) => notifyError(err));
|
void dispatch(loadUnits()).unwrap().catch((err) => notifyError(err));
|
||||||
}, [dispatch, unitsStatus]);
|
}, [dispatch, unitsStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (brandsStatus !== 'idle') return;
|
||||||
|
void dispatch(loadBrands()).unwrap().catch((err) => notifyError(err));
|
||||||
|
}, [dispatch, brandsStatus]);
|
||||||
|
|
||||||
const syncCategoryListPhoto = (next) => {
|
const syncCategoryListPhoto = (next) => {
|
||||||
const photo = primaryPhoto(next);
|
const photo = primaryPhoto(next);
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -618,6 +651,41 @@ export default function ArticleView({
|
|||||||
onArticleUpdated?.(res.article);
|
onArticleUpdated?.(res.article);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveBrand = async (draft) => {
|
||||||
|
const name = String(draft.name ?? '').trim();
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('Bitte eine Marke angeben.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = brands.find(
|
||||||
|
(row) => String(row.name ?? '').toLowerCase() === name.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let brandId;
|
||||||
|
if (existing) {
|
||||||
|
brandId = existing.id;
|
||||||
|
} else {
|
||||||
|
const created = await dispatch(upsertBrand({ name })).unwrap();
|
||||||
|
requireDefined(created.id, 'brand.id');
|
||||||
|
brandId = created.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await article.setBrandIdBySku(articleData.sku, brandId);
|
||||||
|
requireObject(res, 'PUT /api/article/:sku/brand');
|
||||||
|
requireObject(res.article, 'article');
|
||||||
|
onArticleUpdated?.(res.article);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveEans = async (draft) => {
|
||||||
|
const eans = (draft.eans ?? [])
|
||||||
|
.map((ean) => String(ean).trim())
|
||||||
|
.filter((ean) => ean.length > 0);
|
||||||
|
const res = await article.setEansBySku(articleData.sku, eans);
|
||||||
|
requireObject(res, 'PUT /api/article/:sku/eans');
|
||||||
|
requireObject(res.article, 'article');
|
||||||
|
onArticleUpdated?.(res.article);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSavePrice = async (draft) => {
|
const handleSavePrice = async (draft) => {
|
||||||
const saved = priceDraftValue(articleData, company);
|
const saved = priceDraftValue(articleData, company);
|
||||||
let nextArticle = articleData;
|
let nextArticle = articleData;
|
||||||
@@ -741,28 +809,85 @@ export default function ArticleView({
|
|||||||
</PanelValue>
|
</PanelValue>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile
|
<EditablePanelTile
|
||||||
wide={1}
|
wide={1}
|
||||||
tileKey={PANEL_TILE_KEYS.brand}
|
tileKey={PANEL_TILE_KEYS.brand}
|
||||||
title="Marke"
|
title="Marke"
|
||||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||||
>
|
value={brandDraftValue(articleData)}
|
||||||
<PanelValue>{brandName(articleData)}</PanelValue>
|
onSave={handleSaveBrand}
|
||||||
</PanelTile>
|
isDirty={brandDraftDirty}
|
||||||
|
canSave={brandDraftValid}
|
||||||
|
renderDisplay={(saved) => (
|
||||||
|
<PanelValue>{saved.name || null}</PanelValue>
|
||||||
|
)}
|
||||||
|
renderEdit={(draft, onChange, inputRef) => (
|
||||||
|
<Autocomplete
|
||||||
|
fullWidth
|
||||||
|
freeSolo
|
||||||
|
options={brands}
|
||||||
|
value={draft.name ?? ''}
|
||||||
|
inputValue={draft.name ?? ''}
|
||||||
|
getOptionLabel={(option) =>
|
||||||
|
typeof option === 'string' ? option : String(option.name ?? '')
|
||||||
|
}
|
||||||
|
onInputChange={(_event, value) => onChange({ name: value })}
|
||||||
|
onChange={(_event, option) => {
|
||||||
|
onChange({ name: option ? String(option.name ?? '') : '' });
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
inputRef={inputRef}
|
||||||
|
label="Marke"
|
||||||
|
size="small"
|
||||||
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<PanelTile
|
<EditablePanelTile
|
||||||
wide={1}
|
wide={1}
|
||||||
tileKey={PANEL_TILE_KEYS.identifiers}
|
tileKey={PANEL_TILE_KEYS.identifiers}
|
||||||
title="SKU / EAN"
|
title="SKU / EAN"
|
||||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||||
>
|
contentAlignItems="flex-start"
|
||||||
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
value={eansDraftValue(articleData)}
|
||||||
{eans.map((ean) => (
|
onSave={handleSaveEans}
|
||||||
<PanelValue key={ean} label="EAN">
|
isDirty={eansDraftDirty}
|
||||||
{ean}
|
canSave={eansDraftValid}
|
||||||
</PanelValue>
|
renderDisplay={(saved) => (
|
||||||
))}
|
<>
|
||||||
</PanelTile>
|
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
||||||
|
{(saved.eans ?? []).map((ean) => (
|
||||||
|
<PanelValue key={ean} label="EAN">
|
||||||
|
{ean}
|
||||||
|
</PanelValue>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
renderEdit={(draft, onChange, inputRef) => (
|
||||||
|
<Autocomplete
|
||||||
|
fullWidth
|
||||||
|
multiple
|
||||||
|
freeSolo
|
||||||
|
options={[]}
|
||||||
|
value={draft.eans ?? []}
|
||||||
|
onChange={(_event, eans) => onChange({ eans })}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
inputRef={inputRef}
|
||||||
|
label="EANs (Enter zum Hinzufügen)"
|
||||||
|
size="small"
|
||||||
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<EditablePanelTile
|
<EditablePanelTile
|
||||||
wide={1}
|
wide={1}
|
||||||
|
|||||||
100
src/store/brandSlice.js
Normal file
100
src/store/brandSlice.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { brand as brandApi } from '../api';
|
||||||
|
import { requireArray, requireObject } from '../utils/apiShape';
|
||||||
|
import { catchBackendError } from '../utils/backendError';
|
||||||
|
import {
|
||||||
|
bootstrapSession,
|
||||||
|
createCompany,
|
||||||
|
loginWithGoogle,
|
||||||
|
logout,
|
||||||
|
selectCompany,
|
||||||
|
} from './authSlice';
|
||||||
|
|
||||||
|
function sortBrands(brands) {
|
||||||
|
return [...brands].sort((a, b) =>
|
||||||
|
String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadBrands = createAsyncThunk('brand/load', async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const res = await brandApi.list();
|
||||||
|
requireObject(res, 'GET /api/brand');
|
||||||
|
return requireArray(res.brands, 'brands');
|
||||||
|
} catch (err) {
|
||||||
|
return catchBackendError(rejectWithValue)(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const upsertBrand = createAsyncThunk(
|
||||||
|
'brand/upsert',
|
||||||
|
async (body, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const res = await brandApi.upsert(body);
|
||||||
|
requireObject(res, 'PUT /api/brand');
|
||||||
|
requireObject(res.brand, 'brand');
|
||||||
|
return res.brand;
|
||||||
|
} catch (err) {
|
||||||
|
return catchBackendError(rejectWithValue)(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
brands: [],
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const brandSlice = createSlice({
|
||||||
|
name: 'brand',
|
||||||
|
initialState,
|
||||||
|
reducers: {},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(loadBrands.pending, (state) => {
|
||||||
|
state.status = 'loading';
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(loadBrands.fulfilled, (state, action) => {
|
||||||
|
state.status = 'succeeded';
|
||||||
|
state.brands = sortBrands(action.payload);
|
||||||
|
})
|
||||||
|
.addCase(loadBrands.rejected, (state, action) => {
|
||||||
|
state.status = 'failed';
|
||||||
|
state.error = action.error.message;
|
||||||
|
})
|
||||||
|
.addCase(upsertBrand.fulfilled, (state, action) => {
|
||||||
|
const row = action.payload;
|
||||||
|
state.brands = sortBrands([
|
||||||
|
...state.brands.filter((b) => b.id !== row.id),
|
||||||
|
row,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.addCase(selectCompany.fulfilled, () => initialState)
|
||||||
|
.addCase(createCompany.fulfilled, () => initialState)
|
||||||
|
.addCase(logout.fulfilled, () => initialState);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectBrands = (state) => state.brand.brands;
|
||||||
|
export const selectBrandsStatus = (state) => state.brand.status;
|
||||||
|
export const selectBrandsError = (state) => state.brand.error;
|
||||||
|
|
||||||
|
export function brandLoadListener(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(loadBrands());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default brandSlice.reducer;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
|
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
|
||||||
import authReducer from './authSlice';
|
import authReducer from './authSlice';
|
||||||
|
import brandReducer, { brandLoadListener } from './brandSlice';
|
||||||
import categoryReducer from './categorySlice';
|
import categoryReducer from './categorySlice';
|
||||||
import companyReducer, { companyLoadListener } from './companySlice';
|
import companyReducer, { companyLoadListener } from './companySlice';
|
||||||
import unitReducer, { unitLoadListener } from './unitSlice';
|
import unitReducer, { unitLoadListener } from './unitSlice';
|
||||||
@@ -7,6 +8,7 @@ import unitReducer, { unitLoadListener } from './unitSlice';
|
|||||||
const listenerMiddleware = createListenerMiddleware();
|
const listenerMiddleware = createListenerMiddleware();
|
||||||
companyLoadListener(listenerMiddleware);
|
companyLoadListener(listenerMiddleware);
|
||||||
unitLoadListener(listenerMiddleware);
|
unitLoadListener(listenerMiddleware);
|
||||||
|
brandLoadListener(listenerMiddleware);
|
||||||
|
|
||||||
function reduxDevLogger() {
|
function reduxDevLogger() {
|
||||||
return (storeApi) => (next) => (action) => {
|
return (storeApi) => (next) => (action) => {
|
||||||
@@ -15,6 +17,7 @@ function reduxDevLogger() {
|
|||||||
(!action.type?.startsWith('auth/') &&
|
(!action.type?.startsWith('auth/') &&
|
||||||
!action.type?.startsWith('company/') &&
|
!action.type?.startsWith('company/') &&
|
||||||
!action.type?.startsWith('category/') &&
|
!action.type?.startsWith('category/') &&
|
||||||
|
!action.type?.startsWith('brand/') &&
|
||||||
!action.type?.startsWith('unit/'))
|
!action.type?.startsWith('unit/'))
|
||||||
) {
|
) {
|
||||||
return next(action);
|
return next(action);
|
||||||
@@ -30,6 +33,7 @@ function reduxDevLogger() {
|
|||||||
after.auth.status === 'authed' && after.auth.user?.selectedCompanyId == null,
|
after.auth.status === 'authed' && after.auth.user?.selectedCompanyId == null,
|
||||||
},
|
},
|
||||||
company: after.company.current,
|
company: after.company.current,
|
||||||
|
brands: after.brand.brands.length,
|
||||||
units: after.unit.units.length,
|
units: after.unit.units.length,
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
@@ -41,6 +45,7 @@ export const store = configureStore({
|
|||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
company: companyReducer,
|
company: companyReducer,
|
||||||
category: categoryReducer,
|
category: categoryReducer,
|
||||||
|
brand: brandReducer,
|
||||||
unit: unitReducer,
|
unit: unitReducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user