This commit is contained in:
2026-05-22 06:08:51 +00:00
parent c078a257e9
commit bafec754d1
131 changed files with 2346 additions and 893 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,11 @@
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"dev": "astro dev --host",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"start": "node ./dist/server/entry.mjs"
},
"dependencies": {
"@astrojs/node": "^10.0.6",

View File

@@ -4,7 +4,7 @@
import Button from './Button.astro';
interface Props {
tip: 'dashboard' | 'radni-nalog' | 'stroj' | 'vlasnik';
tip: 'dashboard' | 'radni-nalog' | 'stroj' | 'vlasnik' | 'vozilo'; // DODANO: vozilo
podaci: any;
}
@@ -16,117 +16,138 @@ const formatDate = (date: string) =>
---
<aside class="space-y-6">
<!-- SEKCIJA 1: GLAVNE AKCIJE (Uvijek vidljivo) -->
<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>
<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>
<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>
<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>
<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>
{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' && (
<>
<!-- AKCIJE ZA KUPCA -->
<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>
<!-- DETALJI TVRTKE (Metadata preseljena s glavnog ekrana) -->
<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>
</>
)}
<!-- SEKCIJA 2: KONTEKSTUALNI DETALJI (Ovdje selimo manje bitne stvari) -->
<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>
@@ -134,16 +155,12 @@ const formatDate = (date: string) =>
{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">Prioritet</span>
<span class="text-xs font-black text-red-500 uppercase italic">Visok</span>
<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">Admin Sustava</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">Ažurirano</span>
<span class="text-xs font-black dark:text-gray-200">{formatDate(podaci.datum_azuriranja)}</span>
<span class="text-xs font-black dark:text-gray-200">{podaci.izvrsitelj_ime}</span>
</div>
</>
)}
@@ -164,6 +181,19 @@ const formatDate = (date: string) =>
</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>

View File

@@ -8,9 +8,26 @@ 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] = [];
acc[dateKey].push(event);
// 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;
}, {});
@@ -70,18 +87,13 @@ const currentYearMonth = "2026-05";
<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>
</div>
<script>
function formatStatusLocal(status) {
const map = { 'u_radu': 'U radu', 'planirano': 'Planirano', 'zavrseno': 'Završeno', 'servis': 'Na servisu' };
return map[status] || status;
}
function initKalendar() {
const grid = document.getElementById('days-grid');
const panel = document.getElementById('event-panel');
@@ -99,44 +111,72 @@ const currentYearMonth = "2026-05";
const allItems = Array.from(grid.children);
const index = allItems.indexOf(cell);
// 1. Pozicioniranje strelice (Matematički precizno)
// 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
// 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 (text-left poravnanje)
eventList.innerHTML = eventsData.map(ev => `
<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 flex-wrap justify-between items-start gap-2">
<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>
<span class="text-[10px] font-black bg-white/20 px-3 py-1 rounded-full uppercase tracking-widest border border-white/10">
${formatStatusLocal(ev.tip)}
</span>
</div>
<p class="text-sm opacity-90 font-medium mt-2 leading-relaxed max-w-2xl text-left w-full">
// 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 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>
`;
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>
</div>
`).join('');
`;
}).join('');
targetCell.after(panel);
panel.classList.remove('hidden');

View File

@@ -17,22 +17,23 @@ const {
prikaziNaslov = true,
} = Astro.props;
// 1. Dohvat filtera iz URL-a (npr. ?status=u_radu)
const statusFilter = Astro.url.searchParams.get('status');
// 1. Dohvat filtera iz URL-a (pretvaramo u mala slova radi sigurnosti)
const statusFilter = Astro.url.searchParams.get('status')?.toLowerCase();
// 2. Dohvat podataka s API-ja
const { nalozi = [] } = await fetchDashboardData();
// 3. Logika filtriranja (Serverska strana)
// 3. Logika filtriranja (Serverska strana - tolerantna na velika/mala slova)
let filtriraniNalozi = nalozi;
if (statusFilter) {
filtriraniNalozi = nalozi.filter(n => n.status === statusFilter);
filtriraniNalozi = nalozi.filter(n => n.status?.toLowerCase() === statusFilter);
}
// 4. Izračun brojeva za NaslovList (uvijek se računa iz originalne liste nalozi)
const planiranoCount = nalozi.filter(n => n.status === 'planirano').length;
const uRaduCount = nalozi.filter(n => n.status === 'u_radu').length;
// 4. POPRAVLJENO: Izračun brojeva uz obvezno pretvaranje u mala slova (.toLowerCase())
// Na ovaj način 'PLANIRANO' s backenda sigurno postaje 'planirano' u JS-u
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
// 5. Primjena limita na prikaz (npr. top 5 nalozi)
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
@@ -49,10 +50,10 @@ const prikazaniNaslov = statusFilter
naslov={prikazaniNaslov}
ukupno={filtriraniNalozi.length}
label1="Planirano"
count1={planiranoCount}
count1={planiranoCount} // Sada će ispravno prikazati stvarni broj
filter1="planirano"
label2="U radu"
count2={uRaduCount}
count2={uRaduCount} // Sada će ispravno prikazati stvarni broj
filter2="u_radu"
/>
)}
@@ -63,13 +64,13 @@ const prikazaniNaslov = statusFilter
prikazaniNalozi.map((n) => (
<GenericKarticaItem
href={`/operativa/radni-nalozi/${n.id}`}
status={n.status}
statusBojaClass={getStatusColorClass(n.status)}
ikona={n.status === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice'}
status={n.status?.toLowerCase()} // Šaljemo ujednačeni lowercase u podkomponente
statusBojaClass={getStatusColorClass(n.status?.toLowerCase())}
ikona={n.status?.toLowerCase() === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice'}
naslov={`#${n.broj_naloga}`}
subNaslov={formatStatus(n.status)}
metaTekst={`${n.klijent_naziv} | ${n.vozilo_naziv} | ${n.stroj_naziv}`}
statusPrikaz={formatStatus(n.status)}
subNaslov={formatStatus(n.status?.toLowerCase())}
metaTekst={`${n.klijent_naziv || 'Nepoznat klijent'} | ${n.vozilo_naziv || 'Bez vozila'} | ${n.stroj_naziv || 'Bez stroja'}`}
statusPrikaz={formatStatus(n.status?.toLowerCase())}
bojaTeme={n.boja_teme || 'blue'}
/>
))
@@ -89,6 +90,6 @@ const prikazaniNaslov = statusFilter
// Prvo pokretanje
initFilters();
// Podrška za Astro View Transitions (ako ih koristiš)
// Podrška za Astro View Transitions
document.addEventListener('astro:after-swap', initFilters);
</script>

468
001.FRONTEND/src/lib/api.js Normal file
View File

@@ -0,0 +1,468 @@
// src/lib/api.js
const API_BASE = import.meta.env.PUBLIC_API_URL;
/**
* POMOĆNA FUNKCIJA:端 Dohvaća token i postavlja Headere
*/
function getAuthHeaders(bodyData = {}) {
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`; // Usklađeno sa Simple JWT standardom
}
// Provjera je li bodyData FormData (za slike/naloge)
const isFormData = bodyData instanceof FormData;
// Ako NIJE FormData, šaljemo JSON
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/',
// Radni nalozi
radniNalozi: (params = {}) => {
const baseUrl = '/operativa/radni-nalozi/';
// Čistimo null i undefined vrijednosti iz filtera
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/', // USKLAĐENO: Prebačeno pod zajednički /fleet/ namespace
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 trenutno prijavljenog korisnika
*/
export async function fetchCurrentUser() {
try {
const url = `${API_BASE}${routes.trenutniKorisnik()}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
return await handleResponse(res);
} catch (e) {
console.error("fetchCurrentUser Failure:", 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 [];
}
}
export async function fetchSljedeciBrojNaloga() {
try {
// Koristimo novu rutu iz routes objekta
const url = `${API_BASE}${routes.sljedeciBrojNaloga()}`;
const res = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
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;
}
}

View File

@@ -0,0 +1,156 @@
---
// 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 { getStatusColorClass } from "../../../utils/ui";
// 1. Dohvat ID-ja iz URL parametara
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;
try {
// Simulacija dohvata preko tvog centraliziranog klijenta ili izravnog fetch-a
// Ako tvoj api.js ima metodu npr. fetchVozilo(id), zamijeni ovo s: vozilo = await fetchVozilo(id);
const svaVozilaResponse = await fetch("http://127.0.0.1:8000/api/fleet/vozila/"); // Primjer internog DRF endpointa
const vozila = await svaVozilaResponse.json();
vozilo = vozila.find((v) => v.id === parseInt(id));
} catch (error) {
console.error("Greška pri dohvaćanju podataka o vozilu:", error);
}
// Ako vozilo ne postoji, preusmjeravamo na 404 ili na listu vozila
if (!vozilo) {
return Astro.redirect("/fleet/vozila?error=not-found");
}
// Pomoćna funkcija za formatiranje statusa lokalno ako zakaže status_prikaz
function formatStatusLocal(status) {
const map = {
'aktivan': 'Aktivan',
'servis': 'Na servisu',
'neaktivan': 'Izvan pogona'
};
return map[status?.toLowerCase()] || status;
}
const statusKlasa = getStatusColorClass(vozilo.status?.toLowerCase());
---
<Layout title={`Vozilo | ${vozilo.naziv}`}>
<div class="max-w-4xl mx-auto px-4 py-8 font-sans">
<a
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"
>
<i class="fa-solid fa-arrow-left-long"></i> Povratak na popis
</a>
<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="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">
<a
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"
>
<i class="fa-solid fa-file-invoice mr-1.5 opacity-70"></i> Radni Nalozi
</a>
<button
id="btn-brzi-servis"
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"
>
<i class="fa-solid fa-screwdriver-wrench mr-1.5"></i> Otvori Servis
</button>
</div>
</div>
</div>
</Layout>
<script>
// Klijentska skripta za brze akcije na stranici vozila
function initDetaljiVozila() {
const servisBtn = document.getElementById('btn-brzi-servis');
if (servisBtn) {
servisBtn.addEventListener('click', () => {
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}`);
window.location.href = `/operativa/radni-nalozi/novo?vozilo_id=${voziloId}&tip=servis`;
});
}
}
// Podrška za Astro View Transitions i standardno učitavanje
initDetaljiVozila();
document.addEventListener('astro:after-swap', initDetaljiVozila);
</script>

View File

@@ -3,16 +3,18 @@
import Layout from "../../../layouts/Layout.astro";
import Button from "../../../components/Button.astro";
import AkcijePanel from "../../../components/AkcijePanel.astro";
import WelcomeHeader from "../../../components/WelcomeHeader.astro"; // Importamo novi header
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
import NaslovList from "../../../components/NaslovList.astro";
import GenericKarticaItem from "../../../components/GenericKarticaItem.astro";
// Importiranje utilitija
// Importiranje utilitija i centraliziranog API-ja
import { fetchDashboardData, fetchCurrentUser } from "../../../lib/api";
import { getStatusColorClass } from "../../../utils/ui";
import { getStatusColorClass, formatStatus } from "../../../utils/ui";
// 1. DOHVAT PODATAKA
// Paralelno dohvaćamo podatke o vozilima i trenutnom korisniku za Header
// 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()
@@ -20,66 +22,75 @@ const [dashboardData, user] = await Promise.all([
const vozila = dashboardData?.vozila || [];
// 2. LOGIKA STATISTIKE
const aktivnaVozila = vozila.filter(v => v.status === 'aktivan').length;
const naServisu = vozila.filter(v => v.status === 'servis').length;
const neaktivnaVozila = vozila.filter(v => v.status === 'neaktivan').length;
// 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;
// Podaci za WelcomeHeader
const imeKorisnika = user?.first_name || "Serviser";
// const ulogaKorisnika = user?.is_serviser ? "Terenski Tehničar" : "Logistika";
// 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">
<!-- WELCOME HEADER -->
<WelcomeHeader />
<!-- LISTA VOZILA + -->
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
<!-- LIJEVI STUPAC: POPIS -->
<div class="md:col-span-7 space-y-6">
<NaslovList
naslov="Aktivna Flota"
ukupno={vozila.length}
naslov={prikazaniNaslov}
ukupno={filtriraniVozila.length}
label1="Spremni"
filter1="planirano"
count1={aktivnaVozila}
filter1="aktivan" count1={aktivnaVozila}
label2="Servis"
count2={naServisu}
filter2="u_radu"
filter2="servis" count2={naServisu}
label3="Neaktivni"
count3={neaktivnaVozila}
filter3="neaktivan"
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">
{vozila.length > 0 ? vozila.map((v) => (
{prikazanaVozila.length > 0 ? prikazanaVozila.map((v) => (
<GenericKarticaItem
href={`/fleet/vozila/${v.id}`}
status={v.status}
statusBojaClass={getStatusColorClass(v.status)}
ikona={v.status === 'aktivan' ? 'fa-truck' : 'fa-screwdriver-wrench'}
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()} KM | Lokacija: Baza`}
statusPrikaz={v.status_prikaz}
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">Baza podataka je prazna</p>
<Button variant="outline" class="mt-6" id="btn-dodaj-prvo">Dodaj prvo vozilo</Button>
<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>
<!-- DESNI PANEL - AKCIJE -->
<div class="md:col-span-3">
<AkcijePanel />
</div>
@@ -89,6 +100,7 @@ const imeKorisnika = user?.first_name || "Serviser";
</Layout>
<script>
// Inicijalizacija klikova na kartice i filtere unutar flote
import { initFilters } from "../../../scripts/filters";
initFilters();
document.addEventListener('astro:after-swap', initFilters);

View File

@@ -7,10 +7,10 @@ import RadniNalogLista from "../components/RadniNalogLista.astro";
import StatsGrid from "../components/StatsGrid.astro";
import Kalendar from "../components/Kalendar/Kalendar.astro";
// API i Utils
// API i Centralizirani Utils iz api.js
import { fetchDashboardData, fetchCurrentUser } from "../lib/api";
// 1. Dohvat podataka (paralelno radi brzine)
// 1. Dohvat podataka (paralelno radi brzine i performansi u homelabu)
const [data, user] = await Promise.all([
fetchDashboardData(),
fetchCurrentUser()
@@ -19,19 +19,22 @@ const [data, user] = await Promise.all([
const nalozi = data?.nalozi || [];
const imeKorisnika = user?.first_name || "Kolega";
// 2. Kalkulacija statistike samo za StatsGrid (ostalo ide u RadniNalogLista)
const naloziURadu = nalozi.filter(n => n.status === 'u_radu').length;
const planiraniNalozi = nalozi.filter(n => n.status === 'planirano').length;
const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status === 'naplaceno').length;
// 2. POPRAVLJENO: Kalkulacija statistike tolerantna na velika/mala slova (.toLowerCase())
// Osiguravamo da se podaci iz baze (PLANIRANO, U_RADU) savršeno upare sa StatsGrid filterima
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">
<!-- WELCOME HEADER -->
<WelcomeHeader />
<WelcomeHeader ime={imeKorisnika} />
<!-- STATS GRID -->
<div class="grid grid-cols-3 gap-2 sm:gap-6 px-2">
<StatsGrid
label="Planirano"
@@ -57,7 +60,6 @@ const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status ==
/>
</div>
<!-- RADNI NALOZI LISTA + AKCIJE PANEL -->
<div class="grid md:grid-cols-10 gap-8 items-start px-2 pb-10">
<div class="md:col-span-7 space-y-8">
@@ -74,7 +76,8 @@ const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status ==
</div>
</div>
<div>
<div class="mt-5">
<Kalendar />
</div>
</Layout>
@@ -82,7 +85,7 @@ const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status ==
<script>
import { initFilters } from "../scripts/filters.js";
// Inicijalizacija filtera za StatsGrid klikove
// Inicijalizacija filtera za StatsGrid klikove na klijentu
initFilters();
// Re-inicijalizacija kod navigacije (Astro View Transitions)

View File

@@ -6,7 +6,7 @@ import WelcomeHeader from "../components/WelcomeHeader.astro";
---
<Layout title="Prijava u sustav">
<div class="flex items-center justify-center bg-gray-50 dark:bg-gray-950 px-4">
<div class="flex items-center justify-center px-4">
<!-- WELCOME HEADER -->
<WelcomeHeader />

View File

@@ -11,13 +11,20 @@ const { id } = Astro.params;
const API_BASE = import.meta.env.PUBLIC_API_URL;
let nalog = null;
let dostupnaVozila = [];
try {
// Dohvaćamo detaljne podatke (RadniNalogDetaljiSerializer)
const res = await fetch(`${API_BASE}/operativa/radni-nalozi/${id}/?t=${Date.now()}`);
if (res.ok) nalog = await res.json();
// 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 detalja naloga:", e);
console.error("Greška pri dohvatu podataka s API-ja:", e);
}
if (!nalog) return Astro.redirect("/404");
@@ -31,7 +38,6 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
<Layout title={`Nalog #${nalog.broj_naloga}`}>
<div class="w-full space-y-10 pb-20">
<!-- 1. HEADER SEKCIJA -->
<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">
@@ -51,88 +57,79 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
</div>
</div>
<!-- Status Badge -->
<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'}
{"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_display || nalog.status}
</span>
</div>
</header>
<!-- 2. GLAVNI SADRŽAJ (Grid 10 stupaca) -->
<div class="grid md:grid-cols-10 gap-8 items-start px-2">
<div class="md:col-span-7 space-y-8">
<!-- Kartica: Opis kvara -->
<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}"
{nalog.opis_kvara}
</p>
</div>
<!-- Grid: Stroj i Klijent -->
<div class="grid sm:grid-cols-2 gap-6">
<!-- STROJ (Tehnički podaci) -->
<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>
<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}
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} 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>
<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>
<!-- KLIJENT (Kontakt podaci) -->
<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>
<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">
<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">
<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>
<!-- Kartica: Logistika (Vozilo) -->
{nalog.vozilo ? (
<div class="bg-blue-50 dark:bg-blue-900/10 rounded-[2.5rem] p-8 border border-blue-100 dark:border-blue-900/30 flex flex-col sm:flex-row justify-between items-center gap-6">
<div class="text-center sm:text-left">
<label class="text-[9px] font-black uppercase text-blue-400 block mb-2 italic tracking-widest">Servisno Vozilo</label>
<h3 class="text-xl font-black uppercase dark:text-white leading-tight">{nalog.vozilo.naziv}</h3>
<span class="text-sm font-bold text-blue-600">{nalog.vozilo.registracija}</span>
</div>
<div class="px-6 py-2 bg-white dark:bg-gray-800 rounded-2xl border border-blue-100 dark:border-blue-900/20 shadow-sm text-center">
<p class="text-[10px] text-gray-400 uppercase font-black mb-1">Početni KM</p>
<p class="text-lg font-black text-blue-600 leading-none">
{nalog.vozilo.trenutni_kilometri?.toLocaleString() || '0'} km
</p>
<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-walking text-gray-400"></i>
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Radni nalog bez zaduženog vozila</p>
<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>
)}
<!-- SEKCIJA: GALERIJA (PhotoSwipe) -->
<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>
@@ -149,12 +146,53 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
</div>
<!-- DESNI PANEL (3 Stupca) -->
<div class="md:col-span-3 space-y-6 sticky top-10">
<!-- Komponenta za gumbe (Završi, Odgodi, Print...) -->
<AkcijePanel tip="radni-nalog" podaci={nalog} />
<!-- Dodatni info o ažuriranju -->
<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">
@@ -170,12 +208,86 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
</div>
</div>
<!-- QR Kod ili ID -->
<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 uppercase tracking-[0.4em]">Sistemski ID: {nalog.id}</p>
<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>
</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>

View File

@@ -127,7 +127,7 @@ const { strojevi } = await fetchStrojeviData();
const realInput = document.getElementById('real-stroj-input') as HTMLInputElement;
const strojOptions = document.querySelectorAll('.stroj-opt');
// Funkcija za inicijalizaciju broja naloga na klijentu
// Inicijalizacija broja naloga na klijentu
async function initBrojNaloga() {
const displayElement = document.getElementById('broj-text');
if (displayElement) {
@@ -136,7 +136,6 @@ const { strojevi } = await fetchStrojeviData();
}
}
// Pozivamo funkciju čim se skripta učita
initBrojNaloga();
// 1. DROPDOWN LOGIKA
@@ -175,35 +174,55 @@ const { strojevi } = await fetchStrojeviData();
// 3. SLANJE NA API
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('submit-btn');
if (!btn) return;
const gumb = document.getElementById('submit-btn') as HTMLButtonElement;
if (!gumb) return;
btn.setAttribute('disabled', 'true');
const originalContent = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Sinkronizacija...';
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 aktivirao save() logiku
// Brišemo broj_naloga kako bi backend sam aktivirao save() sekvencu
formData.delete('broj_naloga');
// Dodavanje izvrsitelja iz localStorage
const userString = localStorage.getItem('user_info');
if (userString) {
const user = JSON.parse(userString);
formData.append('izvrsitelj', user.id);
}
const nalog = await createNalog(formData);
try {
const nalog = await createNalog(formData);
if (nalog && nalog.id) {
window.location.href = `/operativa/radni-nalozi/${nalog.id}`;
} else {
btn.removeAttribute('disabled');
btn.innerHTML = originalContent;
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');