diff --git a/app/routes/admin/system.py b/app/routes/admin/system.py index ed44480..c07e70d 100644 --- a/app/routes/admin/system.py +++ b/app/routes/admin/system.py @@ -121,47 +121,84 @@ def manual_lending(): # Verbrauchsmaterialien laden consumables = mongodb.find('consumables', {'deleted': {'$ne': True}}, sort=[('name', 1)]) - # Hole aktuelle Ausleihen + # Hole aktuelle Ausleihen (Optimiert mit Aggregation zur Vermeidung von N+1 Problemen) current_lendings = [] # Aktuelle Werkzeug-Ausleihen - active_tool_lendings = mongodb.find('lendings', {'returned_at': None}) - for lending in active_tool_lendings: - tool = mongodb.find_one('tools', {'barcode': lending['tool_barcode']}) - worker = mongodb.find_one('workers', {'barcode': lending['worker_barcode']}) - - if tool and worker: - current_lendings.append({ - 'item_name': tool['name'], - 'item_barcode': tool['barcode'], - 'worker_name': f"{worker['firstname']} {worker['lastname']}", - 'worker_barcode': worker['barcode'], - 'action_date': lending['lent_at'], - 'category': 'Werkzeug', - 'amount': None - }) + tool_lending_pipeline = [ + {'$match': {'returned_at': None}}, + { + '$lookup': { + 'from': 'tools', + 'localField': 'tool_barcode', + 'foreignField': 'barcode', + 'as': 'tool' + } + }, + {'$unwind': '$tool'}, + { + '$lookup': { + 'from': 'workers', + 'localField': 'worker_barcode', + 'foreignField': 'barcode', + 'as': 'worker' + } + }, + {'$unwind': '$worker'}, + { + '$project': { + 'item_name': '$tool.name', + 'item_barcode': '$tool.barcode', + 'worker_name': {'$concat': ['$worker.firstname', ' ', '$worker.lastname']}, + 'worker_barcode': '$worker.barcode', + 'action_date': '$lent_at', + 'category': {'$literal': 'Werkzeug'}, + 'amount': {'$literal': None} + } + } + ] + current_lendings.extend(mongodb.aggregate('lendings', tool_lending_pipeline)) # Aktuelle Verbrauchsmaterial-Ausgaben (letzte 30 Tage) thirty_days_ago = datetime.now() - timedelta(days=30) - recent_consumable_usages = mongodb.find('consumable_usages', { - 'used_at': {'$gte': thirty_days_ago}, - 'quantity': {'$lt': 0} # Nur Ausgaben (negative Werte), nicht Entnahmen - }) - - for usage in recent_consumable_usages: - consumable = mongodb.find_one('consumables', {'barcode': usage['consumable_barcode']}) - worker = mongodb.find_one('workers', {'barcode': usage['worker_barcode']}) - - if consumable and worker: - current_lendings.append({ - 'item_name': consumable['name'], - 'item_barcode': consumable['barcode'], - 'worker_name': f"{worker['firstname']} {worker['lastname']}", - 'worker_barcode': worker['barcode'], - 'action_date': usage['used_at'], - 'category': 'Verbrauchsmaterial', - 'amount': usage['quantity'] - }) + usage_pipeline = [ + { + '$match': { + 'used_at': {'$gte': thirty_days_ago}, + 'quantity': {'$lt': 0} # Nur Ausgaben (negative Werte), nicht Entnahmen + } + }, + { + '$lookup': { + 'from': 'consumables', + 'localField': 'consumable_barcode', + 'foreignField': 'barcode', + 'as': 'consumable' + } + }, + {'$unwind': '$consumable'}, + { + '$lookup': { + 'from': 'workers', + 'localField': 'worker_barcode', + 'foreignField': 'barcode', + 'as': 'worker' + } + }, + {'$unwind': '$worker'}, + { + '$project': { + 'item_name': '$consumable.name', + 'item_barcode': '$consumable.barcode', + 'worker_name': {'$concat': ['$worker.firstname', ' ', '$worker.lastname']}, + 'worker_barcode': '$worker.barcode', + 'action_date': '$used_at', + 'category': {'$literal': 'Verbrauchsmaterial'}, + 'amount': '$quantity' + } + } + ] + current_lendings.extend(mongodb.aggregate('consumable_usages', usage_pipeline)) # Sortiere nach Datum (neueste zuerst) def safe_date_key(lending): diff --git a/app/services/lending_service.py b/app/services/lending_service.py index 96110dd..60b069c 100755 --- a/app/services/lending_service.py +++ b/app/services/lending_service.py @@ -341,27 +341,39 @@ def _process_consumable_lending(item_barcode: str, worker_barcode: str, action: @staticmethod def get_active_lendings() -> list: - """Holt alle aktiven Ausleihen""" + """Holt alle aktiven Ausleihen (Optimiert mit Aggregation)""" try: - active_lendings = mongodb.find('lendings', {'returned_at': None}) - - # Erweitere mit Tool- und Worker-Informationen - enriched_lendings = [] - for lending in active_lendings: - tool = mongodb.find_one('tools', {'barcode': lending['tool_barcode']}) - worker = mongodb.find_one('workers', {'barcode': lending['worker_barcode']}) - - if tool and worker: - enriched_lendings.append({ - **lending, - 'tool_name': tool['name'], - 'worker_name': f"{worker['firstname']} {worker['lastname']}", - 'lent_at': lending['lent_at'] - }) - - # Sortiere nach Datum (neueste zuerst) - enriched_lendings.sort(key=lambda x: x.get('lent_at', datetime.min), reverse=True) - return enriched_lendings + pipeline = [ + {'$match': {'returned_at': None}}, + { + '$lookup': { + 'from': 'tools', + 'localField': 'tool_barcode', + 'foreignField': 'barcode', + 'as': 'tool_info' + } + }, + {'$unwind': '$tool_info'}, + { + '$lookup': { + 'from': 'workers', + 'localField': 'worker_barcode', + 'foreignField': 'barcode', + 'as': 'worker_info' + } + }, + {'$unwind': '$worker_info'}, + { + '$addFields': { + 'tool_name': '$tool_info.name', + 'worker_name': {'$concat': ['$worker_info.firstname', ' ', '$worker_info.lastname']} + } + }, + {'$project': {'tool_info': 0, 'worker_info': 0}}, + {'$sort': {'lent_at': -1}} + ] + + return mongodb.aggregate('lendings', pipeline) except Exception as e: logger.error(f"Fehler beim Laden aktiver Ausleihen: [Interner Fehler]") @@ -369,28 +381,40 @@ def get_active_lendings() -> list: @staticmethod def get_recent_consumable_usage(limit: int = 10) -> list: - """Holt die letzten Verbrauchsmaterial-Entnahmen""" + """Holt die letzten Verbrauchsmaterial-Entnahmen (Optimiert mit Aggregation)""" try: - recent_usages = mongodb.find('consumable_usages') - # Sortiere und limitiere - recent_usages.sort(key=lambda x: x.get('used_at', datetime.min), reverse=True) - recent_usages = recent_usages[:limit] - - # Erweitere mit Consumable- und Worker-Informationen - enriched_usages = [] - for usage in recent_usages: - consumable = mongodb.find_one('consumables', {'barcode': usage['consumable_barcode']}) - worker = mongodb.find_one('workers', {'barcode': usage['worker_barcode']}) - - if consumable and worker: - enriched_usages.append({ - 'consumable_name': consumable['name'], - 'quantity': usage['quantity'], - 'worker_name': f"{worker['firstname']} {worker['lastname']}", - 'used_at': usage['used_at'] - }) - - return enriched_usages + pipeline = [ + {'$sort': {'used_at': -1}}, + {'$limit': limit}, + { + '$lookup': { + 'from': 'consumables', + 'localField': 'consumable_barcode', + 'foreignField': 'barcode', + 'as': 'consumable' + } + }, + {'$unwind': '$consumable'}, + { + '$lookup': { + 'from': 'workers', + 'localField': 'worker_barcode', + 'foreignField': 'barcode', + 'as': 'worker' + } + }, + {'$unwind': '$worker'}, + { + '$project': { + 'consumable_name': '$consumable.name', + 'quantity': '$quantity', + 'worker_name': {'$concat': ['$worker.firstname', ' ', '$worker.lastname']}, + 'used_at': '$used_at' + } + } + ] + + return mongodb.aggregate('consumable_usages', pipeline) except Exception as e: logger.error(f"Fehler beim Laden der Verbrauchsmaterial-Entnahmen: [Interner Fehler]") @@ -399,7 +423,7 @@ def get_recent_consumable_usage(limit: int = 10) -> list: @staticmethod def get_worker_consumable_history(worker_barcode: str) -> List[Dict[str, Any]]: """ - Holt die Verbrauchsmaterial-Historie für einen Mitarbeiter + Holt die Verbrauchsmaterial-Historie für einen Mitarbeiter (Optimiert mit Aggregation) Args: worker_barcode: Barcode des Mitarbeiters @@ -408,33 +432,27 @@ def get_worker_consumable_history(worker_barcode: str) -> List[Dict[str, Any]]: List[Dict]: Liste der Verbrauchsmaterial-Ausgaben """ try: - # Hole alle Verbrauchsmaterial-Ausgaben des Mitarbeiters - usages = mongodb.find('consumable_usages', {'worker_barcode': worker_barcode}) - - # Erweitere mit Consumable-Informationen - enriched_usages = [] - for usage in usages: - consumable = mongodb.find_one('consumables', {'barcode': usage['consumable_barcode']}) - if consumable: - usage['consumable_name'] = consumable.get('name', '') - usage['consumable_barcode'] = usage['consumable_barcode'] - enriched_usages.append(usage) - - # Sortiere nach Datum (neueste zuerst) - def safe_date_key(usage): - used_at = usage.get('used_at') - if isinstance(used_at, str): - try: - return datetime.strptime(used_at, '%Y-%m-%d %H:%M:%S') - except (ValueError, TypeError): - return datetime.min - elif isinstance(used_at, datetime): - return used_at - else: - return datetime.min - - enriched_usages.sort(key=safe_date_key, reverse=True) - return enriched_usages + pipeline = [ + {'$match': {'worker_barcode': worker_barcode}}, + { + '$lookup': { + 'from': 'consumables', + 'localField': 'consumable_barcode', + 'foreignField': 'barcode', + 'as': 'consumable_info' + } + }, + {'$unwind': '$consumable_info'}, + { + '$addFields': { + 'consumable_name': '$consumable_info.name' + } + }, + {'$project': {'consumable_info': 0}}, + {'$sort': {'used_at': -1}} + ] + + return mongodb.aggregate('consumable_usages', pipeline) except Exception as e: logger.error(f"Fehler beim Laden der Verbrauchsmaterial-Historie: [Interner Fehler]") diff --git a/app/services/statistics_service.py b/app/services/statistics_service.py index 01d7f4f..0bd398d 100755 --- a/app/services/statistics_service.py +++ b/app/services/statistics_service.py @@ -93,19 +93,42 @@ def _get_ticket_statistics() -> Dict[str, int]: @staticmethod def _get_overdue_loans() -> List[Dict[str, Any]]: - """Findet alle überfälligen Ausleihen""" + """Findet alle überfälligen Ausleihen (Optimiert mit Aggregation zur Vermeidung von N+1 Problemen)""" try: today = datetime.now().date() - # Finde alle aktiven Ausleihen mit Rückgabedatum - active_loans = list(mongodb.find('lendings', { - 'returned_at': None, - 'expected_return_date': {'$exists': True, '$ne': None} - })) + # Verwende Aggregation zum Beziehen aller Daten in einem Rutsch + pipeline = [ + { + '$match': { + 'returned_at': None, + 'expected_return_date': {'$exists': True, '$ne': None} + } + }, + { + '$lookup': { + 'from': 'tools', + 'localField': 'tool_barcode', + 'foreignField': 'barcode', + 'as': 'tool_info' + } + }, + {'$unwind': {'path': '$tool_info', 'preserveNullAndEmptyArrays': True}}, + { + '$lookup': { + 'from': 'workers', + 'localField': 'worker_barcode', + 'foreignField': 'barcode', + 'as': 'worker_info' + } + }, + {'$unwind': {'path': '$worker_info', 'preserveNullAndEmptyArrays': True}} + ] + all_active_loans = mongodb.aggregate('lendings', pipeline) overdue_loans = [] - for loan in active_loans: + for loan in all_active_loans: expected_date = loan.get('expected_return_date') if not expected_date: continue @@ -113,20 +136,26 @@ def _get_overdue_loans() -> List[Dict[str, Any]]: # Konvertiere String zu datetime falls nötig if isinstance(expected_date, str): try: - expected_date = datetime.strptime(expected_date, '%Y-%m-%d') - except ValueError: + # Versuche verschiedene Formate + for fmt in ('%Y-%m-%d', '%Y-%m-%d %H:%M:%S'): + try: + expected_date = datetime.strptime(expected_date, fmt) + break + except ValueError: + continue + else: + continue + except Exception: continue # Prüfe ob überfällig - if expected_date.date() < today: - # Hole Tool-Informationen - tool = mongodb.find_one('tools', {'barcode': loan.get('tool_barcode')}) + if hasattr(expected_date, 'date') and expected_date.date() < today: + tool = loan.get('tool_info', {}) + worker = loan.get('worker_info', {}) - # Hole Mitarbeiter-Informationen - worker = mongodb.find_one('workers', { - 'barcode': loan.get('worker_barcode'), - 'deleted': {'$ne': True} - }) + # Filter gelöschte Worker + if worker and worker.get('deleted') == True: + worker = {} # Berechne Tage überfällig days_overdue = (today - expected_date.date()).days @@ -134,7 +163,7 @@ def _get_overdue_loans() -> List[Dict[str, Any]]: overdue_loans.append({ 'tool_name': tool.get('name') if tool else 'Unbekanntes Werkzeug', 'tool_barcode': loan.get('tool_barcode'), - 'worker_name': f"{worker['firstname']} {worker['lastname']}" if worker else 'Unbekannt', + 'worker_name': f"{worker.get('firstname', '')} {worker.get('lastname', '')}".strip() if worker else 'Unbekannt', 'worker_barcode': loan.get('worker_barcode'), 'expected_return_date': expected_date, 'days_overdue': days_overdue,