Implement bulk delete functionality for item groups with UI enhancements
This commit is contained in:
Binary file not shown.
+123
-47
@@ -4148,6 +4148,79 @@ def duplicate_item():
|
||||
return jsonify({'success': False, 'message': 'Serverfehler beim Duplizieren'}), 500
|
||||
|
||||
|
||||
def _soft_delete_item_groups(db, root_item_ids, username):
|
||||
"""Soft-delete one or more item groups and their borrow records."""
|
||||
now = datetime.datetime.now()
|
||||
unique_group_item_ids = []
|
||||
seen_ids = set()
|
||||
|
||||
for root_item_id in root_item_ids:
|
||||
for group_item_id in it.get_group_item_ids(root_item_id):
|
||||
if group_item_id not in seen_ids:
|
||||
seen_ids.add(group_item_id)
|
||||
unique_group_item_ids.append(group_item_id)
|
||||
|
||||
if not unique_group_item_ids:
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'Element nicht gefunden.',
|
||||
'soft_deleted_items': 0,
|
||||
'soft_deleted_borrows': 0,
|
||||
'group_item_ids': [],
|
||||
}
|
||||
|
||||
delete_success = True
|
||||
soft_deleted_items = 0
|
||||
|
||||
for group_item_id in unique_group_item_ids:
|
||||
result = db['items'].update_one(
|
||||
{'_id': ObjectId(group_item_id), 'Deleted': {'$ne': True}},
|
||||
{'$set': {
|
||||
'Deleted': True,
|
||||
'DeletedAt': now,
|
||||
'DeletedBy': username,
|
||||
'LastUpdated': now,
|
||||
'Verfuegbar': False,
|
||||
}}
|
||||
)
|
||||
if result.modified_count > 0:
|
||||
soft_deleted_items += 1
|
||||
elif result.matched_count == 0:
|
||||
delete_success = False
|
||||
|
||||
borrow_result = db['ausleihungen'].update_many(
|
||||
{
|
||||
'Item': {'$in': unique_group_item_ids},
|
||||
'Status': {'$ne': 'deleted'}
|
||||
},
|
||||
{'$set': {
|
||||
'Status': 'deleted',
|
||||
'DeletedAt': now,
|
||||
'DeletedBy': username,
|
||||
'LastUpdated': now,
|
||||
}}
|
||||
)
|
||||
|
||||
_append_audit_event(
|
||||
db,
|
||||
'inventory_item_soft_deleted',
|
||||
{
|
||||
'root_item_ids': root_item_ids,
|
||||
'group_item_ids': unique_group_item_ids,
|
||||
'soft_deleted_items': soft_deleted_items,
|
||||
'soft_deleted_borrow_records': borrow_result.modified_count,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
'success': delete_success,
|
||||
'soft_deleted_items': soft_deleted_items,
|
||||
'soft_deleted_borrows': borrow_result.modified_count,
|
||||
'group_item_ids': unique_group_item_ids,
|
||||
'message': 'OK' if delete_success else 'Fehler beim revisionssicheren Deaktivieren des Elements.',
|
||||
}
|
||||
|
||||
|
||||
@app.route('/delete_item/<id>', methods=['POST', 'GET'])
|
||||
def delete_item(id):
|
||||
"""
|
||||
@@ -4182,57 +4255,14 @@ def delete_item(id):
|
||||
flash('Element nicht gefunden.', 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
|
||||
# GoBD hardening: keep files and records immutable, use soft-delete markers only.
|
||||
delete_success = True
|
||||
soft_deleted_items = 0
|
||||
soft_deleted_borrows = 0
|
||||
now = datetime.datetime.now()
|
||||
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
|
||||
for group_item_id in group_item_ids:
|
||||
result = db['items'].update_one(
|
||||
{'_id': ObjectId(group_item_id), 'Deleted': {'$ne': True}},
|
||||
{'$set': {
|
||||
'Deleted': True,
|
||||
'DeletedAt': now,
|
||||
'DeletedBy': session.get('username', ''),
|
||||
'LastUpdated': now,
|
||||
'Verfuegbar': False,
|
||||
}}
|
||||
)
|
||||
if result.modified_count > 0:
|
||||
soft_deleted_items += 1
|
||||
elif result.matched_count == 0:
|
||||
delete_success = False
|
||||
|
||||
borrow_result = db['ausleihungen'].update_many(
|
||||
{
|
||||
'Item': {'$in': group_item_ids},
|
||||
'Status': {'$ne': 'deleted'}
|
||||
},
|
||||
{'$set': {
|
||||
'Status': 'deleted',
|
||||
'DeletedAt': now,
|
||||
'DeletedBy': session.get('username', ''),
|
||||
'LastUpdated': now,
|
||||
}}
|
||||
)
|
||||
soft_deleted_borrows = borrow_result.modified_count
|
||||
|
||||
_append_audit_event(
|
||||
db,
|
||||
'inventory_item_soft_deleted',
|
||||
{
|
||||
'root_item_id': id,
|
||||
'group_item_ids': group_item_ids,
|
||||
'soft_deleted_items': soft_deleted_items,
|
||||
'soft_deleted_borrow_records': soft_deleted_borrows,
|
||||
}
|
||||
)
|
||||
delete_result = _soft_delete_item_groups(db, [id], session.get('username', ''))
|
||||
delete_success = bool(delete_result.get('success'))
|
||||
soft_deleted_items = int(delete_result.get('soft_deleted_items', 0))
|
||||
soft_deleted_borrows = int(delete_result.get('soft_deleted_borrows', 0))
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error during soft-delete for item group {id}: {str(e)}")
|
||||
delete_success = False
|
||||
@@ -4248,6 +4278,52 @@ def delete_item(id):
|
||||
return redirect(url_for('home_admin'))
|
||||
|
||||
|
||||
@app.route('/bulk_delete_items', methods=['POST'])
|
||||
def bulk_delete_items():
|
||||
"""Soft-delete multiple selected item groups in one request."""
|
||||
if 'username' not in session:
|
||||
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
|
||||
if not us.check_admin(session['username']):
|
||||
return jsonify({'success': False, 'message': 'Administratorrechte erforderlich'}), 403
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
item_ids = payload.get('item_ids') or request.form.getlist('item_ids')
|
||||
if isinstance(item_ids, str):
|
||||
item_ids = [item_ids]
|
||||
item_ids = [str(item_id).strip() for item_id in item_ids if str(item_id).strip()]
|
||||
|
||||
if not item_ids:
|
||||
return jsonify({'success': False, 'message': 'Keine Elemente ausgewählt.'}), 400
|
||||
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
|
||||
result = _soft_delete_item_groups(db, item_ids, session.get('username', ''))
|
||||
if not result.get('success'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': result.get('message', 'Fehler beim Löschen.'),
|
||||
'deleted_items': result.get('soft_deleted_items', 0),
|
||||
'deleted_borrows': result.get('soft_deleted_borrows', 0),
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f"{result.get('soft_deleted_items', 0)} Elemente revisionssicher deaktiviert.",
|
||||
'deleted_items': result.get('soft_deleted_items', 0),
|
||||
'deleted_borrows': result.get('soft_deleted_borrows', 0),
|
||||
'group_item_ids': result.get('group_item_ids', []),
|
||||
})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error during bulk delete: {str(e)}")
|
||||
return jsonify({'success': False, 'message': 'Fehler beim Sammellöschen.'}), 500
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
@app.route('/edit_item/<id>', methods=['POST'])
|
||||
def edit_item(id):
|
||||
"""
|
||||
|
||||
@@ -26,6 +26,57 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bulk-delete-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid #f2c4c4;
|
||||
background: #fff8f8;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.bulk-delete-toolbar strong {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.bulk-delete-summary {
|
||||
color: #7a4a4a;
|
||||
font-size: 0.92rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bulk-delete-toolbar small {
|
||||
color: #7a4a4a;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bulk-delete-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bulk-delete-actions button {
|
||||
border: 1px solid #cfd8dc;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bulk-delete-actions .danger {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
border-color: #c82333;
|
||||
}
|
||||
|
||||
/* Filter styles */
|
||||
.filter-container {
|
||||
display: flex;
|
||||
@@ -237,6 +288,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-card.bulk-selected {
|
||||
border-color: #dc3545;
|
||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.18), 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
background: #fffdfd;
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
@@ -257,6 +314,31 @@
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.bulk-delete-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bulk-delete-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: #6b1f1f;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bulk-delete-toggle input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #dc3545;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-card .item-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
@@ -2112,6 +2194,18 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
</div>
|
||||
</h1>
|
||||
<div id="items-indicator" class="items-indicator">Objekte im System: 0</div>
|
||||
<div class="bulk-delete-toolbar">
|
||||
<div>
|
||||
<strong>Massenlöschung nach Kriterien</strong>
|
||||
<div class="bulk-delete-summary" id="bulk-delete-summary">0 Elemente ausgewählt</div>
|
||||
<small>Filter zuerst setzen, dann alle sichtbaren Elemente markieren oder einzelne Karten per Checkbox auswählen.</small>
|
||||
</div>
|
||||
<div class="bulk-delete-actions">
|
||||
<button type="button" onclick="selectVisibleItems()">Alle sichtbaren auswählen</button>
|
||||
<button type="button" onclick="clearBulkSelection()">Auswahl leeren</button>
|
||||
<button type="button" id="bulk-delete-button" class="danger" onclick="deleteSelectedItems()" disabled>Auswahl löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-container">
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
@@ -2492,6 +2586,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||
let currentUsername = '';
|
||||
let allItems = []; // Cache for all items
|
||||
let bulkDeleteSelection = new Set();
|
||||
const studentCardsModuleEnabled = {{ 'true' if student_cards_module_enabled else 'false' }};
|
||||
const studentDefaultBorrowDays = {{ student_default_borrow_days|default(14) }};
|
||||
const studentMaxBorrowDays = {{ student_max_borrow_days|default(365) }};
|
||||
@@ -2822,6 +2917,109 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
filter3: []
|
||||
};
|
||||
}
|
||||
|
||||
function updateBulkDeleteSummary() {
|
||||
const summary = document.getElementById('bulk-delete-summary');
|
||||
const button = document.getElementById('bulk-delete-button');
|
||||
const count = bulkDeleteSelection.size;
|
||||
|
||||
if (summary) {
|
||||
summary.textContent = `${count} ${count === 1 ? 'Element' : 'Elemente'} ausgewählt`;
|
||||
}
|
||||
if (button) {
|
||||
button.disabled = count === 0;
|
||||
button.textContent = count === 0 ? 'Auswahl löschen' : `Auswahl löschen (${count})`;
|
||||
}
|
||||
}
|
||||
|
||||
function setBulkDeleteSelection(itemId, checked) {
|
||||
const normalizedId = String(itemId || '').trim();
|
||||
if (!normalizedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
bulkDeleteSelection.add(normalizedId);
|
||||
} else {
|
||||
bulkDeleteSelection.delete(normalizedId);
|
||||
}
|
||||
|
||||
document.querySelectorAll(`.item-card[data-item-id='${normalizedId}']`).forEach(card => {
|
||||
card.classList.toggle('bulk-selected', checked);
|
||||
const checkbox = card.querySelector('.bulk-delete-checkbox');
|
||||
if (checkbox && checkbox.checked !== checked) {
|
||||
checkbox.checked = checked;
|
||||
}
|
||||
});
|
||||
|
||||
updateBulkDeleteSummary();
|
||||
}
|
||||
|
||||
function selectVisibleItems() {
|
||||
document.querySelectorAll('.item-card').forEach(card => {
|
||||
if (card.style.display === 'none') {
|
||||
return;
|
||||
}
|
||||
const itemId = String(card.dataset.itemId || '').trim();
|
||||
if (!itemId) {
|
||||
return;
|
||||
}
|
||||
bulkDeleteSelection.add(itemId);
|
||||
card.classList.add('bulk-selected');
|
||||
const checkbox = card.querySelector('.bulk-delete-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
|
||||
updateBulkDeleteSummary();
|
||||
}
|
||||
|
||||
function clearBulkSelection() {
|
||||
bulkDeleteSelection.clear();
|
||||
document.querySelectorAll('.item-card.bulk-selected').forEach(card => {
|
||||
card.classList.remove('bulk-selected');
|
||||
const checkbox = card.querySelector('.bulk-delete-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
updateBulkDeleteSummary();
|
||||
}
|
||||
|
||||
function deleteSelectedItems() {
|
||||
const selectedIds = [...bulkDeleteSelection];
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Bitte zuerst mindestens ein Element auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Wirklich ${selectedIds.length} ausgewählte Elemente revisionssicher deaktivieren?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('{{ url_for('bulk_delete_items') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ item_ids: selectedIds })
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || 'Fehler beim Sammellöschen.');
|
||||
}
|
||||
clearBulkSelection();
|
||||
alert(data.message || 'Auswahl gelöscht.');
|
||||
loadItems();
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.message || 'Fehler beim Sammellöschen.');
|
||||
});
|
||||
}
|
||||
|
||||
// Load predefined filter values for dropdowns
|
||||
function loadPredefinedFilterValues(filterNumber) {
|
||||
@@ -3148,6 +3346,12 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
${appointmentBadge}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="bulk-delete-row">
|
||||
<label class="bulk-delete-toggle">
|
||||
<input type="checkbox" class="bulk-delete-checkbox" data-item-id="${item._id}" ${bulkDeleteSelection.has(String(item._id)) ? 'checked' : ''} onchange="setBulkDeleteSelection('${item._id}', this.checked)">
|
||||
Für Löschung markieren
|
||||
</label>
|
||||
</div>
|
||||
${isAvailableForBorrow && !item.BlockedNow ?
|
||||
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
|
||||
${isGroupedItem ? `
|
||||
@@ -3225,6 +3429,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
itemsContainer.scrollLeft = 0;
|
||||
applyFilters();
|
||||
updateBulkDeleteSummary();
|
||||
|
||||
// Setup proper image display for all cards
|
||||
setupCardImagesDisplay();
|
||||
|
||||
Reference in New Issue
Block a user