Situazione allievi per scuola: dettaglio, riprogrammazione ripristinata, email di massa sicura

This commit is contained in:
2026-10-05 09:13:23 +02:00
parent 2dec8bf20a
commit 1c2ee7d646
6 changed files with 357 additions and 451 deletions
+88 -47
View File
@@ -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
View File
@@ -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);
}
+37 -66
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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