From 9cb93bddeb5dd9ad66d2fc26caf893b47b8b67a1 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 18 Oct 2026 17:47:41 +0200 Subject: [PATCH] u --- .agents/skills/frontend-routes/SKILL.md | 1 + src/api/carrier.js | 2 +- src/appRouter.jsx | 5 +- src/automats/carrierAutomat.js | 2 +- src/components/carrier/CarrierItem.jsx | 6 +- .../manifest/ShipmentDetailDialog.jsx | 4 +- src/nav.js | 8 +- src/routes/auftrag/versandarten.jsx | 270 ++++++++++++++++++ 8 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 src/routes/auftrag/versandarten.jsx diff --git a/.agents/skills/frontend-routes/SKILL.md b/.agents/skills/frontend-routes/SKILL.md index aafd812..4d175a6 100644 --- a/.agents/skills/frontend-routes/SKILL.md +++ b/.agents/skills/frontend-routes/SKILL.md @@ -35,6 +35,7 @@ Reference guide for the backend LLM agent when emitting route navigation actions | **Order Detail** | `/auftrag/bestellung/:orderId` | Order document ID: `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` | | **Shipments (Ausliefern)** | `/auftrag/ausliefern` | Static (shipment manifests & parcels) | `/auftrag/ausliefern` | | **Payments (Zahlungen)** | `/auftrag/zahlungen` | Static (order payments ledger) | `/auftrag/zahlungen` | +| **Carriers (Versandarten)** | `/auftrag/versandarten` | Static (Versanddienstleister & Versandarten) | `/auftrag/versandarten` | | **Customer List** | `/kunde` | Static | `/kunde` | | **New Customer** | `/kunde/neu` | Static form | `/kunde/neu` | diff --git a/src/api/carrier.js b/src/api/carrier.js index de7dfbd..ad4ebcf 100644 --- a/src/api/carrier.js +++ b/src/api/carrier.js @@ -1,7 +1,7 @@ import { http } from './http.js'; /** - * Company-scoped carriers / Lieferarten. + * Company-scoped carriers / Versandarten. * Shape: Carrier = { id, name, sort, hasPackages, isReverse } */ export const carrier = { diff --git a/src/appRouter.jsx b/src/appRouter.jsx index 66e84a7..0987fa0 100644 --- a/src/appRouter.jsx +++ b/src/appRouter.jsx @@ -23,7 +23,7 @@ import AuftragBestellungNeuPage from './routes/auftrag/bestellung/neu'; import AuftragBestellungEditPage from './routes/auftrag/bestellung/_orderId'; import AuftragAusliefernPage from './routes/auftrag/ausliefern'; import AuftragZahlungenPage from './routes/auftrag/zahlungen'; -import AuftragLieferartenPage from './routes/auftrag/lieferarten'; +import AuftragVersandartenPage from './routes/auftrag/versandarten'; import KundeListePage from './routes/kunde'; import KundeNeuPage from './routes/kunde/neu'; @@ -107,7 +107,8 @@ const routes = [ { path: 'bestellung/:orderId', element: }, { path: 'ausliefern', element: }, { path: 'zahlungen', element: }, - { path: 'lieferarten', element: }, + { path: 'versandarten', element: }, + { path: 'lieferarten', element: }, { path: ':quoteId', element: }, ], diff --git a/src/automats/carrierAutomat.js b/src/automats/carrierAutomat.js index c6a428e..7bf5bc8 100644 --- a/src/automats/carrierAutomat.js +++ b/src/automats/carrierAutomat.js @@ -1,6 +1,6 @@ /** * @file carrierAutomat.js - * @description Manages cached shipping carriers / Lieferarten (`GET /api/carrier`) for the active company. + * @description Manages cached shipping carriers / Versandarten (`GET /api/carrier`) for the active company. * * Upstream Triggers: * - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers diff --git a/src/components/carrier/CarrierItem.jsx b/src/components/carrier/CarrierItem.jsx index 589e35f..8299959 100644 --- a/src/components/carrier/CarrierItem.jsx +++ b/src/components/carrier/CarrierItem.jsx @@ -56,7 +56,7 @@ class CarrierItem extends PureComponent { async updateCarrier(item, patch) { try { await carrierAutomat.actions.update(item.id, patch); - enqueueSnackbar('Lieferart gespeichert.', { variant: 'success' }); + enqueueSnackbar('Versandart gespeichert.', { variant: 'success' }); } catch (err) { this.setState({ draft: item.name ?? '' }); notifyError(err); @@ -93,11 +93,11 @@ class CarrierItem extends PureComponent { handleDelete = () => { const item = this.props.carrier ?? this.props.item; if (!item) return; - if (!window.confirm(`Lieferart „${item.name}“ wirklich löschen?`)) return; + if (!window.confirm(`Versandart „${item.name}“ wirklich löschen?`)) return; void (async () => { try { await carrierAutomat.actions.remove(item.id); - enqueueSnackbar('Lieferart gelöscht.', { variant: 'success' }); + enqueueSnackbar('Versandart gelöscht.', { variant: 'success' }); } catch (err) { notifyError(err); } diff --git a/src/components/manifest/ShipmentDetailDialog.jsx b/src/components/manifest/ShipmentDetailDialog.jsx index bd56183..25f7815 100644 --- a/src/components/manifest/ShipmentDetailDialog.jsx +++ b/src/components/manifest/ShipmentDetailDialog.jsx @@ -118,14 +118,14 @@ class AddPackageDialog extends PureComponent { this.setState({ carrierId: e.target.value })} error={carriers.length === 0} helperText={ carriers.length === 0 - ? 'Keine Lieferarten eingerichtet (unter Auftrag → Lieferarten anlegen)' + ? 'Keine Versandarten eingerichtet (unter Auftrag → Versandarten anlegen)' : undefined } > diff --git a/src/nav.js b/src/nav.js index e385265..9520400 100644 --- a/src/nav.js +++ b/src/nav.js @@ -229,11 +229,11 @@ export const SUB_NAV = { help: 'Zahlungseingänge erfassen und Auftragszahlungen überwachen', }, { - segment: 'lieferarten', - label: 'Lieferarten', - title: 'Lieferarten', + segment: 'versandarten', + label: 'Versandarten', + title: 'Versandarten', icon: LocalShippingOutlinedIcon, - help: 'Versanddienstleister und Lieferarten (Carriers) verwalten', + help: 'Versanddienstleister und Versandarten (Carriers) verwalten', }, ], '/kunde': [ diff --git a/src/routes/auftrag/versandarten.jsx b/src/routes/auftrag/versandarten.jsx new file mode 100644 index 0000000..6164cd0 --- /dev/null +++ b/src/routes/auftrag/versandarten.jsx @@ -0,0 +1,270 @@ +import { PureComponent, createRef } from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import List from '@mui/material/List'; +import Typography from '@mui/material/Typography'; +import Sortable from 'sortablejs'; +import { enqueueSnackbar } from 'notistack'; +import BackendError from '../../components/BackendError'; +import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import { PanelTile, PanelTileGrid } from 'dashgui/panels'; +import withNavigate from '../../components/withNavigate'; +import { backendErrorFrom } from '../../utils/backendError'; +import { notifyError } from '../../utils/notify'; +import carrierAutomat from '../../automats/carrierAutomat'; +import { CarrierItem } from '../../components/carrier'; +import { GREEN_PANEL } from '../../colors'; + +class VersandartenSearchField extends PureComponent { + constructor(props) { + super(props); + const initial = carrierAutomat.state.filter || ''; + this.state = { + input: initial, + submitting: false, + }; + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentDidMount() { + this.unsub = carrierAutomat.subscribe(this, (state) => ({ + input: state.filter, + })); + } + + componentWillUnmount() { + this.unsub?.(); + } + + onInputChange(input) { + this.setState({ input }); + carrierAutomat.actions.filter(input); + } + + async handleSubmit() { + const name = this.state.input.trim(); + if (!name || this.state.submitting) return; + + this.setState({ submitting: true }); + try { + const row = await carrierAutomat.actions.create({ + name, + hasPackages: true, + isReverse: false, + }); + enqueueSnackbar(`Versandart „${row.name || name}“ angelegt.`, { variant: 'success' }); + this.setState({ input: '', submitting: false }); + carrierAutomat.actions.filter(''); + } catch (err) { + this.setState({ submitting: false }); + notifyError(err); + } + } + + render() { + const { inputRef } = this.props; + const { input, submitting } = this.state; + const canSubmit = Boolean(input.trim()) && !submitting; + + return ( + + + + this.onInputChange(next)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + void this.handleSubmit(); + } + }} + /> + + + + + ); + } +} + +class AuftragVersandartenPage extends PureComponent { + constructor(props) { + super(props); + const initialCarriers = + carrierAutomat.state.filteredCarriers ?? carrierAutomat.state.carriers ?? []; + this.state = { + status: carrierAutomat.state.status === 'ready' ? 'ready' : 'loading', + error: carrierAutomat.state.error ? backendErrorFrom(carrierAutomat.state.error) : null, + carriers: initialCarriers, + filter: carrierAutomat.state.filter ?? '', + }; + this.searchRef = createRef(); + this.listRef = createRef(); + this._sortable = null; + this._cancelled = false; + this.handleRetry = this.handleRetry.bind(this); + this._initSortable = this._initSortable.bind(this); + this._destroySortable = this._destroySortable.bind(this); + } + + componentDidMount() { + this._cancelled = false; + + this.unsub = carrierAutomat.subscribe(this, (state) => ({ + carriers: state.filteredCarriers ?? state.carriers, + filter: state.filter ?? '', + status: state.status, + error: state.error ? backendErrorFrom(state.error) : null, + })); + + if (carrierAutomat.state.status !== 'ready') { + void carrierAutomat.actions.load().catch((err) => notifyError(err)); + } else { + this._initSortable(); + } + } + + componentDidUpdate(prevProps, prevState) { + if ( + prevState.status !== this.state.status || + prevState.carriers !== this.state.carriers || + prevState.filter !== this.state.filter + ) { + if (this.state.status === 'ready') { + this._initSortable(); + } + } + } + + componentWillUnmount() { + this._cancelled = true; + this.unsub?.(); + this._destroySortable(); + } + + _initSortable() { + this._destroySortable(); + const el = this.listRef.current; + if (!el) return; + + const isFiltered = Boolean(this.state.filter?.trim()); + + this._sortable = 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; + + this.setState( + (prev) => { + const next = [...prev.carriers]; + const [moved] = next.splice(oldIndex, 1); + next.splice(newIndex, 0, moved); + return { carriers: next }; + }, + () => { + const ids = this.state.carriers.map((c) => c.id); + void this._persistOrder(ids); + } + ); + }, + }); + } + + _destroySortable() { + this._sortable?.destroy(); + this._sortable = null; + } + + async _persistOrder(carrierIds) { + try { + await carrierAutomat.actions.reorder(carrierIds); + } catch (err) { + if (this._cancelled) return; + notifyError(err); + void carrierAutomat.actions.reload(); + } + } + + handleRetry() { + void carrierAutomat.actions.load().catch((err) => notifyError(err)); + } + + render() { + const { status, error, carriers, filter } = this.state; + + if (status === 'loading' || status === 'idle') { + return ( + + + + ); + } + + if (status === 'failed') { + return ; + } + + const trimmedFilter = filter?.trim() ?? ''; + + return ( + <> + + + + + {carriers.length === 0 ? ( + + {trimmedFilter + ? `Keine Treffer für „${trimmedFilter}“.` + : 'Noch keine Versandarten angelegt.'} + + ) : ( + carriers.map((c) => ( + + )) + )} + + + + + ); + } +} + +export { CarrierItem, VersandartenSearchField }; +export default withNavigate(AuftragVersandartenPage);