Situazione allievi per scuola: dettaglio, riprogrammazione ripristinata, email di massa sicura
This commit is contained in:
+88
-47
@@ -2,19 +2,12 @@
|
||||
|
||||
/**
|
||||
* SchoolMail
|
||||
* Invio email agli allievi a nome della scuola corrente, con il template standard
|
||||
* (mail/emailtemplate2.php). Se l'invio fallisce registra l'errore e ritorna false:
|
||||
* non deve mai bloccare l'operazione che l'ha generata.
|
||||
* Email agli allievi a nome di una scuola, con il template standard (mail/emailtemplate2.php).
|
||||
* Se l'invio fallisce registra l'errore e ritorna false: non blocca mai l'operazione.
|
||||
*
|
||||
* Uso:
|
||||
* SchoolMail::toStudent($email, $firstname, 'Oggetto', [
|
||||
* 'Prima riga del messaggio.',
|
||||
* 'Data e ora: <strong>10-03-2026 18:30</strong>',
|
||||
* ], 'Testo semplice alternativo');
|
||||
*
|
||||
* Opzioni:
|
||||
* $school riga della tabella schools (default: scuola corrente)
|
||||
* $ccSchool true = copia all'email della scuola
|
||||
* SchoolMail::toStudent($email, $firstname, 'Oggetto', ['riga 1', 'riga 2']);
|
||||
* SchoolMail::toStudent($email, $firstname, 'Oggetto', $lines, '', $schoolRow, true); // copia alla scuola
|
||||
* SchoolMail::toMany([['email'=>..,'first_name'=>..], ...], 'Oggetto', $lines); // invio di massa
|
||||
*
|
||||
* Le righe sono HTML: i dati variabili vanno passati già con htmlspecialchars().
|
||||
*/
|
||||
@@ -35,20 +28,98 @@ class SchoolMail
|
||||
?array $school = null,
|
||||
bool $ccSchool = false
|
||||
): bool {
|
||||
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
$school = $school ?? SchoolContext::get();
|
||||
$mail = self::mailer($school);
|
||||
try {
|
||||
$mail->addAddress($to);
|
||||
if ($ccSchool && !empty($school['email']) && filter_var($school['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addCC($school['email'], (string) $school['name']);
|
||||
}
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = self::render($firstName, $lines, $school);
|
||||
$mail->AltBody = $altText !== '' ? $altText : strip_tags(implode("\n", $lines));
|
||||
$mail->send();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
error_log('SchoolMail error (' . $to . '): ' . $mail->ErrorInfo);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$school = $school ?? SchoolContext::get();
|
||||
/**
|
||||
* Invio di massa con una sola connessione SMTP.
|
||||
* @param array $recipients [['email' => ..., 'first_name' => ...], ...]
|
||||
* @return array{sent:int, failed:int}
|
||||
*/
|
||||
public static function toMany(array $recipients, string $subject, array $lines, ?array $school = null): array
|
||||
{
|
||||
$school = $school ?? SchoolContext::get();
|
||||
$mail = self::mailer($school);
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$sent = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($recipients as $r) {
|
||||
$to = (string) ($r['email'] ?? '');
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$mail->clearAddresses();
|
||||
$mail->addAddress($to);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = self::render((string) ($r['first_name'] ?? ''), $lines, $school);
|
||||
$mail->AltBody = strip_tags(implode("\n", $lines));
|
||||
$mail->send();
|
||||
$sent++;
|
||||
} catch (Exception $e) {
|
||||
error_log('SchoolMail bulk error (' . $to . '): ' . $mail->ErrorInfo);
|
||||
$failed++;
|
||||
$mail->getSMTPInstance()->reset();
|
||||
}
|
||||
}
|
||||
$mail->smtpClose();
|
||||
return ['sent' => $sent, 'failed' => $failed];
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private static function mailer(array $school): PHPMailer
|
||||
{
|
||||
$schoolName = (string) ($school['name'] ?? 'YogiBook');
|
||||
$appUrl = rtrim($_ENV['APP_URL'] ?? 'https://yogibook.yogasoul.it', '/');
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
|
||||
$mail->Port = (int) $_ENV['MAIL_PORT'];
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 20;
|
||||
$mail->isHTML(true);
|
||||
$mail->setFrom($_ENV['MAIL_FROM_ADDRESS'], $schoolName ?: ($_ENV['MAIL_FROM_NAME'] ?? 'YogiBook'));
|
||||
if (!empty($school['email']) && filter_var($school['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addReplyTo($school['email'], $schoolName);
|
||||
}
|
||||
return $mail;
|
||||
}
|
||||
|
||||
private static function render(string $firstName, array $lines, array $school): string
|
||||
{
|
||||
$schoolName = (string) ($school['name'] ?? 'YogiBook');
|
||||
$appUrl = rtrim($_ENV['APP_URL'] ?? 'https://yogibook.yogasoul.it', '/');
|
||||
$h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
$p = "<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>";
|
||||
$pe = "</span></p>";
|
||||
$btn = "box-sizing: border-box; display: inline-block; text-decoration: none; text-align: center; color: #FFFFFF; background-color: #3AAEE0; border-radius: 4px; font-size: 14px;";
|
||||
|
||||
$body = "<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 18px; line-height: 34.2px;'><strong>Ciao " . $h($firstName) . ",</strong></span></p>";
|
||||
$greeting = trim($firstName) !== '' ? 'Ciao ' . $h($firstName) . ',' : 'Ciao,';
|
||||
$body = "<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 18px; line-height: 34.2px;'><strong>{$greeting}</strong></span></p>";
|
||||
foreach ($lines as $line) {
|
||||
$body .= $p . $line . $pe;
|
||||
}
|
||||
@@ -63,36 +134,6 @@ class SchoolMail
|
||||
. "<span style='display: block; padding: 10px 20px; line-height: 120%;'>YogiBook - " . $h($schoolName) . "</span></a>";
|
||||
$mailmessage1 = '';
|
||||
include __DIR__ . '/../mail/emailtemplate2.php';
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
|
||||
$mail->Port = (int) $_ENV['MAIL_PORT'];
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->isHTML(true);
|
||||
|
||||
$mail->setFrom($_ENV['MAIL_FROM_ADDRESS'], $schoolName ?: ($_ENV['MAIL_FROM_NAME'] ?? 'YogiBook'));
|
||||
if (!empty($school['email'])) {
|
||||
$mail->addReplyTo($school['email'], $schoolName);
|
||||
}
|
||||
$mail->addAddress($to);
|
||||
if ($ccSchool && !empty($school['email']) && filter_var($school['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addCC($school['email'], $schoolName);
|
||||
}
|
||||
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $mailmessage1 ?: $body;
|
||||
$mail->AltBody = $altText !== '' ? $altText : strip_tags(implode("\n", $lines));
|
||||
$mail->send();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
error_log('SchoolMail error (' . $to . '): ' . $mail->ErrorInfo);
|
||||
return false;
|
||||
}
|
||||
return $mailmessage1 ?: $body;
|
||||
}
|
||||
}
|
||||
|
||||
+32
-27
@@ -1,32 +1,37 @@
|
||||
<?php require_once('Connections/bkngstm.php'); ?>
|
||||
<?php require_once('webassist/mysqli/rsobj.php'); ?>
|
||||
<?php
|
||||
// Buffer: scarta l'HTML degli include, risposta JSON pura
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
|
||||
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
|
||||
// Verifica della connessione
|
||||
if ($conn->connect_error) {
|
||||
die("Connessione fallita: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
// Ottieni l'ID dall'URL
|
||||
if (isset($_GET['id'])) {
|
||||
$id = $_GET['id'];
|
||||
|
||||
// Cancellazione del record dalla tabella bookingclass
|
||||
$sql = "DELETE FROM bookingclass WHERE idbookingclass = $id";
|
||||
|
||||
if ($conn->query($sql) === TRUE) {
|
||||
// Reindirizzamento alla pagina userpanel.php
|
||||
header("Location: userpanel.php");
|
||||
exit();
|
||||
} else {
|
||||
echo "Errore durante la cancellazione del record: " . $conn->error;
|
||||
/**
|
||||
* Rimozione di una prenotazione da parte dello staff (da situationusers.php).
|
||||
* POST: id = idbookingclass. Risposta: { success, message }
|
||||
*/
|
||||
function jsonOut(array $payload, int $code = 200): void
|
||||
{
|
||||
if (ob_get_length() !== false) {
|
||||
ob_end_clean();
|
||||
}
|
||||
} else {
|
||||
echo "ID non fornito.";
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
if (!SchoolContext::isStaff()) {
|
||||
jsonOut(['success' => false, 'message' => 'Accesso non autorizzato.'], 403);
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if (!SchoolScope::belongs('bookingclass', $id)) {
|
||||
jsonOut(['success' => false, 'message' => 'Prenotazione non trovata.'], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$pdo->prepare("DELETE FROM bookingclass WHERE idbookingclass = ? AND idschool = ?")->execute([$id, $idschool]);
|
||||
jsonOut(['success' => true]);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('deleteclass: ' . $ex->getMessage());
|
||||
jsonOut(['success' => false, 'message' => 'Errore durante la rimozione.'], 500);
|
||||
}
|
||||
|
||||
@@ -1,75 +1,46 @@
|
||||
<?php
|
||||
// Start output buffering to capture any unintended output
|
||||
// Buffer: scarta l'HTML degli include, risposta JSON pura
|
||||
ob_start();
|
||||
|
||||
// Enable error reporting for debugging (remove in production)
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Include the headscript file (ensure it doesn't output anything)
|
||||
require_once('include/headscript.php');
|
||||
|
||||
// Establish database connection
|
||||
$conn = mysqli_connect($servername, $username, $password, $dbname);
|
||||
if (!$conn) {
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Connessione al database fallita']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if id_utente is provided
|
||||
if (isset($_POST['id_utente'])) {
|
||||
$id_utente = intval($_POST['id_utente']);
|
||||
|
||||
// Query to fetch class details with necessary fields
|
||||
$sql = "
|
||||
SELECT
|
||||
bc.idbookingclass,
|
||||
bc.bookingstart,
|
||||
bc.status,
|
||||
bc.lostlesson,
|
||||
bc.prevbookingstart,
|
||||
bc.idorder,
|
||||
ob.order_date_created,
|
||||
s.servicename,
|
||||
s.day,
|
||||
s.time
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN orderbook ob ON bc.idorder = ob.idorderbook
|
||||
LEFT JOIN service s ON bc.idservice = s.idservice
|
||||
WHERE bc.iduser = $id_utente
|
||||
ORDER BY bc.bookingstart DESC
|
||||
";
|
||||
|
||||
$result = mysqli_query($conn, $sql);
|
||||
if (!$result) {
|
||||
/**
|
||||
* Lezioni di un allievo nella scuola corrente (dettaglio di situationusers.php).
|
||||
* POST: id_utente. Risposta: array di lezioni, oppure { error }.
|
||||
*/
|
||||
function jsonOut($payload, int $code = 200): void
|
||||
{
|
||||
if (ob_get_length() !== false) {
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Errore nella query: ' . mysqli_error($conn)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$classes = [];
|
||||
|
||||
// Fetch all rows and store in an array
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$classes[] = $row;
|
||||
}
|
||||
|
||||
// Clear the output buffer, set the header, and output JSON
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($classes);
|
||||
mysqli_close($conn);
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// If id_utente is not provided, return an error
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'ID utente non fornito']);
|
||||
mysqli_close($conn);
|
||||
exit;
|
||||
?>
|
||||
if (!SchoolContext::isSchoolAdmin()) {
|
||||
jsonOut(['error' => 'Accesso non autorizzato.'], 403);
|
||||
}
|
||||
|
||||
$userId = (int) ($_POST['id_utente'] ?? 0);
|
||||
if ($userId <= 0 || !SchoolScope::isMember($userId)) {
|
||||
jsonOut([]);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$st = $pdo->prepare(
|
||||
"SELECT bc.idbookingclass, bc.bookingstart, bc.status, bc.lostlesson, bc.prevbookingstart,
|
||||
bc.idorder, ob.order_date_created, s.servicename, s.day, s.time
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN orderbook ob ON bc.idorder = ob.idorderbook
|
||||
LEFT JOIN service s ON bc.idservice = s.idservice
|
||||
WHERE bc.iduser = ? AND bc.idschool = ? AND bc.status <> 'cancelled'
|
||||
ORDER BY bc.bookingstart DESC"
|
||||
);
|
||||
$st->execute([$userId, $idschool]);
|
||||
jsonOut($st->fetchAll());
|
||||
} catch (Throwable $ex) {
|
||||
error_log('fetch_class_details: ' . $ex->getMessage());
|
||||
jsonOut(['error' => 'Errore nel caricamento.'], 500);
|
||||
}
|
||||
|
||||
+27
-44
@@ -1,54 +1,37 @@
|
||||
<?php
|
||||
// mark_lost.php
|
||||
|
||||
// Start output buffering to capture any unexpected output
|
||||
// Buffer: scarta l'HTML degli include, risposta JSON pura
|
||||
ob_start();
|
||||
|
||||
// Set the content type to JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Include database connection (assuming headscript.php contains this)
|
||||
require_once('include/headscript.php');
|
||||
|
||||
// Suppress errors to prevent them from being output
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(0);
|
||||
|
||||
// Connect to the database
|
||||
$conn = mysqli_connect($servername, $username, $password, $dbname);
|
||||
|
||||
if (!$conn) {
|
||||
// Clean the output buffer before sending the response
|
||||
ob_end_clean();
|
||||
echo json_encode(['success' => false, 'message' => 'Connessione al database fallita']);
|
||||
/**
|
||||
* Segna come persa una prenotazione (da situationusers.php).
|
||||
* POST: id = idbookingclass. Risposta: { success, message }
|
||||
*/
|
||||
function jsonOut(array $payload, int $code = 200): void
|
||||
{
|
||||
if (ob_get_length() !== false) {
|
||||
ob_end_clean();
|
||||
}
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_POST['id'])) {
|
||||
$id = intval($_POST['id']);
|
||||
|
||||
$sql = "UPDATE bookingclass SET lostlesson = 'Y' WHERE idbookingclass = ?";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'i', $id);
|
||||
|
||||
if (mysqli_stmt_execute($stmt)) {
|
||||
// Clean the output buffer before sending the response
|
||||
ob_end_clean();
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
// Clean the output buffer before sending the response
|
||||
ob_end_clean();
|
||||
echo json_encode(['success' => false, 'message' => 'Errore durante l\'aggiornamento: ' . mysqli_error($conn)]);
|
||||
}
|
||||
|
||||
mysqli_stmt_close($stmt);
|
||||
} else {
|
||||
// Clean the output buffer before sending the response
|
||||
ob_end_clean();
|
||||
echo json_encode(['success' => false, 'message' => 'ID non fornito']);
|
||||
if (!SchoolContext::isStaff()) {
|
||||
jsonOut(['success' => false, 'message' => 'Accesso non autorizzato.'], 403);
|
||||
}
|
||||
|
||||
mysqli_close($conn);
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if (!SchoolScope::belongs('bookingclass', $id)) {
|
||||
jsonOut(['success' => false, 'message' => 'Prenotazione non trovata.'], 404);
|
||||
}
|
||||
|
||||
// Ensure no extra output is sent
|
||||
exit;
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$pdo->prepare("UPDATE bookingclass SET lostlesson = 'Y' WHERE idbookingclass = ? AND idschool = ?")->execute([$id, $idschool]);
|
||||
jsonOut(['success' => true]);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('mark_lost: ' . $ex->getMessage());
|
||||
jsonOut(['success' => false, 'message' => 'Errore durante l\'aggiornamento.'], 500);
|
||||
}
|
||||
|
||||
+73
-154
@@ -1,160 +1,79 @@
|
||||
<?php
|
||||
// Includi il file mailer.php per caricare le dipendenze e le variabili .env
|
||||
require_once 'class/mailer.php';
|
||||
// Buffer: scarta l'HTML degli include, risposta JSON pura
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
require_once('class/SchoolMail.php');
|
||||
|
||||
// Importa le classi necessarie
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
// Imposta l'header per JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Funzione per generare il template HTML dell'email
|
||||
function generateEmailTemplate($message)
|
||||
/**
|
||||
* Invio email dello staff agli allievi della scuola corrente (da situationusers.php).
|
||||
* POST: subject, body (testo semplice), recipients (JSON: [{id:..}, ...]).
|
||||
* Gli indirizzi vengono SEMPRE presi dal database e solo per i membri della scuola:
|
||||
* eventuali email inviate dal browser vengono ignorate.
|
||||
*/
|
||||
function jsonOut(array $payload, int $code = 200): void
|
||||
{
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email da YogiBook</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background-color: #e6f0fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.header img {
|
||||
max-width: 50px; /* Ridotto da 150px a 100px per rimpicciolire il logo */
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.content p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.footer {
|
||||
background-color: #e6f0fa;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
.footer a {
|
||||
color: #4a90e2;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.yoga-decoration {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: linear-gradient(to right, #4a90e2, #87ceeb);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="yoga-decoration"></div>
|
||||
<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;">
|
||||
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>$message</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Ti aspettiamo alla tua prossima lezione di yoga!</p>
|
||||
<p><a href="https://yogasoul.it">YogaSoul</a> | <a href="mailto:info@yogasoul.it">Contattaci</a></p>
|
||||
</div>
|
||||
<div class="yoga-decoration"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
if (ob_get_length() !== false) {
|
||||
ob_end_clean();
|
||||
}
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Gestione della richiesta
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new Exception('Metodo non consentito.');
|
||||
}
|
||||
|
||||
$subject = isset($_POST['subject']) ? trim($_POST['subject']) : '';
|
||||
$body = isset($_POST['body']) ? trim($_POST['body']) : '';
|
||||
$recipients = isset($_POST['recipients']) ? json_decode($_POST['recipients'], true) : [];
|
||||
|
||||
if (empty($subject) || empty($body) || empty($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);
|
||||
|
||||
// Configura PHPMailer
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
// Configurazione server SMTP con dati da .env
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$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
|
||||
|
||||
// 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
|
||||
$mail->send();
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Email inviata con successo.']);
|
||||
} catch (Exception $e) {
|
||||
// Log dell'errore completo
|
||||
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()]);
|
||||
if (!SchoolContext::isSchoolAdmin()) {
|
||||
jsonOut(['success' => false, 'message' => 'Accesso non autorizzato.'], 403);
|
||||
}
|
||||
|
||||
$subject = trim((string) ($_POST['subject'] ?? ''));
|
||||
$body = trim((string) ($_POST['body'] ?? ''));
|
||||
$list = json_decode((string) ($_POST['recipients'] ?? '[]'), true);
|
||||
|
||||
if ($subject === '' || $body === '' || !is_array($list) || empty($list)) {
|
||||
jsonOut(['success' => false, 'message' => 'Oggetto, messaggio e destinatari sono obbligatori.']);
|
||||
}
|
||||
if (mb_strlen($subject) > 200 || mb_strlen($body) > 20000) {
|
||||
jsonOut(['success' => false, 'message' => 'Oggetto o messaggio troppo lunghi.']);
|
||||
}
|
||||
|
||||
$ids = array_values(array_unique(array_filter(array_map(fn($r) => (int) ($r['id'] ?? 0), $list))));
|
||||
if (empty($ids) || count($ids) > 1000) {
|
||||
jsonOut(['success' => false, 'message' => 'Elenco destinatari non valido.']);
|
||||
}
|
||||
|
||||
// Solo membri attivi della scuola, email dal DB
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$st = $pdo->prepare(
|
||||
"SELECT u.email, u.first_name
|
||||
FROM auth_users u
|
||||
JOIN school_user su ON su.user_id = u.id AND su.idschool = ? AND su.active = 'Y'
|
||||
WHERE u.id IN ($ph)"
|
||||
);
|
||||
$st->execute(array_merge([$idschool], $ids));
|
||||
$recipients = $st->fetchAll();
|
||||
|
||||
if (empty($recipients)) {
|
||||
jsonOut(['success' => false, 'message' => 'Nessun destinatario valido in questa scuola.']);
|
||||
}
|
||||
|
||||
// Il messaggio dell'admin è testo semplice: righe/paragrafi, con escape
|
||||
$lines = [];
|
||||
foreach (preg_split("/\R{2,}/", $body) as $para) {
|
||||
$para = trim($para);
|
||||
if ($para !== '') {
|
||||
$lines[] = nl2br(htmlspecialchars($para, ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
}
|
||||
// Se l'admin ha già scritto "Ciao ..." in apertura, il template aggiunge comunque il saluto personalizzato:
|
||||
if (!empty($lines) && preg_match('/^ciao\b/i', strip_tags($lines[0]))) {
|
||||
array_shift($lines);
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
$res = SchoolMail::toMany($recipients, $subject, $lines);
|
||||
|
||||
jsonOut([
|
||||
'success' => $res['sent'] > 0,
|
||||
'message' => 'Email inviate: ' . $res['sent'] . ($res['failed'] ? ' · non inviate: ' . $res['failed'] : '') . '.',
|
||||
]);
|
||||
|
||||
+100
-113
@@ -2,9 +2,10 @@
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Statistiche utenti: lezioni acquistate/praticate/programmate/perse/da programmare,
|
||||
* con dettaglio lezioni per utente, riprogrammazione e invio email (singola/massa).
|
||||
* Situazione allievi della scuola corrente: lezioni acquistate/praticate/programmate/
|
||||
* perse/da programmare, dettaglio lezioni, riprogrammazione e invio email.
|
||||
*/
|
||||
SchoolContext::requireSchoolAdmin();
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
@@ -16,29 +17,43 @@ $serverCurrentDateTime = date('Y-m-d H:i:s');
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
auth_users.id AS id_utente,
|
||||
auth_users.first_name AS nome_utente,
|
||||
auth_users.last_name AS cognome_utente,
|
||||
auth_users.email AS email_utente,
|
||||
COALESCE(orderbook_lessons.lezioni_acquistate, 0) AS lezioni_acquistate,
|
||||
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,
|
||||
COALESCE(orderbook_lessons.lezioni_acquistate, 0)
|
||||
- COUNT(CASE WHEN bookingclass.bookingstart > CURDATE() THEN 1 END)
|
||||
- COUNT(CASE WHEN bookingclass.bookingstart <= CURDATE() AND bookingclass.lostlesson = 'N' THEN 1 END)
|
||||
- COUNT(CASE WHEN bookingclass.lostlesson = 'Y' THEN 1 END) AS lezioni_da_programmare
|
||||
FROM auth_users
|
||||
u.id AS id_utente,
|
||||
u.first_name AS nome_utente,
|
||||
u.last_name AS cognome_utente,
|
||||
u.email AS email_utente,
|
||||
COALESCE(ol.lezioni_acquistate, 0) AS lezioni_acquistate,
|
||||
COUNT(CASE WHEN bc.bookingstart <= NOW() AND bc.lostlesson = 'N' THEN 1 END) AS lezioni_praticate,
|
||||
COUNT(CASE WHEN bc.bookingstart > NOW() AND bc.lostlesson = 'N' THEN 1 END) AS lezioni_programmate,
|
||||
COUNT(CASE WHEN bc.lostlesson = 'Y' THEN 1 END) AS lezioni_perse
|
||||
FROM school_user su
|
||||
JOIN auth_users u ON u.id = su.user_id
|
||||
LEFT JOIN (
|
||||
SELECT iduser, SUM(quantityclass) AS lezioni_acquistate
|
||||
FROM orderbook
|
||||
GROUP BY iduser
|
||||
) AS orderbook_lessons ON auth_users.id = orderbook_lessons.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";
|
||||
FROM orderbook
|
||||
WHERE idschool = :idschool1
|
||||
GROUP BY iduser
|
||||
) ol ON ol.iduser = u.id
|
||||
LEFT JOIN bookingclass bc
|
||||
ON bc.iduser = u.id
|
||||
AND bc.idschool = :idschool2
|
||||
AND bc.status <> 'cancelled'
|
||||
WHERE su.idschool = :idschool3
|
||||
AND su.active = 'Y'
|
||||
AND su.school_role = 'student'
|
||||
GROUP BY u.id, u.first_name, u.last_name, u.email, ol.lezioni_acquistate
|
||||
ORDER BY u.last_name, u.first_name";
|
||||
|
||||
$users = $pdo->query($sql)->fetchAll();
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':idschool1' => $idschool, ':idschool2' => $idschool, ':idschool3' => $idschool]);
|
||||
$users = $st->fetchAll();
|
||||
|
||||
foreach ($users as &$u) {
|
||||
$u['lezioni_da_programmare'] = max(
|
||||
0,
|
||||
(int) $u['lezioni_acquistate'] - (int) $u['lezioni_praticate'] - (int) $u['lezioni_programmate'] - (int) $u['lezioni_perse']
|
||||
);
|
||||
}
|
||||
unset($u);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
@@ -328,7 +343,7 @@ $users = $pdo->query($sql)->fetchAll();
|
||||
</div>
|
||||
|
||||
<!-- Modal dettaglio classi -->
|
||||
<div class="modal fade" id="classModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal fade" id="classModal" tabindex="-1" aria-hidden="true" data-bs-focus="false">
|
||||
<div class="modal-dialog modal-xl modal-xl-custom">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -622,106 +637,78 @@ $users = $pdo->query($sql)->fetchAll();
|
||||
});
|
||||
});
|
||||
|
||||
// Riprogramma: apri modale
|
||||
// Riprogramma (stesso flusso del pannello classi)
|
||||
$(document).on('click', '.reprogram-class', function() {
|
||||
var id = $(this).data('id'),
|
||||
servicename = $(this).data('servicename'),
|
||||
day = $(this).data('day'),
|
||||
time = $(this).data('time');
|
||||
$('#reprogramIdBookingClass').val(id);
|
||||
$('#reprogramModalLabel').text('Riprogramma: ' + servicename);
|
||||
$.ajax({
|
||||
url: 'fetch_available_classes.php',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
var sel = $('#classSelect').empty();
|
||||
$.each(data, function(i, c) {
|
||||
var opt = $('<option>').val(c.id).text(c.servicename + ' (' + c.day + ' ' + c.time + ')');
|
||||
if (c.servicename === servicename && c.day === day && c.time === time) opt.prop('selected', true);
|
||||
sel.append(opt);
|
||||
});
|
||||
$('#reprogramDate').datepicker({
|
||||
dateFormat: 'yy-mm-dd',
|
||||
changeMonth: true,
|
||||
changeYear: true
|
||||
});
|
||||
loadAvailableDates(sel.val());
|
||||
$('#reprogramModal').modal('show');
|
||||
},
|
||||
error: () => alertBox({
|
||||
title: 'Errore',
|
||||
text: 'Errore nel caricamento delle classi.',
|
||||
icon: 'error'
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
$('#classSelect').on('change', function() {
|
||||
loadAvailableDates($(this).val());
|
||||
});
|
||||
|
||||
function loadAvailableDates(classId) {
|
||||
$.ajax({
|
||||
url: 'fetch_available_dates.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
class_id: classId
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
$('#reprogramDate').datepicker('option', 'beforeShowDay', function(date) {
|
||||
var s = $.datepicker.formatDate('yy-mm-dd', date);
|
||||
return [data.availableDates.includes(s), ''];
|
||||
});
|
||||
},
|
||||
error: () => alertBox({
|
||||
title: 'Errore',
|
||||
text: 'Errore nel caricamento delle date.',
|
||||
icon: 'error'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$('#reprogramBtn').on('click', function() {
|
||||
var id = $('#reprogramIdBookingClass').val(),
|
||||
classId = $('#classSelect').val(),
|
||||
newDate = $('#reprogramDate').val();
|
||||
var idbookingclass = $(this).data('id');
|
||||
var idU = $('#classModal').data('current-id'),
|
||||
nomeU = $('#userName').text();
|
||||
if (!newDate || !classId) {
|
||||
alertBox({
|
||||
title: 'Errore',
|
||||
text: 'Seleziona classe e data.',
|
||||
icon: 'error'
|
||||
|
||||
$.getJSON('get_available_classes.php', {
|
||||
t: Date.now()
|
||||
}).done(function(data) {
|
||||
if (data.error) {
|
||||
alertBox({
|
||||
title: 'Errore',
|
||||
text: data.error,
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var now = new Date(),
|
||||
selectedSet = false,
|
||||
opts = '';
|
||||
(data.classes || []).forEach(function(c) {
|
||||
var cd = new Date((c.iso_dateschedule || '').replace(' ', 'T'));
|
||||
var sel = (!selectedSet && cd >= now) ? 'selected' : '';
|
||||
if (sel) selectedSet = true;
|
||||
opts += '<option value="' + c.idserviceschedule + '" ' + sel + '>' +
|
||||
$('<div>').text(c.servicename + ' - ' + c.display_dateschedule).html() +
|
||||
' (' + c.occupied_seats + '/' + c.maxcapacity + ')</option>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
$.post('reprogram_class.php', {
|
||||
id: id,
|
||||
class_id: classId,
|
||||
new_date: newDate
|
||||
}, null, 'json')
|
||||
.done(function(resp) {
|
||||
if (resp.success) {
|
||||
if (!opts) {
|
||||
alertBox({
|
||||
title: 'Nessuna lezione',
|
||||
text: 'Non ci sono lezioni disponibili.',
|
||||
icon: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
alertBox({
|
||||
title: 'Riprogramma lezione',
|
||||
html: '<select id="rp-select" class="form-select">' + opts + '</select>' +
|
||||
'<div class="form-check mt-3 text-start"><input class="form-check-input" type="checkbox" id="rp-flag" checked>' +
|
||||
'<label class="form-check-label" for="rp-flag">Conta come riprogrammazione del pacchetto</label></div>',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Riprogramma',
|
||||
cancelButtonText: 'Annulla',
|
||||
preConfirm: () => ({
|
||||
idbookingclass: idbookingclass,
|
||||
idserviceschedule: document.getElementById('rp-select').value,
|
||||
is_reprogrammed: document.getElementById('rp-flag').checked ? 'Y' : 'N'
|
||||
})
|
||||
}).then(function(r) {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('reprogramclass.php', r.value, null, 'json').done(function() {
|
||||
alertBox({
|
||||
title: 'Fatto',
|
||||
text: 'Lezione riprogrammata.',
|
||||
icon: 'success'
|
||||
});
|
||||
$('#reprogramModal').modal('hide');
|
||||
refreshTable(idU, nomeU);
|
||||
} else alertBox({
|
||||
title: 'Errore',
|
||||
text: resp.message || 'Errore durante la riprogrammazione.',
|
||||
icon: 'error'
|
||||
}).fail(function(xhr) {
|
||||
var msg = (xhr.responseJSON && xhr.responseJSON.error) || 'Errore durante la riprogrammazione.';
|
||||
alertBox({
|
||||
title: 'Errore',
|
||||
text: msg,
|
||||
icon: 'error'
|
||||
});
|
||||
});
|
||||
})
|
||||
.fail(() => alertBox({
|
||||
title: 'Errore',
|
||||
text: 'Errore durante la riprogrammazione.',
|
||||
icon: 'error'
|
||||
}));
|
||||
});
|
||||
}).fail(() => alertBox({
|
||||
title: 'Errore',
|
||||
text: 'Errore nel caricamento delle lezioni.',
|
||||
icon: 'error'
|
||||
}));
|
||||
});
|
||||
|
||||
// Email singola
|
||||
@@ -811,7 +798,7 @@ $users = $pdo->query($sql)->fetchAll();
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('/public/send_email.php', {
|
||||
$.post('send_email.php', {
|
||||
subject: subject,
|
||||
body: body,
|
||||
recipients: recipients
|
||||
|
||||
Reference in New Issue
Block a user