feat: dodaj bilješke widget i admin kalendar/notifikacije
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

Uveden je novi Notes drawer s kalendar integracijom, zajednički DRY
left-slide mehanizam te međusobno isključivo prikazivanje s Kalendar
widgetom.

Backend i frontend su prošireni za servisne bilješke (uključujući slanje
na više servisera), dodan je users/servicers endpoint, te su uvedene
warning notifikacije kada serviser ima više od 2 zadatka isti dan.

U admin su dodane stavke za Bilješke i zaseban Kalendar zadataka (proxy
TaskCalendar) radi lakšeg operativnog pregleda.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-07-20 06:33:36 +02:00
parent 61996bd1a0
commit 1a18498b30
20 changed files with 1312 additions and 113 deletions

View File

@@ -2,11 +2,14 @@ from rest_framework import serializers
import re
from pathlib import Path
from decimal import Decimal, InvalidOperation
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from modules.task_management.models import Task
from .models import (
Crane,
Vehicle,
ServiceContextNote,
VehicleNotification,
WorkOrder,
WorkOrderAdditionalCostsTable,
@@ -18,6 +21,8 @@ from .models import (
)
from . import services
User = get_user_model()
class VehicleSerializer(serializers.ModelSerializer):
# Čitljivo ime klijenta — read-only, za prikaz u frontendu i adminu
@@ -95,6 +100,134 @@ class VehicleNotificationSerializer(serializers.ModelSerializer):
read_only_fields = ['id', 'title', 'message', 'metadata', 'level', 'is_sent', 'created_at']
# is_read is writable via the "mark_read" action, but normally read-only in list
class ServiceContextNoteSerializer(serializers.ModelSerializer):
created_by_name = serializers.SerializerMethodField()
recipient_name = serializers.SerializerMethodField()
work_order_label = serializers.SerializerMethodField()
task_title = serializers.SerializerMethodField()
class Meta:
model = ServiceContextNote
fields = [
'id',
'created_by',
'created_by_name',
'recipient',
'recipient_name',
'audience_source',
'work_order',
'work_order_label',
'task',
'task_title',
'note',
'note_date',
'is_closed',
'closed_at',
'created_at',
'updated_at',
]
read_only_fields = fields
def get_created_by_name(self, obj):
return obj.created_by.get_full_name() or obj.created_by.email
def get_recipient_name(self, obj):
return obj.recipient.get_full_name() or obj.recipient.email
def get_work_order_label(self, obj):
if not obj.work_order_id:
return None
display_code = str(getattr(obj.work_order, 'display_code', '') or '').strip().upper()
return display_code or f"WO-{obj.work_order_id}"
def get_task_title(self, obj):
if not obj.task_id:
return None
return obj.task.title
class ServiceContextNoteCreateSerializer(serializers.Serializer):
AUDIENCE_CHOICES = ['self', 'member', 'team']
note = serializers.CharField(required=True, allow_blank=False, trim_whitespace=True)
note_date = serializers.DateField(required=False, allow_null=True)
audience = serializers.ChoiceField(choices=AUDIENCE_CHOICES, default='self')
target_user = serializers.PrimaryKeyRelatedField(
queryset=User.objects.filter(is_active=True),
required=False,
allow_null=True,
)
target_users = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.filter(is_active=True)),
required=False,
allow_empty=False,
)
work_order = serializers.PrimaryKeyRelatedField(
queryset=WorkOrder.objects.filter(is_active=True),
required=False,
allow_null=True,
)
task = serializers.PrimaryKeyRelatedField(
queryset=Task.objects.filter(is_active=True),
required=False,
allow_null=True,
)
def validate(self, attrs):
request = self.context.get('request')
user = getattr(request, 'user', None)
if user is None:
raise serializers.ValidationError('Autentikacija je obavezna.')
audience = attrs.get('audience', 'self')
target_user = attrs.get('target_user')
target_users = attrs.get('target_users') or []
work_order = attrs.get('work_order')
task = attrs.get('task')
note = str(attrs.get('note', '') or '').strip()
can_manage_recipients = bool(
getattr(user, 'is_serviser', False)
or getattr(user, 'is_staff', False)
or (getattr(user, 'is_team_member', False) and not getattr(user, 'is_serviser', False))
)
if not note:
raise serializers.ValidationError({'note': 'Bilješka ne može biti prazna.'})
if not can_manage_recipients and audience != 'self':
raise serializers.ValidationError({'audience': 'Samo supervisor ili serviser može slati bilješke drugim članovima tima.'})
if audience == 'member' and not target_user and len(target_users) == 0:
raise serializers.ValidationError({'target_users': 'Odaberite barem jednog servisera.'})
if audience in {'self', 'team'} and target_user:
raise serializers.ValidationError({'target_user': 'Odabrani tip publike ne koristi ciljano polje člana tima.'})
if audience in {'self', 'team'} and len(target_users) > 0:
raise serializers.ValidationError({'target_users': 'Odabrani tip publike ne koristi listu servisera.'})
if target_user and not getattr(target_user, 'is_serviser', False):
raise serializers.ValidationError({'target_user': 'Bilješku je moguće poslati samo serviseru.'})
for selected_user in target_users:
if not getattr(selected_user, 'is_serviser', False):
raise serializers.ValidationError({'target_users': 'Bilješku je moguće poslati samo serviserima.'})
if work_order:
if not work_order.is_active or str(work_order.status or '').lower() != 'open':
raise serializers.ValidationError({'work_order': 'Moguće je povezati samo aktivni otvoreni putni nalog.'})
if task:
status = str(task.status or '').lower()
if status in {'zavrsen', 'neaktivan'}:
raise serializers.ValidationError({'task': 'Moguće je povezati samo aktivni radni zadatak.'})
if task and work_order and task.work_order_id and str(task.work_order_id) != str(work_order.id):
raise serializers.ValidationError({'task': 'Radni zadatak ne pripada odabranom putnom nalogu.'})
attrs['note'] = note
if task and not work_order and task.work_order_id:
attrs['work_order'] = task.work_order
if audience == 'member' and len(target_users) == 0 and target_user:
attrs['target_users'] = [target_user]
return attrs
class WorkOrderSerializer(serializers.ModelSerializer):
crane = serializers.SerializerMethodField()
crane_label = serializers.SerializerMethodField()