diff --git a/CLAUDE.md b/CLAUDE.md
index 517074b..9b4d500 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -36,7 +36,7 @@ composer test
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
-Current suite: **750 tests, 2114 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.
diff --git a/admin/templates/integrations/logs.php b/admin/templates/integrations/logs.php
new file mode 100644
index 0000000..29889cf
--- /dev/null
+++ b/admin/templates/integrations/logs.php
@@ -0,0 +1,19 @@
+= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
+
+
diff --git a/autoload/Domain/Integrations/IntegrationsRepository.php b/autoload/Domain/Integrations/IntegrationsRepository.php
index 8074358..e23a98e 100644
--- a/autoload/Domain/Integrations/IntegrationsRepository.php
+++ b/autoload/Domain/Integrations/IntegrationsRepository.php
@@ -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
diff --git a/autoload/admin/Controllers/IntegrationsController.php b/autoload/admin/Controllers/IntegrationsController.php
index 29d6888..e182a90 100644
--- a/autoload/admin/Controllers/IntegrationsController.php
+++ b/autoload/admin/Controllers/IntegrationsController.php
@@ -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 = '
'
+ . '
'
+ . htmlspecialchars( $context, ENT_QUOTES, 'UTF-8' )
+ . '
';
+ }
+
+ $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', [
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 8c2b0fb..9791af4 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,6 +4,15 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
+## 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)
diff --git a/docs/TESTING.md b/docs/TESTING.md
index e470a86..585b3d1 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -23,7 +23,7 @@ composer test # standard
## Aktualny stan
```text
-OK (750 tests, 2114 assertions)
+OK (758 tests, 2135 assertions)
```
Zweryfikowano: 2026-02-22 (ver. 0.304)
diff --git a/tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php b/tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php
index 96a979d..8bd2771 100644
--- a/tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php
+++ b/tests/Unit/Domain/Integrations/IntegrationsRepositoryTest.php
@@ -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());
+ }
}
diff --git a/tests/Unit/admin/Controllers/IntegrationsControllerTest.php b/tests/Unit/admin/Controllers/IntegrationsControllerTest.php
index 79e87bd..3cd51db 100644
--- a/tests/Unit/admin/Controllers/IntegrationsControllerTest.php
+++ b/tests/Unit/admin/Controllers/IntegrationsControllerTest.php
@@ -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 = [
diff --git a/updates/versions.php b/updates/versions.php
index b96b56b..2c56836 100644
--- a/updates/versions.php
+++ b/updates/versions.php
@@ -1,5 +1,5 @@
-$current_ver = 309;
+$current_ver = 310;
for ($i = 1; $i <= $current_ver; $i++)
{