Bestellliste im ERP-Home ergänzen

This commit is contained in:
2026-06-15 14:54:07 +02:00
parent 359dd2cbe3
commit fe7e7b6575
3 changed files with 487 additions and 2 deletions
+145
View File
@@ -175,3 +175,148 @@ function insert_sales_order_line(PDO $pdo, array $fields): int
return (int) $id;
}
function normalize_sales_order_sort_column(string $sortColumn): string
{
$allowed = [
'order_date',
'external_ref',
'last_name',
'first_name',
'total_amount',
];
return in_array($sortColumn, $allowed, true) ? $sortColumn : 'order_date';
}
function normalize_sales_order_sort_direction(string $direction, string $sortColumn): string
{
$direction = strtoupper(trim($direction));
if ($direction !== 'ASC' && $direction !== 'DESC') {
return $sortColumn === 'order_date' ? 'DESC' : 'ASC';
}
return $direction;
}
function escape_sales_order_search_term(string $searchTerm): string
{
return strtr($searchTerm, [
'\\' => '\\\\',
'%' => '\\%',
'_' => '\\_',
]);
}
function get_sales_order_overview(PDO $pdo, array $filters = []): array
{
$searchTerm = trim((string) ($filters['search'] ?? ''));
$sortColumn = normalize_sales_order_sort_column((string) ($filters['sort_column'] ?? 'order_date'));
$sortDirection = normalize_sales_order_sort_direction((string) ($filters['sort_direction'] ?? ''), $sortColumn);
$limit = max(1, (int) ($filters['limit'] ?? 20));
$pageSize = max(1, (int) ($filters['page_size'] ?? 20));
$filterSql = '';
$params = [];
if ($searchTerm !== '') {
$filterSql = <<<'SQL'
WHERE (
so.external_ref ILIKE :search_term ESCAPE '\'
OR COALESCE(ad.last_name, '') ILIKE :search_term ESCAPE '\'
OR COALESCE(ad.first_name, '') ILIKE :search_term ESCAPE '\'
)
SQL;
$params[':search_term'] = '%' . escape_sales_order_search_term($searchTerm) . '%';
}
$sortExpressions = [
'order_date' => 'so.order_date',
'external_ref' => 'so.external_ref',
'last_name' => 'COALESCE(ad.last_name, \'\')',
'first_name' => 'COALESCE(ad.first_name, \'\')',
'total_amount' => 'COALESCE(so.total_amount, 0)',
];
$sortExpression = $sortExpressions[$sortColumn] ?? 'so.order_date';
$baseFromSql = <<<'SQL'
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 = 'shipping'
ORDER BY a.id DESC
LIMIT 1
) ad ON TRUE
SQL;
$countStmt = $pdo->prepare(
'SELECT COUNT(*) ' . $baseFromSql . "\n" . $filterSql
);
foreach ($params as $key => $value) {
$countStmt->bindValue($key, $value, PDO::PARAM_STR);
}
$countStmt->execute();
$totalCount = (int) $countStmt->fetchColumn();
$listSql = <<<'SQL'
SELECT
so.id,
so.external_ref,
so.order_date,
so.total_amount,
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
SQL;
$listSql .= "\n" . $baseFromSql . "\n" . $filterSql;
$listSql .= "\nORDER BY {$sortExpression} {$sortDirection}, so.id {$sortDirection}";
$listSql .= "\nLIMIT :limit";
$listStmt = $pdo->prepare($listSql);
foreach ($params as $key => $value) {
$listStmt->bindValue($key, $value, PDO::PARAM_STR);
}
$listStmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$listStmt->execute();
$rows = [];
foreach ($listStmt->fetchAll() as $row) {
$rows[] = [
'id' => (int) $row['id'],
'external_ref' => (string) $row['external_ref'],
'order_date' => (string) $row['order_date'],
'total_amount' => $row['total_amount'] !== null ? (float) $row['total_amount'] : null,
'first_name' => (string) $row['first_name'],
'last_name' => (string) $row['last_name'],
'street' => (string) $row['street'],
'house_number' => (string) $row['house_number'],
'zip' => (string) $row['zip'],
'city' => (string) $row['city'],
'country_name' => (string) $row['country_name'],
];
}
return [
'rows' => $rows,
'search' => $searchTerm,
'sort_column' => $sortColumn,
'sort_direction' => $sortDirection,
'limit' => $limit,
'page_size' => $pageSize,
'total_count' => $totalCount,
'has_more' => $totalCount > $limit,
'next_limit' => min($totalCount, $limit + $pageSize),
];
}
+333 -1
View File
@@ -3,7 +3,194 @@ declare(strict_types=1);
require_once __DIR__ . '/../service.php';
function render_auth_home_page(array $user, array $otcProducts = []): void
function auth_current_path(): string
{
$requestUri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
$path = parse_url($requestUri, PHP_URL_PATH);
if (!is_string($path) || $path === '') {
return '/';
}
return $path;
}
function auth_build_path_with_query(array $query = []): string
{
$query = array_filter(
$query,
static fn ($value): bool => $value !== null && $value !== ''
);
$path = auth_current_path();
$queryString = http_build_query($query, '', '&', PHP_QUERY_RFC3986);
return $queryString === '' ? $path : $path . '?' . $queryString;
}
function auth_format_bestellung_date(?string $value): string
{
if ($value === null || $value === '') {
return '';
}
try {
return (new DateTimeImmutable($value))->format('d.m.Y H:i');
} catch (Throwable) {
return $value;
}
}
function auth_format_bestellung_total(?float $value): string
{
if ($value === null) {
return '';
}
return 'CHF ' . number_format($value, 2, '.', '\'');
}
function auth_render_bestellungen_large_table(array $bestellungenTable): string
{
$rows = $bestellungenTable['rows'] ?? [];
$search = (string) ($bestellungenTable['search'] ?? '');
$sortColumn = (string) ($bestellungenTable['sort_column'] ?? 'order_date');
$sortDirection = strtoupper((string) ($bestellungenTable['sort_direction'] ?? 'DESC'));
$limit = (int) ($bestellungenTable['limit'] ?? 20);
$pageSize = (int) ($bestellungenTable['page_size'] ?? 20);
$hasMore = (bool) ($bestellungenTable['has_more'] ?? false);
$nextLimit = (int) ($bestellungenTable['next_limit'] ?? $limit);
$sortColumns = [
'order_date' => 'Bestelldatum',
'external_ref' => 'Bestellnummer',
'last_name' => 'Nachname',
'first_name' => 'Vorname',
'total_amount' => 'Gesamtsumme',
];
$html = [];
$html[] = '<section class="sg-card-list-page" aria-label="Bestellungen">';
$html[] = '<article class="sg-card sg-large-table" data-component="large-table" data-bestellungen-large-table="true" data-bestellungen-search="' . auth_escape_html($search) . '" data-bestellungen-sort-column="' . auth_escape_html($sortColumn) . '" data-bestellungen-sort-direction="' . auth_escape_html($sortDirection) . '" data-bestellungen-limit="' . $limit . '" data-bestellungen-page-size="' . $pageSize . '" role="table" aria-label="Bestellungen">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
$html[] = '<div class="sg-strong">Bestellungen</div>';
$html[] = '<span class="sg-search-field-row" data-component="search-field">';
$html[] = '<span class="sg-input-single-line-wrap" data-has-value="' . ($search !== '' ? 'true' : 'false') . '" data-component="single-line-input" data-component-state="' . ($search !== '' ? 'active' : 'inactive-selectable') . '">';
$html[] = '<input class="sg-interaction-element sg-input-single-line sg-search-field-input sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" placeholder="Suche" aria-label="Bestellungen durchsuchen" value="' . auth_escape_html($search) . '" data-bestellungen-search-input>';
$html[] = '<button class="sg-input-clear-button" type="button" aria-label="Eingabe löschen" data-bestellungen-search-clear>×</button>';
$html[] = '</span>';
$html[] = '</span>';
$html[] = '</div>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
$headerIndex = 0;
foreach ($sortColumns as $column => $label) {
$isActive = $column === $sortColumn;
$headerDirection = $isActive ? strtolower($sortDirection) : 'none';
$iconDirection = $isActive ? strtolower($sortDirection) : 'ascending';
$ariaLabel = $isActive
? $label . ' ' . ($sortDirection === 'ASC' ? 'aufsteigend sortiert' : 'absteigend sortiert')
: $label . ' sortieren';
$targetDirection = $isActive
? ($sortDirection === 'ASC' ? 'DESC' : 'ASC')
: ($column === 'order_date' ? 'DESC' : 'ASC');
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '">';
$html[] = '<button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-bestellungen-sort-button="true" data-bestellungen-sort-column="' . auth_escape_html($column) . '" data-bestellungen-sort-direction="' . auth_escape_html($targetDirection) . '">';
$html[] = '<span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span>';
$html[] = '<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span>';
$html[] = '</button>';
$html[] = '</div>';
$headerIndex++;
}
$html[] = '</div>';
foreach ($rows as $index => $row) {
$isStriped = $index % 2 === 1;
$rowClass = 'sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row';
if ($isStriped) {
$rowClass .= ' sg-large-table__row--striped-light';
}
$orderNumber = (string) ($row['external_ref'] ?? '');
$orderDate = auth_format_bestellung_date((string) ($row['order_date'] ?? ''));
$totalAmount = auth_format_bestellung_total(isset($row['total_amount']) ? (float) $row['total_amount'] : null);
$firstName = (string) ($row['first_name'] ?? '');
$lastName = (string) ($row['last_name'] ?? '');
$street = (string) ($row['street'] ?? '');
$houseNumber = (string) ($row['house_number'] ?? '');
$zip = (string) ($row['zip'] ?? '');
$city = (string) ($row['city'] ?? '');
$country = (string) ($row['country_name'] ?? '');
$html[] = '<div class="' . $rowClass . '" role="row" data-component-part="large-table-row" data-bestellungen-row="true">';
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($orderDate) . '</div>';
$html[] = '<div class="sg-large-table__cell" role="cell">';
$html[] = '<button class="sg-interaction-element sg-hyperlink" type="button" aria-haspopup="dialog" aria-expanded="false" data-order-drawer-open="true" data-order-id="' . (int) ($row['id'] ?? 0) . '" data-order-number="' . auth_escape_html($orderNumber) . '" data-order-date="' . auth_escape_html($orderDate) . '" data-order-total="' . auth_escape_html($totalAmount) . '" data-order-first-name="' . auth_escape_html($firstName) . '" data-order-last-name="' . auth_escape_html($lastName) . '" data-order-street="' . auth_escape_html($street) . '" data-order-house-number="' . auth_escape_html($houseNumber) . '" data-order-zip="' . auth_escape_html($zip) . '" data-order-city="' . auth_escape_html($city) . '" data-order-country="' . auth_escape_html($country) . '">' . auth_escape_html($orderNumber) . '</button>';
$html[] = '</div>';
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($lastName) . '</div>';
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($firstName) . '</div>';
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($totalAmount) . '</div>';
$html[] = '</div>';
}
if ($hasMore) {
$loadMoreUrl = auth_build_path_with_query([
'bestellungen_search' => $search !== '' ? $search : null,
'bestellungen_sort' => $sortColumn,
'bestellungen_dir' => $sortDirection,
'bestellungen_limit' => $nextLimit,
]);
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row" data-large-table-load-more-row="true">';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell">';
$html[] = '<div class="sg-navigation-card-layout sg-large-table__load-more-layout">';
$html[] = '<div class="sg-navigation-card-block">';
$html[] = '<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden">';
$html[] = '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">';
$html[] = '<div class="sg-navigation-card-center">';
$html[] = '<a class="sg-hyperlink" href="' . auth_escape_html($loadMoreUrl) . '" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</article>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '</article>';
$html[] = '</section>';
$html[] = '<aside class="sg-card-list-page-drawer" aria-label="Bestell-Details" aria-hidden="true" data-bestellungen-drawer="true" data-open="false">';
$html[] = '<div class="sg-card-list-page-drawer__header">';
$html[] = '<h2 class="sg-heading-h2 sg-card-list-page-drawer__title" data-order-drawer-title>Bestellung</h2>';
$html[] = '<button class="sg-interaction-element sg-hyperlink" type="button" data-order-drawer-close>schliessen</button>';
$html[] = '</div>';
$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[] = '<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[] = '</aside>';
return implode('', $html);
}
function render_auth_home_page(array $user, array $otcProducts = [], array $bestellungenTable = []): void
{
$otcProductRows = '';
foreach ($otcProducts as $product) {
@@ -112,6 +299,7 @@ function render_auth_home_page(array $user, array $otcProducts = []): void
echo '</div>';
echo '</div>';
echo '</article>';
echo auth_render_bestellungen_large_table($bestellungenTable);
echo '</div>';
echo '</section>';
echo '</div>';
@@ -310,6 +498,150 @@ function render_auth_home_page(array $user, array $otcProducts = []): void
echo " mediaQuery.addEventListener('change', syncMode);";
echo "})();";
echo "(() => {";
echo " const table = document.querySelector('[data-bestellungen-large-table]');";
echo " if (!table) { return; }";
echo " const searchInput = table.querySelector('[data-bestellungen-search-input]');";
echo " const clearButton = table.querySelector('[data-bestellungen-search-clear]');";
echo " const sortButtons = Array.from(table.querySelectorAll('[data-bestellungen-sort-button]'));";
echo " const drawer = document.querySelector('[data-bestellungen-drawer]');";
echo " const drawerTitle = drawer ? drawer.querySelector('[data-order-drawer-title]') : null;";
echo " const drawerOrderNumber = drawer ? drawer.querySelector('[data-order-drawer-order-number]') : null;";
echo " const drawerOrderDate = drawer ? drawer.querySelector('[data-order-drawer-order-date]') : null;";
echo " const drawerAddressLine1 = drawer ? drawer.querySelector('[data-order-drawer-address-line-1]') : null;";
echo " const drawerAddressLine2 = drawer ? drawer.querySelector('[data-order-drawer-address-line-2]') : null;";
echo " const drawerAddressLine3 = drawer ? drawer.querySelector('[data-order-drawer-address-line-3]') : null;";
echo " const drawerTotal = drawer ? drawer.querySelector('[data-order-drawer-total]') : null;";
echo " const searchWrap = searchInput ? searchInput.closest('[data-component=\"single-line-input\"]') : null;";
echo " const defaultLimit = parseInt(table.dataset.bestellungenPageSize || table.dataset.bestellungenLimit || '20', 10);";
echo " const currentSortColumn = table.dataset.bestellungenSortColumn || 'order_date';";
echo " const currentSortDirection = (table.dataset.bestellungenSortDirection || 'DESC').toUpperCase();";
echo " let searchTimer = null;";
echo " const setWrapState = (input) => {";
echo " if (!searchWrap || !input) { return; }";
echo " searchWrap.setAttribute('data-has-value', String(input.value.trim().length > 0));";
echo " searchWrap.dataset.componentState = input.value.trim().length > 0 ? 'active' : 'inactive-selectable';";
echo " };";
echo " const buildUrl = (params) => {";
echo " const url = new URL(window.location.href);";
echo " Object.entries(params).forEach(([key, value]) => {";
echo " if (value === null || value === undefined || value === '') {";
echo " url.searchParams.delete(key);";
echo " } else {";
echo " url.searchParams.set(key, String(value));";
echo " }";
echo " });";
echo " return url.toString();";
echo " };";
echo " const navigate = (params) => {";
echo " window.location.assign(buildUrl(params));";
echo " };";
echo " const currentSearchValue = () => (searchInput ? searchInput.value.trim() : '');";
echo " const resetLimit = () => defaultLimit;";
echo " const closeDrawer = () => {";
echo " if (!drawer) { return; }";
echo " drawer.dataset.open = 'false';";
echo " drawer.setAttribute('aria-hidden', 'true');";
echo " document.querySelectorAll('[data-order-drawer-open]').forEach((trigger) => {";
echo " trigger.setAttribute('aria-expanded', 'false');";
echo " });";
echo " };";
echo " const setDrawerField = (node, value) => {";
echo " if (node) { node.textContent = value || ''; }";
echo " };";
echo " const formatAddressLine = (street, houseNumber, zip, city, country) => {";
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 " return [streetLine, cityLine, country].filter((part) => part && part.length > 0).join(' · ');";
echo " };";
echo " const openDrawer = (trigger) => {";
echo " if (!drawer) { return; }";
echo " const orderNumber = trigger.dataset.orderNumber || '';";
echo " const orderDate = trigger.dataset.orderDate || '';";
echo " const total = trigger.dataset.orderTotal || '';";
echo " const firstName = trigger.dataset.orderFirstName || '';";
echo " const lastName = trigger.dataset.orderLastName || '';";
echo " const street = trigger.dataset.orderStreet || '';";
echo " const houseNumber = trigger.dataset.orderHouseNumber || '';";
echo " const zip = trigger.dataset.orderZip || '';";
echo " const city = trigger.dataset.orderCity || '';";
echo " const country = trigger.dataset.orderCountry || '';";
echo " drawer.dataset.open = 'true';";
echo " drawer.setAttribute('aria-hidden', 'false');";
echo " if (drawerTitle) { drawerTitle.textContent = orderNumber !== '' ? 'Bestellung ' + orderNumber : 'Bestellung'; }";
echo " setDrawerField(drawerOrderNumber, orderNumber);";
echo " setDrawerField(drawerOrderDate, orderDate);";
echo " setDrawerField(drawerAddressLine1, formatAddressLine(street, houseNumber, '', '', ''));";
echo " setDrawerField(drawerAddressLine2, formatAddressLine('', '', zip, city, ''));";
echo " setDrawerField(drawerAddressLine3, country);";
echo " setDrawerField(drawerTotal, total);";
echo " document.querySelectorAll('[data-order-drawer-open]').forEach((button) => {";
echo " button.setAttribute('aria-expanded', String(button === trigger));";
echo " });";
echo " };";
echo " sortButtons.forEach((button) => {";
echo " button.addEventListener('click', () => {";
echo " const sortColumn = button.dataset.bestellungenSortColumn || currentSortColumn;";
echo " const sortDirection = button.dataset.bestellungenSortDirection || currentSortDirection;";
echo " navigate({";
echo " bestellungen_search: currentSearchValue(),";
echo " bestellungen_sort: sortColumn,";
echo " bestellungen_dir: sortDirection,";
echo " bestellungen_limit: resetLimit(),";
echo " });";
echo " });";
echo " });";
echo " if (searchInput) {";
echo " searchInput.addEventListener('input', () => {";
echo " setWrapState(searchInput);";
echo " if (searchTimer) { window.clearTimeout(searchTimer); }";
echo " searchTimer = window.setTimeout(() => {";
echo " navigate({";
echo " bestellungen_search: currentSearchValue(),";
echo " bestellungen_sort: currentSortColumn,";
echo " bestellungen_dir: currentSortDirection,";
echo " bestellungen_limit: resetLimit(),";
echo " });";
echo " }, 250);";
echo " });";
echo " }";
echo " if (clearButton && searchInput) {";
echo " clearButton.addEventListener('click', () => {";
echo " if (searchTimer) { window.clearTimeout(searchTimer); searchTimer = null; }";
echo " searchInput.value = '';";
echo " setWrapState(searchInput);";
echo " navigate({";
echo " bestellungen_search: '',";
echo " bestellungen_sort: currentSortColumn,";
echo " bestellungen_dir: currentSortDirection,";
echo " bestellungen_limit: resetLimit(),";
echo " });";
echo " });";
echo " }";
echo " document.addEventListener('click', (event) => {";
echo " const openTrigger = event.target.closest('[data-order-drawer-open]');";
echo " if (openTrigger) {";
echo " event.preventDefault();";
echo " openDrawer(openTrigger);";
echo " return;";
echo " }";
echo " const closeTrigger = event.target.closest('[data-order-drawer-close]');";
echo " if (closeTrigger) {";
echo " event.preventDefault();";
echo " closeDrawer();";
echo " return;";
echo " }";
echo " if (drawer && drawer.dataset.open === 'true' && !event.target.closest('[data-bestellungen-drawer=\"true\"]')) {";
echo " closeDrawer();";
echo " }";
echo " });";
echo " document.addEventListener('keydown', (event) => {";
echo " if (event.key === 'Escape') {";
echo " closeDrawer();";
echo " }";
echo " });";
echo " setWrapState(searchInput);";
echo "})();";
echo "(() => {";
echo " const overlay = document.querySelector('[data-otc-order-overlay]');";
echo " if (!overlay) { return; }";
echo " const form = overlay.querySelector('[data-otc-order-form]');";