/** * Main application logic for Scenar Creator v3. * State management, UI wiring, modal handling. */ const App = { state: { event: { title: '', subtitle: '', date: '', location: '' }, program_types: [], blocks: [] }, init() { this.bindEvents(); this.newScenario(); }, // --- State --- getDocument() { this.syncEventFromUI(); return { version: '1.0', event: { ...this.state.event }, program_types: this.state.program_types.map(pt => ({ ...pt })), blocks: this.state.blocks.map(b => ({ ...b })) }; }, loadDocument(doc) { this.state.event = { ...doc.event }; 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().split('T')[0]; this.state = { event: { title: 'Nová akce', subtitle: '', date: 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 = document.getElementById('eventDate').value || null; 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('eventDate').value = this.state.event.date || ''; 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 = ` `; // Color change row.querySelector('input[type="color"]').addEventListener('change', (e) => { this.state.program_types[i].color = e.target.value; this.renderCanvas(); }); // Name change row.querySelector('input[type="text"]').addEventListener('change', (e) => { this.state.program_types[i].name = e.target.value.trim(); }); // Remove row.querySelector('.type-remove').addEventListener('click', () => { this.state.program_types.splice(i, 1); this.renderTypes(); this.renderCanvas(); }); container.appendChild(row); }); }, addType() { const id = 'type_' + this.uid().substring(0, 6); this.state.program_types.push({ id, name: 'Nový typ', color: '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0') }); this.renderTypes(); }, // --- Blocks --- createBlock(date, start, end) { const typeId = this.state.program_types.length > 0 ? this.state.program_types[0].id : 'main'; const block = { id: this.uid(), date: date || this.state.event.date || new Date().toISOString().split('T')[0], start: start || '09:00', end: end || '10:00', title: 'Nový blok', type_id: typeId, responsible: null, notes: null }; this.state.blocks.push(block); this.renderCanvas(); this.editBlock(block.id); }, updateBlockTime(blockId, start, end) { const block = this.state.blocks.find(b => b.id === blockId); if (block) { block.start = start; block.end = end; this.renderCanvas(); } }, deleteBlock(blockId) { this.state.blocks = this.state.blocks.filter(b => b.id !== blockId); this.renderCanvas(); }, // --- Canvas rendering --- renderCanvas() { const dates = this.getUniqueDates(); Canvas.renderTimeAxis(); Canvas.renderDayColumns(dates); Canvas.renderBlocks(this.state.blocks, this.state.program_types); }, getUniqueDates() { const dateSet = new Set(); this.state.blocks.forEach(b => dateSet.add(b.date)); if (this.state.event.date) dateSet.add(this.state.event.date); const dates = Array.from(dateSet).sort(); return dates.length > 0 ? dates : [new Date().toISOString().split('T')[0]]; }, // --- Modal --- editBlock(blockId) { const block = this.state.blocks.find(b => b.id === blockId); if (!block) return; 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 || ''; // Populate type select const select = document.getElementById('modalBlockType'); select.innerHTML = ''; this.state.program_types.forEach(pt => { const opt = document.createElement('option'); opt.value = pt.id; opt.textContent = pt.name; if (pt.id === block.type_id) opt.selected = true; select.appendChild(opt); }); document.getElementById('blockModal').classList.remove('hidden'); }, saveBlockFromModal() { const blockId = document.getElementById('modalBlockId').value; const block = this.state.blocks.find(b => b.id === blockId); if (!block) return; block.title = document.getElementById('modalBlockTitle').value.trim() || 'Blok'; block.type_id = document.getElementById('modalBlockType').value; block.start = document.getElementById('modalBlockStart').value; block.end = document.getElementById('modalBlockEnd').value; block.responsible = document.getElementById('modalBlockResponsible').value.trim() || null; block.notes = document.getElementById('modalBlockNotes').value.trim() || null; this.closeModal(); this.renderCanvas(); }, closeModal() { document.getElementById('blockModal').classList.add('hidden'); }, // --- Toast --- toast(message, type) { const el = document.getElementById('toast'); el.textContent = message; el.className = 'toast ' + (type || 'info'); setTimeout(() => el.classList.add('hidden'), 3000); }, // --- PDF --- async generatePdf() { try { const doc = this.getDocument(); if (doc.blocks.length === 0) { this.toast('Přidejte alespoň jeden blok', 'error'); return; } const blob = await API.postBlob('/api/generate-pdf', doc); API.downloadBlob(blob, 'scenar_timetable.pdf'); this.toast('PDF vygenerován', 'success'); } catch (err) { this.toast('Chyba: ' + err.message, 'error'); } }, // --- Utility --- uid() { return 'xxxx-xxxx-xxxx'.replace(/x/g, () => Math.floor(Math.random() * 16).toString(16) ); }, // --- Event binding --- bindEvents() { // 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'); }); }); // Header buttons document.getElementById('newScenarioBtn').addEventListener('click', () => this.newScenario()); document.getElementById('exportJsonBtn').addEventListener('click', () => exportJson()); document.getElementById('generatePdfBtn').addEventListener('click', () => this.generatePdf()); // Import JSON document.getElementById('importJsonInput').addEventListener('change', (e) => { if (e.target.files[0]) importJson(e.target.files[0]); e.target.value = ''; }); // Sidebar document.getElementById('addTypeBtn').addEventListener('click', () => this.addType()); document.getElementById('addBlockBtn').addEventListener('click', () => this.createBlock()); // Modal document.getElementById('modalSaveBtn').addEventListener('click', () => this.saveBlockFromModal()); document.getElementById('modalClose').addEventListener('click', () => this.closeModal()); document.getElementById('modalDeleteBtn').addEventListener('click', () => { const blockId = document.getElementById('modalBlockId').value; this.closeModal(); this.deleteBlock(blockId); }); // Close modal on overlay click document.getElementById('blockModal').addEventListener('click', (e) => { if (e.target.classList.contains('modal-overlay')) this.closeModal(); }); // Drag & drop JSON on canvas const canvasWrapper = document.querySelector('.canvas-wrapper'); if (canvasWrapper) { canvasWrapper.addEventListener('dragover', (e) => e.preventDefault()); canvasWrapper.addEventListener('drop', (e) => { e.preventDefault(); const file = e.dataTransfer.files[0]; if (file && file.name.endsWith('.json')) { importJson(file); } }); } } }; // Boot document.addEventListener('DOMContentLoaded', () => App.init());