diff --git a/README.md b/README.md
index 6527d43..169f53b 100644
--- a/README.md
+++ b/README.md
@@ -25,9 +25,10 @@
## Documentazione
-Vedi `readme.txt` per changelog e note GDPR (formato WordPress.org).
-Vedi `deploy/wiki-tonitalia-pluginsoci.md` per la guida operativa completa.
+- Changelog e note GDPR: `readme.txt`
+- Guida operativa completa: [WIKI.md](WIKI.md)
+- Download zip release: [releases/README.md](releases/README.md)
-## Versione
+## Versione attuale
-Vedi intestazione in `ton-italia-registration.php` (`TON_REG_VERSION`).
+**1.8.2** — vedi `TON_REG_VERSION` in `ton-italia-registration.php`.
diff --git a/WIKI.md b/WIKI.md
new file mode 100644
index 0000000..3d6886e
--- /dev/null
+++ b/WIKI.md
@@ -0,0 +1,435 @@
+# Wiki — Plugin TON Italia Registration
+
+**Repository:** `https://niphredil.duckdns.org/git/fabio/tonitalia-pluginsoci.git`
+**Cartella WordPress:** `wp-content/plugins/ton-italia-registration/`
+**Versione attuale:** 1.8.2
+**Organizzazione:** TON ITALIA ODV
+
+---
+
+## 1. Scopo del plugin
+
+Gestisce il processo di **domanda di ammissione a socio e tesseramento** TON ITALIA ODV:
+
+- Form pubblico fedele al modulo PDF (campi anagrafici, dichiarazioni, privacy).
+- Creazione utente WordPress **Subscriber** (senza accesso wp-admin).
+- Notifiche email admin e richiedente.
+- Pannello back-office per ammissione, documenti e GDPR.
+- CAPTCHA integrato (nessun servizio esterno).
+
+---
+
+## 2. Requisiti
+
+| Requisito | Valore |
+|-----------|--------|
+| WordPress | 6.0+ |
+| PHP | 8.0+ (8.1+ consigliato in produzione Aruba) |
+| Permessi admin | `manage_options` per gestione iscrizioni |
+| SMTP / mail | `wp_mail` funzionante (hosting o plugin SMTP) |
+
+---
+
+## 3. Installazione
+
+1. Copiare `ton-italia-registration` in `wp-content/plugins/`.
+2. **Plugin → Plugin installati → Attiva** “TON Italia Registration”.
+3. All’attivazione: creazione tabella DB `{prefisso}ton_registrations`, cartella upload `Documenti Iscritti`, opzioni default (testi da PDF).
+4. Creare pagina (es. “Iscrizione socio”) e inserire:
+
+```
+[ton_registration_form]
+```
+
+5. **TON Iscrizioni → Impostazioni**: verificare testi, IBAN, email, CAPTCHA.
+
+### Attributo shortcode opzionale
+
+```
+[ton_registration_form class="mia-classe-css"]
+```
+
+Aggiunge una classe al wrapper `.ton-reg-form` per stili custom nel tema.
+
+---
+
+## 4. Flusso utente (frontend)
+
+```mermaid
+flowchart TD
+ A[Utente apre pagina con shortcode] --> B[Compila form]
+ B --> C[CAPTCHA + validazione]
+ C --> D{OK?}
+ D -->|No| E[Messaggio errore sulla pagina]
+ D -->|Sì| F[Crea utente Subscriber]
+ F --> G[Salva riga DB + meta]
+ G --> H[Email admin + email utente]
+ H --> I[Messaggio successo]
+```
+
+### 4.1 Sezioni del form (ordine)
+
+1. **Quota associativa** — testo informativo (bonifico, IBAN, quota) da impostazioni; checkbox opzionale “Ho effettuato il bonifico”.
+2. **Dati anagrafici** — cognome, nome, nascita, CF, residenza, telefono, email.
+3. **Dichiarazione** — testo statuto (editabile), luogo/data, checkbox accettazione (sostitutivo firma).
+4. **Privacy** — informativa breve, checkbox privacy obbligatoria, newsletter (opzionale o obbligatoria da settings).
+5. **Verifica anti-spam** — se CAPTCHA attivo: somma matematica + honeypot.
+
+### 4.2 Formato date
+
+Le date (**nascita** e **dichiarazione**) sono in formato **gg-mm-aaaa** (es. `25-05-1990`).
+In database vengono salvate come `aaaa-mm-gg`.
+
+### 4.3 Dopo l’invio
+
+- Redirect alla stessa pagina con `?ton_reg=success` (messaggio HTML da impostazioni).
+- Creazione account con **email** univoca; login generato dalla parte locale dell’email (`mario`, `mario1`, … se collisioni).
+- Email utente con link **reimposta password** (WordPress standard).
+- Ruolo: **subscriber** — bloccato su `/wp-admin/` e barra admin nascosta.
+
+---
+
+## 5. Sicurezza form
+
+| Meccanismo | Descrizione |
+|------------|-------------|
+| Nonce WordPress | `ton_reg_nonce` su ogni submit |
+| Honeypot | Campo nascosto `ton_reg_website` — se compilato, rifiuto |
+| Tempo minimo | Form non inviabile prima di ~3 secondi dal caricamento |
+| CAPTCHA matematico | Domanda casuale (es. 4+7), risposta in transient legata al token |
+| Rate limit IP | Default: max 5 tentativi / 15 min (impostazioni) |
+| Validazione server | CF, CAP 5 cifre, province 2 lettere, email, date, checkbox obbligatori |
+| Duplicati | Blocco se email o CF già in iscrizione `pending` o `admitted` |
+
+---
+
+## 6. Database
+
+### Tabella `{prefisso}ton_registrations`
+
+Contiene tutti i campi del form + metadati legali:
+
+| Campo | Note |
+|-------|------|
+| `registration_ip` | IP al momento dell’invio (finalità legali) |
+| `user_agent` | Troncato a 255 caratteri |
+| `privacy_text_version` | Versione informativa al consenso |
+| `consent_statuto_at`, `consent_privacy_at` | Timestamp UTC |
+| `consenso_statuto`, `consenso_privacy`, `consenso_newsletter` | 0/1 |
+| `status` | `pending` \| `admitted` \| `rejected` |
+| `libro_associati_date` | Solo admin — data Libro Associati |
+| `doc_identita_id`, `doc_bonifico_id`, `doc_pagamento_id` | ID attachment Media |
+| `admin_notes`, `gdpr_log` | Note e log azioni GDPR |
+| `anonymized_at` | Se anonimizzato |
+
+Mirror parziale anche in `user_meta` con prefisso `ton_reg_*`.
+
+### Tabella `{prefisso}ton_reg_membership_cards` (v1.7+)
+
+Tessere PDF generate per `(registration_id, year)`.
+
+### Tabella `{prefisso}ton_reg_membership_renewals` (v1.8+)
+
+Quote associative per anno: `status` (`pending` \| `paid`), timestamp promemoria gennaio.
+
+---
+
+## 7. Pannello admin — TON Iscrizioni
+
+Menu WordPress: **TON Iscrizioni** (icona groups).
+
+### 7.1 Elenco
+
+- Filtri: Tutti / In attesa / Ammessi / Non ammessi / **Quota da pagare**.
+- Ricerca per nome, email, codice fiscale.
+- Colonne: ID, cognome, nome, email, stato, **Rinnovo** (quota anno corrente), data.
+- Link **Dettaglio** per ogni riga.
+- Pulsante **Esporta CSV Libro Soci**: scarica tutti i soci **ammessi** (esclusi anonimizzati) in CSV con anagrafica, contatti, data iscrizione e data Libro Associati. File UTF-8 con separatore `;` per Excel italiano.
+
+### 7.2 Dettaglio singola iscrizione
+
+Barra navigazione: **Socio precedente** / **Socio successivo** (mantiene filtri e ordinamento dell'elenco).
+
+**Documenti** (solo backend, upload admin):
+
+| Tipo | Colonna DB | Formati |
+|------|------------|---------|
+| Documento di identità | `doc_identita_id` | PDF, JPG, PNG |
+| Ricevuta versamento tessera | `doc_pagamento_id` | PDF, JPG, PNG |
+
+File salvati in Media WordPress, percorso fisico:
+
+```
+wp-content/uploads/Documenti Iscritti/{nome-cognome-codicefiscale}/file.pdf
+```
+
+La cartella base viene creata all’**attivazione** plugin; la sottocartella iscritto alla prima iscrizione o al primo upload.
+
+**Tessere PDF (v1.7+)** — per soci ammessi:
+
+- Generazione automatica al passaggio in stato **Ammesso**.
+- Template PDF annuale configurabile in **TON Iscrizioni → Tessere**.
+- Batch «Genera tessere nuovo anno» per tutti gli ammessi.
+- PDF in `Documenti Iscritti/{socio}/tessere/`.
+
+**Quote associative annuali (v1.8+)** — checkbox «Pagata» per ogni anno solare, dall'anno iscrizione all'anno in corso (l'anno successivo compare a gennaio).
+
+**Consensi e dichiarazioni** (sezione dedicata):
+
+- Dichiarazione statuto — Sì/No + data/ora registrata.
+- Privacy e trattamento dati — Sì/No + data/ora + versione informativa.
+- Newsletter — accettato o non espresso.
+- Bonifico dichiarato — checkbox form opzionale.
+- Luogo e data dichiarazione.
+
+**Dati anagrafici** — tabella read-only dei campi inviati.
+
+**Gestione stato (Consiglio Direttivo):**
+
+- Stato: In attesa / **Ammesso** / **Non ammesso**.
+- Data Libro Associati.
+- Note admin.
+
+**GDPR:**
+
+- Esporta JSON / CSV.
+- Anonimizza (mantiene record, cancella PII).
+- Elimina definitivamente (utente WP + riga DB).
+
+### 7.3 Impostazioni
+
+| Gruppo | Opzioni |
+|--------|---------|
+| Quota | Anno sociale, importo €, beneficiario, IBAN |
+| Testi form | Istruzioni bonifico (HTML), dichiarazione statuto, privacy, newsletter, minorenni, messaggio successo |
+| Email | Oggetto/corpo admin e utente; email promemoria rinnovo (1 e 25 gennaio); riepilogo admin rinnovi; email notifiche; contatto GDPR |
+| Comportamento | CAPTCHA on/off, newsletter obbligatoria, rate limit, elimina utenti alla disinstallazione |
+| Mailchimp | Integrazione attiva, API key, List ID (Audience), tag (es. «Socio TON ITALIA»), test connessione |
+
+### 7.4 Integrazione Mailchimp (v1.6+)
+
+Quando un richiedente spunta il **consenso newsletter** nel form, l’email viene sincronizzata con Mailchimp (solo se l’integrazione è attiva e configurata). L’iscrizione socio **non** viene mai bloccata da errori Mailchimp.
+
+**Configurazione:**
+
+1. Mailchimp → Account → Extras → **API keys** → crea chiave.
+2. Audience → Settings → **Audience ID** (List ID).
+3. In **TON Iscrizioni → Impostazioni → Integrazione Mailchimp**: attiva, inserisci API key e List ID, salva.
+4. Clic **Test connessione Mailchimp** (verifica credenziali e memorizza eventuali marketing permissions GDPR della lista).
+
+**Comportamento tecnico:**
+
+- Sync asincrona (WP-Cron) dopo invio form riuscito.
+- Upsert sicuro: `status_if_new: subscribed` (non re-iscrive contatti unsubscribed senza nuovo consenso).
+- Se l’utente si è disiscritto in passato ma spunta di nuovo la newsletter sul form TON, viene ripristinato `subscribed`.
+- Contatti in stato `cleaned` (bounce): errore in admin, gestione manuale.
+- Tag applicati via API dedicata (max 5, separati da virgola nelle impostazioni).
+
+**Dettaglio iscrizione:** sotto consenso newsletter compare lo stato sync (`pending`, `synced`, `error`) con pulsante **Riprova iscrizione Mailchimp** se necessario.
+
+**Sicurezza:** API key salvata con `autoload = no`, campo password in admin (lasciare vuoto per non modificare), accesso solo a `manage_options`.
+
+**Placeholder email:** `{nome}`, `{cognome}`, `{email}`, `{codice_fiscale}`, `{ip}`, `{login_url}`, `{gdpr_contact}`, `{admin_url}`
+
+**Placeholder istruzioni bonifico:** `{quota}`, `{anno}`, `{beneficiario}`, `{iban}`, `{nome}`, `{cognome}`
+
+In cima alla pagina: istruzioni per shortcode `[ton_registration_form]`.
+
+### 7.5 Profilo socio (v1.7.1+)
+
+I Subscriber accedono solo a **Profilo** in wp-admin (`profile.php`) e possono scaricare la tessera PDF dell'anno.
+
+- Placeholder email `{profile_url}`.
+- Shortcode legacy `[ton_registration_profile]` mantenuto per retrocompatibilità.
+
+---
+
+## 8. Email
+
+### Admin
+
+Inviata a `ton_reg_admin_email` (default: email amministratore sito).
+Contiene riepilogo e link alla scheda iscrizione in admin.
+
+### Utente
+
+Inviata all’email del form.
+Include istruzioni bonifico, contatto GDPR, invito a impostare password via `wp_lostpassword_url()`.
+Viene anche inviata la notifica WordPress standard nuovo utente (`wp_send_new_user_notifications`).
+
+**Nota:** configurare SPF/DKIM su Aruba o usare plugin SMTP se le mail non arrivano.
+
+### Rinnovo quota annuale (v1.8+)
+
+La tessera associativa copre l'**anno solare** (1 gennaio – 31 dicembre). Il plugin traccia il pagamento della quota per ogni anno nella tabella `ton_reg_membership_renewals`.
+
+**Gestione admin:**
+
+- **Elenco iscrizioni:** colonna **Rinnovo** con stato quota anno corrente (Pagato / Da pagare / Sollecito / Non pagato).
+- Filtro **Quota da pagare** (ammessi senza quota pagata per l'anno in corso).
+- **Dettaglio iscrizione:** sezione **Quote associative annuali** con checkbox «Pagata» per ogni anno (da anno iscrizione all'anno in corso; l'anno successivo compare automaticamente a gennaio).
+
+**Promemoria automatici (WP-Cron giornaliero):**
+
+| Data | Destinatari | Contenuto |
+|------|-------------|-----------|
+| **1 gennaio** | Soci ammessi senza quota pagata | Primo promemoria con istruzioni bonifico |
+| **25 gennaio** | Stessi soci ancora senza pagamento | Secondo sollecito |
+| Stesse date | Email admin (`ton_reg_admin_email`) | Riepilogo elenco soci da pagare |
+
+Testi email configurabili in **Impostazioni** (oggetto/corpo primo e secondo promemoria, riepilogo admin).
+
+**Placeholder email rinnovo:** `{nome}`, `{cognome}`, `{email}`, `{anno}`, `{quota}`, `{beneficiario}`, `{iban}`, `{payment_instructions}`, `{profile_url}`, `{gdpr_contact}`. Admin rinnovi: `{lista_soci}`, `{numero_soci}`, `{anno}`, `{admin_url}`.
+
+**Soci già ammessi prima dell'aggiornamento:** nessun backfill automatico; risultano «da pagare» finché l'admin non spunta la checkbox o fino ai promemoria di gennaio.
+
+**Anno inizio promemoria** (`ton_reg_renewal_reminders_start_year`, default **2027**): nessuna email automatica parte all'installazione/aggiornamento del plugin; i primi invii avvengono solo dal **1 gennaio** dell'anno configurato (poi sollecito il 25 gennaio). Modificabile in **Impostazioni → Anno inizio promemoria rinnovo**.
+
+**Affidabilità cron:** WP-Cron richiede visite al sito; per invii puntuali a inizio gennaio configurare un cron di sistema che chiami `wp-cron.php`.
+
+---
+
+## 9. GDPR
+
+### Raccolta
+
+- Informativa visibile prima dei checkbox privacy.
+- Consensi espliciti con timestamp.
+- IP registrato (base giuridica documentata nelle impostazioni editabili).
+
+### Diritti (azioni admin)
+
+| Azione | Effetto |
+|--------|---------|
+| **Esporta JSON/CSV** | Dump dati iscrizione + meta utente WP |
+| **Anonimizza** | PII sostituiti con `ANONIMIZZATO`; mantiene ID e date consenso; utente WP con email fittizia |
+| **Elimina** | `wp_delete_user()` + cancellazione riga tabella + documenti |
+
+Contatto predefinito interessato: `qeshet.tonitalia@gmail.com` (modificabile).
+
+### Privacy by design
+
+- Nessun tracking terze parti nel form.
+- Minimizzazione campi rispetto al PDF modulo.
+
+---
+
+## 10. Disinstallazione
+
+**Disattivazione:** non cancella dati.
+
+**Eliminazione plugin** (da Plugin → Elimina):
+
+- Drop tabella `ton_registrations` e `ton_reg_membership_renewals`.
+- Rimozione opzioni `ton_reg_*` e transient `ton_reg_*`.
+- Eliminazione cartella `wp-content/uploads/Documenti Iscritti/`.
+- Gli utenti **Subscriber** creati dal plugin **restano** (default).
+
+Opzione **“Elimina utenti alla disinstallazione”** in Impostazioni: se attiva prima della rimozione, cancella anche gli utenti WP collegati alle iscrizioni.
+
+---
+
+## 11. Struttura codice (sviluppatori)
+
+```
+ton-italia-registration.php Bootstrap
+includes/
+ class-plugin.php Orchestrazione, upgrade DB
+ class-registration-form.php Shortcode + HTML form
+ class-registration-handler.php Submit, validazione, save
+ class-captcha.php Anti-spam
+ class-user-manager.php Subscriber + login univoco
+ class-database.php CRUD tabella
+ class-mailer.php wp_mail + promemoria rinnovo
+ class-membership-renewals.php Quote annuali per anno
+ class-renewal-reminders.php Cron promemoria gennaio
+ class-membership-card.php Generazione tessera PDF (FPDI)
+ class-membership-cards.php CRUD tessere
+ class-members-export.php Export CSV Libro Soci
+ class-profile.php Profilo subscriber + download tessera
+ class-gdpr.php Export / anonimizza / erase
+ class-documents.php Upload Media per iscritto
+ class-defaults.php Testi default PDF
+admin/
+ class-admin.php Menu, settings, dettaglio
+ class-registration-list-table.php
+assets/css, assets/js Form frontend
+uninstall.php Hook disinstallazione
+```
+
+---
+
+## 12. Upgrade versione plugin
+
+1. Backup DB e cartella `Documenti Iscritti`.
+2. Sostituire cartella plugin (o `git pull`).
+3. Visitare qualsiasi pagina admin — `maybe_upgrade()` esegue `dbDelta` se serve nuove colonne.
+4. Verificare **TON Iscrizioni → Elenco** e una scheda dettaglio.
+
+---
+
+## 13. Risoluzione problemi
+
+| Problema | Soluzione |
+|----------|-----------|
+| Form non appare | Pagina contiene shortcode? Plugin attivo? |
+| Email non arrivano | Test SMTP; spam; `wp_mail` log |
+| “Email già registrata” | Utente WP o iscrizione attiva esistente |
+| Elenco vuoto ma conta 1 iscrizione | Aggiornare a v1.3+ (fix paginazione) |
+| PHP compatibility warning | Hosting PHP ≥ 8.0 |
+| Upload documento fallisce | Permessi `uploads/`, max upload PHP, formato PDF/JPG/PNG |
+| Subscriber accede wp-admin | Plugin blocca redirect; verificare ruolo subscriber |
+
+---
+
+## 14. Workflow operativo consigliato (segreteria)
+
+1. Richiesta arriva → email admin + stato **In attesa**.
+2. Verificare dati e caricare **documento identità** / **ricevute** in dettaglio.
+3. Verificare bonifico su conto corrente.
+4. Consiglio Direttivo: impostare **Ammesso** o **Non ammesso** + data Libro Associati.
+5. Segnare **Quota pagata** per l'anno di iscrizione nella scheda socio.
+6. Nel dettaglio iscrizione usare **Socio precedente** / **Socio successivo** per scorrere l'elenco senza tornare indietro.
+7. A gennaio: verificare colonna **Rinnovo** / filtro **Quota da pagare**; i promemoria email partono il 1 e il 25 gennaio.
+8. Comunicazione esterna all'iscritto (fuori plugin o email manuale).
+9. Su richiesta cancellazione dati: **Anonimizza** o **Elimina** da scheda GDPR.
+
+---
+
+## 15. Repository Git
+
+```bash
+cd wp-content/plugins/ton-italia-registration
+git pull origin master
+git add .
+git commit -m "v1.x: descrizione"
+git push origin master
+```
+
+Pacchetti zip release in `releases/ton-italia-registration-X.Y.Z.zip` (nel repository GitBucket, tag `vX.Y.Z`).
+
+**Download ultima release (1.8.2):**
+`https://niphredil.duckdns.org/git/fabio/tonitalia-pluginsoci/raw/v1.8.2/releases/ton-italia-registration-1.8.2.zip`
+
+Per generare un nuovo zip dal progetto sito: `./deploy/build-plugin-zip.sh` nella root di tents-of-nations, poi copiare in `releases/` del repo plugin.
+
+**Importante — struttura zip:** il file zip deve contenere la cartella `ton-italia-registration/` come radice. WordPress deve **sostituire** la cartella esistente, non crearne una nuplicata.
+
+---
+
+| Versione | Novità principali |
+|----------|-------------------|
+| 1.6 | Integrazione Mailchimp su consenso newsletter |
+| 1.5 | Ricevuta pagamento (3° documento) |
+| 1.4 | Sezione consensi in dettaglio admin |
+| 1.3 | Fix elenco iscrizioni |
+| 1.2 | Date form gg-mm-aaaa |
+| 1.1 | Upload documenti identità/bonifico |
+| 1.0 | Release iniziale |
+
+---
+
+## 17. Contatti tecnici
+
+- Sviluppo: Fabio Arrigoni
+- GDPR operativo TON: `qeshet.tonitalia@gmail.com` (configurabile in plugin)
diff --git a/admin/class-admin.php b/admin/class-admin.php
index df2bfd4..eeb4a48 100644
--- a/admin/class-admin.php
+++ b/admin/class-admin.php
@@ -27,6 +27,11 @@
add_action( 'admin_post_ton_reg_delete_document', array( __CLASS__, 'delete_document' ) );
add_action( 'admin_post_ton_reg_mailchimp_test', array( __CLASS__, 'mailchimp_test' ) );
add_action( 'admin_post_ton_reg_mailchimp_retry', array( __CLASS__, 'mailchimp_retry' ) );
+ add_action( 'admin_post_ton_reg_save_card_settings', array( __CLASS__, 'save_card_settings' ) );
+ add_action( 'admin_post_ton_reg_upload_card_template', array( __CLASS__, 'upload_card_template' ) );
+ add_action( 'admin_post_ton_reg_batch_cards', array( __CLASS__, 'batch_cards' ) );
+ add_action( 'admin_post_ton_reg_generate_card', array( __CLASS__, 'generate_card' ) );
+ add_action( 'admin_post_ton_reg_export_libro_soci', array( __CLASS__, 'export_libro_soci' ) );
}
/**
@@ -95,6 +100,7 @@
$plain_text = array(
'ton_reg_social_year',
+ 'ton_reg_renewal_reminders_start_year',
'ton_reg_membership_fee',
'ton_reg_beneficiary',
'ton_reg_iban',
@@ -102,6 +108,9 @@
'ton_reg_gdpr_contact',
'ton_reg_email_admin_subject',
'ton_reg_email_user_subject',
+ 'ton_reg_email_renewal_first_subject',
+ 'ton_reg_email_renewal_second_subject',
+ 'ton_reg_email_renewal_admin_subject',
'ton_reg_rate_limit_max',
'ton_reg_rate_limit_window',
'ton_reg_privacy_version',
@@ -112,6 +121,7 @@
'ton_reg_mailchimp_api_key',
'ton_reg_mailchimp_list_id',
'ton_reg_mailchimp_permission_ids',
+ 'ton_reg_success_page_id',
);
register_setting(
@@ -136,6 +146,17 @@
)
);
+ register_setting(
+ 'ton_reg_settings',
+ 'ton_reg_success_page_id',
+ array(
+ 'type' => 'integer',
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_success_page_id' ),
+ 'show_in_rest' => false,
+ 'default' => 0,
+ )
+ );
+
$options = array_keys( TON_Reg_Defaults::options() );
foreach ( $options as $option ) {
if ( in_array( $option, $skip_auto, true ) ) {
@@ -180,6 +201,23 @@
}
/**
+ * @param mixed $value Page ID.
+ * @return int
+ */
+ public static function sanitize_success_page_id( $value ) {
+ $page_id = absint( $value );
+ if ( ! $page_id ) {
+ return 0;
+ }
+
+ if ( ! TON_Reg_Registration_Handler::is_publicly_viewable_page( $page_id ) ) {
+ return 0;
+ }
+
+ return $page_id;
+ }
+
+ /**
* List page.
*/
public static function page_list() {
@@ -196,30 +234,43 @@
$table->prepare_items();
$status = isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : '';
+ $renewal_unpaid = ! empty( $_GET['renewal_unpaid'] );
?>
-
+
' . esc_html__( 'Non trovato.', 'ton-italia-registration' ) . '
';
return;
}
+
+ $list_context = self::get_detail_list_context();
+ $adjacent = TON_Reg_Database::get_adjacent_ids( $id, $list_context );
?>
-
+
-
←
+
@@ -255,12 +309,17 @@
+
+
+
+
status;
+ $new_status = sanitize_text_field( wp_unslash( $_POST['status'] ?? 'pending' ) );
+
TON_Reg_Database::update(
$id,
array(
- 'status' => sanitize_text_field( wp_unslash( $_POST['status'] ?? 'pending' ) ),
+ 'status' => $new_status,
'libro_associati_date' => sanitize_text_field( wp_unslash( $_POST['libro_associati_date'] ?? '' ) ) ?: null,
'admin_notes' => sanitize_textarea_field( wp_unslash( $_POST['admin_notes'] ?? '' ) ),
)
);
- wp_safe_redirect( admin_url( 'admin.php?page=ton-registrations&action=view&id=' . $id . '&updated=1' ) );
+ $redirect_args = array( 'updated' => '1' );
+ $card_result = TON_Reg_Membership_Card::maybe_generate_on_admission( $id, $old_status, $new_status );
+ if ( is_wp_error( $card_result ) ) {
+ $redirect_args['card_error'] = rawurlencode( $card_result->get_error_message() );
+ } elseif ( null !== $card_result ) {
+ $redirect_args['card_generated'] = '1';
+ }
+
+ self::save_renewal_checkboxes( $id, $row );
+
+ wp_safe_redirect(
+ add_query_arg(
+ $redirect_args,
+ self::detail_url( $id, self::get_detail_list_context() )
+ )
+ );
exit;
}
/**
+ * Export admitted members as Libro Soci CSV.
+ */
+ public static function export_libro_soci() {
+ if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'ton_reg_export_libro_soci' ) ) {
+ wp_die( esc_html__( 'Non autorizzato.', 'ton-italia-registration' ), 403 );
+ }
+
+ TON_Reg_Members_Export::stream_libro_soci_csv();
+ }
+
+ /**
* GDPR export download.
*/
public static function gdpr_export() {
@@ -904,4 +1287,417 @@
);
exit;
}
+
+ /**
+ * Membership cards section on registration detail.
+ *
+ * @param int $id Registration ID.
+ * @param object $row Registration row.
+ */
+ private static function render_membership_cards_section( $id, $row ) {
+ $cards = TON_Reg_Membership_Cards::get_by_registration( $id );
+ $active_year = TON_Reg_Membership_Card::get_active_year();
+ ?>
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+ | year ); ?> |
+ created_at ) ); ?> |
+ generation_context ) ); ?> |
+
+
+
+
+ |
+
+
+
+
+
+
+ status && empty( $row->anonymized_at ) ) : ?>
+
+
+
' . esc_html__( 'Libreria PDF (FPDI) non trovata in vendor/.', 'ton-italia-registration' ) . '
';
+ return;
+ }
+
+ $active_year = TON_Reg_Membership_Card::get_active_year();
+ $config = TON_Reg_Membership_Card::get_template_config( $active_year );
+ $template_id = (int) ( $config['template_attachment_id'] ?? 0 );
+ $template_url = $template_id ? wp_get_attachment_url( $template_id ) : '';
+ $admitted_count = TON_Reg_Database::count(
+ array(
+ 'status' => 'admitted',
+ 'exclude_anonymized' => true,
+ )
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+ (int) ( $current['template_attachment_id'] ?? 0 ),
+ 'name_x' => isset( $_POST['name_x'] ) ? (float) wp_unslash( $_POST['name_x'] ) : 42.5,
+ 'name_y' => isset( $_POST['name_y'] ) ? (float) wp_unslash( $_POST['name_y'] ) : 118.0,
+ 'font_family' => sanitize_text_field( wp_unslash( $_POST['font_family'] ?? 'Helvetica' ) ),
+ 'font_style' => sanitize_text_field( wp_unslash( $_POST['font_style'] ?? '' ) ),
+ 'font_size' => isset( $_POST['font_size'] ) ? (float) wp_unslash( $_POST['font_size'] ) : 14,
+ 'text_color' => sanitize_text_field( wp_unslash( $_POST['text_color'] ?? '#000000' ) ),
+ 'uppercase' => ! empty( $_POST['uppercase'] ),
+ 'align' => sanitize_text_field( wp_unslash( $_POST['align'] ?? 'C' ) ),
+ 'cell_width' => isset( $_POST['cell_width'] ) ? (float) wp_unslash( $_POST['cell_width'] ) : 80,
+ )
+ );
+
+ wp_safe_redirect( admin_url( 'admin.php?page=ton-reg-settings&card_settings_saved=1' ) );
+ exit;
+ }
+
+ /**
+ * Upload card template PDF for active year.
+ */
+ public static function upload_card_template() {
+ TON_Reg_Membership_Card::assert_can_manage_cards();
+ check_admin_referer( 'ton_reg_upload_card_template', 'ton_reg_card_template_nonce' );
+
+ $year = isset( $_POST['active_year'] ) ? (int) $_POST['active_year'] : TON_Reg_Membership_Card::get_active_year();
+ $file = isset( $_FILES['card_template'] ) ? $_FILES['card_template'] : null;
+
+ $attachment_id = TON_Reg_Membership_Card::upload_template_pdf( $file, $year );
+
+ if ( is_wp_error( $attachment_id ) ) {
+ wp_safe_redirect(
+ add_query_arg(
+ 'card_error',
+ rawurlencode( $attachment_id->get_error_message() ),
+ admin_url( 'admin.php?page=ton-reg-settings' )
+ )
+ );
+ exit;
+ }
+
+ update_post_meta( $attachment_id, '_ton_reg_card_template_year', $year );
+
+ $current = TON_Reg_Membership_Card::get_template_config( $year );
+ $old_id = (int) ( $current['template_attachment_id'] ?? 0 );
+ if ( $old_id && $old_id !== (int) $attachment_id ) {
+ wp_delete_attachment( $old_id, true );
+ }
+
+ TON_Reg_Membership_Card::save_template_config(
+ $year,
+ array(
+ 'template_attachment_id' => (int) $attachment_id,
+ )
+ );
+
+ wp_safe_redirect( admin_url( 'admin.php?page=ton-reg-settings&card_template_ok=1' ) );
+ exit;
+ }
+
+ /**
+ * Chunked batch card generation.
+ */
+ public static function batch_cards() {
+ TON_Reg_Membership_Card::assert_can_manage_cards();
+ check_admin_referer( 'ton_reg_batch_cards', 'ton_reg_batch_cards_nonce' );
+
+ $year = isset( $_POST['year'] ) ? (int) $_POST['year'] : TON_Reg_Membership_Card::get_active_year();
+ $offset = isset( $_POST['offset'] ) ? (int) $_POST['offset'] : 0;
+ $force_regenerate = ! empty( $_POST['force_regenerate'] );
+ $chunk_size = 20;
+
+ $generated = isset( $_POST['batch_generated'] ) ? (int) $_POST['batch_generated'] : 0;
+ $skipped = isset( $_POST['batch_skipped'] ) ? (int) $_POST['batch_skipped'] : 0;
+ $errors = isset( $_POST['batch_errors'] ) ? (int) $_POST['batch_errors'] : 0;
+
+ $rows = TON_Reg_Database::query(
+ array(
+ 'status' => 'admitted',
+ 'exclude_anonymized' => true,
+ 'limit' => $chunk_size,
+ 'offset' => $offset,
+ 'orderby' => 'id',
+ 'order' => 'ASC',
+ )
+ );
+
+ foreach ( $rows as $row ) {
+ $existing = TON_Reg_Membership_Cards::get_by_registration_year( (int) $row->id, $year );
+ if ( $existing && ! $force_regenerate ) {
+ ++$skipped;
+ continue;
+ }
+
+ $result = TON_Reg_Membership_Card::generate_for_registration(
+ (int) $row->id,
+ $year,
+ array(
+ 'context' => TON_Reg_Membership_Card::CONTEXT_BATCH,
+ 'force_regenerate' => $force_regenerate,
+ )
+ );
+
+ if ( is_wp_error( $result ) ) {
+ ++$errors;
+ } else {
+ ++$generated;
+ }
+ }
+
+ $processed = count( $rows );
+ if ( $processed < $chunk_size ) {
+ wp_safe_redirect(
+ add_query_arg(
+ array(
+ 'card_batch_done' => '1',
+ 'batch_generated' => $generated,
+ 'batch_skipped' => $skipped,
+ 'batch_errors' => $errors,
+ ),
+ admin_url( 'admin.php?page=ton-reg-settings' )
+ )
+ );
+ exit;
+ }
+
+ ?>
+
+
+
+
+
+
+ TON_Reg_Membership_Card::CONTEXT_MANUAL,
+ 'force_regenerate' => ! empty( $_POST['force_regenerate'] ),
+ )
+ );
+
+ $redirect = admin_url( 'admin.php?page=ton-registrations&action=view&id=' . $id );
+
+ if ( is_wp_error( $result ) ) {
+ wp_safe_redirect( add_query_arg( 'card_error', rawurlencode( $result->get_error_message() ), $redirect ) );
+ exit;
+ }
+
+ wp_safe_redirect( add_query_arg( 'card_generated', '1', $redirect ) );
+ exit;
+ }
}
diff --git a/admin/class-registration-list-table.php b/admin/class-registration-list-table.php
index cebd5df..b007578 100644
--- a/admin/class-registration-list-table.php
+++ b/admin/class-registration-list-table.php
@@ -17,6 +17,41 @@
class TON_Reg_Registration_List_Table extends WP_List_Table {
/**
+ * @var array
+ */
+ private $renewal_statuses = array();
+
+ /**
+ * @var int
+ */
+ private $renewal_year = 0;
+
+ /**
+ * @var string
+ */
+ private $list_status = '';
+
+ /**
+ * @var string
+ */
+ private $list_search = '';
+
+ /**
+ * @var bool
+ */
+ private $list_renewal_unpaid = false;
+
+ /**
+ * @var string
+ */
+ private $list_orderby = 'created_at';
+
+ /**
+ * @var string
+ */
+ private $list_order = 'DESC';
+
+ /**
* Constructor.
*/
public function __construct() {
@@ -39,6 +74,7 @@
'nome' => __( 'Nome', 'ton-italia-registration' ),
'email' => __( 'Email', 'ton-italia-registration' ),
'status' => __( 'Stato', 'ton-italia-registration' ),
+ 'renewal' => __( 'Rinnovo', 'ton-italia-registration' ),
'created_at' => __( 'Data', 'ton-italia-registration' ),
);
}
@@ -48,11 +84,74 @@
* @return string
*/
public function column_id( $item ) {
- $view_url = admin_url( 'admin.php?page=ton-registrations&action=view&id=' . (int) $item->id );
+ return (string) (int) $item->id;
+ }
+
+ /**
+ * @param object $item Item.
+ * @return string
+ */
+ public function column_cognome( $item ) {
+ $view_url = $this->build_detail_url( (int) $item->id );
+ $name = trim( ( $item->cognome ?? '' ) . ' ' . ( $item->nome ?? '' ) );
$actions = array(
'view' => '' . esc_html__( 'Dettaglio', 'ton-italia-registration' ) . '',
);
- return (int) $item->id . $this->row_actions( $actions );
+
+ return '' . esc_html( $name ) . '' . $this->row_actions( $actions );
+ }
+
+ /**
+ * @param int $id Registration ID.
+ * @return string
+ */
+ private function build_detail_url( $id ) {
+ return TON_Reg_Admin::detail_url( $id, $this->get_list_context() );
+ }
+
+ /**
+ * @return array
+ */
+ public function get_list_context() {
+ return array(
+ 'status' => $this->list_status,
+ 'search' => $this->list_search,
+ 'renewal_unpaid' => $this->list_renewal_unpaid,
+ 'renewal_year' => $this->renewal_year,
+ 'orderby' => $this->list_orderby,
+ 'order' => $this->list_order,
+ );
+ }
+
+ /**
+ * @param object $item Item.
+ * @return string
+ */
+ public function column_email( $item ) {
+ $email = $item->email ?? '';
+ if ( ! $email ) {
+ return '';
+ }
+
+ return '' . esc_html( $email ) . '';
+ }
+
+ /**
+ * @param object $item Item.
+ * @return string
+ */
+ public function column_created_at( $item ) {
+ $value = $item->created_at ?? '';
+ if ( ! $value ) {
+ return '';
+ }
+
+ $timestamp = strtotime( $value );
+ if ( ! $timestamp ) {
+ return esc_html( $value );
+ }
+
+ return esc_html( wp_date( 'd/m/Y H:i', $timestamp ) );
}
/**
@@ -65,11 +164,32 @@
'admitted' => __( 'Ammesso', 'ton-italia-registration' ),
'rejected' => __( 'Non ammesso', 'ton-italia-registration' ),
);
- $label = $labels[ $item->status ] ?? $item->status;
+ $status = $item->status ?? '';
+ $label = $labels[ $status ] ?? $status;
+ $class = 'ton-reg-status ton-reg-status--' . sanitize_html_class( $status );
+
if ( $item->anonymized_at ) {
$label .= ' (' . __( 'anonimizzato', 'ton-italia-registration' ) . ')';
+ $class .= ' ton-reg-status--anonymized';
}
- return esc_html( $label );
+
+ return '' . esc_html( $label ) . '';
+ }
+
+ /**
+ * @param object $item Item.
+ * @return string
+ */
+ public function column_renewal( $item ) {
+ if ( 'admitted' !== ( $item->status ?? '' ) || ! empty( $item->anonymized_at ) ) {
+ return '—';
+ }
+
+ $id = (int) $item->id;
+ $status = $this->renewal_statuses[ $id ] ?? TON_Reg_Membership_Renewals::STATUS_PENDING;
+ $badge = TON_Reg_Membership_Renewals::display_badge( $status, $this->renewal_year );
+
+ return '' . esc_html( $badge['label'] ) . '';
}
/**
@@ -103,29 +223,53 @@
* Prepare items.
*/
public function prepare_items() {
- $per_page = 20;
- $status = isset( $_REQUEST['status'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['status'] ) ) : '';
- $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '';
+ $per_page = 20;
+ $status = isset( $_REQUEST['status'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['status'] ) ) : '';
+ $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '';
+ $renewal_unpaid = ! empty( $_REQUEST['renewal_unpaid'] );
+ $this->renewal_year = TON_Reg_Membership_Renewals::current_year();
+ $this->list_status = $status;
+ $this->list_search = $search;
+ $this->list_renewal_unpaid = $renewal_unpaid;
- $total = TON_Reg_Database::count(
- array(
- 'status' => $status,
- 'search' => $search,
- )
+ $orderby = isset( $_REQUEST['orderby'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at';
+ $order = isset( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : 'DESC';
+ $allowed_orderby = array( 'id', 'created_at', 'cognome', 'nome', 'email', 'status' );
+ $this->list_orderby = in_array( $orderby, $allowed_orderby, true ) ? $orderby : 'created_at';
+ $this->list_order = 'ASC' === strtoupper( $order ) ? 'ASC' : 'DESC';
+
+ $query_args = array(
+ 'status' => $status,
+ 'search' => $search,
+ 'renewal_unpaid' => $renewal_unpaid,
+ 'renewal_year' => $this->renewal_year,
+ 'orderby' => $this->list_orderby,
+ 'order' => $this->list_order,
);
+ $total = TON_Reg_Database::count( $query_args );
+
$total_pages = max( 1, (int) ceil( $total / $per_page ) );
$page = max( 1, min( $this->get_pagenum(), $total_pages ) );
$this->items = TON_Reg_Database::query(
- array(
- 'status' => $status,
- 'search' => $search,
- 'limit' => $per_page,
- 'offset' => ( $page - 1 ) * $per_page,
+ array_merge(
+ $query_args,
+ array(
+ 'limit' => $per_page,
+ 'offset' => ( $page - 1 ) * $per_page,
+ )
)
);
+ $ids = array_map(
+ static function ( $item ) {
+ return (int) $item->id;
+ },
+ $this->items
+ );
+ $this->renewal_statuses = TON_Reg_Membership_Renewals::get_statuses_for_registrations( $ids, $this->renewal_year );
+
$this->set_pagination_args(
array(
'total_items' => $total,
diff --git a/assets/css/admin.css b/assets/css/admin.css
index c81a577..fc7bcd3 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -88,3 +88,390 @@
color: #646970;
font-weight: 400;
}
+
+/* —— Elenco iscrizioni (admin) —— */
+
+.ton-reg-status {
+ display: inline-block;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.3;
+ padding: 0.2rem 0.55rem;
+ border-radius: 999px;
+ white-space: nowrap;
+}
+
+.ton-reg-status--pending {
+ background: #fff3cd;
+ color: #664d03;
+}
+
+.ton-reg-status--admitted {
+ background: #d8f3dc;
+ color: #1b4332;
+}
+
+.ton-reg-status--rejected {
+ background: #ffe5e5;
+ color: #9b2226;
+}
+
+.ton-reg-status--anonymized {
+ background: #f0f0f1;
+ color: #50575e;
+}
+
+.ton-reg-admin-list .ton-reg-admin-list__filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem 0.75rem;
+ float: none;
+ margin: 0.75rem 0 1rem;
+}
+
+.ton-reg-admin-list .ton-reg-admin-list__filters li {
+ display: inline;
+ margin: 0;
+}
+
+.ton-reg-admin-list .ton-reg-list-search {
+ clear: both;
+ margin: 0 0 1rem;
+}
+
+.ton-reg-admin-list .ton-reg-admin-list__search {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ float: none;
+ margin: 0;
+ width: 100%;
+}
+
+.ton-reg-admin-list .ton-reg-admin-list__search input[type="search"] {
+ flex: 1 1 12rem;
+ margin: 0;
+ max-width: none;
+ width: auto;
+}
+
+.ton-reg-list-table .wp-list-table .column-cognome .row-title {
+ font-size: 14px;
+}
+
+.ton-reg-list-table .tablenav {
+ margin-top: 0.75rem;
+}
+
+@media (max-width: 782px) {
+ .ton-reg-admin-list h1 {
+ font-size: 1.4rem;
+ }
+
+ .ton-reg-admin-list .ton-reg-admin-list__search {
+ align-items: stretch;
+ }
+
+ .ton-reg-admin-list .ton-reg-admin-list__search input[type="search"],
+ .ton-reg-admin-list .ton-reg-admin-list__search .button {
+ width: 100%;
+ }
+
+ .ton-reg-list-table {
+ margin: 0 -10px;
+ }
+
+ .ton-reg-list-table .wp-list-table {
+ border: 0;
+ }
+
+ .ton-reg-list-table .wp-list-table thead,
+ .ton-reg-list-table .wp-list-table tfoot {
+ display: none;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody {
+ display: block;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr {
+ background: #fff;
+ border: 1px solid #c3c4c7;
+ border-radius: 6px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+ display: flex;
+ flex-direction: column;
+ margin: 0 0 0.75rem;
+ padding: 0.35rem 0.75rem 0.75rem;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr td {
+ border: 0;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ justify-content: space-between;
+ padding: 0.45rem 0;
+ text-align: right;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr td::before {
+ color: #646970;
+ content: attr(data-colname);
+ flex: 0 0 5.5rem;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.4;
+ text-align: left;
+ text-transform: none;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr td.column-cognome {
+ align-items: flex-start;
+ border-bottom: 1px solid #dcdcde;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ justify-content: flex-start;
+ margin-bottom: 0.35rem;
+ order: 1;
+ padding: 0.35rem 0 0.65rem;
+ text-align: left;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr td.column-cognome::before {
+ display: none;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr td.column-cognome .row-title {
+ display: block;
+ font-size: 1rem;
+ line-height: 1.35;
+ text-align: left;
+ width: 100%;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr td.column-cognome .row-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ justify-content: flex-start;
+ margin: 0;
+ padding: 0;
+ position: static;
+ visibility: visible;
+ width: 100%;
+ }
+
+ .ton-reg-list-table .wp-list-table .column-nome {
+ display: none;
+ }
+
+ .ton-reg-list-table .wp-list-table .column-id {
+ font-size: 12px;
+ order: 5;
+ }
+
+ .ton-reg-list-table .wp-list-table .column-status {
+ order: 2;
+ }
+
+ .ton-reg-list-table .wp-list-table .column-renewal {
+ order: 3;
+ }
+
+ .ton-reg-list-table .wp-list-table .column-email {
+ order: 4;
+ word-break: break-word;
+ }
+
+ .ton-reg-list-table .wp-list-table .column-created_at {
+ order: 5;
+ }
+
+ .ton-reg-list-table .wp-list-table .row-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ margin-top: 0.65rem;
+ }
+
+ .ton-reg-list-table .wp-list-table tbody tr td.column-cognome .row-actions {
+ margin-top: 0;
+ }
+
+ .ton-reg-list-table .wp-list-table .row-actions .view a {
+ background: #2271b1;
+ border-radius: 4px;
+ color: #fff;
+ display: inline-block;
+ font-weight: 600;
+ padding: 0.45rem 0.85rem;
+ text-decoration: none;
+ }
+
+ .ton-reg-list-table .wp-list-table .row-actions .view a:hover,
+ .ton-reg-list-table .wp-list-table .row-actions .view a:focus {
+ background: #135e96;
+ color: #fff;
+ }
+
+ .ton-reg-list-table .tablenav .tablenav-pages {
+ float: none;
+ margin: 0.5rem 0 0;
+ text-align: center;
+ width: 100%;
+ }
+
+ .ton-reg-list-table .tablenav .displaying-num {
+ display: block;
+ float: none;
+ margin: 0 0 0.5rem;
+ text-align: center;
+ width: 100%;
+ }
+
+ .ton-reg-admin-detail .form-table,
+ .ton-reg-admin-detail .form-table tbody,
+ .ton-reg-admin-detail .form-table tr,
+ .ton-reg-admin-detail .form-table th,
+ .ton-reg-admin-detail .form-table td {
+ display: block;
+ width: 100%;
+ }
+
+ .ton-reg-admin-detail .form-table th {
+ border-bottom: 0;
+ padding: 0.75rem 0 0.25rem;
+ }
+
+ .ton-reg-admin-detail .form-table td {
+ margin: 0 0 0.75rem;
+ padding: 0 0 0.75rem;
+ }
+
+ .ton-reg-admin-detail .form-table tr {
+ border-bottom: 1px solid #dcdcde;
+ margin-bottom: 0.25rem;
+ }
+
+ .ton-reg-admin-detail .ton-reg-consents th {
+ width: auto;
+ }
+
+ .ton-reg-admin-detail .ton-reg-doc-block__upload {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .ton-reg-admin-detail .ton-reg-doc-block__upload .button {
+ width: 100%;
+ }
+
+ .ton-reg-detail-nav__pager {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .ton-reg-detail-nav__pager .button,
+ .ton-reg-detail-nav__pager .button.disabled {
+ text-align: center;
+ width: 100%;
+ }
+
+ .ton-reg-detail-nav__current {
+ order: -1;
+ text-align: left;
+ }
+}
+
+.ton-reg-admin-list__export {
+ margin: 0.75rem 0 1rem;
+}
+
+.ton-reg-admin-list__export .description {
+ margin-left: 0.5rem;
+}
+
+.ton-reg-detail-nav {
+ margin: 0 0 1.25rem;
+}
+
+.ton-reg-detail-nav__back {
+ margin: 0 0 0.75rem;
+}
+
+.ton-reg-detail-nav__pager {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem 0.75rem;
+}
+
+.ton-reg-detail-nav__current {
+ color: #1d2327;
+ flex: 1 1 12rem;
+ font-weight: 600;
+ min-width: 0;
+ text-align: center;
+}
+
+.ton-reg-detail-nav__pager .button.disabled {
+ color: #a7aaad;
+ cursor: default;
+ opacity: 0.65;
+ pointer-events: none;
+}
+
+.ton-reg-renewal {
+ display: inline-block;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.3;
+ padding: 0.2rem 0.55rem;
+ border-radius: 999px;
+ white-space: nowrap;
+}
+
+.ton-reg-renewal--paid {
+ background: #d8f3dc;
+ color: #1b4332;
+}
+
+.ton-reg-renewal--pending {
+ background: #fff3cd;
+ color: #664d03;
+}
+
+.ton-reg-renewal--reminder {
+ background: #ffe8cc;
+ color: #9a3412;
+}
+
+.ton-reg-renewal--overdue {
+ background: #ffe5e5;
+ color: #9b2226;
+}
+
+.ton-reg-renewal--na {
+ background: transparent;
+ color: #a7aaad;
+ font-weight: 400;
+ padding: 0;
+}
+
+.ton-reg-profile--admin th {
+ width: 220px;
+}
+
+.ton-reg-profile--admin .ton-reg-profile__notice,
+.ton-reg-profile--admin__notice {
+ margin: 0;
+ padding: 0.75rem 1rem;
+ background: #f0f6fc;
+ border-left: 4px solid #72aee6;
+}
+
+.ton-reg-profile--admin .ton-reg-profile__download {
+ font-weight: 600;
+}
diff --git a/assets/css/form.css b/assets/css/form.css
index 4c7954a..10ef2d7 100644
--- a/assets/css/form.css
+++ b/assets/css/form.css
@@ -5,6 +5,26 @@
line-height: 1.5;
}
+.ton-reg-form__body {
+ margin: 0;
+ width: 100%;
+}
+
+.ton-reg-form__alert {
+ padding: 1rem 1.25rem;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
+
+.ton-reg-form__alert--error {
+ background: #ffe5e5;
+ border: 1px solid #f4a4a4;
+}
+
+.ton-reg-form__alert--error p {
+ margin: 0;
+}
+
.ton-reg-form__intro {
font-weight: 600;
margin-bottom: 1.5rem;
@@ -30,6 +50,7 @@
.ton-reg-form__grid--narrow {
max-width: 24rem;
+ grid-template-columns: 1fr;
}
@media (max-width: 600px) {
@@ -40,6 +61,7 @@
.ton-reg-form__field {
margin: 0;
+ min-width: 0;
}
.ton-reg-form__field label {
@@ -149,20 +171,25 @@
background: #1b4332;
}
-.ton-reg-form--success,
-.ton-reg-form--error {
+.ton-reg-form--success {
+ background: #d8f3dc;
+ border: 1px solid #95d5b2;
+ color: #1b4332;
padding: 1rem 1.25rem;
border-radius: 4px;
}
-.ton-reg-form--success {
- background: #d8f3dc;
- border: 1px solid #95d5b2;
+.ton-reg-form--success p {
+ margin: 0;
+ font-size: 1.05rem;
+ line-height: 1.5;
}
.ton-reg-form--error {
background: #ffe5e5;
border: 1px solid #f4a4a4;
+ padding: 1rem 1.25rem;
+ border-radius: 4px;
}
.ton-reg-form .required {
@@ -176,3 +203,65 @@
border-radius: 4px;
margin-bottom: 1rem;
}
+
+.ton-reg-profile {
+ max-width: 640px;
+ margin: 0 auto;
+}
+
+.ton-reg-profile__title {
+ margin-bottom: 1rem;
+}
+
+.ton-reg-profile__summary {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 0.35rem 1.25rem;
+ margin: 0 0 1.5rem;
+}
+
+.ton-reg-profile__summary dt {
+ font-weight: 600;
+ margin: 0;
+}
+
+.ton-reg-profile__summary dd {
+ margin: 0;
+}
+
+.ton-reg-profile__notice {
+ padding: 0.75rem 1rem;
+ background: #f0f4f8;
+ border-radius: 4px;
+}
+
+.ton-reg-profile__card {
+ padding: 1rem 1.25rem;
+ background: #edf7f0;
+ border: 1px solid #b7dfc4;
+ border-radius: 4px;
+ margin-bottom: 1.5rem;
+}
+
+.ton-reg-profile__card h3 {
+ margin: 0 0 0.75rem;
+}
+
+.ton-reg-profile__history {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.ton-reg-profile__history li {
+ padding: 0.35rem 0;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.ton-reg-profile__download {
+ font-weight: 600;
+}
+
+.ton-reg-profile__password {
+ margin-top: 1.5rem;
+}
diff --git a/assets/js/form.js b/assets/js/form.js
index f99dff7..f1a2544 100644
--- a/assets/js/form.js
+++ b/assets/js/form.js
@@ -1,168 +1,329 @@
(function () {
'use strict';
- var form = document.querySelector('.ton-reg-form form');
- if (!form || typeof tonRegForm === 'undefined') {
- return;
- }
-
- var cfRegex = /^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/;
- var dateRegex = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
-
- function showFieldError(fieldWrap, message) {
- var input = fieldWrap.querySelector('.ton-reg-form__input');
- var error = fieldWrap.querySelector('.ton-reg-form__error');
- if (!input || !error) {
+ function initTonRegForm() {
+ if (window.tonRegFormBootstrapped) {
return;
}
- fieldWrap.classList.add('ton-reg-form__field--invalid');
- error.textContent = message;
- error.hidden = false;
- input.setCustomValidity(message);
- }
- function clearFieldError(fieldWrap) {
- var input = fieldWrap.querySelector('.ton-reg-form__input');
- var error = fieldWrap.querySelector('.ton-reg-form__error');
- if (!input || !error) {
+ var form = document.querySelector('.ton-reg-form__body');
+ if (!form || form.getAttribute('data-ton-reg-js-init') === '1') {
return;
}
- fieldWrap.classList.remove('ton-reg-form__field--invalid');
- error.hidden = true;
- error.textContent = '';
- input.setCustomValidity('');
- }
- function clearAllErrors() {
- form.querySelectorAll('.ton-reg-form__field').forEach(clearFieldError);
- form.querySelectorAll('.ton-reg-form__checkbox--invalid').forEach(function (el) {
- el.classList.remove('ton-reg-form__checkbox--invalid');
- });
- }
+ form.setAttribute('data-ton-reg-js-init', '1');
+ window.tonRegFormBootstrapped = true;
- function getFieldWrap(name) {
- return form.querySelector('.ton-reg-form__field[data-field="' + name + '"]');
- }
+ var messages =
+ typeof tonRegForm !== 'undefined'
+ ? tonRegForm
+ : {
+ msgRequired: 'Compila questo campo.',
+ msgEmail: 'Inserisci un indirizzo email valido.',
+ msgCf: 'Codice fiscale non valido (16 caratteri).',
+ msgCap: 'Il CAP deve essere di 5 cifre.',
+ msgProvincia: 'Inserisci la sigla provincia (2 lettere).',
+ msgDate: 'Inserisci la data nel formato gg-mm-aaaa.',
+ msgDateFuture: 'La data di dichiarazione non può essere futura.',
+ msgCheckbox: 'Devi selezionare questa casella per continuare.',
+ msgCaptcha: 'Inserisci la risposta alla verifica anti-spam.',
+ };
- function validateField(name) {
- var wrap = getFieldWrap(name);
- if (!wrap) {
- return true;
- }
- var input = wrap.querySelector('.ton-reg-form__input');
- if (!input) {
- return true;
+ var cfRegex = /^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/;
+ var dateRegex = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
+ var dateFieldNames = {
+ data_nascita: true,
+ data_dichiarazione: true,
+ };
+
+ function isDateField(name) {
+ return !!dateFieldNames[name];
}
- var value = (input.value || '').trim();
-
- if (input.required && !value) {
- showFieldError(wrap, tonRegForm.msgRequired);
- return false;
+ function isDateInput(input) {
+ return input && input.name && isDateField(input.name);
}
- if (!value) {
+ function padDatePart(part) {
+ var num = parseInt(part, 10);
+ if (part.length === 1 && num >= 1 && num <= 9) {
+ return '0' + part;
+ }
+ return part;
+ }
+
+ function normalizeItalianDate(value) {
+ var trimmed = (value || '').trim();
+ if (!trimmed) {
+ return '';
+ }
+
+ var normalized = trimmed
+ .replace(/[./]/g, '-')
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-');
+ var match = normalized.match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
+ if (!match) {
+ return normalized;
+ }
+
+ return padDatePart(match[1]) + '-' + padDatePart(match[2]) + '-' + match[3];
+ }
+
+ function applyDateNormalization(input) {
+ if (!isDateInput(input)) {
+ return;
+ }
+
+ var previous = input.value;
+ var normalized = normalizeItalianDate(previous);
+ input.value = normalized;
+
+ if (normalized !== previous) {
+ input.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ }
+
+ function normalizeProvince(value) {
+ return (value || '').trim().toUpperCase();
+ }
+
+ function normalizeCodiceFiscale(value) {
+ return (value || '').trim().toUpperCase().replace(/\s+/g, '');
+ }
+
+ function parseItalianDate(value) {
+ if (!dateRegex.test(value)) {
+ return null;
+ }
+ var parts = value.split('-');
+ return new Date(parseInt(parts[2], 10), parseInt(parts[1], 10) - 1, parseInt(parts[0], 10));
+ }
+
+ function isFutureDate(value) {
+ var date = parseItalianDate(value);
+ if (!date) {
+ return false;
+ }
+ var today = new Date();
+ today.setHours(0, 0, 0, 0);
+ date.setHours(0, 0, 0, 0);
+ return date > today;
+ }
+
+ function showFieldError(fieldWrap, message) {
+ var input = fieldWrap.querySelector('.ton-reg-form__input');
+ var error = fieldWrap.querySelector('.ton-reg-form__error');
+ if (!input || !error) {
+ return;
+ }
+ fieldWrap.classList.add('ton-reg-form__field--invalid');
+ error.textContent = message;
+ error.hidden = false;
+ input.setCustomValidity(message);
+ }
+
+ function clearFieldError(fieldWrap) {
+ var input = fieldWrap.querySelector('.ton-reg-form__input');
+ var error = fieldWrap.querySelector('.ton-reg-form__error');
+ if (!input || !error) {
+ return;
+ }
+ fieldWrap.classList.remove('ton-reg-form__field--invalid');
+ error.hidden = true;
+ error.textContent = '';
+ input.setCustomValidity('');
+ }
+
+ function clearAllErrors() {
+ form.querySelectorAll('.ton-reg-form__field').forEach(clearFieldError);
+ form.querySelectorAll('.ton-reg-form__checkbox--invalid').forEach(function (el) {
+ el.classList.remove('ton-reg-form__checkbox--invalid');
+ });
+ }
+
+ function getFieldWrap(name) {
+ return form.querySelector('.ton-reg-form__field[data-field="' + name + '"]');
+ }
+
+ function validateField(name) {
+ var wrap = getFieldWrap(name);
+ if (!wrap) {
+ return true;
+ }
+ var input = wrap.querySelector('.ton-reg-form__input');
+ if (!input) {
+ return true;
+ }
+
+ var value = (input.value || '').trim();
+
+ if (isDateField(name)) {
+ value = normalizeItalianDate(value);
+ input.value = value;
+ }
+
+ if ('provincia_nascita' === name || 'provincia_residenza' === name) {
+ value = normalizeProvince(value);
+ input.value = value;
+ }
+
+ if ('codice_fiscale' === name) {
+ value = normalizeCodiceFiscale(value);
+ input.value = value;
+ }
+
+ if (input.required && !value) {
+ showFieldError(wrap, messages.msgRequired);
+ return false;
+ }
+
+ if (!value) {
+ clearFieldError(wrap);
+ return true;
+ }
+
+ if ('email' === input.type && !input.checkValidity()) {
+ showFieldError(wrap, messages.msgEmail);
+ return false;
+ }
+
+ if ('codice_fiscale' === name) {
+ if (!cfRegex.test(value)) {
+ showFieldError(wrap, messages.msgCf);
+ return false;
+ }
+ }
+
+ if ('cap' === name && !/^[0-9]{5}$/.test(value)) {
+ showFieldError(wrap, messages.msgCap);
+ return false;
+ }
+
+ if (('provincia_nascita' === name || 'provincia_residenza' === name) && !/^[A-Za-z]{2}$/.test(value)) {
+ showFieldError(wrap, messages.msgProvincia);
+ return false;
+ }
+
+ if (isDateField(name) && !dateRegex.test(value)) {
+ showFieldError(wrap, messages.msgDate);
+ return false;
+ }
+
+ if ('data_dichiarazione' === name && dateRegex.test(value) && isFutureDate(value)) {
+ showFieldError(wrap, messages.msgDateFuture);
+ return false;
+ }
+
+ if (input.pattern && !isDateField(name) && !new RegExp('^(?:' + input.pattern + ')$').test(value)) {
+ showFieldError(wrap, messages.msgRequired);
+ return false;
+ }
+
clearFieldError(wrap);
return true;
}
- if ('email' === input.type && !input.checkValidity()) {
- showFieldError(wrap, tonRegForm.msgEmail);
- return false;
+ function validateCheckboxes() {
+ var ok = true;
+ form.querySelectorAll('input[type="checkbox"][required]').forEach(function (cb) {
+ var label = cb.closest('.ton-reg-form__checkbox');
+ if (!cb.checked) {
+ if (label) {
+ label.classList.add('ton-reg-form__checkbox--invalid');
+ }
+ ok = false;
+ } else if (label) {
+ label.classList.remove('ton-reg-form__checkbox--invalid');
+ }
+ });
+ return ok;
}
- if ('codice_fiscale' === name) {
- if (!cfRegex.test(value.toUpperCase())) {
- showFieldError(wrap, tonRegForm.msgCf);
+ function validateCaptcha() {
+ var captcha = form.querySelector('#ton_reg_captcha_answer');
+ if (!captcha) {
+ return true;
+ }
+ if (!captcha.value && captcha.required) {
+ captcha.setCustomValidity(messages.msgCaptcha);
return false;
}
- }
-
- if ('cap' === name && !/^[0-9]{5}$/.test(value)) {
- showFieldError(wrap, tonRegForm.msgCap);
- return false;
- }
-
- if (('provincia_nascita' === name || 'provincia_residenza' === name) && !/^[A-Za-z]{2}$/.test(value)) {
- showFieldError(wrap, tonRegForm.msgProvincia);
- return false;
- }
-
- if (('data_nascita' === name || 'data_dichiarazione' === name) && !dateRegex.test(value)) {
- showFieldError(wrap, tonRegForm.msgDate);
- return false;
- }
-
- if (input.pattern && !new RegExp('^(?:' + input.pattern + ')$').test(value)) {
- showFieldError(wrap, tonRegForm.msgRequired);
- return false;
- }
-
- clearFieldError(wrap);
- return true;
- }
-
- function validateCheckboxes() {
- var ok = true;
- form.querySelectorAll('input[type="checkbox"][required]').forEach(function (cb) {
- var label = cb.closest('.ton-reg-form__checkbox');
- if (!cb.checked) {
- if (label) {
- label.classList.add('ton-reg-form__checkbox--invalid');
- }
- ok = false;
- } else if (label) {
- label.classList.remove('ton-reg-form__checkbox--invalid');
- }
- });
- return ok;
- }
-
- function validateCaptcha() {
- var captcha = form.querySelector('#ton_reg_captcha_answer');
- if (!captcha) {
+ captcha.setCustomValidity('');
return true;
}
- if (!captcha.value && captcha.required) {
- captcha.setCustomValidity(tonRegForm.msgCaptcha);
- return false;
- }
- captcha.setCustomValidity('');
- return true;
- }
- form.querySelectorAll('.ton-reg-form__input').forEach(function (input) {
- input.addEventListener('input', function () {
+ function handleInputNormalization(input) {
var wrap = input.closest('.ton-reg-form__field');
if (wrap) {
clearFieldError(wrap);
}
+
if ('codice_fiscale' === input.name && input.value) {
- input.value = input.value.toUpperCase().replace(/\s+/g, '');
+ input.value = normalizeCodiceFiscale(input.value);
}
+
+ if (isDateInput(input) && input.value) {
+ input.value = input.value.replace(/[./]/g, '-');
+ }
+
if (('provincia_nascita' === input.name || 'provincia_residenza' === input.name) && input.value) {
- input.value = input.value.toUpperCase();
+ input.value = normalizeProvince(input.value);
+ }
+ }
+
+ function handleFieldNormalization(input) {
+ if (!input || !input.name) {
+ return;
+ }
+
+ if (isDateInput(input)) {
+ applyDateNormalization(input);
+ }
+
+ if ('provincia_nascita' === input.name || 'provincia_residenza' === input.name) {
+ input.value = normalizeProvince(input.value);
+ }
+
+ if ('codice_fiscale' === input.name) {
+ input.value = normalizeCodiceFiscale(input.value);
+ }
+
+ validateField(input.name);
+ }
+
+ form.addEventListener('input', function (e) {
+ if (e.target && e.target.classList && e.target.classList.contains('ton-reg-form__input')) {
+ handleInputNormalization(e.target);
}
});
- input.addEventListener('blur', function () {
- if (input.name) {
- validateField(input.name);
- }
- });
- });
+ form.addEventListener(
+ 'focusout',
+ function (e) {
+ if (e.target && e.target.classList && e.target.classList.contains('ton-reg-form__input')) {
+ handleFieldNormalization(e.target);
+ }
+ },
+ true
+ );
- var birthInput = form.querySelector('#ton_reg_data_nascita');
- if (birthInput) {
- birthInput.addEventListener('change', function () {
+ function updateMinorBirthWarning() {
+ var birthInput = form.querySelector('#ton_reg_data_nascita');
+ if (!birthInput) {
+ return;
+ }
+
var warning = form.querySelector('.ton-reg-form__warning');
if (warning) {
warning.remove();
}
- var date = birthInput.value;
+
+ var date = normalizeItalianDate(birthInput.value);
if (!date || !dateRegex.test(date)) {
return;
}
+
var parts = date.split('-');
var birth = new Date(parseInt(parts[2], 10), parseInt(parts[1], 10) - 1, parseInt(parts[0], 10));
var today = new Date();
@@ -179,57 +340,79 @@
'Se sei minorenne, la domanda deve essere firmata da entrambi i genitori o dal tutore legale.';
form.insertBefore(div, form.firstChild);
}
+ }
+
+ var birthInput = form.querySelector('#ton_reg_data_nascita');
+ if (birthInput) {
+ birthInput.addEventListener('change', updateMinorBirthWarning);
+ }
+
+ form.addEventListener('submit', function (e) {
+ clearAllErrors();
+
+ var fields = [
+ 'cognome',
+ 'nome',
+ 'luogo_nascita',
+ 'provincia_nascita',
+ 'data_nascita',
+ 'codice_fiscale',
+ 'comune_residenza',
+ 'provincia_residenza',
+ 'indirizzo',
+ 'numero_civico',
+ 'cap',
+ 'telefono',
+ 'email',
+ 'luogo_dichiarazione',
+ 'data_dichiarazione',
+ ];
+
+ var valid = true;
+ fields.forEach(function (name) {
+ if (!validateField(name)) {
+ valid = false;
+ }
+ });
+
+ if (!validateCheckboxes()) {
+ valid = false;
+ }
+
+ if (!validateCaptcha()) {
+ valid = false;
+ }
+
+ if (!valid) {
+ e.preventDefault();
+ var firstInvalid = form.querySelector(
+ '.ton-reg-form__field--invalid .ton-reg-form__input, .ton-reg-form__checkbox--invalid input, :invalid'
+ );
+ if (firstInvalid && firstInvalid.focus) {
+ firstInvalid.focus();
+ }
+ return;
+ }
+
+ if (!form.checkValidity()) {
+ e.preventDefault();
+ form.reportValidity();
+ }
});
}
- form.addEventListener('submit', function (e) {
- clearAllErrors();
+ window.tonRegFormInit = initTonRegForm;
- var fields = [
- 'cognome',
- 'nome',
- 'luogo_nascita',
- 'provincia_nascita',
- 'data_nascita',
- 'codice_fiscale',
- 'comune_residenza',
- 'provincia_residenza',
- 'indirizzo',
- 'numero_civico',
- 'cap',
- 'telefono',
- 'email',
- 'luogo_dichiarazione',
- 'data_dichiarazione',
- ];
-
- var valid = true;
- fields.forEach(function (name) {
- if (!validateField(name)) {
- valid = false;
- }
- });
-
- if (!validateCheckboxes()) {
- valid = false;
+ if (/[?&]ton_reg=success(?:&|$)/.test(window.location.search)) {
+ var successEl = document.getElementById('ton-reg-success');
+ if (successEl) {
+ successEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
+ }
- if (!validateCaptcha()) {
- valid = false;
- }
-
- if (!valid) {
- e.preventDefault();
- var firstInvalid = form.querySelector('.ton-reg-form__field--invalid .ton-reg-form__input, .ton-reg-form__checkbox--invalid input, :invalid');
- if (firstInvalid && firstInvalid.focus) {
- firstInvalid.focus();
- }
- return;
- }
-
- if (!form.checkValidity()) {
- e.preventDefault();
- form.reportValidity();
- }
- });
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initTonRegForm);
+ } else {
+ initTonRegForm();
+ }
})();
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..1b51dc8
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,11 @@
+{
+ "name": "ton-italia/registration",
+ "description": "TON Italia Registration WordPress plugin dependencies",
+ "require": {
+ "php": ">=8.0",
+ "setasign/fpdi-fpdf": "^2.6"
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/includes/class-activator.php b/includes/class-activator.php
index c10aee4..bdfb6a6 100644
--- a/includes/class-activator.php
+++ b/includes/class-activator.php
@@ -17,7 +17,9 @@
*/
public static function activate() {
TON_Reg_Database::create_table();
+ TON_Reg_Membership_Renewals::create_table();
TON_Reg_Documents::install_folders();
+ TON_Reg_Renewal_Reminders::schedule_cron();
$defaults = TON_Reg_Defaults::options();
foreach ( $defaults as $key => $value ) {
diff --git a/includes/class-autoloader.php b/includes/class-autoloader.php
index 54c20fb..fe4ae55 100644
--- a/includes/class-autoloader.php
+++ b/includes/class-autoloader.php
@@ -40,6 +40,12 @@
'TON_Reg_Mailchimp' => 'includes/class-mailchimp.php',
'TON_Reg_Gdpr' => 'includes/class-gdpr.php',
'TON_Reg_Documents' => 'includes/class-documents.php',
+ 'TON_Reg_Members_Export' => 'includes/class-members-export.php',
+ 'TON_Reg_Membership_Renewals' => 'includes/class-membership-renewals.php',
+ 'TON_Reg_Renewal_Reminders' => 'includes/class-renewal-reminders.php',
+ 'TON_Reg_Membership_Card' => 'includes/class-membership-card.php',
+ 'TON_Reg_Membership_Cards' => 'includes/class-membership-cards.php',
+ 'TON_Reg_Profile' => 'includes/class-profile.php',
'TON_Reg_Registration_Form' => 'includes/class-registration-form.php',
'TON_Reg_Registration_Handler' => 'includes/class-registration-handler.php',
'TON_Reg_Admin' => 'admin/class-admin.php',
diff --git a/includes/class-database.php b/includes/class-database.php
index eef39a1..3ffc1d7 100644
--- a/includes/class-database.php
+++ b/includes/class-database.php
@@ -162,6 +162,57 @@
}
/**
+ * @param WP_User|int|null $user WordPress user or user ID.
+ * @return object|null
+ */
+ public static function get_for_wp_user( $user ) {
+ if ( is_numeric( $user ) ) {
+ $user = get_user_by( 'id', (int) $user );
+ }
+
+ if ( ! $user instanceof WP_User ) {
+ return null;
+ }
+
+ $row = self::get_by_user_id( (int) $user->ID );
+ if ( $row ) {
+ return $row;
+ }
+
+ if ( empty( $user->user_email ) ) {
+ return null;
+ }
+
+ global $wpdb;
+
+ return $wpdb->get_row(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE email = %s AND anonymized_at IS NULL ORDER BY id DESC LIMIT 1',
+ $user->user_email
+ )
+ );
+ }
+
+ /**
+ * @param int $user_id WordPress user ID.
+ * @return object|null
+ */
+ public static function get_by_user_id( $user_id ) {
+ global $wpdb;
+
+ if ( ! $user_id ) {
+ return null;
+ }
+
+ return $wpdb->get_row(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE user_id = %d AND anonymized_at IS NULL ORDER BY id DESC LIMIT 1',
+ $user_id
+ )
+ );
+ }
+
+ /**
* @param array $args Query args.
* @return array
*/
@@ -169,18 +220,25 @@
global $wpdb;
$defaults = array(
- 'status' => '',
- 'search' => '',
- 'limit' => 20,
- 'offset' => 0,
- 'orderby' => 'created_at',
- 'order' => 'DESC',
+ 'status' => '',
+ 'search' => '',
+ 'exclude_anonymized' => false,
+ 'renewal_unpaid' => false,
+ 'renewal_year' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'orderby' => 'created_at',
+ 'order' => 'DESC',
);
$args = wp_parse_args( $args, $defaults );
$where = array( '1=1' );
$params = array();
+ if ( ! empty( $args['exclude_anonymized'] ) ) {
+ $where[] = 'anonymized_at IS NULL';
+ }
+
if ( ! empty( $args['status'] ) ) {
$where[] = 'status = %s';
$params[] = $args['status'];
@@ -192,7 +250,21 @@
$params = array_merge( $params, array( $like, $like, $like, $like ) );
}
- $allowed_orderby = array( 'created_at', 'cognome', 'nome', 'email', 'status' );
+ if ( ! empty( $args['renewal_unpaid'] ) ) {
+ $renewal_year = ! empty( $args['renewal_year'] ) ? (int) $args['renewal_year'] : TON_Reg_Membership_Renewals::current_year();
+ $ren_table = TON_Reg_Membership_Renewals::table_name();
+ $reg_table = self::table_name();
+ $where[] = "status = 'admitted'";
+ $where[] = 'anonymized_at IS NULL';
+ $where[] = "NOT EXISTS (
+ SELECT 1 FROM {$ren_table} ren
+ WHERE ren.registration_id = {$reg_table}.id
+ AND ren.year = {$renewal_year}
+ AND ren.status = '" . esc_sql( TON_Reg_Membership_Renewals::STATUS_PAID ) . "'
+ )";
+ }
+
+ $allowed_orderby = array( 'id', 'created_at', 'cognome', 'nome', 'email', 'status' );
$orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at';
$order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
@@ -216,9 +288,22 @@
public static function count( $args = array() ) {
global $wpdb;
+ $defaults = array(
+ 'status' => '',
+ 'search' => '',
+ 'exclude_anonymized' => false,
+ 'renewal_unpaid' => false,
+ 'renewal_year' => 0,
+ );
+ $args = wp_parse_args( $args, $defaults );
+
$where = array( '1=1' );
$params = array();
+ if ( ! empty( $args['exclude_anonymized'] ) ) {
+ $where[] = 'anonymized_at IS NULL';
+ }
+
if ( ! empty( $args['status'] ) ) {
$where[] = 'status = %s';
$params[] = $args['status'];
@@ -230,6 +315,20 @@
$params = array_merge( $params, array( $like, $like, $like, $like ) );
}
+ if ( ! empty( $args['renewal_unpaid'] ) ) {
+ $renewal_year = ! empty( $args['renewal_year'] ) ? (int) $args['renewal_year'] : TON_Reg_Membership_Renewals::current_year();
+ $ren_table = TON_Reg_Membership_Renewals::table_name();
+ $reg_table = self::table_name();
+ $where[] = "status = 'admitted'";
+ $where[] = 'anonymized_at IS NULL';
+ $where[] = "NOT EXISTS (
+ SELECT 1 FROM {$ren_table} ren
+ WHERE ren.registration_id = {$reg_table}.id
+ AND ren.year = {$renewal_year}
+ AND ren.status = '" . esc_sql( TON_Reg_Membership_Renewals::STATUS_PAID ) . "'
+ )";
+ }
+
$sql = 'SELECT COUNT(*) FROM ' . self::table_name() . ' WHERE ' . implode( ' AND ', $where );
if ( ! empty( $params ) ) {
@@ -238,4 +337,150 @@
return (int) $wpdb->get_var( $sql );
}
+
+ /**
+ * Query registrations for CSV export (no pagination by default).
+ *
+ * @param array $args Query args.
+ * @return array
+ */
+ public static function query_for_export( $args = array() ) {
+ global $wpdb;
+
+ $defaults = array(
+ 'status' => 'admitted',
+ 'search' => '',
+ 'exclude_anonymized' => true,
+ 'limit' => 0,
+ 'orderby' => 'cognome',
+ 'order' => 'ASC',
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ $where = array( '1=1' );
+ $params = array();
+
+ if ( ! empty( $args['exclude_anonymized'] ) ) {
+ $where[] = 'anonymized_at IS NULL';
+ }
+
+ if ( ! empty( $args['status'] ) ) {
+ $where[] = 'status = %s';
+ $params[] = $args['status'];
+ }
+
+ if ( ! empty( $args['search'] ) ) {
+ $like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
+ $where[] = '( cognome LIKE %s OR nome LIKE %s OR email LIKE %s OR codice_fiscale LIKE %s )';
+ $params = array_merge( $params, array( $like, $like, $like, $like ) );
+ }
+
+ $allowed_orderby = array( 'id', 'created_at', 'cognome', 'nome', 'email', 'status', 'libro_associati_date' );
+ $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'cognome';
+ $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
+
+ $sql = 'SELECT * FROM ' . self::table_name() . ' WHERE ' . implode( ' AND ', $where )
+ . " ORDER BY {$orderby} {$order}, id ASC";
+
+ if ( ! empty( $args['limit'] ) ) {
+ $sql .= ' LIMIT %d';
+ $params[] = (int) $args['limit'];
+ }
+
+ if ( ! empty( $params ) ) {
+ $sql = $wpdb->prepare( $sql, $params );
+ }
+
+ return $wpdb->get_results( $sql );
+ }
+
+ /**
+ * Ordered registration IDs for admin prev/next navigation (same filters as query()).
+ *
+ * @param array $args Query args.
+ * @return array
+ */
+ public static function query_ids( $args = array() ) {
+ global $wpdb;
+
+ $defaults = array(
+ 'status' => '',
+ 'search' => '',
+ 'exclude_anonymized' => false,
+ 'renewal_unpaid' => false,
+ 'renewal_year' => 0,
+ 'orderby' => 'created_at',
+ 'order' => 'DESC',
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ $where = array( '1=1' );
+ $params = array();
+
+ if ( ! empty( $args['exclude_anonymized'] ) ) {
+ $where[] = 'anonymized_at IS NULL';
+ }
+
+ if ( ! empty( $args['status'] ) ) {
+ $where[] = 'status = %s';
+ $params[] = $args['status'];
+ }
+
+ if ( ! empty( $args['search'] ) ) {
+ $like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
+ $where[] = '( cognome LIKE %s OR nome LIKE %s OR email LIKE %s OR codice_fiscale LIKE %s )';
+ $params = array_merge( $params, array( $like, $like, $like, $like ) );
+ }
+
+ if ( ! empty( $args['renewal_unpaid'] ) ) {
+ $renewal_year = ! empty( $args['renewal_year'] ) ? (int) $args['renewal_year'] : TON_Reg_Membership_Renewals::current_year();
+ $ren_table = TON_Reg_Membership_Renewals::table_name();
+ $reg_table = self::table_name();
+ $where[] = "status = 'admitted'";
+ $where[] = 'anonymized_at IS NULL';
+ $where[] = "NOT EXISTS (
+ SELECT 1 FROM {$ren_table} ren
+ WHERE ren.registration_id = {$reg_table}.id
+ AND ren.year = {$renewal_year}
+ AND ren.status = '" . esc_sql( TON_Reg_Membership_Renewals::STATUS_PAID ) . "'
+ )";
+ }
+
+ $allowed_orderby = array( 'id', 'created_at', 'cognome', 'nome', 'email', 'status' );
+ $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at';
+ $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
+
+ $sql = 'SELECT id FROM ' . self::table_name() . ' WHERE ' . implode( ' AND ', $where )
+ . " ORDER BY {$orderby} {$order}, id ASC";
+
+ if ( ! empty( $params ) ) {
+ $sql = $wpdb->prepare( $sql, $params );
+ }
+
+ $ids = $wpdb->get_col( $sql );
+ return array_map( 'intval', is_array( $ids ) ? $ids : array() );
+ }
+
+ /**
+ * @param int $current_id Current registration ID.
+ * @param array $args Same filters/order as list view.
+ * @return array{prev:int|null,next:int|null}
+ */
+ public static function get_adjacent_ids( $current_id, $args = array() ) {
+ $current_id = (int) $current_id;
+ $ids = self::query_ids( $args );
+ $index = array_search( $current_id, $ids, true );
+
+ if ( false === $index ) {
+ return array(
+ 'prev' => null,
+ 'next' => null,
+ );
+ }
+
+ return array(
+ 'prev' => $index > 0 ? (int) $ids[ $index - 1 ] : null,
+ 'next' => $index < count( $ids ) - 1 ? (int) $ids[ $index + 1 ] : null,
+ );
+ }
}
diff --git a/includes/class-deactivator.php b/includes/class-deactivator.php
index cf1a6d4..a269b65 100644
--- a/includes/class-deactivator.php
+++ b/includes/class-deactivator.php
@@ -16,6 +16,7 @@
* Deactivate plugin.
*/
public static function deactivate() {
+ TON_Reg_Renewal_Reminders::unschedule_cron();
flush_rewrite_rules();
}
}
diff --git a/includes/class-defaults.php b/includes/class-defaults.php
index e23693a..c6bcac4 100644
--- a/includes/class-defaults.php
+++ b/includes/class-defaults.php
@@ -26,13 +26,21 @@
'ton_reg_privacy_notice' => self::privacy_notice(),
'ton_reg_newsletter_label' => self::newsletter_label(),
'ton_reg_minor_notice' => self::minor_notice(),
- 'ton_reg_success_message' => 'Grazie. La tua domanda di iscrizione è stata ricevuta. Riceverai un\'email di conferma con i prossimi passaggi.
',
+ 'ton_reg_success_message' => 'Grazie per esserti associato a TON Italia, riceverai una email di conferma.
',
+ 'ton_reg_success_page_id' => 0,
'ton_reg_admin_email' => get_option( 'admin_email' ),
'ton_reg_gdpr_contact' => 'qeshet.tonitalia@gmail.com',
'ton_reg_email_admin_subject' => '[TON ITALIA ODV] Nuova domanda di iscrizione socio',
'ton_reg_email_admin_body' => self::email_admin_body(),
'ton_reg_email_user_subject' => 'Conferma iscrizione TON ITALIA ODV',
'ton_reg_email_user_body' => self::email_user_body(),
+ 'ton_reg_email_renewal_first_subject' => 'TON ITALIA ODV — Rinnovo quota associativa {anno}',
+ 'ton_reg_email_renewal_first_body' => self::email_renewal_first_body(),
+ 'ton_reg_email_renewal_second_subject' => 'TON ITALIA ODV — Sollecito rinnovo quota associativa {anno}',
+ 'ton_reg_email_renewal_second_body' => self::email_renewal_second_body(),
+ 'ton_reg_email_renewal_admin_subject' => '[TON ITALIA ODV] Soci con quota {anno} da pagare ({numero_soci})',
+ 'ton_reg_email_renewal_admin_body' => self::email_renewal_admin_body(),
+ 'ton_reg_renewal_reminders_start_year' => '2027',
'ton_reg_captcha_enabled' => '1',
'ton_reg_rate_limit_max' => '5',
'ton_reg_rate_limit_window' => '900',
@@ -120,4 +128,38 @@
. "Ai sensi del GDPR puoi richiedere in qualsiasi momento la cancellazione o l'anonimizzazione dei tuoi dati scrivendo a {gdpr_contact}.\n\n"
. "Cordiali saluti,\nTON ITALIA ODV";
}
+
+ /**
+ * @return string
+ */
+ private static function email_renewal_first_body() {
+ return "Gentile {nome} {cognome},\n\n"
+ . "ti ricordiamo di effettuare il bonifico per il rinnovo della quota associativa dell'anno {anno}.\n\n"
+ . "{payment_instructions}\n\n"
+ . "Dopo il versamento, il Direttivo annoterà il pagamento nel Libro degli associati.\n\n"
+ . "Il tuo profilo socio: {profile_url}\n\n"
+ . "Cordiali saluti,\nTON ITALIA ODV";
+ }
+
+ /**
+ * @return string
+ */
+ private static function email_renewal_second_body() {
+ return "Gentile {nome} {cognome},\n\n"
+ . "non risulta ancora registrato il pagamento della quota associativa per l'anno {anno}.\n\n"
+ . "Ti preghiamo di provvedere al più presto secondo le istruzioni seguenti:\n\n"
+ . "{payment_instructions}\n\n"
+ . "Per qualsiasi chiarimento puoi scrivere a {gdpr_contact}.\n\n"
+ . "Cordiali saluti,\nTON ITALIA ODV";
+ }
+
+ /**
+ * @return string
+ */
+ private static function email_renewal_admin_body() {
+ return "Promemoria rinnovo quote associative per l'anno {anno}.\n\n"
+ . "Soci ammessi senza quota pagata ({numero_soci}):\n\n"
+ . "{lista_soci}\n\n"
+ . "Gestisci le iscrizioni: {admin_url}";
+ }
}
diff --git a/includes/class-documents.php b/includes/class-documents.php
index dfc8496..fc0d81a 100644
--- a/includes/class-documents.php
+++ b/includes/class-documents.php
@@ -1,6 +1,6 @@
*/
public static function get_type_config() {
@@ -32,15 +39,10 @@
'label' => __( 'Documento di identità', 'ton-italia-registration' ),
'title' => __( 'Documento identità', 'ton-italia-registration' ),
),
- self::TYPE_BONIFICO => array(
- 'column' => 'doc_bonifico_id',
- 'label' => __( 'Ricevuta bonifico', 'ton-italia-registration' ),
- 'title' => __( 'Ricevuta bonifico', 'ton-italia-registration' ),
- ),
self::TYPE_PAGAMENTO => array(
'column' => 'doc_pagamento_id',
- 'label' => __( 'Ricevuta pagamento', 'ton-italia-registration' ),
- 'title' => __( 'Ricevuta pagamento', 'ton-italia-registration' ),
+ 'label' => __( 'Ricevuta versamento tessera', 'ton-italia-registration' ),
+ 'title' => __( 'Ricevuta versamento tessera', 'ton-italia-registration' ),
),
);
}
@@ -134,6 +136,65 @@
}
/**
+ * @param object $row Registration row.
+ * @param string $subfolder Subfolder name (e.g. tessere).
+ */
+ public static function ensure_subfolder( $row, $subfolder ) {
+ $path = trailingslashit( self::get_member_path( $row ) ) . sanitize_file_name( $subfolder );
+ wp_mkdir_p( $path );
+ self::write_index_file( $path );
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @param string $file_path Source file path.
+ * @param string $filename Target filename.
+ * @param string $title Attachment title.
+ * @param string $subfolder Optional subfolder.
+ * @param array $meta Post meta.
+ * @return int|WP_Error Attachment ID.
+ */
+ public static function sideload_file( $row, $file_path, $filename, $title, $subfolder = '', $meta = array() ) {
+ if ( ! is_readable( $file_path ) ) {
+ return new WP_Error( 'no_file', __( 'File sorgente non leggibile.', 'ton-italia-registration' ) );
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+
+ self::ensure_member_folder( $row );
+ if ( $subfolder ) {
+ self::ensure_subfolder( $row, $subfolder );
+ }
+
+ $file_array = array(
+ 'name' => sanitize_file_name( $filename ),
+ 'tmp_name' => $file_path,
+ );
+
+ self::$upload_member_slug = self::member_folder_slug( $row );
+ self::$upload_subfolder = $subfolder ? sanitize_file_name( $subfolder ) : null;
+ add_filter( 'upload_dir', array( __CLASS__, 'filter_upload_dir' ) );
+
+ $attachment_id = media_handle_sideload( $file_array, 0, $title );
+
+ remove_filter( 'upload_dir', array( __CLASS__, 'filter_upload_dir' ) );
+ self::$upload_member_slug = null;
+ self::$upload_subfolder = null;
+
+ if ( is_wp_error( $attachment_id ) ) {
+ return $attachment_id;
+ }
+
+ foreach ( $meta as $key => $value ) {
+ update_post_meta( $attachment_id, $key, $value );
+ }
+
+ return (int) $attachment_id;
+ }
+
+ /**
* @param int $registration_id Registration ID.
* @param string $type Document type.
* @param array $file $_FILES item.
@@ -253,6 +314,8 @@
self::delete_document( $registration_id, $type );
}
+ self::delete_legacy_bonifico_document( $registration_id );
+
if ( $remove_folder ) {
$path = self::get_member_path( $row );
if ( is_dir( $path ) ) {
@@ -262,6 +325,25 @@
}
/**
+ * Remove legacy «Ricevuta bonifico» attachment (upload type removed in 1.6.3).
+ *
+ * @param int $registration_id Registration ID.
+ */
+ public static function delete_legacy_bonifico_document( $registration_id ) {
+ $row = TON_Reg_Database::get( $registration_id );
+ if ( ! $row || empty( $row->doc_bonifico_id ) ) {
+ return;
+ }
+
+ $attachment_id = (int) $row->doc_bonifico_id;
+ if ( $attachment_id ) {
+ wp_delete_attachment( $attachment_id, true );
+ }
+
+ TON_Reg_Database::update( $registration_id, array( self::LEGACY_COLUMN_BONIFICO => null ) );
+ }
+
+ /**
* @param array $dirs Upload dirs.
* @return array
*/
@@ -271,6 +353,9 @@
}
$subdir = '/' . self::BASE_FOLDER . '/' . self::$upload_member_slug;
+ if ( self::$upload_subfolder ) {
+ $subdir .= '/' . self::$upload_subfolder;
+ }
$dirs['subdir'] = $subdir;
$dirs['path'] = $dirs['basedir'] . $subdir;
$dirs['url'] = $dirs['baseurl'] . $subdir;
diff --git a/includes/class-mailer.php b/includes/class-mailer.php
index 82483c3..5cc3f33 100644
--- a/includes/class-mailer.php
+++ b/includes/class-mailer.php
@@ -50,6 +50,75 @@
}
/**
+ * @param object $registration Registration row.
+ * @param int $year Target calendar year.
+ * @param string $reminder_type first|last.
+ */
+ public static function send_renewal_reminder_to_member( $registration, $year, $reminder_type ) {
+ $to = $registration->email ?? '';
+ if ( ! is_email( $to ) ) {
+ return;
+ }
+
+ $subject_key = 'first' === $reminder_type ? 'ton_reg_email_renewal_first_subject' : 'ton_reg_email_renewal_second_subject';
+ $body_key = 'first' === $reminder_type ? 'ton_reg_email_renewal_first_body' : 'ton_reg_email_renewal_second_body';
+ $defaults = TON_Reg_Defaults::options();
+
+ $subject = get_option( $subject_key, $defaults[ $subject_key ] ?? '' );
+ $body = get_option( $body_key, $defaults[ $body_key ] ?? '' );
+
+ $replacements = self::renewal_replacements( $registration, (int) $year );
+ $subject = strtr( $subject, $replacements );
+ $body = strtr( $body, $replacements );
+
+ wp_mail( $to, $subject, $body );
+ }
+
+ /**
+ * @param array $members Registration rows.
+ * @param int $year Target calendar year.
+ * @param string $reminder_type first|last.
+ */
+ public static function send_renewal_admin_digest( $members, $year, $reminder_type ) {
+ if ( empty( $members ) ) {
+ return;
+ }
+
+ $to = get_option( 'ton_reg_admin_email', get_option( 'admin_email' ) );
+ if ( ! is_email( $to ) ) {
+ return;
+ }
+
+ $defaults = TON_Reg_Defaults::options();
+ $subject = get_option( 'ton_reg_email_renewal_admin_subject', $defaults['ton_reg_email_renewal_admin_subject'] ?? '' );
+ $body = get_option( 'ton_reg_email_renewal_admin_body', $defaults['ton_reg_email_renewal_admin_body'] ?? '' );
+
+ $lines = array();
+ foreach ( $members as $member ) {
+ $lines[] = sprintf(
+ '- %s %s <%s> (ID %d)',
+ $member->nome ?? '',
+ $member->cognome ?? '',
+ $member->email ?? '',
+ (int) $member->id
+ );
+ }
+
+ $replacements = array(
+ '{anno}' => (string) (int) $year,
+ '{lista_soci}' => implode( "\n", $lines ),
+ '{numero_soci}' => (string) count( $members ),
+ '{quota}' => (string) get_option( 'ton_reg_membership_fee', '10,00' ),
+ '{admin_url}' => admin_url( 'admin.php?page=ton-registrations&status=admitted&renewal_unpaid=1' ),
+ );
+
+ $subject = strtr( $subject, $replacements );
+ $body = strtr( $body, $replacements );
+
+ wp_mail( $to, $subject, $body );
+ }
+
+ /**
* @param object $registration Registration.
* @param int $user_id User ID.
* @return array
@@ -64,8 +133,43 @@
'{codice_fiscale}' => $registration->codice_fiscale,
'{ip}' => $registration->registration_ip,
'{login_url}' => wp_lostpassword_url(),
+ '{profile_url}' => TON_Reg_Profile::profile_url(),
'{gdpr_contact}' => get_option( 'ton_reg_gdpr_contact', '' ),
'{admin_url}' => $admin_url,
);
}
+
+ /**
+ * @param object $registration Registration.
+ * @param int $year Calendar year for payment.
+ * @return array
+ */
+ private static function renewal_replacements( $registration, $year ) {
+ $admin_url = admin_url( 'admin.php?page=ton-registrations&action=view&id=' . (int) $registration->id );
+ $payment = get_option( 'ton_reg_payment_instructions', '' );
+
+ $payment_replacements = array(
+ '{quota}' => (string) get_option( 'ton_reg_membership_fee', '10,00' ),
+ '{anno}' => (string) $year,
+ '{beneficiario}' => (string) get_option( 'ton_reg_beneficiary', '' ),
+ '{iban}' => (string) get_option( 'ton_reg_iban', '' ),
+ '{nome}' => (string) ( $registration->nome ?? '' ),
+ '{cognome}' => (string) ( $registration->cognome ?? '' ),
+ );
+ $payment_instructions = strtr( $payment, $payment_replacements );
+
+ return array_merge(
+ $payment_replacements,
+ array(
+ '{email}' => (string) ( $registration->email ?? '' ),
+ '{codice_fiscale}' => (string) ( $registration->codice_fiscale ?? '' ),
+ '{ip}' => (string) ( $registration->registration_ip ?? '' ),
+ '{login_url}' => wp_lostpassword_url(),
+ '{profile_url}' => TON_Reg_Profile::profile_url(),
+ '{gdpr_contact}' => (string) get_option( 'ton_reg_gdpr_contact', '' ),
+ '{admin_url}' => $admin_url,
+ '{payment_instructions}' => $payment_instructions,
+ )
+ );
+ }
}
diff --git a/includes/class-members-export.php b/includes/class-members-export.php
new file mode 100644
index 0000000..91747d5
--- /dev/null
+++ b/includes/class-members-export.php
@@ -0,0 +1,122 @@
+
+ */
+ public static function get_columns() {
+ return array(
+ __( 'N.', 'ton-italia-registration' ),
+ __( 'Cognome', 'ton-italia-registration' ),
+ __( 'Nome', 'ton-italia-registration' ),
+ __( 'Data di nascita', 'ton-italia-registration' ),
+ __( 'Luogo di nascita', 'ton-italia-registration' ),
+ __( 'Provincia nascita', 'ton-italia-registration' ),
+ __( 'Codice fiscale', 'ton-italia-registration' ),
+ __( 'Indirizzo', 'ton-italia-registration' ),
+ __( 'CAP', 'ton-italia-registration' ),
+ __( 'Comune residenza', 'ton-italia-registration' ),
+ __( 'Provincia residenza', 'ton-italia-registration' ),
+ __( 'Telefono', 'ton-italia-registration' ),
+ __( 'Email', 'ton-italia-registration' ),
+ __( 'Data iscrizione', 'ton-italia-registration' ),
+ __( 'Data Libro Associati', 'ton-italia-registration' ),
+ );
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @param int $index 1-based row number.
+ * @return array
+ */
+ public static function row_to_csv( $row, $index ) {
+ $address = trim( ( $row->indirizzo ?? '' ) . ' ' . ( $row->numero_civico ?? '' ) );
+
+ return array(
+ (string) $index,
+ (string) ( $row->cognome ?? '' ),
+ (string) ( $row->nome ?? '' ),
+ self::format_date_it( $row->data_nascita ?? '' ),
+ (string) ( $row->luogo_nascita ?? '' ),
+ (string) ( $row->provincia_nascita ?? '' ),
+ strtoupper( (string) ( $row->codice_fiscale ?? '' ) ),
+ $address,
+ (string) ( $row->cap ?? '' ),
+ (string) ( $row->comune_residenza ?? '' ),
+ (string) ( $row->provincia_residenza ?? '' ),
+ (string) ( $row->telefono ?? '' ),
+ (string) ( $row->email ?? '' ),
+ self::format_date_it( $row->created_at ?? '' ),
+ self::format_date_it( $row->libro_associati_date ?? '' ),
+ );
+ }
+
+ /**
+ * Stream Libro Soci CSV to output.
+ */
+ public static function stream_libro_soci_csv() {
+ $rows = TON_Reg_Database::query_for_export(
+ array(
+ 'status' => 'admitted',
+ 'exclude_anonymized' => true,
+ 'orderby' => 'cognome',
+ 'order' => 'ASC',
+ )
+ );
+
+ $filename = 'libro-soci-ton-italia-' . gmdate( 'Y-m-d' ) . '.csv';
+
+ nocache_headers();
+ header( 'Content-Type: text/csv; charset=utf-8' );
+ header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
+
+ $out = fopen( 'php://output', 'w' );
+ if ( false === $out ) {
+ wp_die( esc_html__( 'Impossibile generare il file CSV.', 'ton-italia-registration' ) );
+ }
+
+ // UTF-8 BOM for Excel (IT).
+ fprintf( $out, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) );
+
+ fputcsv( $out, self::get_columns(), self::CSV_DELIMITER );
+
+ $index = 1;
+ foreach ( $rows as $row ) {
+ fputcsv( $out, self::row_to_csv( $row, $index ), self::CSV_DELIMITER );
+ ++$index;
+ }
+
+ fclose( $out );
+ exit;
+ }
+
+ /**
+ * @param string|null $value Date or datetime.
+ * @return string
+ */
+ private static function format_date_it( $value ) {
+ if ( empty( $value ) ) {
+ return '';
+ }
+
+ $timestamp = strtotime( (string) $value );
+ if ( false === $timestamp ) {
+ return '';
+ }
+
+ return wp_date( 'd/m/Y', $timestamp );
+ }
+}
diff --git a/includes/class-membership-card.php b/includes/class-membership-card.php
new file mode 100644
index 0000000..2189700
--- /dev/null
+++ b/includes/class-membership-card.php
@@ -0,0 +1,606 @@
+ 2100 ) {
+ $year = (int) gmdate( 'Y' );
+ }
+ return $year;
+ }
+
+ /**
+ * @param int $year Year.
+ * @return void
+ */
+ public static function set_active_year( $year ) {
+ update_option( self::OPTION_ACTIVE_YEAR, (int) $year );
+ }
+
+ /**
+ * @return array>
+ */
+ public static function get_all_templates() {
+ $templates = get_option( self::OPTION_TEMPLATES, array() );
+ return is_array( $templates ) ? $templates : array();
+ }
+
+ /**
+ * @param int $year Year.
+ * @return array
+ */
+ public static function get_template_config( $year ) {
+ $templates = self::get_all_templates();
+ $config = $templates[ (int) $year ] ?? array();
+ return wp_parse_args( $config, self::default_layout() );
+ }
+
+ /**
+ * @return array
+ */
+ public static function default_layout() {
+ return array(
+ 'template_attachment_id' => 0,
+ 'name_x' => 42.5,
+ 'name_y' => 118.0,
+ 'font_family' => 'Helvetica',
+ 'font_style' => '',
+ 'font_size' => 14,
+ 'text_color' => '#000000',
+ 'uppercase' => true,
+ 'align' => 'C',
+ 'cell_width' => 80,
+ );
+ }
+
+ /**
+ * @param int $year Year.
+ * @param array $config Config.
+ */
+ public static function save_template_config( $year, $config ) {
+ $year = (int) $year;
+ $templates = self::get_all_templates();
+ $current = $templates[ $year ] ?? array();
+
+ $templates[ $year ] = array_merge( self::default_layout(), $current, $config );
+ update_option( self::OPTION_TEMPLATES, $templates );
+ }
+
+ /**
+ * Upload a PDF card template to the media library.
+ *
+ * Uses wp_handle_upload + wp_insert_attachment without image metadata generation,
+ * which avoids fatal errors from Imagick/GD on some hosts when processing PDFs.
+ *
+ * @param array|null $file Uploaded file array from $_FILES.
+ * @param int $year Template year.
+ * @return int|WP_Error Attachment ID.
+ */
+ public static function upload_template_pdf( $file, $year ) {
+ if ( ! self::current_user_can_manage_cards() ) {
+ return new WP_Error( 'forbidden', __( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ if ( ! is_array( $file ) || empty( $file['tmp_name'] ) ) {
+ if ( is_array( $file ) && isset( $file['error'] ) && UPLOAD_ERR_NO_FILE !== (int) $file['error'] ) {
+ return new WP_Error( 'upload', self::upload_error_message( (int) $file['error'] ) );
+ }
+ return new WP_Error( 'no_file', __( 'Nessun file selezionato.', 'ton-italia-registration' ) );
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+
+ $check = wp_check_filetype( $file['name'], array( 'pdf' => 'application/pdf' ) );
+ if ( empty( $check['ext'] ) || 'pdf' !== $check['ext'] ) {
+ return new WP_Error( 'mime', __( 'Il template deve essere un file PDF.', 'ton-italia-registration' ) );
+ }
+
+ $upload = wp_handle_upload(
+ $file,
+ array(
+ 'test_form' => false,
+ 'mimes' => array(
+ 'pdf' => 'application/pdf',
+ ),
+ )
+ );
+
+ if ( ! empty( $upload['error'] ) ) {
+ return new WP_Error( 'upload', $upload['error'] );
+ }
+
+ $attachment_id = wp_insert_attachment(
+ array(
+ 'post_mime_type' => $upload['type'],
+ 'post_title' => sprintf(
+ /* translators: %d: year */
+ __( 'Template tessera %d', 'ton-italia-registration' ),
+ (int) $year
+ ),
+ 'post_content' => '',
+ 'post_status' => 'inherit',
+ 'guid' => $upload['url'],
+ ),
+ $upload['file']
+ );
+
+ if ( is_wp_error( $attachment_id ) ) {
+ wp_delete_file( $upload['file'] );
+ return $attachment_id;
+ }
+
+ wp_update_attachment_metadata( $attachment_id, array() );
+
+ return (int) $attachment_id;
+ }
+
+ /**
+ * @param int $code PHP upload error code.
+ * @return string
+ */
+ private static function upload_error_message( $code ) {
+ $messages = array(
+ UPLOAD_ERR_INI_SIZE => __( 'Il file supera il limite di upload del server.', 'ton-italia-registration' ),
+ UPLOAD_ERR_FORM_SIZE => __( 'Il file supera il limite consentito.', 'ton-italia-registration' ),
+ UPLOAD_ERR_PARTIAL => __( 'Upload incompleto. Riprova.', 'ton-italia-registration' ),
+ UPLOAD_ERR_NO_FILE => __( 'Nessun file selezionato.', 'ton-italia-registration' ),
+ UPLOAD_ERR_NO_TMP_DIR => __( 'Cartella temporanea mancante sul server.', 'ton-italia-registration' ),
+ UPLOAD_ERR_CANT_WRITE => __( 'Impossibile scrivere il file sul server.', 'ton-italia-registration' ),
+ UPLOAD_ERR_EXTENSION => __( 'Upload bloccato da un\'estensione PHP.', 'ton-italia-registration' ),
+ );
+
+ return $messages[ $code ] ?? __( 'Errore nel caricamento del template.', 'ton-italia-registration' );
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param int $year Year.
+ * @param array $args Args.
+ * @return int|WP_Error Card ID.
+ */
+ public static function generate_for_registration( $registration_id, $year, $args = array() ) {
+ $defaults = array(
+ 'context' => self::CONTEXT_MANUAL,
+ 'force_regenerate' => false,
+ 'created_by' => get_current_user_id(),
+ 'require_admin' => true,
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ if ( $args['require_admin'] && ! self::current_user_can_manage_cards() ) {
+ return new WP_Error( 'forbidden', __( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ $row = TON_Reg_Database::get( $registration_id );
+ if ( ! $row ) {
+ return new WP_Error( 'not_found', __( 'Iscrizione non trovata.', 'ton-italia-registration' ) );
+ }
+
+ if ( 'admitted' !== $row->status || ! empty( $row->anonymized_at ) ) {
+ return new WP_Error( 'not_admitted', __( 'La tessera può essere generata solo per soci ammessi.', 'ton-italia-registration' ) );
+ }
+
+ $year = (int) $year;
+ $config = self::get_template_config( $year );
+
+ if ( empty( $config['template_attachment_id'] ) ) {
+ return new WP_Error( 'no_template', __( 'Template PDF non configurato per questo anno.', 'ton-italia-registration' ) );
+ }
+
+ $existing = TON_Reg_Membership_Cards::get_by_registration_year( $registration_id, $year );
+ if ( $existing && empty( $args['force_regenerate'] ) ) {
+ return (int) $existing->id;
+ }
+
+ $library = self::load_library();
+ if ( is_wp_error( $library ) ) {
+ return $library;
+ }
+
+ $pdf_path = self::build_pdf_file( $row, $year, $config );
+ if ( is_wp_error( $pdf_path ) ) {
+ return $pdf_path;
+ }
+
+ $attachment_id = self::save_pdf_attachment( $row, $year, $pdf_path );
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
+ @unlink( $pdf_path );
+
+ if ( is_wp_error( $attachment_id ) ) {
+ return $attachment_id;
+ }
+
+ if ( $existing ) {
+ if ( ! empty( $existing->attachment_id ) && (int) $existing->attachment_id !== (int) $attachment_id ) {
+ wp_delete_attachment( (int) $existing->attachment_id, true );
+ }
+
+ TON_Reg_Membership_Cards::update(
+ (int) $existing->id,
+ array(
+ 'attachment_id' => (int) $attachment_id,
+ 'template_attachment_id' => (int) $config['template_attachment_id'],
+ 'layout_snapshot' => wp_json_encode( $config ),
+ 'generation_context' => sanitize_key( $args['context'] ),
+ 'created_at' => current_time( 'mysql', true ),
+ 'created_by' => (int) $args['created_by'],
+ )
+ );
+
+ return (int) $existing->id;
+ }
+
+ $card_id = TON_Reg_Membership_Cards::insert(
+ array(
+ 'registration_id' => (int) $registration_id,
+ 'user_id' => (int) ( $row->user_id ?? 0 ) ?: null,
+ 'year' => $year,
+ 'attachment_id' => (int) $attachment_id,
+ 'template_attachment_id' => (int) $config['template_attachment_id'],
+ 'layout_snapshot' => wp_json_encode( $config ),
+ 'generation_context' => sanitize_key( $args['context'] ),
+ 'created_by' => (int) $args['created_by'],
+ )
+ );
+
+ if ( ! $card_id ) {
+ wp_delete_attachment( (int) $attachment_id, true );
+ return new WP_Error( 'db_error', __( 'Impossibile salvare la tessera nel database.', 'ton-italia-registration' ) );
+ }
+
+ return (int) $card_id;
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param string $old_status Previous status.
+ * @param string $new_status New status.
+ * @return int|WP_Error|null Card ID, error, or null if skipped.
+ */
+ public static function maybe_generate_on_admission( $registration_id, $old_status, $new_status ) {
+ if ( 'admitted' === $old_status || 'admitted' !== $new_status ) {
+ return null;
+ }
+
+ $year = self::get_active_year();
+ if ( TON_Reg_Membership_Cards::get_by_registration_year( $registration_id, $year ) ) {
+ return null;
+ }
+
+ return self::generate_for_registration(
+ $registration_id,
+ self::get_active_year(),
+ array(
+ 'context' => self::CONTEXT_ADMISSION,
+ 'require_admin' => true,
+ )
+ );
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @param int $year Year.
+ * @param array $config Layout config.
+ * @return string|WP_Error Temp file path.
+ */
+ private static function build_pdf_file( $row, $year, $config ) {
+ $template_path = get_attached_file( (int) $config['template_attachment_id'] );
+ if ( ! $template_path || ! is_readable( $template_path ) ) {
+ return new WP_Error( 'template_missing', __( 'File template PDF non trovato.', 'ton-italia-registration' ) );
+ }
+
+ try {
+ $pdf = new Fpdi();
+ $pdf->setSourceFile( $template_path );
+ $tpl_id = $pdf->importPage( 1 );
+ $size = $pdf->getTemplateSize( $tpl_id );
+
+ $orientation = ( $size['width'] > $size['height'] ) ? 'L' : 'P';
+ $pdf->AddPage( $orientation, array( $size['width'], $size['height'] ) );
+ $pdf->useTemplate( $tpl_id );
+
+ $family = self::sanitize_font_family( (string) $config['font_family'] );
+ $style = self::sanitize_font_style( (string) $config['font_style'] );
+ $size_pt = max( 6, min( 72, (float) $config['font_size'] ) );
+
+ $pdf->SetFont( $family, $style, $size_pt );
+
+ list( $r, $g, $b ) = self::parse_hex_color( (string) $config['text_color'] );
+ $pdf->SetTextColor( $r, $g, $b );
+
+ self::write_name_on_pdf( $pdf, $row, $config, $size_pt );
+
+ $tmp = wp_tempnam( 'ton-card-' . $year );
+ if ( ! $tmp ) {
+ return new WP_Error( 'temp_file', __( 'Impossibile creare file temporaneo.', 'ton-italia-registration' ) );
+ }
+
+ $pdf->Output( 'F', $tmp );
+ return $tmp;
+ } catch ( Exception $e ) {
+ return new WP_Error( 'pdf_error', $e->getMessage() );
+ }
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @param int $year Year.
+ * @param string $path Temp PDF path.
+ * @return int|WP_Error Attachment ID.
+ */
+ private static function save_pdf_attachment( $row, $year, $path ) {
+ TON_Reg_Documents::ensure_member_folder( $row );
+ TON_Reg_Documents::ensure_subfolder( $row, self::SUBFOLDER );
+
+ $filename = sprintf( 'Tessera-%d.pdf', (int) $year );
+ $title = sprintf(
+ /* translators: 1: year, 2: full name */
+ __( 'Tessera %1$d — %2$s', 'ton-italia-registration' ),
+ (int) $year,
+ $row->nome . ' ' . $row->cognome
+ );
+
+ return TON_Reg_Documents::sideload_file(
+ $row,
+ $path,
+ $filename,
+ $title,
+ self::SUBFOLDER,
+ array(
+ '_ton_reg_registration_id' => (int) $row->id,
+ '_ton_reg_document_type' => 'tessera',
+ '_ton_reg_card_year' => (int) $year,
+ )
+ );
+ }
+
+ /**
+ * Write member name on the PDF, splitting onto two lines when needed.
+ *
+ * @param Fpdi $pdf PDF instance.
+ * @param object $row Registration row.
+ * @param array $config Layout config.
+ * @param float $size_pt Font size in points.
+ */
+ private static function write_name_on_pdf( $pdf, $row, $config, $size_pt ) {
+ $parts = self::get_name_parts( $row, $config );
+ $nome = $parts['nome'];
+ $cognome = $parts['cognome'];
+ $full_name = trim( $nome . ( $cognome ? ' ' . $cognome : '' ) );
+ $align = in_array( $config['align'], array( 'L', 'C', 'R' ), true ) ? $config['align'] : 'C';
+ $cell_width = max( 0, (float) $config['cell_width'] );
+ $x = (float) $config['name_x'];
+ $y = (float) $config['name_y'];
+
+ $use_two_lines = false;
+ if ( $cell_width > 0 && '' !== $cognome ) {
+ $full_width = $pdf->GetStringWidth( $full_name );
+ if ( $full_width > $cell_width ) {
+ $use_two_lines = true;
+ }
+ }
+
+ if ( $use_two_lines ) {
+ $line_height = max( 4.0, $size_pt * 0.45 );
+ $pdf->SetXY( $x, $y );
+ $pdf->Cell( $cell_width, $line_height, $nome, 0, 2, $align );
+ $pdf->SetX( $x );
+ $pdf->Cell( $cell_width, $line_height, $cognome, 0, 0, $align );
+ return;
+ }
+
+ $pdf->SetXY( $x, $y );
+ $pdf->Cell( $cell_width, 0, $full_name, 0, 0, $align );
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @param array $config Layout config.
+ * @return array{nome:string,cognome:string}
+ */
+ private static function get_name_parts( $row, $config ) {
+ return array(
+ 'nome' => self::format_name_part( (string) ( $row->nome ?? '' ), $config ),
+ 'cognome' => self::format_name_part( (string) ( $row->cognome ?? '' ), $config ),
+ );
+ }
+
+ /**
+ * @param string $text Name part.
+ * @param array $config Layout config.
+ * @return string
+ */
+ private static function format_name_part( $text, $config ) {
+ $text = trim( $text );
+ if ( ! empty( $config['uppercase'] ) ) {
+ if ( function_exists( 'mb_strtoupper' ) ) {
+ $text = mb_strtoupper( $text, 'UTF-8' );
+ } else {
+ $text = strtoupper( $text );
+ }
+ }
+ return $text;
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @param array $config Layout config.
+ * @return string
+ */
+ private static function get_display_name( $row, $config ) {
+ $parts = self::get_name_parts( $row, $config );
+ return trim( $parts['nome'] . ' ' . $parts['cognome'] );
+ }
+
+ /**
+ * @param string $hex Hex color.
+ * @return array{0:int,1:int,2:int}
+ */
+ private static function parse_hex_color( $hex ) {
+ $hex = ltrim( trim( $hex ), '#' );
+ if ( 3 === strlen( $hex ) ) {
+ $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
+ }
+ if ( 6 !== strlen( $hex ) || ! ctype_xdigit( $hex ) ) {
+ return array( 0, 0, 0 );
+ }
+ return array(
+ (int) hexdec( substr( $hex, 0, 2 ) ),
+ (int) hexdec( substr( $hex, 2, 2 ) ),
+ (int) hexdec( substr( $hex, 4, 2 ) ),
+ );
+ }
+
+ /**
+ * @param string $family Font family.
+ * @return string
+ */
+ private static function sanitize_font_family( $family ) {
+ $allowed = array( 'Helvetica', 'Times', 'Courier', 'Arial' );
+ return in_array( $family, $allowed, true ) ? ( 'Arial' === $family ? 'Helvetica' : $family ) : 'Helvetica';
+ }
+
+ /**
+ * @param string $style Font style.
+ * @return string
+ */
+ private static function sanitize_font_style( $style ) {
+ $allowed = array( '', 'B', 'I', 'BI', 'IB' );
+ $style = strtoupper( $style );
+ if ( 'IB' === $style ) {
+ $style = 'BI';
+ }
+ return in_array( $style, $allowed, true ) ? $style : '';
+ }
+
+ /**
+ * @param int $card_id Card ID.
+ * @return bool|WP_Error
+ */
+ public static function current_user_can_download( $card_id ) {
+ $card = TON_Reg_Membership_Cards::get( $card_id );
+ if ( ! $card ) {
+ return new WP_Error( 'not_found', __( 'Tessera non trovata.', 'ton-italia-registration' ) );
+ }
+
+ $row = TON_Reg_Database::get( (int) $card->registration_id );
+
+ if ( self::current_user_can_manage_cards() ) {
+ return true;
+ }
+
+ if ( ! is_user_logged_in() ) {
+ return new WP_Error( 'login', __( 'Accedi per scaricare la tessera.', 'ton-italia-registration' ) );
+ }
+
+ $user_row = TON_Reg_Database::get_for_wp_user( wp_get_current_user() );
+ if ( ! $user_row || (int) $user_row->id !== (int) $card->registration_id ) {
+ return new WP_Error( 'forbidden', __( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ if ( ! $row || 'admitted' !== $row->status || ! empty( $row->anonymized_at ) ) {
+ return new WP_Error( 'forbidden', __( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * @param int $card_id Card ID.
+ * @return void
+ */
+ public static function stream_download( $card_id ) {
+ $can = self::current_user_can_download( $card_id );
+ if ( is_wp_error( $can ) ) {
+ wp_die( esc_html( $can->get_error_message() ), 403 );
+ }
+
+ $card = TON_Reg_Membership_Cards::get( $card_id );
+ $path = get_attached_file( (int) $card->attachment_id );
+
+ if ( ! $path || ! is_readable( $path ) ) {
+ wp_die( esc_html__( 'File non trovato.', 'ton-italia-registration' ), 404 );
+ }
+
+ $filename = basename( $path );
+ nocache_headers();
+ header( 'Content-Type: application/pdf' );
+ header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
+ header( 'Content-Length: ' . (string) filesize( $path ) );
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
+ readfile( $path );
+ exit;
+ }
+
+ /**
+ * @param string $context Generation context key.
+ * @return string
+ */
+ public static function context_label( $context ) {
+ $labels = array(
+ self::CONTEXT_ADMISSION => __( 'Ammissione', 'ton-italia-registration' ),
+ self::CONTEXT_BATCH => __( 'Batch annuale', 'ton-italia-registration' ),
+ self::CONTEXT_MANUAL => __( 'Manuale', 'ton-italia-registration' ),
+ );
+ return $labels[ $context ] ?? $context;
+ }
+}
diff --git a/includes/class-membership-cards.php b/includes/class-membership-cards.php
new file mode 100644
index 0000000..2f15ba0
--- /dev/null
+++ b/includes/class-membership-cards.php
@@ -0,0 +1,182 @@
+prefix . 'ton_reg_membership_cards';
+ }
+
+ /**
+ * Create membership cards table.
+ */
+ public static function create_table() {
+ global $wpdb;
+
+ $table = self::table_name();
+ $charset = $wpdb->get_charset_collate();
+
+ $sql = "CREATE TABLE {$table} (
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ registration_id bigint(20) unsigned NOT NULL,
+ user_id bigint(20) unsigned DEFAULT NULL,
+ year smallint(5) unsigned NOT NULL,
+ attachment_id bigint(20) unsigned NOT NULL,
+ template_attachment_id bigint(20) unsigned NOT NULL DEFAULT 0,
+ layout_snapshot longtext NULL,
+ generation_context varchar(20) NOT NULL DEFAULT 'manual',
+ created_at datetime NOT NULL,
+ created_by bigint(20) unsigned NOT NULL DEFAULT 0,
+ PRIMARY KEY (id),
+ UNIQUE KEY registration_year (registration_id, year),
+ KEY user_id (user_id),
+ KEY year (year)
+ ) {$charset};";
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $sql );
+ }
+
+ /**
+ * @param array $data Row data.
+ * @return int|false
+ */
+ public static function insert( $data ) {
+ global $wpdb;
+
+ if ( empty( $data['created_at'] ) ) {
+ $data['created_at'] = current_time( 'mysql', true );
+ }
+
+ $result = $wpdb->insert( self::table_name(), $data );
+ return false === $result ? false : (int) $wpdb->insert_id;
+ }
+
+ /**
+ * @param int $id Card ID.
+ * @param array $data Data.
+ * @return bool
+ */
+ public static function update( $id, $data ) {
+ global $wpdb;
+
+ return false !== $wpdb->update(
+ self::table_name(),
+ $data,
+ array( 'id' => $id ),
+ null,
+ array( '%d' )
+ );
+ }
+
+ /**
+ * @param int $id Card ID.
+ * @return object|null
+ */
+ public static function get( $id ) {
+ global $wpdb;
+
+ return $wpdb->get_row(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE id = %d',
+ $id
+ )
+ );
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param int $year Card year.
+ * @return object|null
+ */
+ public static function get_by_registration_year( $registration_id, $year ) {
+ global $wpdb;
+
+ return $wpdb->get_row(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE registration_id = %d AND year = %d',
+ $registration_id,
+ $year
+ )
+ );
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @return array
+ */
+ public static function get_by_registration( $registration_id ) {
+ global $wpdb;
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE registration_id = %d ORDER BY year DESC',
+ $registration_id
+ )
+ );
+ }
+
+ /**
+ * @param int $user_id WordPress user ID.
+ * @return array
+ */
+ public static function get_by_user_id( $user_id ) {
+ global $wpdb;
+
+ if ( ! $user_id ) {
+ return array();
+ }
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE user_id = %d ORDER BY year DESC',
+ $user_id
+ )
+ );
+ }
+
+ /**
+ * @param int $id Card ID.
+ * @return bool
+ */
+ public static function delete( $id ) {
+ global $wpdb;
+
+ return false !== $wpdb->delete(
+ self::table_name(),
+ array( 'id' => $id ),
+ array( '%d' )
+ );
+ }
+
+ /**
+ * Delete card row and its attachment.
+ *
+ * @param int $id Card ID.
+ */
+ public static function delete_with_attachment( $id ) {
+ $card = self::get( $id );
+ if ( ! $card ) {
+ return;
+ }
+
+ if ( ! empty( $card->attachment_id ) ) {
+ wp_delete_attachment( (int) $card->attachment_id, true );
+ }
+
+ self::delete( $id );
+ }
+}
diff --git a/includes/class-membership-renewals.php b/includes/class-membership-renewals.php
new file mode 100644
index 0000000..fc74261
--- /dev/null
+++ b/includes/class-membership-renewals.php
@@ -0,0 +1,393 @@
+prefix . 'ton_reg_membership_renewals';
+ }
+
+ /**
+ * Create renewals table.
+ */
+ public static function create_table() {
+ global $wpdb;
+
+ $table = self::table_name();
+ $charset = $wpdb->get_charset_collate();
+
+ $sql = "CREATE TABLE {$table} (
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ registration_id bigint(20) unsigned NOT NULL,
+ year smallint(5) unsigned NOT NULL,
+ status varchar(20) NOT NULL DEFAULT 'pending',
+ paid_at datetime DEFAULT NULL,
+ paid_by bigint(20) unsigned DEFAULT NULL,
+ reminder_first_jan_sent_at datetime DEFAULT NULL,
+ reminder_last_jan_sent_at datetime DEFAULT NULL,
+ created_at datetime NOT NULL,
+ updated_at datetime NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY registration_year (registration_id, year),
+ KEY year (year),
+ KEY status (status)
+ ) {$charset};";
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $sql );
+ }
+
+ /**
+ * Current calendar year (site timezone).
+ *
+ * @return int
+ */
+ public static function current_year() {
+ return (int) wp_date( 'Y' );
+ }
+
+ /**
+ * @param object $registration Registration row.
+ * @return int
+ */
+ public static function admission_year( $registration ) {
+ if ( ! empty( $registration->libro_associati_date ) ) {
+ $timestamp = strtotime( (string) $registration->libro_associati_date );
+ if ( $timestamp ) {
+ return (int) wp_date( 'Y', $timestamp );
+ }
+ }
+
+ if ( ! empty( $registration->created_at ) ) {
+ $timestamp = strtotime( (string) $registration->created_at );
+ if ( $timestamp ) {
+ return (int) wp_date( 'Y', $timestamp );
+ }
+ }
+
+ return self::current_year();
+ }
+
+ /**
+ * Years shown in admin checkboxes: from admission through current calendar year only.
+ *
+ * @param object $registration Registration row.
+ * @return array{start:int,end:int}
+ */
+ public static function get_checkbox_year_range( $registration ) {
+ $start = self::admission_year( $registration );
+ $end = self::current_year();
+
+ if ( $start > $end ) {
+ $start = $end;
+ }
+
+ return array(
+ 'start' => $start,
+ 'end' => $end,
+ );
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param int $year Calendar year.
+ * @return string paid|pending
+ */
+ public static function get_status( $registration_id, $year ) {
+ $row = self::get_by_registration_year( $registration_id, $year );
+ if ( ! $row || self::STATUS_PAID !== (string) $row->status ) {
+ return self::STATUS_PENDING;
+ }
+
+ return self::STATUS_PAID;
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param int $year Calendar year.
+ * @return object|null
+ */
+ public static function get_by_registration_year( $registration_id, $year ) {
+ global $wpdb;
+
+ return $wpdb->get_row(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE registration_id = %d AND year = %d',
+ $registration_id,
+ $year
+ )
+ );
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @return array
+ */
+ public static function get_by_registration( $registration_id ) {
+ global $wpdb;
+
+ return $wpdb->get_results(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE registration_id = %d ORDER BY year ASC',
+ $registration_id
+ )
+ );
+ }
+
+ /**
+ * @param array $registration_ids Registration IDs.
+ * @param int $year Calendar year.
+ * @return array Map registration_id => status.
+ */
+ public static function get_statuses_for_registrations( array $registration_ids, $year ) {
+ global $wpdb;
+
+ $registration_ids = array_values( array_filter( array_map( 'intval', $registration_ids ) ) );
+ if ( empty( $registration_ids ) ) {
+ return array();
+ }
+
+ $placeholders = implode( ',', array_fill( 0, count( $registration_ids ), '%d' ) );
+ $params = array_merge( array( (int) $year ), $registration_ids );
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+ $sql = $wpdb->prepare(
+ 'SELECT registration_id, status FROM ' . self::table_name() . " WHERE year = %d AND registration_id IN ({$placeholders})",
+ $params
+ );
+
+ $rows = $wpdb->get_results( $sql );
+ $map = array();
+
+ foreach ( $registration_ids as $id ) {
+ $map[ $id ] = self::STATUS_PENDING;
+ }
+
+ foreach ( $rows as $row ) {
+ $map[ (int) $row->registration_id ] = (string) $row->status;
+ }
+
+ return $map;
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param int $year Calendar year.
+ * @param bool $paid Whether fee is paid.
+ * @param int $user_id Admin user marking payment.
+ * @return bool
+ */
+ public static function set_paid( $registration_id, $year, $paid, $user_id = 0 ) {
+ global $wpdb;
+
+ $registration_id = (int) $registration_id;
+ $year = (int) $year;
+ $user_id = (int) $user_id;
+ $now = current_time( 'mysql', true );
+ $existing = self::get_by_registration_year( $registration_id, $year );
+
+ $data = array(
+ 'status' => $paid ? self::STATUS_PAID : self::STATUS_PENDING,
+ 'paid_at' => $paid ? $now : null,
+ 'paid_by' => $paid ? ( $user_id ?: null ) : null,
+ 'updated_at' => $now,
+ );
+
+ if ( $existing ) {
+ return false !== $wpdb->update(
+ self::table_name(),
+ $data,
+ array( 'id' => (int) $existing->id ),
+ null,
+ array( '%d' )
+ );
+ }
+
+ $data['registration_id'] = $registration_id;
+ $data['year'] = $year;
+ $data['created_at'] = $now;
+
+ return false !== $wpdb->insert( self::table_name(), $data );
+ }
+
+ /**
+ * Ensure a pending row exists for reminder tracking.
+ *
+ * @param int $registration_id Registration ID.
+ * @param int $year Calendar year.
+ * @return object|null
+ */
+ public static function ensure_pending_row( $registration_id, $year ) {
+ $existing = self::get_by_registration_year( $registration_id, $year );
+ if ( $existing ) {
+ return $existing;
+ }
+
+ $now = current_time( 'mysql', true );
+ global $wpdb;
+
+ $wpdb->insert(
+ self::table_name(),
+ array(
+ 'registration_id' => (int) $registration_id,
+ 'year' => (int) $year,
+ 'status' => self::STATUS_PENDING,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ )
+ );
+
+ return self::get_by_registration_year( $registration_id, $year );
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param int $year Calendar year.
+ * @param string $reminder_type first|last.
+ * @return bool
+ */
+ public static function mark_reminder_sent( $registration_id, $year, $reminder_type ) {
+ global $wpdb;
+
+ $row = self::ensure_pending_row( $registration_id, $year );
+ if ( ! $row ) {
+ return false;
+ }
+
+ $column = 'first' === $reminder_type ? 'reminder_first_jan_sent_at' : 'reminder_last_jan_sent_at';
+ $now = current_time( 'mysql', true );
+
+ return false !== $wpdb->update(
+ self::table_name(),
+ array(
+ $column => $now,
+ 'updated_at' => $now,
+ ),
+ array( 'id' => (int) $row->id ),
+ null,
+ array( '%d' )
+ );
+ }
+
+ /**
+ * Admitted members without paid renewal for a given year.
+ *
+ * @param int $year Calendar year.
+ * @param string $reminder_type first|last — skip if that reminder was already sent.
+ * @return array Registration rows.
+ */
+ public static function get_unpaid_admitted( $year, $reminder_type = '' ) {
+ global $wpdb;
+
+ $reg_table = TON_Reg_Database::table_name();
+ $ren_table = self::table_name();
+ $year = (int) $year;
+
+ $sql = "SELECT r.* FROM {$reg_table} r
+ LEFT JOIN {$ren_table} ren ON ren.registration_id = r.id AND ren.year = %d
+ WHERE r.status = 'admitted'
+ AND r.anonymized_at IS NULL
+ AND r.email <> ''
+ AND ( ren.id IS NULL OR ren.status <> %s )";
+
+ $params = array( $year, self::STATUS_PAID );
+
+ if ( 'first' === $reminder_type ) {
+ $sql .= ' AND ( ren.reminder_first_jan_sent_at IS NULL )';
+ } elseif ( 'last' === $reminder_type ) {
+ $sql .= ' AND ( ren.reminder_last_jan_sent_at IS NULL )';
+ }
+
+ $sql .= ' ORDER BY r.cognome ASC, r.nome ASC, r.id ASC';
+
+ return $wpdb->get_results( $wpdb->prepare( $sql, $params ) );
+ }
+
+ /**
+ * Registration IDs with pending renewal for a year.
+ *
+ * @param int $year Calendar year.
+ * @return array
+ */
+ public static function get_unpaid_registration_ids( $year ) {
+ $rows = self::get_unpaid_admitted( $year );
+ return array_map(
+ static function ( $row ) {
+ return (int) $row->id;
+ },
+ $rows
+ );
+ }
+
+ /**
+ * Display label and CSS class for list table badge.
+ *
+ * @param string $status Renewal status (paid|pending).
+ * @param int $year Calendar year shown.
+ * @return array{label:string,class:string}
+ */
+ public static function display_badge( $status, $year ) {
+ $year = (int) $year;
+
+ if ( self::STATUS_PAID === $status ) {
+ return array(
+ 'label' => sprintf(
+ /* translators: %d: calendar year */
+ __( 'Pagato %d', 'ton-italia-registration' ),
+ $year
+ ),
+ 'class' => 'ton-reg-renewal--paid',
+ );
+ }
+
+ $month = (int) wp_date( 'n' );
+ $day = (int) wp_date( 'j' );
+
+ if ( 1 === $month && $day >= 25 ) {
+ return array(
+ 'label' => sprintf(
+ /* translators: %d: calendar year */
+ __( 'Sollecito %d', 'ton-italia-registration' ),
+ $year
+ ),
+ 'class' => 'ton-reg-renewal--reminder',
+ );
+ }
+
+ if ( $month >= 2 ) {
+ return array(
+ 'label' => sprintf(
+ /* translators: %d: calendar year */
+ __( 'Non pagato %d', 'ton-italia-registration' ),
+ $year
+ ),
+ 'class' => 'ton-reg-renewal--overdue',
+ );
+ }
+
+ return array(
+ 'label' => sprintf(
+ /* translators: %d: calendar year */
+ __( 'Da pagare %d', 'ton-italia-registration' ),
+ $year
+ ),
+ 'class' => 'ton-reg-renewal--pending',
+ );
+ }
+}
diff --git a/includes/class-plugin.php b/includes/class-plugin.php
index f6e5af3..034de65 100644
--- a/includes/class-plugin.php
+++ b/includes/class-plugin.php
@@ -35,11 +35,15 @@
add_action( 'init', array( $this, 'init' ) );
add_action( 'admin_init', array( $this, 'block_subscriber_admin' ) );
+ add_action( 'admin_menu', array( $this, 'trim_subscriber_admin_menu' ), 999 );
+ add_action( 'load-index.php', array( $this, 'redirect_subscriber_dashboard' ) );
add_filter( 'show_admin_bar', array( $this, 'hide_admin_bar_for_subscribers' ) );
TON_Reg_Registration_Form::register();
TON_Reg_Registration_Handler::register();
+ TON_Reg_Profile::register();
add_action( TON_Reg_Mailchimp::CRON_HOOK, array( 'TON_Reg_Mailchimp', 'sync_registration' ) );
+ add_action( TON_Reg_Renewal_Reminders::CRON_HOOK, array( 'TON_Reg_Renewal_Reminders', 'run_daily' ) );
add_action( 'update_option_ton_reg_mailchimp_api_key', array( 'TON_Reg_Mailchimp', 'ensure_api_key_no_autoload' ) );
if ( is_admin() ) {
@@ -95,22 +99,157 @@
$db_version = '1.3';
}
+ if ( version_compare( $db_version, '1.4', '<' ) ) {
+ self::purge_legacy_bonifico_documents();
+ $db_version = '1.4';
+ }
+
+ if ( version_compare( $db_version, '1.5', '<' ) ) {
+ TON_Reg_Membership_Cards::create_table();
+ if ( false === get_option( TON_Reg_Membership_Card::OPTION_ACTIVE_YEAR ) ) {
+ add_option( TON_Reg_Membership_Card::OPTION_ACTIVE_YEAR, (int) gmdate( 'Y' ) );
+ }
+ if ( false === get_option( TON_Reg_Membership_Card::OPTION_TEMPLATES ) ) {
+ add_option( TON_Reg_Membership_Card::OPTION_TEMPLATES, array() );
+ }
+ $db_version = '1.5';
+ }
+
+ if ( version_compare( $db_version, '1.6', '<' ) ) {
+ TON_Reg_Membership_Renewals::create_table();
+ TON_Reg_Renewal_Reminders::schedule_cron();
+
+ $renewal_options = array(
+ 'ton_reg_email_renewal_first_subject',
+ 'ton_reg_email_renewal_first_body',
+ 'ton_reg_email_renewal_second_subject',
+ 'ton_reg_email_renewal_second_body',
+ 'ton_reg_email_renewal_admin_subject',
+ 'ton_reg_email_renewal_admin_body',
+ 'ton_reg_renewal_reminders_start_year',
+ );
+ $defaults = TON_Reg_Defaults::options();
+ foreach ( $renewal_options as $key ) {
+ if ( false === get_option( $key ) && isset( $defaults[ $key ] ) ) {
+ add_option( $key, $defaults[ $key ] );
+ }
+ }
+
+ $db_version = '1.6';
+ }
+
update_option( 'ton_reg_db_version', $db_version );
}
/**
- * Prevent subscribers from accessing wp-admin (except AJAX).
+ * One-time cleanup after removing «Ricevuta bonifico» upload type.
*/
- public function block_subscriber_admin() {
- if ( ! is_user_logged_in() || wp_doing_ajax() ) {
+ private static function purge_legacy_bonifico_documents() {
+ global $wpdb;
+
+ $table = TON_Reg_Database::table_name();
+ $ids = $wpdb->get_col( "SELECT id FROM {$table} WHERE doc_bonifico_id IS NOT NULL AND doc_bonifico_id > 0" );
+
+ if ( ! is_array( $ids ) ) {
return;
}
+ foreach ( $ids as $registration_id ) {
+ TON_Reg_Documents::delete_legacy_bonifico_document( (int) $registration_id );
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ public static function is_restricted_subscriber() {
+ if ( ! is_user_logged_in() ) {
+ return false;
+ }
+
$user = wp_get_current_user();
- if ( in_array( 'subscriber', (array) $user->roles, true ) && ! current_user_can( 'manage_options' ) ) {
- wp_safe_redirect( home_url() );
+ return in_array( 'subscriber', (array) $user->roles, true ) && ! current_user_can( 'manage_options' );
+ }
+
+ /**
+ * Whether a subscriber may access the current wp-admin request.
+ *
+ * @return bool
+ */
+ public static function subscriber_can_access_admin() {
+ global $pagenow;
+
+ if ( 'profile.php' === $pagenow ) {
+ return true;
+ }
+
+ if ( 'admin-post.php' === $pagenow && 'ton_reg_download_card' === ( $_REQUEST['action'] ?? '' ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Prevent subscribers from accessing wp-admin except profile and card download.
+ */
+ public function block_subscriber_admin() {
+ if ( ! is_user_logged_in() || wp_doing_ajax() || current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ if ( ! self::is_restricted_subscriber() ) {
+ return;
+ }
+
+ if ( self::subscriber_can_access_admin() ) {
+ return;
+ }
+
+ global $pagenow;
+ if ( 'index.php' === $pagenow ) {
+ wp_safe_redirect( admin_url( 'profile.php' ) );
exit;
}
+
+ wp_safe_redirect( home_url() );
+ exit;
+ }
+
+ /**
+ * Keep only the Profile menu item for subscribers in wp-admin.
+ */
+ public function trim_subscriber_admin_menu() {
+ if ( ! self::is_restricted_subscriber() ) {
+ return;
+ }
+
+ global $menu;
+
+ if ( ! is_array( $menu ) ) {
+ return;
+ }
+
+ foreach ( $menu as $item ) {
+ if ( ! is_array( $item ) || empty( $item[2] ) ) {
+ continue;
+ }
+ if ( 'profile.php' !== $item[2] ) {
+ remove_menu_page( $item[2] );
+ }
+ }
+ }
+
+ /**
+ * Send subscribers to their profile instead of the dashboard.
+ */
+ public function redirect_subscriber_dashboard() {
+ if ( ! self::is_restricted_subscriber() ) {
+ return;
+ }
+
+ wp_safe_redirect( admin_url( 'profile.php' ) );
+ exit;
}
/**
diff --git a/includes/class-profile.php b/includes/class-profile.php
new file mode 100644
index 0000000..f760f98
--- /dev/null
+++ b/includes/class-profile.php
@@ -0,0 +1,345 @@
+post_content, 'ton_registration_profile' )
+ || false !== strpos( $post->post_content, '[ton_registration_profile' );
+ }
+
+ /**
+ * @return array
+ */
+ private static function status_labels() {
+ return array(
+ 'pending' => __( 'In attesa di valutazione', 'ton-italia-registration' ),
+ 'admitted' => __( 'Ammesso', 'ton-italia-registration' ),
+ 'rejected' => __( 'Non ammesso', 'ton-italia-registration' ),
+ );
+ }
+
+ /**
+ * @param WP_User $user WordPress user.
+ * @param string $context `frontend` or `admin`.
+ * @return string
+ */
+ public static function render_membership_section( $user, $context = 'frontend' ) {
+ $row = TON_Reg_Database::get_for_wp_user( $user );
+ if ( ! $row ) {
+ $message = __( 'Nessuna iscrizione collegata al tuo account.', 'ton-italia-registration' );
+ if ( 'admin' === $context ) {
+ return '' . esc_html( $message ) . '
';
+ }
+ return '' . esc_html( $message ) . '
';
+ }
+
+ $status_labels = self::status_labels();
+ $cards = TON_Reg_Membership_Cards::get_by_registration( (int) $row->id );
+
+ ob_start();
+
+ if ( 'admin' === $context ) {
+ self::render_admin_table_open();
+ self::render_admin_row( __( 'Nome', 'ton-italia-registration' ), trim( $row->nome . ' ' . $row->cognome ) );
+ self::render_admin_row( __( 'Email iscrizione', 'ton-italia-registration' ), $row->email );
+ self::render_admin_row(
+ __( 'Stato iscrizione', 'ton-italia-registration' ),
+ $status_labels[ $row->status ] ?? $row->status
+ );
+ } else {
+ echo '';
+ echo '- ' . esc_html__( 'Nome', 'ton-italia-registration' ) . '
';
+ echo '- ' . esc_html( trim( $row->nome . ' ' . $row->cognome ) ) . '
';
+ echo '- ' . esc_html__( 'Email', 'ton-italia-registration' ) . '
';
+ echo '- ' . esc_html( $row->email ) . '
';
+ echo '- ' . esc_html__( 'Stato iscrizione', 'ton-italia-registration' ) . '
';
+ echo '- ' . esc_html( $status_labels[ $row->status ] ?? $row->status ) . '
';
+ echo '
';
+ }
+
+ if ( 'admitted' !== $row->status || ! empty( $row->anonymized_at ) ) {
+ echo self::render_notice(
+ __( 'La tessera sarà disponibile dopo l\'ammissione.', 'ton-italia-registration' ),
+ $context
+ );
+ } elseif ( empty( $cards ) ) {
+ echo self::render_notice(
+ __( 'Nessuna tessera disponibile al momento.', 'ton-italia-registration' ),
+ $context
+ );
+ } else {
+ self::render_cards_list( $cards, $context );
+ }
+
+ if ( 'admin' === $context ) {
+ self::render_admin_table_close();
+ }
+
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * @param array $cards Card rows.
+ * @param string $context Render context.
+ */
+ private static function render_cards_list( $cards, $context ) {
+ $latest = $cards[0];
+
+ if ( 'admin' === $context ) {
+ self::render_admin_row(
+ sprintf(
+ /* translators: %d: year */
+ __( 'Tessera %d', 'ton-italia-registration' ),
+ (int) $latest->year
+ ),
+ self::render_download_link( (int) $latest->id, __( 'Scarica PDF', 'ton-italia-registration' ) )
+ );
+
+ if ( count( $cards ) > 1 ) {
+ $history = array();
+ foreach ( array_slice( $cards, 1 ) as $card ) {
+ $history[] = esc_html( (string) $card->year ) . ' — ' . self::render_download_link(
+ (int) $card->id,
+ __( 'Scarica', 'ton-italia-registration' )
+ );
+ }
+ self::render_admin_row(
+ __( 'Tessere anni precedenti', 'ton-italia-registration' ),
+ implode( '
', $history )
+ );
+ }
+ return;
+ }
+
+ echo '';
+ echo '
' . esc_html__( 'Tessera', 'ton-italia-registration' ) . ' ' . esc_html( (string) $latest->year ) . '
';
+ echo self::render_download_link( (int) $latest->id, __( 'Scarica tessera', 'ton-italia-registration' ) );
+ echo '';
+
+ if ( count( $cards ) > 1 ) {
+ echo '' . esc_html__( 'Tessere anni precedenti', 'ton-italia-registration' ) . '
';
+ echo '';
+ foreach ( array_slice( $cards, 1 ) as $card ) {
+ echo '- ';
+ echo esc_html( (string) $card->year ) . ' — ';
+ echo self::render_download_link( (int) $card->id, __( 'Scarica', 'ton-italia-registration' ) );
+ echo '
';
+ }
+ echo '
';
+ }
+ }
+
+ /**
+ * @param string $message Notice text.
+ * @param string $context Render context.
+ * @return string
+ */
+ private static function render_notice( $message, $context ) {
+ if ( 'admin' === $context ) {
+ self::render_admin_row( '', '' . esc_html( $message ) . '
' );
+ return '';
+ }
+
+ return '' . esc_html( $message ) . '
';
+ }
+
+ /**
+ * Open membership table on profile.php.
+ */
+ private static function render_admin_table_open() {
+ echo '';
+ }
+
+ /**
+ * @param string $label Row label.
+ * @param string $value Row value (may contain safe HTML for download links).
+ */
+ private static function render_admin_row( $label, $value ) {
+ echo '';
+ if ( '' !== $label ) {
+ echo '| ' . esc_html( $label ) . ' | ';
+ } else {
+ echo '' . esc_html__( 'Avviso', 'ton-italia-registration' ) . ' | ';
+ }
+ echo '' . wp_kses_post( $value ) . ' | ';
+ echo '
';
+ }
+
+ /**
+ * @param WP_User $user WordPress user.
+ */
+ public static function render_admin_profile_section( $user ) {
+ if ( ! $user instanceof WP_User || (int) $user->ID !== get_current_user_id() ) {
+ return;
+ }
+
+ echo '' . esc_html__( 'TON ITALIA — Iscrizione e tessera', 'ton-italia-registration' ) . '
';
+ echo '' . esc_html__( 'Qui puoi consultare lo stato della tua iscrizione e scaricare la tessera socio.', 'ton-italia-registration' ) . '
';
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in render_membership_section().
+ echo self::render_membership_section( $user, 'admin' );
+ }
+
+ /**
+ * @param array $atts Shortcode attributes.
+ * @return string
+ */
+ public static function render_shortcode( $atts ) {
+ $atts = shortcode_atts(
+ array(
+ 'class' => '',
+ ),
+ $atts,
+ 'ton_registration_profile'
+ );
+
+ $classes = trim( 'ton-reg-profile ' . $atts['class'] );
+
+ if ( ! is_user_logged_in() ) {
+ ob_start();
+ echo '';
+ echo '
' . esc_html__( 'Accedi per visualizzare il tuo profilo socio e scaricare la tessera.', 'ton-italia-registration' ) . '
';
+ wp_login_form(
+ array(
+ 'redirect' => self::profile_url(),
+ )
+ );
+ echo '
';
+ return (string) ob_get_clean();
+ }
+
+ ob_start();
+ echo '';
+ echo '
';
+ printf(
+ /* translators: %s: profile URL */
+ wp_kses_post( __( 'Il profilo socio è disponibile nel tuo account WordPress: apri il profilo.', 'ton-italia-registration' ) ),
+ esc_url( self::profile_url() )
+ );
+ echo '
';
+ echo '
' . esc_html__( 'Il mio profilo', 'ton-italia-registration' ) . '
';
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in render_membership_section().
+ echo self::render_membership_section( wp_get_current_user(), 'frontend' );
+ echo '
';
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * @param int $card_id Card ID.
+ * @param string $label Link label.
+ * @return string
+ */
+ private static function render_download_link( $card_id, $label ) {
+ $url = wp_nonce_url(
+ add_query_arg(
+ array(
+ 'action' => 'ton_reg_download_card',
+ 'card_id' => (int) $card_id,
+ ),
+ admin_url( 'admin-post.php' )
+ ),
+ 'ton_reg_download_card_' . (int) $card_id,
+ 'ton_reg_card_nonce'
+ );
+
+ return '' . esc_html( $label ) . '';
+ }
+
+ /**
+ * @return void
+ */
+ public static function handle_download() {
+ $card_id = isset( $_GET['card_id'] ) ? (int) $_GET['card_id'] : 0;
+ check_admin_referer( 'ton_reg_download_card_' . $card_id, 'ton_reg_card_nonce' );
+ TON_Reg_Membership_Card::stream_download( $card_id );
+ }
+
+ /**
+ * @return void
+ */
+ public static function handle_download_login_required() {
+ wp_safe_redirect( wp_login_url( wp_get_referer() ?: home_url() ) );
+ exit;
+ }
+}
diff --git a/includes/class-registration-form.php b/includes/class-registration-form.php
index 5620a11..639b6df 100644
--- a/includes/class-registration-form.php
+++ b/includes/class-registration-form.php
@@ -13,31 +13,61 @@
class TON_Reg_Registration_Form {
/**
+ * Whether the registration form was rendered on the current request.
+ *
+ * @var bool
+ */
+ private static $form_rendered = false;
+
+ /**
* Register hooks.
*/
public static function register() {
add_shortcode( 'ton_registration_form', array( __CLASS__, 'render_shortcode' ) );
+ add_shortcode( 'ton_registration_success', array( __CLASS__, 'render_success_shortcode' ) );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
}
/**
- * Enqueue form assets.
+ * Enqueue form assets when the page content includes the shortcode.
*/
public static function enqueue_assets() {
- if ( ! self::page_has_shortcode() ) {
+ if ( self::page_has_shortcode() ) {
+ self::enqueue_form_assets();
+ }
+ }
+
+ /**
+ * Ensure assets are loaded whenever the form shortcode is actually rendered.
+ */
+ public static function ensure_form_assets() {
+ self::enqueue_form_assets();
+ }
+
+ /**
+ * @return void
+ */
+ private static function enqueue_form_assets() {
+ static $enqueued = false;
+ if ( $enqueued ) {
return;
}
+ $enqueued = true;
+
+ $form_css_path = TON_REG_PLUGIN_DIR . 'assets/css/form.css';
+ $form_js_path = TON_REG_PLUGIN_DIR . 'assets/js/form.js';
+
wp_enqueue_style(
'ton-reg-form',
TON_REG_PLUGIN_URL . 'assets/css/form.css',
array(),
- TON_REG_VERSION
+ file_exists( $form_css_path ) ? (string) filemtime( $form_css_path ) : TON_REG_VERSION
);
wp_enqueue_script(
'ton-reg-form',
TON_REG_PLUGIN_URL . 'assets/js/form.js',
array(),
- TON_REG_VERSION,
+ file_exists( $form_js_path ) ? (string) filemtime( $form_js_path ) : TON_REG_VERSION,
true
);
@@ -51,6 +81,7 @@
'msgCap' => __( 'Il CAP deve essere di 5 cifre.', 'ton-italia-registration' ),
'msgProvincia' => __( 'Inserisci la sigla provincia (2 lettere).', 'ton-italia-registration' ),
'msgDate' => __( 'Inserisci la data nel formato gg-mm-aaaa.', 'ton-italia-registration' ),
+ 'msgDateFuture' => __( 'La data di dichiarazione non può essere futura.', 'ton-italia-registration' ),
'msgCheckbox' => __( 'Devi selezionare questa casella per continuare.', 'ton-italia-registration' ),
'msgCaptcha' => __( 'Inserisci la risposta alla verifica anti-spam.', 'ton-italia-registration' ),
)
@@ -58,11 +89,205 @@
}
/**
+ * Mark form as rendered and attach footer fallback script.
+ *
+ * @return void
+ */
+ private static function mark_form_rendered() {
+ if ( self::$form_rendered ) {
+ return;
+ }
+
+ self::$form_rendered = true;
+ self::ensure_form_assets();
+ add_action( 'wp_footer', array( __CLASS__, 'print_form_field_normalization_script' ), 20 );
+ }
+
+ /**
+ * Inline fallback so field normalization works even if external JS is blocked or cached stale.
+ *
+ * @return void
+ */
+ public static function print_form_field_normalization_script() {
+ if ( ! self::$form_rendered ) {
+ return;
+ }
+ ?>
+
+ post_content, 'ton_registration_form' );
+ if ( $post instanceof WP_Post ) {
+ if ( self::content_has_shortcode( $post->post_content, 'ton_registration_form' ) ) {
+ return true;
+ }
+ if ( self::content_has_shortcode( $post->post_content, 'ton_registration_success' ) ) {
+ return true;
+ }
+ }
+
+ return isset( $_GET['ton_reg'] ) && in_array( $_GET['ton_reg'], array( 'success', 'error' ), true );
+ }
+
+ /**
+ * @param string $content Post content.
+ * @param string $tag Shortcode tag.
+ * @return bool
+ */
+ private static function content_has_shortcode( $content, $tag ) {
+ if ( ! is_string( $content ) || '' === $content ) {
+ return false;
+ }
+
+ if ( has_shortcode( $content, $tag ) ) {
+ return true;
+ }
+
+ if ( false !== strpos( $content, '[' . $tag ) ) {
+ return true;
+ }
+
+ if ( function_exists( 'parse_blocks' ) && false !== strpos( $content, '