Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f206b0db9a |
+1
-12
@@ -34,15 +34,4 @@ public/yogasoulscript/logs/
|
||||
public/user/document/
|
||||
|
||||
# Python venv
|
||||
public/yogasoulscript/venv//index.html
|
||||
/public/.well-known/
|
||||
/index.html
|
||||
/public/.well-known/
|
||||
|
||||
# File generati sul server: mai in git
|
||||
public/logs/*
|
||||
cron/preview/*
|
||||
public/assets/uploads/*
|
||||
public/assets/images/teachers/*
|
||||
public/uploads/*
|
||||
!public/uploads/.htaccess
|
||||
public/yogasoulscript/venv/
|
||||
+1
-1
@@ -40,7 +40,7 @@
|
||||
"laravel/tinker": "^2.7",
|
||||
"laravel/ui": "^4.0",
|
||||
"laravelcollective/html": "^6.3",
|
||||
"phpmailer/phpmailer": "^7.1",
|
||||
"phpmailer/phpmailer": "^6.8",
|
||||
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
||||
"robmorgan/phinx": "^0.16.12",
|
||||
"spatie/laravel-query-builder": "^5.0",
|
||||
|
||||
Generated
+10
-11
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9307a6b8f5f1c5b0a5efb10e35038f7f",
|
||||
"content-hash": "9b689efb6d29cb572794705682d6edc4",
|
||||
"packages": [
|
||||
{
|
||||
"name": "akaunting/laravel-setting",
|
||||
@@ -3428,16 +3428,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v7.1.1",
|
||||
"version": "v6.9.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3451,14 +3451,13 @@
|
||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||
"squizlabs/php_codesniffer": "^3.13.5",
|
||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"squizlabs/php_codesniffer": "^3.7.2",
|
||||
"yoast/phpunit-polyfills": "^1.0.4"
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
@@ -3498,7 +3497,7 @@
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3506,7 +3505,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-18T08:06:14+00:00"
|
||||
"time": "2024-11-24T18:04:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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;
|
||||
use PHPMailer\PHPMailer\Exception as MailException;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit('Accesso consentito solo da CLI');
|
||||
}
|
||||
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
|
||||
// --only-at=17 : esce senza fare nulla se in Italia non sono le 17 (server in UTC)
|
||||
foreach ($argv as $a) {
|
||||
if (preg_match('/^--only-at=(\d{1,2})$/', $a, $m) && (int) date('G') !== (int) $m[1]) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
chdir($root);
|
||||
require_once $root . '/public/class/db-functions.php'; // include anche vendor/autoload.php
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection(); // carica anche il .env
|
||||
|
||||
/* ---------------------------------------------------------------- helper */
|
||||
|
||||
function cfg(string $key, $default = null)
|
||||
{
|
||||
$v = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
|
||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||
}
|
||||
|
||||
function h($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function logmsg(string $m): void
|
||||
{
|
||||
echo '[' . date('Y-m-d H:i:s') . '] ' . $m . PHP_EOL;
|
||||
}
|
||||
|
||||
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'];
|
||||
$months = ['', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'];
|
||||
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);
|
||||
$day = date('Y-m-d');
|
||||
foreach (array_slice($argv, 1) as $a) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a)) {
|
||||
$day = $a;
|
||||
}
|
||||
}
|
||||
|
||||
$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, s.idteacher
|
||||
FROM serviceschedule ss
|
||||
LEFT JOIN service s ON ss.idservice = s.idservice
|
||||
WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2
|
||||
ORDER BY ss.dateschedule"
|
||||
);
|
||||
$stmt->execute([
|
||||
':d1' => $day . ' 00:00:00',
|
||||
':d2' => date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00',
|
||||
]);
|
||||
$classes = $stmt->fetchAll();
|
||||
|
||||
if (!$classes) {
|
||||
logmsg("Nessuna classe il $day. Nessuna mail inviata.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$ids = array_map(fn($c) => (int) $c['idserviceschedule'], $classes);
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmtB = $pdo->prepare(
|
||||
"SELECT bc.idbookingclass, bc.idserviceschedule, bc.lostlesson, bc.status,
|
||||
u.id AS iduser, u.first_name, u.last_name, u.email
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN auth_users u ON bc.iduser = u.id
|
||||
WHERE bc.idserviceschedule IN ($ph)
|
||||
AND bc.status IN ('booked', 'pending', 'waiting')
|
||||
ORDER BY bc.created_at, u.last_name, u.first_name"
|
||||
);
|
||||
$stmtB->execute($ids);
|
||||
|
||||
$bookingsBySchedule = [];
|
||||
foreach ($stmtB->fetchAll() as $b) {
|
||||
$bookingsBySchedule[(int) $b['idserviceschedule']][] = $b;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ template */
|
||||
|
||||
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>';
|
||||
}
|
||||
$numColor = ['booked' => '#1ebf73', 'pending' => '#8a6d00', 'waiting' => '#d1698a'][$type];
|
||||
$html = '';
|
||||
$i = 1;
|
||||
foreach ($list as $p) {
|
||||
$isLost = ($type === 'booked' && ($p['lostlesson'] ?? '') === 'Y');
|
||||
if ($type === 'booked') {
|
||||
$chipHtml = $isLost ? chip('Persa', '#1f2937', '#ffffff') : chip('Confermata', '#e3f2ec', '#1a7f52');
|
||||
} elseif ($type === 'pending') {
|
||||
$chipHtml = chip('In attesa', '#fff4d6', '#8a6d00');
|
||||
} else {
|
||||
$chipHtml = chip('In coda', '#fbeef3', '#d1698a');
|
||||
}
|
||||
$nameColor = $isLost ? '#9ca3af' : '#1f2937';
|
||||
$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(personName($p, $full)) . $email . '</td>'
|
||||
. '<td valign="top" align="right" width="100">' . $chipHtml . '</td>'
|
||||
. '</tr></table></td></tr>';
|
||||
$i++;
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
function section(string $title, string $color, string $rows): string
|
||||
{
|
||||
return '<tr><td style="padding:22px 28px 0;">'
|
||||
. '<div style="font-size:13px;font-weight:700;color:' . $color . ';text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">' . $title . '</div>'
|
||||
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;">' . $rows . '</table>'
|
||||
. '</td></tr>';
|
||||
}
|
||||
|
||||
function statBox(string $value, string $label, string $color): string
|
||||
{
|
||||
return '<td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;">'
|
||||
. '<div style="font-size:22px;font-weight:800;color:' . $color . ';line-height:1.1;">' . $value . '</div>'
|
||||
. '<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">' . $label . '</div>'
|
||||
. '</td>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @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';
|
||||
$max = (int) $cls['maxcapacity'];
|
||||
$nBooked = count($booked);
|
||||
$nLost = count(array_filter($booked, fn($b) => ($b['lostlesson'] ?? '') === 'Y'));
|
||||
$free = $max > 0 ? max(0, $max - $nBooked) : '–';
|
||||
$isFull = $max > 0 && $nBooked >= $max;
|
||||
|
||||
$stats = '<table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr>'
|
||||
. statBox($nBooked . '/' . $max, 'Iscritti', $isFull ? '#c0392b' : '#1a7f52')
|
||||
. statBox((string) $free, 'Posti liberi', '#374151')
|
||||
. statBox((string) count($pending), 'In attesa', '#8a6d00')
|
||||
. statBox((string) count($waiting), 'In coda', '#d1698a')
|
||||
. '</tr></table>';
|
||||
|
||||
$lostNote = $nLost > 0
|
||||
? '<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', $full));
|
||||
if ($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', $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);">'
|
||||
. '<tr><td style="height:8px;background:' . h($accent) . ';font-size:0;line-height:0;"> </td></tr>'
|
||||
. $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>'
|
||||
. '<tr><td style="padding:16px 22px 0;">' . $stats . '<div style="padding:0 6px;">' . $lostNote . '</div></td></tr>'
|
||||
. $body
|
||||
. '<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, bool $full, string $teacherFull, ?string $copyNote): string
|
||||
{
|
||||
$d = new DateTime($cls['dateschedule']);
|
||||
$max = (int) $cls['maxcapacity'];
|
||||
$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) {
|
||||
continue;
|
||||
}
|
||||
$t .= $title . "\n";
|
||||
if (!$list) {
|
||||
$t .= " Nessun partecipante\n";
|
||||
}
|
||||
$i = 1;
|
||||
foreach ($list as $p) {
|
||||
$lost = ($p['status'] === 'booked' && ($p['lostlesson'] ?? '') === 'Y') ? ' (PERSA)' : '';
|
||||
$t .= ' ' . $i++ . '. ' . personName($p, $full) . $lost . "\n";
|
||||
}
|
||||
$t .= "\n";
|
||||
}
|
||||
return $t;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- mailer */
|
||||
|
||||
function makeMailer(): PHPMailer
|
||||
{
|
||||
$m = new PHPMailer(true);
|
||||
$m->isSMTP();
|
||||
$m->Host = cfg('MAIL_HOST');
|
||||
$m->Port = (int) cfg('MAIL_PORT', 465);
|
||||
$m->SMTPAuth = true;
|
||||
$m->Username = cfg('MAIL_USERNAME');
|
||||
$m->Password = cfg('MAIL_PASSWORD');
|
||||
$enc = strtolower((string) cfg('MAIL_ENCRYPTION', 'ssl'));
|
||||
$m->SMTPSecure = $enc === 'ssl' ? PHPMailer::ENCRYPTION_SMTPS : ($enc === 'tls' ? PHPMailer::ENCRYPTION_STARTTLS : '');
|
||||
$m->SMTPKeepAlive = true;
|
||||
$m->CharSet = 'UTF-8';
|
||||
$m->Encoding = 'base64';
|
||||
$m->setFrom(cfg('MAIL_FROM_ADDRESS'), cfg('MAIL_FROM_NAME', 'YogiBook'));
|
||||
$m->isHTML(true);
|
||||
return $m;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- invio */
|
||||
|
||||
$mailer = $dryRun ? null : makeMailer();
|
||||
$sent = 0;
|
||||
$failed = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($classes as $cls) {
|
||||
$sid = (int) $cls['idserviceschedule'];
|
||||
$all = $bookingsBySchedule[$sid] ?? [];
|
||||
$booked = array_values(array_filter($all, fn($b) => $b['status'] === 'booked'));
|
||||
$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']);
|
||||
$baseSubject = $cls['servicename'] . ' · ' . $d->format('d/m') . ' ore ' . $d->format('H:i')
|
||||
. ' · ' . count($booked) . '/' . (int) $cls['maxcapacity'] . ' iscritti';
|
||||
if ($pending) {
|
||||
$baseSubject .= ' · ' . count($pending) . ' in attesa';
|
||||
}
|
||||
if ($waiting) {
|
||||
$baseSubject .= ' · ' . count($waiting) . ' in coda';
|
||||
}
|
||||
|
||||
// Chi riceve cosa
|
||||
$deliveries = [];
|
||||
if ($teacherIsOwner) {
|
||||
if (canReceive($owner)) {
|
||||
$deliveries[] = ['to' => $owner, 'full' => true, 'copy' => null, 'tag' => 'titolare'];
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if ($mailer) {
|
||||
$mailer->smtpClose();
|
||||
}
|
||||
|
||||
logmsg("Fine: $sent inviate, $failed errori, $skipped classi senza destinatari (" . count($classes) . " classi il $day).");
|
||||
exit($failed > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Token dei dispositivi per le notifiche push (Firebase Cloud Messaging).
|
||||
* Un utente puo' avere piu' dispositivi, quindi piu' token.
|
||||
*/
|
||||
final class CreateUserdevicetokenTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('userdevicetoken', ['id' => 'iduserdevicetoken'])
|
||||
->addColumn('iduser', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'auth_users.id',
|
||||
])
|
||||
->addColumn('devicetoken', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => false,
|
||||
'comment' => 'Token FCM del dispositivo',
|
||||
])
|
||||
->addColumn('platform', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => false,
|
||||
'default' => 'android',
|
||||
'comment' => 'android / ios',
|
||||
])
|
||||
->addColumn('appversion', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => true,
|
||||
])
|
||||
->addColumn('active', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'Y',
|
||||
'comment' => 'N quando FCM segnala il token come non valido',
|
||||
])
|
||||
->addColumn('created_at', 'datetime', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('lastseen_at', 'datetime', [
|
||||
'null' => true,
|
||||
'comment' => 'Ultima volta che l\'app ha registrato questo token',
|
||||
])
|
||||
->addIndex(['devicetoken'], ['unique' => true, 'name' => 'uq_userdevicetoken_token'])
|
||||
->addIndex(['iduser', 'active'], ['name' => 'idx_userdevicetoken_user'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateNotificationsettingTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('notificationsetting', ['id' => 'idnotificationsetting'])
|
||||
->addColumn('notificationtype', 'string', [
|
||||
'limit' => 30,
|
||||
'null' => false,
|
||||
'comment' => 'lesson / order / certificate',
|
||||
])
|
||||
->addColumn('daysbefore', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'comment' => 'Giorni di anticipo; 0 = il giorno stesso',
|
||||
])
|
||||
->addColumn('sendhour', 'string', [
|
||||
'limit' => 5,
|
||||
'null' => false,
|
||||
'default' => '08:00',
|
||||
'comment' => 'Ora di invio del cron (HH:MM)',
|
||||
])
|
||||
->addColumn('active', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'Y',
|
||||
])
|
||||
->addColumn('title', 'string', [
|
||||
'limit' => 150,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('body', 'text', [
|
||||
'null' => false,
|
||||
])
|
||||
->addIndex(
|
||||
['notificationtype', 'daysbefore'],
|
||||
['unique' => true, 'name' => 'uq_notificationsetting_type']
|
||||
)
|
||||
->insert([
|
||||
[
|
||||
'notificationtype' => 'lesson',
|
||||
'daysbefore' => 0,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'La tua lezione di oggi',
|
||||
'body' => 'Ciao {first_name}, oggi hai {servicename} alle {time}. Ti aspettiamo!',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'order',
|
||||
'daysbefore' => 5,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Il tuo abbonamento sta per scadere',
|
||||
'body' => 'Ciao {first_name}, il tuo abbonamento scade il {expiredate}. '
|
||||
. 'Ricordati di prenotare le lezioni rimaste.',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'order',
|
||||
'daysbefore' => 0,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Il tuo abbonamento scade oggi',
|
||||
'body' => 'Ciao {first_name}, oggi e\' l\'ultimo giorno del tuo abbonamento.',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'certificate',
|
||||
'daysbefore' => 7,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Certificato medico in scadenza',
|
||||
'body' => 'Ciao {first_name}, il tuo certificato medico scade il {expiredate}.',
|
||||
],
|
||||
[
|
||||
'notificationtype' => 'certificate',
|
||||
'daysbefore' => 5,
|
||||
'sendhour' => '08:00',
|
||||
'active' => 'Y',
|
||||
'title' => 'Certificato medico in scadenza',
|
||||
'body' => 'Ciao {first_name}, mancano pochi giorni: il tuo certificato medico '
|
||||
. 'scade il {expiredate}.',
|
||||
],
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateNotificationlogTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('notificationlog', ['id' => 'idnotificationlog'])
|
||||
->addColumn('iduser', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'auth_users.id',
|
||||
])
|
||||
->addColumn('notificationtype', 'string', [
|
||||
'limit' => 30,
|
||||
'null' => false,
|
||||
'comment' => 'lesson / order / certificate',
|
||||
])
|
||||
->addColumn('idreference', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'Id della lezione, ordine o certificato',
|
||||
])
|
||||
->addColumn('daysbefore', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
])
|
||||
->addColumn('sent_at', 'datetime', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('status', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => false,
|
||||
'default' => 'sent',
|
||||
'comment' => 'sent / failed',
|
||||
])
|
||||
->addColumn('devicecount', 'integer', [
|
||||
'null' => false,
|
||||
'default' => 0,
|
||||
'comment' => 'A quanti dispositivi e stata inviata',
|
||||
])
|
||||
->addColumn('errormessage', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => true,
|
||||
])
|
||||
->addIndex(
|
||||
['iduser', 'notificationtype', 'idreference', 'daysbefore'],
|
||||
['unique' => true, 'name' => 'uq_notificationlog_invio']
|
||||
)
|
||||
->addIndex(['sent_at'], ['name' => 'idx_notificationlog_data'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddMultiTextsToNotificationsetting extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('notificationsetting')
|
||||
->addColumn('title_multi', 'string', [
|
||||
'limit' => 150,
|
||||
'null' => true,
|
||||
'after' => 'title',
|
||||
'comment' => 'Titolo usato quando la notifica raggruppa piu elementi',
|
||||
])
|
||||
->addColumn('body_multi', 'text', [
|
||||
'null' => true,
|
||||
'after' => 'body',
|
||||
'comment' => 'Testo usato quando la notifica raggruppa piu elementi',
|
||||
])
|
||||
->update();
|
||||
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'Le tue lezioni di oggi',
|
||||
body_multi = 'Ciao {first_name}, oggi hai {count} lezioni: {lessons}. Ti aspettiamo!'
|
||||
WHERE notificationtype = 'lesson'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('notificationsetting')
|
||||
->removeColumn('title_multi')
|
||||
->removeColumn('body_multi')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddMultiTextsDefaults extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'I tuoi abbonamenti in scadenza',
|
||||
body_multi = 'Ciao {first_name}, hai {count} abbonamenti in scadenza il {expiredate}: {orders}.'
|
||||
WHERE notificationtype = 'order' AND daysbefore > 0
|
||||
");
|
||||
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'I tuoi abbonamenti scadono oggi',
|
||||
body_multi = 'Ciao {first_name}, oggi scadono {count} dei tuoi abbonamenti: {orders}.'
|
||||
WHERE notificationtype = 'order' AND daysbefore = 0
|
||||
");
|
||||
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = 'Certificati medici in scadenza',
|
||||
body_multi = 'Ciao {first_name}, hai {count} certificati in scadenza il {expiredate}: {documents}.'
|
||||
WHERE notificationtype = 'certificate'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->execute("
|
||||
UPDATE notificationsetting
|
||||
SET title_multi = NULL, body_multi = NULL
|
||||
WHERE notificationtype IN ('order', 'certificate')
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Converte orderbook.quantityclass da varchar(20) a INT.
|
||||
*
|
||||
* quantityclass rappresenta il numero di ticket acquistati: viene usato in
|
||||
* confronti e aritmetica (offset, LIMIT, conteggi). Tenerlo come stringa
|
||||
* causa confronti errati ("10" < "9") e comportamenti imprevisti.
|
||||
*
|
||||
* PRIMA di cambiare il tipo, bonifica i valori esistenti: elimina spazi e
|
||||
* azzera i valori non numerici / vuoti, altrimenti la conversione li
|
||||
* troncherebbe in modo silenzioso.
|
||||
*/
|
||||
final class ConvertQuantityclassToInt extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1) Bonifica: rimuovi spazi ai bordi
|
||||
$this->execute("UPDATE orderbook SET quantityclass = TRIM(quantityclass) WHERE quantityclass IS NOT NULL");
|
||||
|
||||
// 2) Bonifica: i valori non interamente numerici o vuoti diventano 0
|
||||
// (REGEXP '^[0-9]+$' = solo cifre). Rivedi manualmente eventuali 0
|
||||
// risultanti dopo la migration se non te li aspetti.
|
||||
$this->execute("UPDATE orderbook SET quantityclass = '0' WHERE quantityclass IS NULL OR quantityclass = '' OR quantityclass NOT REGEXP '^[0-9]+$'");
|
||||
|
||||
// 3) Cambia il tipo di colonna a INT
|
||||
$table = $this->table('orderbook');
|
||||
$table->changeColumn('quantityclass', 'integer', [
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'signed' => true,
|
||||
])->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Ripristina varchar(20). I valori numerici restano validi come stringa.
|
||||
$table = $this->table('orderbook');
|
||||
$table->changeColumn('quantityclass', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
])->update();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Aggiunge orderbook.is_openflexy CHAR(1) DEFAULT 'N'.
|
||||
*
|
||||
* Marca esplicitamente un ordine come carnet OpenFlexy (biglietti liberi,
|
||||
* nessuna propagazione). Sostituisce il riconoscimento fragile basato sul
|
||||
* nome del servizio (service.servicename = 'OpenFlexy'), che si rompe se
|
||||
* qualcuno rinomina il servizio dal pannello.
|
||||
*
|
||||
* Dopo questa migration puoi popolare il flag sugli ordini OpenFlexy già
|
||||
* esistenti (vedi nota nel metodo up) e poi aggiornare propagatesingleorder.php
|
||||
* per leggere il flag invece del nome.
|
||||
*/
|
||||
final class AddIsOpenflexyToOrderbook extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$table = $this->table('orderbook');
|
||||
$table->addColumn('is_openflexy', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'N',
|
||||
'after' => 'idservice',
|
||||
])->update();
|
||||
|
||||
// Popola il flag sugli ordini già collegati alla classe OpenFlexy
|
||||
// esistente (riconosciuta per nome, una tantum). Usa lower() per
|
||||
// sicurezza sul confronto.
|
||||
$this->execute(
|
||||
"UPDATE orderbook o
|
||||
JOIN service s ON s.idservice = o.idservice
|
||||
SET o.is_openflexy = 'Y'
|
||||
WHERE LOWER(TRIM(s.servicename)) = 'openflexy'"
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$table = $this->table('orderbook');
|
||||
$table->removeColumn('is_openflexy')->update();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Aggiunge la foreign key bookingclass.idorder -> orderbook.idorderbook.
|
||||
*
|
||||
* Con ON DELETE SET NULL: cancellare un ordine NON cancella le prenotazioni
|
||||
* collegate, ma ne slega il riferimento (idorder = NULL). Questo preserva la
|
||||
* storia delle prenotazioni ed evita errori di vincolo alla cancellazione.
|
||||
*
|
||||
* IMPORTANTE: se esistono righe in bookingclass con un idorder che non
|
||||
* corrisponde a nessun orderbook (dati orfani), la creazione della FK
|
||||
* fallisce. Il metodo up() bonifica prima questi orfani portandoli a NULL.
|
||||
*/
|
||||
final class AddForeignKeyBookingclassOrder extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1) Bonifica orfani: idorder che non esiste in orderbook -> NULL
|
||||
$this->execute(
|
||||
"UPDATE bookingclass b
|
||||
LEFT JOIN orderbook o ON o.idorderbook = b.idorder
|
||||
SET b.idorder = NULL
|
||||
WHERE b.idorder IS NOT NULL AND o.idorderbook IS NULL"
|
||||
);
|
||||
|
||||
// 2) Crea la foreign key
|
||||
$table = $this->table('bookingclass');
|
||||
$table->addForeignKey('idorder', 'orderbook', 'idorderbook', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'NO_ACTION',
|
||||
'constraint' => 'fk_bookingclass_orderbook',
|
||||
])->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$table = $this->table('bookingclass');
|
||||
$table->dropForeignKey('idorder', null, ['constraint' => 'fk_bookingclass_orderbook'])->update();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Aggiunge la colonna is_gift a bookingclass.
|
||||
*
|
||||
* is_gift = 'Y' indica una lezione OMAGGIO inserita dall'insegnante:
|
||||
* - non consuma ticket dell'ordine (esclusa dai conteggi residui)
|
||||
* - non è riprogrammabile (legata alla data della classe)
|
||||
* - tipicamente ha idorder = NULL
|
||||
*
|
||||
* Default 'N' = prenotazione normale.
|
||||
*/
|
||||
final class AddIsGiftToBookingclass extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$table = $this->table('bookingclass');
|
||||
$table->addColumn('is_gift', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'N',
|
||||
'after' => 'is_reprogrammed',
|
||||
])->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$table = $this->table('bookingclass');
|
||||
$table->removeColumn('is_gift')->update();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddPropagationTracking extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
// Tabella che raggruppa ogni propagazione
|
||||
$table = $this->table('propagation', ['id' => 'idpropagation']);
|
||||
$table->addColumn('idservice', 'integer')
|
||||
->addColumn('token', 'string', ['limit' => 64])
|
||||
->addColumn('startdate', 'date', ['null' => true])
|
||||
->addColumn('enddate', 'date', ['null' => true])
|
||||
->addColumn('created_at', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['token'], ['unique' => true])
|
||||
->addIndex(['idservice'])
|
||||
->create();
|
||||
|
||||
// Campo token sulle singole occorrenze del calendario
|
||||
$schedule = $this->table('serviceschedule');
|
||||
if (!$schedule->hasColumn('propagation_token')) {
|
||||
$schedule->addColumn('propagation_token', 'string', ['limit' => 64, 'null' => true])
|
||||
->addIndex(['propagation_token'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class FixPropagationCollation extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->execute(
|
||||
"ALTER TABLE propagation CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->execute(
|
||||
"ALTER TABLE propagation CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddWaitingListSupport extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('bookingclass');
|
||||
$table
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
'after' => 'cancellation_token',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddWaitingListEnabledToOption extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('waitinglist_enabled', 'char', [
|
||||
'limit' => 1,
|
||||
'default' => 'N',
|
||||
'null' => false,
|
||||
'after' => 'maxbeforetimecancell',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
final class CreateEventsTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('events', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'null' => true, 'comment' => 'insegnante/creatore -> auth_users.id'])
|
||||
->addColumn('title', 'string', ['limit' => 191, 'null' => false])
|
||||
->addColumn('slug', 'string', ['limit' => 191, 'null' => false, 'comment' => 'URL pubblico / widget'])
|
||||
->addColumn('description', 'text', ['limit' => MysqlAdapter::TEXT_LONG, 'null' => true, 'comment' => 'HTML editor'])
|
||||
->addColumn('organizer_name', 'string', ['limit' => 191, 'null' => true, 'comment' => 'nome libero; se null usa il creatore'])
|
||||
->addColumn('starts_at', 'datetime', ['null' => false])
|
||||
->addColumn('ends_at', 'datetime', ['null' => true])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => true, 'comment' => 'testo per Google Maps'])
|
||||
->addColumn('lat', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('lng', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('cost', 'decimal', ['precision' => 8, 'scale' => 2, 'null' => false, 'default' => 0])
|
||||
->addColumn('currency', 'string', ['limit' => 3, 'null' => false, 'default' => 'EUR'])
|
||||
->addColumn('max_seats', 'integer', ['signed' => false, 'null' => false, 'default' => 0])
|
||||
->addColumn('max_per_booking', 'integer', ['signed' => false, 'null' => false, 'default' => 1])
|
||||
->addColumn('color', 'string', ['limit' => 7, 'null' => false, 'default' => '#4CAF50', 'comment' => 'hex card'])
|
||||
->addColumn('cover_image', 'string', ['limit' => 191, 'null' => true, 'comment' => 'gallery in futuro'])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'null' => false, 'default' => 'draft', 'comment' => 'draft|published|cancelled'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'events_slug_unique'])
|
||||
->addIndex(['status', 'starts_at'], ['name' => 'events_status_starts_at_index'])
|
||||
->addIndex(['user_id'], ['name' => 'events_user_id_index'])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'NO_ACTION',
|
||||
'constraint' => 'events_user_id_foreign',
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateEventBookingsTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('event_bookings', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||
->addColumn('event_id', 'biginteger', ['signed' => false, 'null' => false])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'null' => true, 'comment' => 'null = ospite non registrato'])
|
||||
->addColumn('name', 'string', ['limit' => 191, 'null' => false])
|
||||
->addColumn('email', 'string', ['limit' => 191, 'null' => false, 'comment' => 'raccolta contatti'])
|
||||
->addColumn('phone', 'string', ['limit' => 40, 'null' => true])
|
||||
->addColumn('seats', 'integer', ['signed' => false, 'null' => false, 'default' => 1])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'null' => false, 'default' => 'pending', 'comment' => 'pending|confirmed|cancelled'])
|
||||
->addColumn('amount', 'decimal', ['precision' => 8, 'scale' => 2, 'null' => true, 'comment' => 'pronto per pagamenti'])
|
||||
->addColumn('token', 'string', ['limit' => 64, 'null' => false, 'comment' => 'gestione/annullo ospite via link email'])
|
||||
->addColumn('consent_at', 'datetime', ['null' => true, 'comment' => 'accettazione privacy/termini'])
|
||||
->addColumn('marketing_opt_in', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'comment' => 'consenso marketing Y/N'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addIndex(['token'], ['unique' => true, 'name' => 'event_bookings_token_unique'])
|
||||
->addIndex(['event_id'], ['name' => 'event_bookings_event_id_index'])
|
||||
->addIndex(['email'], ['name' => 'event_bookings_email_index'])
|
||||
->addIndex(['user_id'], ['name' => 'event_bookings_user_id_index'])
|
||||
->addForeignKey('event_id', 'events', 'id', [
|
||||
'delete' => 'CASCADE',
|
||||
'update' => 'NO_ACTION',
|
||||
'constraint' => 'event_bookings_event_id_foreign',
|
||||
])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'NO_ACTION',
|
||||
'constraint' => 'event_bookings_user_id_foreign',
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddEventDefaultsToOptionTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('event_default_color', 'string', ['limit' => 7, 'null' => true, 'default' => '#4CAF50', 'after' => 'colorcancelled'])
|
||||
->addColumn('event_default_max_per_booking', 'integer', ['null' => false, 'default' => 1, 'after' => 'event_default_color'])
|
||||
->addColumn('event_notify_email', 'string', ['limit' => 191, 'null' => true, 'after' => 'event_default_max_per_booking'])
|
||||
->addColumn('event_terms_text', 'text', ['null' => true, 'after' => 'event_notify_email'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateEventLocationsTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('event_locations', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||
->addColumn('name', 'string', ['limit' => 191, 'null' => false, 'comment' => 'etichetta sede, es. "Studio YogaSoul"'])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => false])
|
||||
->addColumn('lat', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('lng', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('is_default', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'comment' => 'Y = preselezionata'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addIndex(['is_default'], ['name' => 'event_locations_is_default_index'])
|
||||
->create();
|
||||
|
||||
// Sede di default (MODIFICA con indirizzo e coordinate reali della scuola)
|
||||
$this->table('event_locations')->insert([
|
||||
'name' => 'Studio YogaSoul',
|
||||
'address' => 'Indirizzo da compilare',
|
||||
'lat' => null,
|
||||
'lng' => null,
|
||||
'is_default' => 'Y',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
])->saveData();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddDetailsAndGalleryToEvents extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('events')
|
||||
->addColumn('instructions', 'text', ['null' => true, 'after' => 'description'])
|
||||
->addColumn('what_to_bring', 'text', ['null' => true, 'after' => 'instructions'])
|
||||
->addColumn('suitable_for', 'text', ['null' => true, 'after' => 'what_to_bring'])
|
||||
->addColumn('gallery', 'text', ['null' => true, 'comment' => 'JSON: array di path immagini', 'after' => 'suitable_for'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddWaitlistToEvents extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('events')
|
||||
->addColumn('waitlist_enabled', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'Y',
|
||||
'comment' => "Y = lista d'attesa quando pieno",
|
||||
'after' => 'max_per_booking'
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddEventEmailOptions extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('event_cc_booking', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_terms_text'])
|
||||
->addColumn('event_cc_promotion', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_booking'])
|
||||
->addColumn('event_cc_user_cancel', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_promotion'])
|
||||
->addColumn('event_cc_event_cancel', 'char', ['limit' => 1, 'null' => false, 'default' => 'Y', 'after' => 'event_cc_user_cancel'])
|
||||
->addColumn('event_cc_reminder', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_event_cancel'])
|
||||
->addColumn('event_reminder_enabled', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_reminder'])
|
||||
->addColumn('event_reminder_hours', 'integer', ['null' => false, 'default' => 24, 'after' => 'event_reminder_enabled'])
|
||||
->update();
|
||||
|
||||
$this->table('event_bookings')
|
||||
->addColumn('offered_at', 'datetime', ['null' => true, 'comment' => 'promozione lista attesa in attesa di riconferma', 'after' => 'consent_at'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddReminderAndOfferSettings extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
// Default generali (scuola)
|
||||
$this->table('option')
|
||||
->addColumn('event_offer_expiry_hours', 'integer', ['null' => false, 'default' => 24, 'after' => 'event_reminder_hours'])
|
||||
->update();
|
||||
|
||||
// Override per singolo evento (NULL = usa il default scuola)
|
||||
$this->table('events')
|
||||
->addColumn('reminder_enabled', 'char', ['limit' => 1, 'null' => true, 'default' => null, 'comment' => 'NULL=default scuola; Y/N override', 'after' => 'waitlist_enabled'])
|
||||
->addColumn('reminder_hours', 'integer', ['null' => true, 'default' => null, 'comment' => 'NULL=default scuola', 'after' => 'reminder_enabled'])
|
||||
->addColumn('offer_expiry_hours', 'integer', ['null' => true, 'default' => null, 'comment' => 'NULL=default scuola', 'after' => 'reminder_hours'])
|
||||
->addColumn('reminder_sent_at', 'datetime', ['null' => true, 'after' => 'offer_expiry_hours'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddPublishAtToEvents extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('events')
|
||||
->addColumn('publish_at', 'datetime', ['null' => true, 'comment' => 'apertura prenotazioni programmata', 'after' => 'status'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolsTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('schools', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschool'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschool', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('name', 'string', ['limit' => 191])
|
||||
->addColumn('slug', 'string', ['limit' => 100, 'comment' => 'usato per URL/sottodominio/widget'])
|
||||
->addColumn('email', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('phone', 'string', ['limit' => 40, 'null' => true])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => true])
|
||||
->addColumn('city', 'string', ['limit' => 100, 'null' => true])
|
||||
->addColumn('postcode', 'string', ['limit' => 20, 'null' => true])
|
||||
->addColumn('country', 'string', ['limit' => 2, 'default' => 'IT'])
|
||||
->addColumn('vat_number', 'string', ['limit' => 30, 'null' => true])
|
||||
->addColumn('logo', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('timezone', 'string', ['limit' => 50, 'default' => 'Europe/Rome'])
|
||||
->addColumn('locale', 'string', ['limit' => 5, 'default' => 'it'])
|
||||
->addColumn('currency', 'string', ['limit' => 3, 'default' => 'EUR'])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'default' => 'active', 'comment' => 'active|suspended|trial'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'schools_slug_unique'])
|
||||
->addIndex(['status'], ['name' => 'schools_status_index'])
|
||||
->create();
|
||||
|
||||
// Seregno = scuola 1 (dati da completare poi dal pannello)
|
||||
$this->table('schools')->insert([
|
||||
'idschool' => 1,
|
||||
'name' => 'Scuola Yoga Seregno',
|
||||
'slug' => 'seregno',
|
||||
'city' => 'Seregno',
|
||||
'country' => 'IT',
|
||||
])->saveData();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('schools')->drop()->save();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolUserTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('school_user', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschooluser'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschooluser', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'comment' => 'auth_users.id'])
|
||||
->addColumn('idschool', 'integer', ['signed' => false])
|
||||
->addColumn('school_role', 'string', ['limit' => 20, 'default' => 'student', 'comment' => 'student|teacher|admin|owner'])
|
||||
->addColumn('active', 'char', ['limit' => 1, 'default' => 'Y'])
|
||||
->addColumn('joined_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['user_id', 'idschool'], ['unique' => true, 'name' => 'school_user_unique'])
|
||||
->addIndex(['idschool'], ['name' => 'school_user_idschool_index'])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addForeignKey('idschool', 'schools', 'idschool', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->create();
|
||||
|
||||
// Tutti gli utenti esistenti appartengono a Seregno (1)
|
||||
$this->execute("
|
||||
INSERT INTO school_user (user_id, idschool, school_role)
|
||||
SELECT id, 1, 'student' FROM auth_users
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('school_user')->drop()->save();
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddIdschoolToDomainTables extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Tabelle applicative che diventano "per scuola".
|
||||
* Le tabelle auth_* (Vanguard) restano globali: il legame
|
||||
* utente-scuola passa da school_user.
|
||||
*/
|
||||
private const TABLES = [
|
||||
'associateclass',
|
||||
'bookingclass',
|
||||
'certificateuserprofile',
|
||||
'copyorder',
|
||||
'copyorder_items',
|
||||
'dayoff',
|
||||
'employee',
|
||||
'events',
|
||||
'event_bookings',
|
||||
'event_locations',
|
||||
'expiryparameter',
|
||||
'location',
|
||||
'option',
|
||||
'orderbook',
|
||||
'propagation',
|
||||
'service',
|
||||
'servicecategory',
|
||||
'serviceschedule',
|
||||
'userprofile',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->addColumn('idschool', 'integer', [
|
||||
'signed' => false,
|
||||
'null' => false,
|
||||
'default' => 1, // TEMPORANEO: verrà rimosso quando tutte le INSERT passeranno idschool
|
||||
])
|
||||
->addIndex(['idschool'], ['name' => "idx_{$name}_idschool"])
|
||||
->update();
|
||||
}
|
||||
|
||||
// Slug eventi: unico per scuola, non più globale
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_slug_unique')
|
||||
->addIndex(['idschool', 'slug'], ['unique' => true, 'name' => 'events_school_slug_unique'])
|
||||
->update();
|
||||
|
||||
// Impostazioni: una sola riga per scuola
|
||||
$this->table('option')
|
||||
->addIndex(['idschool'], ['unique' => true, 'name' => 'option_idschool_unique'])
|
||||
->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->removeIndexByName('option_idschool_unique')
|
||||
->update();
|
||||
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_school_slug_unique')
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'events_slug_unique'])
|
||||
->update();
|
||||
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->removeIndexByName("idx_{$name}_idschool")
|
||||
->removeColumn('idschool')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateBookingclassCancellations extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
// Storico delle lezioni cancellate dagli allievi.
|
||||
// Nessuna pagina esistente legge questa tabella: non cambia il resto del sistema.
|
||||
$this->table('bookingclass_cancellations', ['id' => 'idcancellation'])
|
||||
->addColumn('idbookingclass', 'integer')
|
||||
->addColumn('idserviceschedule', 'integer', ['null' => true])
|
||||
->addColumn('bookingstart', 'datetime')
|
||||
->addColumn('status', 'string', ['limit' => 30])
|
||||
->addColumn('idservice', 'integer')
|
||||
->addColumn('iduser', 'integer', ['signed' => false])
|
||||
->addColumn('idorder', 'integer', ['null' => true])
|
||||
->addColumn('prevbookingstart', 'datetime', ['null' => true])
|
||||
->addColumn('is_reprogrammed', 'char', ['limit' => 1, 'default' => 'N'])
|
||||
->addColumn('is_gift', 'char', ['limit' => 1, 'default' => 'N'])
|
||||
->addColumn('booking_created_at', 'timestamp', ['null' => true])
|
||||
->addColumn('cancelled_at', 'datetime')
|
||||
->addColumn('cancelled_by', 'integer', ['signed' => false])
|
||||
->addColumn('reprogrammed_incremented', 'char', ['limit' => 1, 'default' => 'N'])
|
||||
->addColumn('idschool', 'integer', ['signed' => false, 'default' => 1])
|
||||
->addIndex(['iduser'])
|
||||
->addIndex(['idorder'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Bozze delle newsletter scritte in YogiBook (pubblicate poi su The Newsletter)
|
||||
* e storico delle versioni per non perdere mai il testo.
|
||||
*/
|
||||
final class CreateNewsletterDrafts extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('newsletter_drafts', ['id' => 'iddraft'])
|
||||
->addColumn('title', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('subject', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('preheader', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('blocks', 'text', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG, 'null' => true])
|
||||
->addColumn('settings', 'text', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG, 'null' => true])
|
||||
->addColumn('revision', 'integer', ['default' => 0, 'signed' => false])
|
||||
->addColumn('wp_email_id', 'integer', ['null' => true])
|
||||
->addColumn('wp_pushed_at', 'datetime', ['null' => true])
|
||||
->addColumn('created_by', 'integer', ['signed' => false, 'default' => 0])
|
||||
->addColumn('updated_by', 'integer', ['signed' => false, 'default' => 0])
|
||||
->addColumn('created_at', 'datetime')
|
||||
->addColumn('updated_at', 'datetime')
|
||||
->addColumn('deleted_at', 'datetime', ['null' => true])
|
||||
->addColumn('idschool', 'integer', ['signed' => false, 'default' => 1])
|
||||
->addIndex(['updated_at'])
|
||||
->addIndex(['idschool'])
|
||||
->create();
|
||||
|
||||
$this->table('newsletter_draft_versions', ['id' => 'idversion'])
|
||||
->addColumn('iddraft', 'integer')
|
||||
->addColumn('revision', 'integer', ['signed' => false, 'default' => 0])
|
||||
->addColumn('subject', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('preheader', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('blocks', 'text', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG, 'null' => true])
|
||||
->addColumn('settings', 'text', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG, 'null' => true])
|
||||
->addColumn('note', 'string', ['limit' => 100, 'default' => ''])
|
||||
->addColumn('created_by', 'integer', ['signed' => false, 'default' => 0])
|
||||
->addColumn('created_at', 'datetime')
|
||||
->addIndex(['iddraft', 'created_at'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddIdschoolToDomainTables extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Tabelle applicative che diventano "per scuola".
|
||||
* Le tabelle auth_* (Vanguard) restano globali: il legame
|
||||
* utente-scuola passa da school_user.
|
||||
*/
|
||||
private const TABLES = [
|
||||
'associateclass',
|
||||
'bookingclass',
|
||||
'certificateuserprofile',
|
||||
'copyorder',
|
||||
'copyorder_items',
|
||||
'dayoff',
|
||||
'employee',
|
||||
'events',
|
||||
'event_bookings',
|
||||
'event_locations',
|
||||
'expiryparameter',
|
||||
'location',
|
||||
'option',
|
||||
'orderbook',
|
||||
'propagation',
|
||||
'service',
|
||||
'servicecategory',
|
||||
'serviceschedule',
|
||||
'userprofile',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->addColumn('idschool', 'integer', [
|
||||
'signed' => false,
|
||||
'null' => false,
|
||||
'default' => 1, // TEMPORANEO: verrà rimosso quando tutte le INSERT passeranno idschool
|
||||
])
|
||||
->addIndex(['idschool'], ['name' => "idx_{$name}_idschool"])
|
||||
->update();
|
||||
}
|
||||
|
||||
// Slug eventi: unico per scuola, non più globale
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_slug_unique')
|
||||
->addIndex(['idschool', 'slug'], ['unique' => true, 'name' => 'events_school_slug_unique'])
|
||||
->update();
|
||||
|
||||
// Impostazioni: una sola riga per scuola
|
||||
$this->table('option')
|
||||
->addIndex(['idschool'], ['unique' => true, 'name' => 'option_idschool_unique'])
|
||||
->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->removeIndexByName('option_idschool_unique')
|
||||
->update();
|
||||
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_school_slug_unique')
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'events_slug_unique'])
|
||||
->update();
|
||||
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->removeIndexByName("idx_{$name}_idschool")
|
||||
->removeColumn('idschool')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolUserTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('school_user', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschooluser'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschooluser', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'comment' => 'auth_users.id'])
|
||||
->addColumn('idschool', 'integer', ['signed' => false])
|
||||
->addColumn('school_role', 'string', ['limit' => 20, 'default' => 'student', 'comment' => 'student|teacher|admin|owner'])
|
||||
->addColumn('active', 'char', ['limit' => 1, 'default' => 'Y'])
|
||||
->addColumn('joined_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['user_id', 'idschool'], ['unique' => true, 'name' => 'school_user_unique'])
|
||||
->addIndex(['idschool'], ['name' => 'school_user_idschool_index'])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addForeignKey('idschool', 'schools', 'idschool', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->create();
|
||||
|
||||
// Tutti gli utenti esistenti appartengono a Seregno (1)
|
||||
$this->execute("
|
||||
INSERT INTO school_user (user_id, idschool, school_role)
|
||||
SELECT id, 1, 'student' FROM auth_users
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('school_user')->drop()->save();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolsTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('schools', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschool'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschool', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('name', 'string', ['limit' => 191])
|
||||
->addColumn('slug', 'string', ['limit' => 100, 'comment' => 'usato per URL/sottodominio/widget'])
|
||||
->addColumn('email', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('phone', 'string', ['limit' => 40, 'null' => true])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => true])
|
||||
->addColumn('city', 'string', ['limit' => 100, 'null' => true])
|
||||
->addColumn('postcode', 'string', ['limit' => 20, 'null' => true])
|
||||
->addColumn('country', 'string', ['limit' => 2, 'default' => 'IT'])
|
||||
->addColumn('vat_number', 'string', ['limit' => 30, 'null' => true])
|
||||
->addColumn('logo', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('timezone', 'string', ['limit' => 50, 'default' => 'Europe/Rome'])
|
||||
->addColumn('locale', 'string', ['limit' => 5, 'default' => 'it'])
|
||||
->addColumn('currency', 'string', ['limit' => 3, 'default' => 'EUR'])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'default' => 'active', 'comment' => 'active|suspended|trial'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'schools_slug_unique'])
|
||||
->addIndex(['status'], ['name' => 'schools_status_index'])
|
||||
->create();
|
||||
|
||||
// Seregno = scuola 1 (dati da completare poi dal pannello)
|
||||
$this->table('schools')->insert([
|
||||
'idschool' => 1,
|
||||
'name' => 'Scuola Yoga Seregno',
|
||||
'slug' => 'seregno',
|
||||
'city' => 'Seregno',
|
||||
'country' => 'IT',
|
||||
])->saveData();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('schools')->drop()->save();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
<?php
|
||||
// Connessione al database (adattare le informazioni di connessione)
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/mailer.php';
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
?>
|
||||
<?php
|
||||
// Connessione al database (adattare le informazioni di connessione)
|
||||
include('include/headscript.php');
|
||||
|
||||
$conn = $bkngstm; // riusa la connessione già aperta da bkngstm.php
|
||||
|
||||
// Recupera il parametro "idbookingclass" dalla richiesta GET
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
|
||||
// Recupera il parametro "idbookingclass" dalla richiesta GET
|
||||
if (isset($_GET['idbookingclass'])) {
|
||||
$idbookingclass = (int) $_GET['idbookingclass'];
|
||||
$idbookingclass = $_GET['idbookingclass'];
|
||||
|
||||
// Aggiorna lo status a "booked" e is_reprogrammed a "Y" nella tabella "bookingclass"
|
||||
$query = "UPDATE bookingclass SET status = 'booked', is_reprogrammed = 'Y' WHERE idbookingclass = ?";
|
||||
@@ -39,7 +38,9 @@ if (isset($_GET['idbookingclass'])) {
|
||||
$firstname = $row['first_name'];
|
||||
|
||||
$newtimeformat = date("d-m-Y H:i", strtotime($newtime));
|
||||
|
||||
require 'phpmailer/src/Exception.php';
|
||||
require 'phpmailer/src/PHPMailer.php';
|
||||
require 'phpmailer/src/SMTP.php';
|
||||
// Mail to client
|
||||
$messageedit = "
|
||||
<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>
|
||||
@@ -48,9 +49,9 @@ if (isset($_GET['idbookingclass'])) {
|
||||
<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;'>Se non ricordi la password o non hai sei mai entrata in Yogibbok, per prima cosa resettare la password cliccando di seguito</span></p>
|
||||
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Per il primo accesso devi per prima cosa resettare la password cliccando di seguito</span></p>
|
||||
<br>
|
||||
<a href='https://yogibook.yogasoul.it/password/reset' 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;'>
|
||||
<a href='https://yogibook.yogasoul.it/public/password/reset' 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;'>Reset Password</span></span>
|
||||
</a>
|
||||
<br>
|
||||
@@ -64,21 +65,20 @@ if (isset($_GET['idbookingclass'])) {
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION']; // ssl
|
||||
$mail->Port = (int) $_ENV['MAIL_PORT']; // 465
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->isSMTP(); // Set mailer to use SMTP
|
||||
$mail->Host = 'mail.yogasoul.it'; // Specify main and backup server
|
||||
$mail->SMTPAuth = true; // Enable SMTP authentication
|
||||
$mail->Username = 'info@yogasoul.it'; // SMTP username
|
||||
$mail->Password = '!Testolina88'; // SMTP password
|
||||
$mail->SMTPSecure = 'tls'; // Enable encryption, 'ssl' also accepted
|
||||
$mail->Port = '587';
|
||||
|
||||
include('mail/emailtemplate2.php');
|
||||
// Email body content
|
||||
$htmlContent = $mailmessage1;
|
||||
|
||||
$mail->From = $_ENV['MAIL_FROM_ADDRESS'];
|
||||
$mail->FromName = $_ENV['MAIL_FROM_NAME'];
|
||||
$mail->From = 'info@yogasoul.it';
|
||||
$mail->FromName = 'YogiBook [YogaSoul]';
|
||||
$mail->addAddress($emailuser); // Add a recipient
|
||||
|
||||
$mail->Subject = "YogiBook - Lezione riprogrammata con successo!";
|
||||
@@ -104,3 +104,7 @@ if (isset($_GET['idbookingclass'])) {
|
||||
} else {
|
||||
echo "Parametro 'idbookingclass' non trovato nella richiesta GET.";
|
||||
}
|
||||
|
||||
// Chiudi la connessione al database
|
||||
$conn->close();
|
||||
?>
|
||||
+17
-251
@@ -11,56 +11,9 @@ 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
|
||||
* ---------------------------------------------------------------------- */
|
||||
@@ -73,17 +26,14 @@ 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, idteacher)
|
||||
(servicename, wpcatalognumber, day, time, category, maxcapacity, colorclass, classduration)
|
||||
VALUES
|
||||
(:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration, :idteacher)";
|
||||
(:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$ok = $stmt->execute([
|
||||
':servicename' => $servicename,
|
||||
@@ -94,7 +44,6 @@ if (isset($_POST['classinsert']) && $_POST['classinsert'] === 'Y') {
|
||||
':maxcapacity' => $maxcapacity !== '' ? (int) $maxcapacity : null,
|
||||
':colorclass' => $colorclass,
|
||||
':classduration' => $classduration !== '' ? (float) $classduration : null,
|
||||
':idteacher' => $idteacher,
|
||||
]);
|
||||
|
||||
if ($ok) {
|
||||
@@ -120,46 +69,6 @@ $services = $pdo->query(
|
||||
LEFT JOIN servicecategory ON service.category = servicecategory.idservicecategory
|
||||
ORDER BY service.servicename"
|
||||
)->fetchAll();
|
||||
|
||||
// Propagazioni già effettuate, raggruppate per servizio
|
||||
$propagationsByService = [];
|
||||
$propRows = $pdo->query(
|
||||
"SELECT p.idpropagation, p.idservice, p.token, p.startdate, p.enddate, p.created_at,
|
||||
COUNT(s.idserviceschedule) AS occorrenze
|
||||
FROM propagation p
|
||||
LEFT JOIN serviceschedule s ON s.propagation_token = p.token
|
||||
GROUP BY p.idpropagation
|
||||
ORDER BY p.created_at DESC"
|
||||
)->fetchAll();
|
||||
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">
|
||||
@@ -274,43 +183,6 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
.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>
|
||||
|
||||
@@ -352,40 +224,14 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
<?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; ?>
|
||||
<?php if ($message === 'propdeleted') : ?>
|
||||
<div class="alert alert-success" role="alert">Propagazione eliminata con successo.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($message === 'confirmdelete') : ?>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Attenzione: questa propagazione ha <strong><?php echo (int) ($_GET['count'] ?? 0); ?></strong> prenotazioni utente collegate.
|
||||
Vuoi eliminarla comunque insieme alle lezioni?
|
||||
<a href="cancelpropagation.php?token=<?php echo e($_GET['token'] ?? ''); ?>&confirm=Y"
|
||||
class="btn btn-danger btn-sm ms-2"
|
||||
onclick="return confirm('Confermi la cancellazione definitiva?');">
|
||||
Sì, elimina tutto
|
||||
</a>
|
||||
<a href="admin-services.php" class="btn btn-secondary btn-sm ms-1">Annulla</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($insertFeedback) : ?>
|
||||
<div class="alert alert-<?php echo $insertFeedback['type'] === 'success' ? 'success' : 'danger'; ?>" role="alert">
|
||||
<?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">
|
||||
@@ -468,14 +314,6 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
<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>
|
||||
@@ -491,7 +329,7 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
<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, assegna l'insegnante o modificane i dettagli.</p>
|
||||
<p class="section-lead">Propaga una classe sul calendario, associala o modificane i dettagli.</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table class-table align-middle mb-0">
|
||||
@@ -502,20 +340,16 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
<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="7" class="text-center text-muted py-4">Nessuna classe presente.</td>
|
||||
<td colspan="6" class="text-center text-muted py-4">Nessuna classe presente.</td>
|
||||
</tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ($services as $svc) :
|
||||
$svcTeacherId = (int) $svc['idteacher'];
|
||||
$svcTeacher = $teachersById[$svcTeacherId] ?? null;
|
||||
?>
|
||||
<?php foreach ($services as $svc) : ?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="color-dot" style="background-color: <?php echo e($svc['colorclass'] ?: '#ccc'); ?>;"></span>
|
||||
@@ -528,43 +362,25 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
<?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']; ?>">
|
||||
<button type="button" class="btn btn-info btn-sm">Associa</button>
|
||||
</a>
|
||||
<a href="updateservice.php?idservice=<?php echo (int) $svc['idservice']; ?>" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-lead-pencil font-size-16 align-middle"></i>
|
||||
<a href="updateservice.php?idservice=<?php echo (int) $svc['idservice']; ?>">
|
||||
<button type="button" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-lead-pencil font-size-16 align-middle"></i>
|
||||
</button>
|
||||
</a>
|
||||
<a href="cancelservice.php?idservice=<?php echo (int) $svc['idservice']; ?>"
|
||||
class="btn btn-danger btn-sm"
|
||||
title="Elimina classe"
|
||||
onclick="return confirm(<?php echo e(json_encode('Eliminare la classe ' . $svc['servicename'] . ' (' . $svc['day'] . ' ' . $svc['time'] . ')? L\'operazione non si può annullare.')); ?>);">
|
||||
<i class="bx bx-block font-size-16 align-middle"></i>
|
||||
<a href="cancelservice.php?idservice=<?php echo (int) $svc['idservice']; ?>">
|
||||
<button type="button" class="btn btn-danger btn-sm">
|
||||
<i class="bx bx-block font-size-16 align-middle"></i>
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="expanded-row">
|
||||
<td colspan="7">
|
||||
<td colspan="6">
|
||||
<div class="expanded-content">
|
||||
<form class="calendar-form" method="post" action="nextdateclass.php">
|
||||
<div class="row">
|
||||
@@ -587,40 +403,6 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
<input type="hidden" name="durationtime" value="<?php echo e($svc['classduration']); ?>">
|
||||
<button type="submit" class="btn btn-primary btn-save">Propaga sul calendario</button>
|
||||
</form>
|
||||
|
||||
<?php $props = $propagationsByService[$svc['idservice']] ?? []; ?>
|
||||
<?php if (!empty($props)) : ?>
|
||||
<hr>
|
||||
<div class="section-title" style="font-size:13px;">Propagazioni effettuate</div>
|
||||
<table class="table table-sm mt-2 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Inizio</th>
|
||||
<th>Fine</th>
|
||||
<th>Lezioni</th>
|
||||
<th>Token</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($props as $p) : ?>
|
||||
<tr>
|
||||
<td><?php echo e($p['startdate']); ?></td>
|
||||
<td><?php echo e($p['enddate']); ?></td>
|
||||
<td><?php echo e($p['occorrenze']); ?></td>
|
||||
<td><code style="font-size:11px;"><?php echo e(substr((string)$p['token'], 0, 10)); ?>…</code></td>
|
||||
<td class="text-end">
|
||||
<a href="cancelpropagation.php?token=<?php echo e($p['token']); ?>"
|
||||
class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('Eliminare questa propagazione e tutte le sue lezioni?');">
|
||||
Elimina
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -661,25 +443,9 @@ function teacherOptions(array $teachers, int $selectedId): string
|
||||
palette: [
|
||||
["#FF0000", "#00FF00", "#0000FF"],
|
||||
["#1ebf73", "#f39c12", "#8e44ad"]
|
||||
]
|
||||
});
|
||||
|
||||
// Forza il valore del colore nel campo prima dell'invio del form
|
||||
$('#insertclass').on('submit', function() {
|
||||
var col = $('#colorclass').spectrum('get');
|
||||
if (col) {
|
||||
$('#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')));
|
||||
],
|
||||
change: function(color) {
|
||||
$('#colorclass').val(color.toHexString());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Cancellazione di una prenotazione da parte della scuola (dettaglio lezioni in Statistiche).
|
||||
*
|
||||
* POST: id = idbookingclass
|
||||
* send_email = Y/N invia email di conferma all'allieva
|
||||
* count_reprog = Y/N conta come riprogrammazione (orderbook.reprogrammed + 1)
|
||||
*
|
||||
* Copia la prenotazione nello storico (bookingclass_cancellations), poi la elimina.
|
||||
* Tutto in una transazione; l'email parte solo dopo il commit.
|
||||
*/
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
|
||||
function jsonOut(array $payload, int $code = 200): void
|
||||
{
|
||||
if (ob_get_length() !== false) ob_end_clean();
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonOut(['success' => false, 'message' => 'Metodo non consentito.'], 405);
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$sendEmail = ($_POST['send_email'] ?? 'N') === 'Y';
|
||||
$countReprog = ($_POST['count_reprog'] ?? 'N') === 'Y';
|
||||
$staffId = (int) $iduserlogin; // chi cancella: sempre l'utente della sessione
|
||||
|
||||
if ($id <= 0) {
|
||||
jsonOut(['success' => false, 'message' => 'Prenotazione non valida.'], 400);
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$bk = null;
|
||||
$incremented = 'N';
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Prenotazione + dati per l'email, riga bloccata contro doppi click
|
||||
$st = $pdo->prepare("
|
||||
SELECT bc.idbookingclass, bc.idorder, bc.bookingstart,
|
||||
s.servicename, ss.dateschedule,
|
||||
u.email, u.first_name
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN service s ON s.idservice = bc.idservice
|
||||
LEFT JOIN serviceschedule ss ON ss.idserviceschedule = bc.idserviceschedule
|
||||
LEFT JOIN auth_users u ON u.id = bc.iduser
|
||||
WHERE bc.idbookingclass = ?
|
||||
FOR UPDATE
|
||||
");
|
||||
$st->execute([$id]);
|
||||
$bk = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$bk) {
|
||||
throw new RuntimeException('notfound');
|
||||
}
|
||||
|
||||
// Conta come riprogrammazione (solo se c'è un ordine collegato)
|
||||
if ($countReprog && $bk['idorder'] !== null) {
|
||||
$up = $pdo->prepare("UPDATE orderbook SET reprogrammed = reprogrammed + 1 WHERE idorderbook = ?");
|
||||
$up->execute([(int) $bk['idorder']]);
|
||||
if ($up->rowCount() === 1) {
|
||||
$incremented = 'Y';
|
||||
}
|
||||
}
|
||||
|
||||
// Copia nello storico
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO bookingclass_cancellations
|
||||
(idbookingclass, idserviceschedule, bookingstart, status, idservice, iduser, idorder,
|
||||
prevbookingstart, is_reprogrammed, is_gift, booking_created_at,
|
||||
cancelled_at, cancelled_by, reprogrammed_incremented)
|
||||
SELECT idbookingclass, idserviceschedule, bookingstart, status, idservice, iduser, idorder,
|
||||
prevbookingstart, is_reprogrammed, is_gift, created_at,
|
||||
NOW(), ?, ?
|
||||
FROM bookingclass
|
||||
WHERE idbookingclass = ?
|
||||
");
|
||||
$ins->execute([$staffId, $incremented, $id]);
|
||||
|
||||
// Eliminazione
|
||||
$del = $pdo->prepare("DELETE FROM bookingclass WHERE idbookingclass = ?");
|
||||
$del->execute([$id]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (PDOException $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log("admin_cancel_booking #{$id} (staff #{$staffId}): " . $e->getMessage());
|
||||
jsonOut(['success' => false, 'message' => 'Errore durante la cancellazione. Riprova.'], 500);
|
||||
} catch (RuntimeException $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
jsonOut(['success' => false, 'message' => 'Prenotazione non trovata (forse già cancellata).'], 404);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Email di conferma (dopo il commit, solo se richiesta)
|
||||
* ---------------------------------------------------------------------- */
|
||||
$emailNote = '';
|
||||
|
||||
if ($sendEmail) {
|
||||
$to = trim((string) ($bk['email'] ?? ''));
|
||||
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
$emailNote = ' Email non inviata: indirizzo dell\'allieva mancante o non valido.';
|
||||
} else {
|
||||
try {
|
||||
require_once(__DIR__ . '/class/mailer.php');
|
||||
|
||||
$clean = fn($v) => trim((string) $v, "\" \t\n\r\0\x0B");
|
||||
$enc = strtolower($clean($_ENV['MAIL_ENCRYPTION'] ?? 'ssl'));
|
||||
|
||||
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $clean($_ENV['MAIL_HOST'] ?? '');
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'] ?? '';
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||
$mail->Port = (int) ($_ENV['MAIL_PORT'] ?? 465);
|
||||
if (in_array($enc, ['tls', 'ssl'], true)) {
|
||||
$mail->SMTPSecure = $enc;
|
||||
}
|
||||
$mail->Timeout = 20;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
$fromName = $clean($_ENV['MAIL_FROM_NAME'] ?? 'YogiBook');
|
||||
$mail->setFrom($clean($_ENV['MAIL_FROM_ADDRESS'] ?? ''), $fromName);
|
||||
|
||||
$nome = trim((string) ($bk['first_name'] ?? ''));
|
||||
$className = $bk['servicename'] ?: 'Lezione';
|
||||
$dateRaw = $bk['dateschedule'] ?: $bk['bookingstart'];
|
||||
$dateTxt = $dateRaw ? date('d/m/Y H:i', strtotime($dateRaw)) : '';
|
||||
|
||||
$h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$mail->addAddress($to, $nome);
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = 'Lezione cancellata - ' . $className;
|
||||
$mail->Body = "
|
||||
<div style='font-family:Arial,sans-serif;font-size:14px;color:#222'>
|
||||
<p>Ciao " . $h($nome !== '' ? $nome : 'Yogi') . ",</p>
|
||||
<p>ti confermiamo che la tua lezione è stata <b>cancellata</b>.</p>
|
||||
<p>
|
||||
<b>Lezione:</b> " . $h($className) . "<br>
|
||||
<b>Data/ora:</b> " . $h($dateTxt) . "
|
||||
</p>
|
||||
<p>Se hai domande, rispondi pure a questa email.</p>
|
||||
<p>A presto,<br>" . $h($fromName) . "</p>
|
||||
</div>";
|
||||
$mail->AltBody = "Ciao " . ($nome !== '' ? $nome : 'Yogi') . ",\n"
|
||||
. "ti confermiamo che la tua lezione è stata cancellata.\n"
|
||||
. "Lezione: {$className}\nData/ora: {$dateTxt}\n\n{$fromName}";
|
||||
|
||||
$mail->send();
|
||||
$emailNote = ' Email di conferma inviata.';
|
||||
} catch (Throwable $e) {
|
||||
error_log("admin_cancel_booking #{$id}: email non inviata: " . $e->getMessage());
|
||||
$emailNote = ' Attenzione: la lezione è stata cancellata ma l\'email non è partita.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$reprogNote = $incremented === 'Y' ? ' Conteggiata come riprogrammazione.' : '';
|
||||
|
||||
jsonOut(['success' => true, 'message' => 'Lezione cancellata.' . $reprogNote . $emailNote]);
|
||||
@@ -1,214 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Riprogrammazione di una prenotazione da parte della scuola.
|
||||
*
|
||||
* POST: id = idbookingclass
|
||||
* idschedule = idserviceschedule della nuova lezione
|
||||
* send_email = Y/N email all'allieva con la nuova data
|
||||
* count_reprog = Y/N conta come riprogrammazione (orderbook.reprogrammed + 1)
|
||||
*
|
||||
* Sposta la prenotazione salvando la vecchia lezione in idprevserviceschedule / prevbookingstart.
|
||||
* Controlla posti liberi e doppia prenotazione con le righe bloccate (FOR UPDATE).
|
||||
*/
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
|
||||
function jsonOut(array $payload, int $code = 200): void
|
||||
{
|
||||
if (ob_get_length() !== false) ob_end_clean();
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonOut(['success' => false, 'message' => 'Metodo non consentito.'], 405);
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$idSchedule = (int) ($_POST['idschedule'] ?? 0);
|
||||
$sendEmail = ($_POST['send_email'] ?? 'N') === 'Y';
|
||||
$countReprog = ($_POST['count_reprog'] ?? 'N') === 'Y';
|
||||
$staffId = (int) $iduserlogin;
|
||||
|
||||
if ($id <= 0 || $idSchedule <= 0) {
|
||||
jsonOut(['success' => false, 'message' => 'Seleziona la nuova lezione.'], 400);
|
||||
}
|
||||
|
||||
$errors = [
|
||||
'notfound' => 'Prenotazione non trovata (forse è stata cancellata).',
|
||||
'notarget' => 'La lezione scelta non esiste più.',
|
||||
'same' => 'Hai scelto la stessa lezione di adesso.',
|
||||
'past' => 'La lezione scelta è già iniziata o passata.',
|
||||
'already' => 'L\'allieva è già iscritta alla lezione scelta.',
|
||||
'full' => 'La lezione scelta è piena.',
|
||||
];
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$bk = null;
|
||||
$target = null;
|
||||
$incremented = 'N';
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Prenotazione attuale (bloccata)
|
||||
$st = $pdo->prepare("
|
||||
SELECT bc.idbookingclass, bc.iduser, bc.idorder, bc.idserviceschedule, bc.bookingstart,
|
||||
s.servicename AS old_servicename, ss.dateschedule AS old_date,
|
||||
u.email, u.first_name
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN service s ON s.idservice = bc.idservice
|
||||
LEFT JOIN serviceschedule ss ON ss.idserviceschedule = bc.idserviceschedule
|
||||
LEFT JOIN auth_users u ON u.id = bc.iduser
|
||||
WHERE bc.idbookingclass = ?
|
||||
FOR UPDATE
|
||||
");
|
||||
$st->execute([$id]);
|
||||
$bk = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$bk) throw new RuntimeException('notfound');
|
||||
|
||||
if ((int) $bk['idserviceschedule'] === $idSchedule) throw new RuntimeException('same');
|
||||
|
||||
// Nuova lezione (bloccata: evita due spostamenti contemporanei sull'ultimo posto)
|
||||
$st = $pdo->prepare("
|
||||
SELECT ss.idserviceschedule, ss.idservice, ss.dateschedule, s.servicename, s.maxcapacity
|
||||
FROM serviceschedule ss
|
||||
JOIN service s ON s.idservice = ss.idservice
|
||||
WHERE ss.idserviceschedule = ?
|
||||
FOR UPDATE
|
||||
");
|
||||
$st->execute([$idSchedule]);
|
||||
$target = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$target) throw new RuntimeException('notarget');
|
||||
|
||||
if (strtotime($target['dateschedule']) <= time()) throw new RuntimeException('past');
|
||||
|
||||
// L'allieva è già su quella lezione?
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM bookingclass WHERE idserviceschedule = ? AND iduser = ?");
|
||||
$st->execute([$idSchedule, (int) $bk['iduser']]);
|
||||
if ((int) $st->fetchColumn() > 0) throw new RuntimeException('already');
|
||||
|
||||
// Posti liberi (booked + pending)
|
||||
if ($target['maxcapacity'] !== null) {
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM bookingclass WHERE idserviceschedule = ? AND status IN ('booked','pending')");
|
||||
$st->execute([$idSchedule]);
|
||||
if ((int) $st->fetchColumn() >= (int) $target['maxcapacity']) throw new RuntimeException('full');
|
||||
}
|
||||
|
||||
// Spostamento: la vecchia lezione resta in idprevserviceschedule / prevbookingstart
|
||||
$up = $pdo->prepare("
|
||||
UPDATE bookingclass
|
||||
SET idprevserviceschedule = :prevsched,
|
||||
prevbookingstart = :prevstart,
|
||||
idserviceschedule = :sched,
|
||||
idservice = :svc,
|
||||
bookingstart = :start,
|
||||
is_reprogrammed = 'Y'
|
||||
WHERE idbookingclass = :id
|
||||
");
|
||||
$up->execute([
|
||||
':prevsched' => $bk['idserviceschedule'] !== null ? (int) $bk['idserviceschedule'] : null,
|
||||
':prevstart' => $bk['bookingstart'],
|
||||
':sched' => $idSchedule,
|
||||
':svc' => (int) $target['idservice'],
|
||||
':start' => $target['dateschedule'],
|
||||
':id' => $id,
|
||||
]);
|
||||
|
||||
// Conta come riprogrammazione
|
||||
if ($countReprog && $bk['idorder'] !== null) {
|
||||
$st = $pdo->prepare("UPDATE orderbook SET reprogrammed = reprogrammed + 1 WHERE idorderbook = ?");
|
||||
$st->execute([(int) $bk['idorder']]);
|
||||
if ($st->rowCount() === 1) $incremented = 'Y';
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (PDOException $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log("admin_reprogram_booking #{$id} -> {$idSchedule} (staff #{$staffId}): " . $e->getMessage());
|
||||
jsonOut(['success' => false, 'message' => 'Errore durante la riprogrammazione. Riprova.'], 500);
|
||||
} catch (RuntimeException $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
$code = $e->getMessage();
|
||||
jsonOut(['success' => false, 'message' => $errors[$code] ?? 'Errore durante la riprogrammazione.'], 409);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Email all'allieva (dopo il commit, solo se richiesta)
|
||||
* ---------------------------------------------------------------------- */
|
||||
$fmt = fn($d) => $d ? date('d/m/Y H:i', strtotime($d)) : '';
|
||||
$emailNote = '';
|
||||
|
||||
if ($sendEmail) {
|
||||
$to = trim((string) ($bk['email'] ?? ''));
|
||||
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
$emailNote = ' Email non inviata: indirizzo dell\'allieva mancante o non valido.';
|
||||
} else {
|
||||
try {
|
||||
require_once(__DIR__ . '/class/mailer.php');
|
||||
|
||||
$clean = fn($v) => trim((string) $v, "\" \t\n\r\0\x0B");
|
||||
$enc = strtolower($clean($_ENV['MAIL_ENCRYPTION'] ?? 'ssl'));
|
||||
|
||||
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $clean($_ENV['MAIL_HOST'] ?? '');
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'] ?? '';
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||
$mail->Port = (int) ($_ENV['MAIL_PORT'] ?? 465);
|
||||
if (in_array($enc, ['tls', 'ssl'], true)) {
|
||||
$mail->SMTPSecure = $enc;
|
||||
}
|
||||
$mail->Timeout = 20;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
$fromName = $clean($_ENV['MAIL_FROM_NAME'] ?? 'YogiBook');
|
||||
$mail->setFrom($clean($_ENV['MAIL_FROM_ADDRESS'] ?? ''), $fromName);
|
||||
|
||||
$h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
$nome = trim((string) ($bk['first_name'] ?? ''));
|
||||
$oldName = $bk['old_servicename'] ?: 'Lezione';
|
||||
$oldDate = $fmt($bk['old_date'] ?: $bk['bookingstart']);
|
||||
$newName = $target['servicename'] ?: 'Lezione';
|
||||
$newDate = $fmt($target['dateschedule']);
|
||||
|
||||
$mail->addAddress($to, $nome);
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = 'Lezione riprogrammata - ' . $newName . ' ' . $newDate;
|
||||
$mail->Body = "
|
||||
<div style='font-family:Arial,sans-serif;font-size:14px;color:#222'>
|
||||
<p>Ciao " . $h($nome !== '' ? $nome : 'Yogi') . ",</p>
|
||||
<p>la tua lezione è stata <b>riprogrammata</b>.</p>
|
||||
<p>
|
||||
<b>Prima:</b> " . $h($oldName) . " – " . $h($oldDate) . "<br>
|
||||
<b>Adesso:</b> " . $h($newName) . " – <b>" . $h($newDate) . "</b>
|
||||
</p>
|
||||
<p>Se hai domande, rispondi pure a questa email.</p>
|
||||
<p>A presto,<br>" . $h($fromName) . "</p>
|
||||
</div>";
|
||||
$mail->AltBody = "Ciao " . ($nome !== '' ? $nome : 'Yogi') . ",\n"
|
||||
. "la tua lezione è stata riprogrammata.\n"
|
||||
. "Prima: {$oldName} - {$oldDate}\nAdesso: {$newName} - {$newDate}\n\n{$fromName}";
|
||||
|
||||
$mail->send();
|
||||
$emailNote = ' Email inviata all\'allieva.';
|
||||
} catch (Throwable $e) {
|
||||
error_log("admin_reprogram_booking #{$id}: email non inviata: " . $e->getMessage());
|
||||
$emailNote = ' Attenzione: la lezione è stata spostata ma l\'email non è partita.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$reprogNote = $incremented === 'Y' ? ' Conteggiata come riprogrammazione.' : '';
|
||||
|
||||
jsonOut([
|
||||
'success' => true,
|
||||
'message' => 'Lezione spostata a ' . $fmt($target['dateschedule']) . '.' . $reprogNote . $emailNote,
|
||||
]);
|
||||
@@ -1,109 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Elenco delle lezioni in calendario verso cui spostare una prenotazione (uso scuola).
|
||||
* GET: id = idbookingclass
|
||||
*
|
||||
* Restituisce le lezioni future (prossimi 90 giorni) della stessa classe
|
||||
* e delle classi associate (associateclass), con posti occupati e flag.
|
||||
*/
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
|
||||
function jsonOut(array $payload, int $code = 200): void
|
||||
{
|
||||
if (ob_get_length() !== false) ob_end_clean();
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
jsonOut(['success' => false, 'message' => 'Prenotazione non valida.'], 400);
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
try {
|
||||
// Prenotazione + ordine collegato
|
||||
$st = $pdo->prepare("
|
||||
SELECT bc.idbookingclass, bc.iduser, bc.idservice, bc.idserviceschedule, bc.idorder,
|
||||
o.expireon, o.reprogrammed, o.maxreschedule
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN orderbook o ON o.idorderbook = bc.idorder
|
||||
WHERE bc.idbookingclass = ?
|
||||
");
|
||||
$st->execute([$id]);
|
||||
$bk = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$bk) {
|
||||
jsonOut(['success' => false, 'message' => 'Prenotazione non trovata.'], 404);
|
||||
}
|
||||
|
||||
// Classi ammesse: la stessa + quelle associate
|
||||
$services = [(int) $bk['idservice']];
|
||||
$as = $pdo->prepare("SELECT idassociateservice FROM associateclass WHERE idmainservice = ?");
|
||||
$as->execute([(int) $bk['idservice']]);
|
||||
foreach ($as->fetchAll(PDO::FETCH_COLUMN) as $sid) {
|
||||
$services[] = (int) $sid;
|
||||
}
|
||||
$services = array_values(array_unique($services));
|
||||
$ph = implode(',', array_fill(0, count($services), '?'));
|
||||
|
||||
$sql = "
|
||||
SELECT ss.idserviceschedule, ss.dateschedule,
|
||||
s.servicename, s.maxcapacity,
|
||||
(SELECT COUNT(*) FROM bookingclass b
|
||||
WHERE b.idserviceschedule = ss.idserviceschedule
|
||||
AND b.status IN ('booked','pending')) AS occupied,
|
||||
(SELECT COUNT(*) FROM bookingclass b2
|
||||
WHERE b2.idserviceschedule = ss.idserviceschedule
|
||||
AND b2.iduser = ?) AS mine
|
||||
FROM serviceschedule ss
|
||||
JOIN service s ON s.idservice = ss.idservice
|
||||
WHERE ss.idservice IN ($ph)
|
||||
AND ss.dateschedule > NOW()
|
||||
AND ss.dateschedule <= DATE_ADD(NOW(), INTERVAL 90 DAY)
|
||||
ORDER BY ss.dateschedule
|
||||
LIMIT 200";
|
||||
$ls = $pdo->prepare($sql);
|
||||
$ls->execute(array_merge([(int) $bk['iduser']], $services));
|
||||
|
||||
$giorni = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
|
||||
$expiry = $bk['expireon'] ? strtotime($bk['expireon'] . ' 23:59:59') : null;
|
||||
|
||||
$lessons = [];
|
||||
foreach ($ls->fetchAll(PDO::FETCH_ASSOC) as $l) {
|
||||
$ts = strtotime($l['dateschedule']);
|
||||
$max = $l['maxcapacity'] !== null ? (int) $l['maxcapacity'] : null;
|
||||
$occ = (int) $l['occupied'];
|
||||
|
||||
$lessons[] = [
|
||||
'id' => (int) $l['idserviceschedule'],
|
||||
'label' => $giorni[(int) date('w', $ts)] . ' ' . date('d/m/Y H:i', $ts),
|
||||
'servicename' => $l['servicename'],
|
||||
'occupied' => $occ,
|
||||
'max' => $max,
|
||||
'full' => $max !== null && $occ >= $max,
|
||||
'mine' => (int) $l['mine'] > 0,
|
||||
'after_expiry' => $expiry !== null && $ts > $expiry,
|
||||
];
|
||||
}
|
||||
|
||||
jsonOut([
|
||||
'success' => true,
|
||||
'lessons' => $lessons,
|
||||
'order' => [
|
||||
'has_order' => $bk['idorder'] !== null,
|
||||
'reprogrammed' => (int) ($bk['reprogrammed'] ?? 0),
|
||||
'maxreschedule' => (int) ($bk['maxreschedule'] ?? 0),
|
||||
'expireon' => $bk['expireon'] ? date('d/m/Y', strtotime($bk['expireon'])) : null,
|
||||
],
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log("admin_reprogram_lessons #{$id}: " . $e->getMessage());
|
||||
jsonOut(['success' => false, 'message' => 'Errore nel caricamento delle lezioni.'], 500);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Correzione della data di scadenza di un certificato (uso scuola, pagina Certificati).
|
||||
* POST: id = idcertificateuserprofile
|
||||
* expiry = nuova data (YYYY-MM-DD)
|
||||
*/
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
|
||||
function jsonOut(array $payload, int $code = 200): void
|
||||
{
|
||||
if (ob_get_length() !== false) ob_end_clean();
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonOut(['success' => false, 'message' => 'Metodo non consentito.'], 405);
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$expiry = trim((string) ($_POST['expiry'] ?? ''));
|
||||
|
||||
if ($id <= 0) {
|
||||
jsonOut(['success' => false, 'message' => 'Certificato non valido.'], 400);
|
||||
}
|
||||
|
||||
// Data valida e nel formato esatto YYYY-MM-DD
|
||||
$d = DateTime::createFromFormat('!Y-m-d', $expiry);
|
||||
$err = DateTime::getLastErrors();
|
||||
if (!$d || ($err && ($err['warning_count'] || $err['error_count'])) || $d->format('Y-m-d') !== $expiry) {
|
||||
jsonOut(['success' => false, 'message' => 'Data non valida.'], 422);
|
||||
}
|
||||
|
||||
// Intervallo ragionevole: evita errori di battitura sull'anno
|
||||
$year = (int) $d->format('Y');
|
||||
if ($year < 2000 || $year > (int) date('Y') + 3) {
|
||||
jsonOut(['success' => false, 'message' => 'Controlla l\'anno: la data sembra sbagliata.'], 422);
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
try {
|
||||
$st = $pdo->prepare("SELECT 1 FROM certificateuserprofile WHERE idcertificateuserprofile = ?");
|
||||
$st->execute([$id]);
|
||||
if (!$st->fetchColumn()) {
|
||||
jsonOut(['success' => false, 'message' => 'Certificato non trovato.'], 404);
|
||||
}
|
||||
|
||||
$up = $pdo->prepare("UPDATE certificateuserprofile SET expirydatedocument = ? WHERE idcertificateuserprofile = ?");
|
||||
$up->execute([$expiry, $id]);
|
||||
} catch (PDOException $e) {
|
||||
error_log("admin_update_certificate_expiry #{$id}: " . $e->getMessage());
|
||||
jsonOut(['success' => false, 'message' => 'Errore durante il salvataggio.'], 500);
|
||||
}
|
||||
|
||||
jsonOut([
|
||||
'success' => true,
|
||||
'expiry' => $expiry,
|
||||
'expired' => $expiry < date('Y-m-d'),
|
||||
'message' => 'Scadenza aggiornata.',
|
||||
]);
|
||||
+13
-429
@@ -28,16 +28,8 @@ function avatarColor(string $seed): string
|
||||
return $palette[$idx];
|
||||
}
|
||||
|
||||
$optRow = $pdo->query("SELECT maxbeforetimecancell, show_teacher FROM `option` LIMIT 1")->fetch();
|
||||
$optRow = $pdo->query("SELECT maxbeforetimecancell 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';
|
||||
@@ -52,7 +44,7 @@ if (strtotime($currentMonthStart) === false) {
|
||||
$currentMonthEnd = date("Y-m-t", strtotime($currentMonthStart));
|
||||
|
||||
$sqlSchedule = "SELECT serviceschedule.*, service.servicename, service.colorclass,
|
||||
service.maxcapacity, service.idservice, service.idteacher
|
||||
service.maxcapacity, service.idservice
|
||||
FROM serviceschedule
|
||||
LEFT JOIN service ON serviceschedule.idservice = service.idservice
|
||||
WHERE serviceschedule.dateschedule BETWEEN :start AND DATE_ADD(:end, INTERVAL 1 DAY)
|
||||
@@ -61,16 +53,6 @@ $stmtSchedule = $pdo->prepare($sqlSchedule);
|
||||
$stmtSchedule->execute([':start' => $currentMonthStart, ':end' => $currentMonthEnd]);
|
||||
$scheduleRows = $stmtSchedule->fetchAll();
|
||||
|
||||
// Tipologie di classe presenti nel mese (per la barra filtri), con relativo colore
|
||||
$classTypes = [];
|
||||
foreach ($scheduleRows as $r) {
|
||||
$name = $r['servicename'] ?? '';
|
||||
if ($name !== '' && !isset($classTypes[$name])) {
|
||||
$classTypes[$name] = $r['colorclass'] ?: '#1ebf73';
|
||||
}
|
||||
}
|
||||
ksort($classTypes);
|
||||
|
||||
$bookingsBySchedule = [];
|
||||
$scheduleIds = array_filter(array_map(fn($r) => (int) $r['idserviceschedule'], $scheduleRows));
|
||||
|
||||
@@ -81,9 +63,9 @@ if (!empty($scheduleIds)) {
|
||||
auth_users.first_name, auth_users.last_name, auth_users.id AS iduser
|
||||
FROM bookingclass
|
||||
LEFT JOIN auth_users ON bookingclass.iduser = auth_users.id
|
||||
WHERE bookingclass.idserviceschedule IN ($ph)
|
||||
AND bookingclass.status IN ('booked', 'pending', 'waiting')
|
||||
ORDER BY bookingclass.created_at, auth_users.last_name, auth_users.first_name";
|
||||
WHERE bookingclass.idserviceschedule IN ($ph)
|
||||
AND bookingclass.status = 'booked'
|
||||
ORDER BY auth_users.last_name, auth_users.first_name";
|
||||
$stmtBookings = $pdo->prepare($sqlBookings);
|
||||
$stmtBookings->execute(array_values($scheduleIds));
|
||||
foreach ($stmtBookings->fetchAll() as $b) {
|
||||
@@ -132,46 +114,6 @@ $italianMonths = [
|
||||
margin: 10px 0 26px;
|
||||
}
|
||||
|
||||
.class-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin: 0 0 26px;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 7px 14px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
border-color: #1ebf73;
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: #1ebf73;
|
||||
border-color: #1ebf73;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.month-navigation h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
@@ -206,8 +148,8 @@ $italianMonths = [
|
||||
|
||||
.class-date {
|
||||
flex: 0 0 110px;
|
||||
background: #E1D0B8;
|
||||
color: #ffffff;
|
||||
background: #1ebf73;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -239,38 +181,6 @@ $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;
|
||||
@@ -562,7 +472,6 @@ $italianMonths = [
|
||||
'2' => ['success', 'Lezione ripristinata con successo.'],
|
||||
'3' => ['success', 'Lezione riprogrammata con successo.'],
|
||||
'4' => ['success', 'Classe eliminata con successo.'],
|
||||
'5' => ['success', 'Prenotazione confermata con successo.'],
|
||||
];
|
||||
$m = $msgMap[$_GET['success']] ?? null;
|
||||
if ($m) : ?>
|
||||
@@ -570,35 +479,12 @@ $italianMonths = [
|
||||
<?php endif;
|
||||
endif; ?>
|
||||
|
||||
<?php if (isset($_GET['error'])) :
|
||||
$errMap = [
|
||||
'duplicate' => 'Questo utente è già prenotato in questa classe.',
|
||||
'insert' => 'Errore durante l\'inserimento. Riprova.',
|
||||
'missingdata' => 'Dati mancanti per l\'inserimento.',
|
||||
];
|
||||
$em = $errMap[$_GET['error']] ?? 'Si è verificato un errore.';
|
||||
?>
|
||||
<div class="alert alert-danger" role="alert"><?php echo e($em); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="month-navigation">
|
||||
<a href="?prev_month=<?php echo e(date('Y-m', strtotime('-1 month', strtotime($currentMonthStart)))); ?>"><i class="fas fa-chevron-left fa-2x"></i></a>
|
||||
<h2><?php echo e($italianMonths[date("F", strtotime($currentMonthStart))] . ' ' . date("Y", strtotime($currentMonthStart))); ?></h2>
|
||||
<a href="?next_month=<?php echo e(date('Y-m', strtotime('+1 month', strtotime($currentMonthStart)))); ?>"><i class="fas fa-chevron-right fa-2x"></i></a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($classTypes)) : ?>
|
||||
<div class="class-filters">
|
||||
<button type="button" class="filter-chip active" data-filter="__all__">Tutte</button>
|
||||
<?php foreach ($classTypes as $typeName => $typeColor) : ?>
|
||||
<button type="button" class="filter-chip" data-filter="<?php echo e($typeName); ?>">
|
||||
<span class="filter-dot" style="background: <?php echo e($typeColor); ?>;"></span>
|
||||
<?php echo e($typeName); ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($scheduleRows)) : ?>
|
||||
<div class="empty-month">
|
||||
<i class="fas fa-calendar-xmark fa-2x mb-2"></i>
|
||||
@@ -611,61 +497,27 @@ $italianMonths = [
|
||||
$monthIt = $italianMonths[$dateObj->format("F")];
|
||||
$whenFmt = $dateObj->format("d-m-Y H:i");
|
||||
$scheduleId = (int) $sch['idserviceschedule'];
|
||||
$allRows = $bookingsBySchedule[$scheduleId] ?? [];
|
||||
// separa confermati (booked) da in attesa (pending)
|
||||
$bookings = array_filter($allRows, fn($b) => $b['status'] === 'booked');
|
||||
$pendings = array_filter($allRows, fn($b) => $b['status'] === 'pending');
|
||||
$waitings = array_filter($allRows, fn($b) => $b['status'] === 'waiting');
|
||||
$countPersons = count($bookings); // contatore = solo confermati
|
||||
$countPending = count($pendings);
|
||||
$countWaiting = count($waitings);
|
||||
$bookings = $bookingsBySchedule[$scheduleId] ?? [];
|
||||
$countPersons = count($bookings);
|
||||
$maxcap = (int) $sch['maxcapacity'];
|
||||
$isFull = $maxcap > 0 && $countPersons >= $maxcap;
|
||||
$colorclass = $sch['colorclass'] ?: '#e6f3ff';
|
||||
?>
|
||||
<div class="class-card" data-date="<?php echo e($dateObj->format('Y-m-d')); ?>" data-type="<?php echo e($sch['servicename']); ?>">
|
||||
<div class="class-card">
|
||||
<div class="class-date">
|
||||
<div class="day"><?php echo e($dayNum); ?></div>
|
||||
<div class="month"><?php echo e($monthIt); ?></div>
|
||||
</div>
|
||||
<div class="class-body" style="border-left: 20px solid <?php echo e($colorclass); ?>;">
|
||||
<div class="class-body" style="border-left: 6px solid <?php echo e($colorclass); ?>;">
|
||||
<h3 class="class-title">
|
||||
<?php echo e($sch['servicename']); ?>
|
||||
<span class="capacity-pill <?php echo $isFull ? 'full' : ''; ?>">
|
||||
<i class="fas fa-user-group" style="font-size:11px;"></i>
|
||||
<?php echo $countPersons; ?>/<?php echo $maxcap; ?>
|
||||
</span>
|
||||
<?php if ($countPending > 0) : ?>
|
||||
<span class="capacity-pill" style="background:#fff4d6;color:#8a6d00;">
|
||||
<i class="fas fa-hourglass-half" style="font-size:11px;"></i>
|
||||
<?php echo $countPending; ?> in attesa
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($countWaiting > 0) : ?>
|
||||
<span class="capacity-pill" style="background:#fbeef3;color:#d1698a;">
|
||||
<i class="fas fa-list-ol" style="font-size:11px;"></i>
|
||||
<?php echo $countWaiting; ?> in coda
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
@@ -690,7 +542,7 @@ $italianMonths = [
|
||||
$ini = initials($b['first_name'] ?? '', $b['last_name'] ?? '');
|
||||
$avColor = avatarColor($fullName);
|
||||
?>
|
||||
<div class="booking-item <?php echo $isLost ? 'is-lost' : ''; ?>" data-iduser="<?php echo (int) $b['iduser']; ?>">
|
||||
<div class="booking-item <?php echo $isLost ? 'is-lost' : ''; ?>">
|
||||
<div class="avatar" style="background: <?php echo $isLost ? '#7f8c8d' : $avColor; ?>;"><?php echo e($ini); ?></div>
|
||||
<div class="booking-info">
|
||||
<span class="booking-name"><?php echo e($fullName); ?></span>
|
||||
@@ -714,62 +566,6 @@ $italianMonths = [
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($countPending > 0) : ?>
|
||||
<div class="bookings-header" style="margin-top:18px;">
|
||||
<span class="title" style="color:#8a6d00;"><i class="fas fa-hourglass-half me-1"></i> In attesa di conferma (<?php echo $countPending; ?>)</span>
|
||||
</div>
|
||||
<div class="booking-list">
|
||||
<?php foreach ($pendings as $b) :
|
||||
$idbc = (int) $b['idbookingclass'];
|
||||
$fullName = trim($b['first_name'] . ' ' . $b['last_name']);
|
||||
$ini = initials($b['first_name'] ?? '', $b['last_name'] ?? '');
|
||||
$avColor = avatarColor($fullName);
|
||||
?>
|
||||
<div class="booking-item" style="background:#fffbf0;border-color:#f5e6b8;" data-iduser="<?php echo (int) $b['iduser']; ?>">
|
||||
<div class="avatar" style="background: <?php echo $avColor; ?>;"><?php echo e($ini); ?></div>
|
||||
<div class="booking-info">
|
||||
<span class="booking-name"><?php echo e($fullName); ?></span>
|
||||
<span class="status-chip" style="background:#fff4d6;color:#8a6d00;">In attesa</span>
|
||||
</div>
|
||||
<div class="booking-actions">
|
||||
<button class="mini-btn restore confirm-pending" data-idbookingclass="<?php echo $idbc; ?>"><i class="fas fa-check"></i> Conferma</button>
|
||||
<button class="mini-btn remove" onclick="handleRemoveClick(<?php echo $idbc; ?>)"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($countWaiting > 0) : ?>
|
||||
<div class="bookings-header" style="margin-top:18px;">
|
||||
<span class="title" style="color:#d1698a;"><i class="fas fa-list-ol me-1"></i> Lista d'attesa (<?php echo $countWaiting; ?>)</span>
|
||||
</div>
|
||||
<div class="booking-list">
|
||||
<?php $qpos = 1;
|
||||
foreach ($waitings as $b) :
|
||||
$idbc = (int) $b['idbookingclass'];
|
||||
$fullName = trim($b['first_name'] . ' ' . $b['last_name']);
|
||||
$ini = initials($b['first_name'] ?? '', $b['last_name'] ?? '');
|
||||
$avColor = avatarColor($fullName);
|
||||
?>
|
||||
<div class="booking-item" style="background:#fdf4f8;border-color:#f0d6e2;" data-iduser="<?php echo (int) $b['iduser']; ?>">
|
||||
<div class="avatar" style="background: <?php echo $avColor; ?>;"><?php echo e($ini); ?></div>
|
||||
<div class="booking-info">
|
||||
<span class="booking-name">
|
||||
<span style="display:inline-block;min-width:22px;font-weight:800;color:#d1698a;"><?php echo $qpos; ?>°</span>
|
||||
<?php echo e($fullName); ?>
|
||||
</span>
|
||||
<span class="status-chip" style="background:#fbeef3;color:#d1698a;">In coda</span>
|
||||
</div>
|
||||
<div class="booking-actions">
|
||||
<button class="mini-btn remove" onclick="handleRemoveClick(<?php echo $idbc; ?>)"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php $qpos++;
|
||||
endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="add-box">
|
||||
<strong class="d-block mb-2" style="font-size:14px;"><i class="fas fa-user-plus me-1" style="color:#1ebf73;"></i> Aggiungi partecipante</strong>
|
||||
<form class="add-booking-form" action="inserisci_record.php" method="post">
|
||||
@@ -848,42 +644,6 @@ $italianMonths = [
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// Filtro per tipologia di classe
|
||||
$(document).on('click', '.filter-chip', function() {
|
||||
var filter = $(this).data('filter');
|
||||
$('.filter-chip').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
$('.class-card').each(function() {
|
||||
var show = (filter === '__all__') || ($(this).data('type') === filter);
|
||||
$(this).toggle(show);
|
||||
});
|
||||
});
|
||||
|
||||
// Scroll automatico alla classe di oggi o alla prima futura
|
||||
(function scrollToToday() {
|
||||
var cards = document.querySelectorAll('.class-card[data-date]');
|
||||
if (!cards.length) return;
|
||||
var today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
var target = null;
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var d = new Date(cards[i].getAttribute('data-date') + 'T00:00:00');
|
||||
if (d >= today) {
|
||||
target = cards[i];
|
||||
break;
|
||||
}
|
||||
target = cards[i]; // se tutte passate, resta sull'ultima
|
||||
}
|
||||
if (target) {
|
||||
setTimeout(function() {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
})();
|
||||
|
||||
$(document).on('click', '.mini-btn.lost', function() {
|
||||
var id = $(this).data('idbookingclass');
|
||||
Swal.fire({
|
||||
@@ -901,7 +661,7 @@ $italianMonths = [
|
||||
}
|
||||
});
|
||||
});
|
||||
$(document).on('click', '.mini-btn.restore:not(.confirm-pending)', function() {
|
||||
$(document).on('click', '.mini-btn.restore', function() {
|
||||
var id = $(this).data('idbookingclass');
|
||||
Swal.fire({
|
||||
title: 'Ripristinare questa lezione?',
|
||||
@@ -918,25 +678,6 @@ $italianMonths = [
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.confirm-pending', function() {
|
||||
var id = $(this).data('idbookingclass');
|
||||
Swal.fire({
|
||||
title: 'Confermare la prenotazione?',
|
||||
text: 'La lezione passerà da "in attesa" a confermata.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Sì, conferma',
|
||||
cancelButtonText: 'Annulla'
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.isConfirmed) {
|
||||
$.post('conferma_pending.php', {
|
||||
idbookingclass: id
|
||||
}).done(() => window.location.href = 'adminpanel.php?success=5').fail(() => Swal.fire('Errore durante la conferma', '', 'error'));
|
||||
}
|
||||
});
|
||||
});
|
||||
$(document).on('click', '.mini-btn.reprogram', function() {
|
||||
var idbookingclass = $(this).data('idbookingclass');
|
||||
$.ajax({
|
||||
@@ -1014,163 +755,6 @@ $italianMonths = [
|
||||
});
|
||||
});
|
||||
|
||||
// === Intercetta submit "Aggiungi partecipante" ===
|
||||
$(document).on('submit', '.add-booking-form', function(e) {
|
||||
e.preventDefault();
|
||||
const form = $(this);
|
||||
const userid = form.find('.userid').val();
|
||||
const name = form.find('.name').val().trim();
|
||||
const surname = form.find('.surname').val().trim();
|
||||
|
||||
if (!name || !surname) {
|
||||
Swal.fire('Dati mancanti', 'Inserisci nome e cognome.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUser = userid && parseInt(userid) > 0;
|
||||
|
||||
// Controllo anti-duplicato PRIMA di aprire il modale:
|
||||
// l'utente è già presente in questa classe?
|
||||
if (hasUser) {
|
||||
const panel = form.closest('.bookings-panel');
|
||||
const already = panel.find('.booking-item[data-iduser="' + parseInt(userid) + '"]').length > 0;
|
||||
if (already) {
|
||||
Swal.fire(
|
||||
'Già prenotato',
|
||||
'Questa persona è già presente in questa classe.',
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUser) {
|
||||
$.getJSON('get_user_orders.php', {
|
||||
userid: userid
|
||||
}).done(function(data) {
|
||||
showBookingModal(form, data.orders || []);
|
||||
}).fail(function() {
|
||||
showBookingModal(form, []);
|
||||
});
|
||||
} else {
|
||||
showBookingModal(form, []);
|
||||
}
|
||||
});
|
||||
|
||||
function showBookingModal(form, orders) {
|
||||
const usableOrders = orders.filter(o => o.remaining > 0);
|
||||
let orderOptions = '';
|
||||
usableOrders.forEach(function(o) {
|
||||
const exp = o.expireon ? ' - scade ' + o.expireon : '';
|
||||
orderOptions += '<option value="' + o.idorderbook + '">' +
|
||||
o.service_name + ' (' + o.remaining + '/' + o.tickets + ' residui)' + exp +
|
||||
'</option>';
|
||||
});
|
||||
|
||||
const hasUsableOrders = usableOrders.length > 0;
|
||||
|
||||
const d = new Date();
|
||||
d.setMonth(d.getMonth() + 3);
|
||||
const defExpiry = d.toISOString().slice(0, 10);
|
||||
|
||||
const html =
|
||||
'<div style="text-align:left;">' +
|
||||
'<label style="display:block;margin:8px 0;">' +
|
||||
'<input type="radio" name="bkmode" value="scala" ' + (hasUsableOrders ? 'checked' : 'disabled') + '> ' +
|
||||
'<strong>Scala da un pacchetto</strong>' + (hasUsableOrders ? '' : ' (nessun pacchetto con residui)') +
|
||||
'</label>' +
|
||||
'<select id="bk-order" class="form-control" style="margin:6px 0 14px;" ' + (hasUsableOrders ? '' : 'disabled') + '>' +
|
||||
orderOptions +
|
||||
'</select>' +
|
||||
'<label style="display:block;margin:8px 0;">' +
|
||||
'<input type="radio" name="bkmode" value="omaggio" ' + (hasUsableOrders ? '' : 'checked') + '> ' +
|
||||
'<strong>Aggiungi come omaggio</strong> <span style="color:#6b7280;font-size:12px;">(non consuma pacchetti, non spostabile)</span>' +
|
||||
'</label>' +
|
||||
'<label style="display:block;margin:8px 0;">' +
|
||||
'<input type="radio" name="bkmode" value="nuovo"> ' +
|
||||
'<strong>Crea nuovo ordine</strong> <span style="color:#6b7280;font-size:12px;">(1 lezione)</span>' +
|
||||
'</label>' +
|
||||
'<div id="bk-nuovo-fields" style="display:none;margin:6px 0 0;padding-left:22px;">' +
|
||||
'<label style="font-size:13px;">Scadenza ordine:</label>' +
|
||||
'<input type="date" id="bk-expiry" class="form-control" value="' + defExpiry + '">' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
Swal.fire({
|
||||
title: 'Aggiungi partecipante',
|
||||
html: html,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Inserisci',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#1ebf73',
|
||||
didOpen: () => {
|
||||
document.querySelectorAll('input[name="bkmode"]').forEach(function(r) {
|
||||
r.addEventListener('change', function() {
|
||||
document.getElementById('bk-nuovo-fields').style.display =
|
||||
(this.value === 'nuovo') ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
},
|
||||
preConfirm: () => {
|
||||
const mode = document.querySelector('input[name="bkmode"]:checked');
|
||||
if (!mode) {
|
||||
Swal.showValidationMessage('Seleziona una modalità');
|
||||
return false;
|
||||
}
|
||||
const result = {
|
||||
mode: mode.value
|
||||
};
|
||||
if (mode.value === 'scala') {
|
||||
const sel = document.getElementById('bk-order').value;
|
||||
if (!sel) {
|
||||
Swal.showValidationMessage('Seleziona un pacchetto');
|
||||
return false;
|
||||
}
|
||||
result.idorder = sel;
|
||||
}
|
||||
if (mode.value === 'nuovo') {
|
||||
const exp = document.getElementById('bk-expiry').value;
|
||||
if (!exp) {
|
||||
Swal.showValidationMessage('Inserisci la scadenza');
|
||||
return false;
|
||||
}
|
||||
result.expiry = exp;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}).then((res) => {
|
||||
if (!res.isConfirmed) return;
|
||||
submitBooking(form, res.value);
|
||||
});
|
||||
}
|
||||
|
||||
function submitBooking(form, choice) {
|
||||
form.find('input[name="bkmode"]').remove();
|
||||
form.find('input[name="new_expiry"]').remove();
|
||||
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'bkmode',
|
||||
value: choice.mode
|
||||
}).appendTo(form);
|
||||
|
||||
if (choice.mode === 'scala') {
|
||||
form.find('input[name="idorder"]').val(choice.idorder);
|
||||
} else if (choice.mode === 'omaggio') {
|
||||
form.find('input[name="idorder"]').val('');
|
||||
} else if (choice.mode === 'nuovo') {
|
||||
form.find('input[name="idorder"]').val('');
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'new_expiry',
|
||||
value: choice.expiry
|
||||
}).appendTo(form);
|
||||
}
|
||||
|
||||
form.off('submit');
|
||||
form[0].submit();
|
||||
}
|
||||
|
||||
let searchTimeout;
|
||||
$(document).on('input', '.surname, .name', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once dirname(__DIR__) . '/class/db-functions.php';
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_me_role.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Restituisce il ruolo dell'utente autenticato e un flag comodo
|
||||
* "is_staff" (Admin o teacher) per abilitare le funzioni insegnante nell'app.
|
||||
*
|
||||
* Posizione: public/api/api_me_role.php
|
||||
* Metodo: GET
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$roleId = (int) $user->role_id;
|
||||
|
||||
// nome ruolo da auth_roles (per debug / display)
|
||||
$stmt = $db->prepare("SELECT name FROM auth_roles WHERE id = :rid LIMIT 1");
|
||||
$stmt->execute([':rid' => $roleId]);
|
||||
$roleRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$roleName = $roleRow ? (string) $roleRow['name'] : '';
|
||||
|
||||
// Admin = 1, teacher = 3 → staff (accesso pannello classi)
|
||||
$isStaff = in_array($roleId, [1, 3], true);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'role_id' => $roleId,
|
||||
'role_name' => $roleName,
|
||||
'is_staff' => $isStaff,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_medical_certificates_delete.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Elimina un certificato medico dell'utente autenticato (con ownership check).
|
||||
*
|
||||
* Posizione: public/api/api_medical_certificates_delete.php
|
||||
* Metodo: POST
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
*
|
||||
* Body:
|
||||
* - cert_id (int) id del certificato (idcertificateuserprofile)
|
||||
* accettato sia via form-urlencoded sia via JSON.
|
||||
*
|
||||
* Tabella: certificateuserprofile
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
$userId = (int) $user->id;
|
||||
|
||||
$uploadDir = __DIR__ . '/../user/document/';
|
||||
|
||||
// ==========================================================================
|
||||
// INPUT (JSON o form-urlencoded)
|
||||
// ==========================================================================
|
||||
$raw = file_get_contents('php://input');
|
||||
$json = json_decode($raw, true);
|
||||
if (is_array($json) && isset($json['cert_id'])) {
|
||||
$certId = (int) $json['cert_id'];
|
||||
} else {
|
||||
$certId = (int) ($_POST['cert_id'] ?? 0);
|
||||
}
|
||||
|
||||
if ($certId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Parametro cert_id mancante.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 1) Recupera il certificato e verifica ownership
|
||||
// ==========================================================================
|
||||
$stmt = $db->prepare("
|
||||
SELECT idcertificateuserprofile, filenamedocument
|
||||
FROM certificateuserprofile
|
||||
WHERE idcertificateuserprofile = :cid AND iduser = :uid
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([':cid' => $certId, ':uid' => $userId]);
|
||||
$cert = $stmt->fetch();
|
||||
|
||||
if (!$cert) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Certificato non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 2) Elimina record + file
|
||||
// ==========================================================================
|
||||
try {
|
||||
$del = $db->prepare("
|
||||
DELETE FROM certificateuserprofile
|
||||
WHERE idcertificateuserprofile = :cid AND iduser = :uid
|
||||
LIMIT 1
|
||||
");
|
||||
$del->execute([':cid' => $certId, ':uid' => $userId]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore durante la cancellazione.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rimuovi il file fisico (best effort)
|
||||
$filename = (string) ($cert['filenamedocument'] ?? '');
|
||||
if ($filename !== '') {
|
||||
$path = $uploadDir . $filename;
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// OUTPUT
|
||||
// ==========================================================================
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Certificato eliminato.',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_medical_certificates_list.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Restituisce i certificati medici dell'utente autenticato.
|
||||
*
|
||||
* Posizione: public/api/api_medical_certificates_list.php
|
||||
* Metodo: GET
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
*
|
||||
* Tabella: certificateuserprofile
|
||||
* idcertificateuserprofile, documentdescription, filenamedocument,
|
||||
* iduser, expirydatedocument, uploaded_at
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
$userId = (int) $user->id;
|
||||
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT idcertificateuserprofile, documentdescription, filenamedocument,
|
||||
expirydatedocument, uploaded_at
|
||||
FROM certificateuserprofile
|
||||
WHERE iduser = :uid
|
||||
ORDER BY uploaded_at DESC, idcertificateuserprofile DESC
|
||||
");
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$certificates = [];
|
||||
foreach ($rows as $r) {
|
||||
$filename = (string) ($r['filenamedocument'] ?? '');
|
||||
$expiry = $r['expirydatedocument'] ?? null;
|
||||
$isExpired = ($expiry !== null && $expiry !== '' && $expiry < $today);
|
||||
|
||||
$certificates[] = [
|
||||
'id' => (int) $r['idcertificateuserprofile'],
|
||||
'document_name' => (string) ($r['documentdescription'] ?? ''),
|
||||
'filename' => $filename,
|
||||
'uploaded_at' => $r['uploaded_at'] ?? null,
|
||||
'expiry_date' => $expiry,
|
||||
'is_expired' => $isExpired,
|
||||
// percorso file relativo (la webapp li serve da public/user/document/)
|
||||
'file_url' => $filename !== '' ? '/user/document/' . $filename : null,
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'certificates' => $certificates,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
@@ -1,137 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_medical_certificates_upload.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Carica un certificato medico per l'utente autenticato.
|
||||
*
|
||||
* Posizione: public/api/api_medical_certificates_upload.php
|
||||
* Metodo: POST (multipart/form-data)
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
*
|
||||
* Campi attesi (form-data):
|
||||
* - certificate (file) campo file (PDF/JPG/PNG, max 16MB)
|
||||
* - document_name (text) descrizione documento
|
||||
* - expiry_date (text) YYYY-MM-DD
|
||||
*
|
||||
* Logica ricalcata da certificate.php (webapp).
|
||||
* Tabella: certificateuserprofile
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
$userId = (int) $user->id;
|
||||
|
||||
// La cartella upload è public/user/document/. Questo file sta in public/api/,
|
||||
// quindi saliamo di un livello.
|
||||
$uploadDir = __DIR__ . '/../user/document/';
|
||||
|
||||
$allowedExt = ['pdf', 'jpg', 'jpeg', 'png'];
|
||||
$allowedMime = ['application/pdf', 'image/jpeg', 'image/png'];
|
||||
$maxBytes = 16 * 1024 * 1024; // 16 MB
|
||||
|
||||
// ==========================================================================
|
||||
// INPUT
|
||||
// ==========================================================================
|
||||
$documentName = trim($_POST['document_name'] ?? '');
|
||||
$expiryDate = trim($_POST['expiry_date'] ?? '');
|
||||
|
||||
if ($documentName === '') {
|
||||
$documentName = 'Certificato Medico';
|
||||
}
|
||||
|
||||
if ($expiryDate === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Data di scadenza obbligatoria.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validazione formato data YYYY-MM-DD
|
||||
$d = DateTime::createFromFormat('Y-m-d', $expiryDate);
|
||||
if ($d === false || $d->format('Y-m-d') !== $expiryDate) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Formato data non valido (YYYY-MM-DD).']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// File
|
||||
if (!isset($_FILES['certificate']) || $_FILES['certificate']['error'] !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Nessun file ricevuto o errore upload.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['certificate'];
|
||||
|
||||
if ($file['size'] > $maxBytes) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Il file supera i 16 MB.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$realMime = $finfo->file($file['tmp_name']);
|
||||
|
||||
if (!in_array($ext, $allowedExt, true) || !in_array($realMime, $allowedMime, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Formato non consentito. Usa PDF, JPG o PNG.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// SALVATAGGIO FILE
|
||||
// ==========================================================================
|
||||
if (!is_dir($uploadDir)) {
|
||||
@mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
|
||||
$destination = $uploadDir . $safeName;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Salvataggio del file non riuscito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// INSERT DB
|
||||
// ==========================================================================
|
||||
try {
|
||||
$sql = "INSERT INTO certificateuserprofile
|
||||
(iduser, documentdescription, filenamedocument, expirydatedocument, uploaded_at)
|
||||
VALUES (:iduser, :descr, :fname, :expiry, :uploaded)";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':iduser' => $userId,
|
||||
':descr' => $documentName,
|
||||
':fname' => $safeName,
|
||||
':expiry' => $expiryDate,
|
||||
':uploaded' => date('Y-m-d'),
|
||||
]);
|
||||
$newId = (int) $db->lastInsertId();
|
||||
} catch (Throwable $e) {
|
||||
// rollback file se l'insert fallisce
|
||||
@unlink($destination);
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio del documento.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// OUTPUT
|
||||
// ==========================================================================
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Documento caricato correttamente.',
|
||||
'certificate' => [
|
||||
'id' => $newId,
|
||||
'document_name' => $documentName,
|
||||
'filename' => $safeName,
|
||||
'uploaded_at' => date('Y-m-d'),
|
||||
'expiry_date' => $expiryDate,
|
||||
'is_expired' => false,
|
||||
'file_url' => '/user/document/' . $safeName,
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
@@ -1,255 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_add_booking.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Inserisce un partecipante in una classe. Solo staff (Admin=1 / teacher=3).
|
||||
* Replica inserisci_record.php della webapp (risposta JSON invece di redirect).
|
||||
*
|
||||
* Posizione: public/api/api_teacher_add_booking.php
|
||||
* Metodo: POST
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
*
|
||||
* Body:
|
||||
* idserviceschedule=<int> classe destinazione
|
||||
* idservice=<int> servizio della classe
|
||||
* bookingstart=<datetime> data/ora lezione (dateschedule)
|
||||
* bkmode=<scala|omaggio|nuovo>
|
||||
* user_id=<int> utente esistente (0 se nuovo)
|
||||
* name, surname, email per creare l'utente se user_id=0
|
||||
* idorder=<int> solo per 'scala'
|
||||
* new_expiry=<YYYY-MM-DD> solo per 'nuovo'
|
||||
*
|
||||
* Regole identiche alla webapp:
|
||||
* - status sempre 'booked'
|
||||
* - omaggio: idorder NULL, is_gift='Y'
|
||||
* - nuovo: crea orderbook (1 ticket, maxreschedule 0), is_gift='N'
|
||||
* - scala: aggancia ordine esistente (verifica proprietà + scadenza)
|
||||
* - idorder mai 0 (FK): NULL o id reale
|
||||
* - crea utente se user_id=0 (riusa email esistente)
|
||||
* - anti-duplicato: stesso utente già in questa classe
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Input ---
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$surname = trim($_POST['surname'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$userid = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
|
||||
$idserviceschedule = isset($_POST['idserviceschedule']) ? (int) $_POST['idserviceschedule'] : 0;
|
||||
$idservice = isset($_POST['idservice']) ? (int) $_POST['idservice'] : 0;
|
||||
$bookingstart = trim($_POST['bookingstart'] ?? '');
|
||||
$bkmode = trim($_POST['bkmode'] ?? 'scala'); // scala | omaggio | nuovo
|
||||
$idorderRaw = $_POST['idorder'] ?? '';
|
||||
$newExpiry = trim($_POST['new_expiry'] ?? '');
|
||||
|
||||
$status = 'booked'; // inserimento da insegnante: sempre booked
|
||||
|
||||
// --- Validazione minima ---
|
||||
if ($idserviceschedule <= 0 || $idservice <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Dati mancanti (classe/servizio)']);
|
||||
exit;
|
||||
}
|
||||
// Se nuovo utente, servono almeno nome e cognome
|
||||
if ($userid <= 0 && ($name === '' || $surname === '')) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Nome e cognome obbligatori per un nuovo utente']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 1) Risolvi / crea utente
|
||||
// ------------------------------------------------------------------
|
||||
if ($userid <= 0) {
|
||||
if ($email !== '') {
|
||||
$check = $db->prepare("SELECT id FROM auth_users WHERE email = :email LIMIT 1");
|
||||
$check->execute([':email' => $email]);
|
||||
$existing = $check->fetchColumn();
|
||||
if ($existing) {
|
||||
$userid = (int) $existing;
|
||||
}
|
||||
}
|
||||
|
||||
if ($userid <= 0) {
|
||||
$randomPassword = bin2hex(random_bytes(8));
|
||||
$hashed = password_hash($randomPassword, PASSWORD_BCRYPT);
|
||||
|
||||
$insUser = $db->prepare(
|
||||
"INSERT INTO auth_users
|
||||
(first_name, last_name, email, password, role_id, status, created_at, avatar)
|
||||
VALUES
|
||||
(:first, :last, :email, :password, :role, :status, :created, :avatar)"
|
||||
);
|
||||
$insUser->execute([
|
||||
':first' => $name,
|
||||
':last' => $surname,
|
||||
':email' => $email !== '' ? $email : null,
|
||||
':password' => $hashed,
|
||||
':role' => 2,
|
||||
':status' => 'Active',
|
||||
':created' => date('Y-m-d H:i:s'),
|
||||
':avatar' => 'mediationb.png',
|
||||
]);
|
||||
$userid = (int) $db->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2) Determina idorder e is_gift in base alla modalità
|
||||
// ------------------------------------------------------------------
|
||||
$idorder = null;
|
||||
$isGift = 'N';
|
||||
|
||||
if ($bkmode === 'omaggio') {
|
||||
$idorder = null;
|
||||
$isGift = 'Y';
|
||||
} elseif ($bkmode === 'nuovo') {
|
||||
if ($newExpiry === '') {
|
||||
throw new RuntimeException('Scadenza mancante per il nuovo ordine.');
|
||||
}
|
||||
|
||||
$uStmt = $db->prepare(
|
||||
"SELECT first_name, last_name, email FROM auth_users WHERE id = :uid LIMIT 1"
|
||||
);
|
||||
$uStmt->execute([':uid' => $userid]);
|
||||
$uRow = $uStmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$uEmail = $uRow['email'] ?? ($email !== '' ? $email : null);
|
||||
$uFirst = $uRow['first_name'] ?? $name;
|
||||
$uLast = $uRow['last_name'] ?? $surname;
|
||||
|
||||
$sStmt = $db->prepare(
|
||||
"SELECT servicename FROM service WHERE idservice = :sid LIMIT 1"
|
||||
);
|
||||
$sStmt->execute([':sid' => $idservice]);
|
||||
$serviceName = (string) ($sStmt->fetchColumn() ?: '');
|
||||
|
||||
$firstLessonDate = null;
|
||||
if ($bookingstart !== '') {
|
||||
try {
|
||||
$firstLessonDate = (new DateTime($bookingstart))->format('Y-m-d');
|
||||
} catch (Throwable $e) {
|
||||
$firstLessonDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
$insOrder = $db->prepare(
|
||||
"INSERT INTO orderbook
|
||||
(iduser, idservice, nticket, quantity, quantityclass,
|
||||
expireon, maxreschedule, reprogrammed, status,
|
||||
order_billing_email, cod, product_name,
|
||||
first_name, last_name, first_lesson_date, order_date_created)
|
||||
VALUES
|
||||
(:uid, :service, 1, 1, 1,
|
||||
:expire, 0, 0, 'booked',
|
||||
:bemail, :cod, :pname,
|
||||
:first, :last, :firstlesson, :created)"
|
||||
);
|
||||
$insOrder->execute([
|
||||
':uid' => $userid,
|
||||
':service' => $idservice,
|
||||
':expire' => $newExpiry,
|
||||
':bemail' => $uEmail,
|
||||
':cod' => $serviceName,
|
||||
':pname' => $serviceName,
|
||||
':first' => $uFirst,
|
||||
':last' => $uLast,
|
||||
':firstlesson' => $firstLessonDate,
|
||||
':created' => date('Y-m-d'),
|
||||
]);
|
||||
$idorder = (int) $db->lastInsertId();
|
||||
$isGift = 'N';
|
||||
} else {
|
||||
// 'scala'
|
||||
$idorder = ($idorderRaw !== '' && (int) $idorderRaw > 0) ? (int) $idorderRaw : null;
|
||||
|
||||
if ($idorder !== null) {
|
||||
$chk = $db->prepare(
|
||||
"SELECT expireon FROM orderbook
|
||||
WHERE idorderbook = :oid AND iduser = :uid LIMIT 1"
|
||||
);
|
||||
$chk->execute([':oid' => $idorder, ':uid' => $userid]);
|
||||
$row = $chk->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) {
|
||||
throw new RuntimeException('Ordine non valido per questo utente.');
|
||||
}
|
||||
$exp = $row['expireon'] ?? null;
|
||||
if ($exp !== null && $exp < date('Y-m-d')) {
|
||||
throw new RuntimeException('Il pacchetto selezionato è scaduto.');
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException('Nessun pacchetto selezionato.');
|
||||
}
|
||||
$isGift = 'N';
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2b) Anti-duplicato
|
||||
// ------------------------------------------------------------------
|
||||
$dup = $db->prepare(
|
||||
"SELECT status FROM bookingclass
|
||||
WHERE iduser = :uid
|
||||
AND idserviceschedule = :idschedule
|
||||
AND status != 'cancelled'
|
||||
LIMIT 1"
|
||||
);
|
||||
$dup->execute([':uid' => $userid, ':idschedule' => $idserviceschedule]);
|
||||
$dupStatus = $dup->fetchColumn();
|
||||
if ($dupStatus !== false) {
|
||||
$db->rollBack();
|
||||
http_response_code(409);
|
||||
$msg = ($dupStatus === 'pending')
|
||||
? 'Questo utente ha una prenotazione in attesa in questa classe: confermala o rimuovila dalla sezione "In attesa".'
|
||||
: 'Questo utente è già prenotato in questa classe.';
|
||||
echo json_encode(['success' => false, 'message' => $msg]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 3) Inserimento prenotazione
|
||||
// ------------------------------------------------------------------
|
||||
$insBooking = $db->prepare(
|
||||
"INSERT INTO bookingclass
|
||||
(iduser, idserviceschedule, status, idorder, idservice, bookingstart, is_gift)
|
||||
VALUES
|
||||
(:iduser, :idschedule, :status, :idorder, :idservice, :bookingstart, :isgift)"
|
||||
);
|
||||
$insBooking->execute([
|
||||
':iduser' => $userid,
|
||||
':idschedule' => $idserviceschedule,
|
||||
':status' => $status,
|
||||
':idorder' => $idorder,
|
||||
':idservice' => $idservice,
|
||||
':bookingstart' => $bookingstart !== '' ? $bookingstart : null,
|
||||
':isgift' => $isGift,
|
||||
]);
|
||||
|
||||
$db->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'user_id' => $userid,
|
||||
'is_gift' => $isGift,
|
||||
'idorder' => $idorder,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $ex) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
error_log('api_teacher_add_booking error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore inserimento: ' . $ex->getMessage()]);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_available_classes.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Classi schedulate candidate come destinazione di una riprogrammazione.
|
||||
* Solo staff (Admin=1 / teacher=3).
|
||||
*
|
||||
* Restituisce le classi FUTURE con posti liberi (non piene), escludendo
|
||||
* quelle dove l'allievo indicato è già prenotato.
|
||||
*
|
||||
* Posizione: public/api/api_teacher_available_classes.php
|
||||
* Metodo: GET
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* Query: ?booking_id=<int> (la prenotazione che si vuole spostare)
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$bookingId = isset($_GET['booking_id']) ? (int) $_GET['booking_id'] : 0;
|
||||
if ($bookingId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID prenotazione non valido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Trova utente e classe attuale della prenotazione da spostare
|
||||
$stmt = $db->prepare(
|
||||
"SELECT iduser, idserviceschedule
|
||||
FROM bookingclass
|
||||
WHERE idbookingclass = :id
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':id' => $bookingId]);
|
||||
$booking = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$booking) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Prenotazione non trovata']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$iduser = (int) $booking['iduser'];
|
||||
$currentSchedule = (int) $booking['idserviceschedule'];
|
||||
|
||||
// Classi future con conteggio prenotati (status='booked', coerente col pannello)
|
||||
$sql = "SELECT
|
||||
ss.idserviceschedule,
|
||||
ss.idservice,
|
||||
s.servicename,
|
||||
COALESCE(s.maxcapacity, 0) AS maxcapacity,
|
||||
ss.dateschedule,
|
||||
(SELECT COUNT(*) FROM bookingclass bc
|
||||
WHERE bc.idserviceschedule = ss.idserviceschedule
|
||||
AND bc.status = 'booked') AS booked_count,
|
||||
(SELECT COUNT(*) FROM bookingclass bc2
|
||||
WHERE bc2.idserviceschedule = ss.idserviceschedule
|
||||
AND bc2.iduser = :iduser
|
||||
AND bc2.status = 'booked') AS already_here
|
||||
FROM serviceschedule ss
|
||||
LEFT JOIN service s ON ss.idservice = s.idservice
|
||||
WHERE ss.dateschedule >= NOW()
|
||||
ORDER BY ss.dateschedule ASC";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([':iduser' => $iduser]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$classes = [];
|
||||
foreach ($rows as $r) {
|
||||
$sid = (int) $r['idserviceschedule'];
|
||||
$maxcap = (int) $r['maxcapacity'];
|
||||
$booked = (int) $r['booked_count'];
|
||||
$alreadyHere = ((int) $r['already_here']) > 0;
|
||||
$isFull = $maxcap > 0 && $booked >= $maxcap;
|
||||
|
||||
// Escludi: classe attuale, classi piene, classi dove è già iscritto
|
||||
if ($sid === $currentSchedule) continue;
|
||||
if ($isFull) continue;
|
||||
if ($alreadyHere) continue;
|
||||
|
||||
$classes[] = [
|
||||
'schedule_id' => $sid,
|
||||
'service_id' => (int) $r['idservice'],
|
||||
'class_name' => (string) ($r['servicename'] ?? ''),
|
||||
'date_time' => $r['dateschedule'],
|
||||
'max_capacity' => $maxcap,
|
||||
'booked_count' => $booked,
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'classes' => $classes,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('api_teacher_available_classes error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore recupero classi']);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_classes.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Classi programmate in un mese, CON i partecipanti di ciascuna.
|
||||
* Solo staff (Admin=1 / teacher=3). Sola lettura.
|
||||
*
|
||||
* Posizione: public/api/api_teacher_classes.php
|
||||
* Metodo: GET
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* Query: ?month=YYYY-MM (default: mese corrente)
|
||||
*
|
||||
* Replica la logica di adminpanel.php:
|
||||
* - schedule del mese (serviceschedule + service)
|
||||
* - bookings con status='booked' (le "perse" sono booked + lostlesson='Y')
|
||||
* - conteggio posti = tutti i booked (perse incluse)
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// --- Gate di sicurezza: solo staff ---
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Mese richiesto (default: corrente) ---
|
||||
$month = isset($_GET['month']) ? preg_replace('/[^0-9\-]/', '', $_GET['month']) : date('Y-m');
|
||||
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
|
||||
$month = date('Y-m');
|
||||
}
|
||||
$monthStart = $month . '-01';
|
||||
if (strtotime($monthStart) === false) {
|
||||
$monthStart = date('Y-m-01');
|
||||
}
|
||||
$monthEnd = date('Y-m-t', strtotime($monthStart));
|
||||
|
||||
// --- Classi del mese ---
|
||||
$sqlSchedule = "SELECT serviceschedule.idserviceschedule,
|
||||
serviceschedule.idservice,
|
||||
serviceschedule.dateschedule,
|
||||
service.servicename,
|
||||
service.colorclass,
|
||||
service.maxcapacity
|
||||
FROM serviceschedule
|
||||
LEFT JOIN service ON serviceschedule.idservice = service.idservice
|
||||
WHERE serviceschedule.dateschedule BETWEEN :start AND DATE_ADD(:end, INTERVAL 1 DAY)
|
||||
ORDER BY serviceschedule.dateschedule";
|
||||
$stmt = $db->prepare($sqlSchedule);
|
||||
$stmt->execute([':start' => $monthStart, ':end' => $monthEnd]);
|
||||
$scheduleRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- Bookings di tutte le classi del mese (una query sola) ---
|
||||
$bookingsBySchedule = [];
|
||||
$scheduleIds = array_filter(array_map(fn($r) => (int) $r['idserviceschedule'], $scheduleRows));
|
||||
|
||||
if (!empty($scheduleIds)) {
|
||||
$ph = implode(',', array_fill(0, count($scheduleIds), '?'));
|
||||
$sqlBookings = "SELECT bookingclass.idbookingclass,
|
||||
bookingclass.idserviceschedule,
|
||||
bookingclass.lostlesson,
|
||||
bookingclass.status,
|
||||
auth_users.first_name,
|
||||
auth_users.last_name,
|
||||
auth_users.avatar
|
||||
FROM bookingclass
|
||||
LEFT JOIN auth_users ON bookingclass.iduser = auth_users.id
|
||||
WHERE bookingclass.idserviceschedule IN ($ph)
|
||||
AND bookingclass.status IN ('booked', 'pending')
|
||||
ORDER BY auth_users.last_name, auth_users.first_name";
|
||||
$stmtB = $db->prepare($sqlBookings);
|
||||
$stmtB->execute(array_values($scheduleIds));
|
||||
foreach ($stmtB->fetchAll(PDO::FETCH_ASSOC) as $b) {
|
||||
$sid = (int) $b['idserviceschedule'];
|
||||
$first = trim((string) ($b['first_name'] ?? ''));
|
||||
$last = trim((string) ($b['last_name'] ?? ''));
|
||||
$avatar = trim((string) ($b['avatar'] ?? ''));
|
||||
|
||||
$bookingsBySchedule[$sid][] = [
|
||||
'booking_id' => (int) $b['idbookingclass'],
|
||||
'first_name' => $first,
|
||||
'last_name' => $last,
|
||||
'full_name' => trim($first . ' ' . $last),
|
||||
'is_lost' => ($b['lostlesson'] === 'Y'),
|
||||
'is_pending' => ($b['status'] === 'pending'),
|
||||
// avatar: solo il filename; l'app compone l'URL se serve
|
||||
'avatar' => $avatar !== '' ? $avatar : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Composizione risposta ---
|
||||
$classes = [];
|
||||
foreach ($scheduleRows as $sch) {
|
||||
$sid = (int) $sch['idserviceschedule'];
|
||||
$participants = $bookingsBySchedule[$sid] ?? [];
|
||||
// contatore: solo confermati (booked, perse incluse); i pending NON contano
|
||||
$count = 0;
|
||||
foreach ($participants as $pp) {
|
||||
if (empty($pp['is_pending'])) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
$maxcap = (int) $sch['maxcapacity'];
|
||||
|
||||
$classes[] = [
|
||||
'schedule_id' => $sid,
|
||||
'service_id' => (int) $sch['idservice'],
|
||||
'class_name' => (string) ($sch['servicename'] ?? ''),
|
||||
'color_class' => $sch['colorclass'] ?: null,
|
||||
'date_time' => $sch['dateschedule'], // "YYYY-MM-DD HH:MM:SS"
|
||||
'max_capacity' => $maxcap,
|
||||
'booked_count' => $count,
|
||||
'is_full' => $maxcap > 0 && $count >= $maxcap,
|
||||
'participants' => $participants,
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'month' => $month,
|
||||
'classes' => $classes,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_confirm_pending.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Conferma una prenotazione in attesa: pending -> booked.
|
||||
* Solo staff (Admin=1 / teacher=3).
|
||||
*
|
||||
* Posizione: public/api/api_teacher_confirm_pending.php
|
||||
* Metodo: POST
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* Body: booking_id=<int>
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$bookingId = isset($_POST['booking_id']) ? (int) $_POST['booking_id'] : 0;
|
||||
|
||||
if ($bookingId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID prenotazione non valido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Conferma solo se davvero in pending
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE bookingclass SET status = 'booked'
|
||||
WHERE idbookingclass = :id AND status = 'pending'"
|
||||
);
|
||||
$stmt->execute([':id' => $bookingId]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'booking_id' => $bookingId,
|
||||
'confirmed' => $stmt->rowCount(),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('api_teacher_confirm_pending error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore conferma']);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_delete_booking.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Elimina una prenotazione (record bookingclass). Libera il ticket:
|
||||
* l'allievo se lo ritrova disponibile. Solo staff (Admin=1 / teacher=3).
|
||||
*
|
||||
* Posizione: public/api/api_teacher_delete_booking.php
|
||||
* Metodo: POST
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* Body: booking_id=<int>
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$bookingId = isset($_POST['booking_id']) ? (int) $_POST['booking_id'] : 0;
|
||||
|
||||
if ($bookingId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID prenotazione non valido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare("DELETE FROM bookingclass WHERE idbookingclass = :id");
|
||||
$stmt->execute([':id' => $bookingId]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'booking_id' => $bookingId,
|
||||
'deleted' => $stmt->rowCount(),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('api_teacher_delete_booking error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore eliminazione']);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_mark_lost.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Segna una prenotazione come persa o la ripristina (lostlesson Y/N).
|
||||
* Solo staff (Admin=1 / teacher=3).
|
||||
*
|
||||
* Posizione: public/api/api_teacher_mark_lost.php
|
||||
* Metodo: POST
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* Body: booking_id=<int>&lost=<Y|N>
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$bookingId = isset($_POST['booking_id']) ? (int) $_POST['booking_id'] : 0;
|
||||
$lostRaw = isset($_POST['lost']) ? strtoupper(trim((string) $_POST['lost'])) : '';
|
||||
$lost = ($lostRaw === 'Y') ? 'Y' : 'N';
|
||||
|
||||
if ($bookingId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID prenotazione non valido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE bookingclass SET lostlesson = :lost WHERE idbookingclass = :id"
|
||||
);
|
||||
$stmt->execute([':lost' => $lost, ':id' => $bookingId]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'booking_id' => $bookingId,
|
||||
'lost' => $lost,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('api_teacher_mark_lost error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore aggiornamento']);
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_reprogram.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Sposta una prenotazione su una nuova classe schedulata.
|
||||
* Replica reprogramclass.php della webapp, con gate staff.
|
||||
* Solo staff (Admin=1 / teacher=3).
|
||||
*
|
||||
* Posizione: public/api/api_teacher_reprogram.php
|
||||
* Metodo: POST
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* Body: booking_id=<int>&schedule_id=<int>&is_reprogrammed=<Y|N>
|
||||
*
|
||||
* Regole (identiche alla webapp):
|
||||
* - status resta 'booked', lostlesson non toccato
|
||||
* - is_reprogrammed sulla prenotazione va sempre a 'Y'
|
||||
* - orderbook.reprogrammed +1 solo se flag='Y' e idorder valido
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$bookingId = isset($_POST['booking_id']) ? (int) $_POST['booking_id'] : 0;
|
||||
$newSchedId = isset($_POST['schedule_id']) ? (int) $_POST['schedule_id'] : 0;
|
||||
$flagReprogrammed =
|
||||
(isset($_POST['is_reprogrammed']) && $_POST['is_reprogrammed'] === 'Y') ? 'Y' : 'N';
|
||||
|
||||
if ($bookingId <= 0 || $newSchedId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Dati mancanti']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// Prenotazione corrente (lock)
|
||||
$stmt = $db->prepare(
|
||||
"SELECT idbookingclass, idserviceschedule, bookingstart, idorder, iduser
|
||||
FROM bookingclass
|
||||
WHERE idbookingclass = :id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$stmt->execute([':id' => $bookingId]);
|
||||
$booking = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$booking) {
|
||||
$db->rollBack();
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Prenotazione non trovata']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Classe destinazione
|
||||
$stmt = $db->prepare(
|
||||
"SELECT idserviceschedule, idservice, dateschedule
|
||||
FROM serviceschedule
|
||||
WHERE idserviceschedule = :id"
|
||||
);
|
||||
$stmt->execute([':id' => $newSchedId]);
|
||||
$newSchedule = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$newSchedule) {
|
||||
$db->rollBack();
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Classe destinazione non trovata']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Stessa classe: niente da fare
|
||||
if ((int) $booking['idserviceschedule'] === $newSchedId) {
|
||||
$db->rollBack();
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Classe destinazione uguale a quella attuale']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Anti-duplicato: già prenotato (attivo) nella destinazione?
|
||||
$dup = $db->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM bookingclass
|
||||
WHERE idserviceschedule = :newsched
|
||||
AND iduser = :iduser
|
||||
AND status = 'booked'
|
||||
AND idbookingclass <> :id"
|
||||
);
|
||||
$dup->execute([
|
||||
':newsched' => $newSchedId,
|
||||
':iduser' => (int) $booking['iduser'],
|
||||
':id' => $bookingId,
|
||||
]);
|
||||
if ((int) $dup->fetchColumn() > 0) {
|
||||
$db->rollBack();
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => 'Allievo già prenotato in questa classe']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Valori precedenti da conservare
|
||||
$prevSchedule = $booking['idserviceschedule'];
|
||||
$prevStart = $booking['bookingstart'];
|
||||
|
||||
// Aggiorna in place
|
||||
$upd = $db->prepare(
|
||||
"UPDATE bookingclass SET
|
||||
idserviceschedule = :newsched,
|
||||
idservice = :newservice,
|
||||
bookingstart = :newstart,
|
||||
idprevserviceschedule = :prevsched,
|
||||
prevbookingstart = :prevstart,
|
||||
is_reprogrammed = 'Y',
|
||||
status = 'booked'
|
||||
WHERE idbookingclass = :id"
|
||||
);
|
||||
$upd->execute([
|
||||
':newsched' => (int) $newSchedule['idserviceschedule'],
|
||||
':newservice' => (int) $newSchedule['idservice'],
|
||||
':newstart' => $newSchedule['dateschedule'],
|
||||
':prevsched' => $prevSchedule,
|
||||
':prevstart' => $prevStart,
|
||||
':id' => $bookingId,
|
||||
]);
|
||||
|
||||
// Incrementa contatore riprogrammazioni se richiesto e possibile
|
||||
$idorder = $booking['idorder'] !== null ? (int) $booking['idorder'] : 0;
|
||||
if ($flagReprogrammed === 'Y' && $idorder > 0) {
|
||||
$incr = $db->prepare(
|
||||
"UPDATE orderbook
|
||||
SET reprogrammed = reprogrammed + 1
|
||||
WHERE idorderbook = :idorder"
|
||||
);
|
||||
$incr->execute([':idorder' => $idorder]);
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'booking_id' => $bookingId,
|
||||
'schedule_id' => $newSchedId,
|
||||
'reprogrammed_counted' => ($flagReprogrammed === 'Y' && $idorder > 0),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $ex) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
error_log('api_teacher_reprogram error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore riprogrammazione']);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_search_users.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Ricerca utenti per nome e/o cognome (OR). Solo staff (Admin=1 / teacher=3).
|
||||
* Replica searchemail.php della webapp.
|
||||
*
|
||||
* Posizione: public/api/api_teacher_search_users.php
|
||||
* Metodo: GET
|
||||
* Auth: Bearer token (Sanctum) via _bootstrap.php
|
||||
* Query: ?first=<str>&last=<str> (min 2 caratteri in almeno uno)
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$firstName = trim($_GET['first'] ?? '');
|
||||
$lastName = trim($_GET['last'] ?? '');
|
||||
|
||||
// Serve almeno 2 caratteri in uno dei due campi
|
||||
if (mb_strlen($firstName) < 2 && mb_strlen($lastName) < 2) {
|
||||
echo json_encode(['success' => true, 'results' => []], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if (mb_strlen($firstName) >= 2) {
|
||||
$conditions[] = "first_name LIKE :fn";
|
||||
$params[':fn'] = '%' . $firstName . '%';
|
||||
}
|
||||
if (mb_strlen($lastName) >= 2) {
|
||||
$conditions[] = "last_name LIKE :ln";
|
||||
$params[':ln'] = '%' . $lastName . '%';
|
||||
}
|
||||
|
||||
$where = implode(' OR ', $conditions);
|
||||
|
||||
$sql = "SELECT id, first_name, last_name, email
|
||||
FROM auth_users
|
||||
WHERE $where
|
||||
ORDER BY last_name, first_name
|
||||
LIMIT 15";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$results = [];
|
||||
foreach ($rows as $r) {
|
||||
$results[] = [
|
||||
'id' => (int) $r['id'],
|
||||
'first_name' => (string) ($r['first_name'] ?? ''),
|
||||
'last_name' => (string) ($r['last_name'] ?? ''),
|
||||
'email' => (string) ($r['email'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'results' => $results], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('api_teacher_search_users error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore ricerca']);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* api_teacher_user_orders.php
|
||||
* --------------------------------------------------------------------------
|
||||
* Ordini attivi (non scaduti) di un utente, con ticket residui.
|
||||
* Solo staff (Admin=1 / teacher=3). Replica get_user_orders.php.
|
||||
*
|
||||
* Metodo: GET | Auth: Bearer (Sanctum) via _bootstrap.php | Query: ?user_id=<int>
|
||||
* Residui = ticket - (prenotazioni non cancellate e NON omaggio).
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
// Gate staff
|
||||
$roleId = (int) $user->role_id;
|
||||
if (!in_array($roleId, [1, 3], true)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = isset($_GET['user_id']) ? (int) $_GET['user_id'] : 0;
|
||||
if ($userId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Utente non valido', 'orders' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$sql = "SELECT ob.idorderbook, ob.idservice, ob.nticket, ob.quantityclass,
|
||||
ob.expireon, ob.maxreschedule, ob.reprogrammed,
|
||||
s.servicename
|
||||
FROM orderbook ob
|
||||
LEFT JOIN service s ON ob.idservice = s.idservice
|
||||
WHERE ob.iduser = :uid
|
||||
AND (ob.expireon IS NULL OR ob.expireon >= :today)
|
||||
ORDER BY ob.idorderbook DESC";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([':uid' => $userId, ':today' => $today]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// conta le prenotazioni che consumano ticket (non cancellate, non omaggio)
|
||||
$usedStmt = $db->prepare(
|
||||
"SELECT COUNT(*) AS n
|
||||
FROM bookingclass
|
||||
WHERE idorder = :oid
|
||||
AND status != 'cancelled'
|
||||
AND is_gift = 'N'"
|
||||
);
|
||||
|
||||
$orders = [];
|
||||
foreach ($rows as $o) {
|
||||
$orderId = (int) $o['idorderbook'];
|
||||
|
||||
$tickets = (int) (($o['quantityclass'] !== null && $o['quantityclass'] !== '')
|
||||
? $o['quantityclass']
|
||||
: ($o['nticket'] ?? 0));
|
||||
|
||||
$usedStmt->execute([':oid' => $orderId]);
|
||||
$used = (int) ($usedStmt->fetch(PDO::FETCH_ASSOC)['n'] ?? 0);
|
||||
|
||||
$remaining = $tickets - $used;
|
||||
if ($remaining < 0) {
|
||||
$remaining = 0;
|
||||
}
|
||||
|
||||
$orders[] = [
|
||||
'idorderbook' => $orderId,
|
||||
'service_name' => (string) ($o['servicename'] ?? ''),
|
||||
'tickets' => $tickets,
|
||||
'used' => $used,
|
||||
'remaining' => $remaining,
|
||||
'expireon' => $o['expireon'] ?? null,
|
||||
'maxreschedule' => (int) ($o['maxreschedule'] ?? 0),
|
||||
'reprogrammed' => (int) ($o['reprogrammed'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'orders' => $orders], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('api_teacher_user_orders error: ' . $ex->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Errore ordini: ' . $ex->getMessage(), 'orders' => []]);
|
||||
}
|
||||
@@ -173,7 +173,7 @@ try {
|
||||
(idserviceschedule, iduser, prevbookingstart, idprevserviceschedule,
|
||||
idservice, idorder, bookingstart, status)
|
||||
VALUES
|
||||
(:sched, :uid, NULL, 0, :service, :order, :newstart, 'pending')
|
||||
(:sched, :uid, '0', '0', :service, :order, :newstart, 'pending')
|
||||
");
|
||||
$ins->execute([
|
||||
':sched' => $newScheduleId,
|
||||
|
||||
@@ -88,7 +88,6 @@ $stmt = $db->prepare("
|
||||
ON bc.idserviceschedule = ss.idserviceschedule
|
||||
WHERE bc.iduser = :uid
|
||||
AND bc.status != 'cancelled'
|
||||
AND bc.is_gift = 'N'
|
||||
");
|
||||
$stmt->execute([
|
||||
':now1' => $now,
|
||||
@@ -129,7 +128,6 @@ $stmt = $db->prepare("
|
||||
bc.idservice,
|
||||
bc.idorder,
|
||||
bc.lostlesson,
|
||||
bc.is_gift,
|
||||
ss.dateschedule,
|
||||
s.servicename,
|
||||
s.colorclass,
|
||||
@@ -194,21 +192,13 @@ foreach ($rows as $r) {
|
||||
$reprogrammed = (int) ($r['reprogrammed'] ?? 0);
|
||||
$canReprogram = $reprogrammed < $maxreschedule;
|
||||
|
||||
$isGift = ($r['is_gift'] ?? 'N') === 'Y';
|
||||
|
||||
if ($isGift) {
|
||||
// Omaggio: mai riprogrammabile, cancellabile in base al tempo.
|
||||
$canReschedule = false;
|
||||
$canDelete = $canByTime;
|
||||
} else {
|
||||
// Lezione normale: entrambi soggetti a tempo + limite riprogrammazioni.
|
||||
$canReschedule = $canByTime && $canReprogram;
|
||||
$canDelete = $canByTime && $canReprogram;
|
||||
}
|
||||
// Nella webapp i bottoni "Riprogramma" e "Cancella" compaiono entrambi
|
||||
// solo se ($canByTime && $canReprogram). Riproduciamo la stessa condizione.
|
||||
$canReschedule = $canByTime && $canReprogram;
|
||||
$canDelete = $canByTime && $canReprogram;
|
||||
|
||||
$lessons[] = [
|
||||
'booking_id' => (int) $r['idbookingclass'],
|
||||
'service_id' => (int) ($r['idservice'] ?? 0),
|
||||
'datetime' => $classDt->format('Y-m-d H:i:s'),
|
||||
'date' => $classDt->format('Y-m-d'),
|
||||
'time' => $classDt->format('H:i'),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$userId = (int) $user->id;
|
||||
$metodo = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($metodo === 'POST' || $metodo === 'PUT' || $metodo === 'PATCH') {
|
||||
$input = json_decode((string) file_get_contents('php://input'), true);
|
||||
if (!is_array($input) || !array_key_exists('enabled', $input)) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'Parametro enabled mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$valore = $input['enabled'];
|
||||
|
||||
if (is_bool($valore)) {
|
||||
$nuovo = $valore ? 'Y' : 'N';
|
||||
} else {
|
||||
$nuovo = strtoupper(trim((string) $valore)) === 'Y' ? 'Y' : 'N';
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE auth_users SET pushnotification = :valore WHERE id = :uid");
|
||||
$stmt->execute([':valore' => $nuovo, ':uid' => $userId]);
|
||||
|
||||
echo json_encode(['success' => true, 'push_enabled' => $nuovo]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT pushnotification FROM auth_users WHERE id = :uid LIMIT 1");
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'push_enabled' => (string) ($stmt->fetchColumn() ?: 'Y'),
|
||||
]);
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$userId = (int) $user->id;
|
||||
$metodo = $_SERVER['REQUEST_METHOD'] ?? 'POST';
|
||||
|
||||
$input = json_decode((string) file_get_contents('php://input'), true);
|
||||
if (!is_array($input)) {
|
||||
$input = [];
|
||||
}
|
||||
|
||||
$deviceToken = trim((string) ($input['device_token'] ?? ''));
|
||||
|
||||
if ($deviceToken === '') {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'error' => 'device_token mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($metodo === 'DELETE') {
|
||||
$stmt = $db->prepare("
|
||||
UPDATE userdevicetoken
|
||||
SET active = 'N'
|
||||
WHERE devicetoken = :token AND iduser = :uid
|
||||
");
|
||||
$stmt->execute([':token' => $deviceToken, ':uid' => $userId]);
|
||||
|
||||
echo json_encode(['success' => true, 'deactivated' => $stmt->rowCount()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$platform = strtolower(trim((string) ($input['platform'] ?? 'android')));
|
||||
if (!in_array($platform, ['android', 'ios'], true)) {
|
||||
$platform = 'android';
|
||||
}
|
||||
|
||||
$appVersion = trim((string) ($input['app_version'] ?? ''));
|
||||
if ($appVersion === '') {
|
||||
$appVersion = null;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO userdevicetoken (iduser, devicetoken, platform, appversion, active, lastseen_at)
|
||||
VALUES (:uid, :token, :platform, :versione, 'Y', NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
iduser = VALUES(iduser),
|
||||
platform = VALUES(platform),
|
||||
appversion = COALESCE(VALUES(appversion), appversion),
|
||||
active = 'Y',
|
||||
lastseen_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':uid' => $userId,
|
||||
':token' => $deviceToken,
|
||||
':platform' => $platform,
|
||||
':versione' => $appVersion,
|
||||
]);
|
||||
|
||||
$stmt = $db->prepare("SELECT pushnotification FROM auth_users WHERE id = :uid LIMIT 1");
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
$pushEnabled = (string) ($stmt->fetchColumn() ?: 'Y');
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'push_enabled' => $pushEnabled,
|
||||
]);
|
||||
@@ -1,189 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* Endpoint pubblico PRENOTAZIONE (POST).
|
||||
* Riceve i dati dal widget WordPress e crea la prenotazione.
|
||||
*
|
||||
* Campi attesi (POST form-encoded o JSON):
|
||||
* event_id, name, surname, email, seats, consent (1), marketing (1|0)
|
||||
* website -> honeypot anti-spam (deve restare vuoto)
|
||||
*
|
||||
* Logica posti (conferma immediata):
|
||||
* - seats <= posti rimanenti -> status 'confirmed'
|
||||
* - evento pieno + lista d'attesa ON -> status 'waiting'
|
||||
* - evento pieno + lista d'attesa OFF -> errore sold_out
|
||||
* - 0 < rimanenti < seats -> errore 'partial' (avvisa: solo N posti)
|
||||
*
|
||||
* Anti-oversell: transazione + lock della riga evento (SELECT ... FOR UPDATE),
|
||||
* così due prenotazioni simultanee non sforano la capienza.
|
||||
*/
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
send_json(['ok' => false, 'code' => 'method', 'error' => 'Metodo non consentito.'], 405);
|
||||
}
|
||||
|
||||
// Input: form-encoded oppure JSON
|
||||
$in = $_POST;
|
||||
if (empty($in)) {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw) {
|
||||
$j = json_decode($raw, true);
|
||||
if (is_array($j)) {
|
||||
$in = $j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Honeypot: se compilato, è un bot
|
||||
if (!empty($in['website'])) {
|
||||
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Richiesta non valida.'], 400);
|
||||
}
|
||||
|
||||
$eventId = (int) ($in['event_id'] ?? 0);
|
||||
$name = trim((string) ($in['name'] ?? ''));
|
||||
$surname = trim((string) ($in['surname'] ?? ''));
|
||||
$email = trim((string) ($in['email'] ?? ''));
|
||||
$seats = (int) ($in['seats'] ?? 0);
|
||||
$consent = !empty($in['consent']) && $in['consent'] !== '0';
|
||||
$marketing = (!empty($in['marketing']) && $in['marketing'] !== '0') ? 'Y' : 'N';
|
||||
|
||||
// Validazioni
|
||||
if ($eventId <= 0 || $name === '' || $surname === '') {
|
||||
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Compila nome, cognome ed evento.'], 400);
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Email non valida.'], 400);
|
||||
}
|
||||
if (!$consent) {
|
||||
send_json(['ok' => false, 'code' => 'consent', 'error' => "Devi accettare l'informativa privacy."], 400);
|
||||
}
|
||||
if ($seats < 1) {
|
||||
$seats = 1;
|
||||
}
|
||||
|
||||
$pdo = pdo_conn();
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Lock della riga evento: serializza le prenotazioni dello stesso evento
|
||||
$es = $pdo->prepare("SELECT * FROM events WHERE id = ? FOR UPDATE");
|
||||
$es->execute([$eventId]);
|
||||
$event = $es->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$event || $event['status'] !== 'published') {
|
||||
$pdo->rollBack();
|
||||
send_json(['ok' => false, 'code' => 'not_available', 'error' => 'Evento non disponibile.'], 404);
|
||||
}
|
||||
|
||||
// Prenotazioni chiuse all'orario di inizio
|
||||
if (strtotime($event['starts_at']) <= time()) {
|
||||
$pdo->rollBack();
|
||||
send_json(['ok' => false, 'code' => 'closed', 'error' => 'Le prenotazioni per questo evento sono chiuse.'], 409);
|
||||
}
|
||||
|
||||
$maxSeats = (int) $event['max_seats'];
|
||||
$maxPerBooking = max(1, (int) $event['max_per_booking']);
|
||||
$waitlistOn = ($event['waitlist_enabled'] ?? 'N') === 'Y';
|
||||
|
||||
if ($seats > $maxPerBooking) {
|
||||
$pdo->rollBack();
|
||||
send_json([
|
||||
'ok' => false,
|
||||
'code' => 'too_many',
|
||||
'max' => $maxPerBooking,
|
||||
'error' => "Puoi prenotare al massimo {$maxPerBooking} posti per prenotazione."
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Posti occupati (solo confermate/pending): protetto dal lock evento
|
||||
$os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings
|
||||
WHERE event_id = ? AND status IN ('confirmed','pending','offered')");
|
||||
$os->execute([$eventId]);
|
||||
$occupied = (int) $os->fetchColumn();
|
||||
$remaining = max(0, $maxSeats - $occupied);
|
||||
|
||||
// Decisione stato
|
||||
if ($seats <= $remaining) {
|
||||
$status = 'confirmed';
|
||||
} elseif ($remaining <= 0 && $waitlistOn) {
|
||||
$status = 'waiting';
|
||||
} elseif ($remaining <= 0 && !$waitlistOn) {
|
||||
$pdo->rollBack();
|
||||
send_json(['ok' => false, 'code' => 'sold_out', 'error' => 'Evento esaurito.'], 409);
|
||||
} else {
|
||||
// 0 < remaining < seats: avvisa, non prenota
|
||||
$pdo->rollBack();
|
||||
send_json([
|
||||
'ok' => false,
|
||||
'code' => 'partial',
|
||||
'remaining' => $remaining,
|
||||
'error' => "Sono rimasti solo {$remaining} posti. Riduci il numero di persone; per gli altri potrai iscriverti alla lista d'attesa.",
|
||||
], 409);
|
||||
}
|
||||
|
||||
// Token univoco per gestione/disdetta da link email
|
||||
$token = '';
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$candidate = bin2hex(random_bytes(16));
|
||||
$ck = $pdo->prepare("SELECT 1 FROM event_bookings WHERE token = ? LIMIT 1");
|
||||
$ck->execute([$candidate]);
|
||||
if (!$ck->fetchColumn()) {
|
||||
$token = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('Impossibile generare il token.');
|
||||
}
|
||||
|
||||
$amount = round($seats * (float) $event['cost'], 2);
|
||||
$fullName = trim($name . ' ' . $surname);
|
||||
|
||||
$ins = $pdo->prepare("INSERT INTO event_bookings
|
||||
(event_id, user_id, name, email, phone, seats, status, amount, token,
|
||||
consent_at, marketing_opt_in, created_at, updated_at)
|
||||
VALUES
|
||||
(:event_id, NULL, :name, :email, NULL, :seats, :status, :amount, :token,
|
||||
NOW(), :marketing, NOW(), NOW())");
|
||||
$ins->execute([
|
||||
':event_id' => $eventId,
|
||||
':name' => $fullName,
|
||||
':email' => $email,
|
||||
':seats' => $seats,
|
||||
':status' => $status,
|
||||
':amount' => $amount,
|
||||
':token' => $token,
|
||||
':marketing' => $marketing,
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Email di conferma / lista d'attesa (best-effort: non blocca la prenotazione)
|
||||
try {
|
||||
$bookingRow = ['email' => $email, 'name' => $fullName, 'seats' => $seats, 'status' => $status, 'token' => $token];
|
||||
yse_mail_booking($bookingRow, $event);
|
||||
} catch (Throwable $mailEx) {
|
||||
error_log('[book mail] ' . $mailEx->getMessage());
|
||||
}
|
||||
|
||||
$msg = $status === 'confirmed'
|
||||
? 'Prenotazione confermata! Ti abbiamo inviato una email di riepilogo.'
|
||||
: "Sei in lista d'attesa: ti avviseremo appena si libera un posto.";
|
||||
|
||||
send_json([
|
||||
'ok' => true,
|
||||
'status' => $status,
|
||||
'seats' => $seats,
|
||||
'amount' => $amount,
|
||||
'token' => $token,
|
||||
'message' => $msg,
|
||||
]);
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[apiwp/book] ' . $ex->getMessage());
|
||||
send_json(['ok' => false, 'code' => 'server', 'error' => 'Errore durante la prenotazione. Riprova.'], 500);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Bootstrap per gli endpoint PUBBLICI del modulo Eventi (cartella /api).
|
||||
* NON include headscript.php: niente login/redirect. Serve solo il DB.
|
||||
*
|
||||
* Posiziona la cartella /api allo stesso livello di events.php, class/, include/.
|
||||
* Requisito: class/db-functions.php (DBHandlerSelect) già presente e autonomo.
|
||||
*/
|
||||
|
||||
// Path assoluto a DBHandlerSelect (api/ -> ../class/db-functions.php)
|
||||
require_once dirname(__DIR__) . '/class/db-functions.php';
|
||||
|
||||
// URL pubblico di yogibook: usato per rendere assoluti i path delle immagini.
|
||||
if (!defined('YB_PUBLIC_BASE')) {
|
||||
define('YB_PUBLIC_BASE', 'https://yogibook.yogasoul.it/');
|
||||
}
|
||||
|
||||
// Domini WordPress autorizzati (per la prenotazione via browser).
|
||||
$YB_ALLOWED_ORIGINS = ['https://yogasoul.it', 'https://www.yogasoul.it'];
|
||||
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if ($origin !== '' && in_array($origin, $YB_ALLOWED_ORIGINS, true)) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Vary: Origin');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
}
|
||||
|
||||
// Preflight CORS
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit();
|
||||
}
|
||||
|
||||
function pdo_conn(): PDO
|
||||
{
|
||||
return DBHandlerSelect::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
/** Rende assoluto un path immagine relativo (es. assets/uploads/events/x.jpg). */
|
||||
function public_url(?string $rel): ?string
|
||||
{
|
||||
if ($rel === null || $rel === '') {
|
||||
return null;
|
||||
}
|
||||
if (preg_match('#^https?://#i', $rel)) {
|
||||
return $rel;
|
||||
}
|
||||
return YB_PUBLIC_BASE . ltrim($rel, '/');
|
||||
}
|
||||
|
||||
/** Risponde in JSON e termina. */
|
||||
function send_json($data, int $code = 200): void
|
||||
{
|
||||
if (function_exists('ob_get_level') && ob_get_level()) {
|
||||
ob_clean();
|
||||
}
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit();
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_waitlist.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* Disdetta pubblica di una prenotazione tramite token (link email, senza login).
|
||||
*
|
||||
* GET apiwp/cancel.php?token=... -> mostra pagina di conferma (NON annulla:
|
||||
* evita che gli scanner email prefetchino il link e annullino da soli)
|
||||
* POST (token + confirm=1) -> annulla e, se la prenotazione occupava posti,
|
||||
* promuove automaticamente la prima in lista d'attesa.
|
||||
*/
|
||||
$pdo = pdo_conn();
|
||||
$token = trim($_POST['token'] ?? $_GET['token'] ?? '');
|
||||
$isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
|
||||
|
||||
$booking = null;
|
||||
if ($token !== '') {
|
||||
$q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid
|
||||
FROM event_bookings b JOIN events e ON e.id = b.event_id
|
||||
WHERE b.token = ?");
|
||||
$q->execute([$token]);
|
||||
$booking = $q->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$state = 'form'; // form | notfound | already | toolate | done
|
||||
$promotedCount = 0;
|
||||
$didCancel = false;
|
||||
$promoted = [];
|
||||
|
||||
if (!$booking) {
|
||||
$state = 'notfound';
|
||||
} elseif ($booking['status'] === 'cancelled') {
|
||||
$state = 'already';
|
||||
} elseif (strtotime($booking['ev_starts']) <= time()) {
|
||||
$state = 'toolate';
|
||||
} elseif ($isPost && !empty($_POST['confirm'])) {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
// Lock evento per serializzare con eventuali prenotazioni concorrenti
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $booking['eid']]);
|
||||
|
||||
// Rileggo lo stato aggiornato della prenotazione
|
||||
$bl = $pdo->prepare("SELECT * FROM event_bookings WHERE id = ?");
|
||||
$bl->execute([(int) $booking['id']]);
|
||||
$fresh = $bl->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($fresh && $fresh['status'] !== 'cancelled') {
|
||||
$wasOccupying = in_array($fresh['status'], ['confirmed', 'pending', 'offered'], true);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?")
|
||||
->execute([(int) $fresh['id']]);
|
||||
|
||||
if ($wasOccupying) {
|
||||
$promoted = promoteFromWaitlist($pdo, (int) $booking['eid']);
|
||||
$promotedCount = count($promoted);
|
||||
}
|
||||
$didCancel = true;
|
||||
}
|
||||
$pdo->commit();
|
||||
$state = 'done';
|
||||
|
||||
// Email dopo il commit (best-effort)
|
||||
if ($didCancel) {
|
||||
try {
|
||||
$evStmt = pdo_conn()->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$evStmt->execute([(int) $booking['eid']]);
|
||||
$evRow = $evStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($evRow) {
|
||||
yse_mail_user_cancel($booking, $evRow); // B: conferma disdetta
|
||||
foreach ($promoted as $pr) { // E: promozione con riconferma
|
||||
try {
|
||||
yse_mail_promotion($pr, $evRow);
|
||||
} catch (Throwable $pe) {
|
||||
error_log('[promo mail] ' . $pe->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $mailEx) {
|
||||
error_log('[cancel mail] ' . $mailEx->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[apiwp/cancel] ' . $ex->getMessage());
|
||||
$state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('e')) {
|
||||
function e($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Disdetta prenotazione — YogaSoul</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #b07aa1;
|
||||
--ink: #2b2622;
|
||||
--muted: #8c837c;
|
||||
--line: #ece7e2;
|
||||
--paper: #faf8f6;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 40px rgba(43, 38, 34, .10);
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
padding: 34px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icn {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.icn.warn {
|
||||
background: #fdecea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.icn.ok {
|
||||
background: #e7f7ee;
|
||||
color: #1a7f52;
|
||||
}
|
||||
|
||||
.icn.info {
|
||||
background: #eef0f3;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Fraunces', serif;
|
||||
font-weight: 600;
|
||||
font-size: 23px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4a433d;
|
||||
font-size: 15px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.ev {
|
||||
background: var(--paper);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.ev .t {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ev .d {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 11px;
|
||||
padding: 14px 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
filter: brightness(.94);
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<?php if ($state === 'notfound') : ?>
|
||||
<div class="icn warn">✕</div>
|
||||
<h1>Link non valido</h1>
|
||||
<p>Questa prenotazione non esiste o il link è errato.</p>
|
||||
|
||||
<?php elseif ($state === 'already') : ?>
|
||||
<div class="icn info">✓</div>
|
||||
<h1>Già annullata</h1>
|
||||
<p>Questa prenotazione risulta già annullata.</p>
|
||||
|
||||
<?php elseif ($state === 'toolate') : ?>
|
||||
<div class="icn info">⏱</div>
|
||||
<h1>Non più annullabile</h1>
|
||||
<p>L'evento è già iniziato: non è più possibile annullare online. Contattaci per assistenza.</p>
|
||||
|
||||
<?php elseif ($state === 'error') : ?>
|
||||
<div class="icn warn">!</div>
|
||||
<h1>Si è verificato un errore</h1>
|
||||
<p>Riprova tra poco. Se il problema persiste, contattaci.</p>
|
||||
|
||||
<?php elseif ($state === 'done') : ?>
|
||||
<div class="icn ok">✓</div>
|
||||
<h1>Prenotazione annullata</h1>
|
||||
<p>La tua prenotazione per <strong><?php echo e($booking['ev_title']); ?></strong> è stata annullata.</p>
|
||||
<?php if ($promotedCount > 0) : ?>
|
||||
<p class="muted">Un posto è stato assegnato alla prima persona in lista d'attesa.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else : /* form */ ?>
|
||||
<div class="icn warn">?</div>
|
||||
<h1>Annullare la prenotazione?</h1>
|
||||
<div class="ev">
|
||||
<div class="t"><?php echo e($booking['ev_title']); ?></div>
|
||||
<div class="d"><?php echo e(date('d/m/Y H:i', strtotime($booking['ev_starts']))); ?> · <?php echo (int) $booking['seats']; ?> post<?php echo (int) $booking['seats'] === 1 ? 'o' : 'i'; ?><?php echo $booking['status'] === 'waiting' ? ' (in lista d\'attesa)' : ''; ?></div>
|
||||
</div>
|
||||
<p>Confermi di voler annullare la prenotazione a nome di <strong><?php echo e($booking['name']); ?></strong>?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="token" value="<?php echo e($token); ?>">
|
||||
<input type="hidden" name="confirm" value="1">
|
||||
<button type="submit" class="btn btn-danger">Sì, annulla la prenotazione</button>
|
||||
</form>
|
||||
<p class="muted">Se hai cambiato idea, chiudi pure questa pagina.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,247 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* Riconferma di una promozione da lista d'attesa (stato 'offered' -> 'confirmed').
|
||||
* GET confirm.php?token=... -> pagina di conferma (non modifica nulla)
|
||||
* POST (token + confirm=1) -> conferma il posto e invia l'email di conferma.
|
||||
*/
|
||||
$pdo = pdo_conn();
|
||||
$token = trim($_POST['token'] ?? $_GET['token'] ?? '');
|
||||
$isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
|
||||
|
||||
$booking = null;
|
||||
if ($token !== '') {
|
||||
$q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid
|
||||
FROM event_bookings b JOIN events e ON e.id = b.event_id
|
||||
WHERE b.token = ?");
|
||||
$q->execute([$token]);
|
||||
$booking = $q->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$state = 'form'; // form | notfound | already | invalid | toolate | done | error
|
||||
|
||||
if (!$booking) {
|
||||
$state = 'notfound';
|
||||
} elseif ($booking['status'] === 'confirmed') {
|
||||
$state = 'already';
|
||||
} elseif ($booking['status'] !== 'offered') {
|
||||
// waiting / cancelled / ecc.: non c'è un'offerta da confermare
|
||||
$state = 'invalid';
|
||||
} elseif (strtotime($booking['ev_starts']) <= time()) {
|
||||
$state = 'toolate';
|
||||
} elseif ($isPost && !empty($_POST['confirm'])) {
|
||||
try {
|
||||
$upd = $pdo->prepare("UPDATE event_bookings SET status = 'confirmed', updated_at = NOW() WHERE id = ? AND status = 'offered'");
|
||||
$upd->execute([(int) $booking['id']]);
|
||||
|
||||
if ($upd->rowCount() > 0) {
|
||||
$state = 'done';
|
||||
// Email di conferma definitiva (best-effort)
|
||||
try {
|
||||
$ev = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$ev->execute([(int) $booking['eid']]);
|
||||
$evRow = $ev->fetch(PDO::FETCH_ASSOC);
|
||||
if ($evRow) {
|
||||
$booking['status'] = 'confirmed';
|
||||
yse_mail_booking($booking, $evRow);
|
||||
}
|
||||
} catch (Throwable $me) {
|
||||
error_log('[confirm mail] ' . $me->getMessage());
|
||||
}
|
||||
} else {
|
||||
$state = 'invalid';
|
||||
}
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[apiwp/confirm] ' . $ex->getMessage());
|
||||
$state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
function e($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Conferma posto — YogaSoul</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #1a7f52;
|
||||
--ink: #2b2622;
|
||||
--muted: #8c837c;
|
||||
--line: #ece7e2;
|
||||
--paper: #faf8f6;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 40px rgba(43, 38, 34, .10);
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
padding: 34px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icn {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.icn.ok {
|
||||
background: #e7f7ee;
|
||||
color: #1a7f52;
|
||||
}
|
||||
|
||||
.icn.info {
|
||||
background: #eef0f3;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.icn.warn {
|
||||
background: #fdecea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Fraunces', serif;
|
||||
font-weight: 600;
|
||||
font-size: 23px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4a433d;
|
||||
font-size: 15px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.ev {
|
||||
background: var(--paper);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.ev .t {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ev .d {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 11px;
|
||||
padding: 14px 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.btn-ok {
|
||||
background: #1a7f52;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-ok:hover {
|
||||
filter: brightness(.94);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<?php if ($state === 'notfound') : ?>
|
||||
<div class="icn warn">✕</div>
|
||||
<h1>Link non valido</h1>
|
||||
<p>Questo link non è valido o è scaduto.</p>
|
||||
|
||||
<?php elseif ($state === 'already') : ?>
|
||||
<div class="icn ok">✓</div>
|
||||
<h1>Posto già confermato</h1>
|
||||
<p>Il tuo posto per <strong><?php echo e($booking['ev_title']); ?></strong> è già confermato. Ci vediamo lì!</p>
|
||||
|
||||
<?php elseif ($state === 'invalid') : ?>
|
||||
<div class="icn info">i</div>
|
||||
<h1>Offerta non più disponibile</h1>
|
||||
<p>Questa offerta non è più valida. Se pensi ci sia un errore, contattaci.</p>
|
||||
|
||||
<?php elseif ($state === 'toolate') : ?>
|
||||
<div class="icn info">⏱</div>
|
||||
<h1>Evento già iniziato</h1>
|
||||
<p>Non è più possibile confermare online. Contattaci per assistenza.</p>
|
||||
|
||||
<?php elseif ($state === 'error') : ?>
|
||||
<div class="icn warn">!</div>
|
||||
<h1>Si è verificato un errore</h1>
|
||||
<p>Riprova tra poco. Se il problema persiste, contattaci.</p>
|
||||
|
||||
<?php elseif ($state === 'done') : ?>
|
||||
<div class="icn ok">✓</div>
|
||||
<h1>Posto confermato!</h1>
|
||||
<p>Grazie <strong><?php echo e($booking['name']); ?></strong>, il tuo posto per <strong><?php echo e($booking['ev_title']); ?></strong> è confermato. Ti abbiamo inviato l'email con tutti i dettagli.</p>
|
||||
|
||||
<?php else : /* form */ ?>
|
||||
<div class="icn ok">🎉</div>
|
||||
<h1>Si è liberato un posto!</h1>
|
||||
<div class="ev">
|
||||
<div class="t"><?php echo e($booking['ev_title']); ?></div>
|
||||
<div class="d"><?php echo e(date('d/m/Y H:i', strtotime($booking['ev_starts']))); ?> · <?php echo (int) $booking['seats']; ?> post<?php echo (int) $booking['seats'] === 1 ? 'o' : 'i'; ?></div>
|
||||
</div>
|
||||
<p>Confermi la tua presenza, <strong><?php echo e($booking['name']); ?></strong>?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="token" value="<?php echo e($token); ?>">
|
||||
<input type="hidden" name="confirm" value="1">
|
||||
<button type="submit" class="btn btn-ok">Sì, confermo il posto</button>
|
||||
</form>
|
||||
<p class="muted">Se non puoi partecipare, ignora pure questa email: il posto passerà a un'altra persona.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_waitlist.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* CRON unico del modulo Eventi. Da richiamare periodicamente (es. ogni 10 min).
|
||||
* Protezione: chiave segreta in .env (YSE_CRON_KEY) passata come ?key=...
|
||||
*
|
||||
* Fa tre cose a ogni giro:
|
||||
* 1) Pubblica gli eventi programmati arrivati alla data di apertura.
|
||||
* 2) Invia i promemoria automatici (ore effettive per-evento o default), 1 sola volta.
|
||||
* 3) Applica la scadenza delle offerte lista d'attesa (offered non riconfermati):
|
||||
* li annulla e offre il posto al prossimo in coda (email E).
|
||||
*/
|
||||
|
||||
$pdo = pdo_conn(); // carica anche il .env (per la chiave e le MAIL_*)
|
||||
|
||||
$expected = $_ENV['YSE_CRON_KEY'] ?? '';
|
||||
$provided = (string) ($_GET['key'] ?? '');
|
||||
if ($expected === '' || !hash_equals($expected, $provided)) {
|
||||
send_json(['ok' => false, 'error' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$report = ['published' => 0, 'reminders' => 0, 'offers_expired' => 0, 'promoted' => 0];
|
||||
|
||||
// Default scuola
|
||||
$opt = [];
|
||||
try {
|
||||
$opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (Throwable $e) {
|
||||
$opt = [];
|
||||
}
|
||||
$defRemEnabled = ($opt['event_reminder_enabled'] ?? 'N') === 'Y';
|
||||
$defRemHours = (int) ($opt['event_reminder_hours'] ?? 24);
|
||||
$defOfferHours = (int) ($opt['event_offer_expiry_hours'] ?? 24);
|
||||
|
||||
/* 1) Pubblica gli eventi programmati arrivati a scadenza */
|
||||
try {
|
||||
$pub = $pdo->prepare("UPDATE events SET status = 'published', updated_at = NOW()
|
||||
WHERE status = 'scheduled' AND publish_at IS NOT NULL AND publish_at <= NOW()");
|
||||
$pub->execute();
|
||||
$report['published'] = $pub->rowCount();
|
||||
} catch (Throwable $e) {
|
||||
error_log('[cron publish] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
/* 2) Promemoria automatici (una sola volta per evento) */
|
||||
try {
|
||||
$evs = $pdo->query("SELECT * FROM events
|
||||
WHERE status = 'published' AND reminder_sent_at IS NULL AND starts_at > NOW()")
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($evs as $ev) {
|
||||
$enabled = $ev['reminder_enabled'] !== null ? ($ev['reminder_enabled'] === 'Y') : $defRemEnabled;
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
$hours = $ev['reminder_hours'] !== null ? (int) $ev['reminder_hours'] : $defRemHours;
|
||||
if (strtotime($ev['starts_at']) - $hours * 3600 > time()) {
|
||||
continue; // non ancora l'ora del promemoria
|
||||
}
|
||||
$ps = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? AND status = 'confirmed'");
|
||||
$ps->execute([$ev['id']]);
|
||||
foreach ($ps->fetchAll(PDO::FETCH_ASSOC) as $p) {
|
||||
try {
|
||||
yse_mail_reminder($p, $ev);
|
||||
$report['reminders']++;
|
||||
} catch (Throwable $me) {
|
||||
error_log('[cron reminder] ' . $me->getMessage());
|
||||
}
|
||||
}
|
||||
$pdo->prepare("UPDATE events SET reminder_sent_at = NOW() WHERE id = ?")->execute([$ev['id']]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[cron reminders] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
/* 3) Scadenza offerte lista d'attesa */
|
||||
try {
|
||||
$offers = $pdo->query("SELECT b.*, e.offer_expiry_hours AS ev_offer_hours
|
||||
FROM event_bookings b JOIN events e ON e.id = b.event_id
|
||||
WHERE b.status = 'offered' AND b.offered_at IS NOT NULL")
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($offers as $o) {
|
||||
$hours = $o['ev_offer_hours'] !== null ? (int) $o['ev_offer_hours'] : $defOfferHours;
|
||||
if (strtotime($o['offered_at']) + $hours * 3600 > time()) {
|
||||
continue; // offerta ancora valida
|
||||
}
|
||||
$promoted = [];
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $o['event_id']]);
|
||||
$st = $pdo->prepare("SELECT status FROM event_bookings WHERE id = ?");
|
||||
$st->execute([(int) $o['id']]);
|
||||
if ($st->fetchColumn() === 'offered') {
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?")
|
||||
->execute([(int) $o['id']]);
|
||||
$promoted = promoteFromWaitlist($pdo, (int) $o['event_id']);
|
||||
$report['offers_expired']++;
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
if (!empty($promoted)) {
|
||||
$ev = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$ev->execute([(int) $o['event_id']]);
|
||||
$evRow = $ev->fetch(PDO::FETCH_ASSOC);
|
||||
foreach ($promoted as $pr) {
|
||||
try {
|
||||
yse_mail_promotion($pr, $evRow);
|
||||
$report['promoted']++;
|
||||
} catch (Throwable $pe) {
|
||||
error_log('[cron promo] ' . $pe->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $te) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[cron offer] ' . $te->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[cron offers] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
send_json(['ok' => true, 'report' => $report, 'at' => date('c')]);
|
||||
@@ -1,133 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
/**
|
||||
* Endpoint pubblico LISTA eventi (sola lettura).
|
||||
* Ritorna gli eventi PUBBLICATI e NON ancora terminati, con posti/prezzo/gallery.
|
||||
*
|
||||
* Parametri (query string):
|
||||
* ?id=12 -> un solo evento
|
||||
* ?ids=12,15,20 -> solo questi eventi (nell'ordine per data)
|
||||
* ?limit=3 -> massimo N eventi
|
||||
* Senza parametri: tutti i pubblicati futuri, per data crescente.
|
||||
*
|
||||
* Regole di visibilità:
|
||||
* status = 'published' AND COALESCE(ends_at, starts_at) >= NOW()
|
||||
* (l'evento sparisce da solo quando è terminato)
|
||||
*/
|
||||
try {
|
||||
$pdo = pdo_conn();
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$idsParam = trim($_GET['ids'] ?? '');
|
||||
$limit = isset($_GET['limit']) ? max(1, min(50, (int) $_GET['limit'])) : 0;
|
||||
|
||||
$where = ["e.status IN ('published','scheduled')", "COALESCE(e.ends_at, e.starts_at) >= NOW()"];
|
||||
$params = [];
|
||||
|
||||
if ($id > 0) {
|
||||
$where[] = 'e.id = ?';
|
||||
$params[] = $id;
|
||||
} elseif ($idsParam !== '') {
|
||||
$ids = array_values(array_filter(array_map('intval', explode(',', $idsParam)), function ($v) {
|
||||
return $v > 0;
|
||||
}));
|
||||
if (!empty($ids)) {
|
||||
$where[] = 'e.id IN (' . implode(',', array_fill(0, count($ids), '?')) . ')';
|
||||
foreach ($ids as $v) {
|
||||
$params[] = $v;
|
||||
}
|
||||
} else {
|
||||
send_json(['ok' => true, 'count' => 0, 'events' => []]);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "SELECT e.*, u.first_name AS cf, u.last_name AS cl,
|
||||
COALESCE(b.occupied, 0) AS occupied,
|
||||
COALESCE(w.waiting, 0) AS waiting
|
||||
FROM events e
|
||||
LEFT JOIN auth_users u ON u.id = e.user_id
|
||||
LEFT JOIN (
|
||||
SELECT event_id, SUM(seats) AS occupied
|
||||
FROM event_bookings WHERE status IN ('confirmed','pending','offered')
|
||||
GROUP BY event_id
|
||||
) b ON b.event_id = e.id
|
||||
LEFT JOIN (
|
||||
SELECT event_id, SUM(seats) AS waiting
|
||||
FROM event_bookings WHERE status = 'waiting'
|
||||
GROUP BY event_id
|
||||
) w ON w.event_id = e.id
|
||||
WHERE " . implode(' AND ', $where) . "
|
||||
ORDER BY e.starts_at ASC";
|
||||
|
||||
if ($limit > 0) {
|
||||
$sql .= ' LIMIT ' . (int) $limit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$symbols = ['EUR' => '€', 'USD' => '$', 'GBP' => '£', 'CHF' => 'CHF'];
|
||||
$events = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$maxSeats = (int) $r['max_seats'];
|
||||
$occupied = (int) $r['occupied'];
|
||||
$remaining = max(0, $maxSeats - $occupied);
|
||||
|
||||
$gallery = [];
|
||||
if (!empty($r['gallery'])) {
|
||||
$g = json_decode($r['gallery'], true);
|
||||
if (is_array($g)) {
|
||||
foreach ($g as $path) {
|
||||
if (is_string($path) && $path !== '') {
|
||||
$gallery[] = public_url($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$organizer = trim((string) $r['organizer_name']) !== ''
|
||||
? $r['organizer_name']
|
||||
: trim(($r['cf'] ?? '') . ' ' . ($r['cl'] ?? ''));
|
||||
|
||||
$events[] = [
|
||||
'id' => (int) $r['id'],
|
||||
'title' => $r['title'],
|
||||
'slug' => $r['slug'],
|
||||
'status' => $r['status'],
|
||||
'scheduled' => $r['status'] === 'scheduled',
|
||||
'bookable' => $r['status'] === 'published',
|
||||
'publish_at' => $r['publish_at'],
|
||||
'description' => $r['description'], // HTML (sanificare al rendering in WP)
|
||||
'instructions' => $r['instructions'],
|
||||
'what_to_bring' => $r['what_to_bring'],
|
||||
'suitable_for' => $r['suitable_for'],
|
||||
'organizer' => $organizer !== '' ? $organizer : null,
|
||||
'starts_at' => $r['starts_at'],
|
||||
'ends_at' => $r['ends_at'],
|
||||
'address' => $r['address'],
|
||||
'lat' => $r['lat'] !== null ? (float) $r['lat'] : null,
|
||||
'lng' => $r['lng'] !== null ? (float) $r['lng'] : null,
|
||||
'cost' => (float) $r['cost'],
|
||||
'currency' => $r['currency'],
|
||||
'currency_symbol' => $symbols[$r['currency']] ?? $r['currency'],
|
||||
'color' => $r['color'],
|
||||
'cover_image' => public_url($r['cover_image']),
|
||||
'gallery' => $gallery,
|
||||
'max_seats' => $maxSeats,
|
||||
'max_per_booking' => (int) $r['max_per_booking'],
|
||||
'occupied' => $occupied,
|
||||
'remaining' => $remaining,
|
||||
'sold_out' => $remaining <= 0,
|
||||
'waitlist_enabled' => ($r['waitlist_enabled'] ?? 'N') === 'Y',
|
||||
'waiting' => (int) $r['waiting'],
|
||||
];
|
||||
}
|
||||
|
||||
send_json(['ok' => true, 'count' => count($events), 'events' => $events]);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[api/events] ' . $ex->getMessage());
|
||||
send_json(['ok' => false, 'error' => 'Errore nel recupero degli eventi.'], 500);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 222 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 222 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB |
@@ -33,29 +33,17 @@ if (isset($_POST['formclass']) && $_POST['formclass'] === 'Y') {
|
||||
$idassociateservice = (int) ($_POST['servicelist'] ?? 0);
|
||||
|
||||
if ($idmainservice > 0 && $idassociateservice > 0) {
|
||||
// Evita duplicati: inserisci solo se l'associazione non esiste già
|
||||
$stmtCheck = $pdo->prepare(
|
||||
"SELECT COUNT(*) FROM associateclass
|
||||
WHERE idmainservice = :main AND idassociateservice = :assoc"
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO associateclass (idmainservice, idassociateservice)
|
||||
VALUES (:main, :assoc)"
|
||||
);
|
||||
$stmtCheck->execute([':main' => $idmainservice, ':assoc' => $idassociateservice]);
|
||||
$alreadyExists = (int) $stmtCheck->fetchColumn() > 0;
|
||||
|
||||
if ($alreadyExists) {
|
||||
$insertFeedback = ['type' => 'error', 'text' => 'Questa classe è già associata.'];
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO associateclass (idmainservice, idassociateservice)
|
||||
VALUES (:main, :assoc)"
|
||||
);
|
||||
$ok = $stmt->execute([
|
||||
':main' => $idmainservice,
|
||||
':assoc' => $idassociateservice,
|
||||
]);
|
||||
$insertFeedback = $ok
|
||||
? ['type' => 'success', 'text' => 'Associazione aggiunta con successo.']
|
||||
: ['type' => 'error', 'text' => 'Errore durante l\'associazione. Riprova.'];
|
||||
}
|
||||
$ok = $stmt->execute([
|
||||
':main' => $idmainservice,
|
||||
':assoc' => $idassociateservice,
|
||||
]);
|
||||
$insertFeedback = $ok
|
||||
? ['type' => 'success', 'text' => 'Associazione aggiunta con successo.']
|
||||
: ['type' => 'error', 'text' => 'Errore durante l\'associazione. Riprova.'];
|
||||
} else {
|
||||
$insertFeedback = ['type' => 'error', 'text' => 'Seleziona una classe da associare.'];
|
||||
}
|
||||
@@ -78,21 +66,12 @@ if ($idmain > 0) {
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Elenco delle classi per il dropdown:
|
||||
* esclude la principale E quelle già associate a questa principale.
|
||||
* Elenco di tutte le classi (per il dropdown), escludendo la principale
|
||||
* ---------------------------------------------------------------------- */
|
||||
$stmtAll = $pdo->prepare(
|
||||
"SELECT idservice, servicename
|
||||
FROM service
|
||||
WHERE idservice != :idmain
|
||||
AND idservice NOT IN (
|
||||
SELECT idassociateservice
|
||||
FROM associateclass
|
||||
WHERE idmainservice = :idmain2
|
||||
)
|
||||
ORDER BY servicename"
|
||||
"SELECT idservice, servicename FROM service WHERE idservice != :idmain ORDER BY servicename"
|
||||
);
|
||||
$stmtAll->execute([':idmain' => $idmain, ':idmain2' => $idmain]);
|
||||
$stmtAll->execute([':idmain' => $idmain]);
|
||||
$allServices = $stmtAll->fetchAll();
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
@@ -272,9 +251,6 @@ $associatedClasses = $stmtAssoc->fetchAll();
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (empty($allServices)) : ?>
|
||||
<small class="text-muted">Tutte le classi disponibili sono già state associate.</small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+36
-56
@@ -1,24 +1,33 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once __DIR__ . '/class/mailer.php';
|
||||
require_once('include/waitinglist.php');
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
require_once('include/headscript.php');
|
||||
require 'phpmailer/src/Exception.php';
|
||||
require 'phpmailer/src/PHPMailer.php';
|
||||
require 'phpmailer/src/SMTP.php';
|
||||
|
||||
// Abilita il reporting degli errori per il debug
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Recupera i parametri GET
|
||||
$idorder = isset($_GET['idorder']) ? (int) $_GET['idorder'] : 0;
|
||||
$idnewbooking = isset($_GET['idnewbooking']) ? (int) $_GET['idnewbooking'] : 0;
|
||||
$iduser = isset($_GET['iduser']) ? (int) $_GET['iduser'] : 0;
|
||||
$idservicenew = isset($_GET['idservicenew']) ? (int) $_GET['idservicenew'] : 0;
|
||||
$idorder = isset($_GET['idorder']) ? (int)$_GET['idorder'] : 0;
|
||||
$idnewbooking = isset($_GET['idnewbooking']) ? (int)$_GET['idnewbooking'] : 0;
|
||||
$iduser = isset($_GET['iduser']) ? (int)$_GET['iduser'] : 0;
|
||||
$idservicenew = isset($_GET['idservicenew']) ? (int)$_GET['idservicenew'] : 0;
|
||||
|
||||
// Valida i parametri richiesti
|
||||
if ($idorder === 0 || $idnewbooking === 0 || $iduser === 0 || $idservicenew === 0) {
|
||||
die("Errore: Parametri mancanti.");
|
||||
}
|
||||
|
||||
// Riusa la connessione già aperta da bkngstm.php
|
||||
$conn = $bkngstm;
|
||||
// Crea la connessione al database
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
if ($conn->connect_error) {
|
||||
die("Connessione al database fallita: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
// Recupera dateschedule e servicename
|
||||
$query = "SELECT serviceschedule.dateschedule, service.servicename
|
||||
@@ -36,6 +45,7 @@ if ($resultnew->num_rows > 0) {
|
||||
$servicename = $rownew['servicename'] ?? 'N/D';
|
||||
} else {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
die("Errore: Nessun orario trovato per idserviceschedule = $idnewbooking.");
|
||||
}
|
||||
$stmt->close();
|
||||
@@ -43,35 +53,14 @@ $stmt->close();
|
||||
// Formatta la data per l'email
|
||||
$newtimeformat = date("d-m-Y H:i", strtotime($newtime));
|
||||
|
||||
// Recupera nome ed email dell'utente per la mail
|
||||
$firstname = '';
|
||||
$emailuser = '';
|
||||
$stmtUser = $conn->prepare("SELECT first_name, email FROM auth_users WHERE id = ?");
|
||||
$stmtUser->bind_param("i", $iduser);
|
||||
$stmtUser->execute();
|
||||
$resUser = $stmtUser->get_result();
|
||||
if ($rowUser = $resUser->fetch_assoc()) {
|
||||
$firstname = $rowUser['first_name'] ?? '';
|
||||
$emailuser = $rowUser['email'] ?? '';
|
||||
}
|
||||
$stmtUser->close();
|
||||
|
||||
// Decide lo stato: pending se c'è posto, waiting se la classe è piena
|
||||
$bookingStatus = decideBookingStatus($conn, $idnewbooking);
|
||||
|
||||
if ($bookingStatus === 'full') {
|
||||
// Classe piena e lista d'attesa disattivata: non inseriamo nulla
|
||||
header("Location: userpanel.php?full=Y");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Inserisce la nuova prenotazione in bookingclass
|
||||
$insertQuery = "INSERT INTO bookingclass (idserviceschedule, iduser, prevbookingstart, idprevserviceschedule, idservice, idorder, bookingstart, status)
|
||||
VALUES (?, ?, NULL, '0', ?, ?, ?, ?)";
|
||||
VALUES (?, ?, '0', '0', ?, ?, ?, 'pending')";
|
||||
$stmt = $conn->prepare($insertQuery);
|
||||
$stmt->bind_param("iiiiss", $idnewbooking, $iduser, $idservicenew, $idorder, $newtime, $bookingStatus);
|
||||
$stmt->bind_param("iiiis", $idnewbooking, $iduser, $idservicenew, $idorder, $newtime);
|
||||
if (!$stmt->execute()) {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
die("Errore durante l'inserimento della nuova prenotazione: " . $conn->error);
|
||||
}
|
||||
$stmt->close();
|
||||
@@ -86,16 +75,8 @@ $stmt->bind_param("ii", $iduser, $idorder);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
// Connessione $bkngstm gestita globalmente: non la chiudiamo qui
|
||||
|
||||
// Parametro di ritorno diverso se finito in lista d'attesa
|
||||
$redirectFlag = ($bookingStatus === 'waiting') ? 'waiting=Y' : 'reprogram=Y';
|
||||
|
||||
// Se non abbiamo un'email valida, salta l'invio ma completa comunque il flusso
|
||||
if ($emailuser === '') {
|
||||
header("Location: userpanel.php?$redirectFlag");
|
||||
exit();
|
||||
}
|
||||
// Chiudi la connessione al database
|
||||
$conn->close();
|
||||
|
||||
// Configura il contenuto dell'email
|
||||
$messageedit = "
|
||||
@@ -129,11 +110,11 @@ $messageedit = "
|
||||
<br>
|
||||
<p style='font-size: 14px; line-height: 190%;'>
|
||||
<span style='font-size: 16px; line-height: 30.4px;'>
|
||||
Se non ricordi la password o non hai sei mai entrata in Yogibbok, per prima cosa resettare la password cliccando di seguito:
|
||||
Per il primo accesso devi prima resettare la password cliccando qui:
|
||||
</span>
|
||||
</p>
|
||||
<br>
|
||||
<a href='https://yogibook.yogasoul.it/password/reset' target='_blank' style='box-sizing: border-box; display: inline-block; text-decoration: none; text-align: center; color: #FFFFFF; background-color: #3AAEE0; border-radius: 4px; width: auto; max-width: 100%; font-size: 14px;'>
|
||||
<a href='https://yogibook.yogasoul.it/public/password/reset' target='_blank' style='box-sizing: border-box; display: inline-block; text-decoration: none; text-align: center; color: #FFFFFF; background-color: #3AAEE0; border-radius: 4px; width: auto; max-width: 100%; font-size: 14px;'>
|
||||
<span style='display: block; padding: 10px 20px; line-height: 120%;'>Reset Password</span>
|
||||
</a>
|
||||
<br>
|
||||
@@ -160,26 +141,25 @@ include('mail/emailtemplate2.php');
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
|
||||
$mail->Port = (int) $_ENV['MAIL_PORT'];
|
||||
$mail->Host = 'mail.yogasoul.it';
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = 'info@yogasoul.it';
|
||||
$mail->Password = '!Testolina88';
|
||||
$mail->SMTPSecure = 'tls';
|
||||
$mail->Port = 587;
|
||||
|
||||
$mail->setFrom($_ENV['MAIL_FROM_ADDRESS'], $_ENV['MAIL_FROM_NAME']);
|
||||
$mail->setFrom('info@yogasoul.it', 'YogiBook [YogaSoul]');
|
||||
$mail->addAddress($emailuser);
|
||||
|
||||
$mail->Subject = "Proposta di riprogrammazione!";
|
||||
$mail->Body = $mailmessage1;
|
||||
$mail->Body = $mailmessage1;
|
||||
$mail->AltBody = 'Hai inviato una richiesta di riprogrammazione della tua lezione per il servizio ' . $servicename . ' il ' . $newtimeformat . '. La tua richiesta è in fase di accettazione.';
|
||||
|
||||
$mail->send();
|
||||
} catch (Exception $e) {
|
||||
// La prenotazione è già salvata: logghiamo l'errore mail ma non blocchiamo l'utente
|
||||
error_log("Errore invio mail riprogrammazione (iduser $iduser): " . $mail->ErrorInfo);
|
||||
die("Errore durante l'invio dell'email: " . $mail->ErrorInfo);
|
||||
}
|
||||
|
||||
// Esegui il reindirizzamento
|
||||
header("Location: userpanel.php?$redirectFlag");
|
||||
header("Location: userpanel.php?reprogram=Y");
|
||||
exit();
|
||||
|
||||
+41
-58
@@ -177,70 +177,66 @@ $conn->close();
|
||||
|
||||
<style>
|
||||
.custom-card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
margin-bottom: 18px;
|
||||
margin: 10px auto;
|
||||
display: flex;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
background-color: white;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.custom-card:hover {
|
||||
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.custom-date-box {
|
||||
flex: 0 0 110px;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
background-color: red;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.custom-day {
|
||||
font-size: 44px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.custom-month {
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.custom-event-details {
|
||||
flex: 1;
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 18px 22px;
|
||||
background: #fff;
|
||||
border-left: 20px solid #ccc;
|
||||
/* colore impostato inline per ogni card in base allo stato */
|
||||
padding: 10px 20px;
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.custom-heading {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0 0 4px;
|
||||
margin-top: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.custom-paragraph {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 3px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.custom-actions {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@@ -249,19 +245,16 @@ $conn->close();
|
||||
}
|
||||
|
||||
.custom-action-button {
|
||||
background-color: #f0f0f0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 7px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
background: #eef2f7;
|
||||
color: #374151;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.custom-action-button:hover {
|
||||
filter: brightness(0.94);
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -269,20 +262,14 @@ $conn->close();
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-date-box {
|
||||
flex: 0 0 auto;
|
||||
.custom-date-box,
|
||||
.custom-event-details {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.custom-day {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.custom-month {
|
||||
font-size: 18px;
|
||||
.custom-event-time {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +494,7 @@ $conn->close();
|
||||
|
||||
$mysqli = new mysqli($servername, $username, $password, $dbname);
|
||||
$eventId = $bookedclass->getColumnVal("idserviceschedule");
|
||||
$bookingQuery = "SELECT iduser FROM bookingclass WHERE bookingclass.idserviceschedule = ? AND bookingclass.status IN ('booked','pending')";
|
||||
$bookingQuery = "SELECT iduser FROM bookingclass WHERE bookingclass.idserviceschedule = ? AND bookingclass.status='booked'";
|
||||
$stmt = $mysqli->prepare($bookingQuery);
|
||||
$stmt->bind_param("i", $eventId);
|
||||
$stmt->execute();
|
||||
@@ -538,17 +525,17 @@ $conn->close();
|
||||
<?php } elseif ($freeplace > 0) { ?>
|
||||
<div class="custom-date-box" style="background-color:#1ebf73">
|
||||
<?php } else { ?>
|
||||
<div class="custom-date-box" style="background-color:#d1698a">
|
||||
<div class="custom-date-box" style="background-color:#9C9C9C">
|
||||
<?php } ?>
|
||||
<div class="custom-day"><?php echo $dayInItalian; ?></div>
|
||||
<div class="custom-month"><?php echo $monthInItalian; ?></div>
|
||||
</div>
|
||||
<?php if ($alreadybooked == 'Y') { ?>
|
||||
<div class="custom-event-details" style="background-color:#FFAC7A;border-left-color:#FF6609">
|
||||
<div class="custom-event-details" style="background-color:#FFAC7A">
|
||||
<?php } elseif ($freeplace > 0) { ?>
|
||||
<div class="custom-event-details" style="border-left-color:<?php echo ($bookedclass->getColumnVal("colorclass")); ?>">
|
||||
<div class="custom-event-details" style="background-color:<?php echo ($bookedclass->getColumnVal("colorclass")); ?>">
|
||||
<?php } else { ?>
|
||||
<div class="custom-event-details" style="background-color:#fbeef3;border-left-color:#d1698a">
|
||||
<div class="custom-event-details" style="background-color:#CDCDCD">
|
||||
<?php } ?>
|
||||
<h2 class="custom-heading"><?php echo ($bookedclass->getColumnVal("servicename")); ?> -<?php echo $countPersons; ?>/<?php echo ($bookedclass->getColumnVal("maxcapacity")); ?></h2>
|
||||
<p class="custom-paragraph">Quando: <?php echo $newDateFormat; ?></p>
|
||||
@@ -561,11 +548,7 @@ $conn->close();
|
||||
<?php } elseif ($freeplace > 0) { ?>
|
||||
<a href="rebookandgo.php?idpreviousbooking=<?php echo $idpreviousbooking; ?>&idservicenew=<?php echo $idservicenew; ?>&idnewbooking=<?php echo $idnewbooking; ?>&iduser=<?php echo $iduserlogin; ?>"><button class="booking-button"><i class="fas fa-arrow-circle-right"></i> Riprogramma Classe</button></a>
|
||||
<?php } else { ?>
|
||||
<?php if ($optionquery->getColumnVal("waitinglist_enabled") === 'Y') { ?>
|
||||
<a href="rebookandgo.php?idpreviousbooking=<?php echo $idpreviousbooking; ?>&idservicenew=<?php echo $idservicenew; ?>&idnewbooking=<?php echo $idnewbooking; ?>&iduser=<?php echo $iduserlogin; ?>"><button class="booking-button" style="background-color:#d1698a;color:#fff;"><i class="fas fa-hourglass-half"></i> Mettiti in lista d'attesa</button></a>
|
||||
<?php } else { ?>
|
||||
<button class="booking-button"><i class="fa fa-stop"></i> Classe Piena</button>
|
||||
<?php } ?>
|
||||
<button class="booking-button"><i class="fa fa-stop"></i> Classe Piena</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+547
-567
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,20 @@
|
||||
<?php
|
||||
// Connessione al database
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/mailer.php';
|
||||
// Abilita visualizzazione errori PHP (solo per debug)
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
// Riusa la connessione già aperta da bkngstm.php
|
||||
$conn = $bkngstm;
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Inizializza log
|
||||
$logFile = 'cancella_prenotazione_log.txt';
|
||||
$logMessage = "Esecuzione cancellazione: " . date('Y-m-d H:i:s') . "\n";
|
||||
@@ -40,7 +47,7 @@ if (!$idbookingclass) {
|
||||
// Verifica validità: token corrisponde, lezione futura
|
||||
$query = "SELECT bc.*, ob.expireon, au.email, au.first_name, s.servicename
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN orderbook ob ON bc.idorder = ob.idorderbook
|
||||
LEFT JOIN orderbook ob ON bc.idorder = ob.order_id
|
||||
LEFT JOIN auth_users au ON bc.iduser = au.id
|
||||
LEFT JOIN service s ON bc.idservice = s.idservice
|
||||
WHERE bc.idbookingclass = ?
|
||||
@@ -169,15 +176,19 @@ if (isset($_GET['action']) && $_GET['action'] === 'cancel') {
|
||||
<span style='display:block;padding:10px 20px;line-height:120%;'><span style='line-height: 16.8px;'>YogiBook - YogaSoul</span></span>
|
||||
</a>";
|
||||
|
||||
require_once 'phpmailer/src/Exception.php';
|
||||
require_once 'phpmailer/src/PHPMailer.php';
|
||||
require_once 'phpmailer/src/SMTP.php';
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
|
||||
$mail->Port = (int) $_ENV['MAIL_PORT'];
|
||||
$mail->Host = 'mail.yogasoul.it';
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = 'info@yogasoul.it';
|
||||
$mail->Password = '!Testolina88';
|
||||
$mail->SMTPSecure = 'tls';
|
||||
$mail->Port = 587;
|
||||
|
||||
if (!file_exists('mail/emailtemplate2.php')) {
|
||||
throw new Exception("File emailtemplate2.php non trovato.");
|
||||
@@ -190,8 +201,8 @@ if (isset($_GET['action']) && $_GET['action'] === 'cancel') {
|
||||
|
||||
$htmlContent = str_replace('{message}', $messagecancel, $mailmessage1);
|
||||
|
||||
$mail->From = $_ENV['MAIL_FROM_ADDRESS'];
|
||||
$mail->FromName = $_ENV['MAIL_FROM_NAME'];
|
||||
$mail->From = 'info@yogasoul.it';
|
||||
$mail->FromName = 'YogiBook [YogaSoul]';
|
||||
$mail->addAddress($emailuser);
|
||||
$mail->Subject = "YogiBook - Lezione cancellata con successo!";
|
||||
$mail->Body = $htmlContent;
|
||||
@@ -303,5 +314,5 @@ file_put_contents($logFile, $logMessage, FILE_APPEND);
|
||||
</html>
|
||||
<?php
|
||||
$stmt->close();
|
||||
// Connessione $bkngstm gestita globalmente: non la chiudiamo qui
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -1,306 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Storico delle lezioni cancellate dagli allievi (tabella bookingclass_cancellations).
|
||||
* Sola consultazione: questa pagina non modifica nessun dato.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
// Preavviso leggibile: minuti, ore o giorni
|
||||
function formatPreavviso(?int $minutes): string
|
||||
{
|
||||
if ($minutes === null) return '-';
|
||||
if ($minutes < 60) return $minutes . ' min';
|
||||
if ($minutes < 48 * 60) return floor($minutes / 60) . ' ore';
|
||||
return floor($minutes / 1440) . ' giorni';
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
c.idcancellation,
|
||||
c.iduser,
|
||||
c.idorder,
|
||||
c.is_gift,
|
||||
c.reprogrammed_incremented,
|
||||
c.cancelled_at,
|
||||
COALESCE(ss.dateschedule, c.bookingstart) AS lesson_date,
|
||||
TIMESTAMPDIFF(MINUTE, c.cancelled_at, COALESCE(ss.dateschedule, c.bookingstart)) AS preavviso_min,
|
||||
u.first_name,
|
||||
u.last_name,
|
||||
u.email,
|
||||
s.servicename,
|
||||
o.reprogrammed,
|
||||
o.maxreschedule
|
||||
FROM bookingclass_cancellations c
|
||||
LEFT JOIN auth_users u ON u.id = c.iduser
|
||||
LEFT JOIN service s ON s.idservice = c.idservice
|
||||
LEFT JOIN serviceschedule ss ON ss.idserviceschedule = c.idserviceschedule
|
||||
LEFT JOIN orderbook o ON o.idorderbook = c.idorder
|
||||
ORDER BY c.cancelled_at DESC";
|
||||
|
||||
$rows = $pdo->query($sql)->fetchAll();
|
||||
|
||||
// Contatori ultimi 7 / 30 giorni
|
||||
$limit7 = strtotime('-7 days');
|
||||
$limit30 = strtotime('-30 days');
|
||||
$count7 = 0;
|
||||
$count30 = 0;
|
||||
foreach ($rows as $r) {
|
||||
$t = strtotime($r['cancelled_at']);
|
||||
if ($t >= $limit7) $count7++;
|
||||
if ($t >= $limit30) $count30++;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Cancellazioni</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="YogiBook" 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">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.counter {
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
background: #fbe4c4;
|
||||
color: #8a5a1a;
|
||||
}
|
||||
|
||||
.counter.counter-30 {
|
||||
background: #c4e1fb;
|
||||
color: #1a4f7a;
|
||||
}
|
||||
|
||||
#cancTable thead th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
#cancTable td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.badge-soft {
|
||||
display: inline-block;
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-late {
|
||||
background: #fbc7c4;
|
||||
color: #a12820;
|
||||
}
|
||||
|
||||
.badge-ok {
|
||||
background: #cdfbc4;
|
||||
color: #1a7f2e;
|
||||
}
|
||||
|
||||
.badge-gift {
|
||||
background: #e2d3f5;
|
||||
color: #4c2c92;
|
||||
}
|
||||
|
||||
.badge-neutral {
|
||||
background: #eef0f3;
|
||||
color: #4b5563;
|
||||
}
|
||||
</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">Cancellazioni</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">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Lezioni cancellate dagli allievi</div>
|
||||
<p class="section-lead">Storico delle cancellazioni, dalla più recente. Usa la ricerca per filtrare per nome, lezione o data.</p>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap mb-3">
|
||||
<span class="counter"><i class="fas fa-calendar-week me-1"></i> Ultimi 7 giorni: <strong><?php echo $count7; ?></strong></span>
|
||||
<span class="counter counter-30"><i class="fas fa-calendar-alt me-1"></i> Ultimi 30 giorni: <strong><?php echo $count30; ?></strong></span>
|
||||
<span class="counter badge-neutral"><i class="fas fa-list me-1"></i> Totale: <strong><?php echo count($rows); ?></strong></span>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="cancTable" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Allievo</th>
|
||||
<th>Lezione</th>
|
||||
<th>Data lezione</th>
|
||||
<th>Cancellata il</th>
|
||||
<th>Preavviso</th>
|
||||
<th>Ordine</th>
|
||||
<th>Conta come riprog.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r) :
|
||||
$nome = trim(($r['first_name'] ?? '') . ' ' . ($r['last_name'] ?? ''));
|
||||
if ($nome === '') $nome = 'Utente #' . (int) $r['iduser'];
|
||||
$lessonTs = $r['lesson_date'] ? strtotime($r['lesson_date']) : null;
|
||||
$cancTs = strtotime($r['cancelled_at']);
|
||||
$preMin = $r['preavviso_min'] !== null ? (int) $r['preavviso_min'] : null;
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php echo e($nome); ?>
|
||||
<?php if (!empty($r['email'])) : ?>
|
||||
<span class="user-email"><?php echo e($r['email']); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($r['servicename'] ?? '-'); ?></td>
|
||||
<td data-order="<?php echo $lessonTs ? date('Y-m-d H:i', $lessonTs) : ''; ?>">
|
||||
<?php echo $lessonTs ? e(date('d/m/Y H:i', $lessonTs)) : '-'; ?>
|
||||
</td>
|
||||
<td data-order="<?php echo e(date('Y-m-d H:i:s', $cancTs)); ?>">
|
||||
<?php echo e(date('d/m/Y H:i', $cancTs)); ?>
|
||||
</td>
|
||||
<td data-order="<?php echo $preMin ?? 0; ?>">
|
||||
<?php if ($preMin !== null && $preMin < 24 * 60) : ?>
|
||||
<span class="badge-soft badge-late"><?php echo e(formatPreavviso($preMin)); ?></span>
|
||||
<?php else : ?>
|
||||
<span class="badge-soft badge-ok"><?php echo e(formatPreavviso($preMin)); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo $r['idorder'] !== null ? '#' . (int) $r['idorder'] : '-'; ?></td>
|
||||
<td>
|
||||
<?php if ($r['is_gift'] === 'Y' || $r['idorder'] === null) : ?>
|
||||
<span class="badge-soft badge-gift">Omaggio</span>
|
||||
<?php elseif ($r['reprogrammed_incremented'] === 'L') : ?>
|
||||
<span class="badge-soft badge-late">Limite superato: lezione persa</span>
|
||||
<?php elseif ($r['reprogrammed_incremented'] === 'Y') : ?>
|
||||
<span class="badge-soft badge-neutral">
|
||||
Sì<?php if ($r['reprogrammed'] !== null) : ?> (<?php echo (int) $r['reprogrammed']; ?>/<?php echo (int) $r['maxreschedule']; ?>)<?php endif; ?>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="badge-soft badge-neutral">No</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</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>
|
||||
$('#cancTable').DataTable({
|
||||
paging: true,
|
||||
pageLength: 50,
|
||||
searching: true,
|
||||
ordering: true,
|
||||
order: [
|
||||
[3, 'desc']
|
||||
],
|
||||
language: {
|
||||
url: "//cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json"
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$token = $_GET['token'] ?? '';
|
||||
$confirm = ($_GET['confirm'] ?? '') === 'Y';
|
||||
|
||||
if ($token === '') {
|
||||
header('Location: admin-services.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Conta le prenotazioni utente collegate alle lezioni di questa propagazione
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT COUNT(*) FROM bookingclass b
|
||||
JOIN serviceschedule s ON b.idserviceschedule = s.idserviceschedule
|
||||
WHERE s.propagation_token = :token"
|
||||
);
|
||||
$stmt->execute([':token' => $token]);
|
||||
$prenotazioni = (int) $stmt->fetchColumn();
|
||||
|
||||
// Se ci sono prenotazioni e non è ancora arrivata la conferma esplicita -> chiedi
|
||||
if ($prenotazioni > 0 && !$confirm) {
|
||||
header('Location: admin-services.php?message=confirmdelete&token=' . urlencode($token) . '&count=' . $prenotazioni);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Procedi con la cancellazione
|
||||
$stmt = $pdo->prepare("DELETE FROM serviceschedule WHERE propagation_token = :token");
|
||||
$stmt->execute([':token' => $token]);
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM propagation WHERE token = :token");
|
||||
$stmt->execute([':token' => $token]);
|
||||
|
||||
header('Location: admin-services.php?message=propdeleted');
|
||||
exit;
|
||||
+3
-82
@@ -58,7 +58,7 @@ $today = date('Y-m-d');
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
window.certTable = $('#certificateTable').DataTable({
|
||||
$('#certificateTable').DataTable({
|
||||
"language": {
|
||||
"url": "//cdn.datatables.net/plug-ins/1.11.5/i18n/it-IT.json"
|
||||
},
|
||||
@@ -85,66 +85,6 @@ $today = date('Y-m-d');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Chip della scadenza (stesso aspetto dopo la modifica)
|
||||
function expChipHtml(id, user, date, expired) {
|
||||
var chip = $('<span>')
|
||||
.addClass('exp-chip exp-edit ' + (expired ? 'exp-expired' : 'exp-ok'))
|
||||
.attr({
|
||||
'data-id': id,
|
||||
'data-date': date,
|
||||
'data-user': user,
|
||||
'title': 'Clicca per modificare la scadenza'
|
||||
})
|
||||
.text(date + ' ')
|
||||
.append('<i class="fas fa-pen"></i>');
|
||||
return $('<div>').append(chip).html();
|
||||
}
|
||||
|
||||
// Clic sulla scadenza: scegli la nuova data e salva
|
||||
$(document).on('click', '.exp-edit', function() {
|
||||
var chip = $(this);
|
||||
var id = chip.attr('data-id');
|
||||
var user = chip.attr('data-user') || '';
|
||||
var current = chip.attr('data-date') || '';
|
||||
|
||||
Swal.fire({
|
||||
title: 'Modifica scadenza',
|
||||
html: 'Certificato di <strong>' + $('<div>').text(user).html() + '</strong>',
|
||||
input: 'date',
|
||||
inputValue: (/^\d{4}-\d{2}-\d{2}$/.test(current) && current !== '0000-00-00') ? current : '',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Salva',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#1ebf73',
|
||||
showLoaderOnConfirm: true,
|
||||
inputValidator: (v) => !v ? 'Scegli una data.' : undefined,
|
||||
preConfirm: (v) => {
|
||||
return $.post('admin_update_certificate_expiry.php', {
|
||||
id: id,
|
||||
expiry: v
|
||||
}, null, 'json')
|
||||
.then(function(resp) {
|
||||
if (!resp || !resp.success) throw new Error((resp && resp.message) || 'Errore durante il salvataggio.');
|
||||
return resp;
|
||||
})
|
||||
.catch(function(err) {
|
||||
Swal.showValidationMessage((err.responseJSON && err.responseJSON.message) || err.message || 'Errore durante il salvataggio.');
|
||||
});
|
||||
}
|
||||
}).then(function(r) {
|
||||
if (!r.isConfirmed || !r.value) return;
|
||||
var td = chip.closest('td');
|
||||
window.certTable.cell(td).data(expChipHtml(id, user, r.value.expiry, r.value.expired)).draw(false);
|
||||
Swal.fire({
|
||||
title: 'Fatto',
|
||||
text: 'Scadenza aggiornata al ' + r.value.expiry.split('-').reverse().join('/') + '.',
|
||||
icon: 'success',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -216,21 +156,6 @@ $today = date('Y-m-d');
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.exp-edit {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.exp-edit i {
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.exp-edit:hover {
|
||||
filter: brightness(0.95);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.exp-ok {
|
||||
background: #e3f2ec;
|
||||
color: #1a7f52;
|
||||
@@ -331,12 +256,8 @@ $today = date('Y-m-d');
|
||||
</td>
|
||||
<td><?php echo e($d['documentdescription']); ?></td>
|
||||
<td>
|
||||
<span class="exp-chip exp-edit <?php echo $isExpired ? 'exp-expired' : 'exp-ok'; ?>"
|
||||
data-id="<?php echo $idc; ?>"
|
||||
data-date="<?php echo e($d['expirydatedocument']); ?>"
|
||||
data-user="<?php echo e($fullName); ?>"
|
||||
title="Clicca per modificare la scadenza">
|
||||
<?php echo e($d['expirydatedocument']); ?> <i class="fas fa-pen"></i>
|
||||
<span class="exp-chip <?php echo $isExpired ? 'exp-expired' : 'exp-ok'; ?>">
|
||||
<?php echo e($d['expirydatedocument']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo e($d['uploaded_at'] ?? 'N/D'); ?></td>
|
||||
|
||||
@@ -1,658 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Trasforma i blocchi della newsletter (JSON di YogiBook) in HTML email
|
||||
* compatibile con il composer di The Newsletter (editor = 2):
|
||||
* ogni blocco ha il wrapper "tnpc-row" e le sue opzioni in data-json (base64),
|
||||
* così in WordPress la newsletter resta modificabile nel composer.
|
||||
*
|
||||
* Funziona anche al contrario: fromComposerHtml() importa una newsletter del plugin.
|
||||
*/
|
||||
class NewsletterRenderer
|
||||
{
|
||||
public const WIDTH = 600;
|
||||
|
||||
public const BLOCK_TYPES = ['header', 'heading', 'text', 'image', 'button', 'columns', 'separator', 'social', 'footer', 'raw'];
|
||||
|
||||
/** Impostazioni grafiche predefinite (come le newsletter YogaSoul attuali). */
|
||||
public static function defaultSettings(): array
|
||||
{
|
||||
return [
|
||||
'background' => '#f4f4f4',
|
||||
'block_background' => '#ffffff',
|
||||
'font_family' => 'Verdana, Geneva, sans-serif',
|
||||
'font_size' => 16,
|
||||
'text_color' => '#222222',
|
||||
'link_color' => '#007bff',
|
||||
'title_font_size' => 28,
|
||||
'title_color' => '#222222',
|
||||
'button_background' => '#256f9c',
|
||||
'button_color' => '#ffffff',
|
||||
'social' => [
|
||||
'facebook' => 'https://www.facebook.com/yogasoul.it/',
|
||||
'instagram' => 'https://www.instagram.com/aura_yogasoul/',
|
||||
'youtube' => 'https://www.youtube.com/@yogasoul6746',
|
||||
'whatsapp' => 'https://wa.me/393534982534',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** Blocchi di una newsletter nuova. */
|
||||
public static function starterBlocks(): array
|
||||
{
|
||||
return [
|
||||
self::newBlock('header'),
|
||||
self::newBlock('text', ['html' => '<h1 style="text-align: center;">Ciao {name}</h1><p>come stai?</p><p>Scrivi qui il testo della newsletter…</p>']),
|
||||
self::newBlock('image'),
|
||||
self::newBlock('button'),
|
||||
self::newBlock('separator'),
|
||||
self::newBlock('social'),
|
||||
self::newBlock('footer'),
|
||||
];
|
||||
}
|
||||
|
||||
/** Valori predefiniti per tipo di blocco. */
|
||||
public static function newBlock(string $type, array $values = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'header' => ['logo_url' => 'https://yogasoul.it/wp-content/uploads/2025/01/Logo-YogaSoul_parziale-1.png', 'logo_width' => 80, 'link' => 'https://yogasoul.it', 'pad_top' => 24, 'pad_bottom' => 24],
|
||||
'heading' => ['text' => 'Titolo', 'align' => 'center', 'font_size' => '', 'color' => '', 'background' => '', 'pad_top' => 30, 'pad_bottom' => 10],
|
||||
'text' => ['html' => '<p>Testo…</p>', 'background' => '', 'background_2' => '', 'pad_top' => 20, 'pad_bottom' => 20],
|
||||
'image' => ['url' => '', 'alt' => '', 'link' => '', 'width' => 600, 'pad_top' => 0, 'pad_bottom' => 0],
|
||||
'button' => ['label' => 'Prenota la tua lezione', 'url' => 'https://yogasoul.it', 'button_bg' => '', 'color' => '', 'font_size' => 20, 'width' => 300, 'radius' => 6, 'pad_top' => 20, 'pad_bottom' => 20],
|
||||
'columns' => ['image_url' => '', 'image_alt' => '', 'link' => '', 'image_side' => 'left', 'html' => '<p>Testo accanto all\'immagine…</p>', 'pad_top' => 20, 'pad_bottom' => 20],
|
||||
'separator' => ['color' => '#dddddd', 'height' => 1, 'pad_top' => 20, 'pad_bottom' => 20],
|
||||
'social' => ['icon_size' => 32, 'pad_top' => 15, 'pad_bottom' => 15],
|
||||
'footer' => ['profile_label' => "Modifica l'iscrizione", 'view_label' => 'Vedi online', 'unsub_label' => 'Disiscriviti', 'pad_top' => 15, 'pad_bottom' => 15],
|
||||
'raw' => ['html' => '', 'wp_id' => ''],
|
||||
];
|
||||
|
||||
return array_merge(
|
||||
['id' => bin2hex(random_bytes(6)), 'type' => $type],
|
||||
$defaults[$type] ?? [],
|
||||
$values
|
||||
);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* Rendering
|
||||
* ===================================================================== */
|
||||
|
||||
/**
|
||||
* HTML completo della newsletter.
|
||||
* $forPreview = true sostituisce i tag del plugin con valori di esempio.
|
||||
*/
|
||||
public static function renderDocument(array $blocks, array $settings, string $subject, string $preheader, bool $forPreview = false): string
|
||||
{
|
||||
$s = self::settings($settings);
|
||||
|
||||
$head = file_get_contents(__DIR__ . '/newsletter_head.html');
|
||||
$head = str_replace('{{LINK_COLOR}}', self::color($s['link_color'], '#007bff'), $head);
|
||||
|
||||
$body = '';
|
||||
foreach ($blocks as $b) {
|
||||
if (is_array($b) && isset($b['type'])) {
|
||||
$body .= self::renderBlock($b, $s) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$bg = self::color($s['background'], '#f4f4f4');
|
||||
|
||||
$html = $head
|
||||
. '<body style="margin: 0; padding: 0; line-height: normal; word-spacing: normal;" dir="ltr">' . "\n"
|
||||
. '<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">' . self::h($preheader) . "</div>\n\n"
|
||||
. "<table cellpadding='0' cellspacing='0' border='0' width='100%' role='presentation'>\n<tr>\n"
|
||||
. "<td bgcolor='{$bg}' valign='top' class='wrapper'><!-- tnp -->"
|
||||
. $body
|
||||
. "<!-- /tnp -->\n</td>\n</tr>\n</table>\n\n</body>\n</html>";
|
||||
|
||||
if ($forPreview) {
|
||||
$html = strtr($html, [
|
||||
'{name}' => 'Maria',
|
||||
'{email_subject}' => self::h($subject),
|
||||
'{unsubscription_url}' => '#',
|
||||
'{profile_url}' => '#',
|
||||
'{email_url}' => '#',
|
||||
'{subscription_confirm_url}' => '#',
|
||||
]);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/** Versione solo testo (campo message_text del plugin). */
|
||||
public static function renderText(array $blocks, string $preheader): string
|
||||
{
|
||||
$out = [];
|
||||
if ($preheader !== '') $out[] = $preheader;
|
||||
|
||||
foreach ($blocks as $b) {
|
||||
switch ($b['type'] ?? '') {
|
||||
case 'heading':
|
||||
$out[] = strtoupper(trim((string) ($b['text'] ?? '')));
|
||||
break;
|
||||
case 'text':
|
||||
case 'columns':
|
||||
case 'raw':
|
||||
$out[] = self::htmlToText((string) ($b['html'] ?? ''));
|
||||
break;
|
||||
case 'button':
|
||||
$out[] = trim(strip_tags(str_ireplace(['<br>', '<br/>', '<br />'], ' ', (string) ($b['label'] ?? '')))) . ': ' . ($b['url'] ?? '');
|
||||
break;
|
||||
case 'image':
|
||||
if (!empty($b['link'])) $out[] = ($b['alt'] ?: 'Immagine') . ' (' . $b['link'] . ')';
|
||||
break;
|
||||
case 'footer':
|
||||
$out[] = "Modifica l'iscrizione: {profile_url}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return trim(preg_replace("/\n{3,}/", "\n\n", implode("\n\n", array_filter($out, fn($x) => trim($x) !== ''))));
|
||||
}
|
||||
|
||||
public static function renderBlock(array $b, array $s): string
|
||||
{
|
||||
$type = $b['type'];
|
||||
|
||||
if ($type === 'raw') {
|
||||
return (string) ($b['html'] ?? '');
|
||||
}
|
||||
|
||||
$padTop = (int) ($b['pad_top'] ?? 15);
|
||||
$padBottom = (int) ($b['pad_bottom'] ?? 15);
|
||||
$bg = self::color($b['background'] ?? '', self::color($s['block_background'], '#ffffff'));
|
||||
// Sfumatura (solo blocchi di testo, come fa il composer)
|
||||
$bg2 = in_array($type, ['text', 'columns'], true) ? self::color($b['background_2'] ?? '', '') : '';
|
||||
$bgCss = 'background-color: ' . $bg . ';' . ($bg2 !== '' ? ' background-image: linear-gradient(180deg, ' . $bg . ', ' . $bg2 . ');' : '');
|
||||
|
||||
switch ($type) {
|
||||
case 'header':
|
||||
[$inner, $padLR, $wpId, $opts] = self::header($b, $s);
|
||||
break;
|
||||
case 'heading':
|
||||
[$inner, $padLR, $wpId, $opts] = self::heading($b, $s);
|
||||
break;
|
||||
case 'text':
|
||||
[$inner, $padLR, $wpId, $opts] = self::text($b, $s);
|
||||
break;
|
||||
case 'image':
|
||||
[$inner, $padLR, $wpId, $opts] = self::image($b, $s);
|
||||
break;
|
||||
case 'button':
|
||||
[$inner, $padLR, $wpId, $opts] = self::button($b, $s);
|
||||
break;
|
||||
case 'columns':
|
||||
[$inner, $padLR, $wpId, $opts] = self::columns($b, $s);
|
||||
break;
|
||||
case 'separator':
|
||||
[$inner, $padLR, $wpId, $opts] = self::separator($b, $s);
|
||||
break;
|
||||
case 'social':
|
||||
[$inner, $padLR, $wpId, $opts] = self::social($b, $s);
|
||||
break;
|
||||
case 'footer':
|
||||
[$inner, $padLR, $wpId, $opts] = self::footer($b, $s);
|
||||
break;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
$opts = array_merge([
|
||||
'block_padding_top' => $padTop,
|
||||
'block_padding_bottom' => $padBottom,
|
||||
'block_padding_right' => $padLR,
|
||||
'block_padding_left' => $padLR,
|
||||
'block_background' => self::color($b['background'] ?? '', ''),
|
||||
'block_background_2' => $bg2,
|
||||
'block_width' => (string) self::WIDTH,
|
||||
'block_align' => 'center',
|
||||
'block_border_color' => '',
|
||||
'block_border_radius' => '',
|
||||
'inline_edits' => '',
|
||||
'block_background_angle' => '180',
|
||||
'block_background_wide' => '0',
|
||||
'block_class' => '',
|
||||
], $opts, ['block_id' => $wpId]);
|
||||
|
||||
$json = base64_encode(json_encode($opts));
|
||||
$w = self::WIDTH;
|
||||
|
||||
// Colonna centrale di 600px con lo sfondo del blocco, fuori lo sfondo generale
|
||||
// (come le newsletter YogaSoul attuali: block_background_wide = 0)
|
||||
return '<table border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="border-collapse: collapse; width: 100%;" class="tnpc-row tnpc-row-block" data-id="' . $wpId . '">' . "\n"
|
||||
. '<tbody><tr><td align="center" style="padding: 0;" class="edit-block">'
|
||||
. '<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" align="center" cellspacing="0" width="' . $w . '"><tr><td width="' . $w . '" style="vertical-align:top;width:' . $w . 'px;"><![endif]-->'
|
||||
. '<table role="presentation" type="options" data-json="' . $json . '" class="tnpc-block-content" border="0" cellpadding="0" align="center" cellspacing="0" width="100%" style="border-collapse: separate !important; width: 100%!important; max-width: ' . $w . 'px!important">' . "\n"
|
||||
. '<tbody><tr><td align="center" style="text-align: center; line-height: normal !important; letter-spacing: normal; mso-line-height-rule: exactly; outline: none; padding: ' . $padTop . 'px ' . $padLR . 'px ' . $padBottom . 'px ' . $padLR . 'px;' . $bgCss . '" bgcolor="' . $bg . '" class="">'
|
||||
. $inner
|
||||
. '</td></tr></tbody></table><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
/* ---- singoli blocchi: [html interno, padding laterale, id composer, opzioni composer] ---- */
|
||||
|
||||
private static function header(array $b, array $s): array
|
||||
{
|
||||
$logo = self::url($b['logo_url'] ?? '');
|
||||
$width = max(20, min(600, (int) ($b['logo_width'] ?? 80)));
|
||||
$link = self::url($b['link'] ?? '');
|
||||
|
||||
$img = $logo !== ''
|
||||
? '<img src="' . self::h($logo) . '" width="' . $width . '" border="0" alt="YogaSoul" style="display:inline-block; height:auto; max-width:100%;">'
|
||||
: '';
|
||||
$inner = $link !== '' && $img !== '' ? '<a href="' . self::h($link) . '" target="_blank">' . $img . '</a>' : $img;
|
||||
|
||||
return [$inner, 16, 'header', [
|
||||
'font_family' => '', 'font_size' => '', 'font_color' => '', 'font_weight' => '',
|
||||
'logo_width' => (string) $width, 'block_layout' => 'logo', 'block_style' => '',
|
||||
'title_font_family' => '', 'title_font_size' => '', 'title_font_weight' => '', 'title_font_color' => '',
|
||||
]];
|
||||
}
|
||||
|
||||
private static function heading(array $b, array $s): array
|
||||
{
|
||||
$size = (int) ($b['font_size'] ?: $s['title_font_size']);
|
||||
$color = self::color($b['color'] ?? '', self::color($s['title_color'], '#222222'));
|
||||
$align = in_array($b['align'] ?? '', ['left', 'center', 'right'], true) ? $b['align'] : 'center';
|
||||
$font = self::font($s['font_family']);
|
||||
$text = (string) ($b['text'] ?? '');
|
||||
|
||||
$inner = '<table border="0" cellspacing="0" cellpadding="0" width="100%" role="presentation"><tbody><tr>'
|
||||
. '<td align="' . $align . '" valign="middle" dir="ltr" style="text-align: ' . $align . ';">'
|
||||
. '<div style="font-size: ' . $size . 'px;font-family: ' . $font . ';font-weight: bold;color: ' . $color . '; padding: 0; line-height: 130% !important; letter-spacing: normal;" role="heading" aria-level="1">'
|
||||
. nl2br(self::h($text))
|
||||
. '</div></td></tr></tbody></table>';
|
||||
|
||||
return [$inner, 15, 'heading', [
|
||||
'text' => $text, 'align' => $align, 'font_family' => '', 'font_size' => (string) $size,
|
||||
'font_color' => $color, 'font_weight' => 'bold',
|
||||
]];
|
||||
}
|
||||
|
||||
private static function text(array $b, array $s): array
|
||||
{
|
||||
$html = self::cleanHtml((string) ($b['html'] ?? ''));
|
||||
$inner = self::textCell($html, $s);
|
||||
|
||||
return [$inner, 15, 'text', ['html' => $html]];
|
||||
}
|
||||
|
||||
private static function textCell(string $html, array $s): string
|
||||
{
|
||||
return '<table width="100%" style="width: 100%!important" border="0" cellpadding="0" cellspacing="0"><tbody><tr>'
|
||||
. '<td width="100%" valign="top" align="left" class="text" style="mso-line-height-rule: exactly; font-size: ' . (int) $s['font_size'] . 'px;font-family: ' . self::font($s['font_family']) . ';font-weight: normal;color: ' . self::color($s['text_color'], '#222222') . '; line-height: 1.5;">'
|
||||
. $html
|
||||
. '</td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
private static function image(array $b, array $s): array
|
||||
{
|
||||
$url = self::url($b['url'] ?? '');
|
||||
$alt = (string) ($b['alt'] ?? '');
|
||||
$link = self::url($b['link'] ?? '');
|
||||
$width = max(50, min(self::WIDTH, (int) ($b['width'] ?? self::WIDTH)));
|
||||
|
||||
if ($url === '') {
|
||||
$inner = '<div style="padding:30px;border:2px dashed #cccccc;color:#999999;font-family:Arial,sans-serif;font-size:14px;">Immagine non ancora scelta</div>';
|
||||
} else {
|
||||
$img = '<img src="' . self::h($url) . '" width="' . $width . '" alt="' . self::h($alt) . '" border="0" style="display: block; margin: 0 auto; height: auto; max-width: ' . $width . 'px !important; width: 100%; padding: 0; border: 0; font-size: 12px">';
|
||||
$inner = $link !== '' ? '<a href="' . self::h($link) . '" target="_blank" style="display:block;">' . $img . '</a>' : $img;
|
||||
}
|
||||
|
||||
return [$inner, 0, 'image', [
|
||||
'image' => ['id' => '', 'url' => $url],
|
||||
'image-alt' => '', 'image_alt' => $alt, 'url' => $link, 'image-url' => $link,
|
||||
'width' => (string) $width, 'align' => 'center', 'placeholder' => '',
|
||||
]];
|
||||
}
|
||||
|
||||
private static function button(array $b, array $s): array
|
||||
{
|
||||
$label = self::cleanHtml((string) ($b['label'] ?? ''), '<br><strong><em><b><i>');
|
||||
$url = self::url($b['url'] ?? '');
|
||||
$bg = self::color($b['button_bg'] ?? '', self::color($s['button_background'], '#256f9c'));
|
||||
$color = self::color($b['color'] ?? '', self::color($s['button_color'], '#ffffff'));
|
||||
$size = max(10, min(60, (int) ($b['font_size'] ?? 20)));
|
||||
$width = max(80, min(self::WIDTH, (int) ($b['width'] ?? 300)));
|
||||
$radius = max(0, min(50, (int) ($b['radius'] ?? 6)));
|
||||
$font = self::font($s['font_family']);
|
||||
|
||||
$inner = '<table border="0" cellpadding="0" cellspacing="0" role="presentation" align="center" style="border-collapse:separate !important;line-height:100%;width:' . $width . 'px;max-width:100%;"><tbody><tr>'
|
||||
. '<td align="center" bgcolor="' . $bg . '" role="presentation" style="border-collapse:separate !important;cursor:auto;mso-padding-alt:12px 25px;background:' . $bg . ';border-radius:' . $radius . 'px;" valign="middle">'
|
||||
. '<a href="' . self::h($url) . '" style="display:inline-block;color:' . $color . ';font-family:' . $font . ';font-size:' . $size . 'px;font-weight:bold;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 25px;mso-padding-alt:0px;border-radius:' . $radius . 'px;" target="_blank">'
|
||||
. $label
|
||||
. '</a></td></tr></tbody></table>';
|
||||
|
||||
return [$inner, 0, 'cta', [
|
||||
'button_label' => $label, 'button_url' => $url,
|
||||
'button_font_family' => $s['font_family'], 'button_font_size' => (string) $size, 'button_font_weight' => 'bold',
|
||||
'button_font_color' => $color, 'button_background' => $bg, 'button_border_color' => '',
|
||||
'align' => 'center', 'button_width' => (string) $width, 'button_align' => 'center',
|
||||
'block_style' => '', 'button_border_radius' => (string) $radius, 'list' => '', 'unlist' => '',
|
||||
]];
|
||||
}
|
||||
|
||||
/** Immagine + testo affiancati (in WordPress diventa un blocco "testo"). */
|
||||
private static function columns(array $b, array $s): array
|
||||
{
|
||||
$url = self::url($b['image_url'] ?? '');
|
||||
$alt = self::h($b['image_alt'] ?? '');
|
||||
$link = self::url($b['link'] ?? '');
|
||||
$html = self::cleanHtml((string) ($b['html'] ?? ''));
|
||||
$right = ($b['image_side'] ?? 'left') === 'right';
|
||||
|
||||
$img = $url !== ''
|
||||
? '<img src="' . self::h($url) . '" width="270" alt="' . $alt . '" border="0" style="display:block;width:100%;max-width:270px;height:auto;border:0;">'
|
||||
: '<div style="padding:30px 10px;border:2px dashed #cccccc;color:#999999;font-size:13px;text-align:center;">Immagine</div>';
|
||||
if ($url !== '' && $link !== '') {
|
||||
$img = '<a href="' . self::h($link) . '" target="_blank">' . $img . '</a>';
|
||||
}
|
||||
|
||||
$imgCell = '<td class="responsive" width="50%" valign="top" style="padding:0 10px 10px 10px;">' . $img . '</td>';
|
||||
$textCell = '<td class="responsive" width="50%" valign="top" style="padding:0 10px 10px 10px;text-align:left;">' . $html . '</td>';
|
||||
|
||||
$table = '<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"><tbody><tr>'
|
||||
. ($right ? $textCell . $imgCell : $imgCell . $textCell)
|
||||
. '</tr></tbody></table>';
|
||||
|
||||
return [self::textCell($table, $s), 15, 'text', ['html' => $table]];
|
||||
}
|
||||
|
||||
private static function separator(array $b, array $s): array
|
||||
{
|
||||
$color = self::color($b['color'] ?? '', '#dddddd');
|
||||
$height = max(1, min(10, (int) ($b['height'] ?? 1)));
|
||||
|
||||
$inner = '<table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr>'
|
||||
. '<td style="border-bottom: ' . $height . 'px solid ' . $color . '; font-size:0; line-height:0;"> </td>'
|
||||
. '</tr></tbody></table>';
|
||||
|
||||
return [$inner, 20, 'separator', ['color' => $color, 'height' => $height]];
|
||||
}
|
||||
|
||||
private static function social(array $b, array $s): array
|
||||
{
|
||||
$size = max(16, min(64, (int) ($b['icon_size'] ?? 32)));
|
||||
$links = '';
|
||||
|
||||
foreach (['facebook', 'youtube', 'instagram', 'whatsapp'] as $net) {
|
||||
$url = self::url($s['social'][$net] ?? '');
|
||||
if ($url === '') continue;
|
||||
$links .= '<a href="' . self::h($url) . '" style="line-height: normal; text-decoration: none;" target="_blank">'
|
||||
. '<img src="https://yogasoul.it/wp-content/plugins/newsletter/images/social-1/' . $net . '.png" width="' . $size . '" height="' . $size . '" alt="' . $net . '" border="0"></a> ';
|
||||
}
|
||||
|
||||
$inner = '<table border="0" cellspacing="0" cellpadding="0" width="100%" role="presentation"><tbody><tr><td align="center" valign="middle">'
|
||||
. $links . '</td></tr></tbody></table>';
|
||||
|
||||
return [$inner, 15, 'social', ['type' => '1', 'width' => (string) $size]];
|
||||
}
|
||||
|
||||
private static function footer(array $b, array $s): array
|
||||
{
|
||||
$style = 'font-size: 13px; font-family: Helvetica, Arial, sans-serif; font-weight: normal; color: #444444; text-decoration: none;';
|
||||
$sep = '<span style="' . $style . '"> | </span>';
|
||||
|
||||
$parts = [];
|
||||
if (trim((string) ($b['profile_label'] ?? '')) !== '') {
|
||||
$parts[] = '<a style="' . $style . '" href="{profile_url}">' . self::h($b['profile_label']) . '</a>';
|
||||
}
|
||||
if (trim((string) ($b['view_label'] ?? '')) !== '') {
|
||||
$parts[] = '<a style="' . $style . '" href="{email_url}">' . self::h($b['view_label']) . '</a>';
|
||||
}
|
||||
if (trim((string) ($b['unsub_label'] ?? '')) !== '') {
|
||||
$parts[] = '<a style="' . $style . '" href="{unsubscription_url}">' . self::h($b['unsub_label']) . '</a>';
|
||||
}
|
||||
|
||||
return [implode($sep, $parts), 15, 'footer', [
|
||||
'view' => (string) ($b['view_label'] ?? ''), 'profile' => (string) ($b['profile_label'] ?? ''),
|
||||
'font_family' => 'Helvetica, Arial, sans-serif', 'font_size' => '13', 'font_color' => '#444444', 'font_weight' => 'normal',
|
||||
]];
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* Import da The Newsletter (HTML del composer -> blocchi YogiBook)
|
||||
* ===================================================================== */
|
||||
|
||||
public static function fromComposerHtml(string $html, string $options = ''): array
|
||||
{
|
||||
$blocks = [];
|
||||
|
||||
$start = strpos($html, '<!-- tnp -->');
|
||||
$end = strrpos($html, '<!-- /tnp -->');
|
||||
$body = ($start !== false && $end !== false) ? substr($html, $start + 12, $end - $start - 12) : $html;
|
||||
|
||||
// Ogni blocco inizia con la tabella "tnpc-row"
|
||||
preg_match_all('/<table[^>]*class="[^"]*tnpc-row[^"]*"[^>]*>/i', $body, $m, PREG_OFFSET_CAPTURE);
|
||||
$starts = array_map(fn($x) => $x[1], $m[0]);
|
||||
|
||||
foreach ($starts as $i => $pos) {
|
||||
$chunk = substr($body, $pos, isset($starts[$i + 1]) ? $starts[$i + 1] - $pos : null);
|
||||
$chunk = rtrim($chunk);
|
||||
|
||||
preg_match('/data-id="([^"]+)"/', $chunk, $idm);
|
||||
preg_match('/data-json="([^"]+)"/', $chunk, $jm);
|
||||
$wpId = $idm[1] ?? '';
|
||||
$o = $jm ? (json_decode(base64_decode($jm[1]), true) ?: []) : [];
|
||||
|
||||
$pad = ['pad_top' => (int) ($o['block_padding_top'] ?? 15), 'pad_bottom' => (int) ($o['block_padding_bottom'] ?? 15)];
|
||||
$bg = ['background' => (string) ($o['block_background'] ?? '')];
|
||||
|
||||
switch ($wpId) {
|
||||
case 'text':
|
||||
$blocks[] = self::newBlock('text', $pad + $bg + [
|
||||
'html' => (string) ($o['html'] ?? ''),
|
||||
'background_2' => (string) ($o['block_background_2'] ?? ''),
|
||||
]);
|
||||
break;
|
||||
case 'heading':
|
||||
$blocks[] = self::newBlock('heading', $pad + $bg + [
|
||||
'text' => (string) ($o['text'] ?? ''), 'align' => $o['align'] ?? 'center',
|
||||
'font_size' => (string) ($o['font_size'] ?? ''), 'color' => (string) ($o['font_color'] ?? ''),
|
||||
]);
|
||||
break;
|
||||
case 'image':
|
||||
$blocks[] = self::newBlock('image', $pad + [
|
||||
'url' => (string) ($o['image']['url'] ?? ''), 'alt' => (string) ($o['image_alt'] ?? ''),
|
||||
'link' => (string) ($o['url'] ?? ''),
|
||||
'width' => min(self::WIDTH, (int) ($o['width'] ?? self::WIDTH) ?: self::WIDTH),
|
||||
]);
|
||||
break;
|
||||
case 'cta':
|
||||
$blocks[] = self::newBlock('button', $pad + [
|
||||
'label' => (string) ($o['button_label'] ?? ''), 'url' => (string) ($o['button_url'] ?? ''),
|
||||
'button_bg' => (string) ($o['button_background'] ?? ''), 'color' => (string) ($o['button_font_color'] ?? ''),
|
||||
'font_size' => (int) ($o['button_font_size'] ?? 20) ?: 20,
|
||||
'width' => (int) ($o['button_width'] ?? 300) ?: 300,
|
||||
'radius' => (int) ($o['button_border_radius'] ?? 0),
|
||||
]);
|
||||
break;
|
||||
case 'separator':
|
||||
$blocks[] = self::newBlock('separator', $pad + ['color' => (string) ($o['color'] ?? '#dddddd'), 'height' => (int) ($o['height'] ?? 1) ?: 1]);
|
||||
break;
|
||||
case 'social':
|
||||
$blocks[] = self::newBlock('social', $pad + ['icon_size' => (int) ($o['width'] ?? 32) ?: 32]);
|
||||
break;
|
||||
case 'footer':
|
||||
$blocks[] = self::newBlock('footer', $pad + [
|
||||
'profile_label' => (string) ($o['profile'] ?? "Modifica l'iscrizione"),
|
||||
'view_label' => (string) ($o['view'] ?? 'Vedi online'),
|
||||
'unsub_label' => '',
|
||||
]);
|
||||
break;
|
||||
case 'header':
|
||||
preg_match('/<img[^>]*src="([^"]+)"/i', $chunk, $im);
|
||||
preg_match('/<a[^>]*href="([^"]+)"/i', $chunk, $am);
|
||||
$blocks[] = self::newBlock('header', $pad + [
|
||||
'logo_url' => $im[1] ?? '', 'link' => $am[1] ?? '', 'logo_width' => (int) ($o['logo_width'] ?? 80) ?: 80,
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
// Blocco che YogiBook non sa modificare: lo conserviamo identico
|
||||
$blocks[] = self::newBlock('raw', ['html' => $chunk, 'wp_id' => $wpId]);
|
||||
}
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/** Impostazioni grafiche ricavate dalle opzioni serializzate del plugin. */
|
||||
public static function settingsFromWpOptions(array $o): array
|
||||
{
|
||||
$s = self::defaultSettings();
|
||||
$map = [
|
||||
'composer_background' => 'background',
|
||||
'composer_block_background' => 'block_background',
|
||||
'composer_text_font_family' => 'font_family',
|
||||
'composer_text_font_size' => 'font_size',
|
||||
'composer_text_font_color' => 'text_color',
|
||||
'composer_title_font_color' => 'title_color',
|
||||
'composer_button_background_color' => 'button_background',
|
||||
'composer_button_font_color' => 'button_color',
|
||||
];
|
||||
foreach ($map as $wp => $ours) {
|
||||
if (!empty($o[$wp])) $s[$ours] = is_numeric($o[$wp]) ? (int) $o[$wp] : (string) $o[$wp];
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
/** Opzioni "composer_*" da scrivere nel plugin. */
|
||||
public static function wpComposerOptions(array $settings): array
|
||||
{
|
||||
$s = self::settings($settings);
|
||||
return [
|
||||
'composer_background' => $s['background'],
|
||||
'composer_block_background' => $s['block_background'],
|
||||
'composer_width' => (string) self::WIDTH,
|
||||
'composer_text_font_family' => $s['font_family'],
|
||||
'composer_text_font_size' => (string) $s['font_size'],
|
||||
'composer_text_font_weight' => 'normal',
|
||||
'composer_text_font_color' => $s['text_color'],
|
||||
'composer_title_font_family' => $s['font_family'],
|
||||
'composer_title_font_size' => (string) $s['title_font_size'],
|
||||
'composer_title_font_weight' => 'bold',
|
||||
'composer_title_font_color' => $s['title_color'],
|
||||
'composer_button_font_family' => $s['font_family'],
|
||||
'composer_button_font_size' => '16',
|
||||
'composer_button_font_weight' => 'bold',
|
||||
'composer_button_font_color' => $s['button_color'],
|
||||
'composer_button_background_color' => $s['button_background'],
|
||||
'composer_button_background' => $s['button_background'],
|
||||
'composer_button_border_color' => $s['button_background'],
|
||||
];
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* Controlli prima di pubblicare
|
||||
* ===================================================================== */
|
||||
|
||||
public static function checks(array $blocks, string $subject, string $preheader, string $html): array
|
||||
{
|
||||
$warn = [];
|
||||
$len = fn($t) => mb_strlen(trim($t));
|
||||
|
||||
if ($len($subject) === 0) $warn[] = ['error', "Manca l'oggetto."];
|
||||
elseif ($len($subject) > 70) $warn[] = ['warning', "Oggetto lungo (" . $len($subject) . " caratteri): sul telefono viene tagliato verso i 40-60."];
|
||||
|
||||
if ($len($preheader) === 0) $warn[] = ['warning', "Manca il preheader (la riga di anteprima sotto l'oggetto)."];
|
||||
elseif ($len($preheader) > 120) $warn[] = ['warning', 'Preheader molto lungo: viene mostrato solo l\'inizio.'];
|
||||
|
||||
$hasFooter = false;
|
||||
foreach ($blocks as $i => $b) {
|
||||
$n = $i + 1;
|
||||
switch ($b['type'] ?? '') {
|
||||
case 'image':
|
||||
if (empty($b['url'])) $warn[] = ['error', "Blocco $n: immagine non scelta."];
|
||||
elseif (trim((string) ($b['alt'] ?? '')) === '') $warn[] = ['warning', "Blocco $n: immagine senza testo alternativo (serve se le immagini sono bloccate)."];
|
||||
break;
|
||||
case 'columns':
|
||||
if (empty($b['image_url'])) $warn[] = ['warning', "Blocco $n: colonne senza immagine."];
|
||||
break;
|
||||
case 'button':
|
||||
if (trim((string) ($b['url'] ?? '')) === '' || ($b['url'] ?? '') === '#') $warn[] = ['error', "Blocco $n: pulsante senza link."];
|
||||
break;
|
||||
case 'footer':
|
||||
$hasFooter = true;
|
||||
break;
|
||||
case 'raw':
|
||||
if (preg_match('/unsubscription_url|profile_url/', (string) ($b['html'] ?? ''))) $hasFooter = true;
|
||||
break;
|
||||
}
|
||||
$all = json_encode($b);
|
||||
if (strpos($all, 'data:image') !== false) $warn[] = ['error', "Blocco $n: contiene un'immagine incollata dentro il testo. Usa un blocco immagine."];
|
||||
if (preg_match('/src=\\\\?"http:\/\//', $all)) $warn[] = ['warning', "Blocco $n: immagine con indirizzo http (non sicuro)."];
|
||||
}
|
||||
|
||||
if (!$hasFooter) $warn[] = ['error', 'Manca il piè di pagina con il link per gestire o annullare l\'iscrizione.'];
|
||||
|
||||
$kb = (int) round(strlen($html) / 1024);
|
||||
if ($kb > 100) $warn[] = ['error', "La newsletter pesa {$kb} KB: Gmail taglia i messaggi oltre 102 KB e nasconde il fondo (con il link di disiscrizione)."];
|
||||
elseif ($kb > 85) $warn[] = ['warning', "La newsletter pesa {$kb} KB: vicina al limite di Gmail (102 KB)."];
|
||||
|
||||
return $warn;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* Utilità
|
||||
* ===================================================================== */
|
||||
|
||||
public static function settings(array $settings): array
|
||||
{
|
||||
$s = array_merge(self::defaultSettings(), $settings);
|
||||
$s['social'] = array_merge(self::defaultSettings()['social'], is_array($settings['social'] ?? null) ? $settings['social'] : []);
|
||||
$s['font_size'] = max(12, min(22, (int) $s['font_size']));
|
||||
$s['title_font_size'] = max(16, min(60, (int) $s['title_font_size']));
|
||||
return $s;
|
||||
}
|
||||
|
||||
public static function h($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/** Colore esadecimale valido oppure $fallback. */
|
||||
public static function color($c, string $fallback): string
|
||||
{
|
||||
$c = trim((string) $c);
|
||||
return preg_match('/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/', $c) ? $c : $fallback;
|
||||
}
|
||||
|
||||
/** Solo link http(s), mailto, tel o tag del plugin. */
|
||||
public static function url($u): string
|
||||
{
|
||||
$u = trim((string) $u);
|
||||
if ($u === '') return '';
|
||||
if (preg_match('#^(https?://|mailto:|tel:|\{[a-z_]+\})#i', $u)) return $u;
|
||||
if (preg_match('#^[\w.-]+\.[a-z]{2,}#i', $u)) return 'https://' . $u;
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function font($f): string
|
||||
{
|
||||
$f = preg_replace('/[^a-zA-Z0-9 ,\'-]/', '', (string) $f);
|
||||
return $f !== '' ? $f : 'Verdana, Geneva, sans-serif';
|
||||
}
|
||||
|
||||
/** Toglie script, eventi e altri pericoli dall'HTML scritto nell'editor. */
|
||||
public static function cleanHtml(string $html, ?string $allowed = null): string
|
||||
{
|
||||
$html = preg_replace('#<(script|style|iframe|object|embed|form)[^>]*>.*?</\1>#is', '', $html);
|
||||
$html = preg_replace('#<(script|style|iframe|object|embed|form|input|button|meta|link)[^>]*/?>#i', '', $html);
|
||||
$html = preg_replace('/\son\w+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $html);
|
||||
$html = preg_replace('/(href|src)\s*=\s*(["\'])\s*javascript:[^"\']*\2/i', '$1="#"', $html);
|
||||
if ($allowed !== null) $html = strip_tags($html, $allowed);
|
||||
return trim($html);
|
||||
}
|
||||
|
||||
private static function htmlToText(string $html): string
|
||||
{
|
||||
$html = preg_replace('#<a[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>#is', '$2 ($1)', $html);
|
||||
$html = preg_replace('#<br\s*/?>#i', "\n", $html);
|
||||
$html = preg_replace('#</(p|h[1-6]|div|li|tr)>#i', "\n", $html);
|
||||
$html = preg_replace('#<li[^>]*>#i', '- ', $html);
|
||||
$text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$text = preg_replace("/[ \t\x{00A0}]+/u", ' ', $text);
|
||||
$text = preg_replace("/ *\n */", "\n", $text);
|
||||
return trim(preg_replace("/\n{3,}/", "\n\n", $text));
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Collegamento al database di WordPress per The Newsletter.
|
||||
*
|
||||
* Usa un utente MySQL dedicato che può leggere/scrivere SOLO la tabella delle newsletter.
|
||||
* Variabili nel .env di YogiBook:
|
||||
* WP_DB_HOST, WP_DB_NAME, WP_DB_USER, WP_DB_PASSWORD, WP_DB_PREFIX (default wp_),
|
||||
* WP_ADMIN_URL (default https://yogasoul.it/wp-admin/)
|
||||
*/
|
||||
class NewsletterWp
|
||||
{
|
||||
private static ?PDO $pdo = null;
|
||||
|
||||
public static function isConfigured(): bool
|
||||
{
|
||||
return !empty($_ENV['WP_DB_NAME']) && !empty($_ENV['WP_DB_USER']);
|
||||
}
|
||||
|
||||
public static function adminUrl(): string
|
||||
{
|
||||
return rtrim($_ENV['WP_ADMIN_URL'] ?? 'https://yogasoul.it/wp-admin/', '/') . '/admin.php?page=newsletter_emails_index';
|
||||
}
|
||||
|
||||
private static function table(): string
|
||||
{
|
||||
$prefix = preg_replace('/[^a-zA-Z0-9_]/', '', $_ENV['WP_DB_PREFIX'] ?? 'wp_');
|
||||
return '`' . $prefix . 'newsletter_emails`';
|
||||
}
|
||||
|
||||
private static function db(): PDO
|
||||
{
|
||||
if (!self::isConfigured()) {
|
||||
throw new RuntimeException('Collegamento a WordPress non configurato (manca WP_DB_* nel file .env).');
|
||||
}
|
||||
if (self::$pdo === null) {
|
||||
$host = $_ENV['WP_DB_HOST'] ?? 'localhost';
|
||||
$port = '3306';
|
||||
if (strpos($host, ':') !== false) {
|
||||
[$host, $port] = explode(':', $host, 2);
|
||||
}
|
||||
try {
|
||||
self::$pdo = new PDO(
|
||||
"mysql:host={$host};port={$port};dbname={$_ENV['WP_DB_NAME']};charset=utf8mb4",
|
||||
$_ENV['WP_DB_USER'],
|
||||
$_ENV['WP_DB_PASSWORD'] ?? '',
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_TIMEOUT => 5,
|
||||
]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
error_log('NewsletterWp: connessione fallita: ' . $e->getMessage());
|
||||
throw new RuntimeException('Collegamento al database di WordPress non riuscito: controlla WP_DB_* nel file .env.');
|
||||
}
|
||||
}
|
||||
return self::$pdo;
|
||||
}
|
||||
|
||||
private static function unserializeOptions(?string $raw): array
|
||||
{
|
||||
if ($raw === null || $raw === '') return [];
|
||||
$o = @unserialize($raw, ['allowed_classes' => false]);
|
||||
return is_array($o) ? $o : [];
|
||||
}
|
||||
|
||||
/** Ultime newsletter del plugin (per l'import e per lo stato). */
|
||||
public static function listEmails(int $limit = 30): array
|
||||
{
|
||||
$st = self::db()->prepare("SELECT id, subject, status, created, sent, total FROM " . self::table() . "
|
||||
WHERE type = 'message' ORDER BY id DESC LIMIT " . max(1, min(100, $limit)));
|
||||
$st->execute();
|
||||
return $st->fetchAll();
|
||||
}
|
||||
|
||||
/** Stato di più newsletter in una sola query: [id => status]. */
|
||||
public static function statuses(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
|
||||
if (!$ids) return [];
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$st = self::db()->prepare("SELECT id, status FROM " . self::table() . " WHERE id IN ($ph)");
|
||||
$st->execute($ids);
|
||||
$out = [];
|
||||
foreach ($st->fetchAll() as $r) $out[(int) $r['id']] = $r['status'];
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Legge una newsletter del plugin e la converte in blocchi YogiBook. */
|
||||
public static function import(int $id): array
|
||||
{
|
||||
$st = self::db()->prepare("SELECT id, subject, message, options, editor FROM " . self::table() . " WHERE id = ?");
|
||||
$st->execute([$id]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) {
|
||||
throw new RuntimeException('Newsletter non trovata in WordPress.');
|
||||
}
|
||||
|
||||
$opts = self::unserializeOptions($row['options']);
|
||||
|
||||
return [
|
||||
'subject' => (string) $row['subject'],
|
||||
'preheader' => (string) ($opts['preheader'] ?? ''),
|
||||
'blocks' => NewsletterRenderer::fromComposerHtml((string) $row['message']),
|
||||
'settings' => NewsletterRenderer::settingsFromWpOptions($opts),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea o aggiorna la bozza in The Newsletter.
|
||||
* Aggiorna solo se è ancora "new" (mai partita). Restituisce [id, created(bool)].
|
||||
*/
|
||||
public static function push(?int $wpId, string $subject, string $preheader, string $html, string $text, array $settings): array
|
||||
{
|
||||
$db = self::db();
|
||||
$composerOpts = NewsletterRenderer::wpComposerOptions($settings);
|
||||
|
||||
if ($wpId) {
|
||||
$st = $db->prepare("SELECT status, options FROM " . self::table() . " WHERE id = ?");
|
||||
$st->execute([$wpId]);
|
||||
$row = $st->fetch();
|
||||
|
||||
if ($row) {
|
||||
if ($row['status'] !== 'new') {
|
||||
throw new RuntimeException('In WordPress questa newsletter è già "' . $row['status'] . '": non la modifico. Duplica la bozza per crearne una nuova.');
|
||||
}
|
||||
|
||||
// Mantiene tutto quello scelto in WordPress (liste, mittente, utm...)
|
||||
$opts = array_merge(self::unserializeOptions($row['options']), $composerOpts, ['preheader' => $preheader]);
|
||||
unset($opts['error_message']);
|
||||
|
||||
$up = $db->prepare("UPDATE " . self::table() . "
|
||||
SET subject = ?, message = ?, message_text = ?, options = ?, editor = 2, updated = ?
|
||||
WHERE id = ? AND status = 'new'");
|
||||
$up->execute([$subject, $html, $text, serialize($opts), time(), $wpId]);
|
||||
|
||||
return [$wpId, false];
|
||||
}
|
||||
// Cancellata in WordPress: la ricreiamo
|
||||
}
|
||||
|
||||
// Nuova: il pubblico parte uguale all'ultima newsletter (si controlla in WordPress)
|
||||
$base = [];
|
||||
$last = $db->query("SELECT options FROM " . self::table() . " WHERE type = 'message' ORDER BY id DESC LIMIT 1")->fetchColumn();
|
||||
if ($last) {
|
||||
$prev = self::unserializeOptions($last);
|
||||
foreach (['status', 'wp_users', 'lists', 'lists_operator', 'lists_exclude', 'countries', 'regions', 'cities',
|
||||
'sender_email', 'sender_name', 'text_message_mode', 'utm_source', 'utm_campaign', 'utm_medium', 'utm_term', 'utm_content'] as $k) {
|
||||
if (array_key_exists($k, $prev)) $base[$k] = $prev[$k];
|
||||
}
|
||||
}
|
||||
$opts = array_merge(['status' => 'C', 'wp_users' => '0', 'lists' => [], 'lists_operator' => 'or', 'lists_exclude' => []], $base, $composerOpts, ['preheader' => $preheader]);
|
||||
|
||||
$ins = $db->prepare("INSERT INTO " . self::table() . "
|
||||
(language, subject, message, subject2, message2, name2, status, total, last_id, sent, track, list,
|
||||
type, query, editor, sex, theme, message_text, preferences, send_on, token, options, private, version, updated)
|
||||
VALUES ('', ?, ?, '', '', '', 'new', 0, 0, 0, 1, 0,
|
||||
'message', '', 2, '', '', ?, '', ?, '', ?, 0, '', ?)");
|
||||
$ins->execute([$subject, $html, $text, time(), serialize($opts), time()]);
|
||||
|
||||
return [(int) $db->lastInsertId(), true];
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
<?php
|
||||
// Debug iniziale
|
||||
error_log("== Inizio mailer.php ==");
|
||||
|
||||
// Percorso assoluto alla root (es. /home/wnunknek/yogibook.yogasoul.it/)
|
||||
$rootPath = dirname(__DIR__, 2);
|
||||
error_log("Root path: $rootPath");
|
||||
|
||||
// Percorso al vendor
|
||||
$vendorPath = $rootPath . '/vendor/autoload.php';
|
||||
error_log("Vendor path: $vendorPath");
|
||||
|
||||
if (!file_exists($vendorPath)) {
|
||||
die("❌ vendor/autoload.php non trovato in $vendorPath");
|
||||
@@ -20,3 +24,5 @@ use PHPMailer\PHPMailer\Exception;
|
||||
// ✅ Caricamento variabili .env
|
||||
$dotenv = Dotenv::createImmutable($rootPath);
|
||||
$dotenv->load();
|
||||
|
||||
error_log("✅ Variabili .env caricate: " . print_r($_ENV, true));
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="https://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title>{email_subject}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="format-detection" content="address=no">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="format-detection" content="email=no">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
|
||||
<style type="text/css">
|
||||
#outlook a{padding:0;}
|
||||
.ReadMsgBody{width:100%;} .ExternalClass{width:100%;}
|
||||
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
|
||||
|
||||
body { margin: 0; padding: 0; height: 100%!important; width: 100%!important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-line-height-rule: exactly;}
|
||||
table,td { border-collapse: collapse !important; mso-table-lspace: 0pt; mso-table-rspace: 0pt;}
|
||||
img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; max-width: 100%!important; -ms-interpolation-mode: bicubic;}
|
||||
img.aligncenter { display: block; margin: 0 auto;}
|
||||
/* TinyMCE of WP */
|
||||
.alignleft { float: left; margin: 7px 15px 7px 0px; }
|
||||
.alignright { float: right; margin: 7px 0px 7px 15px; }
|
||||
.aligncenter { display: block; margin-left: auto; margin-right: auto; }
|
||||
.screen-reader-text { display: none }
|
||||
figcaption { display: block; }
|
||||
@media screen and (max-width: 525px) {
|
||||
.wrapper { padding-left: px; padding-right: px; padding-bottom: px; padding-top: px;}
|
||||
.m-d-block {display: block}
|
||||
.m-d-none {display: none; visibility: hidden; color: transparent;}
|
||||
.m-pb-1 { padding-bottom: 16px!important; }
|
||||
.p-0 { padding: 0 !important; }
|
||||
.pt-1, .padding-top-15 { padding-top: 15px!important; }
|
||||
.pb-1, .padding-bottom-15 { padding-bottom: 15px!important; }
|
||||
.dh { display: hidden }
|
||||
.fs-90 {
|
||||
font-size: 90%;
|
||||
}
|
||||
.responsive { width:100%!important; }
|
||||
table.responsive { width:100%!important; float: none; display: table; padding-left: 0; padding-right: 0; }
|
||||
table[class="responsive"] { width:100%!important; float: none; display: table; padding-left: 0; padding-right: 0; }
|
||||
img { max-width: 100%!important }
|
||||
img[class="responsive"] { max-width: 100%!important; }
|
||||
/* "width: auto" restores the natural dimensions forced with attributes for Outlook */
|
||||
.fluid { max-width: 100%!important; width: auto; }
|
||||
img[class="fluid"] { max-width: 100%!important; width: auto; }
|
||||
|
||||
.block { display: block; }
|
||||
|
||||
td[class="responsive"]{width:100%!important; max-width: 100%!important; display: block; padding-left: 0 !important; padding-right: 0!important; float: none; }
|
||||
td.responsive { width:100%!important; max-width: 100%!important; display: block; padding-left: 0 !important; padding-right: 0!important; float: none; }
|
||||
|
||||
td[class="section-padding-bottom-image"]{
|
||||
padding: 50px 15px 0 15px !important;
|
||||
}
|
||||
/* For grids */
|
||||
.max-width-100 { max-width: 100%!important; }
|
||||
|
||||
/* Obsolete */
|
||||
.tnp-grid-column {
|
||||
max-width: 100%!important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Custom CSS */
|
||||
/* End Custom CSS */
|
||||
|
||||
|
||||
/* Text */
|
||||
.text p {
|
||||
margin: 0 0 15px 0 !important;
|
||||
}
|
||||
.text a {
|
||||
color: {{LINK_COLOR}};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Html */
|
||||
.html-td-global p {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
/* Last posts */
|
||||
@media (max-width: 525px) {
|
||||
.posts-1-column {
|
||||
width: 100%!important;
|
||||
}
|
||||
.posts-1-image {
|
||||
width: 100%!important;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
|
||||
class PushSender
|
||||
{
|
||||
/** @var string */
|
||||
private $projectId;
|
||||
|
||||
/**
|
||||
* @var string Endpoint di Google. Si puo' sovrascrivere con FCM_OAUTH_URL /
|
||||
* FCM_BASE_URL nel .env: serve SOLO per i test locali, che puntano a un
|
||||
* finto server FCM. In produzione queste variabili non vanno impostate.
|
||||
*/
|
||||
private $oauthUrl;
|
||||
|
||||
/** @var string */
|
||||
private $baseUrl;
|
||||
|
||||
/** @var array Contenuto del service account JSON */
|
||||
private $credentials;
|
||||
|
||||
/** @var string|null Token OAuth2 in cache per la durata dello script */
|
||||
private $accessToken = null;
|
||||
|
||||
/** @var int Timestamp di scadenza del token in cache */
|
||||
private $accessTokenExpiry = 0;
|
||||
|
||||
/**
|
||||
* @var string|null Se l'autenticazione con Google fallisce, il motivo resta qui:
|
||||
* gli invii successivi escono subito invece di ritentare per ogni utente.
|
||||
*/
|
||||
private $authFallita = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$rootPath = dirname(__DIR__, 2);
|
||||
|
||||
// Le variabili possono essere gia' state caricate da un altro require:
|
||||
// safeLoad non esplode se il .env manca o e' gia' in memoria.
|
||||
$dotenv = Dotenv::createImmutable($rootPath);
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$this->projectId = $_ENV['FCM_PROJECT_ID'] ?? '';
|
||||
$credentialsPath = $_ENV['FCM_CREDENTIALS_PATH'] ?? '';
|
||||
$this->oauthUrl = $_ENV['FCM_OAUTH_URL'] ?? 'https://oauth2.googleapis.com/token';
|
||||
$this->baseUrl = $_ENV['FCM_BASE_URL'] ?? 'https://fcm.googleapis.com';
|
||||
|
||||
if ($this->projectId === '' || $credentialsPath === '') {
|
||||
throw new RuntimeException(
|
||||
'FCM_PROJECT_ID o FCM_CREDENTIALS_PATH mancanti nel file .env'
|
||||
);
|
||||
}
|
||||
|
||||
// Percorso relativo -> lo consideriamo a partire dalla root del progetto.
|
||||
if ($credentialsPath[0] !== '/') {
|
||||
$credentialsPath = $rootPath . '/' . $credentialsPath;
|
||||
}
|
||||
|
||||
if (!is_readable($credentialsPath)) {
|
||||
throw new RuntimeException("Service account non leggibile: $credentialsPath");
|
||||
}
|
||||
|
||||
$json = json_decode((string) file_get_contents($credentialsPath), true);
|
||||
|
||||
if (!is_array($json) || empty($json['client_email']) || empty($json['private_key'])) {
|
||||
throw new RuntimeException("Service account non valido: $credentialsPath");
|
||||
}
|
||||
|
||||
$this->credentials = $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia la notifica a tutti i dispositivi attivi dell'utente.
|
||||
* I token rifiutati da FCM vengono disattivati (active = 'N').
|
||||
*
|
||||
* @return array ['sent' => int, 'failed' => int, 'errors' => string[], 'no_devices' => bool]
|
||||
* no_devices = l'utente non ha dispositivi registrati: non e'
|
||||
* un errore, semplicemente non ha (ancora) aperto l'app.
|
||||
*/
|
||||
public function sendToUser(PDO $db, int $iduser, string $title, string $body, array $data = []): array
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT iduserdevicetoken, devicetoken
|
||||
FROM userdevicetoken
|
||||
WHERE iduser = :uid AND active = 'Y'
|
||||
");
|
||||
$stmt->execute([':uid' => $iduser]);
|
||||
$devices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$esito = ['sent' => 0, 'failed' => 0, 'errors' => [], 'no_devices' => !$devices];
|
||||
|
||||
foreach ($devices as $device) {
|
||||
$risultato = $this->sendToToken($device['devicetoken'], $title, $body, $data);
|
||||
|
||||
if ($risultato['ok']) {
|
||||
$esito['sent']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$esito['failed']++;
|
||||
$esito['errors'][] = $risultato['error'];
|
||||
|
||||
// Token non piu' valido (app disinstallata, token rigenerato):
|
||||
// lo spegniamo per non riprovarci ad ogni giro di cron.
|
||||
if ($risultato['invalid_token']) {
|
||||
$upd = $db->prepare("
|
||||
UPDATE userdevicetoken SET active = 'N' WHERE iduserdevicetoken = :id
|
||||
");
|
||||
$upd->execute([':id' => $device['iduserdevicetoken']]);
|
||||
}
|
||||
}
|
||||
|
||||
return $esito;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia la notifica a un singolo token.
|
||||
*
|
||||
* @return array ['ok' => bool, 'error' => string, 'invalid_token' => bool]
|
||||
*/
|
||||
public function sendToToken(string $deviceToken, string $title, string $body, array $data = []): array
|
||||
{
|
||||
// Autenticazione gia' fallita in questo giro: inutile riprovare per ogni
|
||||
// singolo utente, il cron deve solo finire in fretta e scrivere il log.
|
||||
if ($this->authFallita !== null) {
|
||||
return ['ok' => false, 'error' => $this->authFallita, 'invalid_token' => false];
|
||||
}
|
||||
|
||||
try {
|
||||
$accessToken = $this->getAccessToken();
|
||||
} catch (Throwable $e) {
|
||||
// Un problema di credenziali o di rete non deve interrompere il cron:
|
||||
// lo registriamo e le notifiche restanti risulteranno "failed".
|
||||
$this->authFallita = 'auth: ' . $e->getMessage();
|
||||
|
||||
return ['ok' => false, 'error' => $this->authFallita, 'invalid_token' => false];
|
||||
}
|
||||
|
||||
$url = "{$this->baseUrl}/v1/projects/{$this->projectId}/messages:send";
|
||||
|
||||
// FCM accetta solo stringhe dentro "data".
|
||||
$payloadData = [];
|
||||
foreach ($data as $chiave => $valore) {
|
||||
$payloadData[(string) $chiave] = (string) $valore;
|
||||
}
|
||||
|
||||
$messaggio = [
|
||||
'message' => [
|
||||
'token' => $deviceToken,
|
||||
'notification' => [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
],
|
||||
// Attenzione: un array PHP vuoto diventa [] in JSON, ma FCM si
|
||||
// aspetta un oggetto e risponde 400. Se non ci sono dati da
|
||||
// mandare, il campo va proprio omesso.
|
||||
'data' => (object) $payloadData,
|
||||
'android' => ['priority' => 'high'],
|
||||
'apns' => [
|
||||
'headers' => ['apns-priority' => '10'],
|
||||
'payload' => ['aps' => ['sound' => 'default']],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Content-Type: application/json; charset=UTF-8',
|
||||
],
|
||||
CURLOPT_POSTFIELDS => json_encode($messaggio, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
|
||||
$risposta = curl_exec($ch);
|
||||
$codice = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$errCurl = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($risposta === false) {
|
||||
return ['ok' => false, 'error' => "curl: $errCurl", 'invalid_token' => false];
|
||||
}
|
||||
|
||||
if ($codice >= 200 && $codice < 300) {
|
||||
return ['ok' => true, 'error' => '', 'invalid_token' => false];
|
||||
}
|
||||
|
||||
$decodificata = json_decode((string) $risposta, true);
|
||||
$statoErrore = $decodificata['error']['status'] ?? '';
|
||||
|
||||
// UNREGISTERED / NOT_FOUND = app disinstallata o token non piu' valido.
|
||||
//
|
||||
// INVALID_ARGUMENT NON va messo qui: quasi sempre vuol dire che siamo
|
||||
// noi a mandare un messaggio malformato, e spegnere i dispositivi degli
|
||||
// utenti per un nostro errore li lascerebbe senza notifiche per sempre.
|
||||
$tokenNonValido = in_array($statoErrore, ['UNREGISTERED', 'NOT_FOUND'], true);
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => "HTTP $codice: " . substr((string) $risposta, 0, 300),
|
||||
'invalid_token' => $tokenNonValido,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Token OAuth2 per FCM: JWT firmato con la chiave del service account,
|
||||
* scambiato con un access token da Google. Vale un'ora, lo teniamo in cache.
|
||||
*/
|
||||
private function getAccessToken(): string
|
||||
{
|
||||
if ($this->accessToken !== null && time() < $this->accessTokenExpiry - 60) {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
$adesso = time();
|
||||
|
||||
$header = ['alg' => 'RS256', 'typ' => 'JWT'];
|
||||
$claim = [
|
||||
'iss' => $this->credentials['client_email'],
|
||||
'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
|
||||
'aud' => 'https://oauth2.googleapis.com/token',
|
||||
'iat' => $adesso,
|
||||
'exp' => $adesso + 3600,
|
||||
];
|
||||
|
||||
$daFirmare = $this->base64UrlEncode(json_encode($header))
|
||||
. '.' . $this->base64UrlEncode(json_encode($claim));
|
||||
|
||||
$firma = '';
|
||||
if (!openssl_sign($daFirmare, $firma, $this->credentials['private_key'], OPENSSL_ALGO_SHA256)) {
|
||||
throw new RuntimeException('Firma del JWT fallita (chiave del service account non valida?)');
|
||||
}
|
||||
|
||||
$jwt = $daFirmare . '.' . $this->base64UrlEncode($firma);
|
||||
|
||||
$ch = curl_init($this->oauthUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
'assertion' => $jwt,
|
||||
]),
|
||||
]);
|
||||
|
||||
$risposta = curl_exec($ch);
|
||||
$codice = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$decodificata = json_decode((string) $risposta, true);
|
||||
|
||||
if ($codice !== 200 || empty($decodificata['access_token'])) {
|
||||
throw new RuntimeException(
|
||||
'Impossibile ottenere il token OAuth2 da Google: ' . substr((string) $risposta, 0, 300)
|
||||
);
|
||||
}
|
||||
|
||||
$this->accessToken = $decodificata['access_token'];
|
||||
$this->accessTokenExpiry = $adesso + (int) ($decodificata['expires_in'] ?? 3600);
|
||||
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
private function base64UrlEncode(string $dati): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($dati), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sostituisce i segnaposto nei testi presi da notificationsetting.
|
||||
* Esempio: pushRender('Ciao {first_name}', ['first_name' => 'Anna'])
|
||||
*/
|
||||
function pushRender(string $testo, array $valori): string
|
||||
{
|
||||
foreach ($valori as $chiave => $valore) {
|
||||
$testo = str_replace('{' . $chiave . '}', (string) $valore, $testo);
|
||||
}
|
||||
|
||||
return $testo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legge le impostazioni di un tipo di notifica (lesson / order / certificate).
|
||||
* Ritorna solo le righe attive, ordinate dal preavviso piu' lungo.
|
||||
*/
|
||||
function pushSettings(PDO $db, string $tipo): array
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT idnotificationsetting, notificationtype, daysbefore, sendhour,
|
||||
title, title_multi, body, body_multi
|
||||
FROM notificationsetting
|
||||
WHERE notificationtype = :tipo AND active = 'Y'
|
||||
ORDER BY daysbefore DESC
|
||||
");
|
||||
$stmt->execute([':tipo' => $tipo]);
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se una notifica e' gia' stata inviata (chiave anti-duplicato).
|
||||
*
|
||||
* Contano solo gli invii andati a buon fine: se il tentativo precedente era
|
||||
* fallito (Firebase irraggiungibile, credenziali sbagliate) il giro successivo
|
||||
* del cron deve riprovare.
|
||||
*/
|
||||
function pushGiaInviata(PDO $db, int $iduser, string $tipo, int $idreference, int $daysbefore): bool
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT 1
|
||||
FROM notificationlog
|
||||
WHERE iduser = :uid
|
||||
AND notificationtype = :tipo
|
||||
AND idreference = :idref
|
||||
AND daysbefore = :giorni
|
||||
AND status = 'sent'
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':uid' => $iduser,
|
||||
':tipo' => $tipo,
|
||||
':idref' => $idreference,
|
||||
':giorni' => $daysbefore,
|
||||
]);
|
||||
|
||||
return (bool) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrive l'esito dell'invio nello storico.
|
||||
*
|
||||
* La chiave unica impedisce i doppioni; se la riga esiste gia' (tentativo
|
||||
* precedente fallito) viene aggiornata con l'esito nuovo.
|
||||
*/
|
||||
function pushRegistraInvio(
|
||||
PDO $db,
|
||||
int $iduser,
|
||||
string $tipo,
|
||||
int $idreference,
|
||||
int $daysbefore,
|
||||
array $esito
|
||||
): void {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO notificationlog
|
||||
(iduser, notificationtype, idreference, daysbefore, status, devicecount, errormessage)
|
||||
VALUES
|
||||
(:uid, :tipo, :idref, :giorni, :stato, :dispositivi, :errore)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
devicecount = VALUES(devicecount),
|
||||
errormessage = VALUES(errormessage),
|
||||
sent_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':uid' => $iduser,
|
||||
':tipo' => $tipo,
|
||||
':idref' => $idreference,
|
||||
':giorni' => $daysbefore,
|
||||
':stato' => $esito['sent'] > 0 ? 'sent' : 'failed',
|
||||
':dispositivi' => $esito['sent'],
|
||||
':errore' => $esito['errors'] ? substr(implode(' | ', $esito['errors']), 0, 255) : null,
|
||||
]);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
// Buffer per scartare eventuale HTML degli include: risposta JSON pulita
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Conferma una prenotazione in attesa: pending -> booked.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function jsonOut(array $payload): void
|
||||
{
|
||||
if (ob_get_length() !== false) {
|
||||
ob_end_clean();
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$idbookingclass = isset($_POST['idbookingclass']) ? (int) $_POST['idbookingclass'] : 0;
|
||||
|
||||
if ($idbookingclass <= 0) {
|
||||
jsonOut(['success' => false, 'error' => 'ID prenotazione non valido.']);
|
||||
}
|
||||
|
||||
try {
|
||||
// Conferma solo se effettivamente in pending (evita conferme accidentali)
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE bookingclass SET status = 'booked'
|
||||
WHERE idbookingclass = :id AND status = 'pending'"
|
||||
);
|
||||
$stmt->execute([':id' => $idbookingclass]);
|
||||
jsonOut(['success' => true]);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('conferma_pending error: ' . $ex->getMessage());
|
||||
jsonOut(['success' => false, 'error' => 'Errore durante la conferma.']);
|
||||
}
|
||||
+17
-124
@@ -30,7 +30,10 @@ $userId = (int)$iduserlogin;
|
||||
/***********************
|
||||
* DB
|
||||
***********************/
|
||||
$conn = $bkngstm;
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
if ($conn->connect_error) {
|
||||
die("Connessione fallita: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
/***********************
|
||||
* 1) USER INFO (auth_users)
|
||||
@@ -51,9 +54,6 @@ $userName = trim(($u['first_name'] ?? '') . ' ' . ($u['last_name'] ?? ''));
|
||||
$stmt = $conn->prepare("
|
||||
SELECT
|
||||
bc.idbookingclass,
|
||||
bc.idorder,
|
||||
bc.status,
|
||||
bc.bookingstart,
|
||||
s.servicename,
|
||||
ss.dateschedule
|
||||
FROM bookingclass bc
|
||||
@@ -69,6 +69,7 @@ $bk = $res->fetch_assoc();
|
||||
$stmt->close();
|
||||
|
||||
if (!$bk) {
|
||||
$conn->close();
|
||||
header("Location: userpanel.php?deleted=0&err=notfound");
|
||||
exit();
|
||||
}
|
||||
@@ -80,127 +81,20 @@ $classDateTxt = $classDateRaw ? date("d-m-Y H:i", strtotime($classDateRaw)) : '
|
||||
$cancelDateTxt = date("d-m-Y H:i");
|
||||
|
||||
/***********************
|
||||
* 3) CONTROLLI (stesse regole di userpanel.php)
|
||||
* 3) DELETE (safe)
|
||||
***********************/
|
||||
// Solo prenotazioni confermate: pending e lista d'attesa hanno i loro percorsi
|
||||
if (($bk['status'] ?? '') !== 'booked') {
|
||||
header("Location: userpanel.php?deleted=0&err=status");
|
||||
exit();
|
||||
}
|
||||
$stmt = $conn->prepare("DELETE FROM bookingclass WHERE idbookingclass = ? AND iduser = ?");
|
||||
$stmt->bind_param("ii", $id, $userId);
|
||||
|
||||
// Finestra oraria
|
||||
$classTime = new DateTime($classDateRaw ?: $bk['bookingstart']);
|
||||
$currentTime = new DateTime();
|
||||
|
||||
// Lezione già iniziata o passata: non si cancella
|
||||
if ($classTime <= $currentTime) {
|
||||
header("Location: userpanel.php?deleted=0&err=window");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Lezione di oggi: fino alle 17:00 entro le 00:01, dopo le 17:00 entro le 12:00
|
||||
if ($classTime->format('Y-m-d') === $currentTime->format('Y-m-d')) {
|
||||
$classHour = (int) $classTime->format('H');
|
||||
$classMinute = (int) $classTime->format('i');
|
||||
$isBefore1700 = ($classHour < 17) || ($classHour === 17 && $classMinute === 0);
|
||||
$deadline = new DateTime($classTime->format($isBefore1700 ? 'Y-m-d 00:01:00' : 'Y-m-d 12:00:00'));
|
||||
if ($currentTime > $deadline) {
|
||||
header("Location: userpanel.php?deleted=0&err=window");
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
/***********************
|
||||
* 3b) STORICO + RIPROGRAMMAZIONE + CANCELLAZIONE
|
||||
* Tutto in una transazione: o riesce tutto o niente.
|
||||
***********************/
|
||||
$idOrder = $bk['idorder'] !== null ? (int) $bk['idorder'] : null;
|
||||
$ord = null;
|
||||
$incremented = 'N';
|
||||
$ticketLost = false;
|
||||
|
||||
try {
|
||||
$conn->begin_transaction();
|
||||
|
||||
// Limite riprogrammazioni (stessa regola del pulsante Riprogramma).
|
||||
// FOR UPDATE: blocca la riga dell'ordine contro doppi click.
|
||||
if ($idOrder !== null) {
|
||||
$stmt = $conn->prepare("SELECT reprogrammed, maxreschedule FROM orderbook WHERE idorderbook = ? FOR UPDATE");
|
||||
$stmt->bind_param("i", $idOrder);
|
||||
$stmt->execute();
|
||||
$ord = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
|
||||
if ($ord) {
|
||||
if ((int) $ord['reprogrammed'] >= (int) $ord['maxreschedule']) {
|
||||
// Limite raggiunto: può cancellare (libera il posto) ma il ticket va perso
|
||||
$ticketLost = true;
|
||||
} else {
|
||||
$incremented = 'Y';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copia nello storico (prima di eliminare)
|
||||
$stmt = $conn->prepare("
|
||||
INSERT INTO bookingclass_cancellations
|
||||
(idbookingclass, idserviceschedule, bookingstart, status, idservice, iduser, idorder,
|
||||
prevbookingstart, is_reprogrammed, is_gift, booking_created_at,
|
||||
cancelled_at, cancelled_by, reprogrammed_incremented)
|
||||
SELECT idbookingclass, idserviceschedule, bookingstart, status, idservice, iduser, idorder,
|
||||
prevbookingstart, is_reprogrammed, is_gift, created_at,
|
||||
NOW(), ?, ?
|
||||
FROM bookingclass
|
||||
WHERE idbookingclass = ? AND iduser = ?
|
||||
");
|
||||
$storicoFlag = $ticketLost ? 'L' : $incremented;
|
||||
$stmt->bind_param("isii", $userId, $storicoFlag, $id, $userId);
|
||||
$stmt->execute();
|
||||
$copied = $stmt->affected_rows;
|
||||
if (!$stmt->execute()) {
|
||||
$err = $stmt->error;
|
||||
$stmt->close();
|
||||
if ($copied !== 1) {
|
||||
throw new RuntimeException('notfound');
|
||||
}
|
||||
|
||||
// La cancellazione conta come una riprogrammazione
|
||||
if ($incremented === 'Y') {
|
||||
$stmt = $conn->prepare("UPDATE orderbook SET reprogrammed = reprogrammed + 1 WHERE idorderbook = ?");
|
||||
$stmt->bind_param("i", $idOrder);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
// Limite raggiunto: la prenotazione resta come persa (status 'lost'),
|
||||
// il posto in sala si libera ma il ticket non torna da programmare.
|
||||
// Altrimenti eliminazione normale: il ticket torna disponibile.
|
||||
if ($ticketLost) {
|
||||
$stmt = $conn->prepare("UPDATE bookingclass SET status = 'lost', lostlesson = 'Y' WHERE idbookingclass = ? AND iduser = ?");
|
||||
} else {
|
||||
$stmt = $conn->prepare("DELETE FROM bookingclass WHERE idbookingclass = ? AND iduser = ?");
|
||||
}
|
||||
$stmt->bind_param("ii", $id, $userId);
|
||||
$stmt->execute();
|
||||
$deleted = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
if ($deleted !== 1) {
|
||||
throw new RuntimeException('notfound');
|
||||
}
|
||||
|
||||
$conn->commit();
|
||||
} catch (Throwable $e) {
|
||||
try {
|
||||
$conn->rollback();
|
||||
} catch (Throwable $ignored) {
|
||||
}
|
||||
|
||||
$code = in_array($e->getMessage(), ['limit', 'notfound'], true) ? $e->getMessage() : 'db';
|
||||
if ($code === 'db') {
|
||||
error_log("delete_lesson #{$id} utente #{$userId}: " . $e->getMessage());
|
||||
}
|
||||
header("Location: userpanel.php?deleted=0&err=" . $code);
|
||||
exit();
|
||||
$conn->close();
|
||||
die("Errore durante la cancellazione del record: " . htmlspecialchars($err));
|
||||
}
|
||||
// Connessione $bkngstm gestita globalmente: non la chiudiamo qui
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
/***********************
|
||||
* 4) EMAIL (TO utente + CC info@yogasoul.it)
|
||||
@@ -255,8 +149,7 @@ try {
|
||||
$mail->Body = "
|
||||
<div style='font-family:Arial,sans-serif;font-size:14px;color:#222'>
|
||||
<p>Ciao " . htmlspecialchars($userName ?: 'Yogi') . ",</p>
|
||||
<p>La tua lezione è stata <b>cancellata</b>.</p>" . ($ticketLost ? "
|
||||
<p>Avendo già usato tutte le riprogrammazioni del pacchetto, questa lezione non potrà essere recuperata.</p>" : "") . "
|
||||
<p>La tua lezione è stata <b>cancellata</b>.</p>
|
||||
<p>
|
||||
<b>Lezione:</b> " . htmlspecialchars($className) . "<br>
|
||||
<b>Data/ora prevista:</b> " . htmlspecialchars($classDateTxt) . "<br>
|
||||
@@ -276,5 +169,5 @@ try {
|
||||
}
|
||||
|
||||
// Redirect
|
||||
header("Location: userpanel.php?deleted=" . ($ticketLost ? 'lost' : '1'));
|
||||
header("Location: userpanel.php?deleted=1");
|
||||
exit();
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
require_once('include/headscript.php'); // per avere le credenziali DB, se sono lì
|
||||
|
||||
// Se le credenziali sono in headscript.php, sposta la connessione DOPO il require.
|
||||
// Adatta questa parte a come sono definite $servername ecc. nelle tue altre pagine.
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Connessione fallita: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header("Location: riepilogo.php?message=error_delete");
|
||||
exit();
|
||||
}
|
||||
|
||||
$idorder = (int) $_GET['id'];
|
||||
|
||||
$conn->begin_transaction();
|
||||
|
||||
try {
|
||||
// 1. Cancella tutte le prenotazioni di questo ordine
|
||||
$stmt1 = $conn->prepare("DELETE FROM bookingclass WHERE idorder = ?");
|
||||
$stmt1->bind_param("i", $idorder);
|
||||
$stmt1->execute();
|
||||
$stmt1->close();
|
||||
|
||||
// 2. Cancella l'ordine
|
||||
$stmt2 = $conn->prepare("DELETE FROM orderbook WHERE idorderbook = ?");
|
||||
$stmt2->bind_param("i", $idorder);
|
||||
$stmt2->execute();
|
||||
$stmt2->close();
|
||||
|
||||
$conn->commit();
|
||||
header("Location: riepilogo.php?message=success_delete");
|
||||
exit();
|
||||
} catch (Exception $e) {
|
||||
$conn->rollback();
|
||||
header("Location: riepilogo.php?message=error_delete");
|
||||
exit();
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
+40
-25
@@ -1,36 +1,51 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/class/mailer.php';
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
ini_set ('display_errors', 1);
|
||||
ini_set ('display_startup_errors', 1);
|
||||
error_reporting (E_ALL);
|
||||
|
||||
$name = "Claudio";
|
||||
//Now include the following following files based
|
||||
//on the correct file path. Third file is required only if you want to enable SMTP.
|
||||
|
||||
|
||||
|
||||
require 'phpmailer/src/Exception.php';
|
||||
require 'phpmailer/src/PHPMailer.php';
|
||||
require 'phpmailer/src/SMTP.php';
|
||||
$name="Claudio";
|
||||
//mail to client
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
|
||||
$mail->Port = (int) $_ENV['MAIL_PORT'];
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->isHTML(true);
|
||||
|
||||
include('mail/emailtemplate2.php');
|
||||
$htmlContent = $mailmessage1;
|
||||
$mail->isSMTP(); // Set mailer to use SMTP
|
||||
$mail->Host = 'mail.yogasoul.it'; // Specify main and backup server
|
||||
$mail->SMTPAuth = true; // Enable SMTP authentication
|
||||
$mail->Username = 'info@yogasoul.it'; // SMTP username
|
||||
$mail->Password = '!Testolina88'; // SMTP password
|
||||
$mail->SMTPSecure = 'tls'; // Enable encryption, 'ssl' also accepted
|
||||
$mail->Port = '587';
|
||||
|
||||
|
||||
$mail->setFrom($_ENV['MAIL_FROM_ADDRESS'], $_ENV['MAIL_FROM_NAME']);
|
||||
$mail->addAddress('info@claudiosironi.com');
|
||||
include('mail/emailtemplate2.php');
|
||||
// Email body content
|
||||
|
||||
$mail->Subject = 'Comunicazione YogiBook';
|
||||
$mail->Body = $htmlContent;
|
||||
$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
|
||||
//$trfnmbmail=$appformn.'r'.$revnumb;
|
||||
$htmlContent = $mailmessage1;
|
||||
|
||||
|
||||
$mail->From = 'info@yogasoul.it';
|
||||
$mail->FromName = 'YogiBook [YogaSoul]';
|
||||
$mail->addAddress('info@claudiosironi.com'); // Add a recipient
|
||||
|
||||
|
||||
|
||||
|
||||
$mail->Subject = 'Comunicazione YogiBook';
|
||||
$mail->Body = $htmlContent;
|
||||
$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
|
||||
|
||||
$mail->send();
|
||||
echo 'Message has been sent';
|
||||
} catch (Exception $e) {
|
||||
error_log("Errore invio mail (emailsend): " . $mail->ErrorInfo);
|
||||
echo 'Invio non riuscito';
|
||||
}
|
||||
// echo 'Message has been sent';
|
||||
|
||||
?>
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_mailer.php'); // stub mailer (template vero: step successivo)
|
||||
|
||||
/**
|
||||
* Annullamento evento.
|
||||
* Sceglierne uno soft (consigliato): imposta status='cancelled' e mantiene le
|
||||
* prenotazioni/contatti raccolti. Se notify=1 invia la mail di annullamento a
|
||||
* tutti i partecipanti attivi (recuperati PRIMA di annullare).
|
||||
*
|
||||
* Chiamato da events.php: event_delete.php?id=123¬ify=1|0
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$notify = (int) ($_GET['notify'] ?? 0) === 1;
|
||||
|
||||
if ($id <= 0) {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$event) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
// 1) Recupera i partecipanti attivi PRIMA di annullare (servono per la mail)
|
||||
$pStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? AND status <> 'cancelled'");
|
||||
$pStmt->execute([$id]);
|
||||
$participants = $pStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
try {
|
||||
// 2) Annulla l'evento (soft) e le relative prenotazioni
|
||||
$pdo->prepare("UPDATE events SET status = 'cancelled', updated_at = NOW() WHERE id = ?")->execute([$id]);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE event_id = ? AND status <> 'cancelled'")->execute([$id]);
|
||||
|
||||
// 3) Notifica (opzionale). Il template vero arriva nello step successivo.
|
||||
if ($notify && !empty($participants)) {
|
||||
foreach ($participants as $p) {
|
||||
try {
|
||||
yse_mail_event_cancel($p, $event);
|
||||
} catch (Throwable $mailEx) {
|
||||
error_log('[event_delete mail] ' . $mailEx->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: events.php?message=cancelled');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_delete] ' . $ex->getMessage());
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
@@ -1,964 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - form creazione/modifica (una sola pagina).
|
||||
* - event_form.php -> nuovo evento
|
||||
* - event_form.php?id=123 -> modifica evento esistente
|
||||
* Invia a event_save.php (insert/update + redirect).
|
||||
*
|
||||
* Extra rispetto alla v1:
|
||||
* - flatpickr per data/ora (fine = inizio + 1h automatica)
|
||||
* - sede da event_locations (default scuola) o indirizzo personalizzato
|
||||
* - "Calcola coordinate" da indirizzo (geocoding via event_geocode.php)
|
||||
* - copertina drag&drop -> event_upload_cover.php -> assets/uploads/events/
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$ev = null;
|
||||
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$ev = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$ev) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Default dal record unico di `option`
|
||||
$opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||
$defaultColor = $opt['event_default_color'] ?? '#4CAF50';
|
||||
$defaultMaxPerBooking = (int) ($opt['event_default_max_per_booking'] ?? 1);
|
||||
$defReminderEnabled = ($opt['event_reminder_enabled'] ?? 'N') === 'Y';
|
||||
$defReminderHours = (int) ($opt['event_reminder_hours'] ?? 24);
|
||||
$defOfferHours = (int) ($opt['event_offer_expiry_hours'] ?? 24);
|
||||
|
||||
// Sedi salvate (se la tabella non esiste ancora, si degrada su indirizzo libero)
|
||||
$locations = [];
|
||||
try {
|
||||
$locations = $pdo->query("SELECT * FROM event_locations ORDER BY is_default DESC, name ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) {
|
||||
$locations = [];
|
||||
}
|
||||
|
||||
$val = [
|
||||
'title' => $ev['title'] ?? '',
|
||||
'slug' => $ev['slug'] ?? '',
|
||||
'organizer_name' => $ev['organizer_name'] ?? '',
|
||||
'starts_at' => !empty($ev['starts_at']) ? date('Y-m-d H:i', strtotime($ev['starts_at'])) : '',
|
||||
'ends_at' => !empty($ev['ends_at']) ? date('Y-m-d H:i', strtotime($ev['ends_at'])) : '',
|
||||
'address' => $ev['address'] ?? '',
|
||||
'lat' => $ev['lat'] ?? '',
|
||||
'lng' => $ev['lng'] ?? '',
|
||||
'cost' => $ev['cost'] ?? '0.00',
|
||||
'currency' => $ev['currency'] ?? 'EUR',
|
||||
'max_seats' => $ev['max_seats'] ?? '',
|
||||
'max_per_booking' => $ev['max_per_booking'] ?? $defaultMaxPerBooking,
|
||||
'color' => $ev['color'] ?? $defaultColor,
|
||||
'cover_image' => $ev['cover_image'] ?? '',
|
||||
'status' => $ev['status'] ?? 'draft',
|
||||
'description' => $ev['description'] ?? '',
|
||||
'instructions' => $ev['instructions'] ?? '',
|
||||
'what_to_bring' => $ev['what_to_bring'] ?? '',
|
||||
'suitable_for' => $ev['suitable_for'] ?? '',
|
||||
'gallery' => $ev['gallery'] ?? '',
|
||||
'waitlist_enabled' => ($ev['waitlist_enabled'] ?? 'Y') === 'Y',
|
||||
'reminder_enabled' => $ev['reminder_enabled'] ?? null,
|
||||
'reminder_hours' => $ev['reminder_hours'] ?? '',
|
||||
'offer_expiry_hours' => $ev['offer_expiry_hours'] ?? '',
|
||||
'schedule_enabled' => (($ev['status'] ?? '') === 'scheduled') || !empty($ev['publish_at']),
|
||||
'publish_at' => !empty($ev['publish_at']) ? date('Y-m-d H:i', strtotime($ev['publish_at'])) : '',
|
||||
];
|
||||
|
||||
$isEdit = $id > 0;
|
||||
$pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - <?php echo e($pageTitle); ?></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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">
|
||||
<!-- Date/ora -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/it.min.js"></script>
|
||||
<!-- Editor testo ricco -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background: #f7f8fa;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#editor {
|
||||
height: 240px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow,
|
||||
.ql-container.ql-snow {
|
||||
border-color: #e2e4e9;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
input[type="color"].form-control {
|
||||
padding: 4px;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-geocode {
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #2f7be7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-geocode:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
border-radius: 8px;
|
||||
padding: 10px 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Dropzone copertina */
|
||||
.dropzone {
|
||||
border: 2px dashed #d5d9e0;
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
padding: 22px;
|
||||
text-align: center;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropzone.dragover {
|
||||
border-color: #1ebf73;
|
||||
background: #eef6f1;
|
||||
}
|
||||
|
||||
.dz-empty i {
|
||||
font-size: 34px;
|
||||
color: #b6bcc6;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dz-empty p {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dz-empty small {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dz-browse {
|
||||
color: #2f7be7;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dz-preview img {
|
||||
max-height: 170px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dz-remove {
|
||||
margin-top: 12px;
|
||||
border: 1px solid #f2c9c4;
|
||||
background: #fff;
|
||||
color: #c0392b;
|
||||
border-radius: 8px;
|
||||
padding: 6px 14px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dz-remove:hover {
|
||||
background: #fdecea;
|
||||
}
|
||||
|
||||
.dz-progress {
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.coord-flash {
|
||||
border-color: #1ebf73 !important;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.18) !important;
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.gallery-cell {
|
||||
position: relative;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
.gallery-cell img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gallery-remove {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gallery-remove:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
</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"><?php echo e($pageTitle); ?></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">
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title"><?php echo e($pageTitle); ?></div>
|
||||
<p class="section-lead">Compila i dati dell'evento. I campi con * sono obbligatori.</p>
|
||||
|
||||
<form action="event_save.php" method="POST" id="event-form">
|
||||
<input type="hidden" name="id" value="<?php echo (int) $id; ?>">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label class="form-label">Titolo *</label>
|
||||
<input type="text" class="form-control" name="title" required
|
||||
value="<?php echo e($val['title']); ?>" placeholder="Es. Yoga in Gravidanza & Amaca">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Slug</label>
|
||||
<input type="text" class="form-control" name="slug"
|
||||
value="<?php echo e($val['slug']); ?>" placeholder="generato dal titolo">
|
||||
<div class="hint">URL pubblico del widget. Vuoto = generato dal titolo.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Data e ora inizio *</label>
|
||||
<input type="text" class="form-control" id="starts_at" name="starts_at" required
|
||||
value="<?php echo e($val['starts_at']); ?>" placeholder="Scegli data e ora" autocomplete="off">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Data e ora fine</label>
|
||||
<input type="text" class="form-control" id="ends_at" name="ends_at"
|
||||
value="<?php echo e($val['ends_at']); ?>" placeholder="Auto: inizio + 1h" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sede / indirizzo -->
|
||||
<div class="row">
|
||||
<?php if (!empty($locations)) : ?>
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label">Sede</label>
|
||||
<select id="location-select" class="form-select">
|
||||
<?php foreach ($locations as $loc) : ?>
|
||||
<option value="<?php echo (int) $loc['id']; ?>"
|
||||
data-address="<?php echo e($loc['address']); ?>"
|
||||
data-lat="<?php echo e($loc['lat']); ?>"
|
||||
data-lng="<?php echo e($loc['lng']); ?>">
|
||||
<?php echo e($loc['name']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<option value="custom">➕ Indirizzo personalizzato</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-7 mb-3">
|
||||
<label class="form-label">Indirizzo</label>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" class="form-control" id="address-input" name="address"
|
||||
value="<?php echo e($val['address']); ?>" placeholder="Via, numero, città">
|
||||
<button type="button" class="btn-geocode" id="btn-geocode"><i class="bx bx-map-pin"></i> Calcola</button>
|
||||
</div>
|
||||
<div class="hint" id="geo-hint">Per un indirizzo personalizzato, premi "Calcola" per ottenere le coordinate mappa.</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Indirizzo</label>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" class="form-control" id="address-input" name="address"
|
||||
value="<?php echo e($val['address']); ?>" placeholder="Via, numero, città">
|
||||
<button type="button" class="btn-geocode" id="btn-geocode"><i class="bx bx-map-pin"></i> Calcola coordinate</button>
|
||||
</div>
|
||||
<div class="hint" id="geo-hint">Premi "Calcola coordinate" per ottenere lat/lng dall'indirizzo.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="row" id="coords-row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Latitudine</label>
|
||||
<input type="text" class="form-control" id="lat-input" name="lat" value="<?php echo e($val['lat']); ?>" placeholder="45.6600">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Longitudine</label>
|
||||
<input type="text" class="form-control" id="lng-input" name="lng" value="<?php echo e($val['lng']); ?>" placeholder="8.7900">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Organizzatore</label>
|
||||
<input type="text" class="form-control" name="organizer_name"
|
||||
value="<?php echo e($val['organizer_name']); ?>" placeholder="Lascia vuoto = tu">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descrizione</label>
|
||||
<div id="editor"></div>
|
||||
<input type="hidden" name="description" id="description-input">
|
||||
<div class="hint">Testo formattato, elenchi, link e immagini.</div>
|
||||
</div>
|
||||
|
||||
<!-- Dettagli aggiuntivi (facoltativi) -->
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Istruzioni</label>
|
||||
<textarea class="form-control" name="instructions" rows="3" placeholder="Come arrivare, orari, note pratiche…"><?php echo e($val['instructions']); ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Cosa portare</label>
|
||||
<textarea class="form-control" name="what_to_bring" rows="3" placeholder="Tappetino, acqua, abbigliamento comodo…"><?php echo e($val['what_to_bring']); ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Per chi è adatto</label>
|
||||
<textarea class="form-control" name="suitable_for" rows="3" placeholder="Principianti, gravidanza, tutti i livelli…"><?php echo e($val['suitable_for']); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:-8px;">Questi campi sono facoltativi e verranno usati anche nella card pubblica e nelle email.</p>
|
||||
|
||||
<!-- Copertina drag&drop -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Immagine di copertina</label>
|
||||
<div class="dropzone" id="cover-dropzone">
|
||||
<div class="dz-empty" id="dz-empty" style="<?php echo $val['cover_image'] ? 'display:none;' : ''; ?>">
|
||||
<i class="bx bx-cloud-upload"></i>
|
||||
<p>Trascina qui l'immagine oppure <span class="dz-browse">sfoglia</span></p>
|
||||
<small>JPG, PNG, WEBP o GIF · max 5 MB</small>
|
||||
</div>
|
||||
<div class="dz-preview" id="dz-preview" style="<?php echo $val['cover_image'] ? '' : 'display:none;'; ?>">
|
||||
<img id="dz-img" src="<?php echo e($val['cover_image']); ?>" alt="Copertina">
|
||||
<div><button type="button" class="dz-remove" id="dz-remove"><i class="bx bx-trash"></i> Rimuovi</button></div>
|
||||
</div>
|
||||
<div class="dz-progress" id="dz-progress" style="display:none;"><i class="bx bx-loader bx-spin"></i> Caricamento…</div>
|
||||
<input type="file" id="cover-file" accept="image/*" hidden>
|
||||
</div>
|
||||
<input type="hidden" name="cover_image" id="cover_image" value="<?php echo e($val['cover_image']); ?>">
|
||||
<div class="hint">Immagine principale mostrata nella card.</div>
|
||||
</div>
|
||||
|
||||
<!-- Galleria foto -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Galleria foto</label>
|
||||
<div class="dropzone" id="gallery-dropzone">
|
||||
<div class="dz-empty">
|
||||
<i class="bx bx-images"></i>
|
||||
<p>Trascina qui le immagini oppure <span class="dz-browse">sfoglia</span></p>
|
||||
<small>Più immagini insieme · JPG, PNG, WEBP o GIF · max 5 MB l'una</small>
|
||||
</div>
|
||||
<input type="file" id="gallery-file" accept="image/*" multiple hidden>
|
||||
</div>
|
||||
<div class="gallery-grid" id="gallery-grid"></div>
|
||||
<input type="hidden" name="gallery" id="gallery-input" value="<?php echo e($val['gallery']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Costo</label>
|
||||
<input type="number" step="0.01" min="0" class="form-control" name="cost" value="<?php echo e($val['cost']); ?>">
|
||||
<div class="hint">0 = gratuito.</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Valuta</label>
|
||||
<select name="currency" class="form-select">
|
||||
<?php foreach (['EUR', 'USD', 'GBP', 'CHF'] as $cur) : ?>
|
||||
<option value="<?php echo $cur; ?>" <?php echo $val['currency'] === $cur ? 'selected' : ''; ?>><?php echo $cur; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Posti totali *</label>
|
||||
<input type="number" min="1" class="form-control" name="max_seats" required value="<?php echo e($val['max_seats']); ?>" placeholder="Es. 15">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Max posti per persona</label>
|
||||
<input type="number" min="1" class="form-control" name="max_per_booking" value="<?php echo e($val['max_per_booking']); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Colore card</label>
|
||||
<input type="color" class="form-control" name="color" value="<?php echo e($val['color']); ?>">
|
||||
</div>
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label">Stato</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="draft" <?php echo $val['status'] === 'draft' ? 'selected' : ''; ?>>Bozza</option>
|
||||
<option value="published" <?php echo $val['status'] === 'published' ? 'selected' : ''; ?>>Pubblicato</option>
|
||||
<option value="cancelled" <?php echo $val['status'] === 'cancelled' ? 'selected' : ''; ?>>Annullato</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Lista d'attesa</label>
|
||||
<div class="form-check form-switch" style="padding-top:8px;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="waitlist_enabled" name="waitlist_enabled" value="Y" <?php echo $val['waitlist_enabled'] ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="waitlist_enabled" style="font-weight:400;">Consenti iscrizione a evento pieno</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apertura programmata -->
|
||||
<div class="row">
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label">Apertura prenotazioni</label>
|
||||
<div class="form-check form-switch" style="padding-top:8px;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="schedule_enabled" name="schedule_enabled" value="Y" <?php echo $val['schedule_enabled'] ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="schedule_enabled" style="font-weight:400;">Apri in una data futura (programmato)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3" id="publish-at-wrap" style="<?php echo $val['schedule_enabled'] ? '' : 'display:none;'; ?>">
|
||||
<label class="form-label">Data/ora apertura</label>
|
||||
<input type="text" class="form-control" id="publish_at" name="publish_at" value="<?php echo e($val['publish_at']); ?>" placeholder="Quando aprire le prenotazioni" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:-8px;">Se attivo, l'evento resta "Programmato" (visibile ma non prenotabile) finché il cron non lo apre alla data indicata.</p>
|
||||
|
||||
<!-- Promemoria e lista d'attesa (override per evento) -->
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Promemoria automatico</label>
|
||||
<select name="reminder_enabled" class="form-select">
|
||||
<option value="">Predefinito (scuola: <?php echo $defReminderEnabled ? 'attivo' : 'disattivo'; ?>)</option>
|
||||
<option value="Y" <?php echo $val['reminder_enabled'] === 'Y' ? 'selected' : ''; ?>>Attivo</option>
|
||||
<option value="N" <?php echo $val['reminder_enabled'] === 'N' ? 'selected' : ''; ?>>Disattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Promemoria: ore prima</label>
|
||||
<input type="number" min="1" max="168" class="form-control" name="reminder_hours" value="<?php echo e($val['reminder_hours']); ?>" placeholder="Default: <?php echo $defReminderHours; ?>">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Scadenza offerta lista d'attesa (ore)</label>
|
||||
<input type="number" min="1" max="336" class="form-control" name="offer_expiry_hours" value="<?php echo e($val['offer_expiry_hours']); ?>" placeholder="Default: <?php echo $defOfferHours; ?>">
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:-8px;">Lascia vuoto o "Predefinito" per usare le impostazioni generali (Opzioni Eventi).</p>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<a href="events.php" class="btn btn-light btn-back"><i class="bx bx-arrow-back"></i> Indietro</a>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
<i class="bx bx-save"></i> <?php echo $isEdit ? 'Salva modifiche' : 'Crea evento'; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</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>
|
||||
/* ---------- Date/ora con flatpickr (fine = inizio + 1h) ---------- */
|
||||
var fpOpts = {
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
minuteIncrement: 15,
|
||||
dateFormat: "Y-m-d H:i",
|
||||
altInput: true,
|
||||
altFormat: "l j F Y, H:i",
|
||||
locale: "it"
|
||||
};
|
||||
|
||||
var endPicker = flatpickr("#ends_at", fpOpts);
|
||||
flatpickr("#publish_at", fpOpts);
|
||||
document.getElementById('schedule_enabled').addEventListener('change', function() {
|
||||
document.getElementById('publish-at-wrap').style.display = this.checked ? '' : 'none';
|
||||
});
|
||||
var startPicker = flatpickr("#starts_at", Object.assign({}, fpOpts, {
|
||||
onChange: function(dates) {
|
||||
if (!dates[0]) return;
|
||||
endPicker.set('minDate', dates[0]);
|
||||
var end = endPicker.selectedDates[0];
|
||||
if (!end || end <= dates[0]) {
|
||||
endPicker.setDate(new Date(dates[0].getTime() + 60 * 60 * 1000), false);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
/* ---------- Sede / indirizzo ---------- */
|
||||
var addressInput = document.getElementById('address-input');
|
||||
var latInput = document.getElementById('lat-input');
|
||||
var lngInput = document.getElementById('lng-input');
|
||||
var geoHint = document.getElementById('geo-hint');
|
||||
var locationSelect = document.getElementById('location-select');
|
||||
var currentAddress = <?php echo json_encode($val['address'], JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
function clearCoords() {
|
||||
latInput.value = '';
|
||||
lngInput.value = '';
|
||||
}
|
||||
|
||||
// Dichiarazione (hoisted): usata anche in fase di preselezione qui sotto.
|
||||
function applyLocation() {
|
||||
if (!locationSelect) return;
|
||||
var opt = locationSelect.options[locationSelect.selectedIndex];
|
||||
if (locationSelect.value === 'custom') {
|
||||
// Indirizzo personalizzato: campo vuoto, nessuna coordinata finché non premi "Calcola"
|
||||
addressInput.value = '';
|
||||
clearCoords();
|
||||
geoHint.textContent = 'Scrivi l\'indirizzo e premi "Calcola" per le coordinate.';
|
||||
addressInput.focus();
|
||||
} else {
|
||||
// Sede salvata: indirizzo e coordinate REALI della sede
|
||||
addressInput.value = opt.getAttribute('data-address') || '';
|
||||
latInput.value = opt.getAttribute('data-lat') || '';
|
||||
lngInput.value = opt.getAttribute('data-lng') || '';
|
||||
geoHint.textContent = (latInput.value && lngInput.value) ?
|
||||
'Coordinate della sede caricate.' :
|
||||
'Questa sede non ha coordinate salvate: premi "Calcola".';
|
||||
}
|
||||
}
|
||||
|
||||
if (locationSelect) {
|
||||
var matched = null;
|
||||
Array.prototype.forEach.call(locationSelect.options, function(opt) {
|
||||
if (opt.value !== 'custom' && currentAddress !== '' && opt.getAttribute('data-address') === currentAddress) {
|
||||
matched = opt.value;
|
||||
}
|
||||
});
|
||||
if (currentAddress === '') {
|
||||
locationSelect.selectedIndex = 0; // nuovo evento -> sede di default (scuola)
|
||||
applyLocation();
|
||||
} else if (matched) {
|
||||
locationSelect.value = matched; // l'indirizzo salvato combacia con una sede
|
||||
applyLocation();
|
||||
} else {
|
||||
locationSelect.value = 'custom'; // indirizzo personalizzato salvato: NON toccare i valori esistenti
|
||||
}
|
||||
locationSelect.addEventListener('change', applyLocation);
|
||||
}
|
||||
|
||||
// Se modifichi l'indirizzo a mano, le vecchie coordinate non valgono più:
|
||||
// le azzero subito, così non restano valori "di default" fuorvianti.
|
||||
addressInput.addEventListener('input', function() {
|
||||
clearCoords();
|
||||
geoHint.textContent = 'Indirizzo modificato: premi "Calcola" per aggiornare le coordinate.';
|
||||
});
|
||||
|
||||
/* ---------- Geocoding indirizzo -> lat/lng ---------- */
|
||||
document.getElementById('btn-geocode').addEventListener('click', function() {
|
||||
var q = addressInput.value.trim();
|
||||
if (q === '') {
|
||||
addressInput.focus();
|
||||
return;
|
||||
}
|
||||
geoHint.textContent = 'Ricerca coordinate…';
|
||||
fetch('event_geocode.php?q=' + encodeURIComponent(q))
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
latInput.value = data.lat;
|
||||
lngInput.value = data.lng;
|
||||
latInput.classList.add('coord-flash');
|
||||
lngInput.classList.add('coord-flash');
|
||||
setTimeout(function() {
|
||||
latInput.classList.remove('coord-flash');
|
||||
lngInput.classList.remove('coord-flash');
|
||||
}, 900);
|
||||
geoHint.innerHTML = '<span style="color:#1a7f52;font-weight:600;">✓ Coordinate aggiornate:</span> ' +
|
||||
data.lat + ', ' + data.lng + (data.display ? ' — ' + data.display : '');
|
||||
} else {
|
||||
geoHint.textContent = data.error || 'Indirizzo non trovato.';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
geoHint.textContent = 'Errore durante il geocoding.';
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- Editor Quill ---------- */
|
||||
var quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{
|
||||
'header': [2, 3, false]
|
||||
}],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{
|
||||
'list': 'ordered'
|
||||
}, {
|
||||
'list': 'bullet'
|
||||
}],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
var initialHtml = <?php echo json_encode($val['description'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
|
||||
if (initialHtml && initialHtml.trim() !== '') {
|
||||
quill.clipboard.dangerouslyPasteHTML(initialHtml);
|
||||
}
|
||||
|
||||
/* ---------- Copertina drag&drop ---------- */
|
||||
var dz = document.getElementById('cover-dropzone');
|
||||
var fileInput = document.getElementById('cover-file');
|
||||
var dzEmpty = document.getElementById('dz-empty');
|
||||
var dzPreview = document.getElementById('dz-preview');
|
||||
var dzProgress = document.getElementById('dz-progress');
|
||||
var dzImg = document.getElementById('dz-img');
|
||||
var coverHidden = document.getElementById('cover_image');
|
||||
|
||||
dz.addEventListener('click', function(ev) {
|
||||
if (ev.target.id === 'dz-remove' || ev.target.closest('#dz-remove')) return;
|
||||
fileInput.click();
|
||||
});
|
||||
['dragenter', 'dragover'].forEach(function(t) {
|
||||
dz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
dz.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
['dragleave', 'drop'].forEach(function(t) {
|
||||
dz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
dz.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
dz.addEventListener('drop', function(ev) {
|
||||
if (ev.dataTransfer.files.length) uploadCover(ev.dataTransfer.files[0]);
|
||||
});
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (fileInput.files.length) uploadCover(fileInput.files[0]);
|
||||
});
|
||||
document.getElementById('dz-remove').addEventListener('click', function() {
|
||||
coverHidden.value = '';
|
||||
dzImg.src = '';
|
||||
dzPreview.style.display = 'none';
|
||||
dzEmpty.style.display = '';
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
function uploadCover(file) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
Swal.fire('Formato non valido', 'Seleziona un file immagine.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
Swal.fire('File troppo grande', "L'immagine supera i 5 MB.", 'warning');
|
||||
return;
|
||||
}
|
||||
dzEmpty.style.display = 'none';
|
||||
dzPreview.style.display = 'none';
|
||||
dzProgress.style.display = 'block';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('cover', file);
|
||||
fetch('event_upload_cover.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
dzProgress.style.display = 'none';
|
||||
if (data.ok) {
|
||||
coverHidden.value = data.path;
|
||||
dzImg.src = data.path + '?t=' + Date.now();
|
||||
dzPreview.style.display = 'block';
|
||||
} else {
|
||||
dzEmpty.style.display = '';
|
||||
Swal.fire('Errore', data.error || 'Caricamento non riuscito.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
dzProgress.style.display = 'none';
|
||||
dzEmpty.style.display = '';
|
||||
Swal.fire('Errore', 'Caricamento non riuscito.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Galleria foto (multi-upload) ---------- */
|
||||
var galleryInput = document.getElementById('gallery-input');
|
||||
var galleryGrid = document.getElementById('gallery-grid');
|
||||
var galleryFile = document.getElementById('gallery-file');
|
||||
var galleryDz = document.getElementById('gallery-dropzone');
|
||||
var galleryItems = [];
|
||||
try {
|
||||
galleryItems = JSON.parse(galleryInput.value || '[]');
|
||||
if (!Array.isArray(galleryItems)) galleryItems = [];
|
||||
} catch (e) {
|
||||
galleryItems = [];
|
||||
}
|
||||
|
||||
function renderGallery() {
|
||||
galleryGrid.innerHTML = '';
|
||||
galleryItems.forEach(function(path, idx) {
|
||||
var cell = document.createElement('div');
|
||||
cell.className = 'gallery-cell';
|
||||
var img = document.createElement('img');
|
||||
img.src = path;
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'gallery-remove';
|
||||
btn.setAttribute('data-idx', idx);
|
||||
btn.innerHTML = '×';
|
||||
cell.appendChild(img);
|
||||
cell.appendChild(btn);
|
||||
galleryGrid.appendChild(cell);
|
||||
});
|
||||
galleryInput.value = JSON.stringify(galleryItems);
|
||||
}
|
||||
renderGallery();
|
||||
|
||||
galleryGrid.addEventListener('click', function(ev) {
|
||||
var btn = ev.target.closest('.gallery-remove');
|
||||
if (!btn) return;
|
||||
galleryItems.splice(parseInt(btn.getAttribute('data-idx'), 10), 1);
|
||||
renderGallery();
|
||||
});
|
||||
galleryDz.addEventListener('click', function() {
|
||||
galleryFile.click();
|
||||
});
|
||||
['dragenter', 'dragover'].forEach(function(t) {
|
||||
galleryDz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
galleryDz.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
['dragleave', 'drop'].forEach(function(t) {
|
||||
galleryDz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
galleryDz.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
galleryDz.addEventListener('drop', function(ev) {
|
||||
if (ev.dataTransfer.files.length) uploadGallery(ev.dataTransfer.files);
|
||||
});
|
||||
galleryFile.addEventListener('change', function() {
|
||||
if (galleryFile.files.length) uploadGallery(galleryFile.files);
|
||||
});
|
||||
|
||||
function uploadGallery(files) {
|
||||
Array.prototype.forEach.call(files, function(file) {
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
Swal.fire('File troppo grande', file.name + ' supera i 5 MB.', 'warning');
|
||||
return;
|
||||
}
|
||||
var fd = new FormData();
|
||||
fd.append('cover', file); // stesso handler della copertina
|
||||
fetch('event_upload_cover.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
galleryItems.push(data.path);
|
||||
renderGallery();
|
||||
} else {
|
||||
Swal.fire('Errore', data.error || 'Caricamento non riuscito.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Caricamento non riuscito.', 'error');
|
||||
});
|
||||
});
|
||||
galleryFile.value = '';
|
||||
}
|
||||
|
||||
/* ---------- Slug suggerito dal titolo ---------- */
|
||||
(function() {
|
||||
var titleInput = document.querySelector('input[name="title"]');
|
||||
var slugInput = document.querySelector('input[name="slug"]');
|
||||
if (!titleInput || !slugInput) return;
|
||||
// In modifica lo slug esiste già: lo consideriamo "toccato" e non lo sovrascriviamo.
|
||||
var slugEdited = slugInput.value.trim() !== '';
|
||||
|
||||
function slugify(s) {
|
||||
return s.toString().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
}
|
||||
slugInput.addEventListener('input', function() {
|
||||
slugEdited = slugInput.value.trim() !== '';
|
||||
});
|
||||
titleInput.addEventListener('input', function() {
|
||||
if (!slugEdited) slugInput.value = slugify(titleInput.value);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---------- Submit ---------- */
|
||||
document.getElementById('event-form').addEventListener('submit', function() {
|
||||
var html = quill.root.innerHTML;
|
||||
document.getElementById('description-input').value = (html === '<p><br></p>') ? '' : html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Geocoding indirizzo -> coordinate. Proxy server-side verso OpenStreetMap /
|
||||
* Nominatim (gratuito, senza API key). Ritorna JSON:
|
||||
* { ok:true, lat:45.66, lng:8.79, display:"..." } oppure { ok:false, error:"..." }
|
||||
*
|
||||
* Nota: Nominatim chiede max 1 richiesta/secondo e uno User-Agent identificativo
|
||||
* (impostato sotto). Quando avrai la chiave Google Maps si può passare alla
|
||||
* Geocoding API di Google per maggiore precisione, cambiando solo questo file.
|
||||
*/
|
||||
if (function_exists('ob_get_level') && ob_get_level()) {
|
||||
ob_clean();
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
if ($q === '') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Indirizzo mancante.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$url = 'https://nominatim.openstreetmap.org/search?format=json&limit=1&addressdetails=0&q=' . rawurlencode($q);
|
||||
$ua = 'YogiBook/1.0 (info@yogasoul.it)';
|
||||
$json = false;
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERAGENT => $ua,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
]);
|
||||
$json = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
if ($json === false) {
|
||||
$ctx = stream_context_create(['http' => [
|
||||
'method' => 'GET',
|
||||
'header' => "User-Agent: {$ua}\r\n",
|
||||
'timeout' => 8,
|
||||
]]);
|
||||
$json = @file_get_contents($url, false, $ctx);
|
||||
}
|
||||
|
||||
$data = $json ? json_decode($json, true) : null;
|
||||
|
||||
if (!empty($data[0]['lat']) && !empty($data[0]['lon'])) {
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'lat' => round((float) $data[0]['lat'], 7),
|
||||
'lng' => round((float) $data[0]['lon'], 7),
|
||||
'display' => $data[0]['display_name'] ?? '',
|
||||
]);
|
||||
} else {
|
||||
echo json_encode(['ok' => false, 'error' => 'Indirizzo non trovato. Prova a essere più specifico.']);
|
||||
}
|
||||
exit();
|
||||
@@ -1,471 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - gestione Sedi (event_locations): elenco, aggiungi, modifica,
|
||||
* elimina e "imposta come predefinita".
|
||||
* - event_locations.php -> elenco + form nuovo
|
||||
* - event_locations.php?edit=ID -> form in modifica
|
||||
* - event_locations.php?delete=ID -> elimina (con conferma via modale)
|
||||
* - event_locations.php?makedefault=ID -> imposta predefinita
|
||||
* Il salvataggio (insert/update) passa da event_locations_save.php.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
// Azioni inline (prima di ogni output)
|
||||
if (isset($_GET['delete'])) {
|
||||
$delId = (int) $_GET['delete'];
|
||||
$pdo->prepare("DELETE FROM event_locations WHERE id = ?")->execute([$delId]);
|
||||
header('Location: event_locations.php?message=deleted');
|
||||
exit();
|
||||
}
|
||||
if (isset($_GET['makedefault'])) {
|
||||
$defId = (int) $_GET['makedefault'];
|
||||
// Una sola predefinita: Y su questa, N su tutte le altre
|
||||
$pdo->prepare("UPDATE event_locations SET is_default = IF(id = ?, 'Y', 'N'), updated_at = NOW()")->execute([$defId]);
|
||||
header('Location: event_locations.php?message=default');
|
||||
exit();
|
||||
}
|
||||
|
||||
$editId = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
|
||||
$edit = null;
|
||||
if ($editId > 0) {
|
||||
$st = $pdo->prepare("SELECT * FROM event_locations WHERE id = ?");
|
||||
$st->execute([$editId]);
|
||||
$edit = $st->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$tableMissing = false;
|
||||
$locations = [];
|
||||
try {
|
||||
$locations = $pdo->query("SELECT * FROM event_locations ORDER BY is_default DESC, name ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $ex) {
|
||||
$tableMissing = true;
|
||||
}
|
||||
|
||||
$message = $_GET['message'] ?? '';
|
||||
|
||||
$val = [
|
||||
'id' => $edit['id'] ?? 0,
|
||||
'name' => $edit['name'] ?? '',
|
||||
'address' => $edit['address'] ?? '',
|
||||
'lat' => $edit['lat'] ?? '',
|
||||
'lng' => $edit['lng'] ?? '',
|
||||
'is_default' => ($edit['is_default'] ?? 'N') === 'Y',
|
||||
];
|
||||
$isEdit = $editId > 0 && $edit;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Sedi</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.btn-geocode {
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #2f7be7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-geocode:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.coord-flash {
|
||||
border-color: #1ebf73 !important;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.18) !important;
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
|
||||
.loc-table thead th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: #6b7280;
|
||||
border-bottom: 2px solid #eef0f3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loc-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.loc-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.default-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: #e7f7ee;
|
||||
color: #1a7f52;
|
||||
}
|
||||
|
||||
.btn-act {
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-act:hover {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.btn-act.danger {
|
||||
color: #c0392b;
|
||||
border-color: #f2c9c4;
|
||||
}
|
||||
|
||||
.btn-act.danger:hover {
|
||||
background: #fdecea;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 34px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #d5d9e0;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
border-radius: 8px;
|
||||
padding: 10px 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</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">Sedi</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 ($message === 'success') : ?>
|
||||
<div class="alert alert-success" role="alert">Sede salvata.</div>
|
||||
<?php elseif ($message === 'deleted') : ?>
|
||||
<div class="alert alert-warning" role="alert">Sede eliminata.</div>
|
||||
<?php elseif ($message === 'default') : ?>
|
||||
<div class="alert alert-success" role="alert">Sede predefinita aggiornata.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Errore: nome e indirizzo sono obbligatori.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($tableMissing) : ?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
La tabella <code>event_locations</code> non esiste ancora. Esegui la migration Phinx <code>CreateEventLocationsTable</code> e ricarica.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<!-- Form -->
|
||||
<div class="col-12">
|
||||
<div class="card admin-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="section-title"><?php echo $isEdit ? 'Modifica sede' : 'Nuova sede'; ?></div>
|
||||
<p class="section-lead">Le sedi salvate compaiono nella tendina "Sede" del form evento.</p>
|
||||
|
||||
<form action="event_locations_save.php" method="POST" id="loc-form">
|
||||
<input type="hidden" name="id" value="<?php echo (int) $val['id']; ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nome sede *</label>
|
||||
<input type="text" class="form-control" name="name" required
|
||||
value="<?php echo e($val['name']); ?>" placeholder="Es. Studio YogaSoul">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Indirizzo *</label>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" class="form-control" id="address-input" name="address" required
|
||||
value="<?php echo e($val['address']); ?>" placeholder="Via, numero, città">
|
||||
<button type="button" class="btn-geocode" id="btn-geocode"><i class="bx bx-map-pin"></i> Calcola</button>
|
||||
</div>
|
||||
<div class="hint" id="geo-hint">Premi "Calcola" per ottenere le coordinate dall'indirizzo.</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Latitudine</label>
|
||||
<input type="text" class="form-control" id="lat-input" name="lat" value="<?php echo e($val['lat']); ?>" placeholder="45.6600">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Longitudine</label>
|
||||
<input type="text" class="form-control" id="lng-input" name="lng" value="<?php echo e($val['lng']); ?>" placeholder="8.7900">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="is_default" name="is_default" value="Y" <?php echo $val['is_default'] ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="is_default">Imposta come sede predefinita</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-save"><i class="bx bx-save"></i> <?php echo $isEdit ? 'Salva' : 'Aggiungi'; ?></button>
|
||||
<?php if ($isEdit) : ?>
|
||||
<a href="event_locations.php" class="btn btn-light btn-save">Annulla</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Elenco -->
|
||||
<div class="col-12">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Sedi salvate</div>
|
||||
<p class="section-lead">Rubrica delle sedi riutilizzabili.</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table loc-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sede</th>
|
||||
<th>Indirizzo</th>
|
||||
<th>Coordinate</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($locations)) : ?>
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
Nessuna sede salvata. Aggiungine una dal form a sinistra.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else :
|
||||
foreach ($locations as $loc) :
|
||||
$hasCoords = $loc['lat'] !== null && $loc['lng'] !== null && $loc['lat'] !== '' && $loc['lng'] !== '';
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="loc-name"><?php echo e($loc['name']); ?></span>
|
||||
<?php if ($loc['is_default'] === 'Y') : ?>
|
||||
<span class="default-badge"><i class="bx bxs-star"></i> Predefinita</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($loc['address']); ?></td>
|
||||
<td><?php echo $hasCoords ? e($loc['lat']) . ', ' . e($loc['lng']) : '<span style="color:#9ca3af;">—</span>'; ?></td>
|
||||
<td class="text-end">
|
||||
<a href="event_locations.php?edit=<?php echo (int) $loc['id']; ?>" class="btn-act"><i class="bx bx-edit"></i> Modifica</a>
|
||||
<?php if ($loc['is_default'] !== 'Y') : ?>
|
||||
<a href="event_locations.php?makedefault=<?php echo (int) $loc['id']; ?>" class="btn-act"><i class="bx bx-star"></i> Predefinita</a>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn-act danger btn-del"
|
||||
data-id="<?php echo (int) $loc['id']; ?>"
|
||||
data-name="<?php echo e($loc['name']); ?>">
|
||||
<i class="bx bx-trash"></i> Elimina
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</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 addressInput = document.getElementById('address-input');
|
||||
var latInput = document.getElementById('lat-input');
|
||||
var lngInput = document.getElementById('lng-input');
|
||||
var geoHint = document.getElementById('geo-hint');
|
||||
|
||||
// Modifica indirizzo -> le vecchie coordinate non valgono più
|
||||
addressInput.addEventListener('input', function() {
|
||||
latInput.value = '';
|
||||
lngInput.value = '';
|
||||
geoHint.textContent = 'Indirizzo modificato: premi "Calcola" per aggiornare le coordinate.';
|
||||
});
|
||||
|
||||
document.getElementById('btn-geocode').addEventListener('click', function() {
|
||||
var q = addressInput.value.trim();
|
||||
if (q === '') {
|
||||
addressInput.focus();
|
||||
return;
|
||||
}
|
||||
geoHint.textContent = 'Ricerca coordinate…';
|
||||
fetch('event_geocode.php?q=' + encodeURIComponent(q))
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
latInput.value = data.lat;
|
||||
lngInput.value = data.lng;
|
||||
latInput.classList.add('coord-flash');
|
||||
lngInput.classList.add('coord-flash');
|
||||
setTimeout(function() {
|
||||
latInput.classList.remove('coord-flash');
|
||||
lngInput.classList.remove('coord-flash');
|
||||
}, 900);
|
||||
geoHint.innerHTML = '<span style="color:#1a7f52;font-weight:600;">✓ Coordinate aggiornate:</span> ' +
|
||||
data.lat + ', ' + data.lng + (data.display ? ' — ' + data.display : '');
|
||||
} else {
|
||||
geoHint.textContent = data.error || 'Indirizzo non trovato.';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
geoHint.textContent = 'Errore durante il geocoding.';
|
||||
});
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.btn-del').on('click', function() {
|
||||
var id = $(this).data('id');
|
||||
var name = $(this).data('name');
|
||||
Swal.fire({
|
||||
title: 'Eliminare la sede?',
|
||||
html: 'Stai per eliminare <strong>' + $('<div>').text(name).html() + '</strong>.<br><small>Gli eventi che la usavano mantengono il loro indirizzo.</small>',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Elimina',
|
||||
cancelButtonText: 'Chiudi',
|
||||
confirmButtonColor: '#e74c3c',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = 'event_locations.php?delete=' + id;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Salvataggio sede (insert/update) da event_locations.php.
|
||||
* Se "predefinita" è spuntata, la imposta come unica default.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: event_locations.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$isDefault = (isset($_POST['is_default']) && $_POST['is_default'] === 'Y') ? 'Y' : 'N';
|
||||
|
||||
$lat = ($_POST['lat'] ?? '') !== '' ? (float) $_POST['lat'] : null;
|
||||
$lng = ($_POST['lng'] ?? '') !== '' ? (float) $_POST['lng'] : null;
|
||||
|
||||
if ($name === '' || $address === '') {
|
||||
header('Location: event_locations.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("UPDATE event_locations
|
||||
SET name = :name, address = :address, lat = :lat, lng = :lng,
|
||||
is_default = :is_default, updated_at = NOW()
|
||||
WHERE id = :id");
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':address' => $address,
|
||||
':lat' => $lat,
|
||||
':lng' => $lng,
|
||||
':is_default' => $isDefault,
|
||||
':id' => $id,
|
||||
]);
|
||||
$savedId = $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO event_locations (name, address, lat, lng, is_default, created_at, updated_at)
|
||||
VALUES (:name, :address, :lat, :lng, :is_default, NOW(), NOW())");
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':address' => $address,
|
||||
':lat' => $lat,
|
||||
':lng' => $lng,
|
||||
':is_default' => $isDefault,
|
||||
]);
|
||||
$savedId = (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Una sola predefinita
|
||||
if ($isDefault === 'Y') {
|
||||
$pdo->prepare("UPDATE event_locations SET is_default = IF(id = ?, 'Y', 'N'), updated_at = NOW()")->execute([$savedId]);
|
||||
}
|
||||
|
||||
header('Location: event_locations.php?message=success');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_locations_save] ' . $ex->getMessage());
|
||||
header('Location: event_locations.php?message=error');
|
||||
exit();
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Opzioni Eventi (lato scuola): indirizzo per le copie, quali email mettere in
|
||||
* copia (per tipo), e impostazioni promemoria. Salva in event_options_save.php.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$message = $_GET['message'] ?? '';
|
||||
|
||||
$ccEmail = $opt['event_notify_email'] ?? '';
|
||||
$reminderH = (int) ($opt['event_reminder_hours'] ?? 24);
|
||||
$offerH = (int) ($opt['event_offer_expiry_hours'] ?? 24);
|
||||
function isY($opt, $k)
|
||||
{
|
||||
return ($opt[$k] ?? 'N') === 'Y';
|
||||
}
|
||||
|
||||
$types = [
|
||||
['event_cc_booking', 'Prenotazione', 'Quando qualcuno prenota (o entra in lista d\'attesa).'],
|
||||
['event_cc_promotion', 'Promozione lista d\'attesa', 'Quando una persona in attesa viene promossa.'],
|
||||
['event_cc_user_cancel', 'Disdetta utente', 'Quando un iscritto annulla la sua prenotazione.'],
|
||||
['event_cc_event_cancel', 'Annullo evento', 'Quando annulli un intero evento (a tutti gli iscritti).'],
|
||||
['event_cc_reminder', 'Promemoria', 'Il promemoria inviato prima dell\'evento.'],
|
||||
];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Opzioni Eventi</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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">
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.opt-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.opt-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.opt-label {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.opt-help {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
width: 3em;
|
||||
height: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
</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">Opzioni Eventi</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 ($message === 'success') : ?>
|
||||
<div class="alert alert-success" role="alert">Opzioni salvate.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Errore durante il salvataggio.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="event_options_save.php" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card admin-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Copie alla scuola</div>
|
||||
<p class="section-lead">Scegli su quali email l'insegnante/scuola riceve una copia (CC).</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email della scuola per le copie</label>
|
||||
<input type="email" class="form-control" name="event_notify_email" value="<?php echo e($ccEmail); ?>" placeholder="info@yogasoul.it">
|
||||
<div class="opt-help">Se vuota, non viene inviata nessuna copia anche se le voci sotto sono attive.</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($types as $t) : ?>
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label"><?php echo e($t[1]); ?></div>
|
||||
<div class="opt-help"><?php echo e($t[2]); ?></div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="<?php echo e($t[0]); ?>" value="Y" <?php echo isY($opt, $t[0]) ? 'checked' : ''; ?>>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Promemoria</div>
|
||||
<p class="section-lead">Invio automatico del promemoria agli iscritti prima dell'evento (richiede il cron, vedi setup).</p>
|
||||
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label">Attiva promemoria</div>
|
||||
<div class="opt-help">Invia un'email di promemoria agli iscritti confermati.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="event_reminder_enabled" value="Y" <?php echo isY($opt, 'event_reminder_enabled') ? 'checked' : ''; ?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label">Ore prima dell'evento</div>
|
||||
<div class="opt-help">Quante ore prima dell'inizio inviare il promemoria.</div>
|
||||
</div>
|
||||
<div style="width:120px;">
|
||||
<input type="number" min="1" max="168" class="form-control" name="event_reminder_hours" value="<?php echo e($reminderH); ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label">Scadenza offerta lista d'attesa (ore)</div>
|
||||
<div class="opt-help">Default: quanto tempo una persona promossa ha per riconfermare prima che il posto passi al prossimo. Modificabile per singolo evento.</div>
|
||||
</div>
|
||||
<div style="width:120px;">
|
||||
<input type="number" min="1" max="336" class="form-control" name="event_offer_expiry_hours" value="<?php echo e($offerH); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-bar">
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-save me-1"></i> Salva opzioni</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Salvataggio Opzioni Eventi (email/copie/promemoria) nella tabella `option`.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: event_options.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function yn(string $k): string
|
||||
{
|
||||
return (isset($_POST[$k]) && $_POST[$k] === 'Y') ? 'Y' : 'N';
|
||||
}
|
||||
|
||||
$ccEmail = trim($_POST['event_notify_email'] ?? '');
|
||||
$reminderH = max(1, min(168, (int) ($_POST['event_reminder_hours'] ?? 24)));
|
||||
$offerH = max(1, min(336, (int) ($_POST['event_offer_expiry_hours'] ?? 24)));
|
||||
|
||||
try {
|
||||
// Individua la riga singola di configurazione
|
||||
$idption = (int) $pdo->query("SELECT idption FROM `option` ORDER BY idption ASC LIMIT 1")->fetchColumn();
|
||||
|
||||
$sql = "UPDATE `option` SET
|
||||
event_notify_email = :cc_email,
|
||||
event_cc_booking = :cc_booking,
|
||||
event_cc_promotion = :cc_promotion,
|
||||
event_cc_user_cancel = :cc_user_cancel,
|
||||
event_cc_event_cancel = :cc_event_cancel,
|
||||
event_cc_reminder = :cc_reminder,
|
||||
event_reminder_enabled = :rem_enabled,
|
||||
event_reminder_hours = :rem_hours,
|
||||
event_offer_expiry_hours = :offer_hours
|
||||
WHERE idption = :idption";
|
||||
$pdo->prepare($sql)->execute([
|
||||
':cc_email' => $ccEmail !== '' ? $ccEmail : null,
|
||||
':cc_booking' => yn('event_cc_booking'),
|
||||
':cc_promotion' => yn('event_cc_promotion'),
|
||||
':cc_user_cancel' => yn('event_cc_user_cancel'),
|
||||
':cc_event_cancel' => yn('event_cc_event_cancel'),
|
||||
':cc_reminder' => yn('event_cc_reminder'),
|
||||
':rem_enabled' => yn('event_reminder_enabled'),
|
||||
':rem_hours' => $reminderH,
|
||||
':offer_hours' => $offerH,
|
||||
':idption' => $idption,
|
||||
]);
|
||||
|
||||
header('Location: event_options.php?message=success');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_options_save] ' . $ex->getMessage());
|
||||
header('Location: event_options.php?message=error');
|
||||
exit();
|
||||
}
|
||||
@@ -1,489 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_waitlist.php');
|
||||
require_once('include/event_mailer.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - elenco partecipanti di un evento.
|
||||
* Mostra riepilogo (posti, iscrizioni, incasso) e la tabella delle prenotazioni.
|
||||
* Gestisce anche l'annullamento della singola iscrizione via ?cancel_booking=ID.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function initials(string $first, string $last): string
|
||||
{
|
||||
$ini = strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1));
|
||||
return $ini !== '' ? $ini : '?';
|
||||
}
|
||||
|
||||
function avatarColor(string $seed): string
|
||||
{
|
||||
$palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#d35400'];
|
||||
return $palette[abs(crc32($seed)) % count($palette)];
|
||||
}
|
||||
|
||||
$eventId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if ($eventId <= 0) {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Annullamento singola iscrizione (deve avvenire prima di ogni output)
|
||||
if (isset($_GET['cancel_booking'])) {
|
||||
$bookingId = (int) $_GET['cancel_booking'];
|
||||
$promoted = [];
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([$eventId]);
|
||||
|
||||
$bl = $pdo->prepare("SELECT status FROM event_bookings WHERE id = ? AND event_id = ?");
|
||||
$bl->execute([$bookingId, $eventId]);
|
||||
$bStatus = $bl->fetchColumn();
|
||||
|
||||
if ($bStatus !== false && $bStatus !== 'cancelled') {
|
||||
$wasOccupying = in_array($bStatus, ['confirmed', 'pending', 'offered'], true);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?")
|
||||
->execute([$bookingId, $eventId]);
|
||||
if ($wasOccupying) {
|
||||
// libera posti -> offre alla prima in lista d'attesa
|
||||
$promoted = promoteFromWaitlist($pdo, $eventId);
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
// Email di promozione (E, con riconferma) ai nuovi "offered"
|
||||
if (!empty($promoted)) {
|
||||
$ev = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$ev->execute([$eventId]);
|
||||
$evRow = $ev->fetch(PDO::FETCH_ASSOC);
|
||||
if ($evRow) {
|
||||
foreach ($promoted as $pr) {
|
||||
try {
|
||||
yse_mail_promotion($pr, $evRow);
|
||||
} catch (Throwable $me) {
|
||||
error_log('[promo mail] ' . $me->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[participants cancel] ' . $ex->getMessage());
|
||||
}
|
||||
header('Location: event_participants.php?id=' . $eventId . '&message=booking_cancelled');
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$eventId]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$event) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
$bStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? ORDER BY created_at DESC, id DESC");
|
||||
$bStmt->execute([$eventId]);
|
||||
$bookings = $bStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Riepilogo: posti occupati = confermate/pending/offerte (in riconferma); lista d'attesa a parte
|
||||
$bookedSeats = 0;
|
||||
$waitingSeats = 0;
|
||||
$activeCount = 0;
|
||||
$revenue = 0.0;
|
||||
foreach ($bookings as $b) {
|
||||
if (in_array($b['status'], ['confirmed', 'pending', 'offered'], true)) {
|
||||
$bookedSeats += (int) $b['seats'];
|
||||
$activeCount++;
|
||||
$revenue += (float) $b['amount'];
|
||||
} elseif ($b['status'] === 'waiting') {
|
||||
$waitingSeats += (int) $b['seats'];
|
||||
}
|
||||
}
|
||||
$maxSeats = (int) $event['max_seats'];
|
||||
$message = $_GET['message'] ?? '';
|
||||
|
||||
function bookingBadge(string $status): array
|
||||
{
|
||||
switch ($status) {
|
||||
case 'confirmed':
|
||||
return ['Confermata', '#e7f7ee', '#1a7f52'];
|
||||
case 'cancelled':
|
||||
return ['Annullata', '#fdecea', '#c0392b'];
|
||||
case 'waiting':
|
||||
return ["Lista d'attesa", '#eef2fb', '#3a5bb8'];
|
||||
case 'offered':
|
||||
return ['Da riconfermare', '#fff4e5', '#b26a00'];
|
||||
default:
|
||||
return ['In attesa', '#fff4e5', '#b26a00'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Partecipanti</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: #f7f8fa;
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.part-table thead th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: #6b7280;
|
||||
border-bottom: 2px solid #eef0f3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.part-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.guest-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
background: #eef0f3;
|
||||
border-radius: 10px;
|
||||
padding: 1px 7px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.btn-cancel-b {
|
||||
border: 1px solid #f2c9c4;
|
||||
background: #fff;
|
||||
color: #c0392b;
|
||||
border-radius: 8px;
|
||||
padding: 5px 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-cancel-b:hover {
|
||||
background: #fdecea;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 44px 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 34px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #d5d9e0;
|
||||
}
|
||||
|
||||
.yes-mark {
|
||||
color: #1a7f52;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.no-mark {
|
||||
color: #c0392b;
|
||||
font-weight: 700;
|
||||
}
|
||||
</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">Partecipanti</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 ($message === 'booking_cancelled') : ?>
|
||||
<div class="alert alert-warning" role="alert">Iscrizione annullata.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="section-title">Partecipanti</div>
|
||||
<p class="section-lead mb-0">
|
||||
<strong style="color:#1f2937;"><?php echo e($event['title']); ?></strong>
|
||||
· <?php echo e(date('d-m-Y H:i', strtotime($event['starts_at']))); ?>
|
||||
</p>
|
||||
</div>
|
||||
<a href="events.php" class="btn btn-light" style="border-radius:8px;font-weight:600;"><i class="bx bx-arrow-back"></i> Tutti gli eventi</a>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $bookedSeats; ?> / <?php echo $maxSeats; ?></div>
|
||||
<div class="stat-label">Posti prenotati</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $waitingSeats; ?></div>
|
||||
<div class="stat-label">In lista d'attesa</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $activeCount; ?></div>
|
||||
<div class="stat-label">Iscrizioni attive</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo e(number_format($revenue, 2, ',', '.')); ?> <?php echo e($event['currency']); ?></div>
|
||||
<div class="stat-label">Incasso stimato</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table part-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partecipante</th>
|
||||
<th>Email</th>
|
||||
<th>Telefono</th>
|
||||
<th>Posti</th>
|
||||
<th>Marketing</th>
|
||||
<th>Iscritto il</th>
|
||||
<th>Stato</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($bookings)) : ?>
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users"></i>
|
||||
Ancora nessun iscritto a questo evento.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else :
|
||||
foreach ($bookings as $b) :
|
||||
// Il nome può essere un unico campo: proviamo a spezzarlo per le iniziali
|
||||
$nameParts = preg_split('/\s+/', trim($b['name']), 2);
|
||||
$first = $nameParts[0] ?? '';
|
||||
$last = $nameParts[1] ?? '';
|
||||
$isGuest = empty($b['user_id']);
|
||||
[$stLabel, $stBg, $stColor] = bookingBadge($b['status']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-cell">
|
||||
<div class="avatar" style="background: <?php echo avatarColor($b['name']); ?>;">
|
||||
<?php echo e(initials($first, $last)); ?>
|
||||
</div>
|
||||
<span class="user-name"><?php echo e($b['name']); ?>
|
||||
<?php if ($isGuest) : ?><span class="guest-tag">ospite</span><?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><?php echo e($b['email']); ?></td>
|
||||
<td><?php echo e($b['phone'] ?: '—'); ?></td>
|
||||
<td><?php echo (int) $b['seats']; ?></td>
|
||||
<td>
|
||||
<?php if (($b['marketing_opt_in'] ?? 'N') === 'Y') : ?>
|
||||
<span class="yes-mark">Sì</span>
|
||||
<?php else : ?>
|
||||
<span class="no-mark">No</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($b['created_at'] ? date('d-m-Y', strtotime($b['created_at'])) : '—'); ?></td>
|
||||
<td><span class="status-badge" style="background: <?php echo $stBg; ?>; color: <?php echo $stColor; ?>;"><?php echo e($stLabel); ?></span></td>
|
||||
<td class="text-end">
|
||||
<?php if ($b['status'] !== 'cancelled') : ?>
|
||||
<button type="button" class="btn-cancel-b btn-cancel-booking"
|
||||
data-id="<?php echo (int) $b['id']; ?>"
|
||||
data-name="<?php echo e($b['name']); ?>">
|
||||
<i class="bx bx-block"></i> Annulla
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<span style="color:#9ca3af;font-size:13px;">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</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>
|
||||
$(document).ready(function() {
|
||||
$('.btn-cancel-booking').on('click', function() {
|
||||
var id = $(this).data('id');
|
||||
var name = $(this).data('name');
|
||||
Swal.fire({
|
||||
title: 'Annullare iscrizione?',
|
||||
html: 'Stai per annullare l\'iscrizione di <strong>' + $('<div>').text(name).html() + '</strong>.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Annulla iscrizione',
|
||||
cancelButtonText: 'Chiudi',
|
||||
confirmButtonColor: '#e74c3c',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = 'event_participants.php?id=<?php echo $eventId; ?>&cancel_booking=' + id;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,222 +0,0 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_mailer.php');
|
||||
|
||||
/**
|
||||
* Promemoria manuale: apre un editor col testo prefissato (modificabile) e il
|
||||
* punto di ritrovo come link mappa (prefill dal luogo evento). Invio da
|
||||
* event_reminder_send.php a tutti gli iscritti confermati.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$eventId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if ($eventId <= 0) {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$eventId]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$event) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
$cnt = $pdo->prepare("SELECT COUNT(*) FROM event_bookings WHERE event_id = ? AND status = 'confirmed'");
|
||||
$cnt->execute([$eventId]);
|
||||
$recipients = (int) $cnt->fetchColumn();
|
||||
|
||||
$defaultMsg = "Ti ricordiamo il tuo prossimo appuntamento. Ti aspettiamo, a presto!";
|
||||
$meetingLink = yse_maps_link($event); // prefill: link mappa derivato dall'indirizzo/coordinate
|
||||
$message = $_GET['message'] ?? '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Promemoria</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.recap {
|
||||
background: #f7f8fa;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
</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">Promemoria</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 ($message === 'sent') : ?>
|
||||
<div class="alert alert-success" role="alert">Promemoria inviato.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Errore durante l'invio.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Invia promemoria</div>
|
||||
<p class="section-lead">Controlla il testo e il punto di ritrovo, poi invia agli iscritti confermati.</p>
|
||||
|
||||
<div class="recap">
|
||||
<strong style="color:#1f2937;"><?php echo e($event['title']); ?></strong> · <?php echo e(date('d-m-Y H:i', strtotime($event['starts_at']))); ?><br>
|
||||
Destinatari confermati: <strong><?php echo $recipients; ?></strong>
|
||||
</div>
|
||||
|
||||
<form action="event_reminder_send.php" method="POST" id="rem-form">
|
||||
<input type="hidden" name="id" value="<?php echo (int) $eventId; ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Messaggio</label>
|
||||
<textarea class="form-control" name="message" rows="5"><?php echo e($defaultMsg); ?></textarea>
|
||||
<div class="hint">Data, ora e luogo dell'evento vengono aggiunti automaticamente sotto al messaggio.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Punto di ritrovo (link mappa)</label>
|
||||
<input type="url" class="form-control" name="meeting_link" value="<?php echo e($meetingLink); ?>" placeholder="Incolla qui il link di Google Maps del punto di ritrovo">
|
||||
<div class="hint">Prefissato dal luogo dell'evento. Puoi incollare un link di Google Maps più preciso (es. la condivisione della posizione). Lascia vuoto per non includerlo.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<a href="events.php" class="btn btn-light" style="border-radius:8px;font-weight:600;"><i class="bx bx-arrow-back"></i> Indietro</a>
|
||||
<button type="submit" class="btn btn-success" style="border-radius:8px;font-weight:600;" <?php echo $recipients === 0 ? 'disabled' : ''; ?>>
|
||||
<i class="bx bx-send"></i> Invia a <?php echo $recipients; ?> iscritt<?php echo $recipients === 1 ? 'o' : 'i'; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</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>
|
||||
document.getElementById('rem-form').addEventListener('submit', function(ev) {
|
||||
ev.preventDefault();
|
||||
var form = this;
|
||||
Swal.fire({
|
||||
title: 'Inviare il promemoria?',
|
||||
text: 'Verrà inviato a <?php echo $recipients; ?> iscritti confermati.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Invia',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#1ebf73',
|
||||
reverseButtons: true
|
||||
}).then(function(r) {
|
||||
if (r.isConfirmed) form.submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user