no redux
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
||||
categorySlug,
|
||||
getCategoryBreadcrumbs,
|
||||
getCategoryChildren,
|
||||
} from '../../store/categorySlice';
|
||||
} from '../../utils/categoryUtils';
|
||||
|
||||
export { ROOT_CATEGORY_NAME, categorySlug };
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, createRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Component, PureComponent, createRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import TextField from '@mui/material/TextField';
|
||||
@@ -19,41 +18,49 @@ import {
|
||||
import { TILE_SIZE } from 'dashgui/tiles';
|
||||
import withCategoryRoute from './withCategoryRoute';
|
||||
import { artikelDetailPath, articleSku } from './articleRoute';
|
||||
import { selectCompanyId } from '../../store/companySlice';
|
||||
import { selectPosterminals, loadPosterminals } from '../../store/posterminalSlice';
|
||||
import {
|
||||
buildCategoryPath,
|
||||
defaultCategoryPath,
|
||||
resolveCategoryIdFromSlugPath,
|
||||
ROOT_CATEGORY_NAME,
|
||||
} from './categoryRoute';
|
||||
import {
|
||||
categoryDisplayName,
|
||||
deleteCategory,
|
||||
moveCategory,
|
||||
loadCategoryArticles,
|
||||
loadCategoryTree,
|
||||
navigateToCategory,
|
||||
renameCategory,
|
||||
reorderCategories,
|
||||
selectCanNavigateBack,
|
||||
selectCategoryArticles,
|
||||
selectCategoryArticlesStatus,
|
||||
selectCategoryBreadcrumbs,
|
||||
selectCategoryMap,
|
||||
selectCategoryError,
|
||||
selectCategoryTreeStatus,
|
||||
selectChildCategories,
|
||||
selectRenameMode,
|
||||
patchCategoryPosterminalIds,
|
||||
uploadCategoryImage,
|
||||
deleteCategoryImage,
|
||||
} from '../../store/categorySlice';
|
||||
import {
|
||||
enableTerminalCategory,
|
||||
disableTerminalCategory,
|
||||
} from '../../store/posterminalSlice';
|
||||
import categoryAutomat from '../../automats/categoryAutomat';
|
||||
import posterminalAutomat from '../../automats/posterminalAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import { categoryDisplayName, getCategoryBreadcrumbs, getCategoryChildren } from '../../utils/categoryUtils';
|
||||
|
||||
// ─── Helper selectors for this page ────────────────────────────────────────────
|
||||
function selectFromCategory(s) {
|
||||
const { categories, rootCategoryId, currentCategoryId, articles, articlesTotal, renameMode, treeStatus, articlesStatus, error } = s;
|
||||
const breadcrumbs = getCategoryBreadcrumbs(categories, currentCategoryId, rootCategoryId);
|
||||
const childCategories = getCategoryChildren(categories, currentCategoryId);
|
||||
const canGoBack =
|
||||
currentCategoryId != null &&
|
||||
rootCategoryId != null &&
|
||||
String(currentCategoryId) !== String(rootCategoryId) &&
|
||||
categories?.[String(currentCategoryId)]?.parentId != null &&
|
||||
categories[String(currentCategoryId)].parentId !== 0;
|
||||
|
||||
return {
|
||||
categories,
|
||||
rootCategoryId,
|
||||
treeStatus,
|
||||
articlesStatus,
|
||||
error,
|
||||
currentCategoryId,
|
||||
breadcrumbs,
|
||||
childCategories,
|
||||
articles,
|
||||
articlesTotal,
|
||||
canGoBack,
|
||||
renameMode,
|
||||
};
|
||||
}
|
||||
|
||||
function selectTerminals(s) {
|
||||
return { terminals: s.terminals };
|
||||
}
|
||||
|
||||
const gridSx = {
|
||||
display: 'grid',
|
||||
@@ -61,10 +68,15 @@ const gridSx = {
|
||||
gap: 2,
|
||||
};
|
||||
|
||||
class ArtikelKategorienPage extends Component {
|
||||
class ArtikelKategorienPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { drafts: {} };
|
||||
this.state = {
|
||||
drafts: {},
|
||||
...selectFromCategory(categoryAutomat.state),
|
||||
...selectTerminals(posterminalAutomat.state),
|
||||
companyId: companyAutomat.state.current?.id ?? null,
|
||||
};
|
||||
this.gridRef = createRef();
|
||||
this.sortableModule = null;
|
||||
this.droppableModule = null;
|
||||
@@ -74,6 +86,11 @@ class ArtikelKategorienPage extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
setLastArticleSku(null);
|
||||
this._unsubCat = categoryAutomat.subscribe(this, selectFromCategory);
|
||||
this._unsubTerm = posterminalAutomat.subscribe(this, selectTerminals);
|
||||
this._unsubCompany = companyAutomat.subscribe(this, (s) => ({
|
||||
companyId: s.current?.id ?? null,
|
||||
}));
|
||||
this.ensureTreeLoaded();
|
||||
this.ensurePosterminalsLoaded();
|
||||
this.syncCategoryFromUrl();
|
||||
@@ -82,37 +99,36 @@ class ArtikelKategorienPage extends Component {
|
||||
this.syncDroppable();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.companyId !== prevProps.companyId) {
|
||||
componentDidUpdate(prevState) {
|
||||
// Note: PureComponent — comparisons use this.state not this.props
|
||||
if (this.state.companyId !== prevState.companyId) {
|
||||
this.ensureTreeLoaded();
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.treeStatus === 'succeeded' &&
|
||||
prevProps.treeStatus !== 'succeeded'
|
||||
) {
|
||||
if (this.state.treeStatus === 'succeeded' && prevState.treeStatus !== 'succeeded') {
|
||||
this.syncCategoryFromUrl();
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.treeStatus === 'succeeded' &&
|
||||
this.props.categorySlugPath !== prevProps.categorySlugPath
|
||||
this.state.treeStatus === 'succeeded' &&
|
||||
this.props.categorySlugPath !== (this._prevSlugPath ?? this.props.categorySlugPath)
|
||||
) {
|
||||
this.syncCategoryFromUrl();
|
||||
}
|
||||
this._prevSlugPath = this.props.categorySlugPath;
|
||||
|
||||
if (this.props.currentCategoryId !== prevProps.currentCategoryId) {
|
||||
if (this.state.currentCategoryId !== prevState.currentCategoryId) {
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
|
||||
if (this.props.renameMode !== prevProps.renameMode) {
|
||||
if (this.state.renameMode !== prevState.renameMode) {
|
||||
this.syncSortable();
|
||||
this.syncDroppable();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unsubCat?.();
|
||||
this._unsubTerm?.();
|
||||
this._unsubCompany?.();
|
||||
this.sortableModule?.destroy();
|
||||
this.droppableModule?.destroy();
|
||||
}
|
||||
@@ -122,19 +138,21 @@ class ArtikelKategorienPage extends Component {
|
||||
if (!el) return;
|
||||
if (!this.sortableModule) {
|
||||
this.sortableModule = new CategorySortable(el, {
|
||||
onReorder: this.props.reorderCategories,
|
||||
onReorder: ({ categoryIds }) =>
|
||||
categoryAutomat.actions.reorderCategories({ categoryIds }),
|
||||
});
|
||||
}
|
||||
if (!this.droppableModule) {
|
||||
this.droppableModule = new CategoryDroppable(el, {
|
||||
onMove: this.props.moveCategory,
|
||||
onMove: ({ categoryId, parentId }) =>
|
||||
categoryAutomat.actions.moveCategory({ categoryId, parentId }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
syncSortable = () => {
|
||||
this.#ensureModules();
|
||||
if (this.props.renameMode) {
|
||||
if (this.state.renameMode) {
|
||||
this.sortableModule?.destroy();
|
||||
this.sortableModule?.activate();
|
||||
} else {
|
||||
@@ -144,7 +162,7 @@ class ArtikelKategorienPage extends Component {
|
||||
|
||||
syncDroppable = () => {
|
||||
this.#ensureModules();
|
||||
if (this.props.renameMode) {
|
||||
if (this.state.renameMode) {
|
||||
this.droppableModule?.destroy();
|
||||
this.droppableModule?.activate();
|
||||
} else {
|
||||
@@ -157,33 +175,32 @@ class ArtikelKategorienPage extends Component {
|
||||
};
|
||||
|
||||
ensureTreeLoaded = () => {
|
||||
const { companyId, treeStatus } = this.props;
|
||||
const { companyId, treeStatus } = this.state;
|
||||
if (!companyId || treeStatus !== 'idle') return;
|
||||
this.props.loadCategoryTree();
|
||||
void categoryAutomat.actions.loadTree();
|
||||
};
|
||||
|
||||
ensurePosterminalsLoaded = () => {
|
||||
const { companyId } = this.props;
|
||||
const { companyId } = this.state;
|
||||
if (!companyId) return;
|
||||
this.props.loadPosterminals();
|
||||
void posterminalAutomat.actions.load();
|
||||
};
|
||||
|
||||
ensureArticlesLoaded = () => {
|
||||
const { currentCategoryId, articlesStatus, rootCategoryId, treeStatus } = this.props;
|
||||
const { currentCategoryId, articlesStatus, rootCategoryId, treeStatus } = this.state;
|
||||
if (treeStatus !== 'succeeded' || !currentCategoryId || articlesStatus !== 'idle') return;
|
||||
if (String(currentCategoryId) === String(rootCategoryId)) return;
|
||||
this.props.loadCategoryArticles(currentCategoryId);
|
||||
void categoryAutomat.actions.loadArticles(currentCategoryId);
|
||||
};
|
||||
|
||||
syncCategoryFromUrl = () => {
|
||||
const {
|
||||
categories,
|
||||
rootCategoryId,
|
||||
categorySlugPath,
|
||||
currentCategoryId,
|
||||
treeStatus,
|
||||
navigate,
|
||||
} = this.props;
|
||||
} = this.state;
|
||||
const { categorySlugPath, navigate } = this.props;
|
||||
|
||||
if (treeStatus !== 'succeeded' || !categories || rootCategoryId == null) return;
|
||||
|
||||
@@ -206,13 +223,13 @@ class ArtikelKategorienPage extends Component {
|
||||
}
|
||||
|
||||
if (resolvedId !== currentCategoryId) {
|
||||
this.props.navigateToCategory(resolvedId);
|
||||
categoryAutomat.actions.navigateTo(resolvedId);
|
||||
}
|
||||
};
|
||||
|
||||
navigateToCategoryById = (categoryId) => {
|
||||
const { categories, rootCategoryId, navigate } = this.props;
|
||||
navigate(buildCategoryPath(categories, rootCategoryId, categoryId));
|
||||
const { categories, rootCategoryId } = this.state;
|
||||
this.props.navigate(buildCategoryPath(categories, rootCategoryId, categoryId));
|
||||
};
|
||||
|
||||
handleDraftChange(categoryId, value) {
|
||||
@@ -228,10 +245,7 @@ class ArtikelKategorienPage extends Component {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.props.renameCategory({
|
||||
categoryId,
|
||||
name: trimmed,
|
||||
}).unwrap();
|
||||
await categoryAutomat.actions.renameCategory({ categoryId, name: trimmed });
|
||||
enqueueSnackbar('Kategorie umbenannt.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
@@ -240,7 +254,7 @@ class ArtikelKategorienPage extends Component {
|
||||
|
||||
handleDeleteCategory = async (category) => {
|
||||
try {
|
||||
await this.props.deleteCategory(category.id).unwrap();
|
||||
await categoryAutomat.actions.deleteCategory(category.id);
|
||||
enqueueSnackbar('Kategorie gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
@@ -248,18 +262,18 @@ class ArtikelKategorienPage extends Component {
|
||||
};
|
||||
|
||||
handleRetryTree = () => {
|
||||
if (this.props.companyId) this.props.loadCategoryTree();
|
||||
if (this.state.companyId) void categoryAutomat.actions.loadTree();
|
||||
};
|
||||
|
||||
handleRetryArticles = () => {
|
||||
const { currentCategoryId } = this.props;
|
||||
const { currentCategoryId } = this.state;
|
||||
if (currentCategoryId) {
|
||||
this.props.loadCategoryArticles(currentCategoryId);
|
||||
void categoryAutomat.actions.loadArticles(currentCategoryId);
|
||||
}
|
||||
};
|
||||
|
||||
handleNavigateBack = () => {
|
||||
const { breadcrumbs } = this.props;
|
||||
const { breadcrumbs } = this.state;
|
||||
if (breadcrumbs.length < 2) return;
|
||||
this.navigateToCategoryById(breadcrumbs[breadcrumbs.length - 2].id);
|
||||
};
|
||||
@@ -283,7 +297,8 @@ class ArtikelKategorienPage extends Component {
|
||||
articles,
|
||||
renameMode,
|
||||
terminals,
|
||||
} = this.props;
|
||||
drafts,
|
||||
} = this.state;
|
||||
|
||||
if (!companyId) {
|
||||
return (
|
||||
@@ -347,8 +362,8 @@ class ArtikelKategorienPage extends Component {
|
||||
{childCategories.map((item, index) => {
|
||||
const label = categoryDisplayName(item.name, item.id, rootCategoryId);
|
||||
const draftName =
|
||||
this.state.drafts[item.id] !== undefined
|
||||
? this.state.drafts[item.id]
|
||||
drafts[item.id] !== undefined
|
||||
? drafts[item.id]
|
||||
: label;
|
||||
const editProps = renameMode
|
||||
? {
|
||||
@@ -359,13 +374,12 @@ class ArtikelKategorienPage extends Component {
|
||||
terminals,
|
||||
categoryPosterminalIds: item.posterminalIds || [],
|
||||
onTerminalToggle: (terminalId, enabled) => {
|
||||
const thunk = enabled
|
||||
? this.props.enableTerminalCategory
|
||||
: this.props.disableTerminalCategory;
|
||||
void thunk({ terminalId, categoryId: item.id })
|
||||
.unwrap()
|
||||
const action = enabled
|
||||
? posterminalAutomat.actions.enableCategory
|
||||
: posterminalAutomat.actions.disableCategory;
|
||||
void action({ terminalId, categoryId: item.id })
|
||||
.then(() => {
|
||||
this.props.patchCategoryPosterminalIds({
|
||||
categoryAutomat.actions.patchCategoryPosterminalIds({
|
||||
categoryId: item.id,
|
||||
terminalId,
|
||||
enabled,
|
||||
@@ -382,10 +396,9 @@ class ArtikelKategorienPage extends Component {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
// Prepare image (resize and encode to AVIF)
|
||||
const { prepareArticlePhoto } = await import('../../utils/prepareArticlePhoto');
|
||||
const avifFile = await prepareArticlePhoto(file);
|
||||
await this.props.uploadCategoryImage({
|
||||
await categoryAutomat.actions.uploadCategoryImage({
|
||||
categoryId: item.id,
|
||||
file: avifFile,
|
||||
});
|
||||
@@ -396,7 +409,7 @@ class ArtikelKategorienPage extends Component {
|
||||
input.click();
|
||||
},
|
||||
onPhotoDelete: () => {
|
||||
void this.props.deleteCategoryImage(item.id)
|
||||
void categoryAutomat.actions.deleteCategoryImage(item.id)
|
||||
.catch(notifyError);
|
||||
},
|
||||
nameInput: (
|
||||
@@ -478,36 +491,4 @@ class ArtikelKategorienPage extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
companyId: selectCompanyId(state),
|
||||
categories: selectCategoryMap(state),
|
||||
rootCategoryId: state.category.rootCategoryId,
|
||||
treeStatus: selectCategoryTreeStatus(state),
|
||||
articlesStatus: selectCategoryArticlesStatus(state),
|
||||
error: selectCategoryError(state),
|
||||
currentCategoryId: state.category.currentCategoryId,
|
||||
breadcrumbs: selectCategoryBreadcrumbs(state),
|
||||
childCategories: selectChildCategories(state),
|
||||
articles: selectCategoryArticles(state),
|
||||
canGoBack: selectCanNavigateBack(state),
|
||||
renameMode: selectRenameMode(state),
|
||||
terminals: selectPosterminals(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadCategoryTree,
|
||||
loadCategoryArticles,
|
||||
loadPosterminals,
|
||||
navigateToCategory,
|
||||
renameCategory,
|
||||
deleteCategory,
|
||||
moveCategory,
|
||||
reorderCategories,
|
||||
enableTerminalCategory,
|
||||
disableTerminalCategory,
|
||||
patchCategoryPosterminalIds,
|
||||
uploadCategoryImage,
|
||||
deleteCategoryImage,
|
||||
};
|
||||
|
||||
export default withCategoryRoute(connect(mapStateToProps, mapDispatchToProps)(ArtikelKategorienPage));
|
||||
export default withCategoryRoute(ArtikelKategorienPage);
|
||||
|
||||
@@ -31,6 +31,7 @@ import { requireArray, requireDefined, requireObject } from '../../utils/apiShap
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
|
||||
import { generateTextThumbnail } from '../../utils/textThumbnail';
|
||||
import { extractPdfText } from '../../utils/pdfText';
|
||||
import { getDeliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
|
||||
import { setLastDeliveryPath, userSettingsAutomat, setViewerPosition } from '../../automats';
|
||||
@@ -477,12 +478,19 @@ class LieferungNeuPage extends PureComponent {
|
||||
const text = await extractPdfText(file, { layout: true });
|
||||
const txtName = file.name.replace(/\.pdf$/i, '') + '.txt';
|
||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
||||
let txtThumbnail = null;
|
||||
try {
|
||||
txtThumbnail = await generateTextThumbnail(text);
|
||||
} catch (thumbErr) {
|
||||
console.error('Fehler beim Erstellen des Text-Thumbnails:', thumbErr);
|
||||
}
|
||||
const txtAttachment = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
file: txtFile,
|
||||
name: txtFile.name,
|
||||
size: txtFile.size,
|
||||
type: 'text/plain',
|
||||
thumbnail: txtThumbnail,
|
||||
text,
|
||||
parentPdfId: pdfId,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
@@ -499,6 +507,12 @@ class LieferungNeuPage extends PureComponent {
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Lesen der Datei:', err);
|
||||
}
|
||||
let thumbnail = null;
|
||||
try {
|
||||
thumbnail = await generateTextThumbnail(text || file);
|
||||
} catch (thumbErr) {
|
||||
console.error('Fehler beim Erstellen des Text-Thumbnails:', thumbErr);
|
||||
}
|
||||
const attachmentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const textAttachment = {
|
||||
id: attachmentId,
|
||||
@@ -506,6 +520,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: isCsv ? 'text/csv' : 'text/plain',
|
||||
thumbnail,
|
||||
text,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
@@ -537,6 +552,13 @@ class LieferungNeuPage extends PureComponent {
|
||||
const txtName = attachment.name.replace(/\.pdf$/i, '') + '.txt';
|
||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
||||
|
||||
let txtThumbnail = null;
|
||||
try {
|
||||
txtThumbnail = await generateTextThumbnail(text);
|
||||
} catch (thumbErr) {
|
||||
console.error('Fehler beim Aktualisieren des Text-Thumbnails:', thumbErr);
|
||||
}
|
||||
|
||||
const existingTxt = this.state.attachments?.find(
|
||||
(a) => a.parentPdfId === attachment.id || a.name === txtName,
|
||||
);
|
||||
@@ -546,6 +568,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
file: txtFile,
|
||||
size: txtFile.size,
|
||||
text,
|
||||
thumbnail: txtThumbnail || existingTxt.thumbnail,
|
||||
isDirty: true,
|
||||
});
|
||||
} else {
|
||||
@@ -555,6 +578,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
name: txtFile.name,
|
||||
size: txtFile.size,
|
||||
type: 'text/plain',
|
||||
thumbnail: txtThumbnail,
|
||||
text,
|
||||
parentPdfId: attachment.id,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
@@ -996,7 +1020,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
handleSaveTextEditor(newText) {
|
||||
async handleSaveTextEditor(newText) {
|
||||
const attachment = this.state.textEditorAttachment;
|
||||
if (!attachment) return;
|
||||
|
||||
@@ -1004,11 +1028,19 @@ class LieferungNeuPage extends PureComponent {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
|
||||
let thumbnail = attachment.thumbnail;
|
||||
try {
|
||||
thumbnail = await generateTextThumbnail(newText);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Aktualisieren des Text-Thumbnails:', err);
|
||||
}
|
||||
|
||||
this.automat.actions.updateAttachment({
|
||||
id: attachment.id,
|
||||
file: newFile,
|
||||
size: newFile.size,
|
||||
text: newText,
|
||||
thumbnail,
|
||||
isDirty: true,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user