This commit is contained in:
seb
2026-08-03 05:11:50 +02:00
parent 5b5c77f870
commit 4db4948090
14 changed files with 1410 additions and 1178 deletions

View File

@@ -0,0 +1,119 @@
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { EditablePanelTile } from '../../panels';
import { article } from '../../../api';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import { loadBrands, selectBrands, selectBrandsStatus, upsertBrand } from '../../../store/brandSlice';
import { notifyError } from '../../../utils/notify';
import { PANEL_TILE_KEYS } from '../../../theme';
function PanelValue({ label, children }) {
if (children == null || children === '') return null;
return (
<Box sx={{ mb: 1 }}>
{label ? (
<Typography variant="caption" color="text.secondary" display="block">
{label}
</Typography>
) : null}
<Typography variant="body2" component="div">
{children}
</Typography>
</Box>
);
}
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;
}
const SECOND_ROW_MIN_HEIGHT = 220;
export default function ArticleViewBrand({ article: articleData, onArticleUpdated }) {
const dispatch = useDispatch();
const brands = useSelector(selectBrands);
const brandsStatus = useSelector(selectBrandsStatus);
useEffect(() => {
if (brandsStatus !== 'idle') return;
void dispatch(loadBrands()).unwrap().catch((err) => notifyError(err));
}, [dispatch, brandsStatus]);
const handleSave = 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);
};
return (
<EditablePanelTile
wide={1}
tileKey={PANEL_TILE_KEYS.brand}
title="Marke"
minHeight={SECOND_ROW_MIN_HEIGHT}
value={brandDraftValue(articleData)}
onSave={handleSave}
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 } }}
/>
)}
/>
)}
/>
);
}