diff --git a/src/automats/deliveryEditorAutomat.js b/src/automats/deliveryEditorAutomat.js index ec6aae2..12fb723 100644 --- a/src/automats/deliveryEditorAutomat.js +++ b/src/automats/deliveryEditorAutomat.js @@ -62,6 +62,14 @@ import { article as articleApi, supplier as supplierApi } from '../api'; * @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity. */ +function parseNum(value) { + if (value == null || value === '') return null; + const cleaned = String(value).trim().replace('%', '').trim().replace(',', '.'); + if (cleaned === '') return null; + const n = Number(cleaned); + return Number.isFinite(n) ? n : null; +} + /** * Normalizes a partial line into an editor line with a stable key. * @@ -69,14 +77,22 @@ import { article as articleApi, supplier as supplierApi } from '../api'; * @returns {DeliveryLine} Normalized line. */ export function createEmptyLineitem(line = {}) { - const netVal = line.netprice ?? line.net ?? null; - const taxVal = line.taxrate ?? line.tax ?? null; + const netRaw = line.netprice ?? line.net ?? line.eknetto ?? line.netPrice ?? null; + const taxRaw = line.taxrate ?? line.tax ?? line.mwst ?? line.taxRate ?? null; + const amountRaw = line.amount ?? line.count ?? line.qty ?? line.quantity ?? null; + const puRaw = line.pu ?? line.vpe ?? null; + + const netVal = netRaw != null ? (parseNum(netRaw) ?? netRaw) : null; + const taxVal = taxRaw != null ? (parseNum(taxRaw) ?? taxRaw) : null; + const amountVal = amountRaw != null ? (parseNum(amountRaw) ?? amountRaw) : null; + const puVal = puRaw != null ? (parseNum(puRaw) ?? puRaw) : null; + return { key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - name: line.name ?? null, - sku: line.sku ?? null, - amount: line.amount ?? null, - pu: line.pu ?? null, + name: line.name ?? line.articleName ?? line.title ?? null, + sku: line.sku ?? line.liefArtNr ?? line.articleNumber ?? null, + amount: amountVal, + pu: puVal, unit: line.unit ?? null, netprice: netVal, net: netVal, @@ -657,6 +673,82 @@ export function createDeliveryEditorAutomat(options = {}) { }); }, + /** + * Loads a delivery manifest (e.g. from draft_delivery_manifest tool) into the editor. + * @param {Object} manifestData + * @returns {void} + */ + loadDraftManifest(manifestData) { + if (!manifestData || typeof manifestData !== 'object') return; + const data = manifestData.manifest ?? manifestData; + const basedata = data.basedata ?? data.baseData ?? data; + const rawItems = + data.items?.items ?? + data.items ?? + data.lines ?? + data.lineitems ?? + []; + const itemsList = Array.isArray(rawItems) ? rawItems : []; + + const partyId = + basedata.lieferant_id ?? + basedata.lieferantId ?? + basedata.partyId ?? + basedata.deliveryEntity ?? + basedata.supplier_id ?? + basedata.supplierId ?? + null; + const supplierName = + basedata.lieferantName ?? + basedata.supplierName ?? + basedata.unmatchedSupplierName ?? + null; + const extNum = + basedata.manifestNr ?? + basedata.manifestNumber ?? + basedata.externalNumber ?? + basedata.external_number ?? + basedata.invoiceNumber ?? + basedata.invoice_number ?? + null; + const date = basedata.date ?? null; + const type = basedata.manifestType ?? basedata.type ?? 'rechnung'; + + let matchedPartyId = partyId != null && partyId !== '' ? partyId : null; + if (!matchedPartyId && supplierName && automat.state.deliveryEntities?.length > 0) { + const normName = String(supplierName).trim().toLowerCase(); + const found = automat.state.deliveryEntities.find( + (e) => + (e.name && String(e.name).trim().toLowerCase() === normName) || + (e.company && String(e.company).trim().toLowerCase() === normName) + ); + if (found?.id != null) { + matchedPartyId = found.id; + } + } + + const lines = itemsList.map((item, idx) => + createEmptyLineitem({ + ...item, + key: item.key ?? `manifest-${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 6)}`, + }) + ); + + automat.actions.setDelivery({ + id: null, + deliveryId: null, + date, + partyId: matchedPartyId, + deliveryEntity: matchedPartyId, + unmatchedSupplierName: matchedPartyId != null ? null : supplierName, + externalNumber: extNum, + external_number: extNum, + type, + lines, + lineitems: lines, + }); + }, + /** Serializes the delivery into a VFS draft manifest format. @returns {object} */ getManifest() { const state = automat.state; @@ -724,6 +816,7 @@ export const { setId, setDeliveryEntities, setDelivery, + loadDraftManifest, setDate, setDeliveryEntity, setPartyId, diff --git a/src/components/chat/ChatFlowList.jsx b/src/components/chat/ChatFlowList.jsx index 9d18c12..a4d4a3e 100644 --- a/src/components/chat/ChatFlowList.jsx +++ b/src/components/chat/ChatFlowList.jsx @@ -26,6 +26,10 @@ import LensBlurIcon from '@mui/icons-material/LensBlur'; import CalculateIcon from '@mui/icons-material/Calculate'; import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner'; import DirectionsIcon from '@mui/icons-material/Directions'; +import EditNoteIcon from '@mui/icons-material/EditNote'; +import Button from '@mui/material/Button'; +import { enqueueSnackbar } from 'notistack'; +import { deliveryEditorAutomat, setLastDeliveryPath } from '../../automats'; import HealingMarkdown from './HealingMarkdown'; import { VfsAttachmentLink, @@ -95,6 +99,8 @@ const TOOL_ICON_MAP = { ean_checksum: QrCodeScannerIcon, 'set-user-route': DirectionsIcon, set_user_route: DirectionsIcon, + draft_delivery_manifest: EditNoteIcon, + 'draft-delivery-manifest': EditNoteIcon, }; function formatMessageTime(isoString) { @@ -486,16 +492,145 @@ class UrlFaviconIcon extends PureComponent { } } +export function isDraftDeliveryManifestTool(name) { + if (!name || typeof name !== 'string') return false; + const n = name.trim().toLowerCase(); + return n === 'draft_delivery_manifest' || n === 'draft-delivery-manifest'; +} + +function tryExtractObject(val) { + if (!val) return null; + if (typeof val === 'object') return val; + if (typeof val === 'string') { + const trimmed = val.trim(); + try { + return JSON.parse(trimmed); + } catch {} + const match = trimmed.match(/\{[\s\S]*\}/); + if (match) { + try { + return JSON.parse(match[0]); + } catch {} + } + } + return null; +} + +export function parseDraftDeliveryManifest(tool) { + if (!tool) return null; + const candidates = [tool.parameters, tool.arguments, tool.content, tool.result, tool]; + for (const c of candidates) { + const obj = tryExtractObject(c); + if (obj && typeof obj === 'object') { + if (obj.manifest && typeof obj.manifest === 'object') { + return obj.manifest; + } + if (obj.basedata || obj.baseData || obj.items || obj.lines || obj.lineitems) { + return obj; + } + } + } + return null; +} + +class DraftDeliveryManifestUseButton extends PureComponent { + state = { + applied: false, + }; + + handleClick = async (e) => { + e?.stopPropagation(); + const { tool } = this.props; + const manifest = parseDraftDeliveryManifest(tool); + if (!manifest) { + enqueueSnackbar('Keine gültigen Lieferscheindaten gefunden', { variant: 'warning' }); + return; + } + + deliveryEditorAutomat.actions.loadDraftManifest(manifest); + setLastDeliveryPath('/lieferung/neu'); + + this.setState({ applied: true }); + setTimeout(() => { + this.setState({ applied: false }); + }, 2500); + + const count = + manifest.items?.numberOfItemsToAdd ?? + (Array.isArray(manifest.items?.items) + ? manifest.items.items.length + : Array.isArray(manifest.items) + ? manifest.items.length + : Array.isArray(manifest.lines) + ? manifest.lines.length + : 0); + + enqueueSnackbar( + count > 0 + ? `${count} Positionen in den Lieferschein-Editor geladen` + : 'Lieferschein in den Editor geladen', + { variant: 'success' } + ); + + try { + const { router } = await import('../../appRouter'); + if (router?.navigate) { + const curPath = + router.state?.location?.pathname || + (typeof window !== 'undefined' ? window.location.pathname : ''); + if (!curPath.startsWith('/lieferung/neu')) { + void router.navigate('/lieferung/neu'); + } + } + } catch (err) { + console.warn('[DraftDeliveryManifestUseButton] Navigation failed:', err); + } + }; + + render() { + const { applied } = this.state; + return ( + + ); + } +} + function WorkIconView({ tool }) { if (!tool || !tool.name) return null; const isGreen = tool?.state === 'green'; const isUrlGet = tool?.name === 'url_get'; + const isDraftDelivery = isDraftDeliveryManifestTool(tool?.name); const ToolNameIcon = tool?.name ? TOOL_ICON_MAP[tool.name] : null; const prefix = ToolNameIcon ? tool?.name : null; const tooltipText = formatToolTooltip(tool?.content, tool?.result, tool?.parameters || tool?.arguments, prefix); const targetUrl = isUrlGet ? extractUrlFromTool(tool) : null; - return ( + const iconElement = ( ); + + if (isDraftDelivery) { + return ( + + {iconElement} + + + ); + } + + return iconElement; } function UsageChipView({ usage }) {