add from v002

This commit is contained in:
2026-05-20 21:24:00 +02:00
parent 1a6ba86d0b
commit 83fb8c69bb
98 changed files with 10878 additions and 0 deletions

6
001.BACKEND/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/env/Scripts/python.exe",
"python.analysis.extraPaths": [
"${workspaceFolder}"
]
}

View File

16
001.BACKEND/core/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for core project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,209 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 6.0.5.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
import os
from pathlib import Path
from datetime import timedelta
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv(
'DJANGO_SECRET_KEY',
'django-insecure-!228(8gy#3a7l-@_^g1s4bipj&@*+_415+ulx0^-9jw(%ksdvy',
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DJANGO_DEBUG', 'True').lower() in ('1', 'true', 'yes', 'on')
# Custom user model
AUTH_USER_MODEL = 'users.CustomUser'
ALLOWED_HOSTS = [
host.strip()
for host in os.getenv('DJANGO_ALLOWED_HOSTS', '127.0.0.1,localhost').split(',')
if host.strip()
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# plugins
'fleet',
'corsheaders',
'django_filters',
'rest_framework_simplejwt',
'rest_framework',
# Aplikacije
'users',
'kupci',
'operations',
'kalendar',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # Mora biti prvi!
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Europe/Zagreb'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
# Automatski koristi filtriranje na svim API endpointima koji podržavaju filtriranje
if DEBUG:
# Razvojni način: Svatko može čitati i pisati bez tokena
DEFAULT_PERMISSION_CLASSES = [
'rest_framework.permissions.AllowAny',
]
else:
# Produkcijski način: Pristup samo uz ispravan JWT token
DEFAULT_PERMISSION_CLASSES = [
'rest_framework.permissions.IsAuthenticated',
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend'
],
# JWT Autentikacija
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
# Postavi da su po defaultu svi API-ji zaključani (samo za prijavljene)
'DEFAULT_PERMISSION_CLASSES': DEFAULT_PERMISSION_CLASSES
}
# Podesiti koliko dugo vrijede JWT tokeni (npr. 1 sat za pristup, 30 dana za refresh)
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'AUTH_HEADER_TYPES': ('Bearer',),
}
# Media files (npr. slike vozila)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# CORS postavke - dozvoli frontend aplikaciji da komunicira s backendom
if DEBUG:
CORS_ALLOW_ALL_ORIGINS = True
else:
CORS_ALLOWED_ORIGINS = [
"http://localhost:4321", # Ovo je adresa na kojoj će Astro frontend biti dostupan
"http://127.0.0.1:4321",
]
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
CORS_ALLOW_HEADERS = [
"accept",
"authorization",
"content-type",
"user-agent",
"x-csrftoken",
"x-requested-with",
]

43
001.BACKEND/core/urls.py Normal file
View File

@@ -0,0 +1,43 @@
"""
URL configuration for core project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
# Admin panel
path('admin/', admin.site.urls),
# API endpointi
path('api/kupci/', include('kupci.urls')),
path('api/fleet/', include('fleet.urls')),
path('api/operativa/', include('operations.urls')),
path('api/kalendar/', include('kalendar.urls')),
path('api/users/', include('users.urls')),
# Endpoint za dobivanje tokena (Login)
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
# Endpoint za osvježavanje tokena (da se ne mora stalno logirati)
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
# Dodajemo pristup media datotekama samo u development fazi
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
001.BACKEND/core/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for core project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()

View File

View File

@@ -0,0 +1,90 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Stroj, Vozilo
from operations.models import RadniNalog, PutniNalog # Importamo modele iz druge aplikacije
# --- INLINE KLASE ---
class PutniNalogInline(admin.TabularInline):
"""Prikazuje putne naloge unutar pregleda vozila."""
model = PutniNalog
extra = 0
fields = ('broj_naloga', 'datum_izdavanja', 'korisnik', 'pocetna_km', 'zavrsna_km', 'status')
readonly_fields = ('broj_naloga', 'datum_izdavanja', 'korisnik', 'pocetna_km', 'zavrsna_km', 'status')
can_delete = False
verbose_name = "Povijest putovanja"
verbose_name_plural = "Povijest putovanja za ovo vozilo"
class RadniNalogInline(admin.TabularInline):
model = RadniNalog
extra = 0
# Umjesto običnog polja 'broj_naloga', koristimo metodu 'link_na_nalog'
fields = ('link_na_nalog', 'datum_kreiranja', 'klijent', 'izvrsitelj', 'status')
readonly_fields = ('link_na_nalog', 'datum_kreiranja', 'klijent', 'izvrsitelj', 'status')
can_delete = False
def link_na_nalog(self, obj):
if obj.id:
# Generiramo URL za admin promjenu Radnog Naloga
url = reverse('admin:operations_radninalog_change', args=[obj.id])
return mark_safe(f'<a href="{url}" style="font-weight:bold;">RN-{obj.broj_naloga}</a>')
return "-"
link_na_nalog.short_description = 'Broj naloga'
# --- GLAVNE ADMIN KLASE ---
@admin.register(Vozilo)
class VoziloAdmin(admin.ModelAdmin):
list_display = ('naziv', 'registracija', 'status', 'pocetni_kilometri', 'trenutni_kilometri')
search_fields = ('naziv', 'registracija')
list_filter = ('status',)
ordering = ('naziv',)
# Dodajemo inline za putne naloge
inlines = [PutniNalogInline]
@admin.register(Stroj)
class StrojAdmin(admin.ModelAdmin):
list_display = (
'serijski_broj',
'prikaz_naziva',
'vlasnik',
'tip',
'radni_sati',
'registracija',
'datum_zadnjeg_atesta'
)
search_fields = (
'serijski_broj',
'naziv',
'marka',
'model_stroja',
'vlasnik__naziv',
'vlasnik__oib'
)
list_filter = ('tip', 'marka', 'vlasnik')
autocomplete_fields = ['vlasnik']
# Dodajemo inline za radne naloge
inlines = [RadniNalogInline]
fieldsets = (
('Osnovne informacije', {
'fields': ('vlasnik', 'naziv', 'tip', 'serijski_broj')
}),
('Detalji o modelu', {
'fields': (('marka', 'model_stroja'), 'godina_proizvodnje')
}),
('Tehnički podaci i održavanje', {
'fields': ('radni_sati', 'registracija', 'datum_zadnjeg_atesta'),
'classes': ('collapse',)
}),
)
def prikaz_naziva(self, obj):
return f"{obj.marka} {obj.model_stroja}".strip() or obj.naziv
prikaz_naziva.short_description = 'Marka i Model'

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class FleetConfig(AppConfig):
name = 'fleet'

View File

@@ -0,0 +1,86 @@
import random
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from faker import Faker
from fleet.models import Stroj
from kupci.models import Kupac
class Command(BaseCommand):
help = 'Popunjava bazu podataka isključivo Liebherr strojevima koristeći Faker'
def add_arguments(self, parser):
parser.add_argument(
'total',
type=int,
help='Broj Liebherr strojeva koje želiš kreirati',
default=10
)
def handle(self, *args, **kwargs):
total = kwargs['total']
fake = Faker(['hr_HR']) # Koristimo hrvatski lokalitet
kupci = list(Kupac.objects.all())
if not kupci:
self.stdout.write(self.style.ERROR("Greška: Nema kupaca u bazi. Prvo pokreni populate_kupci!"))
return
# Katalog usklađen s tvojim TIP_STROJA choices
liebherr_katalog = {
'dizalica_toranj': [
'132 EC-H 8 Litronic',
'280 EC-H 12',
'81 K.1',
'172 EC-B 8'
],
'dizalica_auto': [
'LTM 1030-2.1',
'LTM 1050-3.1',
'LTM 1060-3.1',
'LTM 1120-4.1',
'LTC 1050-3.1'
]
}
self.stdout.write(self.style.MIGRATE_HEADING(f'🚀 Generiram {total} Liebherr strojeva...'))
created_count = 0
for _ in range(total):
# Odabir tipa (usklađeno s tvojim modelom)
tip = random.choice(['dizalica_toranj', 'dizalica_auto'])
model_naziv = random.choice(liebherr_katalog[tip])
# Faker za generiranje serijskog broja i registracije
serijski = f"LE-{fake.bothify(text='######')}" # npr. LE-482931
# Registracija samo za autodizalice (Faker format)
reg = None
if tip == 'dizalica_auto':
# Generira format sličan ZG-1234-LH
reg = f"ZG-{fake.numerify('####')}-LH"
# Radni sati
sati = random.uniform(500.0, 12000.0) if tip == 'dizalica_toranj' else random.uniform(100.0, 5000.0)
# Faker za datume (unutar zadnjih godinu dana)
datum_atesta = fake.date_between(start_date='-1y', end_date='today')
try:
Stroj.objects.create(
vlasnik=random.choice(kupci),
naziv=f"Liebherr {model_naziv}",
serijski_broj=serijski,
marka='Liebherr',
model_stroja=model_naziv,
godina_proizvodnje=random.randint(2015, 2024),
tip=tip,
radni_sati=round(sati, 2),
registracija=reg,
datum_zadnjeg_atesta=datum_atesta
)
created_count += 1
except Exception as e:
self.stderr.write(f"Greška kod S/N {serijski}: {e}")
continue
self.stdout.write(self.style.SUCCESS(f'✅ Uspješno dodano {created_count} Liebherr strojeva!'))

View File

@@ -0,0 +1,71 @@
import random
from django.core.management.base import BaseCommand
from django.core.exceptions import ValidationError
from faker import Faker
from fleet.models import Vozilo
class Command(BaseCommand):
help = 'Popunjava bazu podataka s gospodarskim vozilima koristeći Faker'
def add_arguments(self, parser):
parser.add_argument(
'total',
type=int,
help='Broj vozila koje želiš kreirati',
default=5
)
def handle(self, *args, **kwargs):
total = kwargs['total']
fake = Faker(['hr_HR'])
# Modeli koji se pojavljuju u tvom radnom nalogu (CSV)
modeli_podaci = [
'VW Transporter T6',
'Mercedes-Benz Vito',
'Citroen Jumper',
'Ford Transit Custom',
'Renault Kangoo Maxi',
'Peugeot Boxer',
'Opel Vivaro',
]
self.stdout.write(self.style.MIGRATE_HEADING(f'🚀 Kreiram {total} vozila u flotu...'))
brojac = 0
while brojac < total:
model_naziv = random.choice(modeli_podaci)
# Generiranje registracije koristeći Faker numerify/bothify
# Format: ZG 1234 AB
gradovi = ['ZG', 'ST', 'RI', 'OS', 'VZ', 'PU', 'KA', 'KC']
grad = random.choice(gradovi)
reg_oznaka = fake.numerify(text='####') # npr. 6732
reg_slova = fake.bothify(text='??', letters='ABCDEFGHIJKLMNOPQRSTUVWXYZ') # npr. II
registracija = f"{grad}{reg_oznaka}{reg_slova}"
# Kilometri: Koristimo tvoju logiku da trenutni budu veći od početnih
pocetni = random.randint(0, 150000)
dodatni = random.randint(500, 20000)
trenutni = pocetni + dodatni
try:
# Koristimo registraciju u nazivu radi lakšeg prepoznavanja u Astro frontendu
vozilo = Vozilo.objects.create(
naziv=f"{model_naziv} ({grad})",
registracija=registracija,
pocetni_kilometri=pocetni,
trenutni_kilometri=trenutni,
status=random.choice(['aktivan', 'aktivan', 'servis', 'neaktivan'])
)
brojac += 1
self.stdout.write(f" ✅ Kreirano: {vozilo.naziv} [{vozilo.registracija}]")
except ValidationError:
# Ako Faker pogodi postojeću registraciju, samo nastavi dalje
continue
except Exception as e:
self.stderr.write(f" ❌ Greška: {e}")
break
self.stdout.write(self.style.SUCCESS(f'\nGotovo! Uspješno dodano {brojac} vozila u flotu.'))

View File

@@ -0,0 +1,88 @@
from django.db import models
from django.core.exceptions import ValidationError
class Vozilo(models.Model):
STATUS_CHOICES = [
('aktivan', 'Aktivan'),
('servis', 'Na Servisu'),
('neaktivan', 'Izvan Pogona'),
]
naziv = models.CharField(max_length=50, verbose_name="Interni naziv")
registracija = models.CharField(max_length=15, unique=True, db_index=True)
# Kilometri
pocetni_kilometri = models.PositiveIntegerField(default=0)
trenutni_kilometri = models.PositiveIntegerField(default=0)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='aktivan'
)
class Meta:
verbose_name = "Vozilo"
verbose_name_plural = "Vozila"
ordering = ['naziv']
def clean(self):
# Osnovna logika: trenutni kilometri ne smiju biti manji od početnih
if self.trenutni_kilometri < self.pocetni_kilometri:
raise ValidationError({
'trenutni_kilometri': "Trenutni kilometri ne mogu biti manji od početnih."
})
def save(self, *args, **kwargs):
# Logika za nove zapise
if not self.pk:
self.trenutni_kilometri = self.pocetni_kilometri
self.registracija = self.registracija.upper().replace(" ", "")
# Pozivamo clean() prije spremanja (Django to ne radi automatski u save())
self.full_clean()
super().save(*args, **kwargs)
def __str__(self):
return f"{self.naziv} ({self.registracija})"
class Stroj(models.Model):
TIP_STROJA = [
('dizalica_toranj', 'Toranjska dizalica'),
('dizalica_auto', 'Autodizalica'),
('vilicar', 'Viličar'),
('platforma', 'Radna platforma'),
]
# Poveznica s kupcem: Jedan kupac može imati mnogo strojeva
# --- RJEŠENJE ZA LOOP: Koristimo string 'kupci.Kupac' ---
vlasnik = models.ForeignKey(
'kupci.Kupac',
on_delete=models.CASCADE,
related_name='strojevi',
verbose_name="Vlasnik/Kupac"
)
naziv = models.CharField(max_length=100, help_text="Npr. Liebherr LTM 1030")
serijski_broj = models.CharField(max_length=50, unique=True)
marka = models.CharField(max_length=50, blank=True)
model_stroja = models.CharField(max_length=50, blank=True)
godina_proizvodnje = models.PositiveIntegerField(null=True, blank=True)
tip = models.CharField(max_length=30, choices=TIP_STROJA)
# Podaci za održavanje
radni_sati = models.DecimalField(max_digits=12, decimal_places=2, default=0)
registracija = models.CharField(max_length=20, blank=True, null=True, help_text="Za autodizalice i vozila")
datum_zadnjeg_atesta = models.DateField(null=True, blank=True)
class Meta:
verbose_name = "Stroj"
verbose_name_plural = "Strojevi"
ordering = ['marka', 'model_stroja']
def __str__(self):
# Mala nadogradnja da ne vraća prazan string ako marka/model nedostaju
oznaka = f"{self.marka} {self.model_stroja}".strip() or self.naziv
return f"{oznaka} (S/N: {self.serijski_broj})"

View File

@@ -0,0 +1,115 @@
from rest_framework import serializers
from .models import Stroj, Vozilo
class StrojShortSerializer(serializers.ModelSerializer):
"""
Kraća verzija serijalizatora za brze liste ili dropdown izbornike.
"""
class Meta:
model = Stroj
fields = ['id', 'naziv', 'serijski_broj', 'registracija']
class StrojSerializer(serializers.ModelSerializer):
"""
Glavni serializer za strojeve/dizalice.
"""
# Prikazuje naziv vlasnika (kupca) umjesto samo ID-a
# (source='vlasnik.naziv' jer je u modelu ForeignKey nazvan 'vlasnik')
vlasnik_naziv = serializers.ReadOnlyField(source='vlasnik.naziv')
# get_tip_display() pretvara 'dizalica_auto' u 'Autodizalica' (čitljivije za UI)
tip_human_readable = serializers.CharField(source='get_tip_display', read_only=True)
radni_nalozi = serializers.SerializerMethodField()
class Meta:
model = Stroj
fields = [
'id',
'vlasnik',
'vlasnik_naziv',
'naziv',
'marka',
'model_stroja',
'serijski_broj',
'tip',
'tip_human_readable',
'godina_proizvodnje',
'radni_sati',
'registracija',
'datum_zadnjeg_atesta',
'radni_nalozi', # Novo polje
]
def get_radni_nalozi(self, obj):
# Lokalni import rješava kružni import error s operations aplikacijom
from operations.serializers import RadniNalogSerializer
# Dohvaćamo radne naloge i sortiramo ih tako da najnoviji budu prvi
queryset = obj.radni_nalozi.all().order_by('-datum_kreiranja')
# Vraćamo serijalizirane podatke
return RadniNalogSerializer(queryset, many=True, context=self.context).data
class VoziloSerializer(serializers.ModelSerializer):
"""
Osnovni serializer koji se koristi za Create/Update operacije.
"""
class Meta:
model = Vozilo
fields = '__all__'
class VoziloListaSerializer(serializers.ModelSerializer):
"""
Serializer za brzi prikaz u tablicama.
"""
# Ljudski čitljiv status (npr. 'Na Servisu' umjesto 'servis')
status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Vozilo
fields = ['id', 'naziv', 'registracija', 'trenutni_kilometri', 'status', 'status_prikaz']
class VoziloListaSerializer(serializers.ModelSerializer):
"""
Serializer za brzi prikaz u tablicama.
"""
# Ljudski čitljiv status (npr. 'Na Servisu' umjesto 'servis')
status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Vozilo
fields = ['id', 'naziv', 'registracija', 'trenutni_kilometri', 'status', 'status_prikaz']
class VoziloDetaljiSerializer(serializers.ModelSerializer):
"""
Serializer s dodatnom statistikom za detaljni prikaz vozila.
"""
ukupno_predeno = serializers.SerializerMethodField()
status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
putni_nalozi = serializers.SerializerMethodField()
class Meta:
model = Vozilo
fields = [
'id',
'naziv',
'registracija',
'pocetni_kilometri',
'trenutni_kilometri',
'ukupno_predeno',
'status',
'status_prikaz',
'putni_nalozi',
]
def get_ukupno_predeno(self, obj):
"""Izračunava razliku između trenutnih i početnih kilometara"""
return obj.trenutni_kilometri - obj.pocetni_kilometri
def get_putni_nalozi(self, obj):
from operations.serializers import PutniNalogSerializer
queryset = obj.putni_nalozi.all()
return PutniNalogSerializer(queryset, many=True, context=self.context).data

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
001.BACKEND/fleet/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import StrojViewSet, VoziloViewSet
router = DefaultRouter()
router.register(r'strojevi', StrojViewSet, basename='stroj')
router.register(r'vozila', VoziloViewSet, basename='vozilo')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,72 @@
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Stroj, Vozilo
from .serializers import (
VoziloSerializer,
VoziloListaSerializer,
VoziloDetaljiSerializer,
StrojSerializer
)
class StrojViewSet(viewsets.ModelViewSet):
"""
ViewSet za pregled, kreiranje i uređivanje strojeva i dizalica.
"""
# select_related radi SQL JOIN i povlači podatke o kupcu odmah
queryset = Stroj.objects.all().select_related('vlasnik').prefetch_related('radni_nalozi')
serializer_class = StrojSerializer
# Dodajemo mogućnosti filtriranja, pretrage i sortiranja
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
# Omogućuje filtriranje strojeva po vlasniku ili tipu
# npr. /api/strojevi/?vlasnik=1 ili /api/strojevi/?tip=dizalica_auto
filterset_fields = ['vlasnik', 'tip']
# Omogućuje pretragu po nazivu, marki ili serijskom broju
search_fields = ['naziv', 'marka', 'serijski_broj', 'registracija']
# Omogućuje sortiranje po radnim satima ili godini proizvodnje
ordering_fields = ['radni_sati', 'godina_proizvodnje', 'datum_zadnjeg_atesta']
ordering = ['-datum_zadnjeg_atesta'] # Defaultno prikazuje one koji su zadnji atestirani
class VoziloViewSet(viewsets.ModelViewSet):
"""
ViewSet za pregled, kreiranje i uređivanje voznog parka.
Usklađen sa StrojViewSet arhitekturom.
"""
queryset = Vozilo.objects.all()
# Postavljanje filter backenda (isto kao u StrojViewSet)
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
# Omogućuje filtriranje vozila po statusu
# npr. /api/fleet/vozila/?status=u_radu
filterset_fields = ['status']
# Omogućuje pretragu po nazivu i registraciji
search_fields = ['naziv', 'registracija']
# Omogućuje sortiranje po kilometrima ili statusu
ordering_fields = ['trenutni_kilometri', 'status']
ordering = ['naziv'] # Defaultno sortiranje po abecedi naziva
def get_serializer_class(self):
"""
Dinamički odabir serializera za optimalne performanse.
"""
if self.action == 'list':
return VoziloListaSerializer
if self.action == 'retrieve':
return VoziloDetaljiSerializer
# Default za POST/PUT/PATCH
return VoziloSerializer

View File

View 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',)

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class KalendarConfig(AppConfig):
name = 'kalendar'

View 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}"

View File

@@ -0,0 +1,46 @@
from rest_framework import serializers
from .models import Dogadaj
from operations.serializers import RadniNalogListaSerializer
class DogadajSerializer(serializers.ModelSerializer):
# 1. Mapiranje za standardne kalendarske knjižnice (FullCalendar traži title, start, end)
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()
class Meta:
model = Dogadaj
fields = [
'id', 'title', 'start', 'end', 'tip', 'tip_display',
'opis', '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 # Poziva @property iz models.py
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 za vizualni kalendar"""
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

View 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()

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View 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'),
]

View 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')

View File

View File

@@ -0,0 +1,41 @@
from django.contrib import admin
from .models import Kupac
@admin.register(Kupac)
class KupacAdmin(admin.ModelAdmin):
# Polja koja se vide u glavnoj tablici
list_display = ('naziv', 'oib', 'grad', 'telefon', 'prikaz_broja_strojeva', 'aktivno')
# Brzo uređivanje "aktivno" statusa direktno iz tablice
list_editable = ('aktivno',)
# Filtriranje s desne strane
list_filter = ('tip', 'aktivno', 'grad')
# Pretraga po ključnim poljima
# OBAVEZNO dodaj search_fields kako bi autocomplete radio u fleet i nalozi
search_fields = ('naziv', 'oib', 'email', 'grad')
# Organizacija forme za unos
fieldsets = (
('Osnovni podaci', {
'fields': (('naziv', 'tip'), 'oib')
}),
('Kontakt i lokacija', {
'fields': ('email', 'telefon', 'adresa', ('grad', 'postanski_broj'))
}),
('Dodatno', {
'fields': ('napomena', 'aktivno'),
'classes': ('collapse',)
}),
)
# Čitanje sistemskih podataka bez mogućnosti izmjene
readonly_fields = ('datum_kreiranja',)
def prikaz_broja_strojeva(self, obj):
"""Prikazuje broj strojeva direktno u listi kupaca"""
# Koristimo property koji si već definirao u modelu
return obj.broj_strojeva
prikaz_broja_strojeva.short_description = 'Strojevi'

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class KupciConfig(AppConfig):
name = 'kupci'

View File

View File

@@ -0,0 +1,8 @@
from typing import Any
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def handle(self, *args: Any, **options: Any):
print("hello world")

View File

@@ -0,0 +1,55 @@
import random
from django.core.management.base import BaseCommand
from faker import Faker
from kupci.models import Kupac
class Command(BaseCommand):
help = 'Popunjava bazu podataka s testnim kupcima'
def add_arguments(self, parser):
parser.add_argument(
'total',
type=int,
help='Broj kupaca koje želiš kreirati',
default=10
)
def handle(self, *args, **kwargs):
total = kwargs['total']
fake = Faker(['hr_HR'])
self.stdout.write(self.style.MIGRATE_HEADING(f'🚀 Kreiram {total} kupaca...'))
created_count = 0
for _ in range(total):
tip = random.choice(['pravno', 'fizicko'])
if tip == 'pravno':
naziv = fake.company()
else:
naziv = fake.name()
# Generiramo OIB (nasumičnih 11 znamenki)
oib = "".join([str(random.randint(0, 9)) for _ in range(11)])
try:
# Koristimo get_or_create ili create da izbjegnemo duplikate OIB-a
# ako je polje unique=True u modelu
Kupac.objects.create(
naziv=naziv,
oib=oib,
email=fake.email(),
telefon=fake.phone_number(),
adresa=fake.street_address(),
grad=fake.city(),
postanski_broj=fake.postcode(),
tip=tip,
napomena="Automatski generiran testni kupac.",
aktivno=True
)
created_count += 1
except Exception as e:
# Ako OIB slučajno bude isti, samo nastavi dalje
continue
self.stdout.write(self.style.SUCCESS(f'✅ Uspješno dodano {created_count} novih kupaca!'))

View File

@@ -0,0 +1,49 @@
from django.db import models
class Kupac(models.Model):
TIP_KUPCA = [
('pravno', 'Pravna osoba (Tvrtka)'),
('fizicko', 'Fizička osoba'),
]
naziv = models.CharField(
max_length=255,
help_text="Ime i prezime ili puni naziv tvrtke"
)
oib = models.CharField(
max_length=11,
unique=True,
null=True,
blank=True,
verbose_name="OIB"
)
email = models.EmailField(blank=True)
telefon = models.CharField(max_length=50, blank=True)
adresa = models.CharField(max_length=255, blank=True)
grad = models.CharField(max_length=100, blank=True)
postanski_broj = models.CharField(max_length=10, blank=True)
tip = models.CharField(
max_length=10,
choices=TIP_KUPCA,
default='pravno'
)
napomena = models.TextField(blank=True, help_text="Interni podaci o kupcu")
# Sistemski podaci
datum_kreiranja = models.DateTimeField(auto_now_add=True)
aktivno = models.BooleanField(default=True)
class Meta:
verbose_name = "Kupac"
verbose_name_plural = "Kupci"
ordering = ['naziv']
def __str__(self):
return self.naziv
@property
def broj_strojeva(self):
"""Vraća broj strojeva povezanih s ovim kupcem"""
return self.strojevi.count()

View File

@@ -0,0 +1,73 @@
from rest_framework import serializers
from .models import Kupac
from fleet.models import Stroj
from fleet.serializers import StrojSerializer
class KupacSerializer(serializers.ModelSerializer):
"""
Serializer za listu kupaca.
Fokus je na brzini i osnovnim kontaktnim podacima.
"""
# Izračunato polje iz modela (@property broj_strojeva)
broj_strojeva = serializers.IntegerField(read_only=True)
class Meta:
model = Kupac
fields = [
'id',
'naziv',
'grad',
'telefon',
'email',
'tip',
'aktivno',
'broj_strojeva' # Korisno za prikaz u tablici (npr. "5 strojeva")
]
class KupacDetaljiSerializer(serializers.ModelSerializer):
# Ugniježđeni strojevi - prikazujemo puni objekt, ne samo ID
strojevi = StrojSerializer(many=True, read_only=True)
# Dodatna polja koja nisu u bazi, ali su korisna za Astro frontend
ukupan_broj_strojeva = serializers.IntegerField(source='broj_strojeva', read_only=True)
class Meta:
model = Kupac
fields = [
'id',
'naziv',
'oib',
'email',
'telefon',
'adresa',
'grad',
'postanski_broj',
'tip',
'napomena',
'aktivno',
'datum_kreiranja',
'ukupan_broj_strojeva',
'strojevi' # Lista svih strojeva s punim detaljima
]
class KupacListaSerializer(serializers.ModelSerializer):
"""
Optimizirani serializer za prikaz u tablici (listi).
Vraća samo osnovne podatke potrebne za pregled svih klijenata.
"""
# Dohvaćamo property iz modela koji broji strojeve
broj_strojeva = serializers.IntegerField(read_only=True)
class Meta:
model = Kupac
fields = [
'id',
'naziv',
'grad',
'oib',
'telefon',
'tip',
'aktivno',
'broj_strojeva'
]

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
001.BACKEND/kupci/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import KupacViewSet
# Kreiramo router i registriramo naš ViewSet
# 'r' ispred stringa označava raw string (dobra praksa za regex/putanje)
router = DefaultRouter()
router.register(r'svi', KupacViewSet, basename='kupac')
# Definiramo URL obrasce
urlpatterns = [
# Sve rute koje router generira (npr. /api/kupci/ i /api/kupci/{id}/)
path('', include(router.urls)),
]

View File

@@ -0,0 +1,35 @@
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Kupac
from .serializers import KupacSerializer, KupacDetaljiSerializer, KupacListaSerializer
class KupacViewSet(viewsets.ModelViewSet):
"""
API endpoint koji omogućuje pregled, pretragu i uređivanje kupaca.
"""
# Optimizacija: prefetch_related smanjuje broj upita jer odmah vuče strojeve
queryset = Kupac.objects.all().prefetch_related('strojevi')
serializer_class = KupacSerializer
# Dodajemo filter backende
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
# 1. Točno filtriranje (npr. ?tip=pravno)
filterset_fields = ['tip', 'aktivno', 'grad']
# 2. Pretraga (npr. ?search=gradnja)
# Pretražuje naziv ili OIB
search_fields = ['naziv', 'oib']
# 3. Sortiranje (npr. ?ordering=-datum_kreiranja)
ordering_fields = ['naziv', 'datum_kreiranja']
ordering = ['naziv'] # Defaultno sortiranje
def get_serializer_class(self):
if self.action == 'retrieve': # Ako se gleda pojedinačni kupac
return KupacDetaljiSerializer # Onaj s ugniježđenim strojevima
return KupacListaSerializer # Obični bez strojeva

22
001.BACKEND/manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

View File

@@ -0,0 +1,112 @@
# src/operations/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import RadniNalog, RadniNalogSlika, PutniNalog
# --- INLINES ---
class RadniNalogSlikaInline(admin.TabularInline):
"""Omogućuje dodavanje više slika unutar samog radnog naloga."""
model = RadniNalogSlika
extra = 1
fields = ('slika', 'opis')
class RadniNalogInline(admin.StackedInline):
"""
Omogućuje dodavanje i pregled radnih naloga direktno unutar Putnog naloga.
Ovo je ključno za terenski rad (jedno putovanje -> više odrađenih strojeva).
"""
model = RadniNalog
extra = 0
autocomplete_fields = ['stroj', 'klijent', 'izvrsitelj']
fieldsets = (
(None, {
'fields': (('broj_naloga', 'status'), ('klijent', 'stroj'), 'opis_kvara')
}),
)
# --- ADMIN REGISTRACIJE ---
@admin.register(PutniNalog)
class PutniNalogAdmin(admin.ModelAdmin):
list_display = ('broj_naloga', 'prikaz_vozila', 'korisnik', 'pocetna_km', 'zavrsna_km', 'status_marker')
list_filter = ('status', 'datum_izdavanja', 'vozilo')
search_fields = ('broj_naloga', 'vozilo__registracija', 'korisnik__last_name')
autocomplete_fields = ['vozilo', 'korisnik']
# Inlines omogućuju da vidiš servise (RN) unutar putovanja (PN)
inlines = [RadniNalogInline]
# pocetna_km je readonly jer se automatski povlači iz flote pri kreiranju
readonly_fields = ('pocetna_km',)
fieldsets = (
('Logistika puta', {
'fields': (('broj_naloga', 'status'), ('vozilo', 'korisnik'), 'relacija', 'mjesto_odredista')
}),
('Kilometraža i vrijeme', {
'fields': (('pocetna_km', 'zavrsna_km'), ('vrijeme_polaska', 'vrijeme_povratka')),
'description': 'Završni kilometri će automatski ažurirati bazu vozila po zatvaranju naloga.'
}),
)
def prikaz_vozila(self, obj):
return format_html('<b>{}</b>', obj.vozilo.registracija)
prikaz_vozila.short_description = "Vozilo"
def status_marker(self, obj):
color = 'green' if obj.status == 'ZAVRSEN' else 'orange'
return format_html('<span style="color: {}; font-weight: bold;">{}</span>', color, obj.get_status_display())
status_marker.short_description = "Status"
@admin.register(RadniNalog)
class RadniNalogAdmin(admin.ModelAdmin):
# Prikazujemo stroj i putni nalog kako bismo znali gdje je servis odrađen
list_display = ('prikaz_broja', 'datum_kreiranja', 'klijent', 'stroj', 'status_boja', 'povezani_pn')
list_filter = ('status', 'datum_kreiranja', 'izvrsitelj', 'stroj__tip')
# Pretraga po serijskom broju stroja je ključna za servisere
search_fields = (
'broj_naloga',
'klijent__naziv',
'stroj__naziv',
'stroj__serijski_broj'
)
# Autocomplete za lakše biranje među stotinama strojeva i klijenata
autocomplete_fields = ['klijent', 'stroj', 'putni_nalog', 'izvrsitelj']
inlines = [RadniNalogSlikaInline]
fieldsets = (
('Subjekt servisa', {
'fields': (('broj_naloga', 'status'), ('klijent', 'stroj'), ('izvrsitelj', 'putni_nalog'))
}),
('Opis i dokumentacija', {
'fields': ('opis_kvara', 'slika_kvara', 'potpis_klijenta')
}),
)
def prikaz_broja(self, obj):
return format_html('<b>RN-{}</b>', obj.broj_naloga)
prikaz_broja.short_description = "Nalog"
def povezani_pn(self, obj):
if obj.putni_nalog:
return obj.putni_nalog.broj_naloga
return "Nema (Radiona)"
povezani_pn.short_description = "Putni nalog"
def status_boja(self, obj):
boje = {
'PLANIRANO': '#6b7280',
'U_RADU': '#2563eb',
'ZAVRSENO': '#10b981',
}
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
boje.get(obj.status, 'black'),
obj.get_status_display()
)
status_boja.short_description = "Status"

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class OperationsConfig(AppConfig):
name = 'operations'

View File

@@ -0,0 +1,132 @@
import random
import os
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.core.files import File
from operations.models import RadniNalog, RadniNalogSlika, PutniNalog
from fleet.models import Vozilo, Stroj
from kupci.models import Kupac
from kalendar.services import kreiraj_kalendarski_unos
User = get_user_model()
class Command(BaseCommand):
help = 'Popunjava bazu s Putnim nalozima, Radnim nalozima i slikama'
def add_arguments(self, parser):
parser.add_argument('total_putovanja', type=int, help='Broj putnih naloga', default=3)
parser.add_argument('--img', type=str, default='test_assets/kvar.jpg')
def handle(self, *args, **kwargs):
total_putovanja = kwargs['total_putovanja']
test_img_path = kwargs['img']
kupci = Kupac.objects.prefetch_related('strojevi').all()
vozila = list(Vozilo.objects.all())
serviseri = list(User.objects.filter(is_staff=True))
if not kupci.exists() or not vozila or not serviseri:
self.stdout.write(self.style.ERROR("Baza mora imati kupce, vozila i servisere (is_staff=True)."))
return
# Provjera slike
if not os.path.exists(test_img_path):
self.stdout.write(self.style.WARNING(f"Testna slika {test_img_path} nije pronađena. Generiram bez slika."))
test_img_path = None
opisi_kvarova = [
"Godišnji periodični servis dizalice.",
"Zamjena hidrauličnog crijeva i dopuna ulja.",
"Kalibracija senzora opterećenja.",
"Popravak daljinskog upravljača.",
"Hitna intervencija: Stroj u kvaru."
]
self.stdout.write(f'🚀 Generiranje {total_putovanja} putovanja...')
success_count_rn = 0
trenutna_godina = timezone.now().year
for i in range(total_putovanja):
serviser = random.choice(serviseri)
vozilo = random.choice(vozila)
broj_pn = f"PN-{trenutna_godina}-{PutniNalog.objects.count() + 1:04d}"
# 1. Kreiraj PUTNI NALOG (u statusu OTVOREN)
putni_nalog = PutniNalog.objects.create(
broj_naloga=broj_pn,
vozilo=vozilo,
korisnik=serviser,
relacija="Zagreb - Teren - Zagreb",
mjesto_odredista="Teren",
)
self.stdout.write(self.style.MIGRATE_LABEL(f"Otvoren {broj_pn} (Vozilo: {vozilo.registracija})"))
# 2. Kreiraj 1 do 3 RADNA NALOGA za ovaj putni nalog
za_odraditi = random.randint(1, 3)
kreirani_radni_nalozi = []
for j in range(za_odraditi):
klijenti_sa_strojima = [k for k in kupci if k.strojevi.exists()]
klijent = random.choice(klijenti_sa_strojima)
stroj = random.choice(list(klijent.strojevi.all()))
broj_rn = f"{trenutna_godina}-{RadniNalog.objects.count() + 1:04d}"
try:
nalog = RadniNalog(
broj_naloga=broj_rn,
klijent=klijent,
stroj=stroj,
putni_nalog=putni_nalog,
opis_kvara=random.choice(opisi_kvarova),
izvrsitelj=serviser,
status=RadniNalog.StatusRadaChoices.PLANIRANO
)
if test_img_path:
with open(test_img_path, 'rb') as f:
nalog.slika_kvara.save(f'rn_{broj_rn}.jpg', File(f), save=False)
nalog.save()
kreirani_radni_nalozi.append(nalog)
# Dodatne slike u galeriju
if test_img_path:
for k in range(random.randint(1, 2)):
with open(test_img_path, 'rb') as f:
RadniNalogSlika.objects.create(
radni_nalog=nalog,
slika=File(f, name=f'gal_{broj_rn}_{k}.jpg'),
opis=f"Detalj kvara {k+1}"
)
# 5. Upis u kalendar
kreiraj_kalendarski_unos(nalog)
success_count_rn += 1
self.stdout.write(f" ✅ RN-{broj_rn} dodan u {broj_pn}")
except Exception as e:
self.stderr.write(f" ❌ Greška kod RN-{broj_rn}: {e}")
# 3. NASUMIČNO ZATVARANJE (Simulacija završenog posla)
# 50% šanse da ćemo u skripti odmah "završiti" ovaj put
if random.random() > 0.5:
try:
# Prvo moramo zatvoriti sve radne naloge da bi PN prošao validaciju
for rn in kreirani_radni_nalozi:
rn.status = RadniNalog.StatusRadaChoices.ZAVRSENO
rn.save()
# Dodajemo završne kilometre (uvijek više od početnih)
putni_nalog.zavrsna_km = putni_nalog.pocetna_km + random.randint(50, 500)
putni_nalog.status = PutniNalog.StatusChoices.ZAVRSEN
putni_nalog.save()
self.stdout.write(self.style.SUCCESS(f" 🏁 {broj_pn} automatski ZATVOREN. Kilometri vozila ažurirani."))
except Exception as e:
self.stdout.write(self.style.WARNING(f" ⚠️ PN-{broj_pn} nije zatvoren: {e}"))
self.stdout.write(self.style.SUCCESS(f'\nGotovo! Generirano {total_putovanja} PN i {success_count_rn} RN.'))

View File

@@ -0,0 +1,220 @@
import os
from PIL import Image
import io
from django.db import models, transaction
from django.conf import settings
from django.utils import timezone
from django.core.files.base import ContentFile
from django.core.exceptions import ValidationError
# --- FUNKCIJE ZA DINAMIČKE PUTANJE ---
def putanja_slike_naloga(instance, filename):
# Kreira putanju: radni-nalozi/broj_naloga/originalni_naziv.ekstenzija
return f'radni-nalozi/{instance.broj_naloga}/{filename}'
def putanja_galerije_naloga(instance, filename):
# instance je objekt RadniNalogSlika koji ima ForeignKey na radni_nalog
# Sprema u isti folder gdje je i glavna slika kvara
return f'radni-nalozi/{instance.radni_nalog.broj_naloga}/galerija/{filename}'
class PutniNalog(models.Model):
class StatusChoices(models.TextChoices):
OTVOREN = 'OTVOREN', 'Otvoren'
ZAVRSEN = 'ZAVRSEN', 'Završen'
broj_naloga = models.CharField(max_length=50, unique=True)
datum_izdavanja = models.DateField(default=timezone.now)
vozilo = models.ForeignKey(
'fleet.Vozilo',
on_delete=models.PROTECT,
related_name='putni_nalozi',
verbose_name="Službeno vozilo"
)
korisnik = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
relacija = models.CharField(max_length=255)
mjesto_odredista = models.CharField(max_length=255)
pocetna_km = models.PositiveIntegerField(
editable=False,
help_text="Automatski preuzeto iz modela Vozilo"
)
zavrsna_km = models.PositiveIntegerField(null=True, blank=True)
vrijeme_polaska = models.DateTimeField(default=timezone.now)
vrijeme_povratka = models.DateTimeField(null=True, blank=True)
status = models.CharField(
max_length=15,
choices=StatusChoices.choices,
default=StatusChoices.OTVOREN
)
class Meta:
verbose_name = "Putni nalog"
verbose_name_plural = "Putni nalozi"
def clean(self):
super().clean()
if self.status == self.StatusChoices.ZAVRSEN:
if self.zavrsna_km is None:
raise ValidationError({'zavrsna_km': "Morate unijeti završne kilometre prije zatvaranja naloga."})
otvoreni_radni_nalozi = self.radni_nalozi.exclude(status='ZAVRSENO').exists()
if otvoreni_radni_nalozi:
raise ValidationError({
'status': "Putni nalog ne može biti završen jer postoje povezani radni nalozi koji još nisu zatvoreni."
})
def save(self, *args, **kwargs):
if not self.pk:
self.pocetna_km = self.vozilo.trenutni_kilometri
self.full_clean()
if self.status == self.StatusChoices.ZAVRSEN and self.zavrsna_km:
with transaction.atomic():
super().save(*args, **kwargs)
v = self.vozilo
v.trenutni_kilometri = self.zavrsna_km
v.pocetni_kilometri = self.zavrsna_km
v.save()
else:
super().save(*args, **kwargs)
def __str__(self):
return f"{self.broj_naloga} - {self.vozilo.registracija}"
class RadniNalog(models.Model):
class StatusRadaChoices(models.TextChoices):
PLANIRANO = 'PLANIRANO', 'Planirano'
U_RADU = 'U_RADU', 'U radu'
ZAVRSENO = 'ZAVRSENO', 'Završeno'
broj_naloga = models.CharField(max_length=20, unique=True, blank=True)
stroj = models.ForeignKey(
'fleet.Stroj',
on_delete=models.CASCADE,
related_name='radni_nalozi',
verbose_name="Stroj na popravku"
)
putni_nalog = models.ForeignKey(
'PutniNalog',
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='radni_nalozi'
)
klijent = models.ForeignKey(
'kupci.Kupac',
on_delete=models.PROTECT,
related_name='radni_nalozi'
)
opis_kvara = models.TextField()
izvrsitelj = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='dodijeljeni_nalozi'
)
status = models.CharField(
max_length=20,
choices=StatusRadaChoices.choices,
default=StatusRadaChoices.PLANIRANO
)
datum_kreiranja = models.DateTimeField(auto_now_add=True, verbose_name="Datum kreiranja")
datum_azuriranja = models.DateTimeField(auto_now=True, verbose_name="Zadnja izmjena")
slika_kvara = models.ImageField(upload_to=putanja_slike_naloga, null=True, blank=True)
potpis_klijenta = models.ImageField(upload_to='potpisi/%Y/%m/', null=True, blank=True)
def save(self, *args, **kwargs):
# 1. AUTOMATSKO GENERIRANJE BROJA NALOGA
if not self.broj_naloga:
godina = timezone.now().year
prefix = f"RN-{godina}-"
# Koristimo atomsku transakciju i select_for_update da spriječimo
# da dva korisnika dobiju isti broj u istoj milisekundi
with transaction.atomic():
zadnji_nalog = RadniNalog.objects.select_for_update().filter(
broj_naloga__startswith=prefix
).order_by('-broj_naloga').first()
if zadnji_nalog:
try:
# RN-2026-0001 -> uzimamo "0001"
zadnji_broj_str = zadnji_nalog.broj_naloga.split('-')[-1]
novi_broj = int(zadnji_broj_str) + 1
except (ValueError, IndexError):
novi_broj = 1
else:
novi_broj = 1
self.broj_naloga = f"{prefix}{novi_broj:04d}"
# 2. OBRADA SLIKE
if self.slika_kvara:
try:
self.slika_kvara = self.rescale_image(self.slika_kvara)
except Exception as e:
print(f"Greška pri obradi slike: {e}")
# 3. PRIMARNO SPREMANJE
super().save(*args, **kwargs)
# 4. LOGIKA ZA PUTNI NALOG
if self.putni_nalog and self.status == self.StatusRadaChoices.ZAVRSENO:
pn = self.putni_nalog
# Provjeravamo ima li nalog još uvijek otvorenih radnih naloga
ostali_otvoreni = pn.radni_nalozi.exclude(
id=self.id, # Isključujemo trenutni nalog jer je on upravo završen
status=self.StatusRadaChoices.ZAVRSENO
).exists()
if not ostali_otvoreni and pn.zavrsna_km:
pn.status = 'ZAVRSEN'
pn.save()
def rescale_image(self, image_field):
img = Image.open(image_field)
if img.height > 1200 or img.width > 1200:
output_size = (1200, 1200)
img.thumbnail(output_size)
img_io = io.BytesIO()
img_format = img.format if img.format else 'JPEG'
img.save(img_io, format=img_format, quality=90)
return ContentFile(img_io.getvalue(), name=image_field.name)
return image_field
class Meta:
verbose_name = "Radni nalog"
verbose_name_plural = "Radni nalozi"
def __str__(self):
return f"RN-{self.broj_naloga} | {self.stroj.naziv}"
class RadniNalogSlika(models.Model):
radni_nalog = models.ForeignKey(
RadniNalog,
related_name='slike',
on_delete=models.CASCADE
)
# POPRAVLJENO: uklonjeni navodnici oko funkcije
slika = models.ImageField(upload_to=putanja_galerije_naloga, verbose_name="Slika")
opis = models.CharField(max_length=100, blank=True, verbose_name="Kratki opis slike")
class Meta:
verbose_name = "Dodatna slika naloga"
verbose_name_plural = "Dodatne slike naloga"
def __str__(self):
return f"Slika za nalog {self.radni_nalog.broj_naloga}"

View File

@@ -0,0 +1,86 @@
from rest_framework import serializers
from .models import RadniNalog, RadniNalogSlika
from kupci.serializers import KupacListaSerializer
from fleet.serializers import VoziloListaSerializer, StrojSerializer
# --- POMOĆNI SERIALIZER ZA SLIKE ---
class RadniNalogSlikaSerializer(serializers.ModelSerializer):
class Meta:
model = RadniNalogSlika
fields = ['id', 'slika', 'opis']
# --- 1. LISTA SERIALIZER ---
class RadniNalogListaSerializer(serializers.ModelSerializer):
klijent_naziv = serializers.CharField(source='klijent.naziv', read_only=True)
stroj_naziv = serializers.CharField(source='stroj.naziv', read_only=True)
izvrsitelj_ime = serializers.CharField(source='izvrsitelj.get_full_name', read_only=True)
class Meta:
model = RadniNalog
fields = [
'id',
'broj_naloga',
'klijent_naziv',
'stroj_naziv',
'status',
'izvrsitelj_ime',
'datum_kreiranja'
]
# --- 2. DETALJI SERIALIZER ---
class RadniNalogDetaljiSerializer(serializers.ModelSerializer):
klijent = KupacListaSerializer(read_only=True)
stroj = StrojSerializer(read_only=True)
slike = RadniNalogSlikaSerializer(many=True, read_only=True)
# VOZILO dohvaćamo preko putnog naloga
vozilo = serializers.SerializerMethodField()
status_display = serializers.CharField(source='get_status_display', read_only=True)
izvrsitelj_ime = serializers.CharField(source='izvrsitelj.get_full_name', read_only=True)
class Meta:
model = RadniNalog
fields = [
'id', 'broj_naloga', 'klijent', 'stroj', 'putni_nalog', 'vozilo', 'opis_kvara',
'izvrsitelj', 'izvrsitelj_ime', 'status', 'status_display',
'datum_kreiranja', 'datum_azuriranja', 'slika_kvara',
'potpis_klijenta', 'slike'
]
read_only_fields = ['datum_kreiranja', 'datum_azuriranja']
def get_vozilo(self, obj):
# Ako je radni nalog vezan za putni nalog, vrati podatke o vozilu
if obj.putni_nalog and obj.putni_nalog.vozilo:
return {
"id": obj.putni_nalog.vozilo.id,
"naziv": obj.putni_nalog.vozilo.naziv,
"registracija": obj.putni_nalog.vozilo.registracija,
"trenutni_kilometri": obj.putni_nalog.vozilo.trenutni_kilometri
}
return None
# --- 3. DEFAULT SERIALIZER ---
class RadniNalogSerializer(serializers.ModelSerializer):
# Dozvoljavamo unos, ali nije obavezan ako ga backend generira
broj_naloga = serializers.CharField(required=False, read_only=True)
class Meta:
model = RadniNalog
exclude = []
def validate_broj_naloga(self, value):
# Provjera jedinstvenosti prije nego što dođe do baze
if RadniNalog.objects.filter(broj_naloga=value).exists():
raise serializers.ValidationError("Radni nalog s ovim brojem već postoji.")
return value
def validate(self, data):
# Tvoja postojeća validacija klijenta i stroja
stroj = data.get('stroj')
klijent = data.get('klijent')
if stroj and klijent and stroj.vlasnik != klijent:
raise serializers.ValidationError(
{"stroj": "Odabrani stroj ne pripada odabranom klijentu."}
)
return data

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RadniNalogViewSet
# Kreiramo router i registriramo ViewSet za radne naloge
router = DefaultRouter()
router.register(r'radni-nalozi', RadniNalogViewSet, basename='radni-nalog')
urlpatterns = [
# Uključujemo sve rute koje router generira
path('', include(router.urls)),
]

View File

@@ -0,0 +1,106 @@
from rest_framework.decorators import action
from django.utils import timezone
from rest_framework import viewsets, filters, status # Dodan status
from django_filters.rest_framework import DjangoFilterBackend
from .models import RadniNalog, RadniNalogSlika, PutniNalog
from rest_framework.response import Response
from .serializers import (
RadniNalogSerializer,
RadniNalogListaSerializer,
RadniNalogDetaljiSerializer
)
from kalendar.services import kreiraj_kalendarski_unos
class RadniNalogViewSet(viewsets.ModelViewSet):
"""
API endpoint za upravljanje radnim nalozima.
Vozilo se sada filtrira i pretražuje preko Putnog Naloga.
"""
# 1. queryset: UKLONJEN 'vozilo' iz select_related
queryset = RadniNalog.objects.all().select_related(
'klijent', 'izvrsitelj', 'putni_nalog__vozilo'
)
serializer_class = RadniNalogSerializer
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
# 2. filterset_fields: 'vozilo' zamijenjeno putanjom preko putnog naloga
filterset_fields = ['status', 'izvrsitelj', 'klijent', 'putni_nalog__vozilo']
# 3. search_fields: pretraga ide preko putni_nalog__vozilo__registracija
search_fields = [
'broj_naloga',
'klijent__naziv',
'putni_nalog__vozilo__registracija',
'putni_nalog__vozilo__naziv'
]
ordering_fields = ['datum_kreiranja', 'status', 'broj_naloga']
ordering = ['-datum_kreiranja']
def get_serializer_class(self):
if self.action == 'list':
return RadniNalogListaSerializer
if self.action == 'retrieve':
return RadniNalogDetaljiSerializer
return RadniNalogSerializer
def get_queryset(self):
# Optimizacija upita ovisno o akciji
queryset = self.queryset
if self.action == 'retrieve':
queryset = queryset.prefetch_related('slike')
return queryset
def perform_create(self, serializer):
# Postavljamo izvršitelja ako nije poslan
if not serializer.validated_data.get('izvrsitelj'):
radni_nalog = serializer.save(izvrsitelj=self.request.user)
else:
radni_nalog = serializer.save()
# Kalendar servis
try:
kreiraj_kalendarski_unos(radni_nalog)
except Exception as e:
print(f"Greška pri kreiranju kalendarskog zapisa: {e}")
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
instance = serializer.instance
# Obrada višestrukih slika
files = request.FILES.getlist('slike')
for f in files:
RadniNalogSlika.objects.create(radni_nalog=instance, slika=f)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@action(detail=False, methods=['get'], url_path='sljedeci-broj')
def dohvati_sljedeci_broj(self, request):
godina = timezone.now().year
prefix = f"{godina}-"
zadnji_nalog = RadniNalog.objects.filter(
broj_naloga__startswith=prefix
).order_by('-broj_naloga').first()
if zadnji_nalog:
try:
zadnji_broj = int(zadnji_nalog.broj_naloga.split('-')[-1])
novi_broj = zadnji_broj + 1
except (ValueError, IndexError):
novi_broj = 1
else:
novi_broj = 1
return Response({"broj_naloga": f"{prefix}{novi_broj:04d}"})

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

View File

@@ -0,0 +1,13 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
# Definiramo koja polja se vide u admin listi i formama
list_display = ['email', 'username', 'first_name', 'last_name', 'is_staff', 'is_serviser']
fieldsets = UserAdmin.fieldsets + (
('Dodatni podaci', {'fields': ('telefon', 'oib', 'is_serviser')}),
)
admin.site.register(CustomUser, CustomUserAdmin)

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

View File

@@ -0,0 +1,18 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
# Email koristimo za login, pa mora biti jedinstven
email = models.EmailField(unique=True)
# Dodatna polja za tvoju tvrtku
telefon = models.CharField(max_length=20, blank=True)
oib = models.CharField(max_length=11, blank=True, null=True)
is_serviser = models.BooleanField(default=False) # Razlikovanje uloga
# Govorimo Djangu da koristi email umjesto username-a
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
def __str__(self):
return f"{self.first_name} {self.last_name} ({self.email})"

View File

@@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import CustomUser
class UserMeSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['id', 'email', 'first_name', 'last_name', 'is_serviser']

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
001.BACKEND/users/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserViewSet
router = DefaultRouter()
router.register(r'', UserViewSet, basename='user')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,27 @@
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.conf import settings
from .models import CustomUser
from rest_framework import serializers
from .models import CustomUser
class UserMeSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['id', 'email', 'first_name', 'last_name', 'is_serviser']
class UserViewSet(viewsets.ViewSet):
# Akcija 'me' bit će dostupna na /api/users/me/
@action(detail=False, methods=['get'])
def me(self, request):
# 1. Provjera je li korisnik stvarno logiran (ima validan token)
if not request.user.is_authenticated:
return Response(
{"detail": "Niste prijavljeni. U DEBUG modu pristup je dozvoljen, ali nema podataka za AnonymousUser."},
status=status.HTTP_200_OK # Možeš vratiti 200 s praznim podacima ili 401
)
# 2. Ako je logiran, normalno serijaliziraj
serializer = UserMeSerializer(request.user)
return Response(serializer.data)