';
+
+ // 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(\Shared\Html\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(\Shared\Html\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 \Shared\Html\Html::input_switch([
+ 'label' => $field->label,
+ 'name' => $name,
+ 'id' => $id,
+ 'checked' => (bool) $value,
+ ]);
+
+ case FormFieldType::SELECT:
+ return $this->wrapWithError(\Shared\Html\Html::select([
+ 'label' => $field->label,
+ 'name' => $name,
+ 'id' => $id,
+ 'value' => $value ?? '',
+ 'values' => $field->options,
+ 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
+ ]), $error);
+
+ default: // TEXT, URL, etc.
+ if (!empty($field->attributes['icon_content'])) {
+ $iconJs = (string)($field->attributes['icon_js'] ?? '');
+ if ($iconJs !== '') {
+ $iconJs = str_replace('{lang}', (string)$languageId, $iconJs);
+ }
+
+ return $this->wrapWithError(\Shared\Html\Html::input_icon([
+ 'label' => $field->label,
+ 'name' => $name,
+ 'id' => $id,
+ 'value' => $value ?? '',
+ 'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text',
+ 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
+ 'icon_content' => (string)$field->attributes['icon_content'],
+ 'icon_class' => (string)($field->attributes['icon_class'] ?? ''),
+ 'icon_js' => $iconJs,
+ ]), $error);
+ }
+
+ return $this->wrapWithError(\Shared\Html\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..133e91b
--- /dev/null
+++ b/autoload/admin/Support/Forms/FormRequestHandler.php
@@ -0,0 +1,159 @@
+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 CSRF
+ $csrfToken = isset($postData['_csrf_token']) ? (string) $postData['_csrf_token'] : '';
+ if (!\Shared\Security\CsrfToken::validate($csrfToken)) {
+ $result['errors'] = ['csrf' => 'Nieprawidłowy token bezpieczeństwa. Odśwież stronę i spróbuj ponownie.'];
+ return $result;
+ }
+
+ // 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/Support/TableListRequestFactory.php b/autoload/admin/Support/TableListRequestFactory.php
new file mode 100644
index 0000000..801729c
--- /dev/null
+++ b/autoload/admin/Support/TableListRequestFactory.php
@@ -0,0 +1,99 @@
+,
+ * filters:array