added notification and cancellation mail

This commit is contained in:
Claudio 2025-10-08 17:33:26 +02:00
parent 8c3c3982ac
commit 26fb165c98
18 changed files with 1221 additions and 365 deletions

2
.env
View File

@ -1,5 +1,5 @@
APP_ENV=production APP_ENV=production
APP_DEBUG=false APP_DEBUG=true
APP_KEY=base64:aj3bR0zA9I8nZ1Rm5alncE4QFTPNoHVkd8YSRJEImwY= APP_KEY=base64:aj3bR0zA9I8nZ1Rm5alncE4QFTPNoHVkd8YSRJEImwY=
APP_URL=https://yogibook.yogasoul.it APP_URL=https://yogibook.yogasoul.it

View File

@ -0,0 +1,139 @@
<?php
// Abilita visualizzazione errori PHP
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// Connessione al database
include('include/headscript.php');
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connessione al database fallita: " . $conn->connect_error);
}
// Recupera parametri GET: idbookingclass e token
if (isset($_GET['idbookingclass']) && isset($_GET['token'])) {
$idbookingclass = $_GET['idbookingclass'];
$token = $_GET['token'];
// Verifica validità: token corrisponde, lezione futura e prima delle 12:00 del giorno
$query = "SELECT * FROM bookingclass
WHERE idbookingclass = ?
AND cancellation_token = ?
AND status = 'booked'
AND bookingstart > NOW()
AND NOW() <= DATE_ADD(DATE(bookingstart), INTERVAL 12 HOUR)";
$stmt = $conn->prepare($query);
if ($stmt) {
$stmt->bind_param("is", $idbookingclass, $token);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$bookingstart = $row['bookingstart'];
$newtimeformat = date("d-m-Y H:i", strtotime($bookingstart));
// Aggiorna status a 'cancelled' e invalida token
$updateQuery = "UPDATE bookingclass
SET status = 'cancelled', cancellation_token = NULL
WHERE idbookingclass = ?";
$updateStmt = $conn->prepare($updateQuery);
$updateStmt->bind_param("i", $idbookingclass);
$updateStmt->execute();
// Recupera dati utente e servizio
$dataQuery = "SELECT bookingclass.*, auth_users.*, service.*
FROM bookingclass
LEFT JOIN auth_users ON bookingclass.iduser = auth_users.id
LEFT JOIN service ON bookingclass.idservice = service.idservice
WHERE bookingclass.idbookingclass = ?";
$dataStmt = $conn->prepare($dataQuery);
$dataStmt->bind_param("i", $idbookingclass);
$dataStmt->execute();
$dataResult = $dataStmt->get_result();
$dataRow = $dataResult->fetch_assoc();
$emailuser = $dataRow['email'];
$firstname = $dataRow['first_name'] ?? 'Utente';
// Prepara messaggio email
$messagecancel = "<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 18px; line-height: 34.2px;'><strong><span style='line-height: 34.2px; font-size: 18px;'> Ciao $firstname , </span></strong></span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>La tua lezione è stata cancellata con successo! </span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Dettaglio: $newtimeformat</span></p>
<br>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Per vedere e gestire le tue lezioni clicca qui: https://yogibook.yogasoul.it </span></p>
<br>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Ci vediamo sul tappetino!</span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Il Team Yogasoul</span></p>";
// Definisci $messageedit per il template
$messageedit = $messagecancel;
// Definisci $buttonedit (pulsante generico)
$buttonedit = "<a href='https://yogibook.yogasoul.it/' target='_blank' class='v-button v-font-size' style='box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #3AAEE0; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;'>
<span style='display:block;padding:10px 20px;line-height:120%;'><span style='line-height: 16.8px;'>YogiBook - YogaSoul</span></span>
</a>";
require 'phpmailer/src/Exception.php';
require 'phpmailer/src/PHPMailer.php';
require 'phpmailer/src/SMTP.php';
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'mail.yogasoul.it';
$mail->SMTPAuth = true;
$mail->Username = 'info@yogasoul.it';
$mail->Password = '!Testolina88';
$mail->SMTPSecure = 'tls';
$mail->Port = 587;
// Verifica esistenza file template
if (!file_exists('mail/emailtemplate2.php')) {
throw new Exception("File emailtemplate2.php non trovato.");
}
include('mail/emailtemplate2.php');
// Verifica che $mailmessage1 esista
if (!isset($mailmessage1)) {
throw new Exception("Variabile \$mailmessage1 non definita in emailtemplate2.php.");
}
// Sostituisci placeholder (per compatibilità)
$htmlContent = str_replace('{message}', $messagecancel, $mailmessage1);
$mail->From = 'info@yogasoul.it';
$mail->FromName = 'YogiBook [YogaSoul]';
$mail->addAddress($emailuser);
$mail->Subject = "YogiBook - Lezione cancellata con successo!";
$mail->Body = $htmlContent;
$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
$mail->send();
// Mostra landing di conferma
echo "<h1>Cancellazione confermata</h1>";
echo "<p>La lezione del $newtimeformat è stata cancellata con successo.</p>";
echo "<a href='https://yogibook.yogasoul.it'>Torna al portale</a>";
} catch (Exception $e) {
echo "Errore invio email: " . $mail->ErrorInfo;
}
} else {
echo "Link non valido o scaduto.";
}
$stmt->close();
} else {
echo "Errore nella preparazione della query: " . $conn->error;
}
} else {
echo "Parametri mancanti.";
}
$conn->close();

View File

@ -0,0 +1,26 @@
<?php
require_once('include/headscript.php');
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die(json_encode(['error' => 'Connessione al database fallita']));
}
// Query per ottenere tutte le classi disponibili
$sql = "SELECT id, servicename, day, time FROM classes ORDER BY servicename, day, time";
$result = mysqli_query($conn, $sql);
$classes = [];
while ($row = mysqli_fetch_assoc($result)) {
$classes[] = [
'id' => $row['id'],
'servicename' => $row['servicename'],
'day' => $row['day'],
'time' => $row['time']
];
}
mysqli_close($conn);
header('Content-Type: application/json');
echo json_encode($classes);

View File

@ -0,0 +1,35 @@
<?php
require_once('include/headscript.php');
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die(json_encode(['error' => 'Connessione al database fallita']));
}
$class_id = isset($_POST['class_id']) ? intval($_POST['class_id']) : 0;
if ($class_id <= 0) {
die(json_encode(['error' => 'ID classe non valido']));
}
// Query per ottenere le date disponibili per la classe specificata
// Supponiamo che ci sia una tabella 'class_schedule' con le date disponibili
$sql = "SELECT DISTINCT DATE(bookingstart) as available_date
FROM class_schedule
WHERE class_id = ?
ORDER BY available_date";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $class_id);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$availableDates = [];
while ($row = mysqli_fetch_assoc($result)) {
$availableDates[] = $row['available_date'];
}
mysqli_stmt_close($stmt);
mysqli_close($conn);
header('Content-Type: application/json');
echo json_encode(['availableDates' => $availableDates]);

162
public/promemoria-cron.php Normal file
View File

@ -0,0 +1,162 @@
<?php
// Abilita visualizzazione errori PHP
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
include('include/headscript.php');
// Verifica connessione al database
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connessione al database fallita: " . $conn->connect_error);
}
// Inizializza contatore e log
$emailCount = 0;
$errors = [];
$logFile = 'promemoria_cron_log.txt';
$logMessage = "Esecuzione cron: " . date('Y-m-d H:i:s') . "\n";
// Seleziona prenotazioni per domani
$tomorrow = date('Y-m-d', strtotime('+1 day'));
$query = "SELECT bc.*, au.email, au.first_name, s.servicename
FROM bookingclass bc
LEFT JOIN auth_users au ON bc.iduser = au.id
LEFT JOIN service s ON bc.idservice = s.idservice
WHERE DATE(bc.bookingstart) = ? AND bc.status = 'booked'";
$stmt = $conn->prepare($query);
if (!$stmt) {
$errors[] = "Errore preparazione query: " . $conn->error;
file_put_contents($logFile, $logMessage . "Errore query: " . $conn->error . "\n", FILE_APPEND);
echo "Errore preparazione query: " . $conn->error;
exit;
}
$stmt->bind_param("s", $tomorrow);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
$logMessage .= "Nessuna prenotazione trovata per domani.\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
echo "Nessuna prenotazione trovata per domani.\n";
exit;
}
while ($row = $result->fetch_assoc()) {
$idbookingclass = $row['idbookingclass'];
$token = $row['cancellation_token'];
// Genera token se assente
if (empty($token)) {
$token = bin2hex(random_bytes(32));
$updateQuery = "UPDATE bookingclass SET cancellation_token = ? WHERE idbookingclass = ?";
$updateStmt = $conn->prepare($updateQuery);
if ($updateStmt) {
$updateStmt->bind_param("si", $token, $idbookingclass);
$updateStmt->execute();
} else {
$errors[] = "Errore preparazione query token per ID $idbookingclass: " . $conn->error;
}
}
$firstname = $row['first_name'] ?? 'Utente';
$emailuser = $row['email'];
$servicename = $row['servicename'] ?? 'Sconosciuta';
$bookingstart = $row['bookingstart'];
$dataformat = date("d-m-Y H:i", strtotime($bookingstart));
// Verifica email valida
if (!filter_var($emailuser, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Email non valida per ID $idbookingclass: $emailuser";
$logMessage .= "Email non valida per ID $idbookingclass: $emailuser\n";
continue;
}
// Link cancellazione
$link = "https://yogibook.yogasoul.it/cancella-prenotazione.php?idbookingclass=$idbookingclass&token=$token";
// Messaggio email
$message = "<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 18px; line-height: 34.2px;'><strong>Ciao $firstname,</strong></span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Promemoria: domani hai la lezione $servicename del $dataformat.</span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Puoi cancellarla fino alle 12:00 cliccando qui:</span></p>
<a href='$link' target='_blank'>Cancella prenotazione</a>
<br>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Ci vediamo sul tappetino!</span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Il Team Yogasoul</span></p>";
// Definisci $messageedit per il template
$messageedit = $message;
// Definisci $buttonedit (vuoto o pulsante generico)
$buttonedit = "<a href='https://yogibook.yogasoul.it/' target='_blank' class='v-button v-font-size' style='box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #3AAEE0; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;'>
<span style='display:block;padding:10px 20px;line-height:120%;'><span style='line-height: 16.8px;'>YogiBook - YogaSoul</span></span>
</a>";
require 'phpmailer/src/Exception.php';
require 'phpmailer/src/PHPMailer.php';
require 'phpmailer/src/SMTP.php';
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'mail.yogasoul.it';
$mail->SMTPAuth = true;
$mail->Username = 'info@yogasoul.it';
$mail->Password = '!Testolina88';
$mail->SMTPSecure = 'tls';
$mail->Port = 587;
// Verifica esistenza file template
if (!file_exists('mail/emailtemplate2.php')) {
throw new Exception("File emailtemplate2.php non trovato.");
}
include('mail/emailtemplate2.php');
// Verifica che $mailmessage1 esista
if (!isset($mailmessage1)) {
throw new Exception("Variabile \$mailmessage1 non definita in emailtemplate2.php.");
}
// Sostituisci placeholder (anche se non usato, per compatibilità)
$htmlContent = str_replace('{message}', $message, $mailmessage1);
$mail->From = 'info@yogasoul.it';
$mail->FromName = 'YogiBook [YogaSoul]';
$mail->addAddress($emailuser);
$mail->Subject = "YogiBook - Promemoria lezione domani!";
$mail->Body = $htmlContent;
$mail->AltBody = 'Promemoria lezione.';
$mail->send();
$emailCount++;
$logMessage .= "Email inviata a $emailuser per lezione ID $idbookingclass ($dataformat)\n";
} catch (Exception $e) {
$errors[] = "Errore invio email a $emailuser (ID $idbookingclass): " . $mail->ErrorInfo;
$logMessage .= "Errore invio a $emailuser (ID $idbookingclass): " . $mail->ErrorInfo . "\n";
}
sleep(2); // Delay 2 secondi
}
// Scrivi log
file_put_contents($logFile, $logMessage, FILE_APPEND);
// Output debug
echo "Esecuzione completata: $emailCount email inviate.\n";
if (!empty($errors)) {
echo "Errori rilevati:\n";
foreach ($errors as $error) {
echo "- $error\n";
}
} else {
echo "Nessun errore.\n";
}
echo "Dettagli nel file di log: $logFile\n";
$conn->close();

View File

@ -0,0 +1,4 @@
Esecuzione cron: 2025-10-08 13:39:42
Esecuzione cron: 2025-10-08 13:40:48
Esecuzione cron: 2025-10-08 13:54:46
Email inviata a info@claudiosironi.com per lezione ID 8 (09-10-2025 18:15)

View File

@ -0,0 +1,57 @@
<?php
require_once('include/headscript.php');
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die(json_encode(['success' => false, 'message' => 'Connessione al database fallita']));
}
$id_booking_class = isset($_POST['id']) ? intval($_POST['id']) : 0;
$class_id = isset($_POST['class_id']) ? intval($_POST['class_id']) : 0;
$new_date = isset($_POST['new_date']) ? $_POST['new_date'] : '';
if ($id_booking_class <= 0 || $class_id <= 0 || empty($new_date)) {
die(json_encode(['success' => false, 'message' => 'Dati non validi']));
}
// Ottieni i dettagli della classe attuale
$sql = "SELECT bookingstart, servicename, day, time FROM bookingclass WHERE idbookingclass = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $id_booking_class);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$current_class = mysqli_fetch_assoc($result);
if (!$current_class) {
die(json_encode(['success' => false, 'message' => 'Lezione non trovata']));
}
// Ottieni i dettagli della nuova classe
$sql = "SELECT servicename, day, time FROM classes WHERE id = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $class_id);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$new_class = mysqli_fetch_assoc($result);
if (!$new_class) {
die(json_encode(['success' => false, 'message' => 'Classe non trovata']));
}
// Aggiorna la lezione
$sql = "UPDATE bookingclass
SET bookingstart = ?, prevbookingstart = ?, servicename = ?, day = ?, time = ?
WHERE idbookingclass = ?";
$stmt = mysqli_prepare($conn, $sql);
$new_bookingstart = $new_date . ' ' . $new_class['time'] . ':00';
mysqli_stmt_bind_param($stmt, 'sssssi', $new_bookingstart, $current_class['bookingstart'], $new_class['servicename'], $new_class['day'], $new_class['time'], $id_booking_class);
if (mysqli_stmt_execute($stmt)) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Errore durante l\'aggiornamento della lezione']);
}
mysqli_stmt_close($stmt);
mysqli_close($conn);

View File

@ -17,7 +17,7 @@ SELECT
auth_users.id AS id_utente, auth_users.id AS id_utente,
auth_users.first_name AS nome_utente, auth_users.first_name AS nome_utente,
auth_users.last_name AS cognome_utente, auth_users.last_name AS cognome_utente,
auth_users.email AS email_utente, -- Aggiungiamo l'email dell'utente auth_users.email AS email_utente,
COALESCE(lezioni_acquistate, 0) AS lezioni_acquistate, COALESCE(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() 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.bookingstart > CURDATE() THEN 1 END) AS lezioni_programmate,
@ -70,7 +70,7 @@ if (!$result) {
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" /> <link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
@ -84,13 +84,15 @@ if (!$result) {
</script> </script>
<script> <script>
$(function() { $(function() {
$("#dayoff").datepicker({ dateFormat: "yy-mm-dd" }); $("#dayoff").datepicker({
dateFormat: "yy-mm-dd"
});
}); });
</script> </script>
<!-- DataTables CSS --> <!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<!-- DataTables JS --> <!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<style> <style>
.custom-card { .custom-card {
@ -192,27 +194,33 @@ if (!$result) {
/* Styling for the modal table */ /* Styling for the modal table */
.lost-lesson { .lost-lesson {
background-color: #ffcc80 !important; /* Orange for lost lessons */ background-color: #ffcc80 !important;
/* Orange for lost lessons */
} }
.past-lesson { .past-lesson {
background-color: #c6d9f0 !important; /* Lighter grayish-blue for past lessons */ background-color: #c6d9f0 !important;
/* Lighter grayish-blue for past lessons */
} }
.past-lesson-reprogrammed { .past-lesson-reprogrammed {
background-color: #a9bcd8 !important; /* Slightly different gray for past reprogrammed lessons */ background-color: #a9bcd8 !important;
/* Slightly different gray for past reprogrammed lessons */
} }
.future-lesson { .future-lesson {
background-color: #b3ffb3 !important; /* Softer pastel green for future lessons */ background-color: #b3ffb3 !important;
/* Softer pastel green for future lessons */
} }
.future-lesson-reprogrammed { .future-lesson-reprogrammed {
background-color: #a3e6a3 !important; /* Slightly different green for future reprogrammed lessons */ background-color: #a3e6a3 !important;
/* Slightly different green for future reprogrammed lessons */
} }
.status-reprogrammed { .status-reprogrammed {
background-color: #fffacd !important; /* Light yellow for "Riprogrammata" status cell */ background-color: #fffacd !important;
/* Light yellow for "Riprogrammata" status cell */
} }
.btn-warning { .btn-warning {
@ -226,9 +234,21 @@ if (!$result) {
border-color: #e07b00 !important; border-color: #e07b00 !important;
} }
.btn-danger {
background-color: #dc3545 !important;
border-color: #dc3545 !important;
color: white !important;
}
.btn-danger:hover {
background-color: #c82333 !important;
border-color: #c82333 !important;
}
/* Custom modal width */ /* Custom modal width */
.modal-xl-custom { .modal-xl-custom {
max-width: 1200px; /* Wider modal */ max-width: 1200px;
/* Wider modal */
} }
/* Stile per il pulsante email */ /* Stile per il pulsante email */
@ -262,6 +282,18 @@ if (!$result) {
height: 200px; height: 200px;
resize: vertical; resize: vertical;
} }
/* Stile per il pulsante Riprogramma */
.btn-reprogram {
background-color: #28a745 !important;
border-color: #28a745 !important;
color: white !important;
}
.btn-reprogram:hover {
background-color: #218838 !important;
border-color: #218838 !important;
}
</style> </style>
<script> <script>
function confirmDelete(id, deletePageUrl) { function confirmDelete(id, deletePageUrl) {
@ -342,7 +374,6 @@ if (!$result) {
<button class="btn btn-email-all" id="sendEmailToAll">Invia Email a Tutti</button> <button class="btn btn-email-all" id="sendEmailToAll">Invia Email a Tutti</button>
<div class="table-responsive"> <div class="table-responsive">
<table id="userStatsTable" class="table table-striped mb-0"> <table id="userStatsTable" class="table table-striped mb-0">
<thead> <thead>
<tr> <tr>
<th>ID Utente</th> <th>ID Utente</th>
@ -353,7 +384,7 @@ if (!$result) {
<th>Lezioni Programmate</th> <th>Lezioni Programmate</th>
<th>Lezioni Perse</th> <th>Lezioni Perse</th>
<th>Lezioni da Programmare</th> <th>Lezioni da Programmare</th>
<th>Azione Email</th> <!-- Nuova colonna --> <th>Azione Email</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -368,7 +399,6 @@ if (!$result) {
echo "<td>" . $row['lezioni_programmate'] . "</td>"; echo "<td>" . $row['lezioni_programmate'] . "</td>";
echo "<td>" . $row['lezioni_perse'] . "</td>"; echo "<td>" . $row['lezioni_perse'] . "</td>";
echo "<td>" . $row['lezioni_da_programmare'] . "</td>"; echo "<td>" . $row['lezioni_da_programmare'] . "</td>";
// Nuova colonna "Azione Email" con il pulsante
echo "<td>"; echo "<td>";
echo "<button class='btn btn-sm btn-email send-email' data-id='" . $row['id_utente'] . "' data-nome='" . $row['nome_utente'] . "' data-email='" . $row['email_utente'] . "'><i class='fas fa-envelope'></i></button>"; echo "<button class='btn btn-sm btn-email send-email' data-id='" . $row['id_utente'] . "' data-nome='" . $row['nome_utente'] . "' data-email='" . $row['email_utente'] . "'><i class='fas fa-envelope'></i></button>";
echo "</td>"; echo "</td>";
@ -449,6 +479,35 @@ if (!$result) {
</div> </div>
</div> </div>
<!-- Modal per Riprogrammare Lezione -->
<div class="modal fade" id="reprogramModal" tabindex="-1" aria-labelledby="reprogramModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="reprogramModalLabel">Riprogramma Lezione</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="reprogramForm">
<div class="mb-3">
<label for="classSelect" class="form-label">Seleziona Classe</label>
<select class="form-control" id="classSelect" required></select>
</div>
<div class="mb-3">
<label for="reprogramDate" class="form-label">Seleziona Data</label>
<input type="text" class="form-control" id="reprogramDate" readonly required>
</div>
<input type="hidden" id="reprogramIdBookingClass" name="id_booking_class">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Chiudi</button>
<button type="button" class="btn btn-primary" id="reprogramBtn">Riprogramma</button>
</div>
</div>
</div>
</div>
<!-- JAVASCRIPT --> <!-- JAVASCRIPT -->
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script> <script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="assets/libs/metismenujs/metismenujs.min.js"></script> <script src="assets/libs/metismenujs/metismenujs.min.js"></script>
@ -456,21 +515,15 @@ if (!$result) {
<script src="assets/libs/eva-icons/eva.min.js"></script> <script src="assets/libs/eva-icons/eva.min.js"></script>
<script src="assets/js/app.js"></script> <script src="assets/js/app.js"></script>
<!-- Aggiorna il link a SweetAlert2 --> <script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> $('#userStatsTable').DataTable({
paging: false,
<!-- JavaScript -->
<script>
$('#userStatsTable').DataTable({
paging: false, // ❌ disattiva paginazione
searching: true, searching: true,
ordering: true, ordering: true,
language: { language: {
url: "//cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json" url: "//cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json"
} }
}); });
$(document).ready(function() { $(document).ready(function() {
// Function to format date as "DD - Month - YYYY HH:mm:ss" // Function to format date as "DD - Month - YYYY HH:mm:ss"
@ -500,14 +553,15 @@ $('#userStatsTable').DataTable({
$.ajax({ $.ajax({
url: 'fetch_class_details.php', url: 'fetch_class_details.php',
method: 'POST', method: 'POST',
data: { id_utente: id_utente }, data: {
id_utente: id_utente
},
dataType: 'json', dataType: 'json',
success: function(data) { success: function(data) {
console.log('Refresh Success Response:', data); console.log('Refresh Success Response:', data);
var tbody = $('#classDetailsBody'); var tbody = $('#classDetailsBody');
tbody.empty(); tbody.empty();
// Ensure data is an array
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
data = [data]; data = [data];
} }
@ -570,7 +624,18 @@ $('#userStatsTable').DataTable({
.addClass('btn btn-sm btn-warning mark-lost') .addClass('btn btn-sm btn-warning mark-lost')
.text('P') .text('P')
.data('id', row.idbookingclass); .data('id', row.idbookingclass);
actionTd.append(pButton); var rButton = $('<button>')
.addClass('btn btn-sm btn-danger remove-class')
.text('Rimuovi')
.data('id', row.idbookingclass);
var reprogramButton = $('<button>')
.addClass('btn btn-sm btn-reprogram reprogram-class')
.text('Riprogramma')
.data('id', row.idbookingclass)
.data('servicename', row.servicename)
.data('day', row.day)
.data('time', row.time);
actionTd.append(pButton).append(' ').append(rButton).append(' ').append(reprogramButton);
} else { } else {
actionTd.text('-'); actionTd.text('-');
} }
@ -597,6 +662,362 @@ $('#userStatsTable').DataTable({
refreshTable(id_utente, nome_utente); refreshTable(id_utente, nome_utente);
}); });
// Gestione pulsante Rimuovi
$(document).on('click', '.remove-class', function() {
var id = $(this).data('id');
var id_utente = $('#classModal').data('current-id');
var nome_utente = $('#userName').text();
Swal.fire({
title: "Sei sicuro?",
text: "Questa lezione verrà rimossa definitivamente!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Sì, rimuovi!",
cancelButtonText: "Annulla",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: 'deleteclass.php',
method: 'POST',
data: {
id: id
},
dataType: 'json',
success: function(response) {
if (response.success) {
Swal.fire({
title: "Successo",
text: "Lezione rimossa con successo.",
icon: "success",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
refreshTable(id_utente, nome_utente);
} else {
Swal.fire({
title: "Errore",
text: response.message || "Errore durante la rimozione della lezione.",
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
},
error: function(xhr, status, error) {
console.log('Remove Class AJAX Error:', xhr, status, error);
Swal.fire({
title: "Errore",
text: "Errore durante la rimozione della lezione: " + error,
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
});
}
});
});
// Gestione pulsante Riprogramma
$(document).on('click', '.reprogram-class', function() {
var id = $(this).data('id');
var servicename = $(this).data('servicename');
var day = $(this).data('day');
var time = $(this).data('time');
$('#reprogramIdBookingClass').val(id);
$('#reprogramModalLabel').text('Riprogramma Lezione: ' + servicename);
// Carica le classi disponibili
$.ajax({
url: 'fetch_available_classes.php',
method: 'GET',
dataType: 'json',
success: function(data) {
var classSelect = $('#classSelect');
classSelect.empty();
$.each(data, function(index, classe) {
var optionText = classe.servicename + ' (' + classe.day + ' ' + classe.time + ')';
var option = $('<option>')
.val(classe.id)
.text(optionText)
.data('servicename', classe.servicename)
.data('day', classe.day)
.data('time', classe.time);
if (classe.servicename === servicename && classe.day === day && classe.time === time) {
option.prop('selected', true);
}
classSelect.append(option);
});
// Inizializza il datepicker
$('#reprogramDate').datepicker({
dateFormat: 'yy-mm-dd',
changeMonth: true,
changeYear: true,
onSelect: function(dateText) {
// Puoi aggiungere logica qui se necessario
}
});
// Carica le date disponibili per la classe selezionata
loadAvailableDates(classSelect.val());
$('#reprogramModal').modal('show');
},
error: function(xhr, status, error) {
console.log('Fetch Classes AJAX Error:', xhr, status, error);
Swal.fire({
title: "Errore",
text: "Errore nel caricamento delle classi disponibili.",
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
});
});
// Gestione cambio classe nel menu a tendina
$('#classSelect').on('change', function() {
var classId = $(this).val();
loadAvailableDates(classId);
});
// Funzione per caricare le date disponibili
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 dateStr = $.datepicker.formatDate('yy-mm-dd', date);
return [data.availableDates.includes(dateStr), ''];
});
},
error: function(xhr, status, error) {
console.log('Fetch Available Dates AJAX Error:', xhr, status, error);
Swal.fire({
title: "Errore",
text: "Errore nel caricamento delle date disponibili.",
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
});
}
// Gestione pulsante Riprogramma
$('#reprogramBtn').on('click', function() {
var id = $('#reprogramIdBookingClass').val();
var classId = $('#classSelect').val();
var newDate = $('#reprogramDate').val();
var id_utente = $('#classModal').data('current-id');
var nome_utente = $('#userName').text();
if (!newDate || !classId) {
Swal.fire({
title: "Errore",
text: "Per favore, seleziona una classe e una data.",
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
return;
}
$.ajax({
url: 'reprogram_class.php',
method: 'POST',
data: {
id: id,
class_id: classId,
new_date: newDate
},
dataType: 'json',
success: function(response) {
if (response.success) {
Swal.fire({
title: "Successo",
text: "Lezione riprogrammata con successo.",
icon: "success",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
$('#reprogramModal').modal('hide');
refreshTable(id_utente, nome_utente);
} else {
Swal.fire({
title: "Errore",
text: response.message || "Errore durante la riprogrammazione della lezione.",
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
},
error: function(xhr, status, error) {
console.log('Reprogram AJAX Error:', xhr, status, error);
Swal.fire({
title: "Errore",
text: "Errore durante la riprogrammazione della lezione: " + error,
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
});
});
// Gestione invio email individuale
$('.send-email').on('click', function() {
var id_utente = $(this).data('id');
var nome_utente = $(this).data('nome');
var email_utente = $(this).data('email');
$('#emailModalLabel').text('Invia Email a ' + nome_utente);
$('#emailBody').val('Ciao ' + nome_utente.split(' ')[0] + ',\n\n');
$('#emailRecipients').val(JSON.stringify([{
id: id_utente,
email: email_utente,
nome: nome_utente
}]));
$('#emailModal').modal('show');
});
// Gestione invio email a tutti
$('#sendEmailToAll').on('click', function() {
var recipients = [];
$('.send-email').each(function() {
var id_utente = $(this).data('id');
var email_utente = $(this).data('email');
var nome_utente = $(this).data('nome');
if ([4, 10, 3].includes(id_utente)) {
recipients.push({
id: id_utente,
email: email_utente,
nome: nome_utente
});
}
});
if (recipients.length === 0) {
Swal.fire({
title: "Attenzione",
text: "Nessun utente idoneo trovato per l'invio dell'email.",
icon: "warning",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
return;
}
$('#emailModalLabel').text('Invia Email a Tutti');
$('#emailBody').val('Ciao a tutti,\n\n');
$('#emailRecipients').val(JSON.stringify(recipients));
$('#emailModal').modal('show');
});
// Gestione invio email
$('#sendEmailBtn').on('click', function() {
var subject = $('#emailSubject').val();
var body = $('#emailBody').val();
var recipients = JSON.parse($('#emailRecipients').val());
if (!subject || !body) {
Swal.fire({
title: "Errore",
text: "Per favore, compila sia l'oggetto che il messaggio.",
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
return;
}
$.ajax({
url: '/public/send_email.php',
method: 'POST',
data: {
subject: subject,
body: body,
recipients: JSON.stringify(recipients)
},
dataType: 'json',
success: function(response) {
if (response.success) {
Swal.fire({
title: "Successo",
text: response.message,
icon: "success",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
$('#emailModal').modal('hide');
} else {
Swal.fire({
title: "Errore",
text: response.message,
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
},
error: function(xhr, status, error) {
console.log('Send Email AJAX Error:', xhr, status, error);
Swal.fire({
title: "Errore",
text: "Errore durante l'invio dell'email: " + error,
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
});
});
// Gestione mark-lost (già presente, ma incluso per completezza)
$(document).on('click', '.mark-lost', function() { $(document).on('click', '.mark-lost', function() {
var id = $(this).data('id'); var id = $(this).data('id');
var id_utente = $('#classModal').data('current-id'); var id_utente = $('#classModal').data('current-id');
@ -620,7 +1041,9 @@ $('#userStatsTable').DataTable({
$.ajax({ $.ajax({
url: 'mark_lost.php', url: 'mark_lost.php',
method: 'POST', method: 'POST',
data: { id: id }, data: {
id: id
},
dataType: 'json', dataType: 'json',
timeout: 10000, timeout: 10000,
success: function(response) { success: function(response) {
@ -671,134 +1094,8 @@ $('#userStatsTable').DataTable({
} }
}); });
}); });
// Gestione invio email individuale
$('.send-email').on('click', function() {
var id_utente = $(this).data('id');
var nome_utente = $(this).data('nome');
var email_utente = $(this).data('email');
// Imposta il titolo del modal
$('#emailModalLabel').text('Invia Email a ' + nome_utente);
// Precompila il corpo dell'email con "Ciao [Nome]"
$('#emailBody').val('Ciao ' + nome_utente.split(' ')[0] + ',\n\n');
// Imposta il destinatario (solo questo utente), includendo il nome
$('#emailRecipients').val(JSON.stringify([{id: id_utente, email: email_utente, nome: nome_utente}]));
// Mostra il modal
$('#emailModal').modal('show');
}); });
</script>
</body>
// Gestione invio email a tutti
$('#sendEmailToAll').on('click', function() {
// Raccogli tutti gli utenti idonei (ID 4, 10, 3)
var recipients = [];
$('.send-email').each(function() {
var id_utente = $(this).data('id');
var email_utente = $(this).data('email');
var nome_utente = $(this).data('nome');
// Limita agli utenti con ID 4, 10, 3
if ([4, 10, 3].includes(id_utente)) {
recipients.push({id: id_utente, email: email_utente, nome: nome_utente});
}
});
if (recipients.length === 0) {
Swal.fire({
title: "Attenzione",
text: "Nessun utente idoneo trovato per l'invio dell'email.",
icon: "warning",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
return;
}
// Imposta il titolo del modal
$('#emailModalLabel').text('Invia Email a Tutti');
// Precompila il corpo dell'email con "Ciao a tutti"
$('#emailBody').val('Ciao a tutti,\n\n');
// Imposta i destinatari
$('#emailRecipients').val(JSON.stringify(recipients));
// Mostra il modal
$('#emailModal').modal('show');
});
// Gestione invio email
$('#sendEmailBtn').on('click', function() {
var subject = $('#emailSubject').val();
var body = $('#emailBody').val();
var recipients = JSON.parse($('#emailRecipients').val());
if (!subject || !body) {
Swal.fire({
title: "Errore",
text: "Per favore, compila sia l'oggetto che il messaggio.",
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
return;
}
// Invia l'email tramite AJAX
$.ajax({
url: '/public/send_email.php', // Usa il percorso relativo corretto
method: 'POST',
data: {
subject: subject,
body: body,
recipients: JSON.stringify(recipients)
},
dataType: 'json',
success: function(response) {
if (response.success) {
Swal.fire({
title: "Successo",
text: response.message,
icon: "success",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
$('#emailModal').modal('hide');
} else {
Swal.fire({
title: "Errore",
text: response.message,
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
},
error: function(xhr, status, error) {
console.log('Send Email AJAX Error:', xhr, status, error);
Swal.fire({
title: "Errore",
text: "Errore durante l'invio dell'email: " + error,
icon: "error",
focusConfirm: true,
didOpen: () => {
document.body.removeAttribute('aria-hidden');
}
});
}
});
});
});
</script>
</body>
</html> </html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

View File

@ -0,0 +1,36 @@
YogaSoul Autoresponder — Istruzioni rapide
COS'È
- Crea BOZZE di risposta (non invia) per le ultime X email NON LETTE in INBOX.
- Le bozze vengono salvate nella cartella Drafts/Bozze dellaccount IMAP, in thread (In-Reply-To/References) con citazione del messaggio originale.
FILE NELLA CARTELLA
- yogasoul_autoresponder.exe (oppure: email_autoresponder_yogasoul.py se lo usi con Python)
- config.json (credenziali e impostazioni)
- prompt_template.txt (testo/tono della risposta: modificabile liberamente)
- yogasoul_knowledge_base.json (dati corsi, link, ecc.)
COME SI USA (Windows, senza Python)
1) Apri la cartella “YogaSoul Autoresponder”.
2) Modifica `config.json` con:
- imap_server, email_address, email_password
- openai_api_key
- max_to_process (es. 5), throttle_seconds (es. 1.5)
3) (Opz.) Modifica `prompt_template.txt` e/o `yogasoul_knowledge_base.json`.
4) Doppio click su `yogasoul_autoresponder.exe` (oppure su `run_autoresponder.bat` se fornito).
5) Controlla in Posta le BOZZE: dovresti vedere una bozza per ogni email non letta (fino a max_to_process).
IMPOSTAZIONI UTILI (config.json)
- "max_to_process": quante email non lette processare (consigliato 5).
- "throttle_seconds": pausa tra le richieste (consigliato 12s).
- "mark_as_seen": true per segnare come lette dopo la bozza (false per lasciarle non lette).
- "preferred_draft_folder": cartella locale alternativa se il server non ha Drafts.
TROUBLESHOOTING
- Nessuna bozza: verifica credenziali IMAP/API in config.json; verifica che ci siano email UNSEEN.
- Bozze “schiacciate”: il programma converte in HTML con <p>/<br>; se serve, modifica il prompt.
- Rate limit/blocchi: alza "throttle_seconds" (es. 1.52.0) e tieni "max_to_process" basso (5).
- Cartella bozze: il programma cerca \Drafts; in fallback crea INBOX.BozzaRisposte.
SICUREZZA
- `config.json` contiene password in chiaro: conserva la cartella su un PC/utente fidato.

View File

@ -0,0 +1,11 @@
{
"imap_server": "mail.yogasoul.it",
"email_address": "info@yogasoul.it",
"email_password": "!Testolina88",
"openai_api_key": "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",
"openai_model": "gpt-3.5-turbo",
"preferred_draft_folder": "BozzaRisposte",
"mark_as_seen": true,
"throttle_seconds": 2,
"max_to_process": 5
}

View File

@ -13,20 +13,43 @@ from email.utils import parseaddr
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.header import decode_header, make_header from email.header import decode_header, make_header
from datetime import datetime from datetime import datetime
from pathlib import Path
import sys
# ========================== # ==========================
# CONFIG (compila qui) # CONFIG da config.json (accanto allo script/EXE)
# ========================== # ==========================
IMAP_SERVER = "mail.yogasoul.it" def app_dir() -> Path:
EMAIL_ADDRESS = "info@yogasoul.it" # se "frozen" (PyInstaller), usa la cartella dell'eseguibile
EMAIL_PASSWORD = "!Testolina88" # <— METTI LA PASSWORD QUI return Path(sys.executable).parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent
OPENAI_API_KEY = "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" # <— METTI LA OPENAI KEY QUI
KB_PATH = "yogasoul_knowledge_base.json" CONFIG_PATH = app_dir() / "config.json"
OPENAI_MODEL = "gpt-3.5-turbo"
PREFERRED_DRAFT_FOLDER = "BozzaRisposte" # se non trova Drafts, crea INBOX.<questa> def load_config():
MARK_AS_SEEN = True # False per lasciare le email non lette try:
THROTTLE_SECONDS = 0 # ritardo tra email (0 = nessuno) with open(CONFIG_PATH, "r", encoding="utf-8") as f:
MAX_TO_PROCESS = 5 # ⬅️ NUOVO: processa al massimo N email UNSEEN (ultime) cfg = json.load(f)
print(f"[CONFIG] Caricato: {CONFIG_PATH}")
return cfg
except Exception as e:
print(f"[ERRORE] Impossibile leggere {CONFIG_PATH}: {e}")
return {}
CFG = load_config()
# valori letti dal config (con default sensati)
IMAP_SERVER = CFG.get("imap_server", "mail.yogasoul.it")
EMAIL_ADDRESS = CFG.get("email_address", "")
EMAIL_PASSWORD = CFG.get("email_password", "")
OPENAI_API_KEY = CFG.get("openai_api_key", "")
OPENAI_MODEL = CFG.get("openai_model", "gpt-3.5-turbo")
PREFERRED_DRAFT_FOLDER = CFG.get("preferred_draft_folder", "BozzaRisposte")
MARK_AS_SEEN = bool(CFG.get("mark_as_seen", True))
THROTTLE_SECONDS = float(CFG.get("throttle_seconds", 0) or 0)
MAX_TO_PROCESS = int(CFG.get("max_to_process", 5) or 5)
# Percorsi esterni (accanto all'eseguibile)
KB_PATH = str(app_dir() / CFG.get("kb_path", "yogasoul_knowledge_base.json"))
PROMPT_PATH = str(app_dir() / CFG.get("prompt_path", "prompt_template.txt"))
# ========================== # ==========================
# UTILS # UTILS
@ -72,7 +95,6 @@ def _ensure_html_blocks(s):
body = "<p>{}</p>".format(html_lib.escape(s).replace("\n", "<br>")) body = "<p>{}</p>".format(html_lib.escape(s).replace("\n", "<br>"))
return "<!doctype html><html><body>{}</body></html>".format(body) return "<!doctype html><html><body>{}</body></html>".format(body)
def _make_quoted_original(body_text): def _make_quoted_original(body_text):
"""Crea il blocco citato del messaggio originale, safe-escaped.""" """Crea il blocco citato del messaggio originale, safe-escaped."""
if not body_text: if not body_text:
@ -84,6 +106,14 @@ def _make_quoted_original(body_text):
f"<blockquote style='margin:0 0 0 1em; padding-left:1em; border-left:3px solid #ddd'>{escaped}</blockquote>" f"<blockquote style='margin:0 0 0 1em; padding-left:1em; border-left:3px solid #ddd'>{escaped}</blockquote>"
) )
def _inject_before_body_end(html_src: str, addition: str) -> str:
"""Inserisce 'addition' prima di </body> in modo case-insensitive; se manca </body>, appende."""
m = re.search(r'</\s*body\s*>', html_src, flags=re.I)
if not m:
return html_src + addition
start = m.start()
return html_src[:start] + addition + html_src[start:]
def load_knowledge_base(path=KB_PATH): def load_knowledge_base(path=KB_PATH):
if not os.path.isfile(path): if not os.path.isfile(path):
print(f"[ATTENZIONE] KB non trovata: {path}. Proseguo senza.") print(f"[ATTENZIONE] KB non trovata: {path}. Proseguo senza.")
@ -98,20 +128,39 @@ def load_knowledge_base(path=KB_PATH):
def assert_config(): def assert_config():
problems = [] problems = []
if not isinstance(IMAP_SERVER, str) or not IMAP_SERVER.strip(): if not isinstance(IMAP_SERVER, str) or not IMAP_SERVER.strip():
problems.append("IMAP_SERVER mancante") problems.append("imap_server mancante in config.json")
if not isinstance(EMAIL_ADDRESS, str) or not EMAIL_ADDRESS.strip(): if not isinstance(EMAIL_ADDRESS, str) or not EMAIL_ADDRESS.strip():
problems.append("EMAIL_ADDRESS mancante") problems.append("email_address mancante in config.json")
if not isinstance(EMAIL_PASSWORD, str) or not EMAIL_PASSWORD.strip() or EMAIL_PASSWORD == "INSERISCI_LA_PASSWORD": if not isinstance(EMAIL_PASSWORD, str) or not EMAIL_PASSWORD.strip():
problems.append("EMAIL_PASSWORD non impostata") problems.append("email_password mancante in config.json")
if not isinstance(OPENAI_API_KEY, str) or not OPENAI_API_KEY.strip() or OPENAI_API_KEY == "INSERISCI_OPENAI_API_KEY": if not isinstance(OPENAI_API_KEY, str) or not OPENAI_API_KEY.strip():
problems.append("OPENAI_API_KEY non impostata") problems.append("openai_api_key mancante in config.json")
if problems: if problems:
print("[CONFIG] Correggi questi parametri prima di proseguire:") print("[CONFIG] Correggi config.json:")
for p in problems: for p in problems:
print(" -", p) print(" -", p)
return False return False
return True return True
# ==========================
# Prompt esterno
# ==========================
def load_prompt(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
print(f"[PROMPT] Caricato: {path}")
return f.read()
except Exception as e:
print(f"[ATTENZIONE] Prompt non trovato o illeggibile: {path} ({e})")
return ""
def render_prompt(template: str, **vars_) -> str:
# Sostituzione semplice stile {{nome}}
out = template
for k, v in vars_.items():
out = out.replace(f"{{{{{k}}}}}", str(v))
return out
# ========================== # ==========================
# IMAP helpers robusti # IMAP helpers robusti
# ========================== # ==========================
@ -229,25 +278,33 @@ def generate_response(email_info, kb):
f"Corpo: {email_info.get('body_text','')}" f"Corpo: {email_info.get('body_text','')}"
) )
prompt = f""" # Prompt da file esterno (con fallback interno)
Sei Aurora, fondatrice di YogaSoul (www.yogasoul.it), stile zen e informale, diretto. template = load_prompt(PROMPT_PATH)
Rispondi in italiano, amichevole e rilassato, con emoticon yoga (🌿, 🧘, 😊) senza esagerare. if not template:
Saluta con "Ciao {nickname}, bello sentirti!" e firma con "Namaste, Aurora - YogaSoul". template = (
Usa la knowledge base per corsi, orari, prezzi, benefici, insegnanti. "Sei Aurora, fondatrice di YogaSoul (www.yogasoul.it), stile zen e informale, diretto.\n"
Includi sempre il link_prenotazione specifico come <a href='link'>Iscriviti qui</a> per prenotazioni, "Rispondi in italiano, amichevole e rilassato, con emoticon yoga (🌿, 🧘‍♀️, 😊) senza esagerare.\n"
e il calendario <a href='https://yogasoul.it/wp-content/uploads/2025/08/Calendario-settembre-2025-2.jpg'>qui</a>. "Saluta con \"Ciao {{nickname}}, bello sentirti!\" e firma con \"Namaste, Aurora - YogaSoul\".\n"
Se non sai, scrivi: "Contattami per dettagli! 🧘‍♀️". "Usa la knowledge base per corsi, orari, prezzi, benefici, insegnanti.\n"
"Includi sempre il link_prenotazione specifico come <a href='link'>Iscriviti qui</a> per prenotazioni,\n"
"e il calendario <a href='https://yogasoul.it/wp-content/uploads/2025/08/Calendario-settembre-2025-2.jpg'>qui</a>.\n"
"Se non sai, scrivi: \"Contattami per dettagli! 🧘‍♀️\".\n\n"
"{{policy}}\n\n"
"Knowledge Base:\n"
"{{kb_json}}\n\n"
"Email ricevuta:\n"
"{{email_text}}\n\n"
"Scrivi la risposta in HTML pulito (usa <p>, <ul>/<li> se utile; niente CSS superfluo).\n"
)
{policy} prompt = render_prompt(
template,
nickname=nickname,
policy=policy,
kb_json=kb_json,
email_text=email_text,
)
Knowledge Base:
{kb_json}
Email ricevuta:
{email_text}
Scrivi la risposta in HTML pulito (usa <p>, <ul>/<li> se utile; niente CSS superfluo).
"""
resp = client.chat.completions.create( resp = client.chat.completions.create(
model=OPENAI_MODEL, model=OPENAI_MODEL,
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
@ -283,7 +340,7 @@ def fetch_all_unseen(mail, limit=None):
return results return results
if limit and limit > 0: if limit and limit > 0:
ids = ids[-limit:] # ⬅️ prendi SOLO le ultime N non lette ids = ids[-limit:] # prendi SOLO le ultime N non lette
for seq_id in ids: for seq_id in ids:
typ, msg_data = mail.fetch(seq_id, "(RFC822)") typ, msg_data = mail.fetch(seq_id, "(RFC822)")
@ -335,9 +392,10 @@ def fetch_all_unseen(mail, limit=None):
def prepare_reply_mime(email_info, response_html): def prepare_reply_mime(email_info, response_html):
"""Costruisce il MIME HTML della risposta con quote e thread headers.""" """Costruisce il MIME HTML della risposta con quote e thread headers."""
reply_body = (response_html.replace("</body>", _make_quoted_original(email_info.get("body_text","")) + "</body>") reply_body = _inject_before_body_end(
if "</body>" in response_html.lower() response_html,
else response_html + _make_quoted_original(email_info.get("body_text",""))) _make_quoted_original(email_info.get("body_text",""))
)
msg = MIMEText(reply_body, "html", "utf-8") msg = MIMEText(reply_body, "html", "utf-8")
subj = email_info.get("subject") or "[Risposta automatica] - YogaSoul" subj = email_info.get("subject") or "[Risposta automatica] - YogaSoul"
@ -388,9 +446,23 @@ def append_draft(mail, folder, msg):
# MAIN # MAIN
# ========================== # ==========================
def main(): def main():
if not assert_config(): # Controllo config di base
problems = []
if not isinstance(IMAP_SERVER, str) or not IMAP_SERVER.strip():
problems.append("imap_server mancante in config.json")
if not isinstance(EMAIL_ADDRESS, str) or not EMAIL_ADDRESS.strip():
problems.append("email_address mancante in config.json")
if not isinstance(EMAIL_PASSWORD, str) or not EMAIL_PASSWORD.strip():
problems.append("email_password mancante in config.json")
if not isinstance(OPENAI_API_KEY, str) or not OPENAI_API_KEY.strip():
problems.append("openai_api_key mancante in config.json")
if problems:
print("[CONFIG] Correggi config.json:")
for p in problems:
print(" -", p)
return return
# Login IMAP
try: try:
mail = imaplib.IMAP4_SSL(IMAP_SERVER) mail = imaplib.IMAP4_SSL(IMAP_SERVER)
mail.login(EMAIL_ADDRESS, EMAIL_PASSWORD) mail.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
@ -403,7 +475,7 @@ def main():
drafts_folder, delim = ensure_drafts_folder(mail) drafts_folder, delim = ensure_drafts_folder(mail)
print(f"[DEBUG] drafts_folder='{drafts_folder}' delim='{delim}'") print(f"[DEBUG] drafts_folder='{drafts_folder}' delim='{delim}'")
emails = fetch_all_unseen(mail, limit=MAX_TO_PROCESS) # ⬅️ usa il limite emails = fetch_all_unseen(mail, limit=MAX_TO_PROCESS)
if not emails: if not emails:
print("Nessuna nuova email da processare.") print("Nessuna nuova email da processare.")
mail.logout() mail.logout()

View File

@ -0,0 +1,17 @@
Sei Aurora, fondatrice di YogaSoul (www.yogasoul.it), stile zen e informale, diretto.
Rispondi in italiano, amichevole e rilassato, con emoticon yoga (🌿, 🧘‍♀️, 😊) senza esagerare.
Saluta con "Ciao {{nickname}}, bello sentirti!" e firma con "Namaste, Aurora - YogaSoul".
Usa la knowledge base per corsi, orari, prezzi, benefici, insegnanti.
Includi sempre il link_prenotazione specifico come <a href='link'>Iscriviti qui</a> per prenotazioni,
e il calendario <a href='https://yogasoul.it/wp-content/uploads/2025/08/Calendario-settembre-2025-2.jpg'>qui</a>.
Se non sai, scrivi: "Contattami per dettagli! 🧘‍♀️".
{{policy}}
Knowledge Base:
{{kb_json}}
Email ricevuta:
{{email_text}}
Scrivi la risposta in HTML pulito (usa <p>, <ul>/<li> se utile; niente CSS superfluo).