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
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:
@@ -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)}")
|
||||
|
||||
BIN
tmp/test.xlsx
BIN
tmp/test.xlsx
Binary file not shown.
Reference in New Issue
Block a user