This commit is contained in:
2026-03-28 11:33:56 +01:00
parent 412e235512
commit aa8e21862d
132 changed files with 24381 additions and 10882 deletions

View File

@@ -0,0 +1,282 @@
<?php
/**
* Uniwersalny szablon formularza edycji
*
* @var FormEditViewModel $form
*/
use admin\Support\Forms\FormFieldRenderer;
use admin\ViewModels\Forms\FormFieldType;
$form = $this->form;
$renderer = new FormFieldRenderer($form);
// Przygotuj filemanager key
\Shared\Helpers\Helpers::set_session('admin', true);
if (
empty($_SESSION['rfm_akey']) ||
(($_SESSION['rfm_akey_expires'] ?? 0) < time())
) {
$_SESSION['rfm_akey'] = bin2hex(random_bytes(16));
}
$_SESSION['rfm_akey_expires'] = time() + 20 * 60;
$_SESSION['can_use_rfm'] = true;
?>
<script type="text/javascript" src="/libraries/ckeditor/ckeditor.js"></script>
<script type="text/javascript" src="/libraries/ckeditor/adapters/jquery.js"></script>
<!-- iCheck -->
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/minimal.css" />
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/blue.css" />
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.min.js"></script>
<style>
.icheckbox_minimal-blue { margin-top: 10px; }
</style>
<div class="row">
<div class="col col-xs-12">
<div class="g-container" data="table:<?= $form->formId ?>">
<div class="panel panel-info panel-border top">
<div class="panel-heading">
<span class="panel-title"><?= htmlspecialchars($form->title) ?></span>
</div>
<div class="panel-heading p10 pl15" id="g-menu" style="height: auto;">
<?php foreach ($form->actions as $action): ?>
<?php if ($action->name === 'save'): ?>
<?php if ($form->persist): ?>
<a href="#" id="g-edit-save-close" class="btn btn-system btn-sm"
persist_edit="0"
back_url="<?= htmlspecialchars($action->backUrl ?? '') ?>"
url="<?= htmlspecialchars($action->url) ?>">
<i class="fa fa-check-circle mr5"></i>Zatwierdź i zamknij
</a>
<?php endif; ?>
<a href="#" id="g-edit-save" class="btn btn-success btn-sm"
persist_edit="<?= $form->persist ? '1' : '0' ?>"
back_url="<?= htmlspecialchars($action->backUrl ?? '') ?>"
url="<?= htmlspecialchars($action->url) ?>">
<i class="fa fa-check-circle mr5"></i>Zatwierdź
</a>
<?php elseif ($action->name === 'cancel'): ?>
<a href="<?= htmlspecialchars($action->url) ?>" class="btn btn-dark btn-sm" id="g-edit-cancel">
<i class="fa fa-reply mr5"></i>Wstecz
</a>
<?php elseif ($action->name === 'preview'): ?>
<a href="<?= htmlspecialchars($action->url) ?>" class="btn btn-info btn-sm" target="_blank">
<i class="fa fa-eye mr5"></i><?= htmlspecialchars($action->label) ?>
</a>
<?php else: ?>
<a href="<?= htmlspecialchars($action->url) ?>" class="btn <?= htmlspecialchars($action->cssClass) ?> btn-sm">
<?= htmlspecialchars($action->label) ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="panel-body">
<form method="<?= $form->method ?>" id="fg-<?= $form->formId ?>" class="g-form form-horizontal"
action="<?= htmlspecialchars($form->action) ?>" enctype="multipart/form-data">
<input type="hidden" name="_form_id" value="<?= htmlspecialchars($form->formId) ?>">
<?php foreach ($form->hiddenFields as $name => $value): ?>
<input type="hidden" name="<?= htmlspecialchars($name) ?>" value="<?= htmlspecialchars($value ?? '') ?>">
<?php endforeach; ?>
<?php if ($form->hasTabs()): ?>
<!-- Formularz z zakładkami -->
<div id="form-tabs-<?= $form->formId ?>">
<ul class="resp-tabs-list form-tabs">
<?php foreach ($form->tabs as $tab): ?>
<li><i class="fa <?= htmlspecialchars($tab->icon) ?>"></i><?= htmlspecialchars($tab->label) ?></li>
<?php endforeach; ?>
</ul>
<div class="resp-tabs-container form-tabs">
<?php foreach ($form->tabs as $tab): ?>
<div>
<?php
$tabFields = $form->getFieldsForTab($tab->id);
$langSections = $form->getLangSectionsForTab($tab->id);
?>
<?php foreach ($tabFields as $field): ?>
<?= $renderer->renderField($field) ?>
<?php endforeach; ?>
<?php foreach ($langSections as $section): ?>
<?= $renderer->renderLangSection($section) ?>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<!-- Formularz bez zakładek -->
<?php foreach ($form->fields as $field): ?>
<?php if ($field->type === FormFieldType::LANG_SECTION): ?>
<?= $renderer->renderLangSection($field) ?>
<?php else: ?>
<?= $renderer->renderField($field) ?>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
</form>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$(function() {
// Inicjalizacja datepickerów
$('input.date').datetimepicker({
format: "YYYY-MM-DD",
pickTime: false
});
$('input.datetime').datetimepicker({
format: "YYYY-MM-DD HH:mm"
});
// Inicjalizacja zakładek
<?php if ($form->hasTabs()): ?>
$('#form-tabs-<?= $form->formId ?>').easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'form-tabs',
type: 'vertical'
});
<?php endif; ?>
// Inicjalizacja iCheck
$('.icheck').iCheck({
checkboxClass: 'icheckbox_minimal-blue',
radioClass: 'iradio_minimal-blue'
});
function showFormMessage(type, text) {
var safeText = $('<div/>').text(text || '').html();
var alertClass = type === 'error' ? 'alert-danger' : 'alert-primary';
var html = '' +
'<div class="row js-form-message">' +
'<div class="col col-xs-12">' +
'<div class="alert ' + alertClass + ' alert-dismissable">' +
'<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' +
'<i class="fa fa-info pr10"></i>' + safeText +
'</div>' +
'</div>' +
'</div>';
$('#content .js-form-message').remove();
$('#content').prepend(html);
}
// Obsługa przycisków zapisu
$('#g-edit-save, #g-edit-save-close').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var url = $btn.attr('url');
var backUrl = $btn.attr('back_url');
var persist = $btn.attr('persist_edit');
var formId = 'fg-<?= $form->formId ?>';
// Synchronizuj zawartosc CKEditor do textarea przed serializacja
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances) {
for (var instanceName in CKEDITOR.instances) {
if (Object.prototype.hasOwnProperty.call(CKEDITOR.instances, instanceName)) {
CKEDITOR.instances[instanceName].updateElement();
}
}
}
// Zbierz dane formularza
var formData = $('#' + formId).serialize();
$.ajax({
url: url,
type: 'POST',
data: formData,
dataType: 'json',
success: function(response) {
if (response.success) {
var successMessage = response.message || 'Zmiany zostały zapisane.';
if (backUrl && persist === '0') {
window.location.href = backUrl;
} else {
showFormMessage('success', successMessage);
}
} else {
var errors = response.errors ? Object.values(response.errors).join(', ') : 'Błąd walidacji';
showFormMessage('error', 'Błąd: ' + errors);
}
},
error: function(xhr, status, error) {
// Fallback - tradycyjne submit formularza
$('#' + formId).attr('action', url).submit();
}
});
});
disable_menu();
});
</script>
<?php
// Renderowanie CKEditor dla pól edytora (zwykłych)
foreach ($form->fields as $field):
if ($field->type === FormFieldType::EDITOR):
?>
<script type="text/javascript">
$(function() {
$('#<?= $field->id ?>').ckeditor({
toolbar: '<?= $field->editorToolbar ?>',
height: '<?= $field->editorHeight ?>'
});
});
</script>
<?php
endif;
endforeach;
?>
<?php if ($form->hasLangSections()): ?>
<script type="text/javascript">
$(function() {
// Inicjalizacja zakładek językowych
$('[id^="languages-"].languages-tabs').each(function() {
$(this).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'languages-tabs',
type: 'horizontal'
});
});
// Inicjalizacja CKEditor dla pól w sekcjach językowych
<?php
foreach ($form->fields as $section):
if ($section->type === FormFieldType::LANG_SECTION && $section->langFields):
foreach ($section->langFields as $langField):
if ($langField->type === FormFieldType::EDITOR && $form->languages):
foreach ($form->languages as $lang):
if ($lang['status']):
?>
$('#<?= $langField->getLocalizedId($lang['id']) ?>').ckeditor({
toolbar: '<?= $langField->editorToolbar ?>',
height: '<?= $langField->editorHeight ?>'
});
<?php
endif;
endforeach;
endif;
endforeach;
endif;
endforeach;
?>
});
</script>
<?php endif; ?>
<script>CKEDITOR.dtd.$removeEmpty['span'] = false;</script>

View File

@@ -0,0 +1,171 @@
<?php
/**
* Zastepnik gridEdit — generuje panel z przyciskami zapisu/anulowania i formularzem.
*
* Wymagane zmienne:
* $gridId — identyfikator formularza (np. 'layout-edit')
* $gridTitle — tytuł panelu
* $gridSaveUrl — URL zapisu (AJAX POST)
* $gridBackUrl — URL powrotu po zapisie / anulowaniu
* $gridHidden — tablica ukrytych pól [['name' => ..., 'value' => ...], ...]
* $gridContent — HTML zawartość formularza (z ob_get_clean())
* $gridPersist — (bool) czy zostać na stronie po zapisie
*/
$gridPersist = !empty($gridPersist);
?>
<!-- iCheck -->
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/minimal.css" />
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/blue.css" />
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.min.js"></script>
<!-- impromptu -->
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/impromptu/jquery-impromptu.css" />
<script type="text/javascript" src="/libraries/grid/plugins/impromptu/jquery-impromptu.js"></script>
<div class="row">
<div class="col col-xs-12">
<div class="g-container" data="table:<?= htmlspecialchars($gridId) ?>">
<div class="panel panel-info panel-border top">
<div class="panel-heading">
<span class="panel-title"><?= htmlspecialchars($gridTitle) ?></span>
</div>
<?php if (!empty($gridSaveUrl)): ?>
<div class="panel-heading p10 pl15" id="g-menu" style="height: auto;">
<?php if ($gridPersist): ?>
<a href="#" id="g-edit-save-close" class="btn btn-system btn-sm"
persist_edit="0"
back_url="<?= htmlspecialchars($gridBackUrl) ?>"
url="<?= htmlspecialchars($gridSaveUrl) ?>">
<i class="fa fa-check-circle mr5"></i>Zatwierdź i zamknij
</a>
<?php endif; ?>
<a href="#" id="g-edit-save" class="btn btn-success btn-sm"
persist_edit="<?= $gridPersist ? '1' : '0' ?>"
id_param="id"
back_url="<?= htmlspecialchars($gridBackUrl) ?>"
url="<?= htmlspecialchars($gridSaveUrl) ?>">
<i class="fa fa-check-circle mr5"></i>Zatwierdź
</a>
<a href="<?= htmlspecialchars($gridBackUrl) ?>" class="btn btn-dark btn-sm" id="g-edit-cancel">
<i class="fa fa-reply mr5"></i>Wstecz
</a>
</div>
<?php endif; ?>
<div class="panel-body">
<form method="POST" id="fg-<?= htmlspecialchars($gridId) ?>" class="g-form form-horizontal" enctype="multipart/form-data">
<?php if (!empty($gridHidden) && is_array($gridHidden)): ?>
<?php foreach ($gridHidden as $h): ?>
<input type="hidden" name="<?= htmlspecialchars($h['name']) ?>" id="<?= htmlspecialchars($h['name']) ?>" value="<?= htmlspecialchars($h['value'] ?? '') ?>" />
<?php endforeach; ?>
<?php endif; ?>
<?= $gridContent ?>
</form>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
jQuery(function() {
jQuery('.g-checkbox, .g-radio, .icheck').iCheck({
checkboxClass: 'icheckbox_minimal-blue',
radioClass: 'iradio_minimal-blue'
});
function showGridMessage(type, text) {
var safeText = $('<div/>').text(text || '').html();
var alertClass = type === 'error' ? 'alert-danger' : 'alert-primary';
var html = '<div class="row js-form-message"><div class="col col-xs-12"><div class="alert ' + alertClass + ' alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button><i class="fa fa-info pr10"></i>' + safeText + '</div></div></div>';
$('#content .js-form-message').remove();
$('#content').prepend(html);
}
$('#g-edit-save, #g-edit-save-close').on('click', function(e) {
e.preventDefault();
var $btn = $(this);
var url = $btn.attr('url');
var backUrl = $btn.attr('back_url');
var persist = $btn.attr('persist_edit');
var formId = 'fg-<?= $gridId ?>';
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances) {
for (var i in CKEDITOR.instances) {
if (CKEDITOR.instances.hasOwnProperty(i)) CKEDITOR.instances[i].updateElement();
}
}
// Serializacja kompatybilna z grid.js — JSON w parametrze 'values'
var values = $('#' + formId).serializeArray();
var formattedValues = {};
$.each(values, function(i, field) {
var fieldName = field.name.replace(/\[\]$/, '');
var nestedMatch = fieldName.match(/^(.+?)\[(.+?)\]$/);
if (nestedMatch) {
var mainField = nestedMatch[1];
var subField = nestedMatch[2];
if (!formattedValues[mainField]) {
formattedValues[mainField] = {};
}
formattedValues[mainField][subField] = field.value;
} else if (field.name.indexOf('[]', field.name.length - 2) !== -1) {
if (!formattedValues[fieldName]) {
formattedValues[fieldName] = [];
}
formattedValues[fieldName].push(field.value);
} else {
formattedValues[fieldName] = field.value;
}
});
// Normalizacja checkboxów tablicowych (boolean-like)
(function() {
var $form = $('#' + formId);
var groups = {};
$form.find('input[type="checkbox"][name$="[]"]').each(function() {
var n = this.name;
(groups[n] = groups[n] || []).push(this);
});
$.each(groups, function(nameWithBrackets, inputs) {
var vals = {};
$.each(inputs, function(_, el) { vals[(el.getAttribute('value') || '').toLowerCase()] = true; });
var boolSet = {'': true, '1': true, 'on': true, 'true': true, 'yes': true};
var isBool = true;
$.each(vals, function(v) { if (!boolSet[v]) isBool = false; });
if (isBool) {
var baseKey = nameWithBrackets.replace(/\[\]$/, '');
formattedValues[baseKey] = $.map(inputs, function(el) { return el.checked ? '1' : '0'; });
}
});
})();
$.ajax({
url: url,
type: 'POST',
data: {
gtable: formId.replace('fg-', ''),
values: JSON.stringify(formattedValues),
a: 'gsave'
},
dataType: 'json',
success: function(r) {
if (r.status === 'ok' || r.success) {
if (backUrl && persist === '0') {
window.location.href = backUrl;
} else {
showGridMessage('success', r.msg || r.message || 'Zmiany zostały zapisane.');
if (r.id) {
$('#fg-' + formId.replace('fg-','') + ' input[name="id"]').val(r.id);
}
}
} else {
showGridMessage('error', r.msg || r.message || 'Wystąpił błąd.');
}
},
error: function() {
showGridMessage('error', 'Wystąpił błąd połączenia.');
}
});
});
});
</script>

View File

@@ -0,0 +1,600 @@
<?php
$list = $this->list;
$buildUrl = function(array $params = []) use ($list): string {
$query = array_merge($list->query, $params);
foreach ($query as $key => $value) {
if ($value === '' || $value === null) {
unset($query[$key]);
}
}
$qs = http_build_query($query);
return $list->basePath . $qs;
};
$currentSort = $list->sort['column'] ?? '';
$currentDir = strtoupper($list->sort['dir'] ?? 'DESC');
$page = max(1, (int)($list->pagination['page'] ?? 1));
$totalPages = max(1, (int)($list->pagination['total_pages'] ?? 1));
$total = (int)($list->pagination['total'] ?? 0);
$perPage = (int)($list->pagination['per_page'] ?? 15);
$hasActiveFilters = false;
foreach ($list->filters as $filter) {
if (isset($filter['value']) && (string)$filter['value'] !== '') {
$hasActiveFilters = true;
break;
}
}
$isCompactColumn = function(array $column): bool {
$key = strtolower(trim((string)($column['key'] ?? '')));
$label = strtolower(trim((string)($column['label'] ?? '')));
if (in_array($key, ['status', 'active', 'enabled', 'is_active', 'start', 'default'], true)) {
return true;
}
if (in_array($label, ['status', 'aktywny', 'aktywnosc', 'active', 'domyslny', 'domyślny', 'default'], true)) {
return true;
}
return false;
};
?>
<div class="panel">
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<?php if (!empty($list->createUrl) && !empty($list->createLabel)): ?>
<a href="<?= htmlspecialchars($list->createUrl, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-success btn-sm">
<i class="fa fa-plus-circle mr5"></i><?= htmlspecialchars($list->createLabel, ENT_QUOTES, 'UTF-8'); ?>
</a>
<?php endif; ?>
</div>
<div class="col-sm-4 text-right">
<div class="table-list-header-actions">
<span class="text-muted">Wyników: <?= $total; ?></span>
<?php if (!empty($list->filters)): ?>
<button type="button" class="btn btn-default btn-sm js-filter-toggle-btn<?= $hasActiveFilters ? ' active' : ''; ?>" title="Filtry">
<i class="fa fa-filter"></i>
<?php if ($hasActiveFilters): ?>
<span class="badge badge-primary table-filter-badge"><?= count(array_filter($list->filters, function($f) { return isset($f['value']) && (string)$f['value'] !== ''; })); ?></span>
<?php endif; ?>
</button>
<?php endif; ?>
<div class="table-col-toggle-wrapper">
<button type="button" class="btn btn-default btn-sm js-col-toggle-btn" title="Widoczność kolumn">
<i class="fa fa-columns"></i>
</button>
<div class="table-col-toggle-dropdown js-col-toggle-dropdown">
<div class="table-col-toggle-header">Widoczność kolumn</div>
<?php foreach ($list->columns as $colIndex => $column): ?>
<?php $colKey = (string)($column['key'] ?? 'col_' . $colIndex); ?>
<label class="table-col-toggle-item">
<span class="table-col-switch">
<input type="checkbox" class="js-col-toggle-checkbox" data-col-key="<?= htmlspecialchars($colKey, ENT_QUOTES, 'UTF-8'); ?>" checked />
<span class="table-col-switch-slider"></span>
</span>
<?= htmlspecialchars((string)($column['label'] ?? $colKey), ENT_QUOTES, 'UTF-8'); ?>
</label>
<?php endforeach; ?>
<div class="table-col-toggle-footer">
<button type="button" class="btn btn-default btn-xs js-col-toggle-reset">Pokaż wszystkie</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="panel-body">
<div class="js-table-filters-wrapper table-filters-wrapper<?= $hasActiveFilters ? ' open' : ''; ?>">
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" data-path-submit="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="row mb15 js-table-filters-form">
<?php foreach ($list->filters as $filter): ?>
<?php
$filterKey = (string)($filter['key'] ?? '');
$inputId = 'filter_' . preg_replace('/[^a-zA-Z0-9_]+/', '_', $filterKey);
$filterType = (string)($filter['type'] ?? '');
$isCompactFilter = false;
if ($filterType === 'select') {
$options = (array)($filter['options'] ?? []);
$maxOptionLen = 0;
foreach ($options as $optionLabel) {
$len = strlen(trim((string)$optionLabel));
if ($len > $maxOptionLen) {
$maxOptionLen = $len;
}
}
// Krotkie selekty (np. tak/nie) nie musza zajmowac szerokiej kolumny.
$isCompactFilter = count($options) <= 5 && $maxOptionLen <= 12;
}
$filterColClass = $isCompactFilter ? 'col-sm-1 col-xs-6 mb10' : 'col-sm-2 mb10';
?>
<div class="<?= htmlspecialchars($filterColClass, ENT_QUOTES, 'UTF-8'); ?>">
<label for="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>" class="control-label">
<?= htmlspecialchars((string)($filter['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
</label>
<?php if ($filterType === 'select'): ?>
<select
id="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>"
name="<?= htmlspecialchars($filter['key'], ENT_QUOTES, 'UTF-8'); ?>"
class="form-control input-sm<?= $isCompactFilter ? ' js-filter-compact-select' : ''; ?>"
<?= $isCompactFilter ? 'data-compact-filter="1"' : ''; ?>
title="<?= htmlspecialchars($filter['label'], ENT_QUOTES, 'UTF-8'); ?>"
>
<?php foreach (($filter['options'] ?? []) as $value => $label): ?>
<option value="<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>"<?= ((string)($filter['value'] ?? '') === (string)$value) ? ' selected="selected"' : ''; ?>>
<?= htmlspecialchars((string)$label, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<?php elseif (($filter['type'] ?? '') === 'date'): ?>
<input
type="date"
id="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>"
name="<?= htmlspecialchars($filter['key'], ENT_QUOTES, 'UTF-8'); ?>"
value="<?= htmlspecialchars((string)($filter['value'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"
class="form-control input-sm"
title="<?= htmlspecialchars($filter['label'], ENT_QUOTES, 'UTF-8'); ?>"
/>
<?php else: ?>
<input
type="text"
id="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>"
name="<?= htmlspecialchars($filter['key'], ENT_QUOTES, 'UTF-8'); ?>"
value="<?= htmlspecialchars((string)($filter['value'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"
class="form-control input-sm"
placeholder="<?= htmlspecialchars($filter['label'], ENT_QUOTES, 'UTF-8'); ?>"
title="<?= htmlspecialchars($filter['label'], ENT_QUOTES, 'UTF-8'); ?>"
/>
<?php endif; ?>
</div>
<?php endforeach; ?>
<input type="hidden" name="sort" value="<?= htmlspecialchars((string)$currentSort, ENT_QUOTES, 'UTF-8'); ?>" />
<input type="hidden" name="dir" value="<?= htmlspecialchars((string)$currentDir, ENT_QUOTES, 'UTF-8'); ?>" />
<input type="hidden" name="per_page" value="<?= $perPage; ?>" />
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm">Szukaj</button>
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm js-table-filters-clear">Wyczyść</a>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover table-striped table-bordered mbn table-list-table">
<thead>
<tr>
<?php foreach ($list->columns as $colIndex => $column): ?>
<?php
$colKey = (string)($column['key'] ?? 'col_' . $colIndex);
$sortKey = (string)($column['sort_key'] ?? ($column['key'] ?? ''));
$isAllowedSortKey = empty($list->sortableColumns) || in_array($sortKey, $list->sortableColumns, true);
$isSortable = !empty($column['sortable']) && $sortKey !== '' && $isAllowedSortKey;
$isCurrent = $isSortable && $currentSort === $sortKey;
$nextDir = ($isCurrent && $currentDir === 'ASC') ? 'DESC' : 'ASC';
$sortUrl = $buildUrl([
'sort' => $sortKey,
'dir' => $nextDir,
'page' => 1,
]);
$headerClass = trim((string)($column['class'] ?? ''));
if ($isCompactColumn($column)) {
$headerClass = trim($headerClass . ' table-col-compact');
}
?>
<th class="<?= htmlspecialchars($headerClass, ENT_QUOTES, 'UTF-8'); ?>" data-col-key="<?= htmlspecialchars($colKey, ENT_QUOTES, 'UTF-8'); ?>">
<?php if ($isSortable): ?>
<a href="<?= htmlspecialchars($sortUrl, ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars((string)($column['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
<?php if ($isCurrent): ?>
<span aria-hidden="true"><?= $currentDir === 'ASC' ? '↑' : '↓'; ?></span>
<?php else: ?>
<span class="text-muted" aria-hidden="true">↕</span>
<?php endif; ?>
</a>
<?php else: ?>
<?= htmlspecialchars((string)($column['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
<?php endif; ?>
</th>
<?php endforeach; ?>
<th class="text-center" style="width: 160px;">Akcje</th>
</tr>
</thead>
<tbody>
<?php if (is_array($list->rows) && !empty($list->rows)): ?>
<?php foreach ($list->rows as $row): ?>
<tr>
<?php foreach ($list->columns as $colIndex => $column): ?>
<?php
$key = $column['key'] ?? '';
$colKey = (string)($column['key'] ?? 'col_' . $colIndex);
$raw = !empty($column['raw']);
$value = $row[$key] ?? '';
$cellClass = trim((string)($column['class'] ?? ''));
if ($isCompactColumn($column)) {
$cellClass = trim($cellClass . ' table-col-compact');
}
?>
<td class="<?= htmlspecialchars($cellClass, ENT_QUOTES, 'UTF-8'); ?>" data-col-key="<?= htmlspecialchars($colKey, ENT_QUOTES, 'UTF-8'); ?>">
<?php if ($raw): ?>
<?= (string)$value; ?>
<?php else: ?>
<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>
<?php endif; ?>
</td>
<?php endforeach; ?>
<td class="text-center">
<?php foreach (($row['_actions'] ?? []) as $action): ?>
<?php
$confirmMessage = (string)($action['confirm'] ?? '');
$needsConfirm = $confirmMessage !== '';
$actionClass = (string)($action['class'] ?? 'btn btn-default btn-xs');
if ($needsConfirm) {
$actionClass .= ' js-table-action-confirm';
}
?>
<a
href="<?= htmlspecialchars((string)($action['url'] ?? '#'), ENT_QUOTES, 'UTF-8'); ?>"
class="<?= htmlspecialchars($actionClass, ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($needsConfirm): ?>
data-confirm-title="Potwierdzenie"
data-confirm-message="<?= htmlspecialchars($confirmMessage, ENT_QUOTES, 'UTF-8'); ?>"
data-confirm-ok="<?= htmlspecialchars((string)($action['confirm_ok'] ?? 'Usun'), ENT_QUOTES, 'UTF-8'); ?>"
data-confirm-cancel="<?= htmlspecialchars((string)($action['confirm_cancel'] ?? 'Anuluj'), ENT_QUOTES, 'UTF-8'); ?>"
<?php endif; ?>
>
<?= htmlspecialchars((string)($action['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
</a>
<?php endforeach; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="<?= count($list->columns) + 1; ?>">
<div class="alert alert-danger mbn"><?= htmlspecialchars((string)$list->emptyMessage, ENT_QUOTES, 'UTF-8'); ?></div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="row mt15">
<div class="col-sm-6">
<ul class="pagination">
<?php $prevPage = max(1, $page - 1); ?>
<?php $nextPage = min($totalPages, $page + 1); ?>
<li class="<?= $page <= 1 ? 'disabled' : ''; ?>">
<a href="<?= htmlspecialchars($buildUrl(['page' => 1]), ENT_QUOTES, 'UTF-8'); ?>"><i class="fa fa-fast-backward"></i></a>
</li>
<li class="<?= $page <= 1 ? 'disabled' : ''; ?>">
<a href="<?= htmlspecialchars($buildUrl(['page' => $prevPage]), ENT_QUOTES, 'UTF-8'); ?>"><i class="fa fa-backward"></i></a>
</li>
<?php for ($i = max(1, $page - 3); $i <= min($totalPages, $page + 3); $i++): ?>
<li class="<?= $i === $page ? 'active' : ''; ?>">
<a href="<?= htmlspecialchars($buildUrl(['page' => $i]), ENT_QUOTES, 'UTF-8'); ?>"><?= $i; ?></a>
</li>
<?php endfor; ?>
<li class="<?= $page >= $totalPages ? 'disabled' : ''; ?>">
<a href="<?= htmlspecialchars($buildUrl(['page' => $nextPage]), ENT_QUOTES, 'UTF-8'); ?>"><i class="fa fa-forward"></i></a>
</li>
<li class="<?= $page >= $totalPages ? 'disabled' : ''; ?>">
<a href="<?= htmlspecialchars($buildUrl(['page' => $totalPages]), ENT_QUOTES, 'UTF-8'); ?>"><i class="fa fa-fast-forward"></i></a>
</li>
</ul>
</div>
<div class="col-sm-6 text-right">
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" data-path-submit="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline table-list-per-page-form">
<?php foreach ($list->query as $key => $value): ?>
<?php if ($key !== 'per_page' && $key !== 'page'): ?>
<input type="hidden" name="<?= htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8'); ?>" value="<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>" />
<?php endif; ?>
<?php endforeach; ?>
<input type="hidden" name="page" value="1" />
Wyświetlaj
<select name="per_page" class="form-control input-sm js-per-page-select">
<?php foreach ($list->perPageOptions as $opt): ?>
<option value="<?= (int)$opt; ?>"<?= ((int)$opt === $perPage) ? ' selected="selected"' : ''; ?>><?= (int)$opt; ?></option>
<?php endforeach; ?>
</select>
rekordów
</form>
</div>
</div>
</div>
</div>
<script type="text/javascript">
// Table state persistence — redirect ASAP to saved view
(function() {
var basePath = <?= json_encode($list->basePath); ?>;
var stateKey = 'tableListQuery_' + basePath;
var clearKey = 'tableListCleared_' + basePath;
var pathname = window.location.pathname.replace(/\/+$/, '/');
var bp = basePath.replace(/\/+$/, '/');
var queryPart = '';
if (pathname.length > bp.length && pathname.indexOf(bp) === 0) {
queryPart = pathname.substring(bp.length);
}
if (!queryPart && window.location.search) {
queryPart = window.location.search.substring(1);
}
try {
var justCleared = sessionStorage.getItem(clearKey) === '1';
sessionStorage.removeItem(clearKey);
if (queryPart) {
localStorage.setItem(stateKey, queryPart);
} else if (!justCleared) {
var saved = localStorage.getItem(stateKey);
if (saved) {
window.location.replace(basePath + saved);
}
}
} catch (e) {}
})();
</script>
<script type="text/javascript">
(function($) {
if (!$) {
return;
}
$(document).off('click.tableListConfirm', '.js-table-action-confirm');
$(document).on('click.tableListConfirm', '.js-table-action-confirm', function(e) {
e.preventDefault();
var $link = $(this);
var href = $link.attr('href');
var title = $link.data('confirmTitle') || 'Potwierdzenie';
var message = $link.data('confirmMessage') || 'Czy na pewno chcesz kontynuowac?';
var okLabel = $link.data('confirmOk') || 'Usun';
var cancelLabel = $link.data('confirmCancel') || 'Anuluj';
if (!href) {
return false;
}
if (typeof $.confirm === 'function') {
$.confirm({
title: String(title),
content: String(message),
type: 'red',
boxWidth: '560px',
useBootstrap: false,
animation: 'scale',
closeAnimation: 'scale',
backgroundDismissAnimation: 'shake',
container: 'body',
theme: 'modern',
columnClass: '',
typeAnimated: true,
lazyOpen: false,
draggable: false,
closeIcon: true,
containerFluid: true,
escapeKey: true,
backgroundDismiss: true,
animationBounce: 1.1,
watchInterval: 100,
offsetTop: 0,
offsetBottom: 0,
customClass: 'table-list-confirm-dialog',
buttons: {
cancel: {
text: String(cancelLabel),
btnClass: 'btn-default'
},
confirm: {
text: String(okLabel),
btnClass: 'btn-danger',
action: function() {
window.location.href = href;
}
}
}
});
return false;
}
if (window.confirm(String(message))) {
window.location.href = href;
}
return false;
});
$(document).off(
'change.tableListAutoFilter',
'.js-table-filters-form select, .js-table-filters-form input[type="date"], .js-table-filters-form input[type="text"]'
);
$(document).on(
'change.tableListAutoFilter',
'.js-table-filters-form select, .js-table-filters-form input[type="date"], .js-table-filters-form input[type="text"]',
function() {
var form = $(this).closest('form');
if (form.length) {
form.trigger('submit');
}
}
);
// --- Column visibility toggle ---
var storageKey = 'tableListCols_' + <?= json_encode($list->basePath); ?>;
function getHiddenCols() {
try {
var data = localStorage.getItem(storageKey);
if (data) {
var parsed = JSON.parse(data);
if (Array.isArray(parsed)) {
return parsed;
}
}
} catch (e) {}
return [];
}
function saveHiddenCols(hiddenArr) {
try {
localStorage.setItem(storageKey, JSON.stringify(hiddenArr));
} catch (e) {}
}
function applyColumnVisibility(hiddenCols) {
var $table = $('.table-list-table');
$table.find('th[data-col-key], td[data-col-key]').each(function() {
var key = $(this).attr('data-col-key');
if (hiddenCols.indexOf(key) !== -1) {
$(this).addClass('table-col-hidden');
} else {
$(this).removeClass('table-col-hidden');
}
});
$('.js-col-toggle-checkbox').each(function() {
var key = $(this).attr('data-col-key');
$(this).prop('checked', hiddenCols.indexOf(key) === -1);
});
}
// Apply saved state on load
var hiddenCols = getHiddenCols();
if (hiddenCols.length) {
applyColumnVisibility(hiddenCols);
}
// Toggle dropdown open/close
$(document).off('click.colToggleBtn', '.js-col-toggle-btn');
$(document).on('click.colToggleBtn', '.js-col-toggle-btn', function(e) {
e.stopPropagation();
var $dropdown = $(this).siblings('.js-col-toggle-dropdown');
$dropdown.toggleClass('open');
});
// Prevent closing when clicking inside dropdown
$(document).off('click.colToggleDropdown', '.js-col-toggle-dropdown');
$(document).on('click.colToggleDropdown', '.js-col-toggle-dropdown', function(e) {
e.stopPropagation();
});
// Close dropdown on outside click
$(document).off('click.colToggleClose');
$(document).on('click.colToggleClose', function() {
$('.js-col-toggle-dropdown').removeClass('open');
});
// Checkbox change — toggle column
$(document).off('change.colToggle', '.js-col-toggle-checkbox');
$(document).on('change.colToggle', '.js-col-toggle-checkbox', function() {
var key = $(this).attr('data-col-key');
var isChecked = $(this).is(':checked');
var current = getHiddenCols();
if (isChecked) {
current = $.grep(current, function(v) { return v !== key; });
} else {
if (current.indexOf(key) === -1) {
current.push(key);
}
}
saveHiddenCols(current);
applyColumnVisibility(current);
});
// Reset — show all columns
$(document).off('click.colToggleReset', '.js-col-toggle-reset');
$(document).on('click.colToggleReset', '.js-col-toggle-reset', function() {
saveHiddenCols([]);
applyColumnVisibility([]);
});
// --- Filter toggle ---
var filterStorageKey = 'tableListFilters_' + <?= json_encode($list->basePath); ?>;
function isFilterVisible() {
try {
return localStorage.getItem(filterStorageKey) === '1';
} catch (e) {}
return false;
}
function saveFilterState(visible) {
try {
localStorage.setItem(filterStorageKey, visible ? '1' : '0');
} catch (e) {}
}
var $filterWrapper = $('.js-table-filters-wrapper');
var $filterBtn = $('.js-filter-toggle-btn');
var hasActiveFilters = $filterWrapper.hasClass('open');
if (!hasActiveFilters && isFilterVisible()) {
$filterWrapper.addClass('open');
$filterBtn.addClass('active');
}
$(document).off('click.filterToggle', '.js-filter-toggle-btn');
$(document).on('click.filterToggle', '.js-filter-toggle-btn', function() {
var $wrapper = $('.js-table-filters-wrapper');
var $btn = $(this);
var isOpen = $wrapper.hasClass('open');
if (isOpen) {
$wrapper.removeClass('open');
$btn.removeClass('active');
saveFilterState(false);
} else {
$wrapper.addClass('open');
$btn.addClass('active');
saveFilterState(true);
}
});
// --- Path-based form submission (admin URL routing) ---
$(document).off('submit.tablePathSubmit', 'form[data-path-submit]');
$(document).on('submit.tablePathSubmit', 'form[data-path-submit]', function(e) {
e.preventDefault();
var basePath = $(this).attr('data-path-submit');
var data = $(this).serializeArray();
var parts = [];
for (var i = 0; i < data.length; i++) {
if (String(data[i].value) !== '') {
parts.push(encodeURIComponent(data[i].name) + '=' + encodeURIComponent(data[i].value));
}
}
window.location.href = basePath + (parts.length ? parts.join('&') : '');
});
// Per-page select auto-submit
$(document).off('change.tablePerPage', '.js-per-page-select');
$(document).on('change.tablePerPage', '.js-per-page-select', function() {
$(this).closest('form').trigger('submit');
});
// --- Table state clear on "Wyczyść" ---
var stateStorageKey = 'tableListQuery_' + <?= json_encode($list->basePath); ?>;
var stateClearKey = 'tableListCleared_' + <?= json_encode($list->basePath); ?>;
$(document).off('click.tableClearState', '.js-table-filters-clear');
$(document).on('click.tableClearState', '.js-table-filters-clear', function() {
try {
localStorage.removeItem(stateStorageKey);
sessionStorage.setItem(stateClearKey, '1');
} catch (e) {}
});
})(window.jQuery);
</script>