From 1c2ee7d6468c3c640fbbcfd8f082bd6fd5065cc2 Mon Sep 17 00:00:00 2001 From: solocla Date: Mon, 5 Oct 2026 09:13:23 +0200 Subject: [PATCH] Situazione allievi per scuola: dettaglio, riprogrammazione ripristinata, email di massa sicura --- public/class/SchoolMail.php | 135 +++++++++++++------- public/deleteclass.php | 59 +++++---- public/fetch_class_details.php | 103 ++++++--------- public/mark_lost.php | 71 ++++------- public/send_email.php | 227 +++++++++++---------------------- public/situationusers.php | 213 +++++++++++++++---------------- 6 files changed, 357 insertions(+), 451 deletions(-) diff --git a/public/class/SchoolMail.php b/public/class/SchoolMail.php index d1f7d31..26c466d 100644 --- a/public/class/SchoolMail.php +++ b/public/class/SchoolMail.php @@ -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: 10-03-2026 18:30', - * ], '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 = "

"; $pe = "

"; $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 = "

Ciao " . $h($firstName) . ",

"; + $greeting = trim($firstName) !== '' ? 'Ciao ' . $h($firstName) . ',' : 'Ciao,'; + $body = "

{$greeting}

"; foreach ($lines as $line) { $body .= $p . $line . $pe; } @@ -63,36 +134,6 @@ class SchoolMail . "YogiBook - " . $h($schoolName) . ""; $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; } } diff --git a/public/deleteclass.php b/public/deleteclass.php index 01a568c..5e1ec5c 100644 --- a/public/deleteclass.php +++ b/public/deleteclass.php @@ -1,32 +1,37 @@ - - 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); +} diff --git a/public/fetch_class_details.php b/public/fetch_class_details.php index 41ab324..f7e8070 100644 --- a/public/fetch_class_details.php +++ b/public/fetch_class_details.php @@ -1,75 +1,46 @@ '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; -?> \ No newline at end of file +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); +} diff --git a/public/mark_lost.php b/public/mark_lost.php index b97aa59..9a24d18 100644 --- a/public/mark_lost.php +++ b/public/mark_lost.php @@ -1,54 +1,37 @@ 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; \ No newline at end of file +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); +} diff --git a/public/send_email.php b/public/send_email.php index accb177..bd6d6af 100644 --- a/public/send_email.php +++ b/public/send_email.php @@ -1,160 +1,79 @@ - - - - - Email da YogiBook - - - -
-
-
- YogaSoul - My WordPress Blog - -
-
-

$message

-
- -
-
- - -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
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'] : '') . '.', +]); diff --git a/public/situationusers.php b/public/situationusers.php index aaefc9b..ab2ff5f 100644 --- a/public/situationusers.php +++ b/public/situationusers.php @@ -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); ?> @@ -328,7 +343,7 @@ $users = $pdo->query($sql)->fetchAll(); -