feat: v4.2.0 - series blocks (add to all days, delete one/all in series); 37 tests
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Some checks failed
Build & Push Docker / build (push) Has been cancelled
This commit is contained in:
@@ -946,3 +946,61 @@ body {
|
||||
margin-bottom: 4px;
|
||||
color: var(--header-bg);
|
||||
}
|
||||
|
||||
/* Modal footer with left/right split */
|
||||
.modal-footer-left {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Outline danger button (for "Smazat sérii") */
|
||||
.btn-danger-outline {
|
||||
background: transparent;
|
||||
color: var(--danger);
|
||||
border: 1px solid var(--danger);
|
||||
}
|
||||
|
||||
.btn-danger-outline:hover {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Series checkbox row */
|
||||
.series-row {
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.series-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.series-label input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.series-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-light);
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="header-title">Scenár Creator</h1>
|
||||
<span class="header-version">v4.0</span>
|
||||
<span class="header-version">v4.2</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<label class="btn btn-secondary btn-sm" id="importJsonBtn">
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="tab-content hidden" id="tab-docs">
|
||||
<div class="docs-container">
|
||||
<h2>Scenár Creator — Dokumentace</h2>
|
||||
<p class="docs-version">Verze 4.2 | <a href="/docs" target="_blank">Swagger API</a> | <a href="/api/sample">Vzorový JSON</a></p>
|
||||
<p class="docs-version">Verze 4.2.0 | <a href="/docs" target="_blank">Swagger API</a> | <a href="/api/sample">Vzorový JSON</a></p>
|
||||
|
||||
<h3>Jak začít</h3>
|
||||
<ol>
|
||||
@@ -103,10 +103,12 @@
|
||||
<h3>Práce s bloky</h3>
|
||||
<ul>
|
||||
<li><strong>Přidání:</strong> Klikněte na „+ Přidat blok" nebo klikněte na prázdné místo v řádku dne.</li>
|
||||
<li><strong>Přidání do všech dnů:</strong> V modalu nového bloku zaškrtněte „Přidat do každého dne kurzu" — vytvoří identický blok pro každý den akce (sdílená série).</li>
|
||||
<li><strong>Přesun:</strong> Chytněte blok a táhněte doleva/doprava. Snap na 15 minut.</li>
|
||||
<li><strong>Změna délky:</strong> Chytněte pravý okraj bloku a táhněte.</li>
|
||||
<li><strong>Úprava:</strong> Klikněte na blok — otevře se formulář.</li>
|
||||
<li><strong>Smazání:</strong> V editačním formuláři klikněte na „Smazat blok".</li>
|
||||
<li><strong>Úprava:</strong> Klikněte na blok — otevře se formulář s editací jednoho bloku.</li>
|
||||
<li><strong>Smazání jednoho bloku:</strong> V editačním formuláři klikněte na „Smazat jen tento".</li>
|
||||
<li><strong>Smazání celé série:</strong> Pokud byl blok přidán jako součást série (zaškrtávací políčko), zobrazí se tlačítko „Smazat sérii" — smaže všechny bloky se stejným series_id.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Formulář bloku</h3>
|
||||
@@ -151,6 +153,7 @@
|
||||
<tr><td>blocks[].type_id</td><td>string</td><td>ID typu programu (musí existovat v program_types)</td></tr>
|
||||
<tr><td>blocks[].responsible</td><td>string?</td><td>Garant — zobrazí se v editoru i PDF</td></tr>
|
||||
<tr><td>blocks[].notes</td><td>string?</td><td>Poznámka — jen v PDF, jako horní index + stránka 2</td></tr>
|
||||
<tr><td>blocks[].series_id</td><td>string?</td><td>Sdílené ID série — bloky přidané přes „Přidat do všech dnů" sdílejí toto ID</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -216,9 +219,20 @@
|
||||
<label>Poznámka</label>
|
||||
<textarea id="modalBlockNotes" rows="3" placeholder="Poznámka"></textarea>
|
||||
</div>
|
||||
<!-- Shown only when creating a new block -->
|
||||
<div class="form-group series-row hidden" id="seriesRow">
|
||||
<label class="series-label">
|
||||
<input type="checkbox" id="modalAddToAllDays">
|
||||
Přidat do každého dne kurzu
|
||||
</label>
|
||||
<p class="series-hint">Vytvoří identický blok pro každý den akce (sdílená série).</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger btn-sm" id="modalDeleteBtn">Smazat blok</button>
|
||||
<div class="modal-footer-left">
|
||||
<button class="btn btn-danger btn-sm hidden" id="modalDeleteBtn">Smazat jen tento</button>
|
||||
<button class="btn btn-danger-outline btn-sm hidden" id="modalDeleteSeriesBtn">Smazat sérii</button>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" id="modalSaveBtn">Uložit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,18 @@ const App = {
|
||||
this._populateDaySelect(block.date);
|
||||
this._updateDuration();
|
||||
|
||||
document.getElementById('modalDeleteBtn').style.display = 'inline-block';
|
||||
// Show delete buttons; series delete only if block belongs to a series
|
||||
document.getElementById('modalDeleteBtn').classList.remove('hidden');
|
||||
document.getElementById('seriesRow').classList.add('hidden');
|
||||
const seriesBtn = document.getElementById('modalDeleteSeriesBtn');
|
||||
if (block.series_id) {
|
||||
const seriesCount = this.state.blocks.filter(b => b.series_id === block.series_id).length;
|
||||
seriesBtn.textContent = `Smazat sérii (${seriesCount} bloků)`;
|
||||
seriesBtn.classList.remove('hidden');
|
||||
} else {
|
||||
seriesBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('blockModal').classList.remove('hidden');
|
||||
},
|
||||
|
||||
@@ -196,7 +207,12 @@ const App = {
|
||||
this._populateDaySelect(date);
|
||||
this._updateDuration();
|
||||
|
||||
document.getElementById('modalDeleteBtn').style.display = 'none';
|
||||
// Hide delete buttons, show series row
|
||||
document.getElementById('modalDeleteBtn').classList.add('hidden');
|
||||
document.getElementById('modalDeleteSeriesBtn').classList.add('hidden');
|
||||
document.getElementById('seriesRow').classList.remove('hidden');
|
||||
document.getElementById('modalAddToAllDays').checked = false;
|
||||
|
||||
document.getElementById('blockModal').classList.remove('hidden');
|
||||
},
|
||||
|
||||
@@ -281,14 +297,32 @@ const App = {
|
||||
if (!start || !end) { this.toast('Zadejte čas začátku a konce', 'error'); return; }
|
||||
|
||||
if (blockId) {
|
||||
// Edit existing
|
||||
// Edit existing block (no series expansion on edit — user edits only this one)
|
||||
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 });
|
||||
const existing = this.state.blocks[idx];
|
||||
Object.assign(this.state.blocks[idx], {
|
||||
date, title, type_id, start, end, responsible, notes,
|
||||
series_id: existing.series_id || null
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// New block
|
||||
this.state.blocks.push({ id: this.uid(), date, title, type_id, start, end, responsible, notes });
|
||||
const addToAll = document.getElementById('modalAddToAllDays').checked;
|
||||
if (addToAll) {
|
||||
// Add a copy to every day in the event range, all sharing a series_id
|
||||
const series_id = this.uid();
|
||||
const dates = this.getDates();
|
||||
for (const d of dates) {
|
||||
this.state.blocks.push({ id: this.uid(), date: d, title, type_id, start, end, responsible, notes, series_id });
|
||||
}
|
||||
document.getElementById('blockModal').classList.add('hidden');
|
||||
this.renderCanvas();
|
||||
this.toast(`Blok přidán do ${dates.length} dnů`, 'success');
|
||||
return;
|
||||
} else {
|
||||
this.state.blocks.push({ id: this.uid(), date, title, type_id, start, end, responsible, notes, series_id: null });
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('blockModal').classList.add('hidden');
|
||||
@@ -305,6 +339,23 @@ const App = {
|
||||
this.toast('Blok smazán', 'success');
|
||||
},
|
||||
|
||||
_deleteBlockSeries() {
|
||||
const blockId = document.getElementById('modalBlockId').value;
|
||||
if (!blockId) return;
|
||||
const block = this.state.blocks.find(b => b.id === blockId);
|
||||
if (!block || !block.series_id) {
|
||||
// Fallback: delete just this one
|
||||
this._deleteBlock();
|
||||
return;
|
||||
}
|
||||
const seriesId = block.series_id;
|
||||
const count = this.state.blocks.filter(b => b.series_id === seriesId).length;
|
||||
this.state.blocks = this.state.blocks.filter(b => b.series_id !== seriesId);
|
||||
document.getElementById('blockModal').classList.add('hidden');
|
||||
this.renderCanvas();
|
||||
this.toast(`Série smazána (${count} bloků)`, 'success');
|
||||
},
|
||||
|
||||
// ─── Toast ────────────────────────────────────────────────────────
|
||||
|
||||
toast(message, type = 'success') {
|
||||
@@ -376,6 +427,7 @@ const App = {
|
||||
});
|
||||
document.getElementById('modalSaveBtn').addEventListener('click', () => this._saveModal());
|
||||
document.getElementById('modalDeleteBtn').addEventListener('click', () => this._deleteBlock());
|
||||
document.getElementById('modalDeleteSeriesBtn').addEventListener('click', () => this._deleteBlockSeries());
|
||||
|
||||
// Duration ↔ end sync (hours + minutes fields)
|
||||
const durUpdater = () => {
|
||||
|
||||
Reference in New Issue
Block a user