This commit is contained in:
seb
2026-10-13 22:05:30 +02:00
parent bc583a180f
commit bf7ff349f3
14 changed files with 82 additions and 47 deletions

View File

@@ -28,7 +28,7 @@ Reference guide for the backend LLM agent when emitting route navigation actions
| **Delivery Detail** | `/lieferung/:deliveryId` | Delivery document ID: `/lieferung/${deliveryId}` | `/lieferung/42` | | **Delivery Detail** | `/lieferung/:deliveryId` | Delivery document ID: `/lieferung/${deliveryId}` | `/lieferung/42` |
| **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` | | **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` |
| **Quote List (Angebote)** | `/auftrag` | Static | `/auftrag` | | **Quote List (Angebote)** | `/auftrag` | Static | `/auftrag` |
| **New Quote** | `/auftrag/neu` | Static draft form | `/auftrag/neu` | | **New Quote** | `/auftrag/neuesangebot` | Static draft form (Neues Angebot) | `/auftrag/neuesangebot` |
| **Quote Detail** | `/auftrag/:quoteId` | Quote document ID: `/auftrag/${quoteId}` | `/auftrag/1001` | | **Quote Detail** | `/auftrag/:quoteId` | Quote document ID: `/auftrag/${quoteId}` | `/auftrag/1001` |
| **Order List (Bestellungen)**| `/auftrag/bestellung` | Static | `/auftrag/bestellung` | | **Order List (Bestellungen)**| `/auftrag/bestellung` | Static | `/auftrag/bestellung` |
| **New Order** | `/auftrag/bestellung/neu` | Static draft form | `/auftrag/bestellung/neu` | | **New Order** | `/auftrag/bestellung/neu` | Static draft form | `/auftrag/bestellung/neu` |

1
.vscode/tmstate.json vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"groups":{},"history":{},"addons":{},"selectedGroup":null,"previousSelectedGroup":null,"quickSlots":{}}

View File

@@ -428,7 +428,7 @@ The application routing is defined in [src/appRouter.jsx](file:///home/seb/src/d
│ └── /:deliveryId → LieferungEditPage (Delivery inspection & editing) │ └── /:deliveryId → LieferungEditPage (Delivery inspection & editing)
├── /auftrag ├── /auftrag
│ ├── index → AuftragAngebotPage (Quotes / Angebote) │ ├── index → AuftragAngebotPage (Quotes / Angebote)
│ ├── /neu → AuftragAngebotNeuPage (Create quote) │ ├── /neuesangebot → AuftragAngebotNeuPage (Create quote / Neues Angebot)
│ ├── /:quoteId → AuftragAngebotEditPage (Edit quote) │ ├── /:quoteId → AuftragAngebotEditPage (Edit quote)
│ ├── /bestellung → AuftragBestellungPage (Orders / Bestellungen) │ ├── /bestellung → AuftragBestellungPage (Orders / Bestellungen)
│ ├── /bestellung/neu → AuftragBestellungNeuPage (Create order) │ ├── /bestellung/neu → AuftragBestellungNeuPage (Create order)

View File

@@ -16,7 +16,7 @@ import LieferungNeuPage from './routes/lieferung/neu';
import LieferungLieferantenPage from './routes/lieferung/lieferanten'; import LieferungLieferantenPage from './routes/lieferung/lieferanten';
import LieferungEditPage from './routes/lieferung/_deliveryId'; import LieferungEditPage from './routes/lieferung/_deliveryId';
import AuftragAngebotPage from './routes/auftrag'; import AuftragAngebotPage from './routes/auftrag';
import AuftragAngebotNeuPage from './routes/auftrag/neu'; import AuftragAngebotNeuPage from './routes/auftrag/neuesangebot';
import AuftragAngebotEditPage from './routes/auftrag/_quoteId'; import AuftragAngebotEditPage from './routes/auftrag/_quoteId';
import AuftragBestellungPage from './routes/auftrag/bestellung'; import AuftragBestellungPage from './routes/auftrag/bestellung';
import AuftragBestellungNeuPage from './routes/auftrag/bestellung/neu'; import AuftragBestellungNeuPage from './routes/auftrag/bestellung/neu';
@@ -97,7 +97,8 @@ const routes = [
element: <SectionLayout sectionPath="/auftrag" />, element: <SectionLayout sectionPath="/auftrag" />,
children: [ children: [
{ index: true, element: <AuftragAngebotPage /> }, { index: true, element: <AuftragAngebotPage /> },
{ path: 'neu', element: <AuftragAngebotNeuPage /> }, { path: 'neuesangebot', element: <AuftragAngebotNeuPage /> },
{ path: 'neu', element: <Navigate to="/auftrag/neuesangebot" replace /> },
{ path: 'bestellung', element: <AuftragBestellungPage /> }, { path: 'bestellung', element: <AuftragBestellungPage /> },
{ path: 'bestellung/neu', element: <AuftragBestellungNeuPage /> }, { path: 'bestellung/neu', element: <AuftragBestellungNeuPage /> },
{ path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> }, { path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> },

View File

@@ -82,7 +82,7 @@ export default function SectionLayout({ sectionPath }) {
let pageTitle = isDetail let pageTitle = isDetail
? title ? title
: (activeSub?.title ?? activeSub?.label ?? title); : (activeSub?.title ?? activeSub?.label ?? title);
if (pathname === '/auftrag/neu') pageTitle = 'Neues Angebot'; if (pathname === '/auftrag/neuesangebot' || pathname === '/auftrag/neu') pageTitle = 'Neues Angebot';
else if (isAngebotDoc) pageTitle = angebotLabel; else if (isAngebotDoc) pageTitle = angebotLabel;
else if (pathname === '/auftrag/bestellung/neu') pageTitle = 'Neue Bestellung'; else if (pathname === '/auftrag/bestellung/neu') pageTitle = 'Neue Bestellung';

View File

@@ -14,6 +14,8 @@ import ManifestLines from './ManifestLines';
import ManifestPdfDialog from './ManifestPdfDialog'; import ManifestPdfDialog from './ManifestPdfDialog';
import ArticleSearchDialog from './ArticleSearchDialog'; import ArticleSearchDialog from './ArticleSearchDialog';
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat'; import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import entityAutomat from '../../automats/entityAutomat';
import supplierAutomat from '../../automats/supplierAutomat';
import { netFromGross, getArticleGross } from '../../utils/price'; import { netFromGross, getArticleGross } from '../../utils/price';
import { import {
buildManifestBody, buildManifestBody,
@@ -31,11 +33,18 @@ class ManifestForm extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.cfg = manifestConfig(props.kind); this.cfg = manifestConfig(props.kind);
const initialParties = props.kind === 'delivery'
? (supplierAutomat.state.status === 'ready' ? (supplierAutomat.state.suppliers ?? []) : [])
: (entityAutomat.state.status === 'ready' ? (entityAutomat.state.items ?? []) : []);
const partiesReady = props.kind === 'delivery'
? supplierAutomat.state.status === 'ready'
: entityAutomat.state.status === 'ready';
this.state = { this.state = {
status: props.mode === 'edit' ? 'loading' : 'loadingParties', status: props.mode === 'edit' ? 'loading' : (partiesReady ? 'ready' : 'loadingParties'),
error: null, error: null,
parties: [], parties: initialParties,
partiesLoading: true, partiesLoading: !partiesReady,
date: '', date: '',
partyId: null, partyId: null,
number: '', number: '',
@@ -74,6 +83,18 @@ class ManifestForm extends PureComponent {
componentDidMount() { componentDidMount() {
this._cancelled = false; this._cancelled = false;
if (this.props.kind === 'delivery') {
this.unsubParties = supplierAutomat.subscribe(this, (state) => ({
parties: state.status === 'ready' ? (state.suppliers ?? []) : [],
partiesLoading: state.status === 'loading',
}));
} else {
this.unsubParties = entityAutomat.subscribe(this, (state) => ({
parties: state.items ?? [],
partiesLoading: state.status === 'loading',
}));
}
if (this.props.mode === 'edit') void this.loadEdit(); if (this.props.mode === 'edit') void this.loadEdit();
else void this.loadPartiesOnly(); else void this.loadPartiesOnly();
} }
@@ -89,6 +110,7 @@ class ManifestForm extends PureComponent {
componentWillUnmount() { componentWillUnmount() {
this._cancelled = true; this._cancelled = true;
this.unsubParties?.();
if (this._parseAbortController) { if (this._parseAbortController) {
this._parseAbortController.abort(); this._parseAbortController.abort();
this._parseAbortController = null; this._parseAbortController = null;
@@ -96,7 +118,9 @@ class ManifestForm extends PureComponent {
} }
async loadPartiesOnly() { async loadPartiesOnly() {
if (this.state.parties.length === 0) {
this.setState({ status: 'loadingParties', error: null, partiesLoading: true }); this.setState({ status: 'loadingParties', error: null, partiesLoading: true });
}
try { try {
const parties = await loadParties(this.props.kind); const parties = await loadParties(this.props.kind);
if (this._cancelled) return; if (this._cancelled) return;
@@ -107,11 +131,13 @@ class ManifestForm extends PureComponent {
}); });
} catch (err) { } catch (err) {
if (this._cancelled) return; if (this._cancelled) return;
if (this.state.parties.length === 0) {
this.setState({ this.setState({
status: 'failed', status: 'failed',
error: backendErrorFrom(err), error: backendErrorFrom(err),
partiesLoading: false, partiesLoading: false,
}); });
}
notifyError(err); notifyError(err);
} }
} }

View File

@@ -226,7 +226,9 @@ class ManifestFormLine extends PureComponent {
let val = line[id]; let val = line[id];
if (val == null || val === '') { if (val == null || val === '') {
if (id === 'tax' || id === 'taxrate') { if (id === 'name' && this.state.cachedArticle?.name) {
val = this.state.cachedArticle.name;
} else if (id === 'tax' || id === 'taxrate') {
val = line.taxrate ?? line.tax ?? ''; val = line.taxrate ?? line.tax ?? '';
} else if (id === 'net' || id === 'netprice') { } else if (id === 'net' || id === 'netprice') {
val = line.netprice ?? line.net ?? ''; val = line.netprice ?? line.net ?? '';

View File

@@ -111,7 +111,7 @@ class ManifestHeader extends PureComponent {
const useExternalNumber = Boolean(cfg.externalNumber); const useExternalNumber = Boolean(cfg.externalNumber);
const selected = const selected =
parties.find((row) => row.id === partyId) ?? null; (parties || []).find((row) => row.id === partyId) ?? null;
const isDateEmpty = !date || String(date).trim() === ''; const isDateEmpty = !date || String(date).trim() === '';
const isPartyEmpty = partyId == null || partyId === ''; const isPartyEmpty = partyId == null || partyId === '';
@@ -147,12 +147,12 @@ class ManifestHeader extends PureComponent {
/> />
<Autocomplete <Autocomplete
size="small" size="small"
options={parties} options={parties || []}
value={selected} value={selected}
loading={Boolean(partiesLoading)} loading={Boolean(partiesLoading)}
disabled={saving || pdfUploading} disabled={saving || pdfUploading}
getOptionLabel={(option) => partyDisplayName(kind, option)} getOptionLabel={(option) => partyDisplayName(kind, option)}
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) => option?.id === value?.id}
onChange={this.handlePartyChange} onChange={this.handlePartyChange}
sx={{ sx={{
flex: { xs: '1 1 100%', sm: '2 1 200px' }, flex: { xs: '1 1 100%', sm: '2 1 200px' },

View File

@@ -18,6 +18,7 @@ export { withManifestId } from './withManifestId';
export { export {
manifestConfig, manifestConfig,
LINE_COLUMNS, LINE_COLUMNS,
QUOTE_LINE_COLUMNS,
ORDER_LINE_COLUMNS, ORDER_LINE_COLUMNS,
DELIVERY_LINE_COLUMNS, DELIVERY_LINE_COLUMNS,
emptyLine, emptyLine,

View File

@@ -9,6 +9,8 @@ import customergroupAutomat from '../../automats/customergroupAutomat';
import deliveryAutomat from '../../automats/deliveryAutomat'; import deliveryAutomat from '../../automats/deliveryAutomat';
import quoteAutomat from '../../automats/quoteAutomat'; import quoteAutomat from '../../automats/quoteAutomat';
import orderAutomat from '../../automats/orderAutomat'; import orderAutomat from '../../automats/orderAutomat';
import { getArticle } from '../../automats/artikelAutomat';
import { selectCompanyTaxrate } from '../../automats/companyAutomat';
import { netFromGross } from '../../utils/price'; import { netFromGross } from '../../utils/price';
import { import {
GREEN_PANEL, GREEN_PANEL,
@@ -22,7 +24,8 @@ export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']);
export function entityDisplayName(entity) { export function entityDisplayName(entity) {
if (!entity) return ''; if (!entity) return '';
return [entity.firstName, entity.name].filter(Boolean).join(' '); const fullName = [entity.firstName, entity.name].filter(Boolean).join(' ');
return fullName || entity.name || entity.firstName || (entity.number ? `Kunde ${entity.number}` : (entity.id ? `Kunde #${entity.id}` : ''));
} }
export function sortEntities(entities) { export function sortEntities(entities) {
@@ -81,19 +84,20 @@ export function dateInputValue(iso) {
} }
export function emptyLine() { export function emptyLine() {
const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19';
return { return {
key: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, key: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
amount: '', amount: '1',
sku: '', sku: '',
tax: '', name: '',
rebate: '', tax: normTax,
taxrate: normTax,
rebate: '0',
net: '', net: '',
gross: '', gross: '',
name: '', pu: '1',
pu: '',
unit: '', unit: '',
netprice: '', netprice: '',
taxrate: '',
}; };
} }
@@ -148,9 +152,8 @@ export function hasArticleLines(lines) {
if (!l) return false; if (!l) return false;
const sku = String(l.sku ?? '').trim(); const sku = String(l.sku ?? '').trim();
const name = String(l.name ?? '').trim(); const name = String(l.name ?? '').trim();
const amount = String(l.amount ?? '').trim();
const net = String(l.net ?? l.netprice ?? l.gross ?? '').trim(); const net = String(l.net ?? l.netprice ?? l.gross ?? '').trim();
return Boolean(sku || name || amount || net); return Boolean(sku || name || net);
}); });
} }
@@ -162,7 +165,7 @@ export function buildLines(lines, { requireAtLeastOne }) {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
const sku = String(line.sku ?? '').trim(); const sku = String(line.sku ?? '').trim();
const name = String(line.name ?? '').trim(); const name = String(line.name ?? (sku ? getArticle(sku)?.name : '') ?? '').trim();
const rawAmount = getFirstVal(line.amount, line.count); const rawAmount = getFirstVal(line.amount, line.count);
const amount = parseNum(rawAmount); const amount = parseNum(rawAmount);
const rawNet = getFirstVal(line.net, line.netprice, line.netPrice, line.eknetto); const rawNet = getFirstVal(line.net, line.netprice, line.netPrice, line.eknetto);
@@ -177,8 +180,7 @@ export function buildLines(lines, { requireAtLeastOne }) {
const rebate = parseNum(getFirstVal(line.rebate)); const rebate = parseNum(getFirstVal(line.rebate));
const rawPu = getFirstVal(line.pu, line.vpe); const rawPu = getFirstVal(line.pu, line.vpe);
const pu = parseNum(rawPu); const pu = parseNum(rawPu);
const blank = const blank = !sku && !name && net == null && gross == null;
!sku && !name && amount == null && net == null && gross == null && tax == null && rebate == null;
if (blank) continue; if (blank) continue;
const missing = []; const missing = [];
@@ -499,12 +501,15 @@ export async function buildManifestBody(
export const LINE_COLUMNS = [ export const LINE_COLUMNS = [
{ id: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' }, { id: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' },
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' }, { id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' },
{ id: 'name', label: 'Name', width: 200, align: 'left', type: 'text' },
{ id: 'tax', label: 'Tax', width: 70, align: 'right', type: 'number' }, { id: 'tax', label: 'Tax', width: 70, align: 'right', type: 'number' },
{ id: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' }, { id: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' },
{ id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' }, { id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' },
{ id: 'gross', label: 'Gross', width: 95, align: 'right', type: 'number' }, { id: 'gross', label: 'Gross', width: 95, align: 'right', type: 'number' },
]; ];
export const QUOTE_LINE_COLUMNS = LINE_COLUMNS;
export const ORDER_LINE_COLUMNS = [ export const ORDER_LINE_COLUMNS = [
{ id: 'pos', label: 'Pos', width: 44, align: 'center' }, { id: 'pos', label: 'Pos', width: 44, align: 'center' },
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' }, { id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' },
@@ -557,11 +562,8 @@ export async function loadParties(cfgOrKind) {
const list = await supplierAutomat.actions.load(); const list = await supplierAutomat.actions.load();
return sortSuppliers(list); return sortSuppliers(list);
} }
const [entities, groups] = await Promise.all([ const entities = await entityAutomat.actions.ensureLoaded();
entityAutomat.actions.ensureLoaded(), return entities ?? entityAutomat.state.items ?? [];
customergroupAutomat.actions.ensureLoaded(),
]);
return filterCustomerEntities(entities, groups);
} }
export function partyDisplayName(cfgOrKind, party) { export function partyDisplayName(cfgOrKind, party) {
@@ -623,7 +625,7 @@ export function manifestConfig(cfgOrKind) {
docField: 'quote', docField: 'quote',
partyField: 'entityId', partyField: 'entityId',
listPath: '/auftrag', listPath: '/auftrag',
createPath: '/auftrag/neu', createPath: '/auftrag/neuesangebot',
editPath: (id) => `/auftrag/${id}`, editPath: (id) => `/auftrag/${id}`,
dateLabel: 'Gültig bis', dateLabel: 'Gültig bis',
partyLabel: 'Kunde', partyLabel: 'Kunde',

View File

@@ -312,7 +312,7 @@ export function resolveActiveSub(pathname, sectionPath) {
sectionPath === '/kunde' && rest && !knownSegments.has(rest); sectionPath === '/kunde' && rest && !knownSegments.has(rest);
const isLieferungDetail = const isLieferungDetail =
sectionPath === '/lieferung' && rest && !knownSegments.has(rest); sectionPath === '/lieferung' && rest && !knownSegments.has(rest);
// Angebot neu/edit: /auftrag/neu or /auftrag/:quoteId (not a known sub) // Angebot neu/edit: /auftrag/neuesangebot, /auftrag/neu or /auftrag/:quoteId (not a known sub)
const isAngebotDoc = const isAngebotDoc =
sectionPath === '/auftrag' && rest && !knownSegments.has(rest); sectionPath === '/auftrag' && rest && !knownSegments.has(rest);
if (isArticleDetail || isKundeDetail || isLieferungDetail || isAngebotDoc) { if (isArticleDetail || isKundeDetail || isLieferungDetail || isAngebotDoc) {

View File

@@ -1,10 +1,2 @@
import { PureComponent } from 'react'; export { default } from './neuesangebot';
import { ManifestForm } from '../../components/manifest';
class AuftragAngebotNeuPage extends PureComponent {
render() {
return <ManifestForm kind="quote" mode="create" />;
}
}
export default AuftragAngebotNeuPage;

View File

@@ -0,0 +1,10 @@
import { PureComponent } from 'react';
import { ManifestForm } from '../../components/manifest';
class AuftragAngebotNeuPage extends PureComponent {
render() {
return <ManifestForm kind="quote" mode="create" />;
}
}
export default AuftragAngebotNeuPage;