email certificati

This commit is contained in:
2026-10-05 15:06:57 +02:00
parent d47741e6d7
commit 81a75f1f72
3 changed files with 178 additions and 42 deletions
+63 -33
View File
@@ -8,6 +8,7 @@ use PHPMailer\PHPMailer\Exception;
// Imposta l'header per JSON
header('Content-Type: application/json');
ini_set('display_errors', '0'); // gli avvisi vanno nel log, non nella risposta
// Funzione per generare il template HTML dell'email
function generateEmailTemplate($message)
@@ -40,7 +41,7 @@ function generateEmailTemplate($message)
text-align: center;
}
.header img {
max-width: 50px; /* Ridotto da 150px a 100px per rimpicciolire il logo */
max-width: 100px; /* Ridotto da 150px a 100px per rimpicciolire il logo */
}
.content {
padding: 30px;
@@ -77,8 +78,8 @@ function generateEmailTemplate($message)
<div class="header">
<img src="https://yogasoul.it/wp-content/uploads/2021/03/logotransizione_final2.png"
alt="YogaSoul - My WordPress Blog"
width="150"
style="display: block; width: 50px; height: auto;">
width="120"
style="display: block; width: 120px; max-width: 120px; height: auto; margin: 0 auto;">
</div>
<div class="content">
@@ -95,6 +96,16 @@ function generateEmailTemplate($message)
HTML;
}
// Trasforma il testo del modulo in HTML:
// **testo** -> grassetto, link cliccabili, a capo -> <br>
function formatEmailBody(string $text): string
{
$html = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
$html = preg_replace('/\*\*([^*\n]+?)\*\*/', '<strong>$1</strong>', $html);
$html = preg_replace('~(https?://[^\s<]+)~', '<a href="$1">$1</a>', $html);
return nl2br($html);
}
// Gestione della richiesta
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
@@ -105,26 +116,15 @@ try {
$body = isset($_POST['body']) ? trim($_POST['body']) : '';
$recipients = isset($_POST['recipients']) ? json_decode($_POST['recipients'], true) : [];
if (empty($subject) || empty($body) || empty($recipients)) {
if (empty($subject) || empty($body) || empty($recipients) || !is_array($recipients)) {
throw new Exception('Dati mancanti.');
}
// Prepara tutti gli indirizzi email ricevuti
$to = array_column($recipients, 'email');
if (empty($to)) {
throw new Exception('Nessun destinatario valido trovato.');
}
// Sostituisci i newline con <br> per l'HTML
$htmlBody = nl2br($body);
$emailContent = generateEmailTemplate($htmlBody);
// Invii multipli: più tempo a disposizione dello script
@set_time_limit(300);
// Configura PHPMailer
$mail = new PHPMailer(true);
// Configurazione server SMTP con dati da .env
$mail->isSMTP();
$mail->Host = $_ENV['MAIL_HOST'];
$mail->SMTPAuth = true;
@@ -132,29 +132,59 @@ try {
$mail->Password = $_ENV['MAIL_PASSWORD'];
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
$mail->Port = (int) $_ENV['MAIL_PORT'];
$mail->Timeout = 5; // Timeout di 5 secondi
$mail->SMTPKeepAlive = false; // Disabilita la connessione persistente
$mail->Timeout = 5;
$mail->SMTPKeepAlive = true; // una sola connessione per tutti gli invii
$mail->CharSet = 'UTF-8'; // accenti ed emoji corretti
// Mittente
$mail->setFrom($_ENV['MAIL_FROM_ADDRESS'], $_ENV['MAIL_FROM_NAME']);
// Destinatari principali
foreach ($to as $recipient) {
$mail->addAddress($recipient);
}
// Contenuto dell'email
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $emailContent;
$mail->AltBody = strip_tags($emailContent); // Versione testuale dell'email
// Invia l'email
$sent = 0;
$failed = [];
// Un'email per ogni destinatario: nessuno vede gli indirizzi degli altri
foreach ($recipients as $r) {
$email = trim($r['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$failed[] = $email !== '' ? $email : '(email vuota)';
continue;
}
$nome = trim((string) ($r['nome'] ?? ''));
if ($nome === '') {
$nome = 'allieva/o';
}
$personalText = str_replace('{nome}', $nome, $body);
try {
$mail->clearAddresses();
$mail->addAddress($email, $nome);
$mail->Body = generateEmailTemplate(formatEmailBody($personalText));
$mail->AltBody = str_replace('**', '', $personalText);
$mail->send();
echo json_encode(['success' => true, 'message' => 'Email inviata con successo.']);
$sent++;
} catch (Exception $e) {
// Log dell'errore completo
$failed[] = $email;
error_log('Errore invio a ' . $email . ': ' . $mail->ErrorInfo);
$mail->getSMTPInstance()->reset();
}
}
$mail->smtpClose();
if ($sent === 0) {
throw new Exception('Nessuna email inviata.');
}
$message = 'Email inviate: ' . $sent;
if (!empty($failed)) {
$message .= '. Non inviate (' . count($failed) . '): ' . implode(', ', $failed);
}
echo json_encode(['success' => true, 'message' => $message]);
} catch (\Throwable $e) {
error_log('Errore in send_email.php: ' . $e->getMessage() . ' in ' . $e->getFile() . ' at line ' . $e->getLine());
echo json_encode(['success' => false, 'message' => "Errore nell'invio dell'email: " . $e->getMessage()]);
}
+114 -8
View File
@@ -21,6 +21,9 @@ SELECT
auth_users.last_name AS cognome_utente,
auth_users.email AS email_utente,
COALESCE(orderbook_lessons.lezioni_acquistate, 0) AS lezioni_acquistate,
MAX(CASE WHEN active_orders.iduser IS NOT NULL THEN 1 ELSE 0 END) AS ha_ordine_attivo,
MAX(cert.ultima_scadenza) AS ultima_scadenza_cert,
MAX(CASE WHEN cert.ultima_scadenza IS NULL OR cert.ultima_scadenza < CURDATE() THEN 1 ELSE 0 END) AS certificato_scaduto,
COUNT(CASE WHEN bookingclass.bookingstart <= CURDATE() AND bookingclass.lostlesson = 'N' THEN 1 END) AS lezioni_praticate,
COUNT(CASE WHEN bookingclass.bookingstart > CURDATE() THEN 1 END) AS lezioni_programmate,
COUNT(CASE WHEN bookingclass.lostlesson = 'Y' THEN 1 END) AS lezioni_perse,
@@ -34,11 +37,23 @@ LEFT JOIN (
FROM orderbook
GROUP BY iduser
) AS orderbook_lessons ON auth_users.id = orderbook_lessons.iduser
LEFT JOIN (
SELECT DISTINCT iduser
FROM orderbook
WHERE expireon >= CURDATE()
) AS active_orders ON auth_users.id = active_orders.iduser
LEFT JOIN (
SELECT iduser, MAX(expirydatedocument) AS ultima_scadenza
FROM certificateuserprofile
GROUP BY iduser
) AS cert ON auth_users.id = cert.iduser
LEFT JOIN bookingclass ON auth_users.id = bookingclass.iduser
GROUP BY auth_users.id, auth_users.last_name
ORDER BY auth_users.last_name";
$users = $pdo->query($sql)->fetchAll();
$activeUsersCount = count(array_filter($users, fn($u) => (int) $u['ha_ordine_attivo'] === 1));
$expiredCertCount = count(array_filter($users, fn($u) => (int) $u['ha_ordine_attivo'] === 1 && (int) $u['certificato_scaduto'] === 1));
?>
<!doctype html>
<html lang="it">
@@ -147,6 +162,43 @@ $users = $pdo->query($sql)->fetchAll();
filter: brightness(0.93);
}
.active-counter {
background: #cdfbc4;
color: #1a7f2e;
border-radius: 8px;
padding: 8px 14px;
font-size: 14px;
font-weight: 600;
}
.btn-email-cert {
background: #dc3545;
}
.cert-counter {
background: #fbc7c4;
color: #a12820;
}
.cert-badge {
display: inline-block;
border-radius: 6px;
padding: 2px 8px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.cert-ok {
background: #cdfbc4;
color: #1a7f2e;
}
.cert-scaduto {
background: #fbc7c4;
color: #a12820;
}
.lost-lesson {
background-color: #ffcc80 !important;
}
@@ -271,7 +323,12 @@ $users = $pdo->query($sql)->fetchAll();
<div class="section-title">Statistiche utenti</div>
<p class="section-lead">Ciao <?php echo e($firstname); ?>, clicca sul nome di un utente per il dettaglio delle lezioni.</p>
<button class="btn-email-all" id="sendEmailToAll"><i class="fas fa-envelope me-1"></i> Invia email a tutti</button>
<div class="d-flex align-items-center gap-3 flex-wrap mb-3">
<button class="btn-email-all mb-0" id="sendEmailToAll"><i class="fas fa-envelope me-1"></i> Invia email agli abbonati attivi</button>
<span class="active-counter"><i class="fas fa-user-check me-1"></i> Utenti con abbonamento attivo: <strong><?php echo $activeUsersCount; ?></strong> su <?php echo count($users); ?></span>
<button class="btn-email-all btn-email-cert mb-0" id="sendEmailCertExpired"><i class="fas fa-file-medical me-1"></i> Invia email certificato scaduto</button>
<span class="active-counter cert-counter"><i class="fas fa-triangle-exclamation me-1"></i> Abbonati con certificato scaduto: <strong><?php echo $expiredCertCount; ?></strong></span>
</div>
<div class="table-responsive">
<table id="userStatsTable" class="table table-striped">
@@ -285,6 +342,7 @@ $users = $pdo->query($sql)->fetchAll();
<th>Program.</th>
<th>Perse</th>
<th>Da progr.</th>
<th>Certificato</th>
<th>Email</th>
</tr>
</thead>
@@ -305,11 +363,25 @@ $users = $pdo->query($sql)->fetchAll();
<td><span class="stat-num stat-prog"><?php echo (int) $u['lezioni_programmate']; ?></span></td>
<td><span class="stat-num stat-perse"><?php echo (int) $u['lezioni_perse']; ?></span></td>
<td><span class="stat-num stat-daprog"><?php echo (int) $u['lezioni_da_programmare']; ?></span></td>
<td>
<?php $scad = $u['ultima_scadenza_cert']; ?>
<?php if (empty($scad)) : ?>
<span class="cert-badge cert-scaduto">Mancante</span>
<?php elseif ($scad === '0000-00-00') : ?>
<span class="cert-badge cert-scaduto">Data non valida</span>
<?php elseif ((int) $u['certificato_scaduto'] === 1) : ?>
<span class="cert-badge cert-scaduto">Scaduto <?php echo e(date('d/m/Y', strtotime($scad))); ?></span>
<?php else : ?>
<span class="cert-badge cert-ok">Valido fino al <?php echo e(date('d/m/Y', strtotime($scad))); ?></span>
<?php endif; ?>
</td>
<td>
<button class="btn btn-sm btn-secondary send-email"
data-id="<?php echo (int) $u['id_utente']; ?>"
data-nome="<?php echo e($u['nome_utente']); ?>"
data-email="<?php echo e($u['email_utente']); ?>">
data-email="<?php echo e($u['email_utente']); ?>"
data-attivo="<?php echo (int) $u['ha_ordine_attivo']; ?>"
data-certscaduto="<?php echo (int) $u['certificato_scaduto']; ?>">
<i class="fas fa-envelope"></i>
</button>
</td>
@@ -378,6 +450,7 @@ $users = $pdo->query($sql)->fetchAll();
<label for="emailBody" class="form-label">Messaggio</label>
<textarea class="form-control" id="emailBody" required></textarea>
</div>
<div class="text-muted mb-2" style="font-size:12px;">Scrivi <code>{nome}</code> per il nome del destinatario e <code>**testo**</code> per il grassetto.</div>
<div id="emailRecipientCount" class="text-muted" style="font-size:13px;"></div>
<input type="hidden" id="emailRecipients" name="recipients">
</form>
@@ -428,6 +501,29 @@ $users = $pdo->query($sql)->fetchAll();
<script>
const serverNow = '<?php echo $serverCurrentDateTime; ?>';
const TESTO_CERTIFICATO = `Gentile {nome},
ti ricordiamo che il certificato di idoneità alla pratica è **obbligatorio** per la partecipazione alle attività Yoga svolte presso YogaSoul.
Come riportato nella descrizione di ogni pacchetto da 4 o 12 lezioni acquistato, è necessario munirsi del documento di buona salute e 👉 **caricarlo sulla piattaforma YogiBOOK** ➡️ https://yogibook.yogasoul.it/ per partecipare alle lezioni.
👉 **Come fare:**
- Accedi alla tua **area personale** (le credenziali ti sono state fornite per mail)
- Seleziona la sezione **Certificati**
- Carica il file in formato **PDF o JPG**
- La data di scadenza che dovrà essere da voi inserita sarà **la stessa riportata sul documento +364 giorni** ➡️ anno 2027
- Il documento verrà da noi controllato e archiviato
👉 **Se il tuo documento è in scadenza o già scaduto** ti chiediamo di controllare e provvedere ad inserire quello nuovo.
Il certificato è indispensabile per garantire una pratica sicura e conforme alle normative vigenti ed essere in regola qualora ci fossero dei controlli interni.
**In assenza del certificato medico, la partecipazione alle attività non sarà consentita** e YogaSoul declina ogni responsabilità per documenti non caricati o non consegnati.
Ti invitiamo pertanto a procedere quanto prima, al fine di evitare sospensioni dei pacchetti o limitazioni nella partecipazione alle lezioni.
Ti ringraziamo per la collaborazione e per l’attenzione alla sicurezza di tutti.
**TEAM YogaSoul**`.replace(/^[ \t]+/gm, '');
// Helper Swal che rimuove aria-hidden (fix focus modali Bootstrap)
function alertBox(opts) {
return Swal.fire(Object.assign({
@@ -572,9 +668,9 @@ $users = $pdo->query($sql)->fetchAll();
icon: 'error'
});
})
.fail(() => alertBox({
.fail((xhr) => alertBox({
title: 'Errore',
text: 'Errore durante la rimozione.',
text: 'Errore durante l\'invio: ' + (xhr.responseText || xhr.statusText || 'nessuna risposta').substring(0, 400),
icon: 'error'
}));
}
@@ -741,9 +837,13 @@ $users = $pdo->query($sql)->fetchAll();
});
// Email a TUTTI con routine di conferma robusta
$('#sendEmailToAll').on('click', function() {
$('#sendEmailToAll, #sendEmailCertExpired').on('click', function() {
var selector = this.id === 'sendEmailCertExpired' ?
'.send-email[data-attivo="1"][data-certscaduto="1"]' :
'.send-email[data-attivo="1"]';
var isCert = this.id === 'sendEmailCertExpired';
var recipients = [];
$('.send-email').each(function() {
$(selector).each(function() {
recipients.push({
id: $(this).data('id'),
email: $(this).data('email'),
@@ -789,7 +889,13 @@ $users = $pdo->query($sql)->fetchAll();
}).then((result) => {
if (result.isConfirmed) {
$('#emailModalLabel').text('Invia email a ' + recipients.length + ' utenti');
$('#emailBody').val('Ciao,\n\n');
if (isCert) {
$('#emailSubject').val('Promemoria - Certificato idoneità alla pratica Yoga');
$('#emailBody').val(TESTO_CERTIFICATO);
} else {
$('#emailSubject').val('');
$('#emailBody').val('Ciao {nome},\n\n');
}
$('#emailRecipientCount').text(recipients.length + ' destinatari');
$('#emailRecipients').val(JSON.stringify(recipients));
$('#emailModal').modal('show');
@@ -811,7 +917,7 @@ $users = $pdo->query($sql)->fetchAll();
return;
}
$.post('/public/send_email.php', {
$.post('send_email.php', {
subject: subject,
body: body,
recipients: recipients
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB