Bestellnummer:
'; - $html[] = 'Bestelldatum:
'; - $html[] = 'Lieferadresse:
'; - $html[] = ''; - $html[] = ''; - $html[] = ''; - $html[] = 'Gesamtsumme:
'; - $html[] = 'Detailinhalt wird noch definiert.
'; - $html[] = 'diff --git a/modules/erp/bestellungen/service.php b/modules/erp/bestellungen/service.php index 3c5a632..51463a9 100644 --- a/modules/erp/bestellungen/service.php +++ b/modules/erp/bestellungen/service.php @@ -374,6 +374,150 @@ function get_sales_order_overview(PDO $pdo, array $filters = []): array ]; } +function get_sales_order_detail(PDO $pdo, int $orderId): ?array +{ + $orderStmt = $pdo->prepare( + "SELECT + so.id, + so.external_ref, + so.order_date, + so.order_source, + so.order_status, + so.payment_status, + so.amount_net, + so.amount_shipping, + so.amount_tax, + so.amount_discount, + so.total_amount, + so.currency, + COALESCE(ad.first_name, '') AS first_name, + COALESCE(ad.last_name, '') AS last_name, + COALESCE(ad.street, '') AS street, + COALESCE(ad.house_number, '') AS house_number, + COALESCE(ad.zip, '') AS zip, + COALESCE(ad.city, '') AS city, + COALESCE(ad.country_name, '') AS country_name, + COALESCE(pm.label, '') AS payment_method, + COALESCE(sm.label, '') AS shipping_method + FROM sales_order so + LEFT JOIN LATERAL ( + SELECT a.first_name, a.last_name, a.street, a.house_number, a.zip, a.city, a.country_name + FROM address a + WHERE a.party_id = so.party_id AND a.type IN ('shipping', 'billing') + ORDER BY CASE WHEN a.type = 'shipping' THEN 0 ELSE 1 END, a.id DESC + LIMIT 1 + ) ad ON TRUE + LEFT JOIN payment_method pm ON pm.id = so.payment_method_id + LEFT JOIN shipping_method sm ON sm.id = so.shipping_method_id + WHERE so.id = :order_id" + ); + $orderStmt->execute([':order_id' => $orderId]); + $order = $orderStmt->fetch(); + if (!is_array($order)) { + return null; + } + + $lineStmt = $pdo->prepare( + "SELECT + sol.id, + sol.line_no, + sol.sellable_item_id, + COALESCE(si.item_code, sol.raw_external_article_number, '') AS item_code, + COALESCE(si.display_name, sol.raw_external_title, '') AS item_name, + COALESCE(sol.raw_external_article_number, '') AS raw_article_number, + COALESCE(sol.raw_external_title, '') AS raw_title, + sol.sku, + sol.qty, + sol.qty_cancelled, + sol.line_status, + sol.unit_price, + sol.line_total, + sic.product_id, + p.name AS product_name, + sic.qty_per_item, + a.id AS allocation_id, + a.qty AS allocated_qty, + a.allocation_status, + sl.lot_number, + sl.status AS lot_status + FROM sales_order_line sol + LEFT JOIN sellable_item si ON si.id = sol.sellable_item_id + LEFT JOIN sellable_item_component sic ON sic.sellable_item_id = sol.sellable_item_id + LEFT JOIN product p ON p.id = sic.product_id + LEFT JOIN sales_order_line_lot_allocation a + ON a.sales_order_line_id = sol.id AND a.product_id = sic.product_id + LEFT JOIN stock_lot sl ON sl.id = a.lot_id + WHERE sol.sales_order_id = :order_id + ORDER BY sol.line_no, p.name NULLS LAST, a.id" + ); + $lineStmt->execute([':order_id' => $orderId]); + + $lines = []; + foreach ($lineStmt->fetchAll() as $row) { + $lineId = (int) $row['id']; + if (!isset($lines[$lineId])) { + $lines[$lineId] = [ + 'id' => $lineId, + 'line_no' => (int) $row['line_no'], + 'item_code' => (string) $row['item_code'], + 'item_name' => (string) $row['item_name'], + 'raw_article_number' => (string) $row['raw_article_number'], + 'raw_title' => (string) $row['raw_title'], + 'sku' => (string) ($row['sku'] ?? ''), + 'qty' => (float) $row['qty'], + 'qty_cancelled' => (float) $row['qty_cancelled'], + 'line_status' => (string) $row['line_status'], + 'unit_price' => $row['unit_price'] !== null ? (float) $row['unit_price'] : null, + 'line_total' => $row['line_total'] !== null ? (float) $row['line_total'] : null, + 'product_allocations' => [], + ]; + } + + if ($row['product_id'] !== null) { + $lines[$lineId]['product_allocations'][] = [ + 'product_id' => (int) $row['product_id'], + 'product_name' => (string) $row['product_name'], + 'requested_qty' => (float) $row['qty'] * (float) $row['qty_per_item'], + 'allocated_qty' => $row['allocated_qty'] !== null ? (float) $row['allocated_qty'] : null, + 'allocation_status' => $row['allocation_status'] !== null ? (string) $row['allocation_status'] : null, + 'lot_number' => $row['lot_number'] !== null ? (string) $row['lot_number'] : null, + 'lot_status' => $row['lot_status'] !== null ? (string) $row['lot_status'] : null, + ]; + } + } + + $detail = [ + 'id' => (int) $order['id'], + 'external_ref' => (string) $order['external_ref'], + 'order_date' => (string) $order['order_date'], + 'order_source' => (string) $order['order_source'], + 'order_status' => (string) $order['order_status'], + 'payment_status' => (string) $order['payment_status'], + 'payment_method' => (string) $order['payment_method'], + 'shipping_method' => (string) $order['shipping_method'], + 'customer' => [ + 'first_name' => (string) $order['first_name'], + 'last_name' => (string) $order['last_name'], + 'street' => (string) $order['street'], + 'house_number' => (string) $order['house_number'], + 'zip' => (string) $order['zip'], + 'city' => (string) $order['city'], + 'country_name' => (string) $order['country_name'], + ], + 'amounts' => [ + 'net' => $order['amount_net'] !== null ? (float) $order['amount_net'] : null, + 'shipping' => $order['amount_shipping'] !== null ? (float) $order['amount_shipping'] : null, + 'tax' => $order['amount_tax'] !== null ? (float) $order['amount_tax'] : null, + 'discount' => $order['amount_discount'] !== null ? (float) $order['amount_discount'] : null, + 'total' => $order['total_amount'] !== null ? (float) $order['total_amount'] : null, + 'currency' => (string) $order['currency'], + ], + 'lines' => array_values($lines), + ]; + + return $detail; +} + function get_sales_order_realtime_snapshot(PDO $pdo, int $limit = 1000): array { $limit = max(1, min(2000, $limit)); diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index 9264a20..6286ae1 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -172,21 +172,7 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string $html[] = ''; $html[] = ''; $html[] = '
Bestellnummer:
'; - $html[] = 'Bestelldatum:
'; - $html[] = 'Lieferadresse:
'; - $html[] = ''; - $html[] = ''; - $html[] = ''; - $html[] = 'Gesamtsumme:
'; - $html[] = 'Detailinhalt wird noch definiert.
'; - $html[] = 'Bestelldetails werden geladen …
Bestellnummer: ' + escapeHtml(orderNumber) + '
',"; echo " 'Bestelldatum: ' + escapeHtml(orderDate) + '
',"; @@ -803,7 +790,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " 'Gesamtsumme: ' + escapeHtml(total) + '
',"; echo " 'Detailinhalt wird noch definiert.
',"; echo " '' + escapeHtml(label) + ': ' + escapeHtml(content || 'Nicht vorhanden') + '
';"; + echo " const renderCards = (order) => {"; + echo " const c = order.customer || {}; const a = order.amounts || {}; const address = [[c.street, c.house_number].filter(Boolean).join(' '), [c.zip, c.city].filter(Boolean).join(' '), c.country_name].filter(Boolean).join('' + escapeHtml(line.item_name) + ' · ' + escapeHtml(line.item_code) + ' · Menge: ' + escapeHtml(String(line.qty)) + '
' + value('Positionsstatus', line.line_status) + value('Einzelpreis', formatAmount(line.unit_price, a.currency || 'CHF')) + value('Positionssumme', formatAmount(line.line_total, a.currency || 'CHF')) + 'Kunde: ' + escapeHtml([c.first_name, c.last_name].filter(Boolean).join(' ') || 'Nicht vorhanden') + '
Lieferadresse:
' + address + '
Keine Artikel vorhanden.
Keine Chargenzuordnung vorhanden.
') + 'Bestelldetails werden geladen …
'; }"; + echo " fetch('/api/bestellungen/detail.php?id=' + encodeURIComponent(orderId), { credentials: 'same-origin' }).then((response) => response.ok ? response.json() : Promise.reject(new Error('Bestelldetails konnten nicht geladen werden.'))).then((payload) => { if (!payload.ok) { throw new Error('Bestelldetails konnten nicht geladen werden.'); } if (cards) { cards.innerHTML = renderCards(payload.order); } }).catch((error) => { if (cards) { cards.innerHTML = '' + escapeHtml(error.message) + '
'; } });"; echo " contentRoot.querySelectorAll('[data-order-drawer-open]').forEach((button) => {"; echo " button.setAttribute('aria-expanded', String(button === trigger));"; echo " });"; @@ -879,6 +857,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " document.body.appendChild(bestellungenMobileDetailHost);"; echo " document.body.classList.add('sg-bestellungen-mobile-detail-open');"; echo " }"; + echo " fetch('/api/bestellungen/detail.php?id=' + encodeURIComponent(trigger.dataset.orderId || ''), { credentials: 'same-origin' }).then((response) => response.ok ? response.json() : Promise.reject(new Error('Bestelldetails konnten nicht geladen werden.'))).then((payload) => { if (!payload.ok || !bestellungenMobileDetailHost) { throw new Error('Bestelldetails konnten nicht geladen werden.'); } const order = payload.order; const cards = bestellungenMobileDetailHost.querySelector('[data-bestellungen-mobile-cards]'); const lines = (order.lines || []).map((line) => '' + escapeHtml(line.item_name) + ' · ' + escapeHtml(line.item_code) + ' · Menge: ' + escapeHtml(String(line.qty)) + ' · Status: ' + escapeHtml(line.line_status) + '
').join(''); const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '' + escapeHtml(item.product_name) + ': ' + escapeHtml(item.lot_number || 'Nicht zugeordnet') + ' · ' + escapeHtml(item.allocation_status || 'Nicht vorhanden') + '
')).join(''); if (cards) { cards.innerHTML = 'Bestellnummer: ' + escapeHtml(order.external_ref) + '
Bestelldatum: ' + escapeHtml(formatDate(order.order_date)) + '
Verkaufskanal: ' + escapeHtml(order.order_source) + '
Status: ' + escapeHtml(order.order_status) + '
Keine Artikel vorhanden.
') + 'Keine Chargenzuordnung vorhanden.
') + '' + escapeHtml(error.message) + '
'; } });"; echo " bestellungenDetailOpen = true;"; echo " window.history.pushState({ bestellungenView: 'detail', orderId: trigger.dataset.orderId || '' }, '', window.location.href);"; echo " };"; diff --git a/public/api/bestellungen/detail.php b/public/api/bestellungen/detail.php new file mode 100644 index 0000000..08770f9 --- /dev/null +++ b/public/api/bestellungen/detail.php @@ -0,0 +1,34 @@ + false, 'error' => 'Unauthorized']); + exit; +} + +$orderId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); +if (!is_int($orderId) || $orderId <= 0) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Invalid order id']); + exit; +} + +$detail = get_sales_order_detail($pdo, $orderId); +if ($detail === null) { + http_response_code(404); + echo json_encode(['ok' => false, 'error' => 'Order not found']); + exit; +} + +echo json_encode(['ok' => true, 'order' => $detail], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);