feat: refactor to FastAPI architecture v2.0
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
293
app/static/css/app.css
Normal file
293
app/static/css/app.css
Normal file
@@ -0,0 +1,293 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 2px solid #3498db;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 24px;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom: none;
|
||||
background: #ecf0f1;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #fff;
|
||||
border-color: #3498db;
|
||||
border-bottom: 2px solid #fff;
|
||||
margin-bottom: -2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab:hover:not(.active) {
|
||||
background: #d5dbdb;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Types */
|
||||
.type-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.type-row input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.type-row input[type="color"] {
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.type-code {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* Schedule table */
|
||||
#scheduleTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#scheduleTable th,
|
||||
#scheduleTable td {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#scheduleTable th {
|
||||
background: #ecf0f1;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#scheduleTable input {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#scheduleTable input[type="date"] {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
#scheduleTable input[type="time"] {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
/* Editor area */
|
||||
.editor-area {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.editor-area h2 {
|
||||
margin-bottom: 10px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.editor-area h3 {
|
||||
margin: 15px 0 8px;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
/* Imported type editor */
|
||||
.imported-type-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.imported-type-row input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.imported-type-row input[type="color"] {
|
||||
width: 36px;
|
||||
height: 28px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Block list */
|
||||
.block-item {
|
||||
background: #f9f9f9;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #3498db;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Status message */
|
||||
.status-message {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
background: #d5f5e3;
|
||||
color: #27ae60;
|
||||
border: 1px solid #27ae60;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
background: #fadbd8;
|
||||
color: #e74c3c;
|
||||
border: 1px solid #e74c3c;
|
||||
}
|
||||
|
||||
/* JSON import */
|
||||
.json-import {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 20px 0 10px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
134
app/static/index.html
Normal file
134
app/static/index.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Scenar Creator</title>
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Scenar Creator</h1>
|
||||
<p class="subtitle">Tvorba časových harmonogramů</p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab(event, 'importTab')">Importovat Excel</button>
|
||||
<button class="tab" onclick="switchTab(event, 'builderTab')">Vytvořit inline</button>
|
||||
</div>
|
||||
|
||||
<!-- Import Excel Tab -->
|
||||
<div id="importTab" class="tab-content active">
|
||||
<form id="importForm" onsubmit="return handleImport(event)">
|
||||
<div class="form-group">
|
||||
<label for="importTitle">Název akce:</label>
|
||||
<input type="text" id="importTitle" name="title" maxlength="200" required placeholder="Název události">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="importDetail">Detail:</label>
|
||||
<input type="text" id="importDetail" name="detail" maxlength="500" required placeholder="Popis události">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="excelFile">Excel soubor:</label>
|
||||
<input type="file" id="excelFile" name="file" accept=".xlsx,.xls" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Importovat</button>
|
||||
<a href="/api/template" class="btn btn-secondary">Stáhnout šablonu</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Builder Tab -->
|
||||
<div id="builderTab" class="tab-content">
|
||||
<form id="builderForm" onsubmit="return handleBuild(event)">
|
||||
<div class="form-group">
|
||||
<label for="builderTitle">Název akce:</label>
|
||||
<input type="text" id="builderTitle" name="title" maxlength="200" required placeholder="Název události">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="builderDetail">Detail:</label>
|
||||
<input type="text" id="builderDetail" name="detail" maxlength="500" required placeholder="Popis události">
|
||||
</div>
|
||||
|
||||
<h3>Typy programů</h3>
|
||||
<div id="typesContainer">
|
||||
<div class="type-row" data-index="0">
|
||||
<input type="text" name="type_name_0" placeholder="Kód typu (např. WORKSHOP)" class="type-code">
|
||||
<input type="text" name="type_desc_0" placeholder="Popis">
|
||||
<input type="color" name="type_color_0" value="#0070C0">
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeTypeRow(0)">X</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="addTypeRow()">+ Přidat typ</button>
|
||||
|
||||
<h3>Časový harmonogram</h3>
|
||||
<datalist id="availableTypes"></datalist>
|
||||
<div id="scheduleContainer">
|
||||
<table id="scheduleTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Začátek</th>
|
||||
<th>Konec</th>
|
||||
<th>Program</th>
|
||||
<th>Typ</th>
|
||||
<th>Garant</th>
|
||||
<th>Poznámka</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scheduleBody">
|
||||
<tr data-index="0">
|
||||
<td><input type="date" name="datum_0" required></td>
|
||||
<td><input type="time" name="zacatek_0" required></td>
|
||||
<td><input type="time" name="konec_0" required></td>
|
||||
<td><input type="text" name="program_0" required placeholder="Název bloku"></td>
|
||||
<td><input type="text" name="typ_0" list="availableTypes" required placeholder="Typ"></td>
|
||||
<td><input type="text" name="garant_0" placeholder="Garant"></td>
|
||||
<td><input type="text" name="poznamka_0" placeholder="Poznámka"></td>
|
||||
<td><button type="button" class="btn btn-danger btn-sm" onclick="removeScheduleRow(0)">X</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="addScheduleRow()">+ Přidat řádek</button>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" name="format" value="excel">Stáhnout Excel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="handleBuildPdf()">Stáhnout PDF</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Import results / editor area -->
|
||||
<div id="editorArea" class="editor-area" style="display:none;">
|
||||
<h2>Importovaná data</h2>
|
||||
<div id="importedInfo"></div>
|
||||
|
||||
<h3>Typy programů</h3>
|
||||
<div id="importedTypesContainer"></div>
|
||||
|
||||
<h3>Bloky</h3>
|
||||
<div id="importedBlocksContainer"></div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" onclick="generateExcelFromImport()">Stáhnout Excel</button>
|
||||
<button class="btn btn-primary" onclick="generatePdfFromImport()">Stáhnout PDF</button>
|
||||
<button class="btn btn-secondary" onclick="exportJson()">Exportovat JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JSON import -->
|
||||
<div class="json-import">
|
||||
<label>Importovat JSON: <input type="file" id="jsonFile" accept=".json" onchange="handleJsonImport(event)"></label>
|
||||
</div>
|
||||
|
||||
<div id="statusMessage" class="status-message" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script src="/static/js/export.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
61
app/static/js/api.js
Normal file
61
app/static/js/api.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* API fetch wrapper for Scenar Creator.
|
||||
*/
|
||||
|
||||
const API = {
|
||||
async post(url, body, isJson = true) {
|
||||
const opts = { method: 'POST' };
|
||||
if (isJson) {
|
||||
opts.headers = { 'Content-Type': 'application/json' };
|
||||
opts.body = JSON.stringify(body);
|
||||
} else {
|
||||
opts.body = body; // FormData
|
||||
}
|
||||
const res = await fetch(url, opts);
|
||||
return res;
|
||||
},
|
||||
|
||||
async postJson(url, body) {
|
||||
const res = await this.post(url, body, true);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(err.detail || 'API error');
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async postBlob(url, body) {
|
||||
const res = await this.post(url, body, true);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(err.detail || 'API error');
|
||||
}
|
||||
return res.blob();
|
||||
},
|
||||
|
||||
async postFormData(url, formData) {
|
||||
const res = await this.post(url, formData, false);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(err.detail || 'API error');
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async get(url) {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
258
app/static/js/app.js
Normal file
258
app/static/js/app.js
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Main application logic for Scenar Creator SPA.
|
||||
*/
|
||||
|
||||
window.currentDocument = null;
|
||||
let typeCounter = 1;
|
||||
let scheduleCounter = 1;
|
||||
|
||||
/* --- Tab switching --- */
|
||||
function switchTab(event, tabId) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
}
|
||||
|
||||
/* --- Status messages --- */
|
||||
function showStatus(message, type) {
|
||||
const el = document.getElementById('statusMessage');
|
||||
el.textContent = message;
|
||||
el.className = 'status-message ' + type;
|
||||
el.style.display = 'block';
|
||||
setTimeout(() => { el.style.display = 'none'; }, 5000);
|
||||
}
|
||||
|
||||
/* --- Type management --- */
|
||||
function addTypeRow() {
|
||||
const container = document.getElementById('typesContainer');
|
||||
const idx = typeCounter++;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'type-row';
|
||||
div.setAttribute('data-index', idx);
|
||||
div.innerHTML = `
|
||||
<input type="text" name="type_name_${idx}" placeholder="Kód typu" class="type-code">
|
||||
<input type="text" name="type_desc_${idx}" placeholder="Popis">
|
||||
<input type="color" name="type_color_${idx}" value="#0070C0">
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeTypeRow(${idx})">X</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
updateTypeDatalist();
|
||||
}
|
||||
|
||||
function removeTypeRow(idx) {
|
||||
const row = document.querySelector(`.type-row[data-index="${idx}"]`);
|
||||
if (row) row.remove();
|
||||
updateTypeDatalist();
|
||||
}
|
||||
|
||||
function updateTypeDatalist() {
|
||||
const datalist = document.getElementById('availableTypes');
|
||||
datalist.innerHTML = '';
|
||||
document.querySelectorAll('#typesContainer .type-row').forEach(row => {
|
||||
const nameInput = row.querySelector('input[name^="type_name_"]');
|
||||
if (nameInput && nameInput.value.trim()) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = nameInput.value.trim();
|
||||
datalist.appendChild(opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update datalist on type name changes
|
||||
document.getElementById('typesContainer').addEventListener('input', function (e) {
|
||||
if (e.target.name && e.target.name.startsWith('type_name_')) {
|
||||
updateTypeDatalist();
|
||||
}
|
||||
});
|
||||
|
||||
/* --- Schedule management --- */
|
||||
function addScheduleRow() {
|
||||
const tbody = document.getElementById('scheduleBody');
|
||||
const idx = scheduleCounter++;
|
||||
const tr = document.createElement('tr');
|
||||
tr.setAttribute('data-index', idx);
|
||||
tr.innerHTML = `
|
||||
<td><input type="date" name="datum_${idx}" required></td>
|
||||
<td><input type="time" name="zacatek_${idx}" required></td>
|
||||
<td><input type="time" name="konec_${idx}" required></td>
|
||||
<td><input type="text" name="program_${idx}" required placeholder="Název bloku"></td>
|
||||
<td><input type="text" name="typ_${idx}" list="availableTypes" required placeholder="Typ"></td>
|
||||
<td><input type="text" name="garant_${idx}" placeholder="Garant"></td>
|
||||
<td><input type="text" name="poznamka_${idx}" placeholder="Poznámka"></td>
|
||||
<td><button type="button" class="btn btn-danger btn-sm" onclick="removeScheduleRow(${idx})">X</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
function removeScheduleRow(idx) {
|
||||
const row = document.querySelector(`#scheduleBody tr[data-index="${idx}"]`);
|
||||
if (row) row.remove();
|
||||
}
|
||||
|
||||
/* --- Build ScenarioDocument from builder form --- */
|
||||
function buildDocumentFromForm() {
|
||||
const title = document.getElementById('builderTitle').value.trim();
|
||||
const detail = document.getElementById('builderDetail').value.trim();
|
||||
|
||||
if (!title || !detail) {
|
||||
throw new Error('Název akce a detail jsou povinné');
|
||||
}
|
||||
|
||||
// Collect types
|
||||
const programTypes = [];
|
||||
document.querySelectorAll('#typesContainer .type-row').forEach(row => {
|
||||
const code = row.querySelector('input[name^="type_name_"]').value.trim();
|
||||
const desc = row.querySelector('input[name^="type_desc_"]').value.trim();
|
||||
const color = row.querySelector('input[name^="type_color_"]').value;
|
||||
if (code) {
|
||||
programTypes.push({ code, description: desc, color });
|
||||
}
|
||||
});
|
||||
|
||||
// Collect blocks
|
||||
const blocks = [];
|
||||
document.querySelectorAll('#scheduleBody tr').forEach(tr => {
|
||||
const inputs = tr.querySelectorAll('input');
|
||||
const datum = inputs[0].value;
|
||||
const zacatek = inputs[1].value;
|
||||
const konec = inputs[2].value;
|
||||
const program = inputs[3].value.trim();
|
||||
const typ = inputs[4].value.trim();
|
||||
const garant = inputs[5].value.trim() || null;
|
||||
const poznamka = inputs[6].value.trim() || null;
|
||||
|
||||
if (datum && zacatek && konec && program && typ) {
|
||||
blocks.push({ datum, zacatek, konec, program, typ, garant, poznamka });
|
||||
}
|
||||
});
|
||||
|
||||
if (blocks.length === 0) {
|
||||
throw new Error('Přidejte alespoň jeden blok');
|
||||
}
|
||||
|
||||
return {
|
||||
version: "1.0",
|
||||
event: { title, detail },
|
||||
program_types: programTypes,
|
||||
blocks
|
||||
};
|
||||
}
|
||||
|
||||
/* --- Handle builder form submit (Excel) --- */
|
||||
async function handleBuild(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const doc = buildDocumentFromForm();
|
||||
const blob = await API.postBlob('/api/generate-excel', doc);
|
||||
API.downloadBlob(blob, 'scenar_timetable.xlsx');
|
||||
showStatus('Excel vygenerován', 'success');
|
||||
} catch (err) {
|
||||
showStatus('Chyba: ' + err.message, 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* --- Handle builder PDF --- */
|
||||
async function handleBuildPdf() {
|
||||
try {
|
||||
const doc = buildDocumentFromForm();
|
||||
const blob = await API.postBlob('/api/generate-pdf', doc);
|
||||
API.downloadBlob(blob, 'scenar_timetable.pdf');
|
||||
showStatus('PDF vygenerován', 'success');
|
||||
} catch (err) {
|
||||
showStatus('Chyba: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Handle Excel import --- */
|
||||
async function handleImport(event) {
|
||||
event.preventDefault();
|
||||
const form = document.getElementById('importForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const result = await API.postFormData('/api/import-excel', formData);
|
||||
if (result.success && result.document) {
|
||||
window.currentDocument = result.document;
|
||||
showImportedDocument(result.document);
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
showStatus('Import OK, warnings: ' + result.warnings.join('; '), 'success');
|
||||
} else {
|
||||
showStatus('Excel importován', 'success');
|
||||
}
|
||||
} else {
|
||||
showStatus('Import failed: ' + (result.errors || []).join('; '), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showStatus('Chyba importu: ' + err.message, 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* --- Show imported document in editor --- */
|
||||
function showImportedDocument(doc) {
|
||||
const area = document.getElementById('editorArea');
|
||||
area.style.display = 'block';
|
||||
|
||||
// Info
|
||||
document.getElementById('importedInfo').innerHTML =
|
||||
`<strong>${doc.event.title}</strong> — ${doc.event.detail}`;
|
||||
|
||||
// Types
|
||||
const typesHtml = doc.program_types.map((pt, i) => `
|
||||
<div class="imported-type-row">
|
||||
<input type="text" value="${pt.code}" data-field="code" data-idx="${i}">
|
||||
<input type="text" value="${pt.description}" data-field="description" data-idx="${i}">
|
||||
<input type="color" value="${pt.color}" data-field="color" data-idx="${i}">
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('importedTypesContainer').innerHTML = typesHtml;
|
||||
|
||||
// Blocks
|
||||
const blocksHtml = doc.blocks.map(b =>
|
||||
`<div class="block-item">${b.datum} ${b.zacatek}–${b.konec} | <strong>${b.program}</strong> [${b.typ}] ${b.garant || ''}</div>`
|
||||
).join('');
|
||||
document.getElementById('importedBlocksContainer').innerHTML = blocksHtml;
|
||||
}
|
||||
|
||||
/* --- Get current document (with any edits from import editor) --- */
|
||||
function getCurrentDocument() {
|
||||
if (!window.currentDocument) {
|
||||
throw new Error('No document loaded');
|
||||
}
|
||||
// Update types from editor
|
||||
const typeRows = document.querySelectorAll('#importedTypesContainer .imported-type-row');
|
||||
if (typeRows.length > 0) {
|
||||
window.currentDocument.program_types = Array.from(typeRows).map(row => ({
|
||||
code: row.querySelector('[data-field="code"]').value.trim(),
|
||||
description: row.querySelector('[data-field="description"]').value.trim(),
|
||||
color: row.querySelector('[data-field="color"]').value,
|
||||
}));
|
||||
}
|
||||
return window.currentDocument;
|
||||
}
|
||||
|
||||
/* --- Generate Excel from imported data --- */
|
||||
async function generateExcelFromImport() {
|
||||
try {
|
||||
const doc = getCurrentDocument();
|
||||
const blob = await API.postBlob('/api/generate-excel', doc);
|
||||
API.downloadBlob(blob, 'scenar_timetable.xlsx');
|
||||
showStatus('Excel vygenerován', 'success');
|
||||
} catch (err) {
|
||||
showStatus('Chyba: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Generate PDF from imported data --- */
|
||||
async function generatePdfFromImport() {
|
||||
try {
|
||||
const doc = getCurrentDocument();
|
||||
const blob = await API.postBlob('/api/generate-pdf', doc);
|
||||
API.downloadBlob(blob, 'scenar_timetable.pdf');
|
||||
showStatus('PDF vygenerován', 'success');
|
||||
} catch (err) {
|
||||
showStatus('Chyba: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
34
app/static/js/export.js
Normal file
34
app/static/js/export.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* JSON import/export for Scenar Creator.
|
||||
*/
|
||||
|
||||
function exportJson() {
|
||||
if (!window.currentDocument) {
|
||||
showStatus('No document to export', 'error');
|
||||
return;
|
||||
}
|
||||
const json = JSON.stringify(window.currentDocument, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
API.downloadBlob(blob, 'scenar_export.json');
|
||||
}
|
||||
|
||||
function handleJsonImport(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
try {
|
||||
const doc = JSON.parse(e.target.result);
|
||||
if (!doc.event || !doc.blocks || !doc.program_types) {
|
||||
throw new Error('Invalid ScenarioDocument format');
|
||||
}
|
||||
window.currentDocument = doc;
|
||||
showImportedDocument(doc);
|
||||
showStatus('JSON imported successfully', 'success');
|
||||
} catch (err) {
|
||||
showStatus('JSON import error: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
Reference in New Issue
Block a user