Refactor: Excel import jde přímo do inline editoru s výběrem barev
Some checks failed
Build & Push Docker / build (push) Has been cancelled

Problém:
- V kroku 2 uživatel nastavoval barvy, ale ty se neaplikovaly v kroku 3
- Zbytečný mezikrok kde uživatel musel klikat 'Generovat' nebo 'Upravit'

Řešení:
- Step 2 nyní přímo zobrazuje inline editor s načtenými daty z Excelu
- Uživatel může upravit řádky, typy a nastavit barvy přímo v jednom kroku
- Odstraněn step 2b (duplikát) a step 3 (již nepotřebný)
- Zjednodušen workflow: Import Excel -> Inline editor -> Generovat

Změny:
- cgi-bin/scenar.py: Step 2 nyní renderuje inline editor přímo
- Odstraněny nepotřebné kroky 2b a 3
- Barvy se nyní nastavují přímo v inline editoru u typů programu

Testy: 18/18 unit testů prošlo 
This commit is contained in:
Martin Sukany
2025-11-13 16:24:33 +01:00
parent 2f4c930739
commit 87f1fc2c7a
2 changed files with 38 additions and 179 deletions

View File

@@ -511,104 +511,7 @@ elif step == '2' and file_item is not None and file_item.filename:
if data.empty:
render_error("Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.")
else:
# Extract program types
program_types = sorted([str(t).strip() for t in data["Typ"].dropna().unique()])
file_content_base64 = base64.b64encode(file_content).decode('utf-8')
print('''<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Scenar Creator - Typy programu</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f0f4f8; color: #333; }
.form-container { max-width: 600px; margin: auto; padding: 20px; background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.1); border-radius: 8px; }
.form-group { margin-bottom: 15px; }
label { display: block; font-weight: bold; margin-bottom: 5px; }
input[type="text"], input[type="color"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
input[type="text"] { width: 100%; }
input[type="color"] { width: 60px; }
button { padding: 10px 20px; background: #007BFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="form-container">
<h1>Typy programu</h1>
<p>Vyplň popis a barvu pro každý typ programu:</p>
<form action="/cgi-bin/scenar.py" method="post">
<input type="hidden" name="title" value="''' + html.escape(title) + '''">
<input type="hidden" name="detail" value="''' + html.escape(detail) + '''">
<input type="hidden" name="file_content_base64" value="''' + html.escape(file_content_base64) + '''">
<input type="hidden" name="step" value="3">
''')
for i, typ in enumerate(program_types, start=0):
print(f''' <div class="form-group">
<label>Typ: {html.escape(typ)}</label>
<input type="text" name="desc_{i}" placeholder="Popis" required value="{html.escape(typ)}">
<input type="color" name="color_{i}" value="#3498db" required>
<input type="hidden" name="type_code_{i}" value="{html.escape(typ)}">
</div>''')
print(''' <div style="display: flex; gap: 10px;">
<button type="submit" formaction="/cgi-bin/scenar.py" formmethod="post">Generovat scénář »</button>
<button type="button" onclick="goToInlineEditor()">Upravit v inline editoru »</button>
</div>
<script>
function goToInlineEditor() {
const form = document.querySelector('form');
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'step';
input.value = '2b';
form.appendChild(input);
// Change form's step value
document.querySelector('input[name="step"]').value = '2b';
form.submit();
}
</script>
</form>
</div>
</body>
</html>''')
except ValidationError as e:
render_error(f"Chyba validace: {str(e)}")
except (TemplateError, ScenarsError) as e:
render_error(f"Chyba: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
render_error(f"Neočekávaná chyba: {str(e)}")
elif step == '2b':
"""Load Excel data into inline editor for editing."""
try:
# Get file content from either file upload or base64
file_content = None
if file_item is not None and file_item.filename:
file_content = file_item.file.read()
else:
file_content_base64 = form.getvalue('file_content_base64', '')
if file_content_base64:
file_content = base64.b64decode(file_content_base64)
if not file_content:
render_error("Chyba: Soubor nebyl nalezen.")
else:
file_size = len(file_content)
# Validate inputs
validate_inputs(title, detail, file_size)
# Read Excel
data, error_rows = read_excel(file_content, show_debug)
if data.empty:
render_error("Načtená data jsou prázdná nebo obsahují neplatné hodnoty. Zkontrolujte vstupní soubor.")
else:
# Instead of showing type selection form, go directly to inline editor (step 2b)
# Extract program types and prepare for inline editor
program_types = sorted([str(t).strip() for t in data["Typ"].dropna().unique()])
@@ -632,9 +535,12 @@ textarea { resize: vertical; min-height: 80px; }
button { padding: 10px 20px; background: #007BFF; color: white; border: none;
border-radius: 4px; cursor: pointer; font-size: 14px; }
button:hover { background: #0056b3; }
button.danger { background: #dc3545; }
button.danger:hover { background: #c82333; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f0f0f0; }
.type-def { margin-bottom: 15px; padding: 10px; background: #f9f9f9; border-left: 3px solid #007BFF; }
</style>
</head>
<body>
@@ -701,7 +607,7 @@ th { background: #f0f0f0; }
<td><input type="text" name="garant_{row_counter}" value="{html.escape(garant)}" style="width: 100%; padding: 4px; border: 1px solid #ccc;"></td>
<td><input type="text" name="poznamka_{row_counter}" value="{html.escape(poznamka)}" style="width: 100%; padding: 4px; border: 1px solid #ccc;"></td>
<td style="text-align: center;">
<button type="button" onclick="removeScheduleRow({row_counter})" style="background: #dc3545; color: white; padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer;">Smazat</button>
<button type="button" onclick="removeScheduleRow({row_counter})" class="danger" style="padding: 4px 8px;">Smazat</button>
</td>
</tr>
''')
@@ -714,24 +620,29 @@ th { background: #f0f0f0; }
</div>
<div class="card">
<h2>Typy programu</h2>
<h2>Typy programu (nastavení barev a popisů)</h2>
<div id="typesContainer">
''')
# Load type definitions
type_counter = 0
for type_name in program_types:
print(f''' <div class="type-def" data-type-id="{type_counter}" style="margin-bottom: 15px; padding: 10px; background: #f9f9f9; border-left: 3px solid #007BFF;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Typ: <input type="text" name="type_name_{type_counter}" value="{html.escape(type_name)}" style="width: 200px; padding: 4px; border: 1px solid #ccc; margin-left: 5px;"></label>
<label style="display: block; margin-bottom: 5px;">Popis: <input type="text" name="type_desc_{type_counter}" placeholder="Popis" style="width: 100%; padding: 4px; border: 1px solid #ccc; margin-top: 3px;"></label>
<label style="display: block;">Barva: <input type="color" name="type_color_{type_counter}" value="#3498db" style="margin-left: 5px; width: 60px;"></label>
<button type="button" onclick="removeTypeRow({type_counter})" style="margin-top: 8px; background: #dc3545; color: white; padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer;">Smazat</button>
print(f''' <div class="type-def" data-type-id="{type_counter}">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">
Typ: <input type="text" name="type_name_{type_counter}" value="{html.escape(type_name)}" readonly style="width: 200px; padding: 4px; border: 1px solid #ccc; margin-left: 5px; background: #f0f0f0;">
</label>
<label style="display: block; margin-bottom: 5px;">
Popis: <input type="text" name="type_desc_{type_counter}" placeholder="Popis typu" value="{html.escape(type_name)}" style="width: 100%; padding: 4px; border: 1px solid #ccc; margin-top: 3px;">
</label>
<label style="display: block;">
Barva: <input type="color" name="type_color_{type_counter}" value="#3498db" style="margin-left: 5px; width: 60px;">
</label>
</div>
''')
type_counter += 1
print(''' </div>
<button type="button" onclick="addTypeRow()">+ Přidat typ</button>
<button type="button" onclick="addTypeRow()">+ Přidat nový typ</button>
</div>
<div class="card" style="text-align: center;">
@@ -744,6 +655,8 @@ th { background: #f0f0f0; }
<script>
let scheduleRowCounter = ''' + str(row_counter) + ''';
let typeRowCounter = ''' + str(type_counter) + ''';
const availableTypes = ''' + str(program_types) + '''.replace(/'/g, '"');
const typesArray = JSON.parse(availableTypes);
function addScheduleRow() {
const table = document.getElementById('importedScheduleTable');
@@ -751,6 +664,12 @@ th { background: #f0f0f0; }
const newRow = document.createElement('tr');
newRow.className = 'schedule-row';
newRow.dataset.rowId = rowId;
let typeOptions = '<option value="">-- Zvolte typ --</option>';
typesArray.forEach(type => {
typeOptions += `<option value="${type}">${type}</option>`;
});
newRow.innerHTML = `
<td><input type="date" name="datum_${rowId}" style="width: 100%; padding: 4px; border: 1px solid #ccc;"></td>
<td><input type="time" name="zacatek_${rowId}" style="width: 100%; padding: 4px; border: 1px solid #ccc;"></td>
@@ -758,18 +677,13 @@ th { background: #f0f0f0; }
<td><input type="text" name="program_${rowId}" style="width: 100%; padding: 4px; border: 1px solid #ccc;"></td>
<td>
<select name="typ_${rowId}" class="typ-select" style="width: 100%; padding: 4px; border: 1px solid #ccc;">
<option value="">-- Zvolte typ --</option>
''')
for type_option in program_types:
print(f' <option value="{html.escape(type_option)}">{html.escape(type_option)}</option>')
print(''' </select>
${typeOptions}
</select>
</td>
<td><input type="text" name="garant_${rowId}" style="width: 100%; padding: 4px; border: 1px solid #ccc;"></td>
<td><input type="text" name="poznamka_${rowId}" style="width: 100%; padding: 4px; border: 1px solid #ccc;"></td>
<td style="text-align: center;">
<button type="button" onclick="removeScheduleRow(${rowId})" style="background: #dc3545; color: white; padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer;">Smazat</button>
<button type="button" onclick="removeScheduleRow(${rowId})" class="danger" style="padding: 4px 8px;">Smazat</button>
</td>
`;
table.appendChild(newRow);
@@ -786,15 +700,17 @@ th { background: #f0f0f0; }
const newTypeDiv = document.createElement('div');
newTypeDiv.className = 'type-def';
newTypeDiv.dataset.typeId = typeId;
newTypeDiv.style.marginBottom = '15px';
newTypeDiv.style.padding = '10px';
newTypeDiv.style.background = '#f9f9f9';
newTypeDiv.style.borderLeft = '3px solid #007BFF';
newTypeDiv.innerHTML = `
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Typ: <input type="text" name="type_name_${typeId}" style="width: 200px; padding: 4px; border: 1px solid #ccc; margin-left: 5px;"></label>
<label style="display: block; margin-bottom: 5px;">Popis: <input type="text" name="type_desc_${typeId}" placeholder="Popis" style="width: 100%; padding: 4px; border: 1px solid #ccc; margin-top: 3px;"></label>
<label style="display: block;">Barva: <input type="color" name="type_color_${typeId}" value="#3498db" style="margin-left: 5px; width: 60px;"></label>
<button type="button" onclick="removeTypeRow(${typeId})" style="margin-top: 8px; background: #dc3545; color: white; padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer;">Smazat</button>
<label style="display: block; font-weight: bold; margin-bottom: 5px;">
Typ: <input type="text" name="type_name_${typeId}" placeholder="Název typu" style="width: 200px; padding: 4px; border: 1px solid #ccc; margin-left: 5px;">
</label>
<label style="display: block; margin-bottom: 5px;">
Popis: <input type="text" name="type_desc_${typeId}" placeholder="Popis typu" style="width: 100%; padding: 4px; border: 1px solid #ccc; margin-top: 3px;">
</label>
<label style="display: block;">
Barva: <input type="color" name="type_color_${typeId}" value="#3498db" style="margin-left: 5px; width: 60px;">
</label>
<button type="button" onclick="removeTypeRow(${typeId})" class="danger" style="margin-top: 8px; padding: 4px 8px;">Smazat typ</button>
`;
container.appendChild(newTypeDiv);
}
@@ -811,63 +727,6 @@ th { background: #f0f0f0; }
render_error(f"Chyba validace: {str(e)}")
except (TemplateError, ScenarsError) as e:
render_error(f"Chyba: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in 2b: {str(e)}")
render_error(f"Neočekávaná chyba: {str(e)}")
elif step == '3' and title and detail:
try:
file_content_base64 = form.getvalue('file_content_base64', '')
if not file_content_base64:
render_error("Chyba: Soubor nebyl nalezen.")
else:
file_content = base64.b64decode(file_content_base64)
data, error_rows = read_excel(file_content, show_debug)
if data.empty:
render_error("Načtená data jsou prázdná.")
else:
program_descriptions, program_colors = get_program_types(form)
wb = create_timetable(data, title, detail, program_descriptions, program_colors)
os.makedirs(TMP_DIR, exist_ok=True)
filename = f"{title}.xlsx"
safe_name = "".join(ch if ch.isalnum() or ch in "._- " else "_" for ch in filename)
file_path = os.path.join(TMP_DIR, safe_name)
wb.save(file_path)
print(f'''<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Scenar Creator - Výsledek</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
.container {{ max-width: 800px; margin: 0 auto; }}
.card {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
.success {{ color: #28a745; font-weight: bold; }}
.download-btn {{ display: inline-block; margin-top: 10px; padding: 10px 20px; background: #28a745;
color: white; text-decoration: none; border-radius: 4px; }}
.download-btn:hover {{ background: #218838; }}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>✅ Scénář úspěšně vygenerován!</h1>
<p class="success">{html.escape(title)}</p>
<p>{html.escape(detail)}</p>
<a href="/tmp/{html.escape(safe_name)}" download class="download-btn">📥 Stáhnout scénář</a>
<p style="margin-top: 20px; font-size: 12px; color: #666;">
<a href="/cgi-bin/scenar.py">← Zpět na úvodní formulář</a>
</p>
</div>
</div>
</body>
</html>''')
except ScenarsError as e:
render_error(f"Chyba: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
render_error(f"Neočekávaná chyba: {str(e)}")

Binary file not shown.