This commit is contained in:
seb
2026-09-22 07:20:31 +02:00
parent 536e7b1e2a
commit 5e13b8f9cd
32 changed files with 2485 additions and 1908 deletions

View File

@@ -1,6 +1,5 @@
import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import Box from '@mui/material/Box';
import Popover from '@mui/material/Popover';
import MenuList from '@mui/material/MenuList';
@@ -14,13 +13,7 @@ import { enqueueSnackbar } from 'notistack';
import PageLayout from './PageLayout';
import { NAV_ITEMS, SUB_NAV, resolveActiveSub } from '../nav';
import { isKategorienPath } from '../pages/artikel/categoryRoute';
import {
selectRenameMode,
setRenameMode,
selectCategoryMap,
selectCategoryRootId,
loadCategoryTree,
} from '../store/categorySlice';
import categoryAutomat from '../automats/categoryAutomat';
import { ActionElevatedPill, ToggleElevatedPill } from 'dashgui/ElevatedPill';
import { article, category } from '../api';
import { importArtikelCsv } from '../utils/importArtikel';
@@ -37,15 +30,31 @@ function triggerDownload(blob, filename) {
URL.revokeObjectURL(url);
}
function selectCatSlice(s) {
return {
renameMode: s.renameMode,
categories: s.categories,
rootCategoryId: s.rootCategoryId,
};
}
export default function SectionLayout({ sectionPath }) {
const { pathname } = useLocation();
const dispatch = useDispatch();
const renameMode = useSelector(selectRenameMode);
const categories = useSelector(selectCategoryMap);
const rootCategoryId = useSelector(selectCategoryRootId);
const importInputRef = useRef(null);
const [extrasAnchor, setExtrasAnchor] = useState(null);
const [importing, setImporting] = useState(false);
const [catState, setCatState] = useState(() => selectCatSlice(categoryAutomat.state));
useEffect(() => {
// Automat supports callback subscriptions: (state, partial) => void
const unsub = categoryAutomat.subscribe((state) => {
setCatState(selectCatSlice(state));
}, selectCatSlice);
return unsub;
}, []);
const { renameMode, categories, rootCategoryId } = catState;
const title = NAV_ITEMS.find((item) => item.path === sectionPath)?.label ?? '';
const subNav = SUB_NAV[sectionPath];
const subSegment = resolveActiveSub(pathname, sectionPath);
@@ -109,7 +118,7 @@ export default function SectionLayout({ sectionPath }) {
},
});
if (result.ok > 0 && rootCategoryId != null) {
await dispatch(loadCategoryTree()).unwrap();
await categoryAutomat.actions.loadTree();
}
if (result.ok > 0) {
enqueueSnackbar(
@@ -136,7 +145,7 @@ export default function SectionLayout({ sectionPath }) {
<ToggleElevatedPill
label="Edit"
checked={renameMode}
onToggle={(checked) => dispatch(setRenameMode(checked))}
onToggle={(checked) => categoryAutomat.actions.setRenameMode(checked)}
/>
<ActionElevatedPill
label="Extras"

View File

@@ -14,7 +14,7 @@ import CheckIcon from '@mui/icons-material/Check';
import FolderIcon from '@mui/icons-material/Folder';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import Box from '@mui/material/Box';
import { getCategoryChildren, categoryDisplayName } from '../../store/categorySlice';
import { getCategoryChildren, categoryDisplayName } from '../../utils/categoryUtils';
function CategoryNode({
categories,
categoryId,

View File

@@ -1,10 +1,6 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import { ActionTile } from '../tiles';
import {
createArticles as createArticlesThunk,
createArticle as createArticleThunk,
} from '../../store/categorySlice';
import categoryAutomat from '../../automats/categoryAutomat';
import ArticleNameDialog from './ArticleNameDialog';
class NewArticleTile extends Component {
@@ -28,15 +24,16 @@ class NewArticleTile extends Component {
const { parentId } = this.props;
const list = (names ?? []).filter(Boolean);
if (list.length <= 1) {
const article = await this.props
.createArticle({ parentId, name: list[0] ?? '' })
.unwrap();
const article = await categoryAutomat.actions.createArticle({
parentId,
name: list[0] ?? '',
});
if (article) {
this.props.onOpenArticle?.(article);
}
return;
}
const created = await this.props.createArticles({ parentId, names: list }).unwrap();
const created = await categoryAutomat.actions.createArticles({ parentId, names: list });
if (created?.[0]) {
this.props.onOpenArticle?.(created[0]);
}
@@ -62,9 +59,4 @@ class NewArticleTile extends Component {
}
}
const mapDispatchToProps = {
createArticle: createArticleThunk,
createArticles: createArticlesThunk,
};
export default connect(null, mapDispatchToProps)(NewArticleTile);
export default NewArticleTile;

View File

@@ -2,7 +2,7 @@ import { Component, PureComponent } from 'react';
import { EditablePanelTile } from 'dashgui/panels';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
import { getCategoryBreadcrumbs } from '../../../utils/categoryUtils';
import { Box, Chip, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import CategoryPickerDialog from '../CategoryPickerDialog';

View File

@@ -169,12 +169,7 @@ function priceDraftValid(draft) {
function companyFromStore(store) {
if (!store) return null;
const companies = store.companies;
const session = store.authMe;
if (companies == null || session == null) return null;
const companyId = session.user?.selectedCompanyId;
if (companyId == null) return null;
return companies.find((c) => c.id === companyId) ?? null;
return store.company ?? null;
}
class PriceAmountField extends Component {
@@ -395,18 +390,12 @@ export default class ArticleViewPrice extends PureComponent {
async load() {
const store = this.context;
try {
const [unitList, companies, session] = await Promise.all([
const [unitList, company] = await Promise.all([
store.unitPromise,
store.companyPromise,
store.authMePromise,
]);
const companyId = requireDefined(session.user?.selectedCompanyId, 'selectedCompanyId');
const current = requireDefined(
companies.find((c) => c.id === companyId),
'company',
);
if (!this._cancelled) {
this.setState({ units: unitList, company: current });
this.setState({ units: unitList, company: company ?? null });
}
} catch (err) {
if (!this._cancelled) notifyError(err);

View File

@@ -1,10 +1,6 @@
import { Component } from 'react';
import { connect } from 'react-redux';
import { ActionTile } from '../tiles';
import {
createCategories as createCategoriesThunk,
createCategory as createCategoryThunk,
} from '../../store/categorySlice';
import categoryAutomat from '../../automats/categoryAutomat';
import CategoryNameDialog from './CategoryNameDialog';
class NewCategoryTile extends Component {
@@ -28,10 +24,10 @@ class NewCategoryTile extends Component {
const { parentId } = this.props;
const list = (names ?? []).filter(Boolean);
if (list.length <= 1) {
await this.props.createCategory({ parentId, name: list[0] ?? '' }).unwrap();
await categoryAutomat.actions.createCategory({ parentId, name: list[0] ?? '' });
return;
}
await this.props.createCategories({ parentId, names: list }).unwrap();
await categoryAutomat.actions.createCategories({ parentId, names: list });
}
render() {
@@ -55,9 +51,4 @@ class NewCategoryTile extends Component {
}
}
const mapDispatchToProps = {
createCategory: createCategoryThunk,
createCategories: createCategoriesThunk,
};
export default connect(null, mapDispatchToProps)(NewCategoryTile);
export default NewCategoryTile;

View File

@@ -8,8 +8,43 @@ import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import SyncIcon from '@mui/icons-material/Sync';
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
import { generateTextThumbnail } from '../../utils/textThumbnail';
export class Attachment extends PureComponent {
state = {
generatedThumbnail: null,
};
componentDidMount() {
this.ensureThumbnail();
}
componentDidUpdate(prevProps) {
if (this.props.attachment !== prevProps.attachment) {
this.ensureThumbnail();
}
}
async ensureThumbnail() {
const { attachment } = this.props;
if (!attachment || attachment.thumbnail) return;
const isCsv = attachment.type === 'text/csv' || attachment.name?.toLowerCase().endsWith('.csv');
const isText = attachment.type?.startsWith('text/') || attachment.name?.toLowerCase().endsWith('.txt') || isCsv;
if (!isText) return;
const source = attachment.text || attachment.file;
if (!source) return;
try {
const thumb = await generateTextThumbnail(source);
if (thumb && this.props.attachment === attachment) {
this.setState({ generatedThumbnail: thumb });
}
} catch (err) {
console.error('Fehler beim Generieren des Text-Thumbnails:', err);
}
}
handleRescan = (e) => {
e?.stopPropagation?.();
const { attachment, onRescan } = this.props;
@@ -56,7 +91,7 @@ export class Attachment extends PureComponent {
render() {
const { attachment, disabled, sx } = this.props;
const thumbnail = attachment?.thumbnail;
const thumbnail = attachment?.thumbnail || this.state.generatedThumbnail;
const name = attachment?.name || 'Dokument';
const size = attachment?.size;
const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf');

View File

@@ -341,10 +341,18 @@ export function collectFilesForUpload(attachments = [], { onlyNew = false } = {}
}
}
let thumbavif = null;
if (attachment.thumbnail) {
thumbavif = attachment.thumbnail.includes(',')
? attachment.thumbnail.split(',')[1]
: attachment.thumbnail;
}
files.push({
file: fileToUpload,
name: attachment.name || fileToUpload.name,
mimetype: attachment.type || fileToUpload.type,
thumbavif,
});
}
return files;
@@ -389,6 +397,10 @@ export async function buildManifestBody(
const filesToUpload = collectFilesForUpload(form.attachments, { onlyNew: isUpdate });
if (filesToUpload.length > 0) {
const formData = new FormData();
body.filesMeta = filesToUpload.map((item) => ({
name: item.name,
thumbavif: item.thumbavif ?? null,
}));
formData.append('data', JSON.stringify(body));
for (const item of filesToUpload) {
formData.append('files', item.file, item.name);