import { PureComponent, createRef } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import TuneIcon from '@mui/icons-material/Tune'; import Sortable from 'sortablejs'; import { enqueueSnackbar } from 'notistack'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import FillSpace from 'dashgui/FillSpace'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import BackendError from '../BackendError'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; import propertyAutomat, { selectProperties } from '../../automats/propertyAutomat'; import ArtikelMerkmalText from './ArtikelMerkmalText'; import { ORANGE_PANEL } from '../../colors'; /** * Tab 2: Text-Merkmale */ class TextMerkmaleTab extends PureComponent { // #region Constructor & State constructor(props) { super(props); const initialProperties = selectProperties() || []; this.state = { propStatus: propertyAutomat.state.status === 'succeeded' ? 'ready' : 'loading', error: propertyAutomat.state.error || null, properties: initialProperties, propFilter: '', propSubmitting: false, propDeletingUnused: false, propPage: 1, }; this._cancelled = false; this.propListRef = createRef(); this._propSortable = null; this.handleRetry = this.handleRetry.bind(this); this.handleCreateProperty = this.handleCreateProperty.bind(this); this.handleDeleteUnusedProperties = this.handleDeleteUnusedProperties.bind(this); } // #endregion // #region Lifecycle componentDidMount() { this._cancelled = false; console.log('TextMerkmaleTab mounted', this.props); this.unsubProp = propertyAutomat.subscribe((state) => { if (this._cancelled) return; if (state.status === 'succeeded') { this.setState({ propStatus: 'ready', error: null, properties: state.properties, }); } else if (state.status === 'failed') { this.setState({ propStatus: 'failed', error: backendErrorFrom(state.error) }); } }); if (propertyAutomat.state.status !== 'succeeded') { void this.load(); } else { this._initPropSortable(); } } componentDidUpdate(prevProps, prevState) { if ( (prevState.properties !== this.state.properties || prevState.propFilter !== this.state.propFilter || prevState.propPage !== this.state.propPage || prevState.propStatus !== this.state.propStatus) && this.propListRef.current ) { if (this.state.propStatus === 'ready') { this._initPropSortable(); } } } componentWillUnmount() { this._cancelled = true; this.unsubProp?.(); this._destroySortables(); } // #endregion // #region Sortable Reorder _initPropSortable() { this._propSortable?.destroy(); const el = this.propListRef.current; if (!el) return; const isFiltered = Boolean(this.state.propFilter?.trim()); this._propSortable = Sortable.create(el, { handle: '.drag-handle', animation: 150, ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', disabled: isFiltered, onEnd: (evt) => { const { oldIndex, newIndex } = evt; if (oldIndex === newIndex) return; const offset = ((this.state.propPage || 1) - 1) * 8; const realOld = offset + oldIndex; const realNew = offset + newIndex; if (realOld === realNew) return; this.setState( (prev) => { const next = [...prev.properties]; const [moved] = next.splice(realOld, 1); next.splice(realNew, 0, moved); return { properties: next }; }, () => { const ids = this.state.properties.map((p) => p.id); void this._persistPropOrder(ids); } ); }, }); } _destroySortables() { this._propSortable?.destroy(); this._propSortable = null; } async _persistPropOrder(ids) { try { await propertyAutomat.actions.reorderProperties(ids); } catch (err) { if (this._cancelled) return; notifyError(err); void propertyAutomat.actions.load(); } } // #endregion // #region Actions & Handlers async load() { this.setState({ propStatus: propertyAutomat.state.status === 'succeeded' ? 'ready' : 'loading', error: null, }); try { await propertyAutomat.actions.load(); } catch (err) { if (this._cancelled) return; this.setState({ error: backendErrorFrom(err) }); } } handleRetry() { void this.load(); } async handleCreateProperty() { const name = this.state.propFilter.trim(); if (!name || this.state.propSubmitting) return; this.setState({ propSubmitting: true }); try { const row = await propertyAutomat.actions.createProperty({ name, exclusive: true, }); if (this._cancelled) return; this.setState({ propFilter: '', propSubmitting: false }); enqueueSnackbar(`Merkmal „${row?.name || name}“ angelegt.`, { variant: 'success' }); } catch (err) { if (this._cancelled) return; this.setState({ propSubmitting: false }); notifyError(err); } } async handleDeleteUnusedProperties() { const { properties, propDeletingUnused } = this.state; if (propDeletingUnused) return; const unused = properties.filter((p) => (p.articleCount ?? 0) === 0); if (!unused.length) return; if (!window.confirm(`Alle ${unused.length} ungenutzten Text-Merkmale wirklich löschen?`)) { return; } this.setState({ propDeletingUnused: true }); try { await propertyAutomat.actions.deleteProperties(unused.map((p) => p.id)); enqueueSnackbar(`${unused.length} ungenutzte Text-Merkmale gelöscht.`, { variant: 'success' }); } catch (err) { notifyError(err); } finally { if (!this._cancelled) { this.setState({ propDeletingUnused: false }); } } } // #endregion // #region Render render() { const { propStatus, error, properties, propFilter, propSubmitting, propDeletingUnused, propPage, } = this.state; if (propStatus === 'loading' && !properties.length) { return ( ); } if (error && !properties.length) { return ; } const propFilterTrimmed = propFilter.trim(); const filteredProperties = propFilterTrimmed ? properties.filter((p) => p.name.toLowerCase().includes(propFilterTrimmed.toLowerCase())) : properties; const unusedPropCount = properties.filter((p) => (p.articleCount ?? 0) === 0).length; return ( {/* Tile 3: Text-Merkmale */} Text-Merkmale this.setState({ propFilter: next })} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); void this.handleCreateProperty(); } }} /> this.setState({ propPage: page })} renderItem={ArtikelMerkmalText} /> ); } // #endregion } export default TextMerkmaleTab;