Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4461644cb6 | |||
| 73db5c5fe0 | |||
| d609775eb6 | |||
| c1a6a145dc | |||
| 1091854797 | |||
| a5954ce8cb | |||
| 06318d0d2d | |||
| 5ed3c906dd | |||
| 8ce74d3e62 | |||
| ee25c46ff3 | |||
| f7fe2fcc11 | |||
| 4449007a15 | |||
| 6770d42f99 | |||
| 333f9fc89a | |||
| a286236834 | |||
| 84cc8de833 |
+10
-64
@@ -228,7 +228,6 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab
|
||||
client.close()
|
||||
|
||||
|
||||
# Admin route to trigger rollover manually
|
||||
@app.route('/admin/trigger_school_year_rollover', methods=['POST'])
|
||||
def admin_trigger_school_year_rollover():
|
||||
if 'username' not in session:
|
||||
@@ -3038,42 +3037,6 @@ def optimized_image(filename):
|
||||
app.logger.error(f"Error serving optimized image {filename}: {str(e)}")
|
||||
return Response("Optimized image not found", status=404)
|
||||
|
||||
|
||||
# @app.route('/QRCodes/<filename>')
|
||||
# def qrcode_file(filename):
|
||||
# """
|
||||
# Serve QR code files from the QRCodes directory.
|
||||
#
|
||||
# Args:
|
||||
# filename (str): Name of the QR code file to serve
|
||||
#
|
||||
# Returns:
|
||||
# flask.Response: The requested QR code file or placeholder image if not found
|
||||
# """
|
||||
# try:
|
||||
# # Check production path first
|
||||
# prod_path = "/var/Inventarsystem/Web/QRCodes"
|
||||
# dev_path = app.config['QR_CODE_FOLDER']
|
||||
# if os.path.exists(os.path.join(prod_path, filename)):
|
||||
# return send_from_directory(prod_path, filename)
|
||||
# if os.path.exists(os.path.join(dev_path, filename)):
|
||||
# return send_from_directory(dev_path, filename)
|
||||
#
|
||||
# # Use a placeholder image if file not found - first try SVG, then PNG
|
||||
# svg_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.svg')
|
||||
# png_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
|
||||
#
|
||||
# if os.path.exists(svg_placeholder_path):
|
||||
# return send_from_directory(app.static_folder, 'img/no-image.svg')
|
||||
# elif os.path.exists(png_placeholder_path):
|
||||
# return send_from_directory(app.static_folder, 'img/no-image.png')
|
||||
# else:
|
||||
# return send_from_directory(app.static_folder, 'favicon.ico')
|
||||
# except Exception as e:
|
||||
# print(f"Error serving QR code {filename}: {str(e)}")
|
||||
# return Response("QR code not found", status=404)
|
||||
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def catch_all_files(filename):
|
||||
"""
|
||||
@@ -3136,32 +3099,6 @@ def test_connection():
|
||||
"""
|
||||
return {'status': 'success', 'message': 'Connection successful', 'status_code': 200}
|
||||
|
||||
""" if sucess in deployment the funktion can be removed
|
||||
@app.route('/user_status')
|
||||
def user_status():
|
||||
|
||||
API endpoint to get the current user's status (username, admin status).
|
||||
Used by JavaScript in templates to personalize the UI.
|
||||
|
||||
Returns:
|
||||
JSON: User status information or error if not authenticated
|
||||
|
||||
if 'username' in session:
|
||||
is_admin = us.check_admin(session['username'])
|
||||
return jsonify({
|
||||
'authenticated': True,
|
||||
'username': session['username'],
|
||||
'is_admin': is_admin
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'authenticated': False,
|
||||
'error': 'Not logged in'
|
||||
}), 401
|
||||
"""
|
||||
|
||||
|
||||
##################################################### changes to be made to account for the new account permison managment system ##############################
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
@@ -3203,6 +3140,8 @@ def home_admin():
|
||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||
return redirect(url_for('library_view'))
|
||||
|
||||
filter_names = it.get_filter_names()
|
||||
|
||||
return render_template(
|
||||
'main_admin.html',
|
||||
username=session['username'],
|
||||
@@ -3212,6 +3151,7 @@ def home_admin():
|
||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
||||
school_info=_get_school_info_for_export(),
|
||||
filter_names=filter_names,
|
||||
open_item=request.args.get('open_item')
|
||||
)
|
||||
|
||||
@@ -4707,12 +4647,15 @@ def upload_admin():
|
||||
if not _action_access_allowed(permissions, 'can_insert'):
|
||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
filter_names = it.get_filter_names()
|
||||
|
||||
return render_template(
|
||||
'upload_admin.html',
|
||||
username=session['username'],
|
||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||
filter_names=filter_names,
|
||||
show_library_features=False,
|
||||
upload_mode='item',
|
||||
page_title='Artikel hochladen',
|
||||
@@ -7005,12 +6948,15 @@ def item_edit(id):
|
||||
|
||||
current_item['IndividualCodes'] = '\n'.join(individual_codes)
|
||||
|
||||
filter_names = it.get_filter_names()
|
||||
|
||||
return render_template(
|
||||
'edit_library.html',
|
||||
username=session['username'],
|
||||
item=current_item,
|
||||
show_library_features=show_library_features,
|
||||
library_module_enabled=library_module_active,
|
||||
filter_names=filter_names,
|
||||
page_title=f"Bearbeiten: {current_item.get('Name', '')}"
|
||||
)
|
||||
|
||||
|
||||
@@ -911,9 +911,9 @@ def get_filter_names():
|
||||
if names_doc and 'names' in names_doc:
|
||||
return names_doc['names']
|
||||
return {
|
||||
'1': 'Fach/Kategorie',
|
||||
'2': 'System/Bereich',
|
||||
'3': 'Typ/Art'
|
||||
'1': 'Jahrgangsstufe',
|
||||
'2': 'Fachgebiet',
|
||||
'3': 'Schlagwort'
|
||||
}
|
||||
|
||||
def set_filter_name(filter_num, name):
|
||||
|
||||
@@ -78,15 +78,15 @@
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="filter_name_1">Name Filter 1</label>
|
||||
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Fach/Kategorie') }}" placeholder="z. B. Fach/Kategorie">
|
||||
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Klassenstufe') }}" placeholder="z. B. Klassenstufe">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter_name_2">Name Filter 2</label>
|
||||
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'System/Bereich') }}" placeholder="z. B. System/Bereich">
|
||||
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'Fach') }}" placeholder="z. B. Fach">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter_name_3">Name Filter 3</label>
|
||||
<input type="text" id="filter_name_3" name="filter_name_3" value="{{ filter_names.get('3', 'Typ/Art') }}" placeholder="z. B. Typ/Art">
|
||||
<input type="text" id="filter_name_3" name="filter_name_3" value="{{ filter_names.get('3', 'Schlagwort') }}" placeholder="z. B. Schlagwort">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -120,9 +120,64 @@
|
||||
<p>Diese Angaben erscheinen künftig im amtlichen Audit-PDF und in anderen Behördenberichten.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone: Schuljahreswechsel -->
|
||||
<div class="card danger-zone" style="grid-column: 1 / -1; border-color: #fca5a5;">
|
||||
<div class="card-header" style="background: #fef2f2; border-bottom: 1px solid #fca5a5;">
|
||||
<h2 style="color: #991b1b;">Erweiterte Aktionen</h2>
|
||||
</div>
|
||||
<div class="card-body" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
|
||||
<div>
|
||||
<strong style="color: #7f1d1d; display: block; font-size: 1.05rem; margin-bottom: 4px;">Schuljahreswechsel durchführen</strong>
|
||||
<span style="color: #991b1b; font-size: 0.9rem;">Diese Aktion bereitet das System auf das neue Schuljahr vor (z. B. Hochstufen von Klassen).</span>
|
||||
</div>
|
||||
<button type="button"
|
||||
id="btn-rollover"
|
||||
class="btn btn-danger"
|
||||
onclick="triggerSchoolYearRollover()">
|
||||
Schuljahreswechsel auslösen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function triggerSchoolYearRollover() {
|
||||
if (!confirm('Möchtest du den Schuljahreswechsel wirklich durchführen? Dies kann nicht rückgängig gemacht werden!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btn-rollover');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird ausgeführt...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/trigger_school_year_rollover', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.ok) {
|
||||
alert('Schuljahreswechsel erfolgreich durchgeführt!\n\nZusammenfassung:\n' + JSON.stringify(data.summary, null, 2));
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler: ' + (data.message || 'Schuljahreswechsel konnte nicht durchgeführt werden.'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Rollover error:', error);
|
||||
alert('Netzwerk- oder Serverfehler beim Ausführen des Schuljahreswechsels.');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Schuljahreswechsel auslösen';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
@@ -245,12 +300,17 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #1d4ed8;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -261,11 +321,21 @@
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #e5e7eb;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.preview-card p {
|
||||
margin: 0 0 10px;
|
||||
line-height: 1.45;
|
||||
@@ -283,5 +353,15 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.danger-zone .card-body {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.danger-zone .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -274,7 +274,7 @@
|
||||
{% if not show_library_features %}
|
||||
<!-- ================= SYSTEM FILTERS 1-3 (INVENTORY / OTHER ITEMS ONLY) ================= -->
|
||||
<div class="filter-inputs">
|
||||
<h3>Unterrichtsfach (Filter 1):</h3>
|
||||
<h3>{{ filter_names.get('1', 'Jahrgangsstufe') }}</h3>
|
||||
<div class="multi-filter">
|
||||
{% for idx in range(4) %}
|
||||
<div class="form-group">
|
||||
@@ -286,7 +286,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h3>Jahrgangsstufe (Filter 2):</h3>
|
||||
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
|
||||
<div class="multi-filter">
|
||||
{% for idx in range(4) %}
|
||||
<div class="form-group">
|
||||
@@ -298,7 +298,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h3>Schlagwort (Filter 3):</h3>
|
||||
<h3>{{ filter_names.get('3', 'Schlagwort') }}</h3>
|
||||
<div class="multi-filter">
|
||||
{% for idx in range(4) %}
|
||||
<div class="form-group">
|
||||
|
||||
+209
-161
@@ -613,8 +613,200 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
{% endblock %}
|
||||
<script>
|
||||
|
||||
// =========================================================================
|
||||
// CUSTOM MODAL HELPERS (Replaces native browser alert, confirm & prompt)
|
||||
// =========================================================================
|
||||
function createModalOverlay() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100vw';
|
||||
overlay.style.height = '100vh';
|
||||
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.zIndex = '999999';
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function createModalBox() {
|
||||
const box = document.createElement('div');
|
||||
box.style.backgroundColor = '#fff';
|
||||
box.style.padding = '24px';
|
||||
box.style.borderRadius = '8px';
|
||||
box.style.boxShadow = '0 10px 25px rgba(0,0,0,0.2)';
|
||||
box.style.fontFamily = 'sans-serif';
|
||||
box.style.minWidth = '320px';
|
||||
box.style.maxWidth = '90%';
|
||||
box.style.textAlign = 'left';
|
||||
box.style.color = '#333';
|
||||
return box;
|
||||
}
|
||||
|
||||
function customAlert(message) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = createModalOverlay();
|
||||
const box = createModalBox();
|
||||
|
||||
const text = document.createElement('p');
|
||||
text.textContent = message;
|
||||
text.style.marginBottom = '20px';
|
||||
text.style.lineHeight = '1.5';
|
||||
text.style.whiteSpace = 'pre-wrap';
|
||||
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.style.display = 'flex';
|
||||
btnContainer.style.justifyContent = 'flex-end';
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = 'OK';
|
||||
btn.style.padding = '8px 16px';
|
||||
btn.style.cursor = 'pointer';
|
||||
btn.style.border = 'none';
|
||||
btn.style.backgroundColor = '#007bff';
|
||||
btn.style.color = '#fff';
|
||||
btn.style.borderRadius = '4px';
|
||||
|
||||
btn.onclick = () => {
|
||||
document.body.removeChild(overlay);
|
||||
resolve();
|
||||
};
|
||||
|
||||
btnContainer.appendChild(btn);
|
||||
box.appendChild(text);
|
||||
box.appendChild(btnContainer);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
btn.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function customConfirm(message) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = createModalOverlay();
|
||||
const box = createModalBox();
|
||||
|
||||
const text = document.createElement('p');
|
||||
text.textContent = message;
|
||||
text.style.marginBottom = '20px';
|
||||
text.style.lineHeight = '1.5';
|
||||
text.style.whiteSpace = 'pre-wrap';
|
||||
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.style.display = 'flex';
|
||||
btnContainer.style.justifyContent = 'flex-end';
|
||||
btnContainer.style.gap = '10px';
|
||||
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = 'Abbrechen';
|
||||
cancelBtn.style.padding = '8px 16px';
|
||||
cancelBtn.style.cursor = 'pointer';
|
||||
cancelBtn.style.border = '1px solid #ccc';
|
||||
cancelBtn.style.backgroundColor = '#f8f9fa';
|
||||
cancelBtn.style.color = '#333';
|
||||
cancelBtn.style.borderRadius = '4px';
|
||||
|
||||
const okBtn = document.createElement('button');
|
||||
okBtn.textContent = 'OK';
|
||||
okBtn.style.padding = '8px 16px';
|
||||
okBtn.style.cursor = 'pointer';
|
||||
okBtn.style.border = 'none';
|
||||
okBtn.style.backgroundColor = '#007bff';
|
||||
okBtn.style.color = '#fff';
|
||||
okBtn.style.borderRadius = '4px';
|
||||
|
||||
const closeAndResolve = (val) => {
|
||||
document.body.removeChild(overlay);
|
||||
resolve(val);
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => closeAndResolve(false);
|
||||
okBtn.onclick = () => closeAndResolve(true);
|
||||
|
||||
btnContainer.appendChild(cancelBtn);
|
||||
btnContainer.appendChild(okBtn);
|
||||
box.appendChild(text);
|
||||
box.appendChild(btnContainer);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
okBtn.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function customPrompt(message, defaultValue = '') {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = createModalOverlay();
|
||||
const box = createModalBox();
|
||||
|
||||
const text = document.createElement('p');
|
||||
text.textContent = message;
|
||||
text.style.marginBottom = '15px';
|
||||
text.style.lineHeight = '1.5';
|
||||
text.style.whiteSpace = 'pre-wrap';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = defaultValue;
|
||||
input.style.width = '100%';
|
||||
input.style.marginBottom = '20px';
|
||||
input.style.padding = '10px';
|
||||
input.style.boxSizing = 'border-box';
|
||||
input.style.border = '1px solid #ccc';
|
||||
input.style.borderRadius = '4px';
|
||||
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.style.display = 'flex';
|
||||
btnContainer.style.justifyContent = 'flex-end';
|
||||
btnContainer.style.gap = '10px';
|
||||
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = 'Abbrechen';
|
||||
cancelBtn.style.padding = '8px 16px';
|
||||
cancelBtn.style.cursor = 'pointer';
|
||||
cancelBtn.style.border = '1px solid #ccc';
|
||||
cancelBtn.style.backgroundColor = '#f8f9fa';
|
||||
cancelBtn.style.color = '#333';
|
||||
cancelBtn.style.borderRadius = '4px';
|
||||
|
||||
const okBtn = document.createElement('button');
|
||||
okBtn.textContent = 'OK';
|
||||
okBtn.style.padding = '8px 16px';
|
||||
okBtn.style.cursor = 'pointer';
|
||||
okBtn.style.border = 'none';
|
||||
okBtn.style.backgroundColor = '#007bff';
|
||||
okBtn.style.color = '#fff';
|
||||
okBtn.style.borderRadius = '4px';
|
||||
|
||||
const closeAndResolve = (val) => {
|
||||
document.body.removeChild(overlay);
|
||||
resolve(val);
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => closeAndResolve(null);
|
||||
okBtn.onclick = () => closeAndResolve(input.value);
|
||||
|
||||
input.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') closeAndResolve(input.value);
|
||||
if (e.key === 'Escape') closeAndResolve(null);
|
||||
};
|
||||
|
||||
btnContainer.appendChild(cancelBtn);
|
||||
btnContainer.appendChild(okBtn);
|
||||
|
||||
box.appendChild(text);
|
||||
box.appendChild(input);
|
||||
box.appendChild(btnContainer);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
}
|
||||
// =========================================================================
|
||||
// 1. GLOBAL STATE DEFINITIONS
|
||||
// =========================================================================
|
||||
@@ -1242,150 +1434,8 @@
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CUSTOM MODAL HELPERS (Replaces native alert/prompt)
|
||||
// 4. UI INTERACTIONS & UTILITIES
|
||||
// =========================================================================
|
||||
function createModalOverlay() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100vw';
|
||||
overlay.style.height = '100vh';
|
||||
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.zIndex = '999999';
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function createModalBox() {
|
||||
const box = document.createElement('div');
|
||||
box.style.backgroundColor = '#fff';
|
||||
box.style.padding = '24px';
|
||||
box.style.borderRadius = '8px';
|
||||
box.style.boxShadow = '0 10px 25px rgba(0,0,0,0.2)';
|
||||
box.style.fontFamily = 'sans-serif';
|
||||
box.style.minWidth = '320px';
|
||||
box.style.maxWidth = '90%';
|
||||
box.style.textAlign = 'left';
|
||||
box.style.color = '#333';
|
||||
return box;
|
||||
}
|
||||
|
||||
function customAlert(message) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = createModalOverlay();
|
||||
const box = createModalBox();
|
||||
|
||||
const text = document.createElement('p');
|
||||
text.textContent = message;
|
||||
text.style.marginBottom = '20px';
|
||||
text.style.lineHeight = '1.5';
|
||||
text.style.whiteSpace = 'pre-wrap';
|
||||
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.style.display = 'flex';
|
||||
btnContainer.style.justifyContent = 'flex-end';
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = 'OK';
|
||||
btn.style.padding = '8px 16px';
|
||||
btn.style.cursor = 'pointer';
|
||||
btn.style.border = 'none';
|
||||
btn.style.backgroundColor = '#007bff';
|
||||
btn.style.color = '#fff';
|
||||
btn.style.borderRadius = '4px';
|
||||
|
||||
btn.onclick = () => {
|
||||
document.body.removeChild(overlay);
|
||||
resolve();
|
||||
};
|
||||
|
||||
btnContainer.appendChild(btn);
|
||||
box.appendChild(text);
|
||||
box.appendChild(btnContainer);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
btn.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function customPrompt(message, defaultValue = '') {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = createModalOverlay();
|
||||
const box = createModalBox();
|
||||
|
||||
const text = document.createElement('p');
|
||||
text.textContent = message;
|
||||
text.style.marginBottom = '15px';
|
||||
text.style.lineHeight = '1.5';
|
||||
text.style.whiteSpace = 'pre-wrap';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = defaultValue;
|
||||
input.style.width = '100%';
|
||||
input.style.marginBottom = '20px';
|
||||
input.style.padding = '10px';
|
||||
input.style.boxSizing = 'border-box';
|
||||
input.style.border = '1px solid #ccc';
|
||||
input.style.borderRadius = '4px';
|
||||
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.style.display = 'flex';
|
||||
btnContainer.style.justifyContent = 'flex-end';
|
||||
btnContainer.style.gap = '10px';
|
||||
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = 'Abbrechen';
|
||||
cancelBtn.style.padding = '8px 16px';
|
||||
cancelBtn.style.cursor = 'pointer';
|
||||
cancelBtn.style.border = '1px solid #ccc';
|
||||
cancelBtn.style.backgroundColor = '#f8f9fa';
|
||||
cancelBtn.style.color = '#333';
|
||||
cancelBtn.style.borderRadius = '4px';
|
||||
|
||||
const okBtn = document.createElement('button');
|
||||
okBtn.textContent = 'OK';
|
||||
okBtn.style.padding = '8px 16px';
|
||||
okBtn.style.cursor = 'pointer';
|
||||
okBtn.style.border = 'none';
|
||||
okBtn.style.backgroundColor = '#007bff';
|
||||
okBtn.style.color = '#fff';
|
||||
okBtn.style.borderRadius = '4px';
|
||||
|
||||
const closeAndResolve = (val) => {
|
||||
document.body.removeChild(overlay);
|
||||
resolve(val);
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => closeAndResolve(null);
|
||||
okBtn.onclick = () => closeAndResolve(input.value);
|
||||
|
||||
input.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') closeAndResolve(input.value);
|
||||
if (e.key === 'Escape') closeAndResolve(null);
|
||||
};
|
||||
|
||||
btnContainer.appendChild(cancelBtn);
|
||||
btnContainer.appendChild(okBtn);
|
||||
|
||||
box.appendChild(text);
|
||||
box.appendChild(input);
|
||||
box.appendChild(btnContainer);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ORIGINAL SCRIPT (Updated to use custom modals)
|
||||
// =========================================================================
|
||||
|
||||
async function borrowItem(itemId) {
|
||||
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
|
||||
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
|
||||
@@ -1395,8 +1445,6 @@
|
||||
|
||||
const defaultCardId = activeStudentCardId || '';
|
||||
const promptCardResult = await customPrompt('Bitte Bibliotheksausweis-ID eingeben:', defaultCardId);
|
||||
|
||||
// Return early if the user clicked "Cancel" (prompt returns null)
|
||||
if (promptCardResult === null) return;
|
||||
|
||||
const cardId = promptCardResult.trim().toUpperCase();
|
||||
@@ -1412,7 +1460,6 @@
|
||||
const durationInput = promptDurationResult.trim();
|
||||
|
||||
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1);
|
||||
|
||||
const countPromptResult = await customPrompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1');
|
||||
if (countPromptResult === null) return;
|
||||
|
||||
@@ -1432,7 +1479,7 @@
|
||||
const csrfField = document.createElement('input');
|
||||
csrfField.type = 'hidden';
|
||||
csrfField.name = 'csrf_token';
|
||||
csrfField.value = '{{ csrf_token }}'; // Note: Template syntax remains intact
|
||||
csrfField.value = '{{ csrf_token }}';
|
||||
form.appendChild(csrfField);
|
||||
|
||||
const cardField = document.createElement('input');
|
||||
@@ -1502,13 +1549,13 @@
|
||||
const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch' };
|
||||
return labels[type] || type;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
function showItemDetail(itemId) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const detailModal = document.getElementById('detailModal');
|
||||
@@ -1542,9 +1589,9 @@
|
||||
: `/uploads/${image}`;
|
||||
|
||||
return `
|
||||
<div class="item-image-wrapper" style="width: 100%; height: auto;">
|
||||
<video src="${videoSrc}" class="item-image ${index === 0 ? 'active-image' : ''}" id="modal-image-${index}" controls preload="metadata" style="width: 100%; height: auto; max-height: 300px;"></video>
|
||||
</div>`;
|
||||
<div class="item-image-wrapper" style="width: 100%; height: auto;">
|
||||
<video src="${videoSrc}" class="item-image ${index === 0 ? 'active-image' : ''}" id="modal-image-${index}" controls preload="metadata" style="width: 100%; height: auto; max-height: 300px;"></video>
|
||||
</div>`;
|
||||
} else {
|
||||
const imageSrc = thumbnailInfo && thumbnailInfo.has_preview
|
||||
? thumbnailInfo.preview_url
|
||||
@@ -1553,15 +1600,15 @@
|
||||
: `/uploads/${image}`);
|
||||
|
||||
return `
|
||||
<div class="item-image-wrapper">
|
||||
<img src="${imageSrc}"
|
||||
alt="Buchcover / Bild"
|
||||
class="item-image ${index === 0 ? 'active-image' : ''}"
|
||||
id="modal-image-${index}"
|
||||
loading="lazy"
|
||||
onload="console.log('Bild geladen:', '${imageSrc}')"
|
||||
onerror="console.error('FEHLER beim Laden des Bildes im DOM:', '${imageSrc}')">
|
||||
</div>`;
|
||||
<div class="item-image-wrapper">
|
||||
<img src="${imageSrc}"
|
||||
alt="Buchcover / Bild"
|
||||
class="item-image ${index === 0 ? 'active-image' : ''}"
|
||||
id="modal-image-${index}"
|
||||
loading="lazy"
|
||||
onload="console.log('Bild geladen:', '${imageSrc}')"
|
||||
onerror="console.error('FEHLER beim Laden des Bildes im DOM:', '${imageSrc}')">
|
||||
</div>`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
@@ -1746,3 +1793,4 @@
|
||||
window.location.href = `/item_edit/${itemId}`;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -2343,7 +2343,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<div class="filter-container">
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
<label>Unterrichtsfach:</label>
|
||||
<label>{{ filter_names.get('2', 'Fach') }}</label>
|
||||
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter1-dropdown')">▼</button>
|
||||
<button type="button" class="clear-filter" onclick="clearFilter(1)">Clear</button>
|
||||
</div>
|
||||
@@ -2357,7 +2357,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
<label>Jahrgangsstufe:</label>
|
||||
<label>{{ filter_names.get('1', 'Klassenstufe') }}</label>
|
||||
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter2-dropdown')">▼</button>
|
||||
<button type="button" class="clear-filter" onclick="clearFilter(2)">Clear</button>
|
||||
</div>
|
||||
@@ -2371,7 +2371,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
<label>Schlagwort:</label>
|
||||
<label>{{ filter_names.get('3', 'Schlagwort') }}</label>
|
||||
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter3-dropdown')">▼</button>
|
||||
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
|
||||
</div>
|
||||
@@ -2420,9 +2420,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||
<span>Name</span>
|
||||
<span>Ort</span>
|
||||
<span>Unterrichtsfach</span>
|
||||
<span>Jahrgangsstufe</span>
|
||||
<span>Schlagwort</span>
|
||||
<span>{{ filter_names.get('2', 'Fach') }}</span>
|
||||
<span>{{ filter_names.get('1', 'Jahrgangsstufe') }}</span>
|
||||
<span>{{ filter_names.get('3', 'Schlagwort') }}</span>
|
||||
<span>Barcode</span>
|
||||
<span>Anzahl</span>
|
||||
</div>
|
||||
@@ -3418,14 +3418,24 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
|
||||
${isGroupedItem ? `
|
||||
<div class="grouped-borrow-controls" style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
|
||||
<label style="font-size:0.85rem; margin:0;">Anzahl:</label>
|
||||
<input type="number" name="exemplare_count" min="1" max="${availableGroupedCount}" value="1" style="width:74px; padding:4px;">
|
||||
<label style="font-size:0.85rem; margin:0;">oder Code:</label>
|
||||
<select id="specific-item-card-${item._id}" name="specific_item_id" style="max-width:190px; padding:4px;">
|
||||
<option value="">Automatisch wählen</option>
|
||||
${groupedAvailableUnits.map(unit => `<option value="${unit.id}">${unit.code}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
<label style="font-size:0.85rem; margin:0;">Anzahl:</label>
|
||||
<input type="number"
|
||||
name="exemplare_count"
|
||||
min="1"
|
||||
max="${availableGroupedCount}"
|
||||
value="1"
|
||||
style="width:74px; padding:4px;"
|
||||
oninput="if(this.value){ this.form.querySelector('[name=specific_item_id]').value = ''; }">
|
||||
|
||||
<label style="font-size:0.85rem; margin:0;">oder Code:</label>
|
||||
<select id="specific-item-card-${item._id}"
|
||||
name="specific_item_id"
|
||||
style="max-width:190px; padding:4px;"
|
||||
onchange="if(this.value !== ''){ this.form.querySelector('[name=exemplare_count]').value = '1'; }">
|
||||
<option value="">Automatisch wählen</option>
|
||||
${groupedAvailableUnits.map(unit => `<option value="${unit.id}">${unit.code}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
<button class="ausleihen" type="submit">Ausleihen</button>
|
||||
</form>`
|
||||
: isBorrowedByMe ?
|
||||
|
||||
@@ -1,135 +1,173 @@
|
||||
<!--
|
||||
Copyright 2025-2026 AIIrondev
|
||||
|
||||
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||||
See Legal/LICENSE for the full license text.
|
||||
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
-->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Filter verwalten{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="mb-4">Filterwerte verwalten</h1>
|
||||
|
||||
<div class="row">
|
||||
<!-- Filter 1 -->
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Jahrgang') }} (Filter 1)</h2>
|
||||
<div class="container py-4">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center pb-3 mb-4 border-bottom gap-3">
|
||||
<div>
|
||||
<h1 class="h2 fw-bold mb-1">Filterwerte verwalten</h1>
|
||||
<p class="text-muted mb-0">Verwalten und Bearbeiten Sie die Zuordnungswerte für das Inventar.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('home_admin') }}" class="btn btn-outline-secondary d-inline-flex align-items-center">
|
||||
<i class="bi bi-arrow-left me-2"></i> Zurück zur Admin-Übersicht
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm border-0 h-100 rounded-3">
|
||||
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
|
||||
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('1', 'Jahrgangsstufe') }}
|
||||
</h2>
|
||||
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 1</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
||||
<label class="form-label fw-medium text-secondary">Neuen Wert hinzufügen</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||
<input type="text" name="value" class="form-control" placeholder="Wert eingeben..." required>
|
||||
<button type="submit" class="btn btn-primary d-inline-flex align-items-center">
|
||||
<i class="bi bi-plus-lg me-1"></i> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h5 class="mb-3">Vorhandene Werte</h5>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="h6 text-uppercase text-muted fw-bold mb-0">Vorhandene Werte</h3>
|
||||
<span class="badge bg-secondary rounded-pill">{{ filter1_values|length if filter1_values else 0 }}</span>
|
||||
</div>
|
||||
|
||||
{% if filter1_values %}
|
||||
<div class="list-group">
|
||||
<div class="list-group list-group-flush border rounded-3 overflow-hidden">
|
||||
{% for value in filter1_values %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>{{ value }}</span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-1-{{ loop.index }}')">Bearbeiten</button>
|
||||
<div class="list-group-item p-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-medium">{{ value }}</span>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">
|
||||
<i class="bi bi-pencil me-1"></i> Bearbeiten
|
||||
</button>
|
||||
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
<button type="submit" class="btn btn-outline-danger"
|
||||
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
||||
Entfernen
|
||||
<i class="bi bi-trash me-1"></i> Entfernen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form id="edit-1-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=1, old_value=value) }}" style="display: none;" class="mt-2">
|
||||
|
||||
<form id="edit-1-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=1, old_value=value) }}" style="display: none;" class="mt-3 pt-3 border-top">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-success" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">
|
||||
<i class="bi bi-check-lg me-1"></i> Speichern
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">Keine Werte definiert.</div>
|
||||
<div class="alert alert-light border text-center text-muted mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i> Keine Werte definiert.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter 2 -->
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'Fach') }} (Filter 2)</h2>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm border-0 h-100 rounded-3">
|
||||
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
|
||||
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('2', 'Fachgebiet') }}
|
||||
</h2>
|
||||
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 2</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
||||
<label class="form-label fw-medium text-secondary">Neuen Wert hinzufügen</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||
<input type="text" name="value" class="form-control" placeholder="Wert eingeben..." required>
|
||||
<button type="submit" class="btn btn-primary d-inline-flex align-items-center">
|
||||
<i class="bi bi-plus-lg me-1"></i> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h5 class="mb-3">Vorhandene Werte</h5>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="h6 text-uppercase text-muted fw-bold mb-0">Vorhandene Werte</h3>
|
||||
<span class="badge bg-secondary rounded-pill">{{ filter2_values|length if filter2_values else 0 }}</span>
|
||||
</div>
|
||||
|
||||
{% if filter2_values %}
|
||||
<div class="list-group">
|
||||
<div class="list-group list-group-flush border rounded-3 overflow-hidden">
|
||||
{% for value in filter2_values %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>{{ value }}</span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-2-{{ loop.index }}')">Bearbeiten</button>
|
||||
<div class="list-group-item p-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-medium">{{ value }}</span>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">
|
||||
<i class="bi bi-pencil me-1"></i> Bearbeiten
|
||||
</button>
|
||||
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
<button type="submit" class="btn btn-outline-danger"
|
||||
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
||||
Entfernen
|
||||
<i class="bi bi-trash me-1"></i> Entfernen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form id="edit-2-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=2, old_value=value) }}" style="display: none;" class="mt-2">
|
||||
|
||||
<form id="edit-2-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=2, old_value=value) }}" style="display: none;" class="mt-3 pt-3 border-top">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-success" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">
|
||||
<i class="bi bi-check-lg me-1"></i> Speichern
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">Keine Werte definiert.</div>
|
||||
<div class="alert alert-light border text-center text-muted mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i> Keine Werte definiert.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>Hinweis:</strong> Beim Entfernen eines Filterwertes wird dieser nicht aus bestehenden Objekten entfernt.
|
||||
Bereits verwendete Werte bleiben in den Objekten erhalten.
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
|
||||
|
||||
<div class="alert alert-warning border-0 shadow-sm d-flex align-items-center mt-4 rounded-3" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-4 me-3 text-warning"></i>
|
||||
<div>
|
||||
<strong>Hinweis:</strong> Beim Entfernen eines Filterwertes wird dieser nicht automatisch aus bestehenden Objekten gelöscht. Bereits zugewiesene Werte bleiben in bestehenden Datensätzen erhalten.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleEdit(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el.style.display === 'none') {
|
||||
if (el.style.display === 'none' || el.style.display === '') {
|
||||
el.style.display = 'block';
|
||||
const input = el.querySelector('input[type="text"]');
|
||||
if (input) input.focus();
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -670,7 +670,6 @@
|
||||
<form method="POST" action="{{ url_for('upload_inventory_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="inventory_excel" accept=".xlsx,.csv" required>
|
||||
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('inventory')">Beispiel-CSV herunterladen</button>
|
||||
<button type="button" class="btn btn-outline-secondary" title="Format-Hilfe" onclick="showCsvHelp('inventory')">?</button>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Inventar importieren</button>
|
||||
</form>
|
||||
@@ -781,7 +780,7 @@
|
||||
{% else %}
|
||||
<!-- Normal Item Mode: Standard Filters -->
|
||||
<div class="filter-inputs">
|
||||
<h3>Unterrichtsfach:</h3>
|
||||
<h3>{{ filter_names.get('1', 'Jahrgangsstufe') }}</h3>
|
||||
<div class="multi-filter">
|
||||
<div class="form-group">
|
||||
<label for="filter1-1">Wert 1:</label>
|
||||
@@ -813,7 +812,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Jahrgangsstufe:</h3>
|
||||
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
|
||||
<div class="multi-filter">
|
||||
<div class="form-group">
|
||||
<label for="filter2-1">Wert 1:</label>
|
||||
@@ -845,7 +844,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Schlagwort:</h3>
|
||||
<h3>{{ filter_names.get('3', 'Schlagwort') }}</h3>
|
||||
<div class="multi-filter">
|
||||
<div class="form-group">
|
||||
<label for="filter3-1">Wert 1:</label>
|
||||
|
||||
+83
-67
@@ -430,6 +430,7 @@ PY
|
||||
initialize_tenant_database() {
|
||||
local tenant_id="$1"
|
||||
local mode="$2"
|
||||
local admin_password="${3:-admin123}"
|
||||
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -z "$APP_CONTAINER" ]; then
|
||||
@@ -438,7 +439,7 @@ initialize_tenant_database() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker exec -i "$APP_CONTAINER" python3 - "$tenant_id" "$mode" <<'PY'
|
||||
docker exec -i "$APP_CONTAINER" python3 - "$tenant_id" "$mode" "$admin_password" <<'PY'
|
||||
import sys, os, re, datetime, hashlib
|
||||
sys.path.insert(0, "/app")
|
||||
sys.path.insert(0, "/app/Web")
|
||||
@@ -449,6 +450,8 @@ import Web.modules.database.user as us
|
||||
|
||||
tenant_id = sys.argv[1].lower()
|
||||
mode = sys.argv[2]
|
||||
admin_password = sys.argv[3] if len(sys.argv) > 3 else "admin123"
|
||||
|
||||
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
||||
db_name = f"inventar_{sanitized}" if sanitized else settings.MONGODB_DB
|
||||
|
||||
@@ -458,7 +461,7 @@ users = db["users"]
|
||||
permission_defaults = us.build_default_permission_payload("full_access")
|
||||
admin_doc = {
|
||||
"Username": "admin",
|
||||
"Password": us.hashing("admin123"),
|
||||
"Password": us.hashing(admin_password),
|
||||
"Admin": True,
|
||||
"active_ausleihung": None,
|
||||
"name": dp.encrypt_text("admin"),
|
||||
@@ -488,7 +491,7 @@ if mode == "trial":
|
||||
|
||||
client.close()
|
||||
|
||||
print(f"Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123")
|
||||
print(f"Tenant {sys.argv[1]} database initialized. Default admin: admin / {admin_password}")
|
||||
PY
|
||||
}
|
||||
|
||||
@@ -813,6 +816,8 @@ case "$COMMAND" in
|
||||
fi
|
||||
|
||||
PORT_ARG="${3:-}"
|
||||
PASSWORD_ARG="${4:-admin123}" # Optional 4th parameter; defaults to 'admin123' if omitted
|
||||
|
||||
if [ -n "$PORT_ARG" ]; then
|
||||
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
|
||||
echo "Error: Port must be a numeric value."
|
||||
@@ -829,11 +834,9 @@ case "$COMMAND" in
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo "Adding new tenant '$TENANT_ID'..."
|
||||
echo "Initializing database for $TENANT_ID..."
|
||||
initialize_tenant_database "$TENANT_ID" "standard"
|
||||
initialize_tenant_database "$TENANT_ID" "standard" "$PASSWORD_ARG"
|
||||
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
||||
;;
|
||||
|
||||
@@ -872,81 +875,94 @@ case "$COMMAND" in
|
||||
;;
|
||||
|
||||
remove)
|
||||
FORCE_REMOVE=false
|
||||
TENANT_ARG="${2:-}"
|
||||
FORCE_REMOVE=false
|
||||
|
||||
if [ "$TENANT_ARG" = "--yes" ] || [ "$TENANT_ARG" = "-y" ]; then
|
||||
FORCE_REMOVE=true
|
||||
TENANT_ID="${3:-}"
|
||||
else
|
||||
TENANT_ID="$TENANT_ARG"
|
||||
# Fix: Prüfe sowohl Position 2 als auch 3 auf das -y Flag
|
||||
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
||||
FORCE_REMOVE=true
|
||||
TENANT_ID="${3:-}"
|
||||
elif [ "${3:-}" = "--yes" ] || [ "${3:-}" = "-y" ]; then
|
||||
FORCE_REMOVE=true
|
||||
TENANT_ID="${2:-}"
|
||||
else
|
||||
TENANT_ID="${2:-}"
|
||||
fi
|
||||
|
||||
if [ -z "$TENANT_ID" ]; then
|
||||
echo "Error: Please provide a tenant_id to remove."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$FORCE_REMOVE" != true ]; then
|
||||
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
||||
read confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
echo "Removal canceled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$TENANT_ID" ]; then
|
||||
echo "Error: Please provide a tenant_id to remove."
|
||||
exit 1
|
||||
fi
|
||||
echo "Removing tenant '$TENANT_ID'..."
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
port_to_remove=""
|
||||
|
||||
if [ "$FORCE_REMOVE" != true ]; then
|
||||
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
||||
read confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
echo "Removal canceled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
# MongoDB-Datenbank via PyMongo direkt im Container droppen
|
||||
# Fix 1: Wir hängen '|| true' an, damit das Skript nicht abstürzt, falls der Befehl fehlschlägt.
|
||||
docker exec -i "$APP_CONTAINER" python3 -c '
|
||||
import sys, os
|
||||
try:
|
||||
import pymongo
|
||||
tenant_id = sys.argv[1]
|
||||
# In Docker Compose heißt der Host oft "mongodb" statt "localhost"
|
||||
mongo_uri = os.environ.get("MONGO_URI", "mongodb://mongodb:27017/")
|
||||
client = pymongo.MongoClient(mongo_uri, serverSelectionTimeoutMS=2000)
|
||||
|
||||
echo "Removing tenant '$TENANT_ID'..."
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
port_to_remove=""
|
||||
# Fix 2: Wir versuchen beide Formate zu löschen, da "list" zeigt, dass sie "test" und nicht "inventar_test" heißen.
|
||||
dbs_to_drop = [f"inventar_{tenant_id}", tenant_id]
|
||||
existing_dbs = client.list_database_names()
|
||||
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
# MongoDB-Datenbank via PyMongo direkt im Container droppen
|
||||
docker exec -i "$APP_CONTAINER" python3 -c '
|
||||
import sys, os
|
||||
try:
|
||||
import pymongo
|
||||
tenant_id = sys.argv[1]
|
||||
mongo_uri = os.environ.get("MONGO_URI", "mongodb://localhost:27017/")
|
||||
client = pymongo.MongoClient(mongo_uri, serverSelectionTimeoutMS=2000)
|
||||
db_name = f"inventar_{tenant_id}"
|
||||
if db_name in client.list_database_names():
|
||||
for db_name in dbs_to_drop:
|
||||
if db_name in existing_dbs:
|
||||
client.drop_database(db_name)
|
||||
print(f"Dropped database: {db_name}")
|
||||
except Exception as e:
|
||||
print(f"Error dropping database: {e}", file=sys.stderr)
|
||||
' "$TENANT_ID" > /dev/null 2>&1
|
||||
except Exception as e:
|
||||
print(f"Error dropping database: {e}", file=sys.stderr)
|
||||
' "$TENANT_ID" > /dev/null 2>&1 || true
|
||||
|
||||
# Konfiguration und Port via Host-Funktion bereinigen und Port ermitteln
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
port_to_remove=""
|
||||
fi
|
||||
|
||||
echo "Tenant '$TENANT_ID' database and config removed."
|
||||
# Konfiguration und Port via Host-Funktion bereinigen und Port ermitteln
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
||||
fi
|
||||
port_to_remove=""
|
||||
fi
|
||||
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
remove_runtime_port "$port_to_remove"
|
||||
fi
|
||||
remove_tenant_nginx_config "$TENANT_ID"
|
||||
sync_tenant_port_map
|
||||
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||
restart_app_container
|
||||
fi
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
echo "Removed tenant '$TENANT_ID' and cleaned runtime port $port_to_remove."
|
||||
echo "Tenant '$TENANT_ID' database and config removed."
|
||||
else
|
||||
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
||||
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
remove_runtime_port "$port_to_remove" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
remove_tenant_nginx_config "$TENANT_ID"
|
||||
sync_tenant_port_map
|
||||
|
||||
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||
restart_app_container || true
|
||||
fi
|
||||
|
||||
if [ -n "$port_to_remove" ]; then
|
||||
echo "Removed tenant '$TENANT_ID' and cleaned runtime port $port_to_remove."
|
||||
else
|
||||
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
||||
fi
|
||||
;;
|
||||
|
||||
restart-tenant)
|
||||
|
||||
Reference in New Issue
Block a user