oazaSrdceWeb / Calendar.php
Calendar.php
Raw
<?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;
    }
}
?>