refactor solution to PHP through AI

This commit is contained in:
2026-07-20 14:27:17 +02:00
parent a77f6f19cf
commit 151bc70775
21 changed files with 2563 additions and 41 deletions
+190
View File
@@ -0,0 +1,190 @@
---
name: elixforms-php-common-library
description: >
Documentazione della libreria condivisa PHP common/ del progetto elixForms.
Descrive la classe base ElixFormsComponent, i metodi factory per i campi form,
ElixFormsElement per il layout, QueryParamHelper per i query parameter,
e il pattern di ereditarietà per creare pagine custom.
Attiva questa skill quando lavori sui componenti PHP condivisi, crei nuovi tipi di campo,
modifichi la classe base, o integri con la piattaforma elixForms in PHP.
---
# Libreria Common PHP elixForms
## Panoramica
La cartella `php/common/` contiene la libreria condivisa PHP usata da tutte le pagine custom PHP. È basata su **Bootstrap Italia + HTML nativo** e implementa un sistema di form con campi dinamici, analogo alla libreria React in `react/common/`.
## Architettura delle Classi
```
ElixFormsComponent (classe astratta, PHP 8.0+)
└── SceltaCarrieraPage (classe concreta, in scelta-carriera/index.php)
└── [AltrePageCustom] (classi concrete nelle sotto-cartelle)
```
### Namespace
Tutte le classi usano il namespace PSR-4:
```php
namespace ElixForms\Common;
```
Le pagine importano con:
```php
require_once __DIR__ . '/../common/ElixFormsComponent.php';
use ElixForms\Common\ElixFormsComponent;
```
## File della Libreria
### `ElixFormsComponent.php` (Classe Base Astratta)
Equivalente di `ElixFormsComponentAbstract.tsx` in React.
#### Proprietà
```php
protected array $properties; // Equivalente di IElixFormsComponentProperties
```
Chiavi supportate in `$properties`:
| Chiave | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
| `moduleName` | string | Sì | Nome del modulo nel breadcrumb |
| `cardTitle` | string | Sì | Titolo della card principale |
| `userDisplayName` | string | No | Nome utente nell'header |
| `headerTitle` | string | No | Titolo sotto il breadcrumb |
| `headerHeroImageSrc` | string | No | URL immagine hero |
| `cardDescription` | string | No | Descrizione nella card |
| `alertInfoTitle` | string | No | Titolo alert informativo |
| `alertInfoMessage` | string | No | Messaggio alert informativo |
| `instructionsTitle` | string | No | Titolo istruzioni |
| `instructionsMessage` | string | No | Messaggio istruzioni |
| `submitText` | string | No | Testo bottone submit (default: 'INVIA') |
| `additionalFieldsJson` | string | No | JSON schema per campi dinamici |
| `isDarkTheme` | bool | No | Tema scuro (riservato per uso futuro) |
#### Metodi Factory (Campi Form)
| Metodo | Tipo Campo | HTML Output |
|---|---|---|
| `createTextInput($name, $label, $required)` | Testo | `<input type="text">` |
| `createTextAreaInput($name, $label, $required)` | Textarea | `<textarea>` |
| `createNumberInput($name, $label, $required)` | Numerico | `<input type="number">` |
| `createBooleanInput($name, $label, $required)` | Booleano (Sì/No) | Radio buttons |
| `createRadioInput($name, $label, $options, $required)` | Radio | Radio buttons |
| `createCheckboxInput($name, $label, $options)` | Checkbox multipli | Checkboxes + hidden |
| `createDropdownInput($name, $label, $options, $required)` | Dropdown | `<select>` |
| `createHiddenInput($name)` | Nascosto | `<input type="hidden">` |
#### Metodi Estensibili (Override)
```php
// Campi custom del form — OBBLIGATORIO sovrascrivere
protected function createCustomFormFields(): string { return ''; }
// Contenuto extra prima del form
protected function renderExtraContentPre(): string { return ''; }
// Contenuto extra dopo il form
protected function renderExtraContentPost(): string { return ''; }
```
#### Metodo Principale
```php
public function render(): string
```
Genera l'HTML **completo** della pagina (`<!DOCTYPE html>` ... `</html>`), includendo:
- `<head>` con CSS Bootstrap Italia e design system UniPR
- Header con logo UniPR e nome utente
- Breadcrumb
- Card con alert, istruzioni, form e campi
- Footer "powered by elixForms"
- Script JS per checkbox
### `ElixFormsElement.php` (Wrapper Layout)
Genera il layout a due colonne per ogni campo form:
```php
$element = new ElixFormsElement($labelHtml, $inputHtml, $separator);
echo $element->render();
```
Output HTML:
```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">{label}</div>
</div>
<div class="col-12 col-md-8">
{input}
</div>
</div>
```
### `QueryParamHelper.php` (Utility Query String)
Classe statica per leggere parametri dalla query string `$_GET`:
```php
QueryParamHelper::getCheckedFromQuery('COL0006'); // int[] (checkbox, valori separati da virgola)
QueryParamHelper::getOptionFromQuery('COL0015'); // ?int (radio/dropdown)
QueryParamHelper::getBooleanFromQuery('COL0004'); // ?bool (boolean)
QueryParamHelper::getDecodedTextFromQuery('COL0002'); // ?string (testo URL-decoded)
```
### `HttpClient.php` (Utility Richieste HTTP REST)
Classe orientata agli oggetti per effettuare chiamate HTTP server-to-server sicure verso Web Service esterni (es. endpoint contratti):
```php
use ElixForms\Common\HttpClient;
$client = new HttpClient([
'X-Api-Key' => 'chiave-api-custom',
'Accept' => 'application/json'
]);
$client->setBasicAuth('username', 'password');
$client->setTimeout(10); // timeout in secondi
// Richiesta GET
$response = $client->get('http://api-endpoint/cerca', [
'parametro1' => 'valore1'
]);
```
**Best Practices gestite da HttpClient:**
- **Timeout**: Gestione dei timeout di connessione ed esecuzione per evitare blocchi infiniti del server Apache.
- **Sicurezza delle credenziali**: Permette di eseguire chiamate server-side mantenendo chiavi API e password nascoste al browser del client.
- **Error Handling**: Se il server remoto restituisce codici HTTP diversi da 20x (es. 404, 500), solleva una `RuntimeException` contenente il codice di stato generico senza includere nel messaggio d'errore l'eventuale payload della risposta (es. tag HTML dell'errore di Apache) per motivi di sicurezza ed integrità del JSON finale.
## Differenze rispetto alla versione React
| Aspetto | React | PHP |
|---|---|---|
| **Rendering** | Client-side (JSX → DOM) | Server-side (PHP → HTML) |
| **State** | `this.state` + `this.setState()` | Valori da `$_GET` (stateless) |
| **Checkbox sync** | React state + hidden input | JS client-side + hidden input |
| **Build** | Vite → single bundle JS | Nessuna build necessaria |
| **CSS** | Iniettato nel JS | `<link>` tags nell'HTML |
| **Output** | Singolo file `.js` | File `.php` serviti da Apache |
## Parametri Obbligatori elixForms
Gestiti automaticamente come hidden inputs dalla classe base:
- `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`
## Note Importanti
- Il form fa POST a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave`
- L'encoding charset è `ISO-8859-1` per compatibilità con il backend
- Gli ID dei campi form **non devono mai essere manipolati** (vedi AGENTS.md)
- L'ID dell'hidden input dei checkbox e dei mandatory fields ha suffisso `_hidden`
- I checkbox usano un attributo `data-param` e uno script JS per sincronizzare il valore con l'hidden input
+258
View File
@@ -0,0 +1,258 @@
---
name: elixforms-php-create-new-page
description: >
Guida step-by-step per creare una nuova pagina custom PHP nel progetto elixForms.
Copre la struttura dei file, l'importazione della libreria common,
le convenzioni di naming, e la configurazione per il deploy su Apache.
Attiva questa skill quando devi creare una nuova pagina PHP,
duplicare una pagina esistente, o scaffoldare un nuovo micro-progetto PHP nel monorepo.
---
# Creare una Nuova Pagina PHP elixForms Custom
## Prerequisiti
- PHP 8.0+ installato
- Web server Apache configurato per servire la cartella `php/`
## Step 1: Creare la Cartella della Pagina
```powershell
cd php
mkdir <nome-pagina>
```
Il nome della cartella deve essere in **kebab-case** (es. `scelta-carriera`, `recupero-proposta`).
## Step 2: Creare il File index.php
Creare `php/<nome-pagina>/index.php`:
```php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../common/ElixFormsComponent.php';
use ElixForms\Common\ElixFormsComponent;
/**
* Pagina custom "<NomePagina>" per elixForms.
*/
class NomePaginaPage extends ElixFormsComponent
{
protected function createCustomFormFields(): string
{
$html = '';
// Aggiungi qui i campi del form usando i metodi factory:
// $html .= $this->createTextInput('COL0001', 'Campo Testo', true);
// $html .= $this->createDropdownInput('COL0002', 'Scelta', [...options], true);
return $html;
}
// Opzionale: contenuto extra prima del form
// protected function renderExtraContentPre(): string { return ''; }
// Opzionale: contenuto extra dopo il form
// protected function renderExtraContentPost(): string { return ''; }
}
// Entry point
$page = new NomePaginaPage([
'moduleName' => 'Nome Modulo',
'cardTitle' => 'Titolo Pagina',
'cardDescription' => 'Descrizione della pagina.',
'userDisplayName' => '', // Verrà popolato da elixForms
'headerTitle' => '',
'headerHeroImageSrc' => '',
'alertInfoTitle' => '',
'alertInfoMessage' => '',
'instructionsTitle' => '',
'instructionsMessage' => '',
'submitText' => 'INVIA',
]);
echo $page->render();
```
## Step 3: Definire i Campi Custom
Sovrascrivere `createCustomFormFields()` con i campi necessari:
```php
protected function createCustomFormFields(): string
{
$html = '';
// Dropdown
$html .= $this->createDropdownInput('COL0015', 'Scelta', [
['value' => 0, 'label' => 'Opzione A'],
['value' => 1, 'label' => 'Opzione B'],
['value' => 2, 'label' => 'Opzione C'],
], true);
// Testo
$html .= $this->createTextInput('COL0002', 'Nome', true);
// Textarea
$html .= $this->createTextAreaInput('COL0003', 'Note', false);
// Booleano (Sì/No)
$html .= $this->createBooleanInput('COL0004', 'Conferma', true);
// Radio
$html .= $this->createRadioInput('COL0005', 'Preferenza', [
['value' => 1, 'label' => 'Prima scelta'],
['value' => 2, 'label' => 'Seconda scelta'],
], true);
// Checkbox
$html .= $this->createCheckboxInput('COL0006', 'Opzioni Multiple', [
['value' => 1, 'label' => 'Opzione 1'],
['value' => 2, 'label' => 'Opzione 2'],
]);
// Numerico
$html .= $this->createNumberInput('COL0007', 'Quantità', true);
return $html;
}
```
## Step 4: Opzionale — Aggiungere File di Configurazione
Per pagine complesse, creare un file `config.json` nella cartella della pagina:
```json
{
"component": {
"moduleName": "Nome Modulo",
"cardTitle": "Titolo",
"cardDescription": "Descrizione",
"submitText": "CONFERMA"
},
"elixForms": {
"inputs": {
"codiceFiscale": "COL0009"
},
"outputs": {
"campo1": "COL0001",
"campo2": "COL0002"
}
}
}
```
Caricare nel PHP:
```php
$config = json_decode(file_get_contents(__DIR__ . '/config.json'), true);
$page = new NomePaginaPage($config['component']);
echo $page->render();
```
## Step 4.1: Pagine Interattive e Chiamate Asincrone (AJAX)
Se la pagina richiede autocompletamento o interattività (es. ricerca contratti):
### 1. Proxy Script PHP Locale
Per non esporre credenziali sensibili (API Key, Basic Auth) al client, creare sempre uno script proxy locale (es. `search-contratti.php`) che:
- Riceve i dati dal client.
- Esegue la chiamata protetta tramite `HttpClient`.
- Valida con `json_decode` che la risposta sia JSON valido prima di inviarla.
- Risponde con `500` e un JSON d'errore pulito in caso di eccezioni.
### 2. Risoluzione Percorso URL in JavaScript
Il server integrato PHP non esegue il redirect automatico per aggiungere lo slash alla fine delle cartelle. In JavaScript, calcolare sempre la base directory dinamica per supportare URL sia con che senza slash finale o con `index.php` esplicito:
```javascript
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);
```
### 3. Parsing Sicuro degli Errori HTTP (Fetch)
Non eseguire direttamente `response.json()` su risposte d'errore (500/404) o non-JSON per evitare il crash `Unexpected token '<'`. Utilizzare la tecnica del `response.text()`:
```javascript
fetch(url)
.then(function(response) {
return response.text().then(function(text) {
var data = null;
try {
data = JSON.parse(text);
} catch (e) {
// Risposta non JSON (es. HTML d'errore del server)
}
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;
});
})
```
## Step 5: Testing Locale
Per testare le pagine localmente, è necessario avviare il server web dalla cartella radice `php/` (in modo che la cartella `common/` sia accessibile correttamente):
### Opzione 1: PHP CLI Server locale
```powershell
cd php
php -S localhost:8080
```
Aprire nel browser:
```
http://localhost:8080/<nome-pagina>/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&COL0002=test&COL0015=3
```
### Opzione 2: 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
```
Aprire nel browser:
```
http://localhost:8080/<nome-pagina>/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&COL0002=test&COL0015=3
```
## Step 6: Deploy su Apache
1. Caricare la cartella `php/` (o la singola cartella della pagina) sul server Apache
2. Assicurarsi che Apache abbia `mod_php` o `php-fpm` configurato
3. La pagina sarà accessibile via URL diretto (es. `https://server.example.com/php/nome-pagina/`)
4. elixForms reindirizza l'utente alla pagina passando i parametri in query string
## Struttura Finale
```
php/<nome-pagina>/
├── index.php ← Entry point e classe pagina
└── config.json ← Opzionale: configurazione separata
```
## Checklist Nuova Pagina PHP
- [ ] Cartella creata in `php/<nome-pagina>/`
- [ ] `index.php` con `declare(strict_types=1)` e namespace
- [ ] Classe che estende `ElixFormsComponent`
- [ ] `createCustomFormFields()` sovrascritta con i campi corretti
- [ ] Proprietà di configurazione impostate correttamente
- [ ] `require_once __DIR__ . '/../common/ElixFormsComponent.php'`
- [ ] Test locale con `php -S` o Docker
- [ ] Verifica che i campi form producano l'HTML corretto
- [ ] Verifica che i parametri obbligatori vengano letti da `$_GET`
@@ -0,0 +1,124 @@
---
name: elixforms-php-project-architecture
description: >
Architettura e struttura della parte PHP del progetto elixForms Custom Pages.
Descrive la cartella php/, la libreria common condivisa, il pattern delle pagine,
le convenzioni di naming, e il deploy su Apache.
Attiva questa skill quando lavori sulla parte PHP del progetto,
crei nuove pagine PHP, modifichi l'architettura, o devi capire come è organizzato il codice PHP.
---
# Architettura PHP del Progetto elixForms Custom Pages
## Panoramica
La cartella `php/` contiene le custom page elixForms realizzate in **PHP 8.0+ puro** (nessun framework). Ogni pagina è un file PHP autonomo che include la libreria `common/` e produce HTML server-side con **Bootstrap Italia**.
Questa struttura è l'equivalente PHP del monorepo React in `react/`.
## Struttura del Workspace
```
elixforms-custom-pages/
├── react/ ← Custom pages React (esistente)
└── php/ ← Custom pages PHP
├── AGENTS.md ← Regole dell'agente PHP
├── common/ ← Libreria condivisa PHP
│ ├── ElixFormsComponent.php ← Classe base astratta
│ ├── ElixFormsElement.php ← Wrapper layout label+input
│ └── QueryParamHelper.php ← Utility query string
└── scelta-carriera/ ← Esempio di pagina custom
└── index.php ← Entry point (classe + rendering)
```
## Pattern Architetturale: Singolo File PHP per Pagina
Ogni pagina (es. `scelta-carriera/`) è un file PHP autonomo che:
1. **Include la libreria `common/`** tramite `require_once`
2. **Definisce una classe** che estende `ElixFormsComponent`
3. **Sovrascrive `createCustomFormFields()`** per i campi specifici
4. **Istanzia e renderizza** la pagina con `echo $page->render()`
5. **Genera HTML completo** (dalla `<!DOCTYPE html>` al `</html>`)
6. **Non richiede build** — servito direttamente da Apache
## Namespace e Autoloading
Il progetto usa **namespace PSR-4**:
```php
namespace ElixForms\Common; // Per le classi in common/
```
L'autoloading è gestito manualmente con `require_once` (non Composer):
```php
require_once __DIR__ . '/../common/ElixFormsComponent.php';
// ElixFormsComponent.php include internamente:
// - QueryParamHelper.php
// - ElixFormsElement.php
```
## Requisiti PHP
- **Versione minima**: PHP 8.0+
- **Funzionalità usate**:
- `declare(strict_types=1)` — strict typing
- Typed properties (`protected array $properties`)
- `match` expression (nel renderField)
- Named arguments
- Union types (`int|null`, `string|null`)
- Arrow functions (`fn() =>`)
## CSS Strategy
Le pagine PHP usano `<link>` tags per caricare i CSS (non bundle JS):
```html
<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">
```
> **Nota**: Stessi CSS del progetto React, ma caricati via `<link>` anziché `@import url()` nel JS bundle.
## Integrazione con elixForms
Identica alla versione React:
1. **Query parameters**: Parametri obbligatori passati nell'URL da elixForms
2. **Form POST**: Il form fa submit a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave`
3. **Hidden inputs**: I parametri query vengono inseriti come campi hidden nel form
4. **Encoding**: `accept-charset="ISO-8859-1"` per compatibilità con il backend
## Convenzioni di Naming
- **Cartelle pagina**: kebab-case (es. `scelta-carriera`)
- **File entry point**: sempre `index.php`
- **Classi PHP**: PascalCase con suffisso `Page` (es. `SceltaCarrieraPage`)
- **Namespace**: `ElixForms\Common` per la libreria condivisa
- **Configurazione**: opzionale `config.json` nella cartella della pagina
## Differenze con il Progetto React
| Aspetto | React (`react/`) | PHP (`php/`) |
|---|---|---|
| **Linguaggio** | TypeScript/JSX | PHP 8.0+ |
| **Rendering** | Client-side | Server-side |
| **Build tool** | Vite (single bundle JS) | Nessuno (file serviti direttamente) |
| **Dipendenze** | npm (React, Vite, plugin) | Nessuna (PHP built-in) |
| **CSS loading** | `@import url()` + CSS injection plugin | `<link>` tags |
| **State management** | React state (`this.setState`) | Stateless (`$_GET`) |
| **Deploy** | Upload del bundle `.js` | Upload dei file `.php` |
| **Dev server** | `npm run dev` (Vite HMR) | `php -S` / Docker |
## Deploy su Apache
1. Caricare la cartella `php/` sul server Apache
2. Configurazione Apache minima:
- `mod_php` o `php-fpm` abilitato
- `DirectoryIndex index.php` (default Apache)
3. La pagina è accessibile via URL: `https://server/php/nome-pagina/`
4. elixForms reindirizza l'utente alla pagina passando i parametri via query string
+171
View File
@@ -0,0 +1,171 @@
# Port elixForms Custom Pages in PHP + Creazione Agenti React e PHP
## Obiettivo
Creare una versione PHP della libreria `common/` e una pagina di esempio `scelta-carriera`, replicando la stessa architettura e logica attualmente implementata in React/TypeScript. Inoltre, creare due agenti specializzati ("React" e "PHP") e le relative skill per il mondo PHP.
## Panoramica dell'architettura React attuale
```mermaid
graph TD
A["ElixFormsComponentAbstract.tsx<br/>(classe astratta)"] --> B["SceltaCarrieraComponent.tsx<br/>(override createCustomFormFields)"]
A --> C["RecuperoPropostaCctDaContrattiComponent.tsx<br/>(override createCustomFormFields + autocomplete)"]
A --> D["ElixFormsElement.tsx<br/>(layout label+input)"]
A --> E["QueryParamHelper.tsx<br/>(lettura query params)"]
A --> F["ElixFormsTypes.tsx<br/>(tipi opzioni)"]
B --> G["App.jsx + main.jsx<br/>(entry point pagina)"]
C --> H["App.jsx + main.jsx<br/>(entry point pagina)"]
```
## Differenze chiave React → PHP
| Aspetto | React | PHP |
|---|---|---|
| **Rendering** | Client-side (JSX → DOM) | Server-side (PHP → HTML) |
| **State management** | `this.state` + `this.setState()` | Valori da `$_GET` / `$_POST` (stateless) |
| **Form field factory** | Callback con oggetto factory | Metodi della classe base chiamati direttamente |
| **Build** | Vite → singolo bundle JS | Nessuna build — file PHP serviti direttamente |
| **CSS** | Iniettati nel JS via plugin | `<link>` tag nell'HTML |
| **Bootstrap Italia** | Classi HTML nel JSX | Stesse classi HTML nel template PHP |
> [!IMPORTANT]
> In PHP non serve un build tool. Ogni pagina è un singolo file PHP (o un set di file PHP) che include la libreria common e produce HTML direttamente.
## Proposed Changes
### 1. Libreria Common PHP
#### [NEW] [ElixFormsComponent.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsComponent.php)
Classe astratta PHP che replica `ElixFormsComponentAbstract.tsx`:
- **Proprietà**: `$properties` (array associativo equivalente a `IElixFormsComponentProperties`)
- **Metodi factory**: `createTextInput()`, `createTextAreaInput()`, `createNumberInput()`, `createBooleanInput()`, `createRadioInput()`, `createCheckboxInput()`, `createDropdownInput()`, `createHiddenInput()`
- **Metodi di layout**: `render()` che genera l'intera pagina HTML (header Bootstrap Italia, breadcrumb, card, form, footer)
- **Metodi estensibili**: `createCustomFormFields()`, `renderExtraContentPre()`, `renderExtraContentPost()` — da sovrascrivere nelle pagine figlie
- **Validazione**: Controllo parametri obbligatori (`mandatoryFormFieldNames`) dalla query string
- I valori dei campi vengono pre-popolati da `$_GET` (equivalente di `QueryParamHelper`)
#### [NEW] [ElixFormsElement.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsElement.php)
Classe wrapper per layout label+input (replica `ElixFormsElement.tsx`):
- Genera il layout a due colonne Bootstrap (`row`, `col-12 col-md-4`, `col-12 col-md-8`)
#### [NEW] [QueryParamHelper.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/QueryParamHelper.php)
Classe statica utility (replica `QueryParamHelper.tsx`):
- `getCheckedFromQuery($paramName)``int[]`
- `getOptionFromQuery($paramName)``?int`
- `getBooleanFromQuery($paramName)``?bool`
- `getDecodedTextFromQuery($paramName)``?string`
---
### 2. Pagina di Esempio
#### [NEW] [index.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/scelta-carriera/index.php)
Pagina di esempio che replica `scelta-carriera` React:
- Estende `ElixFormsComponent` sovrascrivendo `createCustomFormFields()`
- Definisce dropdown, text input, textarea, boolean, radio, checkbox
- Include il CSS di Bootstrap Italia e del design system UniPR via `<link>`
---
### 3. Skill PHP (nuove)
#### [NEW] [SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-common-library/SKILL.md)
Skill `elixforms-php-common-library`:
- Documenta la libreria PHP common, i metodi della classe base, le proprietà, e il pattern di estensione
#### [NEW] [SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-create-new-page/SKILL.md)
Skill `elixforms-php-create-new-page`:
- Guida step-by-step per creare una nuova pagina PHP custom
#### [NEW] [SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-project-architecture/SKILL.md)
Skill `elixforms-php-project-architecture`:
- Descrive la struttura della cartella `php/` e le convenzioni del monorepo PHP
---
### 4. Agenti (AGENTS.md personalizzati)
> [!IMPORTANT]
> Gli "agenti" nella struttura Gemini vengono realizzati creando file `AGENTS.md` nelle rispettive cartelle. In questo modo ogni agente "scopre" automaticamente le regole specifiche per il proprio contesto.
#### [NEW] [AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/react/AGENTS.md)
Agente **"React"** — regole specifiche per il framework React:
- Usa React + TypeScript class components per la libreria common
- Usa Vite per il bundling single-file
- Bootstrap Italia con classi HTML nel JSX
- Regola sugli ID dei form fields (ripresa dal root)
- Riferimenti alle skill React esistenti
#### [NEW] [AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/AGENTS.md)
Agente **"PHP"** — regole specifiche per PHP:
- PHP puro, nessun framework
- Bootstrap Italia con classi HTML nei template PHP
- Pattern ereditarietà con classe base
- Stessa regola sugli ID dei form fields
- Riferimenti alle skill PHP
---
## Struttura finale del workspace
```
elixforms-custom-pages/
├── .agents/
│ ├── AGENTS.md ← Regole globali (invariate)
│ └── skills/
│ ├── common-library/ ← Skill React (esistente)
│ ├── create-new-page/ ← Skill React (esistente)
│ ├── css-design-system/ ← Skill condivisa (esistente)
│ ├── custom-workflow-logic/ ← Skill condivisa (esistente)
│ ├── dev-workflow/ ← Skill React (esistente)
│ ├── project-architecture/ ← Skill React (esistente)
│ ├── typescript-react-conventions/ ← Skill React (esistente)
│ ├── vite-build-config/ ← Skill React (esistente)
│ ├── php-common-library/ ← [NEW] Skill PHP
│ ├── php-create-new-page/ ← [NEW] Skill PHP
│ └── php-project-architecture/ ← [NEW] Skill PHP
├── react/
│ ├── AGENTS.md ← [NEW] Agente React
│ ├── common/
│ ├── scelta-carriera/
│ └── recupero-proposta-cct-da-contratti/
└── php/
├── AGENTS.md ← [NEW] Agente PHP
├── common/
│ ├── ElixFormsComponent.php ← [NEW]
│ ├── ElixFormsElement.php ← [NEW]
│ └── QueryParamHelper.php ← [NEW]
└── scelta-carriera/
└── index.php ← [NEW]
```
## Open Questions
> [!IMPORTANT]
> **Versione PHP**: quale versione minima di PHP devo usare? (es. PHP 7.4+ o PHP 8.0+ con typed properties, union types, match expression, ecc.)
> [!IMPORTANT]
> **Namespace PHP**: vuoi usare i namespace PSR-4 (es. `namespace ElixForms\Common;`) o preferisci un approccio più semplice con `require_once`?
> [!IMPORTANT]
> **Deploy**: le pagine PHP verranno servite da un web server Apache/Nginx? O dovranno essere incluse/embedded in un sistema esistente?
## Verification Plan
### Manual Verification
- Verificare che la pagina PHP `scelta-carriera/index.php` generi HTML valido con la stessa struttura del corrispettivo React
- Verificare che i campi del form (dropdown, text, textarea, boolean, radio, checkbox) siano tutti presenti e funzionanti
- Verificare che i parametri obbligatori vengano correttamente letti dalla query string
- Verificare che il form faccia POST a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave` con encoding `ISO-8859-1`
- Verificare che le skill e gli agenti siano correttamente scoperti da Gemini
@@ -0,0 +1,57 @@
# Porting "recupero-proposta-cct-da-contratti" in PHP
## Obiettivo
Convertire la pagina custom `recupero-proposta-cct-da-contratti` in PHP, mantenendo la stessa logica di funzionamento della versione React/TypeScript.
## Architettura e Sicurezza
La pagina originale React esegue una chiamata client-side fetch ad un web service esterno (`http://localhost:8000/contratti/cerca`) utilizzando credenziali Basic Auth ed una API Key custom. Poiché in PHP eseguiamo il rendering server-side, esporre le credenziali nel codice JavaScript inviato al browser è una vulnerabilità di sicurezza.
### Soluzione Proposta: PHP Proxy Script
Proponiamo la creazione di un file `search-contratti.php` locale alla pagina che:
1. Riceve in GET il parametro di ricerca `term` ed il codice fiscale dell'utente `cod_fis`.
2. Esegue una chiamata cURL server-to-server verso il WS reale usando le credenziali memorizzate in sicurezza in `config.json`.
3. Restituisce il JSON dei contratti trovati al client.
Il componente client-side eseguirà la fetch locale verso questo file `search-contratti.php` ed implementerà un dropdown di autocompletamento in JavaScript nativo (Vanilla JS) per mantenere le stesse animazioni ed interazioni della versione React (debounce, navigazione con frecce tastiera, selezione ed autocompilazione dei campi nascosti del form).
```
[Browser Client]
│ (fetch /term=...)
[search-contratti.php] (Proxy locale in php/)
│ (cURL server-to-server + Basic Auth & API Key)
[Web Service Contratti]
```
## Proposed Changes
### Componente: `php/recupero-proposta-cct-da-contratti/`
#### [NEW] [config.json](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/recupero-proposta-cct-da-contratti/config.json)
Copia del file di configurazione con le credenziali API, l'endpoint del Web Service ed i mapping dei campi elixForms.
#### [NEW] [search-contratti.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/recupero-proposta-cct-da-contratti/search-contratti.php)
Script PHP proxy che:
- Valida l'input.
- Effettua la richiesta HTTP GET al WS esterno tramite `curl`.
- Passa l'header `Authorization: Basic ...` e `X-Api-Key: ...`.
- Ritorna `application/json` con i risultati.
#### [NEW] [index.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/recupero-proposta-cct-da-contratti/index.php)
Pagina custom principale:
- Eredita da `ElixFormsComponent`.
- Legge il Codice Fiscale dell'utente tramite `QueryParamHelper::getDecodedTextFromQuery`.
- Genera i campi nascosti per i dati di output (`idDomanda`, `idRicevuta`, `codiceContratto`, `titoloContratto`).
- Rendering dell'input di ricerca ed implementazione del JavaScript nativo per l'autocompletamento (sincronizzato con il file proxy `search-contratti.php`).
## Verification Plan
### Manual Verification
- Avviare il server PHP locale: `php -S localhost:8080` nella cartella `php/recupero-proposta-cct-da-contratti/`.
- Verificare che il proxy `search-contratti.php?term=test&cod_fis=...` risponda correttamente con i dati mock o reali del WS.
- Verificare che digitando nel campo di ricerca "Cerca Contratto" vengano visualizzati i risultati nel menu a discesa.
- Testare la navigazione del menu a discesa con la tastiera (Frecce Su/Giù, Enter, Esc).
- Verificare che alla selezione di un contratto, i campi hidden vengano correttamente compilati con i valori associati (`idDomanda`, `idRicevuta`, ecc.) e venga mostrata la card riepilogativa del contratto selezionato.
+44
View File
@@ -0,0 +1,44 @@
# Walkthrough — Port PHP + Agenti Custom
Ho completato tutte le attività pianificate per la realizzazione della struttura in PHP delle custom pages elixForms e per la configurazione dei due agenti dedicati ("React" e "PHP").
## Modifiche e Creazioni Effettuate
### 1. Libreria Comune PHP (`php/common/`)
Abbiamo replicato la stessa logica e architettura della libreria comune React in PHP 8.0+:
- **[QueryParamHelper.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/QueryParamHelper.php)**: Fornisce metodi statici tipizzati per estrarre e decodificare i dati provenienti dalla query string (`$_GET`).
- **[ElixFormsElement.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsElement.php)**: Gestisce il layout standard a due colonne (label a sinistra, input a destra) con le classi Bootstrap.
- **[ElixFormsComponent.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsComponent.php)**: Classe astratta base che definisce il ciclo di rendering dell'intera pagina e tutti i metodi factory dei campi (testo, textarea, checkbox, radio, dropdown, boolean). Gestisce automaticamente la presenza dei parametri obbligatori elixForms ed inserisce il JavaScript client-side per la sincronizzazione dei checkbox.
### 2. Pagina di Esempio PHP
- **[index.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/scelta-carriera/index.php)**: Una pagina "Scelta Carriera" d'esempio che estende `ElixFormsComponent` e definisce tutti i campi form (dropdown, text, textarea, boolean, radio, checkbox), replicando fedelmente la controparte React.
### 3. Nuove Skill PHP
Abbiamo creato tre nuove skill all'interno di `.agents/skills/` per guidare lo sviluppo PHP futuro:
- **[php-common-library/SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-common-library/SKILL.md)**: Dettaglia i metodi factory, il layout ed i parametri del componente base.
- **[php-create-new-page/SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-create-new-page/SKILL.md)**: Guida passo-passo per lo scaffolding e testing locale di nuove pagine.
- **[php-project-architecture/SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-project-architecture/SKILL.md)**: Definisce le differenze architetturali rispetto al mondo React e le modalità di deploy su Apache.
### 4. Configurazione degli Agenti Dedicati
Abbiamo strutturato due file di istruzioni separati che istruiscono l'IDE sul comportamento da adottare a seconda che si stia lavorando in React o in PHP:
- **[react/AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/react/AGENTS.md)** (Agente React): Focalizzato su Vite, bundling single-file, TypeScript (`import type`), e React Class/Functional Components.
- **[php/AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/AGENTS.md)** (Agente PHP): Focalizzato su PHP 8.0+ puro, PSR-4 namespaces, server locale integrato, e deploy diretto su Apache.
---
## Istruzioni per il Test Locale PHP
1. Navigare nella cartella della pagina di esempio:
```bash
cd php/scelta-carriera
```
2. Avviare il server built-in di PHP:
```bash
php -S localhost:8080
```
3. Visitare l'URL inserendo dei parametri fittizi di prova per bypassare il controllo di autenticazione:
```
http://localhost:8080/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&custom-workflow-back-url=http://example.com&custom-workflow-generic-id=1&custom-workflow-current-tabrel-genid=2&custom-workflow-source-field=src&crc=abc&MODULE_TESTMODE_KEY=key&ELANG=it&COL0002=ValoreTest
```
4. Il form caricherà la pagina con il tema e layout corretti di Bootstrap Italia e i campi pre-popolati.
+8
View File
@@ -0,0 +1,8 @@
FROM php:8.5.8-cli
RUN pecl install xdebug && docker-php-ext-enable xdebug
WORKDIR /app
EXPOSE 8000 9023
CMD ["php", "-S", "0.0.0.0:8000"]
+9
View File
@@ -0,0 +1,9 @@
#zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20250925/xdebug.so
[xdebug]
xdebug.mode=develop,debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9023
xdebug.log_level=0
xdebug.idekey=VSCODE
+38 -41
View File
@@ -5,8 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Node - Recupero Proposta CCT da contratti",
"name": "Recupero Proposta CCT da contratti (BROWSER)",
"cwd": "${workspaceFolder}/react/recupero-proposta-cct-da-contratti", "cwd": "${workspaceFolder}/react/recupero-proposta-cct-da-contratti",
"env": { "env": {
"NO_COLOR": "true" "NO_COLOR": "true"
@@ -17,20 +16,19 @@
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"], "runtimeArgs": ["run", "dev"],
"skipFiles": [ "skipFiles": [
"<node_internals>/**", "<node_internals>/**",
"**/node_modules/**", "**/node_modules/**",
"**/@vite/client/**" "**/@vite/client/**"
], ],
"serverReadyAction": { "serverReadyAction": {
"action": "debugWithChrome", "action": "debugWithChrome",
"pattern": "Local:\\s+http://localhost:([0-9]+)/", "pattern": "Local:\\s+http://localhost:([0-9]+)/",
"uriFormat": "http://localhost:%s", "uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/react/recupero-proposta-cct-da-contratti/src" "webRoot": "${workspaceFolder}/react/recupero-proposta-cct-da-contratti/src"
}, }
}, },
{ {
"name": "Node - Scelta Carriere",
"name": "Scelta Carriere (BROWSER)",
"cwd": "${workspaceFolder}/react/scelta-carriera", "cwd": "${workspaceFolder}/react/scelta-carriera",
"env": { "env": {
"NO_COLOR": "true" "NO_COLOR": "true"
@@ -41,50 +39,49 @@
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"], "runtimeArgs": ["run", "dev"],
"skipFiles": [ "skipFiles": [
"<node_internals>/**", "<node_internals>/**",
"**/node_modules/**", "**/node_modules/**",
"**/@vite/client/**" "**/@vite/client/**"
], ],
"serverReadyAction": { "serverReadyAction": {
"action": "debugWithChrome", "action": "debugWithChrome",
"pattern": "Local:\\s+http://localhost:([0-9]+)/", "pattern": "Local:\\s+http://localhost:([0-9]+)/",
"uriFormat": "http://localhost:%s", "uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/react/scelta-carriera/src" "webRoot": "${workspaceFolder}/react/scelta-carriera/src"
}, }
}, },
{ {
"name": "Scelta Carriere (DEBUG ONLY)", "name": "WWW - Listen for Xdebug",
"cwd": "${workspaceFolder}/react/scelta-carriera", "type": "php",
"env": {
"NO_COLOR": "true"
},
"outputCapture": "console",
"type": "node",
"request": "launch", "request": "launch",
"runtimeExecutable": "npm", "port": 9023,
"runtimeArgs": ["run", "dev"], "pathMappings": {
"skipFiles": [ "/app": "${workspaceFolder}/php"
"<node_internals>/**", },
"**/node_modules/**", "log": true
"**/@vite/client/**"
]
}, },
{ {
"name": "Recupero Proposta CCT da contratti (DEBUG ONLY)", "name": "WWW - Launch Built-in web server",
"cwd": "${workspaceFolder}/react/recupero-proposta-cct-da-contratti", "type": "php",
"env": {
"NO_COLOR": "true"
},
"outputCapture": "console",
"type": "node",
"request": "launch", "request": "launch",
"runtimeExecutable": "npm", "runtimeArgs": [
"runtimeArgs": ["run", "dev"], "-dxdebug.mode=debug",
"skipFiles": [ "-dxdebug.start_with_request=yes",
"<node_internals>/**", "-S",
"**/node_modules/**", "localhost:0"
"**/@vite/client/**" ],
] "program": "",
}, "cwd": "${workspaceRoot}",
"port": 9023,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
},
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
}
] ]
} }
+41
View File
@@ -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.
+610
View File
@@ -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">&gt;</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;
}
}
+59
View File
@@ -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;
}
}
+153
View File
@@ -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;
}
}
+91
View File
@@ -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]);
}
}
+19
View File
@@ -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);
}
+91
View File
@@ -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();
+36
View File
@@ -0,0 +1,36 @@
# Agente React — 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 **React e TypeScript** nel monorepo `react/`.
## Gestione ID dei Form Element
Gli ID dei campi del form (es. nel Dropdown, Checkbox, TextField) creati tramite i metodi `create...` in [ElixFormsComponentAbstract.tsx](file:///c:/__Git/_UniPR/elixforms-custom-pages/react/common/src/ElixFormsComponentAbstract.tsx) **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. **Libreria Common (`react/common/`)**:
- Scritta in **TypeScript** (`.tsx` o `.ts`).
- Usa **React Class Components** (non hooks/functional components) ereditando da `React.Component`.
- Lo stato è gestito in modo immutabile tramite `this.setState`.
- I checkbox mantengono uno stato locale tramite un `Map<string, number[]>` interno.
2. **Pagine Custom (`react/<nome-pagina>/`)**:
- Possono essere in `.jsx` o `.tsx` (TypeScript con `checkJs: true`).
- Usano **Functional Components** ed eventualmente Hooks se necessario.
- Istanziano la pagina tramite la classe: `new ElixForms.ElixFormsReact(props).render()`.
3. **Import di Tipi**:
- Avendo `verbatimModuleSyntax: true` abilitato in TypeScript, è obbligatorio usare `import type` per i tipi (es. `import type { JSX } from 'react';`).
## Configurazione di Build e Sviluppo
- Ogni pagina è un'applicazione Vite autonoma configurata in `vite.config.js`.
- La build deve produrre un **singolo bundle JS** autolimitato (`codeSplitting: false`, `manualChunks: undefined`).
- I CSS locali e SCSS moduli devono essere iniettati nel bundle finale tramite il plugin `vite-plugin-css-injected-by-js`.
- I CSS globali del design system di UniPR non vanno inclusi nella build ma caricati a runtime via `@import url(...)` in `App.css`.
## Skill React Correlate
Fai riferimento alle seguenti skill per maggiori dettagli operativi:
- [elixforms-common-library](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/common-library/SKILL.md): Documentazione componenti e factory.
- [elixforms-create-new-page](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/create-new-page/SKILL.md): Scaffolding di una nuova pagina React.
- [elixforms-typescript-conventions](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/typescript-react-conventions/SKILL.md): Linee guida di tipizzazione.
- [elixforms-vite-build-config](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/vite-build-config/SKILL.md): Bundling e build singola.
- [elixforms-dev-workflow](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/dev-workflow/SKILL.md): Comandi dev, build e debug in VS Code.
@@ -0,0 +1,17 @@
{
"folders": [
{
"name": "elixforms-custom-pages",
"path": "../../.."
},
{
"name": "elixforms-webservices",
"path": "../../../../elixforms-webservices"
},
{
"name": "SSH FS - Dev Server",
"uri": "ssh://dev.unipr.it/home/dev68/dev"
}
],
"settings": {}
}