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);