<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kalendarz z Firebase</title> <!-- FullCalendar CSS --> <link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css" rel="stylesheet"> <!-- Najpierw FullCalendar, potem RRule --> <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/locales/pl.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/rrule@2.6.4/dist/es5/rrule.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@fullcalendar/rrule@5.11.3/main.global.min.js"></script> <!-- Firebase SDK --> <script src="https://www.gstatic.com/firebasejs/8.6.8/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/8.6.8/firebase-database.js"></script> <!-- Tippy.js (tooltip) --> <script src="https://unpkg.com/@popperjs/core@2"></script> <script src="https://unpkg.com/tippy.js@6"></script> <!-- FullCalendar List View CSS + JS --> <link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/list/main.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/list/main.min.js"></script> <style> .headerNaglowek { position: relative; width: 100%; height: auto; min-height: 200px; background: url('http://eclipse.elektron.pk.edu.pl/~plany-wieik/lib/exe/fetch.php?media=logo_2.png') no-repeat center center; background-size: contain; display: flex; justify-content: center; align-items: center; } .headerNaglowek img { width: 100%; max-width: 600px; height: auto; } .logo { margin: 0 20px; min-height: 100px; } .clickable-area { position: absolute; width: 80px; height: 80px; background-color: rgba(255, 0, 0, 0); cursor: pointer; } .left-logo { top: 15px; left: 8.1%; } .right-logo { top: 15px; right: 51%; } .clickable-area:hover { background-color: rgba(255, 0, 0, 0.0); } body { font-family: Comic Sans MS, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4; } .tab-container { display: flex; border-bottom: 3px solid #007BFF; margin-top: 20px; padding: 0; } .tab { padding: 12px 20px; cursor: pointer; background-color: #e0e0e0; border-radius: 5px 5px 0 0; margin-right: 10px; font-size: 1.2em; transition: background-color 0.3s ease; } .tab.active { background-color: #007BFF; color: white; font-weight: bold; } .sub-tab-container { display: flex; border-bottom: 2px solid #0056b3; margin: 0; padding: 0; margin-left: 10px; margin-top: 10px; } .sub-tab { padding: 8px 15px; cursor: pointer; background-color: #d0d0d0; border-radius: 5px 5px 0 0; margin-right: 8px; transition: background-color 0.3s ease; } .sub-tab.active { background-color: #0056b3; color: white; font-weight: bold; } .tab-content { display: none; padding: 0; background-color: white; border: 1px solid #ccc; border-radius: 5px; } .tab-content.active { display: block; width: 100%; } .selector-container { margin: 0; padding: 5px; display: inline-block; vertical-align: top; width: fit-content; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 5px; text-align: center; font-size: 1.3em; margin-left: 20px; } select { padding: 8px; font-size: 1.3em; line-height: 1.6em; } .iframe-container { width: 100%; height: 90vh; display: flex; justify-content: center; align-items: center; background: white; position: relative; } iframe { position: absolute; top: 60px; left: 0; /* width: 100vw;*/ height: calc(100vh - 60px); border: none; background: white; } .tab-content.active .pdf-viewer { display: block; } h2 { text-align: center; font-style: italic; font-weight: bold; } #calendar-kierunek { /* margin: 20px;*/ width: 100%; min-height: 80vh; display: block; } .filter-container { margin: 20px; text-align: center; } .filter-container label { font-size: 1.2em; margin-right: 10px; } .filter-container select { padding: 8px; font-size: 1em; border-radius: 5px; border: 1px solid #ccc; } @media (min-width: 1400px) { .headerNaglowek { max-width: 100vw; } .headerNaglowek img { max-width: 100vw; } } @media (max-width: 768px) { .headerNaglowek { width: 100%; height: auto; min-height: 80px; padding: 1px; } .headerNaglowek img { margin-top:5px; max-width: 300px; } .tab-container, .sub-tab-container { gap: 1px; } .tab { padding: 6px 10px; margin-right: 3px; font-size: 3vw; } .sub-tab { padding: 6px 10px; margin-right: 1px; font-size: 3vw; } h2 { font-size: 5vw; } .filter-container { margin: 10px; width: 100%; padding: 0 10px; box-sizing: border-box; } .filter-container label { display: block; margin-bottom: 30px; } .filter-container select { width: 90%; max-width: 100%; } } #calendar-kierunek { width: 100%; max-width: 100%; margin: 0 auto; background-color: white; padding: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); border-radius: 10px; min-height: 80vh; /* zachowuje wysokość */ box-sizing: border-box; overflow-x: auto; /* zabezpieczenie, gdyby coś mimo wszystko wylało */ } #calendar-kierunek-n { width: 100%; max-width: 100%; margin: 0 auto; background-color: white; padding: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); border-radius: 10px; min-height: 80vh; /* zachowuje wysokość */ box-sizing: border-box; overflow-x: auto; /* zabezpieczenie, gdyby coś mimo wszystko wylało */ } #calendar-sala { width: 100%; max-width: 100%; margin: 0 auto; background-color: white; padding: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); border-radius: 10px; min-height: 80vh; /* zachowuje wysokość */ box-sizing: border-box; overflow-x: auto; /* zabezpieczenie, gdyby coś mimo wszystko wylało */ } #calendar-instructor { width: 100%; max-width: 100%; margin: 0 auto; background-color: white; padding: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); border-radius: 10px; min-height: 80vh; /* zachowuje wysokość */ box-sizing: border-box; overflow-x: auto; /* zabezpieczenie, gdyby coś mimo wszystko wylało */ } .fc-timegrid-slot { height: 35px !important; /* Wysokość slotów */ } /* Zmiana koloru czcionki dla tytułów wydarzeń */ .fc-event-title { color: white; font-size: 10px; /* Dostosuj rozmiar czcionki według potrzeb */ white-space: normal; /* Zawijanie tekstu */ } .fc-event { border: none !important; border-line: none !important; } /* Styl dla dropdown */ #faculty-select, #faculty-select-n { margin-bottom: 20px; padding: 10px; font-size: 1.5em; border-radius: 5px; border: 1px solid #ccc; width: auto; height: 50px; } #room-select, #room-select-n { margin-bottom: 20px; padding: 10px; font-size: 1.5em; border-radius: 5px; border: 1px solid #ccc; width: auto; height: 50px; } #instructor-select,#instructor-select-n { margin-bottom: 20px; padding: 10px; font-size: 1.5em; border-radius: 5px; border: 1px solid #ccc; width: auto; height: 50px; } .tippy-box { white-space: pre-line; max-width: 400px; text-align: center; } .fc-bg-event { background-color: #d0d0d0 !important; /* jasnoszary */ } .fc-list-event-title { font-weight: bold; } .fc-list-day-text { font-size: 1.1em; color: #007BFF; } /* Pełna szerokość listy wydarzeń */ .fc .fc-list-event { width: 100% !important; max-width: 100% !important; display: flex; flex-direction: column; align-items: flex-start; } /* Szeroki wiersz */ .fc .fc-list-item { width: 100% !important; max-width: 100% !important; } /* Wyrównanie tekstu */ .fc .fc-list-event-title { white-space: normal !important; word-wrap: break-word; width: 100%; } /* Rozszerzenie tabeli listy */ .fc .fc-list-table { table-layout: auto !important; width: 100% !important; } .fc-list-table { width: 100% !important; } /* Usuwamy pogrubienie i poprawiamy rozmiar czcionki wydarzeń */ .fc-event-title, .fc-event-time, .fc-event-main { font-weight: normal !important; /* usuwa pogrubienie */ font-size: 0.9em !important; /* zmniejszamy czcionkę */ line-height: 1.2 !important; /* odstępy między liniami */ white-space: normal !important; /* zawijanie tekstu */ } /* ==== widoczność całej treści wydarzeń ==== */ .fc-event { white-space: normal !important; overflow-wrap: break-word !important; height: auto !important; /* pozwala rosnąć w pionie */ min-height: 20px; /* niech nie będą „żyletkami” */ } /* dopilnuj, że wnętrze kafelka też zawija */ .fc-timegrid-event .fc-event-main { white-space: normal !important; overflow-wrap: break-word !important; } /* przydatne w timeGrid: tekst godziny również może się łamać */ .fc-timegrid-event .fc-event-time { white-space: normal !important; } /* Nagłówki dni tygodnia */ .fc-col-header-cell-cushion { font-weight: bold; font-size: 11px; } /* Godziny w siatce (po lewej stronie) */ .fc-timegrid-slot-label { font-weight: normal; font-size: 10px; } /* ====== WYJŚCIE z wąskiego kontenera DokuWiki ====== */ #plan-viewport{ width: 100vw; position: relative; left: 50%; right: 50%; margin-left: -50vw; /* kotwica do lewej krawędzi okna */ margin-right: -50vw; /* kotwica do prawej krawędzi okna */ } /* ====== 80% szerokości OKNA, wyśrodkowane ====== */ #plan-wrapper{ width: 80vw; max-width: 1600px; /* opcjonalny limit */ margin: 0 auto; padding: 0 15px; box-sizing: border-box; } /* Telefon – wygodniej szerzej */ /* Telefon – pełna szerokość */ @media (max-width: 991px){ #plan-wrapper { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; } } /* Nic w środku nie próbuje mieć 100vw */ #plan-wrapper *{ max-width: 100%; } /* Iframe liczy szerokość do wrappera, nie do okna */ #plan-wrapper iframe{ width: 100% !important; left: 0 !important; } /* Czytelność samych godzin */ .fc-timegrid-slot-label, .fc-timegrid-slot-label-cushion { font-size: 12px !important; /* dopasuj do potrzeb */ line-height: 1.4 !important; white-space: nowrap !important; padding-right: 8px !important; text-align: right !important; } /* (opcjonalnie) responsywność */ @media (max-width: 768px){ .fc .fc-timegrid .fc-scrollgrid > colgroup > col:first-child, .fc .fc-timegrid .fc-scrollgrid-section > table > colgroup > col:first-child, .fc .fc-timegrid-axis, .fc .fc-timegrid-axis .fc-timegrid-axis-frame, .fc .fc-timegrid-slot-label { width: 56px !important; min-width: 56px !important; } } /* usuń ramkę DokuWiki wokół całego contentu */ .dokuwiki .panel, .dokuwiki .panel-default, .dokuwiki .panel-default > .panel-heading, .dokuwiki .panel-default > .panel-body { border: none !important; box-shadow: none !important; background: transparent !important; border-radius: 0 !important; } /* ===== Widok TABELI dla Niestacjonarnych ===== */ .scroll-container{ width:100%; height:90vh; overflow:auto; position:relative; } .grid-container{ display:grid; width: 95vw; transform-origin: top left; transform:scale(0.5); */ border-left:1px solid rgba(0,0,0,.3); border-bottom:1px solid rgba(0,0,0,.3); border-top:1px solid rgba(0,0,0,.3); } /* Gdy host jest pinchowalny, przechwytujemy gest */ .scroll-container.pinchable{ /* pozwól na natywne przewijanie w pionie jednym palcem */ touch-action: pan-x pan-y; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; /* opcjonalnie: unikaj „przeskoku” strony przy końcu listy */ } .pinch-content{ transform-origin: 0 0; will-change: transform; } .header-cell{ background:#007BFF; color:#fff; font-weight:700; font-size:12px; text-align:center; padding:4px; border-bottom:1px solid #ddd; display:flex; flex-direction:column; justify-content:center; line-height:1.2; } .header-break{ background:#ccc; color:transparent; border-bottom:1px solid #ddd; } .date-cell{ background:#f9f9f9; color:#000; font-weight:700; font-size:11px; text-align:center; border-bottom:1px solid #ccc; padding:4px; position:relative; } .cell,.break-cell{ position:relative; border-bottom:1px solid #ccc; min-height:50px; } .break-cell,.header-break,.date-cell,.header-cell{ position:relative; z-index:0; } .break-cell::before,.header-break::before,.date-cell::before,.header-cell::before{ content:""; position:absolute; left:0; top:0; width:1px; height:100%; background:rgba(0,0,0,.15); z-index:-1; } .date-cell::after,.header-cell:first-child::after,.cell:last-child::after,.header-cell:last-child::after,.break-cell:last-child::after,.header-break:last-child::after,.last-column::after{ content:""; position:absolute; right:0; top:0; width:1px; height:100%; background:rgba(0,0,0,.3); z-index:-1; } .event{ position:absolute; background:#1e90ff; color:#fff; border-radius:4px; font-size:9px; padding:4px 6px; line-height:1.2; overflow:hidden; white-space:normal; z-index:1; } .week-gray{ background:rgba(0,0,0,.1); } /* chowamy siatkę FC gdy aktywna tabela */ .fc-hidden-by-table { display: none !important; } #plan-wrapper .grid-container .event { max-width: none !important; /* nie ograniczaj do szerokości komórki */ display: block; /* pewność, że width działa jak trzeba */ } /* (zostaw z-index/overflow z poprzedniej poprawki) */ .scroll-container, .grid-container { overflow: visible !important; } .grid-container { position: relative; } .grid-container .cell, .grid-container .break-cell, .grid-container .header-cell, .grid-container .header-break, .grid-container .date-cell { z-index: auto !important; overflow: visible !important; } .grid-container .event { position: absolute; z-index: 999 !important; } </style> </head> <body> <!-- OUTER: pas o szerokości okna, żeby wyjść z kontenera DokuWiki --> <div id="plan-viewport"> <!-- INNER: właściwa treść o szerokości 80% okna --> <div id="plan-wrapper"> <!-- TU zostaje cała Twoja dotychczasowa zawartość (nagłówek, zakładki, kalendarz, itd.) --> <div class="headerNaglowek"> <a href="https://www.pk.edu.pl"> <div class="clickable-area left-logo"></div> </a> <a href="https://www.wieik.pk.edu.pl"> <div class="clickable-area right-logo"></div> </a> </div> <div class="tab-container"> <div class="tab active" onclick="openTab('tab1')">Stacjonarne</div> <div class="tab" onclick="openTab('tab2')">Niestacjonarne</div> </div> <div id="tab1" class="tab-content active"> <div class="sub-tab-container"> <div class="sub-tab active" onclick="openSubTab('tab1', 'sub1')">Harmonogram</div> <div class="sub-tab" onclick="openSubTab('tab1', 'sub2')">Sale</div> <div class="sub-tab" onclick="openSubTab('tab1', 'sub3')">Wykładowcy</div> </div> <div id="tab1-sub1" class="tab-content active"> <h2>Harmonogram semestr zimowy 2025/26</h2> <div class="filter-container"> <!-- <label for="faculty-select">Wybierz kierunek:</label> --> <select id="faculty-select"> <option value="EiAs1">Elektrotechnika i Automatyka semestr 1</option> <option value="EiAs3">Elektrotechnika i Automatyka semestr 3</option> <option value="EiAs5">Elektrotechnika i Automatyka semestr 5</option> <option value="EiAs7">Elektrotechnika i Automatyka semestr 7</option> <option value="EiAs7A">Elektrotechnika i Automatyka AwE semestr 7</option> <option value="EiAs7I">Elektrotechnika i Automatyka ISE semestr 7</option> <option value="IwIKs1">Informatyka w Inżynierii Komputerowej semestr 1</option> <option value="IwIKs3">Informatyka w Inżynierii Komputerowej semestr 3</option> <option value="IwIKs5">Informatyka w Inżynierii Komputerowej semestr 5</option> <option value="IwIKs7">Informatyka w Inżynierii Komputerowej semestr 7</option> <option value="Its2">Infotronika semestr 2</option> <option value="AwP4.0">Automatyka w Przemyśle 4.0 semestr 2</option> <option value="EE">Elektroenergetyka semestr 2</option> <option value="Erasmus">Erasmus</option> </select> </div> <div id="calendar-kierunek"></div> </div> <div id="tab1-sub2" class="tab-content"> <h2> Sale </h2> <div class="filter-container"> <!-- <label for="room-select">Wybierz salę:</label> --> <select id="room-select"> <option value="01A" > 01A </option> <option value="01B" > 01B </option> <option value="013A" > 013A </option> <option value="013B" > 013B </option> <option value="04" > 04 </option> <option value="06" > 06 </option> <option value="7" > 7 </option> <option value="8" > 8 </option> <option value="9" > 9 </option> <option value="10" > 10 </option> <option value="11" > 11 </option> <option value="12" > 12 </option> <option value="13" > 13 </option> <option value="18" > 18 </option> <option value="19" > 19 </option> <option value="101B" > 101B </option> <option value="101C" > 101C </option> <option value="109B" > 109B </option> <option value="201" > 201 </option> <option value="202" > 202 </option> <option value="208B" > 208B </option> <option value="208C" > 208C </option> <option value="208E" > 208E </option> <option value="208G" > 208G </option> <option value="209" > 209 </option> <option value="A1" > A1 </option> <option value="A2" > A2 </option> <option value="A3" > A3 </option> <option value="A4" > A4 </option> <option value="LZT b10-14" > LZT b10-14 </option> <option value="TWN 1/2" > TWN 1/2 </option> <option value="WA2" > WA2 </option> <option value="WA202" > WA202 </option> <option value="WA203" > WA203 </option> </select> </div> <div id="calendar-sala"></div> </div> <div id="tab1-sub3" class="tab-content"> <h2>Wykładowcy</h2> <div class="filter-container"> <!-- <label for="instructor-select">Wybierz wykładowcę:</label> --> <select id="instructor-select"> <option value="dr inż. P. Albrechtowicz"> Albrechtowicz Paweł </option> <option value="dr inż. W. Bańkosz"> Bańkosz Wojciech </option> <option value="dr inż. S. Bartel"> Bartel Sebastian </option> <option value="dr inż. S. Bąk"> Bąk Sławomir </option> <option value="dr inż. K. Bolek"> Bolek Krzysztof </option> <option value="mgr M. Ból"> Ból Marcin </option> <option value="dr inż. A. Chochla"> Chochla Adam </option> <option value="mgr inż. K. Chojkowski"> Chojkowski Kacper </option> <option value="prof. I. Chrabąszcz"> Chrabąszcz Ireneusz </option> <option value="mgr inż. K. Czajkowski"> Czajkowski Krzysztof </option> <option value="dr J. Czajkowski"> Czajkowski Jan</option> <option value="mgr inż. M. Czechowski"> Czechowski Mirosław </option> <option value="dr inż. W. Czuchra"> Czuchra Wojciech </option> <option value="dr inż. D. Dorota"> Dorota Dariusz </option> <option value="prof. P. Drozdowski"> Drozdowski Piotr </option> <option value="dr inż. A. Drwal"> Drwal Andrzej </option> <option value="dr inż. A. Duda"> Duda Arkadiusz </option> <option value="dr inż. M. Dudzik"> Dudzik Marek </option> <option value="dr M. Duras"> Duras Maciej </option> <option value="dr inż. A. Dziechciarz"> Dziechciarz Arkadiusz </option> <option value="mgr J. Firganek"> Firganek Justyna </option> <option value="mgr inż. T. Gębarowski"> Gębarowski Tomasz </option> <option value="mgr inż. M. Gibas"> Gibas Maciej </option> <option value="prof. J. Gołdasz"> Gołdasz Janusz </option> <option value="dr inż. D. Grela"> Grela Damian </option> <option value="mgr A. Gunia-Tracz"> Gunia-Tracz Anna </option> <option value="mgr inż. D. Gutenko"> Gutenko Denys </option> <option value="mgr inż. K. Hatłas"> Hatłas Kacper </option> <option value="dr inż. K. Hawron"> Hawron Konrad </option> <option value="mgr inż. R. Jacukowicz"> Jacukowicz Rafał </option> <option value="prof. M. Jaraczewski"> Jaraczewski Marcin </option> <option value="mgr inż. M. Jaśkiewicz"> Jaśkiewicz Michał </option> <option value="mgr inż. A. Kania"> Kania Agata </option> <option value="mgr P. Kędra"> Kędra Piotr </option> <option value="mgr inż. K. Kiełkowicz"> Kiełkowicz Kazimierz </option> <option value="mgr inż. G. Klęczar"> Klęczar Grzegorz </option> <option value="prof. K. Kluszczyński"> Kluszczyński Krzysztof </option> <option value="mgr inż. T. Kocot"> Kocot Tomasz </option> <option value="prof. Z. Kokosiński"> Kokosiński Zbigniew </option> <option value="dr inż. P. Król"> Król Paweł </option> <option value="mgr inż. J. Krupiński"> Krupiński Jan </option> <option value="mgr inż. B. Ligęza"> Ligęza Bartłomiej </option> <option value="mgr inż. D. Liszka"> Liszka Damian </option> <option value="mgr inż. W. Maciołek"> Maciołek Wacław </option> <option value="dr inż. T. Makowski"> Makowski Tomasz </option> <option value="dr inż. Z. Małecka"> Małecka Zuzanna </option> <option value="prof. P. Małka"> Małka Piotr </option> <option value="mgr inż. D. Mamcarz"> Mamcarz Dominik </option> <option value="mgr inż. M. Markowska"> Markowska Monika </option> <option value="prof. W. Mazgaj"> Mazgaj Witold </option> <option value="prof. R. Mielnik"> Mielnik Ryszard </option> <option value="mgr inż. Z. Mielnik"> Mielnik Zuzanna </option> <option value="mgr inż. S. Nachman"> Nachman Szymon </option> <option value="mgr inż. T. Naczyński"> Naczyński Tymoteusz </option> <option value="mgr inż. P. Natkaniec"> Natkaniec Piotr </option> <option value="mgr inż. G. Nowakowski"> Nowakowski Grzegorz </option> <option value="dr inż. M. Orkisz"> Orkisz Michał </option> <option value="mgr inż. P. Oziębło"> Oziębło Paweł </option> <option value="dr inż. M. Pawlik"> Pawlik Marcin </option> <option value="dr inż. G. Pędrak"> Pędrak Grzegorz </option> <option value="dr M. Piekarski"> Piekarski Marian </option> <option value="dr inż. M. Piękoś"> Piękoś Mariusz </option> <option value="dr inż. Z. Pilch"> Pilch Zbigniew </option> <option value="mgr inż. Ł. Pluszyński"> Pluszyński Łukasz </option> <option value="dr inż. J. Prusak"> Prusak Janusz </option> <option value="dr inż. N. Radwan-Pragłowska"> Radwan-Pragłowska Natalia </option> <option value="mgr inż. M. Raźny"> Raźny Mateusz </option> <option value="dr inż. A. Romańska"> Romańska Anna </option> <option value="dr inż. B. Rozegnał"> Rozegnał Bartosz </option> <option value="dr inż. R. Rucki"> Rucki Rafał </option> <option value="prof. R. Sałat"> Sałat Robert </option> <option value="prof. V. Samotyy"> Samotyy Volodymyr </option> <option value="dr inż. K. Schiff"> Schiff Krzysztof </option> <option value="dr inż. A. Shymanska"> Shymanska Anna </option> <option value="dr inż. M. Sieja"> Sieja Marek </option> <option value="dr inż. T. Sieńko"> Sieńko Tomasz </option> <option value="dr inż. M. Sierżęga"> Sierżęga Michał </option> <option value="dr inż. D. Smugała"> Smugała Dariusz </option> <option value="mgr inż. K. Sołtys"> Sołtys Krzysztof </option> <option value="dr inż. Ł. Sołtysek"> Sołtysek Łukasz </option> <option value="dr inż. J. Strug"> Strug Joanna </option> <option value="dr inż. A. Suchenia"> Suchenia Anna </option> <option value="dr inż. K. Suchenia"> Suchenia Karol </option> <option value="dr inż. A. Surówka"> Surówka Adam </option> <option value="mgr inż. B. Sysło"> Sysło Bartłomiej </option> <option value="mgr E. Szabat"> Szabat Elżbieta </option> <option value="prof. J. Szczepanik"> Szczepanik Jerzy </option> <option value="prof. A. Szromba"> Szromba Andrzej </option> <option value="mgr inż. L. Szul"> Szul Lucyna </option> <option value="dr inż. Z. Szular"> Szular Zbigniew </option> <option value="prof. Ł. Ścisło"> Ścisło Łukasz </option> <option value="dr inż. M. Święcicki"> Święcicki Mariusz </option> <option value="mgr inż. M. Tadus"> Tadus Marek </option> <option value="mgr E. Targosz"> Targosz Ewa </option> <option value="prof. S. Telenyk"> Telenyk Sergii </option> <option value="mgr inż. P. Tofilski"> Tofilski Paweł </option> <option value="prof. K. Tomczyk"> Tomczyk Krzysztof </option> <option value="dr inż. J. Tulicki"> Tulicki Jarosław </option> <option value="prof. A. Warzecha"> Warzecha Adam </option> <option value="mgr inż. M. Wawro"> Wawro Marcin </option> <option value="prof. K. Węc"> Węc Klaudia </option> <option value="prof. T. Węgiel"> Węgiel Tomasz </option> <option value="dr inż. M. Węgrzyn"> Węgrzyn Mariusz </option> <option value="mgr inż. R. Wiśniewski"> Wiśniewski Robert </option> <option value="mgr inż. M. Worwa"> Worwa Miłosz </option> <option value="dr inż. B. Woszczyna"> Woszczyna Bartosz </option> <option value="mgr inż. J. Zielonka"> Zielonka Jakub </option> <option value="dr inż. S. Żaba"> Żaba Sławomir </option> <option value="mgr inż. M. Żabiński"> Żabiński Mateusz </option> </select> </div> <div id="calendar-instructor"></div> <div id="n-table-host-instructor" class="scroll-container" style="display:none;"> <div id="n-grid-instructor" class="grid-container"></div> </div> </div> </div> <div id="tab2" class="tab-content"> <div class="sub-tab-container"> <div class="sub-tab active" onclick="openSubTab('tab2', 'sub1')">Harmonogram</div> <div class="sub-tab" onclick="openSubTab('tab2', 'sub2')">Sale</div> <div class="sub-tab" onclick="openSubTab('tab2', 'sub3')">Wykładowcy</div> </div> <div id="tab2-sub1" class="tab-content active"> <h2>Harmonogram semestr zimowy 2025/26</h2> <div class="filter-container"> <!-- <label for="faculty-select">Wybierz kierunek:</label> --> <select id="faculty-select-n"> <option value="nEiAs1">Elektrotechnika i Automatyka semestr 1</option> <option value="nEiAs3">Elektrotechnika i Automatyka semestr 3</option> <option value="nEiAs5">Elektrotechnika i Automatyka semestr 5</option> <option value="nEiAs7 AwUE">Elektrotechnika i Automatyka AwUE semestr 7</option> <option value="nEiAs7 ISE">Elektrotechnika i Automatyka ISE semestr 7</option> <option value="nIwIKs1">Informatyka w Inżynierii Komputerowej semestr 1</option> <option value="nIwIKs3">Informatyka w Inżynierii Komputerowej semestr 3</option> <option value="nIwIKs5">Informatyka w Inżynierii Komputerowej semestr 5</option> <option value="nIwIKs7">Informatyka w Inżynierii Komputerowej semestr 7</option> <option value="n2sEiAs1">Elektrotechnika i Automatyka II stopień semestr 1</option> <option value="n2sEiAs3">Elektrotechnika i Automatyka II stopień semestr 3</option> <option value="n2sIts1">Infotronika II stopień semestr 1</option> </select> </div> <div id="calendar-kierunek-n"></div> <div id="n-table-host-kierunek" class="scroll-container" style="display:none;"> <div id="n-grid-kierunek" class="grid-container"></div> </div> </div> <div id="tab2-sub2" class="tab-content"> <h2> Sale </h2> <div class="filter-container"> <!-- <label for="room-select">Wybierz salę:</label> --> <select id="room-select-n"> <option value="01A" > 01A </option> <option value="01B" > 01B </option> <option value="013A" > 013A </option> <option value="013B" > 013B </option> <option value="04" > 04 </option> <option value="06" > 06 </option> <option value="7" > 7 </option> <option value="8" > 8 </option> <option value="9" > 9 </option> <option value="10" > 10 </option> <option value="11" > 11 </option> <option value="12" > 12 </option> <option value="13" > 13 </option> <option value="18" > 18 </option> <option value="19" > 19 </option> <option value="101B" > 101B </option> <option value="101C" > 101C </option> <option value="109B" > 109B </option> <option value="201" > 201 </option> <option value="202" > 202 </option> <option value="208B" > 208B </option> <option value="208C" > 208C </option> <option value="208E" > 208E </option> <option value="208G" > 208G </option> <option value="209" > 209 </option> <option value="A1" > A1 </option> <option value="A2" > A2 </option> <option value="A3" > A3 </option> <option value="A4" > A4 </option> <option value="LZT b10-14" > LZT b10-14 </option> <option value="TWN 1/2" > TWN 1/2 </option> <option value="WA2" > WA2 </option> <option value="WA202" > WA202 </option> <option value="WA203" > WA203 </option> </select> </div> <div id="calendar-sala-n"></div> <div id="n-table-host-sala" class="scroll-container" style="display:none;"> <div id="n-grid-sala" class="grid-container"></div> </div> </div> <div id="tab2-sub3" class="tab-content"> <h2>Wykładowcy</h2> <div class="filter-container"> <!-- <label for="instructor-select">Wybierz wykładowcę:</label> --> <select id="instructor-select-n"> <option value="dr inż. P. Albrechtowicz"> Albrechtowicz Paweł </option> <option value="dr inż. W. Bańkosz"> Bańkosz Wojciech </option> <option value="dr inż. S. Bartel"> Bartel Sebastian </option> <option value="dr inż. S. Bąk"> Bąk Sławomir </option> <option value="dr inż. K. Bolek"> Bolek Krzysztof </option> <option value="mgr M. Ból"> Ból Marcin </option> <option value="dr inż. A. Chochla"> Chochla Adam </option> <option value="mgr inż. K. Chojkowski"> Chojkowski Kacper </option> <option value="prof. I. Chrabąszcz"> Chrabąszcz Ireneusz </option> <option value="mgr inż. K. Czajkowski"> Czajkowski Krzysztof </option> <option value="dr J. Czajkowski"> Czajkowski Jan</option> <option value="mgr inż. M. Czechowski"> Czechowski Mirosław </option> <option value="dr inż. W. Czuchra"> Czuchra Wojciech </option> <option value="dr inż. D. Dorota"> Dorota Dariusz </option> <option value="prof. P. Drozdowski"> Drozdowski Piotr </option> <option value="dr inż. A. Drwal"> Drwal Andrzej </option> <option value="dr inż. A. Duda"> Duda Arkadiusz </option> <option value="dr inż. M. Dudzik"> Dudzik Marek </option> <option value="dr M. Duras"> Duras Maciej </option> <option value="dr inż. A. Dziechciarz"> Dziechciarz Arkadiusz </option> <option value="mgr J. Firganek"> Firganek Justyna </option> <option value="mgr inż. T. Gębarowski"> Gębarowski Tomasz </option> <option value="mgr inż. M. Gibas"> Gibas Maciej </option> <option value="prof. J. Gołdasz"> Gołdasz Janusz </option> <option value="dr inż. D. Grela"> Grela Damian </option> <option value="mgr A. Gunia-Tracz"> Gunia-Tracz Anna </option> <option value="mgr inż. D. Gutenko"> Gutenko Denys </option> <option value="mgr inż. K. Hatłas"> Hatłas Kacper </option> <option value="dr inż. K. Hawron"> Hawron Konrad </option> <option value="mgr inż. R. Jacukowicz"> Jacukowicz Rafał </option> <option value="prof. M. Jaraczewski"> Jaraczewski Marcin </option> <option value="mgr inż. M. Jaśkiewicz"> Jaśkiewicz Michał </option> <option value="mgr inż. A. Kania"> Kania Agata </option> <option value="mgr P. Kędra"> Kędra Piotr </option> <option value="mgr inż. K. Kiełkowicz"> Kiełkowicz Kazimierz </option> <option value="mgr inż. G. Klęczar"> Klęczar Grzegorz </option> <option value="prof. K. Kluszczyński"> Kluszczyński Krzysztof </option> <option value="mgr inż. T. Kocot"> Kocot Tomasz </option> <option value="prof. Z. Kokosiński"> Kokosiński Zbigniew </option> <option value="dr inż. P. Król"> Król Paweł </option> <option value="mgr inż. J. Krupiński"> Krupiński Jan </option> <option value="mgr inż. B. Ligęza"> Ligęza Bartłomiej </option> <option value="mgr inż. D. Liszka"> Liszka Damian </option> <option value="mgr inż. W. Maciołek"> Maciołek Wacław </option> <option value="dr inż. T. Makowski"> Makowski Tomasz </option> <option value="dr inż. Z. Małecka"> Małecka Zuzanna </option> <option value="prof. P. Małka"> Małka Piotr </option> <option value="mgr inż. D. Mamcarz"> Mamcarz Dominik </option> <option value="mgr inż. M. Markowska"> Markowska Monika </option> <option value="prof. W. Mazgaj"> Mazgaj Witold </option> <option value="prof. R. Mielnik"> Mielnik Ryszard </option> <option value="mgr inż. Z. Mielnik"> Mielnik Zuzanna </option> <option value="mgr inż. S. Nachman"> Nachman Szymon </option> <option value="mgr inż. T. Naczyński"> Naczyński Tymoteusz </option> <option value="mgr inż. P. Natkaniec"> Natkaniec Piotr </option> <option value="mgr inż. G. Nowakowski"> Nowakowski Grzegorz </option> <option value="dr inż. M. Orkisz"> Orkisz Michał </option> <option value="mgr inż. P. Oziębło"> Oziębło Paweł </option> <option value="dr inż. M. Pawlik"> Pawlik Marcin </option> <option value="dr inż. G. Pędrak"> Pędrak Grzegorz </option> <option value="dr M. Piekarski"> Piekarski Marian </option> <option value="dr inż. M. Piękoś"> Piękoś Mariusz </option> <option value="dr inż. Z. Pilch"> Pilch Zbigniew </option> <option value="mgr inż. Ł. Pluszyński"> Pluszyński Łukasz </option> <option value="dr inż. J. Prusak"> Prusak Janusz </option> <option value="dr inż. N. Radwan-Pragłowska"> Radwan-Pragłowska Natalia </option> <option value="mgr inż. M. Raźny"> Raźny Mateusz </option> <option value="dr inż. A. Romańska"> Romańska Anna </option> <option value="dr inż. B. Rozegnał"> Rozegnał Bartosz </option> <option value="dr inż. R. Rucki"> Rucki Rafał </option> <option value="prof. R. Sałat"> Sałat Robert </option> <option value="prof. V. Samotyy"> Samotyy Volodymyr </option> <option value="dr inż. K. Schiff"> Schiff Krzysztof </option> <option value="dr inż. A. Shymanska"> Shymanska Anna </option> <option value="dr inż. M. Sieja"> Sieja Marek </option> <option value="dr inż. T. Sieńko"> Sieńko Tomasz </option> <option value="dr inż. M. Sierżęga"> Sierżęga Michał </option> <option value="dr inż. D. Smugała"> Smugała Dariusz </option> <option value="mgr inż. K. Sołtys"> Sołtys Krzysztof </option> <option value="dr inż. Ł. Sołtysek"> Sołtysek Łukasz </option> <option value="dr inż. J. Strug"> Strug Joanna </option> <option value="dr inż. A. Suchenia"> Suchenia Anna </option> <option value="dr inż. K. Suchenia"> Suchenia Karol </option> <option value="dr inż. A. Surówka"> Surówka Adam </option> <option value="mgr inż. B. Sysło"> Sysło Bartłomiej </option> <option value="mgr E. Szabat"> Szabat Elżbieta </option> <option value="prof. J. Szczepanik"> Szczepanik Jerzy </option> <option value="prof. A. Szromba"> Szromba Andrzej </option> <option value="mgr inż. L. Szul"> Szul Lucyna </option> <option value="dr inż. Z. Szular"> Szular Zbigniew </option> <option value="prof. Ł. Ścisło"> Ścisło Łukasz </option> <option value="dr inż. M. Święcicki"> Święcicki Mariusz </option> <option value="mgr inż. M. Tadus"> Tadus Marek </option> <option value="mgr E. Targosz"> Targosz Ewa </option> <option value="prof. S. Telenyk"> Telenyk Sergii </option> <option value="mgr inż. P. Tofilski"> Tofilski Paweł </option> <option value="prof. K. Tomczyk"> Tomczyk Krzysztof </option> <option value="dr inż. J. Tulicki"> Tulicki Jarosław </option> <option value="prof. A. Warzecha"> Warzecha Adam </option> <option value="mgr inż. M. Wawro"> Wawro Marcin </option> <option value="prof. K. Węc"> Węc Klaudia </option> <option value="prof. T. Węgiel"> Węgiel Tomasz </option> <option value="dr inż. M. Węgrzyn"> Węgrzyn Mariusz </option> <option value="mgr inż. R. Wiśniewski"> Wiśniewski Robert </option> <option value="mgr inż. M. Worwa"> Worwa Miłosz </option> <option value="dr inż. B. Woszczyna"> Woszczyna Bartosz </option> <option value="mgr inż. J. Zielonka"> Zielonka Jakub </option> <option value="dr inż. S. Żaba"> Żaba Sławomir </option> <option value="mgr inż. M. Żabiński"> Żabiński Mateusz </option> </select> </div> <div id="calendar-instructor-n"></div> <!-- Niestacjonarne → Wykładowcy --> <div id="n-table-host-instructor-n" class="scroll-container" style="display:none;"> <div id="n-grid-instructor-n" class="grid-container"></div> </div> </div> </div> <script> window._caloscActive = window._caloscActive || {}; // flaga per kalendarz const KNOWN_CAL_IDS = [ 'calendar-kierunek','calendar-kierunek-n', 'calendar-sala','calendar-sala-n', 'calendar-instructor','calendar-instructor-n' ]; // Lista kolorów //const kolory = [ // '#36b9e4', '#0ea3ff', '#ff3352', '#ff4f09', '#1eb984', // '#0cff0c', '#50ffff', '#b29600', '#faa700', '#ff9435', // '#ffb432', '#ffa63d', '#ff4f09', '#5555fb', '#b567b5', // '#9c10bf', '#b28200', '#ecec5a', '#ff5b92', '#ffff43' //]; const kolory = [ '#1a237e','#1e90ff','#00bfff','#800080','#c71585','#ff69b4', '#32cd32','#006400','#800000','#ff4500','#ff6347','#4682b4', '#da70d6','#a0522d','#5f9ea0','#4169e1','#556b2f' ]; const koloryPrzedmiotow = new Map(); function przypiszKolorPrzedmiotowi(przedmiot) { if (!koloryPrzedmiotow.has(przedmiot)) { const index = koloryPrzedmiotow.size % kolory.length; koloryPrzedmiotow.set(przedmiot, kolory[index]); } return koloryPrzedmiotow.get(przedmiot); } // --- REZERWACJE: pobranie i mapowanie do FullCalendar --- async function fetchRezerwacje() { const url = "https://rezerwacja-91513-default-rtdb.europe-west1.firebasedatabase.app/rezerwacje.json"; const res = await fetch(url); const data = await res.json() || {}; const out = []; Object.values(data).forEach(ev => { const eventType = ev.eventType || ev.rodzaj || 'Rezerwacja'; const instructor = ev.instructor || ev.prowadzacy || ''; // 2 możliwe struktury: (A) terminy[ ] LUB (B) pojedyncze pola data/start/koniec/room if (Array.isArray(ev.terminy) && ev.terminy.length) { ev.terminy.forEach(t => { const start = t.start || `${t.startDate}T${t.startTime}:00`; const end = t.end || `${t.startDate}T${t.endTime}:00`; out.push({ title: `${eventType}${instructor ? ", " + instructor : ""}${t.room ? ", s. " + t.room : ""}`, start, end, extendedProps: { room: t.room || '', eventType, instructor } }); }); } else if (ev.data && (ev.start || ev.koniec)) { const start = `${ev.data}T${(ev.start||'00:00')}:00`; const end = `${ev.data}T${(ev.koniec||'00:00')}:00`; out.push({ title: `${eventType}${instructor ? ", " + instructor : ""}${ev.room ? ", s. " + ev.room : ""}`, start, end, extendedProps: { room: ev.room || '', eventType, instructor } }); } }); return out; } let zablokowaneZakresy = []; function pobierzZablokowaneDaty() { return fetch("https://plan-219ec-default-rtdb.europe-west1.firebasedatabase.app/zablokowaneDaty.json") .then(res => res.json()) .then(data => { const zakresy = []; for (const key in data) { const entry = data[key]; if (entry.od && entry.do) { zakresy.push({ od: entry.od, do: entry.do }); } else if (typeof entry === 'string') { zakresy.push({ od: entry, do: entry }); } } return zakresy; }); } function czyDataZablokowana(dataStr) { const data = new Date(dataStr); return zablokowaneZakresy.some(zakres => { const od = new Date(zakres.od); const do_ = new Date(zakres.do); return data >= od && data <= do_; }); } function ukryjSlotyPrzed16DlaPiatku(calendarEl) { const dni = calendarEl.querySelectorAll('.fc-timegrid-slot'); dni.forEach(slot => { const time = slot.getAttribute('data-time'); const colIndex = slot.closest('table').querySelector('thead .fc-col-header-cell[data-day-index="5"]'); if (colIndex && time < '16:00:00') { const cells = slot.querySelectorAll('td'); if (cells.length >= 6) { cells[5].style.display = 'none'; // kolumna piątkowa ma index 5 } } }); } document.addEventListener('DOMContentLoaded', function () { const calendarEl = document.getElementById('calendar-kierunek'); const facultySelect = document.getElementById('faculty-select'); const calendar = new FullCalendar.Calendar(calendarEl, { initialView: 'timeGridWeek', slotEventOverlap: false, // initialView: 'listYear', // initialDate: '2025-03-01', //ograniczenie wyświetlania validRange: { start: '2025-09-01', end: '2025-10-01' }, headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }, allDaySlot: false, height: 'auto', weekends: false, slotMinTime: '07:00:00', slotMaxTime: '21:30:00', locale: 'pl', timeZone: 'local', headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }, events: async function (fetchInfo, successCallback, failureCallback) { try { const selectedFaculty = facultySelect.value; // 1) STACJONARNE (jak było) const res = await fetch("https://plan-219ec-default-rtdb.europe-west1.firebasedatabase.app/wydarzenia.json"); const data = await res.json(); const events = []; for (const key in data) { const eventData = data[key]; if (selectedFaculty === "Wszystkie" || eventData.faculty === selectedFaculty) { const e = { title: eventData.title, extendedProps: { room: eventData.room, eventType: eventData.eventType, instructor: eventData.instructor || eventData.prowadzacy || '' // NEW } }; if (eventData.rrule) { e.rrule = eventData.rrule; if (eventData.duration) e.duration = eventData.duration; if (eventData.excludedDates) { e.exdate = Object.values(eventData.excludedDates).map(dateStr => eventData.startTime ? `${dateStr}T${eventData.startTime}` : dateStr ); } } else { e.start = eventData.start; e.end = eventData.end; } events.push(e); } } successCallback(events); } catch (err) { console.error("Błąd pobierania wydarzeń:", err); failureCallback(err); } }, eventDidMount: function (info) { const przedmiot = info.event.extendedProps.eventType; const kolor = przypiszKolorPrzedmiotowi(przedmiot); info.el.style.backgroundColor = kolor; info.el.style.borderColor = kolor; info.el.setAttribute('data-tippy-content', info.event.title); if (czyDataZablokowana(info.event.start)) { info.el.style.display = 'none'; } } }); window._calendars = window._calendars || {}; window._calendars['calendar-kierunek'] = calendar; calendar.render(); pobierzZablokowaneDaty().then(zakresy => { zablokowaneZakresy = zakresy; calendar.refetchEvents(); calendar.addEventSource({ events: function (fetchInfo, successCallback) { const bgEvents = zablokowaneZakresy.map(z => ({ start: z.od, end: z.do, display: 'background', color: '#e0e0e0' })); successCallback(bgEvents); } }); }); facultySelect.addEventListener('change', () => { calendar.refetchEvents(); refreshNTable('calendar-kierunek', 'faculties', facultySelect.value); }); tippy.delegate('#calendar-kierunek', { target: '.fc-event', placement: 'top', theme: 'light', arrow: true, animation: 'scale', offset: [0, 8], duration: [200, 150], inertia: true, delay: [0, 0], }); // Analogiczne kalendarze: SALA i INSTRUCTOR const calendarFactory = (elementId, selectId, filterFn) => { const calendarEl = document.getElementById(elementId); const selectEl = document.getElementById(selectId); const calendar = new FullCalendar.Calendar(calendarEl, { initialView: 'timeGridWeek', slotEventOverlap: false, // initialView: 'listYear', //initialDate: '2025-03-01', //ograniczenie wyświetlania validRange: { start: '2025-09-01', end: '2025-10-01' }, headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }, allDaySlot: false, height: 'auto', weekends: false, slotMinTime: '07:00:00', slotMaxTime: '21:30:00', locale: 'pl', timeZone: 'local', headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }, events: async function (fetchInfo, successCallback, failureCallback) { const selected = selectEl.value; try { // STACJONARNE const res = await fetch("https://plan-219ec-default-rtdb.europe-west1.firebasedatabase.app/wydarzenia.json"); const data = await res.json(); const events = []; for (const key in data) { const eventData = data[key]; if (filterFn(eventData, selected)) { const e = { title: eventData.title, extendedProps: { room: eventData.room, eventType: eventData.eventType, instructor: eventData.instructor || eventData.prowadzacy || '' } }; if (eventData.rrule) { e.rrule = eventData.rrule; if (eventData.duration) e.duration = eventData.duration; if (eventData.excludedDates) { e.exdate = Object.values(eventData.excludedDates).map(dateStr => eventData.startTime ? `${dateStr}T${eventData.startTime}` : dateStr ); } } else { e.start = eventData.start; e.end = eventData.end; } events.push(e); } } // REZERWACJE → tylko tam, gdzie trzeba let rez = await fetchRezerwacje(); if (elementId.includes('sala')) { // zakładka SALA rez = rez.filter(e => (e.extendedProps.room || '') === selected); } else if (elementId.includes('instructor')) { // zakładka WYKŁADOWCY // jeśli chcesz dopasowanie „zawiera”, użyj includes zamiast === const sel = String(selected).trim(); rez = rez.filter(e => (e.extendedProps.instructor || '').includes(sel)); } else { // np. Harmonogram – nie dokładamy rezerwacji rez = []; } events.push(...rez); successCallback(events); } catch (error) { console.error("Błąd pobierania danych:", error); failureCallback(error); } }, eventDidMount: function (info) { const przedmiot = info.event.extendedProps.eventType; const kolor = przypiszKolorPrzedmiotowi(przedmiot); info.el.style.backgroundColor = kolor; info.el.style.borderColor = kolor; info.el.setAttribute('data-tippy-content', info.event.title); // 🔒 Ukryj, jeśli wypada w zablokowanym dniu if (czyDataZablokowana(info.event.start)) { info.el.style.display = 'none'; } } }); window._calendars = window._calendars || {}; window._calendars[elementId] = calendar; calendar.render(); calendar.addEventSource({ events: function (fetchInfo, successCallback) { const bgEvents = zablokowaneZakresy.map(z => ({ start: z.od, end: z.do, display: 'background', color: '#e0e0e0' })); successCallback(bgEvents); } }); // dobierz poprawny kontekst do filtra tabeli: const ctx = elementId.includes('sala') ? 'rooms' : elementId.includes('instructor') ? 'instructors' : 'faculties'; selectEl.addEventListener('change', () => { calendar.refetchEvents(); // odśwież FC refreshNTable(elementId, ctx, selectEl.value); // odśwież „Całość” jeśli aktywna }); tippy.delegate(`#${elementId}`, { target: '.fc-event', placement: 'top', theme: 'light', arrow: true, animation: 'scale', offset: [0, 8], duration: [200, 150], inertia: true, delay: [0, 0], }); }; calendarFactory('calendar-sala', 'room-select', (event, selected) => selected === 'Wszystkie' || event.room === selected ); calendarFactory('calendar-instructor', 'instructor-select', (event, selected) => selected === 'Wszystko' || event.title.includes(selected) ); // ================= calendarFactoryNiestacjonarne ================= const dniNiestacjonarne = [1, 2, 3, 4]; const calendarFactoryNiestacjonarne = (elementId, selectId, filterFn) => { const calendarEl = document.getElementById(elementId); const selectEl = document.getElementById(selectId); const calendar = new FullCalendar.Calendar(calendarEl, { initialView: 'timeGridWeek', //ograniczenie validRange: { start: '2025-09-01', end: '2025-10-01' }, slotEventOverlap: false, allDaySlot: false, hiddenDays: [1,2,3,4], height: 'auto', weekends: true, slotMinTime: '07:00:00', slotMaxTime: '21:30:00', locale: 'pl', timeZone: 'local', customButtons:{ calosc:{ text:'Całość', click:() => _toggleNTable(elementId, selectEl.value) } }, headerToolbar:{ left:'prev,next today', center:'title', right:'dayGridMonth,timeGridWeek,timeGridDay,calosc' }, events: async function (fetchInfo, successCallback, failureCallback) { const selected = selectEl.value; try { // 1) NIestacjonarne – jak było const res = await fetch("https://niestacjonarne-fcc4f-default-rtdb.europe-west1.firebasedatabase.app/wydarzenian.json"); const data = await res.json(); const events = []; for (const key in data) { const e = data[key]; // terminy (bierzemy salę z terminu) if (e.terminy) { Object.values(e.terminy).forEach(termin => { if (filterFn(e, selected, termin)) { events.push({ title: e.title, title2: e.eventType, start: termin.start, end: termin.end, extendedProps: { room: termin.room || '', faculty: e.faculty || '', instructor: e.instructor || '', eventType: e.eventType, group: termin.group || '' } }); } }); } else if (e.rrule) { if (filterFn(e, selected)) { events.push({ title: e.title, rrule: e.rrule, extendedProps: { room: e.room || '', faculty: e.faculty || '', instructor: e.instructor || '', eventType: e.eventType }, duration: e.duration || undefined }); } } else if (e.start && e.end) { if (filterFn(e, selected)) { events.push({ title: e.title, start: e.start, end: e.end, extendedProps: { room: e.room || '', faculty: e.faculty || '', instructor: e.instructor || '', eventType: e.eventType } }); } } } // 2) REZERWACJE – dopinamy TYLKO do „Sala N” i „Prowadzący N” let rez = []; if (elementId.includes('sala-n')) { rez = (await fetchRezerwacje()).filter(ev => (ev.extendedProps.room || '') === String(selected) ); } else if (elementId.includes('instructor-n')) { // jeśli w selectach masz skróty / różne warianty nazwisk, zmień na .includes(...) const sel = String(selected).trim(); rez = (await fetchRezerwacje()).filter(ev => (ev.extendedProps.instructor || '').includes(sel) ); } else { // „Harmonogram N” – nie dokładamy rezerwacji rez = []; } events.push(...rez); successCallback(events); } catch (err) { console.error(err); failureCallback(err); } }, eventContent: function(arg) { const start = arg.timeText ? `${arg.timeText}` : ''; const title2 = arg.event.extendedProps.eventType || arg.event.title2; // 👈 nazwa zdarzenia const group = arg.event.extendedProps.group; const instructor = arg.event.extendedProps.instructor; const room = arg.event.extendedProps.room; const faculty = arg.event.extendedProps.faculty; return { html: ` <div style="font-size:10px; color:#fff;"> <div><b>${start}</b></div> <div>${title2}${group ? `, ${group}` : ''}</div> ${instructor ? `<div>${instructor}</div>` : ''} ${room ? `<div>s. ${room}</div>` : ''} ${faculty ? `<div>${faculty}</div>` : ''} </div> ` }; }, eventDidMount: function (info) { const kolor = przypiszKolorPrzedmiotowi(info.event.extendedProps.eventType); info.el.style.backgroundColor = kolor; info.el.style.borderColor = kolor; const title2 = info.event.extendedProps.eventType || info.event.title2; const group = info.event.extendedProps.group; const instructor = info.event.extendedProps.instructor; const room = info.event.extendedProps.room; const faculty = info.event.extendedProps.faculty; // tooltip w jednej linii, bez godziny const tooltipText = [ title2, group, instructor, room ? `s. ${room}` : null, faculty ].filter(Boolean).join(', '); info.el.setAttribute('data-tippy-content', tooltipText); if (czyDataZablokowana(info.event.start)) { info.el.style.display = 'none'; } } }); window._calendars = window._calendars || {}; window._calendars[elementId] = calendar; calendar.render(); calendar.render(); // dobierz poprawny kontekst do filtra tabeli: const ctx = elementId.includes('sala') ? 'rooms' : elementId.includes('instructor') ? 'instructors' : 'faculties'; selectEl.addEventListener('change', () => { calendar.refetchEvents(); // odśwież FC refreshNTable(elementId, ctx, selectEl.value); // odśwież „Całość” jeśli aktywna }); tippy.delegate(`#${elementId}`, { target: '.fc-event', placement: 'top', theme: 'light', arrow: true, animation: 'scale', offset: [0, 8], duration: [200, 150], inertia: true, delay: [0, 0], }); }; calendarFactoryNiestacjonarne('calendar-kierunek-n', 'faculty-select-n', (event, selected) => selected === 'Wszystkie' || event.faculty === selected ); calendarFactoryNiestacjonarne('calendar-sala-n', 'room-select-n', (event, selected, termin) => { const norm = s => String(s || '') .toLowerCase() .replace(/\u00A0/g, ' ') .replace(/\s+/g, ' ') .trim(); if (norm(selected) === 'wszystkie') return true; return norm(termin?.room) === norm(selected); } ); // 2) Wykładowcy N – „zawiera”, z normalizacją (obsłuży wpisy typu „X / Y”) calendarFactoryNiestacjonarne('calendar-instructor-n', 'instructor-select-n', (event, selected) => { const norm = s => String(s || '') .toLowerCase() .replace(/\u00A0/g, ' ') // NBSP -> spacja .replace(/\s*\/\s*/g, ' / ')// ujednolicenie odstępów przy „/” .replace(/\s+/g, ' ') // pojedyncze spacje .trim(); if (norm(selected) === 'wszyscy') return true; return norm(event?.instructor).includes(norm(selected)); } ); }); function _refreshVisibleCalendars(){ requestAnimationFrame(() => { if (!window._calendars) return; Object.values(window._calendars).forEach(cal => { if (cal && cal.el && cal.el.offsetParent !== null) { cal.updateSize(); } }); }); } function openTab(tabId) { // 1) przełącz TYLKO główne panele document.getElementById('tab1').classList.remove('active'); document.getElementById('tab2').classList.remove('active'); document.getElementById(tabId).classList.add('active'); // 2) przełącz przyciski głównych zakładek document.querySelectorAll('.tab').forEach(btn => btn.classList.remove('active')); const clickedTab = Array.from(document.querySelectorAll('.tab')) .find(btn => btn.textContent === (tabId === 'tab1' ? 'Stacjonarne' : 'Niestacjonarne')); if (clickedTab) clickedTab.classList.add('active'); // 3) w aktywnej zakładce włącz domyślną sub-zakładkę (sub1) const subPanels = document.querySelectorAll(`#${tabId} .tab-content`); const subButtons = document.querySelectorAll(`#${tabId} .sub-tab`); subPanels.forEach(p => p.classList.remove('active')); subButtons.forEach(b => b.classList.remove('active')); const firstSubPanel = document.getElementById(`${tabId}-sub1`); if (firstSubPanel) firstSubPanel.classList.add('active'); const firstSubBtn = Array.from(subButtons).find(b => /Harmonogram/i.test(b.textContent)) || subButtons[0]; if (firstSubBtn) firstSubBtn.classList.add('active'); // 4) przelicz rozmiary widocznych kalendarzy _refreshVisibleCalendars(); // 5) dopasuj tryb „Całość/Kalendarz” do preferencji const calEl = firstSubPanel ? firstSubPanel.querySelector('[id^="calendar-"]') : null; if (calEl && typeof ensureCaloscState === 'function') { ensureCaloscState(calEl.id); } } function openSubTab(tabId, subTabId, evt) { // zapamiętaj, czy poprzedni widoczny kalendarz w tej zakładce miał „Całość” const tabRoot = document.getElementById(tabId); const prevPanel = tabRoot.querySelector('.tab-content.active'); let prevHadCalosc = false; if (prevPanel) { const prevCal = prevPanel.querySelector('[id^="calendar-"]'); if (prevCal) { const root = getFcRoot(prevCal); const harness = getViewHarness(root); prevHadCalosc = !!(harness && harness.classList.contains('fc-hidden-by-table')); } } // przełącz sub-zakładkę (jak było) document.querySelectorAll(`#${tabId} .tab-content`).forEach(st => st.classList.remove('active')); document.querySelectorAll(`#${tabId} .sub-tab`).forEach(btn => btn.classList.remove('active')); document.getElementById(`${tabId}-${subTabId}`).classList.add('active'); const e = evt || window.event; // na wypadek wywołania inline bez parametru if (e && e.target) { const clickedSub = Array.from(document.querySelectorAll(`#${tabId} .sub-tab`)) .find(btn => btn.textContent === e.target.textContent); if (clickedSub) clickedSub.classList.add('active'); } // przelicz rozmiary _refreshVisibleCalendars(); // jeżeli poprzednio było „Całość”, włącz je też w nowej sub-zakładce const newPanel = document.getElementById(`${tabId}-${subTabId}`); const newCal = newPanel ? newPanel.querySelector('[id^="calendar-"]') : null; if (newCal && typeof ensureCaloscState === 'function') { if (prevHadCalosc) window._caloscPreferred = true; ensureCaloscState(newCal.id); } } /* ===== Helpery dla trybu „Całość” – wklej raz, wysoko w pliku ===== */ (function(){ // jeśli już są – nie nadpisuj if (!window.getFcRoot) { window.getFcRoot = function(el){ return el?.classList?.contains('fc') ? el : (el?.querySelector?.('.fc') || el); }; } if (!window.getViewHarness) { window.getViewHarness = function(root){ return root?.querySelector?.('.fc-view-harness') || root?.querySelector?.('.fc-scroller-harness') || root?.querySelector?.('.fc-scrollgrid') || root; }; } if (!window.getViewKey) { window.getViewKey = function(id){ if (id.includes('kierunek')) return 'kierunek'; if (id.includes('sala')) return 'sala'; return 'instructor'; }; } // >>> TO jest funkcja, której brakuje <<< if (!window.resolveHostGrid) { window.resolveHostGrid = function(elementId, harness){ const viewKey = getViewKey(elementId); const suffix = elementId.includes('-n') ? '-n' : ''; // 1) spróbuj wziąć host wpięty zaraz za harness let host = harness?.nextElementSibling; if (!(host && host.id && host.id.startsWith('n-table-host'))) host = null; let grid = host?.querySelector('.grid-container') || null; // 2) spróbuj po ID (z sufiksem -n, a potem bez) if (!host){ host = document.getElementById(`n-table-host-${viewKey}${suffix}`) || document.getElementById(`n-table-host-${viewKey}`); grid = host?.querySelector('.grid-container') || document.getElementById(`n-grid-${viewKey}${suffix}`) || document.getElementById(`n-grid-${viewKey}`); } // 3) jeśli nadal brak – UTWÓRZ host + grid i wstaw po harness if (!host){ host = document.createElement('div'); host.id = `n-table-host-${viewKey}${suffix}`; host.className = 'scroll-container'; host.style.display = 'none'; grid = document.createElement('div'); grid.id = `n-grid-${viewKey}${suffix}`; grid.className = 'grid-container'; host.appendChild(grid); } // przypnij host dokładnie za harness (żeby zajął jego miejsce) if (harness && host.previousElementSibling !== harness){ harness.insertAdjacentElement('afterend', host); } return { host, grid }; }; } })(); /* ====== Tabela „Całość” w miejscu kalendarza (Niestacjonarne) ====== */ (() => { const KOLUMNA_SZ = 75, KOLUMNA_PRZERWA = KOLUMNA_SZ/10; const startDate = new Date("2025-09-01"); const endDate = new Date("2025-10-01"); //----------------zmiana daty////////////////////////////////////// // sloty 08:15..20:15 (45' + 15' przerwy) const sloty = (()=>{ const out=[]; let s=new Date(); s.setHours(8,15,0,0); while (s.getHours()<20 || (s.getHours()===20 && s.getMinutes()<=15)){ for(let i=0;i<2;i++){ const st=s.toTimeString().slice(0,5); const k=new Date(s.getTime()+45*60000); out.push({type:"blok",start:st,end:k.toTimeString().slice(0,5)}); s=k; } const k2=new Date(s.getTime()+15*60000); if (k2.getHours()<20 || (k2.getHours()===20 && k2.getMinutes()<=15)) { out.push({type:"przerwa",start:s.toTimeString().slice(0,5),end:k2.toTimeString().slice(0,5)}); } s=k2; } return out; })(); // tylko piątek–niedziela const dni=[]; for (let d=new Date(startDate); d<=endDate; d.setDate(d.getDate()+1)){ const day = new Date(d); if ([5,6,0].includes(day.getDay())) dni.push(new Date(day)); } const kolorLista=["#1e90ff","#ff6347","#32cd32","#ff69b4","#ffa500","#8a2be2","#1a237e","#00bfff","#800080","#c71585","#006400","#800000","#ff4500","#4682b4","#da70d6","#a0522d","#5f9ea0","#4169e1","#556b2f"]; const kolorPrzedmiot=new Map(); let cachedData=null; async function fetchData(){ if (cachedData) return cachedData; const res = await fetch("https://niestacjonarne-fcc4f-default-rtdb.europe-west1.firebasedatabase.app/wydarzenian.json"); cachedData = await res.json() || {}; return cachedData; } function obliczOffsetPx(time){ const [h,m]=time.split(":").map(Number); const total=h*60+m; let px=0; for (let s of sloty){ const [sh,sm]=s.start.split(":").map(Number), [eh,em]=s.end.split(":").map(Number); const a=sh*60+sm, b=eh*60+em; if (total < a) break; if (total <= b){ const dur=b-a, prog=(total-a)/dur; px += prog*(s.type==="przerwa"?KOLUMNA_PRZERWA:KOLUMNA_SZ); break; } px += (s.type==="przerwa"?KOLUMNA_PRZERWA:KOLUMNA_SZ); } return px; } function createCell(content, cls=""){ const div=document.createElement("div"); div.textContent=content; if(cls) div.classList.add(cls); return div; } // ——— helpery wspólne ——— /* ---------- Helpery DOM / FC ---------- */ function getFcRoot(el){ return el?.classList?.contains('fc') ? el : (el?.querySelector?.('.fc') || el); } function getViewHarness(root){ return root?.querySelector?.('.fc-view-harness') || root?.querySelector?.('.fc-scroller-harness') || root?.querySelector?.('.fc-scrollgrid') || root; } function getViewKey(id){ if (id.includes('kierunek')) return 'kierunek'; if (id.includes('sala')) return 'sala'; return 'instructor'; } function ctxFromElementId(id){ if (id.includes('sala')) return 'rooms'; if (id.includes('instructor')) return 'instructors'; return 'faculties'; } /* ---------- Host + grid dla tabeli ---------- */ function resolveHostGrid(elementId, harness){ const viewKey = getViewKey(elementId); const suffix = elementId.includes('-n') ? '-n' : ''; // 1) spróbuj host wpięty tuż po harness let host = harness?.nextElementSibling; if (!(host && host.id && host.id.startsWith('n-table-host'))) host = null; let grid = host?.querySelector('.grid-container') || null; // 2) po ID (z sufiksem, potem bez) if (!host){ host = document.getElementById(`n-table-host-${viewKey}${suffix}`) || document.getElementById(`n-table-host-${viewKey}`); grid = host?.querySelector('.grid-container') || document.getElementById(`n-grid-${viewKey}${suffix}`) || document.getElementById(`n-grid-${viewKey}`); } // 3) utwórz jeśli brak if (!host){ host = document.createElement('div'); host.id = `n-table-host-${viewKey}${suffix}`; host.className = 'scroll-container'; host.style.display = 'none'; grid = document.createElement('div'); grid.id = `n-grid-${viewKey}${suffix}`; grid.className = 'grid-container'; host.appendChild(grid); } // 4) przypnij za harness if (harness && host.previousElementSibling !== harness){ harness.insertAdjacentElement('afterend', host); } return { host, grid }; } /* ---------- Odczyt aktualnej wartości filtra dla danego kalendarza ---------- */ // === Preferencja trybu „Całość” + pomocnicze === window._caloscPreferred = window._caloscPreferred ?? false; function getSelectIdFor(elementId){ const n = elementId.includes('-n') ? '-n' : ''; if (elementId.includes('kierunek')) return `faculty-select${n}`; if (elementId.includes('sala')) return `room-select${n}`; return `instructor-select${n}`; } function getCurrentFilterValue(elementId){ const sel = document.getElementById(getSelectIdFor(elementId)); return sel ? sel.value : ''; } function ensureCaloscState(elementId){ const root = getFcRoot(document.getElementById(elementId)); const harness = getViewHarness(root); const isOn = !!(harness && harness.classList.contains('fc-hidden-by-table')); if (window._caloscPreferred && !isOn){ _toggleNTable(elementId, getCurrentFilterValue(elementId)); } else if (!window._caloscPreferred && isOn){ _toggleNTable(elementId, getCurrentFilterValue(elementId)); } } // ===== Pinch-zoom dla hosta tabeli ===== function enablePinchZoom(host, grid){ if (!host || !grid) return; if (host._pinchEnabled) return; host.classList.add('pinchable'); let baseScale = 1; let tempScale = 1; let startDist = 0; const MIN = 0.2, MAX = 4; const pointers = new Map(); const clamp = x => Math.max(MIN, Math.min(MAX, x)); const distance = (a,b) => Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY); const apply = (scale) => { grid.style.transform = `scale(${scale})`; const r = grid.getBoundingClientRect(); host.style.height = Math.ceil(r.height + 12) + 'px'; }; host.addEventListener('pointerdown', (e) => { // NIE przechwytujemy jednego palca – pozwalamy scrollować natywnie pointers.set(e.pointerId, e); if (pointers.size === 2){ const [a,b] = [...pointers.values()]; startDist = distance(a,b); tempScale = baseScale; // nadal nie musimy setPointerCapture – ważne, by preventDefault robić tylko przy 2 palcach } }); host.addEventListener('pointermove', (e) => { if (!pointers.has(e.pointerId)) return; pointers.set(e.pointerId, e); if (pointers.size === 2 && startDist){ const [a,b] = [...pointers.values()]; const factor = distance(a,b) / startDist; const next = clamp(tempScale * factor); apply(next); // BLOKUJEMY domyślne zachowanie TYLKO dla 2 palców (żeby strona nie przewijała) e.preventDefault(); } // dla 1 palca – brak preventDefault => przewijanie w pionie działa normalnie }, { passive: false }); const end = (e) => { if (pointers.has(e.pointerId)) pointers.delete(e.pointerId); if (pointers.size < 2 && startDist){ const m = grid.style.transform.match(/scale\(([^)]+)\)/); baseScale = clamp(m ? parseFloat(m[1]) : baseScale); apply(baseScale); startDist = 0; } }; host.addEventListener('pointerup', end); host.addEventListener('pointercancel', end); host.addEventListener('pointerleave', end); // Inicjalnie przelicz wysokość requestAnimationFrame(() => apply(baseScale)); window.addEventListener('orientationchange', () => setTimeout(() => apply(baseScale), 250)); window.addEventListener('resize', () => apply(baseScale)); host._pinchEnabled = true; } /* ---------- Tryb Całość: ustaw deterministycznie (nie tylko toggle) ---------- */ /* ---------- Tryb Całość: ustaw deterministycznie (nie tylko toggle) ---------- */ async function setCaloscMode(elementId, on, explicitValue){ const calEl = document.getElementById(elementId); const fcRoot = getFcRoot(calEl); const harness = getViewHarness(fcRoot); if (!fcRoot || !harness) return; const { host, grid } = resolveHostGrid(elementId, harness); if (!host || !grid) return; const btn = fcRoot.querySelector('.fc-calosc-button'); _caloscActive[elementId] = !!on; // zapamiętaj stan per zakładka if (on){ harness.classList.add('fc-hidden-by-table'); host.style.display = 'block'; btn?.classList.add('fc-button-active'); // >>> podłącz pinch dla „Całość” enablePinchZoom(host, grid); const ctx = ctxFromElementId(elementId); const value = explicitValue ?? getCurrentFilterValue(elementId); if (typeof renderTableN === 'function'){ await renderTableN(grid, ctx, value); } } else { host.style.display = 'none'; harness.classList.remove('fc-hidden-by-table'); btn?.classList.remove('fc-button-active'); window._calendars?.[elementId]?.updateSize?.(); } } /* ---------- Odświeżanie tabeli przy zmianie selektora ---------- */ async function refreshNTable(elementId, ctx, filterValue){ const calEl = document.getElementById(elementId); const fcRoot = getFcRoot(calEl); const harness = getViewHarness(fcRoot); const isOn = (_caloscActive[elementId] === true) || (harness && harness.classList.contains('fc-hidden-by-table')); if (!isOn) return; const { grid } = resolveHostGrid(elementId, harness); if (grid && typeof renderTableN === 'function'){ await renderTableN(grid, ctx, filterValue); } } /* ---------- Publiczny toggle: korzysta z flagi, nie ze stylów ---------- */ window._toggleNTable = async function(elementId, selectedValue){ const wantOn = !_caloscActive[elementId]; await setCaloscMode(elementId, wantOn, selectedValue); }; /* ---------- Powiąż select ⇄ kalendarz (per zakładka) ---------- */ function wireSelect(selectId, calId, ctx){ const sel = document.getElementById(selectId); if (!sel) return; sel.dataset.calId = calId; sel.dataset.ctx = ctx; sel.addEventListener('change', () => { const calEl = document.getElementById(calId); const fcRoot = getFcRoot(calEl); const harness = getViewHarness(fcRoot); // FC zawsze odświeżamy: window._calendars?.[calId]?.refetchEvents?.(); // Jeśli „Całość” jest aktywna (po klasie LUB fladze) – renderuj ją na twardo: const caloscOn = (_caloscActive[calId] === true) || (harness && harness.classList.contains('fc-hidden-by-table')); if (caloscOn) { // deterministycznie ustaw tryb + od razu narysuj grid z aktualnym filtrem setCaloscMode(calId, true, sel.value); } }); } /* ---------- Przywróć stan Całość przy przełączaniu zakładek ---------- */ function applyCaloscForVisible(){ // wołaj po kliknięciu w zakładki lub po zmianie layoutu KNOWN_CAL_IDS.forEach(async id => { const el = document.getElementById(id); if (!el) return; const visible = !!(el.offsetParent !== null); // widoczny w DOM if (!visible) return; // Ustaw zgodnie z flagą, ale bez podwójnego rysowania: if (_caloscActive[id]){ await setCaloscMode(id, true); // zapewnij tabelę dla widocznej zakładki } else { await setCaloscMode(id, false); // zapewnij kalendarz dla widocznej zakładki } }); } // mapowanie: który select obsługuje który kalendarz i kontekst tabeli const MAP = { 'faculty-select': { calId: 'calendar-kierunek', ctx: 'faculties' }, 'room-select': { calId: 'calendar-sala', ctx: 'rooms' }, 'instructor-select': { calId: 'calendar-instructor', ctx: 'instructors' }, 'faculty-select-n': { calId: 'calendar-kierunek-n', ctx: 'faculties' }, 'room-select-n': { calId: 'calendar-sala-n', ctx: 'rooms' }, 'instructor-select-n': { calId: 'calendar-instructor-n', ctx: 'instructors' }, }; // delegacja: zadziała niezależnie od tego, jak dropdown jest renderowany document.addEventListener('change', (e) => { const sel = e.target.closest('#faculty-select,#room-select,#instructor-select,#faculty-select-n,#room-select-n,#instructor-select-n'); if (!sel) return; const { calId, ctx } = MAP[sel.id] || {}; if (!calId) return; // 1) zawsze odśwież FC window._calendars?.[calId]?.refetchEvents?.(); // 2) jeżeli „Całość” jest aktywna -> zrób twardy render tabeli z nową wartością const calEl = document.getElementById(calId); const fcRoot = getFcRoot(calEl); const harness = getViewHarness(fcRoot); const caloscOn = (_caloscActive?.[calId] === true) || (harness && harness.classList.contains('fc-hidden-by-table')); if (caloscOn) { setCaloscMode(calId, true, sel.value); // deterministyczny render tabeli } }); // (opcjonalnie) niektóre biblioteki emitują 'input' zamiast 'change' document.addEventListener('input', (e) => { if (e.target && e.target.matches('select')) { const evt = new Event('change', { bubbles: true }); e.target.dispatchEvent(evt); } }); /* ---------- Inicjalizacja powiązań ---------- */ function setupCaloscWiring(){ // Stacjonarne wireSelect('faculty-select', 'calendar-kierunek', 'faculties'); wireSelect('room-select', 'calendar-sala', 'rooms'); wireSelect('instructor-select', 'calendar-instructor', 'instructors'); // Niestacjonarne wireSelect('faculty-select-n', 'calendar-kierunek-n', 'faculties'); wireSelect('room-select-n', 'calendar-sala-n', 'rooms'); wireSelect('instructor-select-n', 'calendar-instructor-n', 'instructors'); // Przywracaj stan przy kliknięciu w zakładki / sub-zakładki: document.addEventListener('click', (e)=>{ if (e.target.closest('.tab, .sub-tab')) { // po przełączeniu DOM zmienia display — odczekaj klatkę: setTimeout(applyCaloscForVisible, 0); } }); // Na start: zastosuj stan dla aktualnie widocznych applyCaloscForVisible(); } // RENDER TABELI z łączeniem 45′+45′ async function renderTableN(container, ctx, filterValue){ const data = await fetchData(); const allRezerwacje = await fetchRezerwacje(); container.innerHTML = ""; // kolumny: data + sloty let templateCols = "50px "; sloty.forEach(s => templateCols += (s.type==="przerwa" ? `${KOLUMNA_PRZERWA}px ` : `${KOLUMNA_SZ}px `)); container.style.gridTemplateColumns = templateCols; // nagłówek container.appendChild(createCell("", "header-cell")); sloty.forEach((s,idx)=>{ const div=document.createElement("div"); div.classList.add(s.type==="przerwa" ? "header-break" : "header-cell"); if (idx===sloty.length-1) div.classList.add("last-column"); if (s.type==="blok") div.innerHTML = `<span>${s.start}</span><span>${s.end}</span>`; container.appendChild(div); }); // helpers const hm = s => { if(!s) return null; const t = s.includes('T') ? s.split('T')[1] : s; return t.slice(0,5); }; const toMin = s => { const t = s.includes('T') ? s.split('T')[1] : s; const [h,m] = t.split(':'); return (+h)*60 + (+m); }; const toHM = m => String(Math.floor(m/60)).padStart(2,'0') + ':' + String(m%60).padStart(2,'0'); const norm = s => (s ?? '').toString().normalize('NFKC').replace(/\s+/g,' ').trim(); const subjOf = ev => norm(ev.eventType || ev.title); // NAZWA przedmiotu: eventType LUB title const sameOrEmpty = (a,b) => !norm(a) || !norm(b) || norm(a)===norm(b); let szary=false, lastDate=null; dni.forEach((day, dayIndex)=>{ if (lastDate){ const diff=(day-lastDate)/(1000*60*60*24); if (diff>4) szary=!szary; } lastDate = day; const dateCell = createCell(day.toLocaleDateString('pl-PL',{day:'2-digit',month:'2-digit'}), "date-cell"); if (szary) dateCell.classList.add("week-gray"); container.appendChild(dateCell); // puste komórki czasu sloty.forEach((s,idx)=>{ const cell=document.createElement("div"); cell.classList.add(s.type==="przerwa" ? "break-cell" : "cell"); if (szary) cell.classList.add("week-gray"); if (idx===sloty.length-1) cell.classList.add("last-column"); container.appendChild(cell); }); const dateStr = day.toISOString().slice(0,10); // filtr: kierunek / sala / prowadzący const wydarzenia = Object.values(data).filter(ev=>{ if (!ev?.terminy) return false; if (ctx==='faculties' && filterValue && ev.faculty !== filterValue) return false; if (ctx==='instructors' && filterValue && norm(ev.instructor) !== norm(filterValue)) return false; if (ctx==='rooms' && filterValue){ const ma = Object.values(ev.terminy).some(t => norm(t.room) === norm(filterValue)); if (!ma) return false; } return Object.values(ev.terminy).some(t=> t.startDate===dateStr); }); // 1) segmenty w minutach const segments = []; wydarzenia.forEach(ev=>{ Object.values(ev.terminy).forEach(t=>{ if (t.startDate===dateStr){ if (ctx==='rooms' && filterValue && norm(t.room)!==norm(filterValue)) return; const st = toMin(t.startTime); const en = hm(t.end) ? toMin(hm(t.end)) : st + 45; segments.push({ ev, t, start:st, end:en, key: subjOf(ev) }); // key = nazwa przedmiotu } }); }); if (ctx === 'rooms' || ctx === 'instructors') { const rezForDay = allRezerwacje.filter(r => (r.start || '').slice(0,10) === dateStr); rezForDay.forEach(r => { const rRoom = norm(r.extendedProps?.room); const rInstr = norm(r.extendedProps?.instructor); // filtr per kontekst if (ctx === 'rooms' && filterValue && rRoom !== norm(filterValue)) return; if (ctx === 'instructors' && filterValue && !rInstr.includes(norm(filterValue))) return; const startHM = hm(r.start); // "HH:MM" const endHM = hm(r.end); if (!startHM || !endHM) return; const st = toMin(startHM); const en = toMin(endHM); // zbuduj „udawaną” strukturę jak dla wydarzeń niestacjonarnych const pseudoEv = { title: r.title || 'Rezerwacja', eventType: r.extendedProps?.eventType || 'Rezerwacja', instructor: r.extendedProps?.instructor || '', faculty: '', terminy: { '0': { startDate: dateStr, startTime: startHM, end: endHM, room: r.extendedProps?.room || '', group: '' } } }; const pseudoT = pseudoEv.terminy['0']; segments.push({ ev: pseudoEv, t: pseudoT, start: st, end: en, key: subjOf(pseudoEv) // nazwa do kolorowania/scalania }); }); } segments.sort((a,b)=> a.start - b.start); // 2) scalanie sąsiednich (<=20 min przerwy), toleruj puste pola const merged = []; for (const seg of segments){ const last = merged[merged.length-1]; if ( last && last.key === seg.key && sameOrEmpty(last.ev.instructor, seg.ev.instructor) && sameOrEmpty(last.t.group, seg.t.group) && sameOrEmpty(last.t.room, seg.t.room) && seg.start - last.end >= 0 && seg.start - last.end <= 20 ){ last.end = Math.max(last.end, seg.end); } else { merged.push({...seg}); } } // 3) rysowanie bez kolizji (poziomy) const poziomy = []; let maxPoziom = 1; for (const m of merged){ const startHM = toHM(m.start); const endHM = toHM(m.end); const startPx = obliczOffsetPx(startHM); const endPx = obliczOffsetPx(endHM); let poziom = 0; while (poziomy[poziom]?.some(e => e.start < endPx && startPx < e.end)) poziom++; (poziomy[poziom] ||= []).push({ start:startPx, end:endPx }); maxPoziom = Math.max(maxPoziom, poziom+1); if (!kolorPrzedmiot.has(m.key)) { kolorPrzedmiot.set(m.key, kolorLista[kolorPrzedmiot.size % kolorLista.length]); } const kolor = kolorPrzedmiot.get(m.key); const all = Array.from(container.children); const firstCellIndex = (dayIndex + 1) * (sloty.length + 1); const startCell = all[firstCellIndex + 1]; const salaTxt = norm(m.t.room) ? `s. ${norm(m.t.room)}` : ''; const prowTxt = norm(m.ev.instructor); const grupaTxt = norm(m.t.group) ? `, ${norm(m.t.group)}` : ''; const nazwa = m.ev.eventType || m.ev.title || ''; const el = document.createElement('div'); el.className = 'event'; el.style.left = `${startPx}px`; el.style.width = `${Math.max(2, endPx - startPx)}px`; el.style.top = `${poziom * 48}px`; el.style.height = `46px`; el.style.background = kolor; el.innerHTML = ` <div><b>${startHM} - ${endHM}</b></div> <div>${nazwa}${grupaTxt}${prowTxt ? ', ' + prowTxt : ''}</div> <div>${salaTxt}</div> `; // === TOOLTIP identyczny jak w kalendarzu (jedna linia, bez godziny) === const title2 = (m.ev.eventType || m.ev.title || '').trim(); const groupTxt = (m.t.group || '').trim(); const instrTxt = (m.ev.instructor || '').trim(); const roomTxt = (m.t.room ? `s. ${m.t.room}` : '').trim(); const facultyTxt = (m.ev.faculty || '').trim(); const tooltipText = [title2, groupTxt, instrTxt, roomTxt, facultyTxt] .filter(Boolean) .join(', '); // podpowiedź jak w FC el.setAttribute('data-tippy-content', tooltipText); // użyj tippy jak w kalendarzu (gdy biblioteka jest załadowana) if (window.tippy) { tippy(el, { placement: 'top', theme: 'light', arrow: true, animation: 'scale', offset: [0, 8], duration: [200, 150], inertia: true, delay: [0, 0], }); } else { // fallback: natywny tooltip przeglądarki el.title = tooltipText; } startCell.appendChild(el); } // 4) wysokość wiersza const all2 = Array.from(container.children); const firstCellIndex2 = (dayIndex + 1) * (sloty.length + 1); for (let i = firstCellIndex2; i < firstCellIndex2 + sloty.length + 1; i++) { all2[i].style.minHeight = `${maxPoziom * 50}px`; } }); } // powrót z przycisków Miesiąc/Tydzień/Dzień document.addEventListener('click', (e)=>{ const btn = e.target.closest('.fc-dayGridMonth-button, .fc-timeGridWeek-button, .fc-timeGridDay-button'); if (!btn) return; const fcRoot = getFcRoot(btn.closest('.fc')); const harness = getViewHarness(fcRoot); harness?.classList.remove('fc-hidden-by-table'); const host = harness?.nextElementSibling; if (host && host.id?.startsWith('n-table-host')) host.style.display = 'none'; fcRoot?.querySelector('.fc-calosc-button')?.classList.remove('fc-button-active'); if (typeof _refreshVisibleCalendars === 'function') _refreshVisibleCalendars(); }); // Ctrl+scroll – zoom tylko tabeli (aktywny host) let scale=1; ['n-table-host-kierunek','n-table-host-sala','n-table-host-instructor'].forEach(id=>{ const host=document.getElementById(id); if (!host) return; host.addEventListener('wheel', function(e){ if (e.ctrlKey){ e.preventDefault(); scale += e.deltaY<0 ? 0.05 : -0.05; scale = Math.max(0.5, Math.min(scale, 2)); host.querySelector('.grid-container').style.transform = `scale(${scale})`; } }, {passive:false}); }); })(); /* === Zmiana selektorów → refetch FC + odśwież „Całość” (mapowanie 1:1) === */ function wireSelect(selectId, calId, ctx){ const sel = document.getElementById(selectId); if (!sel) return; sel.dataset.calId = calId; sel.dataset.ctx = ctx; sel.addEventListener('change', () => { window._calendars?.[calId]?.refetchEvents?.(); // FullCalendar refreshNTable(calId, ctx, sel.value); // „Całość” (jeśli aktywna) }); } /** * Wywołaj raz po załadowaniu DOM. * Podpina wszystkie standardowe selektory, jeśli istnieją w dokumencie. */ function setupCaloscWiring(){ // Stacjonarne wireSelect('faculty-select', 'calendar-kierunek', 'faculties'); wireSelect('room-select', 'calendar-sala', 'rooms'); wireSelect('instructor-select', 'calendar-instructor', 'instructors'); // Niestacjonarne wireSelect('faculty-select-n', 'calendar-kierunek-n', 'faculties'); wireSelect('room-select-n', 'calendar-sala-n', 'rooms'); wireSelect('instructor-select-n', 'calendar-instructor-n', 'instructors'); } // auto-init po załadowaniu document.addEventListener('DOMContentLoaded', setupCaloscWiring); </script> </div> </div> </div> </body>