push notifications
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Token dei dispositivi per le notifiche push (Firebase Cloud Messaging).
|
||||
* Un utente puo' avere piu' dispositivi, quindi piu' token.
|
||||
*/
|
||||
final class CreateUserdevicetokenTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('userdevicetoken', ['id' => 'iduserdevicetoken'])
|
||||
->addColumn('iduser', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'auth_users.id',
|
||||
])
|
||||
->addColumn('devicetoken', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => false,
|
||||
'comment' => 'Token FCM del dispositivo',
|
||||
])
|
||||
->addColumn('platform', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => false,
|
||||
'default' => 'android',
|
||||
'comment' => 'android / ios',
|
||||
])
|
||||
->addColumn('appversion', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => true,
|
||||
])
|
||||
->addColumn('active', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'Y',
|
||||
'comment' => 'N quando FCM segnala il token come non valido',
|
||||
])
|
||||
->addColumn('created_at', 'datetime', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('lastseen_at', 'datetime', [
|
||||
'null' => true,
|
||||
'comment' => 'Ultima volta che l\'app ha registrato questo token',
|
||||
])
|
||||
->addIndex(['devicetoken'], ['unique' => true, 'name' => 'uq_userdevicetoken_token'])
|
||||
->addIndex(['iduser', 'active'], ['name' => 'idx_userdevicetoken_user'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateNotificationsettingTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('notificationsetting', ['id' => 'idnotificationsetting'])
|
||||
->addColumn('notificationtype', 'string', [
|
||||
'limit' => 30,
|
||||
'null' => false,
|
||||
'comment' => 'lesson / order / certificate',
|
||||
])
|
||||
->addColumn('daysbefore', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'comment' => 'Giorni di anticipo; 0 = il giorno stesso',
|
||||
])
|
||||
->addColumn('sendhour', 'string', [
|
||||
'limit' => 5,
|
||||
'null' => false,
|
||||
'default' => '08:00',
|
||||
'comment' => 'Ora di invio del cron (HH:MM)',
|
||||
])
|
||||
->addColumn('active', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'Y',
|
||||
])
|
||||
->addColumn('title', 'string', [
|
||||
'limit' => 150,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('body', 'text', [
|
||||
'null' => false,
|
||||
])
|
||||
->addIndex(
|
||||
['notificationtype', 'daysbefore'],
|
||||
['unique' => true, 'name' => 'uq_notificationsetting_type']
|
||||
)
|
||||
->insert([
|
||||
[
|
||||
'notificationtype' => 'lesson',
|
||||
'daysbefore' => 0,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'La tua lezione di oggi',
|
||||
'body' => 'Ciao {first_name}, oggi hai {servicename} alle {time}. Ti aspettiamo!',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'order',
|
||||
'daysbefore' => 5,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Il tuo abbonamento sta per scadere',
|
||||
'body' => 'Ciao {first_name}, il tuo abbonamento scade il {expiredate}. '
|
||||
. 'Ricordati di prenotare le lezioni rimaste.',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'order',
|
||||
'daysbefore' => 0,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Il tuo abbonamento scade oggi',
|
||||
'body' => 'Ciao {first_name}, oggi e\' l\'ultimo giorno del tuo abbonamento.',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'certificate',
|
||||
'daysbefore' => 7,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Certificato medico in scadenza',
|
||||
'body' => 'Ciao {first_name}, il tuo certificato medico scade il {expiredate}.',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'certificate',
|
||||
'daysbefore' => 5,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Certificato medico in scadenza',
|
||||
'body' => 'Ciao {first_name}, mancano pochi giorni: il tuo certificato medico '
|
||||
. 'scade il {expiredate}.',
|
||||
],
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateNotificationlogTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('notificationlog', ['id' => 'idnotificationlog'])
|
||||
->addColumn('iduser', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'auth_users.id',
|
||||
])
|
||||
->addColumn('notificationtype', 'string', [
|
||||
'limit' => 30,
|
||||
'null' => false,
|
||||
'comment' => 'lesson / order / certificate',
|
||||
])
|
||||
->addColumn('idreference', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'Id della lezione, ordine o certificato',
|
||||
])
|
||||
->addColumn('daysbefore', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
])
|
||||
->addColumn('sent_at', 'datetime', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('status', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => false,
|
||||
'default' => 'sent',
|
||||
'comment' => 'sent / failed',
|
||||
])
|
||||
->addColumn('devicecount', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'comment' => 'A quanti dispositivi e stata inviata',
|
||||
])
|
||||
->addColumn('errormessage', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => true,
|
||||
])
|
||||
->addIndex(
|
||||
['iduser', 'notificationtype', 'idreference', 'daysbefore'],
|
||||
['unique' => true, 'name' => 'uq_notificationlog_invio']
|
||||
)
|
||||
->addIndex(['sent_at'], ['name' => 'idx_notificationlog_data'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddMultiTextsToNotificationsetting extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('notificationsetting')
|
||||
->addColumn('title_multi', 'string', [
|
||||
'limit' => 150,
|
||||
'null' => true,
|
||||
'after' => 'title',
|
||||
'comment' => 'Titolo usato quando la notifica raggruppa piu elementi',
|
||||
])
|
||||
->addColumn('body_multi', 'text', [
|
||||
'null' => true,
|
||||
'after' => 'body',
|
||||
'comment' => 'Testo usato quando la notifica raggruppa piu elementi',
|
||||
])
|
||||
->update();
|
||||
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'Le tue lezioni di oggi',
|
||||
body_multi = 'Ciao {first_name}, oggi hai {count} lezioni: {lessons}. Ti aspettiamo!'
|
||||
WHERE notificationtype = 'lesson'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('notificationsetting')
|
||||
->removeColumn('title_multi')
|
||||
->removeColumn('body_multi')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddMultiTextsDefaults extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'I tuoi abbonamenti in scadenza',
|
||||
body_multi = 'Ciao {first_name}, hai {count} abbonamenti in scadenza il {expiredate}: {orders}.'
|
||||
WHERE notificationtype = 'order' AND daysbefore > 0
|
||||
");
|
||||
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'I tuoi abbonamenti scadono oggi',
|
||||
body_multi = 'Ciao {first_name}, oggi scadono {count} dei tuoi abbonamenti: {orders}.'
|
||||
WHERE notificationtype = 'order' AND daysbefore = 0
|
||||
");
|
||||
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'Certificati medici in scadenza',
|
||||
body_multi = 'Ciao {first_name}, hai {count} certificati in scadenza il {expiredate}: {documents}.'
|
||||
WHERE notificationtype = 'certificate'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = NULL, body_multi = NULL
|
||||
WHERE notificationtype IN ('order', 'certificate')
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$userId = (int) $user->id;
|
||||
$metodo = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($metodo === 'POST' || $metodo === 'PUT' || $metodo === 'PATCH') {
|
||||
$input = json_decode((string) file_get_contents('php://input'), true);
|
||||
if (!is_array($input) || !array_key_exists('enabled', $input)) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Parametro enabled mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$valore = $input['enabled'];
|
||||
|
||||
if (is_bool($valore)) {
|
||||
$nuovo = $valore ? 'Y' : 'N';
|
||||
} else {
|
||||
$nuovo = strtoupper(trim((string) $valore)) === 'Y' ? 'Y' : 'N';
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE auth_users SET pushnotification = :valore WHERE id = :uid");
|
||||
$stmt->execute([':valore' => $nuovo, ':uid' => $userId]);
|
||||
|
||||
echo json_encode(['success' => true, 'push_enabled' => $nuovo]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT pushnotification FROM auth_users WHERE id = :uid LIMIT 1");
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'push_enabled' => (string) ($stmt->fetchColumn() ?: 'Y'),
|
||||
]);
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$userId = (int) $user->id;
|
||||
$metodo = $_SERVER['REQUEST_METHOD'] ?? 'POST';
|
||||
|
||||
$input = json_decode((string) file_get_contents('php://input'), true);
|
||||
if (!is_array($input)) {
|
||||
$input = [];
|
||||
}
|
||||
|
||||
$deviceToken = trim((string) ($input['device_token'] ?? ''));
|
||||
|
||||
if ($deviceToken === '') {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'device_token mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($metodo === 'DELETE') {
|
||||
$stmt = $db->prepare("
|
||||
UPDATE userdevicetoken
|
||||
SET active = 'N'
|
||||
WHERE devicetoken = :token AND iduser = :uid
|
||||
");
|
||||
$stmt->execute([':token' => $deviceToken, ':uid' => $userId]);
|
||||
|
||||
echo json_encode(['success' => true, 'deactivated' => $stmt->rowCount()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$platform = strtolower(trim((string) ($input['platform'] ?? 'android')));
|
||||
if (!in_array($platform, ['android', 'ios'], true)) {
|
||||
$platform = 'android';
|
||||
}
|
||||
|
||||
$appVersion = trim((string) ($input['app_version'] ?? ''));
|
||||
if ($appVersion === '') {
|
||||
$appVersion = null;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO userdevicetoken (iduser, devicetoken, platform, appversion, active, lastseen_at)
|
||||
VALUES (:uid, :token, :platform, :versione, 'Y', NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
iduser = VALUES(iduser),
|
||||
platform = VALUES(platform),
|
||||
appversion = COALESCE(VALUES(appversion), appversion),
|
||||
active = 'Y',
|
||||
lastseen_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':uid' => $userId,
|
||||
':token' => $deviceToken,
|
||||
':platform' => $platform,
|
||||
':versione' => $appVersion,
|
||||
]);
|
||||
|
||||
$stmt = $db->prepare("SELECT pushnotification FROM auth_users WHERE id = :uid LIMIT 1");
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
$pushEnabled = (string) ($stmt->fetchColumn() ?: 'Y');
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'push_enabled' => $pushEnabled,
|
||||
]);
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
|
||||
class PushSender
|
||||
{
|
||||
/** @var string */
|
||||
private $projectId;
|
||||
|
||||
/**
|
||||
* @var string Endpoint di Google. Si puo' sovrascrivere con FCM_OAUTH_URL /
|
||||
* FCM_BASE_URL nel .env: serve SOLO per i test locali, che puntano a un
|
||||
* finto server FCM. In produzione queste variabili non vanno impostate.
|
||||
*/
|
||||
private $oauthUrl;
|
||||
|
||||
/** @var string */
|
||||
private $baseUrl;
|
||||
|
||||
/** @var array Contenuto del service account JSON */
|
||||
private $credentials;
|
||||
|
||||
/** @var string|null Token OAuth2 in cache per la durata dello script */
|
||||
private $accessToken = null;
|
||||
|
||||
/** @var int Timestamp di scadenza del token in cache */
|
||||
private $accessTokenExpiry = 0;
|
||||
|
||||
/**
|
||||
* @var string|null Se l'autenticazione con Google fallisce, il motivo resta qui:
|
||||
* gli invii successivi escono subito invece di ritentare per ogni utente.
|
||||
*/
|
||||
private $authFallita = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$rootPath = dirname(__DIR__, 2);
|
||||
|
||||
// Le variabili possono essere gia' state caricate da un altro require:
|
||||
// safeLoad non esplode se il .env manca o e' gia' in memoria.
|
||||
$dotenv = Dotenv::createImmutable($rootPath);
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$this->projectId = $_ENV['FCM_PROJECT_ID'] ?? '';
|
||||
$credentialsPath = $_ENV['FCM_CREDENTIALS_PATH'] ?? '';
|
||||
$this->oauthUrl = $_ENV['FCM_OAUTH_URL'] ?? 'https://oauth2.googleapis.com/token';
|
||||
$this->baseUrl = $_ENV['FCM_BASE_URL'] ?? 'https://fcm.googleapis.com';
|
||||
|
||||
if ($this->projectId === '' || $credentialsPath === '') {
|
||||
throw new RuntimeException(
|
||||
'FCM_PROJECT_ID o FCM_CREDENTIALS_PATH mancanti nel file .env'
|
||||
);
|
||||
}
|
||||
|
||||
// Percorso relativo -> lo consideriamo a partire dalla root del progetto.
|
||||
if ($credentialsPath[0] !== '/') {
|
||||
$credentialsPath = $rootPath . '/' . $credentialsPath;
|
||||
}
|
||||
|
||||
if (!is_readable($credentialsPath)) {
|
||||
throw new RuntimeException("Service account non leggibile: $credentialsPath");
|
||||
}
|
||||
|
||||
$json = json_decode((string) file_get_contents($credentialsPath), true);
|
||||
|
||||
if (!is_array($json) || empty($json['client_email']) || empty($json['private_key'])) {
|
||||
throw new RuntimeException("Service account non valido: $credentialsPath");
|
||||
}
|
||||
|
||||
$this->credentials = $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia la notifica a tutti i dispositivi attivi dell'utente.
|
||||
* I token rifiutati da FCM vengono disattivati (active = 'N').
|
||||
*
|
||||
* @return array ['sent' => int, 'failed' => int, 'errors' => string[], 'no_devices' => bool]
|
||||
* no_devices = l'utente non ha dispositivi registrati: non e'
|
||||
* un errore, semplicemente non ha (ancora) aperto l'app.
|
||||
*/
|
||||
public function sendToUser(PDO $db, int $iduser, string $title, string $body, array $data = []): array
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT iduserdevicetoken, devicetoken
|
||||
FROM userdevicetoken
|
||||
WHERE iduser = :uid AND active = 'Y'
|
||||
");
|
||||
$stmt->execute([':uid' => $iduser]);
|
||||
$devices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$esito = ['sent' => 0, 'failed' => 0, 'errors' => [], 'no_devices' => !$devices];
|
||||
|
||||
foreach ($devices as $device) {
|
||||
$risultato = $this->sendToToken($device['devicetoken'], $title, $body, $data);
|
||||
|
||||
if ($risultato['ok']) {
|
||||
$esito['sent']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$esito['failed']++;
|
||||
$esito['errors'][] = $risultato['error'];
|
||||
|
||||
// Token non piu' valido (app disinstallata, token rigenerato):
|
||||
// lo spegniamo per non riprovarci ad ogni giro di cron.
|
||||
if ($risultato['invalid_token']) {
|
||||
$upd = $db->prepare("
|
||||
UPDATE userdevicetoken SET active = 'N' WHERE iduserdevicetoken = :id
|
||||
");
|
||||
$upd->execute([':id' => $device['iduserdevicetoken']]);
|
||||
}
|
||||
}
|
||||
|
||||
return $esito;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia la notifica a un singolo token.
|
||||
*
|
||||
* @return array ['ok' => bool, 'error' => string, 'invalid_token' => bool]
|
||||
*/
|
||||
public function sendToToken(string $deviceToken, string $title, string $body, array $data = []): array
|
||||
{
|
||||
// Autenticazione gia' fallita in questo giro: inutile riprovare per ogni
|
||||
// singolo utente, il cron deve solo finire in fretta e scrivere il log.
|
||||
if ($this->authFallita !== null) {
|
||||
return ['ok' => false, 'error' => $this->authFallita, 'invalid_token' => false];
|
||||
}
|
||||
|
||||
try {
|
||||
$accessToken = $this->getAccessToken();
|
||||
} catch (Throwable $e) {
|
||||
// Un problema di credenziali o di rete non deve interrompere il cron:
|
||||
// lo registriamo e le notifiche restanti risulteranno "failed".
|
||||
$this->authFallita = 'auth: ' . $e->getMessage();
|
||||
|
||||
return ['ok' => false, 'error' => $this->authFallita, 'invalid_token' => false];
|
||||
}
|
||||
|
||||
$url = "{$this->baseUrl}/v1/projects/{$this->projectId}/messages:send";
|
||||
|
||||
// FCM accetta solo stringhe dentro "data".
|
||||
$payloadData = [];
|
||||
foreach ($data as $chiave => $valore) {
|
||||
$payloadData[(string) $chiave] = (string) $valore;
|
||||
}
|
||||
|
||||
$messaggio = [
|
||||
'message' => [
|
||||
'token' => $deviceToken,
|
||||
'notification' => [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
],
|
||||
// Attenzione: un array PHP vuoto diventa [] in JSON, ma FCM si
|
||||
// aspetta un oggetto e risponde 400. Se non ci sono dati da
|
||||
// mandare, il campo va proprio omesso.
|
||||
'data' => (object) $payloadData,
|
||||
'android' => ['priority' => 'high'],
|
||||
'apns' => [
|
||||
'headers' => ['apns-priority' => '10'],
|
||||
'payload' => ['aps' => ['sound' => 'default']],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Content-Type: application/json; charset=UTF-8',
|
||||
],
|
||||
CURLOPT_POSTFIELDS => json_encode($messaggio, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
|
||||
$risposta = curl_exec($ch);
|
||||
$codice = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$errCurl = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($risposta === false) {
|
||||
return ['ok' => false, 'error' => "curl: $errCurl", 'invalid_token' => false];
|
||||
}
|
||||
|
||||
if ($codice >= 200 && $codice < 300) {
|
||||
return ['ok' => true, 'error' => '', 'invalid_token' => false];
|
||||
}
|
||||
|
||||
$decodificata = json_decode((string) $risposta, true);
|
||||
$statoErrore = $decodificata['error']['status'] ?? '';
|
||||
|
||||
// UNREGISTERED / NOT_FOUND = app disinstallata o token non piu' valido.
|
||||
//
|
||||
// INVALID_ARGUMENT NON va messo qui: quasi sempre vuol dire che siamo
|
||||
// noi a mandare un messaggio malformato, e spegnere i dispositivi degli
|
||||
// utenti per un nostro errore li lascerebbe senza notifiche per sempre.
|
||||
$tokenNonValido = in_array($statoErrore, ['UNREGISTERED', 'NOT_FOUND'], true);
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => "HTTP $codice: " . substr((string) $risposta, 0, 300),
|
||||
'invalid_token' => $tokenNonValido,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Token OAuth2 per FCM: JWT firmato con la chiave del service account,
|
||||
* scambiato con un access token da Google. Vale un'ora, lo teniamo in cache.
|
||||
*/
|
||||
private function getAccessToken(): string
|
||||
{
|
||||
if ($this->accessToken !== null && time() < $this->accessTokenExpiry - 60) {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
$adesso = time();
|
||||
|
||||
$header = ['alg' => 'RS256', 'typ' => 'JWT'];
|
||||
$claim = [
|
||||
'iss' => $this->credentials['client_email'],
|
||||
'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
|
||||
'aud' => 'https://oauth2.googleapis.com/token',
|
||||
'iat' => $adesso,
|
||||
'exp' => $adesso + 3600,
|
||||
];
|
||||
|
||||
$daFirmare = $this->base64UrlEncode(json_encode($header))
|
||||
. '.' . $this->base64UrlEncode(json_encode($claim));
|
||||
|
||||
$firma = '';
|
||||
if (!openssl_sign($daFirmare, $firma, $this->credentials['private_key'], OPENSSL_ALGO_SHA256)) {
|
||||
throw new RuntimeException('Firma del JWT fallita (chiave del service account non valida?)');
|
||||
}
|
||||
|
||||
$jwt = $daFirmare . '.' . $this->base64UrlEncode($firma);
|
||||
|
||||
$ch = curl_init($this->oauthUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
'assertion' => $jwt,
|
||||
]),
|
||||
]);
|
||||
|
||||
$risposta = curl_exec($ch);
|
||||
$codice = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$decodificata = json_decode((string) $risposta, true);
|
||||
|
||||
if ($codice !== 200 || empty($decodificata['access_token'])) {
|
||||
throw new RuntimeException(
|
||||
'Impossibile ottenere il token OAuth2 da Google: ' . substr((string) $risposta, 0, 300)
|
||||
);
|
||||
}
|
||||
|
||||
$this->accessToken = $decodificata['access_token'];
|
||||
$this->accessTokenExpiry = $adesso + (int) ($decodificata['expires_in'] ?? 3600);
|
||||
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
private function base64UrlEncode(string $dati): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($dati), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sostituisce i segnaposto nei testi presi da notificationsetting.
|
||||
* Esempio: pushRender('Ciao {first_name}', ['first_name' => 'Anna'])
|
||||
*/
|
||||
function pushRender(string $testo, array $valori): string
|
||||
{
|
||||
foreach ($valori as $chiave => $valore) {
|
||||
$testo = str_replace('{' . $chiave . '}', (string) $valore, $testo);
|
||||
}
|
||||
|
||||
return $testo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legge le impostazioni di un tipo di notifica (lesson / order / certificate).
|
||||
* Ritorna solo le righe attive, ordinate dal preavviso piu' lungo.
|
||||
*/
|
||||
function pushSettings(PDO $db, string $tipo): array
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT idnotificationsetting, notificationtype, daysbefore, sendhour,
|
||||
title, title_multi, body, body_multi
|
||||
FROM notificationsetting
|
||||
WHERE notificationtype = :tipo AND active = 'Y'
|
||||
ORDER BY daysbefore DESC
|
||||
");
|
||||
$stmt->execute([':tipo' => $tipo]);
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se una notifica e' gia' stata inviata (chiave anti-duplicato).
|
||||
*
|
||||
* Contano solo gli invii andati a buon fine: se il tentativo precedente era
|
||||
* fallito (Firebase irraggiungibile, credenziali sbagliate) il giro successivo
|
||||
* del cron deve riprovare.
|
||||
*/
|
||||
function pushGiaInviata(PDO $db, int $iduser, string $tipo, int $idreference, int $daysbefore): bool
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT 1
|
||||
FROM notificationlog
|
||||
WHERE iduser = :uid
|
||||
AND notificationtype = :tipo
|
||||
AND idreference = :idref
|
||||
AND daysbefore = :giorni
|
||||
AND status = 'sent'
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':uid' => $iduser,
|
||||
':tipo' => $tipo,
|
||||
':idref' => $idreference,
|
||||
':giorni' => $daysbefore,
|
||||
]);
|
||||
|
||||
return (bool) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrive l'esito dell'invio nello storico.
|
||||
*
|
||||
* La chiave unica impedisce i doppioni; se la riga esiste gia' (tentativo
|
||||
* precedente fallito) viene aggiornata con l'esito nuovo.
|
||||
*/
|
||||
function pushRegistraInvio(
|
||||
PDO $db,
|
||||
int $iduser,
|
||||
string $tipo,
|
||||
int $idreference,
|
||||
int $daysbefore,
|
||||
array $esito
|
||||
): void {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO notificationlog
|
||||
(iduser, notificationtype, idreference, daysbefore, status, devicecount, errormessage)
|
||||
VALUES
|
||||
(:uid, :tipo, :idref, :giorni, :stato, :dispositivi, :errore)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
devicecount = VALUES(devicecount),
|
||||
errormessage = VALUES(errormessage),
|
||||
sent_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':uid' => $iduser,
|
||||
':tipo' => $tipo,
|
||||
':idref' => $idreference,
|
||||
':giorni' => $daysbefore,
|
||||
':stato' => $esito['sent'] > 0 ? 'sent' : 'failed',
|
||||
':dispositivi' => $esito['sent'],
|
||||
':errore' => $esito['errors'] ? substr(implode(' | ', $esito['errors']), 0, 255) : null,
|
||||
]);
|
||||
}
|
||||
@@ -120,6 +120,12 @@
|
||||
<span class="menu-item" data-key="t-email">Sommario Ordini</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="push-settings.php" class="">
|
||||
<i class="bx bx-bell icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Notifiche Push</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-title" data-key="t-applications">Template</li>
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* push-certificati-cron.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Notifica push per i certificati medici in scadenza.
|
||||
* Di default: una settimana prima (7 giorni) e 5 giorni prima.
|
||||
* Giorni e testi si cambiano dal pannello admin (notificationsetting,
|
||||
* tipo 'certificate').
|
||||
*
|
||||
* Da mettere in cron OGNI ORA:
|
||||
* 0 * * * * /usr/bin/php /percorso/public/push-certificati-cron.php
|
||||
*
|
||||
* Prova immediata: php push-certificati-cron.php --force
|
||||
*
|
||||
* Due regole importanti:
|
||||
* 1. Se l'utente ha rinnovato il certificato (ne esiste uno con scadenza
|
||||
* piu' lontana) NON viene avvisato: il documento vecchio e' gia' stato
|
||||
* sostituito e ricordarne la scadenza sarebbe solo fastidioso.
|
||||
* 2. Se pero' ha piu' documenti validi con la STESSA scadenza (capita con
|
||||
* caricamenti doppi) riceve una notifica sola, non una per documento
|
||||
* (title_multi / body_multi, segnaposti {count} e {documents}).
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/push-functions.php';
|
||||
|
||||
$logFile = __DIR__ . '/logs/push_certificati_cron.log';
|
||||
$forza = in_array('--force', $argv ?? [], true) || isset($_GET['force']);
|
||||
|
||||
function scriviLog(string $file, string $messaggio): void
|
||||
{
|
||||
file_put_contents($file, date('Y-m-d H:i:s') . " - $messaggio\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance()->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."
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* push-lezioni-cron.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Notifica push del mattino: ogni giorno all'ora impostata (default 08:00)
|
||||
* avvisa chi ha lezioni prenotate OGGI.
|
||||
*
|
||||
* L'utente riceve UNA SOLA notifica al giorno: se ha piu' lezioni vengono
|
||||
* raggruppate in un unico messaggio (title_multi / body_multi, con i
|
||||
* segnaposto {count} e {lessons}).
|
||||
*
|
||||
* Da mettere in cron OGNI ORA: lo script controlla da solo se l'ora attuale
|
||||
* corrisponde a notificationsetting.sendhour, cosi' l'orario si cambia dal
|
||||
* pannello admin senza toccare il cron del server.
|
||||
*
|
||||
* 0 * * * * /usr/bin/php /percorso/public/push-lezioni-cron.php
|
||||
*
|
||||
* Per provarlo subito, ignorando l'orario:
|
||||
* php push-lezioni-cron.php --force
|
||||
*
|
||||
* Riceve la notifica solo chi ha auth_users.pushnotification = 'Y'.
|
||||
* Ogni lezione inclusa viene registrata in notificationlog: se lo script gira
|
||||
* due volte nello stesso giorno non si ripete nulla.
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/push-functions.php';
|
||||
|
||||
$logFile = __DIR__ . '/logs/push_lezioni_cron.log';
|
||||
$forza = in_array('--force', $argv ?? [], true) || isset($_GET['force']);
|
||||
|
||||
function scriviLog(string $file, string $messaggio): void
|
||||
{
|
||||
file_put_contents($file, date('Y-m-d H:i:s') . " - $messaggio\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance()->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.");
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* push-ordini-cron.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Notifica push per gli abbonamenti in scadenza.
|
||||
* Di default: 5 giorni prima della scadenza e il giorno stesso.
|
||||
* I giorni di anticipo e i testi si cambiano dal pannello admin
|
||||
* (tabella notificationsetting, tipo 'order').
|
||||
*
|
||||
* Da mettere in cron OGNI ORA, come gli altri script push:
|
||||
* 0 * * * * /usr/bin/php /percorso/public/push-ordini-cron.php
|
||||
*
|
||||
* Prova immediata: php push-ordini-cron.php --force
|
||||
*
|
||||
* Viene avvisato chi ha un abbonamento in scadenza in quella data, senza altre
|
||||
* condizioni.
|
||||
*
|
||||
* Se una persona ha piu' abbonamenti che scadono lo stesso giorno riceve una
|
||||
* notifica sola (title_multi / body_multi, segnaposti {count} e {orders}).
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
require_once __DIR__ . '/class/push-functions.php';
|
||||
|
||||
$logFile = __DIR__ . '/logs/push_ordini_cron.log';
|
||||
$forza = in_array('--force', $argv ?? [], true) || isset($_GET['force']);
|
||||
|
||||
function scriviLog(string $file, string $messaggio): void
|
||||
{
|
||||
file_put_contents($file, date('Y-m-d H:i:s') . " - $messaggio\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
$db = DBHandlerSelect::getInstance()->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."
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
// headscript.php stampa gia' dell'HTML: bufferizziamo l'output cosi', se chi
|
||||
// apre la pagina non e' amministratore, possiamo buttarlo via e fare un
|
||||
// redirect HTTP vero invece di uno via JavaScript.
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
?>
|
||||
<?php
|
||||
|
||||
/**
|
||||
* push-settings.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Pannello admin per le notifiche push dell'app: orario di invio, giorni di
|
||||
* anticipo e testi dei messaggi (tabella notificationsetting).
|
||||
*
|
||||
* I cron (push-lezioni-cron.php, push-ordini-cron.php, push-certificati-cron.php)
|
||||
* girano ogni ora e leggono da qui a che ora devono partire: cambiando l'orario
|
||||
* su questa pagina non serve toccare il cron del server.
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Solo amministratori: da qui si decidono i messaggi inviati a tutti gli iscritti.
|
||||
if ((int) $roleuser !== 1) {
|
||||
ob_end_clean();
|
||||
redirectTo('/');
|
||||
}
|
||||
|
||||
ob_end_flush();
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->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',
|
||||
];
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Notifiche push</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="YogiBook - Impostazioni notifiche push" name="description" />
|
||||
<meta content="Advanced Creative Solutions" name="author" />
|
||||
<link rel="shortcut icon" href="assets/images/favicon.ico">
|
||||
|
||||
<!-- Bootstrap Css -->
|
||||
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
|
||||
<!-- Icons Css -->
|
||||
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
|
||||
<!-- App Css-->
|
||||
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
||||
|
||||
<style>
|
||||
.custom-card {
|
||||
margin: 20px auto;
|
||||
background-color: white;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.push-hint {
|
||||
font-size: 12px;
|
||||
color: #74788d;
|
||||
}
|
||||
|
||||
.push-placeholders {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #556ee6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.btn-salva-push').on('click', function() {
|
||||
var card = $(this).closest('.custom-card');
|
||||
|
||||
$.ajax({
|
||||
url: 'updatepushsetting.php',
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
idnotificationsetting: card.data('id'),
|
||||
active: card.find('.push-active').is(':checked') ? 'Y' : 'N',
|
||||
daysbefore: card.find('.push-daysbefore').val(),
|
||||
sendhour: card.find('.push-sendhour').val(),
|
||||
title: card.find('.push-title').val(),
|
||||
body: card.find('.push-body').val(),
|
||||
title_multi: card.find('.push-title-multi').val(),
|
||||
body_multi: card.find('.push-body-multi').val()
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
Swal.fire({
|
||||
title: 'Salvato!',
|
||||
text: 'Impostazione aggiornata con successo.',
|
||||
icon: 'success',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: 'Errore!',
|
||||
text: response.error,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire({
|
||||
title: 'Errore!',
|
||||
text: 'Errore durante la richiesta AJAX.',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="layout-wrapper">
|
||||
<header id="page-topbar" class="isvertical-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex">
|
||||
<?php include('include/logoarea.php'); ?>
|
||||
<button type="button" class="btn btn-sm px-3 font-size-24 header-item waves-effect vertical-menu-btn">
|
||||
<i class="bx bx-menu align-middle"></i>
|
||||
</button>
|
||||
<div class="page-title-box align-self-center d-none d-md-block">
|
||||
<h4 class="page-title mb-0">Notifiche push</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="custom-card">
|
||||
<h4>Notifiche push dell'app</h4>
|
||||
<p class="push-hint mb-0">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($impostazioni as $riga) : ?>
|
||||
<?php $tipo = $riga['notificationtype']; ?>
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="custom-card" data-id="<?php echo (int) $riga['idnotificationsetting']; ?>">
|
||||
<h5>
|
||||
<?php echo htmlspecialchars($etichette[$tipo] ?? $tipo); ?>
|
||||
<?php if ($tipo !== 'lesson') : ?>
|
||||
<small class="text-muted">
|
||||
— <?php echo (int) $riga['daysbefore'] === 0
|
||||
? 'il giorno della scadenza'
|
||||
: (int) $riga['daysbefore'] . ' giorni prima'; ?>
|
||||
</small>
|
||||
<?php endif; ?>
|
||||
</h5>
|
||||
<p class="push-hint"><?php echo $spiegazioni[$tipo] ?? ''; ?></p>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input push-active" type="checkbox"
|
||||
id="active-<?php echo (int) $riga['idnotificationsetting']; ?>"
|
||||
<?php echo $riga['active'] === 'Y' ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label"
|
||||
for="active-<?php echo (int) $riga['idnotificationsetting']; ?>">
|
||||
Notifica attiva
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Ora di invio</label>
|
||||
<input type="text" class="form-control push-sendhour"
|
||||
value="<?php echo htmlspecialchars($riga['sendhour']); ?>"
|
||||
placeholder="08:00">
|
||||
<span class="push-hint">Formato HH:MM</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Giorni di anticipo</label>
|
||||
<input type="number" min="0" max="60" class="form-control push-daysbefore"
|
||||
value="<?php echo (int) $riga['daysbefore']; ?>"
|
||||
<?php echo $tipo === 'lesson' ? 'readonly' : ''; ?>>
|
||||
<span class="push-hint">
|
||||
<?php echo $tipo === 'lesson'
|
||||
? 'Sempre il giorno stesso'
|
||||
: '0 = il giorno della scadenza'; ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Titolo</label>
|
||||
<input type="text" class="form-control push-title" maxlength="150"
|
||||
value="<?php echo htmlspecialchars($riga['title']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Testo</label>
|
||||
<textarea class="form-control push-body" rows="2"><?php
|
||||
echo htmlspecialchars($riga['body']);
|
||||
?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Titolo quando ci sono <?php echo $etichetteMulti[$tipo] ?? 'piu\' elementi'; ?>
|
||||
</label>
|
||||
<input type="text" class="form-control push-title-multi" maxlength="150"
|
||||
value="<?php echo htmlspecialchars((string) $riga['title_multi']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Testo quando ci sono <?php echo $etichetteMulti[$tipo] ?? 'piu\' elementi'; ?>
|
||||
</label>
|
||||
<textarea class="form-control push-body-multi" rows="2"><?php
|
||||
echo htmlspecialchars((string) $riga['body_multi']);
|
||||
?></textarea>
|
||||
<span class="push-hint">
|
||||
In quel caso parte comunque UNA sola notifica. Se questi campi restano
|
||||
vuoti viene usato il testo qui sopra.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mb-2">
|
||||
<span class="push-hint">Segnaposto disponibili: </span>
|
||||
<span class="push-placeholders"><?php echo $segnaposti[$tipo] ?? ''; ?></span>
|
||||
</p>
|
||||
|
||||
<button type="button" class="btn btn-primary btn-salva-push">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
|
||||
<script src="assets/libs/simplebar/simplebar.min.js"></script>
|
||||
<script src="assets/libs/eva-icons/eva.min.js"></script>
|
||||
<script src="assets/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* updatepushsetting.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Salva una riga della tabella notificationsetting (chiamato in AJAX da
|
||||
* push-settings.php).
|
||||
*
|
||||
* Richiede la sessione di un amministratore: da qui si decidono i testi e gli
|
||||
* orari delle notifiche inviate a tutti gli iscritti.
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Non usiamo include/headscript.php: quello stampa HTML e qui dobbiamo poter
|
||||
// mandare gli header JSON. extra/auth.php avvia la sessione senza output.
|
||||
require_once __DIR__ . '/../extra/auth.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => 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,
|
||||
]);
|
||||
Reference in New Issue
Block a user