<?php
/**
* @file Calendar.php
* @brief Komponenta pro zobrazení interaktivního kalendáře s událostmi.
*
* Tento skript:
* - Umožňuje zobrazit měsíční kalendář s událostmi načtenými z databáze (tabulka 'events').
* - Podporuje AJAX navigaci mezi měsíci bez reloadu stránky.
* - Zobrazuje různé typy událostí (semináře, ozdravné pobyty, firemní, ostatní).
* - Umožňuje napojení na modální okna s detaily událostí, lektorů nebo kategorií.
* - Lze jednoduše vložit na jiné stránky pomocí include a volání metod třídy Calendar.
* - Obsahuje responzivní CSS pro různé velikosti zařízení.
* - Pro administraci(lokace admin.php) zobrazuje barvy a jejich legendu.
*
* Použití:
* - Vložte soubor pomocí include/require a vytvořte instanci: $calendar = new Calendar();
* - Načtěte události: $calendar->load_events_from_db($conn);
* - Vypište kalendář: echo $calendar;
* - Pro AJAX navigaci mezi měsíci zavolejte: Calendar::handleAjaxRequest($conn);
* -- Tím zajistíte obsluhu AJAX požadavků pro navigaci mezi měsíci bez reloadu stránky.
*/
?>
<style>
.calendar {
display: flex;
flex-flow: column;
}
.calendar .header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
z-index: 2;
}
.calendar .header .month-year-nav {
display: flex;
align-items: center;
gap: 15px;
}
.calendar .header .month-year {
font-size: 20px;
letter-spacing: 1px;
font-weight: bold;
color: white;
padding: 20px 0;
margin: 0;
}
.calendar .header .nav-arrow {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
font-size: 18px;
font-weight: bold;
padding: 8px 12px;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.3s ease;
min-width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.calendar .header .nav-arrow:hover {
background-color: rgba(255, 255, 255, 0.3);
}
.calendar .header .nav-arrow:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.calendar .header .nav-arrow:disabled:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.calendar .header .legend {
display: flex;
align-items: center;
margin-left: 20px;
}
.calendar .header .legend .legend-item {
display: flex;
align-items: center;
margin-right: 10px;
}
.calendar .header .legend .legend-item .color-box {
width: 15px;
height: 15px;
margin-right: 5px;
border-radius: 3px;
}
.calendar .header .legend .legend-item.yellow .color-box {
background-color: #f7c30d;
}
.calendar .header .legend .legend-item.green .color-box {
background-color: #51ce57;
}
.calendar .header .legend .legend-item.blue .color-box {
background-color: #518fce;
}
.calendar .header .legend .legend-item.red .color-box {
background-color: #ce5151;
}
.calendar .days {
display: flex;
flex-flow: wrap;
z-index: 1;
border-radius: 10px;
}
.calendar .days .day_name {
width: calc(100% / 7);
text-align: start;
padding: 20px;
text-transform: uppercase;
font-size: 12px;
font-weight: bold;
color: #818589;
color: #fff;
background-color: rgba(68, 140, 214, 0.6);
border: none;
}
.calendar .days .day_name:nth-child(1) {
border-top-left-radius: 10px;
}
.calendar .days .day_name:nth-child(7) {
border: none;
border-top-right-radius: 10px;
}
.calendar .days .day_num {
display: flex;
flex-flow: column;
width: calc(100% / 7);
padding: 15px;
font-weight: bold;
color: white;
cursor: pointer;
min-height: 50px;
min-width: none;
border: 1px solid rgba(68, 140, 214, 0.2);
}
.calendar .days .day_num span {
display: inline-flex;
width: 30px;
font-size: 14px;
}
.calendar .days .day_num .event {
margin-top: 10px;
font-weight: 500;
font-size: 12px;
padding: 2px 2px;
border-radius: 4px;
background-color: #f7c30d;
color: #fff;
word-wrap: break-word;
text-align: start;
}
.calendar .days .day_num .event:hover {
transform: scale(1.04);
}
.calendar .days .day_num .event.green {
background-color: #51ce57;
}
.calendar .days .day_num .event.blue {
background-color: #518fce;
}
.calendar .days .day_num .event.red {
background-color: #ce5151;
}
.calendar .days .day_num:hover {
background-color: rgba(253, 253, 253, 0.3);
}
.calendar .days .day_num.ignore {
background-color: transparent;
color: transparent;
cursor: inherit;
border-radius: 0;
}
.calendar .days .day_num.selected {
background-color: rgba(241, 242, 243, 0.4);
cursor: inherit;
}
@media (max-width: 768px) {
.calendar .header {
flex-direction: column;
align-items: stretch;
}
.calendar .header .month-year-nav {
justify-content: center;
}
.calendar .header .legend {
margin-left: 0;
justify-content: center;
flex-wrap: wrap;
}
.calendar .days .day_num .event {
display: inline-block;
width: 15px;
height: 15px;
background-color: #f7c30d;
border-radius: 50%;
margin: 5px auto;
color: transparent;
}
.calendar .days .day_num .event {
color: transparent;
}
}
@media (max-width: 576px) {
.legend {
font-size: 14px;
}
.calendar .header .month-year {
font-size: 18px;
}
.calendar .header .nav-arrow {
font-size: 16px;
min-width: 35px;
height: 35px;
}
}
</style>
<script>
//Globální proměnné pro aktuální měsíc a rok
let currentCalendarMonth = new Date().getMonth() + 1;
let currentCalendarYear = new Date().getFullYear();
// Načtení měsíce a roku z URL parametrů, pokud jsou přítomny
if (new URLSearchParams(window.location.search).get('month')) {
currentCalendarMonth = parseInt(new URLSearchParams(window.location.search).get('month'));
}
if (new URLSearchParams(window.location.search).get('year')) {
currentCalendarYear = parseInt(new URLSearchParams(window.location.search).get('year'));
}
function navigateMonth(direction) {
let newMonth = currentCalendarMonth;
let newYear = currentCalendarYear;
if (direction === 'next') {
newMonth++;
if (newMonth > 12) {
newMonth = 1;
newYear++;
}
//Aktuálně nelze zpětně v čase
} else if (direction === 'prev') {
newMonth--;
if (newMonth < 1) {
newMonth = 12;
newYear--;
}
}
// Zabrání prohlížení předešlého měsíce než je aktuální
const now = new Date();
const targetDate = new Date(newYear, newMonth - 1, 1);
const currentDate = new Date(now.getFullYear(), now.getMonth(), 1);
if (targetDate < currentDate) {
return; // Zastaví funkci, pokud je cílové datum před aktuálním
}
// Aktualizuje globální proměnné
currentCalendarMonth = newMonth;
currentCalendarYear = newYear;
// Upraví údaj v URL bez refreshe stránky
const newUrl = new URL(window.location);
newUrl.searchParams.set('month', newMonth);
newUrl.searchParams.set('year', newYear);
window.history.pushState({}, '', newUrl);
// Načte nový obsah kalendáře skrze AJAX
loadCalendarMonth(newMonth, newYear);
}
function loadCalendarMonth(month, year) {
const calendarContainer = document.querySelector('.calendar');
if (!calendarContainer) return;
// Zamezí v používání tlačítka při načítání
const navButtons = document.querySelectorAll('.nav-arrow');
navButtons.forEach(btn => {
btn.style.opacity = '0.5';
btn.style.pointerEvents = 'none';
});
// Vytvoří AJAX požadavek
const xhr = new XMLHttpRequest();
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('month', month);
currentUrl.searchParams.set('year', year);
currentUrl.searchParams.set('ajax', '1'); // Přidá AJAX parametr
xhr.open('GET', currentUrl.toString(), true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
// Obnoví možnost používání tlačítka (posun měsíce)
navButtons.forEach(btn => {
btn.style.opacity = '';
btn.style.pointerEvents = '';
});
if (xhr.status === 200) {
// Aktualizuje kalendář
const tempDiv = document.createElement('div');
tempDiv.innerHTML = xhr.responseText;
const newCalendar = tempDiv.querySelector('.calendar');
if (newCalendar) {
// Estetika přechodu
calendarContainer.style.transition = 'opacity 0.15s ease';
calendarContainer.style.opacity = '0.7';
setTimeout(() => {
calendarContainer.innerHTML = newCalendar.innerHTML;
calendarContainer.style.opacity = '1';
// Znovu připojí event listenery na nové prvky medailonek-link
attachEventListeners();
setTimeout(() => {
calendarContainer.style.transition = '';
}, 150);
}, 75);
}
} else {
console.error('Failed to load calendar month');
window.location.href = currentUrl.toString().replace('&ajax=1', '');
}
}
};
xhr.send();
}
function attachEventListeners() {
// Znovu připojí event listenery na nové prvky medailonek-link
const eventLinks = document.querySelectorAll('.medailonek-link');
eventLinks.forEach(link => {
// Zruší jakékoliv existující event listenery, aby nedocházelo k duplikaci
link.replaceWith(link.cloneNode(true));
});
// Znovu připojí event listenery na nové prvky medailonek-link
const freshEventLinks = document.querySelectorAll('.medailonek-link');
freshEventLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const dataUrl = this.getAttribute('data-url');
if (dataUrl) {
// Zkontroluje, zda existuje vlastní funkce pro zpracování události
if (typeof handleEventClick === 'function') {
handleEventClick(dataUrl);
}
// Standardní modální funkčnost
else if (dataUrl.includes('#modalPlace')) {
// Získá parametry z data-url
const url = new URL(dataUrl, window.location.origin);
const params = new URLSearchParams(url.search);
// Zkusí otevřít modální okno s parametry
if (typeof openModal === 'function') {
openModal(params);
} else if (typeof showModal === 'function') {
showModal(params);
} else {
// Fallback: navigace na URL
window.location.href = dataUrl;
}
}
// Akce podle kategorií
else if (dataUrl.includes('category=')) {
const url = new URL(dataUrl, window.location.origin);
const params = new URLSearchParams(url.search);
const category = params.get('category');
const eventId = params.get('eventId');
// Zkusí otevřít modální okno podle kategorie
if (typeof openCategoryModal === 'function') {
openCategoryModal(category, eventId);
} else if (typeof handleCategoryEvent === 'function') {
handleCategoryEvent(category, eventId);
} else {
// Fallback: navigace na URL
window.location.href = dataUrl;
}
}
// Akce s lektory
else if (dataUrl.includes('lectorId=')) {
const url = new URL(dataUrl, window.location.origin);
const params = new URLSearchParams(url.search);
const lectorId = params.get('lectorId');
const eventId = params.get('eventId');
// Zkusí otevřít modální okno s lektorem
if (typeof openLectorModal === 'function') {
openLectorModal(lectorId, eventId);
} else if (typeof handleLectorEvent === 'function') {
handleLectorEvent(lectorId, eventId);
} else {
// Fallback: navigace na URL
window.location.href = dataUrl;
}
}
// Univerzální fallback
else {
window.location.href = dataUrl;
}
}
});
});
// Inicializuje modální okna, pokud je funkce dostupná
if (typeof initializeModals === 'function') {
initializeModals();
}
if (typeof initCalendarEvents === 'function') {
initCalendarEvents();
}
if (typeof reinitializeEventHandlers === 'function') {
reinitializeEventHandlers();
}
}
// Připojí event listenery po načtení DOMu
document.addEventListener('DOMContentLoaded', function() {
attachEventListeners();
});
</script>
<?php
class Calendar
{
private $active_year, $active_month, $active_day;
private $events = [];
/**
* Konstruktor kalendáře.
* @param string|null $date Datum ve formátu Y-m-d nebo null pro dnešní datum.
* Nastaví aktivní měsíc, rok a den podle zadaného data nebo aktuálního dne.
*/
public function __construct($date = null)
{
// Nejprve zkontroluje parametry v URL
$month = isset($_GET['month']) ? $_GET['month'] : null;
$year = isset($_GET['year']) ? $_GET['year'] : null;
$today = new DateTime();
if ($month && $year) {
$this->active_year = $year;
$this->active_month = $month;
// Pokud je aktuální měsíc a rok, nastaví aktuální den, jinak 1
if ($year == $today->format('Y') && $month == $today->format('m')) {
$this->active_day = $today->format('d');
} else {
$this->active_day = 1;
}
} else {
$this->active_year = $date != null ? date('Y', strtotime($date)) : $today->format('Y');
$this->active_month = $date != null ? date('m', strtotime($date)) : $today->format('m');
$this->active_day = $date != null ? date('d', strtotime($date)) : $today->format('d');
}
}
/**
* Přidá událost do kalendáře.
* @param string $txt Název události
* @param string $date Datum začátku události
* @param int $days Počet dní trvání události
* @param string $color Barva události (blue, green, yellow, red)
* @param int|null $lector_id ID lektora (volitelné)
* @param int|null $category Kategorie události (volitelné)
* @param int|null $event_id ID události (volitelné)
*/
public function add_event($txt, $date, $days = 1, $color = '', $lector_id = null, $category = null, $event_id = null)
{
$color = $color ? ' ' . $color : $color;
$this->events[] = [$txt, $date, $days, $color, $lector_id, $category, $event_id];
}
/**
* Načte události z databáze a přidá je do kalendáře.
* @param mysqli $conn Připojení k databázi
*/
public function load_events_from_db($conn)
{
include 'db_connection.php'; ///< Připojení k databázi.
$conn->set_charset("utf8mb4");
$sql = "SELECT * FROM events";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$datefrom = $row['timefrom'];
$dateto = $row['timeto'];
$name = $row['heading'];
$lector_id = $row['lector_id'];
$category = $row['category'];
$event_id = $row['id'];
$color = 'blue';
// Pokud je kalendář v adminu, použije barvu z databáze
if (basename($_SERVER['PHP_SELF']) == 'admin.php') {
$color = $row['color'];
}
// Převede číselnou barvu na textovou
switch ($color) {
case 1:
$color = 'blue';
break;
case 2:
$color = 'green';
break;
case 3:
$color = 'yellow';
break;
case 4:
$color = 'red';
break;
default:
$color = '';
break;
}
$days = (strtotime($dateto) - strtotime($datefrom)) / (60 * 60 * 24) + 1;
$this->add_event($name, $datefrom, $days, $color, $lector_id, $category, $event_id);
}
}
}
/**
* Statická metoda pro obsluhu AJAX požadavků na změnu měsíce.
* @param mysqli $conn Připojení k databázi
*/
public static function handleAjaxRequest($conn)
{
if (isset($_GET['ajax']) && $_GET['ajax'] == '1') {
$month = isset($_GET['month']) ? $_GET['month'] : date('m');
$year = isset($_GET['year']) ? $_GET['year'] : date('Y');
$today = new DateTime();
$calendar = new Calendar();
$calendar->active_month = $month;
$calendar->active_year = $year;
// Pokud je aktuální měsíc a rok, nastaví aktuální den, jinak 1
if ($year == $today->format('Y') && $month == $today->format('m')) {
$calendar->active_day = $today->format('d');
} else {
$calendar->active_day = 1;
}
$calendar->load_events_from_db($conn);
// Vrátí pouze HTML kalendáře pro AJAX požadavek
echo $calendar->__toString();
exit;
}
}
/**
* Vrací true, pokud je aktivní měsíc v minulosti.
* @return bool
*/
private function isPastMonth()
{
$now = new Date();
$currentMonth = $now->getMonth() + 1;
$currentYear = $now->getFullYear();
return ($this->active_year < $currentYear) ||
($this->active_year == $currentYear && $this->active_month < $currentMonth);
}
/**
* Vygeneruje HTML kód kalendáře.
* @return string
*/
public function __toString()
{
$num_days = date('t', strtotime($this->active_day . '-' . $this->active_month . '-' . $this->active_year));
$days = [1 => 'Po', 2 => 'Út', 3 => 'St', 4 => 'Čt', 5 => 'Pá', 6 => 'So', 0 => 'Ne'];
$months = ['January' => 'Leden', 'February' => 'Únor', 'March' => 'Březen', 'April' => 'Duben', 'May' => 'Květen', 'June' => 'Červen', 'July' => 'Červenec', 'August' => 'Srpen', 'September' => 'Září', 'October' => 'Říjen', 'November' => 'Listopad', 'December' => 'Prosinec'];
$first_day_of_week = (date('w', strtotime($this->active_year . '-' . $this->active_month . '-1')) + 6) % 7;
// Zjistí, zda je aktuální měsíc v minulosti
$now = new DateTime();
$current_month = new DateTime($this->active_year . '-' . $this->active_month . '-01');
$is_past_month = $current_month < new DateTime($now->format('Y-m-01'));
$html = '<div class="calendar">';
$html .= '<div class="header">';
// Navigace mezi měsíci
$html .= '<div class="month-year-nav">';
$prev_disabled = $is_past_month ? 'disabled' : '';
$html .= '<button class="nav-arrow" onclick="navigateMonth(\'prev\')" ' . $prev_disabled . '>‹</button>';
$html .= '<div class="month-year">';
$month_year = date('F Y', strtotime($this->active_year . '-' . $this->active_month . '-' . $this->active_day));
$month_year = str_replace(array_keys($months), array_values($months), $month_year);
$html .= $month_year;
$html .= '</div>';
$html .= '<button class="nav-arrow" onclick="navigateMonth(\'next\')">›</button>';
$html .= '</div>';
// Legenda barev (pouze na admin stránce)
if (basename($_SERVER['PHP_SELF']) == 'admin.php') {
$html .= '<div class="legend">';
$html .= '<div class="legend-item blue"><div class="color-box"></div>Semináře</div>';
$html .= '<div class="legend-item green"><div class="color-box"></div>Ozdravné pobyty</div>';
$html .= '<div class="legend-item yellow"><div class="color-box"></div>Firemní</div>';
$html .= '<div class="legend-item red"><div class="color-box"></div>Ostatní</div>';
$html .= '</div>';
}
$html .= '</div>';
// Výpis dnů v týdnu
$html .= '<div class="days">';
foreach ($days as $day) {
$html .= '<div class="day_name">' . $day . '</div>';
}
// Prázdné dny na začátku měsíce
for ($i = 0; $i < $first_day_of_week; $i++) {
$html .= '<div class="day_num ignore"></div>';
}
// Výpis jednotlivých dnů a událostí
for ($i = 1; $i <= $num_days; $i++) {
$selected = '';
// Zvýrazní aktuální den pouze pokud je aktuální měsíc a rok
if (
$i == $this->active_day &&
$this->active_month == date('m') &&
$this->active_year == date('Y')
) {
$selected = ' selected';
}
$html .= '<div class="day_num' . $selected . '">';
$html .= '<span>' . $i . '</span>';
// Výpis událostí pro daný den
foreach ($this->events as $event) {
for ($d = 0; $d <= ($event[2] - 1); $d++) {
if (date('y-m-d', strtotime($this->active_year . '-' . $this->active_month . '-' . $i . ' -' . $d . ' day')) == date('y-m-d', strtotime($event[1]))) {
$monthParam = '&month=' . $this->active_month . '&year=' . $this->active_year;
$data_url = '?lectorId=' . $event[4] . '&eventId=' . $event[6] . $monthParam . '#modalPlace';
if ($event[5] == 1 || $event[5] == 2) {
$data_url = '?category=' . $event[5] . '&eventId=' . $event[6] . $monthParam . '#modalPlace';
}
$html .= '<div class="medailonek-link event' . $event[3] . '" data-url="' . $data_url . '">';
$html .= $event[0];
$html .= '</div>';
}
}
}
$html .= '</div>';
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
}
?>