Export und Aktion gefixt

Dieser Commit ist enthalten in:
2025-06-15 22:22:08 +02:00
Ursprung df60ce6d18
Commit d65e5d333c
2 geänderte Dateien mit 198 neuen und 41 gelöschten Zeilen

Datei anzeigen

@@ -3192,11 +3192,12 @@ def export_resources():
# SQL Query mit Filtern # SQL Query mit Filtern
query = """ query = """
SELECT r.id, r.type, r.value, r.status, r.license_id, r.created_at, r.allocated_at, SELECT r.id, r.resource_type, r.resource_value, r.status, r.allocated_to_license,
r.created_at, r.status_changed_at,
l.license_key, c.name as customer_name, c.email as customer_email, l.license_key, c.name as customer_name, c.email as customer_email,
l.type as license_type l.license_type
FROM resource_pools r FROM resource_pools r
LEFT JOIN licenses l ON r.license_id = l.id LEFT JOIN licenses l ON r.allocated_to_license = l.id
LEFT JOIN customers c ON l.customer_id = c.id LEFT JOIN customers c ON l.customer_id = c.id
WHERE 1=1 WHERE 1=1
""" """
@@ -3204,11 +3205,11 @@ def export_resources():
# Filter für Testdaten # Filter für Testdaten
if not show_test: if not show_test:
query += " AND (l.is_test = false OR l.is_test IS NULL)" query += " AND (r.is_test = false OR r.is_test IS NULL)"
# Filter für Ressourcentyp # Filter für Ressourcentyp
if filter_type: if filter_type:
query += " AND r.type = %s" query += " AND r.resource_type = %s"
params.append(filter_type) params.append(filter_type)
# Filter für Status # Filter für Status
@@ -3218,7 +3219,7 @@ def export_resources():
# Suchfilter # Suchfilter
if search_query: if search_query:
query += " AND (r.value ILIKE %s OR l.license_key ILIKE %s OR c.name ILIKE %s)" query += " AND (r.resource_value ILIKE %s OR l.license_key ILIKE %s OR c.name ILIKE %s)"
params.extend([f'%{search_query}%', f'%{search_query}%', f'%{search_query}%']) params.extend([f'%{search_query}%', f'%{search_query}%', f'%{search_query}%'])
query += " ORDER BY r.id DESC" query += " ORDER BY r.id DESC"

Datei anzeigen

@@ -165,6 +165,33 @@
margin-bottom: 1rem; margin-bottom: 1rem;
opacity: 0.5; opacity: 0.5;
} }
/* Dropdown Verbesserungen */
.dropdown-menu {
min-width: 220px;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
border: none;
}
.dropdown-item {
padding: 0.5rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
transition: all 0.2s ease;
}
.dropdown-item:hover {
background-color: #f8f9fa;
transform: translateX(2px);
}
.dropdown-item i {
width: 20px;
text-align: center;
}
/* Quick Action Buttons */
.btn-success {
font-weight: 500;
}
</style> </style>
{% endblock %} {% endblock %}
@@ -292,13 +319,17 @@
<h5 class="mb-0">📋 Ressourcen-Liste</h5> <h5 class="mb-0">📋 Ressourcen-Liste</h5>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"> <button class="btn btn-sm btn-secondary dropdown-toggle"
type="button"
id="exportDropdown"
data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-download"></i> Export <i class="bi bi-download"></i> Export
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu" aria-labelledby="exportDropdown">
<li><a class="dropdown-item" href="/export/resources?format=excel&type={{ filter_type }}&status={{ filter_status }}&search={{ search_query }}&show_test={{ show_test }}"> <li><a class="dropdown-item" href="/export/resources?format=excel&type={{ resource_type }}&status={{ status_filter }}&search={{ search }}&show_test={{ show_test }}">
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li> <i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
<li><a class="dropdown-item" href="/export/resources?format=csv&type={{ filter_type }}&status={{ filter_status }}&search={{ search_query }}&show_test={{ show_test }}"> <li><a class="dropdown-item" href="/export/resources?format=csv&type={{ resource_type }}&status={{ status_filter }}&search={{ search }}&show_test={{ show_test }}">
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li> <i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
</ul> </ul>
</div> </div>
@@ -318,7 +349,7 @@
<th width="140">Status</th> <th width="140">Status</th>
<th>Zugewiesen an</th> <th>Zugewiesen an</th>
<th width="180">Letzte Änderung</th> <th width="180">Letzte Änderung</th>
<th width="140" class="text-center">Aktionen</th> <th width="200" class="text-center">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -343,7 +374,7 @@
<code class="me-2">{{ resource[2] }}</code> <code class="me-2">{{ resource[2] }}</code>
<button class="copy-btn" onclick="copyToClipboard('{{ resource[2] }}', this)" <button class="copy-btn" onclick="copyToClipboard('{{ resource[2] }}', this)"
title="Kopieren"> title="Kopieren">
<i class="fas fa-copy"></i> <i class="bi bi-clipboard"></i>
</button> </button>
</div> </div>
</td> </td>
@@ -394,29 +425,96 @@
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">
<a href="{{ url_for('resource_history', resource_id=resource[0]) }}" {% if resource[3] == 'quarantine' %}
class="btn btn-action btn-sm btn-outline-info" <!-- Quick Action für Quarantäne -->
title="Historie anzeigen"> <form method="post" action="/resources/release"
<i class="fas fa-history"></i> style="display: inline-block; margin-right: 5px;">
</a>
{% if resource[3] == 'available' %}
<button class="btn btn-action btn-sm btn-outline-warning"
onclick="showQuarantineModal({{ resource[0] }})"
title="In Quarantäne setzen">
<i class="fas fa-ban"></i>
</button>
{% elif resource[3] == 'quarantine' %}
<form method="post" action="{{ url_for('release_resources') }}"
style="display: inline;">
<input type="hidden" name="resource_ids[]" value="{{ resource[0] }}"> <input type="hidden" name="resource_ids[]" value="{{ resource[0] }}">
<button type="submit" <button type="submit"
class="btn btn-action btn-sm btn-outline-success" class="btn btn-sm btn-success">
title="Ressource freigeben"> <i class="bi bi-check-circle"></i> Freigeben
<i class="fas fa-check"></i>
</button> </button>
</form> </form>
{% endif %} {% endif %}
<!-- Dropdown für weitere Aktionen -->
<div class="dropdown" style="display: inline-block;">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton{{ resource[0] }}"
data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i> Aktionen
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ resource[0] }}">
<!-- Historie immer verfügbar -->
<li>
<a class="dropdown-item"
href="{{ url_for('resource_history', resource_id=resource[0]) }}">
<i class="bi bi-clock-history text-info"></i> Historie anzeigen
</a>
</li>
<li><hr class="dropdown-divider"></li>
{% if resource[3] == 'available' %}
<!-- Aktionen für verfügbare Ressourcen -->
<li>
<button class="dropdown-item"
onclick="showQuarantineModal({{ resource[0] }})">
<i class="bi bi-exclamation-triangle text-warning"></i> In Quarantäne setzen
</button>
</li>
{% elif resource[3] == 'allocated' %}
<!-- Aktionen für zugeteilte Ressourcen -->
{% if resource[4] %}
<li>
<a class="dropdown-item"
href="{{ url_for('edit_license', license_id=resource[4]) }}?ref=resources{{ '&show_test=true' if show_test else '' }}">
<i class="bi bi-file-text text-primary"></i> Lizenz bearbeiten
</a>
</li>
{% endif %}
{% if resource[10] %}
<li>
<a class="dropdown-item"
href="{{ url_for('customers_licenses', customer_id=resource[10], show_test=show_test) }}">
<i class="bi bi-person text-primary"></i> Kunde anzeigen
</a>
</li>
{% endif %}
{% elif resource[3] == 'quarantine' %}
<!-- Aktionen für Quarantäne-Ressourcen -->
<li>
<form method="post" action="/resources/release"
style="display: contents;">
<input type="hidden" name="resource_ids[]" value="{{ resource[0] }}">
<button type="submit" class="dropdown-item">
<i class="bi bi-check-circle text-success"></i> Ressource freigeben
</button>
</form>
</li>
{% if resource[9] %}
<li>
<button class="dropdown-item"
onclick="extendQuarantine({{ resource[0] }})">
<i class="bi bi-calendar-plus text-warning"></i> Quarantäne verlängern
</button>
</li>
{% endif %}
{% endif %}
<li><hr class="dropdown-divider"></li>
<!-- Kopieren immer verfügbar -->
<li>
<button class="dropdown-item"
onclick="copyToClipboard('{{ resource[2] }}', this)">
<i class="bi bi-clipboard text-secondary"></i> Ressource kopieren
</button>
</li>
</ul>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -425,7 +523,7 @@
</div> </div>
{% else %} {% else %}
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-inbox"></i> <i class="bi bi-inbox"></i>
<h4>Keine Ressourcen gefunden</h4> <h4>Keine Ressourcen gefunden</h4>
<p>Ändern Sie Ihre Filterkriterien oder fügen Sie neue Ressourcen hinzu.</p> <p>Ändern Sie Ihre Filterkriterien oder fügen Sie neue Ressourcen hinzu.</p>
</div> </div>
@@ -440,13 +538,13 @@
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" <a class="page-link"
href="{{ url_for('resources', page=1, type=resource_type, status=status_filter, search=search, show_test=show_test) }}"> href="{{ url_for('resources', page=1, type=resource_type, status=status_filter, search=search, show_test=show_test) }}">
<i class="fas fa-angle-double-left"></i> Erste <i class="bi bi-chevron-double-left"></i> Erste
</a> </a>
</li> </li>
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" <a class="page-link"
href="{{ url_for('resources', page=page-1, type=resource_type, status=status_filter, search=search, show_test=show_test) }}"> href="{{ url_for('resources', page=page-1, type=resource_type, status=status_filter, search=search, show_test=show_test) }}">
<i class="fas fa-angle-left"></i> Zurück <i class="bi bi-chevron-left"></i> Zurück
</a> </a>
</li> </li>
@@ -464,13 +562,13 @@
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" <a class="page-link"
href="{{ url_for('resources', page=page+1, type=resource_type, status=status_filter, search=search, show_test=show_test) }}"> href="{{ url_for('resources', page=page+1, type=resource_type, status=status_filter, search=search, show_test=show_test) }}">
Weiter <i class="fas fa-angle-right"></i> Weiter <i class="bi bi-chevron-right"></i>
</a> </a>
</li> </li>
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" <a class="page-link"
href="{{ url_for('resources', page=total_pages, type=resource_type, status=status_filter, search=search, show_test=show_test) }}"> href="{{ url_for('resources', page=total_pages, type=resource_type, status=status_filter, search=search, show_test=show_test) }}">
Letzte <i class="fas fa-angle-double-right"></i> Letzte <i class="bi bi-chevron-double-right"></i>
</a> </a>
</li> </li>
</ul> </ul>
@@ -568,6 +666,12 @@
</div> </div>
<script> <script>
// Debug Bootstrap
console.log('Bootstrap loaded:', typeof bootstrap !== 'undefined');
if (typeof bootstrap !== 'undefined') {
console.log('Bootstrap version:', bootstrap.Dropdown.VERSION);
}
// Live-Filtering // Live-Filtering
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('filterForm'); const form = document.getElementById('filterForm');
@@ -590,21 +694,36 @@ document.addEventListener('DOMContentLoaded', function() {
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl) return new bootstrap.Tooltip(tooltipTriggerEl)
}); });
// Bootstrap Dropdowns explizit initialisieren
var dropdownElementList = [].slice.call(document.querySelectorAll('.dropdown-toggle'))
var dropdownList = dropdownElementList.map(function (dropdownToggleEl) {
return new bootstrap.Dropdown(dropdownToggleEl)
});
// Export Dropdown manuell aktivieren falls nötig
const exportBtn = document.getElementById('exportDropdown');
if (exportBtn) {
exportBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const dropdown = bootstrap.Dropdown.getOrCreateInstance(this);
dropdown.toggle();
});
}
}); });
// Copy to Clipboard mit besserem Feedback // Copy to Clipboard mit besserem Feedback
function copyToClipboard(text, button) { function copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
// Button Icon ändern // Original Text speichern
const icon = button.querySelector('i'); const originalText = button.innerHTML;
icon.classList.remove('fa-copy'); button.innerHTML = '<i class="bi bi-check"></i> Kopiert!';
icon.classList.add('fa-check');
button.classList.add('copied'); button.classList.add('copied');
// Nach 2 Sekunden zurücksetzen // Nach 2 Sekunden zurücksetzen
setTimeout(() => { setTimeout(() => {
icon.classList.remove('fa-check'); button.innerHTML = originalText;
icon.classList.add('fa-copy');
button.classList.remove('copied'); button.classList.remove('copied');
}, 2000); }, 2000);
}); });
@@ -626,5 +745,42 @@ function toggleTestResources() {
currentUrl.searchParams.set('show_test', showTest); currentUrl.searchParams.set('show_test', showTest);
window.location.href = currentUrl.toString(); window.location.href = currentUrl.toString();
} }
// Quarantäne verlängern
function extendQuarantine(resourceId) {
if (confirm('Möchten Sie die Quarantäne für diese Ressource verlängern?')) {
// TODO: Implementiere Modal für Quarantäne-Verlängerung
alert('Diese Funktion wird noch implementiert.');
}
}
// Fallback für Dropdowns
document.addEventListener('click', function(e) {
// Prüfe ob ein Dropdown-Toggle geklickt wurde
if (e.target.closest('.dropdown-toggle')) {
const button = e.target.closest('.dropdown-toggle');
const dropdown = button.nextElementSibling;
if (dropdown && dropdown.classList.contains('dropdown-menu')) {
// Toggle show class
dropdown.classList.toggle('show');
button.setAttribute('aria-expanded', dropdown.classList.contains('show'));
// Schließe andere Dropdowns
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
if (menu !== dropdown) {
menu.classList.remove('show');
menu.previousElementSibling.setAttribute('aria-expanded', 'false');
}
});
}
} else {
// Schließe alle Dropdowns wenn außerhalb geklickt
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
menu.classList.remove('show');
menu.previousElementSibling.setAttribute('aria-expanded', 'false');
});
}
});
</script> </script>
{% endblock %} {% endblock %}