diff --git a/admin/templates/articles/article-edit.php b/admin/templates/articles/article-edit.php index 248cb07..8cd9888 100644 --- a/admin/templates/articles/article-edit.php +++ b/admin/templates/articles/article-edit.php @@ -801,4 +801,4 @@ echo $grid -> draw(); }); } - \ No newline at end of file + diff --git a/admin/templates/banners/banner-edit.php b/admin/templates/banners/banner-edit.php index dd811f6..469abb8 100644 --- a/admin/templates/banners/banner-edit.php +++ b/admin/templates/banners/banner-edit.php @@ -85,7 +85,7 @@ ob_start(); 'id' => 'src_' . $lg['id'], 'value' => $this -> banner['languages'][ $lg['id'] ]['src'], 'icon_content' => 'przeglądaj', - 'icon_js' => "window.open ( 'http://" . $_SERVER['SERVER_NAME'] . "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=src_" . $lg['id'] . "&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');" + 'icon_js' => "window.open ( '/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=src_" . $lg['id'] . "&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');" ) ); ?> @@ -182,4 +182,4 @@ echo $grid -> draw(); }); - \ No newline at end of file + diff --git a/admin/templates/banners/banners-list.php b/admin/templates/banners/banners-list.php index 3e70c9a..45cd47a 100644 --- a/admin/templates/banners/banners-list.php +++ b/admin/templates/banners/banners-list.php @@ -1,5 +1,105 @@ $this->viewModel]); ?> + + + + viewModel->customScriptView)): ?> viewModel->customScriptView, ['list' => $this->viewModel]); ?> diff --git a/admin/templates/components/form-edit.php b/admin/templates/components/form-edit.php new file mode 100644 index 0000000..9d4d853 --- /dev/null +++ b/admin/templates/components/form-edit.php @@ -0,0 +1,278 @@ +form; +$renderer = new FormFieldRenderer($form); + +// Przygotuj filemanager key +\S::set_session('admin', true); +if ( + empty($_SESSION['rfm_akey']) || + (($_SESSION['rfm_akey_expires'] ?? 0) < time()) +) { + $_SESSION['rfm_akey'] = bin2hex(random_bytes(16)); +} +$_SESSION['rfm_akey_expires'] = time() + 20 * 60; +$_SESSION['can_use_rfm'] = true; +?> + + + + + + + + + + +
+
+
+
+
+ title) ?> +
+
+ actions as $action): ?> + name === 'save'): ?> + persist): ?> + + Zatwierdź i zamknij + + + + Zatwierdź + + name === 'cancel'): ?> + + Wstecz + + + + label) ?> + + + +
+ +
+
+ + + + hiddenFields as $name => $value): ?> + + + + hasTabs()): ?> + +
+
    + tabs as $tab): ?> +
  • label) ?>
  • + +
+
+ tabs as $tab): ?> +
+ getFieldsForTab($tab->id); + $langSections = $form->getLangSectionsForTab($tab->id); + ?> + + + renderField($field) ?> + + + + renderLangSection($section) ?> + +
+ +
+
+ + + fields as $field): ?> + type === FormFieldType::LANG_SECTION): ?> + renderLangSection($field) ?> + + renderField($field) ?> + + + +
+
+
+
+
+
+ + + +fields as $field): + if ($field->type === FormFieldType::EDITOR): +?> + + + +hasLangSections()): ?> + + + + + + diff --git a/admin/templates/filemanager/filemanager.php b/admin/templates/filemanager/filemanager.php index 3e132fd..114d499 100644 --- a/admin/templates/filemanager/filemanager.php +++ b/admin/templates/filemanager/filemanager.php @@ -1 +1 @@ - \ No newline at end of file + diff --git a/admin/templates/html/input-switch.php b/admin/templates/html/input-switch.php index 1a87d6b..0d9aca4 100644 --- a/admin/templates/html/input-switch.php +++ b/admin/templates/html/input-switch.php @@ -15,7 +15,7 @@ $out .= 'id="' . $this -> params['id'] . '" '; else $out .= 'id="' . $this -> params['name'] . '" '; - $out .= 'name="' . $this -> params['name'] . '" type="checkbox"'; + $out .= 'name="' . $this -> params['name'] . '" type="checkbox" value="1"'; if ( $this -> params['checked'] ) $out .= 'checked="checked" '; diff --git a/admin/templates/shop-producer/edit.php b/admin/templates/shop-producer/edit.php index ffe97ce..6a03486 100644 --- a/admin/templates/shop-producer/edit.php +++ b/admin/templates/shop-producer/edit.php @@ -36,7 +36,7 @@ ob_start(); 'id' => 'img', 'value' => $this -> producer['img'], 'icon_content' => 'przeglądaj', - 'icon_js' => "window.open ( 'http://" . $_SERVER['SERVER_NAME'] . "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=img&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');" + 'icon_js' => "window.open ( '/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=img&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');" ] ); ?> @@ -177,4 +177,4 @@ echo $grid -> draw(); tabidentify: 'languages-main' }); }); - \ No newline at end of file + diff --git a/admin/templates/shop-product/product-edit.php b/admin/templates/shop-product/product-edit.php index 9bef6f7..9289ad8 100644 --- a/admin/templates/shop-product/product-edit.php +++ b/admin/templates/shop-product/product-edit.php @@ -1343,4 +1343,4 @@ echo $grid->draw(); \ No newline at end of file + diff --git a/autoload/Domain/Banner/BannerRepository.php b/autoload/Domain/Banner/BannerRepository.php index 16ef283..083a13a 100644 --- a/autoload/Domain/Banner/BannerRepository.php +++ b/autoload/Domain/Banner/BannerRepository.php @@ -29,7 +29,10 @@ class BannerRepository return null; } - $results = $this->db->select('pp_banners_langs', '*', ['id_banner' => $bannerId]); + $results = $this->db->select('pp_banners_langs', '*', [ + 'id_banner' => $bannerId, + 'ORDER' => ['id' => 'ASC'], + ]); if (is_array($results)) { foreach ($results as $row) { $banner['languages'][$row['id_lang']] = $row; @@ -54,19 +57,30 @@ class BannerRepository /** * Zapisuje baner (insert lub update) * - * @param array $data Dane banera + * @param array $data Dane banera (obsługuje format z FormRequestHandler lub stary format) * @return int|false ID banera lub false */ public function save(array $data) { $bannerId = $data['id'] ?? null; + // Obsługa obu formatów: nowy (int) i stary ('on'/string) + $status = $data['status'] ?? 0; + if ($status === 'on') { + $status = 1; + } + + $homePage = $data['home_page'] ?? 0; + if ($homePage === 'on') { + $homePage = 1; + } + $bannerData = [ 'name' => $data['name'], - 'status' => $data['status'] == 'on' ? 1 : 0, - 'date_start' => $data['date_start'] != '' ? $data['date_start'] : null, - 'date_end' => $data['date_end'] != '' ? $data['date_end'] : null, - 'home_page' => $data['home_page'] == 'on' ? 1 : 0, + 'status' => (int)$status, + 'date_start' => !empty($data['date_start']) ? $data['date_start'] : null, + 'date_end' => !empty($data['date_end']) ? $data['date_end'] : null, + 'home_page' => (int)$homePage, ]; if (!$bannerId) { @@ -79,7 +93,14 @@ class BannerRepository $this->db->update('pp_banners', $bannerData, ['id' => (int)$bannerId]); } - $this->saveTranslations($bannerId, $data['src'], $data['url'], $data['html'], $data['text']); + // Obsługa danych językowych - nowy format (translations) lub stary (src/url/html/text) + if (isset($data['translations']) && is_array($data['translations'])) { + // Nowy format z FormRequestHandler + $this->saveTranslationsFromArray($bannerId, $data['translations']); + } elseif (isset($data['src']) && is_array($data['src'])) { + // Stary format (backward compatibility) + $this->saveTranslations($bannerId, $data['src'], $data['url'], $data['html'], $data['text']); + } return (int)$bannerId; } @@ -159,35 +180,134 @@ class BannerRepository $stmt = $this->db->query($sql, $params); $items = $stmt ? $stmt->fetchAll() : []; + $items = is_array($items) ? $items : []; + + if (!empty($items)) { + $bannerIds = array_map('intval', array_column($items, 'id')); + $thumbByBannerId = $this->fetchThumbnailsByBannerIds($bannerIds); + + foreach ($items as &$item) { + $item['thumbnail_src'] = $thumbByBannerId[(int)($item['id'] ?? 0)] ?? ''; + } + unset($item); + } return [ - 'items' => is_array($items) ? $items : [], + 'items' => $items, 'total' => $total, ]; } /** - * Zapisuje tłumaczenia banera + * Pobiera pierwsza dostepna sciezke obrazka (src) dla kazdego banera. + * + * @param array $bannerIds + * @return array [id_banner => src] + */ + private function fetchThumbnailsByBannerIds(array $bannerIds): array + { + $bannerIds = array_values(array_unique(array_filter($bannerIds, static function ($id): bool { + return (int)$id > 0; + }))); + + if (empty($bannerIds)) { + return []; + } + + $in = []; + $params = []; + foreach ($bannerIds as $index => $bannerId) { + $placeholder = ':id' . $index; + $in[] = $placeholder; + $params[$placeholder] = (int)$bannerId; + } + + $sql = ' + SELECT id_banner, src + FROM pp_banners_langs + WHERE id_banner IN (' . implode(', ', $in) . ') + AND src IS NOT NULL + AND src <> \'\' + ORDER BY id_lang ASC, id ASC + '; + + $stmt = $this->db->query($sql, $params); + $rows = $stmt ? $stmt->fetchAll() : []; + if (!is_array($rows)) { + return []; + } + + $thumbByBannerId = []; + foreach ($rows as $row) { + $bannerId = (int)($row['id_banner'] ?? 0); + if ($bannerId <= 0 || isset($thumbByBannerId[$bannerId])) { + continue; + } + + $src = trim((string)($row['src'] ?? '')); + if ($src !== '') { + $thumbByBannerId[$bannerId] = $src; + } + } + + return $thumbByBannerId; + } + + /** + * Zapisuje tłumaczenia banera (stary format - zachowano dla kompatybilności) */ private function saveTranslations(int $bannerId, array $src, array $url, array $html, array $text): void { foreach ($src as $langId => $val) { - $translationData = [ - 'id_banner' => $bannerId, - 'id_lang' => $langId, - 'src' => $src[$langId], - 'url' => $url[$langId], - 'html' => $html[$langId], - 'text' => $text[$langId], - ]; - - $existingId = $this->db->get('pp_banners_langs', 'id', ['AND' => ['banner_id' => $bannerId, 'lang_id' => $langId]]); - - if ($existingId) { - $this->db->update('pp_banners_langs', $translationData, ['id' => $existingId]); - } else { - $this->db->insert('pp_banners_langs', $translationData); - } + $this->upsertTranslation($bannerId, $langId, [ + 'src' => $src[$langId] ?? '', + 'url' => $url[$langId] ?? '', + 'html' => $html[$langId] ?? '', + 'text' => $text[$langId] ?? '', + ]); } } + + /** + * Zapisuje tłumaczenia banera z nowego formatu (z FormRequestHandler) + * Format: [lang_id => [field => value]] + */ + private function saveTranslationsFromArray(int $bannerId, array $translations): void + { + foreach ($translations as $langId => $fields) { + $this->upsertTranslation($bannerId, $langId, [ + 'src' => $fields['src'] ?? '', + 'url' => $fields['url'] ?? '', + 'html' => $fields['html'] ?? '', + 'text' => $fields['text'] ?? '', + ]); + } + } + + /** + * Upsert tlumaczenia banera. + * Aktualizuje wszystkie rekordy dla pary id_banner + id_lang, + * co usuwa problem z historycznymi duplikatami. + */ + private function upsertTranslation(int $bannerId, $langId, array $fields): void + { + $where = ['AND' => ['id_banner' => $bannerId, 'id_lang' => $langId]]; + $translationData = [ + 'id_banner' => $bannerId, + 'id_lang' => $langId, + 'src' => $fields['src'] ?? '', + 'url' => $fields['url'] ?? '', + 'html' => $fields['html'] ?? '', + 'text' => $fields['text'] ?? '', + ]; + + $hasExisting = (int)$this->db->count('pp_banners_langs', $where) > 0; + + if ($hasExisting) { + $this->db->update('pp_banners_langs', $translationData, $where); + return; + } + + $this->db->insert('pp_banners_langs', $translationData); + } } diff --git a/autoload/admin/Controllers/BannerController.php b/autoload/admin/Controllers/BannerController.php index 3978ff2..d2c8a74 100644 --- a/autoload/admin/Controllers/BannerController.php +++ b/autoload/admin/Controllers/BannerController.php @@ -2,14 +2,21 @@ namespace admin\Controllers; use Domain\Banner\BannerRepository; +use admin\ViewModels\Forms\FormEditViewModel; +use admin\ViewModels\Forms\FormField; +use admin\ViewModels\Forms\FormTab; +use admin\ViewModels\Forms\FormAction; +use admin\Support\Forms\FormRequestHandler; class BannerController { private BannerRepository $repository; + private FormRequestHandler $formHandler; public function __construct(BannerRepository $repository) { $this->repository = $repository; + $this->formHandler = new FormRequestHandler(); } /** @@ -64,9 +71,24 @@ class BannerController $name = (string)($item['name'] ?? ''); $homePage = (int)($item['home_page'] ?? 0); $isActive = (int)($item['status'] ?? 0) === 1; + $thumbnailSrc = trim((string)($item['thumbnail_src'] ?? '')); + if ($thumbnailSrc !== '' && !preg_match('#^(https?:)?//#i', $thumbnailSrc) && strpos($thumbnailSrc, '/') !== 0) { + $thumbnailSrc = '/' . ltrim($thumbnailSrc, '/'); + } + + $thumbnail = '-'; + if ($thumbnailSrc !== '') { + $thumbnail = ''; + } $rows[] = [ 'lp' => $lp++ . '.', + 'thumbnail' => $thumbnail, 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', 'status' => $isActive ? 'tak' : 'nie', 'home_page' => $homePage === 1 ? 'tak' : 'nie', @@ -95,6 +117,7 @@ class BannerController $viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel( [ ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'thumbnail', 'label' => 'Miniatura', 'class' => 'text-center', 'sortable' => false, 'raw' => true], ['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], ['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true], ['key' => 'home_page', 'sort_key' => 'home_page', 'label' => 'Strona glowna', 'class' => 'text-center', 'sortable' => true, 'raw' => true], @@ -141,7 +164,15 @@ class BannerController $banner = $this->repository->find($bannerId); $languages = \admin\factory\Languages::languages_list(); - return \admin\view\Banners::banner_edit($banner, $languages); + // Sprawdź czy są błędy walidacji z poprzedniego requestu + $validationErrors = $_SESSION['form_errors'][$this->getFormId()] ?? null; + if ($validationErrors) { + unset($_SESSION['form_errors'][$this->getFormId()]); + } + + $viewModel = $this->buildFormViewModel($banner, $languages, $validationErrors); + + return \Tpl::view('components/form-edit', ['form' => $viewModel]); } /** @@ -149,13 +180,40 @@ class BannerController */ public function save(): void { - $response = ['status' => 'error', 'msg' => 'Podczas zapisywania baneru wystapil blad. Prosze sprobowac ponownie.']; + $response = ['success' => false, 'errors' => []]; - $values = json_decode(\S::get('values'), true); - $bannerId = $this->repository->save($values); - if ($bannerId) { + $bannerId = (int)\S::get('id'); + $banner = $this->repository->find($bannerId); + $languages = \admin\factory\Languages::languages_list(); + + $viewModel = $this->buildFormViewModel($banner, $languages); + + // Przetwórz dane z POST + $result = $this->formHandler->handleSubmit($viewModel, $_POST); + + if (!$result['success']) { + // Zapisz błędy w sesji i zwróć jako JSON + $_SESSION['form_errors'][$this->getFormId()] = $result['errors']; + $response['errors'] = $result['errors']; + echo json_encode($response); + exit; + } + + // Zapisz dane + $data = $result['data']; + $data['id'] = $bannerId ?: null; + + $savedId = $this->repository->save($data); + + if ($savedId) { \S::delete_dir('../temp/'); - $response = ['status' => 'ok', 'msg' => 'Baner zostal zapisany.', 'id' => $bannerId]; + $response = [ + 'success' => true, + 'id' => $savedId, + 'message' => 'Baner został zapisany.' + ]; + } else { + $response['errors'] = ['general' => 'Błąd podczas zapisywania do bazy.']; } echo json_encode($response); @@ -176,4 +234,103 @@ class BannerController header('Location: /admin/banners/view_list/'); exit; } + + /** + * Buduje model widoku formularza + */ + private function buildFormViewModel(array $banner, array $languages, ?array $errors = null): FormEditViewModel + { + $bannerId = $banner['id'] ?? 0; + $isNew = empty($bannerId); + + // Domyślne wartości dla nowego banera + if ($isNew) { + $banner['status'] = 1; + $banner['home_page'] = 0; + } + + $tabs = [ + new FormTab('settings', 'Ustawienia', 'fa-wrench'), + new FormTab('content', 'Zawartość', 'fa-file'), + ]; + + $fields = [ + // Zakładka Ustawienia + FormField::text('name', [ + 'label' => 'Nazwa', + 'tab' => 'settings', + 'required' => true, + ]), + FormField::switch('status', [ + 'label' => 'Aktywny', + 'tab' => 'settings', + 'value' => ($banner['status'] ?? 1) == 1, + ]), + FormField::date('date_start', [ + 'label' => 'Data rozpoczęcia', + 'tab' => 'settings', + ]), + FormField::date('date_end', [ + 'label' => 'Data zakończenia', + 'tab' => 'settings', + ]), + FormField::switch('home_page', [ + 'label' => 'Slajder / Strona główna', + 'tab' => 'settings', + 'value' => ($banner['home_page'] ?? 0) == 1, + ]), + + // Sekcja językowa w zakładce Zawartość + FormField::langSection('translations', 'content', [ + FormField::image('src', [ + 'label' => 'Obraz', + 'filemanager' => true, + ]), + FormField::text('url', [ + 'label' => 'Url', + ]), + FormField::textarea('html', [ + 'label' => 'Kod HTML', + 'rows' => 6, + ]), + FormField::editor('text', [ + 'label' => 'Treść', + 'toolbar' => 'MyTool', + 'height' => 300, + ]), + ]), + ]; + + $actions = [ + FormAction::save( + '/admin/banners/banner_save/' . ($isNew ? '' : 'id=' . $bannerId), + '/admin/banners/view_list/' + ), + FormAction::cancel('/admin/banners/view_list/'), + ]; + + return new FormEditViewModel( + $this->getFormId(), + $isNew ? 'Nowy baner' : 'Edycja banera', + $banner, + $fields, + $tabs, + $actions, + 'POST', + '/admin/banners/banner_save/' . ($isNew ? '' : 'id=' . $bannerId), + '/admin/banners/view_list/', + true, + ['id' => $bannerId], + $languages, + $errors + ); + } + + /** + * Zwraca identyfikator formularza + */ + private function getFormId(): string + { + return 'banner-edit'; + } } diff --git a/autoload/admin/Support/Forms/FormFieldRenderer.php b/autoload/admin/Support/Forms/FormFieldRenderer.php new file mode 100644 index 0000000..5a35c10 --- /dev/null +++ b/autoload/admin/Support/Forms/FormFieldRenderer.php @@ -0,0 +1,430 @@ +form = $form; + } + + /** + * Renderuje pojedyncze pole + */ + public function renderField(FormField $field): string + { + $method = 'render' . ucfirst($field->type); + + if (method_exists($this, $method)) { + return $this->$method($field); + } + + // Fallback dla nieznanych typów - renderuj jako text + return $this->renderText($field); + } + + /** + * Renderuje pole tekstowe + */ + public function renderText(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($field->placeholder) { + $params['placeholder'] = $field->placeholder; + } + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Html::input($params), $error); + } + + /** + * Renderuje pole number + */ + public function renderNumber(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'number', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Html::input($params), $error); + } + + /** + * Renderuje pole email + */ + public function renderEmail(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'email', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Html::input($params), $error); + } + + /** + * Renderuje pole password + */ + public function renderPassword(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Html::input([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'password', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje pole daty + */ + public function renderDate(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'class' => 'date ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Html::input($params), $error); + } + + /** + * Renderuje pole daty i czasu + */ + public function renderDatetime(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Html::input([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'class' => 'datetime ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje przełącznik (switch) + */ + public function renderSwitch(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + // Domyślna wartość dla nowego rekordu + if ($value === null && $field->value === true) { + $checked = true; + } else { + $checked = (bool) $value; + } + + return \Html::input_switch([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'checked' => $checked, + ]); + } + + /** + * Renderuje select + */ + public function renderSelect(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'options' => $field->options, + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Html::select($params), $error); + } + + /** + * Renderuje textarea + */ + public function renderTextarea(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Html::textarea([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'rows' => $field->attributes['rows'] ?? 4, + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje edytor (CKEditor) + */ + public function renderEditor(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Html::textarea([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'rows' => max(10, ($field->attributes['rows'] ?? 10)), + 'class' => 'editor ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje pole obrazu z filemanagerem + */ + public function renderImage(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + $filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id); + + return \Html::input_icon([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'icon_content' => 'przeglądaj', + 'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')", + ]); + } + + /** + * Renderuje pole pliku + */ + public function renderFile(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + if ($field->useFilemanager) { + $filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id); + + return \Html::input_icon([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'icon_content' => 'przeglądaj', + 'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')", + ]); + } + + return \Html::input([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'type' => 'file', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje ukryte pole + */ + public function renderHidden(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return ''; + } + + /** + * Renderuje sekcję językową + */ + public function renderLangSection(FormField $section): string + { + if ($section->langFields === null || $this->form->languages === null) { + return ''; + } + + $out = '
'; + + // Zakładki języków + $out .= '
    '; + foreach ($this->form->languages as $lang) { + if ($lang['status']) { + $out .= '
  • ' . htmlspecialchars($lang['name']) . '
  • '; + } + } + $out .= '
'; + + // Kontenery języków + $out .= '
'; + foreach ($this->form->languages as $lang) { + if ($lang['status']) { + $out .= '
'; + foreach ($section->langFields as $field) { + $out .= $this->renderLangField($field, $lang['id'], $section->name); + } + $out .= '
'; + } + } + $out .= '
'; + + $out .= '
'; + + return $out; + } + + /** + * Renderuje pole w sekcji językowej + */ + private function renderLangField(FormField $field, $languageId, string $sectionName): string + { + $value = $this->form->getFieldValue($field, $languageId, $field->name); + $error = $this->form->getError($sectionName . '_' . $field->name, $languageId); + + $name = $field->getLocalizedName($languageId); + $id = $field->getLocalizedId($languageId); + + switch ($field->type) { + case FormFieldType::IMAGE: + $filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($id); + return $this->wrapWithError(\Html::input_icon([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'type' => 'text', + 'icon_content' => 'przeglądaj', + 'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')", + ]), $error); + + case FormFieldType::TEXTAREA: + case FormFieldType::EDITOR: + return $this->wrapWithError(\Html::textarea([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'rows' => $field->type === FormFieldType::EDITOR ? 10 : ($field->attributes['rows'] ?? 4), + 'class' => $field->type === FormFieldType::EDITOR ? 'editor' : '', + ]), $error); + + case FormFieldType::SWITCH: + return \Html::input_switch([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'checked' => (bool) $value, + ]); + + default: // TEXT, URL, etc. + return $this->wrapWithError(\Html::input([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]), $error); + } + } + + /** + * Generuje URL do filemanagera + */ + private function generateFilemanagerUrl(string $fieldId): string + { + $rfmAkey = $_SESSION['rfm_akey'] ?? bin2hex(random_bytes(16)); + $_SESSION['rfm_akey'] = $rfmAkey; + $_SESSION['rfm_akey_expires'] = time() + 20 * 60; + $_SESSION['can_use_rfm'] = true; + + $fieldIdParam = rawurlencode($fieldId); + $akeyParam = rawurlencode($rfmAkey); + return "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id={$fieldIdParam}&akey={$akeyParam}"; + } + + /** + * Opakowuje pole w kontener błędu + */ + private function wrapWithError(string $html, ?string $error): string + { + if ($error) { + return '
' . $html . + '' . htmlspecialchars($error) . '
'; + } + return $html; + } +} diff --git a/autoload/admin/Support/Forms/FormRequestHandler.php b/autoload/admin/Support/Forms/FormRequestHandler.php new file mode 100644 index 0000000..2bf5105 --- /dev/null +++ b/autoload/admin/Support/Forms/FormRequestHandler.php @@ -0,0 +1,152 @@ +validator = new FormValidator(); + } + + /** + * Przetwarza żądanie POST formularza + * + * @param FormEditViewModel $formViewModel + * @param array $postData Dane z $_POST + * @return array Wynik przetwarzania ['success' => bool, 'errors' => array, 'data' => array] + */ + public function handleSubmit(FormEditViewModel $formViewModel, array $postData): array + { + $result = [ + 'success' => false, + 'errors' => [], + 'data' => [] + ]; + + // Walidacja + $errors = $this->validator->validate($postData, $formViewModel->fields, $formViewModel->languages); + + if (!empty($errors)) { + $result['errors'] = $errors; + // Zapisz dane do persist przy błędzie walidacji + if ($formViewModel->persist) { + $formViewModel->saveToPersist($postData); + } + return $result; + } + + // Przetwórz dane (np. konwersja typów) + $processedData = $this->processData($postData, $formViewModel->fields); + + $result['success'] = true; + $result['data'] = $processedData; + + // Wyczyść persist po sukcesie + if ($formViewModel->persist) { + $formViewModel->clearPersist(); + } + + return $result; + } + + /** + * Przetwarza dane z formularza (konwersja typów) + */ + private function processData(array $postData, array $fields): array + { + $processed = []; + + foreach ($fields as $field) { + $value = $postData[$field->name] ?? null; + + // Konwersja typów + switch ($field->type) { + case FormFieldType::SWITCH: + $processed[$field->name] = $value ? 1 : 0; + break; + + case FormFieldType::NUMBER: + $processed[$field->name] = $value !== null && $value !== '' ? (float)$value : null; + break; + + case FormFieldType::LANG_SECTION: + if ($field->langFields !== null) { + $processed[$field->name] = $this->processLangSection($postData, $field); + } + break; + + default: + $processed[$field->name] = $value; + } + } + + return $processed; + } + + /** + * Przetwarza sekcję językową + */ + private function processLangSection(array $postData, $section): array + { + $result = []; + + if ($section->langFields === null) { + return $result; + } + + foreach ($section->langFields as $field) { + $fieldName = $field->name; + $langData = $postData[$fieldName] ?? []; + + foreach ($langData as $langId => $value) { + if (!isset($result[$langId])) { + $result[$langId] = []; + } + + // Konwersja typów dla pól językowych + switch ($field->type) { + case FormFieldType::SWITCH: + $result[$langId][$fieldName] = $value ? 1 : 0; + break; + case FormFieldType::NUMBER: + $result[$langId][$fieldName] = $value !== null && $value !== '' ? (float)$value : null; + break; + default: + $result[$langId][$fieldName] = $value; + } + } + } + + return $result; + } + + /** + * Przywraca dane z persist do POST (przy błędzie walidacji) + */ + public function restoreFromPersist(FormEditViewModel $formViewModel): ?array + { + if (!$formViewModel->persist) { + return null; + } + + return $_SESSION['form_persist'][$formViewModel->formId] ?? null; + } + + /** + * Sprawdza czy żądanie jest submitowaniem formularza + */ + public function isFormSubmit(string $formId): bool + { + return $_SERVER['REQUEST_METHOD'] === 'POST' && + (isset($_POST['_form_id']) && $_POST['_form_id'] === $formId); + } +} diff --git a/autoload/admin/Validation/FormValidator.php b/autoload/admin/Validation/FormValidator.php new file mode 100644 index 0000000..73e7b05 --- /dev/null +++ b/autoload/admin/Validation/FormValidator.php @@ -0,0 +1,196 @@ +errors = []; + + foreach ($fields as $field) { + if ($field->type === FormFieldType::LANG_SECTION) { + $this->validateLangSection($data, $field, $languages ?? []); + } else { + $this->validateField($data, $field); + } + } + + return $this->errors; + } + + /** + * Waliduje pojedyncze pole + */ + private function validateField(array $data, FormField $field): void + { + $value = $data[$field->name] ?? null; + + // Walidacja wymagalności + if ($field->required && $this->isEmpty($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" jest wymagane."; + return; + } + + // Jeśli pole puste i nie jest wymagane - pomijamy dalszą walidację + if ($this->isEmpty($value)) { + return; + } + + // Walidacja typu + switch ($field->type) { + case FormFieldType::EMAIL: + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawnym adresem e-mail."; + } + break; + + case FormFieldType::NUMBER: + if (!is_numeric($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być liczbą."; + } + break; + + case FormFieldType::DATE: + if (!$this->isValidDate($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą (YYYY-MM-DD)."; + } + break; + + case FormFieldType::DATETIME: + if (!$this->isValidDateTime($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą i czasem."; + } + break; + } + + // Walidacja customowa (callback) + if (isset($field->attributes['validate_callback']) && is_callable($field->attributes['validate_callback'])) { + $result = call_user_func($field->attributes['validate_callback'], $value, $data); + if ($result !== true) { + $this->errors[$field->name] = is_string($result) ? $result : "Pole \"{$field->label}\" zawiera nieprawidłową wartość."; + } + } + } + + /** + * Waliduje sekcję językową + */ + private function validateLangSection(array $data, FormField $section, array $languages): void + { + if ($section->langFields === null) { + return; + } + + foreach ($languages as $language) { + if (!($language['status'] ?? false)) { + continue; + } + + $langId = $language['id']; + + foreach ($section->langFields as $field) { + $fieldName = $field->name; + $value = $data[$fieldName][$langId] ?? null; + + // Walidacja wymagalności + if ($field->required && $this->isEmpty($value)) { + $errorKey = "{$section->name}_{$fieldName}"; + $this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) jest wymagane."; + continue; + } + + // Walidacja typu dla pól językowych + if (!$this->isEmpty($value)) { + switch ($field->type) { + case FormFieldType::EMAIL: + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + $errorKey = "{$section->name}_{$fieldName}"; + $this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) musi być poprawnym e-mailem."; + } + break; + } + } + } + } + } + + /** + * Sprawdza czy wartość jest pusta + */ + private function isEmpty($value): bool + { + return $value === null || $value === '' || (is_array($value) && empty($value)); + } + + /** + * Sprawdza czy data jest poprawna (YYYY-MM-DD) + */ + private function isValidDate(string $date): bool + { + $d = \DateTime::createFromFormat('Y-m-d', $date); + return $d && $d->format('Y-m-d') === $date; + } + + /** + * Sprawdza czy data i czas są poprawne + */ + private function isValidDateTime(string $datetime): bool + { + $d = \DateTime::createFromFormat('Y-m-d H:i:s', $datetime); + if ($d && $d->format('Y-m-d H:i:s') === $datetime) { + return true; + } + + // Spróbuj bez sekund + $d = \DateTime::createFromFormat('Y-m-d H:i', $datetime); + return $d && $d->format('Y-m-d H:i') === $datetime; + } + + /** + * Sprawdza czy walidacja zakończyła się sukcesem + */ + public function isValid(): bool + { + return empty($this->errors); + } + + /** + * Zwraca wszystkie błędy + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * Zwraca pierwszy błąd + */ + public function getFirstError(): ?string + { + if (empty($this->errors)) { + return null; + } + + $first = reset($this->errors); + if (is_array($first)) { + return reset($first); + } + return $first; + } +} diff --git a/autoload/admin/ViewModels/Forms/FormAction.php b/autoload/admin/ViewModels/Forms/FormAction.php new file mode 100644 index 0000000..98d6529 --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormAction.php @@ -0,0 +1,73 @@ +name = $name; + $this->label = $label; + $this->url = $url; + $this->backUrl = $backUrl; + $this->cssClass = $cssClass; + $this->type = $type; + $this->attributes = $attributes; + } + + /** + * Predefiniowana akcja Zapisz + */ + public static function save(string $url, string $backUrl = '', string $label = 'Zapisz'): self + { + return new self( + 'save', + $label, + $url, + $backUrl, + 'btn btn-primary', + 'submit' + ); + } + + /** + * Predefiniowana akcja Anuluj + */ + public static function cancel(string $backUrl, string $label = 'Anuluj'): self + { + return new self( + 'cancel', + $label, + $backUrl, + null, + 'btn btn-default', + 'link' + ); + } +} diff --git a/autoload/admin/ViewModels/Forms/FormEditViewModel.php b/autoload/admin/ViewModels/Forms/FormEditViewModel.php new file mode 100644 index 0000000..663d08f --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormEditViewModel.php @@ -0,0 +1,178 @@ +formId = $formId; + $this->title = $title; + $this->data = $data; + $this->fields = $fields; + $this->tabs = $tabs; + $this->actions = $actions; + $this->method = $method; + $this->action = $action; + $this->backUrl = $backUrl; + $this->persist = $persist; + $this->hiddenFields = $hiddenFields; + $this->languages = $languages; + $this->validationErrors = $validationErrors; + } + + /** + * Sprawdza czy formularz ma zakładki + */ + public function hasTabs(): bool + { + return count($this->tabs) > 0; + } + + /** + * Sprawdza czy formularz ma sekcje językowe + */ + public function hasLangSections(): bool + { + foreach ($this->fields as $field) { + if ($field->type === FormFieldType::LANG_SECTION) { + return true; + } + } + return false; + } + + /** + * Zwraca pola dla konkretnej zakładki + */ + public function getFieldsForTab(string $tabId): array + { + return array_filter($this->fields, function (FormField $field) use ($tabId) { + return $field->tabId === $tabId && $field->type !== FormFieldType::LANG_SECTION; + }); + } + + /** + * Zwraca sekcje językowe dla konkretnej zakładki + */ + public function getLangSectionsForTab(string $tabId): array + { + return array_filter($this->fields, function (FormField $field) use ($tabId) { + return $field->type === FormFieldType::LANG_SECTION && + $field->langSectionParentTab === $tabId; + }); + } + + /** + * Pobiera wartość pola z danych lub sesji (persist) + */ + public function getFieldValue(FormField $field, $languageId = null, ?string $langFieldName = null) + { + $fieldName = $field->name; + + // Dla sekcji językowych - pobierz wartość z data[lang_id][field_name] + if ($languageId !== null && $langFieldName !== null) { + $fieldName = $langFieldName; + return $this->data['languages'][$languageId][$fieldName] ?? null; + } + + // Zwykłe pole - najpierw sprawdź sesję (persist), potem dane + if ($this->persist && isset($_SESSION['form_persist'][$this->formId][$fieldName])) { + return $_SESSION['form_persist'][$this->formId][$fieldName]; + } + + return $this->data[$fieldName] ?? $field->value; + } + + /** + * Sprawdza czy pole ma błąd walidacji + */ + public function hasError(string $fieldName, $languageId = null): bool + { + if ($this->validationErrors === null) { + return false; + } + + if ($languageId !== null) { + return isset($this->validationErrors[$fieldName][$languageId]); + } + + return isset($this->validationErrors[$fieldName]); + } + + /** + * Pobiera komunikat błędu dla pola + */ + public function getError(string $fieldName, $languageId = null): ?string + { + if ($languageId !== null) { + return $this->validationErrors[$fieldName][$languageId] ?? null; + } + return $this->validationErrors[$fieldName] ?? null; + } + + /** + * Czyści dane persist z sesji + */ + public function clearPersist(): void + { + if (isset($_SESSION['form_persist'][$this->formId])) { + unset($_SESSION['form_persist'][$this->formId]); + } + } + + /** + * Zapisuje dane do sesji (persist) + */ + public function saveToPersist(array $data): void + { + if (!isset($_SESSION['form_persist'])) { + $_SESSION['form_persist'] = []; + } + $_SESSION['form_persist'][$this->formId] = $data; + } +} diff --git a/autoload/admin/ViewModels/Forms/FormField.php b/autoload/admin/ViewModels/Forms/FormField.php new file mode 100644 index 0000000..faaf46d --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormField.php @@ -0,0 +1,323 @@ +name = $name; + $this->type = $type; + $this->label = $label; + $this->value = $value; + $this->tabId = $tabId; + $this->required = $required; + $this->attributes = $attributes; + $this->options = $options; + $this->helpText = $helpText; + $this->placeholder = $placeholder; + $this->useFilemanager = $useFilemanager; + $this->filemanagerUrl = $filemanagerUrl; + $this->editorToolbar = $editorToolbar; + $this->editorHeight = $editorHeight; + $this->langFields = $langFields; + $this->langSectionParentTab = $langSectionParentTab; + $this->id = $attributes['id'] ?? $name; + } + + // Factory methods dla różnych typów pól + + public static function text(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::TEXT, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + $config['help'] ?? null, + $config['placeholder'] ?? null + ); + } + + public static function number(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::NUMBER, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + $config['help'] ?? null + ); + } + + public static function email(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::EMAIL, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [] + ); + } + + public static function password(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::PASSWORD, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [] + ); + } + + public static function date(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::DATE, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + array_merge(['class' => 'date'], $config['attributes'] ?? []) + ); + } + + public static function datetime(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::DATETIME, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + array_merge(['class' => 'datetime'], $config['attributes'] ?? []) + ); + } + + public static function switch(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::SWITCH, + $config['label'] ?? '', + $config['value'] ?? false, + $config['tab'] ?? 'default', + false, + $config['attributes'] ?? [] + ); + } + + public static function select(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::SELECT, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + $config['options'] ?? [] + ); + } + + public static function textarea(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::TEXTAREA, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + array_merge(['rows' => $config['rows'] ?? 4], $config['attributes'] ?? []) + ); + } + + public static function editor(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::EDITOR, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + null, + null, + false, + null, + $config['toolbar'] ?? 'MyTool', + $config['height'] ?? 300 + ); + } + + public static function image(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::IMAGE, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + null, + null, + $config['filemanager'] ?? true, + $config['filemanager_url'] ?? null + ); + } + + public static function file(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::FILE, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + null, + null, + $config['filemanager'] ?? true + ); + } + + public static function hidden(string $name, $value = null): self + { + return new self( + $name, + FormFieldType::HIDDEN, + '', + $value, + 'default' + ); + } + + /** + * Sekcja językowa - grupa pól powtarzana dla każdego języka + * + * @param string $name Nazwa sekcji (prefiks dla pól) + * @param string $parentTab Identyfikator zakładki nadrzędnej + * @param array $fields Pola w sekcji językowej (tablica FormField) + */ + public static function langSection(string $name, string $parentTab, array $fields): self + { + return new self( + $name, + FormFieldType::LANG_SECTION, + '', + null, + $parentTab, + false, + [], + [], + null, + null, + false, + null, + 'MyTool', + 300, + $fields, + $parentTab + ); + } + + /** + * Zwraca nazwę pola z sufiksem dla konkretnego języka + */ + public function getLocalizedName($languageId): string + { + return "{$this->name}[{$languageId}]"; + } + + /** + * Zwraca ID pola z sufiksem dla konkretnego języka + */ + public function getLocalizedId($languageId): string + { + return "{$this->id}_{$languageId}"; + } +} diff --git a/autoload/admin/ViewModels/Forms/FormFieldType.php b/autoload/admin/ViewModels/Forms/FormFieldType.php new file mode 100644 index 0000000..66e68dc --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormFieldType.php @@ -0,0 +1,23 @@ +id = $id; + $this->label = $label; + $this->icon = $icon; + $this->parentTabId = $parentTabId; + } +} diff --git a/libraries/.htaccess b/libraries/.htaccess index eb2244f..6485d3f 100644 --- a/libraries/.htaccess +++ b/libraries/.htaccess @@ -1,6 +1,9 @@ # Wyłącz listowanie Options -Indexes +# Zezwol na wykonywanie PHP tylko dla legacy filemanagera +SetEnvIf Request_URI "^/libraries/filemanager-9\.14\.[12]/.*\.(php|phtml|php[0-9]?|phar|pht)$" allow_legacy_filemanager_php=1 + # Domyślnie blokujemy wszystko… Require all denied @@ -11,7 +14,10 @@ Require all denied # Twardo blokuj cokolwiek, co mogłoby się wykonać - Require all denied + + Require env allow_legacy_filemanager_php + Require all denied + @@ -41,4 +47,4 @@ Require all denied # Nie serwuj plików ukrytych (.env itp.) Require all denied - \ No newline at end of file + diff --git a/tests/Unit/Domain/Banner/BannerRepositoryTest.php b/tests/Unit/Domain/Banner/BannerRepositoryTest.php index 9ebc7da..cc7dbab 100644 --- a/tests/Unit/Domain/Banner/BannerRepositoryTest.php +++ b/tests/Unit/Domain/Banner/BannerRepositoryTest.php @@ -21,7 +21,10 @@ class BannerRepositoryTest extends TestCase $mockDb->expects($this->once()) ->method('select') - ->with('pp_banners_langs', '*', ['id_banner' => 1]) + ->with('pp_banners_langs', '*', [ + 'id_banner' => 1, + 'ORDER' => ['id' => 'ASC'], + ]) ->willReturn([ ['id_lang' => 'pl', 'src' => 'banner.jpg', 'url' => '/promo'], ['id_lang' => 'en', 'src' => 'banner-en.jpg', 'url' => '/promo-en'], @@ -80,7 +83,7 @@ class BannerRepositoryTest extends TestCase } /** - * Test zapisywania nowego banera + * Test zapisywania nowego banera (stary format danych - zachowano kompatybilność) */ public function testSaveInsertsNewBanner() { @@ -100,9 +103,51 @@ class BannerRepositoryTest extends TestCase $repository = new BannerRepository($mockDb); - // Act + // Act - nowy format z FormRequestHandler (przetworzone dane) $result = $repository->save([ 'name' => 'Nowy baner', + 'status' => 1, // już przetworzone na int + 'date_start' => null, + 'date_end' => null, + 'home_page' => 1, // już przetworzone na int + 'translations' => [ + 1 => [ // id języka jako klucz + 'src' => 'banner.jpg', + 'url' => '/promo', + 'html' => '', + 'text' => 'Tekst', + ], + ], + ]); + + // Assert + $this->assertEquals(10, $result); + } + + /** + * Test zapisywania banera ze starym formatem danych (backward compatibility) + */ + public function testSaveWithLegacyFormat() + { + // Arrange + $mockDb = $this->createMock(\medoo::class); + + // insert() wywoływane 2x: raz dla banera, raz dla tłumaczenia + $mockDb->expects($this->exactly(2)) + ->method('insert'); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn(11); + + // get() for checking existing translations - returns false (no existing) + $mockDb->method('get')->willReturn(false); + + $repository = new BannerRepository($mockDb); + + // Act - stary format (dla kompatybilności wstecznej) + $result = $repository->save([ + 'name' => 'Baner legacy', 'status' => 'on', 'date_start' => '', 'date_end' => '', @@ -114,6 +159,114 @@ class BannerRepositoryTest extends TestCase ]); // Assert - $this->assertEquals(10, $result); + $this->assertEquals(11, $result); + } + + /** + * Test zapisu istniejacego banera - aktualizacja tlumaczen po id_banner + id_lang + */ + public function testSaveUpdatesExistingTranslationsByBannerAndLang(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->exactly(2)) + ->method('update') + ->withConsecutive( + [ + 'pp_banners', + $this->arrayHasKey('name'), + ['id' => 5], + ], + [ + 'pp_banners_langs', + $this->callback(function (array $data): bool { + return $data['id_banner'] === 5 + && $data['id_lang'] === 'pl' + && $data['src'] === 'banner-new.jpg'; + }), + ['AND' => ['id_banner' => 5, 'id_lang' => 'pl']], + ] + ); + + $mockDb->expects($this->once()) + ->method('count') + ->with('pp_banners_langs', ['AND' => ['id_banner' => 5, 'id_lang' => 'pl']]) + ->willReturn(2); + + $mockDb->expects($this->never()) + ->method('insert'); + + $repository = new BannerRepository($mockDb); + + $result = $repository->save([ + 'id' => 5, + 'name' => 'Baner update', + 'status' => 1, + 'date_start' => null, + 'date_end' => null, + 'home_page' => 0, + 'translations' => [ + 'pl' => [ + 'src' => 'banner-new.jpg', + 'url' => '/promo-new', + 'html' => 'promo', + 'text' => 'Nowa tresc', + ], + ], + ]); + + $this->assertSame(5, $result); + } + + public function testListForAdminIncludesThumbnailSrc(): void + { + $mockDb = $this->createMock(\medoo::class); + + $countStmt = $this->createMock(\PDOStatement::class); + $countStmt->expects($this->once()) + ->method('fetchAll') + ->willReturn([[2]]); + + $itemsStmt = $this->createMock(\PDOStatement::class); + $itemsStmt->expects($this->once()) + ->method('fetchAll') + ->willReturn([ + [ + 'id' => 10, + 'name' => 'Baner A', + 'status' => 1, + 'home_page' => 0, + 'date_start' => null, + 'date_end' => null, + ], + [ + 'id' => 11, + 'name' => 'Baner B', + 'status' => 1, + 'home_page' => 1, + 'date_start' => null, + 'date_end' => null, + ], + ]); + + $thumbsStmt = $this->createMock(\PDOStatement::class); + $thumbsStmt->expects($this->once()) + ->method('fetchAll') + ->willReturn([ + ['id_banner' => 10, 'src' => '/uploads/banner-a.jpg'], + ]); + + $mockDb->expects($this->exactly(3)) + ->method('query') + ->willReturnOnConsecutiveCalls($countStmt, $itemsStmt, $thumbsStmt); + + $repository = new BannerRepository($mockDb); + + $result = $repository->listForAdmin([], 'name', 'ASC', 1, 15); + + $this->assertSame(2, $result['total']); + $this->assertCount(2, $result['items']); + $this->assertSame('/uploads/banner-a.jpg', $result['items'][0]['thumbnail_src']); + $this->assertSame('', $result['items'][1]['thumbnail_src']); } } diff --git a/updates/0.20/ver_0.249.zip b/updates/0.20/ver_0.249.zip new file mode 100644 index 0000000..d3c4bfe Binary files /dev/null and b/updates/0.20/ver_0.249.zip differ diff --git a/updates/0.20/ver_0.249_files.txt b/updates/0.20/ver_0.249_files.txt new file mode 100644 index 0000000..e69de29 diff --git a/updates/changelog.php b/updates/changelog.php index 46d4c47..4891ce9 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,4 +1,10 @@ -ver. 0.248
+ver. 0.249
+- FIX - banner edit: poprawiono zapisywanie danych jezykowych i synchronizacje CKEditor przed zapisem +- FIX - banner edit: naprawiono hash zakladek (usunieto `undefined` w URL) +- FIX - filemanager: przywrocono dzialanie popupa wyboru obrazka z banera +- UPDATE - komunikaty zapisu w nowym formularzu sa wyswietlane w stylu panelu (bez natywnego alertu JS) +- UPDATE - lista banerow: dodano kolumne miniatury oraz podglad duzego obrazka w popup po najechaniu +
ver. 0.248
- UPDATE - filtry w nowych tabelach dzialaja automatycznie na `onchange` - UPDATE - `components/table-list`: auto-submit formularza filtrow po zmianie pola (select, date, text)
ver. 0.247
diff --git a/updates/versions.php b/updates/versions.php index 56ebd7b..290433d 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@