# backend/modules/task_management/services.py from django.db import transaction, models from rest_framework.exceptions import ValidationError from django.core.exceptions import ValidationError as DjangoValidationError from django.contrib.auth import get_user_model import logging from .models import Task, TaskTemplate, TaskWorkEntry from .exceptions import OpenWorkOrderBlockingTaskClose from modules.fleet.models import VehicleServiceRecord logger = logging.getLogger(__name__) User = get_user_model() def _task_overload_recipients(task): recipients = [] if task.assigned_to_id: recipients.append(task.assigned_to) supervisors = User.objects.filter(is_active=True).filter( models.Q(is_staff=True) | (models.Q(is_team_member=True) & models.Q(is_serviser=False)) ) recipients.extend(supervisors) unique = {} for recipient in recipients: unique[str(recipient.id)] = recipient return list(unique.values()) class TaskService: @staticmethod def create_task_entry(data: dict, user=None) -> Task: """ Univerzalna metoda: prihvaća podatke i opcionalnog korisnika. """ payload = dict(data) template_id = payload.pop('template_id', None) if payload.get('status') == 'DONE' and (user and not user.is_staff): raise ValidationError({"status": "Samo admini mogu odmah označiti zadatak kao gotov."}) template = None if template_id: template = TaskTemplate.objects.filter(id=template_id, is_active=True).prefetch_related('entries').first() if not template: raise ValidationError({"template_id": "Odabrani template nije pronađen."}) payload['title'] = template.title payload['description'] = template.description payload['status'] = 'aktivan' payload.setdefault('work_order', None) title = (payload.get('title') or '').strip() if title and len(title) < 3: raise ValidationError({"title": "Naslov zadatka mora imati barem 3 znaka."}) if not title: raise ValidationError({"title": "Naslov zadatka je obavezan."}) if user and 'assigned_to' not in payload: payload['assigned_to'] = user entry = Task(**payload) try: entry.full_clean() except DjangoValidationError as exc: logger.error(f"Neuspješna validacija zadatka: {exc}") raise ValidationError(exc.message_dict if hasattr(exc, 'message_dict') else exc.messages) with transaction.atomic(): entry.save() TaskService._notify_if_servicer_overloaded(entry) if template: TaskService._create_work_entries_from_template(task=entry, template=template) TaskService._create_service_records_from_template(task=entry, template=template, user=user) logger.info(f"Zadatak uspješno kreiran: {entry.id}") return entry @staticmethod def _notify_if_servicer_overloaded(entry: Task): if not entry.assigned_to_id or not entry.scheduled_date: return if not getattr(entry.assigned_to, 'is_serviser', False): return task_count = Task.objects.filter( is_active=True, assigned_to=entry.assigned_to, scheduled_date=entry.scheduled_date, status__in=('aktivan', 'servis', 'spreman_za_zavrsetak'), ).count() if task_count <= 2: return from modules.fleet.services import NotificationService schedule_label = entry.scheduled_date.strftime('%d.%m.%Y') title = f"Preopterećenje rasporeda: {entry.assigned_to.get_full_name() or entry.assigned_to.email}" message = ( f"Serviseru {entry.assigned_to.get_full_name() or entry.assigned_to.email} " f"je dodijeljeno {task_count} zadatka za {schedule_label}. " f"Sustav je dopustio i dodatni zadatak." ) metadata = { "entity_type": "task_overload", "assigned_to_id": str(entry.assigned_to_id), "task_id": str(entry.id), "scheduled_date": entry.scheduled_date.isoformat(), "task_count": task_count, "section": "dashboard", } for recipient in _task_overload_recipients(entry): NotificationService.create_notification( recipient=recipient, title=title, message=message, level='warning', send_email=False, metadata=metadata, ) @staticmethod def _create_work_entries_from_template(*, task: Task, template: TaskTemplate): template_entries = list(template.entries.filter(is_active=True).order_by('sort_order', 'created_at')[:10]) for idx, source in enumerate(template_entries, start=1): TaskWorkEntry.objects.create( task=task, template_entry=source, sort_order=idx, title=source.title, work_description=source.work_description, active_faults=source.active_faults, explanation_hr=source.explanation_hr, ) @staticmethod def _create_service_records_from_template(*, task: Task, template: TaskTemplate, user): if not user: raise ValidationError({ "template_id": "Kreiranje taska iz templatea zahtijeva prijavljenog korisnika za servisne zapise." }) if not task.vehicle_id: raise ValidationError({ "vehicle": "Task iz templatea mora imati dodijeljenu dizalicu za generiranje servisnih zapisa." }) template_entries = list(template.entries.filter(is_active=True).order_by('sort_order', 'created_at')[:10]) for source in template_entries: description_parts = [] if source.work_description: description_parts.append(source.work_description.strip()) if source.explanation_hr: description_parts.append(f"Obrazloženje (HR): {source.explanation_hr.strip()}") if source.active_faults: description_parts.append(f"Aktivne greške: {source.active_faults.strip()}") description = "\n\n".join([part for part in description_parts if part]).strip() or source.title VehicleServiceRecord.objects.create( vehicle=task.vehicle, task=task, performed_by=user, service_title=source.title, description=description, parts=(source.active_faults or '').strip(), cost=0, ) @staticmethod def update_task_entry(instance, data, user, auto_close_work_order=False): if not user.is_staff and instance.assigned_to != user: raise ValidationError("Nemate dozvolu za izmjenu ovog zadatka.") target_status = data.get('status', instance.status) target_work_order = data.get('work_order', instance.work_order) closing_statuses = {'neaktivan', 'zavrsen'} if target_status in closing_statuses: if not target_work_order: raise ValidationError({"work_order": "Putni nalog je obavezan prije zatvaranja zadatka."}) if target_work_order.status == 'open': if not auto_close_work_order: work_order_label = f"WO-{str(target_work_order.id).split('-')[0].upper()}" raise OpenWorkOrderBlockingTaskClose(detail={ "code": "OPEN_WORK_ORDER_BLOCKING_TASK_CLOSE", "message": f"Putni nalog {work_order_label} je još otvoren. Želite li ga automatski zatvoriti?", "work_order_id": str(target_work_order.id), "work_order_label": work_order_label, }) if target_work_order.end_mileage is None: raise ValidationError({ "work_order": "Automatsko zatvaranje putnog naloga nije moguće bez završne kilometraže." }) from modules.fleet.services import VehicleService target_work_order = VehicleService.update_work_order(target_work_order, {"status": "closed"}) data['work_order'] = target_work_order for attr, value in data.items(): if attr == 'template_id': continue setattr(instance, attr, value) if 'title' in data and len(str(data.get('title') or '').strip()) < 3: raise ValidationError({"title": "Naslov zadatka mora imati barem 3 znaka."}) try: instance.full_clean() except DjangoValidationError as exc: raise ValidationError(exc.message_dict if hasattr(exc, 'message_dict') else exc.messages) instance.save() return instance