added teacher and option teacher in school

This commit is contained in:
2026-10-01 11:34:07 +02:00
parent 38b7aca148
commit 76fcfb2ba3
14 changed files with 1354 additions and 86 deletions
+166 -53
View File
@@ -1,13 +1,19 @@
<?php
/**
* YogiBook - Riepilogo classi giornaliero per l'insegnante.
* Invia UNA mail per ogni classe del giorno.
* YogiBook - Riepilogo classi giornaliero.
* Invia UNA mail per ogni classe del giorno:
* - all'insegnante della classe (se notifiche attive, attivo e con email)
* -> versione riservata: nomi "Mario R.", senza email dei partecipanti
* - al titolare, se diverso dall'insegnante (se notifiche attive e con email)
* -> copia separata con i dati completi
* - se l'insegnante È il titolare: una sola mail completa
*
* Uso:
* php cron/class_report.php -> classi di oggi
* php cron/class_report.php 2026-09-25 -> classi di una data specifica
* php cron/class_report.php --dry-run -> NON invia, salva le anteprime HTML in cron/preview/
* php cron/class_report.php --only-at=17 -> esce senza fare nulla se in Italia non sono le 17
*/
use PHPMailer\PHPMailer\PHPMailer;
@@ -56,12 +62,35 @@ function chip(string $label, string $bg, string $fg): string
return '<span style="display:inline-block;background:' . $bg . ';color:' . $fg . ';font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">' . h($label) . '</span>';
}
/** Nome completo (versione titolare) */
function fullName(array $p): string
{
$n = trim(($p['first_name'] ?? '') . ' ' . ($p['last_name'] ?? ''));
return $n !== '' ? $n : 'Utente #' . (int) ($p['iduser'] ?? 0);
}
/** Nome + iniziale del cognome (versione insegnante): "Mario R." */
function shortName(array $p): string
{
$first = trim((string) ($p['first_name'] ?? ''));
$last = trim((string) ($p['last_name'] ?? ''));
if ($first === '' && $last === '') {
return 'Utente #' . (int) ($p['iduser'] ?? 0);
}
$ini = $last !== '' ? ' ' . mb_strtoupper(mb_substr($last, 0, 1)) . '.' : '';
return ($first !== '' ? $first : '') . $ini;
}
function personName(array $p, bool $full): string
{
return $full ? fullName($p) : shortName($p);
}
function teacherName(?array $t): string
{
return $t ? trim($t['nameemployee'] . ' ' . $t['surnameemployee']) : '';
}
function italianDate(DateTime $d): string
{
$days = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'];
@@ -69,6 +98,27 @@ function italianDate(DateTime $d): string
return $days[(int) $d->format('w')] . ' ' . (int) $d->format('j') . ' ' . $months[(int) $d->format('n')] . ' ' . $d->format('Y');
}
/** Motivo per cui non può ricevere ('' = può ricevere) */
function whyNot(?array $t): string
{
if (!$t) return 'nessun insegnante assegnato';
if (($t['active'] ?? 'N') !== 'Y') return 'insegnante non attivo';
if (($t['notifications'] ?? 'N') !== 'Y') return 'notifiche disattivate';
if (empty($t['email'])) return 'email mancante';
if (!filter_var($t['email'], FILTER_VALIDATE_EMAIL)) return 'email non valida';
return '';
}
/** Può ricevere mail? notifiche attive, attivo, email valida */
function canReceive(?array $t): bool
{
return $t
&& ($t['notifications'] ?? 'N') === 'Y'
&& ($t['active'] ?? 'N') === 'Y'
&& !empty($t['email'])
&& filter_var($t['email'], FILTER_VALIDATE_EMAIL);
}
/* ----------------------------------------------------------- argomenti */
$dryRun = in_array('--dry-run', $argv, true);
@@ -79,18 +129,27 @@ foreach (array_slice($argv, 1) as $a) {
}
}
$recipients = array_values(array_filter(array_map('trim', explode(',', (string) cfg('TEACHER_EMAIL', '')))));
if (!$recipients && !$dryRun) {
logmsg('ERRORE: TEACHER_EMAIL non impostato nel .env');
exit(1);
}
$location = cfg('CLASS_LOCATION', 'via Valassina 62/B Seregno - Sala Contesto Yoga');
// Le mail agli insegnanti (non titolari) partono solo se l'opzione "Mostra insegnante" è attiva
$optRow = $pdo->query("SELECT show_teacher FROM `option` LIMIT 1")->fetch();
$teacherMailsEnabled = ($optRow['show_teacher'] ?? 'N') === 'Y';
/* ---------------------------------------------------------- insegnanti */
$employees = [];
$owner = null;
foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, email, is_owner, notifications, active FROM employee")->fetchAll() as $t) {
$employees[(int) $t['idemployee']] = $t;
if ($t['is_owner'] === 'Y' && $owner === null) {
$owner = $t;
}
}
/* ------------------------------------------------------ classi del giorno */
$stmt = $pdo->prepare(
"SELECT ss.idserviceschedule, ss.dateschedule, s.servicename, s.colorclass, s.maxcapacity
"SELECT ss.idserviceschedule, ss.dateschedule, s.servicename, s.colorclass, s.maxcapacity, s.idteacher
FROM serviceschedule ss
LEFT JOIN service s ON ss.idservice = s.idservice
WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2
@@ -127,7 +186,7 @@ foreach ($stmtB->fetchAll() as $b) {
/* ------------------------------------------------------------ template */
function personRows(array $list, string $type): string
function personRows(array $list, string $type, bool $full): string
{
if (!$list) {
return '<tr><td style="padding:16px;color:#9ca3af;font-size:14px;text-align:center;">Nessun partecipante</td></tr>';
@@ -145,12 +204,14 @@ function personRows(array $list, string $type): string
$chipHtml = chip('In coda', '#fbeef3', '#d1698a');
}
$nameColor = $isLost ? '#9ca3af' : '#1f2937';
$email = !empty($p['email']) ? '<br><span style="font-size:12px;color:#6b7280;font-weight:400;">' . h($p['email']) . '</span>' : '';
$email = ($full && !empty($p['email']))
? '<br><span style="font-size:12px;color:#6b7280;font-weight:400;">' . h($p['email']) . '</span>'
: '';
$html .= '<tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;">'
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>'
. '<td width="34" valign="top" style="font-weight:800;color:' . $numColor . ';font-size:14px;padding-top:1px;">' . $i . ($type === 'waiting' ? '°' : '.') . '</td>'
. '<td valign="top" style="font-size:15px;color:' . $nameColor . ';font-weight:600;">' . h(fullName($p)) . $email . '</td>'
. '<td valign="top" style="font-size:15px;color:' . $nameColor . ';font-weight:600;">' . h(personName($p, $full)) . $email . '</td>'
. '<td valign="top" align="right" width="100">' . $chipHtml . '</td>'
. '</tr></table></td></tr>';
$i++;
@@ -174,7 +235,12 @@ function statBox(string $value, string $label, string $color): string
. '</td>';
}
function renderEmail(array $cls, array $booked, array $pending, array $waiting, string $location): string
/**
* @param bool $full true = versione titolare (nomi completi + email)
* @param string $teacherFull nome insegnante da mostrare in testata
* @param string|null $copyNote nota "copia per il titolare" (solo se il titolare non è l'insegnante)
*/
function renderEmail(array $cls, array $booked, array $pending, array $waiting, string $location, bool $full, string $teacherFull, ?string $copyNote): string
{
$d = new DateTime($cls['dateschedule']);
$accent = $cls['colorclass'] ?: '#1ebf73';
@@ -195,42 +261,54 @@ function renderEmail(array $cls, array $booked, array $pending, array $waiting,
? '<div style="font-size:13px;color:#6b7280;margin-top:6px;">di cui ' . $nLost . ' segnat' . ($nLost === 1 ? 'a' : 'e') . ' come pers' . ($nLost === 1 ? 'a' : 'e') . '</div>'
: '';
$body = section('Partecipanti confermati (' . $nBooked . ')', '#374151', personRows($booked, 'booked'));
$body = section('Partecipanti confermati (' . $nBooked . ')', '#374151', personRows($booked, 'booked', $full));
if ($pending) {
$body .= section('In attesa di conferma (' . count($pending) . ')', '#8a6d00', personRows($pending, 'pending'));
$body .= section('In attesa di conferma (' . count($pending) . ')', '#8a6d00', personRows($pending, 'pending', $full));
}
if ($waiting) {
$body .= section('Lista d\'attesa (' . count($waiting) . ')', '#d1698a', personRows($waiting, 'waiting'));
$body .= section('Lista d\'attesa (' . count($waiting) . ')', '#d1698a', personRows($waiting, 'waiting', $full));
}
$copyBanner = $copyNote
? '<tr><td style="padding:14px 28px 0;"><div style="background:#f5eee4;color:#5a4630;border-radius:10px;padding:10px 14px;font-size:13px;">' . $copyNote . '</div></td></tr>'
: '';
$teacherLine = $teacherFull !== ''
? '<div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">' . h($teacherFull) . '</strong></div>'
: '';
return '<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . h($cls['servicename']) . '</title></head>'
. '<body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;">'
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center">'
. '<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);">'
// barra colore classe
. '<tr><td style="height:8px;background:' . h($accent) . ';font-size:0;line-height:0;">&nbsp;</td></tr>'
// intestazione
. $copyBanner
. '<tr><td style="padding:26px 28px 6px;">'
. '<div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div>'
. '<h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">' . h($cls['servicename']) . '</h1>'
. '<div style="font-size:15px;color:#374151;font-weight:600;">' . h(italianDate($d)) . ' · ore ' . $d->format('H:i') . '</div>'
. '<div style="font-size:13px;color:#6b7280;margin-top:4px;">' . h($location) . '</div>'
. $teacherLine
. '</td></tr>'
// statistiche
. '<tr><td style="padding:16px 22px 0;">' . $stats . '<div style="padding:0 6px;">' . $lostNote . '</div></td></tr>'
// elenchi
. $body
// footer
. '<tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il ' . date('d/m/Y \a\l\l\e H:i') . '</td></tr>'
. '</table></td></tr></table></body></html>';
}
function renderText(array $cls, array $booked, array $pending, array $waiting, string $location): string
function renderText(array $cls, array $booked, array $pending, array $waiting, string $location, bool $full, string $teacherFull, ?string $copyNote): string
{
$d = new DateTime($cls['dateschedule']);
$max = (int) $cls['maxcapacity'];
$t = $cls['servicename'] . "\n" . italianDate($d) . ' ore ' . $d->format('H:i') . "\n" . $location . "\n\n";
$t .= 'Iscritti: ' . count($booked) . '/' . $max . ' | In attesa: ' . count($pending) . ' | In coda: ' . count($waiting) . "\n\n";
$t = '';
if ($copyNote) {
$t .= strip_tags($copyNote) . "\n\n";
}
$t .= $cls['servicename'] . "\n" . italianDate($d) . ' ore ' . $d->format('H:i') . "\n" . $location . "\n";
if ($teacherFull !== '') {
$t .= 'Insegnante: ' . $teacherFull . "\n";
}
$t .= "\nIscritti: " . count($booked) . '/' . $max . ' | In attesa: ' . count($pending) . ' | In coda: ' . count($waiting) . "\n\n";
$blocks = ['CONFERMATI' => $booked, 'IN ATTESA DI CONFERMA' => $pending, "LISTA D'ATTESA" => $waiting];
foreach ($blocks as $title => $list) {
if ($title !== 'CONFERMATI' && !$list) {
@@ -243,7 +321,7 @@ function renderText(array $cls, array $booked, array $pending, array $waiting, s
$i = 1;
foreach ($list as $p) {
$lost = ($p['status'] === 'booked' && ($p['lostlesson'] ?? '') === 'Y') ? ' (PERSA)' : '';
$t .= ' ' . $i++ . '. ' . fullName($p) . $lost . "\n";
$t .= ' ' . $i++ . '. ' . personName($p, $full) . $lost . "\n";
}
$t .= "\n";
}
@@ -276,6 +354,7 @@ function makeMailer(): PHPMailer
$mailer = $dryRun ? null : makeMailer();
$sent = 0;
$failed = 0;
$skipped = 0;
foreach ($classes as $cls) {
$sid = (int) $cls['idserviceschedule'];
@@ -284,51 +363,85 @@ foreach ($classes as $cls) {
$pending = array_values(array_filter($all, fn($b) => $b['status'] === 'pending'));
$waiting = array_values(array_filter($all, fn($b) => $b['status'] === 'waiting'));
$teacher = $employees[(int) $cls['idteacher']] ?? null;
$teacherIsOwner = $teacher && $owner && (int) $teacher['idemployee'] === (int) $owner['idemployee'];
$tName = teacherName($teacher);
$d = new DateTime($cls['dateschedule']);
$subject = $cls['servicename'] . ' · ' . $d->format('d/m') . ' ore ' . $d->format('H:i')
$baseSubject = $cls['servicename'] . ' · ' . $d->format('d/m') . ' ore ' . $d->format('H:i')
. ' · ' . count($booked) . '/' . (int) $cls['maxcapacity'] . ' iscritti';
if ($pending) {
$subject .= ' · ' . count($pending) . ' in attesa';
$baseSubject .= ' · ' . count($pending) . ' in attesa';
}
if ($waiting) {
$subject .= ' · ' . count($waiting) . ' in coda';
$baseSubject .= ' · ' . count($waiting) . ' in coda';
}
$html = renderEmail($cls, $booked, $pending, $waiting, $location);
$text = renderText($cls, $booked, $pending, $waiting, $location);
if ($dryRun) {
$dir = __DIR__ . '/preview';
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
// Chi riceve cosa
$deliveries = [];
if ($teacherIsOwner) {
if (canReceive($owner)) {
$deliveries[] = ['to' => $owner, 'full' => true, 'copy' => null, 'tag' => 'titolare'];
}
$file = $dir . '/classe_' . $sid . '.html';
file_put_contents($file, $html);
logmsg("[DRY-RUN] $subject -> $file");
} else {
if (!$teacherMailsEnabled) {
logmsg("Classe #$sid ({$cls['servicename']}): mail all'insegnante non inviata (opzione insegnanti disattivata)");
} elseif (canReceive($teacher)) {
$deliveries[] = ['to' => $teacher, 'full' => false, 'copy' => null, 'tag' => 'insegnante'];
} else {
logmsg("Classe #$sid ({$cls['servicename']}): insegnante " . ($tName ?: '-') . " escluso: " . whyNot($teacher));
}
if (canReceive($owner)) {
$note = $tName !== ''
? '<strong>Copia per il titolare</strong> · classe di ' . h($tName)
: '<strong>Copia per il titolare</strong> · classe senza insegnante assegnato';
$deliveries[] = ['to' => $owner, 'full' => true, 'copy' => $note, 'tag' => 'titolare'];
}
}
if (!$deliveries) {
$skipped++;
logmsg("SALTATA classe #$sid ({$cls['servicename']}): nessun destinatario (notifiche disattivate o email mancante).");
continue;
}
try {
$mailer->clearAddresses();
foreach ($recipients as $r) {
$mailer->addAddress($r, cfg('TEACHER_NAME', ''));
foreach ($deliveries as $dv) {
$subject = ($dv['copy'] ? '[Copia] ' : '') . $baseSubject;
$html = renderEmail($cls, $booked, $pending, $waiting, $location, $dv['full'], $tName, $dv['copy']);
$text = renderText($cls, $booked, $pending, $waiting, $location, $dv['full'], $tName, $dv['copy']);
$toName = teacherName($dv['to']);
if ($dryRun) {
$dir = __DIR__ . '/preview';
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
$file = $dir . '/classe_' . $sid . '_' . $dv['tag'] . '.html';
file_put_contents($file, $html);
logmsg("[DRY-RUN] a {$toName} <{$dv['to']['email']}> ({$dv['tag']}): $subject -> $file");
continue;
}
$mailer->Subject = $subject;
$mailer->Body = $html;
$mailer->AltBody = $text;
$mailer->send();
$sent++;
logmsg("OK: $subject");
} catch (MailException $e) {
$failed++;
logmsg("ERRORE classe #$sid: " . $mailer->ErrorInfo);
try {
$mailer->clearAddresses();
$mailer->addAddress($dv['to']['email'], $toName);
$mailer->Subject = $subject;
$mailer->Body = $html;
$mailer->AltBody = $text;
$mailer->send();
$sent++;
logmsg("OK a {$toName} ({$dv['tag']}): $subject");
} catch (MailException $e) {
$failed++;
logmsg("ERRORE classe #$sid a {$dv['to']['email']}: " . $mailer->ErrorInfo);
}
sleep(1); // evita limiti di invio del server SMTP
}
sleep(1); // evita limiti di invio del server SMTP
}
if ($mailer) {
$mailer->smtpClose();
}
logmsg("Fine: $sent inviate, $failed errori (" . count($classes) . " classi il $day).");
logmsg("Fine: $sent inviate, $failed errori, $skipped classi senza destinatari (" . count($classes) . " classi il $day).");
exit($failed > 0 ? 1 : 0);
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;">&nbsp;</td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia Benucci<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@claudiosironi.com</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:15</td></tr></table></td></tr></table></body></html>
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;">&nbsp;</td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">Francesca -</strong></div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia B.</td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:30</td></tr></table></td></tr></table></body></html>
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;">&nbsp;</td></tr><tr><td style="padding:14px 28px 0;"><div style="background:#f5eee4;color:#5a4630;border-radius:10px;padding:10px 14px;font-size:13px;"><strong>Copia per il titolare</strong> · classe di Francesca -</div></td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">Francesca -</strong></div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia Benucci<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@claudiosironi.com</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:30</td></tr></table></td></tr></table></body></html>
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class ExtendEmployeeForTeachers extends AbstractMigration
{
public function up(): void
{
$this->table('employee')
->addColumn('email', 'string', ['limit' => 150, 'null' => true, 'after' => 'surnameemployee'])
->addColumn('phone', 'string', ['limit' => 40, 'null' => true, 'after' => 'email'])
->addColumn('photo', 'string', ['limit' => 191, 'null' => true, 'after' => 'phone'])
->addColumn('is_owner', 'char', [
'limit' => 1,
'null' => false,
'default' => 'N',
'comment' => 'Y = titolare della scuola (uno solo)',
'after' => 'photo',
])
->addColumn('notifications', 'char', [
'limit' => 1,
'null' => false,
'default' => 'Y',
'comment' => 'Y = riceve le email',
'after' => 'is_owner',
])
->addColumn('active', 'char', ['limit' => 1, 'null' => false, 'default' => 'Y', 'after' => 'notifications'])
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP'])
->update();
// L'insegnante già presente (id più basso) diventa il titolare
$this->execute("UPDATE `employee` SET `is_owner` = 'Y' ORDER BY `idemployee` LIMIT 1");
}
public function down(): void
{
$this->table('employee')
->removeColumn('email')
->removeColumn('phone')
->removeColumn('photo')
->removeColumn('is_owner')
->removeColumn('notifications')
->removeColumn('active')
->removeColumn('created_at')
->removeColumn('updated_at')
->update();
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddShowTeacherToOption extends AbstractMigration
{
public function change(): void
{
$this->table('option')
->addColumn('show_teacher', 'char', [
'limit' => 1,
'null' => false,
'default' => 'N',
'comment' => 'Y = mostra l\'insegnante nelle card delle classi',
'after' => 'waitinglist_enabled',
])
->update();
}
}
+173 -6
View File
@@ -11,9 +11,56 @@ function e($value): string
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function teacherInitials(string $first, string $last): string
{
$ini = mb_strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1));
return $ini !== '' ? $ini : '?';
}
function teacherColor(string $seed): string
{
$palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#b08d57'];
return $palette[abs(crc32($seed)) % count($palette)];
}
$message = $_GET['message'] ?? 'N';
$insertFeedback = null;
/* -------------------------------------------------------------------------
* Insegnanti (titolare per primo, poi attivi, poi alfabetico)
* ---------------------------------------------------------------------- */
$teachers = $pdo->query(
"SELECT idemployee, nameemployee, surnameemployee, photo, is_owner, active
FROM employee
ORDER BY is_owner DESC, active DESC, surnameemployee, nameemployee"
)->fetchAll();
$teachersById = [];
$ownerId = 0;
foreach ($teachers as $t) {
$teachersById[(int) $t['idemployee']] = $t;
if ($t['is_owner'] === 'Y' && !$ownerId) {
$ownerId = (int) $t['idemployee'];
}
}
/* -------------------------------------------------------------------------
* Assegnazione insegnante a una classe esistente (POST)
* ---------------------------------------------------------------------- */
if (($_POST['action'] ?? '') === 'assignteacher') {
$idservice = (int) ($_POST['idservice'] ?? 0);
$idteacher = (int) ($_POST['idteacher'] ?? 0);
if ($idservice > 0 && isset($teachersById[$idteacher])) {
$stmt = $pdo->prepare("UPDATE service SET idteacher = :t WHERE idservice = :s");
$stmt->execute([':t' => $idteacher, ':s' => $idservice]);
header('Location: ' . $_SERVER['PHP_SELF'] . '?message=teacherassigned');
} else {
header('Location: ' . $_SERVER['PHP_SELF'] . '?message=teachererror');
}
exit;
}
/* -------------------------------------------------------------------------
* Inserimento nuova classe (POST) con prepared statement
* ---------------------------------------------------------------------- */
@@ -26,14 +73,17 @@ if (isset($_POST['classinsert']) && $_POST['classinsert'] === 'Y') {
$maxcapacity = $_POST['maxcapacity'] ?? '';
$colorclass = trim($_POST['colorclass'] ?? '');
$classduration = $_POST['classduration'] ?? '';
$idteacher = (int) ($_POST['idteacher'] ?? 0);
if ($servicename === '') {
$insertFeedback = ['type' => 'error', 'text' => 'La descrizione della classe è obbligatoria.'];
} elseif (!isset($teachersById[$idteacher])) {
$insertFeedback = ['type' => 'error', 'text' => 'Seleziona l\'insegnante della classe.'];
} else {
$sql = "INSERT INTO service
(servicename, wpcatalognumber, day, time, category, maxcapacity, colorclass, classduration)
(servicename, wpcatalognumber, day, time, category, maxcapacity, colorclass, classduration, idteacher)
VALUES
(:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration)";
(:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration, :idteacher)";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':servicename' => $servicename,
@@ -44,6 +94,7 @@ if (isset($_POST['classinsert']) && $_POST['classinsert'] === 'Y') {
':maxcapacity' => $maxcapacity !== '' ? (int) $maxcapacity : null,
':colorclass' => $colorclass,
':classduration' => $classduration !== '' ? (float) $classduration : null,
':idteacher' => $idteacher,
]);
if ($ok) {
@@ -83,6 +134,32 @@ $propRows = $pdo->query(
foreach ($propRows as $pr) {
$propagationsByService[$pr['idservice']][] = $pr;
}
/**
* Opzioni del select insegnanti.
* Gli insegnanti non attivi compaiono solo se già assegnati a quella classe.
*/
function teacherOptions(array $teachers, int $selectedId): string
{
$html = '';
$found = false;
foreach ($teachers as $t) {
$tid = (int) $t['idemployee'];
$isSel = $tid === $selectedId;
if ($t['active'] !== 'Y' && !$isSel) {
continue;
}
if ($isSel) $found = true;
$label = trim($t['nameemployee'] . ' ' . $t['surnameemployee'])
. ($t['is_owner'] === 'Y' ? ' (titolare)' : '')
. ($t['active'] !== 'Y' ? ' (non attivo)' : '');
$html .= '<option value="' . $tid . '"' . ($isSel ? ' selected' : '') . '>' . e($label) . '</option>';
}
if (!$found) {
$html = '<option value="" selected disabled>— Non assegnato —</option>' . $html;
}
return $html;
}
?>
<!doctype html>
<html lang="it">
@@ -197,6 +274,43 @@ foreach ($propRows as $pr) {
.action-btns .btn {
margin: 2px;
}
.teacher-cell {
display: flex;
align-items: center;
gap: 8px;
min-width: 210px;
}
.teacher-mini {
flex: 0 0 30px;
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 11px;
font-weight: 800;
}
.teacher-mini.none {
background: #f3f4f6;
color: #c0c4cc;
}
.teacher-select {
padding: 5px 30px 5px 10px !important;
font-size: 13px;
}
.teacher-select.unassigned {
border-color: #f0b27a;
background-color: #fff8f1;
color: #b9770e;
}
</style>
</head>
@@ -238,6 +352,12 @@ foreach ($propRows as $pr) {
<?php if ($message === 'inserted') : ?>
<div class="alert alert-success" role="alert">Classe inserita con successo.</div>
<?php endif; ?>
<?php if ($message === 'teacherassigned') : ?>
<div class="alert alert-success" role="alert">Insegnante assegnato con successo.</div>
<?php endif; ?>
<?php if ($message === 'teachererror') : ?>
<div class="alert alert-danger" role="alert">Impossibile assegnare l'insegnante. Riprova.</div>
<?php endif; ?>
<?php if ($message === 'success') : ?>
<div class="alert alert-success" role="alert">Propagazione avvenuta con successo.</div>
<?php endif; ?>
@@ -261,6 +381,11 @@ foreach ($propRows as $pr) {
<?php echo e($insertFeedback['text']); ?>
</div>
<?php endif; ?>
<?php if (empty($teachers)) : ?>
<div class="alert alert-warning" role="alert">
Non ci sono insegnanti: <a href="insegnanti.php">aggiungine almeno uno</a> prima di creare una classe.
</div>
<?php endif; ?>
<!-- Form inserimento classe -->
<div class="row">
@@ -343,6 +468,14 @@ foreach ($propRows as $pr) {
<input type="text" class="form-control" id="colorclass" name="colorclass" value="#1ebf73">
</div>
</div>
<div class="col-lg-4">
<div class="mb-3">
<label for="idteacher" class="form-label">Insegnante</label>
<select name="idteacher" id="idteacher" class="form-select" required>
<?php echo teacherOptions($teachers, $ownerId); ?>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-save">Inserisci classe</button>
@@ -358,7 +491,7 @@ foreach ($propRows as $pr) {
<div class="card admin-card">
<div class="card-body">
<div class="section-title">Classi esistenti</div>
<p class="section-lead">Propaga una classe sul calendario, associala o modificane i dettagli.</p>
<p class="section-lead">Propaga una classe sul calendario, associala, assegna l'insegnante o modificane i dettagli.</p>
<div class="table-responsive">
<table class="table class-table align-middle mb-0">
@@ -369,16 +502,20 @@ foreach ($propRows as $pr) {
<th>Orario</th>
<th>Durata</th>
<th>Categoria</th>
<th>Insegnante</th>
<th class="text-end">Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($services)) : ?>
<tr>
<td colspan="6" class="text-center text-muted py-4">Nessuna classe presente.</td>
<td colspan="7" class="text-center text-muted py-4">Nessuna classe presente.</td>
</tr>
<?php else : ?>
<?php foreach ($services as $svc) : ?>
<?php foreach ($services as $svc) :
$svcTeacherId = (int) $svc['idteacher'];
$svcTeacher = $teachersById[$svcTeacherId] ?? null;
?>
<tr>
<td>
<span class="color-dot" style="background-color: <?php echo e($svc['colorclass'] ?: '#ccc'); ?>;"></span>
@@ -391,6 +528,25 @@ foreach ($propRows as $pr) {
<?php echo e($svc['classduration']); ?>
</td>
<td><?php echo e($svc['namecategory']); ?></td>
<td>
<form method="post" class="teacher-cell">
<input type="hidden" name="action" value="assignteacher">
<input type="hidden" name="idservice" value="<?php echo (int) $svc['idservice']; ?>">
<?php if ($svcTeacher && $svcTeacher['photo']) : ?>
<img class="teacher-mini" src="assets/images/teachers/<?php echo e($svcTeacher['photo']); ?>" alt="">
<?php elseif ($svcTeacher) :
$tName = trim($svcTeacher['nameemployee'] . ' ' . $svcTeacher['surnameemployee']); ?>
<span class="teacher-mini" style="background:<?php echo teacherColor($tName); ?>;"><?php echo e(teacherInitials($svcTeacher['nameemployee'], $svcTeacher['surnameemployee'])); ?></span>
<?php else : ?>
<span class="teacher-mini none"><i class="fas fa-user"></i></span>
<?php endif; ?>
<select name="idteacher" class="form-select teacher-select <?php echo $svcTeacher ? '' : 'unassigned'; ?>"
data-current="<?php echo $svcTeacherId; ?>"
data-classname="<?php echo e($svc['servicename'] . ' ' . $svc['day'] . ' ' . $svc['time']); ?>">
<?php echo teacherOptions($teachers, $svcTeacherId); ?>
</select>
</form>
</td>
<td class="text-end action-btns">
<button type="button" class="btn btn-primary btn-sm toggle-form">Propaga</button>
<a href="associate-services.php?id=<?php echo (int) $svc['idservice']; ?>">
@@ -407,7 +563,7 @@ foreach ($propRows as $pr) {
</td>
</tr>
<tr class="expanded-row">
<td colspan="6">
<td colspan="7">
<div class="expanded-content">
<form class="calendar-form" method="post" action="nextdateclass.php">
<div class="row">
@@ -514,6 +670,17 @@ foreach ($propRows as $pr) {
$('#colorclass').val(col.toHexString());
}
});
// Cambio insegnante dalla tabella: conferma e salva
$('.teacher-select').on('change', function() {
var sel = $(this);
var teacherName = sel.find('option:selected').text();
if (confirm('Assegnare ' + teacherName + ' alla classe ' + sel.data('classname') + '?')) {
sel.closest('form')[0].submit();
} else {
sel.val(String(sel.data('current')) === '0' ? '' : String(sel.data('current')));
}
});
});
// Espansione riga propagazione
+58 -2
View File
@@ -28,8 +28,16 @@ function avatarColor(string $seed): string
return $palette[$idx];
}
$optRow = $pdo->query("SELECT maxbeforetimecancell FROM `option` LIMIT 1")->fetch();
$optRow = $pdo->query("SELECT maxbeforetimecancell, show_teacher FROM `option` LIMIT 1")->fetch();
$timetocancel = $optRow ? (int) $optRow['maxbeforetimecancell'] : 24;
$showTeacher = ($optRow['show_teacher'] ?? 'N') === 'Y';
$teachersById = [];
if ($showTeacher) {
foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, photo FROM employee")->fetchAll() as $t) {
$teachersById[(int) $t['idemployee']] = $t;
}
}
if (isset($_GET['prev_month'])) {
$currentMonthStart = preg_replace('/[^0-9\-]/', '', $_GET['prev_month']) . '-01';
@@ -44,7 +52,7 @@ if (strtotime($currentMonthStart) === false) {
$currentMonthEnd = date("Y-m-t", strtotime($currentMonthStart));
$sqlSchedule = "SELECT serviceschedule.*, service.servicename, service.colorclass,
service.maxcapacity, service.idservice
service.maxcapacity, service.idservice, service.idteacher
FROM serviceschedule
LEFT JOIN service ON serviceschedule.idservice = service.idservice
WHERE serviceschedule.dateschedule BETWEEN :start AND DATE_ADD(:end, INTERVAL 1 DAY)
@@ -231,6 +239,38 @@ $italianMonths = [
margin: 0 0 4px;
}
.class-teacher {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.class-teacher strong {
color: #374151;
font-weight: 600;
}
.teacher-avatar-sm {
flex: 0 0 26px;
width: 26px;
height: 26px;
border-radius: 50%;
object-fit: cover;
background: #E1D0B8;
color: #5a4630;
font-size: 11px;
font-weight: 800;
display: inline-flex;
align-items: center;
justify-content: center;
}
.teacher-avatar-sm.none {
background: #fdf0e3;
color: #e67e22;
}
.class-meta {
color: #6b7280;
font-size: 14px;
@@ -610,6 +650,22 @@ $italianMonths = [
</h3>
<p class="class-meta"><i class="far fa-clock me-1"></i><?php echo e($whenFmt); ?></p>
<p class="class-meta"><i class="fas fa-location-dot me-1"></i>via Valassina 62/B Seregno - Sala Contesto Yoga</p>
<?php if ($showTeacher) :
$tch = $teachersById[(int) ($sch['idteacher'] ?? 0)] ?? null; ?>
<p class="class-meta class-teacher">
<?php if ($tch) : ?>
<?php if (!empty($tch['photo'])) : ?>
<img class="teacher-avatar-sm" src="assets/images/teachers/<?php echo e($tch['photo']); ?>" alt="">
<?php else : ?>
<span class="teacher-avatar-sm"><?php echo e(initials($tch['nameemployee'], $tch['surnameemployee'])); ?></span>
<?php endif; ?>
Insegnante: <strong><?php echo e(trim($tch['nameemployee'] . ' ' . $tch['surnameemployee'])); ?></strong>
<?php else : ?>
<span class="teacher-avatar-sm none"><i class="fas fa-user" style="font-size:10px;"></i></span>
<span style="color:#e67e22;font-weight:600;">Insegnante non assegnato</span>
<?php endif; ?>
</p>
<?php endif; ?>
<div class="class-toolbar">
<button class="btn-soft btn-bookings" onclick="togglePanel(this)"><i class="fas fa-list-ul me-1"></i> Vedi prenotazioni</button>
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

+6
View File
@@ -166,6 +166,12 @@
<span class="menu-item">Opzioni Scuola</span>
</a>
</li>
<li>
<a href="insegnanti.php" class="">
<i class="bx bx-slider-alt icon nav-icon"></i>
<span class="menu-item">Insegnanti</span>
</a>
</li>
<!-- Template (demo) -->
<li class="menu-title" data-key="t-applications">Template</li>
+783
View File
@@ -0,0 +1,783 @@
<?php ob_start();
require_once('include/headscript.php');
/**
* YogiBook - Gestione insegnanti (CRUD)
* Tabella: employee
* - un solo titolare (is_owner = 'Y'), non eliminabile né disattivabile
* - notifiche email attivabili/disattivabili
* - foto opzionale (JPG/PNG/WEBP, max 3 MB) in assets/images/teachers/
*/
$pdo = DBHandlerSelect::getInstance()->getConnection();
if (!function_exists('e')) {
function e($value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
}
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (empty($_SESSION['csrf_teachers'])) {
$_SESSION['csrf_teachers'] = bin2hex(random_bytes(16));
}
$csrf = $_SESSION['csrf_teachers'];
define('PHOTO_DIR_REL', 'assets/images/teachers/');
$photoDirAbs = __DIR__ . '/' . PHOTO_DIR_REL;
/* ================================================================ helper */
function flashRedirect(string $type, string $msg): void
{
$_SESSION['teachers_flash'] = [$type, $msg];
header('Location: insegnanti.php');
exit;
}
function teacherInitials(string $first, string $last): string
{
$ini = mb_strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1));
return $ini !== '' ? $ini : '?';
}
function teacherColor(string $seed): string
{
$palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#b08d57'];
return $palette[abs(crc32($seed)) % count($palette)];
}
function deletePhotoFile(string $dir, ?string $name): void
{
if ($name && preg_match('/^teacher_[a-f0-9]{16}\.(jpg|png|webp)$/', $name) && is_file($dir . $name)) {
@unlink($dir . $name);
}
}
/** Ritorna il nome file salvato, null se nessun file caricato. Lancia RuntimeException se non valido. */
function handlePhotoUpload(array $file, string $dir): ?string
{
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
return null;
}
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Errore nel caricamento della foto.');
}
if ($file['size'] > 3 * 1024 * 1024) {
throw new RuntimeException('La foto supera i 3 MB.');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
$ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'][$mime] ?? null;
if (!$ext || @getimagesize($file['tmp_name']) === false) {
throw new RuntimeException('Formato foto non valido: usa JPG, PNG o WEBP.');
}
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
throw new RuntimeException('La cartella delle foto non è scrivibile.');
}
$name = 'teacher_' . bin2hex(random_bytes(8)) . '.' . $ext;
if (!move_uploaded_file($file['tmp_name'], $dir . $name)) {
throw new RuntimeException('Impossibile salvare la foto.');
}
return $name;
}
function findTeacher(PDO $pdo, int $id): ?array
{
$st = $pdo->prepare("SELECT * FROM employee WHERE idemployee = :id");
$st->execute([':id' => $id]);
$row = $st->fetch();
return $row ?: null;
}
/* ================================================================ azioni POST */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($csrf, (string) ($_POST['csrf'] ?? ''))) {
flashRedirect('danger', 'Sessione scaduta, riprova.');
}
$action = $_POST['action'] ?? '';
$id = (int) ($_POST['id'] ?? 0);
try {
/* ---------- crea / modifica */
if ($action === 'save') {
$name = trim((string) ($_POST['nameemployee'] ?? ''));
$surname = trim((string) ($_POST['surnameemployee'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$phone = trim((string) ($_POST['phone'] ?? ''));
$notify = isset($_POST['notifications']) ? 'Y' : 'N';
$active = isset($_POST['active']) ? 'Y' : 'N';
if ($name === '' || $surname === '') {
flashRedirect('danger', 'Nome e cognome sono obbligatori.');
}
if (mb_strlen($name) > 100 || mb_strlen($surname) > 100) {
flashRedirect('danger', 'Nome o cognome troppo lunghi.');
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
flashRedirect('danger', 'Indirizzo email non valido.');
}
if ($phone !== '' && !preg_match('/^[0-9 +().\/-]{5,40}$/', $phone)) {
flashRedirect('danger', 'Numero di telefono non valido.');
}
if ($email !== '') {
$dup = $pdo->prepare("SELECT COUNT(*) AS c FROM employee WHERE email = :email AND idemployee <> :id");
$dup->execute([':email' => $email, ':id' => $id]);
if ((int) $dup->fetch()['c'] > 0) {
flashRedirect('danger', 'Questa email è già usata da un altro insegnante.');
}
}
if ($id > 0) {
$current = findTeacher($pdo, $id);
if (!$current) {
flashRedirect('danger', 'Insegnante non trovato.');
}
if ($current['is_owner'] === 'Y' && $active === 'N') {
flashRedirect('danger', 'Il titolare non può essere disattivato.');
}
$photo = $current['photo'];
$newPhoto = handlePhotoUpload($_FILES['photo'] ?? [], $photoDirAbs);
if ($newPhoto !== null) {
deletePhotoFile($photoDirAbs, $photo);
$photo = $newPhoto;
} elseif (isset($_POST['remove_photo'])) {
deletePhotoFile($photoDirAbs, $photo);
$photo = null;
}
$st = $pdo->prepare("UPDATE employee SET nameemployee = :n, surnameemployee = :s, email = :e,
phone = :p, photo = :ph, notifications = :nt, active = :a
WHERE idemployee = :id");
$st->execute([
':n' => $name,
':s' => $surname,
':e' => $email ?: null,
':p' => $phone ?: null,
':ph' => $photo,
':nt' => $notify,
':a' => $active,
':id' => $id,
]);
flashRedirect('success', 'Insegnante aggiornato.');
}
// nuovo
$photo = handlePhotoUpload($_FILES['photo'] ?? [], $photoDirAbs);
$hasOwner = (int) $pdo->query("SELECT COUNT(*) AS c FROM employee WHERE is_owner = 'Y'")->fetch()['c'] > 0;
$st = $pdo->prepare("INSERT INTO employee
(nameemployee, surnameemployee, email, phone, photo, is_owner, notifications, active, idlocation)
VALUES (:n, :s, :e, :p, :ph, :o, :nt, :a, 0)");
$st->execute([
':n' => $name,
':s' => $surname,
':e' => $email ?: null,
':p' => $phone ?: null,
':ph' => $photo,
':o' => $hasOwner ? 'N' : 'Y',
':nt' => $notify,
':a' => $hasOwner ? $active : 'Y',
]);
flashRedirect('success', 'Insegnante aggiunto' . ($hasOwner ? '.' : ' e impostato come titolare.'));
}
/* ---------- imposta titolare */
if ($action === 'set_owner' && $id > 0) {
if (!findTeacher($pdo, $id)) {
flashRedirect('danger', 'Insegnante non trovato.');
}
$pdo->beginTransaction();
$pdo->exec("UPDATE employee SET is_owner = 'N' WHERE is_owner = 'Y'");
$st = $pdo->prepare("UPDATE employee SET is_owner = 'Y', active = 'Y' WHERE idemployee = :id");
$st->execute([':id' => $id]);
$pdo->commit();
flashRedirect('success', 'Titolare aggiornato.');
}
/* ---------- notifiche on/off */
if ($action === 'toggle_notifications' && $id > 0) {
$st = $pdo->prepare("UPDATE employee SET notifications = IF(notifications = 'Y', 'N', 'Y') WHERE idemployee = :id");
$st->execute([':id' => $id]);
flashRedirect('success', 'Notifiche aggiornate.');
}
/* ---------- elimina */
if ($action === 'delete' && $id > 0) {
$t = findTeacher($pdo, $id);
if (!$t) {
flashRedirect('danger', 'Insegnante non trovato.');
}
if ($t['is_owner'] === 'Y') {
flashRedirect('danger', 'Il titolare non può essere eliminato. Imposta prima un altro titolare.');
}
$cnt = $pdo->prepare("SELECT COUNT(*) AS c FROM service WHERE idteacher = :id");
$cnt->execute([':id' => $id]);
$nServices = (int) $cnt->fetch()['c'];
if ($nServices > 0) {
flashRedirect('danger', 'Impossibile eliminare: ha ' . $nServices . ' classi assegnate. Riassegnale oppure disattiva l\'insegnante.');
}
$pdo->beginTransaction();
$pdo->prepare("DELETE FROM dayoff WHERE idteacher = :id")->execute([':id' => $id]);
$pdo->prepare("DELETE FROM employee WHERE idemployee = :id")->execute([':id' => $id]);
$pdo->commit();
deletePhotoFile($photoDirAbs, $t['photo']);
flashRedirect('success', 'Insegnante eliminato.');
}
flashRedirect('danger', 'Azione non valida.');
} catch (RuntimeException $ex) {
flashRedirect('danger', $ex->getMessage());
} catch (Throwable $ex) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('insegnanti.php: ' . $ex->getMessage());
flashRedirect('danger', 'Errore durante il salvataggio. Riprova.');
}
}
/* ================================================================ lettura */
$flash = $_SESSION['teachers_flash'] ?? null;
unset($_SESSION['teachers_flash']);
$teachers = $pdo->query("SELECT e.*,
(SELECT COUNT(*) FROM service s WHERE s.idteacher = e.idemployee) AS nservices
FROM employee e
ORDER BY e.is_owner DESC, e.active DESC, e.surnameemployee, e.nameemployee")->fetchAll();
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<title>YogiBook - Insegnanti</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
<meta content="Advanced Creative Solutions" name="author" />
<link rel="shortcut icon" href="assets/images/favicon.ico">
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
<link href="assets/css/icons.min.css" 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">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
.teachers-head {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.teachers-head h5 {
margin: 0;
font-weight: 700;
color: #374151;
}
.teacher-card {
background: #fff;
border-radius: 14px;
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.06);
padding: 20px;
margin-bottom: 20px;
height: calc(100% - 20px);
display: flex;
flex-direction: column;
border-top: 5px solid #eef0f3;
transition: box-shadow 0.2s;
}
.teacher-card:hover {
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.1);
}
.teacher-card.owner {
border-top-color: #E1D0B8;
}
.teacher-card.inactive {
opacity: 0.6;
}
.teacher-top {
display: flex;
gap: 14px;
align-items: center;
margin-bottom: 14px;
}
.teacher-avatar {
flex: 0 0 64px;
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 22px;
font-weight: 800;
border: 3px solid #f3f4f6;
}
.teacher-name {
font-size: 17px;
font-weight: 700;
color: #1f2937;
line-height: 1.2;
}
.t-badge {
display: inline-block;
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 20px;
margin-top: 5px;
margin-right: 4px;
}
.t-badge.owner {
background: #E1D0B8;
color: #5a4630;
}
.t-badge.inactive {
background: #eef0f3;
color: #6b7280;
}
.teacher-info {
font-size: 14px;
color: #4b5563;
margin-bottom: 4px;
word-break: break-all;
}
.teacher-info i {
width: 18px;
color: #9ca3af;
}
.teacher-info a {
color: #4b5563;
}
.teacher-info .missing {
color: #c0c4cc;
font-style: italic;
}
.teacher-foot {
margin-top: auto;
padding-top: 14px;
border-top: 1px solid #f0f2f5;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
}
.notify-form .form-check-label {
font-size: 13px;
font-weight: 600;
color: #4b5563;
cursor: pointer;
}
.notify-form .form-check-input {
cursor: pointer;
}
.notify-form .form-check-input:checked {
background-color: #1ebf73;
border-color: #1ebf73;
}
.notify-warning {
font-size: 11px;
color: #e67e22;
font-weight: 600;
}
.t-actions {
display: flex;
gap: 6px;
}
.t-btn {
border: none;
border-radius: 8px;
padding: 6px 11px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: filter 0.15s;
}
.t-btn:hover {
filter: brightness(0.93);
}
.t-btn.edit {
background: #eef2f7;
color: #374151;
}
.t-btn.owner {
background: #f5eee4;
color: #8a6a3f;
}
.t-btn.del {
background: #fdecea;
color: #c0392b;
}
.photo-preview {
width: 84px;
height: 84px;
border-radius: 50%;
object-fit: cover;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
font-size: 28px;
border: 3px solid #eef0f3;
overflow: hidden;
}
.photo-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.empty-teachers {
text-align: center;
color: #9ca3af;
padding: 50px 10px;
}
</style>
</head>
<body>
<div id="layout-wrapper">
<header id="page-topbar" class="isvertical-topbar">
<div class="navbar-header">
<div class="d-flex">
<?php include('include/logoarea.php'); ?>
<button type="button" class="btn btn-sm px-3 font-size-24 header-item waves-effect vertical-menu-btn">
<i class="bx bx-menu align-middle"></i>
</button>
<div class="page-title-box align-self-center d-none d-md-block">
<h4 class="page-title mb-0">Insegnanti</h4>
</div>
</div>
<div class="d-flex">
<?php include('include/languageselection.php'); ?>
<?php include('include/profiletopbar.php'); ?>
</div>
</div>
</header>
<?php include('include/sidebar.php'); ?>
<header class="ishorizontal-topbar">
<div class="navbar-header">
<div class="d-flex"></div>
</div>
<div class="topnav">
<div class="container-fluid">
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
</div>
</div>
</header>
<div class="main-content">
<div class="page-content">
<div class="container-fluid">
<?php if ($flash) : ?>
<div class="alert alert-<?php echo e($flash[0]); ?> alert-dismissible fade show" role="alert">
<?php echo e($flash[1]); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Chiudi"></button>
</div>
<?php endif; ?>
<div class="teachers-head">
<div>
<h5>Insegnanti della scuola</h5>
<small class="text-muted"><?php echo count($teachers); ?> insegnant<?php echo count($teachers) === 1 ? 'e' : 'i'; ?></small>
</div>
<button type="button" class="btn btn-primary" onclick="openTeacherModal(null)">
<i class="fas fa-user-plus me-1"></i> Nuovo insegnante
</button>
</div>
<?php if (empty($teachers)) : ?>
<div class="empty-teachers">
<i class="fas fa-chalkboard-teacher fa-2x mb-2"></i>
<p>Nessun insegnante. Aggiungi il primo: diventerà automaticamente il titolare.</p>
</div>
<?php else : ?>
<div class="row">
<?php foreach ($teachers as $t) :
$tid = (int) $t['idemployee'];
$isOwner = $t['is_owner'] === 'Y';
$isActive = $t['active'] === 'Y';
$fullName = trim($t['nameemployee'] . ' ' . $t['surnameemployee']);
$photoUrl = $t['photo'] ? PHOTO_DIR_REL . $t['photo'] : '';
$payload = [
'id' => $tid,
'nameemployee' => $t['nameemployee'],
'surnameemployee' => $t['surnameemployee'],
'email' => $t['email'],
'phone' => $t['phone'],
'photo' => $photoUrl,
'notifications' => $t['notifications'] === 'Y',
'active' => $isActive,
'is_owner' => $isOwner,
];
?>
<div class="col-md-6 col-xl-4">
<div class="teacher-card <?php echo $isOwner ? 'owner' : ''; ?> <?php echo $isActive ? '' : 'inactive'; ?>">
<div class="teacher-top">
<?php if ($photoUrl) : ?>
<img class="teacher-avatar" src="<?php echo e($photoUrl); ?>" alt="<?php echo e($fullName); ?>">
<?php else : ?>
<div class="teacher-avatar" style="background:<?php echo teacherColor($fullName); ?>;"><?php echo e(teacherInitials($t['nameemployee'], $t['surnameemployee'])); ?></div>
<?php endif; ?>
<div>
<div class="teacher-name"><?php echo e($fullName); ?></div>
<?php if ($isOwner) : ?><span class="t-badge owner"><i class="fas fa-crown me-1"></i>Titolare</span><?php endif; ?>
<?php if (!$isActive) : ?><span class="t-badge inactive">Non attivo</span><?php endif; ?>
</div>
</div>
<div class="teacher-info"><i class="far fa-envelope"></i>
<?php echo $t['email'] ? '<a href="mailto:' . e($t['email']) . '">' . e($t['email']) . '</a>' : '<span class="missing">email non inserita</span>'; ?>
</div>
<div class="teacher-info"><i class="fas fa-phone"></i>
<?php echo $t['phone'] ? '<a href="tel:' . e(preg_replace('/[^0-9+]/', '', $t['phone'])) . '">' . e($t['phone']) . '</a>' : '<span class="missing">telefono non inserito</span>'; ?>
</div>
<div class="teacher-info"><i class="fas fa-layer-group"></i>
<?php echo (int) $t['nservices']; ?> class<?php echo (int) $t['nservices'] === 1 ? 'e' : 'i'; ?> assegnat<?php echo (int) $t['nservices'] === 1 ? 'a' : 'e'; ?>
</div>
<div class="teacher-foot">
<form method="post" class="notify-form">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
<input type="hidden" name="action" value="toggle_notifications">
<input type="hidden" name="id" value="<?php echo $tid; ?>">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="notify<?php echo $tid; ?>"
<?php echo $t['notifications'] === 'Y' ? 'checked' : ''; ?> onchange="this.form.submit()">
<label class="form-check-label" for="notify<?php echo $tid; ?>">Notifiche email</label>
</div>
<?php if ($t['notifications'] === 'Y' && !$t['email']) : ?>
<div class="notify-warning"><i class="fas fa-exclamation-triangle"></i> manca l'email</div>
<?php endif; ?>
</form>
<div class="t-actions">
<button type="button" class="t-btn edit" title="Modifica"
data-teacher="<?php echo e(json_encode($payload)); ?>"
onclick="openTeacherModal(JSON.parse(this.dataset.teacher))">
<i class="fas fa-pen"></i>
</button>
<?php if (!$isOwner) : ?>
<form method="post" class="confirm-form"
data-title="Impostare come titolare?"
data-text="<?php echo e($fullName); ?> diventerà il titolare della scuola al posto dell'attuale.">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
<input type="hidden" name="action" value="set_owner">
<input type="hidden" name="id" value="<?php echo $tid; ?>">
<button type="submit" class="t-btn owner" title="Imposta come titolare"><i class="fas fa-crown"></i></button>
</form>
<form method="post" class="confirm-form" data-danger="1"
data-title="Eliminare <?php echo e($fullName); ?>?"
data-text="L'operazione non è reversibile.">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?php echo $tid; ?>">
<button type="submit" class="t-btn del" title="Elimina"><i class="fas fa-trash"></i></button>
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php include('include/footer.php'); ?>
</div>
</div>
</div>
<!-- ============================================ MODALE CREA / MODIFICA -->
<div class="modal fade" id="teacherModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" method="post" enctype="multipart/form-data" id="teacherForm">
<div class="modal-header">
<h5 class="modal-title" id="teacherModalTitle">Nuovo insegnante</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
<input type="hidden" name="action" value="save">
<input type="hidden" name="id" id="t_id" value="0">
<div class="d-flex align-items-center gap-3 mb-3">
<div class="photo-preview" id="t_preview"><i class="fas fa-user"></i></div>
<div class="flex-grow-1">
<label class="form-label mb-1">Foto <small class="text-muted">(facoltativa, JPG/PNG/WEBP, max 3 MB)</small></label>
<input type="file" name="photo" id="t_photo" class="form-control form-control-sm" accept="image/jpeg,image/png,image/webp">
<div class="form-check mt-2" id="t_remove_wrap" style="display:none;">
<input class="form-check-input" type="checkbox" name="remove_photo" id="t_remove">
<label class="form-check-label" for="t_remove">Rimuovi foto attuale</label>
</div>
</div>
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label mb-1">Nome *</label>
<input type="text" name="nameemployee" id="t_name" class="form-control" maxlength="100" required>
</div>
<div class="col-6">
<label class="form-label mb-1">Cognome *</label>
<input type="text" name="surnameemployee" id="t_surname" class="form-control" maxlength="100" required>
</div>
<div class="col-12">
<label class="form-label mb-1">Email</label>
<input type="email" name="email" id="t_email" class="form-control" maxlength="150">
</div>
<div class="col-12">
<label class="form-label mb-1">Telefono</label>
<input type="text" name="phone" id="t_phone" class="form-control" maxlength="40">
</div>
</div>
<div class="mt-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="notifications" id="t_notify" checked>
<label class="form-check-label" for="t_notify">Riceve notifiche email</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="active" id="t_active" checked>
<label class="form-check-label" for="t_active">Attivo</label>
</div>
<div class="text-muted" id="t_owner_note" style="font-size:12px;display:none;">
<i class="fas fa-crown me-1" style="color:#b08d57;"></i> È il titolare: non può essere disattivato.
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-1"></i> Salva</button>
</div>
</form>
</div>
</div>
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
<script src="assets/libs/simplebar/simplebar.min.js"></script>
<script src="assets/libs/eva-icons/eva.min.js"></script>
<script src="assets/js/app.js"></script>
<script>
var teacherModal = null;
function setPreview(url) {
var box = document.getElementById('t_preview');
box.innerHTML = url ? '<img src="' + url + '" alt="">' : '<i class="fas fa-user"></i>';
}
function openTeacherModal(t) {
var isEdit = !!t;
document.getElementById('teacherModalTitle').textContent = isEdit ? 'Modifica insegnante' : 'Nuovo insegnante';
document.getElementById('t_id').value = isEdit ? t.id : 0;
document.getElementById('t_name').value = isEdit ? (t.nameemployee || '') : '';
document.getElementById('t_surname').value = isEdit ? (t.surnameemployee || '') : '';
document.getElementById('t_email').value = isEdit ? (t.email || '') : '';
document.getElementById('t_phone').value = isEdit ? (t.phone || '') : '';
document.getElementById('t_notify').checked = isEdit ? t.notifications : true;
var active = document.getElementById('t_active');
active.checked = isEdit ? t.active : true;
active.disabled = isEdit && t.is_owner;
document.getElementById('t_owner_note').style.display = (isEdit && t.is_owner) ? 'block' : 'none';
document.getElementById('t_photo').value = '';
document.getElementById('t_remove').checked = false;
document.getElementById('t_remove_wrap').style.display = (isEdit && t.photo) ? 'block' : 'none';
setPreview(isEdit ? t.photo : '');
if (!teacherModal) teacherModal = new bootstrap.Modal(document.getElementById('teacherModal'));
teacherModal.show();
}
// il titolare ha "Attivo" disabilitato: lo riabilitiamo al submit così il valore viene inviato
document.getElementById('teacherForm').addEventListener('submit', function() {
document.getElementById('t_active').disabled = false;
});
// anteprima foto + controllo dimensione
document.getElementById('t_photo').addEventListener('change', function() {
var f = this.files[0];
if (!f) return;
if (f.size > 3 * 1024 * 1024) {
Swal.fire('Foto troppo grande', 'La foto deve essere al massimo di 3 MB.', 'warning');
this.value = '';
return;
}
var reader = new FileReader();
reader.onload = function(ev) {
setPreview(ev.target.result);
};
reader.readAsDataURL(f);
document.getElementById('t_remove').checked = false;
});
// conferma per titolare / elimina
document.querySelectorAll('.confirm-form').forEach(function(form) {
form.addEventListener('submit', function(ev) {
if (form.dataset.confirmed === '1') return;
ev.preventDefault();
var danger = form.dataset.danger === '1';
Swal.fire({
title: form.dataset.title,
text: form.dataset.text,
icon: danger ? 'warning' : 'question',
showCancelButton: true,
confirmButtonColor: danger ? '#e74c3c' : '#b08d57',
cancelButtonColor: '#6b7280',
confirmButtonText: danger ? 'Sì, elimina' : 'Sì, conferma',
cancelButtonText: 'Annulla'
}).then(function(r) {
if (r.isConfirmed) {
form.dataset.confirmed = '1';
form.submit();
}
});
});
});
</script>
</body>
</html>
+23 -23
View File
@@ -1,33 +1,33 @@
<?php
require_once('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header("Location: options.php");
exit();
header('Location: options.php');
exit;
}
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
header("Location: options.php?message=error");
exit();
}
$waiting = isset($_POST['waitinglist_enabled']) ? 'Y' : 'N';
$showTeacher = isset($_POST['show_teacher']) ? 'Y' : 'N';
$maxCancel = max(0, (int) ($_POST['maxbeforetimecancell'] ?? 0));
// Checkbox non spuntata = nessun invio -> default 'N'
$waitingEnabled = (isset($_POST['waitinglist_enabled']) && $_POST['waitinglist_enabled'] === 'Y') ? 'Y' : 'N';
$maxCancel = isset($_POST['maxbeforetimecancell']) ? (string) (int) $_POST['maxbeforetimecancell'] : '0';
try {
$exists = (int) $pdo->query("SELECT COUNT(*) AS c FROM `option`")->fetch()['c'] > 0;
// Aggiorna l'unico record di option (idption = 1)
$stmt = $conn->prepare("UPDATE `option` SET waitinglist_enabled = ?, maxbeforetimecancell = ? WHERE idption = 1");
$stmt->bind_param("ss", $waitingEnabled, $maxCancel);
if ($exists) {
$st = $pdo->prepare("UPDATE `option`
SET waitinglist_enabled = :w, show_teacher = :s, maxbeforetimecancell = :m
LIMIT 1");
} else {
$st = $pdo->prepare("INSERT INTO `option` (waitinglist_enabled, show_teacher, maxbeforetimecancell)
VALUES (:w, :s, :m)");
}
$st->execute([':w' => $waiting, ':s' => $showTeacher, ':m' => (string) $maxCancel]);
if ($stmt->execute()) {
$stmt->close();
$conn->close();
header("Location: optionsschool.php?message=success");
exit();
} else {
$stmt->close();
$conn->close();
header("Location: optionsschool.php?message=error");
exit();
header('Location: optionsschool.php?message=success');
} catch (Throwable $e) {
error_log('options_save: ' . $e->getMessage());
header('Location: options.php?message=error');
}
exit;
+10
View File
@@ -19,6 +19,7 @@ $conn->close();
$waitingEnabled = ($opt['waitinglist_enabled'] ?? 'N') === 'Y';
$maxCancel = $opt['maxbeforetimecancell'] ?? '0';
$showTeacher = ($opt['show_teacher'] ?? 'N') === 'Y';
?>
<!doctype html>
<html lang="it">
@@ -155,6 +156,15 @@ $maxCancel = $opt['maxbeforetimecancell'] ?? '0';
<input class="form-check-input" type="checkbox" role="switch" id="waitinglist_enabled" name="waitinglist_enabled" value="Y" <?php echo $waitingEnabled ? 'checked' : ''; ?>>
</div>
</div>
<div class="option-row">
<div>
<div class="option-label">Mostra insegnante nelle classi</div>
<div class="option-help">Se attiva, nelle card delle classi viene indicato il nome (e la foto) dell'insegnante.</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="show_teacher" name="show_teacher" value="Y" <?php echo $showTeacher ? 'checked' : ''; ?>>
</div>
</div>
<div class="option-row">
<div>
+60 -2
View File
@@ -59,7 +59,7 @@ $stmtBooked->execute([
$bookedRows = $stmtBooked->fetchAll();
// Lezioni in attesa di conferma (pending) dell'utente, nel mese visualizzato
$sqlPending = "SELECT bookingclass.*, service.servicename, service.colorclass, serviceschedule.dateschedule
$sqlPending = "SELECT bookingclass.*, service.servicename, service.colorclass, service.idteacher, serviceschedule.dateschedule
FROM bookingclass
LEFT JOIN service ON bookingclass.idservice = service.idservice
LEFT JOIN serviceschedule ON bookingclass.idserviceschedule = serviceschedule.idserviceschedule
@@ -76,7 +76,7 @@ $stmtPending->execute([
$pendingRows = $stmtPending->fetchAll();
// Lezioni in lista d'attesa (waiting) dell'utente, nel mese visualizzato
$sqlWaiting = "SELECT bookingclass.*, service.servicename, service.colorclass, service.maxcapacity, serviceschedule.dateschedule
$sqlWaiting = "SELECT bookingclass.*, service.servicename, service.colorclass, service.maxcapacity, service.idteacher, serviceschedule.dateschedule
FROM bookingclass
LEFT JOIN service ON bookingclass.idservice = service.idservice
LEFT JOIN serviceschedule ON bookingclass.idserviceschedule = serviceschedule.idserviceschedule
@@ -157,6 +157,18 @@ $pending = (int) ($rowCounts['pending'] ?? 0);
$toprogram = $totalTickets - $passedRecords - $futureRecords - $pending - $lost;
/* -------------------------------------------------------------------------
* 4b) Insegnanti nelle card (solo se attivo nelle Opzioni)
* ---------------------------------------------------------------------- */
$optShow = $pdo->query("SELECT show_teacher FROM `option` LIMIT 1")->fetch();
$showTeacher = ($optShow['show_teacher'] ?? 'N') === 'Y';
$teachersById = [];
if ($showTeacher) {
foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, photo FROM employee")->fetchAll() as $t) {
$teachersById[(int) $t['idemployee']] = $t;
}
}
/* -------------------------------------------------------------------------
* 5) Pre-caricamento limiti di riprogrammazione per gli ordini coinvolti
* (una sola query invece di una per ogni card, dentro il loop)
@@ -191,6 +203,22 @@ function e($value): string
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function teacherLine(array $row, bool $show, array $teachersById): string
{
if (!$show) return '';
$t = $teachersById[(int) ($row['idteacher'] ?? 0)] ?? null;
if (!$t) return '';
$name = trim($t['nameemployee'] . ' ' . $t['surnameemployee']);
if (!empty($t['photo'])) {
$avatar = '<img class="teacher-avatar-sm" src="assets/images/teachers/' . e($t['photo']) . '" alt="">';
} else {
$ini = mb_strtoupper(mb_substr($t['nameemployee'], 0, 1) . mb_substr($t['surnameemployee'], 0, 1));
$avatar = '<span class="teacher-avatar-sm">' . e($ini) . '</span>';
}
return '<p class="custom-paragraph teacher-line">' . $avatar . 'Insegnante: <strong>' . e($name) . '</strong></p>';
}
$italianMonths = [
"January" => "Gennaio",
"February" => "Febbraio",
@@ -305,6 +333,33 @@ $italianMonths = [
margin: 0 0 4px;
}
.teacher-line {
display: flex;
align-items: center;
gap: 8px;
margin: 4px 0 6px;
}
.teacher-line strong {
color: #374151;
font-weight: 600;
}
.teacher-avatar-sm {
flex: 0 0 26px;
width: 26px;
height: 26px;
border-radius: 50%;
object-fit: cover;
background: #E1D0B8;
color: #5a4630;
font-size: 11px;
font-weight: 800;
display: inline-flex;
align-items: center;
justify-content: center;
}
.custom-paragraph {
color: #6b7280;
font-size: 14px;
@@ -681,6 +736,7 @@ $italianMonths = [
<span style="display:inline-block;background:#fff4d6;color:#8a6d00;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;margin-left:6px;vertical-align:middle;">In attesa</span>
</h2>
<p class="custom-paragraph">Quando: <?php echo e($pWhen); ?></p>
<?php echo teacherLine($prow, $showTeacher, $teachersById); ?>
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>
</div>
</div>
@@ -719,6 +775,7 @@ $italianMonths = [
<span style="display:inline-block;background:#eee;color:#555;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;margin-left:6px;vertical-align:middle;">In coda</span>
</h2>
<p class="custom-paragraph">Quando: <?php echo e($wWhen); ?></p>
<?php echo teacherLine($wrow, $showTeacher, $teachersById); ?>
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>
<?php if ($wPos > 0) : ?>
<p class="custom-paragraph" style="font-weight:700;color:#8a6d00;">
@@ -801,6 +858,7 @@ $italianMonths = [
<div class="custom-event-details" style="border-left-color:<?php echo e($colorclass); ?>">
<h2 class="custom-heading"><?php echo e($servicename); ?></h2>
<p class="custom-paragraph">Quando: <?php echo e($newDateFormat); ?></p>
<?php echo teacherLine($row, $showTeacher, $teachersById); ?>
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>
<div class="custom-actions">
<button class="custom-action-button" onclick="addToCalendar(this)" data-eventname="<?php echo e($servicename); ?>" data-eventdate="<?php echo e($newDateFormat); ?>">