/** * Main application logic for Scenar Creator v4. * Multi-day state, duration input, horizontal canvas. */ const App = { state: { event: { title: '', subtitle: '', date_from: '', date_to: '', location: '' }, program_types: [], blocks: [] }, init() { this.bindEvents(); this.newScenario(); }, // ─── Helpers ─────────────────────────────────────────────────────── uid() { return 'b_' + Math.random().toString(36).slice(2, 10); }, parseTimeToMin(str) { if (!str) return 0; const [h, m] = str.split(':').map(Number); return (h || 0) * 60 + (m || 0); }, minutesToTime(totalMin) { const h = Math.floor(totalMin / 60); const m = totalMin % 60; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; }, // Return sorted list of all dates from date_from to date_to (inclusive) getDates() { const from = this.state.event.date_from || this.state.event.date; const to = this.state.event.date_to || from; if (!from) return []; const dates = []; const cur = new Date(from + 'T12:00:00'); const end = new Date((to || from) + 'T12:00:00'); // Safety: max 31 days let safety = 0; while (cur <= end && safety < 31) { dates.push(cur.toISOString().slice(0, 10)); cur.setDate(cur.getDate() + 1); safety++; } return dates; }, // ─── State ──────────────────────────────────────────────────────── getDocument() { this.syncEventFromUI(); return { version: '1.0', event: { ...this.state.event, date: this.state.event.date_from, // backward compat }, program_types: this.state.program_types.map(pt => ({ ...pt })), blocks: this.state.blocks.map(b => ({ ...b })) }; }, loadDocument(doc) { const ev = doc.event || {}; // Backward compat: if only date exists, use it as date_from = date_to const date_from = ev.date_from || ev.date || ''; const date_to = ev.date_to || date_from; this.state.event = { title: ev.title || '', subtitle: ev.subtitle || '', date_from, date_to, location: ev.location || '', }; this.state.program_types = (doc.program_types || []).map(pt => ({ ...pt })); this.state.blocks = (doc.blocks || []).map(b => ({ ...b, id: b.id || this.uid() })); this.syncEventToUI(); this.renderTypes(); this.renderCanvas(); }, newScenario() { const today = new Date().toISOString().slice(0, 10); this.state = { event: { title: 'Nová akce', subtitle: '', date_from: today, date_to: today, location: '' }, program_types: [ { id: 'main', name: 'Hlavní program', color: '#3B82F6' }, { id: 'rest', name: 'Odpočinek', color: '#22C55E' } ], blocks: [] }; this.syncEventToUI(); this.renderTypes(); this.renderCanvas(); }, // ─── Sync sidebar <-> state ─────────────────────────────────────── syncEventFromUI() { this.state.event.title = document.getElementById('eventTitle').value.trim() || 'Nová akce'; this.state.event.subtitle = document.getElementById('eventSubtitle').value.trim() || null; this.state.event.date_from = document.getElementById('eventDateFrom').value || null; this.state.event.date_to = document.getElementById('eventDateTo').value || this.state.event.date_from; this.state.event.location = document.getElementById('eventLocation').value.trim() || null; }, syncEventToUI() { document.getElementById('eventTitle').value = this.state.event.title || ''; document.getElementById('eventSubtitle').value = this.state.event.subtitle || ''; document.getElementById('eventDateFrom').value = this.state.event.date_from || ''; document.getElementById('eventDateTo').value = this.state.event.date_to || this.state.event.date_from || ''; document.getElementById('eventLocation').value = this.state.event.location || ''; }, // ─── Program types ──────────────────────────────────────────────── renderTypes() { const container = document.getElementById('programTypesContainer'); container.innerHTML = ''; this.state.program_types.forEach((pt, i) => { const row = document.createElement('div'); row.className = 'type-row'; row.innerHTML = ` `; row.querySelector('input[type="color"]').addEventListener('change', (e) => { this.state.program_types[i].color = e.target.value; this.renderCanvas(); }); row.querySelector('input[type="text"]').addEventListener('change', (e) => { this.state.program_types[i].name = e.target.value.trim(); }); row.querySelector('.type-remove').addEventListener('click', () => { this.state.program_types.splice(i, 1); this.renderTypes(); this.renderCanvas(); }); container.appendChild(row); }); }, // ─── Canvas ─────────────────────────────────────────────────────── renderCanvas() { Canvas.render(this.state); }, // ─── Block modal ────────────────────────────────────────────────── // Open modal to edit existing block openBlockModal(blockId) { const block = this.state.blocks.find(b => b.id === blockId); if (!block) return; document.getElementById('modalTitle').textContent = 'Upravit blok'; document.getElementById('modalBlockId').value = block.id; document.getElementById('modalBlockTitle').value = block.title || ''; document.getElementById('modalBlockStart').value = block.start || ''; document.getElementById('modalBlockEnd').value = block.end || ''; document.getElementById('modalBlockResponsible').value = block.responsible || ''; document.getElementById('modalBlockNotes').value = block.notes || ''; this._updateDuration(); this._populateTypeSelect(block.type_id); document.getElementById('modalBlockDate').value = block.date || ''; document.getElementById('modalDeleteBtn').style.display = 'inline-block'; document.getElementById('blockModal').classList.remove('hidden'); }, // Open modal to create new block openNewBlockModal(date, start, end) { document.getElementById('modalTitle').textContent = 'Nový blok'; document.getElementById('modalBlockId').value = ''; document.getElementById('modalBlockTitle').value = ''; document.getElementById('modalBlockStart').value = start || '09:00'; document.getElementById('modalBlockEnd').value = end || '10:00'; document.getElementById('modalBlockResponsible').value = ''; document.getElementById('modalBlockNotes').value = ''; document.getElementById('modalBlockDate').value = date || ''; this._updateDuration(); this._populateTypeSelect(null); document.getElementById('modalDeleteBtn').style.display = 'none'; document.getElementById('blockModal').classList.remove('hidden'); }, _populateTypeSelect(selectedId) { const sel = document.getElementById('modalBlockType'); sel.innerHTML = ''; this.state.program_types.forEach(pt => { const opt = document.createElement('option'); opt.value = pt.id; opt.textContent = pt.name; if (pt.id === selectedId) opt.selected = true; sel.appendChild(opt); }); }, _updateDuration() { const startVal = document.getElementById('modalBlockStart').value; const endVal = document.getElementById('modalBlockEnd').value; if (!startVal || !endVal) { document.getElementById('modalBlockDuration').value = ''; return; } const s = this.parseTimeToMin(startVal); let e = this.parseTimeToMin(endVal); if (e < s) e += 24 * 60; // overnight const dur = e - s; document.getElementById('modalBlockDuration').value = this.minutesToTime(dur); }, _saveModal() { const blockId = document.getElementById('modalBlockId').value; const date = document.getElementById('modalBlockDate').value; const title = document.getElementById('modalBlockTitle').value.trim(); const type_id = document.getElementById('modalBlockType').value; const start = document.getElementById('modalBlockStart').value; const end = document.getElementById('modalBlockEnd').value; const responsible = document.getElementById('modalBlockResponsible').value.trim() || null; const notes = document.getElementById('modalBlockNotes').value.trim() || null; if (!title) { this.toast('Zadejte název bloku', 'error'); return; } if (!start || !end) { this.toast('Zadejte čas začátku a konce', 'error'); return; } if (blockId) { // Edit existing const idx = this.state.blocks.findIndex(b => b.id === blockId); if (idx !== -1) { Object.assign(this.state.blocks[idx], { date, title, type_id, start, end, responsible, notes }); } } else { // New block this.state.blocks.push({ id: this.uid(), date, title, type_id, start, end, responsible, notes }); } document.getElementById('blockModal').classList.add('hidden'); this.renderCanvas(); this.toast('Blok uložen', 'success'); }, _deleteBlock() { const blockId = document.getElementById('modalBlockId').value; if (!blockId) return; this.state.blocks = this.state.blocks.filter(b => b.id !== blockId); document.getElementById('blockModal').classList.add('hidden'); this.renderCanvas(); this.toast('Blok smazán', 'success'); }, // ─── Toast ──────────────────────────────────────────────────────── toast(message, type = 'success') { const el = document.getElementById('toast'); if (!el) return; el.textContent = message; el.className = `toast ${type}`; el.classList.remove('hidden'); clearTimeout(this._toastTimer); this._toastTimer = setTimeout(() => el.classList.add('hidden'), 3000); }, // ─── Events ─────────────────────────────────────────────────────── bindEvents() { // Import JSON document.getElementById('importJsonInput').addEventListener('change', (e) => { if (e.target.files[0]) importJson(e.target.files[0]); e.target.value = ''; }); // Export JSON document.getElementById('exportJsonBtn').addEventListener('click', () => exportJson()); // New scenario document.getElementById('newScenarioBtn').addEventListener('click', () => { if (!confirm('Vytvořit nový scénář? Neuložené změny budou ztraceny.')) return; this.newScenario(); }); // Generate PDF document.getElementById('generatePdfBtn').addEventListener('click', async () => { this.syncEventFromUI(); const doc = this.getDocument(); if (!doc.blocks.length) { this.toast('Žádné bloky k exportu', 'error'); return; } try { this.toast('Generuji PDF…', 'success'); const blob = await API.postBlob('/api/generate-pdf', doc); API.downloadBlob(blob, 'scenar.pdf'); this.toast('PDF staženo', 'success'); } catch (err) { this.toast('Chyba PDF: ' + err.message, 'error'); } }); // Add block button document.getElementById('addBlockBtn').addEventListener('click', () => { const dates = this.getDates(); const date = dates[0] || new Date().toISOString().slice(0, 10); this.openNewBlockModal(date, '09:00', '10:00'); }); // Add type document.getElementById('addTypeBtn').addEventListener('click', () => { this.state.program_types.push({ id: 'type_' + Math.random().toString(36).slice(2, 6), name: 'Nový typ', color: '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0') }); this.renderTypes(); }); // Modal close document.getElementById('modalClose').addEventListener('click', () => { document.getElementById('blockModal').classList.add('hidden'); }); document.getElementById('modalSaveBtn').addEventListener('click', () => this._saveModal()); document.getElementById('modalDeleteBtn').addEventListener('click', () => this._deleteBlock()); // Duration ↔ end sync document.getElementById('modalBlockDuration').addEventListener('input', (e) => { const durStr = e.target.value.trim(); const startVal = document.getElementById('modalBlockStart').value; if (!startVal || !durStr) return; const durParts = durStr.split(':').map(Number); if (durParts.length < 2 || isNaN(durParts[0]) || isNaN(durParts[1])) return; const durMin = durParts[0] * 60 + durParts[1]; if (durMin <= 0) return; const startMin = this.parseTimeToMin(startVal); const endMin = startMin + durMin; document.getElementById('modalBlockEnd').value = this.minutesToTime(endMin % 1440 || endMin); }); document.getElementById('modalBlockEnd').addEventListener('input', () => { this._updateDuration(); }); document.getElementById('modalBlockStart').addEventListener('input', () => { this._updateDuration(); }); // Date range sync: if dateFrom changes and dateTo < dateFrom, set dateTo = dateFrom document.getElementById('eventDateFrom').addEventListener('change', (e) => { const toEl = document.getElementById('eventDateTo'); if (!toEl.value || toEl.value < e.target.value) { toEl.value = e.target.value; } this.syncEventFromUI(); this.renderCanvas(); }); document.getElementById('eventDateTo').addEventListener('change', () => { this.syncEventFromUI(); this.renderCanvas(); }); // Tabs document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden')); tab.classList.add('active'); document.getElementById('tab-' + tab.dataset.tab).classList.remove('hidden'); }); }); // Close modal on overlay click document.getElementById('blockModal').addEventListener('click', (e) => { if (e.target === document.getElementById('blockModal')) { document.getElementById('blockModal').classList.add('hidden'); } }); }, };