Files
orderPRO/resources/views/components/table-list.php
Jacek Pyziak ed057fc304 feat(08-10-receipt-module): phases 08-10 complete — receipt issuing from orders
Phase 08 — DB Foundation:
- 3 new tables: receipt_configs, receipts, receipt_number_counters
- company_settings extended with BDO, REGON, KRS, logo fields

Phase 09 — Receipt Config:
- CRUD for receipt configurations (Settings > Accounting)
- ReceiptConfigController + ReceiptConfigRepository

Phase 10 — Receipt Issuing:
- ReceiptRepository with atomic numbering (INSERT ON DUPLICATE KEY UPDATE)
- ReceiptController with snapshot pattern (seller/buyer/items as JSON)
- "Wystaw paragon" button in order view
- Documents tab showing both receipts and marketplace documents
- Activity log entry on receipt creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:49:06 +01:00

526 lines
21 KiB
PHP

<?php
declare(strict_types=1);
$config = is_array($tableList ?? null) ? $tableList : [];
$basePath = (string) ($config['base_path'] ?? '');
$query = is_array($config['query'] ?? null) ? $config['query'] : [];
$filters = is_array($config['filters'] ?? null) ? $config['filters'] : [];
$columns = is_array($config['columns'] ?? null) ? $config['columns'] : [];
$rows = is_array($config['rows'] ?? null) ? $config['rows'] : [];
$pagination = is_array($config['pagination'] ?? null) ? $config['pagination'] : [];
$perPageOptions = is_array($config['per_page_options'] ?? null) ? $config['per_page_options'] : [20, 50, 100];
$createUrl = (string) ($config['create_url'] ?? '');
$createLabel = (string) ($config['create_label'] ?? '');
$headerActions = is_array($config['header_actions'] ?? null) ? $config['header_actions'] : [];
$emptyMessage = (string) ($config['empty_message'] ?? 'Brak rekordow.');
$showActions = (bool) ($config['show_actions'] ?? true);
$actionsLabel = (string) ($config['actions_label'] ?? 'Akcje');
$listKey = (string) ($config['list_key'] ?? md5($basePath !== '' ? $basePath : 'table-list'));
$selectable = (bool) ($config['selectable'] ?? false);
$selectName = (string) ($config['select_name'] ?? 'selected_ids[]');
$selectValueKey = (string) ($config['select_value_key'] ?? 'id');
$selectColumnLabel = (string) ($config['select_column_label'] ?? 'Wybierz');
$currentSort = (string) ($query['sort'] ?? 'id');
$currentDir = strtoupper((string) ($query['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
$page = max(1, (int) ($pagination['page'] ?? 1));
$totalPages = max(1, (int) ($pagination['total_pages'] ?? 1));
$total = max(0, (int) ($pagination['total'] ?? 0));
$perPage = max(1, (int) ($pagination['per_page'] ?? 20));
$activeFiltersCount = 0;
foreach ($filters as $filter) {
if ((string) ($filter['value'] ?? '') !== '') {
$activeFiltersCount++;
}
}
$hasActiveFilters = $activeFiltersCount > 0;
$buildUrl = static function (array $params = []) use ($basePath, $query): string {
$merged = array_merge($query, $params);
foreach ($merged as $key => $value) {
if ($value === '' || $value === null) {
unset($merged[$key]);
}
}
$qs = http_build_query($merged);
if ($qs === '') {
return $basePath;
}
return $basePath . '?' . $qs;
};
?>
<section class="card table-list" data-table-list-id="<?= $e($listKey) ?>" data-table-list-base="<?= $e($basePath) ?>">
<div class="table-list__header">
<div class="table-list__left">
<?php if ($createUrl !== '' && $createLabel !== ''): ?>
<a href="<?= $e($createUrl) ?>" class="btn btn--primary">
<?= $e($createLabel) ?>
</a>
<?php endif; ?>
<?php foreach ($headerActions as $action): ?>
<?php
$actionType = (string) ($action['type'] ?? 'link');
$actionLabel = (string) ($action['label'] ?? '');
$actionClass = (string) ($action['class'] ?? 'btn btn--secondary');
$actionAttrs = is_array($action['attrs'] ?? null) ? $action['attrs'] : [];
if ($actionLabel === '') {
continue;
}
?>
<?php if ($actionType === 'button'): ?>
<button type="button" class="<?= $e($actionClass) ?>"
<?php foreach ($actionAttrs as $attrKey => $attrValue): ?>
<?= $e((string) $attrKey) ?>="<?= $e((string) $attrValue) ?>"
<?php endforeach; ?>
><?= $e($actionLabel) ?></button>
<?php else: ?>
<a href="<?= $e((string) ($action['url'] ?? '#')) ?>" class="<?= $e($actionClass) ?>"
<?php foreach ($actionAttrs as $attrKey => $attrValue): ?>
<?= $e((string) $attrKey) ?>="<?= $e((string) $attrValue) ?>"
<?php endforeach; ?>
><?= $e($actionLabel) ?></a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="table-list-header-actions">
<span class="muted">Wynikow: <?= $e((string) $total) ?></span>
<?php if (!empty($filters)): ?>
<button type="button" class="btn btn--secondary js-filter-toggle-btn<?= $hasActiveFilters ? ' is-active' : '' ?>" title="Filtry">
Filtry
<?php if ($hasActiveFilters): ?>
<span class="table-filter-badge"><?= $e((string) $activeFiltersCount) ?></span>
<?php endif; ?>
</button>
<?php endif; ?>
<div class="table-col-toggle-wrapper">
<button type="button" class="btn btn--secondary js-col-toggle-btn" title="Widocznosc kolumn">Kolumny</button>
<div class="table-col-toggle-dropdown js-col-toggle-dropdown">
<div class="table-col-toggle-header">Widocznosc kolumn</div>
<?php foreach ($columns as $index => $column): ?>
<?php $colKey = (string) ($column['key'] ?? ('col_' . $index)); ?>
<label class="table-col-toggle-item">
<span class="table-col-switch">
<input type="checkbox" class="js-col-toggle-checkbox" data-col-key="<?= $e($colKey) ?>" checked>
<span class="table-col-switch-slider"></span>
</span>
<?= $e((string) ($column['label'] ?? $colKey)) ?>
</label>
<?php endforeach; ?>
<div class="table-col-toggle-footer">
<button type="button" class="btn btn--secondary js-col-toggle-reset">Pokaz wszystkie</button>
</div>
</div>
</div>
</div>
</div>
<?php if (!empty($filters)): ?>
<div class="table-filters-wrapper js-table-filters-wrapper<?= $hasActiveFilters ? ' is-open' : '' ?>">
<form method="get" action="<?= $e($basePath) ?>" class="table-list-filters js-table-filters-form">
<?php foreach ($filters as $filter): ?>
<?php
$filterKey = (string) ($filter['key'] ?? '');
$filterType = (string) ($filter['type'] ?? 'text');
$filterLabel = (string) ($filter['label'] ?? $filterKey);
$filterValue = (string) ($filter['value'] ?? '');
$inputId = 'filter_' . preg_replace('/[^a-zA-Z0-9_]+/', '_', $filterKey) . '_' . $listKey;
?>
<label class="form-field">
<span class="field-label"><?= $e($filterLabel) ?></span>
<?php if ($filterType === 'select'): ?>
<select class="form-control" id="<?= $e($inputId) ?>" name="<?= $e($filterKey) ?>">
<?php foreach ((array) ($filter['options'] ?? []) as $value => $label): ?>
<option value="<?= $e((string) $value) ?>"<?= (string) $value === $filterValue ? ' selected' : '' ?>>
<?= $e((string) $label) ?>
</option>
<?php endforeach; ?>
</select>
<?php elseif ($filterType === 'date'): ?>
<input class="form-control" id="<?= $e($inputId) ?>" type="date" name="<?= $e($filterKey) ?>" value="<?= $e($filterValue) ?>">
<?php else: ?>
<input class="form-control" id="<?= $e($inputId) ?>" type="text" name="<?= $e($filterKey) ?>" value="<?= $e($filterValue) ?>">
<?php endif; ?>
</label>
<?php endforeach; ?>
<?php foreach ($query as $key => $value): ?>
<?php if (!in_array((string) $key, array_map(static fn (array $filter): string => (string) ($filter['key'] ?? ''), $filters), true) && (string) $key !== 'page'): ?>
<input type="hidden" name="<?= $e((string) $key) ?>" value="<?= $e((string) $value) ?>">
<?php endif; ?>
<?php endforeach; ?>
<label class="form-field">
<span class="field-label">&nbsp;</span>
<div class="filters-actions">
<button type="submit" class="btn btn--primary">Szukaj</button>
<a href="<?= $e($basePath) ?>" class="btn btn--secondary js-table-filters-clear">Wyczysc</a>
</div>
</label>
</form>
</div>
<?php endif; ?>
<div class="table-wrap">
<table class="table table-list-table">
<thead>
<tr>
<?php if ($selectable): ?>
<th class="table-select-col">
<label class="table-select-toggle">
<input type="checkbox" class="js-table-select-all" aria-label="<?= $e($selectColumnLabel) ?>">
</label>
</th>
<?php endif; ?>
<?php foreach ($columns as $index => $column): ?>
<?php
$colKey = (string) ($column['key'] ?? ('col_' . $index));
$label = (string) ($column['label'] ?? $colKey);
$sortable = (bool) ($column['sortable'] ?? false);
$sortKey = (string) ($column['sort_key'] ?? $colKey);
$isCurrent = $sortable && $currentSort === $sortKey;
$nextDir = $isCurrent && $currentDir === 'ASC' ? 'DESC' : 'ASC';
$headerClass = trim((string) ($column['class'] ?? ''));
?>
<th class="<?= $e($headerClass) ?>" data-col-key="<?= $e($colKey) ?>">
<?php if ($sortable): ?>
<a href="<?= $e($buildUrl(['sort' => $sortKey, 'sort_dir' => $nextDir, 'page' => 1])) ?>" class="table-sort-link">
<?= $e($label) ?>
<?php if ($isCurrent): ?>
<span class="table-sort-icon"><?= $currentDir === 'ASC' ? '&uarr;' : '&darr;' ?></span>
<?php else: ?>
<span class="table-sort-icon is-muted">&harr;</span>
<?php endif; ?>
</a>
<?php else: ?>
<?= $e($label) ?>
<?php endif; ?>
</th>
<?php endforeach; ?>
<?php if ($showActions): ?>
<th><?= $e($actionsLabel) ?></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr>
<td colspan="<?= $e((string) (count($columns) + ($showActions ? 1 : 0) + ($selectable ? 1 : 0))) ?>" class="muted">
<?= $e($emptyMessage) ?>
</td>
</tr>
<?php else: ?>
<?php foreach ($rows as $row): ?>
<tr>
<?php if ($selectable): ?>
<?php $selectValue = $row[$selectValueKey] ?? ''; ?>
<td class="table-select-col">
<label class="table-select-toggle">
<input type="checkbox" class="js-table-select-item" name="<?= $e($selectName) ?>" value="<?= $e((string) $selectValue) ?>">
</label>
</td>
<?php endif; ?>
<?php foreach ($columns as $index => $column): ?>
<?php
$colKey = (string) ($column['key'] ?? ('col_' . $index));
$raw = (bool) ($column['raw'] ?? false);
$cellClass = trim((string) ($column['class'] ?? ''));
$value = $row[$colKey] ?? '';
?>
<td class="<?= $e($cellClass) ?>" data-col-key="<?= $e($colKey) ?>">
<?php if ($raw): ?>
<?= (string) $value ?>
<?php else: ?>
<?= $e((string) $value) ?>
<?php endif; ?>
</td>
<?php endforeach; ?>
<?php if ($showActions): ?>
<td>
<?php foreach ((array) ($row['_actions'] ?? []) as $action): ?>
<?php
$actionMethod = strtolower((string) ($action['method'] ?? 'get'));
$actionUrl = (string) ($action['url'] ?? '#');
$actionClass = (string) ($action['class'] ?? 'btn btn--secondary');
$actionLabel = (string) ($action['label'] ?? 'Akcja');
$actionConfirm = trim((string) ($action['confirm'] ?? ''));
$actionConfirmTitle = trim((string) ($action['confirm_title'] ?? 'Potwierdzenie'));
$actionConfirmYes = trim((string) ($action['confirm_yes'] ?? 'Potwierdz'));
$actionConfirmNo = trim((string) ($action['confirm_no'] ?? 'Anuluj'));
$actionParams = is_array($action['params'] ?? null) ? $action['params'] : [];
?>
<?php if ($actionMethod === 'post'): ?>
<form
action="<?= $e($actionUrl) ?>"
method="post"
class="table-inline-action"
<?php if ($actionConfirm !== ''): ?>
data-alert-confirm="<?= $e($actionConfirm) ?>"
data-alert-confirm-title="<?= $e($actionConfirmTitle) ?>"
data-alert-confirm-yes="<?= $e($actionConfirmYes) ?>"
data-alert-confirm-no="<?= $e($actionConfirmNo) ?>"
<?php endif; ?>
>
<input type="hidden" name="_token" value="<?= $e((string) ($csrfToken ?? '')) ?>">
<?php foreach ($actionParams as $paramKey => $paramValue): ?>
<input type="hidden" name="<?= $e((string) $paramKey) ?>" value="<?= $e((string) $paramValue) ?>">
<?php endforeach; ?>
<button type="submit" class="<?= $e($actionClass) ?>"><?= $e($actionLabel) ?></button>
</form>
<?php else: ?>
<a href="<?= $e($actionUrl) ?>" class="<?= $e($actionClass) ?>">
<?= $e($actionLabel) ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="table-list__footer">
<div class="pagination">
<?php $startPage = max(1, $page - 2); ?>
<?php $endPage = min($totalPages, $page + 2); ?>
<a class="pagination__item<?= $page <= 1 ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => 1])) ?>">&laquo;</a>
<a class="pagination__item<?= $page <= 1 ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => max(1, $page - 1)])) ?>">&lsaquo;</a>
<?php for ($i = $startPage; $i <= $endPage; $i++): ?>
<a class="pagination__item<?= $i === $page ? ' is-active' : '' ?>" href="<?= $e($buildUrl(['page' => $i])) ?>">
<?= $e((string) $i) ?>
</a>
<?php endfor; ?>
<a class="pagination__item<?= $page >= $totalPages ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => min($totalPages, $page + 1)])) ?>">&rsaquo;</a>
<a class="pagination__item<?= $page >= $totalPages ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => $totalPages])) ?>">&raquo;</a>
</div>
<form method="get" action="<?= $e($basePath) ?>" class="table-list-per-page-form js-per-page-form">
<?php foreach ($query as $key => $value): ?>
<?php if ((string) $key !== 'per_page' && (string) $key !== 'page'): ?>
<input type="hidden" name="<?= $e((string) $key) ?>" value="<?= $e((string) $value) ?>">
<?php endif; ?>
<?php endforeach; ?>
<input type="hidden" name="page" value="1">
<span>Wyswietlaj</span>
<select class="form-control js-per-page-select" name="per_page">
<?php foreach ($perPageOptions as $opt): ?>
<option value="<?= $e((string) $opt) ?>"<?= (int) $opt === $perPage ? ' selected' : '' ?>><?= $e((string) $opt) ?></option>
<?php endforeach; ?>
</select>
<span>rekordow</span>
</form>
</div>
</section>
<script>
(function() {
var root = document.querySelector('[data-table-list-id="<?= $e($listKey) ?>"]');
if (!root) return;
var basePath = root.getAttribute('data-table-list-base') || '';
var storagePrefix = 'tableList_' + (basePath || 'default') + '_<?= $e($listKey) ?>';
var filterKey = storagePrefix + '_filters_open';
var colsKey = storagePrefix + '_hidden_cols';
var queryKey = storagePrefix + '_query';
var clearKey = storagePrefix + '_cleared';
function readJson(key, fallback) {
try {
var val = localStorage.getItem(key);
if (!val) return fallback;
return JSON.parse(val);
} catch (e) {
return fallback;
}
}
function writeJson(key, val) {
try {
localStorage.setItem(key, JSON.stringify(val));
} catch (e) {}
}
var filterWrapper = root.querySelector('.js-table-filters-wrapper');
var filterBtn = root.querySelector('.js-filter-toggle-btn');
if (filterWrapper && filterBtn) {
var savedOpen = localStorage.getItem(filterKey) === '1';
if (savedOpen && !filterWrapper.classList.contains('is-open')) {
filterWrapper.classList.add('is-open');
filterBtn.classList.add('is-active');
}
filterBtn.addEventListener('click', function() {
var isOpen = filterWrapper.classList.toggle('is-open');
filterBtn.classList.toggle('is-active', isOpen);
try { localStorage.setItem(filterKey, isOpen ? '1' : '0'); } catch (e) {}
});
}
var dropdownBtn = root.querySelector('.js-col-toggle-btn');
var dropdown = root.querySelector('.js-col-toggle-dropdown');
if (dropdownBtn && dropdown) {
dropdownBtn.addEventListener('click', function(ev) {
ev.stopPropagation();
dropdown.classList.toggle('is-open');
});
dropdown.addEventListener('click', function(ev) { ev.stopPropagation(); });
document.addEventListener('click', function() { dropdown.classList.remove('is-open'); });
}
function applyHiddenCols(hiddenCols) {
var allCells = root.querySelectorAll('th[data-col-key], td[data-col-key]');
allCells.forEach(function(cell) {
var key = cell.getAttribute('data-col-key');
cell.classList.toggle('table-col-hidden', hiddenCols.indexOf(key) !== -1);
});
var checkboxes = root.querySelectorAll('.js-col-toggle-checkbox');
checkboxes.forEach(function(cb) {
var key = cb.getAttribute('data-col-key');
cb.checked = hiddenCols.indexOf(key) === -1;
});
}
var hiddenCols = readJson(colsKey, []);
if (!Array.isArray(hiddenCols)) hiddenCols = [];
applyHiddenCols(hiddenCols);
root.querySelectorAll('.js-col-toggle-checkbox').forEach(function(cb) {
cb.addEventListener('change', function() {
var key = cb.getAttribute('data-col-key');
hiddenCols = readJson(colsKey, []);
if (!Array.isArray(hiddenCols)) hiddenCols = [];
if (cb.checked) {
hiddenCols = hiddenCols.filter(function(v) { return v !== key; });
} else if (hiddenCols.indexOf(key) === -1) {
hiddenCols.push(key);
}
writeJson(colsKey, hiddenCols);
applyHiddenCols(hiddenCols);
});
});
var resetBtn = root.querySelector('.js-col-toggle-reset');
if (resetBtn) {
resetBtn.addEventListener('click', function() {
hiddenCols = [];
writeJson(colsKey, hiddenCols);
applyHiddenCols(hiddenCols);
});
}
root.querySelectorAll('.js-per-page-select').forEach(function(select) {
select.addEventListener('change', function() {
var form = select.closest('form');
if (form) form.submit();
});
});
var selectAll = root.querySelector('.js-table-select-all');
var selectItems = Array.prototype.slice.call(root.querySelectorAll('.js-table-select-item'));
function syncSelectAllState() {
if (!selectAll) return;
if (selectItems.length === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
return;
}
var checkedCount = selectItems.filter(function(input) { return input.checked; }).length;
selectAll.checked = checkedCount === selectItems.length;
selectAll.indeterminate = checkedCount > 0 && checkedCount < selectItems.length;
}
if (selectAll) {
selectAll.addEventListener('change', function() {
selectItems.forEach(function(input) {
input.checked = selectAll.checked;
});
syncSelectAllState();
});
}
selectItems.forEach(function(input) {
input.addEventListener('change', syncSelectAllState);
});
syncSelectAllState();
var clearBtn = root.querySelector('.js-table-filters-clear');
if (clearBtn) {
clearBtn.addEventListener('click', function() {
try {
localStorage.removeItem(queryKey);
sessionStorage.setItem(clearKey, '1');
} catch (e) {}
});
}
root.addEventListener('submit', function(event) {
var form = event.target;
if (!form || !form.matches || !form.matches('form[data-alert-confirm]')) return;
if (form.getAttribute('data-confirmed') === '1') {
form.removeAttribute('data-confirmed');
return;
}
var message = form.getAttribute('data-alert-confirm') || '';
if (message === '') return;
event.preventDefault();
var title = form.getAttribute('data-alert-confirm-title') || 'Potwierdzenie';
var confirmYes = form.getAttribute('data-alert-confirm-yes') || 'Potwierdz';
var confirmNo = form.getAttribute('data-alert-confirm-no') || 'Anuluj';
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
window.OrderProAlerts.confirm({
title: title,
message: message,
confirmLabel: confirmYes,
cancelLabel: confirmNo,
danger: true
}).then(function(accepted) {
if (!accepted) return;
form.setAttribute('data-confirmed', '1');
form.submit();
});
return;
}
if (window.confirm(message)) {
form.setAttribute('data-confirmed', '1');
form.submit();
}
});
try {
var query = window.location.search ? window.location.search.substring(1) : '';
var justCleared = sessionStorage.getItem(clearKey) === '1';
sessionStorage.removeItem(clearKey);
if (query) {
localStorage.setItem(queryKey, query);
} else if (!justCleared) {
var saved = localStorage.getItem(queryKey);
if (saved) window.location.replace(basePath + '?' + saved);
}
} catch (e) {}
})();
</script>