From 0ac74b6cf49c18b67edda0634a3d674794b040ea Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 12 Feb 2026 23:26:28 +0100 Subject: [PATCH] refactor newsletter module and disable prepare/user templates --- DATABASE_STRUCTURE.md | 38 ++ PROJECT_STRUCTURE.md | 18 + REFACTORING_PLAN.md | 20 + TESTING.md | 18 + .../newsletter/email-template-edit.php | 68 +-- .../newsletter/email-templates-admin.php | 29 +- .../newsletter/email-templates-user.php | 42 -- admin/templates/newsletter/emails-list.php | 31 +- admin/templates/newsletter/prepare.php | 131 ----- admin/templates/newsletter/preview.php | 41 -- admin/templates/newsletter/settings.php | 52 +- admin/templates/site/main-layout.php | 4 +- .../Newsletter/NewsletterPreviewRenderer.php | 59 ++ .../Newsletter/NewsletterRepository.php | 292 ++++++++++ .../Controllers/NewsletterController.php | 514 ++++++++++++++++++ autoload/admin/class.Site.php | 11 + autoload/admin/controls/class.Newsletter.php | 94 ---- autoload/admin/factory/class.Newsletter.php | 80 +-- autoload/admin/view/class.Newsletter.php | 55 -- autoload/front/factory/class.Newsletter.php | 16 +- .../Newsletter/NewsletterRepositoryTest.php | 78 +++ .../Controllers/NewsletterControllerTest.php | 70 +++ updates/0.20/ver_0.258.zip | Bin 0 -> 23627 bytes updates/0.20/ver_0.258_files.txt | 5 + updates/changelog.php | 16 +- updates/versions.php | 2 +- 26 files changed, 1182 insertions(+), 602 deletions(-) delete mode 100644 admin/templates/newsletter/email-templates-user.php delete mode 100644 admin/templates/newsletter/prepare.php delete mode 100644 admin/templates/newsletter/preview.php create mode 100644 autoload/Domain/Newsletter/NewsletterPreviewRenderer.php create mode 100644 autoload/Domain/Newsletter/NewsletterRepository.php create mode 100644 autoload/admin/Controllers/NewsletterController.php delete mode 100644 autoload/admin/controls/class.Newsletter.php delete mode 100644 autoload/admin/view/class.Newsletter.php create mode 100644 tests/Unit/Domain/Newsletter/NewsletterRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/NewsletterControllerTest.php create mode 100644 updates/0.20/ver_0.258.zip create mode 100644 updates/0.20/ver_0.258_files.txt diff --git a/DATABASE_STRUCTURE.md b/DATABASE_STRUCTURE.md index 323bfc7..ca26eb5 100644 --- a/DATABASE_STRUCTURE.md +++ b/DATABASE_STRUCTURE.md @@ -249,3 +249,41 @@ Przypisanie layoutow do kategorii sklepu. **Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `front\\factory\\Layouts` **Aktualizacja 2026-02-12 (ver. 0.256):** modul `/admin/layouts` korzysta z `Domain\\Layouts\\LayoutsRepository` (DI kontroler + fasada legacy). + +## pp_newsletter +Adresy e-mail zapisane do newslettera. + +| Kolumna | Opis | +|---------|------| +| id | PK | +| email | Adres e-mail subskrybenta | +| hash | Hash potwierdzenia/wypisu | +| status | 1 = potwierdzony, 0 = oczekujacy | + +**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `front\\factory\\Newsletter` + +## pp_newsletter_send +Kolejka wysylki newslettera. + +| Kolumna | Opis | +|---------|------| +| id | PK | +| email | Adres docelowy | +| dates | Zakres dat artykulow (tekst) | +| id_template | FK do `pp_newsletter_templates` (NULL gdy brak szablonu) | + +**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `front\\factory\\Newsletter::newsletter_send()` + +## pp_newsletter_templates +Szablony tresci e-maili (uzytkownik + administracyjne/systemowe). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| name | Nazwa/klucz szablonu | +| text | Tresc HTML szablonu | +| is_admin | 1 = szablon administracyjny/systemowy, 0 = szablon uzytkownika | + +**Uzywane w:** `Domain\\Newsletter\\NewsletterRepository`, `admin\\Controllers\\NewsletterController`, `front\\factory\\Newsletter` + +**Aktualizacja 2026-02-12 (ver. 0.257):** modul `/admin/newsletter` korzysta z `Domain\\Newsletter\\NewsletterRepository` (DI kontroler + fasada legacy). diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 8a54078..1f9a596 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -419,3 +419,21 @@ Aktualnie w suite są też testy modułów `Dictionaries`, `Articles` i `Users` - CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.Layouts.php`, `autoload/admin/view/class.Layouts.php`; `admin/factory/class.Layouts.php` dziala jako fasada do `Domain\\Layouts\\LayoutsRepository`. - UPDATE: `admin\\Controllers\\ArticlesController` pobiera layouty przez `Domain\\Layouts\\LayoutsRepository` (DI). - Testy: 141 tests, 336 assertions + +## Aktualizacja 2026-02-12 (ver. 0.257) +- NOWE: `Domain\\Newsletter\\NewsletterRepository` (subskrybenci, szablony, ustawienia, kolejka wysylki). +- NOWE: `Domain\\Newsletter\\NewsletterPreviewRenderer` (render podgladu newslettera). +- NOWE: `admin\\Controllers\\NewsletterController` (DI) dla akcji `emails_list`, `prepare`, `send`, `preview`, `settings*`, `email_template*`. +- UPDATE: `/admin/newsletter/*` przepiete z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit`. +- UPDATE: nowy endpoint podgladu `/admin/newsletter/preview/` (bez `admin/ajax.php`). +- UPDATE: `admin\\Site` ma fabryke DI dla modulu `Newsletter`. +- UPDATE: `admin\\factory\\Newsletter` dziala jako fasada do `Domain\\Newsletter\\NewsletterRepository`. +- UPDATE: `front\\factory\\Newsletter` nie korzysta z `admin\\view\\Newsletter`. +- CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.Newsletter.php`, `autoload/admin/view/class.Newsletter.php`. +- Testy: 150 tests, 372 assertions + +## Aktualizacja 2026-02-12 (ver. 0.258) +- UPDATE: modul `/admin/newsletter/` - tymczasowo wylaczono akcje `prepare/send/preview` (Wysylka - przygotowanie). +- UPDATE: modul `/admin/newsletter/` - tymczasowo wylaczono liste `email_templates_user` (Szablony uzytkownika). +- UPDATE: lista i edycja szablonow newslettera w panelu ograniczona do szablonow administracyjnych (`is_admin = 1`). +- CLEANUP: usuniete nieuzywane widoki: `admin/templates/newsletter/prepare.php`, `admin/templates/newsletter/preview.php`, `admin/templates/newsletter/email-templates-user.php`. diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md index 6cbc7b8..4d0de26 100644 --- a/REFACTORING_PLAN.md +++ b/REFACTORING_PLAN.md @@ -614,3 +614,23 @@ Gdy `persist = true`: - UPDATE: `admin\\Controllers\\ArticlesController` korzysta z `Domain\\Layouts\\LayoutsRepository` (DI) dla listy layoutow - Testy po zmianie: **141 tests, 336 assertions** + +## Aktualizacja 2026-02-12 (ver. 0.257) +- **Newsletter** - **ZMIGROWANE** (2026-02-12) + - NOWE: `Domain\\Newsletter\\NewsletterRepository` (listy admin, szablony, ustawienia, kolejka wysylki) + - NOWE: `Domain\\Newsletter\\NewsletterPreviewRenderer` (wspolny render podgladu) + - NOWE: `admin\\Controllers\\NewsletterController` (DI) + - UPDATE: routing DI (`admin\\Site`) rozszerzony o modul `Newsletter` + - UPDATE: widoki `/admin/newsletter/*` migrowane na `components/table-list` i `components/form-edit` + - UPDATE: `admin\\factory\\Newsletter` jako fasada do repozytorium + - UPDATE: `front\\factory\\Newsletter` bez zaleznosci od `admin\\view\\Newsletter` + - CLEANUP: usuniete `autoload/admin/controls/class.Newsletter.php`, `autoload/admin/view/class.Newsletter.php` + +- Testy po zmianie: **150 tests, 372 assertions** + +## Aktualizacja 2026-02-12 (ver. 0.258) +- **Newsletter** + - UPDATE: tymczasowo wylaczono flow `prepare/send/preview` (wymaga przebudowy). + - UPDATE: tymczasowo wylaczono modul `Szablony uzytkownika`. + - UPDATE: aktywna obsluga tylko szablonow administracyjnych (`is_admin = 1`). + - CLEANUP: usuniete nieuzywane widoki `prepare.php`, `preview.php`, `email-templates-user.php`. diff --git a/TESTING.md b/TESTING.md index 14598bd..23e7e41 100644 --- a/TESTING.md +++ b/TESTING.md @@ -206,3 +206,21 @@ Nowe testy dodane 2026-02-12: Zaktualizowane testy 2026-02-12: - `tests/Unit/Domain/Languages/LanguagesRepositoryTest.php` (defaultLanguageId) - `tests/Unit/admin/Controllers/ArticlesControllerTest.php` (konstruktor + LayoutsRepository) + +## Aktualizacja suite (release 0.257) +Ostatnio zweryfikowano: 2026-02-12 + +```text +OK (150 tests, 372 assertions) +``` + +Nowe testy dodane 2026-02-12: +- `tests/Unit/Domain/Newsletter/NewsletterRepositoryTest.php` +- `tests/Unit/admin/Controllers/NewsletterControllerTest.php` + +## Aktualizacja suite (release 0.258) +Ostatnio zweryfikowano: 2026-02-12 + +```text +OK (150 tests, 372 assertions) +``` diff --git a/admin/templates/newsletter/email-template-edit.php b/admin/templates/newsletter/email-template-edit.php index 7c6aa1d..3615024 100644 --- a/admin/templates/newsletter/email-template-edit.php +++ b/admin/templates/newsletter/email-template-edit.php @@ -1,67 +1 @@ - - - - 'Nazwa', - 'name' => 'name', - 'id' => 'name', - 'value' => $this -> email_template['name'], - 'inline' => true, - 'readonly' => $this -> email_template['is_admin'] ? true : false - ) - );?> - 'Treść', - 'name' => 'text', - 'id' => 'text', - 'value' => $this ->email_template['text'], - 'inline' => true - ) - );?> - - id = 'email-templates-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja szablonu newslettera'; -$grid -> fields = [ - [ - 'db' => 'id', - 'type' => 'hidden', - 'value' => $this -> email_template['id'] - ] - ]; -$grid -> external_code = $out; -$grid -> actions = [ - 'save' => [ - 'url' => '/admin/newsletter/template_save/', - 'back_url' => $this -> email_template['is_admin'] ? '/admin/newsletter/email_templates_admin/' : '/admin/newsletter/email_templates_user/' - ], - 'cancel' => [ - 'url' => $this -> email_template['is_admin'] ? '/admin/newsletter/email_templates_admin/' : '/admin/newsletter/email_templates_user/' - ] - ]; -$grid -> persist_edit = true; -$grid -> id_param = 'id'; - -echo $grid -> draw(); -?> - \ No newline at end of file + $this->form]); ?> \ No newline at end of file diff --git a/admin/templates/newsletter/email-templates-admin.php b/admin/templates/newsletter/email-templates-admin.php index be182b7..17cbbb8 100644 --- a/admin/templates/newsletter/email-templates-admin.php +++ b/admin/templates/newsletter/email-templates-admin.php @@ -1,28 +1 @@ - gdb_opt = $gdb; -$grid -> order = [ 'column' => 'name', 'type' => 'ASC' ]; -$grid -> where = [ 'is_admin' => 1 ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], - [ - 'name' => 'Nazwa', - 'db' => 'name', - 'php' => 'echo "[name]";', - 'sort' => true - ], - [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/newsletter/email_template_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -echo $grid -> draw(); \ No newline at end of file + $this->viewModel]); ?> \ No newline at end of file diff --git a/admin/templates/newsletter/email-templates-user.php b/admin/templates/newsletter/email-templates-user.php deleted file mode 100644 index 6275e03..0000000 --- a/admin/templates/newsletter/email-templates-user.php +++ /dev/null @@ -1,42 +0,0 @@ - gdb_opt = $gdb; -$grid -> order = [ 'column' => 'name', 'type' => 'ASC' ]; -$grid -> where = [ 'is_admin' => 0 ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], - [ - 'name' => 'Nazwa', - 'db' => 'name', - 'php' => 'echo "[name]";', - 'sort' => true - ], - [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/newsletter/email_template_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ], - [ - 'name' => 'Usuń', - 'action' => [ 'type' => 'delete', 'url' => '/admin/newsletter/email_template_delete/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -$grid -> buttons = [ - [ - 'label' => 'Dodaj szablon', - 'url' => '/admin/newsletter/email_template_edit/', - 'icon' => 'fa-plus-circle', - 'class' => 'btn-success' - ] - ]; -echo $grid -> draw(); \ No newline at end of file diff --git a/admin/templates/newsletter/emails-list.php b/admin/templates/newsletter/emails-list.php index 19f1827..0f89c5b 100644 --- a/admin/templates/newsletter/emails-list.php +++ b/admin/templates/newsletter/emails-list.php @@ -1,30 +1 @@ - gdb_opt = $gdb; -$grid -> order = [ 'column' => 'email', 'type' => 'ASC' ]; -$grid -> search = [ - [ 'name' => 'Email', 'db' => 'email', 'type' => 'text' ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], - [ - 'name' => 'Email', - 'db' => 'email', - 'sort' => true - ], - [ - 'name' => 'Potwierdzony', - 'db' => 'status', - 'sort' => true, - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] - ] - ]; -$grid -> actions = [ 'delete' => true ]; -echo $grid -> draw(); \ No newline at end of file + $this->viewModel]); ?> diff --git a/admin/templates/newsletter/prepare.php b/admin/templates/newsletter/prepare.php deleted file mode 100644 index 565dd21..0000000 --- a/admin/templates/newsletter/prepare.php +++ /dev/null @@ -1,131 +0,0 @@ - -
- -
-
- - - - -
-
-
-
- -
- templates ) ): foreach ( $this -> templates as $template ): - $templates[ $template['id'] ] = $template['name']; - endforeach; endif; - ?> - 'Szablon', - 'name' => 'template', - 'id' => 'template', - 'values' => $templates, - 'value' => $this -> templates['id'] - ));?> - -
- -
-
-
-
- id = 'newsletter-prepare'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Wysyłka newslettera - przygotowanie'; -$grid -> default_buttons = false; -$grid -> external_code = $out; -$grid -> buttons = [ - [ - 'label' => 'Wyślij newsletter', - 'class' => 'btn-success', - 'icon' => 'fa-send', - 'js' => 'send_newsletter();' - ] - ]; -echo $grid -> draw(); -?> - \ No newline at end of file diff --git a/admin/templates/newsletter/preview.php b/admin/templates/newsletter/preview.php deleted file mode 100644 index 642c6d5..0000000 --- a/admin/templates/newsletter/preview.php +++ /dev/null @@ -1,41 +0,0 @@ -
- settings['newsletter_header'] ? $this -> settings['newsletter_header'] : '

--- brak zdefiniowanego nagłówka ---

';?> -
-
- template ) ):?> -
- template['text']?> -
- - articles ) ):?> - articles as $article ):?> - -
- - - - -
- -
-
-
- - - dates ):?> -
- --- brak artykułów w danym okresie --- -
- - -
-
- settings['newsletter_footer'] ? $this -> settings['newsletter_footer'] : '

--- brak zdefiniowanej stopki ---

';?> -
\ No newline at end of file diff --git a/admin/templates/newsletter/settings.php b/admin/templates/newsletter/settings.php index 2da1c9c..3615024 100644 --- a/admin/templates/newsletter/settings.php +++ b/admin/templates/newsletter/settings.php @@ -1,51 +1 @@ - - - - 'Nagłówek', - 'name' => 'newsletter_header', - 'id' => 'newsletter_header', - 'value' => $this -> settings['newsletter_header'], - 'inline' => true - ) - ); - echo \Html::textarea( - array( - 'label' => 'Stopka', - 'name' => 'newsletter_footer', - 'id' => 'newsletter_footer', - 'value' => $this -> settings['newsletter_footer'], - 'inline' => true - ) - ); - -$out = ob_get_clean(); - -$grid = new \gridEdit; -$grid -> id = 'settings-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja ustawień'; -$grid -> actions = [ - 'save' => [ 'url' => '/admin/newsletter/settings_save/', 'back_url' => '/admin/newsletter/settings/' ], - ]; -$grid -> external_code = $out; -echo $grid -> draw(); -?> - - \ No newline at end of file + $this->form]); ?> \ No newline at end of file diff --git a/admin/templates/site/main-layout.php b/admin/templates/site/main-layout.php index 0bf4e71..0d5baf7 100644 --- a/admin/templates/site/main-layout.php +++ b/admin/templates/site/main-layout.php @@ -89,9 +89,7 @@ Newsletter @@ -274,4 +272,4 @@ }); - \ No newline at end of file + diff --git a/autoload/Domain/Newsletter/NewsletterPreviewRenderer.php b/autoload/Domain/Newsletter/NewsletterPreviewRenderer.php new file mode 100644 index 0000000..a14f2e7 --- /dev/null +++ b/autoload/Domain/Newsletter/NewsletterPreviewRenderer.php @@ -0,0 +1,59 @@ +'; + $out .= !empty($settings['newsletter_header']) + ? (string)$settings['newsletter_header'] + : '

--- brak zdefiniowanego naglowka ---

'; + $out .= ''; + + $out .= '
'; + + if (is_array($template) && !empty($template)) { + $out .= '
'; + $out .= (string)($template['text'] ?? ''); + $out .= '
'; + } + + if (is_array($articles) && !empty($articles)) { + foreach ($articles as $article) { + $articleId = (int)($article['id'] ?? 0); + $title = (string)($article['language']['title'] ?? ''); + $seoLink = trim((string)($article['language']['seo_link'] ?? '')); + $url = $seoLink !== '' ? $seoLink : ('a-' . $articleId . '-' . \S::seo($title)); + $entry = !empty($article['language']['entry']) + ? (string)$article['language']['entry'] + : (string)($article['language']['text'] ?? ''); + + $out .= '
'; + $out .= '' + . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') + . ''; + $out .= '
' . $entry . '
'; + $out .= '
'; + $out .= '
'; + } + } elseif (trim($dates) !== '') { + $out .= '
'; + $out .= '--- brak artykulow w danym okresie ---'; + $out .= '
'; + } + + $out .= '
'; + + $out .= '
'; + $out .= !empty($settings['newsletter_footer']) + ? (string)$settings['newsletter_footer'] + : '

--- brak zdefiniowanej stopki ---

'; + $out .= '
'; + + return $out; + } +} diff --git a/autoload/Domain/Newsletter/NewsletterRepository.php b/autoload/Domain/Newsletter/NewsletterRepository.php new file mode 100644 index 0000000..7f7e63e --- /dev/null +++ b/autoload/Domain/Newsletter/NewsletterRepository.php @@ -0,0 +1,292 @@ +db = $db; + $this->settingsRepository = $settingsRepository ?? new SettingsRepository($db); + } + + public function getSettings(): array + { + return $this->settingsRepository->getSettings(); + } + + public function saveSettings(array $values): bool + { + $this->settingsRepository->updateSetting('newsletter_footer', (string)($values['newsletter_footer'] ?? '')); + $this->settingsRepository->updateSetting('newsletter_header', (string)($values['newsletter_header'] ?? '')); + + return true; + } + + public function queueSend(string $dates = '', int $templateId = 0): bool + { + $subscribers = $this->db->select('pp_newsletter', 'email', ['status' => 1]); + if (!is_array($subscribers) || empty($subscribers)) { + return true; + } + + $cleanDates = trim($dates); + $templateId = $templateId > 0 ? $templateId : 0; + + foreach ($subscribers as $subscriber) { + $email = is_array($subscriber) ? (string)($subscriber['email'] ?? '') : (string)$subscriber; + if ($email === '') { + continue; + } + + $this->db->insert('pp_newsletter_send', [ + 'email' => $email, + 'dates' => $cleanDates, + 'id_template' => $templateId > 0 ? $templateId : null, + ]); + } + + return true; + } + + public function templateByName(string $templateName): string + { + return (string)$this->db->get('pp_newsletter_templates', 'text', ['name' => $templateName]); + } + + public function templateDetails(int $templateId): ?array + { + if ($templateId <= 0) { + return null; + } + + $row = $this->db->get('pp_newsletter_templates', '*', ['id' => $templateId]); + if (!is_array($row)) { + return null; + } + + return $row; + } + + public function isAdminTemplate(int $templateId): bool + { + $isAdmin = $this->db->get('pp_newsletter_templates', 'is_admin', ['id' => $templateId]); + return (int)$isAdmin === 1; + } + + public function deleteTemplate(int $templateId): bool + { + if ($templateId <= 0 || $this->isAdminTemplate($templateId)) { + return false; + } + + return (bool)$this->db->delete('pp_newsletter_templates', ['id' => $templateId]); + } + + public function saveTemplate(int $templateId, string $name, string $text): ?int + { + $templateId = max(0, $templateId); + $name = trim($name); + + if ($templateId <= 0) { + if ($name === '') { + return null; + } + + $ok = $this->db->insert('pp_newsletter_templates', [ + 'name' => $name, + 'text' => $text, + 'is_admin' => 0, + ]); + if (!$ok) { + return null; + } + + \S::delete_dir('../temp/'); + return (int)$this->db->id(); + } + + $current = $this->templateDetails($templateId); + if (!is_array($current)) { + return null; + } + + if ((int)($current['is_admin'] ?? 0) === 1) { + $name = (string)($current['name'] ?? ''); + } + + $this->db->update('pp_newsletter_templates', [ + 'name' => $name, + 'text' => $text, + ], [ + 'id' => $templateId, + ]); + + \S::delete_dir('../temp/'); + return $templateId; + } + + public function listTemplatesSimple(bool $adminTemplates = false): array + { + $rows = $this->db->select('pp_newsletter_templates', '*', [ + 'is_admin' => $adminTemplates ? 1 : 0, + 'ORDER' => ['name' => 'ASC'], + ]); + + return is_array($rows) ? $rows : []; + } + + public function deleteSubscriber(int $subscriberId): bool + { + if ($subscriberId <= 0) { + return false; + } + + return (bool)$this->db->delete('pp_newsletter', ['id' => $subscriberId]); + } + + /** + * @return array{items: array>, total: int} + */ + public function listSubscribersForAdmin( + array $filters, + string $sortColumn = 'email', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'pn.id', + 'email' => 'pn.email', + 'status' => 'pn.status', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'pn.email'; + $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['1 = 1']; + $params = []; + + $email = trim((string)($filters['email'] ?? '')); + if ($email !== '') { + if (strlen($email) > 255) { + $email = substr($email, 0, 255); + } + $where[] = 'pn.email LIKE :email'; + $params[':email'] = '%' . $email . '%'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status === '0' || $status === '1') { + $where[] = 'pn.status = :status'; + $params[':status'] = (int)$status; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_newsletter AS pn + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + pn.id, + pn.email, + pn.status + FROM pp_newsletter AS pn + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, pn.id ASC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + return [ + 'items' => is_array($items) ? $items : [], + 'total' => $total, + ]; + } + + /** + * @return array{items: array>, total: int} + */ + public function listTemplatesForAdmin( + bool $adminTemplates, + array $filters, + string $sortColumn = 'name', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'pnt.id', + 'name' => 'pnt.name', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'pnt.name'; + $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['pnt.is_admin = :is_admin']; + $params = [':is_admin' => $adminTemplates ? 1 : 0]; + + $name = trim((string)($filters['name'] ?? '')); + if ($name !== '') { + if (strlen($name) > 255) { + $name = substr($name, 0, 255); + } + $where[] = 'pnt.name LIKE :name'; + $params[':name'] = '%' . $name . '%'; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_newsletter_templates AS pnt + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + pnt.id, + pnt.name, + pnt.is_admin + FROM pp_newsletter_templates AS pnt + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, pnt.id ASC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + return [ + 'items' => is_array($items) ? $items : [], + 'total' => $total, + ]; + } +} + diff --git a/autoload/admin/Controllers/NewsletterController.php b/autoload/admin/Controllers/NewsletterController.php new file mode 100644 index 0000000..ceef967 --- /dev/null +++ b/autoload/admin/Controllers/NewsletterController.php @@ -0,0 +1,514 @@ +repository = $repository; + $this->previewRenderer = $previewRenderer; + $this->formHandler = new FormRequestHandler(); + } + + public function list(): string + { + return $this->emails_list(); + } + + public function view_list(): string + { + return $this->list(); + } + + public function emails_list(): string + { + $sortableColumns = ['email', 'status']; + $filterDefinitions = [ + [ + 'key' => 'email', + 'label' => 'Email', + 'type' => 'text', + ], + [ + 'key' => 'status', + 'label' => 'Potwierdzony', + 'type' => 'select', + 'options' => [ + '' => '- potwierdzony -', + '1' => 'tak', + '0' => 'nie', + ], + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'email' + ); + + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->repository->listSubscribersForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + $email = trim((string)($item['email'] ?? '')); + $status = (int)($item['status'] ?? 0); + + $rows[] = [ + 'lp' => $lp++ . '.', + 'email' => htmlspecialchars($email, ENT_QUOTES, 'UTF-8'), + 'status' => $status === 1 ? 'tak' : 'nie', + '_actions' => [ + [ + 'label' => 'Usun', + 'url' => '/admin/newsletter/email_delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybrany adres email?', + ], + ], + ]; + } + + $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' => 'email', 'sort_key' => 'email', 'label' => 'Email', 'sortable' => true], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Potwierdzony', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $sortDir, + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $sortDir, + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/newsletter/emails_list/', + 'Brak danych w tabeli.' + ); + + return \Tpl::view('newsletter/emails-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function email_delete(): void + { + if ($this->repository->deleteSubscriber((int)\S::get('id'))) { + \S::alert('Adres email zostal usuniety.'); + } + + header('Location: /admin/newsletter/emails_list/'); + exit; + } + + public function delete(): void + { + $this->email_delete(); + } + + public function prepare(): string + { + \S::alert('Funkcjonalnosc "Wysylka - przygotowanie" jest tymczasowo wylaczona.'); + header('Location: /admin/newsletter/emails_list/'); + exit; + } + + public function preview(): string + { + return ''; + } + + public function send(): void + { + \S::alert('Funkcjonalnosc "Wysylka - przygotowanie" jest tymczasowo wylaczona.'); + header('Location: /admin/newsletter/emails_list/'); + exit; + } + + public function settings(): string + { + $settings = $this->repository->getSettings(); + $validationErrors = $_SESSION['form_errors'][$this->settingsFormId()] ?? null; + if ($validationErrors) { + unset($_SESSION['form_errors'][$this->settingsFormId()]); + } + + return \Tpl::view('newsletter/settings', [ + 'form' => $this->buildSettingsFormViewModel($settings, $validationErrors), + ]); + } + + public function settings_save(): void + { + $legacyValues = \S::get('values'); + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + if (!is_array($values)) { + echo json_encode(['status' => 'error', 'msg' => 'Nieprawidlowe dane formularza.']); + exit; + } + + $this->repository->saveSettings($values); + \S::alert('Ustawienia zostaly zapisane.'); + + echo json_encode(['status' => 'ok', 'msg' => 'Ustawienia zostaly zapisane.']); + exit; + } + + $viewModel = $this->buildSettingsFormViewModel($this->repository->getSettings()); + $result = $this->formHandler->handleSubmit($viewModel, $_POST); + if (!$result['success']) { + $_SESSION['form_errors'][$this->settingsFormId()] = $result['errors']; + echo json_encode(['success' => false, 'errors' => $result['errors']]); + exit; + } + + $this->repository->saveSettings($result['data']); + \S::alert('Ustawienia zostaly zapisane.'); + + echo json_encode([ + 'success' => true, + 'message' => 'Ustawienia zostaly zapisane.', + ]); + exit; + } + + public function email_templates_user(): string + { + \S::alert('Funkcjonalnosc "Szablony uzytkownika" jest tymczasowo wylaczona.'); + header('Location: /admin/newsletter/email_templates_admin/'); + exit; + } + + public function email_templates_admin(): string + { + $viewModel = $this->templatesListViewModel(); + return \Tpl::view('newsletter/email-templates-admin', [ + 'viewModel' => $viewModel, + ]); + } + + public function email_template_edit(): string + { + $template = $this->repository->templateDetails((int)\S::get('id')); + if (!is_array($template) || (int)($template['is_admin'] ?? 0) !== 1) { + \S::alert('Dostepne sa tylko szablony administracyjne.'); + header('Location: /admin/newsletter/email_templates_admin/'); + exit; + } + + $formId = $this->templateFormId((int)$template['id']); + $validationErrors = $_SESSION['form_errors'][$formId] ?? null; + if ($validationErrors) { + unset($_SESSION['form_errors'][$formId]); + } + + return \Tpl::view('newsletter/email-template-edit', [ + 'form' => $this->buildTemplateFormViewModel($template, $validationErrors), + ]); + } + + public function template_save(): void + { + $legacyValues = \S::get('values'); + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + $response = ['status' => 'error', 'msg' => 'Podczas zapisywania wystapil blad.']; + + if (is_array($values)) { + $templateId = (int)($values['id'] ?? 0); + $template = $this->repository->templateDetails($templateId); + + if (is_array($template) && (int)($template['is_admin'] ?? 0) === 1) { + $id = $this->repository->saveTemplate( + $templateId, + (string)($values['name'] ?? ''), + (string)($values['text'] ?? '') + ); + if ($id) { + $response = ['status' => 'ok', 'msg' => 'Zmiany zostaly zapisane.', 'id' => $id]; + } + } + } + + echo json_encode($response); + exit; + } + + $template = $this->repository->templateDetails((int)\S::get('id')); + if (!is_array($template) || (int)($template['is_admin'] ?? 0) !== 1) { + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Dostepne sa tylko szablony administracyjne.'], + ]); + exit; + } + + $form = $this->buildTemplateFormViewModel($template); + $result = $this->formHandler->handleSubmit($form, $_POST); + if (!$result['success']) { + $_SESSION['form_errors'][$this->templateFormId((int)$template['id'])] = $result['errors']; + echo json_encode(['success' => false, 'errors' => $result['errors']]); + exit; + } + + $data = $result['data']; + $id = $this->repository->saveTemplate( + (int)($template['id'] ?? 0), + (string)($data['name'] ?? ''), + (string)($data['text'] ?? '') + ); + + if ($id) { + echo json_encode([ + 'success' => true, + 'id' => $id, + 'message' => 'Zmiany zostaly zapisane.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania wystapil blad.'], + ]); + exit; + } + + public function email_template_delete(): void + { + \S::alert('Usuwanie szablonow uzytkownika jest tymczasowo wylaczone.'); + header('Location: /admin/newsletter/email_templates_admin/'); + exit; + } + + private function buildSettingsFormViewModel(array $settings, ?array $errors = null): FormEditViewModel + { + $data = [ + 'newsletter_header' => (string)($settings['newsletter_header'] ?? ''), + 'newsletter_footer' => (string)($settings['newsletter_footer'] ?? ''), + ]; + + $fields = [ + FormField::editor('newsletter_header', [ + 'label' => 'Naglowek', + 'height' => 150, + ]), + FormField::editor('newsletter_footer', [ + 'label' => 'Stopka', + 'height' => 150, + ]), + ]; + + $actionUrl = '/admin/newsletter/settings_save/'; + $actions = [ + FormAction::save($actionUrl, '/admin/newsletter/settings/'), + ]; + + return new FormEditViewModel( + $this->settingsFormId(), + 'Edycja ustawien newslettera', + $data, + $fields, + [], + $actions, + 'POST', + $actionUrl, + '/admin/newsletter/settings/', + true, + [], + null, + $errors + ); + } + + private function buildTemplateFormViewModel(array $template, ?array $errors = null): FormEditViewModel + { + $templateId = (int)($template['id'] ?? 0); + $isAdminTemplate = (int)($template['is_admin'] ?? 0) === 1; + $isNew = $templateId <= 0; + + $data = [ + 'id' => $templateId, + 'name' => (string)($template['name'] ?? ''), + 'text' => (string)($template['text'] ?? ''), + ]; + + $nameAttrs = []; + if ($isAdminTemplate) { + $nameAttrs['readonly'] = 'readonly'; + } + + $fields = [ + FormField::text('name', [ + 'label' => 'Nazwa', + 'required' => true, + 'attributes' => $nameAttrs, + ]), + FormField::editor('text', [ + 'label' => 'Tresc', + 'required' => true, + 'height' => 350, + ]), + ]; + + $backUrl = '/admin/newsletter/email_templates_admin/'; + $actionUrl = '/admin/newsletter/template_save/' . ($isNew ? '' : ('id=' . $templateId)); + $actions = [ + FormAction::save($actionUrl, $backUrl), + FormAction::cancel($backUrl), + ]; + + return new FormEditViewModel( + $this->templateFormId($templateId), + 'Edycja szablonu newslettera', + $data, + $fields, + [], + $actions, + 'POST', + $actionUrl, + $backUrl, + true, + ['id' => $templateId], + null, + $errors + ); + } + + private function templatesListViewModel(): PaginatedTableViewModel + { + $sortableColumns = ['name']; + $filterDefinitions = [ + [ + 'key' => 'name', + 'label' => 'Nazwa', + 'type' => 'text', + ], + ]; + + $basePath = '/admin/newsletter/email_templates_admin/'; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'name' + ); + + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->repository->listTemplatesForAdmin( + true, + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + $name = trim((string)($item['name'] ?? '')); + + $actions = [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/newsletter/email_template_edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + ]; + + $rows[] = [ + 'lp' => $lp++ . '.', + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + '_actions' => $actions, + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + return new PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $sortDir, + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $sortDir, + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + $basePath, + 'Brak danych w tabeli.' + ); + } + + private function settingsFormId(): string + { + return 'newsletter-settings-edit'; + } + + private function templateFormId(int $templateId): string + { + return 'newsletter-template-edit-' . $templateId; + } +} diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 12f12e8..e6c453d 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -275,6 +275,17 @@ class Site new \Domain\Languages\LanguagesRepository( $mdb ) ); }, + 'Newsletter' => function() { + global $mdb; + + return new \admin\Controllers\NewsletterController( + new \Domain\Newsletter\NewsletterRepository( + $mdb, + new \Domain\Settings\SettingsRepository( $mdb ) + ), + new \Domain\Newsletter\NewsletterPreviewRenderer() + ); + }, ]; return self::$newControllers; diff --git a/autoload/admin/controls/class.Newsletter.php b/autoload/admin/controls/class.Newsletter.php deleted file mode 100644 index 97e2090..0000000 --- a/autoload/admin/controls/class.Newsletter.php +++ /dev/null @@ -1,94 +0,0 @@ - updateSetting( 'newsletter_footer', $values['newsletter_footer'] ?? '' ); - $settingsRepository -> updateSetting( 'newsletter_header', $values['newsletter_header'] ?? '' ); - - \S::alert( 'Ustawienia zostały zapisane.' ); - - echo json_encode( [ 'status' => 'ok', 'msg' => 'Ustawienia zostały zapisane.' ] ); - exit; - } - - public static function settings() - { - $settingsRepository = new \Domain\Settings\SettingsRepository(); - - return \admin\view\Newsletter::settings( - $settingsRepository -> getSettings() - ); - } - - public static function email_templates_user() - { - return \admin\view\Newsletter::email_templates_user(); - } - - public static function email_templates_admin() - { - return \admin\view\Newsletter::email_templates_admin(); - } - - public static function email_template_delete() - { - $is_admin = \admin\factory\Newsletter::is_admin_template( \S::get( 'id' ) ); - - if ( !$is_admin and \admin\factory\Newsletter::newsletter_template_delete( \S::get( 'id' ) ) ) - \S::alert( 'Szablon newslettera został usunięty.' ); - - if ( $is_admin ) - header( 'Location: /admin/newsletter/email_templates_admin/' ); - else - header( 'Location: /admin/newsletter/email_templates_user/' ); - exit; - } - - public static function email_template_edit() - { - return \admin\view\Newsletter::email_template_edit( - \admin\factory\Newsletter::email_template_detalis( - \S::get( 'id' ) - ) - ); - } - - public static function template_save() - { - $response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania wystąpił błąd. Proszę spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - if ( $id = \admin\factory\Newsletter::template_save( $values['id'], $values['name'], $values['text'] ) ) - $response = [ 'status' => 'ok', 'msg' => 'Zmiany zostały zapisane.', 'id' => $id ]; - - echo json_encode( $response ); - exit; - } -} diff --git a/autoload/admin/factory/class.Newsletter.php b/autoload/admin/factory/class.Newsletter.php index 21e8d11..ddd14f7 100644 --- a/autoload/admin/factory/class.Newsletter.php +++ b/autoload/admin/factory/class.Newsletter.php @@ -1,76 +1,48 @@ get( 'pp_newsletter_templates', 'is_admin', [ 'id' => (int)$template_id ] ); + return self::repository() -> isAdminTemplate( (int)$template_id ); } - + public static function newsletter_template_delete( $template_id ) { - global $mdb; - return $mdb -> delete( 'pp_newsletter_templates', [ 'id' => (int)$template_id ] ); + return self::repository() -> deleteTemplate( (int)$template_id ); } - + public static function send( $dates, $template ) { - global $mdb; - - $results = $mdb -> select( 'pp_newsletter', 'email', [ 'status' => 1 ] ); - if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row ) - { - $mdb -> insert( 'pp_newsletter_send', [ - 'email' => $row, - 'dates' => $dates, - 'id_template' => $template ? $template : null - ] ); - } - return true; + return self::repository() -> queueSend( (string)$dates, (int)$template ); } - - public static function email_template_detalis ($id_template) + + public static function email_template_detalis( $id_template ) { - global $mdb; - - $result = $mdb -> get ('pp_newsletter_templates', '*', [ 'id' => (int)$id_template ] ); - return $result; + return self::repository() -> templateDetails( (int)$id_template ); } - + public static function template_save( $id, $name, $text ) { - global $mdb; - - if ( !$id ) - { - if ( $mdb -> insert( 'pp_newsletter_templates', [ - 'name' => $name, - 'text' => $text - ] ) ) - { - \S::delete_dir( '../temp/' ); - return $mdb -> id(); - } - } - else - { - $mdb -> update( 'pp_newsletter_templates', [ - 'name' => $name, - 'text' => $text - ], [ - 'id' => (int)$id - ] ); - - \S::delete_dir( '../temp/' ); - return $id; - } + return self::repository() -> saveTemplate( (int)$id, (string)$name, (string)$text ); } - + public static function templates_list() { - global $mdb; - return $mdb -> select( 'pp_newsletter_templates', '*', [ 'is_admin' => 0, 'ORDER' => [ 'name' => 'ASC' ] ] ); + return self::repository() -> listTemplatesSimple( false ); } -} + + private static function repository(): NewsletterRepository + { + global $mdb; + + return new NewsletterRepository( + $mdb, + new SettingsRepository($mdb) + ); + } +} \ No newline at end of file diff --git a/autoload/admin/view/class.Newsletter.php b/autoload/admin/view/class.Newsletter.php deleted file mode 100644 index f40fb74..0000000 --- a/autoload/admin/view/class.Newsletter.php +++ /dev/null @@ -1,55 +0,0 @@ - render( 'newsletter/emails-list' ); - } - - public static function preview( $articles, $settings, $template, $dates = '' ) - { - $tpl = new \Tpl; - $tpl -> articles = $articles; - $tpl -> settings = $settings; - $tpl -> template = $template; - $tpl -> dates = $dates; - return $tpl -> render( 'newsletter/preview' ); - } - - public static function prepare( $templates ) - { - $tpl = new \Tpl; - $tpl -> templates = $templates; - return $tpl -> render( 'newsletter/prepare' ); - } - - public static function settings( $settings ) - { - $tpl = new \Tpl; - $tpl -> settings = $settings; - return $tpl -> render( 'newsletter/settings' ); - } - - public static function email_templates_user() - { - $tpl = new \Tpl; - return $tpl -> render( 'newsletter/email-templates-user' ); - } - - public static function email_templates_admin() - { - $tpl = new \Tpl; - return $tpl -> render( 'newsletter/email-templates-admin' ); - } - - public static function email_template_edit($template) - { - $tpl = new \Tpl; - $tpl -> email_template = $template; - return $tpl -> render( 'newsletter/email-template-edit' ); - } -} - \ No newline at end of file diff --git a/autoload/front/factory/class.Newsletter.php b/autoload/front/factory/class.Newsletter.php index 7521364..ee8da07 100644 --- a/autoload/front/factory/class.Newsletter.php +++ b/autoload/front/factory/class.Newsletter.php @@ -27,6 +27,8 @@ class Newsletter { global $mdb, $settings, $lang; $settingsRepository = new \Domain\Settings\SettingsRepository( $mdb ); + $newsletterRepository = new \Domain\Newsletter\NewsletterRepository( $mdb, $settingsRepository ); + $previewRenderer = new \Domain\Newsletter\NewsletterPreviewRenderer(); $settingsDetails = $settingsRepository -> getSettings(); $results = $mdb -> query( 'SELECT * FROM pp_newsletter_send ORDER BY id ASC LIMIT ' . $limit ) -> fetchAll(); @@ -36,10 +38,15 @@ class Newsletter { $dates = explode( ' - ', $row['dates'] ); - $text = \admin\view\Newsletter::preview( - \admin\factory\Articles::articles_by_date_add( $dates[0], $dates[1] ), + $articles = []; + if ( isset( $dates[0], $dates[1] ) ) + $articles = \admin\factory\Articles::articles_by_date_add( $dates[0], $dates[1] ); + + $text = $previewRenderer -> render( + is_array( $articles ) ? $articles : [], $settingsDetails, - \admin\factory\Newsletter::email_template_detalis($row['id_template']) + $newsletterRepository -> templateDetails( (int)$row['id_template'] ), + (string)$row['dates'] ); if ( $settings['ssl'] ) $base = 'https'; else $base = 'http'; @@ -110,7 +117,8 @@ class Newsletter public static function get_template( $template_name ) { global $mdb; - return $mdb -> get( 'pp_newsletter_templates', 'text', [ 'name' => $template_name ] ); + $repository = new \Domain\Newsletter\NewsletterRepository( $mdb ); + return $repository -> templateByName( (string)$template_name ); } public static function newsletter_signout( $email ) diff --git a/tests/Unit/Domain/Newsletter/NewsletterRepositoryTest.php b/tests/Unit/Domain/Newsletter/NewsletterRepositoryTest.php new file mode 100644 index 0000000..406562c --- /dev/null +++ b/tests/Unit/Domain/Newsletter/NewsletterRepositoryTest.php @@ -0,0 +1,78 @@ +createMock(\medoo::class); + $repository = new NewsletterRepository($mockDb, $this->createMock(SettingsRepository::class)); + + $this->assertNull($repository->templateDetails(0)); + } + + public function testTemplateDetailsReturnsArray(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_newsletter_templates', '*', ['id' => 7]) + ->willReturn(['id' => 7, 'name' => 'Tpl']); + + $repository = new NewsletterRepository($mockDb, $this->createMock(SettingsRepository::class)); + $details = $repository->templateDetails(7); + + $this->assertIsArray($details); + $this->assertSame(7, $details['id']); + } + + public function testSaveSettingsUpdatesHeaderAndFooter(): void + { + $mockDb = $this->createMock(\medoo::class); + $settingsRepository = $this->createMock(SettingsRepository::class); + + $settingsRepository->expects($this->exactly(2)) + ->method('updateSetting') + ->withConsecutive( + ['newsletter_footer', 'Footer'], + ['newsletter_header', 'Header'] + ); + + $repository = new NewsletterRepository($mockDb, $settingsRepository); + + $this->assertTrue($repository->saveSettings([ + 'newsletter_header' => 'Header', + 'newsletter_footer' => 'Footer', + ])); + } + + public function testDeleteTemplateReturnsFalseForAdminTemplate(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_newsletter_templates', 'is_admin', ['id' => 5]) + ->willReturn(1); + + $repository = new NewsletterRepository($mockDb, $this->createMock(SettingsRepository::class)); + + $this->assertFalse($repository->deleteTemplate(5)); + } + + public function testTemplateByNameReturnsText(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_newsletter_templates', 'text', ['name' => '#abc']) + ->willReturn('Template text'); + + $repository = new NewsletterRepository($mockDb, $this->createMock(SettingsRepository::class)); + + $this->assertSame('Template text', $repository->templateByName('#abc')); + } +} \ No newline at end of file diff --git a/tests/Unit/admin/Controllers/NewsletterControllerTest.php b/tests/Unit/admin/Controllers/NewsletterControllerTest.php new file mode 100644 index 0000000..24ddb74 --- /dev/null +++ b/tests/Unit/admin/Controllers/NewsletterControllerTest.php @@ -0,0 +1,70 @@ +repository = $this->createMock(NewsletterRepository::class); + $this->previewRenderer = $this->createMock(NewsletterPreviewRenderer::class); + $this->controller = new NewsletterController($this->repository, $this->previewRenderer); + } + + public function testConstructorAcceptsDependencies(): void + { + $controller = new NewsletterController($this->repository, $this->previewRenderer); + $this->assertInstanceOf(NewsletterController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'emails_list')); + $this->assertTrue(method_exists($this->controller, 'prepare')); + $this->assertTrue(method_exists($this->controller, 'send')); + $this->assertTrue(method_exists($this->controller, 'preview')); + $this->assertTrue(method_exists($this->controller, 'settings')); + $this->assertTrue(method_exists($this->controller, 'settings_save')); + $this->assertTrue(method_exists($this->controller, 'email_templates_user')); + $this->assertTrue(method_exists($this->controller, 'email_templates_admin')); + $this->assertTrue(method_exists($this->controller, 'email_template_edit')); + $this->assertTrue(method_exists($this->controller, 'template_save')); + $this->assertTrue(method_exists($this->controller, 'email_template_delete')); + } + + public function testActionMethodReturnTypes(): void + { + $reflection = new \ReflectionClass($this->controller); + + $this->assertEquals('string', (string)$reflection->getMethod('emails_list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('prepare')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('send')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('preview')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('settings')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('settings_save')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('email_templates_user')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('email_templates_admin')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('email_template_edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('template_save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('email_template_delete')->getReturnType()); + } + + public function testConstructorRequiresRepositoryAndRenderer(): void + { + $reflection = new \ReflectionClass(NewsletterController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(2, $params); + $this->assertEquals('Domain\Newsletter\NewsletterRepository', $params[0]->getType()->getName()); + $this->assertEquals('Domain\Newsletter\NewsletterPreviewRenderer', $params[1]->getType()->getName()); + } +} \ No newline at end of file diff --git a/updates/0.20/ver_0.258.zip b/updates/0.20/ver_0.258.zip new file mode 100644 index 0000000000000000000000000000000000000000..be4886ca069c5d1054c2c28fce040fead0e7cb51 GIT binary patch literal 23627 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 Yn0izB4Dj&pbO)a}d;lO_>+skA0p0niBme*a literal 0 HcmV?d00001 diff --git a/updates/0.20/ver_0.258_files.txt b/updates/0.20/ver_0.258_files.txt new file mode 100644 index 0000000..63fcf25 --- /dev/null +++ b/updates/0.20/ver_0.258_files.txt @@ -0,0 +1,5 @@ +F: ../autoload/admin/controls/class.Newsletter.php +F: ../autoload/admin/view/class.Newsletter.php +F: ../admin/templates/newsletter/prepare.php +F: ../admin/templates/newsletter/preview.php +F: ../admin/templates/newsletter/email-templates-user.php \ No newline at end of file diff --git a/updates/changelog.php b/updates/changelog.php index b402229..98bf05a 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,4 +1,18 @@ -ver. 0.256 - 12.02.2026
+ver. 0.258 - 12.02.2026
+- UPDATE - modul `Newsletter`: funkcjonalnosc `Wysylka - przygotowanie` zostala tymczasowo wylaczona (menu + akcje `prepare/send/preview`) +- UPDATE - modul `Newsletter`: lista `Szablony uzytkownika` zostala tymczasowo wylaczona (menu + akcja `email_templates_user`) +- UPDATE - `NewsletterController`: lista szablonow ograniczona do szablonow administracyjnych (`is_admin = 1`) +- UPDATE - `email_template_edit` i `template_save` obsluguja tylko szablony administracyjne +- CLEANUP - usuniete nieuzywane szablony newslettera: `admin/templates/newsletter/prepare.php`, `admin/templates/newsletter/preview.php`, `admin/templates/newsletter/email-templates-user.php` +- UPDATE - plik do usuniecia dodany w `updates/0.20/ver_0.258_files.txt` +
ver. 0.257 - 12.02.2026
+- NEW - migracja modulu `Newsletter` do architektury Domain + DI (`Domain\\Newsletter\\NewsletterRepository`, `Domain\\Newsletter\\NewsletterPreviewRenderer`, `admin\\Controllers\\NewsletterController`) +- UPDATE - widoki `/admin/newsletter/*` przepiete z legacy `grid/gridEdit` na nowe komponenty (`components/table-list`, `components/form-edit`) + nowy endpoint `/admin/newsletter/preview/` +- UPDATE - routing DI (`admin\\Site`) rozszerzony o moduł `Newsletter` +- UPDATE - `admin\\factory\\Newsletter` dziala jako fasada do nowego repozytorium (backward compatibility) +- UPDATE - `front\\factory\\Newsletter` nie korzysta juz z `admin\\view\\Newsletter` +- CLEANUP - usuniete legacy klasy `autoload/admin/controls/class.Newsletter.php`, `autoload/admin/view/class.Newsletter.php` +
ver. 0.256 - 12.02.2026
- NEW - migracja modulu `Layouts` do architektury Domain + DI (`Domain\\Layouts\\LayoutsRepository`, `admin\\Controllers\\LayoutsController`) - UPDATE - lista `/admin/layouts/view_list/` przepieta z legacy `grid` na `components/table-list` (filtry, sortowanie, paginacja) - UPDATE - `layouts/layout-edit` korzysta z danych z repozytorium (menus/categories), bez wywolan legacy factory w widoku diff --git a/updates/versions.php b/updates/versions.php index b64e3e9..59bbd79 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@