from django.db import transaction from django.conf import settings from django.contrib.auth import get_user_model from rest_framework.exceptions import ValidationError from modules.fleet.tasks import send_email_task from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord, VehicleNotification, VehicleServicePhoto from infrastructure.pusher_service import PusherService import logging logger = logging.getLogger(__name__) User = get_user_model() class NotificationService: @staticmethod def create_notification(recipient=None, title="", message="", level="info", send_email=False, metadata=None): # 1. Kreiraj notifikaciju u DB-u notif = VehicleNotification.objects.create( recipient=recipient, title=title[:255], message=message, metadata=metadata or {}, level=level, is_sent=False ) # 2. Broadcast preko WebSocket (Pusher) tek nakon uspješnog commit-a if recipient: transaction.on_commit(lambda: PusherService.broadcast_notification( user_id=recipient.id, title=title, message=message, level=level, notification_id=notif.id, metadata=metadata )) # 3. Slanje emaila ako je traženo if send_email and recipient and getattr(recipient, 'email', None): email_args = { 'recipient_email': recipient.email, 'subject': title, 'message': message, 'notif_id': notif.id } if settings.DEBUG or getattr(settings, 'CELERY_TASK_ALWAYS_EAGER', False): send_email_task(**email_args) else: transaction.on_commit(lambda: send_email_task.delay(**email_args)) return notif class VehicleService: @staticmethod def create_vehicle(data: dict) -> Vehicle: vehicle = Vehicle(**data) try: vehicle.full_clean() except Exception as e: raise ValidationError({"detail": str(e)}) vehicle.save() return vehicle @staticmethod def update_vehicle(instance: Vehicle, data: dict) -> Vehicle: for k, v in data.items(): setattr(instance, k, v) try: instance.full_clean() except Exception as e: raise ValidationError({"detail": str(e)}) instance.save() return instance @staticmethod def create_work_order(data: dict) -> WorkOrder: # expecting creator in data wo = WorkOrder(**data) try: wo.full_clean() except Exception as e: raise ValidationError({"detail": str(e)}) with transaction.atomic(): wo.save() # obavijesti creatora i serviser-a vozila try: # Notify the creator NotificationService.create_notification( recipient=wo.creator, title=f"Putni nalog kreiran: WO-{wo.pk}", message=f"Putni nalog za dizalicu {wo.vehicle.registration_number} je kreiran.", metadata={ "entity_type": "work_order", "work_order_id": str(wo.pk), "vehicle_id": str(wo.vehicle_id), "crane_id": str(wo.vehicle_id), "section": "work-orders", }, level="info", send_email=False ) # Notify assigned servicer if present servicer = wo.vehicle.assigned_servicer if servicer: NotificationService.create_notification( recipient=servicer, title=f"Novi putni nalog za {wo.vehicle.registration_number}", message=f"Imate novi putni nalog (WO-{wo.pk}) za dizalicu {wo.vehicle.registration_number}.", metadata={ "entity_type": "work_order", "work_order_id": str(wo.pk), "vehicle_id": str(wo.vehicle_id), "crane_id": str(wo.vehicle_id), "section": "work-orders", }, level="info", send_email=False ) except Exception: logger.exception("Greška pri slanju notifikacija za create_work_order") return wo @staticmethod def update_work_order(instance: WorkOrder, data: dict) -> WorkOrder: for k, v in data.items(): setattr(instance, k, v) try: instance.full_clean() except Exception as e: raise ValidationError({"detail": str(e)}) with transaction.atomic(): instance.save() # Ako je nalog zatvoren, obavijestimo kreatora/servisera if instance.status == 'closed': try: NotificationService.create_notification( recipient=instance.creator, title=f"Putni nalog zatvoren: WO-{instance.pk}", message=f"Putni nalog za dizalicu {instance.vehicle.registration_number} je zatvoren.", metadata={ "entity_type": "work_order", "work_order_id": str(instance.pk), "vehicle_id": str(instance.vehicle_id), "crane_id": str(instance.vehicle_id), "section": "work-orders", }, level="info", send_email=False ) servicer = instance.vehicle.assigned_servicer if servicer: NotificationService.create_notification( recipient=servicer, title=f"Putni nalog zatvoren za {instance.vehicle.registration_number}", message=f"Putni nalog (WO-{instance.pk}) je zatvoren.", metadata={ "entity_type": "work_order", "work_order_id": str(instance.pk), "vehicle_id": str(instance.vehicle_id), "crane_id": str(instance.vehicle_id), "section": "work-orders", }, level="info", send_email=False ) except Exception: logger.exception("Greška pri slanju notifikacija za update_work_order") return instance @staticmethod def delete_work_order(instance: WorkOrder, user=None) -> WorkOrder: """ Soft-delete putnog naloga. Samo creator ili staff može obrisati. Vraća deaktivirani instance. """ # Provjera prava: samo creator ili staff if not user: raise ValidationError("Korisnik nije naveden za operaciju brisanja.") if not (user.is_staff or instance.creator == user): raise ValidationError("Nemate dozvolu za brisanje ovog putnog naloga.") instance.is_active = False instance.save(update_fields=['is_active']) # Notifikacija o brisanju try: NotificationService.create_notification( recipient=instance.creator, title=f"Putni nalog obrisan: WO-{instance.pk}", message=f"Putni nalog za dizalicu {instance.vehicle.registration_number} je obrisan od strane {user}.", metadata={ "entity_type": "work_order", "work_order_id": str(instance.pk), "vehicle_id": str(instance.vehicle_id), "crane_id": str(instance.vehicle_id), "section": "work-orders", "deleted": True, }, level="warning", send_email=False ) servicer = instance.vehicle.assigned_servicer if servicer: NotificationService.create_notification( recipient=servicer, title=f"Putni nalog obrisan za {instance.vehicle.registration_number}", message=f"Putni nalog (WO-{instance.pk}) je obrisan.", metadata={ "entity_type": "work_order", "work_order_id": str(instance.pk), "vehicle_id": str(instance.vehicle_id), "crane_id": str(instance.vehicle_id), "section": "work-orders", "deleted": True, }, level="warning", send_email=False ) except Exception: logger.exception("Greška pri slanju notifikacija za delete_work_order") logger.info(f"WorkOrder {instance.pk} soft-deleted by {user}") return instance @staticmethod def create_service_record(data: dict) -> VehicleServiceRecord: rec = VehicleServiceRecord(**data) try: rec.full_clean() except Exception as e: raise ValidationError({"detail": str(e)}) with transaction.atomic(): rec.save() # update vehicle mileage if needed v = rec.vehicle if rec.mileage and rec.mileage > (v.current_mileage or 0): v.current_mileage = rec.mileage v.save() # notify assigned servicer / creator try: # Notify performed_by user NotificationService.create_notification( recipient=rec.performed_by, title=f"Servis zabilježen: {rec.vehicle.registration_number}", message=( f"Zabilježen servis na dizalici {rec.vehicle.registration_number}" + (f" pri kilometraži {rec.mileage}." if rec.mileage is not None else ".") ), metadata={ "entity_type": "service_record", "service_record_id": str(rec.pk), "vehicle_id": str(rec.vehicle_id), "crane_id": str(rec.vehicle_id), "section": "service-records", }, level="info", send_email=False ) # Notify vehicle assigned servicer if different servicer = rec.vehicle.assigned_servicer if servicer and servicer != rec.performed_by: NotificationService.create_notification( recipient=servicer, title=f"Servis na vašem vozilu {rec.vehicle.registration_number}", message=f"Servis je izvršen na dizalici {rec.vehicle.registration_number}.", metadata={ "entity_type": "service_record", "service_record_id": str(rec.pk), "vehicle_id": str(rec.vehicle_id), "crane_id": str(rec.vehicle_id), "section": "service-records", }, level="info", send_email=False ) # If next service is soon (e.g., within 1000 km), create a warning notification next_due = rec.next_service_due_at if next_due is None and rec.mileage is not None: next_due = rec.mileage + rec.vehicle.service_interval_km if next_due is not None and rec.mileage is not None and (next_due - rec.mileage) <= 1000: if servicer: NotificationService.create_notification( recipient=servicer, title=f"Uskoro servis za {rec.vehicle.registration_number}", message=f"Sljedeći servis za dizalicu {rec.vehicle.registration_number} očekuje se na ~{next_due} km.", metadata={ "entity_type": "service_record", "service_record_id": str(rec.pk), "vehicle_id": str(rec.vehicle_id), "crane_id": str(rec.vehicle_id), "section": "service-records", "next_service_due_at": next_due, }, level="warning", send_email=False ) except Exception: logger.exception("Greška pri slanju notifikacija za create_service_record") return rec class VehicleServicePhotoService: """ Servisna klasa za upravljanje VehicleServicePhoto zapisima. Delegira upload, ažuriranje i brisanje s validacijom i sigurnosnim provjerama. """ @staticmethod def upload_photo(data: dict, uploaded_by=None) -> VehicleServicePhoto: """ Kreiraj VehicleServicePhoto s file upload-om. data treba sadržavati: service_record (ID ili instancu), image (UploadedFile), description (opcionalno). """ if not uploaded_by: raise ValidationError("Korisnik je obavezan za upload fotografije.") # Ako je service_record ID ili UUID, dohvati instancu service_record = data.get('service_record') # Provjeri je li već VehicleServiceRecord instanca if not isinstance(service_record, VehicleServiceRecord): # Pokušaj dohvatiti po ID-u (int, str, UUID) try: service_record = VehicleServiceRecord.objects.get(pk=service_record) except (VehicleServiceRecord.DoesNotExist, ValueError, TypeError): raise ValidationError("Servisni zapis ne postoji ili ID nije valjan.") # Kreiraj novi dict s istim podacima ali sigurno postavljenim service_record photo_data = { 'service_record': service_record, 'image': data.get('image'), 'description': data.get('description', ''), 'uploaded_by': uploaded_by } photo = VehicleServicePhoto(**photo_data) try: photo.full_clean() except Exception as e: raise ValidationError({"detail": str(e)}) photo.save() logger.info(f"Fotografija uploadana od strane {uploaded_by} za servisni zapis {service_record.id}") return photo @staticmethod def update_photo(instance: VehicleServicePhoto, data: dict, user=None) -> VehicleServicePhoto: """ Ažuriraj VehicleServicePhoto. Samo uploader ili staff mogu ažurirati. """ # Sigurnosna provjera: samo uploader ili staff mogu ažurirati if not (user and (user.is_staff or instance.uploaded_by == user)): raise ValidationError("Nemate dozvolu za ažuriranje ove fotografije.") for attr, value in data.items(): if attr != 'image': # ne dozvoli ponovno upload slike (opciono) setattr(instance, attr, value) try: instance.full_clean() except Exception as e: raise ValidationError({"detail": str(e)}) instance.save() logger.info(f"Fotografija {instance.id} ažurirana od strane {user}") return instance @staticmethod def delete_photo(instance: VehicleServicePhoto, user=None) -> VehicleServicePhoto: """ Soft-delete VehicleServicePhoto. Samo uploader ili staff mogu obrisati. """ if not (user and (user.is_staff or instance.uploaded_by == user)): raise ValidationError("Nemate dozvolu za brisanje ove fotografije.") instance.is_active = False instance.save(update_fields=['is_active']) logger.info(f"Fotografija {instance.id} soft-deleted od strane {user}") return instance class FleetService: @staticmethod def bulk_close_work_orders(work_orders_qs): updated_vehicles = {} with transaction.atomic(): for wo in work_orders_qs: # 1. Preskoči ako je već zatvoren if wo.status == 'closed': continue # 2. NOVO: Preskoči ako nalog nije kompletan (nema kilometražu) if wo.end_mileage is None: continue # 3. Zatvori nalog wo.status = 'closed' wo.save() # 4. Ažuriraj vozilo vehicle = wo.vehicle current_max = updated_vehicles.get(vehicle.pk, vehicle.current_mileage) if wo.end_mileage > current_max: updated_vehicles[vehicle.pk] = wo.end_mileage # Masovno ažuriranje vozila for v_id, new_mileage in updated_vehicles.items(): Vehicle.objects.filter(pk=v_id).update(current_mileage=new_mileage)