izradena poslovna logika
@@ -101,6 +101,8 @@ USE_TZ = True
|
|||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
||||||
|
|
||||||
|
print(f"DEBUG: {DEBUG}" f" | ALLOWED_HOSTS: {ALLOWED_HOSTS}")
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
DEFAULT_PERMISSION_CLASSES = [ 'rest_framework.permissions.AllowAny' ]
|
DEFAULT_PERMISSION_CLASSES = [ 'rest_framework.permissions.AllowAny' ]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class VoziloListaSerializer(serializers.ModelSerializer):
|
|||||||
Serializer za brzi prikaz u tablicama.
|
Serializer za brzi prikaz u tablicama.
|
||||||
"""
|
"""
|
||||||
# Ljudski čitljiv status (npr. 'Na Servisu' umjesto 'servis')
|
# Ljudski čitljiv status (npr. 'Na Servisu' umjesto 'servis')
|
||||||
status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
|
# status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Vozilo
|
model = Vozilo
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -9,7 +9,7 @@ from fleet.serializers import VoziloListaSerializer, StrojSerializer
|
|||||||
class RadniNalogSlikaSerializer(serializers.ModelSerializer):
|
class RadniNalogSlikaSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RadniNalogSlika
|
model = RadniNalogSlika
|
||||||
fields = ['id', 'slika', 'opis']
|
fields = ['id', 'slika', 'opis', 'radni_nalog']
|
||||||
|
|
||||||
# --- POMOĆNI SERIALIZER ZA PRIKAZ VEZANOG PUTNOG NALOGA ---
|
# --- POMOĆNI SERIALIZER ZA PRIKAZ VEZANOG PUTNOG NALOGA ---
|
||||||
class PutniNalogMinimalSerializer(serializers.ModelSerializer):
|
class PutniNalogMinimalSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import RadniNalogViewSet, PutniNalogViewSet
|
from .views import RadniNalogViewSet, PutniNalogViewSet, RadniNalogSlikaViewSet
|
||||||
|
|
||||||
# Kreiramo router i registriramo ViewSet za radne naloge
|
# Kreiramo router i registriramo ViewSet za radne naloge
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'radni-nalozi', RadniNalogViewSet, basename='radni-nalog')
|
router.register(r'radni-nalozi', RadniNalogViewSet, basename='radni-nalog')
|
||||||
router.register(r'putni-nalozi', PutniNalogViewSet, basename='putni-nalog')
|
router.register(r'putni-nalozi', PutniNalogViewSet, basename='putni-nalog')
|
||||||
|
router.register(r'radni-nalozi-slike', RadniNalogSlikaViewSet, basename='radni-nalog-slika')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Uključujemo sve rute koje router generira
|
# Uključujemo sve rute koje router generira
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from .serializers import (
|
|||||||
RadniNalogSerializer,
|
RadniNalogSerializer,
|
||||||
RadniNalogListaSerializer,
|
RadniNalogListaSerializer,
|
||||||
RadniNalogDetaljiSerializer,
|
RadniNalogDetaljiSerializer,
|
||||||
KreirajPutniNalogSerializer
|
KreirajPutniNalogSerializer,
|
||||||
|
RadniNalogSlikaSerializer
|
||||||
)
|
)
|
||||||
from kalendar.services import kreiraj_kalendarski_unos
|
from kalendar.services import kreiraj_kalendarski_unos
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ class RadniNalogViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = RadniNalog.objects.all().select_related(
|
queryset = RadniNalog.objects.all().select_related(
|
||||||
'klijent', 'izvrsitelj', 'putni_nalog__vozilo'
|
'klijent', 'izvrsitelj', 'putni_nalog__vozilo'
|
||||||
)
|
)
|
||||||
|
# permission_classes = [permissions.IsAuthenticated]
|
||||||
|
# permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
serializer_class = RadniNalogSerializer
|
serializer_class = RadniNalogSerializer
|
||||||
|
|
||||||
@@ -41,6 +44,18 @@ class RadniNalogViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ['datum_kreiranja', 'status', 'broj_naloga']
|
ordering_fields = ['datum_kreiranja', 'status', 'broj_naloga']
|
||||||
ordering = ['-datum_kreiranja']
|
ordering = ['-datum_kreiranja']
|
||||||
|
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
# 1. Izvrši standardni update
|
||||||
|
response = super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# 2. Dohvati svježu instancu nakon spremanja
|
||||||
|
instance = self.get_object()
|
||||||
|
|
||||||
|
# 3. Koristi detaljni serializer kako bi Astro dobio sve podatke (slike, vozilo, klijent)
|
||||||
|
serializer = RadniNalogDetaljiSerializer(instance, context={'request': request})
|
||||||
|
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == 'list':
|
if self.action == 'list':
|
||||||
return RadniNalogListaSerializer
|
return RadniNalogListaSerializer
|
||||||
@@ -70,7 +85,23 @@ class RadniNalogViewSet(viewsets.ModelViewSet):
|
|||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
self.perform_create(serializer)
|
|
||||||
|
if request.data.get('kreiraj_putni') == 'true':
|
||||||
|
# Kreiraj novi putni nalog (moraš imati vozilo definirano!)
|
||||||
|
# Pretpostavka: 'stroj' je poslan u requestu, pa iz njega dobijemo vozilo ako postoji
|
||||||
|
# Ovdje prilagodi logiku prema tome kako tvoj model PutniNalog povezuje vozilo
|
||||||
|
novi_putni = PutniNalog.objects.create(
|
||||||
|
broj_naloga=f"PN-{timezone.now().year}-AUTO", # Ili tvoja logika za broj
|
||||||
|
vozilo=serializer.validated_data['stroj'].vozilo, # Primjer povezivanja
|
||||||
|
korisnik=request.user,
|
||||||
|
relacija="Automatski kreirano iz radnog naloga"
|
||||||
|
)
|
||||||
|
# Spremi nalog s ovim novim putnim nalogom
|
||||||
|
instance = serializer.save(putni_nalog=novi_putni)
|
||||||
|
else:
|
||||||
|
# Standardni put
|
||||||
|
self.perform_create(serializer)
|
||||||
|
instance = serializer.instance
|
||||||
|
|
||||||
instance = serializer.instance
|
instance = serializer.instance
|
||||||
|
|
||||||
@@ -125,7 +156,8 @@ class PutniNalogViewSet(viewsets.ModelViewSet):
|
|||||||
Podržava izradu novog putnog naloga iz radnog naloga (POST) i pregled (GET).
|
Podržava izradu novog putnog naloga iz radnog naloga (POST) i pregled (GET).
|
||||||
"""
|
"""
|
||||||
queryset = PutniNalog.objects.all().order_by('-datum_izdavanja', '-id')
|
queryset = PutniNalog.objects.all().order_by('-datum_izdavanja', '-id')
|
||||||
permission_classes = [permissions.IsAuthenticated] # Preporučeno: Otkomentarisano radi getAuthHeaders
|
# permission_classes = [permissions.IsAuthenticated] # Preporučeno: Otkomentarisano radi getAuthHeaders
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == 'create':
|
if self.action == 'create':
|
||||||
@@ -148,4 +180,37 @@ class PutniNalogViewSet(viewsets.ModelViewSet):
|
|||||||
status=status.HTTP_201_CREATED
|
status=status.HTTP_201_CREATED
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class RadniNalogSlikaViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = RadniNalogSlika.objects.all()
|
||||||
|
serializer_class = RadniNalogSlikaSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
# permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
# 1. Dohvati listu datoteka (mora se podudarati s imenom u FormData)
|
||||||
|
files = request.FILES.getlist('slika')
|
||||||
|
|
||||||
|
# 2. Dohvati nalog_id iz request.data
|
||||||
|
radni_nalog_id = request.data.get('radni_nalog')
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
# Ako nema datoteka, pokušaj standardno kreiranje
|
||||||
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
created_instances = []
|
||||||
|
for file in files:
|
||||||
|
# 3. Ručno kreiraj instancu za svaku sliku
|
||||||
|
# Napomena: 'slika' i 'radni_nalog' su imena polja u tvom modelu
|
||||||
|
instance = RadniNalogSlika.objects.create(
|
||||||
|
radni_nalog_id=radni_nalog_id,
|
||||||
|
slika=file
|
||||||
|
)
|
||||||
|
created_instances.append(instance)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"status": f"Uspješno dodano {len(created_instances)} slika"},
|
||||||
|
status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.astro
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PUBLIC_API_URL=https://v003-backend.captain.mitteworkspace.cloud/api
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
import { defineConfig } from 'astro/config';
|
|
||||||
import node from '@astrojs/node';
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
output: 'server',
|
|
||||||
adapter: node({
|
|
||||||
mode: 'standalone',
|
|
||||||
}),
|
|
||||||
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0',
|
|
||||||
port: 4321,
|
|
||||||
},
|
|
||||||
|
|
||||||
image: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
hostname: 'frontend-operativa.local.mitteworkspace.cloud',
|
|
||||||
port: '',
|
|
||||||
pathname: '/**',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
|
||||||
inlineStylesheets: 'always'
|
|
||||||
},
|
|
||||||
|
|
||||||
vite: {
|
|
||||||
build: {
|
|
||||||
// Smanji intenzitet optimizacije tijekom dev-a
|
|
||||||
minify: false,
|
|
||||||
cssMinify: false,
|
|
||||||
},
|
|
||||||
plugins: [tailwindcss()],
|
|
||||||
// KLJUČNO ZA TAILWIND v4 + NODE standalone SSR:
|
|
||||||
ssr: {
|
|
||||||
// Prisiljava Vite da uključi Tailwind v4 stilove u serverski bundle
|
|
||||||
noExternal: ['tailwindcss', '@tailwindcss/vite']
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
watch: {
|
|
||||||
ignored: ['**/node_modules/**', '**/dist/**'],
|
|
||||||
},
|
|
||||||
allowedHosts: ['.mitteworkspace.cloud']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "poslovanje",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"overrides": {
|
|
||||||
"vite": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22.12.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"dev": "astro dev --host",
|
|
||||||
"build": "astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"astro": "astro",
|
|
||||||
"start": "NODE_OPTIONS=--max-old-space-size=4096 npm run dev"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/node": "^10.0.6",
|
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
|
||||||
"astro": "^6.3.7",
|
|
||||||
"flowbite": "^4.0.1",
|
|
||||||
"photoswipe": "^5.4.4",
|
|
||||||
"tailwindcss": "^4.2.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
001.FRONTEND/public/css/all.min.css
vendored
2
001.FRONTEND/public/js/flowbite.min.js
vendored
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,199 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/AkcijePanel.astro
|
|
||||||
|
|
||||||
import Button from './Button.astro';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
tip: 'dashboard' | 'radni-nalog' | 'stroj' | 'vlasnik' | 'vozilo'; // DODANO: vozilo
|
|
||||||
podaci: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tip, podaci } = Astro.props;
|
|
||||||
|
|
||||||
// Pomoćna funkcija za formatiranje datuma
|
|
||||||
const formatDate = (date: string) =>
|
|
||||||
new Date(date).toLocaleDateString('hr-HR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
||||||
---
|
|
||||||
|
|
||||||
<aside class="space-y-6">
|
|
||||||
<div class="grid gap-3">
|
|
||||||
{tip === 'dashboard' && (
|
|
||||||
<>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<h2 class="text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight italic px-4">
|
|
||||||
Upravljanje
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<a href="/operativa/radni-nalozi/novi" class="group bg-blue-600 text-white p-6 rounded-[2rem] flex items-center gap-4 hover:bg-blue-700 transition-all shadow-xl shadow-blue-500/20 active:scale-95">
|
|
||||||
<div class="bg-white text-blue-600 w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg group-hover:rotate-12 transition-transform shrink-0">
|
|
||||||
<i class="fa-solid fa-plus text-xl"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col text-left">
|
|
||||||
<span class="font-black uppercase text-xs tracking-widest leading-none">Novi nalog</span>
|
|
||||||
<span class="text-[10px] opacity-70 mt-1 italic font-bold uppercase">Inicijaliziraj nalog</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/fleet/vozila" class="group bg-white dark:bg-gray-800 text-gray-900 dark:text-white p-6 rounded-[2rem] flex items-center gap-4 hover:border-blue-600 border border-gray-100 dark:border-gray-700 transition-all shadow-sm active:scale-95">
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 w-12 h-12 rounded-2xl flex items-center justify-center group-hover:rotate-12 transition-transform shrink-0">
|
|
||||||
<i class="fa-solid fa-truck-pickup text-xl"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col text-left">
|
|
||||||
<span class="font-black uppercase text-xs tracking-widest dark:text-white leading-none">Vozni park</span>
|
|
||||||
<span class="text-[10px] text-gray-400 mt-1 italic font-bold uppercase tracking-tighter">Status flote</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/kupci/svi" class="group bg-white dark:bg-gray-800 text-gray-900 dark:text-white p-6 rounded-[2rem] flex items-center gap-4 hover:border-indigo-600 border border-gray-100 dark:border-gray-700 transition-all shadow-sm active:scale-95">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 w-12 h-12 rounded-2xl flex items-center justify-center group-hover:rotate-12 transition-transform shrink-0">
|
|
||||||
<i class="fa-solid fa-address-book text-xl"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col text-left">
|
|
||||||
<span class="font-black uppercase text-xs tracking-widest dark:text-white leading-none">Klijenti</span>
|
|
||||||
<span class="text-[10px] text-gray-400 mt-1 italic font-bold uppercase tracking-tighter">Baza partnera</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<Button variant="primary" class="w-full justify-center gap-2 !rounded-2xl py-4 uppercase font-black italic tracking-widest">
|
|
||||||
<i class="fa-solid fa-print"></i> Ispis naloga
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip === 'radni-nalog' && (
|
|
||||||
<>
|
|
||||||
<button class="w-full py-4 bg-blue-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-blue-700 transition-all shadow-lg shadow-blue-500/20">
|
|
||||||
<i class="fa-solid fa-check-double mr-2"></i> Završi nalog
|
|
||||||
</button>
|
|
||||||
<button class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-gray-200 transition-all">
|
|
||||||
<i class="fa-solid fa-print mr-2"></i> Ispis (PDF)
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip === 'stroj' && (
|
|
||||||
<>
|
|
||||||
<button class="w-full py-4 bg-green-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-green-700 transition-all shadow-lg shadow-green-500/20">
|
|
||||||
<i class="fa-solid fa-plus mr-2"></i> Novi Radni Nalog
|
|
||||||
</button>
|
|
||||||
<button class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest">
|
|
||||||
<i class="fa-solid fa-file-contract mr-2"></i> Atesti i Dokumenti
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip === 'vlasnik' && (
|
|
||||||
<>
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[2.5rem] p-6 shadow-xl border border-gray-100 dark:border-gray-700 mb-6">
|
|
||||||
<label class="text-[9px] font-black uppercase text-indigo-600 block mb-4 tracking-[0.2em] italic">Upravljanje klijentom</label>
|
|
||||||
<div class="grid gap-3">
|
|
||||||
<Button class="w-full py-4 bg-indigo-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-indigo-700 transition-all shadow-lg shadow-indigo-500/20">
|
|
||||||
<i class="fa-solid fa-pen-to-square mr-2"></i> Uredi podatke
|
|
||||||
</Button>
|
|
||||||
<a href={`tel:${podaci.telefon}`} class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-gray-200 transition-all text-center no-underline">
|
|
||||||
<i class="fa-solid fa-phone mr-2"></i> Nazovi klijenta
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-indigo-50/50 dark:bg-indigo-900/10 rounded-[2.5rem] p-6 border border-dashed border-indigo-200 dark:border-indigo-800">
|
|
||||||
<label class="text-[9px] font-black uppercase text-indigo-400 block mb-4 tracking-[0.2em] italic">Podaci o tvrtki</label>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex flex-col border-b border-indigo-100/50 dark:border-indigo-900/30 pb-2">
|
|
||||||
<span class="text-[9px] font-bold text-gray-400 uppercase">OIB</span>
|
|
||||||
<span class="text-sm font-black dark:text-gray-200 tracking-tighter">{podaci.oib}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col border-b border-indigo-100/50 dark:border-indigo-900/30 pb-2">
|
|
||||||
<span class="text-[9px] font-bold text-gray-400 uppercase">Adresa</span>
|
|
||||||
<span class="text-sm font-black dark:text-gray-200 leading-tight">{podaci.adresa}, {podaci.grad}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-[9px] font-bold text-gray-400 uppercase">Email</span>
|
|
||||||
<span class="text-sm font-black text-indigo-600 truncate">{podaci.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip === 'vozilo' && (
|
|
||||||
<>
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[2.5rem] p-6 shadow-xl border border-gray-100 dark:border-gray-700 mb-2">
|
|
||||||
<label class="text-[9px] font-black uppercase text-blue-600 block mb-4 tracking-[0.2em] italic">Upravljanje vozilom</label>
|
|
||||||
<div class="grid gap-3">
|
|
||||||
<Button
|
|
||||||
id="btn-brzi-servis"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full justify-center py-4 rounded-2xl font-black uppercase text-[11px] tracking-widest shadow-lg shadow-blue-500/20"
|
|
||||||
data-id={podaci.id}
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-screwdriver-wrench mr-2"></i> Otvori Servis
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
href={`/operativa/radni-nalozi?vozilo=${podaci.id}`}
|
|
||||||
variant="secondary"
|
|
||||||
class="w-full justify-center py-4 rounded-2xl font-black uppercase text-[11px] tracking-widest"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-file-invoice mr-2 opacity-70"></i> Radni Nalozi
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-900/40 rounded-[2.5rem] p-6 border border-dashed border-gray-200 dark:border-gray-700">
|
|
||||||
<label class="text-[9px] font-black uppercase text-gray-400 block mb-4 tracking-[0.2em] italic">Informacije sustava</label>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
{tip === 'radni-nalog' && (
|
|
||||||
<>
|
|
||||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
|
||||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Status</span>
|
|
||||||
<span class="text-xs font-black text-red-500 uppercase italic">{podaci.status}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
|
||||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Izradio</span>
|
|
||||||
<span class="text-xs font-black dark:text-gray-200">{podaci.izvrsitelj_ime}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip === 'stroj' && (
|
|
||||||
<>
|
|
||||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
|
||||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Zadnji Atest</span>
|
|
||||||
<span class="text-xs font-black text-green-500 italic">{formatDate(podaci.datum_zadnjeg_atesta)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
|
||||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Godina Proizv.</span>
|
|
||||||
<span class="text-xs font-black dark:text-gray-200">{podaci.godina_proizvodnje}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
|
||||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Sustav ID</span>
|
|
||||||
<span class="text-xs font-black dark:text-gray-200">#{podaci.id}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tip === 'vozilo' && (
|
|
||||||
<>
|
|
||||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
|
||||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Sustav ID</span>
|
|
||||||
<span class="text-xs font-black dark:text-gray-200">#{podaci.id}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
|
||||||
<span class="text-[10px] font-bold text-gray-400 uppercase">DB Status</span>
|
|
||||||
<span class="text-xs font-black text-blue-500 uppercase italic">{podaci.status}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/Button.astro
|
|
||||||
interface Props {
|
|
||||||
type?: "button" | "submit";
|
|
||||||
variant?: "primary" | "danger" | "outline";
|
|
||||||
id?: string;
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type = "button", variant = "primary", id, class: className } = Astro.props;
|
|
||||||
|
|
||||||
const variants = {
|
|
||||||
primary: "bg-blue-600 hover:bg-blue-700 text-white shadow-blue-500/20",
|
|
||||||
danger: "bg-red-600 hover:bg-red-700 text-white shadow-red-500/20",
|
|
||||||
outline: "bg-transparent border-2 border-gray-200 text-gray-500 hover:border-blue-600 hover:text-blue-600 shadow-none"
|
|
||||||
};
|
|
||||||
---
|
|
||||||
|
|
||||||
<button
|
|
||||||
type={type}
|
|
||||||
id={id}
|
|
||||||
class:list={[
|
|
||||||
"inline-flex items-center justify-center gap-4 px-10 py-6 rounded-[2rem] font-black uppercase text-xs tracking-[0.4em] transition-all active:scale-[0.98] shadow-2xl",
|
|
||||||
variants[variant],
|
|
||||||
className
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<slot name="icon" />
|
|
||||||
<slot />
|
|
||||||
</button>
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/Gallery.astro
|
|
||||||
import { Image } from 'astro:assets';
|
|
||||||
import 'photoswipe/style.css';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
// Prilagođeno tvom serializeru: 'url' je zapravo 'slika' u JSON-u
|
|
||||||
images: {
|
|
||||||
slika: string;
|
|
||||||
opis?: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
}[];
|
|
||||||
galleryId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { images = [], galleryId } = Astro.props;
|
|
||||||
|
|
||||||
// Filtriramo važeće slike (pazimo na naziv polja 'slika' iz serializera)
|
|
||||||
const validImages = images.filter(img => img && img.slika);
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4" id={galleryId}>
|
|
||||||
{validImages.map((img) => (
|
|
||||||
<a
|
|
||||||
href={img.slika}
|
|
||||||
data-pswp-width={img.width || 1600}
|
|
||||||
data-pswp-height={img.height || 1200}
|
|
||||||
target="_blank"
|
|
||||||
class="group relative block aspect-square overflow-hidden rounded-[2rem] bg-gray-100 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 shadow-sm"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={img.slika}
|
|
||||||
alt={img.opis || "Detalj kvara"}
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
class="object-cover h-full w-full transition-transform duration-500 group-hover:scale-110"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Overlay s opisom ako postoji -->
|
|
||||||
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center text-white p-4 text-center">
|
|
||||||
<i class="fa-solid fa-expand text-xl mb-2"></i>
|
|
||||||
{img.opis && <span class="text-[10px] font-black uppercase tracking-widest">{img.opis}</span>}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{validImages.length === 0 && (
|
|
||||||
<div class="p-12 border-2 border-dashed border-gray-100 dark:border-gray-800 rounded-[2rem] text-center">
|
|
||||||
<i class="fa-solid fa-images text-gray-200 dark:text-gray-700 text-3xl mb-3"></i>
|
|
||||||
<p class="text-gray-400 text-xs font-medium italic">Nema priložene foto dokumentacije.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
|
||||||
|
|
||||||
const initPhotoSwipe = () => {
|
|
||||||
// Hvata sve galerije, neovisno o prefiksu, važno je da imaju ID
|
|
||||||
const lightboxes = document.querySelectorAll('.grid[id]');
|
|
||||||
lightboxes.forEach((el) => {
|
|
||||||
const lightbox = new PhotoSwipeLightbox({
|
|
||||||
gallery: `#${el.id}`,
|
|
||||||
children: 'a',
|
|
||||||
pswpModule: () => import('photoswipe'),
|
|
||||||
// Dodajemo opciju za kretanje kotačićem miša
|
|
||||||
wheelToZoom: true
|
|
||||||
});
|
|
||||||
lightbox.init();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Inicijalizacija pri učitavanju i pri Astro navigaciji (View Transitions)
|
|
||||||
initPhotoSwipe();
|
|
||||||
document.addEventListener('astro:after-swap', initPhotoSwipe);
|
|
||||||
</script>
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/GenericKarticaItem.astro
|
|
||||||
import { getStatusColorClass } from "../utils/ui";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
href: string;
|
|
||||||
status: string;
|
|
||||||
statusBojaClass?: string;
|
|
||||||
ikona: string;
|
|
||||||
naslov: string;
|
|
||||||
subNaslov?: string;
|
|
||||||
metaTekst: string;
|
|
||||||
statusPrikaz: string;
|
|
||||||
bojaTeme?: "blue" | "indigo" | "emerald" | "red"; // Podrška za teme
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
href,
|
|
||||||
status,
|
|
||||||
statusBojaClass,
|
|
||||||
ikona,
|
|
||||||
naslov,
|
|
||||||
subNaslov,
|
|
||||||
metaTekst,
|
|
||||||
statusPrikaz,
|
|
||||||
bojaTeme = "blue"
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
// Mapiranje tema na Tailwind klase
|
|
||||||
const themes = {
|
|
||||||
blue: {
|
|
||||||
hover: "hover:bg-blue-50/30 dark:hover:bg-blue-900/10",
|
|
||||||
text: "group-hover:text-blue-600 dark:group-hover:text-blue-400",
|
|
||||||
iconBg: "group-hover:bg-blue-600",
|
|
||||||
arrow: "group-hover:text-blue-600"
|
|
||||||
},
|
|
||||||
indigo: {
|
|
||||||
hover: "hover:bg-indigo-50/30 dark:hover:bg-indigo-900/10",
|
|
||||||
text: "group-hover:text-indigo-600 dark:group-hover:text-indigo-400",
|
|
||||||
iconBg: "group-hover:bg-indigo-600",
|
|
||||||
arrow: "group-hover:text-indigo-600"
|
|
||||||
},
|
|
||||||
emerald: {
|
|
||||||
hover: "hover:bg-emerald-50/30 dark:hover:bg-emerald-900/10",
|
|
||||||
text: "group-hover:text-emerald-600 dark:group-hover:text-emerald-400",
|
|
||||||
iconBg: "group-hover:bg-emerald-600",
|
|
||||||
arrow: "group-hover:text-emerald-400"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeTheme = themes[bojaTeme] || themes.blue;
|
|
||||||
const resolvedStatusColor = statusBojaClass || getStatusColorClass(status);
|
|
||||||
---
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
class:list={[
|
|
||||||
"nalog-item flex items-center justify-between p-8 border-b border-gray-50 dark:border-gray-700 last:border-0 group transition-all duration-300",
|
|
||||||
activeTheme.hover
|
|
||||||
]}
|
|
||||||
data-status={status}
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-6 text-left">
|
|
||||||
<!-- Ikona s dinamičkom bojom na hoveru -->
|
|
||||||
<div class:list={[
|
|
||||||
"w-14 h-14 rounded-2xl flex items-center justify-center text-xl shadow-inner transition-all duration-300 bg-gray-50 dark:bg-gray-900 text-gray-400 group-hover:text-white",
|
|
||||||
activeTheme.iconBg
|
|
||||||
]}>
|
|
||||||
<i class:list={["fa-solid", ikona, status === 'u_radu' ? 'animate-pulse' : '']}></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class:list={[
|
|
||||||
"font-black text-gray-900 dark:text-white text-xl uppercase tracking-tighter italic transition-colors leading-none",
|
|
||||||
activeTheme.text
|
|
||||||
]}>
|
|
||||||
{naslov}
|
|
||||||
</span>
|
|
||||||
{subNaslov && (
|
|
||||||
<span class="text-[10px] font-black bg-gray-900 text-white px-2 py-0.5 rounded border border-gray-700 uppercase tracking-[0.2em]">
|
|
||||||
{subNaslov}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="text-gray-400 text-[10px] font-black uppercase tracking-widest mt-2 italic leading-none">
|
|
||||||
{metaTekst}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-8">
|
|
||||||
<!-- Status indikator desno -->
|
|
||||||
<div class="hidden lg:flex flex-col items-end text-right">
|
|
||||||
<div class="flex items-center gap-2 mb-1">
|
|
||||||
<div class:list={["w-2 h-2 rounded-full", resolvedStatusColor]}></div>
|
|
||||||
<span class="text-[8px] font-black uppercase text-gray-400 tracking-[0.2em]">Status</span>
|
|
||||||
</div>
|
|
||||||
<span class:list={[
|
|
||||||
"text-xs font-black uppercase italic transition-colors",
|
|
||||||
status === 'zavrseno' || status === 'naplaceno' || status === 'aktivan' ? 'text-emerald-500' : activeTheme.text
|
|
||||||
]}>
|
|
||||||
{statusPrikaz}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Strelica -->
|
|
||||||
<i class:list={[
|
|
||||||
"fa-solid fa-arrow-right-long text-gray-200 group-hover:translate-x-2 transition-all duration-300",
|
|
||||||
activeTheme.arrow
|
|
||||||
]}></i>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/Kalendar.astro
|
|
||||||
import { fetchCalendarEvents } from "../../lib/api";
|
|
||||||
import { getStatusColorClass } from "../../utils/ui";
|
|
||||||
|
|
||||||
// 1. Dohvat podataka s Django API-ja kroz tvoj centralizirani lib
|
|
||||||
const apiEvents = await fetchCalendarEvents();
|
|
||||||
|
|
||||||
// 2. Grupiranje događaja po datumu (YYYY-MM-DD)
|
|
||||||
const eventsByDate = apiEvents.reduce((acc, event) => {
|
|
||||||
if (!event.start) return acc;
|
|
||||||
|
|
||||||
const dateKey = new Date(event.start).toISOString().split('T')[0];
|
|
||||||
if (!acc[dateKey]) acc[dateKey] = [];
|
|
||||||
|
|
||||||
// Koristimo čista, razdvojena polja s novog Django serializera
|
|
||||||
acc[dateKey].push({
|
|
||||||
id: event.id,
|
|
||||||
title: event.title,
|
|
||||||
opis: event.opis_cisti, // Čisti opis kvara s backenda
|
|
||||||
start: event.start,
|
|
||||||
tip: (event.tip || event.status || 'planirano').toLowerCase(),
|
|
||||||
radni_nalog: event.radni_nalog || event.id,
|
|
||||||
je_radni_nalog: event.je_radni_nalog !== undefined ? event.je_radni_nalog : true,
|
|
||||||
izvrsitelj_ime: event.izvrsitelj_ime,
|
|
||||||
klijent: event.klijent || null,
|
|
||||||
stroj: event.stroj || null,
|
|
||||||
vozilo: event.vozilo_naziv // Čisti naziv vozila s backenda
|
|
||||||
});
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Postavke za trenutni prikaz (Svibanj 2026)
|
|
||||||
const daysInMonth = 31;
|
|
||||||
const firstDayOffset = 5; // Petak
|
|
||||||
const currentYearMonth = "2026-05";
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-2xl rounded-[3rem] border border-gray-100 dark:border-gray-700 overflow-hidden font-sans transition-all">
|
|
||||||
|
|
||||||
<div class="flex justify-between items-center bg-gray-50/50 dark:bg-gray-900/50 px-8 py-6 border-b border-gray-100 dark:border-gray-700">
|
|
||||||
<button class="text-gray-400 hover:text-blue-600 transition-colors text-2xl font-black">‹</button>
|
|
||||||
<div class="text-center">
|
|
||||||
<h2 class="text-xl font-black uppercase tracking-tighter italic leading-none">Svibanj 2026</h2>
|
|
||||||
<span class="text-[10px] text-blue-600 font-bold uppercase tracking-[0.2em] mt-1 italic">Raspored servisa</span>
|
|
||||||
</div>
|
|
||||||
<button class="text-gray-400 hover:text-blue-600 transition-colors text-2xl font-black">›</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="calendar-body">
|
|
||||||
<div class="grid grid-cols-7 text-center text-[10px] text-gray-400 dark:text-gray-500 uppercase font-black tracking-[0.2em] py-4 border-b border-gray-50 dark:border-gray-700/50">
|
|
||||||
<div>Ned</div><div>Pon</div><div>Uto</div><div>Sri</div><div>Čet</div><div>Pet</div><div>Sub</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-7 text-center" id="days-grid">
|
|
||||||
{Array.from({ length: firstDayOffset }).map(() => (
|
|
||||||
<div class="py-6 border-b border-r border-gray-50 dark:border-gray-700/10 opacity-0"></div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{Array.from({ length: daysInMonth }).map((_, i) => {
|
|
||||||
const day = i + 1;
|
|
||||||
const dateKey = `${currentYearMonth}-${day.toString().padStart(2, '0')}`;
|
|
||||||
const dayEvents = eventsByDate[dateKey] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="day-cell py-6 relative cursor-pointer hover:bg-blue-50/50 dark:hover:bg-gray-700/30 transition-all border-b border-r border-gray-50 dark:border-gray-700/30 group"
|
|
||||||
data-day={day}
|
|
||||||
data-events={JSON.stringify(dayEvents)}
|
|
||||||
>
|
|
||||||
<span class={`text-3xl font-black italic tracking-tighter transition-colors ${dayEvents.length > 0 ? 'text-gray-900 dark:text-white' : 'text-gray-300 dark:text-gray-600'} group-hover:text-blue-600`}>
|
|
||||||
{day.toString().padStart(2, '0')}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="flex justify-center gap-1 mt-1 h-1.5">
|
|
||||||
{dayEvents.map(event => (
|
|
||||||
<div class={`w-1.5 h-1.5 rounded-full ${getStatusColorClass(event.tip)}`}></div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div id="event-panel" class="hidden col-span-7 bg-blue-600 text-white p-8 relative overflow-visible shadow-2xl">
|
|
||||||
<div class="absolute -top-2 left-0 w-full h-2">
|
|
||||||
<div id="panel-arrow" class="absolute w-0 h-0 border-l-[12px] border-l-transparent border-r-[12px] border-r-transparent border-b-[12px] border-b-blue-600 transition-all duration-300"></div>
|
|
||||||
</div>
|
|
||||||
<div id="event-list" class="flex flex-col gap-6 w-full">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function initKalendar() {
|
|
||||||
const grid = document.getElementById('days-grid');
|
|
||||||
const panel = document.getElementById('event-panel');
|
|
||||||
const arrow = document.getElementById('panel-arrow');
|
|
||||||
const eventList = document.getElementById('event-list');
|
|
||||||
const dayCells = document.querySelectorAll('.day-cell');
|
|
||||||
|
|
||||||
if (!grid || !panel) return;
|
|
||||||
|
|
||||||
dayCells.forEach(cell => {
|
|
||||||
cell.addEventListener('click', () => {
|
|
||||||
const eventsData = JSON.parse(cell.getAttribute('data-events') || '[]');
|
|
||||||
|
|
||||||
if (eventsData.length > 0) {
|
|
||||||
const allItems = Array.from(grid.children);
|
|
||||||
const index = allItems.indexOf(cell);
|
|
||||||
|
|
||||||
// 1. Pozicioniranje strelice
|
|
||||||
const colIndex = index % 7;
|
|
||||||
const percentage = (colIndex * (100 / 7)) + (100 / 7 / 2);
|
|
||||||
arrow.style.left = `calc(${percentage}% - 12px)`;
|
|
||||||
|
|
||||||
// 2. Injekcija panela na kraj tjedna (redka)
|
|
||||||
const rowEndIndex = Math.floor(index / 7) * 7 + 6;
|
|
||||||
const targetCell = allItems[Math.min(rowEndIndex, allItems.length - 1)];
|
|
||||||
|
|
||||||
// 3. Generiranje liste (Struktura točno prema tvojoj ispravno.png skici)
|
|
||||||
eventList.innerHTML = eventsData.map(ev => {
|
|
||||||
const klijentStroj = [ev.klijent, ev.stroj].filter(Boolean).join(' | ');
|
|
||||||
const infoLinija = klijentStroj
|
|
||||||
? `<div class="text-[13px] opacity-80 font-medium tracking-tight mt-1"><i class="fa-solid fa-industry text-[11px] mr-1 opacity-60"></i> ${klijentStroj}</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// Čisti naziv vozila u zasebnom redu (bez labela "Vozilo:")
|
|
||||||
const voziloRed = ev.vozilo
|
|
||||||
? `<p class="text-sm font-medium mt-1 text-white opacity-90">
|
|
||||||
${ev.vozilo}
|
|
||||||
</p>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// ISTAKNUTO: Opis kvara u posebnom uokvirenom kontejneru radi boljeg kontrasta
|
|
||||||
const opisRed = `
|
|
||||||
<div class="mt-3 p-3 bg-black/15 rounded-xl border border-white/10 text-left">
|
|
||||||
<p class="text-[10px] font-black uppercase tracking-widest text-amber-300 opacity-90 mb-1">
|
|
||||||
Opis kvara:
|
|
||||||
</p>
|
|
||||||
<p class="text-sm font-medium text-white leading-relaxed">
|
|
||||||
${ev.opis}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="flex items-start gap-5 border-b border-white/20 pb-6 last:border-0 last:pb-0 group/item">
|
|
||||||
<a href="/operativa/radni-nalozi/${ev.radni_nalog}" class="bg-white/10 hover:bg-white hover:text-blue-600 text-white p-4 rounded-2xl transition-all shadow-lg shrink-0 group-hover/item:scale-105 active:scale-95">
|
|
||||||
<i class="fa-solid ${ev.je_radni_nalog ? 'fa-file-signature' : 'fa-wrench'} text-2xl"></i>
|
|
||||||
</a>
|
|
||||||
<div class="flex flex-col flex-1 text-left">
|
|
||||||
|
|
||||||
<div class="flex justify-between items-start">
|
|
||||||
<a href="/operativa/radni-nalozi/${ev.radni_nalog}" class="hover:underline">
|
|
||||||
<span class="font-black uppercase text-2xl tracking-tighter italic leading-none">${ev.title}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${infoLinija}
|
|
||||||
|
|
||||||
${voziloRed}
|
|
||||||
|
|
||||||
${opisRed}
|
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap gap-3">
|
|
||||||
<span class="text-[10px] font-black uppercase tracking-widest bg-black/30 px-3 py-1.5 rounded-lg flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-user-gear opacity-50"></i> ${ev.izvrsitelj_ime}
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] font-black uppercase tracking-widest bg-black/30 px-3 py-1.5 rounded-lg flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-clock opacity-50"></i> ${new Date(ev.start).toLocaleTimeString('hr-HR', {hour: '2-digit', minute:'2-digit'})} h
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
targetCell.after(panel);
|
|
||||||
panel.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
panel.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initKalendar();
|
|
||||||
document.addEventListener('astro:after-swap', initKalendar);
|
|
||||||
</script>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/ListaStrojeva.astro
|
|
||||||
import NaslovList from "./NaslovList.astro";
|
|
||||||
import GenericKarticaItem from "./GenericKarticaItem.astro";
|
|
||||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
strojevi: any[];
|
|
||||||
naslov?: string;
|
|
||||||
prikaziNaslov?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
strojevi = [],
|
|
||||||
naslov = "Tehničke jedinice",
|
|
||||||
prikaziNaslov = true
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
// Statistika za NaslovList (npr. koliko ih je aktivno ili na servisu ako imaš te podatke)
|
|
||||||
const ukupnoStrojeva = strojevi.length;
|
|
||||||
// Pretpostavljamo da strojevi imaju status, ako nemaju, možemo staviti fiksne labele
|
|
||||||
const uRadu = strojevi.filter(s => s.status === 'u_radu').length;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
|
|
||||||
{prikaziNaslov && (
|
|
||||||
<NaslovList
|
|
||||||
naslov={naslov}
|
|
||||||
ukupno={ukupnoStrojeva}
|
|
||||||
label1="Ukupno"
|
|
||||||
count1={ukupnoStrojeva}
|
|
||||||
label2="Aktivno"
|
|
||||||
count2={uRadu}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
|
||||||
<div id="strojevi-list">
|
|
||||||
{strojevi.length > 0 ? strojevi.map((s) => (
|
|
||||||
<GenericKarticaItem
|
|
||||||
href={`/fleet/strojevi/${s.id}`}
|
|
||||||
status={s.status || 'aktivan'}
|
|
||||||
statusBojaClass={getStatusColorClass(s.status || 'aktivan')}
|
|
||||||
ikona="fa-screwdriver-wrench"
|
|
||||||
naslov={s.naziv}
|
|
||||||
subNaslov={s.serijski_broj}
|
|
||||||
metaTekst={`${s.vlasnik_naziv} | Radni sati: ${parseFloat(s.radni_sati || 0).toLocaleString('hr-HR')} h`}
|
|
||||||
statusPrikaz={formatStatus(s.status || 'aktivan')}
|
|
||||||
/>
|
|
||||||
)) : (
|
|
||||||
<div class="p-20 text-center italic text-gray-400 text-[10px] uppercase tracking-widest leading-none">
|
|
||||||
<i class="fa-solid fa-box-open text-3xl text-gray-100 dark:text-gray-700 mb-4 block"></i>
|
|
||||||
Nema dostupnih tehničkih jedinica
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/LoginForm.astro
|
|
||||||
import Button from "./Button.astro";
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="max-w-md mx-auto w-full p-10 bg-white dark:bg-gray-900 rounded-[3rem] border border-gray-100 dark:border-gray-800 shadow-2xl shadow-blue-500/10">
|
|
||||||
<div class="mb-10 text-center sm:text-left">
|
|
||||||
<div class="inline-flex w-14 h-14 bg-blue-600 rounded-2xl items-center justify-center shadow-lg shadow-blue-500/20 mb-6 rotate-3 sm:-ml-2">
|
|
||||||
<i class="fa-solid fa-lock text-white text-xl"></i>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-4xl font-black text-gray-900 dark:text-white uppercase tracking-tighter italic leading-none">Prijava</h2>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] mt-3 opacity-70 italic">
|
|
||||||
ServisLog <span class="text-blue-600">/</span> Terminal
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="login-form" class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label for="email" class="block text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 mb-2 ml-4 italic">Korisnički E-mail</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
required
|
|
||||||
class="w-full px-6 py-4 bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-blue-600 rounded-2xl outline-none transition-all text-gray-900 dark:text-white font-bold italic shadow-inner"
|
|
||||||
placeholder="serviser@tvrtka.hr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password" class="block text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 mb-2 ml-4 italic">Lozinka</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
required
|
|
||||||
class="w-full px-6 py-4 bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-blue-600 rounded-2xl outline-none transition-all text-gray-900 dark:text-white font-bold tracking-[0.3em] shadow-inner"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-2">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
id="login-btn"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full !py-5 italic"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-bolt" slot="icon"></i>
|
|
||||||
Pokreni Sustav
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { login, fetchCurrentUser } from "../lib/api";
|
|
||||||
// 🚀 Ponovno uvozimo centraliziranu funkciju za rute
|
|
||||||
import { dohvatiRutuPoUlozi } from "../utils/auth";
|
|
||||||
|
|
||||||
const loginForm = document.querySelector('#login-form') as HTMLFormElement;
|
|
||||||
const btn = document.querySelector('#login-btn') as HTMLButtonElement;
|
|
||||||
|
|
||||||
loginForm?.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!btn) return;
|
|
||||||
|
|
||||||
const originalContent = btn.innerHTML;
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-3"></i> AUTENTIFIKACIJA...';
|
|
||||||
|
|
||||||
const formData = new FormData(loginForm);
|
|
||||||
const email = formData.get('email') as string;
|
|
||||||
const password = formData.get('password') as string;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Prijava i pohrana JWT tokena
|
|
||||||
const result = await login(email, password);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// 2. Dohvat profila ulogiranog korisnika
|
|
||||||
const user = await fetchCurrentUser();
|
|
||||||
|
|
||||||
sessionStorage.setItem('pending_toast', JSON.stringify({
|
|
||||||
type: 'success',
|
|
||||||
message: 'Pristup odobren. Dobrodošli u sustav.'
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 3. 🚀 PREUSMJERAVANJE PREKO POMOĆNE FUNKCIJE:
|
|
||||||
// Namjerno NE prosljeđujemo user.id jer želimo da serviser sleti na opću /operativa/serviseri stazu
|
|
||||||
const ciljanaRuta = dohvatiRutuPoUlozi(user?.uloga);
|
|
||||||
|
|
||||||
window.location.href = ciljanaRuta;
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
window.showToast?.(result.error || "Pristup odbijen: Neispravni podaci.", "error");
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalContent;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Login krah na klijentu:", err);
|
|
||||||
// @ts-ignore
|
|
||||||
window.showToast?.("Sustav nedostupan. Provjerite vezu.", "error");
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalContent;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/RadniNalogLista.astro
|
|
||||||
import NaslovList from "./NaslovList.astro";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
limit?: number;
|
|
||||||
naslov?: string;
|
|
||||||
prikaziNaslov?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
limit = 0,
|
|
||||||
naslov = "Zadnje aktivnosti",
|
|
||||||
prikaziNaslov = true,
|
|
||||||
} = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="space-y-6 group" id="radni-nalozi-sekcija" data-state="loading">
|
|
||||||
{prikaziNaslov && (
|
|
||||||
<NaslovList
|
|
||||||
naslov={naslov}
|
|
||||||
ukupno={0}
|
|
||||||
label1="Planirano"
|
|
||||||
count1={0}
|
|
||||||
filter1="planirano"
|
|
||||||
label2="U radu"
|
|
||||||
count2={0}
|
|
||||||
filter2="u_radu"
|
|
||||||
label3="Završeno"
|
|
||||||
count3={0}
|
|
||||||
filter3="zavrseno"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="nalozi-loader"
|
|
||||||
class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse group-data-[state=loaded]:hidden group-data-[state=error]:hidden"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija radnih naloga...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="nalozi-list" class="space-y-0">
|
|
||||||
<div id="nalozi-placeholder" class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest group-data-[state=loaded]:hidden">
|
|
||||||
Nema zapisa za odabrani status
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span id="rn-bridge" data-limit={limit} data-naslov={naslov}></span>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { fetchCurrentUser, fetchDashboardData, fetchServiserTerminalData } from "../lib/api";
|
|
||||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
|
||||||
import { initFilters } from "../scripts/filters.js";
|
|
||||||
|
|
||||||
async function renderirajRadneNalogeKlijentski() {
|
|
||||||
const container = document.getElementById('nalozi-list');
|
|
||||||
const sekcija = document.getElementById('radni-nalozi-sekcija');
|
|
||||||
const bridge = document.getElementById('rn-bridge');
|
|
||||||
|
|
||||||
if (!container || !sekcija || !bridge) return;
|
|
||||||
|
|
||||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
sekcija.setAttribute('data-state', 'error');
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="p-20 text-center flex flex-col items-center justify-center gap-3">
|
|
||||||
<div class="w-12 h-12 bg-red-600/10 text-red-600 rounded-xl flex items-center justify-center text-lg mb-2">
|
|
||||||
<i class="fa-solid fa-shield-halved"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="text-xs font-black uppercase tracking-widest text-red-600">Pristup odbijen</h4>
|
|
||||||
<p class="text-[10px] text-gray-400 font-bold uppercase tracking-wider italic">Niste autorizirani za pregled operativnih naloga flote.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = parseInt(bridge.getAttribute('data-limit') || '0');
|
|
||||||
const originalNaslov = bridge.getAttribute('data-naslov') || 'Zadnje aktivnosti';
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const statusFilter = urlParams.get('status')?.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
sekcija.setAttribute('data-state', 'loading');
|
|
||||||
|
|
||||||
const user = await fetchCurrentUser();
|
|
||||||
const ulogaGore = user?.uloga ? String(user.uloga).toUpperCase().trim() : '';
|
|
||||||
|
|
||||||
let nalozi = [];
|
|
||||||
|
|
||||||
if (ulogaGore === 'SERVISER' && user?.id) {
|
|
||||||
const terminalData = await fetchServiserTerminalData(user.id);
|
|
||||||
nalozi = terminalData?.radni_nalozi || [];
|
|
||||||
} else {
|
|
||||||
const data = await fetchDashboardData();
|
|
||||||
nalozi = data?.nalozi || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Proračun statistike na klijentskoj strani
|
|
||||||
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
|
||||||
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
|
||||||
const zavrsenoCount = nalozi.filter(n => ['zavrsen', 'završeno', 'gotovo', 'zavrseno'].includes(n.status?.toLowerCase())).length;
|
|
||||||
|
|
||||||
// 4. 🚀 USKLAĐENO HVAĆANJE I FILTRIRANJE PREMA TVOM data-filter PROPSU:
|
|
||||||
const card1 = sekcija.querySelector('[data-filter="planirano"]');
|
|
||||||
if (card1) {
|
|
||||||
const span = card1.querySelector('span');
|
|
||||||
if (span) span.textContent = `Planirano: ${planiranoCount}`;
|
|
||||||
if (planiranoCount > 0) card1.classList.remove('hidden'); else card1.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
const card2 = sekcija.querySelector('[data-filter="u_radu"]');
|
|
||||||
if (card2) {
|
|
||||||
const span = card2.querySelector('span');
|
|
||||||
if (span) span.textContent = `U radu: ${uRaduCount}`;
|
|
||||||
if (uRaduCount > 0) card2.classList.remove('hidden'); else card2.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
const card3 = sekcija.querySelector('[data-filter="zavrseno"]');
|
|
||||||
if (card3) {
|
|
||||||
const span = card3.querySelector('span');
|
|
||||||
if (span) span.textContent = `Završeno: ${zavrsenoCount}`;
|
|
||||||
if (zavrsenoCount > 0) card3.classList.remove('hidden'); else card3.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Filtriranje i limitiranje niza prema klijentskom stanju URL-a
|
|
||||||
let filtriraniNalozi = statusFilter ? nalozi.filter(n => n.status?.toLowerCase() === statusFilter) : nalozi;
|
|
||||||
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
|
|
||||||
|
|
||||||
const glavniNaslov = sekcija.querySelector('h2');
|
|
||||||
if (glavniNaslov) {
|
|
||||||
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
|
|
||||||
}
|
|
||||||
|
|
||||||
sekcija.setAttribute('data-state', 'loaded');
|
|
||||||
|
|
||||||
// 7. Generiranje HTML kartica radnih naloga unutar tijela
|
|
||||||
if (prikazaniNalozi.length === 0) {
|
|
||||||
container.innerHTML = `<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">Nema zapisa za odabrani status</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = prikazaniNalozi.map((n) => {
|
|
||||||
const statusMalo = n.status?.toLowerCase();
|
|
||||||
const statusKlasa = getStatusColorClass(statusMalo);
|
|
||||||
const ikona = statusMalo === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice';
|
|
||||||
|
|
||||||
const klijentPrikaz = n.kupac_naziv || n.klijent_naziv || 'Nepoznat klijent';
|
|
||||||
const strojPrikaz = n.stroj_naziv || 'Bez stroja';
|
|
||||||
const voziloPrikaz = n.vozilo_naziv || 'Bez vozila';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<a href="/operativa/radni-nalozi/${n.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
|
|
||||||
<i class="fa-solid ${ikona}"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${n.broj_naloga}</h4>
|
|
||||||
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${formatStatus(statusMalo)}</p>
|
|
||||||
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
|
|
||||||
${klijentPrikaz} | ${voziloPrikaz} | ${strojPrikaz}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
|
|
||||||
${formatStatus(statusMalo)}
|
|
||||||
</span>
|
|
||||||
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
|
|
||||||
<i class="fa-solid fa-chevron-right text-xs"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
initFilters();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Kritična greška u klijentskom renderu radnih naloga:", err);
|
|
||||||
sekcija.setAttribute('data-state', 'error');
|
|
||||||
container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji operativnih podataka.</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderirajRadneNalogeKlijentski();
|
|
||||||
document.addEventListener('astro:after-swap', renderirajRadneNalogeKlijentski);
|
|
||||||
</script>
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/Navbar.astro
|
|
||||||
import site from "../data/site.json";
|
|
||||||
import Button from "./Button.astro";
|
|
||||||
|
|
||||||
const currentPath = Astro.url.pathname;
|
|
||||||
---
|
|
||||||
|
|
||||||
<nav class="sticky top-0 z-[100] w-full bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-100 dark:border-gray-800">
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="flex justify-between h-20 items-center">
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<a href="/" class="flex items-center gap-3 group">
|
|
||||||
<div class="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20 group-hover:rotate-6 transition-transform duration-300">
|
|
||||||
<i class="fa-solid fa-screwdriver-wrench text-white text-lg"></i>
|
|
||||||
</div>
|
|
||||||
<span class="text-xl font-black tracking-tighter text-gray-900 dark:text-white uppercase">
|
|
||||||
{site.title.split(' ')[0]}<span class="text-blue-600">{site.title.split(' ')[1] || ''}</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="nav-auth-section" class="hidden flex items-center gap-4 group/auth" data-state="loading">
|
|
||||||
|
|
||||||
<div class="hidden group-data-[state=anonymous]/auth:block">
|
|
||||||
<Button
|
|
||||||
id="nav-login-btn"
|
|
||||||
href="/login"
|
|
||||||
variant="none"
|
|
||||||
class="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-2xl text-xs font-black uppercase tracking-wider shadow-lg shadow-blue-500/20 active:scale-95 transition-all border border-blue-500 cursor-pointer italic"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-right-to-bracket"></i> Prijava
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hidden group-data-[state=authenticated]/auth:block">
|
|
||||||
<Button
|
|
||||||
id="mobile-menu-button"
|
|
||||||
variant="none"
|
|
||||||
class="w-12 h-12 flex items-center justify-center rounded-2xl bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-300 active:scale-90 transition-all border border-gray-100 dark:border-gray-700 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700/50"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-bars-staggered text-xl"></i>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="mobile-menu"
|
|
||||||
class="hidden absolute top-[calc(100%+0.5rem)] md:left-auto left-4 right-4 md:right-8 md:w-80
|
|
||||||
bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl
|
|
||||||
border border-white/20 dark:border-gray-800/50
|
|
||||||
p-4 rounded-[2.5rem] shadow-2xl shadow-blue-500/5 z-[100]
|
|
||||||
transition-all animate-in fade-in slide-in-from-top-4"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{site.navigation.map((item) => {
|
|
||||||
// Provjera je li trenutna putanja aktivna (uzima u obzir i potencijalne dinamičke ID pod-rute)
|
|
||||||
const isCurrent = currentPath === item.url || (item.url === '/operativa/serviseri' && currentPath.includes('/operativa/serviseri'));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={item.url}
|
|
||||||
data-nav-link={item.url}
|
|
||||||
class={`flex items-center justify-between px-6 py-4 rounded-[1.8rem] font-black text-[11px] uppercase tracking-widest transition-all duration-300 group italic ${
|
|
||||||
isCurrent
|
|
||||||
? "bg-blue-600 text-white shadow-lg shadow-blue-500/20"
|
|
||||||
: "text-gray-600 dark:text-gray-300 hover:bg-blue-600 hover:text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
<i class="fa-solid fa-chevron-right text-[10px] opacity-30 group-hover:translate-x-1 group-hover:opacity-100 transition-all"></i>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div class="w-full mt-1 mb-2">
|
|
||||||
<Button
|
|
||||||
href="/operativa/radni-nalozi/novi"
|
|
||||||
variant="none"
|
|
||||||
class="w-full bg-blue-600 text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center shadow-lg shadow-blue-500/40 active:scale-95 transition-all block italic"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-plus mr-2"></i> Novi nalog
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-2 border-gray-100/50 dark:border-gray-800/50" />
|
|
||||||
|
|
||||||
<div class="w-full">
|
|
||||||
<Button
|
|
||||||
id="logout-mobile"
|
|
||||||
variant="none"
|
|
||||||
class="w-full bg-red-600/10 text-red-600 hover:bg-red-600 hover:text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center active:scale-95 transition-all border border-red-100/30 dark:border-red-900/30 cursor-pointer block italic"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-right-from-bracket mr-2"></i> Odjava iz sustava
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { logout, fetchCurrentUser } from "../lib/api";
|
|
||||||
|
|
||||||
async function setupNavbar() {
|
|
||||||
const btn = document.getElementById('mobile-menu-button');
|
|
||||||
const menu = document.getElementById('mobile-menu');
|
|
||||||
const authSection = document.getElementById('nav-auth-section');
|
|
||||||
const logoutMobile = document.getElementById('logout-mobile');
|
|
||||||
|
|
||||||
if (!authSection) return;
|
|
||||||
|
|
||||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
|
||||||
const stanjeAutentifikacije = token ? 'authenticated' : 'anonymous';
|
|
||||||
|
|
||||||
// 1. Postavljamo stanje u DOM (odmah prebacuje vidljivost gumbova Prijava / Menu)
|
|
||||||
authSection.setAttribute('data-state', stanjeAutentifikacije);
|
|
||||||
authSection.classList.remove('hidden');
|
|
||||||
authSection.classList.add('flex');
|
|
||||||
|
|
||||||
// 2. Dinamičko popravljanje URL putanje za servisera ako imamo token i menu elemente
|
|
||||||
if (token && menu) {
|
|
||||||
let userProfil = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 🚀 OBRANA OD KRAHA: Omotavamo hvat u zaseban try-catch kako greška s API-ja ne bi srušila cijeli navbar
|
|
||||||
userProfil = await fetchCurrentUser();
|
|
||||||
} catch (apiErr) {
|
|
||||||
console.warn("Navbar nije uspio dohvatiti trenutnog korisnika s API-ja:", apiErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userProfil && userProfil.id) {
|
|
||||||
const serviserLink = menu.querySelector('[data-nav-link="/operativa/serviseri"]') as HTMLAnchorElement;
|
|
||||||
if (serviserLink) {
|
|
||||||
serviserLink.href = `/operativa/serviseri/${userProfil.id}`;
|
|
||||||
|
|
||||||
// Popravak aktivne klase ukoliko smo točno na toj dinamičkoj ruti
|
|
||||||
if (window.location.pathname === `/operativa/serviseri/${userProfil.id}`) {
|
|
||||||
serviserLink.classList.add('bg-blue-600', 'text-white', 'shadow-lg', 'shadow-blue-500/20');
|
|
||||||
serviserLink.classList.remove('text-gray-600', 'dark:text-gray-300');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!btn || !menu) return;
|
|
||||||
|
|
||||||
// Resetiranje event listenera kloniranjem (sprječava duplanje na View Transitions okidanju)
|
|
||||||
const newBtn = btn.cloneNode(true) as HTMLButtonElement;
|
|
||||||
btn.parentNode?.replaceChild(newBtn, btn);
|
|
||||||
|
|
||||||
newBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
menu.classList.toggle('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!menu.contains(e.target as Node) && !newBtn.contains(e.target as Node)) {
|
|
||||||
menu.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.querySelectorAll('a').forEach(link => {
|
|
||||||
link.addEventListener('click', () => menu.classList.add('hidden'));
|
|
||||||
});
|
|
||||||
|
|
||||||
logoutMobile?.addEventListener('click', () => {
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Okidanje na inicijalno učitavanje i Astro after-swap
|
|
||||||
setupNavbar();
|
|
||||||
document.addEventListener('astro:after-swap', setupNavbar);
|
|
||||||
</script>
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/PutniNalogLista.astro
|
|
||||||
import NaslovList from "./NaslovList.astro";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
naslov?: string;
|
|
||||||
prikaziNaslov?: boolean;
|
|
||||||
voziloId?: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
naslov = "Putni nalozi i rute",
|
|
||||||
prikaziNaslov = true,
|
|
||||||
voziloId = null,
|
|
||||||
} = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="space-y-6" id="putni-nalozi-sekcija">
|
|
||||||
{prikaziNaslov && (
|
|
||||||
<NaslovList
|
|
||||||
naslov={naslov}
|
|
||||||
ukupno={0}
|
|
||||||
label1="Aktivne rute" count1={0} filter1="aktivan"
|
|
||||||
label2="Završeni" count2={0} filter2="zavrsen"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
|
||||||
<div id="pn-loader" class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse">
|
|
||||||
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija putnih naloga...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="putni-nalozi-list" class="space-y-0"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span id="pn-bridge" data-vozilo-id={voziloId} data-naslov={naslov}></span>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { fetchPutniNaloziData } from "../lib/api";
|
|
||||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
|
||||||
import { initFilters } from "../scripts/filters.js";
|
|
||||||
|
|
||||||
async function renderirajPutneNalogeKlijentski() {
|
|
||||||
const bridge = document.getElementById('pn-bridge');
|
|
||||||
const container = document.getElementById('putni-nalozi-list');
|
|
||||||
const loader = document.getElementById('pn-loader');
|
|
||||||
if (!bridge || !container) return;
|
|
||||||
|
|
||||||
const voziloId = bridge.getAttribute('data-vozilo-id');
|
|
||||||
const originalNaslov = bridge.getAttribute('data-naslov') || 'Putni nalozi i rute';
|
|
||||||
|
|
||||||
// Dohvat aktivnog filtera iz URL-a na klijentu (?status=aktivan)
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const statusFilter = urlParams.get('status')?.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Dohvat s API-ja (Sada sigurno čita localStorage token u pregledniku)
|
|
||||||
const data = await fetchPutniNaloziData();
|
|
||||||
|
|
||||||
// Prilagodba ako API vraća omotan objekt ili čisti niz
|
|
||||||
const sviPutniNalozi = Array.isArray(data) ? data : (data?.results || []);
|
|
||||||
|
|
||||||
// 2. STROGO FILTRIRANJE PREMA ID-JU VOZILA (Na razini podataka)
|
|
||||||
let filtriraniNalozi = sviPutniNalozi;
|
|
||||||
if (voziloId) {
|
|
||||||
const ciljaniId = parseInt(voziloId);
|
|
||||||
filtriraniNalozi = sviPutniNalozi.filter(pn => {
|
|
||||||
const vId = pn.vozilo?.id || pn.vozilo || pn.vozilo_id;
|
|
||||||
return parseInt(vId) === ciljaniId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Izračun statistike na klijentskoj strani (isključivo za ovo vozilo ako je ID prisutan)
|
|
||||||
const aktivniCount = filtriraniNalozi.filter(pn => pn.status?.toLowerCase() === 'aktivan').length;
|
|
||||||
const zavrseniCount = filtriraniNalozi.filter(pn => ['zavrsen', 'završeno'].includes(pn.status?.toLowerCase())).length;
|
|
||||||
|
|
||||||
// Opcionalno dodatno filtriranje po statusu s gornjih gumbića
|
|
||||||
if (statusFilter) {
|
|
||||||
filtriraniNalozi = filtriraniNalozi.filter(pn => pn.status?.toLowerCase() === statusFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Dinamičko ažuriranje brojača i naslova u DOM-u (NaslovList)
|
|
||||||
const sekcija = document.getElementById('putni-nalozi-sekcija');
|
|
||||||
if (sekcija) {
|
|
||||||
const glavniNaslov = sekcija.querySelector('h2');
|
|
||||||
if (glavniNaslov) {
|
|
||||||
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ukupnoBadge = sekcija.querySelector('h2 + span, .ukupno-badge');
|
|
||||||
if (ukupnoBadge) {
|
|
||||||
ukupnoBadge.textContent = filtriraniNalozi.length.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const aktivniBadge = sekcija.querySelector('[data-filter="aktivan"] span, [filter-type="aktivan"]');
|
|
||||||
if (aktivniBadge) aktivniBadge.textContent = aktivniCount.toString();
|
|
||||||
|
|
||||||
const zavrseniBadge = sekcija.querySelector('[data-filter="zavrsen"] span, [filter-type="zavrsen"]');
|
|
||||||
if (zavrseniBadge) zavrseniBadge.textContent = zavrseniCount.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sakrij loader
|
|
||||||
if (loader) loader.classList.add('hidden');
|
|
||||||
|
|
||||||
// 5. Generiranje HTML-a (Preslikana točna struktura tvog GenericKarticaItem)
|
|
||||||
if (filtriraniNalozi.length === 0) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">
|
|
||||||
Nema evidentiranih putnih naloga
|
|
||||||
</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = filtriraniNalozi.map((pn) => {
|
|
||||||
const statusMalo = pn.status?.toLowerCase();
|
|
||||||
const statusKlasa = getStatusColorClass(statusMalo);
|
|
||||||
const relacija = pn.relacija || pn.mjesto_odredista || 'Terenska lokacija';
|
|
||||||
const vozilo = pn.vozilo_naziv || 'Dodijeljeno vozilo';
|
|
||||||
const izvrsitelj = pn.korisnik_ime || 'Vozač tehničar';
|
|
||||||
const pocetniKm = pn.pocetna_km?.toLocaleString('hr-HR') || 0;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<a href="/operativa/putni-nalozi/${pn.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
|
|
||||||
<i class="fa-solid fa-route"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${pn.broj_naloga}</h4>
|
|
||||||
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${relacija}</p>
|
|
||||||
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
|
|
||||||
Vozilo: ${vozilo} | Izvršitelj: ${izvrsitelj} | Početno: ${pocetniKm} KM
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
|
|
||||||
${pn.status_prikaz || formatStatus(statusMalo)}
|
|
||||||
</span>
|
|
||||||
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
|
|
||||||
<i class="fa-solid fa-chevron-right text-xs"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Pokretanje klijentskih filtera nakon što se DOM uspješno izgradi
|
|
||||||
initFilters();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Kritična greška u klijentskom renderu putnih naloga:", err);
|
|
||||||
if (container) container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji putnih naloga.</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Izvršavanje pri učitavanju i re-inicijalizacija kod Astro View Transitions prebacivanja rute
|
|
||||||
renderirajPutneNalogeKlijentski();
|
|
||||||
document.addEventListener('astro:after-swap', renderirajPutneNalogeKlijentski);
|
|
||||||
</script>
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/RadniNalogLista.astro
|
|
||||||
import NaslovList from "./NaslovList.astro";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
limit?: number;
|
|
||||||
naslov?: string;
|
|
||||||
prikaziNaslov?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
limit = 0,
|
|
||||||
naslov = "Zadnje aktivnosti",
|
|
||||||
prikaziNaslov = true,
|
|
||||||
} = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="space-y-6 group" id="radni-nalozi-sekcija" data-state="loading">
|
|
||||||
{prikaziNaslov && (
|
|
||||||
<NaslovList
|
|
||||||
naslov={naslov}
|
|
||||||
sinkronizirano={false} label1="Planirano" count1={0} filter1="planirano"
|
|
||||||
label2="U radu" count2={0} filter2="u_radu"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="nalozi-loader"
|
|
||||||
class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse group-data-[state=loaded]:hidden group-data-[state=error]:hidden"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija radnih naloga...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="nalozi-list" class="space-y-0">
|
|
||||||
<div id="nalozi-placeholder" class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest group-data-[state=loaded]:hidden">
|
|
||||||
Nema zapisa za odabrani status
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span id="rn-bridge" data-limit={limit} data-naslov={naslov}></span>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { fetchDashboardData } from "../lib/api";
|
|
||||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
|
||||||
import { initFilters } from "../scripts/filters.js";
|
|
||||||
|
|
||||||
async function renderirajRadneNalogeKlijentski() {
|
|
||||||
const container = document.getElementById('nalozi-list');
|
|
||||||
const sekcija = document.getElementById('radni-nalozi-sekcija');
|
|
||||||
const bridge = document.getElementById('rn-bridge');
|
|
||||||
|
|
||||||
if (!container || !sekcija || !bridge) return;
|
|
||||||
|
|
||||||
// 1. KLIJENTSKI OSIGURAČ: Provjera autorizacije prije API poziva preko ključa iz tvojeg api.js
|
|
||||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
// Ako nema tokena, odmah zaključaj listu i baci sigurnosni Error izgled
|
|
||||||
sekcija.setAttribute('data-state', 'error');
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="p-20 text-center flex flex-col items-center justify-center gap-3">
|
|
||||||
<div class="w-12 h-12 bg-red-600/10 text-red-600 rounded-xl flex items-center justify-center text-lg mb-2">
|
|
||||||
<i class="fa-solid fa-shield-halved"></i>
|
|
||||||
</div>
|
|
||||||
<h4 class="text-xs font-black uppercase tracking-widest text-red-600">Pristup odbijen</h4>
|
|
||||||
<p class="text-[10px] text-gray-400 font-bold uppercase tracking-wider italic">Niste autorizirani za pregled operativnih naloga flote.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = parseInt(bridge.getAttribute('data-limit') || '0');
|
|
||||||
const originalNaslov = bridge.getAttribute('data-naslov') || 'Zadnje aktivnosti';
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const statusFilter = urlParams.get('status')?.toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Svaki put kad klijent pokrene funkciju, osiguravamo loading stanje
|
|
||||||
sekcija.setAttribute('data-state', 'loading');
|
|
||||||
|
|
||||||
// 2. Dohvat podataka (Šalje fetch zahtjev s ispravnim Authorization zaglavljem)
|
|
||||||
const data = await fetchDashboardData();
|
|
||||||
const nalozi = data?.nalozi || [];
|
|
||||||
|
|
||||||
// 3. Proračun statistike na klijentskoj strani
|
|
||||||
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
|
||||||
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
|
||||||
const zavrsenoCount = nalozi.filter(n => ['zavrsen', 'završeno', 'gotovo', 'zavrseno'].includes(n.status?.toLowerCase())).length;
|
|
||||||
|
|
||||||
// 4. 🛠️ POPRAVLJENO: Upisivanje vrijednosti i inteligentno sakrivanje samo ako je brojač točno 0
|
|
||||||
const card1 = sekcija.querySelector('#card-filter1');
|
|
||||||
if (card1) {
|
|
||||||
const val = card1.querySelector('.count-value');
|
|
||||||
if (val) val.textContent = planiranoCount.toString();
|
|
||||||
|
|
||||||
if (planiranoCount > 0) {
|
|
||||||
card1.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
card1.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const card2 = sekcija.querySelector('#card-filter2');
|
|
||||||
if (card2) {
|
|
||||||
const val = card2.querySelector('.count-value');
|
|
||||||
if (val) val.textContent = uRaduCount.toString();
|
|
||||||
|
|
||||||
if (uRaduCount > 0) {
|
|
||||||
card2.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
card2.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const card3 = sekcija.querySelector('#card-filter3');
|
|
||||||
if (card3) {
|
|
||||||
const val = card3.querySelector('.count-value');
|
|
||||||
if (val) val.textContent = zavrsenoCount.toString();
|
|
||||||
|
|
||||||
if (zavrsenoCount > 0) {
|
|
||||||
card3.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
card3.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Filtriranje i limitiranje niza prema klijentskom stanju URL-a
|
|
||||||
let filtriraniNalozi = statusFilter ? nalozi.filter(n => n.status?.toLowerCase() === statusFilter) : nalozi;
|
|
||||||
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
|
|
||||||
|
|
||||||
// Ažuriranje teksta glavnog naslova ovisno o aktivnom filteru
|
|
||||||
const glavniNaslov = sekcija.querySelector('h2');
|
|
||||||
if (glavniNaslov) {
|
|
||||||
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 🚀 SINKRONIZACIJA PROPSA: Otključavamo kontejner unutar NaslovList.astro prebacivanjem u 'loaded'
|
|
||||||
const statCardsContainer = sekcija.querySelector('#stat-cards-container');
|
|
||||||
if (statCardsContainer) {
|
|
||||||
statCardsContainer.setAttribute('data-state', 'loaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Isključivanje općeg loading stanja s krovne sekcije
|
|
||||||
sekcija.setAttribute('data-state', 'loaded');
|
|
||||||
|
|
||||||
// 7. Generiranje HTML kartica radnih naloga unutar tijela
|
|
||||||
if (prikazaniNalozi.length === 0) {
|
|
||||||
container.innerHTML = `<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">Nema zapisa za odabrani status</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = prikazaniNalozi.map((n) => {
|
|
||||||
const statusMalo = n.status?.toLowerCase();
|
|
||||||
const statusKlasa = getStatusColorClass(statusMalo);
|
|
||||||
const ikona = statusMalo === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<a href="/operativa/radni-nalozi/${n.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
|
|
||||||
<i class="fa-solid ${ikona}"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${n.broj_naloga}</h4>
|
|
||||||
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${formatStatus(statusMalo)}</p>
|
|
||||||
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
|
|
||||||
${n.klijent_naziv || 'Nepoznat klijent'} | ${n.vozilo_naziv || 'Bez vozila'} | ${n.stroj_naziv || 'Bez stroja'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
|
|
||||||
${formatStatus(statusMalo)}
|
|
||||||
</span>
|
|
||||||
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
|
|
||||||
<i class="fa-solid fa-chevron-right text-xs"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Aktiviraj klijentske klikove na kartice filtera
|
|
||||||
initFilters();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Kritična greška u klijentskom renderu radnih naloga:", err);
|
|
||||||
sekcija.setAttribute('data-state', 'error');
|
|
||||||
container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji operativnih podataka.</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prvo pokretanje pri učitavanju stranice
|
|
||||||
renderirajRadneNalogeKlijentski();
|
|
||||||
|
|
||||||
// Ponovno pokretanje prilikom SPA navigacije kroz aplikaciju
|
|
||||||
document.addEventListener('astro:after-swap', renderirajRadneNalogeKlijentski);
|
|
||||||
</script>
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/ServiserTerminal.astro
|
|
||||||
import Button from "./Button.astro";
|
|
||||||
import Toast from "./Toast.astro";
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="space-y-6 group" id="serviser-terminal-sekcija" data-state="loading">
|
|
||||||
|
|
||||||
<div class="group-data-[state=loading]:flex hidden items-center gap-3 text-xs font-black text-gray-400 uppercase tracking-widest animate-pulse bg-white dark:bg-gray-800 p-8 rounded-[2rem] border border-gray-100 dark:border-gray-700/50 shadow-md">
|
|
||||||
<i class="fa-solid fa-circle-notch animate-spin text-blue-600 text-base"></i> Sinkronizacija radnog zadatka s baze...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="terminal-prazno-stanje" class="hidden p-20 text-center bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-[3rem] uppercase font-black opacity-30 italic text-xs tracking-widest shadow-2xl shadow-blue-500/5">
|
|
||||||
Trenutno nemate dodijeljenih aktivnih radnih naloga na terenu.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="terminal-radni-prostor" class="hidden group-data-[state=loaded]:grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
|
|
||||||
<div class="lg:col-span-2 space-y-6">
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 p-8 shadow-xl shadow-blue-500/5">
|
|
||||||
|
|
||||||
<div class="flex justify-between items-start gap-4 border-b border-gray-100 dark:border-gray-700/50 pb-4 mb-6">
|
|
||||||
<div>
|
|
||||||
<h3 id="rn-broj" class="text-xl font-black uppercase tracking-tight text-gray-900 dark:text-white">#RN-UČITAVANJE</h3>
|
|
||||||
<p id="rn-klijent" class="text-xs font-bold text-gray-400 mt-0.5 uppercase tracking-wider">Klijent: ...</p>
|
|
||||||
</div>
|
|
||||||
<span id="rn-status-znacka" class="text-[10px] font-black px-4 py-2 rounded-full border uppercase tracking-wider bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/20 dark:text-amber-400 dark:border-amber-800/50">
|
|
||||||
U RADU
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 mb-1 italic">Prijavljeni kvar / Pogonski stroj flote</h4>
|
|
||||||
<div id="rn-stroj-detalji" class="p-4 bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 rounded-2xl text-xs font-bold text-gray-700 dark:text-gray-300 shadow-inner">
|
|
||||||
Učitavanje podataka o stroju...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4">
|
|
||||||
<Button id="btn-zatvori-rn" variant="none" class="w-full py-4 bg-emerald-600 hover:bg-emerald-700 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg shadow-emerald-500/10 active:scale-95 transition-all">
|
|
||||||
<i class="fa-solid fa-check-double mr-2"></i> Dovrši servis i aktiviraj stroj
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div id="putni-nalog-kartica" class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 p-8 shadow-xl shadow-blue-500/5 flex flex-col justify-between min-h-[300px]">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-3 mb-4">
|
|
||||||
<div class="w-10 h-10 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-xl flex items-center justify-center text-md shadow-sm">
|
|
||||||
<i class="fa-solid fa-truck-ramp-box"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 id="pn-broj" class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">PN-UČITAVANJE</h4>
|
|
||||||
<p id="pn-vozilo" class="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Vozilo: ...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-4 border-gray-100 dark:border-gray-700/50" />
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label for="zavrsna-km" class="block text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 ml-2 italic">Završna KM Vozila</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="zavrsna-km"
|
|
||||||
class="w-full px-4 py-3.5 bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-blue-600 rounded-xl outline-none text-xs font-bold text-gray-900 dark:text-white shadow-inner"
|
|
||||||
placeholder="Unesi stanje brojača (km)..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-6">
|
|
||||||
<Button id="btn-zatvori-pn" variant="danger" class="w-full py-3.5 rounded-2xl text-[10px] tracking-wider italic">
|
|
||||||
<i class="fa-solid fa-flag-checkered mr-2"></i> Zaključaj putni list
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toast />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { fetchRadniNalozi, patchNalog } from "../lib/api";
|
|
||||||
|
|
||||||
const API_BASE = import.meta.env.PUBLIC_API_URL;
|
|
||||||
let aktivniNalog: any = null;
|
|
||||||
|
|
||||||
async function inicijalizirajTerminalServisera() {
|
|
||||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
|
||||||
const sekcija = document.getElementById('serviser-terminal-sekcija');
|
|
||||||
const radniProstor = document.getElementById('terminal-radni-prostor');
|
|
||||||
const praznoStanje = document.getElementById('terminal-prazno-stanje');
|
|
||||||
|
|
||||||
if (!token || !sekcija || !radniProstor || !praznoStanje) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
sekcija.setAttribute('data-state', 'loading');
|
|
||||||
|
|
||||||
// 🛠️ OPTIMIZIRANO: Više ne dohvaćamo korisnika klijentski jer WelcomeHeader to rješava na serveru.
|
|
||||||
// Izravno dohvaćamo radne naloge kroz postojeće REST rute
|
|
||||||
const naloziData = await fetchRadniNalozi();
|
|
||||||
|
|
||||||
// Filtriramo naloge koji su u radu
|
|
||||||
const mojiNalozi = naloziData.filter((n: any) => n.status?.toLowerCase() === 'u_radu');
|
|
||||||
|
|
||||||
if (mojiNalozi.length === 0) {
|
|
||||||
sekcija.setAttribute('data-state', 'loaded');
|
|
||||||
radniProstor.classList.add('hidden');
|
|
||||||
praznoStanje.classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
aktivniNalog = mojiNalozi[0];
|
|
||||||
praznoStanje.classList.add('hidden');
|
|
||||||
radniProstor.classList.remove('hidden');
|
|
||||||
|
|
||||||
// Injekcija podataka u DOM
|
|
||||||
document.getElementById('rn-broj')!.textContent = `#${aktivniNalog.broj_naloga}`;
|
|
||||||
document.getElementById('rn-klijent')!.textContent = `Klijent: ${aktivniNalog.klijent_naziv || 'Opći klijent'}`;
|
|
||||||
document.getElementById('rn-stroj-detalji')!.innerHTML = `
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p><span class="opacity-50 font-black">STROJ U KVARU:</span> ${aktivniNalog.stroj_naziv || 'Nije specificirano'}</p>
|
|
||||||
<p><span class="opacity-50 font-black">DODIJELJENO VOZILO:</span> ${aktivniNalog.vozilo_naziv || 'Bez servisnog vozila'}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Provjera postojanja povezanog putnog naloga
|
|
||||||
const putniKartica = document.getElementById('putni-nalog-kartica');
|
|
||||||
if (putniKartica) {
|
|
||||||
if (aktivniNalog.putni_nalog) {
|
|
||||||
putniKartica.classList.remove('hidden');
|
|
||||||
document.getElementById('pn-broj')!.textContent = `PN-${aktivniNalog.putni_nalog}`;
|
|
||||||
document.getElementById('pn-vozilo')!.textContent = `Vozilo: ${aktivniNalog.vozilo_naziv || 'Servisni kombi'}`;
|
|
||||||
} else {
|
|
||||||
putniKartica.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prebacivanje u loaded stanje -> Tailwind CSS automatski pali radni prostor
|
|
||||||
sekcija.setAttribute('data-state', 'loaded');
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Krah servisnog terminala:", err);
|
|
||||||
sekcija.setAttribute('data-state', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- AKCIJA A: ZATVARANJE PUTNOG NALOGA (PATCH na osnovnu stazu) ---
|
|
||||||
document.getElementById('btn-zatvori-pn')?.addEventListener('click', async (e) => {
|
|
||||||
const btn = e.currentTarget as HTMLButtonElement;
|
|
||||||
const kmInput = document.getElementById('zavrsna-km') as HTMLInputElement;
|
|
||||||
const km = kmInput?.value;
|
|
||||||
|
|
||||||
if (!km || parseInt(km, 10) <= 0) {
|
|
||||||
window.showToast?.("Unesite ispravnu završnu kilometražu vozila.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
const originalText = btn.innerHTML;
|
|
||||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Knjiženje kilometara...';
|
|
||||||
|
|
||||||
const url = `${API_BASE}${API_BASE.endsWith('/') ? '' : '/'}operativa/putni-nalozi/${aktivniNalog.putni_nalog}/`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
zavrsna_km: parseInt(km, 10),
|
|
||||||
status: 'završen'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
window.showToast?.("Putni nalog uspješno zaključen i arhiviran.", "success");
|
|
||||||
document.getElementById('putni-nalog-kartica')?.classList.add('opacity-40');
|
|
||||||
btn.classList.add('hidden');
|
|
||||||
kmInput.disabled = true;
|
|
||||||
} else {
|
|
||||||
window.showToast?.("Greška pri zatvaranju putnog lista.", "error");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
window.showToast?.("Mrežna greška u komunikaciji s DRF-om.", "error");
|
|
||||||
} {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- AKCIJA B: ZATVARANJE RADNOG NALOGA + PROMJENA STATUSI STROJA ---
|
|
||||||
document.getElementById('btn-zatvori-rn')?.addEventListener('click', async (e) => {
|
|
||||||
const btn = e.currentTarget as HTMLButtonElement;
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
const originalText = btn.innerHTML;
|
|
||||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Zatvaranje naloga i ažuriranje flote...';
|
|
||||||
|
|
||||||
// 1. Koristimo postojeću metodu patchNalog za prebacivanje radnog naloga u 'zavrsen'
|
|
||||||
const resNalog = await patchNalog(aktivniNalog.id, { status: 'zavrsen' });
|
|
||||||
|
|
||||||
if (resNalog) {
|
|
||||||
// 2. REST LANČANA REAKCIJA: Šaljemo PATCH izravno na stazu stroja i prebacujemo ga u 'aktivno'
|
|
||||||
if (aktivniNalog.stroj) {
|
|
||||||
const urlStroj = `${API_BASE}${API_BASE.endsWith('/') ? '' : '/'}fleet/strojevi/${aktivniNalog.stroj}/`;
|
|
||||||
await fetch(urlStroj, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ status: 'aktivno' }) // Stroj je popravljen i spreman za pogon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.showToast?.("Radni nalog dovršen. Stroj je uspješno pušten u rad!", "success");
|
|
||||||
|
|
||||||
// Re-render cijelog terminala (prebaciće u prazno stanje jer više nema naloga 'u_radu')
|
|
||||||
setTimeout(() => inicijalizirajTerminalServisera(), 1200);
|
|
||||||
} else {
|
|
||||||
window.showToast?.("Sinkronizacija s DRF poslužiteljem nije uspjela.", "error");
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = originalText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-inicijalizacija i stabilnost kroz View Transitions SPA navigaciju
|
|
||||||
inicijalizirajTerminalServisera();
|
|
||||||
document.addEventListener('astro:after-swap', inicijalizirajTerminalServisera);
|
|
||||||
</script>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/StatsGrid.astro
|
|
||||||
interface Props {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
icon: string;
|
|
||||||
variant?: 'blue' | 'yellow' | 'emerald' | 'gray';
|
|
||||||
animate?: boolean;
|
|
||||||
filterValue?: string; // NOVO: npr. 'u_radu', 'planirano'...
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
icon,
|
|
||||||
variant = 'blue',
|
|
||||||
animate = false,
|
|
||||||
filterValue = "",
|
|
||||||
class: className = ""
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
const themes = {
|
|
||||||
blue: { container: "border-gray-100 dark:border-gray-700", iconBg: "bg-blue-50 dark:bg-blue-900/30", iconColor: "text-blue-600 dark:text-blue-400", labelColor: "text-gray-400" },
|
|
||||||
yellow: { container: "border-yellow-500/20 border-2", iconBg: "bg-yellow-500", iconColor: "text-white", labelColor: "text-yellow-600" },
|
|
||||||
emerald: { container: "border-gray-100 dark:border-gray-700", iconBg: "bg-emerald-50 dark:bg-emerald-900/30", iconColor: "text-emerald-600 dark:text-emerald-400", labelColor: "text-gray-400" },
|
|
||||||
gray: { container: "border-gray-100 dark:border-gray-700", iconBg: "bg-gray-100 dark:bg-gray-800", iconColor: "text-gray-500", labelColor: "text-gray-400" }
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = themes[variant];
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={`stat-card bg-white dark:bg-gray-800 p-3 sm:p-6 rounded-2xl sm:rounded-[2.5rem] shadow-sm border flex flex-row items-center justify-center gap-2 sm:gap-5 cursor-pointer hover:scale-[1.02] active:scale-95 transition-all ${theme.container} ${className}`}
|
|
||||||
>
|
|
||||||
<div class={`p-1.5 sm:p-4 rounded-lg sm:rounded-2xl w-7 h-7 sm:w-16 sm:h-16 flex items-center justify-center flex-none ${theme.iconBg} ${theme.iconColor} ${animate ? 'animate-pulse' : ''}`}>
|
|
||||||
<i class={`fa-solid ${icon} text-xs sm:text-2xl`}></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col justify-center">
|
|
||||||
<h3 class="text-sm sm:text-4xl font-black text-gray-900 dark:text-white leading-none tracking-tighter">
|
|
||||||
{value}
|
|
||||||
</h3>
|
|
||||||
<p class={`text-[6px] sm:text-xs font-black uppercase tracking-widest mt-0.5 sm:mt-1 ${theme.labelColor}`}>
|
|
||||||
{label}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/Toast.astro
|
|
||||||
---
|
|
||||||
<div id="global-toast" class="fixed bottom-0 left-0 w-full z-[999] hidden transition-all duration-500 transform translate-y-full">
|
|
||||||
<div id="toast-bg" class="px-6 py-6 flex items-center shadow-[0_-10px_40px_rgba(0,0,0,0.2)] border-t">
|
|
||||||
<div class="max-w-7xl mx-auto w-full flex items-center gap-6">
|
|
||||||
<!-- Icon Container -->
|
|
||||||
<div id="toast-icon-wrapper" class="w-14 h-14 bg-white/20 rounded-[1.2rem] flex items-center justify-center shadow-inner">
|
|
||||||
<i id="toast-icon" class="fa-solid text-2xl"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Text Content -->
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span id="toast-label" class="text-[10px] font-black uppercase tracking-[0.3em] opacity-70 leading-none italic"></span>
|
|
||||||
<p id="toast-message" class="text-lg font-black uppercase tracking-tight mt-1 italic leading-none text-white"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Close Button -->
|
|
||||||
<button id="close-toast" class="ml-auto w-10 h-10 hover:bg-white/10 rounded-xl transition-colors text-white">
|
|
||||||
<i class="fa-solid fa-xmark"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Logika koja se može pozvati s bilo koje stranice
|
|
||||||
window.showToast = (message, type = 'success') => {
|
|
||||||
const toast = document.getElementById('global-toast');
|
|
||||||
const bg = document.getElementById('toast-bg');
|
|
||||||
const icon = document.getElementById('toast-icon');
|
|
||||||
const label = document.getElementById('toast-label');
|
|
||||||
const msg = document.getElementById('toast-message');
|
|
||||||
|
|
||||||
if (!toast || !bg || !icon || !label || !msg) return;
|
|
||||||
|
|
||||||
// Konfiguracija tema
|
|
||||||
const themes = {
|
|
||||||
success: { bg: 'bg-emerald-600', border: 'border-emerald-500', icon: 'fa-check-double', label: 'Sustav Potvrđuje' },
|
|
||||||
error: { bg: 'bg-red-600', border: 'border-red-500', icon: 'fa-circle-exclamation', label: 'Sustav Odbija' },
|
|
||||||
warning: { bg: 'bg-orange-500', border: 'border-orange-400', icon: 'fa-triangle-exclamation', label: 'Sustav Upozorava' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = themes[type] || themes.success;
|
|
||||||
|
|
||||||
// Primjena stilova
|
|
||||||
bg.className = `${theme.bg} ${theme.border} text-white px-6 py-6 flex items-center shadow-2xl border-t`;
|
|
||||||
icon.className = `fa-solid ${theme.icon} text-2xl text-white`;
|
|
||||||
label.innerText = theme.label;
|
|
||||||
msg.innerText = message;
|
|
||||||
|
|
||||||
// Animacija ulaska
|
|
||||||
toast.classList.remove('hidden', 'translate-y-full');
|
|
||||||
|
|
||||||
// Auto-hide nakon 5s
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.add('translate-y-full');
|
|
||||||
}, 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zatvaranje na gumb
|
|
||||||
document.getElementById('close-toast')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('global-toast')?.classList.add('translate-y-full');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Provjera SessionStorage-a (za poruke nakon redirecta)
|
|
||||||
const pending = sessionStorage.getItem('pending_toast');
|
|
||||||
if (pending) {
|
|
||||||
const { type, message } = JSON.parse(pending);
|
|
||||||
window.showToast(message, type);
|
|
||||||
sessionStorage.removeItem('pending_toast');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/WelcomeHeader.astro
|
|
||||||
import site from "../data/site.json";
|
|
||||||
|
|
||||||
const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
|
|
||||||
const currentPageData = site.navigation.find(item => item.url === currentPath);
|
|
||||||
|
|
||||||
console.log("Current Path:", currentPath);
|
|
||||||
|
|
||||||
const {
|
|
||||||
welcomeHeaderTextH1 = "Dobrodošli",
|
|
||||||
welcomeHeaderTextH1dodatno = "",
|
|
||||||
welcomeHeaderPodnaslov = "Pregled sustava",
|
|
||||||
welcomeHeaderPovratniURL = "",
|
|
||||||
welcomeHeaderDisplay = true
|
|
||||||
} = currentPageData || {};
|
|
||||||
---
|
|
||||||
|
|
||||||
{welcomeHeaderDisplay && (
|
|
||||||
<header class="mb-12 px-2">
|
|
||||||
{welcomeHeaderPovratniURL && (
|
|
||||||
<a href={welcomeHeaderPovratniURL} class="text-blue-600 text-[10px] font-black uppercase tracking-widest no-underline flex items-center gap-2 mb-6 hover:opacity-70 transition-opacity italic">
|
|
||||||
<i class="fa-solid fa-arrow-left text-[8px]"></i> Povratak
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="text-left">
|
|
||||||
<h1 class="text-4xl sm:text-6xl font-black text-gray-900 dark:text-white uppercase tracking-tighter leading-none italic">
|
|
||||||
{welcomeHeaderTextH1} <span class="text-blue-600"> {welcomeHeaderTextH1dodatno}</span>
|
|
||||||
</h1>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400 mt-4 font-medium italic text-lg leading-tight">
|
|
||||||
{welcomeHeaderPodnaslov}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
---
|
|
||||||
// src/layouts/Layout.astro
|
|
||||||
import Nav from "../components/Navbar.astro";
|
|
||||||
import Toast from '../components/Toast.astro';
|
|
||||||
import site from "../data/site.json";
|
|
||||||
import "../styles/global.css";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
title = site.title,
|
|
||||||
description = site.description
|
|
||||||
} = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="hr" class="scroll-smooth">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
<meta name="description" content={description} />
|
|
||||||
|
|
||||||
<title>{title} | {site.title}</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white antialiased min-h-screen flex flex-col">
|
|
||||||
|
|
||||||
<Nav />
|
|
||||||
|
|
||||||
<main class="flex-grow w-full max-w-7xl mx-auto pt-24 pb-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-6 text-center text-sm text-gray-500">
|
|
||||||
© {new Date().getFullYear()} {site.title}. Sva prava pridržana.
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<Toast />
|
|
||||||
|
|
||||||
<script is:inline src="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style is:global>
|
|
||||||
/* Integracija Tailwinda unutar Layouta */
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
</style>
|
|
||||||
@@ -1,539 +0,0 @@
|
|||||||
// src/lib/api.js
|
|
||||||
|
|
||||||
// 1. Osiguravamo da API_BASE uvijek završava s točno jednom kosom crtom
|
|
||||||
const RAW_BASE = import.meta.env.PUBLIC_API_URL;
|
|
||||||
const API_BASE = RAW_BASE.endsWith('/') ? RAW_BASE : `${RAW_BASE}/`;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POMOĆNA FUNKCIJA: Dohvaća token i postavlja Headere.
|
|
||||||
* Podržava i klijentski localStorage i serverski (SSR) proslijeđeni token.
|
|
||||||
* @param {Object|FormData} bodyData - Podaci koji se šalju u body-ju zahtjeva
|
|
||||||
* @param {string|null} ssrToken - Izborni token proslijeđen sa serverske strane (Astro SSR)
|
|
||||||
*/
|
|
||||||
function getAuthHeaders(bodyData = {}, ssrToken = null) {
|
|
||||||
const headers = {};
|
|
||||||
|
|
||||||
// 🚀 PAMETAN DOHVAT TOKENA: Ako je proslijeđen ssrToken (server), koristi njega.
|
|
||||||
// Ako nije, a nalazimo se u pregledniku (klijent), povuci ga iz localStorage.
|
|
||||||
let token = ssrToken;
|
|
||||||
if (!token && typeof window !== 'undefined') {
|
|
||||||
token = localStorage.getItem('access_token');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
headers['Authorization'] = `Bearer ${token}`; // Usklađeno sa Simple JWT standardom
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provjera je li bodyData FormData (za slike/naloge s terena)
|
|
||||||
const isFormData = bodyData instanceof FormData;
|
|
||||||
|
|
||||||
// Ako NIJE FormData, šaljemo standardni JSON content type
|
|
||||||
if (!isFormData) {
|
|
||||||
headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Centralizirana obrada odgovora s Toast podrškom
|
|
||||||
*/
|
|
||||||
async function handleResponse(res) {
|
|
||||||
if (res.status === 401) {
|
|
||||||
console.warn("Token istekao ili je nevažeći.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const errorData = await res.json().catch(() => ({}));
|
|
||||||
console.error("API Error Response:", errorData);
|
|
||||||
|
|
||||||
let msg = "Greška pri sinkronizaciji podataka.";
|
|
||||||
|
|
||||||
if (errorData.detail) {
|
|
||||||
msg = errorData.detail;
|
|
||||||
}
|
|
||||||
else if (typeof errorData === 'object' && errorData !== null) {
|
|
||||||
const kljuceviGresaka = Object.keys(errorData);
|
|
||||||
|
|
||||||
if (kljuceviGresaka.length > 0) {
|
|
||||||
const prvoPolje = kljuceviGresaka[0];
|
|
||||||
const greskaVrijednost = errorData[prvoPolje];
|
|
||||||
|
|
||||||
if (Array.isArray(greskaVrijednost) && greskaVrijednost.length > 0) {
|
|
||||||
msg = `${prvoPolje}: ${greskaVrijednost[0]}`;
|
|
||||||
} else if (typeof greskaVrijednost === 'string') {
|
|
||||||
msg = `${prvoPolje}: ${greskaVrijednost}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.showToast?.(msg, "error");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// VRAĆA PARSIRAN OBJEKT - Stream je zatvoren nakon ove linije!
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- RUTE ---
|
|
||||||
export const routes = {
|
|
||||||
// Autentifikacija
|
|
||||||
login: () => 'token/',
|
|
||||||
trenutniKorisnik: () => 'users/me/',
|
|
||||||
|
|
||||||
// Korisnici i operativni tehničari
|
|
||||||
serviseriSvi: () => 'users/',
|
|
||||||
serviserTerminal: (id) => `users/${id}/terminal/`,
|
|
||||||
|
|
||||||
// Radni nalozi
|
|
||||||
radniNalozi: (params = {}) => {
|
|
||||||
const baseUrl = 'operativa/radni-nalozi/';
|
|
||||||
const cleanParams = Object.fromEntries(
|
|
||||||
Object.entries(params).filter(([_, v]) => v != null)
|
|
||||||
);
|
|
||||||
const queryString = new URLSearchParams(cleanParams).toString();
|
|
||||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
||||||
},
|
|
||||||
radniNalogDetalji: (id) => `operativa/radni-nalozi/${id}/`,
|
|
||||||
sljedeciBrojNaloga: () => 'operativa/radni-nalozi/sljedeci-broj/',
|
|
||||||
|
|
||||||
// Putni nalozi i logistika
|
|
||||||
putniNalozi: () => 'operativa/putni-nalozi/',
|
|
||||||
|
|
||||||
// Vozila i strojevi (Fleet modul)
|
|
||||||
vozila: () => 'fleet/vozila/',
|
|
||||||
strojevi: (vlasnikId = null) => {
|
|
||||||
const baseUrl = 'fleet/strojevi/';
|
|
||||||
if (vlasnikId) return `${baseUrl}?vlasnik=${vlasnikId}`;
|
|
||||||
return baseUrl;
|
|
||||||
},
|
|
||||||
strojDetalji: (id) => `fleet/strojevi/${id}/`,
|
|
||||||
|
|
||||||
// Kupci / Klijenti
|
|
||||||
kupciSvi: () => 'kupci/svi/',
|
|
||||||
kupacDetalji: (id) => `kupci/svi/${id}/`,
|
|
||||||
|
|
||||||
// Kalendar i raspored
|
|
||||||
mojRaspored: () => 'kalendar/moj-raspored/',
|
|
||||||
kalendarDogadaji: () => 'kalendar/dogadaji/'
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- API METODE ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat profila korisnika iz opće liste /api/users/
|
|
||||||
* @param {string|null} ssrToken - Opcionalni token sa servera (Astro.cookies ili slično)
|
|
||||||
*/
|
|
||||||
export async function fetchCurrentUser(ssrToken = null) {
|
|
||||||
if (typeof window === 'undefined' && !ssrToken) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = `${API_BASE}${routes.trenutniKorisnik()}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders({}, ssrToken)
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await handleResponse(res);
|
|
||||||
if (user && user.detail) return null;
|
|
||||||
|
|
||||||
return user;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("fetchCurrentUser Failure:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 🚀 NOVO (DRY): Dohvaća sve korisnike i vraća isključivo filtrirane servisere s terena
|
|
||||||
*/
|
|
||||||
export async function fetchServiseri() {
|
|
||||||
try {
|
|
||||||
const url = `${API_BASE}${routes.serviseriSvi()}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
const korisnici = await handleResponse(res);
|
|
||||||
if (!korisnici) return [];
|
|
||||||
|
|
||||||
const lista = Array.isArray(korisnici) ? korisnici : (korisnici.results || []);
|
|
||||||
|
|
||||||
// Filtriranje na temelju polja 'uloga' iz tvog CustomUser modela
|
|
||||||
return lista.filter(u => String(u.uloga).toUpperCase().trim() === 'SERVISER');
|
|
||||||
} catch (e) {
|
|
||||||
console.error("fetchServiseri Failure:", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 🚀 NOVO (DRY): Dohvaća ugniježđene terminal podatke za specifičnog servisera (Nalozi, Putni, Vozila)
|
|
||||||
* @param {number|string} id - ID servisera čiji se terminal učitava
|
|
||||||
*/
|
|
||||||
export async function fetchServiserTerminalData(id) {
|
|
||||||
try {
|
|
||||||
const url = `${API_BASE}${routes.serviserTerminal(id)}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
return await handleResponse(res);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`fetchServiserTerminalData za ID ${id} neuspješan:`, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat i filtriranje radnih naloga (za tablice i liste u operativi)
|
|
||||||
*/
|
|
||||||
export async function fetchRadniNalozi(params = {}) {
|
|
||||||
try {
|
|
||||||
// Logika čišćenja i slaganja query parametara je sada delegirana routes objektu!
|
|
||||||
const url = `${API_BASE}${routes.radniNalozi(params)}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
return await handleResponse(res);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("fetchRadniNalozi Failure:", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Paralelni dohvat podataka za glavno dispečersko sučelje (Dashboard flote i naloga)
|
|
||||||
*/
|
|
||||||
export async function fetchDashboardData() {
|
|
||||||
try {
|
|
||||||
// Dinamički povlačimo staze iz routes objekta za paralelno okidanje
|
|
||||||
const urlVozila = `${API_BASE}${routes.vozila()}`;
|
|
||||||
const urlNalozi = `${API_BASE}${routes.radniNalozi()}`;
|
|
||||||
|
|
||||||
const [resV, resN] = await Promise.all([
|
|
||||||
fetch(urlVozila, { method: 'GET', headers: getAuthHeaders() }),
|
|
||||||
fetch(urlNalozi, { method: 'GET', headers: getAuthHeaders() })
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Budući da Dashboard podatke često renderiraš u paralelnim karticama,
|
|
||||||
// koristimo brzi i sigurni fallback u slučaju prazne baze ili neočekivanog formata
|
|
||||||
const vozilaData = resV.ok ? await resV.json().catch(() => []) : [];
|
|
||||||
const naloziData = resN.ok ? await resN.json().catch(() => []) : [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
vozila: Array.isArray(vozilaData) ? vozilaData : (vozilaData.results || []),
|
|
||||||
nalozi: Array.isArray(naloziData) ? naloziData : (naloziData.results || [])
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("FetchDashboardData Failure:", e);
|
|
||||||
return { vozila: [], nalozi: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prijava korisnika i pohrana JWT tokena
|
|
||||||
*/
|
|
||||||
export async function login(email, password) {
|
|
||||||
try {
|
|
||||||
// Povlačenje centralizirane rute za token
|
|
||||||
const url = `${API_BASE}${routes.login()}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ email, password })
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
if (!res.ok) throw new Error(data.detail || "Neuspješna prijava");
|
|
||||||
|
|
||||||
localStorage.setItem('access_token', data.access);
|
|
||||||
localStorage.setItem('refresh_token', data.refresh);
|
|
||||||
return { success: true };
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Login Error:", e);
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Odjava korisnika i čišćenje lokalne pohrane
|
|
||||||
*/
|
|
||||||
export function logout() {
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('refresh_token');
|
|
||||||
if (typeof window !== 'undefined') window.location.href = '/login';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat detalja pojedinačnog kupca/klijenta
|
|
||||||
*/
|
|
||||||
export async function fetchKupacDetalji(id) {
|
|
||||||
try {
|
|
||||||
const url = `${API_BASE}${routes.kupacDetalji(id)}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
return await handleResponse(res);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Greška u fetchKupacDetalji za ID ${id}:`, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unos novog servisnog vozila u bazu flote
|
|
||||||
*/
|
|
||||||
export async function createVozilo(payload) {
|
|
||||||
try {
|
|
||||||
const url = `${API_BASE}${routes.vozila()}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await handleResponse(res);
|
|
||||||
if (data && typeof window !== 'undefined') {
|
|
||||||
window.showToast?.("Vozilo uspješno uneseno!", "success");
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Greška u createVozilo:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kreiranje novog radnog naloga (šalje se FormData zbog učitavanja slika s terena)
|
|
||||||
*/
|
|
||||||
export async function createNalog(formData) {
|
|
||||||
try {
|
|
||||||
// Koristimo bazičnu rutu bez parametara za POST zahtjev
|
|
||||||
const url = `${API_BASE}${routes.radniNalozi()}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(formData), // Automatski izbacuje Content-Type za FormData
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
return await handleResponse(res);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Greška u createNalog:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parcijalno ažuriranje radnog naloga (npr. promjena statusa, dodavanje opisa) preko centralizirane rute
|
|
||||||
*/
|
|
||||||
export async function patchNalog(id, data) {
|
|
||||||
try {
|
|
||||||
// Koristimo novu stazu za detalje naloga iz routes objekta
|
|
||||||
const url = `${API_BASE}${routes.radniNalogDetalji(id)}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
|
|
||||||
return await handleResponse(res);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Greška u patchNalog za ID ${id}:`, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat svih kalendarskih događaja (planirani servisi, atesti, tereni)
|
|
||||||
*/
|
|
||||||
export async function fetchCalendarEvents() {
|
|
||||||
try {
|
|
||||||
// Koristimo novu stazu iz routes objekta
|
|
||||||
const url = `${API_BASE}${routes.kalendarDogadaji()}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Centralizirana obrada odgovora s Toast error handlingom
|
|
||||||
const data = await handleResponse(res);
|
|
||||||
|
|
||||||
if (!data) return []; // Siguran fallback ako zahtjev baci grešku
|
|
||||||
|
|
||||||
return Array.isArray(data) ? data : (data.results || []);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("fetchCalendarEvents Failure:", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat osobnog rasporeda servisera iz kalendara
|
|
||||||
*/
|
|
||||||
export async function fetchMojRaspored() {
|
|
||||||
try {
|
|
||||||
// Koristimo stazu iz routes objekta
|
|
||||||
const url = `${API_BASE}${routes.mojRaspored()}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Centralizirana obrada odgovora s Toast error handlingom
|
|
||||||
const data = await handleResponse(res);
|
|
||||||
|
|
||||||
if (!data) return []; // Siguran fallback na prazan niz ako zahtjev ne prođe
|
|
||||||
|
|
||||||
return Array.isArray(data) ? data : (data.results || []);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("fetchMojRaspored Failure:", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat popisa svih kupaca/klijenata za dropdown u novom nalogu
|
|
||||||
*/
|
|
||||||
export async function fetchKupciData() {
|
|
||||||
try {
|
|
||||||
// Koristimo stazu iz routes objekta
|
|
||||||
const url = `${API_BASE}${routes.kupciSvi()}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Centralizirana obrada odgovora
|
|
||||||
const data = await handleResponse(res);
|
|
||||||
|
|
||||||
if (!data) return { kupci: [] }; // Fallback ako je token nevažeći
|
|
||||||
|
|
||||||
// Vraćamo objekt u formatu koji novi.astro destrukturira na serveru
|
|
||||||
return {
|
|
||||||
kupci: Array.isArray(data) ? data : (data.results || [])
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("fetchKupciData Failure:", e);
|
|
||||||
return { kupci: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat popisa strojeva (opcionalno filtrirano po vlasniku) pomoću centralizirane rute
|
|
||||||
*/
|
|
||||||
export async function fetchStrojeviData(vlasnikId = null) {
|
|
||||||
try {
|
|
||||||
// Generiramo ispravan URL preko routes objekta
|
|
||||||
const url = `${API_BASE}${routes.strojevi(vlasnikId)}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Koristimo centraliziranu obradu odgovora (s Toast podrškom)
|
|
||||||
const data = await handleResponse(res);
|
|
||||||
|
|
||||||
if (!data) return { strojevi: [] }; // Siguran fallback ako je zahtjev prekinut (401, 500...)
|
|
||||||
|
|
||||||
// Vraćamo objekt sa strojevima prateći strukturu koju novi.astro očekuje (.map destructuring)
|
|
||||||
return {
|
|
||||||
strojevi: Array.isArray(data) ? data : (data.results || [])
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("fetchStrojeviData Failure:", e);
|
|
||||||
return { strojevi: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvat popisa svih putnih naloga pomoću centralizirane rute i handleResponse-a
|
|
||||||
*/
|
|
||||||
export async function fetchPutniNaloziData() {
|
|
||||||
try {
|
|
||||||
// Koristimo zajedničku stazu iz routes objekta
|
|
||||||
const url = `${API_BASE}${routes.putniNalozi()}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Koristimo centraliziranu obradu odgovora (s Toast error handlingom)
|
|
||||||
const data = await handleResponse(res);
|
|
||||||
|
|
||||||
if (!data) return []; // Fallback ako je handleResponse presreo grešku i vratio null
|
|
||||||
|
|
||||||
// Provjera vraća li Django čistu listu ili paginirani rezultatski objekt (results)
|
|
||||||
return Array.isArray(data) ? data : (data.results || []);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Greška pri dohvatu putnih naloga:", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dohvaća sljedeći slobodni broj radnog naloga s backenda
|
|
||||||
* Osigurano protiv duplih kosih crta i preflight redirecta
|
|
||||||
*/
|
|
||||||
export async function fetchSljedeciBrojNaloga() {
|
|
||||||
try {
|
|
||||||
// Pametno spajanje baze i rute (isto kao u ostalim očišćenim metodama)
|
|
||||||
const base = API_BASE.endsWith('/') ? API_BASE : `${API_BASE}/`;
|
|
||||||
const ruta = routes.sljedeciBrojNaloga();
|
|
||||||
const url = `${base}${ruta}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders() // Koristi centralizirane headere umjesto sirovog objekta
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
return await res.json();
|
|
||||||
} else {
|
|
||||||
console.error("Greška na backendu pri dohvaćanju brojača:", res.status);
|
|
||||||
return { broj_naloga: "RN-2026-XXXX" };
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Mrežna greška u fetchSljedeciBrojNaloga:", err);
|
|
||||||
return { broj_naloga: "RN-2026-XXXX" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kreiranje novog putnog naloga - S koso crtom (trailing slash) za Django usklađenost
|
|
||||||
*/
|
|
||||||
export async function createPutniNalog(radniNalogId, voziloId) {
|
|
||||||
try {
|
|
||||||
// Koristimo novu stazu iz routes objekta
|
|
||||||
const url = `${API_BASE}${routes.putniNalozi()}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify({
|
|
||||||
radni_nalog_id: parseInt(radniNalogId, 10),
|
|
||||||
vozilo: parseInt(voziloId, 10)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Vraća gotov JSON objekt (ili null ako je toast okinuo grešku)
|
|
||||||
return await handleResponse(res);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("createPutniNalog krah:", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/fleet/strojevi/[id].astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import Button from "../../../components/Button.astro";
|
|
||||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
|
||||||
|
|
||||||
// Importiranje utilitija
|
|
||||||
const { id } = Astro.params;
|
|
||||||
const API_BASE = import.meta.env.PUBLIC_API_URL;
|
|
||||||
|
|
||||||
let stroj = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Dohvaćamo točno određeni stroj
|
|
||||||
const res = await fetch(`${API_BASE}/fleet/strojevi/${id}/?t=${Date.now()}`);
|
|
||||||
if (res.ok) stroj = await res.json();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Greška pri dohvatu detalja stroja:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stroj) return Astro.redirect("/404");
|
|
||||||
|
|
||||||
// Logika za Atest status (npr. ako je unutar 30 dana ili prošao)
|
|
||||||
const atestDate = stroj.datum_zadnjeg_atesta ? new Date(stroj.datum_zadnjeg_atesta) : null;
|
|
||||||
const isAtestExpired = atestDate && atestDate < new Date();
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={`Stroj | ${stroj.naziv}`}>
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<header class="px-2 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<a href="/fleet/strojevi" class="text-blue-600 text-[10px] font-black uppercase tracking-[0.3em] hover:underline flex items-center gap-2 no-underline mb-4">
|
|
||||||
<i class="fa-solid fa-arrow-left text-[8px]"></i> Povratak u park
|
|
||||||
</a>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<h1 class="text-4xl sm:text-6xl font-black text-gray-900 dark:text-white uppercase tracking-tighter leading-none italic">
|
|
||||||
{stroj.naziv}
|
|
||||||
</h1>
|
|
||||||
<div class="hidden sm:block px-4 py-1.5 bg-blue-600 text-white rounded-full text-[10px] font-black uppercase tracking-widest italic">
|
|
||||||
{stroj.tip_human_readable}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400 font-medium italic">
|
|
||||||
Vlasnik: <a href={`/kupci/${stroj.vlasnik}`} class="text-blue-600 underline font-bold">{stroj.vlasnik_naziv}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3 px-6 py-4 bg-white dark:bg-gray-800 rounded-[2rem] border border-gray-100 dark:border-gray-700 shadow-xl">
|
|
||||||
<div class="flex flex-col items-end">
|
|
||||||
<span class="text-[8px] font-black uppercase text-gray-400 tracking-widest">Serijski broj</span>
|
|
||||||
<span class="text-xl font-black text-gray-900 dark:text-white tracking-tighter italic">{stroj.serijski_broj}</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-px h-8 bg-gray-100 dark:bg-gray-700 mx-2"></div>
|
|
||||||
<i class="fa-solid fa-barcode text-gray-200 text-2xl"></i>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 px-2">
|
|
||||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-[2.5rem] border border-gray-100 dark:border-gray-700 shadow-sm relative overflow-hidden group">
|
|
||||||
<i class="fa-solid fa-calendar absolute -right-4 -bottom-4 text-6xl opacity-5 group-hover:rotate-12 transition-transform"></i>
|
|
||||||
<span class="text-[9px] font-black uppercase text-gray-400 block mb-2 tracking-widest">Godište</span>
|
|
||||||
<span class="text-4xl font-black dark:text-white italic tracking-tighter">{stroj.godina_proizvodnje || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-[2.5rem] border border-gray-100 dark:border-gray-700 shadow-sm relative overflow-hidden group">
|
|
||||||
<i class="fa-solid fa-gauge-high absolute -right-4 -bottom-4 text-6xl opacity-5 group-hover:rotate-12 transition-transform"></i>
|
|
||||||
<span class="text-[9px] font-black uppercase text-gray-400 block mb-2 tracking-widest">Radni sati</span>
|
|
||||||
<div class="flex items-baseline gap-2">
|
|
||||||
<span class="text-4xl font-black dark:text-white italic tracking-tighter">{parseFloat(stroj.radni_sati).toLocaleString('hr-HR')}</span>
|
|
||||||
<span class="text-xs font-black text-blue-600 uppercase">H</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={`p-8 rounded-[2.5rem] border relative overflow-hidden group ${isAtestExpired ? 'bg-red-50 border-red-100 dark:bg-red-900/10 dark:border-red-900/20' : 'bg-emerald-50 border-emerald-100 dark:bg-emerald-900/10 dark:border-emerald-900/20'}`}>
|
|
||||||
<i class="fa-solid fa-shield-check absolute -right-4 -bottom-4 text-6xl opacity-10 group-hover:rotate-12 transition-transform"></i>
|
|
||||||
<span class={`text-[9px] font-black uppercase block mb-2 tracking-widest ${isAtestExpired ? 'text-red-600' : 'text-emerald-600'}`}>Zadnji atest</span>
|
|
||||||
<span class={`text-2xl font-black italic tracking-tighter ${isAtestExpired ? 'text-red-600' : 'text-emerald-600'}`}>
|
|
||||||
{stroj.datum_zadnjeg_atesta ? new Date(stroj.datum_zadnjeg_atesta).toLocaleDateString('hr-HR') : 'Nema podataka'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-900 dark:bg-black p-8 rounded-[2.5rem] text-white relative overflow-hidden group shadow-2xl">
|
|
||||||
<i class="fa-solid fa-industry absolute -right-4 -bottom-4 text-6xl opacity-20 group-hover:rotate-12 transition-transform"></i>
|
|
||||||
<span class="text-[9px] font-black uppercase text-gray-400 block mb-2 tracking-widest">Proizvođač</span>
|
|
||||||
<span class="text-2xl font-black italic tracking-tighter block uppercase">{stroj.marka}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start px-2">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-8">
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3.5rem] p-10 md:p-16 border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 relative overflow-hidden">
|
|
||||||
<div class="grid sm:grid-cols-2 gap-12 items-center">
|
|
||||||
<div class="space-y-8">
|
|
||||||
<div>
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-[0.3em] text-blue-600 block mb-4 italic">Tehnička Specifikacija</label>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex justify-between border-b border-gray-50 dark:border-gray-700 pb-2">
|
|
||||||
<span class="text-xs font-bold text-gray-400 uppercase italic">Model</span>
|
|
||||||
<span class="text-sm font-black dark:text-white uppercase">{stroj.model_stroja}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between border-b border-gray-50 dark:border-gray-700 pb-2">
|
|
||||||
<span class="text-xs font-bold text-gray-400 uppercase italic">Tip jedinice</span>
|
|
||||||
<span class="text-sm font-black dark:text-white uppercase">{stroj.tip_human_readable}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between border-b border-gray-50 dark:border-gray-700 pb-2">
|
|
||||||
<span class="text-xs font-bold text-gray-400 uppercase italic">Registracija</span>
|
|
||||||
<span class="text-sm font-black dark:text-white uppercase">{stroj.registracija || 'NIJE REGISTRIRAN'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-6">
|
|
||||||
<Button variant="primary" class="!rounded-2xl py-4 w-full justify-center font-black uppercase italic tracking-widest">
|
|
||||||
<i class="fa-solid fa-wrench mr-2"></i> Otvori servisni nalog
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative group">
|
|
||||||
<div class="absolute inset-0 bg-blue-600 rounded-[2.5rem] rotate-3 opacity-10 group-hover:rotate-0 transition-transform duration-500"></div>
|
|
||||||
<div class="relative aspect-square bg-gray-100 dark:bg-gray-900 rounded-[2.5rem] overflow-hidden border border-gray-100 dark:border-gray-700">
|
|
||||||
<img
|
|
||||||
src="http://googleusercontent.com/image_collection/image_retrieval/5305205557646734227"
|
|
||||||
alt={stroj.naziv}
|
|
||||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 grayscale hover:grayscale-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-[2.5rem] p-10 border border-dashed border-gray-200 dark:border-gray-700">
|
|
||||||
<h3 class="text-sm font-black uppercase tracking-[0.2em] mb-4 dark:text-white italic">Status održavanja</h3>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 leading-relaxed italic">
|
|
||||||
Ovaj stroj ({stroj.naziv}) je zadnji put atestiran {stroj.datum_zadnjeg_atesta}. Prema servisnom planu, toranjske dizalice zahtijevaju periodični pregled svakih 12 mjeseci. Trenutni radni sati ({stroj.radni_sati} h) sugeriraju potrebu za redovnim podmazivanjem sklopova.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<AkcijePanel tip="stroj" podaci={stroj} />
|
|
||||||
|
|
||||||
<!-- <div class="mt-8 bg-blue-600 rounded-[2.5rem] p-8 text-white shadow-2xl relative overflow-hidden group">
|
|
||||||
<i class="fa-solid fa-file-pdf absolute -right-4 -top-4 text-7xl opacity-10"></i>
|
|
||||||
<h4 class="text-sm font-black uppercase tracking-widest italic mb-4">Dokumentacija</h4>
|
|
||||||
<div class="space-y-3 relative z-10">
|
|
||||||
<button class="w-full text-left bg-white/10 hover:bg-white/20 p-4 rounded-xl border border-white/10 transition-all">
|
|
||||||
<span class="text-[9px] font-black uppercase block opacity-70">Atestni list</span>
|
|
||||||
<span class="text-xs font-bold italic">Preuzmi PDF</span>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left bg-white/10 hover:bg-white/20 p-4 rounded-xl border border-white/10 transition-all">
|
|
||||||
<span class="text-[9px] font-black uppercase block opacity-70">Tehnički podaci</span>
|
|
||||||
<span class="text-xs font-bold italic">Otvori specifikaciju</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/fleet/strojevi/index.astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
|
||||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
|
||||||
import NaslovList from "../../../components/NaslovList.astro";
|
|
||||||
import GenericKarticaItem from "../../../components/GenericKarticaItem.astro";
|
|
||||||
|
|
||||||
// Importiranje utilitija
|
|
||||||
import { fetchStrojeviData, fetchCurrentUser } from "../../../lib/api";
|
|
||||||
import { getStatusColorClass, formatStatus } from "../../../utils/ui";
|
|
||||||
|
|
||||||
// 1. DOHVAT FILTERA IZ URL-a
|
|
||||||
const url = new URL(Astro.request.url);
|
|
||||||
const vlasnikId = url.searchParams.get('vlasnik');
|
|
||||||
|
|
||||||
// 2. DOHVAT PODATAKA (Paralelno)
|
|
||||||
const [{ strojevi }, user] = await Promise.all([
|
|
||||||
fetchStrojeviData(vlasnikId),
|
|
||||||
fetchCurrentUser()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 3. LOGIKA ZA DINAMIČKE INFORMACIJE
|
|
||||||
const imeKupca = strojevi.length > 0 && vlasnikId ? strojevi[0].vlasnik_naziv : null;
|
|
||||||
const imeKorisnika = user?.first_name || "Serviser";
|
|
||||||
const ulogaKorisnika = user?.is_serviser ? "Tehnička Služba" : "Logistika";
|
|
||||||
|
|
||||||
let dinamickiPodnaslov = "Pregled kompletne strojne flote i dizalica";
|
|
||||||
if (vlasnikId && imeKupca) {
|
|
||||||
dinamickiPodnaslov = `Prikaz strojeva dodijeljenih klijentu: ${imeKupca}`;
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Strojni park | Upravljanje">
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<!-- 1. WELCOME HEADER -->
|
|
||||||
<WelcomeHeader
|
|
||||||
textH1="Strojni park"
|
|
||||||
ime={imeKorisnika}
|
|
||||||
uloga={ulogaKorisnika}
|
|
||||||
podnaslov={dinamickiPodnaslov}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-6">
|
|
||||||
|
|
||||||
<!-- 2. NASLOV LISTE S GUMBOM ZA RESET FILTRA -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<NaslovList
|
|
||||||
naslov={vlasnikId ? "Filtrirani strojevi" : "Tehničke jedinice"}
|
|
||||||
ukupno={strojevi.length}
|
|
||||||
label1="Ukupno"
|
|
||||||
count1={strojevi.length}
|
|
||||||
label2="Na terenu"
|
|
||||||
count2={strojevi.length} // Ovdje možeš dodati filter za npr. 'u_radu'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{vlasnikId && (
|
|
||||||
<div class="px-6">
|
|
||||||
<a href="/fleet/strojevi" class="text-[9px] font-black text-red-500 uppercase tracking-widest hover:text-red-600 transition-colors flex items-center gap-2 italic">
|
|
||||||
<i class="fa-solid fa-circle-xmark"></i> Ukloni filter klijenta
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 3. GENERIČKA LISTA STROJEVA -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
|
||||||
<div id="strojevi-list">
|
|
||||||
{strojevi.length > 0 ? strojevi.map((s) => (
|
|
||||||
<GenericKarticaItem
|
|
||||||
bojaTeme="indigo"
|
|
||||||
href={`/fleet/strojevi/${s.id}`}
|
|
||||||
ikona="fa-screwdriver-wrench"
|
|
||||||
naslov={s.naziv}
|
|
||||||
subNaslov={s.serijski_broj}
|
|
||||||
metaTekst={`${s.vlasnik_naziv} | Radni sati: ${s.radni_sati}h`}
|
|
||||||
status={s.status || 'planirano'}
|
|
||||||
statusPrikaz={formatStatus(s.status || 'planirano')}
|
|
||||||
statusBojaClass={getStatusColorClass(s.status || 'planirano')}
|
|
||||||
/>
|
|
||||||
)) : (
|
|
||||||
<div class="p-24 text-center">
|
|
||||||
<i class="fa-solid fa-box-open text-4xl text-gray-100 dark:text-gray-700 mb-4 block"></i>
|
|
||||||
<p class="italic text-gray-400 text-[10px] uppercase tracking-widest leading-none">Nema strojeva u bazi podataka</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DESNI PANEL -->
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<AkcijePanel tip="fleet" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
// src/pages/fleet/vozila/[id].astro
|
|
||||||
---
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import Button from "../../../components/Button.astro"; // Uvoz tvoje zajedničke komponente za gumbe
|
|
||||||
import { getStatusColorClass } from "../../../utils/ui";
|
|
||||||
|
|
||||||
// 1. Dohvat ID-ja iz URL parametara
|
|
||||||
const { id } = Astro.params;
|
|
||||||
|
|
||||||
let vozilo = null;
|
|
||||||
let radniNalozi = [];
|
|
||||||
|
|
||||||
// 2. Dohvat podataka s Django API-ja (Backend)
|
|
||||||
try {
|
|
||||||
const voziloResponse = await fetch(`http://127.0.0.1:8000/api/fleet/vozila/${id}/`);
|
|
||||||
if (voziloResponse.ok) {
|
|
||||||
vozilo = await voziloResponse.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
const naloziResponse = await fetch(`http://127.0.0.1:8000/api/operativa/radni-nalozi/`);
|
|
||||||
if (naloziResponse.ok) {
|
|
||||||
const sviNalozi = await naloziResponse.json();
|
|
||||||
radniNalozi = sviNalozi.filter((nalog) => nalog.vozilo === parseInt(id) || nalog.vozilo_id === parseInt(id));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Greška pri dohvaćanju podataka s Django API-ja:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!vozilo) {
|
|
||||||
return Astro.redirect("/fleet/vozila?error=not-found");
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatStatusLocal(status) {
|
|
||||||
const map = {
|
|
||||||
'aktivan': 'Aktivan',
|
|
||||||
'servis': 'Na servisu',
|
|
||||||
'neaktivan': 'Izvan pogona'
|
|
||||||
};
|
|
||||||
return map[status?.toLowerCase()] || status;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrioritetClass(prioritet) {
|
|
||||||
const p = prioritet?.toLowerCase();
|
|
||||||
if (p === 'hitan' || p === 'visok') return 'bg-red-500/10 text-red-500 border-red-500/20';
|
|
||||||
if (p === 'srednji') return 'bg-amber-500/10 text-amber-500 border-amber-500/20';
|
|
||||||
return 'bg-blue-500/10 text-blue-500 border-blue-500/20';
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusKlasa = getStatusColorClass(vozilo.status?.toLowerCase());
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={`Vozilo | ${vozilo.naziv}`}>
|
|
||||||
<div class="max-w-4xl mx-auto px-4 py-8 font-sans">
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<Button
|
|
||||||
href="/fleet/vozila"
|
|
||||||
variant="ghost"
|
|
||||||
class="!px-0 text-sm font-black uppercase tracking-widest text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-arrow-left-long mr-2"></i> Povratak na popis
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-2xl rounded-[3rem] border border-gray-100 dark:border-gray-700 overflow-hidden transition-all mb-8">
|
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-between items-center bg-gray-50/50 dark:bg-gray-900/50 px-8 py-8 border-b border-gray-100 dark:border-gray-700 gap-4">
|
|
||||||
<div class="flex items-center gap-5">
|
|
||||||
<div class="bg-blue-600/10 text-blue-600 dark:text-blue-400 p-5 rounded-2xl shadow-md">
|
|
||||||
<i class="fa-solid fa-truck text-3xl"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-3xl md:text-4xl font-black uppercase tracking-tighter italic leading-none">
|
|
||||||
{vozilo.naziv}
|
|
||||||
</h1>
|
|
||||||
<span class="text-sm font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest block mt-1">
|
|
||||||
Interni ID: #{vozilo.id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class={`text-xs font-black px-4 py-2 rounded-full uppercase tracking-widest border shadow-sm ${statusKlasa}`}>
|
|
||||||
{vozilo.status_prikaz || formatStatusLocal(vozilo.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-900/40 p-6 rounded-2xl border border-gray-100 dark:border-gray-700/50">
|
|
||||||
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 dark:text-gray-500 block mb-1">
|
|
||||||
Registracijska oznaka
|
|
||||||
</span>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fa-solid fa-id-card text-xl text-blue-600 dark:text-blue-400 opacity-70"></i>
|
|
||||||
<span class="text-2xl font-black tracking-tight uppercase italic text-gray-800 dark:text-gray-100">
|
|
||||||
{vozilo.registracija}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-900/40 p-6 rounded-2xl border border-gray-100 dark:border-gray-700/50">
|
|
||||||
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 dark:text-gray-500 block mb-1">
|
|
||||||
Trenutna kilometraža
|
|
||||||
</span>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fa-solid fa-gauge text-xl text-blue-600 dark:text-blue-400 opacity-70"></i>
|
|
||||||
<span class="text-2xl font-black tracking-tight text-gray-800 dark:text-gray-100">
|
|
||||||
{vozilo.trenutni_kilometri?.toLocaleString('hr-HR') || 0} <span class="text-sm font-bold text-gray-400 uppercase">KM</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-2 bg-blue-600 text-white p-6 rounded-2xl shadow-xl relative overflow-hidden group">
|
|
||||||
<div class="absolute right-6 bottom-2 opacity-10 text-9xl font-black pointer-events-none transition-transform group-hover:scale-110 duration-500">
|
|
||||||
<i class="fa-solid fa-route"></i>
|
|
||||||
</div>
|
|
||||||
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-white/60 block mb-1">
|
|
||||||
Trenutni raspored / Baza
|
|
||||||
</span>
|
|
||||||
<h3 class="text-xl font-black uppercase tracking-tight italic">
|
|
||||||
Terenska Baza
|
|
||||||
</h3>
|
|
||||||
<p class="text-xs text-white/80 font-medium mt-1 max-w-xl">
|
|
||||||
Vozilo je mapirano na centralni logistički sustav fleet managementa. Sve izmjene kilometara i servisnih naloga sinkroniziraju se u realnom vremenu s radnim nalozima operativnog tima.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-8 py-5 bg-gray-50 dark:bg-gray-900/30 border-t border-gray-100 dark:border-gray-700 flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
href={`/operativa/radni-nalozi?vozilo=${vozilo.id}`}
|
|
||||||
variant="secondary"
|
|
||||||
class="text-xs font-black uppercase tracking-widest rounded-xl shadow-sm"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-file-invoice mr-1.5 opacity-70"></i> Otvori u operativi
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
id="btn-brzi-servis"
|
|
||||||
data-id={vozilo.id}
|
|
||||||
variant="primary"
|
|
||||||
class="text-xs font-black uppercase tracking-widest rounded-xl shadow-lg shadow-blue-600/20"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-screwdriver-wrench mr-1.5"></i> Otvori Servis
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-2xl rounded-[3rem] border border-gray-100 dark:border-gray-700 overflow-hidden p-8">
|
|
||||||
<div class="flex items-center justify-between mb-6 border-b border-gray-100 dark:border-gray-700 pb-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<i class="fa-solid fa-clipboard-list text-2xl text-blue-600 dark:text-blue-400"></i>
|
|
||||||
<h2 class="text-xl font-black uppercase tracking-tight italic">Povijest Radnih Naloga</h2>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs font-black bg-gray-100 dark:bg-gray-900 px-3 py-1.5 rounded-xl uppercase tracking-widest border border-gray-200 dark:border-gray-700">
|
|
||||||
Ukupno: {radniNalozi.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{radniNalozi.length === 0 ? (
|
|
||||||
<div class="text-center py-12 bg-gray-50 dark:bg-gray-900/20 rounded-2xl border border-dashed border-gray-200 dark:border-gray-700">
|
|
||||||
<i class="fa-solid fa-folder-open text-4xl text-gray-300 dark:text-gray-600 mb-3"></i>
|
|
||||||
<p class="text-sm font-bold text-gray-400 uppercase tracking-wider">Nema evidentiranih radnih naloga za ovo vozilo.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="space-y-4">
|
|
||||||
{radniNalozi.map((nalog) => (
|
|
||||||
<div class="flex flex-wrap items-center justify-between p-5 bg-gray-50 dark:bg-gray-900/30 rounded-2xl border border-gray-100 dark:border-gray-700/50 hover:border-blue-600 dark:hover:border-blue-400 transition-all gap-4">
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
<div class="bg-gray-200 dark:bg-gray-800 text-gray-700 dark:text-gray-300 p-3 rounded-xl font-black text-xs">
|
|
||||||
#{nalog.broj_naloga || nalog.id}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-black uppercase tracking-tight text-gray-800 dark:text-gray-100">
|
|
||||||
{nalog.opis_kvara || nalog.naslov || "Opis radova nije definiran"}
|
|
||||||
</h4>
|
|
||||||
<div class="flex items-center gap-4 mt-1 text-xs text-gray-400 font-medium">
|
|
||||||
<span>
|
|
||||||
<i class="fa-solid fa-calendar-days mr-1 text-blue-600/60"></i>
|
|
||||||
{nalog.datum_otvaranja ? new Date(nalog.datum_otvaranja).toLocaleDateString('hr-HR') : "Nepoznat datum"}
|
|
||||||
</span>
|
|
||||||
{nalog.kilometraža_prijave && (
|
|
||||||
<span>
|
|
||||||
<i class="fa-solid fa-gauge-high mr-1 text-blue-600/60"></i>
|
|
||||||
{nalog.kilometraža_prijave.toLocaleString('hr-HR')} KM
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class={`text-[10px] font-black px-2.5 py-1 rounded-lg border uppercase tracking-wider ${getPrioritetClass(nalog.prioritet)}`}>
|
|
||||||
{nalog.prioritet || "Normalno"}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
href={`/operativa/radni-nalozi/${nalog.id}`}
|
|
||||||
variant="ghost"
|
|
||||||
class="!p-2.5 bg-white dark:bg-gray-700 text-gray-500 dark:text-gray-300 rounded-xl border border-gray-200 dark:border-gray-600 hover:text-blue-600 dark:hover:text-blue-400 transition-colors shadow-sm"
|
|
||||||
title="Pregledaj nalog"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-chevron-right"></i>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function initDetaljiVozila() {
|
|
||||||
const servisBtn = document.getElementById('btn-brzi-servis');
|
|
||||||
if (servisBtn) {
|
|
||||||
servisBtn.addEventListener('click', () => {
|
|
||||||
const voziloId = servisBtn.getAttribute('data-id');
|
|
||||||
console.log(`Otvaram brzu servisnu prijavu za vozilo ID: ${voziloId}`);
|
|
||||||
window.location.href = `/operativa/radni-nalozi/novo?vozilo_id=${voziloId}&tip=servis`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initDetaljiVozila();
|
|
||||||
document.addEventListener('astro:after-swap', initDetaljiVozila);
|
|
||||||
</script>
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/fleet/vozila/index.astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import Button from "../../../components/Button.astro";
|
|
||||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
|
||||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
|
||||||
import NaslovList from "../../../components/NaslovList.astro";
|
|
||||||
import GenericKarticaItem from "../../../components/GenericKarticaItem.astro";
|
|
||||||
|
|
||||||
// Importiranje utilitija i centraliziranog API-ja
|
|
||||||
import { fetchDashboardData, fetchCurrentUser } from "../../../lib/api";
|
|
||||||
import { getStatusColorClass, formatStatus } from "../../../utils/ui";
|
|
||||||
|
|
||||||
// 1. DOHVAT FILTERA IZ URL-a (npr. ?status=servis)
|
|
||||||
const statusFilter = Astro.url.searchParams.get('status')?.toLowerCase();
|
|
||||||
|
|
||||||
// 2. DOHVAT PODATAKA
|
|
||||||
const [dashboardData, user] = await Promise.all([
|
|
||||||
fetchDashboardData(),
|
|
||||||
fetchCurrentUser()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const vozila = dashboardData?.vozila || [];
|
|
||||||
|
|
||||||
// 3. LOGIKA STATISTIKE (Uvijek se računa iz originalnog niza, neovisno o filteru)
|
|
||||||
const aktivnaVozila = vozila.filter(v => v.status?.toLowerCase() === 'aktivan').length;
|
|
||||||
const naServisu = vozila.filter(v => v.status?.toLowerCase() === 'servis').length;
|
|
||||||
const neaktivnaVozila = vozila.filter(v => v.status?.toLowerCase() === 'neaktivan').length;
|
|
||||||
|
|
||||||
// 4. LOGIKA FILTRIRANJA (Serverska strana)
|
|
||||||
let filtriraniVozila = vozila;
|
|
||||||
if (statusFilter) {
|
|
||||||
filtriraniVozila = vozila.filter(v => v.status?.toLowerCase() === statusFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Možeš dodati limit kroz props ako ovu stranicu ikada budeš koristio kao parcijalnu komponentu
|
|
||||||
const limit = 0;
|
|
||||||
const prikazanaVozila = limit > 0 ? filtriraniVozila.slice(0, limit) : filtriraniVozila;
|
|
||||||
|
|
||||||
// Dinamički naslov liste ovisno o odabranom filteru flote
|
|
||||||
const prikazaniNaslov = statusFilter
|
|
||||||
? `Flota: ${formatStatus(statusFilter)}`
|
|
||||||
: "Aktivna Flota";
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Vozni park | Flota">
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<WelcomeHeader />
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-6">
|
|
||||||
<NaslovList
|
|
||||||
naslov={prikazaniNaslov}
|
|
||||||
ukupno={filtriraniVozila.length}
|
|
||||||
|
|
||||||
label1="Spremni"
|
|
||||||
filter1="aktivan" count1={aktivnaVozila}
|
|
||||||
|
|
||||||
label2="Servis"
|
|
||||||
filter2="servis" count2={naServisu}
|
|
||||||
|
|
||||||
label3="Neaktivni"
|
|
||||||
filter3="neaktivan" count3={neaktivnaVozila}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
|
||||||
<div id="vozila-list">
|
|
||||||
{prikazanaVozila.length > 0 ? prikazanaVozila.map((v) => (
|
|
||||||
<GenericKarticaItem
|
|
||||||
href={`/fleet/vozila/${v.id}`}
|
|
||||||
status={v.status?.toLowerCase()}
|
|
||||||
statusBojaClass={getStatusColorClass(v.status?.toLowerCase())}
|
|
||||||
ikona={v.status?.toLowerCase() === 'aktivan' ? 'fa-truck' : 'fa-screwdriver-wrench'}
|
|
||||||
naslov={v.naziv}
|
|
||||||
subNaslov={v.registracija}
|
|
||||||
metaTekst={`Prijavljeno: ${v.trenutni_kilometri?.toLocaleString() || 0} KM | Lokacija: ${v.lokacija || v.trenutna_lokacija || 'Baza (Zagreb)'}`}
|
|
||||||
statusPrikaz={v.status_prikaz || formatStatus(v.status?.toLowerCase())}
|
|
||||||
/>
|
|
||||||
)) : (
|
|
||||||
<div class="p-24 text-center">
|
|
||||||
<i class="fa-solid fa-truck-dash text-4xl text-gray-100 dark:text-gray-700 mb-4 block"></i>
|
|
||||||
<p class="italic text-gray-400 text-[10px] uppercase tracking-widest leading-none">Nema vozila za odabrani status</p>
|
|
||||||
{!statusFilter && (
|
|
||||||
<Button variant="outline" class="mt-6" id="btn-dodaj-prvo">Dodaj prvo vozilo</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<AkcijePanel />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Inicijalizacija klikova na kartice i filtere unutar flote
|
|
||||||
import { initFilters } from "../../../scripts/filters";
|
|
||||||
initFilters();
|
|
||||||
document.addEventListener('astro:after-swap', initFilters);
|
|
||||||
</script>
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/index.astro
|
|
||||||
import Layout from "../layouts/Layout.astro";
|
|
||||||
import WelcomeHeader from "../components/WelcomeHeader.astro";
|
|
||||||
import AkcijePanel from "../components/AkcijePanel.astro";
|
|
||||||
import RadniNalogLista from "../components/RadniNalogLista.astro";
|
|
||||||
import StatsGrid from "../components/StatsGrid.astro";
|
|
||||||
import Kalendar from "../components/Kalendar/Kalendar.astro";
|
|
||||||
|
|
||||||
// API i Centralizirani Utils iz api.js
|
|
||||||
import { fetchDashboardData } from "../lib/api";
|
|
||||||
|
|
||||||
// 1. Dohvat podataka s backenda (Samo bazični dashboard podaci koji se mogu poslužiti asinkrono)
|
|
||||||
const data = await fetchDashboardData();
|
|
||||||
|
|
||||||
const nalozi = data?.nalozi || [];
|
|
||||||
|
|
||||||
// 2. Kalkulacija statistike tolerantna na velika/mala slova (.toLowerCase())
|
|
||||||
const naloziURadu = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
|
||||||
const planiraniNalozi = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
|
||||||
|
|
||||||
const zavrseniNalozi = nalozi.filter(n => {
|
|
||||||
const statusMalo = n.status?.toLowerCase();
|
|
||||||
return statusMalo === 'zavrseno' || statusMalo === 'naplaceno';
|
|
||||||
}).length;
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Dashboard | ServisLog">
|
|
||||||
<div class="w-full space-y-5">
|
|
||||||
|
|
||||||
<WelcomeHeader naslov="Kontrolna ploča" />
|
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-2 sm:gap-6 px-2">
|
|
||||||
<StatsGrid
|
|
||||||
label="Planirano"
|
|
||||||
value={planiraniNalozi}
|
|
||||||
icon="fa-calendar-check"
|
|
||||||
variant="blue"
|
|
||||||
filterValue="planirano"
|
|
||||||
/>
|
|
||||||
<StatsGrid
|
|
||||||
label="U radu"
|
|
||||||
value={naloziURadu}
|
|
||||||
icon="fa-screwdriver-wrench"
|
|
||||||
variant="yellow"
|
|
||||||
animate={true}
|
|
||||||
filterValue="u_radu"
|
|
||||||
/>
|
|
||||||
<StatsGrid
|
|
||||||
label="Gotovo"
|
|
||||||
value={zavrseniNalozi}
|
|
||||||
icon="fa-circle-check"
|
|
||||||
variant="emerald"
|
|
||||||
filterValue="zavrseno"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start px-2 pb-10">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-8">
|
|
||||||
<RadniNalogLista
|
|
||||||
limit={5}
|
|
||||||
naslov="Zadnje aktivnosti"
|
|
||||||
prikaziNaslov={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-3 sticky top-28">
|
|
||||||
<AkcijePanel tip="dashboard" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-5">
|
|
||||||
<Kalendar />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { initFilters } from "../scripts/filters.js";
|
|
||||||
import { provjeriPristupStranici } from "../utils/auth.js";
|
|
||||||
|
|
||||||
// 🚀 POKREĆEMO SIGURNOSNI KLIJENTSKI GUARD:
|
|
||||||
// On odmah na klijentu provjerava is_serviser. Ako je ulogiran običan serviser,
|
|
||||||
// prisilno ga i trenutno preusmjerava na njegov dinamički terminal /operativa/serviseri/[id]
|
|
||||||
async function inicijalizirajStranicu() {
|
|
||||||
await provjeriPristupStranici();
|
|
||||||
initFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pokreni odmah pri učitavanju u preglednik
|
|
||||||
inicijalizirajStranicu();
|
|
||||||
|
|
||||||
// Re-inicijalizacija kod klijentske navigacije (Astro View Transitions)
|
|
||||||
document.addEventListener('astro:after-swap', inicijalizirajStranicu);
|
|
||||||
</script>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/kalendar-dogadaja.astro
|
|
||||||
import Layout from "../layouts/Layout.astro";
|
|
||||||
import WelcomeHeader from "../components/WelcomeHeader.astro";
|
|
||||||
import Kalendar from "../components/Kalendar/Kalendar.astro";
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Kalendar | ServisLog">
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<WelcomeHeader naslov="Kalendar događaja" />
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-2 sm:gap-6 px-2">
|
|
||||||
<Kalendar />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { provjeriPristupStranici } from "../utils/auth.js";
|
|
||||||
|
|
||||||
async function inicijalizirajStranicu() {
|
|
||||||
// 🚀 SIGURNOSNI UKLOP: Guard provjerava sesiju na klijentu i štiti kalendar od neautoriziranih upada
|
|
||||||
await provjeriPristupStranici();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pokreni odmah pri učitavanju u preglednik
|
|
||||||
inicijalizirajStranicu();
|
|
||||||
|
|
||||||
// Re-inicijalizacija kod klijentske navigacije kroz ERP (Astro View Transitions)
|
|
||||||
document.addEventListener('astro:after-swap', inicijalizirajStranicu);
|
|
||||||
</script>
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/kupci/[id].astro
|
|
||||||
import Layout from "../../layouts/Layout.astro";
|
|
||||||
import AkcijePanel from "../../components/AkcijePanel.astro";
|
|
||||||
import ListaStrojeva from "../../components/ListaStrojeva.astro";
|
|
||||||
import { fetchStrojeviData, fetchRadniNalozi, fetchKupacDetalji, routes } from "../../lib/api";
|
|
||||||
|
|
||||||
const { id } = Astro.params;
|
|
||||||
|
|
||||||
// 1. Paralelno dohvaćanje podataka (brže renderiranje)
|
|
||||||
// Koristimo status 'u_radu' jer smo utvrdili da Django ne prepoznaje 'otvoreno'
|
|
||||||
const [strojeviData, kupac, nalozi] = await Promise.all([
|
|
||||||
fetchStrojeviData(id),
|
|
||||||
fetchKupacDetalji(id),
|
|
||||||
fetchRadniNalozi({ klijent: id, status: 'u_radu' })
|
|
||||||
]);
|
|
||||||
|
|
||||||
const strojevi = strojeviData?.strojevi || [];
|
|
||||||
|
|
||||||
// 2. Sigurnosni redirect ako kupac ne postoji
|
|
||||||
if (!kupac) return Astro.redirect("/404");
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={`Klijent | ${kupac.naziv}`}>
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<!-- 1. HEADER (Brutalistički stil) -->
|
|
||||||
<header class="px-2 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<a href="/kupci/svi" class="text-indigo-600 text-[10px] font-black uppercase tracking-[0.3em] hover:underline flex items-center gap-2 no-underline mb-4">
|
|
||||||
<i class="fa-solid fa-arrow-left text-[8px]"></i> Povratak klijentima
|
|
||||||
</a>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<h1 class="text-4xl sm:text-6xl font-black text-gray-900 dark:text-white uppercase tracking-tighter leading-none italic">
|
|
||||||
{kupac.naziv}
|
|
||||||
</h1>
|
|
||||||
<div class="hidden sm:block px-4 py-1.5 bg-indigo-600 text-white rounded-full text-[10px] font-black uppercase tracking-widest italic">
|
|
||||||
{kupac.tip || 'Partner'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400 font-medium italic">
|
|
||||||
Glavno sjedište: <span class="text-indigo-600 font-bold">{kupac.grad}, {kupac.adresa}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- OIB Info box -->
|
|
||||||
<div class="flex items-center gap-3 px-6 py-4 bg-white dark:bg-gray-800 rounded-[2rem] border border-gray-100 dark:border-gray-700 shadow-xl">
|
|
||||||
<div class="flex flex-col items-end">
|
|
||||||
<span class="text-[8px] font-black uppercase text-gray-400 tracking-widest">Porezni broj</span>
|
|
||||||
<span class="text-xl font-black text-gray-900 dark:text-white tracking-tighter italic">OIB: {kupac.oib}</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-px h-8 bg-gray-100 dark:bg-gray-700 mx-2"></div>
|
|
||||||
<i class="fa-solid fa-address-card text-gray-200 text-2xl"></i>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 2. QUICK STATS (Brojači) -->
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 px-2">
|
|
||||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-[2.5rem] border border-gray-100 dark:border-gray-700 shadow-sm relative overflow-hidden group">
|
|
||||||
<i class="fa-solid fa-crane absolute -right-4 -bottom-4 text-6xl opacity-5 group-hover:rotate-12 transition-transform"></i>
|
|
||||||
<span class="text-[9px] font-black uppercase text-gray-400 block mb-2 tracking-widest italic">Strojni Park</span>
|
|
||||||
<div class="flex items-baseline gap-2">
|
|
||||||
<span class="text-4xl font-black dark:text-white italic tracking-tighter">{strojevi.length}</span>
|
|
||||||
<span class="text-xs font-black text-indigo-600 uppercase">Jedinica</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Poveznica na filtriranu listu naloga -->
|
|
||||||
<a
|
|
||||||
href={routes.radniNalozi(id, 'u_radu')}
|
|
||||||
class="bg-white dark:bg-gray-800 p-8 rounded-[2.5rem] border border-gray-100 dark:border-gray-700 shadow-sm relative overflow-hidden group hover:border-indigo-500 transition-all no-underline block"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-file-invoice absolute -right-4 -bottom-4 text-6xl opacity-5 group-hover:rotate-12 transition-transform text-indigo-600"></i>
|
|
||||||
<span class="text-[9px] font-black uppercase text-gray-400 block mb-2 tracking-widest italic group-hover:text-indigo-600 transition-colors">
|
|
||||||
Otvoreni radni Nalozi
|
|
||||||
</span>
|
|
||||||
<div class="flex items-baseline gap-2">
|
|
||||||
<span class="text-4xl font-black dark:text-white italic tracking-tighter">
|
|
||||||
{String(nalozi?.length || 0)}
|
|
||||||
</span>
|
|
||||||
<i class="fa-solid fa-arrow-up-right-from-square text-[10px] text-indigo-600 opacity-0 group-hover:opacity-100 transition-opacity ml-2"></i>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 3. MAIN CONTENT (Lista strojeva) -->
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start px-2">
|
|
||||||
<div class="md:col-span-7 space-y-12">
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3.5rem] p-4 md:p-8 border border-gray-100 dark:border-gray-700 shadow-2xl shadow-indigo-500/5">
|
|
||||||
<ListaStrojeva
|
|
||||||
strojevi={strojevi}
|
|
||||||
naslov={`Operativna flota klijenta`}
|
|
||||||
prikaziVlasnika={false}
|
|
||||||
bojaTeme="indigo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-8 py-10 bg-gray-50 dark:bg-gray-900/50 rounded-[2.5rem] border border-dashed border-gray-200 dark:border-gray-700 flex justify-between items-center group">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<h3 class="text-sm font-black uppercase dark:text-white italic tracking-widest">Napredna analitika voznog parka</h3>
|
|
||||||
<p class="text-xs text-gray-500 italic">Pristupite detaljnim izvještajima i povijesti strojeva.</p>
|
|
||||||
</div>
|
|
||||||
<a href={`/fleet/strojevi?vlasnik=${id}`} class="bg-indigo-600 text-white p-4 rounded-2xl hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-500/20">
|
|
||||||
<i class="fa-solid fa-arrow-right-long text-xl"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 4. RIGHT PANEL (Akcije i Detalji) -->
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<!-- AkcijePanel sada prima garantirani 'kupac' objekt -->
|
|
||||||
<AkcijePanel tip="vlasnik" podaci={kupac} />
|
|
||||||
|
|
||||||
<div class="mt-8 bg-indigo-600 rounded-[2.5rem] p-8 text-white shadow-2xl relative overflow-hidden group">
|
|
||||||
<i class="fa-solid fa-comments-dollar absolute -right-4 -top-4 text-7xl opacity-10"></i>
|
|
||||||
<h4 class="text-sm font-black uppercase tracking-widest italic mb-4">Naplata i Ugovori</h4>
|
|
||||||
<div class="space-y-3 relative z-10">
|
|
||||||
<button class="w-full text-left bg-white/10 hover:bg-white/20 p-4 rounded-xl border border-white/10 transition-all">
|
|
||||||
<span class="text-[9px] font-black uppercase block opacity-70">Uvjeti plaćanja</span>
|
|
||||||
<span class="text-xs font-bold italic">30 dana (valuta)</span>
|
|
||||||
</button>
|
|
||||||
<button class="w-full text-left bg-white/10 hover:bg-white/20 p-4 rounded-xl border border-white/10 transition-all">
|
|
||||||
<span class="text-[9px] font-black uppercase block opacity-70">Dokumentacija</span>
|
|
||||||
<span class="text-xs font-bold italic">Preuzmi PDF</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/kupci/svi.astro
|
|
||||||
import Layout from "../../layouts/Layout.astro";
|
|
||||||
import AkcijePanel from "../../components/AkcijePanel.astro";
|
|
||||||
import WelcomeHeader from "../../components/WelcomeHeader.astro";
|
|
||||||
|
|
||||||
// Importiranje nove funkcije
|
|
||||||
import { fetchKupciData } from "../../lib/api";
|
|
||||||
import { getStatusColorClass } from "../../utils/ui";
|
|
||||||
|
|
||||||
// 1. DOHVAT PODATAKA (Koristimo novu funkciju)
|
|
||||||
const { kupci } = await fetchKupciData();
|
|
||||||
|
|
||||||
// 2. LOGIKA STATISTIKE
|
|
||||||
const aktivniKupci = kupci.filter(k => k.aktivno === true).length;
|
|
||||||
const brojPravnih = kupci.filter(k => k.tip === 'pravno').length;
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Partneri i Kupci | Servislog">
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<WelcomeHeader textH1="Baza" textH1dodatno="Partnera" podnaslov="Pregled klijenata, kontakata i strojne flote" />
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-6">
|
|
||||||
<div class="flex justify-between items-center px-4">
|
|
||||||
<h2 class="text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight italic">Popis klijenata</h2>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<span class="text-[10px] font-bold uppercase text-emerald-600 bg-emerald-50 dark:bg-emerald-900/20 px-2 py-1 rounded border border-emerald-200/50 italic">Aktivni: {aktivniKupci}</span>
|
|
||||||
<span class="text-[10px] font-bold uppercase text-blue-600 bg-blue-50 dark:bg-blue-900/20 px-2 py-1 rounded border border-blue-200/50 italic">Tvrtke: {brojPravnih}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
|
||||||
<div id="kupci-list">
|
|
||||||
{kupci.length > 0 ? kupci.map((k) => (
|
|
||||||
<a
|
|
||||||
href={`/kupci/${k.id}`}
|
|
||||||
class="kupac-item flex items-center justify-between p-8 hover:bg-blue-50/30 dark:hover:bg-gray-700/30 border-b border-gray-50 dark:border-gray-700 last:border-0 group transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-6 text-left">
|
|
||||||
<div class="w-14 h-14 rounded-2xl bg-gray-50 dark:bg-gray-900 flex items-center justify-center text-gray-400 group-hover:bg-blue-600 group-hover:text-white transition-all shadow-sm">
|
|
||||||
<i class={`fa-solid ${k.tip === 'pravno' ? 'fa-building-shield' : 'fa-user-gear'} text-xl`}></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="font-black text-gray-900 dark:text-white text-xl uppercase tracking-tighter italic group-hover:text-blue-600 transition-colors leading-none">
|
|
||||||
{k.naziv}
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] font-black bg-gray-100 dark:bg-gray-900 px-2 py-0.5 rounded border border-gray-200 dark:border-gray-700 uppercase tracking-widest text-gray-500">
|
|
||||||
{k.grad}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="text-gray-400 text-[10px] font-black uppercase tracking-widest mt-2">
|
|
||||||
OIB: <span class="text-gray-900 dark:text-gray-200">{k.oib}</span>
|
|
||||||
<span class="text-blue-600/30 mx-1">•</span>
|
|
||||||
Kontakt: {k.telefon}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-8">
|
|
||||||
<div class="hidden lg:flex flex-col items-end text-right">
|
|
||||||
<span class="text-[8px] font-black uppercase text-gray-400 tracking-[0.2em]">Oprema u sustavu</span>
|
|
||||||
<span class="text-xs font-black uppercase italic dark:text-gray-300">
|
|
||||||
{k.broj_strojeva} Jedinica
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<i class="fa-solid fa-arrow-right-long text-gray-200 group-hover:text-blue-600 group-hover:translate-x-2 transition-all"></i>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
)) : (
|
|
||||||
<div class="p-24 text-center">
|
|
||||||
<i class="fa-solid fa-users-viewfinder text-4xl text-gray-100 dark:text-gray-700 mb-4 block"></i>
|
|
||||||
<p class="italic text-gray-400 text-[10px] uppercase tracking-widest">Nema kupaca u bazi</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<AkcijePanel tip="dashboard" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { initFilters } from "../../scripts/filters";
|
|
||||||
initFilters();
|
|
||||||
document.addEventListener('astro:after-swap', initFilters);
|
|
||||||
</script>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/login.astro
|
|
||||||
import Layout from "../layouts/Layout.astro";
|
|
||||||
import LoginForm from "../components/LoginForm.astro";
|
|
||||||
import WelcomeHeader from "../components/WelcomeHeader.astro";
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Prijava u sustav">
|
|
||||||
<div class="flex items-center justify-center px-4">
|
|
||||||
|
|
||||||
<!-- WELCOME HEADER -->
|
|
||||||
<WelcomeHeader />
|
|
||||||
|
|
||||||
<!-- LOGIN FORM -->
|
|
||||||
<LoginForm />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/operativa/radni-nalozi/[id].astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
|
||||||
import Gallery from "../../../components/Gallery.astro";
|
|
||||||
|
|
||||||
// Utiliti za vizualni prikaz
|
|
||||||
import { getStatusColorClass } from "../../../utils/ui";
|
|
||||||
|
|
||||||
const { id } = Astro.params;
|
|
||||||
const API_BASE = import.meta.env.PUBLIC_API_URL;
|
|
||||||
|
|
||||||
let nalog = null;
|
|
||||||
let dostupnaVozila = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Dohvaćamo detaljne podatke o radnom nalogu (RadniNalogDetaljiSerializer)
|
|
||||||
const resNalog = await fetch(`${API_BASE}/operativa/radni-nalozi/${id}/?t=${Date.now()}`);
|
|
||||||
if (resNalog.ok) nalog = await resNalog.json();
|
|
||||||
|
|
||||||
// 2. Ako radni nalog nema vezan putni nalog, dohvaćamo popis vozila za dropdown u klijentskom otoku
|
|
||||||
if (nalog && !nalog.putni_nalog) {
|
|
||||||
const resVozila = await fetch(`${API_BASE}/fleet/vozila/`);
|
|
||||||
if (resVozila.ok) dostupnaVozila = await resVozila.json();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Greška pri dohvatu podataka s API-ja:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nalog) return Astro.redirect("/404");
|
|
||||||
|
|
||||||
// Priprema datuma za prikaz
|
|
||||||
const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR', {
|
|
||||||
day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit'
|
|
||||||
});
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={`Nalog #${nalog.broj_naloga}`}>
|
|
||||||
<div class="w-full space-y-10 pb-20">
|
|
||||||
|
|
||||||
<header class="px-2 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
|
|
||||||
<div>
|
|
||||||
<a href="/" class="text-blue-600 text-[10px] font-black uppercase tracking-[0.3em] hover:underline flex items-center gap-2 mb-6 no-underline">
|
|
||||||
<i class="fa-solid fa-arrow-left text-[8px]"></i> Povratak na dashboard
|
|
||||||
</a>
|
|
||||||
<h1 class="text-4xl md:text-6xl font-black text-gray-900 dark:text-white uppercase tracking-tighter leading-none">
|
|
||||||
Radni nalog <span class="text-blue-600">#{nalog.broj_naloga}</span>
|
|
||||||
</h1>
|
|
||||||
<div class="flex flex-wrap items-center gap-4 mt-4">
|
|
||||||
<p class="text-gray-500 dark:text-gray-400 font-medium italic text-sm">
|
|
||||||
Otvoreno: {datumKreiranja}h
|
|
||||||
</p>
|
|
||||||
<span class="hidden sm:block text-gray-300 dark:text-gray-700">|</span>
|
|
||||||
<p class="text-blue-600 dark:text-blue-400 font-black text-xs uppercase tracking-widest">
|
|
||||||
<i class="fa-solid fa-user-gear mr-1"></i> {nalog.izvrsitelj_ime || 'Nedodijeljeno'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3 px-6 py-3 bg-white dark:bg-gray-800 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-xl">
|
|
||||||
<div class:list={[
|
|
||||||
"w-3 h-3 rounded-full",
|
|
||||||
getStatusColorClass(nalog.status),
|
|
||||||
{"animate-pulse": nalog.status === 'U_RADU'}
|
|
||||||
]}></div>
|
|
||||||
<span class="font-black uppercase text-xs tracking-widest text-gray-700 dark:text-gray-200">
|
|
||||||
{nalog.status_display || nalog.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start px-2">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-8">
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] p-10 md:p-16 border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 relative overflow-hidden">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-[0.3em] text-blue-600 block mb-6 italic">Opis kvara / Zadatak</label>
|
|
||||||
<p class="text-2xl md:text-3xl font-medium text-gray-700 dark:text-gray-200 italic leading-tight tracking-tight">
|
|
||||||
{nalog.opis_kvara}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid sm:grid-cols-2 gap-6">
|
|
||||||
<div class="bg-gray-900 rounded-[2.5rem] p-8 text-white shadow-xl relative overflow-hidden group">
|
|
||||||
<i class="fa-solid fa-screwdriver-wrench absolute -right-4 -bottom-4 text-8xl opacity-10 -rotate-12 group-hover:rotate-0 transition-transform duration-700"></i>
|
|
||||||
<label class="text-[9px] font-black uppercase text-gray-500 block mb-4 italic tracking-widest">Stroj na popravku</label>
|
|
||||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic leading-none mb-2">{nalog.stroj?.naziv}</h3>
|
|
||||||
<span class="inline-block bg-blue-600 text-white px-3 py-1 rounded font-black text-[10px] tracking-widest uppercase mb-4">
|
|
||||||
SN: {nalog.stroj?.serijski_broj || 'Nema SN'}
|
|
||||||
</span>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-xs text-gray-400 font-medium uppercase">Radni sati: <span class="text-white font-black">{nalog.stroj?.radni_sati || 0} h</span></p>
|
|
||||||
<p class="text-xs text-gray-400 font-medium uppercase">Lokacija: <span class="text-white font-black">{nalog.stroj?.lokacija || 'Teren'}</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[2.5rem] p-8 border border-gray-100 dark:border-gray-700 shadow-sm">
|
|
||||||
<label class="text-[9px] font-black uppercase text-gray-400 block mb-4 italic tracking-widest">Klijent / Vlasnik</label>
|
|
||||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic dark:text-white leading-none mb-2">{nalog.klijent?.naziv}</h3>
|
|
||||||
<p class="text-sm text-gray-500 font-medium">{nalog.klijent?.grad} <span class="mx-1 text-blue-500">•</span> OIB: {nalog.klijent?.oib}</p>
|
|
||||||
|
|
||||||
<div class="mt-6 flex flex-wrap gap-4">
|
|
||||||
<a href={`tel:${nalog.klijent?.telefon}`} class="flex items-center gap-2 text-blue-600 font-black text-[10px] uppercase no-underline hover:opacity-70 transition-opacity">
|
|
||||||
<div class="w-7 h-7 rounded-full bg-blue-50 flex items-center justify-center"><i class="fa-solid fa-phone"></i></div> Nazovi
|
|
||||||
</a>
|
|
||||||
<a href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent((nalog.klijent?.adresa || '') + ' ' + (nalog.klijent?.grad || ''))}`} target="_blank" class="flex items-center gap-2 text-gray-500 font-black text-[10px] uppercase no-underline hover:opacity-70 transition-opacity">
|
|
||||||
<div class="w-7 h-7 rounded-full bg-gray-50 flex items-center justify-center"><i class="fa-solid fa-location-dot"></i></div> Navigacija
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{nalog.vozilo ? (
|
|
||||||
<div class="bg-gray-900 rounded-[2.5rem] p-8 text-white shadow-xl relative overflow-hidden group">
|
|
||||||
<i class="fa-solid fa-van-shuttle absolute -right-4 -bottom-4 text-8xl opacity-10 -rotate-12 group-hover:rotate-0 transition-transform duration-700"></i>
|
|
||||||
<label class="text-[9px] font-black uppercase text-gray-500 block mb-4 italic tracking-widest">Servisno vozilo</label>
|
|
||||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic leading-none mb-2">{nalog.vozilo?.naziv}</h3>
|
|
||||||
<span class="inline-block bg-blue-600 text-white px-3 py-1 rounded font-black text-[10px] tracking-widest uppercase mb-4">
|
|
||||||
Registracija: {nalog.vozilo?.registracija || ''}
|
|
||||||
</span>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-xs text-gray-400 font-medium uppercase">Trenutni km: <span class="text-white font-black">{nalog.vozilo.trenutni_kilometri?.toLocaleString() || '0'} km</span></p>
|
|
||||||
<p class="text-xs text-gray-400 font-medium uppercase">Status vozila: <span class="text-white font-black">{nalog.vozilo?.status_prikaz || 'Aktivno na terenu'}</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-900/30 rounded-[2.5rem] p-8 border border-dashed border-gray-200 dark:border-gray-800 flex items-center gap-4 opacity-60">
|
|
||||||
<i class="fa-solid fa-truck-ramp-box text-gray-400"></i>
|
|
||||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Radni nalog bez aktivnog putnog naloga / vozila</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div class="flex items-center justify-between px-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 italic">Foto dokumentacija terena</label>
|
|
||||||
<span class="text-[10px] font-black bg-gray-100 dark:bg-gray-800 px-3 py-1 rounded-full text-gray-500">
|
|
||||||
{nalog.slike?.length || 0} SLIKA
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Gallery
|
|
||||||
images={nalog.slike}
|
|
||||||
galleryId={`gallery-rn-${nalog.id}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-3 space-y-6 sticky top-10">
|
|
||||||
<AkcijePanel tip="radni-nalog" podaci={nalog} />
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[2rem] p-6 border border-gray-100 dark:border-gray-700 shadow-sm">
|
|
||||||
<h4 class="text-[9px] font-black uppercase text-gray-400 mb-4 tracking-widest italic">Logistika i Terenski Put</h4>
|
|
||||||
|
|
||||||
{nalog.putni_nalog ? (
|
|
||||||
<div class="bg-green-50 dark:bg-green-900/10 p-5 rounded-2xl border border-green-100 dark:border-green-900/20 flex flex-col gap-2">
|
|
||||||
<p class="text-xs font-black text-green-700 dark:text-green-400 leading-tight flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-file-circle-check text-sm"></i> Povezan putni nalog #{nalog.putni_nalog.broj_naloga}
|
|
||||||
</p>
|
|
||||||
<div class="text-[11px] text-gray-500 dark:text-gray-400 space-y-1 border-t border-b border-gray-100 dark:border-gray-700/50 py-2 my-1">
|
|
||||||
<p>Polazna kilometraža: <span class="font-black text-gray-700 dark:text-gray-200">{nalog.putni_nalog.pocetna_km?.toLocaleString()} km</span></p>
|
|
||||||
<p class="flex items-center gap-1.5">Status puta: <span class="inline-block bg-green-200 dark:bg-green-900/40 text-green-800 dark:text-green-300 text-[9px] font-black px-2 py-0.5 rounded uppercase tracking-wider">{nalog.putni_nalog.status}</span></p>
|
|
||||||
</div>
|
|
||||||
<a href={`/logistika/putni-nalozi/${nalog.putni_nalog.id}`} class="text-[10px] uppercase font-black text-blue-600 dark:text-blue-400 no-underline hover:underline pt-1 flex items-center gap-1">
|
|
||||||
Otvori putni nalog <i class="fa-solid fa-arrow-right text-[8px]"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="space-y-3 mt-2">
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-relaxed mb-2">
|
|
||||||
Ovaj radni nalog trenutno nema vezan putni nalog. Za potrebe obrade dnevnica odaberite vozilo:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<select
|
|
||||||
id="pn-vozilo-select"
|
|
||||||
class="w-full p-3 text-xs rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 dark:text-white font-black uppercase tracking-wider focus:outline-none focus:border-blue-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
<option value="">-- Odaberi Servisno Vozilo --</option>
|
|
||||||
{dostupnaVozila.map(v => (
|
|
||||||
<option value={v.id}>{v.registracija} - {v.naziv}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="pn-generiraj-btn"
|
|
||||||
data-nalog-id={nalog.id}
|
|
||||||
class="w-full py-4 px-6 bg-blue-600 hover:bg-blue-700 text-white font-black text-xs uppercase tracking-widest rounded-2xl transition-all shadow-lg shadow-blue-500/20 text-center flex items-center justify-center gap-2 cursor-pointer disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<i id="pn-btn-icon" class="fa-solid fa-file-shield"></i>
|
|
||||||
<span id="pn-btn-text">Generiraj Putni Nalog</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-[2rem] p-6 border border-gray-100 dark:border-gray-700 shadow-sm">
|
|
||||||
<h4 class="text-[9px] font-black uppercase text-gray-400 mb-4 tracking-widest">Zadnja aktivnost</h4>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-xl bg-green-50 dark:bg-green-900/20 flex items-center justify-center text-green-600">
|
|
||||||
<i class="fa-solid fa-clock-rotate-left text-xs"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-[10px] text-gray-400 uppercase font-bold leading-none mb-1">Ažurirano</p>
|
|
||||||
<p class="text-xs font-black dark:text-gray-200 leading-none">
|
|
||||||
{new Date(nalog.datum_azuriranja).toLocaleDateString('hr-HR')} u {new Date(nalog.datum_azuriranja).toLocaleTimeString('hr-HR', {hour:'2-digit', minute:'2-digit'})}h
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center p-6 border-2 border-dashed border-gray-100 dark:border-gray-800 rounded-[2rem]">
|
|
||||||
<p class="text-[8px] font-black text-gray-300 dark:text-gray-600 uppercase tracking-[0.4em]">Sistemski ID: {nalog.id}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { createPutniNalog } from '../../../lib/api.js';
|
|
||||||
|
|
||||||
function initLogistika() {
|
|
||||||
const gumb = document.getElementById('pn-generiraj-btn');
|
|
||||||
const select = document.getElementById('pn-vozilo-select');
|
|
||||||
const btnIcon = document.getElementById('pn-btn-icon');
|
|
||||||
const btnText = document.getElementById('pn-btn-text');
|
|
||||||
|
|
||||||
if (!gumb || !select) return;
|
|
||||||
|
|
||||||
gumb.addEventListener('click', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const radniNalogId = gumb.getAttribute('data-nalog-id');
|
|
||||||
const odabranoVoziloId = select.value;
|
|
||||||
|
|
||||||
// 1. Brza klijentska validacija
|
|
||||||
if (!odabranoVoziloId) {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.showToast?.("Molimo odaberite službeno vozilo.", "error");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Pokretanje vizualnog loading stanja
|
|
||||||
gumb.disabled = true;
|
|
||||||
select.disabled = true;
|
|
||||||
if (btnText) btnText.textContent = 'Generiranje...';
|
|
||||||
if (btnIcon) btnIcon.className = 'fa-solid fa-spinner animate-spin';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 3. Slanje zahtjeva na Django backend (Kroz pročišćeni api.js)
|
|
||||||
const data = await createPutniNalog(radniNalogId, odabranoVoziloId);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
// KONZISTENTAN UKLOP (Opcija B):
|
|
||||||
// Spremamo poruku u formatu koji Toast.astro već automatski prepoznaje i čita nakon reloada
|
|
||||||
sessionStorage.setItem(
|
|
||||||
'pending_toast',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'success',
|
|
||||||
message: 'Putni nalog uspješno generiran!'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Instantno osvježavanje stranice - SSR iscrtava novu povezanu zelenu karticu
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
// Ako je krah, handleResponse unutar api.js je već okinuo privremeni crveni showToast
|
|
||||||
ponistiLoading();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Greška unutar klijentske skripte logistike:", err);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.showToast?.("Komunikacija s poslužiteljem nije uspjela.", "error");
|
|
||||||
}
|
|
||||||
ponistiLoading();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function ponistiLoading() {
|
|
||||||
gumb.disabled = false;
|
|
||||||
select.disabled = false;
|
|
||||||
if (btnText) btnText.textContent = 'Generiraj Putni Nalog';
|
|
||||||
if (btnIcon) btnIcon.className = 'fa-solid fa-file-shield';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pokretanje skripte ovisno o Astro životnom ciklusu stranice
|
|
||||||
initLogistika();
|
|
||||||
document.addEventListener('astro:page-load', initLogistika);
|
|
||||||
</script>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/operativa/radni-nalozi/index.astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import Button from "../../../components/Button.astro";
|
|
||||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
|
||||||
import RadniNalogLista from "../../../components/RadniNalogLista.astro";
|
|
||||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
|
||||||
import NaslovList from "../../../components/NaslovList.astro";
|
|
||||||
|
|
||||||
// 1. DOHVAT PODATAKA
|
|
||||||
import { fetchRadniNalozi, fetchCurrentUser } from "../../../lib/api";
|
|
||||||
import { formatStatus, getStatusColorClass } from "../../../utils/ui";
|
|
||||||
import site from "../../../data/site.json";
|
|
||||||
|
|
||||||
// Paralelno dohvaćamo naloge i podatke o trenutnom korisniku
|
|
||||||
const klijentId = Astro.url.searchParams.get('klijent');
|
|
||||||
const statusParam = Astro.url.searchParams.get('status');
|
|
||||||
|
|
||||||
const [nalozi, user] = await Promise.all([
|
|
||||||
fetchRadniNalozi({ klijent: klijentId, status: statusParam }),
|
|
||||||
fetchCurrentUser()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 2. LOGIKA ZA HEADER I BROJAČE
|
|
||||||
const uRadu = nalozi.filter(n => n.status === 'u_radu').length;
|
|
||||||
const planirano = nalozi.filter(n => n.status === 'planirano').length;
|
|
||||||
const siteWelcomeHeader = site.navigation.find(({name}) => name === "Kalendar");
|
|
||||||
|
|
||||||
// Podaci za WelcomeHeader
|
|
||||||
const imeKorisnika = user?.first_name || "Serviser";
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Radni nalozi | Arhiva">
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<!-- WELCOME HEADER -->
|
|
||||||
<WelcomeHeader />
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-6">
|
|
||||||
|
|
||||||
<!-- 3. LISTA NALOGA -->
|
|
||||||
<!-- Postavljamo prikaziNaslov={false} jer smo naslov već ispisali iznad preko NaslovList -->
|
|
||||||
<RadniNalogLista
|
|
||||||
limit={7}
|
|
||||||
statusFilter={statusParam as any}
|
|
||||||
prikaziNaslov={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DESNI PANEL - AKCIJE -->
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<AkcijePanel tip="dashboard" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { initFilters } from "../../../scripts/filters";
|
|
||||||
initFilters();
|
|
||||||
document.addEventListener('astro:after-swap', initFilters);
|
|
||||||
</script>
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/operativa/radni-nalozi/novi.astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import Button from "../../../components/Button.astro";
|
|
||||||
import { fetchKupciData, fetchStrojeviData } from "../../../lib/api";
|
|
||||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
|
||||||
|
|
||||||
// 1. DOHVAT PODATAKA NA SERVERU
|
|
||||||
const { kupci } = await fetchKupciData();
|
|
||||||
const { strojevi } = await fetchStrojeviData();
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Novi Radni Nalog">
|
|
||||||
<div class="max-w-3xl mx-auto space-y-8 px-2 py-10">
|
|
||||||
|
|
||||||
<WelcomeHeader />
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6 border-b border-gray-100 dark:border-gray-700 pb-6 px-2">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Broj Naloga</label>
|
|
||||||
<div id="broj-naloga-display" class="w-full bg-gray-100 dark:bg-gray-900/50 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-2xl p-5 font-black text-lg text-gray-400 flex items-center gap-3">
|
|
||||||
<i class="fa-solid fa-barcode opacity-30"></i>
|
|
||||||
<span id="broj-text">AUTO-GENERIRANO</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-[9px] text-gray-400 ml-1 italic">* Broj će biti dodijeljen nakon spremanja</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Datum Kreiranja</label>
|
|
||||||
<div class="w-full bg-gray-50 dark:bg-gray-900 border-2 border-transparent rounded-2xl p-5 font-bold text-lg dark:text-white flex items-center gap-3">
|
|
||||||
<i class="fa-solid fa-calendar-day text-indigo-600"></i>
|
|
||||||
{new Date().toLocaleDateString('hr-HR')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="novi-nalog-form" class="space-y-6">
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-xl space-y-6">
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Odabir Klijenta *</label>
|
|
||||||
<select name="klijent" required class="w-full bg-gray-50 dark:bg-gray-900 border-2 border-transparent focus:border-indigo-600 rounded-2xl p-5 font-bold text-lg appearance-none cursor-pointer transition-colors dark:text-white">
|
|
||||||
<option value="">-- Odaberi klijenta --</option>
|
|
||||||
{kupci.map(k => <option value={k.id}>{k.naziv}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Odabir Stroja *</label>
|
|
||||||
<div class="custom-select-container relative w-full">
|
|
||||||
<div id="stroj-trigger" class="w-full bg-gray-50 dark:bg-gray-900 border-2 border-transparent p-5 font-bold text-lg rounded-2xl cursor-pointer flex justify-between items-center dark:text-white">
|
|
||||||
<span id="selected-stroj-label">-- Prvo odaberi klijenta --</span>
|
|
||||||
<i class="fa-solid fa-chevron-down text-xs"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="hidden" name="stroj" id="real-stroj-input" required />
|
|
||||||
|
|
||||||
<div id="stroj-options" class="hidden absolute z-50 w-full mt-2 bg-gray-900 border border-gray-700 rounded-2xl shadow-2xl overflow-hidden max-h-60 overflow-y-auto">
|
|
||||||
{strojevi.map(s => (
|
|
||||||
<div
|
|
||||||
class="stroj-opt p-4 hover:bg-indigo-600 cursor-pointer transition-colors text-white font-bold italic border-b border-gray-800 last:border-0 hidden"
|
|
||||||
data-value={s.id}
|
|
||||||
data-vlasnik={s.vlasnik}
|
|
||||||
>
|
|
||||||
{s.naziv} ({s.registracija || s.serijski_broj})
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 p-8 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-xl space-y-6">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Opis problema / Radova *</label>
|
|
||||||
<textarea name="opis_kvara" required rows="4" class="w-full bg-gray-50 dark:bg-gray-900 border-none rounded-3xl p-6 font-medium italic text-lg focus:ring-2 focus:ring-indigo-600 transition-all dark:text-white" placeholder="Što treba napraviti?"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Trenutni Radni Sati</label>
|
|
||||||
<input type="number" name="radni_sati" step="0.1" class="w-full bg-gray-50 dark:bg-gray-900 border-none rounded-2xl p-5 font-black text-2xl text-indigo-600 focus:ring-2 focus:ring-indigo-600 transition-all" placeholder="0.0">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic block">Prioritet zahvata</label>
|
|
||||||
<div class="flex gap-3 mt-2">
|
|
||||||
<input type="checkbox" name="hitno" value="true" id="hitno" class="hidden peer">
|
|
||||||
<label for="hitno" class="flex-1 text-center py-4 bg-gray-50 dark:bg-gray-900 rounded-2xl text-[10px] font-black uppercase peer-checked:bg-red-600 peer-checked:text-white cursor-pointer transition-all border-2 border-transparent peer-checked:border-red-700 dark:text-gray-400">
|
|
||||||
<i class="fa-solid fa-triangle-exclamation mr-2"></i> Hitno
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-indigo-600 p-8 rounded-[3rem] shadow-xl text-white group relative overflow-hidden">
|
|
||||||
<i class="fa-solid fa-camera absolute -right-4 -top-4 text-7xl opacity-10 group-hover:rotate-12 transition-transform duration-500"></i>
|
|
||||||
<label class="text-[10px] font-black uppercase tracking-[0.2em] mb-4 block opacity-70 italic">Slike kvara/terena (opcionalno)</label>
|
|
||||||
<input type="file" name="slike" multiple accept="image/*" class="w-full text-xs file:mr-4 file:py-3 file:px-6 file:rounded-full file:border-0 file:text-[10px] file:font-black file:uppercase file:bg-white file:text-indigo-600 hover:file:bg-indigo-50 cursor-pointer transition-all" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
id="submit-btn"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full py-8 text-xl shadow-2xl shadow-indigo-500/20"
|
|
||||||
>
|
|
||||||
Kreiraj i pokreni nalog <i class="fa-solid fa-bolt ml-2"></i>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { createNalog, fetchSljedeciBrojNaloga } from "../../../lib/api";
|
|
||||||
|
|
||||||
const form = document.getElementById('novi-nalog-form') as HTMLFormElement;
|
|
||||||
const klijentSelect = form.querySelector('select[name="klijent"]') as HTMLSelectElement;
|
|
||||||
const trigger = document.getElementById('stroj-trigger');
|
|
||||||
const optionsList = document.getElementById('stroj-options');
|
|
||||||
const label = document.getElementById('selected-stroj-label');
|
|
||||||
const realInput = document.getElementById('real-stroj-input') as HTMLInputElement;
|
|
||||||
const strojOptions = document.querySelectorAll('.stroj-opt');
|
|
||||||
|
|
||||||
// Inicijalizacija broja naloga na klijentu
|
|
||||||
async function initBrojNaloga() {
|
|
||||||
const displayElement = document.getElementById('broj-text');
|
|
||||||
if (displayElement) {
|
|
||||||
const data = await fetchSljedeciBrojNaloga();
|
|
||||||
displayElement.textContent = data.broj_naloga;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initBrojNaloga();
|
|
||||||
|
|
||||||
// 1. DROPDOWN LOGIKA
|
|
||||||
trigger?.addEventListener('click', () => {
|
|
||||||
optionsList?.classList.toggle('hidden');
|
|
||||||
trigger.classList.toggle('border-indigo-600');
|
|
||||||
});
|
|
||||||
|
|
||||||
strojOptions.forEach(opt => {
|
|
||||||
opt.addEventListener('click', () => {
|
|
||||||
let val = opt.getAttribute('data-value');
|
|
||||||
if (val) val = val.replace(/['"]+/g, '').trim();
|
|
||||||
|
|
||||||
if (label && realInput && optionsList) {
|
|
||||||
label.textContent = opt.textContent;
|
|
||||||
realInput.value = val || "";
|
|
||||||
optionsList.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. FILTRIRANJE STROJEVA
|
|
||||||
klijentSelect?.addEventListener('change', (e) => {
|
|
||||||
const klijentId = (e.target as HTMLSelectElement).value;
|
|
||||||
if (label && realInput) {
|
|
||||||
label.textContent = klijentId ? "-- Odaberi stroj --" : "-- Prvo odaberi klijenta --";
|
|
||||||
realInput.value = "";
|
|
||||||
}
|
|
||||||
strojOptions.forEach(item => {
|
|
||||||
const vlasnikId = item.getAttribute('data-vlasnik');
|
|
||||||
if (vlasnikId == klijentId) item.classList.remove('hidden');
|
|
||||||
else item.classList.add('hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. SLANJE NA API
|
|
||||||
form.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const gumb = document.getElementById('submit-btn') as HTMLButtonElement;
|
|
||||||
if (!gumb) return;
|
|
||||||
|
|
||||||
gumb.setAttribute('disabled', 'true');
|
|
||||||
const originalContent = gumb.innerHTML;
|
|
||||||
gumb.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Sinkronizacija...';
|
|
||||||
|
|
||||||
const formData = new FormData(form);
|
|
||||||
|
|
||||||
// Brišemo broj_naloga kako bi backend sam aktivirao save() sekvencu
|
|
||||||
formData.delete('broj_naloga');
|
|
||||||
|
|
||||||
const userString = localStorage.getItem('user_info');
|
|
||||||
if (userString) {
|
|
||||||
const user = JSON.parse(userString);
|
|
||||||
formData.append('izvrsitelj', user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const nalog = await createNalog(formData);
|
|
||||||
|
|
||||||
if (nalog && nalog.id) {
|
|
||||||
// Budući da views.py sada radi refresh_from_db(), nalog.broj_naloga je ovdje 100% čisti 'RN-2026-XXXX'
|
|
||||||
sessionStorage.setItem(
|
|
||||||
'pending_toast',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'success',
|
|
||||||
message: `Radni nalog ${nalog.broj_naloga} uspješno kreiran!`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
window.location.href = `/operativa/radni-nalozi/${nalog.id}`;
|
|
||||||
} else {
|
|
||||||
ponistiLoading(gumb, originalContent);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Kritičan krah pri izradi naloga:", err);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.showToast?.("Komunikacija s poslužiteljem nije uspjela.", "error");
|
|
||||||
}
|
|
||||||
ponistiLoading(gumb, originalContent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function ponistiLoading(gumb: HTMLElement, originalContent: string) {
|
|
||||||
gumb.removeAttribute('disabled');
|
|
||||||
gumb.innerHTML = originalContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (trigger && optionsList && !trigger.contains(e.target as Node) && !optionsList.contains(e.target as Node)) {
|
|
||||||
optionsList.classList.add('hidden');
|
|
||||||
trigger.classList.remove('border-indigo-600');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/operativa/serviseri/[id].astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
|
||||||
import RadniNalogLista from "../../../components/RadniNalogLista.astro";
|
|
||||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
|
||||||
|
|
||||||
// 🚀 Hvatamo ID servisera iz same putanje URL-a
|
|
||||||
const { id } = Astro.params;
|
|
||||||
|
|
||||||
// 🎯 POPRAVAK: Uklonjen serverski fetchCurrentUser i redirect jer se token nalazi na klijentu!
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Servisni Terminal | Operativa">
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<WelcomeHeader />
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
|
||||||
|
|
||||||
<div class="md:col-span-7 space-y-6">
|
|
||||||
|
|
||||||
{/* Univerzalna CSR lista koja sama povlači naloge preko api.js ovisno o ulogi */}
|
|
||||||
<RadniNalogLista
|
|
||||||
limit={10}
|
|
||||||
naslov="Dodijeljeni zadaci servisera"
|
|
||||||
prikaziNaslov={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<AkcijePanel tip="dashboard" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { initFilters } from "../../../scripts/filters.js";
|
|
||||||
|
|
||||||
// 🎯 POPRAVAK: Potpuno izbačen provjeriPristupStranici.
|
|
||||||
// Ako korisnik nema pravo gledati ovaj profil, Django backend će presresti zahtjev unutar RadniNalogLista!
|
|
||||||
const pokreniSveKlijentskeSkripte = () => {
|
|
||||||
initFilters();
|
|
||||||
};
|
|
||||||
|
|
||||||
pokreniSveKlijentskeSkripte();
|
|
||||||
document.addEventListener('astro:after-swap', pokreniSveKlijentskeSkripte);
|
|
||||||
</script>
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
---
|
|
||||||
// src/pages/operativa/serviseri/index.astro
|
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
|
||||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
|
||||||
import RadniNalogLista from "../../../components/RadniNalogLista.astro";
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="Evidencija Servisera | Operativa">
|
|
||||||
<div class="w-full space-y-10">
|
|
||||||
|
|
||||||
<WelcomeHeader />
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
|
||||||
|
|
||||||
<div id="direktorij-sekcija" class="hidden md:col-span-10 space-y-6">
|
|
||||||
<div class="bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 rounded-[2.5rem] p-6 sm:p-8 shadow-xl shadow-gray-100/40 dark:shadow-none">
|
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 pb-4 border-b border-gray-50 dark:border-gray-800">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight leading-none italic">
|
|
||||||
Registrirani <span class="text-blue-600">Serviseri</span>
|
|
||||||
</h2>
|
|
||||||
<p class="text-xs text-gray-400 mt-2 italic font-medium">Popis tehničara s aktivnim ovlastima za radne naloge</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 border border-blue-100/30 dark:border-blue-900/30 italic font-black uppercase tracking-widest text-[11px] px-4 py-2 rounded-2xl w-max">
|
|
||||||
Ukupno: <span id="serviseri-counter">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="serviseri-loading" class="py-12 flex flex-col items-center justify-center gap-3 text-gray-400 dark:text-gray-500 italic text-sm">
|
|
||||||
<i class="fa-solid fa-circle-notch animate-spin text-blue-600 text-2xl"></i>
|
|
||||||
Dohvaćanje operativnih podataka s poslužitelja...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="serviseri-grid" class="hidden grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="serviseri-empty" class="hidden py-12 text-center text-gray-400 dark:text-gray-500 italic text-sm">
|
|
||||||
<i class="fa-solid fa-user-slash text-2xl mb-2 text-gray-300 dark:text-gray-700 block"></i>
|
|
||||||
Nema registriranih servisera u bazi podataka flote.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="moji-nalozi-sekcija" class="hidden md:col-span-10 space-y-6">
|
|
||||||
<RadniNalogLista
|
|
||||||
limit={10}
|
|
||||||
prikaziNaslov={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { fetchCurrentUser, fetchServiseri } from "../../../lib/api";
|
|
||||||
|
|
||||||
async function ucitajStranicuServisera() {
|
|
||||||
const direktorijSekcija = document.getElementById('direktorij-sekcija');
|
|
||||||
const loadingEl = document.getElementById('serviseri-loading');
|
|
||||||
const gridEl = document.getElementById('serviseri-grid');
|
|
||||||
const emptyEl = document.getElementById('serviseri-empty');
|
|
||||||
const counterEl = document.getElementById('serviseri-counter');
|
|
||||||
const mojiNaloziSekcija = document.getElementById('moji-nalozi-sekcija');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await fetchCurrentUser();
|
|
||||||
const ulogaGore = user?.uloga ? String(user.uloga).toUpperCase().trim() : '';
|
|
||||||
|
|
||||||
// 1. Ako je običan serviser, prikaži sekciju s univerzalnom listom
|
|
||||||
if (ulogaGore === 'SERVISER') {
|
|
||||||
if (direktorijSekcija) direktorijSekcija.classList.add('hidden');
|
|
||||||
if (mojiNaloziSekcija) mojiNaloziSekcija.classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Ako je Admin / Voditelj, prikaži imenik tehničara
|
|
||||||
if (direktorijSekcija) direktorijSekcija.classList.remove('hidden');
|
|
||||||
if (mojiNaloziSekcija) mojiNaloziSekcija.classList.add('hidden');
|
|
||||||
|
|
||||||
const serviseri = await fetchServiseri();
|
|
||||||
if (loadingEl) loadingEl.classList.add('hidden');
|
|
||||||
|
|
||||||
if (!serviseri || serviseri.length === 0) {
|
|
||||||
if (emptyEl) emptyEl.classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counterEl) counterEl.textContent = serviseri.length.toString();
|
|
||||||
|
|
||||||
if (gridEl) {
|
|
||||||
gridEl.innerHTML = serviseri.map((s: any) => {
|
|
||||||
const inicijali = `${s.first_name?.[0] || ''}${s.last_name?.[0] || ''}`.toUpperCase() || 'ST';
|
|
||||||
const imePrezime = `${s.first_name || ''} ${s.last_name || ''}`.trim() || s.email;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="bg-gray-50/50 dark:bg-gray-800/30 border border-gray-100 dark:border-gray-800/60 rounded-[1.8rem] p-5 flex items-center justify-between gap-4 hover:border-blue-500/30 dark:hover:border-blue-500/20 hover:bg-white dark:hover:bg-gray-800/60 transition-all duration-300 group">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 font-black text-xs tracking-widest rounded-2xl flex items-center justify-center border border-blue-500/10 shadow-sm">
|
|
||||||
${inicijali}
|
|
||||||
</div>
|
|
||||||
<div class="text-left">
|
|
||||||
<h3 class="text-sm font-black text-gray-900 dark:text-white uppercase tracking-tight leading-none group-hover:text-blue-600 transition-colors italic">
|
|
||||||
${imePrezime}
|
|
||||||
</h3>
|
|
||||||
<p class="text-[10px] font-medium text-gray-400 mt-2 lowercase select-all">${s.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="/operativa/serviseri/${s.id}" class="w-10 h-10 flex items-center justify-center rounded-xl bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700/80 text-gray-400 group-hover:text-white group-hover:bg-blue-600 group-hover:border-blue-500 active:scale-90 shadow-sm transition-all cursor-pointer">
|
|
||||||
<i class="fa-solid fa-chevron-right text-xs group-hover:translate-x-0.5 transition-transform"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
gridEl.classList.remove('hidden');
|
|
||||||
gridEl.classList.add('grid');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Greška pri učitavanju stranice:", err);
|
|
||||||
if (loadingEl) loadingEl.classList.add('hidden');
|
|
||||||
|
|
||||||
// 🚀 TOAST OKIDAMO SAMO OVDJE NA GLAVNOJ STRANICI ZA IMENIK
|
|
||||||
// @ts-ignore
|
|
||||||
window.showToast?.("Sinkronizacija imenika flote neuspješna.", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ucitajStranicuServisera();
|
|
||||||
document.addEventListener('astro:after-swap', ucitajStranicuServisera);
|
|
||||||
</script>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
// // src/scripts/filters.js
|
|
||||||
|
|
||||||
// Upravlja sa item.style.display na osnovu aktivnog filtera i data-status atributa
|
|
||||||
// Također upravlja sa stilovima kartica (ring, border) i naslovom filtera
|
|
||||||
// export function initFilters() {
|
|
||||||
// const cards = document.querySelectorAll('.stat-card');
|
|
||||||
// const items = document.querySelectorAll('.nalog-item');
|
|
||||||
// const title = document.getElementById('filter-title');
|
|
||||||
// let activeFilter = null;
|
|
||||||
|
|
||||||
// cards.forEach(card => {
|
|
||||||
// card.addEventListener('click', () => {
|
|
||||||
// const filter = card.getAttribute('data-filter');
|
|
||||||
|
|
||||||
// if (activeFilter === filter) {
|
|
||||||
// activeFilter = null;
|
|
||||||
// items.forEach(item => item.style.display = 'flex');
|
|
||||||
// cards.forEach(c => c.classList.remove('ring-4', 'ring-blue-600/20', 'border-blue-600'));
|
|
||||||
// if (title) title.innerText = "Zadnje aktivnosti";
|
|
||||||
// } else {
|
|
||||||
// activeFilter = filter;
|
|
||||||
// cards.forEach(c => c.classList.remove('ring-4', 'ring-blue-600/20', 'border-blue-600'));
|
|
||||||
// card.classList.add('ring-4', 'ring-blue-600/20', 'border-blue-600');
|
|
||||||
|
|
||||||
// if (title) title.innerText = `Filtrirano: ${filter.replace('_', ' ')}`;
|
|
||||||
|
|
||||||
// items.forEach(item => {
|
|
||||||
// const status = item.getAttribute('data-status');
|
|
||||||
// item.style.display = (status === filter) ? 'flex' : 'none';
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// src/scripts/filters.js
|
|
||||||
|
|
||||||
export function initFilters() {
|
|
||||||
const cards = document.querySelectorAll('.stat-card');
|
|
||||||
|
|
||||||
// Dohvaćamo trenutni aktivni filter iz URL-a
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const currentActiveFilter = urlParams.get('status');
|
|
||||||
|
|
||||||
cards.forEach(card => {
|
|
||||||
const filterValue = card.getAttribute('data-filter');
|
|
||||||
|
|
||||||
// Dodaj vizualno aktivno stanje ako se URL podudara s gumbom
|
|
||||||
if (currentActiveFilter === filterValue) {
|
|
||||||
card.classList.add('ring-4', 'ring-blue-600/20', 'border-blue-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
card.addEventListener('click', () => {
|
|
||||||
const newParams = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
if (currentActiveFilter === filterValue) {
|
|
||||||
// Ako kliknemo na već aktivni filter, poništavamo ga
|
|
||||||
newParams.delete('status');
|
|
||||||
} else {
|
|
||||||
// Inače postavljamo novi filter
|
|
||||||
newParams.set('status', filterValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Osvježi stranicu s novim parametrom - to okida novi API request na serveru
|
|
||||||
window.location.search = newParams.toString();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/* src/styles/global.css */
|
|
||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
/* Eksplicitne putanje za Tailwind v4 skener na tvom LXC-u */
|
|
||||||
@source "../**/*.astro";
|
|
||||||
@source "../components/**/*.astro";
|
|
||||||
@source "../layouts/**/*.astro";
|
|
||||||
@source "../pages/**/*.astro";
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// src/utils/auth.js
|
|
||||||
import { fetchCurrentUser } from "../lib/api";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pomoćna funkcija za dohvat sigurne rute (Single Source of Truth)
|
|
||||||
*/
|
|
||||||
export function dohvatiRutuPoUlozi(uloga, userId = '') {
|
|
||||||
const ulogaGore = uloga ? String(uloga).toUpperCase().trim() : '';
|
|
||||||
if (ulogaGore === 'SERVISER') {
|
|
||||||
return userId ? `/operativa/serviseri/${userId}` : '/operativa/serviseri/';
|
|
||||||
}
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jednostavan klijentski guard koji samo provjerava valjanost sesije i ovlasti
|
|
||||||
* @param {string[]} dozvoljeneUloge - Niz uloga koje smiju vidjeti stranicu
|
|
||||||
*/
|
|
||||||
export async function provjeriPristupStranici(dozvoljeneUloge = []) {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
const token = localStorage.getItem('access_token');
|
|
||||||
|
|
||||||
// 1. Ako uopće nema tokena, vrati ga na prijavu
|
|
||||||
if (!token) {
|
|
||||||
if (window.location.pathname !== '/login') {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2. Dohvaćamo profil trenutnog korisnika s backenda
|
|
||||||
const user = await fetchCurrentUser();
|
|
||||||
|
|
||||||
// 3. Ako je token nevažeći/istekao, počisti local pohranu i izbaci ga van
|
|
||||||
if (!user || !user.uloga) {
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('refresh_token');
|
|
||||||
window.location.href = '/login';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const korisnikUlogaGore = String(user.uloga).toUpperCase().trim();
|
|
||||||
|
|
||||||
// 4. Provjera eksplicitnih ovlasti (Samo ako ih je stranica zatražila)
|
|
||||||
if (dozvoljeneUloge.length > 0) {
|
|
||||||
const dozvoljeneUlogeGore = dozvoljeneUloge.map(u => String(u).toUpperCase().trim());
|
|
||||||
|
|
||||||
// Ako korisnik nema ulogu koja je navedena u dozvoljenima za tu stranicu
|
|
||||||
if (!dozvoljeneUlogeGore.includes(korisnikUlogaGore)) {
|
|
||||||
// Saznaj kamo pripada s obzirom na ulogu i preusmjeri ga
|
|
||||||
window.location.href = dohvatiRutuPoUlozi(korisnikUlogaGore, user.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Kritičan krah u provjeri pristupa:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
// src/utils/ui.js
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vraća set Tailwind klasa za specifičnu temu
|
|
||||||
* @param {string} tema - 'indigo', 'blue', 'emerald', 'amber', 'red'
|
|
||||||
*/
|
|
||||||
export function getThemeClasses(tema = "indigo") {
|
|
||||||
const themes = {
|
|
||||||
indigo: {
|
|
||||||
bg: "bg-indigo-50",
|
|
||||||
bgDark: "dark:bg-indigo-900/20",
|
|
||||||
text: "text-indigo-600",
|
|
||||||
textDark: "dark:text-indigo-400",
|
|
||||||
border: "border-indigo-100",
|
|
||||||
borderDark: "dark:border-indigo-900/30",
|
|
||||||
shadow: "shadow-indigo-500/5",
|
|
||||||
hover: "hover:bg-indigo-50/30 dark:hover:bg-indigo-900/10",
|
|
||||||
icon: "group-hover:bg-indigo-600 group-hover:text-white"
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
bg: "bg-blue-50",
|
|
||||||
bgDark: "dark:bg-blue-900/20",
|
|
||||||
text: "text-blue-600",
|
|
||||||
textDark: "dark:text-blue-400",
|
|
||||||
border: "border-blue-100",
|
|
||||||
borderDark: "dark:border-blue-900/30",
|
|
||||||
shadow: "shadow-blue-500/5",
|
|
||||||
hover: "hover:bg-blue-50/30 dark:hover:bg-blue-900/10",
|
|
||||||
icon: "group-hover:bg-blue-600 group-hover:text-white"
|
|
||||||
},
|
|
||||||
emerald: {
|
|
||||||
bg: "bg-emerald-50",
|
|
||||||
bgDark: "dark:bg-emerald-900/20",
|
|
||||||
text: "text-emerald-600",
|
|
||||||
textDark: "dark:text-emerald-400",
|
|
||||||
border: "border-emerald-100",
|
|
||||||
borderDark: "dark:border-emerald-900/30",
|
|
||||||
shadow: "shadow-emerald-500/5",
|
|
||||||
hover: "hover:bg-emerald-50/30 dark:hover:bg-emerald-900/10",
|
|
||||||
icon: "group-hover:bg-emerald-600 group-hover:text-white"
|
|
||||||
},
|
|
||||||
amber: {
|
|
||||||
bg: "bg-amber-50",
|
|
||||||
bgDark: "dark:bg-amber-900/20",
|
|
||||||
text: "text-amber-600",
|
|
||||||
textDark: "dark:text-amber-400",
|
|
||||||
border: "border-amber-100",
|
|
||||||
borderDark: "dark:border-amber-900/30",
|
|
||||||
shadow: "shadow-amber-500/5",
|
|
||||||
hover: "hover:bg-amber-50/30 dark:hover:bg-amber-900/10",
|
|
||||||
icon: "group-hover:bg-amber-600 group-hover:text-white"
|
|
||||||
},
|
|
||||||
red: {
|
|
||||||
bg: "bg-red-50",
|
|
||||||
bgDark: "dark:bg-red-900/20",
|
|
||||||
text: "text-red-600",
|
|
||||||
textDark: "dark:text-red-400",
|
|
||||||
border: "border-red-100",
|
|
||||||
borderDark: "dark:border-red-900/30",
|
|
||||||
shadow: "shadow-red-500/5",
|
|
||||||
hover: "hover:bg-red-50/30 dark:hover:bg-red-900/10",
|
|
||||||
icon: "group-hover:bg-red-600 group-hover:text-white"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return themes[tema] || themes.indigo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Određuje boju kružića i animaciju na temelju statusa (Vozila + Radni Nalozi)
|
|
||||||
*/
|
|
||||||
export function getStatusColorClass(status) {
|
|
||||||
// 1. Provjera postoji li status. Ako ne, vrati sivu boju.
|
|
||||||
if (!status) return 'bg-gray-400';
|
|
||||||
|
|
||||||
// 2. Siguran poziv toLowerCase()
|
|
||||||
const s = String(status).toLowerCase();
|
|
||||||
|
|
||||||
switch (s) {
|
|
||||||
case 'u_radu': return 'bg-yellow-500 animate-pulse';
|
|
||||||
case 'planirano':
|
|
||||||
case 'aktivan': return 'bg-blue-500';
|
|
||||||
case 'zavrseno': return 'bg-emerald-500';
|
|
||||||
case 'naplaceno': return 'bg-gray-500 opacity-50';
|
|
||||||
case 'servis':
|
|
||||||
case 'hitno': return 'bg-red-500 animate-pulse';
|
|
||||||
default: return 'bg-gray-400';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pretvara tehnički status u ljudima čitljiv format
|
|
||||||
* Dodana provjera da spriječi toLowerCase error
|
|
||||||
*/
|
|
||||||
export function formatStatus(status) {
|
|
||||||
// Provjera ako je status prazan
|
|
||||||
if (!status) return 'Nepoznato';
|
|
||||||
|
|
||||||
const labels = {
|
|
||||||
'planirano': 'Planirano',
|
|
||||||
'u_radu': 'U radu',
|
|
||||||
'zavrseno': 'Završeno',
|
|
||||||
'naplaceno': 'Naplaćeno',
|
|
||||||
'aktivan': 'Aktivan',
|
|
||||||
'servis': 'Na servisu',
|
|
||||||
'hitno': 'HITNO'
|
|
||||||
};
|
|
||||||
|
|
||||||
const s = String(status).toLowerCase();
|
|
||||||
return labels[s] || s;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
'./src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}',
|
|
||||||
'./node_modules/flowbite/**/*.js' // Ovo omogućuje Flowbite stilove
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
require('flowbite/plugin') // Registrira Flowbite kao plugin
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "astro/tsconfigs/strict",
|
|
||||||
"include": [".astro/types.d.ts", "**/*"],
|
|
||||||
"exclude": ["dist"]
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
# build output
|
# build output
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# generated types
|
# generated types
|
||||||
.astro/
|
.astro/
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Astro Starter Kit: Basics
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm create astro@latest -- --template basics
|
npm create astro@latest -- --template minimal
|
||||||
```
|
```
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
@@ -13,20 +13,17 @@ Inside of your Astro project, you'll see the following folders and files:
|
|||||||
```text
|
```text
|
||||||
/
|
/
|
||||||
├── public/
|
├── public/
|
||||||
│ └── favicon.svg
|
├── src/
|
||||||
├── src
|
│ └── pages/
|
||||||
│ ├── assets
|
│ └── index.astro
|
||||||
│ │ └── astro.svg
|
|
||||||
│ ├── components
|
|
||||||
│ │ └── Welcome.astro
|
|
||||||
│ ├── layouts
|
|
||||||
│ │ └── Layout.astro
|
|
||||||
│ └── pages
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
## 🧞 Commands
|
## 🧞 Commands
|
||||||
|
|
||||||
38
002.FRONTEND/astro.config.mjs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// @ts-check
|
||||||
|
import node from '@astrojs/node';
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import preact from '@astrojs/preact';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'server',
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
|
vite: {
|
||||||
|
build: {
|
||||||
|
// Smanji intenzitet optimizacije tijekom dev-a
|
||||||
|
minify: false,
|
||||||
|
cssMinify: false,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
allowedHosts: ['.mitteworkspace.cloud'],
|
||||||
|
watch: {
|
||||||
|
ignored: ['**/node_modules/**', '**/dist/**'],
|
||||||
|
usePolling: true,
|
||||||
|
interval: 1000
|
||||||
|
},
|
||||||
|
hmr: {
|
||||||
|
protocol: 'wss', // ili 'wss' ako koristiš HTTPS
|
||||||
|
clientPort: 443 // ili odgovarajući port ako nije 443
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
integrations: [preact({ devtools: true })]
|
||||||
|
});
|
||||||
21
002.FRONTEND/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "operativa-frontend",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev --host",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^10.1.1",
|
||||||
|
"@astrojs/preact": "^5.1.3",
|
||||||
|
"@nanostores/preact": "^1.1.0",
|
||||||
|
"astro": "^6.3.7",
|
||||||
|
"nanostores": "^1.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
002.FRONTEND/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 655 B |
9
002.FRONTEND/public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
22
002.FRONTEND/src/components/Button.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// src/components/Button.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
export default function Button({ children, onClick, loading, disabled, variant = 'primary', label, type = 'button' }) {
|
||||||
|
const isBlocked = loading || disabled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
disabled={isBlocked}
|
||||||
|
onClick={onClick}
|
||||||
|
class={`btn btn-${variant} ${isBlocked ? 'opacity-50' : 'opacity-100'}`}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
cursor: isBlocked ? 'not-allowed' : 'pointer',
|
||||||
|
transition: 'opacity 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Spremanje...' : (children || label)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
002.FRONTEND/src/components/FleetStrojeviLista.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// src/components/fleet/FleetStrojeviLista.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useEffect } from 'preact/hooks';
|
||||||
|
import { useStore } from '@nanostores/preact';
|
||||||
|
import { strojevi, setStrojevi } from '../stores/fleetStore';
|
||||||
|
|
||||||
|
export default function FleetStrojeviLista({ initialData = [] }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData.length > 0) setStrojevi(initialData);
|
||||||
|
}, [initialData]);
|
||||||
|
|
||||||
|
const list = useStore(strojevi);
|
||||||
|
|
||||||
|
if (!list.length) return <div>Učitavanje strojeva...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{list.map(s => (
|
||||||
|
<div key={s.id} class="p-6 border border-gray-200 rounded-2xl bg-white shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex justify-between items-start mb-4">
|
||||||
|
<h3 class="text-xl font-black text-gray-900">{s.naziv}</h3>
|
||||||
|
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-bold rounded-full uppercase">
|
||||||
|
{s.tip_human_readable}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="text-sm text-gray-600 space-y-1 mb-4">
|
||||||
|
<li><strong>Marka/Model:</strong> {s.marka} {s.model_stroja}</li>
|
||||||
|
<li><strong>Serijski br:</strong> {s.serijski_broj}</li>
|
||||||
|
<li><strong>Vlasnik:</strong> {s.vlasnik_naziv}</li>
|
||||||
|
<li><strong>Radni sati:</strong> {s.radni_sati} h</li>
|
||||||
|
{s.registracija && <li><strong>Reg:</strong> {s.registracija}</li>}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`/fleet/strojevi/${s.id}`}
|
||||||
|
class="block text-center w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-700 transition"
|
||||||
|
>
|
||||||
|
Pregled detalja
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
002.FRONTEND/src/components/FleetVozilaLista.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// src/components/fleet/FleetVozilaLista.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useEffect } from 'preact/hooks';
|
||||||
|
import { useStore } from '@nanostores/preact';
|
||||||
|
import { vozila, setVozila } from '../stores/fleetStore';
|
||||||
|
|
||||||
|
export default function FleetVozilaLista({ initialData = [], baseUrl }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData.length > 0) setVozila(initialData);
|
||||||
|
}, [initialData]);
|
||||||
|
|
||||||
|
const list = useStore(vozila);
|
||||||
|
|
||||||
|
if (!list.length) return <div>Učitavanje voznog parka...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{list.map(v => (
|
||||||
|
<div key={v.id} class="p-6 border border-gray-200 rounded-2xl bg-white shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex justify-between items-start mb-4">
|
||||||
|
<h3 class="text-xl font-black text-gray-900">{v.naziv}</h3>
|
||||||
|
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-bold rounded-full uppercase">
|
||||||
|
{v.status_prikaz}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="text-sm text-gray-600 space-y-2 mb-4">
|
||||||
|
<li><strong>Registracija:</strong> {v.registracija}</li>
|
||||||
|
<li><strong>Trenutna km:</strong> {v.trenutni_kilometri.toLocaleString()} km</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`${baseUrl}/${v.id}`}
|
||||||
|
class="block text-center w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-700 transition"
|
||||||
|
>
|
||||||
|
Pregled detalja
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
002.FRONTEND/src/components/Gallery.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// src/components/Gallery.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
export default function Gallery({ images = [] }) {
|
||||||
|
// Uvijek vraćamo isti 'div' wrapper da spriječimo hydration mismatch
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
{!images || images.length === 0 ? (
|
||||||
|
<p class="text-sm text-gray-500 italic">Nema fotografija za ovaj nalog.</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: '1rem' }}>
|
||||||
|
{images.map((item) => (
|
||||||
|
<a
|
||||||
|
href={item.slika}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
key={item.id}
|
||||||
|
style={{ display: 'block', borderRadius: '8px', overflow: 'hidden', border: '1px solid #333' }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={item.slika}
|
||||||
|
alt={item.opis || 'Foto dokumentacija'}
|
||||||
|
style={{ width: '100%', height: '150px', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
002.FRONTEND/src/components/Login.jsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { login } from '../utils/users'; // Uvoz iz novog modula
|
||||||
|
import Button from './Button.jsx'; // Koristimo tvoju Button komponentu
|
||||||
|
import { navigate } from 'astro:transitions/client';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const result = await login(email, password);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Navigiraj bez "hard reloada"
|
||||||
|
navigate('/');
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="max-w-md mx-auto mt-20 p-8 bg-gray-800 rounded-2xl shadow-xl text-white">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Prijava u ServisLog</h1>
|
||||||
|
<form onSubmit={handleLogin} class="space-y-4">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
class="w-full p-3 rounded bg-gray-700 border border-gray-600"
|
||||||
|
value={email}
|
||||||
|
onInput={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Lozinka"
|
||||||
|
class="w-full p-3 rounded bg-gray-700 border border-gray-600"
|
||||||
|
value={password}
|
||||||
|
onInput={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={loading}
|
||||||
|
variant="primary"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Prijava
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
002.FRONTEND/src/components/LogoutButton.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { logoutUser } from '../utils/users'; // Import iz utils-a
|
||||||
|
|
||||||
|
export default function LogoutButton() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={logoutUser}
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors font-bold uppercase text-[10px] tracking-widest italic"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-right-from-bracket"></i>
|
||||||
|
Odjava
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
002.FRONTEND/src/components/RadniNalogDisplay.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// src/components/RadniNalogDisplay.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
|
import { useStore } from '@nanostores/preact';
|
||||||
|
import { $radniNalogDetalji, setNalogDetalji } from '../stores/operativaStore';
|
||||||
|
import RadniNalogEditForm from './edit/RadniNalogEditForm';
|
||||||
|
import Gallery from './Gallery';
|
||||||
|
import RadniNalogLista from './RadniNalogLista';
|
||||||
|
|
||||||
|
const VoziloInfo = ({ vozilo }) => (
|
||||||
|
<section>
|
||||||
|
<h3>Servisno vozilo</h3>
|
||||||
|
{vozilo ? (
|
||||||
|
<ul>
|
||||||
|
<li><strong>Vozilo:</strong> {vozilo.naziv}</li>
|
||||||
|
<li><strong>Registracija:</strong> {vozilo.registracija}</li>
|
||||||
|
<li><strong>Trenutna kilometraža:</strong> {vozilo.trenutni_kilometri} km</li>
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>Nema dodijeljenog servisnog vozila.</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
const OsnovniPodaciPutnogNaloga = ({ nalog }) => (
|
||||||
|
<section>
|
||||||
|
<h3>Osnovni podaci</h3>
|
||||||
|
<p>Kupac: {nalog.klijent?.naziv}</p>
|
||||||
|
<p>Opis: {nalog.opis_kvara}</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function RadniNalogDisplay({ initialNalog }) {
|
||||||
|
// 1. Sinkroniziraj store s prop-om
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialNalog) {
|
||||||
|
setNalogDetalji(initialNalog);
|
||||||
|
}
|
||||||
|
}, [initialNalog]);
|
||||||
|
|
||||||
|
const nalog = useStore($radniNalogDetalji);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
if (!nalog) return <div>Učitavanje...</div>;
|
||||||
|
|
||||||
|
const { vozilo } = nalog;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
<h1>Radni nalog: {nalog.broj_naloga}</h1>
|
||||||
|
<p>Status: {nalog.status_display}</p>
|
||||||
|
<button onClick={() => setIsEditing(true)}>Uredi</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<VoziloInfo vozilo={vozilo} />
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<RadniNalogEditForm
|
||||||
|
nalog={nalog}
|
||||||
|
onSave={() => setIsEditing(false)}
|
||||||
|
onCancel={() => setIsEditing(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OsnovniPodaciPutnogNaloga nalog={nalog} />
|
||||||
|
|
||||||
|
<Gallery images={nalog.slike || []} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
002.FRONTEND/src/components/RadniNalogLista.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// src/components/RadniNalogLista.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useEffect } from 'preact/hooks';
|
||||||
|
import { useStore } from '@nanostores/preact';
|
||||||
|
import { radniNalozi, setRadniNalozi } from '../stores/operativaStore';
|
||||||
|
|
||||||
|
export default function RadniNalogLista({ nalozi = [], limit = 5 }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (nalozi.length > 0) setRadniNalozi(nalozi);
|
||||||
|
}, [nalozi]);
|
||||||
|
|
||||||
|
const podaciIzStora = useStore(radniNalozi);
|
||||||
|
|
||||||
|
if (!podaciIzStora.length) return <div>Učitavanje...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{podaciIzStora.slice(0, limit).map(n => (
|
||||||
|
// KLJUČNA PROMJENA: li je ovdje glavni element, a ne div
|
||||||
|
<li key={n.id} style={{ marginBottom: '10px' }}>
|
||||||
|
{n.broj_naloga} -
|
||||||
|
<a href={`/operativa/radni-nalozi/${n.id}`} style={{ marginLeft: '10px' }}>
|
||||||
|
Pregled
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
002.FRONTEND/src/components/ToastContainer.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useStore } from '@nanostores/preact';
|
||||||
|
import { $toasts } from '../stores/toastStore';
|
||||||
|
|
||||||
|
export default function ToastContainer() {
|
||||||
|
const toasts = useStore($toasts);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="fixed top-5 right-5 z-[9999] flex flex-col gap-2">
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
class={`p-4 rounded-xl shadow-lg text-white font-bold transition-all ${
|
||||||
|
toast.type === 'error' ? 'bg-red-600' : 'bg-blue-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
002.FRONTEND/src/components/auth/AuthStatus.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// src/components/auth/AuthStatus.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
|
import { logoutUser } from '../../utils/users';
|
||||||
|
import Button from '../Button.jsx';
|
||||||
|
|
||||||
|
export default function AuthStatus() {
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Provjeri localStorage pri učitavanju
|
||||||
|
const checkAuth = () => {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
setIsLoggedIn(!!token);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
// Opcionalno: dodaj event listener za promjene u localStorage
|
||||||
|
window.addEventListener('storage', checkAuth);
|
||||||
|
return () => window.removeEventListener('storage', checkAuth);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isLoggedIn ? (
|
||||||
|
<div onClick={logoutUser}>
|
||||||
|
<Button variant="danger" class="!py-2 !px-4 uppercase text-[10px] tracking-widest italic">
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<a href="/login">
|
||||||
|
<Button variant="primary" class="!py-2 !px-4 uppercase text-[10px] tracking-widest italic">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
002.FRONTEND/src/components/edit/EditNalogToggle.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// src/components/edit/EditNalogToggle.jsx
|
||||||
|
// Ovo postaje višak
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import RadniNalogEditForm from './RadniNalogEditForm';
|
||||||
|
import Button from '../Button';
|
||||||
|
|
||||||
|
export default function EditNalogToggle({ nalog }) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'inline-block' }}>
|
||||||
|
<Button
|
||||||
|
label="Uredi radni nalog"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<RadniNalogEditForm
|
||||||
|
nalog={nalog}
|
||||||
|
onSave={() => { setIsEditing(false); window.location.reload(); }}
|
||||||
|
onCancel={() => setIsEditing(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
002.FRONTEND/src/components/edit/ImageUpload.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { uploadSlikaRadniNalog } from '../../lib/api.js';
|
||||||
|
import { showToast } from '../../stores/toastStore';
|
||||||
|
|
||||||
|
export default function ImageUpload({ nalogId, onUploadSuccess }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFileChange = async (e) => {
|
||||||
|
const files = e.target.files; // Uzimamo sve odabrane datoteke
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Ključno: dodajemo sve datoteke pod istim ključem 'slike'
|
||||||
|
// Backend (Django) će ovo dohvatiti pomoću request.FILES.getlist('slike')
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
formData.append('slika', files[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append('radni_nalog', nalogId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadSlikaRadniNalog(formData);
|
||||||
|
showToast("Slike uspješno učitane", "success");
|
||||||
|
onUploadSuccess();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Greška pri uploadu:", err);
|
||||||
|
showToast("Greška pri učitavanju", "error");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
e.target.value = ''; // Resetiraj input nakon uploada
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="mt-4 border-t pt-4">
|
||||||
|
<label class="block text-[10px] font-black uppercase text-gray-500 mb-2 italic">
|
||||||
|
Dodaj fotografije (odaberi više)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple // OVO JE KLJUČNO
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
disabled={loading}
|
||||||
|
class="block w-full text-xs text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer"
|
||||||
|
/>
|
||||||
|
{loading && <p class="text-[10px] text-blue-600 mt-2 animate-pulse">Učitavanje u tijeku...</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
002.FRONTEND/src/components/edit/RadniNalogEditForm.jsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// src/components/edit/RadniNalogEditForm.jsx
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { patchRadniNalog } from '../../lib/api';
|
||||||
|
import { setNalogDetalji } from '../../stores/operativaStore';
|
||||||
|
import { showToast } from '../../stores/toastStore';
|
||||||
|
import Button from '../Button';
|
||||||
|
import ImageUpload from './ImageUpload';
|
||||||
|
|
||||||
|
export default function RadniNalogEditForm({ nalog, onSave, onCancel }) {
|
||||||
|
const [formData, setFormData] = useState({ opis_kvara: nalog.opis_kvara || '' });
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const updateNalog = async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const azuriraniNalog = await patchRadniNalog(nalog.id, data);
|
||||||
|
if (azuriraniNalog) {
|
||||||
|
setNalogDetalji({ ...nalog, ...azuriraniNalog });
|
||||||
|
showToast("Nalog uspješno ažuriran", "success");
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Greška:", error);
|
||||||
|
showToast("Došlo je do greške", "error");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logika za završavanje naloga
|
||||||
|
const handleZavrsi = () => {
|
||||||
|
if (confirm("Jeste li sigurni da želite označiti nalog kao ZAVRŠEN?")) {
|
||||||
|
updateNalog({ status: 'ZAVRSENO' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
updateNalog(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50">
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-8 rounded-3xl w-full max-w-lg shadow-2xl">
|
||||||
|
<h2 class="text-2xl font-black mb-6 uppercase">Uredi nalog #{nalog.broj_naloga}</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} class="space-y-4">
|
||||||
|
<textarea
|
||||||
|
class="w-full p-4 rounded-xl border bg-gray-50 dark:bg-gray-900"
|
||||||
|
value={formData.opis_kvara}
|
||||||
|
onInput={(e) => setFormData({ ...formData, opis_kvara: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageUpload nalogId={nalog.id} />
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 pt-4">
|
||||||
|
<Button label="Spremi izmjene" type="submit" loading={loading} />
|
||||||
|
|
||||||
|
{/* Gumb za završavanje - prikazuje se samo ako nalog već nije završen */}
|
||||||
|
{nalog.status !== 'ZAVRSENO' && (
|
||||||
|
<Button
|
||||||
|
label="Završi radni nalog"
|
||||||
|
variant="success"
|
||||||
|
onClick={handleZavrsi}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button label="Odustani" variant="secondary" onClick={onCancel} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
002.FRONTEND/src/components/edit/RadniNalogEditorStatus.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// src/components/edit/RadniNalogEditorStatus.jsx
|
||||||
|
// Ovo postaje višak
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { patchNalog } from '../../lib/api';
|
||||||
|
import Button from '../Button'; // Import nove komponente
|
||||||
|
|
||||||
|
export default function RadniNalogEditorStatus({ nalogId, pocetniStatus }) {
|
||||||
|
const [status, setStatus] = useState(pocetniStatus);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const azurirajStatus = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const rezultat = await patchNalog(nalogId, { status: 'ZAVRSENO' });
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (rezultat) {
|
||||||
|
setStatus('ZAVRSENO');
|
||||||
|
window.showToast?.("Status ažuriran", "success");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label={status === 'ZAVRSENO' ? 'Završen' : 'Završi radni nalog'}
|
||||||
|
onClick={azurirajStatus}
|
||||||
|
loading={loading}
|
||||||
|
disabled={status === 'ZAVRSENO'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
002.FRONTEND/src/components/novi/NoviRadniNalogForm.jsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// src/components/operativa/novi/NoviRadniNalogForm.jsx
|
||||||
|
import { h } from "preact";
|
||||||
|
import { useState, useEffect } from "preact/hooks";
|
||||||
|
import { navigate } from "astro:transitions/client";
|
||||||
|
import { createNalog, getPutniNalozi, getStrojeviData, fetchKupciData } from "../../lib/api";
|
||||||
|
import { showToast } from "../../stores/toastStore";
|
||||||
|
import Button from "../Button.jsx";
|
||||||
|
|
||||||
|
export default function NoviRadniNalogForm() {
|
||||||
|
const [putniNalogMode, setPutniNalogMode] = useState('none');
|
||||||
|
const [putniNalozi, setPutniNalozi] = useState([]);
|
||||||
|
const [selectedPutniId, setSelectedPutniId] = useState('');
|
||||||
|
|
||||||
|
const [klijenti, setKlijenti] = useState([]);
|
||||||
|
const [klijentId, setKlijentId] = useState('');
|
||||||
|
const [strojevi, setStrojevi] = useState([]);
|
||||||
|
const [selectedStrojId, setSelectedStrojId] = useState('');
|
||||||
|
|
||||||
|
const [isFetchingNaloge, setIsFetchingNaloge] = useState(false);
|
||||||
|
const [isFetchingStrojevi, setIsFetchingStrojevi] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Dohvati listu klijenata pri učitavanju forme
|
||||||
|
useEffect(() => {
|
||||||
|
// Pretpostavljam da imaš funkciju getKlijenti() u api.js
|
||||||
|
fetchKupciData().then(data => setKlijenti(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Dohvat Putnih naloga
|
||||||
|
useEffect(() => {
|
||||||
|
if (putniNalogMode === 'existing') {
|
||||||
|
setIsFetchingNaloge(true);
|
||||||
|
getPutniNalozi()
|
||||||
|
.then(data => setPutniNalozi(data))
|
||||||
|
.finally(() => setIsFetchingNaloge(false));
|
||||||
|
}
|
||||||
|
}, [putniNalogMode]);
|
||||||
|
|
||||||
|
// 2. Dohvat Strojeva ovisno o klijentu
|
||||||
|
useEffect(() => {
|
||||||
|
// Popravljeno: slušamo klijentId, a ne klijenti (koji je niz)
|
||||||
|
if (klijentId) {
|
||||||
|
setIsFetchingStrojevi(true);
|
||||||
|
getStrojeviData(klijentId)
|
||||||
|
.then(data => setStrojevi(data))
|
||||||
|
.finally(() => setIsFetchingStrojevi(false));
|
||||||
|
} else {
|
||||||
|
setStrojevi([]);
|
||||||
|
setSelectedStrojId('');
|
||||||
|
}
|
||||||
|
}, [klijentId]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
|
||||||
|
// Logika za povezivanje putnog naloga koju backend view sada očekuje
|
||||||
|
if (putniNalogMode === 'existing' && selectedPutniId) {
|
||||||
|
formData.append('putni_nalog', selectedPutniId);
|
||||||
|
} else if (putniNalogMode === 'new') {
|
||||||
|
formData.append('kreiraj_putni', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createNalog(formData);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
showToast("Radni nalog uspješno kreiran!", "success");
|
||||||
|
navigate('/operativa/radni-nalozi');
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} class="space-y-6">
|
||||||
|
{/* Ovdje idu tvoja polja za nalog (klijent, stroj, opis...) */}
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-300">Klijent:</label>
|
||||||
|
<select
|
||||||
|
name="klijent"
|
||||||
|
required
|
||||||
|
class="w-full p-3 bg-gray-700 rounded text-white"
|
||||||
|
onChange={(e) => setKlijentId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">-- Odaberite klijenta --</option>
|
||||||
|
|
||||||
|
{/* OVDJE PROVJERI: */}
|
||||||
|
{klijenti.map(k => (
|
||||||
|
<option key={k.id} value={k.id}>
|
||||||
|
{k.naziv}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-300">Stroj (ID):</label>
|
||||||
|
<select
|
||||||
|
name="stroj"
|
||||||
|
required
|
||||||
|
class="w-full p-3 bg-gray-700 rounded text-white"
|
||||||
|
onChange={(e) => setSelectedStrojId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">-- Odaberite stroj --</option>
|
||||||
|
{strojevi.map(stroj => (
|
||||||
|
<option key={stroj.id} value={stroj.id}>
|
||||||
|
{stroj.naziv}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-300">Opis kvara:</label>
|
||||||
|
<textarea name="opis_kvara" required class="w-full p-3 bg-gray-700 rounded"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 border-t pt-4">
|
||||||
|
<label class="block mb-2 font-bold text-gray-300">Putni nalog:</label>
|
||||||
|
<select
|
||||||
|
onChange={(e) => setPutniNalogMode(e.target.value)}
|
||||||
|
class="w-full p-3 rounded bg-gray-700 text-white border border-gray-600"
|
||||||
|
>
|
||||||
|
<option value="none">Bez putnog naloga</option>
|
||||||
|
<option value="new">Kreiraj novi prazni putni nalog</option>
|
||||||
|
<option value="existing">Poveži na postojeći</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{putniNalogMode === 'existing' && (
|
||||||
|
<select
|
||||||
|
value={selectedPutniId}
|
||||||
|
onChange={(e) => setSelectedPutniId(e.target.value)}
|
||||||
|
disabled={isFetchingNaloge}
|
||||||
|
class={`w-full p-3 mt-3 rounded bg-gray-700 text-white border border-gray-500 ${isFetchingNaloge ? 'opacity-50 cursor-wait' : ''}`}
|
||||||
|
>
|
||||||
|
{isFetchingNaloge ? (
|
||||||
|
<option>Učitavanje putnih naloga...</option>
|
||||||
|
) : (
|
||||||
|
<option value="">-- Odaberite postojeći putni nalog --</option>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isFetchingNaloge && putniNalozi.map(pn => (
|
||||||
|
<option key={pn.id} value={pn.id}>
|
||||||
|
{pn.broj_naloga} | {pn.status} | {pn.mjesto_odredista}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" loading={loading} variant="primary" className="w-full">
|
||||||
|
Kreiraj Radni Nalog
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||