prije gemini provjere
This commit is contained in:
0
001.BACKEND/kalendar/__init__.py
Normal file
0
001.BACKEND/kalendar/__init__.py
Normal file
81
001.BACKEND/kalendar/admin.py
Normal file
81
001.BACKEND/kalendar/admin.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from .models import Dogadaj
|
||||
|
||||
@admin.register(Dogadaj)
|
||||
class DogadajAdmin(admin.ModelAdmin):
|
||||
# 1. Postavke liste (Dashboard pregled)
|
||||
list_display = ('prikaz_naslova', 'tip_ikona', 'pocetak', 'kraj', 'get_izvrsitelj', 'link_na_rn')
|
||||
list_filter = ('tip', 'pocetak', 'serviser_u_kalendaru')
|
||||
search_fields = ('naslov', 'opis', 'radni_nalog__broj_naloga', 'klijent__naziv')
|
||||
ordering = ('-pocetak',)
|
||||
|
||||
# 2. Optimizacija baze (Spriječava N+1 problem zbog @property-ja)
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related(
|
||||
'radni_nalog__izvrsitelj',
|
||||
'serviser_u_kalendaru',
|
||||
'klijent',
|
||||
'vozilo'
|
||||
)
|
||||
|
||||
# --- CUSTOM METODE ZA PRIKAZ ---
|
||||
|
||||
def prikaz_naslova(self, obj):
|
||||
"""Bojanje naslova prema tipu za brzu vizualnu orijentaciju"""
|
||||
boje = {
|
||||
'servis': '#2980b9', # Plava
|
||||
'isporuka': '#27ae60', # Zelena
|
||||
'sastanak': '#f39c12', # Narančasta
|
||||
'biljeska': '#7f8c8d', # Siva
|
||||
}
|
||||
boja = boje.get(obj.tip, 'black')
|
||||
return format_html('<span style="color: {}; font-weight: bold;">{}</span>', boja, obj.naslov)
|
||||
prikaz_naslova.short_description = "Naslov"
|
||||
|
||||
def tip_ikona(self, obj):
|
||||
"""Dodaje malu ikonu uz tip događaja"""
|
||||
ikone = {
|
||||
'servis': '🛠️',
|
||||
'isporuka': '🏗️',
|
||||
'sastanak': '🤝',
|
||||
'biljeska': '📝',
|
||||
}
|
||||
return format_html('{} {}', ikone.get(obj.tip, '📅'), obj.get_tip_display())
|
||||
tip_ikona.short_description = "Tip"
|
||||
|
||||
def get_izvrsitelj(self, obj):
|
||||
"""Prikazuje izvršitelja koristeći logiku iz model @property"""
|
||||
korisnik = obj.izvrsitelj
|
||||
if korisnik:
|
||||
return korisnik.get_full_name() or korisnik.username
|
||||
return format_html('<i style="color: #999;">Nije dodijeljeno</i>')
|
||||
get_izvrsitelj.short_description = "Izvršitelj (Serviser)"
|
||||
|
||||
def link_na_rn(self, obj):
|
||||
"""Prikazuje broj radnog naloga ako postoji"""
|
||||
if obj.radni_nalog:
|
||||
return format_html('<b>#{}</b>', obj.radni_nalog.broj_naloga)
|
||||
return "-"
|
||||
link_na_rn.short_description = "Radni Nalog"
|
||||
|
||||
# 3. Organizacija polja u formi (Fieldsets)
|
||||
fieldsets = (
|
||||
('Glavne informacije', {
|
||||
'fields': (('naslov', 'tip'), 'opis')
|
||||
}),
|
||||
('Vrijeme održavanja', {
|
||||
'fields': (('pocetak', 'kraj'),)
|
||||
}),
|
||||
('Poveznice sustava', {
|
||||
'fields': ('radni_nalog', 'klijent', 'vozilo'),
|
||||
'description': "Povežite događaj s klijentom ili radnim nalogom za bolju evidenciju."
|
||||
}),
|
||||
('Odgovornost', {
|
||||
'fields': ('serviser_u_kalendaru',),
|
||||
'description': "NAPOMENA: Ako je povezan Radni Nalog, sustav ignorira ovo polje i koristi servisera s naloga."
|
||||
}),
|
||||
)
|
||||
|
||||
# Postavljanje radnog naloga kao readonly ako želiš spriječiti ručno mijenjanje veza
|
||||
# readonly_fields = ('radni_nalog',)
|
||||
5
001.BACKEND/kalendar/apps.py
Normal file
5
001.BACKEND/kalendar/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KalendarConfig(AppConfig):
|
||||
name = 'kalendar'
|
||||
42
001.BACKEND/kalendar/migrations/0001_initial.py
Normal file
42
001.BACKEND/kalendar/migrations/0001_initial.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-22 03:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('fleet', '0001_initial'),
|
||||
('kupci', '0001_initial'),
|
||||
('operations', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Dogadaj',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('naslov', models.CharField(max_length=200)),
|
||||
('opis', models.TextField(blank=True)),
|
||||
('tip', models.CharField(choices=[('servis', 'Radni Nalog / Servis'), ('isporuka', 'Isporuka Stroja'), ('sastanak', 'Sastanak s kupcem'), ('biljeska', 'Interna bilješka')], default='biljeska', max_length=20)),
|
||||
('pocetak', models.DateTimeField()),
|
||||
('kraj', models.DateTimeField()),
|
||||
('datum_kreiranja', models.DateTimeField(auto_now_add=True)),
|
||||
('datum_azuriranja', models.DateTimeField(auto_now=True)),
|
||||
('klijent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='kupci.kupac')),
|
||||
('radni_nalog', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='kalendar_termin', to='operations.radninalog')),
|
||||
('serviser_u_kalendaru', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dodatni_zadaci', to=settings.AUTH_USER_MODEL)),
|
||||
('vozilo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='fleet.vozilo')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Kalendar',
|
||||
'verbose_name_plural': 'Kalendar',
|
||||
'ordering': ['pocetak'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
001.BACKEND/kalendar/migrations/__init__.py
Normal file
0
001.BACKEND/kalendar/migrations/__init__.py
Normal file
61
001.BACKEND/kalendar/models.py
Normal file
61
001.BACKEND/kalendar/models.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
class Dogadaj(models.Model):
|
||||
TIP_DOGADAJA = [
|
||||
('servis', 'Radni Nalog / Servis'),
|
||||
('isporuka', 'Isporuka Stroja'),
|
||||
('sastanak', 'Sastanak s kupcem'),
|
||||
('biljeska', 'Interna bilješka'),
|
||||
]
|
||||
|
||||
naslov = models.CharField(max_length=200)
|
||||
opis = models.TextField(blank=True)
|
||||
tip = models.CharField(max_length=20, choices=TIP_DOGADAJA, default='biljeska')
|
||||
|
||||
pocetak = models.DateTimeField()
|
||||
kraj = models.DateTimeField()
|
||||
|
||||
# Automatsko praćenje izmjena samog događaja u kalendaru
|
||||
datum_kreiranja = models.DateTimeField(auto_now_add=True)
|
||||
datum_azuriranja = models.DateTimeField(auto_now=True)
|
||||
|
||||
# Veze
|
||||
klijent = models.ForeignKey('kupci.Kupac', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
vozilo = models.ForeignKey('fleet.Vozilo', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
radni_nalog = models.OneToOneField(
|
||||
'operations.RadniNalog',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='kalendar_termin'
|
||||
)
|
||||
|
||||
# Serviser koji je dodijeljen direktno u kalendaru (za sastanke/bilješke)
|
||||
# Ako postoji radni_nalog, koristit ćemo izvrsitelja s naloga
|
||||
serviser_u_kalendaru = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='dodatni_zadaci'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Kalendar"
|
||||
verbose_name_plural = "Kalendar"
|
||||
ordering = ['pocetak']
|
||||
|
||||
@property
|
||||
def izvrsitelj(self):
|
||||
"""
|
||||
Pametna metoda koja vraća servisera:
|
||||
1. Ako je termin vezan za Radni Nalog, uzmi njegovog izvršitelja.
|
||||
2. Ako nije, uzmi servisera upisanog u kalendar.
|
||||
"""
|
||||
if self.radni_nalog:
|
||||
return self.radni_nalog.izvrsitelj
|
||||
return self.serviser_u_kalendaru
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.naslov} - {self.izvrsitelj}"
|
||||
77
001.BACKEND/kalendar/serializers.py
Normal file
77
001.BACKEND/kalendar/serializers.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Dogadaj
|
||||
import re
|
||||
|
||||
class DogadajSerializer(serializers.ModelSerializer):
|
||||
# 1. Mapiranje za standardne kalendarske knjižnice
|
||||
title = serializers.CharField(source='naslov')
|
||||
start = serializers.DateTimeField(source='pocetak')
|
||||
end = serializers.DateTimeField(source='kraj')
|
||||
|
||||
# 2. Dinamički podaci (korištenje @property-ja i Choice polja)
|
||||
izvrsitelj_ime = serializers.SerializerMethodField()
|
||||
tip_display = serializers.CharField(source='get_tip_display', read_only=True)
|
||||
|
||||
# 3. Dodatne informacije za UI
|
||||
boja = serializers.SerializerMethodField()
|
||||
je_radni_nalog = serializers.SerializerMethodField()
|
||||
|
||||
# 4. NOVA RAZDVOJENA POLJA ZA VOCILO I ČISTI OPIS KWARTA
|
||||
vozilo_naziv = serializers.SerializerMethodField()
|
||||
opis_cisti = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Dogadaj
|
||||
fields = [
|
||||
'id', 'title', 'start', 'end', 'tip', 'tip_display',
|
||||
'opis', 'opis_cisti', 'vozilo_naziv', 'izvrsitelj_ime',
|
||||
'boja', 'je_radni_nalog', 'klijent', 'vozilo', 'radni_nalog'
|
||||
]
|
||||
|
||||
def get_izvrsitelj_ime(self, obj):
|
||||
"""Dohvaća ime preko tvog @property izvrsitelj u modelu"""
|
||||
korisnik = obj.izvrsitelj
|
||||
if korisnik:
|
||||
return korisnik.get_full_name() or korisnik.username
|
||||
return "Nije dodijeljeno"
|
||||
|
||||
def get_boja(self, obj):
|
||||
"""Dodjeljuje boju ovisno o tipu događaja"""
|
||||
boje = {
|
||||
'servis': '#3498db', # Plava
|
||||
'isporuka': '#2ecc71', # Zelena
|
||||
'sastanak': '#f1c40f', # Žuta
|
||||
'biljeska': '#95a5a6', # Siva
|
||||
}
|
||||
return boje.get(obj.tip, '#34495e')
|
||||
|
||||
def get_je_radni_nalog(self, obj):
|
||||
"""Pomaže frontendu da zna treba li prikazati link na radni nalog"""
|
||||
return obj.radni_nalog is not None
|
||||
|
||||
def _parsiraj_spojeni_opis(self, obj):
|
||||
"""Pomoćna metoda koja analizira tekst i vraća (vozilo, opis)"""
|
||||
sirovi_tekst = obj.opis or ""
|
||||
|
||||
# Ako tekst sadrži ključne riječi, razdvajamo ga pomoću Regexa na backendu
|
||||
if "Vozilo:" in sirovi_tekst or "Opis kvara:" in sirovi_tekst:
|
||||
vozilo_match = re.search(r"Vozilo:\s*(.*?)(?=\s*Opis kvara:|$)", sirovi_tekst, re.IGNORECASE)
|
||||
opis_match = re.search(r"Opis kvara:\s*(.*)", sirovi_tekst, re.IGNORECASE)
|
||||
|
||||
vozilo = vozilo_match.group(1).strip() if vozilo_match else None
|
||||
opis = opis_match.group(1).strip() if opis_match else sirovi_tekst
|
||||
return vozilo, opis
|
||||
|
||||
# Ako je polje vozilo na modelu već popunjeno kao relacija/tekst, iskoristi ga
|
||||
model_vozilo = str(obj.vozilo) if obj.vozilo else None
|
||||
return model_vozilo, sirovi_tekst
|
||||
|
||||
def get_vozilo_naziv(self, obj):
|
||||
"""Vraća samo čisti naziv vozila"""
|
||||
vozilo, _ = self._parsiraj_spojeni_opis(obj)
|
||||
return vozilo
|
||||
|
||||
def get_opis_cisti(self, obj):
|
||||
"""Vraća samo čisti tekst opisa kvara"""
|
||||
_, opis = self._parsiraj_spojeni_opis(obj)
|
||||
return opis if opis else "Nema dodatnog opisa."
|
||||
52
001.BACKEND/kalendar/services.py
Normal file
52
001.BACKEND/kalendar/services.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from .models import Dogadaj
|
||||
|
||||
def kreiraj_kalendarski_unos(radni_nalog):
|
||||
"""
|
||||
Kreira termin u kalendaru na temelju novog radnog naloga.
|
||||
"""
|
||||
pocetak = timezone.now()
|
||||
kraj = pocetak + timedelta(hours=2)
|
||||
|
||||
# DOHVAĆANJE VOZILA: Budući da vozilo više nije na Radnom nalogu,
|
||||
# moramo ga izvući iz povezanog Putnog naloga (ako postoji).
|
||||
vozilo_obj = None
|
||||
vozilo_str = "Nije definirano"
|
||||
|
||||
if radni_nalog.putni_nalog and radni_nalog.putni_nalog.vozilo:
|
||||
vozilo_obj = radni_nalog.putni_nalog.vozilo
|
||||
vozilo_str = str(vozilo_obj)
|
||||
|
||||
# Kreiramo Dogadaj
|
||||
dogadaj = Dogadaj.objects.create(
|
||||
naslov=f"RN: {radni_nalog.broj_naloga} | {radni_nalog.klijent.naziv}",
|
||||
opis=f"Vozilo: {vozilo_str}\nOpis kvara: {radni_nalog.opis_kvara}",
|
||||
tip='servis',
|
||||
pocetak=pocetak,
|
||||
kraj=kraj,
|
||||
klijent=radni_nalog.klijent,
|
||||
# Ovdje spremamo instancu vozila u polje modela Dogadaj
|
||||
vozilo=vozilo_obj,
|
||||
radni_nalog=radni_nalog,
|
||||
serviser_u_kalendaru=radni_nalog.izvrsitelj
|
||||
)
|
||||
|
||||
return dogadaj
|
||||
|
||||
def azuriraj_kalendarski_unos(radni_nalog):
|
||||
"""
|
||||
Sinkronizira podatke u kalendaru koristeći metodu modela.
|
||||
"""
|
||||
try:
|
||||
# Pokušaj pronaći postojeći događaj preko related_name 'kalendar_termin'
|
||||
dogadaj = radni_nalog.kalendar_termin
|
||||
dogadaj.sync_with_radni_nalog()
|
||||
except Exception:
|
||||
# Ako ne postoji (npr. nalog je stariji od uvođenja kalendara), kreiraj ga
|
||||
from .services import kreiraj_kalendarski_unos
|
||||
kreiraj_kalendarski_unos(radni_nalog)
|
||||
|
||||
def obrisi_kalendarski_unos(radni_nalog):
|
||||
if hasattr(radni_nalog, 'kalendar_termin'):
|
||||
radni_nalog.kalendar_termin.delete()
|
||||
3
001.BACKEND/kalendar/tests.py
Normal file
3
001.BACKEND/kalendar/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
17
001.BACKEND/kalendar/urls.py
Normal file
17
001.BACKEND/kalendar/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import DogadajViewSet, MojRasporedView
|
||||
|
||||
# Kreiramo router i registriramo naš ViewSet za kalendarske događaje
|
||||
router = DefaultRouter()
|
||||
router.register(r'dogadaji', DogadajViewSet, basename='dogadaji')
|
||||
|
||||
# Definiramo URL obrasce
|
||||
urlpatterns = [
|
||||
# Sve CRUD rute koje router generira (npr. /kalendar/dogadaji/)
|
||||
path('', include(router.urls)),
|
||||
|
||||
# Specijalna ruta za "današnji raspored" servisera
|
||||
# Dostupna na: /kalendar/moj-raspored/
|
||||
path('moj-raspored/', MojRasporedView.as_view(), name='moj-raspored'),
|
||||
]
|
||||
55
001.BACKEND/kalendar/views.py
Normal file
55
001.BACKEND/kalendar/views.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from rest_framework import viewsets, generics, filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from .models import Dogadaj
|
||||
from .serializers import DogadajSerializer
|
||||
|
||||
class DogadajViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
Glavni API za kalendar. Podržava pregled svih termina,
|
||||
drag-and-drop (patch) i kreiranje novih bilješki.
|
||||
"""
|
||||
# VAŽNO: select_related('radni_nalog__izvrsitelj') je ključan
|
||||
# jer tvoj model koristi @property izvrsitelj.
|
||||
# Bez ovoga, svaki termin u kalendaru bi radio poseban upit u bazu.
|
||||
queryset = Dogadaj.objects.all().select_related(
|
||||
'radni_nalog__izvrsitelj',
|
||||
'radni_nalog__klijent',
|
||||
'klijent',
|
||||
'vozilo',
|
||||
'serviser_u_kalendaru'
|
||||
)
|
||||
serializer_class = DogadajSerializer
|
||||
|
||||
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
|
||||
filterset_fields = ['tip', 'klijent', 'vozilo']
|
||||
ordering = ['pocetak']
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Opcionalno: Ako želiš da serviseri vide samo svoje termine u kalendaru,
|
||||
otkomentiraj donji dio. Ako dispečer vidi sve, ostavi kako jest.
|
||||
"""
|
||||
qs = super().get_queryset()
|
||||
# if not self.request.user.is_staff:
|
||||
# return qs.filter(Q(radni_nalog__izvrsitelj=self.request.user) | Q(serviser_u_kalendaru=self.request.user))
|
||||
return qs
|
||||
|
||||
class MojRasporedView(generics.ListAPIView):
|
||||
"""
|
||||
Endpoint namijenjen mobilnoj aplikaciji: "Moji zadaci za danas".
|
||||
Pristup: GET /kalendar/api/moj-raspored/
|
||||
"""
|
||||
serializer_class = DogadajSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
danas = timezone.now().date()
|
||||
|
||||
# Filtriramo samo ono što je dodijeljeno ulogiranom korisniku za danas
|
||||
return Dogadaj.objects.filter(
|
||||
Q(pocetak__date=danas) | Q(kraj__date=danas)
|
||||
).filter(
|
||||
Q(radni_nalog__izvrsitelj=user) | Q(serviser_u_kalendaru=user)
|
||||
).select_related('radni_nalog', 'klijent', 'vozilo')
|
||||
Reference in New Issue
Block a user