Compare commits

...

15 Commits

Author SHA1 Message Date
09d266204e ver. 0.314: fix wyszukiwarki admin + title zamówienia
- Fix: globalna wyszukiwarka - Content-Type, Cache-Control, POST,
  FETCH_ASSOC, try/catch wrapper
- New: document.title w szczegółach zamówienia = numer zamówienia

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:32:54 +01:00
8f43f5ab4d build: update package v0.313
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:45:45 +01:00
b17463bcbc ver. 0.313: fix sync płatności Apilo + logowanie decyzji sync
Fix: (int) cast na apilo_order_id (format "PPxxxxxx") dawał 0, przez co
syncApiloPayment() i syncApiloStatus() pomijały wywołanie API Apilo.
Zmiana na empty() w obu metodach.

New: logowanie ApiloLogger w syncApiloPaymentIfNeeded() i
syncApiloStatusIfNeeded() — każda ścieżka decyzyjna zapisuje wpis
do pp_log z kontekstem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:44:11 +01:00
76de81bca4 build: update package v0.312
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:12:33 +01:00
842ed77f5b ver. 0.312: fix krytycznych bugów integracji Apilo
- curl_getinfo() po curl_close() dawał HTTP 0 — przeniesienie przed close
- nieskończona pętla wysyłki zamówienia przy błędzie serwera Apilo (apilo_order_id = -1)
- ceny 0.00 PLN — string "0.00" z MySQL jest truthy, zmiana na (float) > 0
- walidacja zerowych cen przed wysyłką (apilo_order_id = -2)
- niezainicjalizowana $order_message

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:11:28 +01:00
96ed86649a build: update package v0.311
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:51:50 +01:00
fdc4cac593 ver. 0.311: fix race condition Apilo + persistence filtrów + poprawki cen
- Fix: race condition callback płatności przed wysłaniem do Apilo
- Fix: processApiloSyncQueue czeka na apilo_order_id zamiast usuwać task
- Fix: drugie wywołanie processApiloSyncQueue po wysyłce zamówień w cronie
- Fix: ceny w szczegółach zamówienia (effective price zamiast 0 zł)
- New: persistence filtrów tabel admin (localStorage)
- Testy: 760 tests, 2141 assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:50:34 +01:00
8f67d9de0a build: update package v0.310
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:57:20 +01:00
3ae0bc95e0 ver. 0.310: logi integracji w panelu admin
Nowa zakladka "Logi" w sekcji Integracje - podglad tabeli pp_log
z paginacja, sortowaniem, filtrami i rozwijalnym kontekstem JSON.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:54:09 +01:00
92ec5e1194 build: update package v0.309
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:32:51 +01:00
4de5479c41 ver. 0.309: ApiloLogger + cache-busting CSS/JS + poprawki UI
- ApiloLogger: logowanie operacji Apilo do pp_log z kontekstem JSON
- Cache-busting: ?ver=filemtime() dla CSS i JS w admin main-layout
- Fix: inicjalizacja $mdb przed SettingsRepository w admin/index.php
- Fix: rzutowanie (string) w ShopProductController::escapeHtml()
- UI: text-overflow ellipsis dla kategorii produktow + title tooltip
- JS: navigator.clipboard API w copyToClipboard() z fallbackiem
- CSS: uproszczenie .site-content, usuniecie .with-menu
- Migracja: pp_log + kolumny action, order_id, context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:31:28 +01:00
f31630b69c build: update package v0.308
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:01:37 +01:00
efcf06969c ver. 0.308: kolory statusow zamowien + poprawki bezpieczenstwa
- Kolorowe badge statusow na liscie zamowien (pp_shop_statuses.color)
- Walidacja hex koloru z DB (regex), sanityzacja HTML transport
- Polaczenie 2 zapytan SQL w jedno orderStatusData()
- Path-based form submit w table-list.php (admin URL routing)
- 11 nowych testow (750 total, 2114 assertions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:57:56 +01:00
56c931f7da Add remote database host and update migration SQL
- Added a new remote host configuration to config.php for database connection.
- Updated migration script to ensure proper addition of 'min_order_amount' column in pp_shop_payment_methods table.
- Created .gitignore file to exclude cache directory.
- Added project configuration file for Serena with initial settings and tool configurations.
2026-02-22 18:06:15 +01:00
54edbd21f6 build: update package v0.307
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:04:02 +01:00
52 changed files with 1479 additions and 2093 deletions

View File

@@ -14,7 +14,41 @@
"Bash(powershell -Command \"& { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::OpenRead\\(''updates/0.20/ver_0.296.zip''\\).Entries | ForEach-Object { Write-Output $_.FullName } }\")",
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../ver_0.296.zip'' -Force\")",
"Bash(powershell -Command \"Add-Type -AssemblyName System.IO.Compression.FileSystem; [IO.Compression.ZipFile]::OpenRead\\(\\(Resolve-Path ''updates/0.20/ver_0.296.zip''\\)\\).Entries.FullName\")",
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../ver_0.297.zip'' -Force\")"
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../ver_0.297.zip'' -Force\")",
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../../updates/0.20/ver_0.299.zip'' -Force\")",
"Bash(powershell -Command \"Remove-Item -Recurse -Force 'c:/visual studio code/projekty/shopPRO/temp/temp_299'\":*)",
"Bash(powershell -Command \"\\(Get-ChildItem ''c:/visual studio code/projekty/shopPRO/updates/0.20/ver_0.299.zip''\\).Length; [System.IO.Compression.ZipFile]::OpenRead\\(''c:/visual studio code/projekty/shopPRO/updates/0.20/ver_0.299.zip''\\).Entries | ForEach-Object { $_.FullName }\")",
"Bash(powershell -Command \"Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::OpenRead\\(''c:/visual studio code/projekty/shopPRO/updates/0.20/ver_0.299.zip''\\).Entries | ForEach-Object { $_.FullName }\")",
"Bash(unzip:*)",
"mcp__serena__find_symbol",
"mcp__serena__find_file",
"mcp__serena__activate_project",
"mcp__serena__check_onboarding_performed",
"Bash(tail:*)",
"WebFetch(domain:shoppro.project-dc.pl)",
"Bash(cd:*)",
"mcp__serena__get_symbols_overview",
"mcp__serena__search_for_pattern",
"mcp__serena__read_file",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && powershell.exe -ExecutionPolicy Bypass -File \"C:/visual studio code/projekty/shopPRO/test.ps1\" 2>&1)",
"mcp__serena__replace_content",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && npx sass admin/layout/style-scss/style.scss admin/layout/style-css/style.css --source-map 2>&1)",
"Bash(head:*)",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && rm -rf temp/temp_304 && powershell -File \"./build-update.ps1\" -FromTag v0.303 -ToTag v0.304 -ChangelogEntry \"NEW - konfigurowalne limity kwotowe metod platnosci \\(min/max kwota zamowienia\\)\" 2>&1)",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && rm -rf temp/temp_305 && powershell -File \"./build-update.ps1\" -FromTag v0.304 -ToTag v0.305 -ChangelogEntry \"FIX - naprawa kolejnosci atrybutow permutacji, NEW - pasek postepu darmowej dostawy w koszyku\" 2>&1)",
"Bash(xxd:*)",
"mcp__serena__list_dir",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && powershell -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.305 -ToTag v0.306 -ChangelogEntry \"FIX - ukrywanie form dostawy gdy nie ma dostepnych form platnosci\" 2>&1)",
"Bash(powershell:*)",
"Bash(powershell.exe:*)",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && rm -f updates/0.30/ver_0.304.zip updates/0.30/ver_0.304_manifest.json updates/0.30/ver_0.304_sql.txt updates/0.30/ver_0.304_files.txt && powershell -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.303 -ToTag v0.304 -ChangelogEntry \"NEW - konfigurowalne limity kwotowe metod platnosci \\(min/max kwota zamowienia\\)\" 2>&1)",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && rm -f updates/0.30/ver_0.305.zip updates/0.30/ver_0.305_manifest.json updates/0.30/ver_0.305_sql.txt updates/0.30/ver_0.305_files.txt && powershell -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.304 -ToTag v0.305 -ChangelogEntry \"FIX - naprawa kolejnosci atrybutow permutacji, NEW - pasek postepu darmowej dostawy w koszyku\" 2>&1)",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && rm -rf temp/temp_305 && powershell -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.304 -ToTag v0.305 -ChangelogEntry \"FIX - naprawa kolejnosci atrybutow permutacji, NEW - pasek postepu darmowej dostawy w koszyku\" 2>&1)",
"Bash(cd \"/c/visual studio code/projekty/shopPRO\" && rm -rf temp/temp_305 && sleep 2 && powershell -ExecutionPolicy Bypass -Command \"& { \\\\$env:DOTNET_GCServer = 1; & './build-update.ps1' -FromTag v0.304 -ToTag v0.305 -ChangelogEntry 'FIX - naprawa kolejnosci atrybutow permutacji, NEW - pasek postepu darmowej dostawy w koszyku' }\" 2>&1)",
"Bash(python3:*)",
"Bash(python:*)",
"Bash(grep:*)",
"Bash(grep ^<b>ver:*)"
]
}
}

File diff suppressed because one or more lines are too long

1
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cache

117
.serena/project.yml Normal file
View File

@@ -0,0 +1,117 @@
# the name by which the project can be referenced within Serena
project_name: "shopPRO"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- typescript
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# override of the corresponding setting in serena_config.yml, see the documentation there.
# If null or missing, the value from the global config is used.
symbol_info_budget:

View File

@@ -44,3 +44,7 @@ cron/temp/
# IDE
.vscode/
.serena/
# Cache testów
.phpunit.result.cache

View File

@@ -36,7 +36,7 @@ composer test
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
Current suite: **739 tests, 2089 assertions**.
Current suite: **758 tests, 2135 assertions**.
### Creating Updates
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs.

View File

@@ -44,6 +44,15 @@ define( 'REDBEAN_MODEL_PREFIX', '' );
date_default_timezone_set( 'Europe/Warsaw' );
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings();
if ( file_exists( 'config.php' ) )
@@ -79,15 +88,6 @@ if ( !$lang = \Shared\Helpers\Helpers::get_session( 'lang-' . $lang_id ) )
\Shared\Helpers\Helpers::set_session( 'lang-' . $lang_id, $lang );
}
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
$user = \Shared\Helpers\Helpers::get_session( 'user', true );
\admin\App::update();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -455,23 +455,14 @@ body {
}
.site-content {
&.with-menu {
width: 100%;
@include respond-above(xs) {
width: calc(100% - 243px);
margin-left: 243px;
}
}
@include respond-below(md) {
margin-left: 0;
}
margin-left: 0;
background-color: #fff;
margin-left: 244px;
@include respond-above(xs) {
width: calc(100% - 243px);
margin-left: 243px;
}
.top-user {
text-align: right;
@@ -1750,33 +1741,16 @@ li.sort-collapsed.sort-hover div {
}
}
#table-products {
.product-categories {
display: block;
width: 100%;
text-wrap: wrap;
}
.product-categories {
display: block;
width: 100%;
text-wrap: wrap;
.product-name {
display: flex;
justify-content: space-between;
.duplicate-product {
margin-left: 15px;
}
}
.duplicate-product {
float: right;
font-size: 13px;
}
.btn-success {
color: #FFF !important;
&.btn-create-product {
margin-top: 5px;
}
&--cats {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 600px;
}
}
@@ -2104,6 +2078,10 @@ textarea.form-control {
}
.order-details {
.fa-copy {
cursor: pointer !important;
}
.paid-status {
margin-top: 10px;

View File

@@ -9,7 +9,7 @@ $buildUrl = function(array $params = []) use ($list): string {
}
}
$qs = http_build_query($query);
return $list->basePath . ($qs ? ('?' . $qs) : '');
return $list->basePath . $qs;
};
$currentSort = $list->sort['column'] ?? '';
@@ -92,7 +92,7 @@ $isCompactColumn = function(array $column): bool {
<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">
<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'] ?? '');
@@ -162,7 +162,7 @@ $isCompactColumn = function(array $column): bool {
<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">Wyczyść</a>
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm js-table-filters-clear">Wyczyść</a>
</div>
</form>
</div>
@@ -292,7 +292,7 @@ $isCompactColumn = function(array $column): bool {
</ul>
</div>
<div class="col-sm-6 text-right">
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline table-list-per-page-form">
<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'); ?>" />
@@ -300,7 +300,7 @@ $isCompactColumn = function(array $column): bool {
<?php endforeach; ?>
<input type="hidden" name="page" value="1" />
Wyświetlaj
<select name="per_page" class="form-control input-sm" onchange="this.form.submit()">
<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; ?>
@@ -312,6 +312,40 @@ $isCompactColumn = function(array $column): bool {
</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 (!$) {
@@ -529,5 +563,38 @@ $isCompactColumn = function(array $column): bool {
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>

View File

@@ -0,0 +1,19 @@
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
<div class="mt15">
<a href="/admin/integrations/logs_clear/" class="btn btn-danger btn-sm"
onclick="return confirm('Na pewno chcesz usunac wszystkie logi?');">
<i class="fa fa-trash"></i> Wyczysc wszystkie logi
</a>
</div>
<script type="text/javascript">
$(function() {
$('body').on('click', '.log-context-btn', function(e) {
e.preventDefault();
var id = $(this).data('id');
$('#log-context-' + id).toggle();
$(this).text($('#log-context-' + id).is(':visible') ? 'Ukryj' : 'Pokaz');
});
});
</script>

View File

@@ -3,6 +3,7 @@ $orderId = (int)($this -> order['id'] ?? 0);
?>
<div class="site-title">Szczegóły zamówienia: <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?></div>
<script>document.title = 'Zamówienie <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?> - shopPro';</script>
<div class="od-actions mb15">
<a href="/admin/shop_order/list/" class="btn btn-dark btn-sm">
@@ -89,6 +90,19 @@ $orderId = (int)($this -> order['id'] ?? 0);
<div>
<b><?= $this -> order[ 'payment_method' ];?> </b>
</div>
<? if ( !empty($this -> order['apilo_order_id']) ):?>
<br/>
<div>
<i class="fa fa-cloud"></i> Apilo: <b style="color: #27ae60;">tak</b>
&mdash; ID: <b id="order-apilo-id"><?= htmlspecialchars((string)$this -> order['apilo_order_id'], ENT_QUOTES, 'UTF-8');?></b>
<i class="fa fa-copy" onclick="copyToClipboard( 'order-apilo-id' ); return false;"></i>
</div>
<? else:?>
<br/>
<div>
<i class="fa fa-cloud"></i> Apilo: <b style="color: #c0392b;">nie</b>
</div>
<? endif;?>
</div>
</div>
<div class="paid-status panel">
@@ -184,13 +198,14 @@ $orderId = (int)($this -> order['id'] ?? 0);
<?= $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ł
<? $effective = ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
<?= (int)$product['quantity'];?> &times; <?= \Shared\Helpers\Helpers::decimal( $effective );?> = <?= \Shared\Helpers\Helpers::decimal( $effective * $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>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] );?> zł</td>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] * $product[ 'quantity' ] );?> zł</td>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective );?> zł</td>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective * $product[ 'quantity' ] );?> zł</td>
</tr>
<? endforeach; endif;?>
</tbody>

View File

@@ -8,36 +8,36 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="www.project-pro.pl - internetowe rozwi&#261;zania dla biznesu">
<link rel='stylesheet' type="text/css" href='https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700'>
<link rel="stylesheet" type="text/css" href="/libraries/framework/skin/default_skin/css/theme.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/magnific/magnific-popup.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css">
<link rel="stylesheet" type="text/css" href="/libraries/easy-tabs/css/easy-responsive-tabs.css">
<link rel="stylesheet" type="text/css" href="/libraries/bootstrap-4.5.2-dist/css/bootstrap.css">
<link rel="stylesheet" type="text/css" href="/libraries/font-awesome-4.7.0/css/font-awesome.css">
<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/framework/vendor/jquery/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js"></script>
<script type="text/javascript" src="/libraries/framework/js/utility/utility.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/magnific/jquery.magnific-popup.js"></script>
<script type="text/javascript" src="/libraries/easy-tabs/js/easyResponsiveTabs.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/moment.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/pl.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js"></script>
<script type="text/javascript" src="/libraries/jquery-confirm/jquery-confirm.min.js"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.js"></script>
<script type="text/javascript" src="/libraries/functions.js"></script>
<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" />
<link rel="stylesheet" type="text/css" href="/libraries/framework/skin/default_skin/css/theme.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/skin/default_skin/css/theme.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/magnific/magnific-popup.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/magnific/magnific-popup.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/daterange/daterangepicker.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/jquery-confirm/jquery-confirm.min.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/easy-tabs/css/easy-responsive-tabs.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/easy-tabs/css/easy-responsive-tabs.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/bootstrap-4.5.2-dist/css/bootstrap.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/bootstrap-4.5.2-dist/css/bootstrap.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/font-awesome-4.7.0/css/font-awesome.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/font-awesome-4.7.0/css/font-awesome.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/minimal.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/grid/plugins/icheck/skins/minimal/minimal.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/blue.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/grid/plugins/icheck/skins/minimal/blue.css'); ?>">
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery-1.11.1.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery-1.11.1.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/js/utility/utility.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/js/utility/utility.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/magnific/jquery.magnific-popup.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/magnific/jquery.magnific-popup.js'); ?>"></script>
<script type="text/javascript" src="/libraries/easy-tabs/js/easyResponsiveTabs.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/easy-tabs/js/easyResponsiveTabs.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/moment.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/moment/moment.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/pl.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/moment/pl.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/daterange/daterangepicker.js'); ?>"></script>
<script type="text/javascript" src="/libraries/jquery-confirm/jquery-confirm.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/jquery-confirm/jquery-confirm.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/grid/plugins/icheck/icheck.js'); ?>"></script>
<script type="text/javascript" src="/libraries/functions.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/functions.js'); ?>"></script>
<script type="text/javascript" src="/admin/js/functions.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/js/functions.js'); ?>"></script>
<link rel="stylesheet" href="/admin/layout/style-css/style.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/layout/style-css/style.css'); ?>" />
<link rel="stylesheet" href="/admin/layout/style-css/table-list.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/layout/style-css/table-list.css'); ?>" />
<link rel="stylesheet" href="/admin/layout/style-css/order-details-mobile.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/layout/style-css/order-details-mobile.css'); ?>" />
</head>
<body>
<div class="admin-page">
@@ -153,6 +153,11 @@
<i class="fa fa-cogs" aria-hidden="true"></i>shopPRO
</a>
</li>
<li>
<a href="/admin/integrations/logs/">
<i class="fa fa-list-alt" aria-hidden="true"></i>Logi
</a>
</li>
</ul>
</div>
<div class="preview">
@@ -317,7 +322,7 @@
$.ajax({
url: '/admin/settings/globalSearchAjax/',
type: 'GET',
type: 'POST',
dataType: 'json',
data: { q: phrase },
success: function(response) {
@@ -328,8 +333,12 @@
renderResults(response.items || []);
},
error: function() {
$results.html('<div class="admin-global-search-empty">Błąd połączenia</div>').addClass('open');
error: function(xhr) {
var msg = 'Błąd połączenia';
if (xhr.status === 200) {
msg = 'Błąd parsowania odpowiedzi';
}
$results.html('<div class="admin-global-search-empty">' + msg + '</div>').addClass('open');
}
});
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Domain\Integrations;
class ApiloLogger
{
/**
* @param \medoo $db
* @param string $action np. 'send_order', 'payment_sync', 'status_sync', 'status_poll'
* @param int|null $orderId
* @param string $message
* @param mixed $context dane do zapisania jako JSON (request/response)
*/
public static function log($db, string $action, ?int $orderId, string $message, $context = null): void
{
$contextJson = null;
if ($context !== null) {
$contextJson = is_string($context)
? $context
: json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
$db->insert('pp_log', [
'action' => $action,
'order_id' => $orderId,
'message' => $message,
'context' => $contextJson,
'date' => date('Y-m-d H:i:s'),
]);
}
}

View File

@@ -56,6 +56,63 @@ class IntegrationsRepository
return true;
}
// ── Logs ────────────────────────────────────────────────────
/**
* Pobiera logi z tabeli pp_log z paginacją, sortowaniem i filtrowaniem.
*
* @return array{items:array, total:int}
*/
public function getLogs( array $filters, string $sortColumn, string $sortDir, int $page, int $perPage ): array
{
$where = [];
if ( !empty( $filters['log_action'] ) ) {
$where['action[~]'] = '%' . $filters['log_action'] . '%';
}
if ( !empty( $filters['message'] ) ) {
$where['message[~]'] = '%' . $filters['message'] . '%';
}
if ( !empty( $filters['order_id'] ) ) {
$where['order_id'] = (int) $filters['order_id'];
}
$total = $this->db->count( 'pp_log', $where );
$where['ORDER'] = [ $sortColumn => $sortDir ];
$where['LIMIT'] = [ ( $page - 1 ) * $perPage, $perPage ];
$items = $this->db->select( 'pp_log', '*', $where );
if ( !is_array( $items ) ) {
$items = [];
}
return [
'items' => $items,
'total' => (int) $total,
];
}
/**
* Usuwa wpis logu po ID.
*/
public function deleteLog( int $id ): bool
{
$this->db->delete( 'pp_log', [ 'id' => $id ] );
return true;
}
/**
* Czyści wszystkie logi z tabeli pp_log.
*/
public function clearLogs(): bool
{
$this->db->delete( 'pp_log', [] );
return true;
}
// ── Product linking (Apilo) ─────────────────────────────────
public function linkProduct( int $productId, $externalId, $externalName ): bool

View File

@@ -30,6 +30,14 @@ class OrderAdminService
return $this->orders->orderStatuses();
}
/**
* @return array{names: array<int, string>, colors: array<int, string>}
*/
public function statusData(): array
{
return $this->orders->orderStatusData();
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
@@ -385,17 +393,38 @@ class OrderAdminService
global $mdb;
if ($orderId <= 0) {
\Domain\Integrations\ApiloLogger::log(
$mdb,
'resend_order',
$orderId,
'Nieprawidlowe ID zamowienia (orderId <= 0)',
['order_id' => $orderId]
);
return false;
}
$order = $this->orders->findForAdmin($orderId);
if (empty($order) || empty($order['apilo_order_id'])) {
\Domain\Integrations\ApiloLogger::log(
$mdb,
'resend_order',
$orderId,
'Brak zamowienia lub brak apilo_order_id',
['order_found' => !empty($order), 'apilo_order_id' => $order['apilo_order_id'] ?? null]
);
return false;
}
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
$accessToken = $integrationsRepository -> apiloGetAccessToken();
if (!$accessToken) {
\Domain\Integrations\ApiloLogger::log(
$mdb,
'resend_order',
$orderId,
'Nie udalo sie uzyskac tokenu Apilo (access token)',
['apilo_order_id' => $order['apilo_order_id']]
);
return false;
}
@@ -417,13 +446,29 @@ class OrderAdminService
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$apiloResultRaw = curl_exec($ch);
$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$apiloResult = json_decode((string)$apiloResultRaw, true);
if (!is_array($apiloResult) || (int)($apiloResult['updates'] ?? 0) !== 1) {
\Domain\Integrations\ApiloLogger::log(
$mdb,
'resend_order',
$orderId,
'Błąd ponownego wysyłania zamówienia do Apilo (HTTP: ' . $http_code . ')',
['apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code, 'response' => $apiloResult]
);
curl_close($ch);
return false;
}
\Domain\Integrations\ApiloLogger::log(
$mdb,
'resend_order',
$orderId,
'Zamówienie ponownie wysłane do Apilo (apilo_order_id: ' . $order['apilo_order_id'] . ')',
['apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code, 'response' => $apiloResult]
);
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_orders' AND COLUMN_NAME != 'id'";
$stmt = $mdb->query($query);
$columns = $stmt ? $stmt->fetchAll(\PDO::FETCH_COLUMN) : [];
@@ -509,9 +554,26 @@ class OrderAdminService
$error = '';
$sync_failed = false;
$max_attempts = 50; // ~8h przy cronie co 10 min
// Zamówienie jeszcze nie wysłane do Apilo — czekaj na crona
if (!(int)$order['apilo_order_id']) {
$attempts = (int)($task['attempts'] ?? 0) + 1;
if ($attempts >= $max_attempts) {
// Przekroczono limit prób — porzuć task
unset($queue[$key]);
} else {
$task['attempts'] = $attempts;
$task['last_error'] = 'awaiting_apilo_order';
$task['updated_at'] = date('Y-m-d H:i:s');
$queue[$key] = $task;
}
$processed++;
continue;
}
$payment_pending = !empty($task['payment']) && (int)$order['paid'] === 1;
if ($payment_pending && (int)$order['apilo_order_id']) {
if ($payment_pending) {
if (!$this->syncApiloPayment($order)) {
$sync_failed = true;
$error = 'payment_sync_failed';
@@ -519,7 +581,7 @@ class OrderAdminService
}
$status_pending = isset($task['status']) && $task['status'] !== null && $task['status'] !== '';
if (!$sync_failed && $status_pending && (int)$order['apilo_order_id']) {
if (!$sync_failed && $status_pending) {
if (!$this->syncApiloStatus($order, (int)$task['status'])) {
$sync_failed = true;
$error = 'status_sync_failed';
@@ -600,6 +662,17 @@ class OrderAdminService
$apilo_settings = $integrationsRepository->getSettings('apilo');
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
\Domain\Integrations\ApiloLogger::log(
$db,
'payment_sync',
(int)$order['id'],
'Pominięto sync płatności — Apilo wyłączone lub brak tokenu/sync_orders',
[
'enabled' => $apilo_settings['enabled'] ?? false,
'has_token' => !empty($apilo_settings['access-token']),
'sync_orders' => $apilo_settings['sync_orders'] ?? false,
]
);
return;
}
@@ -607,7 +680,24 @@ class OrderAdminService
self::appendApiloLog("SET AS PAID\n" . print_r($order, true));
}
if ($order['apilo_order_id'] && !$this->syncApiloPayment($order)) {
if (!$order['apilo_order_id']) {
// Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync płatności na później
\Domain\Integrations\ApiloLogger::log(
$db,
'payment_sync',
(int)$order['id'],
'Brak apilo_order_id — płatność zakolejkowana do sync',
['apilo_order_id' => $order['apilo_order_id'] ?? null]
);
self::queueApiloSync((int)$order['id'], true, null, 'awaiting_apilo_order');
} elseif (!$this->syncApiloPayment($order)) {
\Domain\Integrations\ApiloLogger::log(
$db,
'payment_sync',
(int)$order['id'],
'Sync płatności nieudany — zakolejkowano ponowną próbę',
['apilo_order_id' => $order['apilo_order_id']]
);
self::queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed');
}
}
@@ -621,6 +711,18 @@ class OrderAdminService
$apilo_settings = $integrationsRepository->getSettings('apilo');
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
\Domain\Integrations\ApiloLogger::log(
$db,
'status_sync',
(int)$order['id'],
'Pominięto sync statusu — Apilo wyłączone lub brak tokenu/sync_orders',
[
'target_status' => $status,
'enabled' => $apilo_settings['enabled'] ?? false,
'has_token' => !empty($apilo_settings['access-token']),
'sync_orders' => $apilo_settings['sync_orders'] ?? false,
]
);
return;
}
@@ -628,7 +730,24 @@ class OrderAdminService
self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true));
}
if ($order['apilo_order_id'] && !$this->syncApiloStatus($order, $status)) {
if (!$order['apilo_order_id']) {
// Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync statusu na później
\Domain\Integrations\ApiloLogger::log(
$db,
'status_sync',
(int)$order['id'],
'Brak apilo_order_id — status zakolejkowany do sync',
['apilo_order_id' => $order['apilo_order_id'] ?? null, 'target_status' => $status]
);
self::queueApiloSync((int)$order['id'], false, $status, 'awaiting_apilo_order');
} elseif (!$this->syncApiloStatus($order, $status)) {
\Domain\Integrations\ApiloLogger::log(
$db,
'status_sync',
(int)$order['id'],
'Sync statusu nieudany — zakolejkowano ponowną próbę',
['apilo_order_id' => $order['apilo_order_id'], 'target_status' => $status]
);
self::queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed');
}
}
@@ -640,7 +759,7 @@ class OrderAdminService
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
if (!(int)$order['apilo_order_id']) {
if (empty($order['apilo_order_id'])) {
return true;
}
@@ -677,6 +796,23 @@ class OrderAdminService
self::appendApiloLog("PAYMENT RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r($apilo_response, true));
}
$success = ($curl_error === '' && $http_code >= 200 && $http_code < 300);
\Domain\Integrations\ApiloLogger::log(
$db,
'payment_sync',
(int)$order['id'],
$success
? 'Płatność zsynchronizowana z Apilo (apilo_order_id: ' . $order['apilo_order_id'] . ')'
: 'Błąd synchronizacji płatności (HTTP: ' . $http_code . ($curl_error ? ', cURL: ' . $curl_error : '') . ')',
[
'apilo_order_id' => $order['apilo_order_id'],
'http_code' => $http_code,
'curl_error' => $curl_error,
'response' => json_decode((string)$apilo_response, true),
]
);
if ($curl_error !== '') return false;
if ($http_code < 200 || $http_code >= 300) return false;
@@ -690,7 +826,7 @@ class OrderAdminService
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
if (!(int)$order['apilo_order_id']) {
if (empty($order['apilo_order_id'])) {
return true;
}
@@ -721,6 +857,24 @@ class OrderAdminService
self::appendApiloLog("STATUS RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r($apilo_result, true));
}
$success = ($curl_error === '' && $http_code >= 200 && $http_code < 300);
\Domain\Integrations\ApiloLogger::log(
$db,
'status_sync',
(int)$order['id'],
$success
? 'Status zsynchronizowany z Apilo (apilo_order_id: ' . $order['apilo_order_id'] . ', status: ' . $status . ')'
: 'Błąd synchronizacji statusu (HTTP: ' . $http_code . ($curl_error ? ', cURL: ' . $curl_error : '') . ')',
[
'apilo_order_id' => $order['apilo_order_id'],
'status' => $status,
'http_code' => $http_code,
'curl_error' => $curl_error,
'response' => json_decode((string)$apilo_result, true),
]
);
if ($curl_error !== '') return false;
if ($http_code < 200 || $http_code >= 300) return false;

View File

@@ -245,25 +245,43 @@ class OrderRepository
public function orderStatuses(): array
{
$rows = $this->db->select('pp_shop_statuses', ['id', 'status'], [
$data = $this->orderStatusData();
return $data['names'];
}
/**
* Zwraca nazwy i kolory statusów w jednym zapytaniu.
*
* @return array{names: array<int, string>, colors: array<int, string>}
*/
public function orderStatusData(): array
{
$rows = $this->db->select('pp_shop_statuses', ['id', 'status', 'color'], [
'ORDER' => ['o' => 'ASC'],
]);
$names = [];
$colors = [];
if (!is_array($rows)) {
return [];
return ['names' => $names, 'colors' => $colors];
}
$result = [];
foreach ($rows as $row) {
$id = (int)($row['id'] ?? 0);
if ($id < 0) {
continue;
}
$result[$id] = (string)($row['status'] ?? '');
$names[$id] = (string)($row['status'] ?? '');
$color = trim((string)($row['color'] ?? ''));
if ($color !== '' && preg_match('/^#[0-9a-fA-F]{3,6}$/', $color)) {
$colors[$id] = $color;
}
}
return $result;
return ['names' => $names, 'colors' => $colors];
}
public function nextOrderId(int $orderId): ?int

View File

@@ -2,6 +2,7 @@
namespace admin\Controllers;
use Domain\Integrations\IntegrationsRepository;
use admin\ViewModels\Common\PaginatedTableViewModel;
class IntegrationsController
{
@@ -12,6 +13,114 @@ class IntegrationsController
$this->repository = $repository;
}
public function logs(): string
{
$sortableColumns = ['id', 'action', 'order_id', 'message', 'date'];
$filterDefinitions = [
[
'key' => 'log_action',
'label' => 'Akcja',
'type' => 'text',
],
[
'key' => 'message',
'label' => 'Wiadomosc',
'type' => 'text',
],
[
'key' => 'order_id',
'label' => 'ID zamowienia',
'type' => 'text',
],
];
$listRequest = \admin\Support\TableListRequestFactory::fromRequest(
$filterDefinitions,
$sortableColumns,
'id'
);
$result = $this->repository->getLogs(
$listRequest['filters'],
$listRequest['sortColumn'],
$listRequest['sortDir'],
$listRequest['page'],
$listRequest['perPage']
);
$rows = [];
$lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1;
foreach ( $result['items'] as $item ) {
$id = (int)($item['id'] ?? 0);
$context = trim( (string)($item['context'] ?? '') );
$contextHtml = '';
if ( $context !== '' ) {
$contextHtml = '<button class="btn btn-xs btn-default log-context-btn" data-id="' . $id . '">Pokaz</button>'
. '<pre class="log-context-pre" id="log-context-' . $id . '" style="display:none;max-height:300px;overflow:auto;margin-top:5px;font-size:11px;white-space:pre-wrap;">'
. htmlspecialchars( $context, ENT_QUOTES, 'UTF-8' )
. '</pre>';
}
$rows[] = [
'lp' => $lp++ . '.',
'action' => htmlspecialchars( (string)($item['action'] ?? ''), ENT_QUOTES, 'UTF-8' ),
'order_id' => $item['order_id'] ? (int)$item['order_id'] : '-',
'message' => htmlspecialchars( (string)($item['message'] ?? ''), ENT_QUOTES, 'UTF-8' ),
'context' => $contextHtml,
'date' => !empty( $item['date'] ) ? date( 'Y-m-d H:i:s', strtotime( (string)$item['date'] ) ) : '-',
];
}
$total = (int)$result['total'];
$totalPages = max( 1, (int)ceil( $total / $listRequest['perPage'] ) );
$viewModel = new PaginatedTableViewModel(
[
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
['key' => 'date', 'sort_key' => 'date', 'label' => 'Data', 'class' => 'text-center', 'sortable' => true],
['key' => 'action', 'sort_key' => 'action', 'label' => 'Akcja', 'sortable' => true],
['key' => 'order_id', 'sort_key' => 'order_id', 'label' => 'Zamowienie', 'class' => 'text-center', 'sortable' => true],
['key' => 'message', 'sort_key' => 'message', 'label' => 'Wiadomosc', 'sortable' => true],
['key' => 'context', 'label' => 'Kontekst', 'sortable' => false, 'raw' => true],
],
$rows,
$listRequest['viewFilters'],
[
'column' => $listRequest['sortColumn'],
'dir' => $listRequest['sortDir'],
],
[
'page' => $listRequest['page'],
'per_page' => $listRequest['perPage'],
'total' => $total,
'total_pages' => $totalPages,
],
array_merge( $listRequest['queryFilters'], [
'sort' => $listRequest['sortColumn'],
'dir' => $listRequest['sortDir'],
'per_page' => $listRequest['perPage'],
] ),
$listRequest['perPageOptions'],
$sortableColumns,
'/admin/integrations/logs/',
'Brak wpisow w logach.'
);
return \Shared\Tpl\Tpl::view( 'integrations/logs', [
'viewModel' => $viewModel,
] );
}
public function logs_clear(): void
{
$this->repository->clearLogs();
\Shared\Helpers\Helpers::alert( 'Logi zostaly wyczyszczone.' );
header( 'Location: /admin/integrations/logs/' );
exit;
}
public function apilo_settings(): string
{
return \Shared\Tpl\Tpl::view( 'integrations/apilo-settings', [

View File

@@ -73,26 +73,45 @@ class SettingsController
*/
public function globalSearchAjax(): void
{
global $mdb;
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$phrase = trim((string)\Shared\Helpers\Helpers::get('q'));
if ($phrase === '' || mb_strlen($phrase) < 2) {
try {
$this->executeGlobalSearch();
} catch (\Throwable $e) {
echo json_encode([
'status' => 'ok',
'status' => 'error',
'items' => [],
]);
exit;
}
exit;
}
private function executeGlobalSearch(): void
{
global $mdb;
$phrase = isset($_REQUEST['q']) ? trim((string)$_REQUEST['q']) : '';
if ($phrase === '' || mb_strlen($phrase) < 2) {
echo json_encode(['status' => 'ok', 'items' => []]);
return;
}
$phrase = mb_substr($phrase, 0, 120);
$phraseNormalized = preg_replace('/\s+/', ' ', $phrase);
$phraseNormalized = trim((string)$phraseNormalized);
$phraseNormalized = trim((string)preg_replace('/\s+/', ' ', $phrase));
$like = '%' . $phrase . '%';
$likeNormalized = '%' . $phraseNormalized . '%';
$items = [];
$defaultLang = (string)$this->languagesRepository->defaultLanguage();
$defaultLang = '1';
try {
$defaultLang = (string)$this->languagesRepository->defaultLanguage();
} catch (\Throwable $e) {
// fallback to '1'
}
// --- Produkty ---
try {
$productStmt = $mdb->query(
'SELECT '
@@ -115,7 +134,10 @@ class SettingsController
$productStmt = false;
}
$productRows = $productStmt ? $productStmt->fetchAll() : [];
$productRows = ($productStmt && method_exists($productStmt, 'fetchAll'))
? $productStmt->fetchAll(\PDO::FETCH_ASSOC)
: [];
if (is_array($productRows)) {
foreach ($productRows as $row) {
$productId = (int)($row['id'] ?? 0);
@@ -147,6 +169,7 @@ class SettingsController
}
}
// --- Zamowienia ---
try {
$orderStmt = $mdb->query(
'SELECT '
@@ -178,7 +201,10 @@ class SettingsController
$orderStmt = false;
}
$orderRows = $orderStmt ? $orderStmt->fetchAll() : [];
$orderRows = ($orderStmt && method_exists($orderStmt, 'fetchAll'))
? $orderStmt->fetchAll(\PDO::FETCH_ASSOC)
: [];
if (is_array($orderRows)) {
foreach ($orderRows as $row) {
$orderId = (int)($row['id'] ?? 0);
@@ -214,11 +240,12 @@ class SettingsController
}
}
echo json_encode([
'status' => 'ok',
'items' => array_slice($items, 0, 20),
]);
exit;
$json = json_encode(['status' => 'ok', 'items' => array_slice($items, 0, 20)]);
if ($json === false) {
echo json_encode(['status' => 'ok', 'items' => []], JSON_UNESCAPED_UNICODE);
return;
}
echo $json;
}
/**

View File

@@ -69,7 +69,9 @@ class ShopOrderController
$listRequest['perPage']
);
$statusesMap = $this->service->statuses();
$statusData = $this->service->statusData();
$statusesMap = $statusData['names'];
$statusColorsMap = $statusData['colors'];
$rows = [];
$lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1;
@@ -77,7 +79,15 @@ class ShopOrderController
$orderId = (int)($item['id'] ?? 0);
$orderNumber = (string)($item['number'] ?? '');
$statusId = (int)($item['status'] ?? 0);
$statusLabel = (string)($statusesMap[$statusId] ?? ('Status #' . $statusId));
$statusLabel = htmlspecialchars((string)($statusesMap[$statusId] ?? ('Status #' . $statusId)), ENT_QUOTES, 'UTF-8');
$statusColor = isset($statusColorsMap[$statusId]) ? $statusColorsMap[$statusId] : '';
if ($statusColor !== '') {
$textColor = $this->contrastTextColor($statusColor);
$statusHtml = '<span class="label" style="background-color:' . htmlspecialchars($statusColor, ENT_QUOTES, 'UTF-8') . ';color:' . $textColor . '">' . $statusLabel . '</span>';
} else {
$statusHtml = $statusLabel;
}
$rows[] = [
'lp' => $lp++ . '.',
@@ -86,13 +96,13 @@ class ShopOrderController
'paid' => ((int)($item['paid'] ?? 0) === 1)
? '<i class="fa fa-check text-success"></i>'
: '<i class="fa fa-times text-dark"></i>',
'status' => htmlspecialchars($statusLabel, ENT_QUOTES, 'UTF-8'),
'status' => $statusHtml,
'summary' => number_format((float)($item['summary'] ?? 0), 2, '.', ' ') . ' zł',
'client' => htmlspecialchars((string)($item['client'] ?? ''), ENT_QUOTES, 'UTF-8') . ' | zamówienia: <strong>' . (int)($item['total_orders'] ?? 0) . '</strong>',
'address' => (string)($item['address'] ?? ''),
'order_email' => (string)($item['order_email'] ?? ''),
'client_phone' => (string)($item['client_phone'] ?? ''),
'transport' => (string)($item['transport'] ?? ''),
'transport' => $this->sanitizeInlineHtml((string)($item['transport'] ?? '')),
'payment_method' => (string)($item['payment_method'] ?? ''),
'_actions' => [
[
@@ -127,7 +137,7 @@ class ShopOrderController
['key' => 'address', 'label' => 'Adres', 'sortable' => false],
['key' => 'order_email', 'sort_key' => 'order_email', 'label' => 'Email', 'sortable' => true],
['key' => 'client_phone', 'sort_key' => 'client_phone', 'label' => 'Telefon', 'sortable' => true],
['key' => 'transport', 'sort_key' => 'transport', 'label' => 'Dostawa', 'sortable' => true],
['key' => 'transport', 'sort_key' => 'transport', 'label' => 'Dostawa', 'sortable' => true, 'raw' => true],
['key' => 'payment_method', 'sort_key' => 'payment_method', 'label' => 'Płatność', 'sortable' => true],
],
$rows,
@@ -361,4 +371,26 @@ class ShopOrderController
return date('Y-m-d H:i', $ts);
}
}
private function contrastTextColor(string $hex): string
{
$hex = ltrim($hex, '#');
if (strlen($hex) === 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
if (strlen($hex) !== 6) {
return '#fff';
}
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
$luminance = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255;
return $luminance > 0.5 ? '#000' : '#fff';
}
private function sanitizeInlineHtml(string $html): string
{
$html = strip_tags($html, '<b><strong><i><em>');
return preg_replace('/<(b|strong|i|em)\s[^>]*>/i', '<$1>', $html);
}
}

View File

@@ -95,7 +95,7 @@ class ShopProductController
. '<a href="/admin/shop_product/product_edit/id=' . $id . '">' . $name . '</a> '
. '<a href="#" class="text-muted duplicate-product" product-id="' . $id . '">duplikuj</a>'
. '</div>'
. '<small class="text-muted product-categories">' . $categories . '</small>'
. '<small class="text-muted product-categories product-categories--cats" title="' . $categories . '">' . $categories . '</small>'
. '<small class="text-muted product-categories">SKU: ' . $sku . ', EAN: ' . $ean . '</small>';
$priceHtml = '<input type="text" class="product-price form-control text-right" product-id="' . $id . '" value="' . htmlspecialchars( (string) $product['price_brutto'], ENT_QUOTES, 'UTF-8' ) . '" style="width: 75px;">';
@@ -688,7 +688,7 @@ class ShopProductController
foreach ( $products as $key => $val ) {
if ( (int) $key !== $productId ) {
$selected = ( is_array( $product['products_related'] ?? null ) && in_array( $key, $product['products_related'] ) ) ? ' selected' : '';
$html .= '<option value="' . (int) $key . '"' . $selected . '>' . $this->escapeHtml( $val ) . '</option>';
$html .= '<option value="' . (int) $key . '"' . $selected . '>' . $this->escapeHtml( (string) $val ) . '</option>';
}
}
$html .= '</select></div></div>';

View File

@@ -1,5 +1,6 @@
<?php
$database['host'] = 'localhost';
$database['remote_host'] = 'host700513.hostido.net.pl';
$database['user'] = 'host117523_shoppro';
$database['password'] = 'mhA9WCEXEnRfTtbN33hL';
$database['name'] = 'host117523_shoppro';

View File

@@ -201,6 +201,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
{
$products = $mdb -> select( 'pp_shop_order_products', '*', [ 'order_id' => $order['id'] ] );
$products_array = [];
$order_message = '';
foreach ( $products as $product )
{
$productRepo = new \Domain\Product\ProductRepository( $mdb );
@@ -211,8 +212,8 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
'ean' => $productRepo->getEanWithFallback( (int)$product['product_id'], true ),
'sku' => $sku ? $sku : md5( $product['product_id'] ),
'originalName' => $product['name'],
'originalPriceWithTax' => $product['price_brutto_promo'] ? str_replace( ',', '.', $product['price_brutto_promo'] ) : str_replace( ',', '.', $product['price_brutto'] ),
'originalPriceWithoutTax' => $product['price_brutto_promo'] ? str_replace( ',', '.', round( $product['price_brutto_promo'] / ( 1 + $product['vat']/100 ), 2 ) ) : str_replace( ',', '.', round( $product['price_brutto'] / ( 1 + $product['vat']/100 ), 2 ) ),
'originalPriceWithTax' => (float)$product['price_brutto_promo'] > 0 ? str_replace( ',', '.', $product['price_brutto_promo'] ) : str_replace( ',', '.', $product['price_brutto'] ),
'originalPriceWithoutTax' => (float)$product['price_brutto_promo'] > 0 ? str_replace( ',', '.', round( $product['price_brutto_promo'] / ( 1 + $product['vat']/100 ), 2 ) ) : str_replace( ',', '.', round( $product['price_brutto'] / ( 1 + $product['vat']/100 ), 2 ) ),
'quantity' => $product['quantity'],
'tax' => number_format( $product['vat'], 2, '.', '' ),
'status' => 1,
@@ -255,6 +256,25 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
'media' => null
];
// Walidacja: sprawdź czy zamówienie ma produkty z cenami > 0
$has_priced_products = false;
foreach ( $products_array as $pa )
{
if ( $pa['type'] == 1 && (float)$pa['originalPriceWithTax'] > 0 )
{
$has_priced_products = true;
break;
}
}
if ( !$has_priced_products )
{
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Pominięto zamówienie - wszystkie produkty mają cenę 0.00', [ 'products' => $products_array ] );
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Apilo: zamówienie #' . $order['id'] . ' ma zerowe ceny produktów', 'Zamówienie #' . $order['id'] . ' nie zostało wysłane do Apilo, ponieważ wszystkie produkty mają cenę 0.00 PLN. Sprawdź zamówienie w panelu sklepu.' );
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => -2 ], [ 'id' => $order['id'] ] );
echo '<p>Pominięto zamówienie #' . $order['id'] . ' - zerowe ceny produktów</p>';
continue;
}
$access_token = $integrationsRepository -> apiloGetAccessToken();
$order_date = new DateTime( $order['date_order'] );
@@ -406,8 +426,11 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
$response = curl_exec( $ch );
if (curl_errno( $ch ) ) {
echo 'Błąd cURL: ' . curl_error( $ch );
$curl_error_send = curl_error( $ch );
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd cURL przy wysyłaniu zamówienia: ' . $curl_error_send, [ 'curl_error' => $curl_error_send ] );
echo 'Błąd cURL: ' . $curl_error_send;
}
$http_code_send = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
$response = json_decode( $response, true );
@@ -423,6 +446,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
{
$apilo_order_id = str_replace( 'Order id: ', '', $response['description'] );
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie już istnieje w Apilo (apilo_order_id: ' . $apilo_order_id . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
echo '<p>Zaktualizowałem id zamówienia na podstawie zamówienia apilo.com</p>';
}
elseif ( $response['message'] == 'Validation error' )
@@ -462,6 +486,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
{
$apilo_order_id = $get_response_data['list'][0]['id'];
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Duplikat idExternal - pobrano apilo_order_id: ' . $apilo_order_id, [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
echo '<p>Zamówienie już istnieje w Apilo. Zaktualizowano ID zamówienia: ' . $apilo_order_id . '</p>';
}
else
@@ -471,6 +496,8 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
echo print_r( $postData, true );
echo '</pre>';
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd: duplikat idExternal, ale nie znaleziono zamówienia w Apilo', [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
$email_data = print_r( $response, true );
$email_data .= print_r( $postData, true );
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com - nie znaleziono zamówienia', $email_data );
@@ -483,17 +510,37 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
echo print_r( $postData, true );
echo '</pre>';
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd walidacji wysyłania zamówienia do Apilo', [ 'http_code' => $http_code_send, 'response' => $response ] );
$email_data = print_r( $response, true );
$email_data .= print_r( $postData, true );
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com', $email_data );
}
}
elseif ( $http_code_send >= 400 || !isset( $response['id'] ) )
{
// Błąd serwera lub brak ID w odpowiedzi — logujemy i pomijamy, NIE ustawiamy apilo_order_id
// żeby zamówienie nie wpadło w nieskończoną pętlę, ustawiamy apilo_order_id na -1 (błąd)
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => -1 ], [ 'id' => $order['id'] ] );
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd wysyłania zamówienia do Apilo (HTTP ' . $http_code_send . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
$email_data = 'HTTP Code: ' . $http_code_send . "\n\n";
$email_data .= print_r( $response, true );
$email_data .= print_r( $postData, true );
\Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia #' . $order['id'] . ' do apilo.com (HTTP ' . $http_code_send . ')', $email_data );
echo '<p>Błąd wysyłania zamówienia do apilo.com: ID: ' . $order['id'] . ' (HTTP ' . $http_code_send . ')</p>';
}
else
{
$mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => $response['id'] ], [ 'id' => $order['id'] ] );
\Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie wysłane do Apilo (apilo_order_id: ' . $response['id'] . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
echo '<p>Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '</p>';
}
}
// Po wysłaniu zamówień: przetwórz kolejkę sync (płatności/statusy oczekujące na apilo_order_id)
$orderAdminService->processApiloSyncQueue( 10 );
}
// sprawdzanie statusów zamówień w apilo.com jeżeli zamówienie nie jest zrealizowane, anulowane lub nieodebrane
@@ -515,6 +562,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
] );
$response = curl_exec( $ch );
$http_code_poll = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$responseData = json_decode( $response, true );
if ( $responseData['id'] and $responseData['status'] )
@@ -524,6 +572,8 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
if ( $shop_status_id )
$orderAdminService->changeStatus( (int)$order['id'], $shop_status_id, false );
\Domain\Integrations\ApiloLogger::log( $mdb, 'status_poll', (int)$order['id'], 'Status pobrany z Apilo (apilo_status: ' . $responseData['status'] . ', shop_status: ' . ($shop_status_id ?: 'brak mapowania') . ')', [ 'apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code_poll, 'response' => $responseData ] );
$orderRepo->updateApiloStatusDate( (int)$order['id'], date( 'Y-m-d H:i:s' ) );
echo '<p>Zaktualizowałem status zamówienia <b>' . $order['number'] . '</b></p>';
}

View File

@@ -4,6 +4,73 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
## ver. 0.314 (2026-02-23) - Fix wyszukiwarki admin + title zamówienia
- **FIX**: Globalna wyszukiwarka w panelu admina przestała zwracać wyniki — dodano `Content-Type: application/json` i `Cache-Control: no-store` (zapobiega cache'owaniu przez proxy/CDN), zmiana AJAX z GET na POST, `fetchAll(PDO::FETCH_ASSOC)`, top-level try/catch z gwarantowaną odpowiedzią JSON
- **NEW**: `document.title` w widoku szczegółów zamówienia pokazuje numer zamówienia (np. "Zamówienie ZAM/123 - shopPro")
---
## ver. 0.313 (2026-02-23) - Fix sync płatności Apilo + logowanie
- **FIX**: `syncApiloPayment()` i `syncApiloStatus()``(int)` cast na `apilo_order_id` (format `"PPxxxxxx"`) dawał `0`, przez co metody pomijały sync z API Apilo. Zmiana na `empty()`
- **NEW**: Logowanie w `syncApiloPaymentIfNeeded()` i `syncApiloStatusIfNeeded()` — każda ścieżka decyzyjna (Apilo wyłączone, brak tokenu, brak `apilo_order_id`, sync nieudany) zapisuje wpis do `pp_log` z kontekstem
---
## ver. 0.312 (2026-02-23) - Fix krytycznych bugów integracji Apilo
- **FIX**: `curl_getinfo()` wywoływane po `curl_close()` — HTTP code zawsze wynosił 0, uniemożliwiając prawidłową obsługę odpowiedzi Apilo
- **FIX**: Nieskończona pętla wysyłania zamówienia — gdy Apilo zwracało błąd serwera, zamówienie nie dostawało `apilo_order_id` i było ponownie wybierane w każdym cyklu crona. Teraz błędne zamówienia oznaczane `apilo_order_id = -1` z powiadomieniem email
- **FIX**: Ceny produktów 0.00 PLN w Apilo — string `"0.00"` z MySQL jest truthy w PHP, więc ternary wybierał `price_brutto_promo` (0.00) zamiast `price_brutto`. Zmiana na `(float)... > 0`
- **FIX**: Walidacja cen przed wysyłką — zamówienia z zerowymi cenami produktów nie są wysyłane do Apilo (`apilo_order_id = -2`) z powiadomieniem email
- **FIX**: Niezainicjalizowana zmienna `$order_message` powodująca PHP warning
---
## ver. 0.311 (2026-02-23) - Fix race condition Apilo + persistence filtrów + poprawki cen
- **FIX**: Race condition — callback płatności przed wysłaniem zamówienia do Apilo nie synchronizował płatności (task trafiał w pustkę). Teraz `syncApiloPaymentIfNeeded` i `syncApiloStatusIfNeeded` kolejkują sync do retry gdy `apilo_order_id` jeszcze nie istnieje
- **FIX**: `processApiloSyncQueue` — zamówienia bez `apilo_order_id` były usuwane z kolejki bez synchronizacji. Teraz czekają (max 50 prób ~8h) aż cron wyśle zamówienie do Apilo
- **FIX**: Drugie wywołanie `processApiloSyncQueue` w cronie po wysyłce zamówień — sync płatności/statusów w tym samym cyklu
- **FIX**: Ceny w szczegółach zamówienia (admin + frontend) — gdy `price_brutto_promo` = 0 lub >= ceny regularnej, wyświetla cenę regularną zamiast 0 zł
- **NEW**: Persistence filtrów tabel w panelu admin — localStorage zapamiętuje ostatni widok (filtry, sortowanie, paginacja) i przywraca go przy powrocie do listy. Przycisk "Wyczyść" resetuje zapisany stan
---
## ver. 0.310 (2026-02-23) - Logi integracji w panelu admin
- **NEW**: Zakładka "Logi" w sekcji Integracje — podgląd tabeli `pp_log` z paginacją, sortowaniem, filtrami (akcja, wiadomość, ID zamówienia) i rozwijalnym kontekstem JSON
- **NEW**: `IntegrationsRepository::getLogs()`, `deleteLog()`, `clearLogs()` — metody do obsługi logów
- **NEW**: `IntegrationsController::logs()`, `logs_clear()` — akcje kontrolera
- **NEW**: Przycisk "Wyczyść wszystkie logi" z potwierdzeniem
---
## ver. 0.309 (2026-02-23) - ApiloLogger + cache-busting CSS/JS + poprawki UI
- **NEW**: `ApiloLogger` — logowanie operacji Apilo do tabeli `pp_log` z kontekstem JSON (send_order, resend_order, payment_sync, status_sync, status_poll)
- **NEW**: Migracja `pp_log` — kolumny `action`, `order_id`, `context` + indeksy
- **NEW**: Cache-busting dla CSS i JS w admin panelu — `?ver=filemtime()` przy wszystkich lokalnych zasobach w `main-layout.php`
- **FIX**: Przeniesienie inicjalizacji `$mdb` przed `SettingsRepository` w `admin/index.php`
- **FIX**: Rzutowanie na `(string)` w `ShopProductController::escapeHtml()` — zapobiega warningom
- **ZMIANA**: Skrocone kategorie produktow na liscie — `text-overflow: ellipsis` z `title` tooltip
- **ZMIANA**: `copyToClipboard()` — uzywa `navigator.clipboard` API z fallbackiem na `<textarea>`
- **ZMIANA**: Uproszczenie CSS `.site-content` — usuniety zbedny `.with-menu`, menu widoczne zawsze na desktop
---
## ver. 0.308 (2026-02-22) - Kolory statusow zamowien + poprawki bezpieczenstwa
- **NEW**: Kolorowe badge statusow na liscie zamowien w admin panelu — kolory pobierane z `pp_shop_statuses.color`, kontrast tekstu obliczany automatycznie
- **FIX**: Walidacja formatu hex koloru z bazy (`/^#[0-9a-fA-F]{3,6}$/`) — odrzucanie nieprawidlowych wartosci
- **FIX**: Sanityzacja HTML w kolumnie "Dostawa" — `strip_tags()` + regex usuwajacy atrybuty z dozwolonych tagow (zapobieganie XSS via `onclick` itp.)
- **OPTYMALIZACJA**: Polaczenie dwoch zapytan SQL do `pp_shop_statuses` (nazwy + kolory) w jedno `orderStatusData()`
- **ZMIANA**: Path-based form submit w `table-list.php` — formularze filtrow i per-page uzywaja JS interceptora z `data-path-submit` zamiast natywnego GET, kompatybilne z admin URL routing
- **NEW**: 11 nowych testow jednostkowych (750 total, 2114 assertions)
---
## ver. 0.307 (2026-02-22) - Przycisk sprawdzania aktualizacji + auto-changelog
- **NEW**: Przycisk "Sprawdz aktualizacje" w panelu admina — ikona odswiezenia obok numeru wersji, klik odpytuje serwer AJAX-em i pokazuje/ukrywa badge "aktualizacja" bez przeladowania strony

View File

@@ -23,7 +23,7 @@ composer test # standard
## Aktualny stan
```text
OK (739 tests, 2089 assertions)
OK (758 tests, 2135 assertions)
```
Zweryfikowano: 2026-02-22 (ver. 0.304)

1
docs/TODO.md Normal file
View File

@@ -0,0 +1 @@
1. Może warto przepisać zadania cron, na tabelę, tak żeby zadania cron się kolejkowały i wykonywały za podstawie wpisów z bazy danych, takż żeby własnie nie było sytuacji, że zamówienie będzie próbowało zmienić status w apilo, zanim to zamówienie tam trafi. Czy wszystkie takie zadania trafiałyby do kolejki i wykonywałyby się chronologicznie. Ewentualnie jakieś inne podejście możesz zaproponować.

View File

@@ -1,10 +1,20 @@
window.copyToClipboard = function( element )
{
var $temp = $( "<input>" );
$( "#" + element ).after( $temp );
$temp.val( $( '#' + element ).text() ).select();
document.execCommand( "copy" );
$temp.remove();
var text = $( '#' + element ).text().trim();
if ( navigator.clipboard && navigator.clipboard.writeText )
{
navigator.clipboard.writeText( text );
}
else
{
var $temp = $( "<textarea>" );
$temp.css({ position: 'fixed', left: '-9999px', top: '-9999px' });
$( "body" ).append( $temp );
$temp.val( text ).select();
document.execCommand( "copy" );
$temp.remove();
}
}
function checkForm(id)

View File

@@ -1,2 +1,2 @@
ALTER TABLE pp_shop_payment_methods ADD COLUMN min_order_amount DECIMAL(10,2) DEFAULT NULL;
ALTER TABLE pp_shop_payment_methods ADD COLUMN min_order_amount DECIMAL(10,2) DEFAULT NULL;
ALTER TABLE pp_shop_payment_methods ADD COLUMN max_order_amount DECIMAL(10,2) DEFAULT NULL;

5
migrations/0.309.sql Normal file
View File

@@ -0,0 +1,5 @@
ALTER TABLE pp_log ADD COLUMN `action` VARCHAR(100) NULL DEFAULT NULL AFTER `id`;
ALTER TABLE pp_log ADD COLUMN `order_id` INT NULL DEFAULT NULL AFTER `action`;
ALTER TABLE pp_log ADD COLUMN `context` TEXT NULL DEFAULT NULL AFTER `message`;
ALTER TABLE pp_log ADD INDEX `idx_action` (`action`);
ALTER TABLE pp_log ADD INDEX `idx_order_id` (`order_id`);

View File

@@ -179,7 +179,7 @@
'id': <?= (int)$product['product_id'];?>,
'name': '<?= $product['name'];?>',
'quantity': <?= $product['quantity'];?>,
'price': <?= $product['price_brutto_promo'];?>
'price': <?= ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
}<? if ( $product != end( $this -> order['products'] ) ) echo ',';?>
<? endforeach;?>
]

View File

@@ -298,4 +298,69 @@ class IntegrationsRepositoryTest extends TestCase
$this->assertSame('1', (string)$result[0]['id']);
$this->assertSame('Przelew', (string)$result[0]['name']);
}
// ── Logs ────────────────────────────────────────────────────
public function testGetLogsReturnsItemsAndTotal(): void
{
$this->mockDb->expects($this->once())
->method('count')
->with('pp_log', $this->anything())
->willReturn(2);
$this->mockDb->expects($this->once())
->method('select')
->with('pp_log', '*', $this->anything())
->willReturn([
['id' => 1, 'action' => 'send_order', 'message' => 'OK', 'date' => '2026-01-01 12:00:00'],
['id' => 2, 'action' => 'status_sync', 'message' => 'Synced', 'date' => '2026-01-02 12:00:00'],
]);
$result = $this->repository->getLogs([], 'id', 'DESC', 1, 15);
$this->assertIsArray($result);
$this->assertArrayHasKey('items', $result);
$this->assertArrayHasKey('total', $result);
$this->assertCount(2, $result['items']);
$this->assertSame(2, $result['total']);
}
public function testGetLogsReturnsEmptyWhenNoResults(): void
{
$this->mockDb->method('count')->willReturn(0);
$this->mockDb->method('select')->willReturn([]);
$result = $this->repository->getLogs([], 'id', 'DESC', 1, 15);
$this->assertSame(0, $result['total']);
$this->assertEmpty($result['items']);
}
public function testGetLogsHandlesNullFromSelect(): void
{
$this->mockDb->method('count')->willReturn(0);
$this->mockDb->method('select')->willReturn(null);
$result = $this->repository->getLogs([], 'id', 'DESC', 1, 15);
$this->assertSame([], $result['items']);
}
public function testDeleteLogCallsDelete(): void
{
$this->mockDb->expects($this->once())
->method('delete')
->with('pp_log', ['id' => 42]);
$this->assertTrue($this->repository->deleteLog(42));
}
public function testClearLogsDeletesAll(): void
{
$this->mockDb->expects($this->once())
->method('delete')
->with('pp_log', []);
$this->assertTrue($this->repository->clearLogs());
}
}

View File

@@ -227,4 +227,110 @@ class OrderAdminServiceTest extends TestCase
$service = $this->createService(null, null, $settingsRepo);
$this->assertSame(150.0, $service->getFreeDeliveryThreshold());
}
// =========================================================================
// processApiloSyncQueue — awaiting apilo_order_id
// =========================================================================
private function getQueuePath(): string
{
// Musi odpowiadać ścieżce w OrderAdminService::apiloSyncQueuePath()
// dirname(autoload/Domain/Order/, 2) = autoload/
return dirname(__DIR__, 4) . '/autoload/temp/apilo-sync-queue.json';
}
private function writeQueue(array $queue): void
{
$path = $this->getQueuePath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($path, json_encode($queue, JSON_PRETTY_PRINT));
}
private function readQueue(): array
{
$path = $this->getQueuePath();
if (!file_exists($path)) return [];
$content = file_get_contents($path);
return $content ? json_decode($content, true) : [];
}
protected function tearDown(): void
{
$path = $this->getQueuePath();
if (file_exists($path)) {
unlink($path);
}
parent::tearDown();
}
public function testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull(): void
{
// Zamówienie bez apilo_order_id — task powinien zostać w kolejce
$this->writeQueue([
'42' => [
'order_id' => 42,
'payment' => 1,
'status' => null,
'attempts' => 0,
'last_error' => 'awaiting_apilo_order',
'updated_at' => '2026-01-01 00:00:00',
],
]);
$orderRepo = $this->createMock(OrderRepository::class);
$orderRepo->method('findRawById')
->with(42)
->willReturn([
'id' => 42,
'apilo_order_id' => null,
'paid' => 1,
'summary' => '100.00',
]);
$service = new OrderAdminService($orderRepo);
$processed = $service->processApiloSyncQueue(10);
$this->assertSame(1, $processed);
$queue = $this->readQueue();
$this->assertArrayHasKey('42', $queue);
$this->assertSame('awaiting_apilo_order', $queue['42']['last_error']);
$this->assertSame(1, $queue['42']['attempts']);
}
public function testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts(): void
{
// Task z 49 próbami — limit to 50, więc powinien zostać usunięty
$this->writeQueue([
'42' => [
'order_id' => 42,
'payment' => 1,
'status' => null,
'attempts' => 49,
'last_error' => 'awaiting_apilo_order',
'updated_at' => '2026-01-01 00:00:00',
],
]);
$orderRepo = $this->createMock(OrderRepository::class);
$orderRepo->method('findRawById')
->with(42)
->willReturn([
'id' => 42,
'apilo_order_id' => null,
'paid' => 1,
'summary' => '100.00',
]);
$service = new OrderAdminService($orderRepo);
$processed = $service->processApiloSyncQueue(10);
$this->assertSame(1, $processed);
$queue = $this->readQueue();
$this->assertArrayNotHasKey('42', $queue);
}
}

View File

@@ -29,6 +29,66 @@ class OrderRepositoryTest extends TestCase
$this->assertSame('W realizacji', $statuses[4]);
}
public function testOrderStatusDataReturnsBothNamesAndColors(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('select')
->willReturnCallback(function ($table, $columns, $where) {
if ($table === 'pp_shop_statuses') {
return [
['id' => 0, 'status' => 'Nowe', 'color' => '#ff0000'],
['id' => 4, 'status' => 'W realizacji', 'color' => '#00ff00'],
['id' => 5, 'status' => 'Wysłane', 'color' => ''],
];
}
return [];
});
$repository = new OrderRepository($mockDb);
$data = $repository->orderStatusData();
$this->assertArrayHasKey('names', $data);
$this->assertArrayHasKey('colors', $data);
$this->assertSame('Nowe', $data['names'][0]);
$this->assertSame('W realizacji', $data['names'][4]);
$this->assertSame('Wysłane', $data['names'][5]);
$this->assertSame('#ff0000', $data['colors'][0]);
$this->assertSame('#00ff00', $data['colors'][4]);
$this->assertArrayNotHasKey(5, $data['colors']);
}
public function testOrderStatusDataFiltersInvalidHexColors(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('select')
->willReturn([
['id' => 1, 'status' => 'OK', 'color' => '#abc'],
['id' => 2, 'status' => 'Bad', 'color' => 'red'],
['id' => 3, 'status' => 'XSS', 'color' => '#000" onclick="alert(1)'],
['id' => 4, 'status' => 'Valid', 'color' => '#AABBCC'],
]);
$repository = new OrderRepository($mockDb);
$data = $repository->orderStatusData();
$this->assertSame('#abc', $data['colors'][1]);
$this->assertArrayNotHasKey(2, $data['colors']);
$this->assertArrayNotHasKey(3, $data['colors']);
$this->assertSame('#AABBCC', $data['colors'][4]);
}
public function testOrderStatusDataReturnsEmptyOnDbFailure(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('select')->willReturn(false);
$repository = new OrderRepository($mockDb);
$data = $repository->orderStatusData();
$this->assertSame([], $data['names']);
$this->assertSame([], $data['colors']);
}
public function testNextAndPrevOrderIdReturnNullForInvalidInput(): void
{
$mockDb = $this->createMock(\medoo::class);

View File

@@ -35,6 +35,33 @@ class IntegrationsControllerTest extends TestCase
);
}
public function testHasLogsMethods(): void
{
$methods = [
'logs',
'logs_clear',
];
foreach ($methods as $method) {
$this->assertTrue(
method_exists($this->controller, $method),
"Method $method does not exist"
);
}
}
public function testLogsReturnsString(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('string', (string) $reflection->getMethod('logs')->getReturnType());
}
public function testLogsClearReturnsVoid(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string) $reflection->getMethod('logs_clear')->getReturnType());
}
public function testHasAllApiloSettingsMethods(): void
{
$methods = [

View File

@@ -85,4 +85,72 @@ class ShopOrderControllerTest extends TestCase
$this->assertEquals('Domain\\Product\\ProductRepository', $params[1]->getType()->getName());
$this->assertTrue($params[1]->isOptional());
}
// --- contrastTextColor tests (via reflection) ---
public function testContrastTextColorReturnsBlackForLightColor(): void
{
$result = $this->invokePrivate('contrastTextColor', ['#ffffff']);
$this->assertSame('#000', $result);
}
public function testContrastTextColorReturnsWhiteForDarkColor(): void
{
$result = $this->invokePrivate('contrastTextColor', ['#000000']);
$this->assertSame('#fff', $result);
}
public function testContrastTextColorHandlesShortHex(): void
{
$result = $this->invokePrivate('contrastTextColor', ['#fff']);
$this->assertSame('#000', $result);
$result = $this->invokePrivate('contrastTextColor', ['#000']);
$this->assertSame('#fff', $result);
}
public function testContrastTextColorDefaultsToWhiteForInvalidHex(): void
{
$result = $this->invokePrivate('contrastTextColor', ['invalid']);
$this->assertSame('#fff', $result);
$result = $this->invokePrivate('contrastTextColor', ['#zz']);
$this->assertSame('#fff', $result);
}
// --- sanitizeInlineHtml tests (via reflection) ---
public function testSanitizeInlineHtmlStripsDisallowedTags(): void
{
$result = $this->invokePrivate('sanitizeInlineHtml', ['<b>Bold</b> <script>alert(1)</script> <em>Italic</em>']);
$this->assertSame('<b>Bold</b> alert(1) <em>Italic</em>', $result);
}
public function testSanitizeInlineHtmlStripsAttributesFromAllowedTags(): void
{
$result = $this->invokePrivate('sanitizeInlineHtml', ['<b onclick="alert(1)">Bold</b>']);
$this->assertSame('<b>Bold</b>', $result);
$result = $this->invokePrivate('sanitizeInlineHtml', ['<strong style="color:red" class="x">text</strong>']);
$this->assertSame('<strong>text</strong>', $result);
}
public function testSanitizeInlineHtmlPreservesCleanTags(): void
{
$result = $this->invokePrivate('sanitizeInlineHtml', ['<b>Bold</b> <i>Italic</i> <strong>Strong</strong> <em>Em</em>']);
$this->assertSame('<b>Bold</b> <i>Italic</i> <strong>Strong</strong> <em>Em</em>', $result);
}
public function testSanitizeInlineHtmlHandlesPlainText(): void
{
$result = $this->invokePrivate('sanitizeInlineHtml', ['Kurier DPD']);
$this->assertSame('Kurier DPD', $result);
}
private function invokePrivate(string $method, array $args)
{
$reflection = new \ReflectionMethod($this->controller, $method);
$reflection->setAccessible(true);
return $reflection->invokeArgs($this->controller, $args);
}
}

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

Binary file not shown.

View File

@@ -0,0 +1,24 @@
{
"changelog": "NEW - przycisk Sprawdz aktualizacje w panelu admina, NEW - auto-generowany changelog z manifestow",
"version": "0.307",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"admin/templates/site/main-layout.php",
"autoload/admin/Controllers/UpdateController.php"
]
},
"checksum_zip": "sha256:175c64537f0238a03903758c2d8ab154277b3889d35551ede17d158e3f62de7d",
"sql": [
],
"date": "2026-02-22",
"directories_deleted": [
]
}

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

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"changelog": "NEW - kolorowe badge statusow zamowien, walidacja hex, sanityzacja HTML transport, optymalizacja SQL",
"version": "0.308",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"admin/templates/components/table-list.php",
"autoload/Domain/Order/OrderAdminService.php",
"autoload/Domain/Order/OrderRepository.php",
"autoload/admin/Controllers/ShopOrderController.php"
]
},
"checksum_zip": "sha256:01bd6e18f737db848913f334a7af78aef729d37b741803f3cbacb7e379969d0e",
"sql": [
],
"date": "2026-02-22",
"directories_deleted": [
]
}

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

Binary file not shown.

View File

@@ -0,0 +1,35 @@
{
"changelog": "NEW - ApiloLogger (logowanie operacji Apilo do pp_log), cache-busting CSS/JS w admin panelu, poprawki UI listy produktow, clipboard API",
"version": "0.309",
"files": {
"added": [
"autoload/Domain/Integrations/ApiloLogger.php"
],
"deleted": [
],
"modified": [
"admin/index.php",
"admin/layout/style-css/style.css",
"admin/layout/style-css/style.css.map",
"admin/layout/style-scss/style.scss",
"admin/templates/site/main-layout.php",
"autoload/Domain/Order/OrderAdminService.php",
"autoload/admin/Controllers/ShopProductController.php",
"cron.php",
"libraries/functions.js"
]
},
"checksum_zip": "sha256:87a3db1a6038da742d21d92b65c21156493af52543b3810694ea91e000acf920",
"sql": [
"ALTER TABLE pp_log ADD COLUMN `action` VARCHAR(100) NULL DEFAULT NULL AFTER `id`;",
"ALTER TABLE pp_log ADD COLUMN `order_id` INT NULL DEFAULT NULL AFTER `action`;",
"ALTER TABLE pp_log ADD COLUMN `context` TEXT NULL DEFAULT NULL AFTER `message`;",
"ALTER TABLE pp_log ADD INDEX `idx_action` (`action`);",
"ALTER TABLE pp_log ADD INDEX `idx_order_id` (`order_id`);"
],
"date": "2026-02-23",
"directories_deleted": [
]
}

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

Binary file not shown.

View File

@@ -0,0 +1,25 @@
{
"changelog": "NEW - Zakladka Logi w sekcji Integracje (podglad pp_log z paginacja, sortowaniem, filtrami)",
"version": "0.310",
"files": {
"added": [
"admin/templates/integrations/logs.php"
],
"deleted": [
],
"modified": [
"admin/templates/site/main-layout.php",
"autoload/Domain/Integrations/IntegrationsRepository.php",
"autoload/admin/Controllers/IntegrationsController.php"
]
},
"checksum_zip": "sha256:e3b14e239230548aba203a83f01c91b00651e5114e92e162f6da7389c6a92975",
"sql": [
],
"date": "2026-02-23",
"directories_deleted": [
]
}

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

Binary file not shown.

View File

@@ -0,0 +1,27 @@
{
"changelog": "FIX - race condition callback płatności Apilo, persistence filtrów tabel admin, poprawki cen zamówień",
"version": "0.311",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"admin/templates/components/table-list.php",
"admin/templates/shop-order/order-details.php",
"autoload/Domain/Order/OrderAdminService.php",
"cron.php",
"templates/shop-order/order-details.php"
]
},
"checksum_zip": "sha256:542f599e844d48bba1378fbe91c06ef00d5487aca56af118ad9c9ea27343ebdb",
"sql": [
],
"date": "2026-02-23",
"directories_deleted": [
]
}

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

Binary file not shown.

View File

@@ -0,0 +1,23 @@
{
"changelog": "FIX - krytyczne bugi integracji Apilo: curl_getinfo po curl_close, nieskończona pętla wysyłki, ceny 0.00 PLN, walidacja cen",
"version": "0.312",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"cron.php"
]
},
"checksum_zip": "sha256:07f9efd02a6a83327ab8dd9403e0c072a5f38b680d6e3f6c67a96d2af8b8fc85",
"sql": [
],
"date": "2026-02-23",
"directories_deleted": [
]
}

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

Binary file not shown.

View File

@@ -0,0 +1,24 @@
{
"changelog": "FIX - sync płatności Apilo (int cast na apilo_order_id PPxxxxxx dawał 0) + logowanie decyzji sync do pp_log",
"version": "0.313",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"admin/templates/shop-order/order-details.php",
"autoload/Domain/Order/OrderAdminService.php"
]
},
"checksum_zip": "sha256:f344da1f3270abfc63653f8912ec1abbc006154db784cfee5a565fc0daaa75f8",
"sql": [
],
"date": "2026-02-23",
"directories_deleted": [
]
}

View File

@@ -1,3 +1,24 @@
<b>ver. 0.313 - 23.02.2026</b><br />
FIX - sync płatności Apilo (int cast na apilo_order_id PPxxxxxx dawał 0) + logowanie decyzji sync do pp_log
<hr>
<b>ver. 0.312 - 23.02.2026</b><br />
FIX - krytyczne bugi integracji Apilo: curl_getinfo po curl_close, nieskoĹ„czona pÄ™tla wysyĹki, ceny 0.00 PLN, walidacja cen
<hr>
<b>ver. 0.311 - 23.02.2026</b><br />
FIX - race condition callback pĹ‚atnoĹ›ci Apilo, persistence filtrÄĹw tabel admin, poprawki cen zamÄĹwieĹ„
<hr>
<b>ver. 0.310 - 23.02.2026</b><br />
NEW - Zakladka Logi w sekcji Integracje (podglad pp_log z paginacja, sortowaniem, filtrami)
<hr>
<b>ver. 0.309 - 23.02.2026</b><br />
NEW - ApiloLogger (logowanie operacji Apilo do pp_log), cache-busting CSS/JS w admin panelu, poprawki UI listy produktow, clipboard API
<hr>
<b>ver. 0.308 - 22.02.2026</b><br />
NEW - kolorowe badge statusow zamowien, walidacja hex, sanityzacja HTML transport, optymalizacja SQL
<hr>
<b>ver. 0.308 - 22.02.2026</b><br />
NEW - kolorowe badge statusow zamowien, walidacja hex, sanityzacja HTML transport, optymalizacja SQL
<hr>
<?php
// Auto-generated changelog from manifest files + legacy entries.
// Scans manifest JSON files, merges with changelog-legacy.json, sorts descending, outputs HTML.

View File

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