Export und Aktion gefixt
Dieser Commit ist enthalten in:
@@ -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"
|
||||||
|
|||||||
@@ -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 %}
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren