# generictable

Editor generico di tabella: tendina con l'elenco delle tabelle lette a runtime dal catalogo del
DB, filtro SQL libero, griglia con colonne auto-rilevate, inserimento/modifica/cancellazione
generici, export Excel. Pensato come strumento di supporto (IT/sviluppo), non come modulo
applicativo con validazione di business — vedi [Limiti e avvertenze](#limiti-e-avvertenze).

Tutta la logica vive qui, in `/opt/gts/shared/generictable/`. Ogni app che lo usa ha in locale
solo 5 file-stub di una riga, un `.layout`, e un `query.json` con la configurazione specifica
delle proprie tabelle. Vedi `/opt/gts/dynamic` o `/opt/gts/ricambi` per due esempi già cablati.

## 1. Prerequisiti nell'app

- `auto_include.php` dell'app deve rendere disponibile una connessione `GTS\DB` valida in `$db`
  (connessione diretta, come in `dynamic`/`ricambi`) oppure in `$dbo` (pattern
  `ctx_session_app("db")`/`auth_db()`, come in altre app della suite) — generictable prova prima
  `$db`, e se non è un'istanza di `GTS\DB` usa `$dbo` come fallback.
- `auto_include_cfg.php` deve avere `"dbtype" => "PGSQL"` (o `"MSSQL"`/`"ORACLE"`) in
  `$gts_app_config`: la scoperta automatica delle tabelle e la deduzione della chiave primaria
  (PGSQL e ORACLE, vedi sotto) dipendono da questo valore.
- Se il DB dell'app ha più schema e si vuole restringere l'elenco a uno solo (es. `dynamic`, che
  separa lo staging `raw` dalle tabelle pulite `public`), aggiungere in `auto_include_cfg.php`:
  ```php
  "generictable_schema" => "public",
  ```
  Se omesso, vengono elencate le tabelle di tutti gli schema del database (comportamento di
  default, adatto alla maggior parte delle app a schema singolo).

## 2. File da creare nell'app (in `/opt/gts/<app>/`)

Cinque stub, tutti identici nello schema — cambia solo il file incluso:

```bash
# Da eseguire nella cartella dell'app (es. cd /opt/gts/miaapp).
# Ognuno di questi file richiama la logica reale in shared/generictable/, passandogli il
# contesto gia' bootstrappato da common_includes.php (per questo il require_once va PRIMA,
# tranne in query_excel.php: generictable_excel.php fa il proprio bootstrap in autonomia,
# esattamente come faceva la vecchia pagina standalone).

cat > query.php << 'EOF'
<?
require_once("common_includes.php");
require_once("/opt/gts/shared/generictable/generictable.php");
?>
EOF

cat > query_data.php << 'EOF'
<?
require_once("common_includes.php");
require_once("/opt/gts/shared/generictable/generictable_data.php");
?>
EOF

cat > query_panel.php << 'EOF'
<?
require_once("common_includes.php");
require_once("/opt/gts/shared/generictable/generictable_panel.php");
?>
EOF

cat > query_cmd.php << 'EOF'
<?
require_once("common_includes.php");
require_once("/opt/gts/shared/generictable/generictable_cmd.php");
?>
EOF

# query_excel.php NON richiede common_includes.php prima: generictable_excel.php lo fa da solo
# (require_once relativo alla propria posizione in shared/), stesso pattern della vecchia
# pagina standalone che sostituisce.
cat > query_excel.php << 'EOF'
<?
require_once("/opt/gts/shared/generictable/generictable_excel.php");
?>
EOF
```

Poi il layout della pagina lista (`query.layout`), che richiama CSS/JS condivisi via path
assoluto — non serve nessun `query.js`/`query.css` locale:

```bash
cat > query.layout << 'EOF'
!bs5 "Tabelle (avanzato)"
!style /shared/generictable/generictable.css
!priority_script /shared/generictable/generictable.js
!script search
!script calendar
!script multiselect
!script tablesorter
%title
%main
 %block
  %table#pdata
   tr
    td.val
     %place-search-1
    td.val
     %place-search-2
    td.val
     %place-search-3
    td.val
     %place-search-4
    td.val
     %place-search-5
    td.val
     %place-search-6
    td.val
     %place-search-7
  %place-btn
 div#flexc.flex-container
  %table#rTable
EOF
```

Non serve invece nessun `query_panel.layout` locale: `generictable_panel.php` inizializza il
template passando il path assoluto del layout condiviso
(`$T->init(__DIR__."/generictable_panel.layout", ...)`), quindi quel file resta solo in
`shared/generictable/` e non va copiato.

## 3. query.json (configurazione specifica dell'app)

Un oggetto con una voce per ogni tabella che si vuole gestire in modo rifinito (le tabelle NON
elencate qui restano comunque consultabili — vedi [Deduzione automatica](#4-deduzione-automatica-della-chiave-primaria-solo-pgsql)):

```json
{
    "nometabella": {
      "chiave": "IDCOLONNA",
      "campi" : {
          "COLONNABOOLEANA": "checkbox",
          "COLONNADATA": "calendar"
      }
    }
}
```

- `"chiave"`: nome della colonna chiave primaria (MAIUSCOLO, coerente col fatto che `GTS\DB::f()`
  restituisce sempre le chiavi in maiuscolo indipendentemente dal case reale in Postgres). Se
  omessa, vedi il paragrafo successivo.
- `"campi"`: opzionale, override del widget per singole colonne:
  - `"checkbox"` — la colonna viene letta/scritta come `'1'`/`'0'` (valore accettato da Postgres
    sia per colonne `boolean` sia `smallint`/`integer` a 0/1). **Non usare su colonne con
    convenzione diversa** (es. flag testuali `'S'`/`'N'` con `CHECK` — vedi `ricambi/query.json`,
    dove `FLGATT` resta volutamente un campo di testo libero per questo motivo).
  - `"calendar"` — la colonna viene letta/scritta come data (`dbDate()`/`hb_calendar()`).
  - qualunque altra colonna (non elencata in `"campi"`) è un semplice campo di testo.

## 4. Deduzione automatica della chiave primaria (PGSQL e ORACLE)

Se una tabella non ha una voce `"chiave"` in `query.json` (es. e' stata appena aggiunta al DB e
nessuno ha ancora toccato il file), `generictable_common.php` prova a dedurla dal catalogo del
DB, in base a `dbtype` (letto da `auto_include_cfg.php`, nessuna configurazione aggiuntiva
richiesta):

- **PGSQL** — `pg_index`/`pg_attribute`.
- **ORACLE** — `user_constraints`/`user_cons_columns` (solo tabelle dello schema/utente
  corrente, stesso perimetro di `USER_TABLES` gia' usato per l'elenco tabelle).
- **MSSQL** — non ancora implementato: le tabelle non presenti in `query.json` restano solo
  consultabili finché non si aggiunge la voce a mano.

In tutti i casi:
- **PK a colonna singola trovata** → tutto funziona come se fosse configurata a mano (modifica,
  cancellazione, auto-calcolo del nuovo ID in inserimento).
- **Nessuna PK, o PK composta da più colonne** (non supportata: lo strumento lavora per singolo
  ID) → la tabella resta **solo consultabile**: niente icone modifica/cancella in lista, niente
  pulsante "Registra" nel panel (sostituito da un avviso che invita ad aggiungere `"chiave"` in
  `query.json`).

## 5. Voce di menu

```xml
<item descr="Utilità" id='Mnu_Utilita'>
  <subitem descr="Tabelle (avanzato)" link="query.php" id='Mnu_Query' fa='fa-database' />
</item>
```

## 6. Permesso di accesso

Tutti e 5 i file (raggiungibili singolarmente via URL diretto, non solo attraverso il menu)
richiedono `ctx_can("querydata")` oppure l'utente master (`ctx_uid()==1`). Se nessun utente ha il
permesso `querydata` configurato in HQ per l'app, solo l'utente master potrà usare lo strumento —
assegnare il permesso da HQ agli utenti che ne hanno bisogno.

## Limiti e avvertenze

- **Il campo "Filtro" è una condizione SQL libera**, concatenata direttamente nella `WHERE` senza
  binding parametrico (stessa scelta della versione originale). Non è un problema di SQL
  injection da utenti anonimi (l'accesso è già ristretto dal permesso `querydata`/utente master),
  ma chi lo usa deve sapere che sta scrivendo SQL vero, non un semplice termine di ricerca.
- **Bypassa tutta la validazione/business logic** delle pagine CRUD "vere" dell'app (ricalcolo
  totali, FK-aware dropdown, vincoli applicativi oltre a quelli imposti dal DB stesso). Va usato
  con consapevolezza, tipicamente da IT/sviluppo per correzioni puntuali.
- **Non supporta "tabelle virtuali"** (un nome in tendina che in realtà è la stessa tabella fisica
  con un filtro precostituito, es. `careglio` aveva `anagrafica1`=Clienti/`anagrafica2`=
  Collaboratori sulla stessa tabella `anagrafica` filtrata per `cliente=true/false`): l'elenco
  tabelle riflette 1:1 le tabelle reali del catalogo. Per questo `careglio` non è stato migrato a
  questo strumento condiviso.
- **Attenzione a `$db` vs `$dbo`** in app dove le due variabili puntano a database diversi (es.
  `mootest`/`appstd`, dove `$dbo` è una connessione Oracle separata, non un alias del DB
  principale): il fallback di generictable usa `$dbo` solo se `$db` non è già una connessione
  valida, quindi se l'app imposta sempre `$db`, generictable interrogherà **quel** database, non
  necessariamente quello che la vecchia pagina locale (se ne aveva una) interrogava di proposito.
  Verificare quale database ci si aspetta di interrogare prima di migrare un'app che ha già una
  copia locale con un comportamento diverso.

## Verifica dopo l'installazione in una nuova app

```bash
# sintassi PHP degli stub appena creati
for f in query.php query_data.php query_panel.php query_cmd.php query_excel.php; do
    php -l "$f"
done

# query.json valido
php -r 'var_dump(json_decode(file_get_contents("query.json")) !== null);'
```

Poi da browser: aprire `query.php`, verificare che la tendina "Tabella" elenchi le tabelle attese
(solo lo schema giusto, se impostato `generictable_schema`), fare una ricerca, aprire una riga in
modifica, salvare, provare "Esporta Excel".
