Compare commits

...

2 Commits

Author SHA1 Message Date
f2b2629d49 ver. 0.301: Collapsible table filters and mobile-responsive order details
- Table filters hidden by default with toggle button (icon + active count badge)
- Filter state persisted in localStorage; auto-show when filters active
- Order details mobile layout: icon-only action bar, full-width stacking,
  compact product list (image + name + qty x price = total), bottom-sheet
  dropdown for integrations menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:53:43 +01:00
b409806f02 ver. 0.300: Manifest-based update system with checksum verification and file backup
Replaces the manual ZIP packaging workflow with an automated build script.
UpdateRepository now supports both manifest JSON format (new) and legacy
_sql.txt/_files.txt format (fallback), enabling a smooth transition for
existing client instances.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:30:58 +01:00
24 changed files with 1276 additions and 227 deletions

41
.updateignore Normal file
View File

@@ -0,0 +1,41 @@
# Dokumentacja (tylko wewnetrzna/deweloperska)
*.md
docs/
CLAUDE.md
AGENTS.md
# Narzedzia deweloperskie
.claude/
.gitignore
.git/
tests/
phpunit.xml
phpunit.phar
composer.json
composer.lock
vendor/
test.ps1
memory/
# Infrastruktura aktualizacji (meta, nie runtime)
updates/changelog.php
updates/versions.php
updates/install.php
.updateignore
build-update.ps1
migrations/
# Pliki konfiguracyjne klienta (wdrazane osobno)
config.php
.htaccess
admin/.htaccess
libraries/version.ini
# Temp / cache / backups
temp/
backups/
cache/
cron/temp/
# IDE
.vscode/

View File

@@ -1,2 +0,0 @@
1. Operacje na kombinacjach nie czyszczą cache
2. Sprawdzić funkcję pod kątem SQL INJECTION

View File

@@ -0,0 +1,229 @@
/* ==========================================================================
Order Details — Mobile Responsive
Scoped to .od-actions and .order-details
All rules inside @media (max-width: 767px) — desktop layout untouched
========================================================================== */
@media (max-width: 767px) {
/* --- 1. Action buttons bar --- */
.od-actions {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 6px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 8px;
}
.od-actions .btn {
flex-shrink: 0;
font-size: 12px;
padding: 6px 10px;
margin-right: 0 !important;
margin-left: 0 !important;
}
/* Hide button labels on small screens — show only icons */
.od-actions .od-actions-label {
display: none;
}
/* Integrations dropdown — push to end, cancel float */
.od-actions .pull-right,
.od-actions .dropdown.pull-right {
float: none !important;
margin-left: auto;
}
/* Dropdown menu must not clip inside scroll container */
.od-actions .dropdown {
position: static;
}
.od-actions + .order-details .dropdown-menu,
#integrationsDropdownMenu.show {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
width: 100%;
max-width: 100%;
border-radius: 12px 12px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
z-index: 1060;
padding: 10px 0;
}
#integrationsDropdownMenu.show .dropdown-item {
padding: 12px 20px;
font-size: 14px;
}
/* --- 2. Full-width columns stacking --- */
.order-details .col-sm-6,
.order-details .col-lg-8,
.order-details .col-lg-4,
.order-details .col-xl-5,
.order-details .col-xl-7 {
width: 100%;
flex: 0 0 100%;
max-width: 100%;
}
/* Spacing between stacked sections */
.order-details .col-sm-6 {
margin-bottom: 15px;
}
/* "Resend confirmation" button — allow text to wrap */
.order-details .resend_order_confirmation_email .btn {
font-size: 12px;
white-space: normal;
text-align: left;
}
/* --- 3. Status section --- */
.order-details .status_select #order-status {
max-width: 100%;
width: 100%;
}
.order-details .buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.order-details .buttons .btn {
width: 100%;
white-space: normal;
font-size: 13px;
}
.order-details .order-status {
font-size: 16px;
}
.order-details .order-history {
margin-top: 10px;
}
/* --- 4. Products table — compact list --- */
.order-details table thead {
display: none;
}
.order-details table {
border: none;
}
.order-details table tbody {
display: block;
}
.order-details table tr.order-product-details {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid #eee;
gap: 0;
}
.order-details table tr.order-product-details:last-child {
border-bottom: none;
}
/* Image cell */
.order-details table td.product-image {
width: 60px;
height: 60px;
flex-shrink: 0;
border: none;
padding: 0;
margin-right: 10px;
float: none;
}
.order-details table td.product-image img {
width: 60px;
height: 60px;
object-fit: contain;
}
/* Name cell — takes remaining space next to image */
.order-details table tr.order-product-details td:nth-child(2) {
flex: 1 1 0;
min-width: 0;
border: none;
padding: 0;
white-space: normal;
}
.order-details table tr.order-product-details td:nth-child(2) a {
font-weight: 600;
font-size: 13px;
display: block;
word-break: break-word;
}
/* Hide quantity, price, discounted-price, total columns on mobile */
.order-details table tr.order-product-details td.tab-center,
.order-details table tr.order-product-details td.tab-right {
display: none;
}
/* Show mobile price summary line */
.od-mobile-price-line {
display: block !important;
margin-top: 4px;
font-size: 13px;
font-weight: 600;
color: #2a3042;
}
/* --- 5. Notes --- */
.order-details #notes {
font-size: 14px;
min-height: 120px;
}
/* --- 6. Order summary panel --- */
.order-details .panel .panel-body {
font-size: 13px;
}
/* --- 7. Paid status --- */
.order-details .paid-status .panel-body a {
display: flex;
align-items: center;
gap: 10px;
}
/* --- 8. Message section — hide empty spacer columns --- */
.order-details .d-none.d-lg-block {
display: none !important;
}
}
/* --- Desktop: action bar spacing (replaces mr5 class) --- */
.od-actions .btn + .btn,
.od-actions .btn + .dropdown {
margin-left: 5px;
}
/* Mobile price line — hidden on desktop */
.od-mobile-price-line {
display: none;
}

View File

@@ -57,6 +57,36 @@
width: 100% !important;
}
/* --- Filter toggle --- */
.table-filters-wrapper {
display: none;
overflow: hidden;
}
.table-filters-wrapper.open {
display: block;
}
.js-filter-toggle-btn.active {
background-color: #e8e8e8;
border-color: #adadad;
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
.table-filter-badge {
display: inline-block;
min-width: 16px;
padding: 2px 5px;
font-size: 10px;
font-weight: 700;
line-height: 1;
color: #fff;
background-color: #337ab7;
border-radius: 10px;
margin-left: 3px;
}
/* --- Column visibility toggle --- */
.table-list-header-actions {

View File

@@ -19,6 +19,14 @@ $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'] ?? '')));
@@ -48,6 +56,14 @@ $isCompactColumn = function(array $column): bool {
<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>
@@ -75,6 +91,7 @@ $isCompactColumn = function(array $column): bool {
</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'); ?>" class="row mb15 js-table-filters-form">
<?php foreach ($list->filters as $filter): ?>
<?php
@@ -148,6 +165,7 @@ $isCompactColumn = function(array $column): bool {
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm">Wyczyść</a>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover table-striped table-bordered mbn table-list-table">
@@ -469,5 +487,47 @@ $isCompactColumn = function(array $column): bool {
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);
}
});
})(window.jQuery);
</script>

View File

@@ -4,23 +4,23 @@ $orderId = (int)($this -> order['id'] ?? 0);
<div class="site-title">Szczegóły zamówienia: <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?></div>
<div class="mb15">
<a href="/admin/shop_order/list/" class="btn btn-dark btn-sm mr5">
<i class="fa fa-reply"></i> Wstecz
<div class="od-actions mb15">
<a href="/admin/shop_order/list/" class="btn btn-dark btn-sm">
<i class="fa fa-reply"></i> <span class="od-actions-label">Wstecz</span>
</a>
<a href="/admin/shop_order/order_edit/order_id=<?= $orderId;?>" class="btn btn-danger btn-sm mr5">
<i class="fa fa-pencil"></i> Edytuj zamówienie
<a href="/admin/shop_order/order_edit/order_id=<?= $orderId;?>" class="btn btn-danger btn-sm">
<i class="fa fa-pencil"></i> <span class="od-actions-label">Edytuj</span>
</a>
<? if ( $this -> prev_order_id ):?>
<a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> prev_order_id;?>" class="btn btn-success btn-sm mr5">
<i class="fa fa-arrow-left"></i> Poprzednie zamówienie
<a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> prev_order_id;?>" class="btn btn-success btn-sm">
<i class="fa fa-arrow-left"></i> <span class="od-actions-label">Poprzednie</span>
</a>
<? endif;?>
<? if ( $this -> next_order_id ):?>
<a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> next_order_id;?>" class="btn btn-success btn-sm mr5">
<i class="fa fa-arrow-right"></i> Następne zamówienie
<a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> next_order_id;?>" class="btn btn-success btn-sm">
<i class="fa fa-arrow-right"></i> <span class="od-actions-label">Następne</span>
</a>
<? endif;?>
@@ -183,6 +183,9 @@ $orderId = (int)($this -> order['id'] ?? 0);
<div class="product-message">
<?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?>
</div>
<div class="od-mobile-price-line">
<?= (int)$product['quantity'];?> &times; <?= \Shared\Helpers\Helpers::decimal( $product['price_brutto_promo'] );?> = <?= \Shared\Helpers\Helpers::decimal( $product['price_brutto_promo'] * $product['quantity'] );?> zł
</div>
</td>
<td class="tab-center"><?= $product[ 'quantity' ];?></td>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto' ] );?> zł</td>

View File

@@ -36,6 +36,7 @@
<script type="text/javascript" src="/admin/js/functions.js"></script>
<link rel="stylesheet" href="/admin/layout/style-css/style.css" />
<link rel="stylesheet" href="/admin/layout/style-css/table-list.css" />
<link rel="stylesheet" href="/admin/layout/style-css/order-details-mobile.css" />
</head>
<body>
<div class="admin-page">

View File

@@ -41,6 +41,17 @@
</div>
</div>
<? if ( !empty( $this->log ) ): ?>
<div class="panel">
<div class="panel-heading">
<span class="panel-title">Log ostatniej aktualizacji</span>
</div>
<div class="panel-body">
<pre style="max-height: 400px; overflow-y: auto; font-size: 12px;"><?= htmlspecialchars( $this->log ); ?></pre>
</div>
</div>
<? endif; ?>
<div class="panel">
<div class="panel-heading">
<span class="panel-title">Changelog</span>

View File

@@ -61,10 +61,284 @@ class UpdateRepository
return [ 'success' => true, 'log' => $log, 'no_updates' => true ];
}
/**
* Dispatcher — próbuje pobrać manifest, jeśli jest → nowa ścieżka, jeśli brak → legacy.
*/
private function downloadAndApply( string $ver, string $dir, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
$manifest = $this->downloadManifest( $baseUrl, $ver );
if ( $manifest !== null ) {
$log[] = '[INFO] Znaleziono manifest dla wersji ' . $ver;
return $this->downloadAndApplyWithManifest( $ver, $dir, $manifest, $log );
}
$log[] = '[INFO] Brak manifestu, używam trybu legacy';
return $this->downloadAndApplyLegacy( $ver, $dir, $log );
}
/**
* Pobiera manifest JSON dla danej wersji.
*
* @return array|null Zdekodowany manifest lub null jeśli brak
*/
private function downloadManifest( string $baseUrl, string $ver )
{
$manifestUrl = $baseUrl . '/ver_' . $ver . '_manifest.json';
$ch = curl_init( $manifestUrl );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_HEADER, false );
curl_setopt( $ch, CURLOPT_TIMEOUT, 15 );
$response = curl_exec( $ch );
$httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( !$response || $httpCode !== 200 ) {
return null;
}
$manifest = json_decode( $response, true );
if ( !is_array( $manifest ) || !isset( $manifest['version'] ) ) {
return null;
}
return $manifest;
}
/**
* Aktualizacja z użyciem manifestu — checksum, backup, SQL z manifestu, usuwanie z manifestu.
*/
private function downloadAndApplyWithManifest( string $ver, string $dir, array $manifest, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
$log[] = '[INFO] Tryb aktualizacji: manifest';
// 1. Pobieranie ZIP
$zipUrl = $baseUrl . '/ver_' . $ver . '.zip';
$log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl;
$file = @file_get_contents( $zipUrl );
if ( $file === false ) {
$log[] = '[ERROR] Nie udało się pobrać pliku ZIP';
return [ 'success' => false, 'log' => $log ];
}
$fileSize = strlen( $file );
$log[] = '[OK] Pobrano plik ZIP, rozmiar: ' . $fileSize . ' bajtów';
if ( $fileSize < 100 ) {
$log[] = '[ERROR] Plik ZIP jest za mały (prawdopodobnie błąd pobierania)';
return [ 'success' => false, 'log' => $log ];
}
$dlHandler = @fopen( 'update.zip', 'w' );
if ( !$dlHandler ) {
$log[] = '[ERROR] Nie udało się otworzyć pliku update.zip do zapisu';
return [ 'success' => false, 'log' => $log ];
}
$written = fwrite( $dlHandler, $file );
fclose( $dlHandler );
if ( $written === false || $written === 0 ) {
$log[] = '[ERROR] Nie udało się zapisać pliku ZIP';
return [ 'success' => false, 'log' => $log ];
}
$log[] = '[OK] Zapisano plik ZIP (' . $written . ' bajtów)';
// 2. Weryfikacja checksum
if ( isset( $manifest['checksum_zip'] ) ) {
$checksumResult = $this->verifyChecksum( 'update.zip', $manifest['checksum_zip'], $log );
$log = $checksumResult['log'];
if ( !$checksumResult['valid'] ) {
@unlink( 'update.zip' );
return [ 'success' => false, 'log' => $log ];
}
}
// 3. Backup plików przed nadpisaniem
$log = $this->createBackup( $manifest, $log );
// 4. SQL z manifestu
if ( !empty( $manifest['sql'] ) ) {
$log[] = '[INFO] Wykonywanie zapytań SQL z manifestu (' . count( $manifest['sql'] ) . ')';
$success = 0;
$errors = 0;
foreach ( $manifest['sql'] as $query ) {
$query = trim( $query );
if ( $query !== '' ) {
if ( $this->db->query( $query ) ) {
$success++;
} else {
$errors++;
$log[] = '[WARNING] Błąd SQL: ' . $query;
}
}
}
$log[] = '[INFO] Wykonano zapytania SQL - sukces: ' . $success . ', błędy: ' . $errors;
}
// 5. Usuwanie plików z manifestu
if ( !empty( $manifest['files']['deleted'] ) ) {
$deletedCount = 0;
foreach ( $manifest['files']['deleted'] as $relativePath ) {
$fullPath = '../' . $relativePath;
if ( file_exists( $fullPath ) ) {
if ( @unlink( $fullPath ) ) {
$deletedCount++;
} else {
$log[] = '[WARNING] Nie udało się usunąć pliku: ' . $fullPath;
}
}
}
$log[] = '[INFO] Usunięto plików: ' . $deletedCount;
}
// 6. Usuwanie katalogów z manifestu
if ( !empty( $manifest['directories_deleted'] ) ) {
$deletedDirs = 0;
foreach ( $manifest['directories_deleted'] as $dirPath ) {
$fullPath = '../' . $dirPath;
if ( is_dir( $fullPath ) ) {
\Shared\Helpers\Helpers::delete_dir( $fullPath );
$deletedDirs++;
}
}
$log[] = '[INFO] Usunięto katalogów: ' . $deletedDirs;
}
// 7. Rozpakowywanie ZIP
$log = $this->extractZip( 'update.zip', $log );
// 8. Aktualizacja wersji
$versionFile = '../libraries/version.ini';
$handle = @fopen( $versionFile, 'w' );
if ( !$handle ) {
$log[] = '[ERROR] Nie udało się otworzyć pliku version.ini do zapisu';
return [ 'success' => false, 'log' => $log ];
}
fwrite( $handle, $ver );
fclose( $handle );
$log[] = '[OK] Zaktualizowano plik version.ini do wersji: ' . $ver;
$log[] = '[SUCCESS] Aktualizacja do wersji ' . $ver . ' zakończona pomyślnie';
return [ 'success' => true, 'log' => $log ];
}
/**
* Weryfikuje sumę kontrolną pliku.
*
* @param string $filePath Ścieżka do pliku
* @param string $expectedChecksum Suma w formacie "sha256:abc123..."
* @param array $log Tablica logów
* @return array{valid: bool, log: array}
*/
private function verifyChecksum( string $filePath, string $expectedChecksum, array $log ): array
{
$parts = explode( ':', $expectedChecksum, 2 );
if ( count( $parts ) !== 2 ) {
$log[] = '[ERROR] Nieprawidłowy format sumy kontrolnej: ' . $expectedChecksum;
return [ 'valid' => false, 'log' => $log ];
}
$algorithm = $parts[0];
$expected = $parts[1];
$actual = @hash_file( $algorithm, $filePath );
if ( $actual === false ) {
$log[] = '[ERROR] Nie udało się obliczyć sumy kontrolnej pliku';
return [ 'valid' => false, 'log' => $log ];
}
if ( $actual !== $expected ) {
$log[] = '[ERROR] Suma kontrolna nie zgadza się! Oczekiwano: ' . $expected . ', otrzymano: ' . $actual;
return [ 'valid' => false, 'log' => $log ];
}
$log[] = '[OK] Suma kontrolna ZIP zgodna';
return [ 'valid' => true, 'log' => $log ];
}
/**
* Tworzy kopię zapasową plików przed aktualizacją.
*
* @param array $manifest Dane z manifestu
* @param array $log Tablica logów
* @return array Zaktualizowana tablica logów
*/
private function createBackup( array $manifest, array $log ): array
{
$version = isset( $manifest['version'] ) ? $manifest['version'] : 'unknown';
$backupDir = '../backups/' . str_replace( '.', '_', $version ) . '_' . date( 'Ymd_His' );
$log[] = '[INFO] Tworzenie kopii zapasowej w: ' . $backupDir;
$projectRoot = realpath( '../' );
if ( !$projectRoot ) {
$log[] = '[WARNING] Nie udało się określić katalogu projektu, pomijam backup';
return $log;
}
$filesToBackup = [];
if ( isset( $manifest['files']['modified'] ) && is_array( $manifest['files']['modified'] ) ) {
$filesToBackup = array_merge( $filesToBackup, $manifest['files']['modified'] );
}
if ( isset( $manifest['files']['deleted'] ) && is_array( $manifest['files']['deleted'] ) ) {
$filesToBackup = array_merge( $filesToBackup, $manifest['files']['deleted'] );
}
if ( empty( $filesToBackup ) ) {
$log[] = '[INFO] Brak plików do backupu';
return $log;
}
$backedUp = 0;
foreach ( $filesToBackup as $relativePath ) {
$sourcePath = $projectRoot . '/' . $relativePath;
if ( !file_exists( $sourcePath ) ) {
continue;
}
$targetPath = $backupDir . '/' . $relativePath;
$targetDir = dirname( $targetPath );
if ( !is_dir( $targetDir ) ) {
@mkdir( $targetDir, 0755, true );
}
if ( @copy( $sourcePath, $targetPath ) ) {
$backedUp++;
} else {
$log[] = '[WARNING] Nie udało się skopiować do backupu: ' . $relativePath;
}
}
$log[] = '[OK] Backup: skopiowano ' . $backedUp . ' plików';
@file_put_contents(
$backupDir . '/manifest.json',
json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE )
);
return $log;
}
/**
* Legacy — stary format aktualizacji (ZIP + _sql.txt + _files.txt).
*/
private function downloadAndApplyLegacy( string $ver, string $dir, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
// Pobieranie ZIP
$zipUrl = $baseUrl . '/ver_' . $ver . '.zip';
$log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl;

View File

@@ -14,9 +14,12 @@ class UpdateController
public function main_view(): string
{
$logContent = @file_get_contents( '../libraries/update_log.txt' );
return \Shared\Tpl\Tpl::view( 'update/main-view', [
'ver' => \Shared\Helpers\Helpers::get_version(),
'new_ver' => \Shared\Helpers\Helpers::get_new_version(),
'log' => $logContent ?: '',
] );
}

388
build-update.ps1 Normal file
View File

@@ -0,0 +1,388 @@
<#
.SYNOPSIS
Automatyczne budowanie paczki aktualizacji shopPRO na podstawie git diff miedzy tagami.
.DESCRIPTION
Skrypt porownuje dwa tagi git, filtruje pliki przez .updateignore,
tworzy ZIP + manifest JSON i aktualizuje changelog.php oraz versions.php.
.PARAMETER FromTag
Tag poczatkowy (np. v0.299). Domyslnie: ostatni tag.
.PARAMETER ToTag
Tag docelowy (np. v0.300). Wymagany.
.PARAMETER ChangelogEntry
Wpis do changelogu. Wymagany (chyba ze -DryRun).
.PARAMETER DryRun
Tylko pokaz co zostaloby zrobione, bez tworzenia plikow.
.EXAMPLE
./build-update.ps1 -ToTag v0.300 -ChangelogEntry "NEW - Manifest-based update system"
./build-update.ps1 -FromTag v0.299 -ToTag v0.300 -ChangelogEntry "NEW - opis" -DryRun
#>
param(
[string]$FromTag = "",
[Parameter(Mandatory=$true)]
[string]$ToTag,
[string]$ChangelogEntry = "",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
# --- Helpers ---
function Write-Step($msg) {
Write-Host " [*] $msg" -ForegroundColor Cyan
}
function Write-Ok($msg) {
Write-Host " [OK] $msg" -ForegroundColor Green
}
function Write-Warn($msg) {
Write-Host " [!] $msg" -ForegroundColor Yellow
}
function Write-Err($msg) {
Write-Host " [ERROR] $msg" -ForegroundColor Red
}
# --- 1. Walidacja tagow ---
Write-Host "`n=== shopPRO Build Update ===" -ForegroundColor White
if (-not $FromTag) {
$FromTag = (git describe --tags --abbrev=0 2>$null)
if (-not $FromTag) {
Write-Err "Nie znaleziono zadnego taga. Uzyj parametru -FromTag."
exit 1
}
Write-Step "Auto-detect FromTag: $FromTag"
}
# Sprawdz czy tagi istnieja
$tagExists = git tag -l $FromTag
if (-not $tagExists) {
Write-Err "Tag '$FromTag' nie istnieje."
exit 1
}
$toTagExists = git tag -l $ToTag
if (-not $toTagExists) {
Write-Warn "Tag '$ToTag' nie istnieje. Uzywam HEAD jako punktu docelowego."
$diffTarget = "HEAD"
} else {
$diffTarget = $ToTag
}
# --- 2. Wersja i katalog ---
$versionNumber = $ToTag -replace '^v', ''
$versionInt = [int]($versionNumber -replace '^0\.', '')
# Oblicz katalog: 0.001-0.009 -> 0.00, 0.010-0.099 -> 0.00, 0.100-0.199 -> 0.10, 0.200-0.299 -> 0.20, 0.300-0.399 -> 0.30
$dirTens = [math]::Floor($versionInt / 100)
$dirStr = "0.{0}0" -f ($dirTens * 10).ToString().PadLeft(1, '0')
# Format: jesli dirTens < 10 to "0.X0", np 0.00, 0.10, 0.20, 0.30
if ($dirTens -lt 10) {
$dirStr = "0.{0}0" -f $dirTens.ToString().PadLeft(1, '0')
} else {
$dirStr = "0.{0}0" -f $dirTens
}
Write-Step "Wersja: $versionNumber (int: $versionInt)"
Write-Step "Katalog: updates/$dirStr/"
# --- 3. Git diff ---
Write-Step "Porownywanie: $FromTag..$diffTarget"
$diffOutput = git diff --name-status "$FromTag..$diffTarget" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Err "git diff nie powiodl sie: $diffOutput"
exit 1
}
$addedFiles = @()
$modifiedFiles = @()
$deletedFiles = @()
$renamedFiles = @()
foreach ($line in ($diffOutput -split "`n")) {
$line = $line.Trim()
if (-not $line) { continue }
$parts = $line -split "`t"
$status = $parts[0]
$filePath = if ($parts.Count -gt 1) { $parts[1] } else { "" }
# Zamien backslash na slash
$filePath = $filePath -replace '\\', '/'
switch -Wildcard ($status) {
"A" { $addedFiles += $filePath }
"M" { $modifiedFiles += $filePath }
"D" { $deletedFiles += $filePath }
"R*" {
# Rename: stary plik = deleted, nowy = added
$newPath = if ($parts.Count -gt 2) { $parts[2] -replace '\\', '/' } else { "" }
$deletedFiles += $filePath
if ($newPath) { $addedFiles += $newPath }
}
}
}
Write-Step "Zmiany: A=$($addedFiles.Count), M=$($modifiedFiles.Count), D=$($deletedFiles.Count)"
# --- 4. Filtrowanie przez .updateignore ---
$ignorePatterns = @()
$ignoreFile = ".updateignore"
if (Test-Path $ignoreFile) {
$ignorePatterns = Get-Content $ignoreFile | ForEach-Object { $_.Trim() } | Where-Object { $_ -and ($_ -notmatch '^\s*#') }
Write-Step "Zaladowano $($ignorePatterns.Count) wzorcow z .updateignore"
}
function Test-Ignored {
param([string]$FilePath)
foreach ($pattern in $ignorePatterns) {
# Wzorzec katalogu (konczy sie na /)
if ($pattern.EndsWith('/')) {
$dirPattern = $pattern.TrimEnd('/')
if ($FilePath -like "$dirPattern/*" -or $FilePath -eq $dirPattern) {
return $true
}
}
# Wzorzec z wildcard
elseif ($pattern.Contains('*')) {
# *.md -> dopasuj nazwe pliku
if ($pattern.StartsWith('*')) {
$fileName = Split-Path $FilePath -Leaf
if ($fileName -like $pattern) { return $true }
}
# Zwykly glob
elseif ($FilePath -like $pattern) { return $true }
}
# Dokladne dopasowanie
else {
if ($FilePath -eq $pattern) { return $true }
}
}
return $false
}
$filteredAdded = @()
$filteredModified = @()
$filteredDeleted = @()
$ignoredCount = 0
foreach ($f in $addedFiles) {
if (Test-Ignored $f) { $ignoredCount++; continue }
$filteredAdded += $f
}
foreach ($f in $modifiedFiles) {
if (Test-Ignored $f) { $ignoredCount++; continue }
$filteredModified += $f
}
foreach ($f in $deletedFiles) {
if (Test-Ignored $f) { $ignoredCount++; continue }
$filteredDeleted += $f
}
Write-Step "Po filtrowaniu: A=$($filteredAdded.Count), M=$($filteredModified.Count), D=$($filteredDeleted.Count) (pominieto: $ignoredCount)"
# Rozdziel usuniete pliki i katalogi
$deletedDirs = @()
$deletedFilesOnly = @()
foreach ($f in $filteredDeleted) {
# Sprawdz czy to byl katalog (w git nie ma katalogow, ale mozemy sprawdzic po wzorcu)
# Git nie trackuje pustych katalogow, wiec deleted entries to zawsze pliki
$deletedFilesOnly += $f
}
# --- 5. Odczyt migracji SQL ---
$sqlQueries = @()
$migrationFile = "migrations/$versionNumber.sql"
if (Test-Path $migrationFile) {
$sqlQueries = Get-Content $migrationFile | Where-Object { $_.Trim() -ne '' }
Write-Step "Znaleziono migracje SQL: $migrationFile ($($sqlQueries.Count) zapytan)"
} else {
Write-Step "Brak migracji SQL ($migrationFile nie istnieje)"
}
# --- 6. Podsumowanie ---
$filesToPack = $filteredAdded + $filteredModified
if ($filesToPack.Count -eq 0 -and $filteredDeleted.Count -eq 0 -and $sqlQueries.Count -eq 0) {
Write-Warn "Brak zmian do pakowania (po filtrowaniu). Przerywam."
exit 0
}
Write-Host "`n--- Pliki do paczki ---" -ForegroundColor White
foreach ($f in $filteredAdded) { Write-Host " A $f" -ForegroundColor Green }
foreach ($f in $filteredModified) { Write-Host " M $f" -ForegroundColor Yellow }
foreach ($f in $filteredDeleted) { Write-Host " D $f" -ForegroundColor Red }
if ($sqlQueries.Count -gt 0) {
Write-Host "`n--- SQL ---" -ForegroundColor White
foreach ($q in $sqlQueries) { Write-Host " $q" -ForegroundColor Gray }
}
# --- DryRun? ---
if ($DryRun) {
Write-Host "`n[DRY RUN] Zadne pliki nie zostaly utworzone.`n" -ForegroundColor Magenta
exit 0
}
# --- 7. Walidacja ChangelogEntry ---
if (-not $ChangelogEntry) {
Write-Err "Parametr -ChangelogEntry jest wymagany (chyba ze uzywasz -DryRun)."
exit 1
}
# --- 8. Tworzenie temp i kopiowanie plikow ---
$tempDir = "temp/temp_$versionInt"
if (Test-Path $tempDir) {
Remove-Item -Recurse -Force $tempDir
}
foreach ($f in $filesToPack) {
$destPath = Join-Path $tempDir $f
$destDir = Split-Path $destPath -Parent
if (-not (Test-Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
if (Test-Path $f) {
Copy-Item $f $destPath -Force
} else {
Write-Warn "Plik nie istnieje (moze zostal usuniety po TAGU): $f"
}
}
Write-Ok "Skopiowano $($filesToPack.Count) plikow do $tempDir"
# --- 9. Tworzenie ZIP ---
$updatesDir = "updates/$dirStr"
if (-not (Test-Path $updatesDir)) {
New-Item -ItemType Directory -Path $updatesDir -Force | Out-Null
}
$zipPath = "$updatesDir/ver_$versionNumber.zip"
if (Test-Path $zipPath) {
Remove-Item $zipPath -Force
}
# Pakuj zawartosc temp dir (bez folderu temp/)
$originalLocation = Get-Location
Set-Location $tempDir
Compress-Archive -Path '*' -DestinationPath "../../$zipPath" -Force
Set-Location $originalLocation
Write-Ok "Utworzono ZIP: $zipPath"
# --- 10. Checksum SHA256 ---
$hash = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower()
Write-Ok "SHA256: $hash"
# --- 11. Manifest JSON ---
$manifest = @{
version = $versionNumber
date = (Get-Date -Format "yyyy-MM-dd")
checksum_zip = "sha256:$hash"
files = @{
modified = $filteredModified
added = $filteredAdded
deleted = $deletedFilesOnly
}
directories_deleted = $deletedDirs
sql = $sqlQueries
changelog = $ChangelogEntry
}
$manifestJson = $manifest | ConvertTo-Json -Depth 4
$manifestPath = "$updatesDir/ver_${versionNumber}_manifest.json"
$manifestJson | Out-File $manifestPath -Encoding UTF8
Write-Ok "Utworzono manifest: $manifestPath"
# --- 12. Legacy _sql.txt i _files.txt (okres przejsciowy) ---
if ($sqlQueries.Count -gt 0) {
$sqlPath = "$updatesDir/ver_${versionNumber}_sql.txt"
($sqlQueries -join "`n") | Out-File $sqlPath -Encoding UTF8 -NoNewline
Write-Ok "Utworzono legacy SQL: $sqlPath"
}
if ($deletedFilesOnly.Count -gt 0 -or $deletedDirs.Count -gt 0) {
$filesContent = @()
foreach ($f in $deletedFilesOnly) { $filesContent += "F: ../$f" }
foreach ($d in $deletedDirs) { $filesContent += "D: ../$d" }
$filesPath = "$updatesDir/ver_${versionNumber}_files.txt"
($filesContent -join "`n") | Out-File $filesPath -Encoding UTF8 -NoNewline
Write-Ok "Utworzono legacy files: $filesPath"
}
# --- 13. Aktualizacja versions.php ---
$versionsFile = "updates/versions.php"
if (Test-Path $versionsFile) {
$content = Get-Content $versionsFile -Raw
$content = $content -replace '\$current_ver\s*=\s*\d+;', "`$current_ver = $versionInt;"
$content | Out-File $versionsFile -Encoding UTF8 -NoNewline
Write-Ok "Zaktualizowano versions.php: `$current_ver = $versionInt"
}
# --- 14. Aktualizacja changelog.php ---
$changelogFile = "updates/changelog.php"
if (Test-Path $changelogFile) {
$dateStr = Get-Date -Format "dd.MM.yyyy"
$newEntry = "<b>ver. $versionNumber - $dateStr</b><br />`n$ChangelogEntry`n<hr>`n"
$changelogContent = Get-Content $changelogFile -Raw
$changelogContent = $newEntry + $changelogContent
$changelogContent | Out-File $changelogFile -Encoding UTF8 -NoNewline
Write-Ok "Zaktualizowano changelog.php"
}
# --- 15. Cleanup ---
if (Test-Path $tempDir) {
Remove-Item -Recurse -Force $tempDir
}
# Usun pusty folder temp jesli nie ma juz w nim nic
if ((Test-Path "temp") -and ((Get-ChildItem "temp" -Force).Count -eq 0)) {
Remove-Item "temp" -Force
}
Write-Ok "Wyczyszczono pliki tymczasowe"
# --- Podsumowanie ---
Write-Host "`n=== Gotowe ===" -ForegroundColor Green
Write-Host " ZIP: $zipPath"
Write-Host " Manifest: $manifestPath"
Write-Host " Wersja: $versionNumber (int: $versionInt)"
Write-Host ""

View File

@@ -4,6 +4,31 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
## ver. 0.301 (2026-02-22) - Mobile responsive: filtry tabel i szczegoly zamowienia
- **NEW**: Filtry w tabelach admina domyslnie ukryte — przycisk toggle z ikona filtra, badge z liczba aktywnych filtrow
- **NEW**: Stan filtrow zapamietywany w `localStorage` per widok; auto-show gdy filtry aktywne
- **NEW**: Mobilna wersja szczegolow zamowienia — responsywny layout (CSS-only, breakpoint 767px)
- **NEW**: Pasek akcji zamowienia — ikony-only na mobile, flex row bez zawijania
- **NEW**: Tabela produktow zamowienia — kompaktowa lista na mobile (obraz + nazwa + linia `qty x cena = suma`)
- **NEW**: Sekcje informacyjne zamowienia — full-width stacking na mobile
- **NEW**: Dropdown integracji jako bottom-sheet na mobile
---
## ver. 0.300 (2026-02-21) - System aktualizacji oparty na manifestach JSON
- **NEW**: Manifest JSON per wersja — zastępuje osobne pliki `_sql.txt` i `_files.txt`
- **NEW**: Weryfikacja checksum SHA256 pobranych paczek ZIP
- **NEW**: Automatyczny backup plików przed nadpisaniem (`backups/` directory)
- **NEW**: `build-update.ps1` — automatyczne budowanie paczek z `git diff` między tagami
- **NEW**: `.updateignore` — wzorce plików wykluczonych z paczek aktualizacji
- **NEW**: `migrations/` — folder na wersjonowane pliki SQL
- **NEW**: Panel "Log ostatniej aktualizacji" w widoku aktualizacji admina
- **UPDATE**: `UpdateRepository` — dual-mode dispatcher (manifest + legacy fallback)
---
## ver. 0.299 (2026-02-21) - Widoczność kolumn w tabelach
- **NEW**: Toggle widoczności kolumn w komponentach `table-list` — przycisk z ikoną kolumn, dropdown z toggle switchami

View File

@@ -23,10 +23,10 @@ composer test # standard
## Aktualny stan
```text
OK (687 tests, 1971 assertions)
OK (692 tests, 1988 assertions)
```
Zweryfikowano: 2026-02-19 (ver. 0.297)
Zweryfikowano: 2026-02-21 (ver. 0.300)
## Konfiguracja
@@ -66,6 +66,7 @@ tests/
| | |-- Settings/SettingsRepositoryTest.php
| | |-- ShopStatus/ShopStatusRepositoryTest.php
| | |-- Transport/TransportRepositoryTest.php
| | |-- Update/UpdateRepositoryTest.php
| | `-- User/UserRepositoryTest.php
| `-- admin/
| `-- Controllers/

View File

@@ -1,136 +1,113 @@
# Instrukcja tworzenia aktualizacji shopPRO
## Struktura aktualizacji
## Nowy sposób (od v0.301) — automatyczny build script
### Wymagania
- Git z tagami wersji (np. `v0.299`, `v0.300`)
- PowerShell
### Workflow
```
1. Pracuj normalnie: commit, push, commit, push...
2. Gdy wersja gotowa:
→ git tag v0.XXX
→ ./build-update.ps1 -ToTag v0.XXX -ChangelogEntry "NEW - opis"
3. Upload plików z updates/0.XX/ na serwer aktualizacji
```
### Użycie build-update.ps1
```powershell
# Podgląd zmian (bez tworzenia plików)
./build-update.ps1 -ToTag v0.301 -DryRun
# Budowanie paczki (auto-detect poprzedniego tagu)
./build-update.ps1 -ToTag v0.301 -ChangelogEntry "NEW - opis zmiany"
# Z jawnym tagiem źródłowym
./build-update.ps1 -FromTag v0.300 -ToTag v0.301 -ChangelogEntry "NEW - opis"
```
### Co robi skrypt automatycznie
1. `git diff --name-status` między tagami → listy dodanych/zmodyfikowanych/usuniętych plików
2. Filtrowanie przez `.updateignore` (pliki deweloperskie, konfiguracyjne itp.)
3. Kopiowanie plików do temp, tworzenie ZIP
4. SHA256 checksum ZIP-a
5. Generowanie `ver_X.XXX_manifest.json`
6. Generowanie legacy `_sql.txt` i `_files.txt` (okres przejściowy)
7. Aktualizacja `versions.php` i `changelog.php`
8. Cleanup
### Pliki wynikowe
- `updates/0.XX/ver_X.XXX.zip` — paczka z plikami
- `updates/0.XX/ver_X.XXX_manifest.json` — manifest z checksumem, listą zmian, SQL
- `updates/0.XX/ver_X.XXX_sql.txt` — legacy SQL (okres przejściowy)
- `updates/0.XX/ver_X.XXX_files.txt` — legacy lista plików do usunięcia (okres przejściowy)
### Migracje SQL
Pliki SQL umieszczaj w `migrations/{version}.sql` (np. `migrations/0.301.sql`).
Build script automatycznie je wczyta i umieści w manifeście + legacy `_sql.txt`.
### Format manifestu
```json
{
"version": "0.301",
"date": "2026-02-22",
"checksum_zip": "sha256:abc123...",
"files": {
"modified": ["autoload/Domain/Order/OrderRepository.php"],
"added": ["autoload/Domain/Order/NewHelper.php"],
"deleted": ["autoload/shop/OldClass.php"]
},
"directories_deleted": [],
"sql": ["ALTER TABLE pp_x ADD COLUMN y INT DEFAULT 0"],
"changelog": "NEW - opis zmiany"
}
```
### .updateignore
Plik w katalogu głównym projektu, wzorce plików wykluczonych z paczek (jak `.gitignore`).
---
## Stary sposób (do v0.300) — ręczne pakowanie
### Struktura aktualizacji
Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesiątki wersji.
### Pliki aktualizacji:
#### Pliki aktualizacji:
- `ver_X.XXX.zip` - paczka ZIP ze zmienionymi plikami (BEZ folderu wersji, bezpośrednio struktura katalogów)
- `ver_X.XXX_sql.txt` - opcjonalny plik z zapytaniami SQL (jeśli wymagane zmiany w bazie)
- `ver_X.XXX_files.txt` - opcjonalny plik z listą plików do **USUNIĘCIA** przy aktualizacji (format: `F: ../sciezka/do/pliku.php`)
- `changelog.php` - historia zmian
- `versions.php` - konfiguracja wersji (zmienna `$current_ver`)
### Zasada pakowania plików
#### Zasada pakowania plików
- Do paczek aktualizacji **nie dodajemy plików `*.md`** (dokumentacja jest tylko wewnętrzna/deweloperska).
- Do paczek aktualizacji **nie dodajemy `updates/changelog.php`** (to plik serwisowy po stronie repozytorium aktualizacji, nie runtime klienta).
- Do paczek aktualizacji **nie dodajemy głównego `.htaccess` z katalogu projektu** (ten plik wdrażamy osobno, poza ZIP aktualizacji).
## Procedura tworzenia nowej aktualizacji
### Procedura ręczna
## Status biezacej aktualizacji (ver. 0.299)
1. Określ numer wersji
2. Utwórz folder tymczasowy: `mkdir -p temp/temp_XXX/sciezka/do/pliku`
3. Skopiuj zmienione pliki do folderu tymczasowego
4. Utwórz ZIP z zawartości folderu (nie z samego folderu!)
5. Usuń folder tymczasowy
6. Zaktualizuj `changelog.php` i `versions.php`
7. (Opcjonalnie) Utwórz `_sql.txt` i `_files.txt`
- Wersja udostepniona: `0.299` (data: 2026-02-21).
**WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji. Struktura ZIP zaczyna się bezpośrednio od katalogów projektu (admin/, autoload/, itp.).
## Status bieżącej aktualizacji (ver. 0.301)
- Wersja udostępniona: `0.301` (data: 2026-02-22).
- Pliki publikacyjne:
- `updates/0.20/ver_0.299.zip`
- `updates/0.30/ver_0.301.zip`
- Pliki metadanych aktualizacji:
- `updates/changelog.php`
- `updates/versions.php` (`$current_ver = 299`)
- Weryfikacja testow przed publikacja:
- `OK (687 tests, 1971 assertions)`
### 1. Określ numer wersji
Sprawdź ostatnią wersję w `updates/` i zwiększ o 1.
### 2. Utwórz folder tymczasowy ze strukturą w katalogu temp
```bash
mkdir -p temp/temp_XXX/sciezka/do/pliku
```
**WAŻNE:** W archiwum ZIP NIE powinno być folderu z nazwą wersji (np. ver_0.234/).
Struktura ZIP powinna zaczynać się bezpośrednio od katalogów projektu (admin/, autoload/, itp.).
### 3. Skopiuj zmienione pliki do folderu tymczasowego
```bash
cp sciezka/do/pliku.php temp/temp_XXX/sciezka/do/pliku.php
```
### 4. Utwórz plik ZIP z zawartości folderu (nie z samego folderu!)
```powershell
cd temp/temp_XXX
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_X.XXX.zip' -Force"
```
### 5. Usuń folder tymczasowy
```bash
rm -rf temp/temp_XXX
```
### 6. Zaktualizuj changelog.php
Dodaj wpis na początku pliku:
```html
<b>ver. X.XXX - DD.MM.YYYY</b><br />
- NEW/FIX/UPDATE - opis zmiany
<hr>
```
Prefiksy:
- `NEW` - nowa funkcjonalność
- `FIX` - naprawa błędu
- `UPDATE` - aktualizacja istniejącej funkcjonalności
### 7. Zaktualizuj versions.php
Zmień wartość `$current_ver` na nowy numer wersji (bez przedrostka 0.):
```php
$current_ver = 234; // dla wersji 0.234
```
### 8. (Opcjonalnie) Utwórz plik SQL
Jeśli aktualizacja wymaga zmian w bazie danych, utwórz plik `ver_X.XXX_sql.txt` z zapytaniami SQL.
### 9. (Opcjonalnie) Utwórz plik z listą plików do usunięcia
Jeśli aktualizacja wymaga usunięcia przestarzałych plików, utwórz plik `ver_X.XXX_files.txt`:
```
F: ../sciezka/do/pliku1.php
F: ../sciezka/do/pliku2.php
```
**UWAGA:** Pliki wymienione w tym pliku zostaną USUNIĘTE z systemu podczas aktualizacji.
## Przykład - aktualizacja 0.234
Zmienione pliki:
- `autoload/admin/controls/class.ShopOrder.php`
- `admin/templates/shop-order/order-details.php`
Opis: Dodanie przycisku do zaznaczania zamówienia jako wysłane do trustmate.io
### Komendy:
```bash
# Utwórz strukturę w folderze tymczasowym
mkdir -p temp/temp_234/autoload/admin/controls
mkdir -p temp/temp_234/admin/templates/shop-order
# Skopiuj pliki
cp autoload/admin/controls/class.ShopOrder.php temp/temp_234/autoload/admin/controls/
cp admin/templates/shop-order/order-details.php temp/temp_234/admin/templates/shop-order/
# Utwórz ZIP z ZAWARTOŚCI folderu (ważne: wejdź do folderu i spakuj '*')
cd temp/temp_234
powershell -Command "Compress-Archive -Path '*' -DestinationPath '../ver_0.234.zip' -Force"
# Wróć i usuń folder tymczasowy
cd ..
rm -rf temp_234
```
### Poprawna struktura ZIP:
```
ver_0.234.zip
├── admin/
│ └── templates/
│ └── shop-order/
│ └── order-details.php
└── autoload/
└── admin/
└── controls/
└── class.ShopOrder.php
```
### NIEPOPRAWNA struktura (do uniknięcia):
```
ver_0.234.zip
└── ver_0.234/ <-- tego folderu NIE powinno być!
├── admin/
└── autoload/
```
- `updates/versions.php` (`$current_ver = 301`)
- Weryfikacja testów przed publikacją:
- `OK (692 tests, 1988 assertions)`

View File

@@ -1,13 +0,0 @@
@echo off
REM Skrypt do uruchamiania testów z PEŁNYMI szczegółami
echo.
echo ================================
echo Testy DEBUG - pełne szczegóły
echo ================================
echo.
C:\xampp\php\php.exe phpunit.phar --debug %*
echo.
pause

View File

@@ -1,13 +0,0 @@
@echo off
REM Skrypt do uruchamiania testów - tylko kropki
echo.
echo ================================
echo Testy jednostkowe shopPRO
echo ================================
echo.
C:\xampp\php\php.exe phpunit.phar %*
echo.
pause

View File

@@ -1,13 +0,0 @@
@echo off
REM Skrypt do uruchamiania testów PHPUnit
echo.
echo ================================
echo Testy jednostkowe shopPRO
echo ================================
echo.
C:\xampp\php\php.exe phpunit.phar --testdox %*
echo.
pause

View File

@@ -1,47 +0,0 @@
Param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$PhpUnitArgs
)
$ErrorActionPreference = "Stop"
function Resolve-PhpExe {
$cmd = Get-Command php -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source) {
return $cmd.Source
}
$candidates = @(
"C:\xampp\php\php.exe",
"C:\php\php.exe",
"C:\Program Files\PHP\php.exe"
)
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
return $candidate
}
}
throw "Nie znaleziono interpretera PHP. Dodaj php do PATH albo zainstaluj PHP (np. XAMPP)."
}
$phpExe = Resolve-PhpExe
$phpUnitPhar = Join-Path $PSScriptRoot "phpunit.phar"
if (-not (Test-Path $phpUnitPhar)) {
throw "Brak pliku phpunit.phar w katalogu projektu: $PSScriptRoot"
}
$args = @($phpUnitPhar, "--do-not-cache-result") + $PhpUnitArgs
Write-Host ""
Write-Host "================================"
Write-Host " Testy jednostkowe shopPRO"
Write-Host "================================"
Write-Host "PHP: $phpExe"
Write-Host "Cmd: $phpExe $($args -join ' ')"
Write-Host ""
& $phpExe @args
exit $LASTEXITCODE

10
test.sh
View File

@@ -1,10 +0,0 @@
#!/bin/bash
# Skrypt do uruchamiania testów PHPUnit
echo ""
echo "================================"
echo " Testy jednostkowe shopPRO"
echo "================================"
echo ""
/c/xampp/php/php.exe phpunit.phar "$@"

View File

@@ -50,7 +50,7 @@ class UpdateRepositoryTest extends TestCase
$repository = new UpdateRepository($db);
$repository->runPendingMigrations();
$this->assertTrue(true); // No exception thrown
$this->assertTrue(true);
}
public function testHasPrivateHelperMethods(): void
@@ -59,6 +59,11 @@ class UpdateRepositoryTest extends TestCase
$privateMethods = [
'downloadAndApply',
'downloadAndApplyLegacy',
'downloadAndApplyWithManifest',
'downloadManifest',
'verifyChecksum',
'createBackup',
'executeSql',
'deleteFiles',
'extractZip',
@@ -77,4 +82,99 @@ class UpdateRepositoryTest extends TestCase
);
}
}
public function testVerifyChecksumValidFormat(): void
{
$db = $this->createMockDb();
$repository = new UpdateRepository($db);
$reflection = new \ReflectionClass($repository);
$method = $reflection->getMethod('verifyChecksum');
$method->setAccessible(true);
// Create a temp file with known content
$tmpFile = tempnam(sys_get_temp_dir(), 'test_checksum_');
file_put_contents($tmpFile, 'test content for checksum');
$expectedHash = hash_file('sha256', $tmpFile);
$result = $method->invoke($repository, $tmpFile, 'sha256:' . $expectedHash, []);
$this->assertTrue($result['valid']);
$this->assertNotEmpty($result['log']);
@unlink($tmpFile);
}
public function testVerifyChecksumInvalidHash(): void
{
$db = $this->createMockDb();
$repository = new UpdateRepository($db);
$reflection = new \ReflectionClass($repository);
$method = $reflection->getMethod('verifyChecksum');
$method->setAccessible(true);
$tmpFile = tempnam(sys_get_temp_dir(), 'test_checksum_');
file_put_contents($tmpFile, 'test content');
$result = $method->invoke($repository, $tmpFile, 'sha256:invalidhash', []);
$this->assertFalse($result['valid']);
@unlink($tmpFile);
}
public function testVerifyChecksumInvalidFormat(): void
{
$db = $this->createMockDb();
$repository = new UpdateRepository($db);
$reflection = new \ReflectionClass($repository);
$method = $reflection->getMethod('verifyChecksum');
$method->setAccessible(true);
$result = $method->invoke($repository, '/tmp/nonexistent', 'badformat', []);
$this->assertFalse($result['valid']);
}
public function testCreateBackupWithEmptyManifest(): void
{
$db = $this->createMockDb();
$repository = new UpdateRepository($db);
$reflection = new \ReflectionClass($repository);
$method = $reflection->getMethod('createBackup');
$method->setAccessible(true);
$manifest = [
'version' => '0.999',
'files' => [],
];
$log = $method->invoke($repository, $manifest, []);
$this->assertIsArray($log);
$hasBackupInfo = false;
foreach ($log as $entry) {
if (strpos($entry, 'Brak plików do backupu') !== false) {
$hasBackupInfo = true;
}
}
$this->assertTrue($hasBackupInfo);
}
public function testDownloadManifestReturnsNullForInvalidUrl(): void
{
$db = $this->createMockDb();
$repository = new UpdateRepository($db);
$reflection = new \ReflectionClass($repository);
$method = $reflection->getMethod('downloadManifest');
$method->setAccessible(true);
$result = $method->invoke($repository, 'http://invalid.nonexistent.test', '0.999');
$this->assertNull($result);
}
}

BIN
updates/0.30/ver_0.300.zip Normal file

Binary file not shown.

View File

@@ -1,3 +1,7 @@
<b>ver. 0.300 - 21.02.2026</b><br />
- NEW - System aktualizacji oparty na manifestach JSON (checksum SHA256, backup plików, automatyczny build)
- NEW - Panel logu aktualizacji w panelu admina
<hr>
<b>ver. 0.299 - 21.02.2026</b><br />
- NEW - Ukrywanie/pokazywanie kolumn w tabelach admina (toggle switch + localStorage)
<hr>

View File

@@ -1,5 +1,5 @@
<?
$current_ver = 299;
$current_ver = 300;
for ($i = 1; $i <= $current_ver; $i++)
{