diff --git a/public/push-certificati-cron.php b/public/push-certificati-cron.php
new file mode 100644
index 00000000..fe222ed3
--- /dev/null
+++ b/public/push-certificati-cron.php
@@ -0,0 +1,205 @@
+getConnection();
+
+$impostazioni = pushSettings($db, 'certificate');
+
+if (!$impostazioni) {
+ scriviLog($logFile, 'Nessuna impostazione attiva per i certificati, esco.');
+ exit;
+}
+
+$sender = null;
+
+foreach ($impostazioni as $impostazione) {
+ $giorniPrima = (int) $impostazione['daysbefore'];
+ $oraPrevista = (int) substr((string) $impostazione['sendhour'], 0, 2);
+
+ if (!$forza && (int) date('G') !== $oraPrevista) {
+ continue;
+ }
+
+ $dataScadenza = date('Y-m-d', strtotime("+$giorniPrima days"));
+
+ scriviLog($logFile, "Controllo certificati con scadenza $dataScadenza (anticipo $giorniPrima giorni).");
+
+ // ----------------------------------------------------------------------
+ // Prendiamo solo i certificati "in corso": la sottoquery cerca la scadenza
+ // piu' lontana dell'utente e la confronta con quella della riga. Se non
+ // coincidono vuol dire che il documento e' gia' stato rinnovato.
+ // Se piu' righe hanno la stessa scadenza massima escono tutte e vengono
+ // poi raggruppate in un unico messaggio.
+ // ----------------------------------------------------------------------
+ $stmt = $db->prepare("
+ SELECT
+ c.idcertificateuserprofile,
+ c.iduser,
+ c.documentdescription,
+ c.expirydatedocument,
+ u.first_name,
+ u.last_name
+ FROM certificateuserprofile c
+ INNER JOIN auth_users u ON c.iduser = u.id
+ WHERE c.expirydatedocument = :scadenza
+ AND c.expirydatedocument = (
+ SELECT MAX(c2.expirydatedocument)
+ FROM certificateuserprofile c2
+ WHERE c2.iduser = c.iduser
+ )
+ AND u.pushnotification = 'Y'
+ AND u.status = 'Active'
+ ");
+ $stmt->execute([':scadenza' => $dataScadenza]);
+ $certificati = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ scriviLog($logFile, 'Certificati trovati: ' . count($certificati));
+
+ if (!$certificati) {
+ continue;
+ }
+
+ // ----------------------------------------------------------------------
+ // Raggruppiamo per utente: piu' documenti con la stessa scadenza -> una
+ // notifica sola.
+ // ----------------------------------------------------------------------
+ $perUtente = [];
+ $saltate = 0;
+
+ foreach ($certificati as $certificato) {
+ $iduser = (int) $certificato['iduser'];
+ $idcertificato = (int) $certificato['idcertificateuserprofile'];
+
+ if (pushGiaInviata($db, $iduser, 'certificate', $idcertificato, $giorniPrima)) {
+ $saltate++;
+ continue;
+ }
+
+ $perUtente[$iduser][] = $certificato;
+ }
+
+ if (!$perUtente) {
+ scriviLog($logFile, "Anticipo $giorniPrima gg -> niente da inviare (saltati: $saltate).");
+ continue;
+ }
+
+ if ($sender === null) {
+ try {
+ $sender = new PushSender();
+ } catch (Throwable $e) {
+ scriviLog($logFile, 'ERRORE configurazione FCM: ' . $e->getMessage());
+ exit;
+ }
+ }
+
+ $inviate = 0;
+ $fallite = 0;
+ $senzaApp = 0;
+
+ foreach ($perUtente as $iduser => $certificatiUtente) {
+ $primo = $certificatiUtente[0];
+ $quanti = count($certificatiUtente);
+
+ $elenco = [];
+ foreach ($certificatiUtente as $c) {
+ $elenco[] = trim((string) ($c['documentdescription'] ?? 'Certificato medico'));
+ }
+
+ $valori = [
+ 'first_name' => $primo['first_name'] ?? '',
+ 'last_name' => $primo['last_name'] ?? '',
+ 'expiredate' => date('d/m/Y', strtotime((string) $primo['expirydatedocument'])),
+ 'count' => $quanti,
+ 'documents' => implode(', ', $elenco),
+ ];
+
+ $titolo = $impostazione['title'];
+ $testo = $impostazione['body'];
+
+ if ($quanti > 1) {
+ if (!empty($impostazione['title_multi'])) {
+ $titolo = $impostazione['title_multi'];
+ }
+ if (!empty($impostazione['body_multi'])) {
+ $testo = $impostazione['body_multi'];
+ }
+ }
+
+ $esito = $sender->sendToUser(
+ $db,
+ (int) $iduser,
+ pushRender($titolo, $valori),
+ pushRender($testo, $valori),
+ [
+ 'type' => 'certificate',
+ 'idcertificate' => (int) $primo['idcertificateuserprofile'],
+ 'count' => $quanti,
+ ]
+ );
+
+ // Utente senza dispositivi registrati: non ha ancora aperto l'app.
+ if ($esito['no_devices']) {
+ $senzaApp++;
+ continue;
+ }
+
+ foreach ($certificatiUtente as $c) {
+ pushRegistraInvio(
+ $db,
+ (int) $iduser,
+ 'certificate',
+ (int) $c['idcertificateuserprofile'],
+ $giorniPrima,
+ $esito
+ );
+ }
+
+ if ($esito['sent'] > 0) {
+ $inviate++;
+ } else {
+ $fallite++;
+ if ($esito['errors']) {
+ scriviLog($logFile, "Utente $iduser: " . implode(' | ', $esito['errors']));
+ }
+ }
+ }
+
+ scriviLog(
+ $logFile,
+ "Anticipo $giorniPrima gg -> inviate: $inviate, saltate: $saltate, "
+ . "fallite: $fallite, senza app: $senzaApp."
+ );
+}
diff --git a/public/push-lezioni-cron.php b/public/push-lezioni-cron.php
new file mode 100644
index 00000000..98ea5dc0
--- /dev/null
+++ b/public/push-lezioni-cron.php
@@ -0,0 +1,192 @@
+getConnection();
+
+// --------------------------------------------------------------------------
+// Impostazioni: per le lezioni c'e' una sola riga (daysbefore = 0)
+// --------------------------------------------------------------------------
+$impostazioni = pushSettings($db, 'lesson');
+
+if (!$impostazioni) {
+ scriviLog($logFile, 'Nessuna impostazione attiva per le lezioni, esco.');
+ exit;
+}
+
+$impostazione = $impostazioni[0];
+$oraPrevista = (int) substr((string) $impostazione['sendhour'], 0, 2);
+
+if (!$forza && (int) date('G') !== $oraPrevista) {
+ // Non e' l'ora giusta: usciamo in silenzio (il cron gira ogni ora).
+ exit;
+}
+
+$giorno = date('Y-m-d');
+scriviLog($logFile, "Avvio invio lezioni del $giorno (ora impostata {$impostazione['sendhour']}).");
+
+// --------------------------------------------------------------------------
+// Lezioni di oggi.
+// Stessi filtri usati da userpanel.php / api/my_lessons.php per le lezioni
+// valide: status 'booked', non perse, non scadute.
+// --------------------------------------------------------------------------
+$stmt = $db->prepare("
+ SELECT
+ bc.idbookingclass,
+ bc.iduser,
+ ss.dateschedule,
+ s.servicename,
+ u.first_name,
+ u.last_name
+ FROM bookingclass bc
+ INNER JOIN serviceschedule ss ON bc.idserviceschedule = ss.idserviceschedule
+ INNER JOIN auth_users u ON bc.iduser = u.id
+ LEFT JOIN service s ON bc.idservice = s.idservice
+ WHERE bc.status = 'booked'
+ AND bc.lostlesson = 'N'
+ AND bc.expirylesson = 'N'
+ AND DATE(ss.dateschedule) = :giorno
+ AND u.pushnotification = 'Y'
+ AND u.status = 'Active'
+ ORDER BY bc.iduser ASC, ss.dateschedule ASC
+");
+$stmt->execute([':giorno' => $giorno]);
+$lezioni = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// --------------------------------------------------------------------------
+// Raggruppiamo per utente: una notifica a testa, non una per lezione.
+// Le lezioni gia' notificate (rilancio manuale dello script) vengono escluse;
+// se per un utente non ne resta nessuna, l'utente viene saltato del tutto.
+// --------------------------------------------------------------------------
+$perUtente = [];
+
+foreach ($lezioni as $lezione) {
+ $iduser = (int) $lezione['iduser'];
+
+ if (pushGiaInviata($db, $iduser, 'lesson', (int) $lezione['idbookingclass'], 0)) {
+ continue;
+ }
+
+ $perUtente[$iduser][] = $lezione;
+}
+
+scriviLog($logFile, 'Lezioni da notificare: ' . count($lezioni) . ', utenti da avvisare: ' . count($perUtente));
+
+if (!$perUtente) {
+ exit;
+}
+
+try {
+ $sender = new PushSender();
+} catch (Throwable $e) {
+ scriviLog($logFile, 'ERRORE configurazione FCM: ' . $e->getMessage());
+ exit;
+}
+
+$inviate = 0;
+$fallite = 0;
+$senzaApp = 0;
+
+foreach ($perUtente as $iduser => $lezioniUtente) {
+ $prima = $lezioniUtente[0];
+ $quante = count($lezioniUtente);
+
+ // Elenco leggibile: "Hatha Yoga alle 19:30, Yin Yoga alle 21:00"
+ $elenco = [];
+ foreach ($lezioniUtente as $l) {
+ $ora = (new DateTime($l['dateschedule']))->format('H:i');
+ $elenco[] = trim(($l['servicename'] ?? '') . " alle $ora");
+ }
+
+ $dataPrima = new DateTime($prima['dateschedule']);
+
+ $valori = [
+ 'first_name' => $prima['first_name'] ?? '',
+ 'last_name' => $prima['last_name'] ?? '',
+ 'servicename' => $prima['servicename'] ?? '',
+ 'date' => $dataPrima->format('d/m/Y'),
+ 'time' => $dataPrima->format('H:i'),
+ 'count' => $quante,
+ 'lessons' => implode(', ', $elenco),
+ ];
+
+ // Con piu' lezioni usiamo i testi "multi", se l'admin li ha compilati.
+ $titolo = $impostazione['title'];
+ $testo = $impostazione['body'];
+
+ if ($quante > 1) {
+ if (!empty($impostazione['title_multi'])) {
+ $titolo = $impostazione['title_multi'];
+ }
+ if (!empty($impostazione['body_multi'])) {
+ $testo = $impostazione['body_multi'];
+ }
+ }
+
+ $esito = $sender->sendToUser(
+ $db,
+ (int) $iduser,
+ pushRender($titolo, $valori),
+ pushRender($testo, $valori),
+ ['type' => 'lesson', 'idbookingclass' => (int) $prima['idbookingclass'], 'count' => $quante]
+ );
+
+ // Utente senza dispositivi registrati: non ha ancora aperto l'app.
+ // Non e' un errore e non va segnato nello storico, altrimenti quando
+ // installera' l'app risulterebbe gia' "notificato".
+ if ($esito['no_devices']) {
+ $senzaApp++;
+ continue;
+ }
+
+ // Registriamo tutte le lezioni incluse nel messaggio: cosi' nessuna di
+ // esse verra' rinotificata se lo script viene rilanciato.
+ foreach ($lezioniUtente as $l) {
+ pushRegistraInvio($db, (int) $iduser, 'lesson', (int) $l['idbookingclass'], 0, $esito);
+ }
+
+ if ($esito['sent'] > 0) {
+ $inviate++;
+ } else {
+ $fallite++;
+ if ($esito['errors']) {
+ scriviLog($logFile, "Utente $iduser: " . implode(' | ', $esito['errors']));
+ }
+ }
+}
+
+scriviLog($logFile, "Fine. Notifiche inviate: $inviate, fallite: $fallite, senza app: $senzaApp.");
diff --git a/public/push-ordini-cron.php b/public/push-ordini-cron.php
new file mode 100644
index 00000000..dc48f726
--- /dev/null
+++ b/public/push-ordini-cron.php
@@ -0,0 +1,189 @@
+getConnection();
+
+$impostazioni = pushSettings($db, 'order');
+
+if (!$impostazioni) {
+ scriviLog($logFile, 'Nessuna impostazione attiva per gli ordini, esco.');
+ exit;
+}
+
+$sender = null;
+
+foreach ($impostazioni as $impostazione) {
+ $giorniPrima = (int) $impostazione['daysbefore'];
+ $oraPrevista = (int) substr((string) $impostazione['sendhour'], 0, 2);
+
+ if (!$forza && (int) date('G') !== $oraPrevista) {
+ continue;
+ }
+
+ // Data di scadenza che ci interessa in questo giro.
+ $dataScadenza = date('Y-m-d', strtotime("+$giorniPrima days"));
+
+ scriviLog($logFile, "Controllo ordini con scadenza $dataScadenza (anticipo $giorniPrima giorni).");
+
+ // ----------------------------------------------------------------------
+ // Ordini che scadono in quella data.
+ // ----------------------------------------------------------------------
+ $stmt = $db->prepare("
+ SELECT
+ ob.idorderbook,
+ ob.iduser,
+ ob.expireon,
+ ob.nticket,
+ ob.product_name,
+ u.first_name,
+ u.last_name
+ FROM orderbook ob
+ INNER JOIN auth_users u ON ob.iduser = u.id
+ WHERE ob.expireon = :scadenza
+ AND (ob.status IS NULL OR ob.status != 'cancelled')
+ AND u.pushnotification = 'Y'
+ AND u.status = 'Active'
+ ");
+ $stmt->execute([':scadenza' => $dataScadenza]);
+ $ordini = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ scriviLog($logFile, 'Ordini trovati: ' . count($ordini));
+
+ if (!$ordini) {
+ continue;
+ }
+
+ // ----------------------------------------------------------------------
+ // Raggruppiamo per utente: chi ha due abbonamenti in scadenza lo stesso
+ // giorno riceve un messaggio solo.
+ // ----------------------------------------------------------------------
+ $perUtente = [];
+ $saltate = 0;
+
+ foreach ($ordini as $ordine) {
+ $iduser = (int) $ordine['iduser'];
+ $idorderbook = (int) $ordine['idorderbook'];
+
+ if (pushGiaInviata($db, $iduser, 'order', $idorderbook, $giorniPrima)) {
+ $saltate++;
+ continue;
+ }
+
+ $perUtente[$iduser][] = $ordine;
+ }
+
+ if (!$perUtente) {
+ scriviLog($logFile, "Anticipo $giorniPrima gg -> niente da inviare (saltati: $saltate).");
+ continue;
+ }
+
+ if ($sender === null) {
+ try {
+ $sender = new PushSender();
+ } catch (Throwable $e) {
+ scriviLog($logFile, 'ERRORE configurazione FCM: ' . $e->getMessage());
+ exit;
+ }
+ }
+
+ $inviate = 0;
+ $fallite = 0;
+ $senzaApp = 0;
+
+ foreach ($perUtente as $iduser => $ordiniUtente) {
+ $primo = $ordiniUtente[0];
+ $quanti = count($ordiniUtente);
+
+ // Elenco leggibile: "Abbonamento 10 lezioni, Lezione singola"
+ $elenco = [];
+ foreach ($ordiniUtente as $o) {
+ $elenco[] = trim((string) ($o['product_name'] ?? 'Abbonamento'));
+ }
+
+ $valori = [
+ 'first_name' => $primo['first_name'] ?? '',
+ 'last_name' => $primo['last_name'] ?? '',
+ 'expiredate' => date('d/m/Y', strtotime((string) $primo['expireon'])),
+ 'servicename' => $primo['product_name'] ?? '',
+ 'count' => $quanti,
+ 'orders' => implode(', ', $elenco),
+ ];
+
+ $titolo = $impostazione['title'];
+ $testo = $impostazione['body'];
+
+ if ($quanti > 1) {
+ if (!empty($impostazione['title_multi'])) {
+ $titolo = $impostazione['title_multi'];
+ }
+ if (!empty($impostazione['body_multi'])) {
+ $testo = $impostazione['body_multi'];
+ }
+ }
+
+ $esito = $sender->sendToUser(
+ $db,
+ (int) $iduser,
+ pushRender($titolo, $valori),
+ pushRender($testo, $valori),
+ ['type' => 'order', 'idorderbook' => (int) $primo['idorderbook'], 'count' => $quanti]
+ );
+
+ // Utente senza dispositivi registrati: non ha ancora aperto l'app.
+ if ($esito['no_devices']) {
+ $senzaApp++;
+ continue;
+ }
+
+ // Registriamo tutti gli ordini inclusi nel messaggio.
+ foreach ($ordiniUtente as $o) {
+ pushRegistraInvio($db, (int) $iduser, 'order', (int) $o['idorderbook'], $giorniPrima, $esito);
+ }
+
+ if ($esito['sent'] > 0) {
+ $inviate++;
+ } else {
+ $fallite++;
+ if ($esito['errors']) {
+ scriviLog($logFile, "Utente $iduser: " . implode(' | ', $esito['errors']));
+ }
+ }
+ }
+
+ scriviLog(
+ $logFile,
+ "Anticipo $giorniPrima gg -> inviate: $inviate, saltate: $saltate, "
+ . "fallite: $fallite, senza app: $senzaApp."
+ );
+}
diff --git a/public/push-settings.php b/public/push-settings.php
new file mode 100644
index 00000000..36223e98
--- /dev/null
+++ b/public/push-settings.php
@@ -0,0 +1,317 @@
+
+getConnection();
+
+$impostazioni = $pdo->query("
+ SELECT idnotificationsetting, notificationtype, daysbefore, sendhour, active,
+ title, title_multi, body, body_multi
+ FROM notificationsetting
+ ORDER BY FIELD(notificationtype, 'lesson', 'order', 'certificate'), daysbefore DESC
+")->fetchAll(PDO::FETCH_ASSOC);
+
+// Etichette e spiegazioni per ogni tipo di notifica.
+$etichette = [
+ 'lesson' => 'Lezioni del giorno',
+ 'order' => 'Scadenza abbonamento',
+ 'certificate' => 'Scadenza certificato medico',
+];
+
+$spiegazioni = [
+ 'lesson' => 'Inviata ogni mattina a chi ha lezioni prenotate per la giornata. '
+ . 'Se una persona ha piu\' lezioni riceve un solo messaggio: in quel caso vengono usati '
+ . 'il titolo e il testo "per piu\' lezioni".',
+ 'order' => 'Inviata ai giorni di anticipo indicati rispetto alla data di scadenza '
+ . 'dell\'abbonamento.',
+ 'certificate' => 'Inviata ai giorni di anticipo indicati rispetto alla scadenza del certificato '
+ . 'medico. Viene considerato solo il certificato piu\' recente di ogni utente.',
+];
+
+$segnaposti = [
+ 'lesson' => '{first_name} {last_name} {servicename} {date} {time} {count} {lessons}',
+ 'order' => '{first_name} {last_name} {expiredate} {servicename} {count} {orders}',
+ 'certificate' => '{first_name} {last_name} {expiredate} {count} {documents}',
+];
+
+// Come si chiama il caso "piu' elementi insieme", tipo per tipo.
+$etichetteMulti = [
+ 'lesson' => 'piu\' lezioni nello stesso giorno',
+ 'order' => 'piu\' abbonamenti in scadenza lo stesso giorno',
+ 'certificate' => 'piu\' certificati con la stessa scadenza',
+];
+?>
+
+
+
+
+
+
+ YogiBook - Notifiche push
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Notifiche push
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Notifiche push dell'app
+
+ Le notifiche partono solo agli utenti che le hanno attivate nell'app.
+ I cron girano ogni ora e controllano l'orario impostato qui sotto,
+ quindi per cambiare l'ora di invio basta modificare questa pagina.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ —
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+ Formato HH:MM
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ In quel caso parte comunque UNA sola notifica. Se questi campi restano
+ vuoti viene usato il testo qui sopra.
+
+
+
+
+ Segnaposto disponibili:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/updatepushsetting.php b/public/updatepushsetting.php
new file mode 100644
index 00000000..871fb944
--- /dev/null
+++ b/public/updatepushsetting.php
@@ -0,0 +1,142 @@
+ false, 'error' => 'Sessione scaduta']);
+ exit;
+}
+
+// Solo amministratori (role_id = 1).
+if ((int) Auth::user()->role_id !== 1) {
+ http_response_code(403);
+ echo json_encode(['success' => false, 'error' => 'Non autorizzato']);
+ exit;
+}
+
+$pdo = DBHandlerSelect::getInstance()->getConnection();
+
+$id = (int) ($_POST['idnotificationsetting'] ?? 0);
+$active = ($_POST['active'] ?? 'N') === 'Y' ? 'Y' : 'N';
+$daysbefore = (int) ($_POST['daysbefore'] ?? 0);
+$sendhour = trim((string) ($_POST['sendhour'] ?? ''));
+$title = trim((string) ($_POST['title'] ?? ''));
+$body = trim((string) ($_POST['body'] ?? ''));
+// Testi usati quando la notifica raggruppa piu' elementi (piu' lezioni in un
+// giorno). Possono restare vuoti: in quel caso si usa il testo singolo.
+//
+// Attenzione alla differenza: campo inviato vuoto = l'admin lo ha svuotato
+// apposta; campo NON inviato = chi chiama non se ne occupa, e quello che c'e'
+// gia' nel database non va perso.
+$aggiornaMulti = array_key_exists('title_multi', $_POST) || array_key_exists('body_multi', $_POST);
+$titleMulti = trim((string) ($_POST['title_multi'] ?? ''));
+$bodyMulti = trim((string) ($_POST['body_multi'] ?? ''));
+
+if ($id <= 0) {
+ echo json_encode(['success' => false, 'error' => 'Impostazione non valida']);
+ exit;
+}
+
+if (!preg_match('/^([01][0-9]|2[0-3]):[0-5][0-9]$/', $sendhour)) {
+ echo json_encode(['success' => false, 'error' => "L'ora deve essere nel formato HH:MM (es. 08:00)"]);
+ exit;
+}
+
+if ($daysbefore < 0 || $daysbefore > 60) {
+ echo json_encode(['success' => false, 'error' => 'I giorni di anticipo devono essere tra 0 e 60']);
+ exit;
+}
+
+if ($title === '' || $body === '') {
+ echo json_encode(['success' => false, 'error' => 'Titolo e testo non possono essere vuoti']);
+ exit;
+}
+
+if (mb_strlen($title) > 150 || mb_strlen($titleMulti) > 150) {
+ echo json_encode(['success' => false, 'error' => 'Il titolo non puo superare i 150 caratteri']);
+ exit;
+}
+
+// Il tipo non si cambia da qui: le righe sono quelle previste dai cron.
+$stmt = $pdo->prepare("
+ SELECT notificationtype FROM notificationsetting WHERE idnotificationsetting = :id LIMIT 1
+");
+$stmt->execute([':id' => $id]);
+$tipo = $stmt->fetchColumn();
+
+if (!$tipo) {
+ echo json_encode(['success' => false, 'error' => 'Impostazione inesistente']);
+ exit;
+}
+
+// Per le lezioni l'anticipo non ha senso: e' sempre l'invio del mattino stesso.
+if ($tipo === 'lesson') {
+ $daysbefore = 0;
+}
+
+// La coppia tipo + giorni รจ unica: due righe uguali romperebbero i cron.
+$stmt = $pdo->prepare("
+ SELECT 1 FROM notificationsetting
+ WHERE notificationtype = :tipo AND daysbefore = :giorni AND idnotificationsetting != :id
+ LIMIT 1
+");
+$stmt->execute([':tipo' => $tipo, ':giorni' => $daysbefore, ':id' => $id]);
+
+if ($stmt->fetchColumn()) {
+ echo json_encode([
+ 'success' => false,
+ 'error' => "Esiste gia' una notifica di questo tipo con $daysbefore giorni di anticipo",
+ ]);
+ exit;
+}
+
+$stmt = $pdo->prepare("
+ UPDATE notificationsetting
+ SET active = :active,
+ daysbefore = :giorni,
+ sendhour = :ora,
+ title = :titolo,
+ body = :testo"
+ . ($aggiornaMulti ? ",
+ title_multi = :titoloMulti,
+ body_multi = :testoMulti" : '') . "
+ WHERE idnotificationsetting = :id
+");
+
+$parametri = [
+ ':active' => $active,
+ ':giorni' => $daysbefore,
+ ':ora' => $sendhour,
+ ':titolo' => $title,
+ ':testo' => $body,
+ ':id' => $id,
+];
+
+if ($aggiornaMulti) {
+ $parametri[':titoloMulti'] = $titleMulti !== '' ? $titleMulti : null;
+ $parametri[':testoMulti'] = $bodyMulti !== '' ? $bodyMulti : null;
+}
+
+$stmt->execute($parametri);
+
+echo json_encode([
+ 'success' => true,
+ 'daysbefore' => $daysbefore,
+]);