<head> <meta charset="UTF-8" /> <title>Rezerwacje – jednorazowe i cykliczne (DokuWiki)</title> <style> body { font-family: Arial, sans-serif; padding: 20px; background-color: #f8f9fa; } h2 { text-align:center; } .toolbar { text-align:center; margin-bottom: 16px; } .toolbar select { padding:8px; min-width:240px; } .form-container { width: 92%; margin: 20px auto; display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; } .form-group { display:flex; flex-direction:column; min-width: 160px; } .form-group label { font-weight: 600; margin-bottom: 4px; } input, select { padding:8px; } .add-btn { background:#00BFFF; color:#fff; border:none; padding:10px 16px; border-radius:4px; cursor:pointer; } .edit-btn { background:orange; color:#fff; border:none; padding:6px 10px; border-radius:4px; cursor:pointer; } .delete-btn { background:#e11; color:#fff; border:none; padding:6px 10px; border-radius:4px; cursor:pointer; } table { border-collapse: collapse; width: 92%; margin: 20px auto; text-align: center; } th, td { border: 1px solid #555; padding: 8px; } th { background-color: #9932CC; color: white; } .hint { text-align:center; color:#555; margin-top:8px; } .muted { color:#555; font-size: 12px; } </style> </head> <body> <h2>Rezerwacje – harmonogram (Wiki)</h2> <!-- GÓRNY FILTR: względem rodzaju wydarzenia --> <div class="toolbar"> <label for="eventTypeFilter"><b>Wybierz rodzaj wydarzenia:</b></label> <select id="eventTypeFilter" onchange="odswiezListe()"> <option value="">-- Wybierz --</option> </select> </div> <!-- FORMULARZ --> <div class="form-container"> <div class="form-group" style="flex:1 1 280px;"> <label for="eventType">Rodzaj wydarzenia</label> <input list="eventTypeList" id="eventType" placeholder="np. Rezerwacja sali / Konsultacje"> <datalist id="eventTypeList"></datalist> </div> <div class="form-group" style="flex:1 1 240px;"> <label for="instructor">Prowadzący</label> <input list="instructorList" id="instructor" placeholder="Imię i nazwisko"> <datalist id="instructorList"></datalist> </div> <div class="form-group"> <label for="date">Data startu</label> <input id="date" type="date"> </div> <div class="form-group"> <label for="repeat">Powtarzanie</label> <select id="repeat" onchange="onRepeatChange()"> <option value="none">Nie powtarza się</option> <option value="daily">Codziennie</option> <option value="weekly">Co tydzień</option> <option value="biweekly">Co 2 tygodnie</option> </select> </div> <div class="form-group" id="endDateWrap" style="display:none;"> <label for="endDate">Koniec wydarzenia</label> <input id="endDate" type="date"> <div class="muted">Wymagane tylko dla wydarzeń cyklicznych</div> </div> <div class="form-group"> <label for="start">Godzina rozpoczęcia</label> <input id="start" type="time"> </div> <div class="form-group"> <label for="end">Godzina zakończenia</label> <input id="end" type="time"> </div> <div class="form-group"> <label for="room">Sala</label> <input list="roomList" id="room" placeholder="np. A1"> <datalist id="roomList"></datalist> </div> <div class="form-group"> <button class="add-btn" onclick="dodajLubZapisz()" id="addBtn">Dodaj</button> <button class="add-btn" style="display:none;background:#16a34a" onclick="anulujEdycje()" id="cancelBtn">Anuluj edycję</button> </div> </div> <table> <thead> <tr> <th>Lp.</th> <th>Sala</th> <th>Data</th> <th>Godzina</th> <th>Osoba</th> <th>Rodzaj</th> <th>Powtarzanie</th> <th>Koniec</th> <th>Akcje</th> </tr> </thead> <tbody id="lista"></tbody> </table> <p class="hint" id="hint"></p> <script> /* ========================================================= 1) KONFIG STRON (DokuWiki) ========================================================= */ const PAGE_REZ = "materialy:strona:baza:bazar"; // <-- REZERWACJE (NOWA BAZA) const PAGE_STAC = "materialy:strona:baza:bazas"; // <-- STACJONARNE (kolizje) const PAGE_NIEST = "materialy:strona:baza:bazan"; // <-- NIESTACJONARNE (kolizje) const PAGE_REZ = "start:bazar"; const DOKU_PHP = "http://eclipse.elektron.pk.edu.pl/~plany-wieik/doku.php"; const CODE_JSON_RE = /<code\b[^>]*\bjson\b[^>]*>[\s\S]*?<\/code>\s*/gim; /* ========================================================= 2) DOKUWIKI: edit/save + JSON in <code json> ========================================================= */ function pageEditUrl(pageId){ const u = new URL(DOKU_PHP); u.searchParams.set("id", pageId); u.searchParams.set("do", "edit"); return u.toString(); } async function fetchEditForm(pageId){ const res = await fetch(pageEditUrl(pageId), { credentials: "same-origin" }); if (!res.ok) throw new Error(`Nie mogę otworzyć edycji strony: ${pageId} (HTTP ${res.status})`); const html = await res.text(); const doc = new DOMParser().parseFromString(html, "text/html"); const ta = doc.querySelector('textarea[name="wikitext"]'); const sectok = doc.querySelector('input[name="sectok"]')?.value || ""; const rev = doc.querySelector('input[name="rev"]')?.value || ""; const id = doc.querySelector('input[name="id"]')?.value || pageId; if (!ta) throw new Error("Brak textarea[name=wikitext] — brak praw edycji lub nietypowy szablon."); if (!sectok) throw new Error("Brak sectok — zwykle oznacza brak zalogowania lub brak uprawnień edycji."); return { id, sectok, rev, wikitext: (ta.value ?? "") }; } async function saveWikitext(pageId, newWikitext, summary = "Zapis z formularza"){ const form = await fetchEditForm(pageId); const body = new URLSearchParams(); body.set("id", form.id); body.set("sectok", form.sectok); if (form.rev) body.set("rev", form.rev); body.set("do", "save"); body.set("wikitext", newWikitext); body.set("summary", summary); body.set("minor", "1"); const res = await fetch(DOKU_PHP, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, body }); const html = await res.text(); if (!res.ok) throw new Error(`Zapis nieudany (HTTP ${res.status})`); if (/conflict|konflikt|permission|uprawn/i.test(html)) { throw new Error("DokuWiki zwróciło konflikt albo brak uprawnień. Otwórz stronę w edycji i sprawdź komunikat."); } return true; } function extractJsonCodeBlocks(wikitext){ const out = []; const re = new RegExp(CODE_JSON_RE.source, "gim"); let m; while ((m = re.exec(wikitext)) !== null){ const full = m[0]; const inner = full .replace(/^[\s\S]*?>/m, "") .replace(/<\/code>[\s\S]*$/im, "") .trim(); out.push(inner); } return out; } function stripAllJsonCodeBlocks(wikitext){ return String(wikitext || "").replace(CODE_JSON_RE, "").trimEnd(); } function setSingleJsonCodeBlock(wikitext, jsonString){ const cleaned = stripAllJsonCodeBlocks(wikitext); const block = `<code json>\n${jsonString}\n</code>\n`; return (cleaned.trimEnd() + "\n\n" + block).trimEnd() + "\n"; } async function wikiLoadJson(pageId){ const form = await fetchEditForm(pageId); const blocks = extractJsonCodeBlocks(form.wikitext); if (!blocks.length) return []; let merged = []; let ok = false; for (const txt of blocks){ try { const data = JSON.parse(txt); if (Array.isArray(data)) { merged = merged.concat(data); ok = true; } } catch (_) {} } if (!ok) return []; // jeśli ktoś ma śmieci w code json, wolimy nie wywalać całej strony return merged; } async function wikiSaveJson(pageId, dataArray, summary){ const form = await fetchEditForm(pageId); const jsonString = JSON.stringify(dataArray, null, 2); const newWikitext = setSingleJsonCodeBlock(form.wikitext, jsonString); await saveWikitext(pageId, newWikitext, summary); } async function ensureSingleJsonBlock(pageId){ const form = await fetchEditForm(pageId); const blocks = extractJsonCodeBlocks(form.wikitext); if (blocks.length <= 1) return; let merged = []; for (const txt of blocks){ try { const data = JSON.parse(txt); if (Array.isArray(data)) merged = merged.concat(data); } catch(_) {} } await wikiSaveJson(pageId, merged, "Scalono wiele bloków JSON do jednego"); } /* ========================================================= 3) STAN ========================================================= */ let DB_REZ = []; // rezerwacje z PAGE_REZ let edytowaneId = null; const $ = id => document.getElementById(id); const hint = t => $('hint').textContent = t || ''; function newId(){ return `${Date.now()}-${Math.random().toString(16).slice(2)}`; } function normTime(hhmm){ if(!hhmm) return ''; const [h,m] = String(hhmm).split(':'); return `${String(h||'0').padStart(2,'0')}:${String(m||'0').padStart(2,'0')}`; } function normText(s){ return (s||'').toString().trim().toLowerCase(); } function toISODate(x){ return String(x || "").slice(0,10); } function excludedSet(ev){ // bazas ma excludedDates: [] const arr = Array.isArray(ev?.excludedDates) ? ev.excludedDates : []; return new Set(arr.map(toISODate).filter(Boolean)); } function listToISOSet(v){ if (!v) return new Set(); if (Array.isArray(v)) return new Set(v.map(toISODate).filter(Boolean)); return new Set(String(v).split(/[,\s]+/).map(toISODate).filter(Boolean)); } function isDateExcluded(ev, dateISO){ return listToISOSet(ev?.excludedDates || ev?.excludeDates).has(dateISO); } function isDateIncluded(ev, dateISO){ return listToISOSet(ev?.includeDates || ev?.includedDates).has(dateISO); } /* Normalizacja sali jak u Ciebie */ function normRoom(s){ if(!s) return ''; const t = s.toString().trim(); const m = t.match(/s\.\s*([A-Za-z0-9-]+)/i); let r = m ? m[1] : null; if(!r){ const m2 = t.match(/\b([A-Za-z]{0,3}\s*-?\s*\d{1,4}[A-Za-z]?)\b/); if (m2) r = m2[1]; } if(!r){ r = t.split(',')[0]; } return r.replace(/\s+/g,'').replace(/[^A-Za-z0-9]/g,'').toLowerCase(); } function overlaps(s1,e1,s2,e2){ return (s1 < e2) && (s2 < e1); } // [s1,e1)∩[s2,e2)≠∅ function addMinutesToTime(hhmm, minutes){ const [h,m] = hhmm.split(':').map(Number); const x = h*60+m+minutes; const H = String(Math.floor(x/60)).padStart(2,'0'); const M = String(x%60).padStart(2,'0'); return `${H}:${M}`; } function sameWeekdayISO(a,b){ const [Ya,Ma,Da] = a.split("-").map(Number); const [Yb,Mb,Db] = b.split("-").map(Number); return new Date(Date.UTC(Ya,Ma-1,Da)).getUTCDay() === new Date(Date.UTC(Yb,Mb-1,Db)).getUTCDay(); } function weeksBetween(a,b){ const [Ya,Ma,Da] = a.split("-").map(Number); const [Yb,Mb,Db] = b.split("-").map(Number); const A = Date.UTC(Ya,Ma-1,Da); const B = Date.UTC(Yb,Mb-1,Db); return Math.floor((B - A) / (7*24*3600*1000)); } function parseHHMM_toMinutes(hhmm){ const [h,m] = String(hhmm||"00:00").split(":").map(Number); return (h*60 + m) || 0; } function durationToMinutes(durationHHMM){ const [h,m] = String(durationHHMM||"00:00").split(":").map(Number); return (h*60 + m) || 0; } /* ========================================================= 4) UI: powtarzanie ========================================================= */ function onRepeatChange(){ const repeat = $('repeat').value; const wrap = $('endDateWrap'); if (repeat === 'none'){ wrap.style.display = 'none'; $('endDate').value = ''; } else { wrap.style.display = 'flex'; } } /* ========================================================= 5) ŁADOWANIE / ZAPIS DB (REZERWACJE) ========================================================= */ async function loadRezerwacje(){ DB_REZ = await wikiLoadJson(PAGE_REZ); DB_REZ = Array.isArray(DB_REZ) ? DB_REZ.map(normalizeRez) : []; } async function saveRezerwacje(summary){ await wikiSaveJson(PAGE_REZ, DB_REZ, summary); } function normalizeRez(ev){ const id = String(ev?.id || newId()); const eventType = (ev?.eventType || "").toString().trim(); const instructor = (ev?.instructor || "").toString().trim(); const repeat = (ev?.repeat || "none"); const endDate = (ev?.endDate || "").toString().slice(0,10); const terminy = Array.isArray(ev?.terminy) ? ev.terminy : []; const t0 = terminy[0] || {}; const startDate = (t0.startDate || "").toString().slice(0,10); const startTime = normTime(t0.startTime || ""); const endTime = normTime(t0.endTime || (t0.end ? String(t0.end).split('T')[1]?.slice(0,5) : "")); const room = (t0.room || ev.room || "").toString(); return { id, eventType, instructor, repeat, endDate, title: (ev?.title || `${eventType}, ${instructor}, ${room}`), terminy: [{ startDate, startTime, endTime, room, start: startDate && startTime ? `${startDate}T${startTime}:00` : "", end: startDate && endTime ? `${startDate}T${endTime}:00` : "" }] }; } /* ========================================================= 6) DATALISTS + FILTR ========================================================= */ function wypelnijDatalists(){ const types = new Set(), instructors = new Set(), rooms = new Set(); DB_REZ.forEach(ev=>{ if (ev.eventType) types.add(ev.eventType); if (ev.instructor) instructors.add(ev.instructor); (ev.terminy||[]).forEach(t=>{ if (t.room){ const onlyRoom = t.room.toString().split(',')[0].trim(); rooms.add(onlyRoom); } }); }); const fill = (id,set)=>{ const el=$(id); el.innerHTML=''; [...set].sort().forEach(v=>el.innerHTML+=`<option value="${v}">`); }; fill('eventTypeList', types); fill('instructorList', instructors); fill('roomList', rooms); } function wypelnijFiltrRodzaju(){ const sel = $('eventTypeFilter'); const current = sel.value; const types = new Set(); DB_REZ.forEach(ev=>{ if (ev.eventType) types.add(ev.eventType); }); sel.innerHTML = '<option value="">-- Wybierz --</option>'; [...types].sort().forEach(t=> sel.innerHTML += `<option value="${t}">${t}</option>`); if ([...types].includes(current)) sel.value = current; } /* ========================================================= 7) KOLIZJE: REZ (bazar) + STAC (bazas) + NIEST (bazan) ========================================================= */ /** Czy rezerwacja (z powtarzaniem) ma wystąpienie w dateISO? */ function rezOccursOnDate(ev, dateISO){ const t = (ev.terminy && ev.terminy[0]) || {}; const startDate = (t.startDate || "").slice(0,10); const repeat = ev.repeat || "none"; const endDate = (ev.endDate || "").slice(0,10); if (!startDate) return false; // wykluczenia / wyjątki (opcjonalnie dla bazar) if (typeof isDateExcluded === "function" && isDateExcluded(ev, dateISO)) return false; if (typeof isDateIncluded === "function" && isDateIncluded(ev, dateISO)) return true; if (repeat === "none"){ return dateISO === startDate; } if (!endDate) return false; if (dateISO < startDate || dateISO > endDate) return false; if (repeat === "daily"){ return true; } if (repeat === "weekly" || repeat === "biweekly"){ if (!sameWeekdayISO(startDate, dateISO)) return false; const w = weeksBetween(startDate, dateISO); const mod = (repeat === "biweekly") ? 2 : 1; return (w % mod === 0); } return false; } function stacOccurrenceOnDate(ev, dateISO){ const startDate = toISODate(ev.startDate); const endDate = toISODate(ev.endDate); const baseStart = normTime(ev.startTime || ""); const durMin = Number(ev.durationMin ?? ev.duration ?? 0); const interval = Number(ev.intervalWeeks ?? ev.interval ?? 1) || 1; if (!startDate || !endDate || !baseStart || !durMin) return null; // zakres if (dateISO < startDate || dateISO > endDate) return null; // excludedDates (bazas) if (excludedSet(ev).has(dateISO)) return null; // overrides (bazas) — format: // overrides: { // "2026-03-10": { startTime:"16:00", durationMin:120, room:"A3", instructor:"...", canceled:true } // } const o = ev?.overrides?.[dateISO]; // override może anulować termin if (o && (o.canceled === true || o.cancelled === true || o.excluded === true)) return null; const hasOverride = !!o; // jeśli NIE ma override, to liczymy regułę cyklu (w tym co 2 tygodnie) if (!hasOverride){ if (!sameWeekdayISO(startDate, dateISO)) return null; const w = weeksBetween(startDate, dateISO); if (w % interval !== 0) return null; } // jeśli jest override — pozwalamy “wymusić” wystąpienie w danym dniu const startTime = normTime((o && o.startTime) ? o.startTime : baseStart) || baseStart; const useDurMin = Number((o && o.durationMin != null) ? o.durationMin : durMin); const endTime = addMinutesToTime(startTime, useDurMin); const room = (o && o.room != null) ? o.room : (ev.room || ""); const person = (o && o.instructor != null) ? o.instructor : (ev.instructor || ""); const title = ev.eventType || ev.title || "Zajęcia"; return { source:"Stacjonarne", title, person, room, start:startTime, end:endTime }; } async function fetchStacForDate(dateISO){ const all = await getStacAll(); const out = []; (Array.isArray(all) ? all : []).forEach(ev=>{ const occ = stacOccurrenceOnDate(ev, dateISO); if (occ) out.push(occ); }); return out; } async function fetchNiestForDate(dateISO){ const all = await getNiestAll(); const out = []; (Array.isArray(all) ? all : []).forEach(ev=>{ const terminy = Array.isArray(ev.terminy) ? ev.terminy : (ev.terminy ? Object.values(ev.terminy) : []); terminy.forEach(t=>{ if (t?.canceled === true || t?.cancelled === true || t?.excluded === true) return; const sd = String(t.startDate || "").slice(0,10); if (sd !== dateISO) return; const start = normTime(t.startTime || (t.start ? String(t.start).split('T')[1]?.slice(0,5) : "")); if (!start) return; let end = normTime(t.endTime || (t.end ? String(t.end).split('T')[1]?.slice(0,5) : "")); if (!end && t.duration){ const mins = durationToMinutes(t.duration); end = addMinutesToTime(start, mins); } if (!end) return; out.push({ source: "Niestacjonarne", title: ev.eventType || ev.title || "Zajęcia", person: ev.instructor || "", room: t.room ?? ev.room ?? "", start, end }); }); }); return out; } function addDaysISO(dateISO, days){ const [Y,M,D] = dateISO.split("-").map(Number); const dt = new Date(Date.UTC(Y, M-1, D)); dt.setUTCDate(dt.getUTCDate() + days); return dt.toISOString().slice(0,10); } function buildOccurrenceDates(startDate, repeat, endDate){ // zwraca listę dat ISO dla nowo dodawanego wydarzenia const out = []; if (!startDate) return out; if (repeat === "none"){ out.push(startDate); return out; } if (!endDate) return out; const stepDays = repeat === "daily" ? 1 : repeat === "weekly" ? 7 : repeat === "biweekly" ? 14 : 0; if (!stepDays) return out; let cur = startDate; while (cur <= endDate){ out.push(cur); cur = addDaysISO(cur, stepDays); } return out; } let _cacheStacAll = null; let _cacheNiestAll = null; async function getStacAll(){ if (_cacheStacAll) return _cacheStacAll; _cacheStacAll = await wikiLoadJson(PAGE_STAC); return _cacheStacAll; } async function getNiestAll(){ if (_cacheNiestAll) return _cacheNiestAll; _cacheNiestAll = await wikiLoadJson(PAGE_NIEST); return _cacheNiestAll; } function resetConflictCaches(){ _cacheStacAll = null; _cacheNiestAll = null; } /** GŁÓWNE SPRAWDZANIE KOLIZJI */ async function checkConflicts(dateISO, startHHMM, endHHMM, room, instructor, excludeId=null){ const sWanted = normTime(startHHMM); const eWanted = normTime(endHHMM); const roomWanted = normRoom(room); const personWanted = normText(instructor); // 1) kolizje z własną bazą (bazar) — uwzględnia cykliczne for (const ev of DB_REZ){ if (excludeId && String(ev.id) === String(excludeId)) continue; if (!rezOccursOnDate(ev, dateISO)) continue; const t = (ev.terminy && ev.terminy[0]) || {}; const s = normTime(t.startTime); const e = normTime(t.endTime); if (!s || !e) continue; if (overlaps(s, e, sWanted, eWanted)){ if (roomWanted && normRoom(t.room) === roomWanted) throw new Error(`Kolizja: sala "${room}" zajęta przez [Rezerwacje] ${ev.eventType} — ${ev.instructor} (${s}–${e})`); if (personWanted && normText(ev.instructor) === personWanted) throw new Error(`Kolizja prowadzącego "${instructor}" z [Rezerwacje] ${ev.eventType} (${s}–${e})`); } } // 2) kolizje z innymi bazami (bazas + bazan) const [stac, niest] = await Promise.all([fetchStacForDate(dateISO), fetchNiestForDate(dateISO)]); for (const x of [...stac, ...niest]){ if (overlaps(x.start, x.end, sWanted, eWanted)){ if (roomWanted && normRoom(x.room) === roomWanted) throw new Error(`Kolizja: sala "${room}" zajęta [${x.source}] ${x.title} — ${x.person} (${x.start}–${x.end})`); if (personWanted && normText(x.person) === personWanted) throw new Error(`Kolizja prowadzącego "${instructor}" [${x.source}] ${x.title} (${x.start}–${x.end})`); } } } /* ========================================================= 8) CRUD: dodawanie / edycja / usuwanie (bazar) ========================================================= */ async function dodajLubZapisz(){ resetConflictCaches(); const eventType = $('eventType').value.trim(); const instructor = $('instructor').value.trim(); const date = $('date').value; const repeat = $('repeat').value; const endDate = $('endDate').value; // wymagane tylko jeśli repeat != none const start = $('start').value; const end = $('end').value; const room = $('room').value.trim(); if (!eventType || !instructor || !date || !start || !end || !room){ alert('Uzupełnij: Rodzaj, Prowadzący, Data startu, Godzina rozpoczęcia, Godzina zakończenia, Sala.'); return; } if (end <= start){ alert('Godzina zakończenia musi być późniejsza niż rozpoczęcie.'); return; } if (repeat !== 'none'){ if (!endDate){ alert('Dla wydarzenia powtarzalnego uzupełnij "Koniec wydarzenia".'); return; } if (endDate < date){ alert('Koniec wydarzenia nie może być wcześniejszy niż data startu.'); return; } } try{ // sprawdzamy wszystkie wystąpienia nowego wydarzenia (a nie tylko pierwszą datę) const occurrenceDates = buildOccurrenceDates(date, repeat, (repeat === "none" ? "" : endDate)); for (const dISO of occurrenceDates){ await checkConflicts(dISO, start, end, room, instructor, edytowaneId); } const termin = { start: `${date}T${start}:00`, end: `${date}T${end}:00`, startDate: date, startTime: start, endTime: end, room }; const payload = { id: edytowaneId || newId(), eventType, instructor, repeat, endDate: (repeat === 'none') ? "" : endDate, terminy: [termin], title: `${eventType}, ${instructor}, ${room}` }; await loadRezerwacje(); // świeże dane (żeby nie nadpisać cudzych zmian) if (edytowaneId){ const idx = DB_REZ.findIndex(e => String(e.id) === String(edytowaneId)); if (idx === -1) throw new Error("Rekord do edycji zniknął (ktoś mógł usunąć w międzyczasie)."); DB_REZ[idx] = payload; await saveRezerwacje("Zmieniono rezerwację (bazar)"); edytowaneId = null; $('addBtn').textContent = 'Dodaj'; $('cancelBtn').style.display = 'none'; } else { DB_REZ.push(payload); await saveRezerwacje("Dodano rezerwację (bazar)"); } wyczyscForm(); await refreshAll(); }catch(err){ alert(err.message || String(err)); } } function wyczyscForm(){ ['eventType','instructor','date','start','end','room','endDate'].forEach(id=>$(id).value=''); $('repeat').value = 'none'; onRepeatChange(); } function anulujEdycje(){ edytowaneId = null; wyczyscForm(); $('addBtn').textContent = 'Dodaj'; $('cancelBtn').style.display = 'none'; odswiezListe(); } function edytuj(id){ const ev = DB_REZ.find(e => String(e.id) === String(id)); if (!ev) return; const t = (ev.terminy && ev.terminy[0]) || {}; $('eventType').value = ev.eventType || ''; $('instructor').value = ev.instructor || ''; $('date').value = t.startDate || ''; $('start').value = t.startTime || ''; $('end').value = t.endTime || ''; $('room').value = t.room || ''; $('repeat').value = ev.repeat || 'none'; $('endDate').value = ev.endDate || ''; onRepeatChange(); edytowaneId = ev.id; $('addBtn').textContent = 'Zapisz'; $('cancelBtn').style.display = 'inline-block'; } async function usun(id){ if (!confirm('Usunąć to wydarzenie?')) return; try{ await loadRezerwacje(); DB_REZ = DB_REZ.filter(e => String(e.id) !== String(id)); await saveRezerwacje("Usunięto rezerwację (bazar)"); if (edytowaneId && String(edytowaneId) === String(id)){ edytowaneId = null; $('addBtn').textContent = 'Dodaj'; $('cancelBtn').style.display = 'none'; wyczyscForm(); } await refreshAll(); }catch(err){ alert("Błąd usuwania: " + (err.message || String(err))); } } /* ========================================================= 9) LISTA (tabela) ========================================================= */ function repeatLabel(v){ if (v === 'daily') return 'Codziennie'; if (v === 'weekly') return 'Co tydzień'; if (v === 'biweekly') return 'Co 2 tygodnie'; return 'Nie'; } function odswiezListe(){ const tbody = $('lista'); tbody.innerHTML=''; const filterType = $('eventTypeFilter').value; let rows = []; DB_REZ.forEach(ev=>{ const t = (ev.terminy && ev.terminy[0]) || {}; rows.push({ id: ev.id, room: t.room || '', date: t.startDate || '', start: t.startTime || '', end: t.endTime || '', instructor: ev.instructor || '', eventType: ev.eventType || '', repeat: ev.repeat || 'none', endDate: ev.endDate || '' }); }); if (filterType) rows = rows.filter(r => r.eventType === filterType); rows.sort((a,b)=>{ if (a.date !== b.date) return a.date.localeCompare(b.date); if (a.start !== b.start) return a.start.localeCompare(b.start); return a.room.localeCompare(b.room); }); rows.forEach((r,idx)=>{ const tr = document.createElement('tr'); tr.innerHTML = ` <td>${idx+1}</td> <td>${r.room}</td> <td>${(r.date || '').split('-').reverse().join('.')}${r.repeat !== 'none' ? `<br><span class="muted">start</span>` : ''}</td> <td>${r.start} – ${r.end}</td> <td>${r.instructor}</td> <td>${r.eventType}</td> <td>${repeatLabel(r.repeat)}</td> <td>${r.repeat === 'none' ? '-' : (r.endDate ? r.endDate.split('-').reverse().join('.') : '-')}</td> <td> <button class="edit-btn" onclick="edytuj('${r.id}')">Edytuj</button> <button class="delete-btn" onclick="usun('${r.id}')">Usuń</button> </td> `; tbody.appendChild(tr); }); hint(rows.length ? '' : 'Brak wpisów dla wybranego rodzaju wydarzenia.'); } /* ========================================================= 10) INIT / REFRESH ========================================================= */ async function refreshAll(){ await loadRezerwacje(); wypelnijDatalists(); wypelnijFiltrRodzaju(); odswiezListe(); } async function init(){ try{ // porządek: scalamy bloki json jeśli ktoś narobił 2+ await Promise.all([ ensureSingleJsonBlock(PAGE_REZ), ensureSingleJsonBlock(PAGE_STAC), ensureSingleJsonBlock(PAGE_NIEST) ]); await refreshAll(); onRepeatChange(); hint('Sprawdzam kolizje: Rezerwacje(bazar) + Stacjonarne(bazas) + Niestacjonarne(bazan) (sala/prowadzący).'); }catch(e){ console.error(e); alert("Błąd inicjalizacji: " + (e.message || String(e))); } } window.onload = init; </script> </body>