diff --git a/.agents/skills/frontend-routes/SKILL.md b/.agents/skills/frontend-routes/SKILL.md index d68155a..aafd812 100644 --- a/.agents/skills/frontend-routes/SKILL.md +++ b/.agents/skills/frontend-routes/SKILL.md @@ -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` | | **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` | | **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` | | **Order List (Bestellungen)**| `/auftrag/bestellung` | Static | `/auftrag/bestellung` | | **New Order** | `/auftrag/bestellung/neu` | Static draft form | `/auftrag/bestellung/neu` | diff --git a/.vscode/tmstate.json b/.vscode/tmstate.json new file mode 100644 index 0000000..ffea1ef --- /dev/null +++ b/.vscode/tmstate.json @@ -0,0 +1 @@ +{"version":3,"groups":{},"history":{},"addons":{},"selectedGroup":null,"previousSelectedGroup":null,"quickSlots":{}} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index cd2bbf2..ca93b96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -428,7 +428,7 @@ The application routing is defined in [src/appRouter.jsx](file:///home/seb/src/d │ └── /:deliveryId → LieferungEditPage (Delivery inspection & editing) ├── /auftrag │ ├── index → AuftragAngebotPage (Quotes / Angebote) -│ ├── /neu → AuftragAngebotNeuPage (Create quote) +│ ├── /neuesangebot → AuftragAngebotNeuPage (Create quote / Neues Angebot) │ ├── /:quoteId → AuftragAngebotEditPage (Edit quote) │ ├── /bestellung → AuftragBestellungPage (Orders / Bestellungen) │ ├── /bestellung/neu → AuftragBestellungNeuPage (Create order) diff --git a/src/appRouter.jsx b/src/appRouter.jsx index cff206d..e16e574 100644 --- a/src/appRouter.jsx +++ b/src/appRouter.jsx @@ -16,7 +16,7 @@ import LieferungNeuPage from './routes/lieferung/neu'; import LieferungLieferantenPage from './routes/lieferung/lieferanten'; import LieferungEditPage from './routes/lieferung/_deliveryId'; import AuftragAngebotPage from './routes/auftrag'; -import AuftragAngebotNeuPage from './routes/auftrag/neu'; +import AuftragAngebotNeuPage from './routes/auftrag/neuesangebot'; import AuftragAngebotEditPage from './routes/auftrag/_quoteId'; import AuftragBestellungPage from './routes/auftrag/bestellung'; import AuftragBestellungNeuPage from './routes/auftrag/bestellung/neu'; @@ -97,7 +97,8 @@ const routes = [ element: , children: [ { index: true, element: }, - { path: 'neu', element: }, + { path: 'neuesangebot', element: }, + { path: 'neu', element: }, { path: 'bestellung', element: }, { path: 'bestellung/neu', element: }, { path: 'bestellung/:orderId', element: }, diff --git a/src/components/SectionLayout.jsx b/src/components/SectionLayout.jsx index 22bec28..0224fd8 100644 --- a/src/components/SectionLayout.jsx +++ b/src/components/SectionLayout.jsx @@ -82,7 +82,7 @@ export default function SectionLayout({ sectionPath }) { let pageTitle = isDetail ? 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 (pathname === '/auftrag/bestellung/neu') pageTitle = 'Neue Bestellung'; diff --git a/src/components/manifest/ManifestForm.jsx b/src/components/manifest/ManifestForm.jsx index 7ee6fd1..d95ff1d 100644 --- a/src/components/manifest/ManifestForm.jsx +++ b/src/components/manifest/ManifestForm.jsx @@ -14,6 +14,8 @@ import ManifestLines from './ManifestLines'; import ManifestPdfDialog from './ManifestPdfDialog'; import ArticleSearchDialog from './ArticleSearchDialog'; import artikelAutomat, { getArticle } from '../../automats/artikelAutomat'; +import entityAutomat from '../../automats/entityAutomat'; +import supplierAutomat from '../../automats/supplierAutomat'; import { netFromGross, getArticleGross } from '../../utils/price'; import { buildManifestBody, @@ -31,11 +33,18 @@ class ManifestForm extends PureComponent { constructor(props) { super(props); 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 = { - status: props.mode === 'edit' ? 'loading' : 'loadingParties', + status: props.mode === 'edit' ? 'loading' : (partiesReady ? 'ready' : 'loadingParties'), error: null, - parties: [], - partiesLoading: true, + parties: initialParties, + partiesLoading: !partiesReady, date: '', partyId: null, number: '', @@ -74,6 +83,18 @@ class ManifestForm extends PureComponent { componentDidMount() { 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(); else void this.loadPartiesOnly(); } @@ -89,6 +110,7 @@ class ManifestForm extends PureComponent { componentWillUnmount() { this._cancelled = true; + this.unsubParties?.(); if (this._parseAbortController) { this._parseAbortController.abort(); this._parseAbortController = null; @@ -96,7 +118,9 @@ class ManifestForm extends PureComponent { } async loadPartiesOnly() { - this.setState({ status: 'loadingParties', error: null, partiesLoading: true }); + if (this.state.parties.length === 0) { + this.setState({ status: 'loadingParties', error: null, partiesLoading: true }); + } try { const parties = await loadParties(this.props.kind); if (this._cancelled) return; @@ -107,11 +131,13 @@ class ManifestForm extends PureComponent { }); } catch (err) { if (this._cancelled) return; - this.setState({ - status: 'failed', - error: backendErrorFrom(err), - partiesLoading: false, - }); + if (this.state.parties.length === 0) { + this.setState({ + status: 'failed', + error: backendErrorFrom(err), + partiesLoading: false, + }); + } notifyError(err); } } diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx index 92fd554..ed9ca8f 100644 --- a/src/components/manifest/ManifestFormLine.jsx +++ b/src/components/manifest/ManifestFormLine.jsx @@ -226,7 +226,9 @@ class ManifestFormLine extends PureComponent { let val = line[id]; 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 ?? ''; } else if (id === 'net' || id === 'netprice') { val = line.netprice ?? line.net ?? ''; diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx index 787f90f..42221b3 100644 --- a/src/components/manifest/ManifestHeader.jsx +++ b/src/components/manifest/ManifestHeader.jsx @@ -111,7 +111,7 @@ class ManifestHeader extends PureComponent { const useExternalNumber = Boolean(cfg.externalNumber); const selected = - parties.find((row) => row.id === partyId) ?? null; + (parties || []).find((row) => row.id === partyId) ?? null; const isDateEmpty = !date || String(date).trim() === ''; const isPartyEmpty = partyId == null || partyId === ''; @@ -147,12 +147,12 @@ class ManifestHeader extends PureComponent { /> partyDisplayName(kind, option)} - isOptionEqualToValue={(option, value) => option.id === value.id} + isOptionEqualToValue={(option, value) => option?.id === value?.id} onChange={this.handlePartyChange} sx={{ flex: { xs: '1 1 100%', sm: '2 1 200px' }, diff --git a/src/components/manifest/index.js b/src/components/manifest/index.js index 6234983..098ce8e 100644 --- a/src/components/manifest/index.js +++ b/src/components/manifest/index.js @@ -18,6 +18,7 @@ export { withManifestId } from './withManifestId'; export { manifestConfig, LINE_COLUMNS, + QUOTE_LINE_COLUMNS, ORDER_LINE_COLUMNS, DELIVERY_LINE_COLUMNS, emptyLine, diff --git a/src/components/manifest/manifestConfig.js b/src/components/manifest/manifestConfig.js index 4c7fb9a..bfbdfde 100644 --- a/src/components/manifest/manifestConfig.js +++ b/src/components/manifest/manifestConfig.js @@ -9,6 +9,8 @@ import customergroupAutomat from '../../automats/customergroupAutomat'; import deliveryAutomat from '../../automats/deliveryAutomat'; import quoteAutomat from '../../automats/quoteAutomat'; import orderAutomat from '../../automats/orderAutomat'; +import { getArticle } from '../../automats/artikelAutomat'; +import { selectCompanyTaxrate } from '../../automats/companyAutomat'; import { netFromGross } from '../../utils/price'; import { GREEN_PANEL, @@ -22,7 +24,8 @@ export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']); export function entityDisplayName(entity) { 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) { @@ -81,19 +84,20 @@ export function dateInputValue(iso) { } export function emptyLine() { + const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; return { key: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - amount: '', + amount: '1', sku: '', - tax: '', - rebate: '', + name: '', + tax: normTax, + taxrate: normTax, + rebate: '0', net: '', gross: '', - name: '', - pu: '', + pu: '1', unit: '', netprice: '', - taxrate: '', }; } @@ -148,9 +152,8 @@ export function hasArticleLines(lines) { if (!l) return false; const sku = String(l.sku ?? '').trim(); const name = String(l.name ?? '').trim(); - const amount = String(l.amount ?? '').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++) { const line = lines[i]; 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 amount = parseNum(rawAmount); 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 rawPu = getFirstVal(line.pu, line.vpe); const pu = parseNum(rawPu); - const blank = - !sku && !name && amount == null && net == null && gross == null && tax == null && rebate == null; + const blank = !sku && !name && net == null && gross == null; if (blank) continue; const missing = []; @@ -499,12 +501,15 @@ export async function buildManifestBody( export const LINE_COLUMNS = [ { id: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' }, { 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: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' }, { id: 'net', label: 'Net', 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 = [ { id: 'pos', label: 'Pos', width: 44, align: 'center' }, { 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(); return sortSuppliers(list); } - const [entities, groups] = await Promise.all([ - entityAutomat.actions.ensureLoaded(), - customergroupAutomat.actions.ensureLoaded(), - ]); - return filterCustomerEntities(entities, groups); + const entities = await entityAutomat.actions.ensureLoaded(); + return entities ?? entityAutomat.state.items ?? []; } export function partyDisplayName(cfgOrKind, party) { @@ -623,7 +625,7 @@ export function manifestConfig(cfgOrKind) { docField: 'quote', partyField: 'entityId', listPath: '/auftrag', - createPath: '/auftrag/neu', + createPath: '/auftrag/neuesangebot', editPath: (id) => `/auftrag/${id}`, dateLabel: 'Gültig bis', partyLabel: 'Kunde', diff --git a/src/nav.js b/src/nav.js index b6a00c0..b16f161 100644 --- a/src/nav.js +++ b/src/nav.js @@ -312,7 +312,7 @@ export function resolveActiveSub(pathname, sectionPath) { sectionPath === '/kunde' && rest && !knownSegments.has(rest); const isLieferungDetail = 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 = sectionPath === '/auftrag' && rest && !knownSegments.has(rest); if (isArticleDetail || isKundeDetail || isLieferungDetail || isAngebotDoc) { diff --git a/src/routes/auftrag/neu.jsx b/src/routes/auftrag/neu.jsx index 75be5b9..059a173 100644 --- a/src/routes/auftrag/neu.jsx +++ b/src/routes/auftrag/neu.jsx @@ -1,10 +1,2 @@ -import { PureComponent } from 'react'; -import { ManifestForm } from '../../components/manifest'; +export { default } from './neuesangebot'; -class AuftragAngebotNeuPage extends PureComponent { - render() { - return ; - } -} - -export default AuftragAngebotNeuPage; diff --git a/src/routes/auftrag/neuesangebot.jsx b/src/routes/auftrag/neuesangebot.jsx new file mode 100644 index 0000000..75be5b9 --- /dev/null +++ b/src/routes/auftrag/neuesangebot.jsx @@ -0,0 +1,10 @@ +import { PureComponent } from 'react'; +import { ManifestForm } from '../../components/manifest'; + +class AuftragAngebotNeuPage extends PureComponent { + render() { + return ; + } +} + +export default AuftragAngebotNeuPage; diff --git a/src/routes/kunde.jsx b/src/routes/kunde.jsx index 13e7bfd..0a72966 100644 --- a/src/routes/kunde.jsx +++ b/src/routes/kunde.jsx @@ -213,7 +213,7 @@ class KundeListePage extends PureComponent { void entityAutomat.actions.load().catch((err) => notifyError(err)); } if (customergroupAutomat.state.status !== 'ready') { - void customergroupAutomat.actions.load().catch(() => {}); + void customergroupAutomat.actions.load().catch(() => { }); } }