Bestellungen-Drawer mit Artikeln und Chargen erweitern

This commit is contained in:
gitea_admin committed 2026-09-06 10:27:54 +02:00
1 parent e43b5d34e0
commit 575650a7f0
3 files changed
+197 -40

No files matched your search

+144
View File
@@ -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 function get_sales_order_realtime_snapshot(PDO $pdo, int $limit = 1000): array
{ {
$limit = max(1, min(2000, $limit)); $limit = max(1, min(2000, $limit));
+19 -40
View File
@@ -172,21 +172,7 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
$html[] = '<button class="sg-interaction-element sg-hyperlink" type="button" data-order-drawer-close>schliessen</button>'; $html[] = '<button class="sg-interaction-element sg-hyperlink" type="button" data-order-drawer-close>schliessen</button>';
$html[] = '</div>'; $html[] = '</div>';
$html[] = '<div class="sg-card-list-page-drawer__content">'; $html[] = '<div class="sg-card-list-page-drawer__content">';
$html[] = '<article class="sg-card sg-vsf-drawer-card" data-component="card" data-pattern="card" aria-label="Bestell-Details">'; $html[] = '<div data-order-drawer-cards><p class="sg-body" data-order-drawer-loading>Bestelldetails werden geladen …</p></div>';
$html[] = '<header class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header">';
$html[] = '<div class="sg-strong">Bestell-Details</div>';
$html[] = '</header>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content">';
$html[] = '<p class="sg-body"><strong>Bestellnummer:</strong> <span data-order-drawer-order-number></span></p>';
$html[] = '<p class="sg-body"><strong>Bestelldatum:</strong> <span data-order-drawer-order-date></span></p>';
$html[] = '<p class="sg-body"><strong>Lieferadresse:</strong></p>';
$html[] = '<p class="sg-body" data-order-drawer-address-line-1></p>';
$html[] = '<p class="sg-body" data-order-drawer-address-line-2></p>';
$html[] = '<p class="sg-body" data-order-drawer-address-line-3></p>';
$html[] = '<p class="sg-body"><strong>Gesamtsumme:</strong> <span data-order-drawer-total></span></p>';
$html[] = '<p class="sg-body">Detailinhalt wird noch definiert.</p>';
$html[] = '</div>';
$html[] = '</article>';
$html[] = '</div>'; $html[] = '</div>';
$html[] = '</aside>'; $html[] = '</aside>';
@@ -777,6 +763,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " const zip = trigger.dataset.orderZip || '';"; echo " const zip = trigger.dataset.orderZip || '';";
echo " const city = trigger.dataset.orderCity || '';"; echo " const city = trigger.dataset.orderCity || '';";
echo " const country = trigger.dataset.orderCountry || '';"; echo " const country = trigger.dataset.orderCountry || '';";
echo " const formatDate = (value) => value ? new Date(value).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' }) : 'Nicht vorhanden';";
echo " const streetLine = [street, houseNumber].filter((part) => part && part.length > 0).join(' ').trim();"; echo " const streetLine = [street, houseNumber].filter((part) => part && part.length > 0).join(' ').trim();";
echo " const cityLine = [zip, city].filter((part) => part && part.length > 0).join(' ').trim();"; echo " const cityLine = [zip, city].filter((part) => part && part.length > 0).join(' ').trim();";
echo " return ["; echo " return [";
@@ -792,7 +779,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " '</article>',"; echo " '</article>',";
echo " '</div>',"; echo " '</div>',";
echo " '</div>',"; echo " '</div>',";
echo " '<article class=\"sg-card sg-vsf-drawer-card\" data-component=\"card\" data-pattern=\"card\" aria-label=\"Bestell-Details\">',"; echo " '<div data-bestellungen-mobile-cards><article class=\"sg-card sg-vsf-drawer-card\" data-component=\"card\" data-pattern=\"card\" aria-label=\"Bestell-Details\">',";
echo " '<div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\">',"; echo " '<div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\">',";
echo " '<p class=\"sg-body\"><strong>Bestellnummer:</strong> <span>' + escapeHtml(orderNumber) + '</span></p>',"; echo " '<p class=\"sg-body\"><strong>Bestellnummer:</strong> <span>' + escapeHtml(orderNumber) + '</span></p>',";
echo " '<p class=\"sg-body\"><strong>Bestelldatum:</strong> <span>' + escapeHtml(orderDate) + '</span></p>',"; echo " '<p class=\"sg-body\"><strong>Bestelldatum:</strong> <span>' + escapeHtml(orderDate) + '</span></p>',";
@@ -803,7 +790,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " '<p class=\"sg-body\"><strong>Gesamtsumme:</strong> <span>' + escapeHtml(total) + '</span></p>',"; echo " '<p class=\"sg-body\"><strong>Gesamtsumme:</strong> <span>' + escapeHtml(total) + '</span></p>',";
echo " '<p class=\"sg-body\">Detailinhalt wird noch definiert.</p>',"; echo " '<p class=\"sg-body\">Detailinhalt wird noch definiert.</p>',";
echo " '</div>',"; echo " '</div>',";
echo " '</article>',"; echo " '</article></div>',";
echo " '</div>'"; echo " '</div>'";
echo " ].join('');"; echo " ].join('');";
echo " };"; echo " };";
@@ -833,30 +820,21 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " return;"; echo " return;";
echo " }"; echo " }";
echo " const drawerTitle = drawer.querySelector('[data-order-drawer-title]');"; echo " const drawerTitle = drawer.querySelector('[data-order-drawer-title]');";
echo " const drawerOrderNumber = drawer.querySelector('[data-order-drawer-order-number]');"; echo " const cards = drawer.querySelector('[data-order-drawer-cards]');";
echo " const drawerOrderDate = drawer.querySelector('[data-order-drawer-order-date]');"; echo " const orderId = trigger.dataset.orderId || '';";
echo " const drawerAddressLine1 = drawer.querySelector('[data-order-drawer-address-line-1]');"; echo " const formatAmount = (value, currency) => value === null || value === undefined ? 'Nicht vorhanden' : currency + ' ' + Number(value).toFixed(2);";
echo " const drawerAddressLine2 = drawer.querySelector('[data-order-drawer-address-line-2]');"; echo " const formatDate = (value) => value ? new Date(value).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' }) : 'Nicht vorhanden';";
echo " const drawerAddressLine3 = drawer.querySelector('[data-order-drawer-address-line-3]');"; echo " const value = (label, content) => '<p class=\"sg-body\"><strong>' + escapeHtml(label) + ':</strong> ' + escapeHtml(content || 'Nicht vorhanden') + '</p>';";
echo " const drawerTotal = drawer.querySelector('[data-order-drawer-total]');"; echo " const renderCards = (order) => {";
echo " const orderNumber = trigger.dataset.orderNumber || '';"; 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('<br>');";
echo " const orderDate = trigger.dataset.orderDate || '';"; echo " const lines = (order.lines || []).map((line) => '<div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\"><p class=\"sg-body\"><strong>' + escapeHtml(line.item_name) + '</strong> · ' + escapeHtml(line.item_code) + ' · Menge: ' + escapeHtml(String(line.qty)) + '</p>' + value('Positionsstatus', line.line_status) + value('Einzelpreis', formatAmount(line.unit_price, a.currency || 'CHF')) + value('Positionssumme', formatAmount(line.line_total, a.currency || 'CHF')) + '</div>').join('');";
echo " const total = trigger.dataset.orderTotal || '';"; echo " const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '<div class=\"sg-body\"><strong>' + escapeHtml(item.product_name) + ':</strong> ' + escapeHtml(item.lot_number || 'Nicht zugeordnet') + ' · ' + escapeHtml(item.allocated_qty === null ? 'Nicht zugeordnet' : String(item.allocated_qty)) + ' · ' + escapeHtml(item.allocation_status || 'Nicht vorhanden') + (item.lot_status === 'virtual_shortage' ? ' · Virtuelle Fehlbestandscharge' : '') + '</div>')).join('');";
echo " const street = trigger.dataset.orderStreet || '';"; echo " return '<article class=\"sg-card sg-vsf-drawer-card\" data-component=\"card\" data-pattern=\"card\" aria-label=\"Bestellübersicht\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header\"><div class=\"sg-strong\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\">' + value('Bestellnummer', order.external_ref) + value('Bestelldatum', formatDate(order.order_date)) + value('Verkaufskanal', order.order_source) + value('Bestellstatus', order.order_status) + value('Zahlungsstatus', order.payment_status) + value('Zahlungsart', order.payment_method) + value('Versandart', order.shipping_method) + '<p class=\"sg-body\"><strong>Kunde:</strong> ' + escapeHtml([c.first_name, c.last_name].filter(Boolean).join(' ') || 'Nicht vorhanden') + '</p><p class=\"sg-body\"><strong>Lieferadresse:</strong><br>' + address + '</p>' + value('Netto', formatAmount(a.net, a.currency || 'CHF')) + value('Versand', formatAmount(a.shipping, a.currency || 'CHF')) + value('Rabatt', formatAmount(a.discount, a.currency || 'CHF')) + value('Steuer', formatAmount(a.tax, a.currency || 'CHF')) + value('Gesamtsumme', formatAmount(a.total, a.currency || 'CHF')) + '</div></article><article class=\"sg-card sg-vsf-drawer-card\" data-component=\"card\" data-pattern=\"card\" aria-label=\"Bestellte Artikel\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header\"><div class=\"sg-strong\">Bestellte Artikel</div></header>' + (lines || '<div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\"><p class=\"sg-body\">Keine Artikel vorhanden.</p></div>') + '</article><article class=\"sg-card sg-vsf-drawer-card\" data-component=\"card\" data-pattern=\"card\" aria-label=\"Chargenzuordnung\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header\"><div class=\"sg-strong\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>';";
echo " const houseNumber = trigger.dataset.orderHouseNumber || '';"; echo " };";
echo " const zip = trigger.dataset.orderZip || '';";
echo " const city = trigger.dataset.orderCity || '';";
echo " const country = trigger.dataset.orderCountry || '';";
echo " const streetLine = [street, houseNumber].filter((part) => part && part.length > 0).join(' ').trim();";
echo " const cityLine = [zip, city].filter((part) => part && part.length > 0).join(' ').trim();";
echo " setDrawerState(true);"; echo " setDrawerState(true);";
echo " if (drawerTitle) { drawerTitle.textContent = orderNumber !== '' ? 'Bestellung ' + orderNumber : 'Bestellung'; }"; echo " if (drawerTitle) { drawerTitle.textContent = trigger.dataset.orderNumber ? 'Bestellung ' + trigger.dataset.orderNumber : 'Bestellung'; }";
echo " if (drawerOrderNumber) { drawerOrderNumber.textContent = orderNumber; }"; echo " if (cards) { cards.innerHTML = '<p class=\"sg-body\">Bestelldetails werden geladen …</p>'; }";
echo " if (drawerOrderDate) { drawerOrderDate.textContent = orderDate; }"; 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 = '<p class=\"sg-body\">' + escapeHtml(error.message) + '</p>'; } });";
echo " if (drawerAddressLine1) { drawerAddressLine1.textContent = streetLine; }";
echo " if (drawerAddressLine2) { drawerAddressLine2.textContent = cityLine; }";
echo " if (drawerAddressLine3) { drawerAddressLine3.textContent = country; }";
echo " if (drawerTotal) { drawerTotal.textContent = total; }";
echo " contentRoot.querySelectorAll('[data-order-drawer-open]').forEach((button) => {"; echo " contentRoot.querySelectorAll('[data-order-drawer-open]').forEach((button) => {";
echo " button.setAttribute('aria-expanded', String(button === trigger));"; echo " button.setAttribute('aria-expanded', String(button === trigger));";
echo " });"; echo " });";
@@ -879,6 +857,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " document.body.appendChild(bestellungenMobileDetailHost);"; echo " document.body.appendChild(bestellungenMobileDetailHost);";
echo " document.body.classList.add('sg-bestellungen-mobile-detail-open');"; echo " document.body.classList.add('sg-bestellungen-mobile-detail-open');";
echo " }"; 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) => '<p class=\"sg-body\"><strong>' + escapeHtml(line.item_name) + '</strong> · ' + escapeHtml(line.item_code) + ' · Menge: ' + escapeHtml(String(line.qty)) + ' · Status: ' + escapeHtml(line.line_status) + '</p>').join(''); const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '<p class=\"sg-body\"><strong>' + escapeHtml(item.product_name) + ':</strong> ' + escapeHtml(item.lot_number || 'Nicht zugeordnet') + ' · ' + escapeHtml(item.allocation_status || 'Nicht vorhanden') + '</p>')).join(''); if (cards) { cards.innerHTML = '<article class=\"sg-card sg-vsf-drawer-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header\"><div class=\"sg-strong\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\"><p class=\"sg-body\"><strong>Bestellnummer:</strong> ' + escapeHtml(order.external_ref) + '</p><p class=\"sg-body\"><strong>Bestelldatum:</strong> ' + escapeHtml(formatDate(order.order_date)) + '</p><p class=\"sg-body\"><strong>Verkaufskanal:</strong> ' + escapeHtml(order.order_source) + '</p><p class=\"sg-body\"><strong>Status:</strong> ' + escapeHtml(order.order_status) + '</p></div></article><article class=\"sg-card sg-vsf-drawer-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header\"><div class=\"sg-strong\">Bestellte Artikel</div></header><div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\">' + (lines || '<p class=\"sg-body\">Keine Artikel vorhanden.</p>') + '</div></article><article class=\"sg-card sg-vsf-drawer-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header\"><div class=\"sg-strong\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>'; } }).catch((error) => { const cards = bestellungenMobileDetailHost?.querySelector('[data-bestellungen-mobile-cards]'); if (cards) { cards.innerHTML = '<p class=\"sg-body\">' + escapeHtml(error.message) + '</p>'; } });";
echo " bestellungenDetailOpen = true;"; echo " bestellungenDetailOpen = true;";
echo " window.history.pushState({ bestellungenView: 'detail', orderId: trigger.dataset.orderId || '' }, '', window.location.href);"; echo " window.history.pushState({ bestellungenView: 'detail', orderId: trigger.dataset.orderId || '' }, '', window.location.href);";
echo " };"; echo " };";
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../modules/shared/db.php';
require_once __DIR__ . '/../../../modules/shared/auth/service.php';
require_once __DIR__ . '/../../../modules/erp/bestellungen/service.php';
header('Content-Type: application/json; charset=utf-8');
$env = expand_env_values(parse_env_file(__DIR__ . '/../../../.env'));
$pdo = connect_database($env);
auth_bootstrap_session();
if (auth_current_user($pdo) === null) {
http_response_code(401);
echo json_encode(['ok' => 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);