refactor solution to PHP through AI
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Agente PHP — Regole e Convenzioni
|
||||
|
||||
Questo file definisce le istruzioni specifiche, le regole e le convenzioni per lo sviluppo e la manutenzione delle pagine custom e dei componenti scritti in **PHP 8.0+** nella cartella `php/`.
|
||||
|
||||
## Gestione ID dei Form Element
|
||||
Gli ID dei campi del form (es. nel Dropdown, Checkbox, TextField) creati tramite i metodi della classe [ElixFormsComponent.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsComponent.php) **non devono mai essere cambiati o manipolati** (ad es. aggiungendo suffissi come `_dropdown`), in quanto la piattaforma elixForms si basa sul loro nome ed ID esatto.
|
||||
In caso di conflitti di ID (ad esempio con input `hidden`), applicare il suffisso `_hidden` all'input hidden e mantenere l'ID originale senza suffissi sull'elemento principale visibile.
|
||||
|
||||
## Convenzioni di Codice e Framework
|
||||
1. **PHP Puro (No Framework)**:
|
||||
- Utilizzare PHP 8.0+ con tipizzazione forte (`declare(strict_types=1);`).
|
||||
- Sfruttare le feature moderne come `match` expression, property promotion e typed properties.
|
||||
- Le pagine custom ereditano da `ElixFormsComponent` e sovrascrivono `createCustomFormFields()`.
|
||||
|
||||
2. **Namespace PSR-4**:
|
||||
- Tutte le classi nella libreria comune usano il namespace `ElixForms\Common;`.
|
||||
- Il caricamento è manuale tramite `require_once __DIR__ . '/../common/ElixFormsComponent.php';`.
|
||||
|
||||
3. **Rendering Server-Side**:
|
||||
- I file PHP generano direttamente l'HTML server-side usando il design system Bootstrap Italia di UniPR.
|
||||
- Non c'è alcun passaggio di build o bundling JS.
|
||||
|
||||
4. **Checkbox e Stato**:
|
||||
- I checkbox inviano il proprio stato serializzato come stringa separata da virgole tramite un input hidden (gestito client-side da uno script JS automatico).
|
||||
|
||||
## Deploy e Server Locale
|
||||
- **Deploy**: Le cartelle vengono caricate direttamente su web server Apache.
|
||||
- **Server locale**:
|
||||
- **PHP CLI**: Avviare il server dalla cartella `php/` tramite `php -S localhost:8080` (le pagine saranno raggiungibili su `http://localhost:8080/<nome-pagina>/`).
|
||||
- **Docker**: Dalla root del repository, eseguire:
|
||||
```bash
|
||||
docker run --rm -p 8080:8080 -v "${PWD}/php:/app" -w /app php:8.2-cli php -S 0.0.0.0:8080
|
||||
```
|
||||
|
||||
## Skill PHP Correlate
|
||||
Fai riferimento alle seguenti skill per maggiori dettagli operativi:
|
||||
- [elixforms-php-common-library](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-common-library/SKILL.md): Classi, metodi factory e helper.
|
||||
- [elixforms-php-create-new-page](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-create-new-page/SKILL.md): Guida passo passo per la creazione di nuove pagine.
|
||||
- [elixforms-php-project-architecture](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-project-architecture/SKILL.md): Struttura delle cartelle, convenzioni e deploy.
|
||||
- [elixforms-css-design-system](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/css-design-system/SKILL.md): Linee guida del design system UniPR.
|
||||
- [elixforms-custom-workflow-logic](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/custom-workflow-logic/SKILL.md): Query parameters ed integrazione elixForms.
|
||||
@@ -0,0 +1,610 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ElixForms\Common;
|
||||
|
||||
require_once __DIR__ . '/QueryParamHelper.php';
|
||||
require_once __DIR__ . '/ElixFormsElement.php';
|
||||
|
||||
/**
|
||||
* Classe astratta base per le custom page PHP di elixForms.
|
||||
* Equivalente PHP di ElixFormsComponentAbstract.tsx nel progetto React.
|
||||
*
|
||||
* Ogni pagina custom deve:
|
||||
* 1. Estendere questa classe
|
||||
* 2. Sovrascrivere createCustomFormFields() per definire i campi del form
|
||||
* 3. Opzionalmente sovrascrivere renderExtraContentPre() / renderExtraContentPost()
|
||||
* 4. Chiamare render() per generare l'HTML completo della pagina
|
||||
*
|
||||
* Uso tipico:
|
||||
* ```php
|
||||
* $page = new MiaPaginaCustom([
|
||||
* 'moduleName' => 'Nome Modulo',
|
||||
* 'cardTitle' => 'Titolo Card',
|
||||
* // ...altre proprietà...
|
||||
* ]);
|
||||
* echo $page->render();
|
||||
* ```
|
||||
*/
|
||||
abstract class ElixFormsComponent
|
||||
{
|
||||
/**
|
||||
* Proprietà della pagina (equivalente a IElixFormsComponentProperties).
|
||||
*
|
||||
* Chiavi supportate:
|
||||
* - isDarkTheme: bool (opzionale)
|
||||
* - userDisplayName: string (opzionale)
|
||||
* - additionalFieldsJson: string (opzionale, JSON schema per campi dinamici)
|
||||
* - moduleName: string (obbligatorio)
|
||||
* - headerTitle: string (opzionale)
|
||||
* - headerHeroImageSrc: string (opzionale)
|
||||
* - cardTitle: string (obbligatorio)
|
||||
* - cardDescription: string (opzionale)
|
||||
* - alertInfoTitle: string (opzionale)
|
||||
* - alertInfoMessage: string (opzionale)
|
||||
* - instructionsTitle: string (opzionale)
|
||||
* - instructionsMessage: string (opzionale)
|
||||
* - submitText: string (opzionale, default 'INVIA')
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $properties;
|
||||
|
||||
/**
|
||||
* Nomi dei parametri obbligatori che elixForms passa in query string.
|
||||
* Se mancano, la pagina mostra un errore di autenticazione.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $mandatoryFormFieldNames = [
|
||||
'RWE2_MODULE_ID',
|
||||
'RWE2_REQUEST_ID',
|
||||
'custom-workflow-back-url',
|
||||
'custom-workflow-generic-id',
|
||||
'custom-workflow-current-tabrel-genid',
|
||||
'custom-workflow-source-field',
|
||||
'crc',
|
||||
'MODULE_TESTMODE_KEY',
|
||||
'ELANG',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $properties Proprietà della pagina
|
||||
*/
|
||||
public function __construct(array $properties)
|
||||
{
|
||||
$this->properties = $properties;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Metodi estensibili (da sovrascrivere nelle pagine figlie)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Genera i campi custom del form.
|
||||
* Da sovrascrivere nelle pagine figlie per definire i campi specifici.
|
||||
*
|
||||
* @return string HTML dei campi custom
|
||||
*/
|
||||
protected function createCustomFormFields(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Contenuto extra da renderizzare PRIMA del form.
|
||||
* Sovrascrivere per aggiungere contenuto personalizzato.
|
||||
*
|
||||
* @return string HTML del contenuto extra
|
||||
*/
|
||||
protected function renderExtraContentPre(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Contenuto extra da renderizzare DOPO il form.
|
||||
* Sovrascrivere per aggiungere contenuto personalizzato.
|
||||
*
|
||||
* @return string HTML del contenuto extra
|
||||
*/
|
||||
protected function renderExtraContentPost(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Metodi factory per campi form
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Crea un campo di testo (input type="text").
|
||||
*
|
||||
* @param string $paramName Nome/ID del campo (corrisponde alla colonna elixForms)
|
||||
* @param string $label Etichetta del campo
|
||||
* @param bool $required Se il campo è obbligatorio
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
protected function createTextInput(string $paramName, string $label, bool $required = false): string
|
||||
{
|
||||
$currentValue = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$requiredAttr = $required ? ' required' : '';
|
||||
|
||||
$labelHtml = '<label class="form-label fw-semibold" for="' . htmlspecialchars($paramName) . '">' . htmlspecialchars($label) . '</label>';
|
||||
$inputHtml = '<input id="' . htmlspecialchars($paramName) . '" name="' . htmlspecialchars($paramName) . '" class="form-control" type="text" value="' . $currentValue . '"' . $requiredAttr . ' />';
|
||||
|
||||
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un campo textarea.
|
||||
*
|
||||
* @param string $paramName Nome/ID del campo
|
||||
* @param string $label Etichetta del campo
|
||||
* @param bool $required Se il campo è obbligatorio
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
protected function createTextAreaInput(string $paramName, string $label, bool $required = false): string
|
||||
{
|
||||
$currentValue = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$requiredAttr = $required ? ' required' : '';
|
||||
|
||||
$labelHtml = '<label class="form-label fw-semibold" for="' . htmlspecialchars($paramName) . '">' . htmlspecialchars($label) . '</label>';
|
||||
$inputHtml = '<textarea id="' . htmlspecialchars($paramName) . '" name="' . htmlspecialchars($paramName) . '" class="form-control" rows="4"' . $requiredAttr . '>' . $currentValue . '</textarea>';
|
||||
|
||||
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un campo numerico (input type="number").
|
||||
*
|
||||
* @param string $paramName Nome/ID del campo
|
||||
* @param string $label Etichetta del campo
|
||||
* @param bool $required Se il campo è obbligatorio
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
protected function createNumberInput(string $paramName, string $label, bool $required = false): string
|
||||
{
|
||||
$currentValue = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$requiredAttr = $required ? ' required' : '';
|
||||
|
||||
$labelHtml = '<label class="form-label fw-semibold" for="' . htmlspecialchars($paramName) . '">' . htmlspecialchars($label) . '</label>';
|
||||
$inputHtml = '<input id="' . htmlspecialchars($paramName) . '" name="' . htmlspecialchars($paramName) . '" class="form-control" type="number" value="' . $currentValue . '"' . $requiredAttr . ' />';
|
||||
|
||||
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un campo booleano (radio Sì/No).
|
||||
*
|
||||
* @param string $paramName Nome/ID del campo
|
||||
* @param string $label Etichetta del campo
|
||||
* @param bool $required Se il campo è obbligatorio
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
protected function createBooleanInput(string $paramName, string $label, bool $required = false): string
|
||||
{
|
||||
$currentValue = QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '';
|
||||
$requiredAttr = $required ? ' required' : '';
|
||||
$escapedName = htmlspecialchars($paramName);
|
||||
$escapedLabel = htmlspecialchars($label);
|
||||
|
||||
$labelHtml = '<span class="form-label fw-semibold">' . $escapedLabel . '</span>';
|
||||
|
||||
$options = [
|
||||
['value' => 'true', 'label' => 'Sì'],
|
||||
['value' => 'false', 'label' => 'No'],
|
||||
];
|
||||
|
||||
$inputHtml = '<div class="d-flex flex-column gap-2" role="radiogroup" aria-label="' . $escapedLabel . '">';
|
||||
foreach ($options as $option) {
|
||||
$id = $escapedName . '_' . $option['value'];
|
||||
$checked = $currentValue === $option['value'] ? ' checked' : '';
|
||||
$inputHtml .= <<<HTML
|
||||
<div class="form-check">
|
||||
<input id="{$id}" class="form-check-input" type="radio" name="{$escapedName}" value="{$option['value']}"{$checked}{$requiredAttr} />
|
||||
<label class="form-check-label" for="{$id}">{$option['label']}</label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
$inputHtml .= '</div>';
|
||||
|
||||
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un campo radio con opzioni personalizzate.
|
||||
*
|
||||
* @param string $paramName Nome/ID del campo
|
||||
* @param string $label Etichetta del campo
|
||||
* @param array<array{value: int, label: string}> $options Opzioni radio
|
||||
* @param bool $required Se il campo è obbligatorio
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
protected function createRadioInput(string $paramName, string $label, array $options, bool $required = false): string
|
||||
{
|
||||
$currentValue = (string)(QueryParamHelper::getOptionFromQuery($paramName) ?? '');
|
||||
$requiredAttr = $required ? ' required' : '';
|
||||
$escapedName = htmlspecialchars($paramName);
|
||||
$escapedLabel = htmlspecialchars($label);
|
||||
|
||||
$labelHtml = '<span class="form-label fw-semibold">' . $escapedLabel . '</span>';
|
||||
|
||||
$inputHtml = '<div class="d-flex flex-column gap-2" role="radiogroup" aria-label="' . $escapedLabel . '">';
|
||||
foreach ($options as $option) {
|
||||
$id = $escapedName . '_' . $option['value'];
|
||||
$checked = $currentValue === (string)$option['value'] ? ' checked' : '';
|
||||
$optionLabel = htmlspecialchars($option['label']);
|
||||
$inputHtml .= <<<HTML
|
||||
<div class="form-check">
|
||||
<input id="{$id}" class="form-check-input" type="radio" name="{$escapedName}" value="{$option['value']}"{$checked}{$requiredAttr} />
|
||||
<label class="form-check-label" for="{$id}">{$optionLabel}</label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
$inputHtml .= '</div>';
|
||||
|
||||
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un campo checkbox con opzioni multiple.
|
||||
* I valori selezionati vengono inviati come stringa separata da virgola in un input hidden.
|
||||
*
|
||||
* @param string $paramName Nome/ID del campo (usato per l'hidden input)
|
||||
* @param string $label Etichetta del campo
|
||||
* @param array<array{value: int, label: string, required?: bool}> $options Opzioni checkbox
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
protected function createCheckboxInput(string $paramName, string $label, array $options): string
|
||||
{
|
||||
$checkedValues = QueryParamHelper::getCheckedFromQuery($paramName);
|
||||
$currentValue = implode(',', $checkedValues);
|
||||
$escapedName = htmlspecialchars($paramName);
|
||||
$escapedLabel = htmlspecialchars($label);
|
||||
|
||||
$labelHtml = '<label class="form-label fw-semibold" for="' . $escapedName . '">' . $escapedLabel . '</label>';
|
||||
|
||||
$inputHtml = '<div class="d-flex flex-column gap-2" role="group" aria-label="' . $escapedLabel . '">';
|
||||
foreach ($options as $entryIndex => $entry) {
|
||||
$id = $escapedName . '_' . $entryIndex;
|
||||
$isChecked = in_array($entryIndex, $checkedValues) ? ' checked' : '';
|
||||
$optionLabel = htmlspecialchars($entry['label']);
|
||||
$inputHtml .= <<<HTML
|
||||
<div class="form-check">
|
||||
<input id="{$id}" class="form-check-input" type="checkbox" value="{$entryIndex}" data-param="{$escapedName}"{$isChecked} />
|
||||
<label class="form-check-label" for="{$id}">{$optionLabel}</label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
$inputHtml .= '</div>';
|
||||
// Hidden input che contiene il valore aggregato (aggiornato via JS client-side)
|
||||
$inputHtml .= '<input type="hidden" id="' . $escapedName . '" name="' . $escapedName . '" value="' . htmlspecialchars($currentValue) . '" />';
|
||||
|
||||
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un campo dropdown (select).
|
||||
*
|
||||
* @param string $paramName Nome/ID del campo
|
||||
* @param string $label Etichetta del campo
|
||||
* @param array<array{value: int, label: string}> $options Opzioni dropdown
|
||||
* @param bool $required Se il campo è obbligatorio
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
protected function createDropdownInput(string $paramName, string $label, array $options, bool $required = false): string
|
||||
{
|
||||
$queryValue = QueryParamHelper::getOptionFromQuery($paramName);
|
||||
$currentValue = $queryValue !== null ? (string)$queryValue : '';
|
||||
$requiredAttr = $required ? ' required' : '';
|
||||
$escapedName = htmlspecialchars($paramName);
|
||||
$escapedLabel = htmlspecialchars($label);
|
||||
|
||||
$labelHtml = '<label class="form-label fw-semibold" for="' . $escapedName . '">' . $escapedLabel . '</label>';
|
||||
|
||||
$inputHtml = '<select id="' . $escapedName . '" name="' . $escapedName . '" class="form-select"' . $requiredAttr . '>';
|
||||
$inputHtml .= '<option value="">Seleziona...</option>';
|
||||
foreach ($options as $option) {
|
||||
$selected = $currentValue === (string)$option['value'] ? ' selected' : '';
|
||||
$optionLabel = htmlspecialchars($option['label']);
|
||||
$inputHtml .= '<option value="' . $option['value'] . '"' . $selected . '>' . $optionLabel . '</option>';
|
||||
}
|
||||
$inputHtml .= '</select>';
|
||||
|
||||
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un campo nascosto (hidden input).
|
||||
* L'ID dell'hidden ha il suffisso _hidden per evitare conflitti.
|
||||
*
|
||||
* @param string $paramName Nome del campo
|
||||
* @return string HTML dell'input hidden
|
||||
*/
|
||||
protected function createHiddenInput(string $paramName): string
|
||||
{
|
||||
$value = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$escapedName = htmlspecialchars($paramName);
|
||||
|
||||
return '<input type="hidden" id="' . $escapedName . '_hidden" name="' . $escapedName . '" value="' . $value . '" />';
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Metodi interni di rendering
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Genera gli hidden input per tutti i parametri obbligatori elixForms.
|
||||
*
|
||||
* @return string HTML degli hidden input
|
||||
*/
|
||||
private function createMandatoryFormFields(): string
|
||||
{
|
||||
$html = '';
|
||||
foreach ($this->mandatoryFormFieldNames as $paramName) {
|
||||
$html .= $this->createHiddenInput($paramName);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera i campi form da uno schema JSON (additionalFieldsJson).
|
||||
*
|
||||
* @return string HTML dei campi aggiuntivi
|
||||
*/
|
||||
private function createAdditionalFormFields(): string
|
||||
{
|
||||
$json = $this->properties['additionalFieldsJson'] ?? '[]';
|
||||
$schema = json_decode($json, true);
|
||||
|
||||
if (!is_array($schema)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '';
|
||||
foreach ($schema as $field) {
|
||||
$html .= $this->renderField($field);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderizza un singolo campo form in base al tipo specificato nello schema.
|
||||
*
|
||||
* @param array<string, mixed> $field Definizione del campo
|
||||
* @return string HTML del campo
|
||||
*/
|
||||
private function renderField(array $field): string
|
||||
{
|
||||
$key = $field['key'] ?? '';
|
||||
$fieldLabel = $field['label'] ?? '';
|
||||
$required = $field['required'] ?? false;
|
||||
$fieldOptions = $field['options'] ?? [];
|
||||
|
||||
return match ($field['type'] ?? '') {
|
||||
'text' => $this->createTextInput($key, $fieldLabel, $required),
|
||||
'textarea' => $this->createTextAreaInput($key, $fieldLabel, $required),
|
||||
'number' => $this->createNumberInput($key, $fieldLabel, $required),
|
||||
'boolean' => $this->createBooleanInput($key, $fieldLabel, $required),
|
||||
'radio' => $this->createRadioInput($key, $fieldLabel, $fieldOptions, $required),
|
||||
'checkbox' => $this->createCheckboxInput($key, $fieldLabel, $fieldOptions),
|
||||
'dropdown' => $this->createDropdownInput($key, $fieldLabel, $fieldOptions, $required),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Render principale
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Genera l'HTML completo della pagina.
|
||||
* Include: head con CSS, header Bootstrap Italia, breadcrumb, card con form, footer.
|
||||
*
|
||||
* @return string HTML completo della pagina
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
$userDisplayName = $this->properties['userDisplayName'] ?? '';
|
||||
$moduleName = htmlspecialchars($this->properties['moduleName'] ?? '');
|
||||
$headerTitle = $this->properties['headerTitle'] ?? '';
|
||||
$headerHeroImageSrc = $this->properties['headerHeroImageSrc'] ?? '';
|
||||
$cardTitle = $this->properties['cardTitle'] ?? '';
|
||||
$cardDescription = $this->properties['cardDescription'] ?? '';
|
||||
$alertInfoTitle = $this->properties['alertInfoTitle'] ?? '';
|
||||
$alertInfoMessage = $this->properties['alertInfoMessage'] ?? '';
|
||||
$instructionsTitle = $this->properties['instructionsTitle'] ?? '';
|
||||
$instructionsMessage = $this->properties['instructionsMessage'] ?? '';
|
||||
$submitText = $this->properties['submitText'] ?? 'INVIA';
|
||||
|
||||
// Verifica parametri obbligatori
|
||||
$missingParams = array_filter($this->mandatoryFormFieldNames, fn($p) => !isset($_GET[$p]));
|
||||
$hasAuthenticationError = count($missingParams) > 0;
|
||||
|
||||
// Contenuti del form
|
||||
$mandatoryFields = $this->createMandatoryFormFields();
|
||||
$additionalFields = $this->createAdditionalFormFields();
|
||||
$customFields = $this->createCustomFormFields();
|
||||
$extraContentPre = $this->renderExtraContentPre();
|
||||
$extraContentPost = $this->renderExtraContentPost();
|
||||
|
||||
// Costruisci l'HTML della pagina
|
||||
ob_start();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($cardTitle) ?> — elixForms</title>
|
||||
<meta name="description" content="<?= htmlspecialchars($cardDescription) ?>">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;400;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-italia@2.18.2/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="https://console-unipr.elixforms.it/rwe2/css/design-bs/util.css">
|
||||
<link rel="stylesheet" href="https://console-unipr.elixforms.it/rwe2/css/themes/blu-italia.css">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<header class="it-header-wrapper text-white">
|
||||
<?php if ($userDisplayName): ?>
|
||||
<div class="it-header-slim-wrapper">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="it-header-slim-wrapper-content">
|
||||
<div class="nav-mobile"></div>
|
||||
<div class="it-header-slim-right-zone">
|
||||
<div class="it-access-top-wrapper">
|
||||
<?= htmlspecialchars($userDisplayName) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="it-header-center-wrapper">
|
||||
<div class="container">
|
||||
<div class="it-header-center-content-wrapper">
|
||||
<div class="it-brand-wrapper">
|
||||
<a href="https://www.unipr.it/" title="Torna alla homepage dell'Università di Parma">
|
||||
<img class="icon" src="https://procedure.unipr.it/UploadImgs/12_marchio_UNIPR_neg_b1_squareLogo.png" alt="Università di Parma" />
|
||||
<div class="it-brand-text">
|
||||
<p class="h2 no_toc">Università di Parma</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="it-right-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container my-2 mx-4">
|
||||
<div class="cmp-breadcrumb" role="navigation">
|
||||
<nav class="breadcrumb-container" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">Procedure Online<span class="separator">></span></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= $moduleName ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<?php if ($headerHeroImageSrc): ?>
|
||||
<img src="<?= htmlspecialchars($headerHeroImageSrc) ?>" alt="Hero Image" class="hero-image img-fluid mb-3" />
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($headerTitle): ?>
|
||||
<p class="h2"><?= htmlspecialchars($headerTitle) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<?php if ($cardTitle): ?>
|
||||
<p class="h3 mb-3"><?= htmlspecialchars($cardTitle) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($cardDescription): ?>
|
||||
<p class="lead text-secondary"><?= htmlspecialchars($cardDescription) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($alertInfoTitle || $alertInfoMessage): ?>
|
||||
<div class="custom-alert custom-alert-warning" role="status">
|
||||
<?php if ($alertInfoTitle): ?><div><b><?= htmlspecialchars($alertInfoTitle) ?></b></div><?php endif; ?>
|
||||
<?php if ($alertInfoMessage): ?><div class="small"><?= htmlspecialchars($alertInfoMessage) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasAuthenticationError): ?>
|
||||
<div class="alert alert-danger" role="alert">Accesso non autorizzato!</div>
|
||||
<?php else: ?>
|
||||
|
||||
<?= $extraContentPre ?>
|
||||
|
||||
<?php if ($instructionsTitle || $instructionsMessage): ?>
|
||||
<div class="custom-alert custom-alert-info" role="status">
|
||||
<?php if ($instructionsTitle): ?><div><b><?= htmlspecialchars($instructionsTitle) ?></b></div><?php endif; ?>
|
||||
<?php if ($instructionsMessage): ?><div class="small"><?= htmlspecialchars($instructionsMessage) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="https://procedure.unipr.it/rwe2/ComeBackToElixAndSave" method="post" accept-charset="ISO-8859-1" class="mt-3">
|
||||
<?= $mandatoryFields ?>
|
||||
<?= $additionalFields ?>
|
||||
<?= $customFields ?>
|
||||
<div class="mt-4 d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary"><?= htmlspecialchars($submitText) ?> <span class="it-arrow-right text-white ms-2"></span></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?= $extraContentPost ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="it-footer text-white">
|
||||
<div class="it-footer-small-prints clearfix">
|
||||
<div class="container">
|
||||
<section>
|
||||
<div class="row clearfix">
|
||||
<div class="col-sm-12">
|
||||
<div class="footer-content text-center p-2">
|
||||
<div class="subfooter-top">powered by <span class="highlight">elixForms</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<?= $this->renderCheckboxScript() ?>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera il JavaScript per la gestione client-side dei checkbox.
|
||||
* Aggiorna l'hidden input con i valori selezionati separati da virgola.
|
||||
*
|
||||
* @return string Tag <script> con il codice JS
|
||||
*/
|
||||
private function renderCheckboxScript(): string
|
||||
{
|
||||
return <<<'SCRIPT'
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
var paramName = this.getAttribute('data-param');
|
||||
var hiddenInput = document.getElementById(paramName);
|
||||
if (!hiddenInput) return;
|
||||
|
||||
var checkboxes = document.querySelectorAll('input[type="checkbox"][data-param="' + paramName + '"]');
|
||||
var values = [];
|
||||
checkboxes.forEach(function(cb) {
|
||||
if (cb.checked) {
|
||||
values.push(cb.value);
|
||||
}
|
||||
});
|
||||
hiddenInput.value = values.join(',');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
SCRIPT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ElixForms\Common;
|
||||
|
||||
/**
|
||||
* Wrapper per layout label + input in un layout a due colonne Bootstrap.
|
||||
* Equivalente PHP di ElixFormsElement.tsx nel progetto React.
|
||||
*
|
||||
* Genera markup compatibile con il design system Bootstrap Italia:
|
||||
* - Colonna sinistra (col-12 col-md-4): label
|
||||
* - Colonna destra (col-12 col-md-8): input
|
||||
*/
|
||||
class ElixFormsElement
|
||||
{
|
||||
private string $labelHtml;
|
||||
private string $inputHtml;
|
||||
private string $separator;
|
||||
|
||||
/**
|
||||
* @param string $labelHtml HTML della label
|
||||
* @param string $inputHtml HTML dell'input
|
||||
* @param string $separator Separatore opzionale tra label e input
|
||||
*/
|
||||
public function __construct(string $labelHtml, string $inputHtml, string $separator = '')
|
||||
{
|
||||
if (empty($labelHtml) || empty($inputHtml)) {
|
||||
throw new \InvalidArgumentException('labelHtml and inputHtml are required.');
|
||||
}
|
||||
|
||||
$this->labelHtml = $labelHtml;
|
||||
$this->inputHtml = $inputHtml;
|
||||
$this->separator = $separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera l'HTML del campo form con layout a due colonne.
|
||||
*
|
||||
* @return string HTML renderizzato
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
$sep = $this->separator !== '' ? htmlspecialchars($this->separator) : '';
|
||||
|
||||
return <<<HTML
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="text-md-end pe-md-3 mt-2">
|
||||
{$this->labelHtml}{$sep}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
{$this->inputHtml}
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ElixForms\Common;
|
||||
|
||||
/**
|
||||
* Client HTTP robusto per effettuare chiamate REST esterne.
|
||||
* Utilizza l'estensione cURL di PHP ed implementa best practices per
|
||||
* la gestione degli header, dell'autenticazione e degli errori.
|
||||
*/
|
||||
class HttpClient
|
||||
{
|
||||
private array $defaultHeaders = [];
|
||||
private ?string $username = null;
|
||||
private ?string $password = null;
|
||||
private int $timeout = 10;
|
||||
|
||||
/**
|
||||
* @param array $defaultHeaders Header predefiniti per ogni richiesta
|
||||
*/
|
||||
public function __construct(array $defaultHeaders = [])
|
||||
{
|
||||
$this->defaultHeaders = $defaultHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imposta le credenziali per l'autenticazione Basic.
|
||||
*/
|
||||
public function setBasicAuth(string $username, string $password): self
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imposta il timeout massimo per la connessione e l'esecuzione.
|
||||
*/
|
||||
public function setTimeout(int $seconds): self
|
||||
{
|
||||
$this->timeout = $seconds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Esegue una richiesta HTTP GET.
|
||||
*
|
||||
* @param string $url URL della richiesta
|
||||
* @param array $queryParams Parametri query aggiuntivi
|
||||
* @param array $headers Header specifici per questa richiesta
|
||||
* @return string Risposta in formato testuale
|
||||
* @throws \RuntimeException In caso di errore curl o codice di stato non 2xx
|
||||
*/
|
||||
public function get(string $url, array $queryParams = [], array $headers = []): string
|
||||
{
|
||||
if (!empty($queryParams)) {
|
||||
$separator = (strpos($url, '?') === false) ? '?' : '&';
|
||||
$url .= $separator . http_build_query($queryParams);
|
||||
}
|
||||
|
||||
return $this->request($url, 'GET', null, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Esegue una richiesta HTTP POST.
|
||||
*
|
||||
* @param string $url URL della richiesta
|
||||
* @param mixed $data Dati da inviare nel body (array, stringa o JSON)
|
||||
* @param array $headers Header specifici per questa richiesta
|
||||
* @return string Risposta in formato testuale
|
||||
* @throws \RuntimeException In caso di errore curl o codice di stato non 2xx
|
||||
*/
|
||||
public function post(string $url, $data, array $headers = []): string
|
||||
{
|
||||
return $this->request($url, 'POST', $data, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo interno per eseguire la richiesta tramite cURL.
|
||||
*/
|
||||
private function request(string $url, string $method, $data = null, array $headers = []): string
|
||||
{
|
||||
$ch = curl_init();
|
||||
if ($ch === false) {
|
||||
throw new \RuntimeException('Impossibile inizializzare cURL.');
|
||||
}
|
||||
|
||||
// Configura le opzioni cURL di base
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
|
||||
|
||||
// Disabilita la verifica SSL in ambiente di sviluppo locale se necessario,
|
||||
// ma di default è attiva per sicurezza.
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
|
||||
// Gestione metodo HTTP
|
||||
$method = strtoupper($method);
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if ($data !== null) {
|
||||
if (is_array($data)) {
|
||||
$postData = http_build_query($data);
|
||||
} else {
|
||||
$postData = $data;
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||
}
|
||||
} elseif ($method !== 'GET') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
}
|
||||
|
||||
// Costruisci gli header
|
||||
$mergedHeaders = array_merge($this->defaultHeaders, $headers);
|
||||
$formattedHeaders = [];
|
||||
foreach ($mergedHeaders as $name => $value) {
|
||||
$formattedHeaders[] = "{$name}: {$value}";
|
||||
}
|
||||
|
||||
if (!empty($formattedHeaders)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders);
|
||||
}
|
||||
|
||||
// Autenticazione Basic
|
||||
if ($this->username !== null && $this->password !== null) {
|
||||
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
curl_setopt($ch, CURLOPT_USERPWD, "{$this->username}:{$this->password}");
|
||||
}
|
||||
|
||||
// Esegui la richiesta
|
||||
$response = curl_exec($ch);
|
||||
$error = curl_error($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
// curl_close non è più necessario in PHP 8.0+ ed è deprecato in PHP 8.5+
|
||||
|
||||
if ($errno !== 0) {
|
||||
throw new \RuntimeException("Errore cURL durante la chiamata a {$url}: [{$errno}] {$error}");
|
||||
}
|
||||
|
||||
if ($statusCode < 200 || $statusCode >= 300) {
|
||||
throw new \RuntimeException("Richiesta fallita con codice di stato HTTP {$statusCode}.");
|
||||
}
|
||||
|
||||
return (string)$response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ElixForms\Common;
|
||||
|
||||
/**
|
||||
* Utility statica per leggere parametri dalla URL corrente ($_GET).
|
||||
* Equivalente PHP di QueryParamHelper.tsx nel progetto React.
|
||||
*/
|
||||
class QueryParamHelper
|
||||
{
|
||||
/**
|
||||
* Ottiene un array di valori numerici dalla query string (per checkbox).
|
||||
* Il parametro è una stringa di valori separati da virgola (es. "1,3,5").
|
||||
*
|
||||
* @param string $paramName Nome del parametro nella query string
|
||||
* @return int[] Array di interi
|
||||
*/
|
||||
public static function getCheckedFromQuery(string $paramName): array
|
||||
{
|
||||
$raw = $_GET[$paramName] ?? '';
|
||||
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = explode(',', (string)$raw);
|
||||
$result = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$trimmed = trim($part);
|
||||
if ($trimmed !== '' && is_numeric($trimmed)) {
|
||||
$result[] = (int)$trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene un singolo valore numerico dalla query string (per radio/dropdown).
|
||||
*
|
||||
* @param string $paramName Nome del parametro nella query string
|
||||
* @return int|null Valore numerico o null se non presente
|
||||
*/
|
||||
public static function getOptionFromQuery(string $paramName): ?int
|
||||
{
|
||||
if (!isset($_GET[$paramName])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $_GET[$paramName];
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene un valore booleano dalla query string.
|
||||
*
|
||||
* @param string $paramName Nome del parametro nella query string
|
||||
* @return bool|null true/false o null se non presente
|
||||
*/
|
||||
public static function getBooleanFromQuery(string $paramName): ?bool
|
||||
{
|
||||
if (!isset($_GET[$paramName])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return strtolower((string)$_GET[$paramName]) === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene un valore testuale decodificato dalla query string.
|
||||
*
|
||||
* @param string $paramName Nome del parametro nella query string
|
||||
* @return string|null Valore decodificato o null se non presente
|
||||
*/
|
||||
public static function getDecodedTextFromQuery(string $paramName): ?string
|
||||
{
|
||||
if (!isset($_GET[$paramName])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urldecode((string)$_GET[$paramName]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
name: elixforms-www
|
||||
|
||||
services:
|
||||
elixforms-www:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ../.docker/php/Dockerfile
|
||||
container_name: elixforms-www
|
||||
working_dir: /app
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "8080:8000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- ../.docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
|
||||
#environment:
|
||||
# XDEBUG_MODE: debug
|
||||
# XDEBUG_CONFIG: client_host=host.docker.internal client_port=9003 start_with_request=yes
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"component": {
|
||||
"minCharsForSearch": 3,
|
||||
"moduleName": "Ripartizione Utili / Compensi - Presentazione Proposta",
|
||||
"headerTitle": "",
|
||||
"headerHeroImageSrc": "",
|
||||
"cardTitle": "Recupero Proposta CCT da Contratti",
|
||||
"cardDescription": "",
|
||||
"alertInfoTitle": "",
|
||||
"alertInfoMessage": "",
|
||||
"instructionsTitle": "Istruzioni di compilazione",
|
||||
"instructionsMessage": "Completa i campi richiesti per procedere con la richiesta.",
|
||||
"submitText": "CONFERMA E PROSEGUI"
|
||||
},
|
||||
"elixForms": {
|
||||
"inputs": {
|
||||
"codiceFiscale": "COL0009"
|
||||
},
|
||||
"outputs": {
|
||||
"idDomanda": "COL0001",
|
||||
"idRicevuta": "COL0002",
|
||||
"codiceContratto": "COL0003",
|
||||
"titoloContratto": "COL0004"
|
||||
}
|
||||
},
|
||||
"contrattiWS": {
|
||||
"apiUrl": "http://localhost:8000/contratti/cerca",
|
||||
"apiUsername": "apiUsername",
|
||||
"apiPassword": "[PASSWORD]",
|
||||
"apiKey": "apiKey"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../common/ElixFormsComponent.php';
|
||||
require_once __DIR__ . '/../common/QueryParamHelper.php';
|
||||
|
||||
use ElixForms\Common\ElixFormsComponent;
|
||||
use ElixForms\Common\QueryParamHelper;
|
||||
|
||||
/**
|
||||
* Pagina custom "Recupero Proposta CCT da Contratti" per elixForms.
|
||||
* Equivalente PHP del componente React RecuperoPropostaCctDaContrattiComponent.
|
||||
*/
|
||||
class RecuperoPropostaCctDaContrattiPage extends ElixFormsComponent
|
||||
{
|
||||
private string $codiceFiscale;
|
||||
private array $fieldKeys;
|
||||
private int $minCharsForSearch;
|
||||
|
||||
public function __construct(array $properties, array $fieldKeys, int $minCharsForSearch = 3)
|
||||
{
|
||||
parent::__construct($properties);
|
||||
$this->fieldKeys = $fieldKeys;
|
||||
$this->minCharsForSearch = $minCharsForSearch;
|
||||
|
||||
// Recupera il codice fiscale dalla query string
|
||||
$this->codiceFiscale = QueryParamHelper::getDecodedTextFromQuery($fieldKeys['codiceFiscale']) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera l'HTML e CSS del componente Autocomplete e i relativi campi nascosti.
|
||||
*/
|
||||
protected function createCustomFormFields(): string
|
||||
{
|
||||
$escapedCodFis = htmlspecialchars($this->codiceFiscale, ENT_QUOTES, 'UTF-8');
|
||||
$minChars = $this->minCharsForSearch;
|
||||
|
||||
// Campi di output definiti nella configurazione
|
||||
$outCodiceContratto = htmlspecialchars($this->fieldKeys['codiceContratto']);
|
||||
$outTitoloContratto = htmlspecialchars($this->fieldKeys['titoloContratto']);
|
||||
$outIdDomanda = htmlspecialchars($this->fieldKeys['idDomanda']);
|
||||
$outIdRicevuta = htmlspecialchars($this->fieldKeys['idRicevuta']);
|
||||
|
||||
// Recupero di eventuali valori correnti dal GET per precompilazione iniziale
|
||||
$currCodice = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['codiceContratto']) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$currTitolo = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['titoloContratto']) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$currDomanda = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['idDomanda']) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$currRicevuta = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['idRicevuta']) ?? '', ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$initialSearchText = '';
|
||||
if ($currCodice !== '' && $currTitolo !== '') {
|
||||
$initialSearchText = "[{$currCodice}] {$currTitolo}";
|
||||
}
|
||||
|
||||
// Layout del campo di ricerca
|
||||
$autocompleteInputHtml = <<<HTML
|
||||
<div class="autocomplete-wrapper" id="autocomplete-container">
|
||||
<input
|
||||
id="autocomplete-search"
|
||||
name="autocomplete-search"
|
||||
className="form-control"
|
||||
class="form-control"
|
||||
type="text"
|
||||
placeholder="Digita almeno {$minChars} caratteri per cercare..."
|
||||
value="{$initialSearchText}"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<div id="autocomplete-dropdown" class="autocomplete-dropdown d-none"></div>
|
||||
|
||||
<div id="selected-contract-card" class="card mt-3 d-none">
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-6"><strong>ID Contratto:</strong> <span id="summary-id"></span></div>
|
||||
<div class="col-12 col-md-6"><strong>Titolo:</strong> <span id="summary-title"></span></div>
|
||||
<div class="col-12 col-md-6"><strong>ID Domanda:</strong> <span id="summary-domanda"></span></div>
|
||||
<div class="col-12 col-md-6"><strong>ID Ricevuta:</strong> <span id="summary-ricevuta"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Nascosti per elixForms -->
|
||||
<input type="hidden" id="codiceContratto_hidden" name="{$outCodiceContratto}" value="{$currCodice}" />
|
||||
<input type="hidden" id="titoloContratto_hidden" name="{$outTitoloContratto}" value="{$currTitolo}" />
|
||||
<input type="hidden" id="idDomanda_hidden" name="{$outIdDomanda}" value="{$currDomanda}" />
|
||||
<input type="hidden" id="idRicevuta_hidden" name="{$outIdRicevuta}" value="{$currRicevuta}" />
|
||||
HTML;
|
||||
|
||||
$labelHtml = '<label class="form-label fw-semibold" htmlFor="autocomplete-search">Cerca Contratto</label>';
|
||||
|
||||
return (new \ElixForms\Common\ElixFormsElement($labelHtml, $autocompleteInputHtml))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inietta i CSS specifici per l'autocomplete e la card.
|
||||
*/
|
||||
protected function renderExtraContentPre(): string
|
||||
{
|
||||
ob_start();
|
||||
?>
|
||||
<style>
|
||||
.autocomplete-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.autocomplete-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: #ffffff;
|
||||
border: 1px solid #a19f9d;
|
||||
border-top: none;
|
||||
border-radius: 0 0 2px 2px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.autocomplete-dropdown-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
border-bottom: 1px solid #f3f2f1;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
.autocomplete-dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.autocomplete-dropdown-item:hover,
|
||||
.autocomplete-dropdown-item.highlighted {
|
||||
background-color: #edebe9;
|
||||
}
|
||||
.autocomplete-dropdown-item .contract-id {
|
||||
font-weight: 600;
|
||||
color: #323130;
|
||||
}
|
||||
.autocomplete-dropdown-item .contract-title {
|
||||
color: #605e5c;
|
||||
}
|
||||
.autocomplete-loading {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: #605e5c;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
.autocomplete-no-results {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: #a19f9d;
|
||||
font-size: 14px;
|
||||
}
|
||||
.autocomplete-error {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: #a4262c;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inietta il codice JavaScript per gestire le chiamate AJAX al proxy ed il comportamento dell'interfaccia.
|
||||
*/
|
||||
protected function renderExtraContentPost(): string
|
||||
{
|
||||
$escapedCodFis = htmlspecialchars($this->codiceFiscale, ENT_QUOTES, 'UTF-8');
|
||||
$minChars = $this->minCharsForSearch;
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var searchInput = document.getElementById('autocomplete-search');
|
||||
var dropdown = document.getElementById('autocomplete-dropdown');
|
||||
var container = document.getElementById('autocomplete-container');
|
||||
var card = document.getElementById('selected-contract-card');
|
||||
|
||||
var summaryId = document.getElementById('summary-id');
|
||||
var summaryTitle = document.getElementById('summary-title');
|
||||
var summaryDomanda = document.getElementById('summary-domanda');
|
||||
var summaryRicevuta = document.getElementById('summary-ricevuta');
|
||||
|
||||
var hiddenCodice = document.getElementById('codiceContratto_hidden');
|
||||
var hiddenTitolo = document.getElementById('titoloContratto_hidden');
|
||||
var hiddenDomanda = document.getElementById('idDomanda_hidden');
|
||||
var hiddenRicevuta = document.getElementById('idRicevuta_hidden');
|
||||
|
||||
var codFis = <?= json_encode($escapedCodFis) ?>;
|
||||
var minChars = <?= $minChars ?>;
|
||||
|
||||
var searchResults = [];
|
||||
var highlightedIndex = -1;
|
||||
var debounceTimer = null;
|
||||
|
||||
// Se i campi hidden sono precompilati all'avvio, mostra la card di riepilogo
|
||||
if (hiddenCodice.value && hiddenTitolo.value) {
|
||||
showSummaryCard({
|
||||
idContratto: hiddenCodice.value,
|
||||
titoloContratto: hiddenTitolo.value,
|
||||
idDomanda: hiddenDomanda.value,
|
||||
idRicevuta: hiddenRicevuta.value
|
||||
});
|
||||
}
|
||||
|
||||
// Input event listener (con debounce)
|
||||
searchInput.addEventListener('input', function() {
|
||||
var text = this.value;
|
||||
|
||||
if (text.length === 0) {
|
||||
clearSelection();
|
||||
hideDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
if (text.length >= minChars) {
|
||||
// Controlla se l'utente ha solo modificato il testo del contratto selezionato
|
||||
if (hiddenCodice.value && text !== "[" + hiddenCodice.value + "] " + hiddenTitolo.value) {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(function() {
|
||||
fetchResults(text);
|
||||
}, 300);
|
||||
} else {
|
||||
hideDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Gestione eventi da tastiera
|
||||
searchInput.addEventListener('keydown', function(event) {
|
||||
if (dropdown.classList.contains('d-none')) return;
|
||||
|
||||
var items = dropdown.querySelectorAll('.autocomplete-dropdown-item');
|
||||
if (items.length === 0) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
highlightedIndex = Math.min(highlightedIndex + 1, items.length - 1);
|
||||
updateHighlight(items);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
||||
updateHighlight(items);
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (highlightedIndex >= 0 && highlightedIndex < searchResults.length) {
|
||||
selectContract(searchResults[highlightedIndex]);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
hideDropdown();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Chiudi il menu a discesa se si clicca all'esterno
|
||||
document.addEventListener('mousedown', function(event) {
|
||||
if (!container.contains(event.target)) {
|
||||
hideDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
function fetchResults(term) {
|
||||
showDropdown();
|
||||
dropdown.innerHTML = '<div class="autocomplete-loading">Ricerca in corso...</div>';
|
||||
highlightedIndex = -1;
|
||||
|
||||
// Calcolo dinamico della base directory per supportare URL con o senza slash finale
|
||||
var baseDir = window.location.pathname;
|
||||
if (!baseDir.endsWith('/') && !baseDir.endsWith('.php')) {
|
||||
baseDir += '/';
|
||||
}
|
||||
var lastSlash = baseDir.lastIndexOf('/');
|
||||
var dir = baseDir.substring(0, lastSlash + 1);
|
||||
|
||||
var url = dir + 'search-contratti.php?term=' + encodeURIComponent(term) + '&cod_fis=' + encodeURIComponent(codFis);
|
||||
|
||||
fetch(url)
|
||||
.then(function(response) {
|
||||
return response.text().then(function(text) {
|
||||
var data = null;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
// Risposta non JSON
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
var errMsg = (data && data.message) ? data.message :
|
||||
(data && data.error) ? data.error :
|
||||
('Errore API: ' + response.status + ' ' + response.statusText);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
if (data === null) {
|
||||
throw new Error('Risposta del server non in formato JSON.');
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
searchResults = data;
|
||||
renderResults(data);
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Errore durante la ricerca contratti:', error);
|
||||
dropdown.innerHTML = '<div class="autocomplete-error">' + error.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
if (results.length === 0) {
|
||||
dropdown.innerHTML = '<div class="autocomplete-no-results">Nessun risultato trovato</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.innerHTML = '';
|
||||
results.forEach(function(contract, index) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'autocomplete-dropdown-item';
|
||||
item.innerHTML = '<span class="contract-id">[' + contract.idContratto + ']</span> ' +
|
||||
'<span class="contract-title">' + contract.titoloContratto + '</span>';
|
||||
|
||||
// Mouse interactions
|
||||
item.addEventListener('mousedown', function(event) {
|
||||
event.preventDefault();
|
||||
selectContract(contract);
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', function() {
|
||||
highlightedIndex = index;
|
||||
var items = dropdown.querySelectorAll('.autocomplete-dropdown-item');
|
||||
updateHighlight(items);
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function updateHighlight(items) {
|
||||
items.forEach(function(item, index) {
|
||||
if (index === highlightedIndex) {
|
||||
item.classList.add('highlighted');
|
||||
// Scorri il menu se l'elemento evidenziato è fuori vista
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.classList.remove('highlighted');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectContract(contract) {
|
||||
searchInput.value = '[' + contract.idContratto + '] ' + contract.titoloContratto;
|
||||
|
||||
hiddenCodice.value = contract.idContratto;
|
||||
hiddenTitolo.value = contract.titoloContratto;
|
||||
hiddenDomanda.value = contract.idDomanda;
|
||||
hiddenRicevuta.value = contract.idRicevuta;
|
||||
|
||||
showSummaryCard(contract);
|
||||
hideDropdown();
|
||||
}
|
||||
|
||||
function showSummaryCard(contract) {
|
||||
summaryId.textContent = contract.idContratto;
|
||||
summaryTitle.textContent = contract.titoloContratto;
|
||||
summaryDomanda.textContent = contract.idDomanda;
|
||||
summaryRicevuta.textContent = contract.idRicevuta;
|
||||
card.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
hiddenCodice.value = '';
|
||||
hiddenTitolo.value = '';
|
||||
hiddenDomanda.value = '';
|
||||
hiddenRicevuta.value = '';
|
||||
card.classList.add('d-none');
|
||||
}
|
||||
|
||||
function showDropdown() {
|
||||
dropdown.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function hideDropdown() {
|
||||
dropdown.classList.add('d-none');
|
||||
highlightedIndex = -1;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Entry Point della Pagina
|
||||
// =============================================================================
|
||||
|
||||
// Lettura e validazione della configurazione
|
||||
$configFile = __DIR__ . '/config.json';
|
||||
if (!file_exists($configFile)) {
|
||||
die("File di configurazione config.json non trovato.");
|
||||
}
|
||||
|
||||
$config = json_decode(file_get_contents($configFile), true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
die("Errore di sintassi nel file config.json.");
|
||||
}
|
||||
|
||||
// Istanziazione ed esecuzione del rendering
|
||||
$page = new RecuperoPropostaCctDaContrattiPage(
|
||||
$config['component'],
|
||||
[
|
||||
'codiceFiscale' => $config['elixForms']['inputs']['codiceFiscale'],
|
||||
'idDomanda' => $config['elixForms']['outputs']['idDomanda'],
|
||||
'idRicevuta' => $config['elixForms']['outputs']['idRicevuta'],
|
||||
'codiceContratto'=> $config['elixForms']['outputs']['codiceContratto'],
|
||||
'titoloContratto'=> $config['elixForms']['outputs']['titoloContratto'],
|
||||
],
|
||||
$config['component']['minCharsForSearch'] ?? 3
|
||||
);
|
||||
|
||||
echo $page->render();
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_once __DIR__ . '/../common/HttpClient.php';
|
||||
|
||||
use ElixForms\Common\HttpClient;
|
||||
|
||||
try {
|
||||
// 1. Lettura dei parametri di input
|
||||
$term = $_GET['term'] ?? '';
|
||||
$codFis = $_GET['cod_fis'] ?? '';
|
||||
|
||||
if (empty($term) || strlen($term) < 3) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Il parametro di ricerca "term" deve contenere almeno 3 caratteri.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($codFis)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Il parametro "cod_fis" (Codice Fiscale) è obbligatorio.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Caricamento configurazione
|
||||
$configFile = __DIR__ . '/config.json';
|
||||
if (!file_exists($configFile)) {
|
||||
throw new \RuntimeException('File di configurazione config.json non trovato.');
|
||||
}
|
||||
|
||||
$configJson = file_get_contents($configFile);
|
||||
$config = json_decode($configJson, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !isset($config['contrattiWS'])) {
|
||||
throw new \RuntimeException('Configurazione del Web Service "contrattiWS" mancante o non valida.');
|
||||
}
|
||||
|
||||
$wsConfig = $config['contrattiWS'];
|
||||
$apiUrl = $wsConfig['apiUrl'] ?? '';
|
||||
$apiUsername = $wsConfig['apiUsername'] ?? '';
|
||||
$apiPassword = $wsConfig['apiPassword'] ?? '';
|
||||
$apiKey = $wsConfig['apiKey'] ?? '';
|
||||
|
||||
// 3. Inizializzazione ed esecuzione della chiamata HTTP con HttpClient
|
||||
$client = new HttpClient([
|
||||
'X-Api-Key' => $apiKey,
|
||||
'Accept' => 'application/json'
|
||||
]);
|
||||
|
||||
// Configura autenticazione basic
|
||||
$client->setBasicAuth($apiUsername, $apiPassword);
|
||||
|
||||
// Esegui la richiesta GET
|
||||
$responseBody = $client->get($apiUrl, [
|
||||
'cod_fis' => $codFis,
|
||||
'term' => $term
|
||||
]);
|
||||
|
||||
// Valida che la risposta sia in formato JSON corretto
|
||||
json_decode($responseBody);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \RuntimeException('Il servizio contratti ha restituito una risposta in un formato non valido (non JSON).');
|
||||
}
|
||||
|
||||
// Ritorna la risposta del WS direttamente
|
||||
echo $responseBody;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'error' => 'Errore durante la ricerca dei contratti.',
|
||||
'message' => 'Servizio temporaneamente non disponibile o risposta non valida.'
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../common/ElixFormsComponent.php';
|
||||
|
||||
use ElixForms\Common\ElixFormsComponent;
|
||||
|
||||
/**
|
||||
* Pagina custom "Scelta Carriera" per elixForms.
|
||||
* Equivalente PHP della pagina React scelta-carriera.
|
||||
*
|
||||
* Estende ElixFormsComponent sovrascrivendo:
|
||||
* - createCustomFormFields(): definisce dropdown, text, textarea, boolean, radio, checkbox
|
||||
* - renderExtraContentPost(): aggiunge un alert informativo in fondo
|
||||
*/
|
||||
class SceltaCarrieraPage extends ElixFormsComponent
|
||||
{
|
||||
protected function createCustomFormFields(): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Dropdown: COL0015 Goals
|
||||
$html .= $this->createDropdownInput('COL0015', 'COL0015 Goals', [
|
||||
['value' => 0, 'label' => 'Not applicable'],
|
||||
['value' => 1, 'label' => 'Goal 1: No poverty'],
|
||||
['value' => 2, 'label' => 'Goal 2: Zero hunger'],
|
||||
['value' => 3, 'label' => 'Goal 3: Good health and well-being'],
|
||||
['value' => 4, 'label' => 'Goal 4: Quality education'],
|
||||
['value' => 5, 'label' => 'Goal 5: Gender equality'],
|
||||
['value' => 6, 'label' => 'Goal 6: Clean water and sanitation'],
|
||||
['value' => 7, 'label' => 'Goal 7: Affordable and clean energy'],
|
||||
['value' => 8, 'label' => 'Goal 8: Decent work and economic growth'],
|
||||
['value' => 9, 'label' => 'Goal 9: Industry, Innovation, and Infrastructure'],
|
||||
['value' => 10, 'label' => 'Goal 10: Reduced inequalities'],
|
||||
['value' => 11, 'label' => 'Goal 11: Sustainable cities and communities'],
|
||||
['value' => 12, 'label' => 'Goal 12: Responsible consumption and production'],
|
||||
['value' => 13, 'label' => 'Goal 13: Climate action'],
|
||||
['value' => 14, 'label' => 'Goal 14: Life below water'],
|
||||
['value' => 15, 'label' => 'Goal 15: Life on land'],
|
||||
['value' => 16, 'label' => 'Goal 16: Peace, justice and strong institutions'],
|
||||
['value' => 17, 'label' => 'Goal 17: Partnerships for the goals'],
|
||||
], true);
|
||||
|
||||
// Text input: COL0002
|
||||
$html .= $this->createTextInput('COL0002', 'Campo STRING', true);
|
||||
|
||||
// Textarea: COL0003
|
||||
$html .= $this->createTextAreaInput('COL0003', 'Campo TEXTAREA', true);
|
||||
|
||||
// Boolean: COL0004
|
||||
$html .= $this->createBooleanInput('COL0004', 'Campo BOOLEAN', true);
|
||||
|
||||
// Radio: COL0005
|
||||
$html .= $this->createRadioInput('COL0005', 'Campo RADIO', [
|
||||
['value' => 1, 'label' => 'Opzione 1'],
|
||||
['value' => 2, 'label' => 'Opzione 2'],
|
||||
['value' => 3, 'label' => 'Altra opzione'],
|
||||
], true);
|
||||
|
||||
// Checkbox: COL0006
|
||||
$html .= $this->createCheckboxInput('COL0006', 'Campo CHECKBOX', [
|
||||
['value' => 4, 'label' => 'Check 1'],
|
||||
['value' => 5, 'label' => 'Check 2'],
|
||||
['value' => 6, 'label' => 'Altro check'],
|
||||
]);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
protected function renderExtraContentPost(): string
|
||||
{
|
||||
return '<div class="alert alert-info mt-4">Questa pagina è la versione PHP della custom page Scelta Carriera.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Entry point — Istanziazione e rendering della pagina
|
||||
// =============================================================================
|
||||
|
||||
$page = new SceltaCarrieraPage([
|
||||
'moduleName' => '',
|
||||
'alertInfoMessage' => '',
|
||||
'userDisplayName' => 'John Doe',
|
||||
'headerTitle' => 'Modulo A/13 - Richiesta di Certificato',
|
||||
'cardTitle' => 'Scelta Carriera',
|
||||
'cardDescription' => 'Benvenuto nella piattaforma di scelta carriera! Esplora le tue opzioni e trova la strada giusta per te.',
|
||||
'headerHeroImageSrc' => '',
|
||||
]);
|
||||
|
||||
echo $page->render();
|
||||
Reference in New Issue
Block a user