Export und Aktion gefixt
Dieser Commit ist enthalten in:
@@ -3192,11 +3192,12 @@ def export_resources():
|
||||
|
||||
# SQL Query mit Filtern
|
||||
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.type as license_type
|
||||
l.license_type
|
||||
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
|
||||
WHERE 1=1
|
||||
"""
|
||||
@@ -3204,11 +3205,11 @@ def export_resources():
|
||||
|
||||
# Filter für Testdaten
|
||||
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
|
||||
if filter_type:
|
||||
query += " AND r.type = %s"
|
||||
query += " AND r.resource_type = %s"
|
||||
params.append(filter_type)
|
||||
|
||||
# Filter für Status
|
||||
@@ -3218,7 +3219,7 @@ def export_resources():
|
||||
|
||||
# Suchfilter
|
||||
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}%'])
|
||||
|
||||
query += " ORDER BY r.id DESC"
|
||||
|
||||
@@ -165,6 +165,33 @@
|
||||
margin-bottom: 1rem;
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -292,13 +319,17 @@
|
||||
<h5 class="mb-0">📋 Ressourcen-Liste</h5>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<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
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/export/resources?format=excel&type={{ filter_type }}&status={{ filter_status }}&search={{ search_query }}&show_test={{ show_test }}">
|
||||
<ul class="dropdown-menu" aria-labelledby="exportDropdown">
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -318,7 +349,7 @@
|
||||
<th width="140">Status</th>
|
||||
<th>Zugewiesen an</th>
|
||||
<th width="180">Letzte Änderung</th>
|
||||
<th width="140" class="text-center">Aktionen</th>
|
||||
<th width="200" class="text-center">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -343,7 +374,7 @@
|
||||
<code class="me-2">{{ resource[2] }}</code>
|
||||
<button class="copy-btn" onclick="copyToClipboard('{{ resource[2] }}', this)"
|
||||
title="Kopieren">
|
||||
<i class="fas fa-copy"></i>
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -394,29 +425,96 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('resource_history', resource_id=resource[0]) }}"
|
||||
class="btn btn-action btn-sm btn-outline-info"
|
||||
title="Historie anzeigen">
|
||||
<i class="fas fa-history"></i>
|
||||
</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;">
|
||||
{% if resource[3] == 'quarantine' %}
|
||||
<!-- Quick Action für Quarantäne -->
|
||||
<form method="post" action="/resources/release"
|
||||
style="display: inline-block; margin-right: 5px;">
|
||||
<input type="hidden" name="resource_ids[]" value="{{ resource[0] }}">
|
||||
<button type="submit"
|
||||
class="btn btn-action btn-sm btn-outline-success"
|
||||
title="Ressource freigeben">
|
||||
<i class="fas fa-check"></i>
|
||||
class="btn btn-sm btn-success">
|
||||
<i class="bi bi-check-circle"></i> Freigeben
|
||||
</button>
|
||||
</form>
|
||||
{% 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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -425,7 +523,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<i class="bi bi-inbox"></i>
|
||||
<h4>Keine Ressourcen gefunden</h4>
|
||||
<p>Ändern Sie Ihre Filterkriterien oder fügen Sie neue Ressourcen hinzu.</p>
|
||||
</div>
|
||||
@@ -440,13 +538,13 @@
|
||||
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
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>
|
||||
</li>
|
||||
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
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>
|
||||
</li>
|
||||
|
||||
@@ -464,13 +562,13 @@
|
||||
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
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>
|
||||
</li>
|
||||
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
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>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -568,6 +666,12 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Debug Bootstrap
|
||||
console.log('Bootstrap loaded:', typeof bootstrap !== 'undefined');
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
console.log('Bootstrap version:', bootstrap.Dropdown.VERSION);
|
||||
}
|
||||
|
||||
// Live-Filtering
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('filterForm');
|
||||
@@ -590,21 +694,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var tooltipList = tooltipTriggerList.map(function (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
|
||||
function copyToClipboard(text, button) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Button Icon ändern
|
||||
const icon = button.querySelector('i');
|
||||
icon.classList.remove('fa-copy');
|
||||
icon.classList.add('fa-check');
|
||||
// Original Text speichern
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="bi bi-check"></i> Kopiert!';
|
||||
button.classList.add('copied');
|
||||
|
||||
// Nach 2 Sekunden zurücksetzen
|
||||
setTimeout(() => {
|
||||
icon.classList.remove('fa-check');
|
||||
icon.classList.add('fa-copy');
|
||||
button.innerHTML = originalText;
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
});
|
||||
@@ -626,5 +745,42 @@ function toggleTestResources() {
|
||||
currentUrl.searchParams.set('show_test', showTest);
|
||||
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>
|
||||
{% endblock %}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren