diff --git a/src/api/carrier.js b/src/api/carrier.js new file mode 100644 index 0000000..de7dfbd --- /dev/null +++ b/src/api/carrier.js @@ -0,0 +1,49 @@ +import { http } from './http.js'; + +/** + * Company-scoped carriers / Lieferarten. + * Shape: Carrier = { id, name, sort, hasPackages, isReverse } + */ +export const carrier = { + /** GET /api/carrier → { carriers: Carrier[] } */ + list() { + return http('/api/carrier'); + }, + + /** + * POST /api/carrier + * Body: { name, sort?, hasPackages?, isReverse? } + * → 201 { carrier: Carrier } + */ + create(body) { + return http('/api/carrier', { method: 'POST', body }); + }, + + /** GET /api/carrier/:id → 200 { carrier: Carrier } */ + get(id) { + return http(`/api/carrier/${id}`); + }, + + /** + * PATCH /api/carrier/:id + * Body: { name?, sort?, hasPackages?, isReverse? } + * → 200 { carrier: Carrier } + */ + update(id, body) { + return http(`/api/carrier/${id}`, { method: 'PATCH', body }); + }, + + /** + * PUT /api/carrier/order + * Body: { carrierIds: number[] } + * → 200 { carrierIds: number[] } + */ + order(body) { + return http('/api/carrier/order', { method: 'PUT', body }); + }, + + /** DELETE /api/carrier/:id → 204 · 409 Conflict if in use */ + delete(id) { + return http(`/api/carrier/${id}`, { method: 'DELETE' }); + }, +}; diff --git a/src/api/index.js b/src/api/index.js index a444375..e7a194c 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -32,3 +32,4 @@ export { payment } from './payment.js'; export { account } from './account.js'; export { mcp } from './mcp.js'; export { backup } from './backup.js'; +export { carrier } from './carrier.js'; diff --git a/src/appRouter.jsx b/src/appRouter.jsx index e16e574..35806fa 100644 --- a/src/appRouter.jsx +++ b/src/appRouter.jsx @@ -23,6 +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 KundeListePage from './routes/kunde'; import KundeNeuPage from './routes/kunde/neu'; @@ -104,6 +105,7 @@ const routes = [ { path: 'bestellung/:orderId', element: }, { path: 'ausliefern', element: }, { path: 'zahlungen', element: }, + { path: 'lieferarten', element: }, { path: ':quoteId', element: }, ], diff --git a/src/automats/carrierAutomat.js b/src/automats/carrierAutomat.js new file mode 100644 index 0000000..c6a428e --- /dev/null +++ b/src/automats/carrierAutomat.js @@ -0,0 +1,251 @@ +/** + * @file carrierAutomat.js + * @description Manages cached shipping carriers / Lieferarten (`GET /api/carrier`) for the active company. + * + * Upstream Triggers: + * - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers + * `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.clear()` on `'anon'`. + * - `sseAutomat`: Calls `actions.reload()` upon receiving a `'carrier'` SSE event. + * - UI / Routes: Calls to `actions.load()`, `actions.reload()`, or mutation actions. + */ + +import { Automat } from 'react-automat'; +import { carrier as carrierApi } from '../api'; +import authAutomat from './authAutomat'; + +/** + * @typedef {Object} Carrier + * @property {number} id Carrier identifier. + * @property {string} name Carrier name. + * @property {number} sort Sort order. + * @property {boolean} hasPackages Whether carrier uses package tracking. + * @property {boolean} isReverse Whether carrier is used for returns. + */ + +/** + * @typedef {Object} CarrierState + * @property {Array} carriers Company-scoped carriers. + * @property {Array} filteredCarriers Filtered carriers according to active filter query. + * @property {string} filter Active search/filter string. + * @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state. + * @property {Error|null} error Last load error. + */ + +/** @type {CarrierState} */ +const initialState = { + carriers: [], + filteredCarriers: [], + filter: '', + status: 'idle', + error: null, +}; + +let carrierAutomat; + +const applyFilter = (carriers, filter) => { + const query = String(filter ?? '').trim().toLowerCase(); + return query ? carriers.filter((c) => String(c.name ?? '').toLowerCase().includes(query)) : carriers; +}; + +const updateStateWithCarriers = (carriers, extra = {}) => { + const list = Array.isArray(carriers) ? carriers : []; + const filteredCarriers = applyFilter(list, carrierAutomat.state.filter); + carrierAutomat.setState({ carriers: list, filteredCarriers, ...extra }); + return list; +}; + +/** + * Fetches the company-scoped carrier list. + * + * @returns {Promise>} Carrier list. + */ +async function fetchCarriers() { + const data = await carrierApi.list(); + if (Array.isArray(data)) return data; + if (data && Array.isArray(data.carriers)) return data.carriers; + return []; +} + +/** In-flight `load()` promise so concurrent callers share one request. */ +let inflightLoad = null; + +/** + * Loads the carrier list once, deduplicating concurrent callers. + * + * @returns {Promise>} Loaded carriers. + */ +function loadCarriers() { + if (inflightLoad) return inflightLoad; + inflightLoad = (async () => { + carrierAutomat.setState({ status: 'loading', error: null }); + try { + const carriers = await fetchCarriers(); + updateStateWithCarriers(carriers, { status: 'ready', error: null }); + return carriers; + } catch (error) { + carrierAutomat.setState({ status: 'failed', error }); + throw error; + } finally { + inflightLoad = null; + } + })(); + return inflightLoad; +} + +const actions = { + /** Sets the active carrier search filter and updates filteredCarriers. */ + filter(query) { + const filter = String(query ?? ''); + carrierAutomat.setState({ + filter, + filteredCarriers: applyFilter(carrierAutomat.state.carriers, filter), + }); + }, + + /** + * Loads the carrier list, exposing a loading/failed lifecycle. + * + * @returns {Promise>} Loaded carriers. + */ + load() { + return loadCarriers(); + }, + + /** + * Silently re-fetches the carrier list while keeping the current state. + * + * @returns {Promise>} Fresh carriers. + */ + async reload() { + try { + const carriers = await fetchCarriers(); + updateStateWithCarriers(carriers, { status: 'ready', error: null }); + return carriers; + } catch (error) { + carrierAutomat.setState({ error }); + throw error; + } + }, + + /** + * Creates a carrier and refreshes the cached list. + * + * @param {{name: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}} body Creation payload. + * @returns {Promise} Created carrier. + */ + async create(body) { + const data = await carrierApi.create(body); + const created = data?.carrier || data; + await carrierAutomat.actions.reload(); + return created; + }, + + /** + * Updates a carrier and refreshes the cached list. + * + * @param {string|number} id Carrier identifier. + * @param {{name?: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}} body Update payload. + * @returns {Promise} Updated carrier. + */ + async update(id, body) { + const data = await carrierApi.update(id, body); + const updated = data?.carrier || data; + await carrierAutomat.actions.reload(); + return updated; + }, + + /** + * Deletes a carrier and refreshes the cached list. + * + * @param {string|number} id Carrier identifier. + * @returns {Promise} + */ + async remove(id) { + await carrierApi.delete(id); + await carrierAutomat.actions.reload(); + }, + + /** + * Persists a new sort order for all carriers. + * + * @param {Array} carrierIds IDs in the desired order. + * @returns {Promise} + */ + async reorder(carrierIds) { + const byId = new Map(carrierAutomat.state.carriers.map((c) => [c.id, c])); + const reordered = carrierIds.map((id) => byId.get(id)).filter(Boolean); + if (reordered.length === carrierAutomat.state.carriers.length) { + updateStateWithCarriers(reordered); + } + await carrierApi.order({ carrierIds }); + await carrierAutomat.actions.reload(); + }, + + /** + * Replaces the cached list without a round-trip. + * + * @param {Array} carriers Carriers to cache. + * @returns {void} + */ + setCarriers(carriers) { + updateStateWithCarriers(carriers, { + status: 'ready', + error: null, + }); + }, + + /** + * Forgets the cached list and returns to the initial state. + * + * @returns {void} + */ + clear() { + carrierAutomat.setState({ ...initialState }); + }, +}; + +/** + * Company-scoped carrier cache. Loads automatically once an authenticated + * session with a selected company exists and is cleared when auth is gone. + * + * @type {Automat} + */ +carrierAutomat = new Automat(initialState, actions, { name: 'carrier' }); + +/** + * Derives the cache key from the auth state: the selected company while + * authenticated, otherwise `null`. + * + * @param {import('./authAutomat').AuthState} authState Auth automat state. + * @returns {string|null} Cache key. + */ +function authKey(authState) { + if (authState?.status !== 'authed' || !authState.user) return null; + const companyId = authState.user.selectedCompanyId; + return companyId == null ? null : String(companyId); +} + +let lastAuthKey; + +/** + * Loads the carrier list when auth/company becomes available and forgets it + * when the session ends. + * + * @param {import('./authAutomat').AuthState} authState Auth automat state. + * @returns {void} + */ +function syncWithAuth(authState) { + const key = authKey(authState); + if (key === lastAuthKey) return; + lastAuthKey = key; + if (key == null) { + carrierAutomat.actions.clear(); + } else { + void carrierAutomat.actions.load().catch(() => {}); + } +} + +authAutomat.subscribe(syncWithAuth); +syncWithAuth(authAutomat.state); + +export default carrierAutomat; diff --git a/src/automats/index.js b/src/automats/index.js index 26d8ac5..d3ebcee 100644 --- a/src/automats/index.js +++ b/src/automats/index.js @@ -46,6 +46,7 @@ export { export { default as supplierAutomat } from './supplierAutomat'; export { default as warehouseAutomat } from './warehouseAutomat'; +export { default as carrierAutomat } from './carrierAutomat'; /** Article search over the derived index (replaces StorageManager.doSearch). */ export { diff --git a/src/automats/sseAutomat.js b/src/automats/sseAutomat.js index 386f6b7..90fb32b 100644 --- a/src/automats/sseAutomat.js +++ b/src/automats/sseAutomat.js @@ -36,6 +36,7 @@ import deliveryAutomat from './deliveryAutomat'; import orderAutomat from './orderAutomat'; import quoteAutomat from './quoteAutomat'; import shipmentAutomat from './shipmentAutomat'; +import carrierAutomat from './carrierAutomat'; /** @@ -223,6 +224,12 @@ function connect() { }); }); + addTrackedListener(es, 'carrier', () => { + void carrierAutomat.actions.reload().catch((err) => { + console.error('[sse] carrier refresh failed', err); + }); + }); + addTrackedListener(es, 'attributename', () => { void attributeAutomat.actions.load().catch((err) => { console.error('[sse] attributename refresh failed', err); diff --git a/src/components/carrier/CarrierItem.jsx b/src/components/carrier/CarrierItem.jsx new file mode 100644 index 0000000..589e35f --- /dev/null +++ b/src/components/carrier/CarrierItem.jsx @@ -0,0 +1,213 @@ +import { PureComponent } from 'react'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import ListItem from '@mui/material/ListItem'; +import Switch from '@mui/material/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import { enqueueSnackbar } from 'notistack'; +import carrierAutomat from '../../automats/carrierAutomat'; +import { notifyError } from '../../utils/notify'; + +class CarrierItem extends PureComponent { + constructor(props) { + super(props); + const item = props.carrier ?? props.item; + this.state = { + draft: item?.name ?? '', + }; + } + + componentDidUpdate(prevProps) { + const prevItem = prevProps.carrier ?? prevProps.item; + const item = this.props.carrier ?? this.props.item; + if (prevItem?.name !== item?.name) { + this.setState({ draft: item?.name ?? '' }); + } else if (prevProps.disabled && !this.props.disabled) { + if (this.state.draft !== item?.name) { + this.setState({ draft: item?.name ?? '' }); + } + } + } + + handleChange = (next) => { + this.setState({ draft: next }); + const item = this.props.carrier ?? this.props.item; + this.props.onChange?.(next, item); + }; + + handleBlur = () => { + const item = this.props.carrier ?? this.props.item; + if (!item) return; + const draft = this.state.draft.trim(); + if (!draft) { + this.setState({ draft: item.name ?? '' }); + return; + } + if (draft !== item.name) { + void this.updateCarrier(item, { name: draft }); + } + }; + + async updateCarrier(item, patch) { + try { + await carrierAutomat.actions.update(item.id, patch); + enqueueSnackbar('Lieferart gespeichert.', { variant: 'success' }); + } catch (err) { + this.setState({ draft: item.name ?? '' }); + notifyError(err); + } + } + + handleToggleHasPackages = (e) => { + const item = this.props.carrier ?? this.props.item; + if (!item) return; + const nextVal = e.target.checked; + void this.updateCarrier(item, { hasPackages: nextVal }); + }; + + handleToggleIsReverse = (e) => { + const item = this.props.carrier ?? this.props.item; + if (!item) return; + const nextVal = e.target.checked; + void this.updateCarrier(item, { isReverse: nextVal }); + }; + + handleKeyDown = (event) => { + const item = this.props.carrier ?? this.props.item; + if (event.key === 'Enter') { + event.preventDefault(); + event.target.blur(); + } + if (event.key === 'Escape') { + event.preventDefault(); + this.setState({ draft: item?.name ?? '' }); + event.target.blur(); + } + }; + + handleDelete = () => { + const item = this.props.carrier ?? this.props.item; + if (!item) return; + if (!window.confirm(`Lieferart „${item.name}“ wirklich löschen?`)) return; + void (async () => { + try { + await carrierAutomat.actions.remove(item.id); + enqueueSnackbar('Lieferart gelöscht.', { variant: 'success' }); + } catch (err) { + notifyError(err); + } + })(); + }; + + render() { + const { disabled, sx } = this.props; + const item = this.props.carrier ?? this.props.item; + const { draft } = this.state; + const name = item?.name ?? ''; + const hasPackages = Boolean(item?.hasPackages); + const isReverse = Boolean(item?.isReverse); + + return ( + + + + + + + + + + + } + label={ + + Pakete + + } + sx={{ m: 0, gap: 0.5 }} + /> + + + + + } + label={ + + Retoure + + } + sx={{ m: 0, gap: 0.5 }} + /> + + + + + + + + ); + } +} + +export default CarrierItem; diff --git a/src/components/carrier/index.js b/src/components/carrier/index.js new file mode 100644 index 0000000..ea08121 --- /dev/null +++ b/src/components/carrier/index.js @@ -0,0 +1 @@ +export { default as CarrierItem } from './CarrierItem'; diff --git a/src/components/manifest/ShipmentDetailDialog.jsx b/src/components/manifest/ShipmentDetailDialog.jsx index bb26f4f..bd56183 100644 --- a/src/components/manifest/ShipmentDetailDialog.jsx +++ b/src/components/manifest/ShipmentDetailDialog.jsx @@ -31,16 +31,17 @@ import { notifyError } from '../../utils/notify'; import { formatDate } from './manifestConfig'; import orderAutomat from '../../automats/orderAutomat'; import shipmentAutomat from '../../automats/shipmentAutomat'; +import carrierAutomat from '../../automats/carrierAutomat'; import { SURFACE_RAISED, WHITE } from '../../colors'; -const CARRIERS = ['DHL', 'UPS', 'DPD', 'GLS', 'Hermes', 'Spedition', 'Selbstabholung']; - // MARK: class AddPackageDialog extends PureComponent { constructor(props) { super(props); + const carriers = carrierAutomat.state.carriers || []; this.state = { - carrier: 'DHL', + carriers, + carrierId: carriers[0]?.id ?? '', trackingCode: '', type: 'parcel', state: 'draft', @@ -49,15 +50,34 @@ class AddPackageDialog extends PureComponent { this.handleSubmit = this.handleSubmit.bind(this); } + componentDidMount() { + this.unsub = carrierAutomat.subscribe(this, (state) => { + const carriers = state.carriers || []; + const currentId = this.state.carrierId; + const valid = carriers.some((c) => c.id === currentId); + return { + carriers, + carrierId: valid ? currentId : (carriers[0]?.id ?? ''), + }; + }); + if (carrierAutomat.state.status !== 'ready') { + void carrierAutomat.actions.load().catch(() => {}); + } + } + + componentWillUnmount() { + this.unsub?.(); + } + async handleSubmit() { const { shipment, onClose, onSuccess } = this.props; - const { carrier, trackingCode, type, state } = this.state; - if (!shipment?.id) return; + const { carrierId, trackingCode, type, state } = this.state; + if (!shipment?.id || !carrierId) return; this.setState({ saving: true }); try { await shippingApi.create(shipment.id, { - carrier: carrier.trim() || undefined, + carrierId: Number(carrierId), trackingCode: trackingCode.trim() || undefined, type, state, @@ -77,7 +97,8 @@ class AddPackageDialog extends PureComponent { render() { const { open, onClose, shipment } = this.props; - const { carrier, trackingCode, type, state, saving } = this.state; + const { carriers, carrierId, trackingCode, type, state, saving } = this.state; + const canSubmit = Boolean(carrierId) && !saving; return ( @@ -97,13 +118,21 @@ class AddPackageDialog extends PureComponent { this.setState({ carrier: e.target.value })} + value={carrierId} + onChange={(e) => this.setState({ carrierId: e.target.value })} + error={carriers.length === 0} + helperText={ + carriers.length === 0 + ? 'Keine Lieferarten eingerichtet (unter Auftrag → Lieferarten anlegen)' + : undefined + } > - {CARRIERS.map((c) => ( - {c} + {carriers.map((c) => ( + + {c.name} + ))} @@ -145,7 +174,7 @@ class AddPackageDialog extends PureComponent { - diff --git a/src/nav.js b/src/nav.js index 57d3dda..4b77d9c 100644 --- a/src/nav.js +++ b/src/nav.js @@ -17,6 +17,7 @@ import StorefrontIcon from '@mui/icons-material/Storefront'; import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import PaymentsIcon from '@mui/icons-material/Payments'; +import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import LabelIcon from '@mui/icons-material/Label'; import HistoryIcon from '@mui/icons-material/History'; @@ -226,6 +227,13 @@ export const SUB_NAV = { icon: PaymentsIcon, help: 'Zahlungseingänge erfassen und Auftragszahlungen überwachen', }, + { + segment: 'lieferarten', + label: 'Lieferarten', + title: 'Lieferarten', + icon: LocalShippingOutlinedIcon, + help: 'Versanddienstleister und Lieferarten (Carriers) verwalten', + }, ], '/kunde': [ { diff --git a/src/routes/auftrag/lieferarten.jsx b/src/routes/auftrag/lieferarten.jsx new file mode 100644 index 0000000..a47c726 --- /dev/null +++ b/src/routes/auftrag/lieferarten.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 LieferartenSearchField 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(`Lieferart „${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 AuftragLieferartenPage 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 Lieferarten angelegt.'} + + ) : ( + carriers.map((c) => ( + + )) + )} + + + + + ); + } +} + +export { CarrierItem, LieferartenSearchField }; +export default withNavigate(AuftragLieferartenPage);