fixed pages

This commit is contained in:
2026-09-04 09:00:30 +02:00
parent 2dfbbad7d4
commit 83af2e2d17
9 changed files with 1008 additions and 641 deletions
@@ -0,0 +1,30 @@
<?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();
}
}
}
@@ -0,0 +1,22 @@
<?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"
);
}
}
+63
View File
@@ -69,6 +69,20 @@ $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;
}
?>
<!doctype html>
<html lang="it">
@@ -227,6 +241,21 @@ $services = $pdo->query(
<?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']); ?>
@@ -401,6 +430,40 @@ $services = $pdo->query(
<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>
+75 -2
View File
@@ -53,6 +53,16 @@ $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));
@@ -114,6 +124,46 @@ $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;
@@ -497,6 +547,18 @@ $italianMonths = [
<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>
@@ -519,12 +581,12 @@ $italianMonths = [
$isFull = $maxcap > 0 && $countPersons >= $maxcap;
$colorclass = $sch['colorclass'] ?: '#e6f3ff';
?>
<div class="class-card" data-date="<?php echo e($dateObj->format('Y-m-d')); ?>">
<div class="class-card" data-date="<?php echo e($dateObj->format('Y-m-d')); ?>" data-type="<?php echo e($sch['servicename']); ?>">
<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: 6px solid <?php echo e($colorclass); ?>;">
<div class="class-body" style="border-left: 20px solid <?php echo e($colorclass); ?>;">
<h3 class="class-title">
<?php echo e($sch['servicename']); ?>
<span class="capacity-pill <?php echo $isFull ? 'full' : ''; ?>">
@@ -692,6 +754,17 @@ $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]');
+51 -38
View File
@@ -177,66 +177,70 @@ $conn->close();
<style>
.custom-card {
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;
background: #fff;
border-radius: 14px;
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.06);
overflow: hidden;
margin-bottom: 18px;
display: flex;
cursor: pointer;
transition: transform 0.2s;
transition: box-shadow 0.2s;
}
.custom-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.1);
}
.custom-date-box {
flex: 1;
background-color: red;
color: white;
flex: 0 0 110px;
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;
font-size: 60px;
font-weight: bold;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
padding: 16px;
}
.custom-day {
font-size: 44px;
font-weight: 800;
line-height: 1;
}
.custom-month {
font-size: 28px;
font-size: 18px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.custom-event-details {
flex: 2;
flex: 1;
display: flex;
flex-direction: column;
padding: 10px 20px;
background-color: lightblue;
padding: 18px 22px;
background: #fff;
border-left: 20px solid #ccc;
/* colore impostato inline per ogni card in base allo stato */
}
.custom-heading {
margin-top: 0;
font-size: 24px;
font-size: 19px;
font-weight: 700;
color: #1f2937;
margin: 0 0 4px;
}
.custom-paragraph {
margin-bottom: 5px;
color: #6b7280;
font-size: 14px;
margin-bottom: 3px;
}
.custom-actions {
display: none;
flex-direction: row;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
@@ -245,16 +249,19 @@ $conn->close();
}
.custom-action-button {
background-color: #f0f0f0;
border: none;
padding: 8px 12px;
border-radius: 4px;
border-radius: 8px;
padding: 7px 14px;
font-size: 13px;
font-weight: 600;
background: #eef2f7;
color: #374151;
cursor: pointer;
transition: background-color 0.2s;
transition: filter 0.15s;
}
.custom-action-button:hover {
background-color: #e0e0e0;
filter: brightness(0.94);
}
@media (max-width: 768px) {
@@ -262,14 +269,20 @@ $conn->close();
flex-direction: column;
}
.custom-date-box,
.custom-event-details {
.custom-date-box {
flex: 0 0 auto;
width: 100%;
border-radius: 0;
flex-direction: row;
gap: 10px;
padding: 12px;
}
.custom-event-time {
font-size: 24px;
.custom-day {
font-size: 30px;
}
.custom-month {
font-size: 18px;
}
}
@@ -531,11 +544,11 @@ $conn->close();
<div class="custom-month"><?php echo $monthInItalian; ?></div>
</div>
<?php if ($alreadybooked == 'Y') { ?>
<div class="custom-event-details" style="background-color:#FFAC7A">
<div class="custom-event-details" style="background-color:#FFAC7A;border-left-color:#FF6609">
<?php } elseif ($freeplace > 0) { ?>
<div class="custom-event-details" style="background-color:<?php echo ($bookedclass->getColumnVal("colorclass")); ?>">
<div class="custom-event-details" style="border-left-color:<?php echo ($bookedclass->getColumnVal("colorclass")); ?>">
<?php } else { ?>
<div class="custom-event-details" style="background-color:#CDCDCD">
<div class="custom-event-details" style="background-color:#CDCDCD;border-left-color:#9C9C9C">
<?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>
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
<?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;
+21 -7
View File
@@ -55,6 +55,19 @@ $daysUntilNext = ($desiredDayValue + 7 - $dayOfWeekStart) % 7;
$currentTs = $startTs + $daysUntilNext * 86400;
$dateEnd = date('Y-m-d', $endTs);
// Token univoco della propagazione + record riepilogativo
$token = bin2hex(random_bytes(16));
$insertPropagation = $pdo->prepare(
"INSERT INTO propagation (idservice, token, startdate, enddate)
VALUES (:idservice, :token, :startdate, :enddate)"
);
$insertPropagation->execute([
':idservice' => $idservice,
':token' => $token,
':startdate' => date('Y-m-d', $startTs),
':enddate' => $dateEnd,
]);
// --- Statement preparati una sola volta e riusati nel loop ---
$checkClass = $pdo->prepare(
"SELECT idserviceschedule FROM serviceschedule
@@ -65,8 +78,8 @@ $checkDayoff = $pdo->prepare(
"SELECT iddayoff FROM dayoff WHERE dayoffdate = :dayoffdate LIMIT 1"
);
$insertSchedule = $pdo->prepare(
"INSERT INTO serviceschedule (idservice, dateschedule, scheduleday, startingtime, durationtime)
VALUES (:idservice, :dateschedule, :scheduleday, :startingtime, :durationtime)"
"INSERT INTO serviceschedule (idservice, dateschedule, scheduleday, startingtime, durationtime, propagation_token)
VALUES (:idservice, :dateschedule, :scheduleday, :startingtime, :durationtime, :propagation_token)"
);
$inserted = 0;
@@ -88,11 +101,12 @@ while (date('Y-m-d', $currentTs) <= $dateEnd) {
if (!$alreadyScheduled && !$isDayoff) {
$insertSchedule->execute([
':idservice' => $idservice,
':dateschedule' => $datetimeSchedule,
':scheduleday' => $dayclass,
':startingtime' => $timeclass,
':durationtime' => $durationtime,
':idservice' => $idservice,
':dateschedule' => $datetimeSchedule,
':scheduleday' => $dayclass,
':startingtime' => $timeclass,
':durationtime' => $durationtime,
':propagation_token' => $token,
]);
$inserted++;
}
+134 -38
View File
@@ -58,6 +58,23 @@ $stmtBooked->execute([
]);
$bookedRows = $stmtBooked->fetchAll();
// Lezioni in attesa di conferma (pending) dell'utente, nel mese visualizzato
$sqlPending = "SELECT bookingclass.*, service.servicename, service.colorclass, serviceschedule.dateschedule
FROM bookingclass
LEFT JOIN service ON bookingclass.idservice = service.idservice
LEFT JOIN serviceschedule ON bookingclass.idserviceschedule = serviceschedule.idserviceschedule
WHERE bookingclass.iduser = :iduser
AND bookingclass.status = 'pending'
AND serviceschedule.dateschedule BETWEEN :monthStart AND DATE_ADD(:monthEnd, INTERVAL 1 DAY)
ORDER BY serviceschedule.dateschedule";
$stmtPending = $pdo->prepare($sqlPending);
$stmtPending->execute([
':iduser' => $iduserlogin,
':monthStart' => $currentMonthStart,
':monthEnd' => $currentMonthEnd,
]);
$pendingRows = $stmtPending->fetchAll();
/* -------------------------------------------------------------------------
* 3) Somma dei ticket acquistati dall'utente
* ---------------------------------------------------------------------- */
@@ -175,67 +192,97 @@ $italianMonths = [
<style>
.custom-card {
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;
background: #fff;
border-radius: 14px;
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.06);
overflow: hidden;
margin-bottom: 18px;
display: flex;
cursor: pointer;
transition: transform 0.2s;
transition: box-shadow 0.2s;
}
.custom-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.1);
}
.pending-card {
cursor: default;
}
.pending-card .custom-date-box {
flex: 0 0 90px;
padding: 10px;
}
.pending-card .custom-day {
font-size: 30px;
}
.pending-card .custom-month {
font-size: 14px;
}
.pending-card .custom-event-details {
padding: 12px 20px;
}
.pending-card .custom-heading {
font-size: 17px;
}
.custom-date-box {
flex: 1;
background-color: red;
color: white;
flex: 0 0 110px;
background: #1ebf73;
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;
font-size: 60px;
font-weight: bold;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
padding: 16px;
}
.custom-day {
font-size: 44px;
font-weight: 800;
line-height: 1;
}
.custom-month {
font-size: 28px;
font-size: 18px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.custom-event-details {
flex: 2;
flex: 1;
display: flex;
flex-direction: column;
padding: 10px 20px;
background-color: lightblue;
padding: 18px 22px;
background: #fff;
border-left: 20px solid #ccc;
/* colore impostato inline per ogni card */
}
.custom-heading {
margin-top: 0;
font-size: 24px;
font-size: 19px;
font-weight: 700;
color: #1f2937;
margin: 0 0 4px;
}
.custom-paragraph {
margin-bottom: 5px;
color: #6b7280;
font-size: 14px;
margin-bottom: 3px;
}
.custom-actions {
display: none;
flex-direction: row;
justify-content: space-between;
margin-top: 10px;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.custom-card.expanded .custom-actions {
@@ -243,16 +290,19 @@ $italianMonths = [
}
.custom-action-button {
background-color: #f0f0f0;
border: none;
padding: 8px 12px;
border-radius: 4px;
border-radius: 8px;
padding: 7px 14px;
font-size: 13px;
font-weight: 600;
background: #eef2f7;
color: #374151;
cursor: pointer;
transition: background-color 0.2s;
transition: filter 0.15s;
}
.custom-action-button:hover {
background-color: #e0e0e0;
filter: brightness(0.94);
}
@media (max-width: 768px) {
@@ -260,14 +310,20 @@ $italianMonths = [
flex-direction: column;
}
.custom-date-box,
.custom-event-details {
.custom-date-box {
flex: 0 0 auto;
width: 100%;
border-radius: 0;
flex-direction: row;
gap: 10px;
padding: 12px;
}
.custom-event-time {
font-size: 24px;
.custom-day {
font-size: 30px;
}
.custom-month {
font-size: 18px;
}
}
@@ -543,10 +599,50 @@ $italianMonths = [
</a>
</div>
<?php if (!empty($pendingRows)) : ?>
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-body">
<h5 style="color:#8a6d00;margin-bottom:14px;">
<i class="fas fa-hourglass-half me-1"></i> In attesa di conferma
</h5>
<?php foreach ($pendingRows as $prow) :
$pDateObj = new DateTime($prow['dateschedule']);
$pDay = $pDateObj->format("d");
$pMonth = $italianMonths[$pDateObj->format("F")];
$pWhen = $pDateObj->format("d-m-Y H:i");
$pColor = $prow['colorclass'] ?? '#1ebf73';
$pName = $prow['servicename'] ?? '';
?>
<div class="custom-card pending-card">
<div class="custom-date-box" style="background:#e0a800;">
<div class="custom-day"><?php echo e($pDay); ?></div>
<div class="custom-month"><?php echo e($pMonth); ?></div>
</div>
<div class="custom-event-details" style="border-left-color:<?php echo e($pColor); ?>">
<h2 class="custom-heading">
<?php echo e($pName); ?>
<span style="display:inline-block;background:#fff4d6;color:#8a6d00;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;margin-left:6px;vertical-align:middle;">In attesa</span>
</h2>
<p class="custom-paragraph">Quando: <?php echo e($pWhen); ?></p>
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-body">
<h5 style="color:#1a7f52;margin-bottom:14px;">
<i class="fas fa-check-circle me-1"></i> Prenotate
</h5>
<?php if (empty($bookedRows)) { ?>
<p>Prenotazioni non presenti per questo mese</p>
<?php
@@ -591,11 +687,11 @@ $italianMonths = [
$servicename = $row['servicename'] ?? '';
?>
<div class="custom-card" onclick="toggleCard(this)">
<div class="custom-date-box" style="background-color:#1ebf73">
<div class="custom-date-box">
<div class="custom-day"><?php echo e($dayInItalian); ?></div>
<div class="custom-month"><?php echo e($monthInItalian); ?></div>
</div>
<div class="custom-event-details" style="background-color:<?php echo e($colorclass); ?>">
<div class="custom-event-details" style="border-left-color:<?php echo e($colorclass); ?>">
<h2 class="custom-heading"><?php echo e($servicename); ?></h2>
<p class="custom-paragraph">Quando: <?php echo e($newDateFormat); ?></p>
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>