CORS error cleaned

This commit is contained in:
2026-05-22 15:50:35 +00:00
parent bafec754d1
commit 96c7ba3ec1
11 changed files with 372 additions and 241 deletions

10
001.BACKEND/.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
venv/
.venv/
__pycache__/
*.pyc
.git/
.gitignore
db.sqlite3
.env
staticfiles/
media/

50
001.BACKEND/Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
M python:3.12-slim AS builder
# Sprječava Python da zapisuje .pyc datoteke na disk
ENV PYTHONDONTWRITEBYTECODE=1
# Sprječava Python da sprema ispis u međumemoriju (odmah vidiš logove)
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Instalacija sistemskih ovisnosti potrebnih za kompajliranje pojedinih Python paketa
# (npr. gcc i libpq-dev ako koristiš PostgreSQL)
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Nadogradnja pip-a i kopiranje requirements datoteke
RUN pip install --no-cache-dir --upgrade pip
COPY requirements.txt .
# Instalacija paketa u lokalni direktorij unutar builder faze
RUN pip install --no-cache-dir --user -r requirements.txt
# --- 2. FAZA: Pokretanje (Prodajni Runtime) ---
FROM python:3.12-slim AS runner
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH=/root/.local/bin:$PATH
WORKDIR /app
# Ako koristiš PostgreSQL na produkciji, ovdje ti treba samo lagana runtime knjižnica
RUN apt-get update && apt-get install --no-install-recommends -y \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Kopiramo instalirane Python pakete iz prve faze
COPY --from=builder /root/.local /root/.local
# Kopiramo ostatak izvornog koda Django aplikacije
COPY . .
# Otvaramo port 8000 na kojem će slušati Gunicorn
EXPOSE 8000
# Pokretanje aplikacije produkcijskim Gunicorn serverom
# Zamijeni "core" s točnim nazivom mape u kojoj ti se nalazi wsgi.py datoteka!
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "core.wsgi:application"]

View File

@@ -1,13 +1,5 @@
""" """
Django settings for core project. Django settings for core project.
Generated by 'django-admin startproject' using Django 6.0.5.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
""" """
import os import os
@@ -20,16 +12,11 @@ BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env') load_dotenv(BASE_DIR / '.env')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv( SECRET_KEY = os.getenv(
'DJANGO_SECRET_KEY', 'DJANGO_SECRET_KEY',
'django-insecure-!228(8gy#3a7l-@_^g1s4bipj&@*+_415+ulx0^-9jw(%ksdvy', 'django-insecure-!228(8gy#3a7l-@_^g1s4bipj&@*+_415+ulx0^-9jw(%ksdvy',
) )
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DJANGO_DEBUG', 'True').lower() in ('1', 'true', 'yes', 'on') DEBUG = os.getenv('DJANGO_DEBUG', 'True').lower() in ('1', 'true', 'yes', 'on')
# Custom user model # Custom user model
@@ -42,7 +29,6 @@ ALLOWED_HOSTS = [
] ]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@@ -93,10 +79,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'core.wsgi.application' WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
@@ -104,93 +86,56 @@ DATABASES = {
} }
} }
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator' },
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator' },
}, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator' },
{ { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator' },
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
] ]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Europe/Zagreb' TIME_ZONE = 'Europe/Zagreb'
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Automatski koristi filtriranje na svim API endpointima koji podržavaju filtriranje
if DEBUG: if DEBUG:
# Razvojni način: Svatko može čitati i pisati bez tokena DEFAULT_PERMISSION_CLASSES = [ 'rest_framework.permissions.AllowAny' ]
DEFAULT_PERMISSION_CLASSES = [
'rest_framework.permissions.AllowAny',
]
else: else:
# Produkcijski način: Pristup samo uz ispravan JWT token DEFAULT_PERMISSION_CLASSES = [ 'rest_framework.permissions.IsAuthenticated' ]
DEFAULT_PERMISSION_CLASSES = [
'rest_framework.permissions.IsAuthenticated',
]
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [ 'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend' 'django_filters.rest_framework.DjangoFilterBackend'
], ],
# JWT Autentikacija
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication', 'rest_framework_simplejwt.authentication.JWTAuthentication',
), ),
# Postavi da su po defaultu svi API-ji zaključani (samo za prijavljene)
'DEFAULT_PERMISSION_CLASSES': DEFAULT_PERMISSION_CLASSES 'DEFAULT_PERMISSION_CLASSES': DEFAULT_PERMISSION_CLASSES
} }
# Podesiti koliko dugo vrijede JWT tokeni (npr. 1 sat za pristup, 30 dana za refresh)
SIMPLE_JWT = { SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30), 'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_HEADER_TYPES': ('Bearer',),
} }
# Media files (npr. slike vozila)
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# CORS POSTAVKE - POPRAVLJENI ZAREZI I FORMALNI ORIGINI
# CORS postavke - dozvoli frontend aplikaciji da komunicira s backendom
if DEBUG: if DEBUG:
CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_ALL_ORIGINS = True
else: else:
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [ CORS_ALLOWED_ORIGINS = [
"http://localhost:4321", # Ovo je adresa na kojoj će Astro frontend biti dostupan host.strip()
"http://127.0.0.1:4321", for host in os.getenv('DJANGO_CORS_ALLOWED_ORIGINS', '').split(',')
if host.strip()
] ]
CORS_ALLOW_METHODS = [ CORS_ALLOW_METHODS = [
"DELETE", "DELETE",
"GET", "GET",
@@ -201,10 +146,19 @@ CORS_ALLOW_METHODS = [
] ]
CORS_ALLOW_HEADERS = [ CORS_ALLOW_HEADERS = [
"accept", 'accept',
"authorization", 'accept-encoding',
"content-type", 'authorization',
"user-agent", 'content-type',
"x-csrftoken", 'dnt',
"x-requested-with", 'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
'access-control-request-private-network',
] ]
# Ako preglednik pošalje PNA preflight zahtjev sa lokalne mreže,
# corsheaders će ga s ovim u potpunosti odobriti bez redirecta
CORS_ALLOW_PRIVATE_NETWORK = True
CORS_PREFLIGHT_MAX_AGE = 86400

View File

@@ -0,0 +1,2 @@
node_modules
.astro

30
001.FRONTEND/Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
# --- 1. FAZA: Izgradnja (Build) ---
FROM node:22-alpine AS builder
WORKDIR /app
# Kopiramo samo datoteke ovisnosti kako bismo iskoristili Docker cache
COPY package*.json ./
RUN npm install
# Kopiramo ostatak izvornog koda i pokrećemo build
COPY . .
RUN npm run build
# --- 2. FAZA: Pokretanje (Run) ---
FROM node:22-alpine AS runner
WORKDIR /app
# Definiramo produkcijsko okruženje
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=4321
# Kopiramo samo izgrađene datoteke iz prve faze (dist) i potrebne module
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
EXPOSE 4321
# Pokretanje aplikacije izravno preko Node-a
CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -1,25 +1,34 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import node from '@astrojs/node'; import node from '@astrojs/node';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({ export default defineConfig({
// Ovo omogućuje dinamičke rute bez getStaticPaths
output: 'server', output: 'server',
adapter: node({ adapter: node({
mode: 'standalone' mode: 'standalone',
}), }),
server: {
host: true,
port: 4321,
},
image: { image: {
// Dodaj domene s kojih Astro smije povlačiti i optimizirati slike // Dodaj domene s kojih Astro smije povlačiti i optimizirati slike
domains: ['localhost', '127.0.0.1'], domains: ['localhost', '127.0.0.1','v003-backend.captain.mitteworkspace.cloud'],
},
build: {
inlineStylesheets: 'always'
}, },
vite: { vite: {
plugins: [tailwindcss()] 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']
},
} }
}); });

View File

@@ -8,7 +8,7 @@
"name": "poslovanje", "name": "poslovanje",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@astrojs/node": "^10.1.1", "@astrojs/node": "^10.0.6",
"@tailwindcss/vite": "^4.2.4", "@tailwindcss/vite": "^4.2.4",
"astro": "^6.1.9", "astro": "^6.1.9",
"flowbite": "^4.0.1", "flowbite": "^4.0.1",
@@ -1186,9 +1186,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.130.0", "version": "0.132.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"funding": { "funding": {
@@ -1206,9 +1206,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1223,9 +1223,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1240,9 +1240,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1257,9 +1257,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1274,9 +1274,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1291,9 +1291,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1308,9 +1308,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1325,9 +1325,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -1342,9 +1342,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -1359,9 +1359,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1376,9 +1376,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1393,9 +1393,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1410,9 +1410,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -1429,9 +1429,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1446,9 +1446,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2337,9 +2337,9 @@
} }
}, },
"node_modules/astro": { "node_modules/astro": {
"version": "6.3.6", "version": "6.3.7",
"resolved": "https://registry.npmjs.org/astro/-/astro-6.3.6.tgz", "resolved": "https://registry.npmjs.org/astro/-/astro-6.3.7.tgz",
"integrity": "sha512-lM30gGI/iASK9Z1WQVnBBYzxVwDv8slkXbJOF7FNJdZQeBrFETpsQvYoLRupM/adt2ObP5hkYAWEeCjofoqlRw==", "integrity": "sha512-zIeDRrI0qNgN1lcCjNqt6/IVCVej7VwSa326cO8uP9BOk1cg4QuffhLnOn2gCgWQr32/wxpSRFfXiLKHglu1Tw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@astrojs/compiler": "^4.0.0", "@astrojs/compiler": "^4.0.0",
@@ -2920,9 +2920,9 @@
} }
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.21.6", "version": "5.22.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.4",
@@ -5305,13 +5305,13 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.130.0", "@oxc-project/types": "=0.132.0",
"@rolldown/pluginutils": "^1.0.0" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
@@ -5321,21 +5321,21 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-android-arm64": "1.0.2",
"@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.2",
"@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.2",
"@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
"@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.2",
"@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.2",
"@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.2",
"@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.2",
"@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.2",
"@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.2",
"@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.2",
"@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.2",
"@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.2",
"@rolldown/binding-win32-x64-msvc": "1.0.1" "@rolldown/binding-win32-x64-msvc": "1.0.2"
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
@@ -5398,9 +5398,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.8.0", "version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -6013,16 +6013,16 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.13", "version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
"postcss": "^8.5.14", "postcss": "^8.5.15",
"rolldown": "1.0.1", "rolldown": "1.0.2",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.16"
}, },
"bin": { "bin": {

View File

@@ -55,13 +55,4 @@ const {
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body {
font-family: 'Inter', system-ui, sans-serif;
}
/* Osigurava da Dark Mode radi ispravno s Flowbite-om */
.dark {
color-scheme: dark;
}
</style> </style>

View File

@@ -1,5 +1,8 @@
// src/lib/api.js // src/lib/api.js
const API_BASE = import.meta.env.PUBLIC_API_URL;
// 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 * POMOĆNA FUNKCIJA:端 Dohvaća token i postavlja Headere
@@ -66,42 +69,41 @@ async function handleResponse(res) {
// --- RUTE --- // --- RUTE ---
export const routes = { export const routes = {
// Autentifikacija // Autentifikacija (Maknute početne kose crte!)
login: () => '/token/', login: () => 'token/',
trenutniKorisnik: () => '/users/me/', trenutniKorisnik: () => 'users/me/',
// Radni nalozi // Radni nalozi
radniNalozi: (params = {}) => { radniNalozi: (params = {}) => {
const baseUrl = '/operativa/radni-nalozi/'; const baseUrl = 'operativa/radni-nalozi/';
// Čistimo null i undefined vrijednosti iz filtera
const cleanParams = Object.fromEntries( const cleanParams = Object.fromEntries(
Object.entries(params).filter(([_, v]) => v != null) Object.entries(params).filter(([_, v]) => v != null)
); );
const queryString = new URLSearchParams(cleanParams).toString(); const queryString = new URLSearchParams(cleanParams).toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl; return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}, },
radniNalogDetalji: (id) => `/operativa/radni-nalozi/${id}/`, radniNalogDetalji: (id) => `operativa/radni-nalozi/${id}/`,
sljedeciBrojNaloga: () => '/operativa/radni-nalozi/sljedeci-broj/', sljedeciBrojNaloga: () => 'operativa/radni-nalozi/sljedeci-broj/',
// Putni nalozi i logistika // Putni nalozi i logistika
putniNalozi: () => '/operativa/putni-nalozi/', putniNalozi: () => 'operativa/putni-nalozi/',
// Vozila i strojevi (Fleet modul) // Vozila i strojevi (Fleet modul)
vozila: () => '/fleet/vozila/', // USKLAĐENO: Prebačeno pod zajednički /fleet/ namespace vozila: () => 'fleet/vozila/',
strojevi: (vlasnikId = null) => { strojevi: (vlasnikId = null) => {
const baseUrl = '/fleet/strojevi/'; const baseUrl = 'fleet/strojevi/';
if (vlasnikId) return `${baseUrl}?vlasnik=${vlasnikId}`; if (vlasnikId) return `${baseUrl}?vlasnik=${vlasnikId}`;
return baseUrl; return baseUrl;
}, },
strojDetalji: (id) => `/fleet/strojevi/${id}/`, strojDetalji: (id) => `fleet/strojevi/${id}/`,
// Kupci / Klijenti // Kupci / Klijenti
kupciSvi: () => '/kupci/svi/', kupciSvi: () => 'kupci/svi/',
kupacDetalji: (id) => `/kupci/svi/${id}/`, kupacDetalji: (id) => `kupci/svi/${id}/`,
// Kalendar i raspored // Kalendar i raspored
mojRaspored: () => '/kalendar/moj-raspored/', mojRaspored: () => 'kalendar/moj-raspored/',
kalendarDogadaji: () => '/kalendar/dogadaji/' kalendarDogadaji: () => 'kalendar/dogadaji/'
}; };
// --- API METODE --- // --- API METODE ---
@@ -418,16 +420,20 @@ export async function fetchPutniNaloziData() {
} }
} }
/**
* Dohvaća sljedeći slobodni broj radnog naloga s backenda
* Osigurano protiv duplih kosih crta i preflight redirecta
*/
export async function fetchSljedeciBrojNaloga() { export async function fetchSljedeciBrojNaloga() {
try { try {
// Koristimo novu rutu iz routes objekta // Pametno spajanje baze i rute (isto kao u ostalim očišćenim metodama)
const url = `${API_BASE}${routes.sljedeciBrojNaloga()}`; const base = API_BASE.endsWith('/') ? API_BASE : `${API_BASE}/`;
const ruta = routes.sljedeciBrojNaloga();
const url = `${base}${ruta}`;
const res = await fetch(url, { const res = await fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: getAuthHeaders() // Koristi centralizirane headere umjesto sirovog objekta
'Content-Type': 'application/json'
}
}); });
if (res.ok) { if (res.ok) {

View File

@@ -1,34 +1,35 @@
---
// src/pages/fleet/vozila/[id].astro // src/pages/fleet/vozila/[id].astro
import Layout from "../../../layouts/Layout.astro"; // Prilagodi putanju svom glavnom layoutu ---
import { fetchCalendarEvents } from "../../../lib/api"; // Pretpostavka lokacije tvog centraliziranog API-ja import Layout from "../../../layouts/Layout.astro";
import Button from "../../../components/Button.astro"; // Uvoz tvoje zajedničke komponente za gumbe
import { getStatusColorClass } from "../../../utils/ui"; import { getStatusColorClass } from "../../../utils/ui";
// 1. Dohvat ID-ja iz URL parametara // 1. Dohvat ID-ja iz URL parametara
const { id } = Astro.params; const { id } = Astro.params;
// 2. Dohvat podataka s Django API-ja
// Ovdje koristimo tvoj centralizirani API klijent. Ako imaš fetchVoziloDetails(id), iskoristi ga.
// Kao siguran fallback, dohvaćamo sva vozila i filtriramo ono koje nam treba.
const response = await fetch(`${Astro.url.origin}/api/fleet/vozila`); // ili izravno tvoj DRF endpoint ako lib nema pojedinačni fetch
let vozilo = null; let vozilo = null;
let radniNalozi = [];
// 2. Dohvat podataka s Django API-ja (Backend)
try { try {
// Simulacija dohvata preko tvog centraliziranog klijenta ili izravnog fetch-a const voziloResponse = await fetch(`http://127.0.0.1:8000/api/fleet/vozila/${id}/`);
// Ako tvoj api.js ima metodu npr. fetchVozilo(id), zamijeni ovo s: vozilo = await fetchVozilo(id); if (voziloResponse.ok) {
const svaVozilaResponse = await fetch("http://127.0.0.1:8000/api/fleet/vozila/"); // Primjer internog DRF endpointa vozilo = await voziloResponse.json();
const vozila = await svaVozilaResponse.json(); }
vozilo = vozila.find((v) => v.id === parseInt(id));
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) { } catch (error) {
console.error("Greška pri dohvaćanju podataka o vozilu:", error); console.error("Greška pri dohvaćanju podataka s Django API-ja:", error);
} }
// Ako vozilo ne postoji, preusmjeravamo na 404 ili na listu vozila
if (!vozilo) { if (!vozilo) {
return Astro.redirect("/fleet/vozila?error=not-found"); return Astro.redirect("/fleet/vozila?error=not-found");
} }
// Pomoćna funkcija za formatiranje statusa lokalno ako zakaže status_prikaz
function formatStatusLocal(status) { function formatStatusLocal(status) {
const map = { const map = {
'aktivan': 'Aktivan', 'aktivan': 'Aktivan',
@@ -38,20 +39,30 @@ function formatStatusLocal(status) {
return map[status?.toLowerCase()] || status; 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()); const statusKlasa = getStatusColorClass(vozilo.status?.toLowerCase());
--- ---
<Layout title={`Vozilo | ${vozilo.naziv}`}> <Layout title={`Vozilo | ${vozilo.naziv}`}>
<div class="max-w-4xl mx-auto px-4 py-8 font-sans"> <div class="max-w-4xl mx-auto px-4 py-8 font-sans">
<a <div class="mb-6">
<Button
href="/fleet/vozila" href="/fleet/vozila"
class="inline-flex items-center gap-2 text-sm font-black uppercase tracking-widest text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors mb-6" 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"></i> Povratak na popis <i class="fa-solid fa-arrow-left-long mr-2"></i> Povratak na popis
</a> </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"> <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 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="flex items-center gap-5">
@@ -74,7 +85,6 @@ const statusKlasa = getStatusColorClass(vozilo.status?.toLowerCase());
</div> </div>
<div class="p-8 grid grid-cols-1 md:grid-cols-2 gap-6"> <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"> <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"> <span class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 dark:text-gray-500 block mb-1">
Registracijska oznaka Registracijska oznaka
@@ -113,44 +123,106 @@ const statusKlasa = getStatusColorClass(vozilo.status?.toLowerCase());
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. 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> </p>
</div> </div>
</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"> <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">
<a <Button
href={`/operativa/radni-nalozi?vozilo=${vozilo.id}`} href={`/operativa/radni-nalozi?vozilo=${vozilo.id}`}
class="px-5 py-2.5 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-black uppercase tracking-widest rounded-xl border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 transition-all active:scale-95 shadow-sm" 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> Radni Nalozi <i class="fa-solid fa-file-invoice mr-1.5 opacity-70"></i> Otvori u operativi
</a> </Button>
<button <Button
id="btn-brzi-servis" id="btn-brzi-servis"
data-id={vozilo.id} data-id={vozilo.id}
class="px-5 py-2.5 bg-blue-600 text-white text-xs font-black uppercase tracking-widest rounded-xl hover:bg-blue-700 transition-all active:scale-95 shadow-lg shadow-blue-600/20" 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 <i class="fa-solid fa-screwdriver-wrench mr-1.5"></i> Otvori Servis
</button> </Button>
</div>
</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> </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> </div>
</Layout> </Layout>
<script> <script>
// Klijentska skripta za brze akcije na stranici vozila
function initDetaljiVozila() { function initDetaljiVozila() {
const servisBtn = document.getElementById('btn-brzi-servis'); const servisBtn = document.getElementById('btn-brzi-servis');
if (servisBtn) { if (servisBtn) {
servisBtn.addEventListener('click', () => { servisBtn.addEventListener('click', () => {
const voziloId = servisBtn.getAttribute('data-id'); const voziloId = servisBtn.getAttribute('data-id');
// Primjer brze operativne akcije (npr. otvaranje modala ili preusmjeravanje)
console.log(`Otvaram brzu servisnu prijavu za vozilo ID: ${voziloId}`); console.log(`Otvaram brzu servisnu prijavu za vozilo ID: ${voziloId}`);
window.location.href = `/operativa/radni-nalozi/novo?vozilo_id=${voziloId}&tip=servis`; window.location.href = `/operativa/radni-nalozi/novo?vozilo_id=${voziloId}&tip=servis`;
}); });
} }
} }
// Podrška za Astro View Transitions i standardno učitavanje
initDetaljiVozila(); initDetaljiVozila();
document.addEventListener('astro:after-swap', initDetaljiVozila); document.addEventListener('astro:after-swap', initDetaljiVozila);
</script> </script>

View File

@@ -1 +1,8 @@
/* src/styles/global.css */
@import "tailwindcss"; @import "tailwindcss";
/* Eksplicitne putanje za Tailwind v4 skener na tvom LXC-u */
@source "../**/*.astro";
@source "../components/**/*.astro";
@source "../layouts/**/*.astro";
@source "../pages/**/*.astro";