ne radi vite server

This commit is contained in:
2026-05-23 21:52:15 +00:00
parent 80f9ba3879
commit 668195e642
32 changed files with 2096 additions and 680 deletions

View File

@@ -54,7 +54,9 @@ import Button from "./Button.astro";
</div>
<script>
import { login } from "../lib/api";
import { login, fetchCurrentUser } from "../lib/api";
// 🚀 Ponovno uvozimo centraliziranu funkciju za rute
import { dohvatiRutuPoUlozi } from "../utils/auth";
const loginForm = document.querySelector('#login-form') as HTMLFormElement;
const btn = document.querySelector('#login-btn') as HTMLButtonElement;
@@ -64,10 +66,8 @@ import Button from "./Button.astro";
if (!btn) return;
// Pohranjujemo originalni sadržaj (ikonu + tekst)
const originalContent = btn.innerHTML;
// Vizualni feedback (Loading stanje)
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-3"></i> AUTENTIFIKACIJA...';
@@ -76,25 +76,32 @@ import Button from "./Button.astro";
const password = formData.get('password') as string;
try {
// 1. Prijava i pohrana JWT tokena
const result = await login(email, password);
if (result.success) {
// 2. Dohvat profila ulogiranog korisnika
const user = await fetchCurrentUser();
sessionStorage.setItem('pending_toast', JSON.stringify({
type: 'success',
message: 'Pristup odobren. Dobrodošli u sustav.'
}));
window.location.href = '/';
// 3. 🚀 PREUSMJERAVANJE PREKO POMOĆNE FUNKCIJE:
// Namjerno NE prosljeđujemo user.id jer želimo da serviser sleti na opću /operativa/serviseri stazu
const ciljanaRuta = dohvatiRutuPoUlozi(user?.uloga);
window.location.href = ciljanaRuta;
} else {
// Poziv tvog global-toast sustava
// @ts-ignore
window.showToast?.(result.error || "Pristup odbijen: Neispravni podaci.", "error");
// Vraćanje gumba u normalu
btn.disabled = false;
btn.innerHTML = originalContent;
}
} catch (err) {
// @ts-ignore
console.error("Login krah na klijentu:", err);
// @ts-ignore
window.showToast?.("Sustav nedostupan. Provjerite vezu.", "error");
btn.disabled = false;
btn.innerHTML = originalContent;

View File

@@ -1,59 +1,193 @@
---
// src/components/NaslovList.astro
// src/components/RadniNalogLista.astro
import NaslovList from "./NaslovList.astro";
interface Props {
naslov: string;
ukupno: number;
label1: string;
count1: number;
filter1: string;
label2: string;
count2: number;
filter2: string;
label3?: string;
count3?: number;
filter3?: string;
limit?: number;
naslov?: string;
prikaziNaslov?: boolean;
}
const { naslov, ukupno, label1, count1, filter1, label2, count2, filter2, label3, count3, filter3 } = Astro.props;
const {
limit = 0,
naslov = "Zadnje aktivnosti",
prikaziNaslov = true,
} = Astro.props;
---
<div class="flex flex-row justify-between items-center w-full min-h-[40px]">
<h2 id="filter-title" class="p-0 m-0 text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight italic leading-none">
{naslov}
</h2>
<div class="flex flex-wrap justify-end items-center gap-2 lg:gap-3">
<div
class="stat-card flex items-center gap-2 px-2.5 py-1.5 bg-emerald-50 dark:bg-emerald-900/20 rounded-xl border border-emerald-100 dark:border-emerald-800/50 cursor-pointer transition-all active:scale-95 hover:border-emerald-400"
data-filter={filter1}
>
<div class="w-2 h-2 rounded-full bg-emerald-500"></div>
<span class="text-[9px] lg:text-[10px] font-black uppercase text-emerald-700 dark:text-emerald-400 whitespace-nowrap">
{label1}: {count1}
</span>
</div>
<div class="space-y-6 group" id="radni-nalozi-sekcija" data-state="loading">
{prikaziNaslov && (
<NaslovList
naslov={naslov}
ukupno={0}
label1="Planirano"
count1={0}
filter1="planirano"
label2="U radu"
count2={0}
filter2="u_radu"
label3="Završeno"
count3={0}
filter3="zavrseno"
/>
)}
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
<div
class="stat-card flex items-center gap-2 px-2.5 py-1.5 bg-red-50 dark:bg-red-900/20 rounded-xl border border-red-100 dark:border-red-800/50 cursor-pointer transition-all active:scale-95 hover:border-red-400"
data-filter={filter2}
id="nalozi-loader"
class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse group-data-[state=loaded]:hidden group-data-[state=error]:hidden"
>
<div class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
<span class="text-[9px] lg:text-[10px] font-black uppercase text-red-700 dark:text-red-400 whitespace-nowrap">
{label2}: {count2}
</span>
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija radnih naloga...
</div>
{label3 && count3 !== undefined && filter3 && (
<div
class="stat-card flex items-center gap-2 px-2.5 py-1.5 bg-gray-50 dark:bg-gray-900/20 rounded-xl border border-gray-100 dark:border-gray-800/50 cursor-pointer transition-all active:scale-95 hover:border-gray-400"
data-filter={filter3}
>
<div class="w-2 h-2 rounded-full bg-gray-400"></div>
<span class="text-[9px] lg:text-[10px] font-black uppercase text-gray-700 dark:text-gray-400 whitespace-nowrap">
{label3}: {count3}
</span>
<div id="nalozi-list" class="space-y-0">
<div id="nalozi-placeholder" class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest group-data-[state=loaded]:hidden">
Nema zapisa za odabrani status
</div>
)}
</div>
</div>
</div>
</div>
<span id="rn-bridge" data-limit={limit} data-naslov={naslov}></span>
<script>
import { fetchCurrentUser, fetchDashboardData, fetchServiserTerminalData } from "../lib/api";
import { getStatusColorClass, formatStatus } from "../utils/ui";
import { initFilters } from "../scripts/filters.js";
async function renderirajRadneNalogeKlijentski() {
const container = document.getElementById('nalozi-list');
const sekcija = document.getElementById('radni-nalozi-sekcija');
const bridge = document.getElementById('rn-bridge');
if (!container || !sekcija || !bridge) return;
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
if (!token) {
sekcija.setAttribute('data-state', 'error');
container.innerHTML = `
<div class="p-20 text-center flex flex-col items-center justify-center gap-3">
<div class="w-12 h-12 bg-red-600/10 text-red-600 rounded-xl flex items-center justify-center text-lg mb-2">
<i class="fa-solid fa-shield-halved"></i>
</div>
<h4 class="text-xs font-black uppercase tracking-widest text-red-600">Pristup odbijen</h4>
<p class="text-[10px] text-gray-400 font-bold uppercase tracking-wider italic">Niste autorizirani za pregled operativnih naloga flote.</p>
</div>
`;
return;
}
const limit = parseInt(bridge.getAttribute('data-limit') || '0');
const originalNaslov = bridge.getAttribute('data-naslov') || 'Zadnje aktivnosti';
const urlParams = new URLSearchParams(window.location.search);
const statusFilter = urlParams.get('status')?.toLowerCase();
try {
sekcija.setAttribute('data-state', 'loading');
const user = await fetchCurrentUser();
const ulogaGore = user?.uloga ? String(user.uloga).toUpperCase().trim() : '';
let nalozi = [];
if (ulogaGore === 'SERVISER' && user?.id) {
const terminalData = await fetchServiserTerminalData(user.id);
nalozi = terminalData?.radni_nalozi || [];
} else {
const data = await fetchDashboardData();
nalozi = data?.nalozi || [];
}
// 3. Proračun statistike na klijentskoj strani
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
const zavrsenoCount = nalozi.filter(n => ['zavrsen', 'završeno', 'gotovo', 'zavrseno'].includes(n.status?.toLowerCase())).length;
// 4. 🚀 USKLAĐENO HVAĆANJE I FILTRIRANJE PREMA TVOM data-filter PROPSU:
const card1 = sekcija.querySelector('[data-filter="planirano"]');
if (card1) {
const span = card1.querySelector('span');
if (span) span.textContent = `Planirano: ${planiranoCount}`;
if (planiranoCount > 0) card1.classList.remove('hidden'); else card1.classList.add('hidden');
}
const card2 = sekcija.querySelector('[data-filter="u_radu"]');
if (card2) {
const span = card2.querySelector('span');
if (span) span.textContent = `U radu: ${uRaduCount}`;
if (uRaduCount > 0) card2.classList.remove('hidden'); else card2.classList.add('hidden');
}
const card3 = sekcija.querySelector('[data-filter="zavrseno"]');
if (card3) {
const span = card3.querySelector('span');
if (span) span.textContent = `Završeno: ${zavrsenoCount}`;
if (zavrsenoCount > 0) card3.classList.remove('hidden'); else card3.classList.add('hidden');
}
// 5. Filtriranje i limitiranje niza prema klijentskom stanju URL-a
let filtriraniNalozi = statusFilter ? nalozi.filter(n => n.status?.toLowerCase() === statusFilter) : nalozi;
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
const glavniNaslov = sekcija.querySelector('h2');
if (glavniNaslov) {
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
}
sekcija.setAttribute('data-state', 'loaded');
// 7. Generiranje HTML kartica radnih naloga unutar tijela
if (prikazaniNalozi.length === 0) {
container.innerHTML = `<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">Nema zapisa za odabrani status</div>`;
return;
}
container.innerHTML = prikazaniNalozi.map((n) => {
const statusMalo = n.status?.toLowerCase();
const statusKlasa = getStatusColorClass(statusMalo);
const ikona = statusMalo === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice';
const klijentPrikaz = n.kupac_naziv || n.klijent_naziv || 'Nepoznat klijent';
const strojPrikaz = n.stroj_naziv || 'Bez stroja';
const voziloPrikaz = n.vozilo_naziv || 'Bez vozila';
return `
<a href="/operativa/radni-nalozi/${n.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
<i class="fa-solid ${ikona}"></i>
</div>
<div>
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${n.broj_naloga}</h4>
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${formatStatus(statusMalo)}</p>
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
${klijentPrikaz} | ${voziloPrikaz} | ${strojPrikaz}
</p>
</div>
</div>
<div class="flex items-center gap-3">
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
${formatStatus(statusMalo)}
</span>
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
<i class="fa-solid fa-chevron-right text-xs"></i>
</div>
</div>
</a>
`;
}).join('');
initFilters();
} catch (err) {
console.error("Kritična greška u klijentskom renderu radnih naloga:", err);
sekcija.setAttribute('data-state', 'error');
container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji operativnih podataka.</div>';
}
}
renderirajRadneNalogeKlijentski();
document.addEventListener('astro:after-swap', renderirajRadneNalogeKlijentski);
</script>

View File

@@ -1,6 +1,7 @@
---
// src/components/Navbar.astro
import site from "../data/site.json";
import Button from "./Button.astro";
const currentPath = Astro.url.pathname;
---
@@ -9,7 +10,6 @@ const currentPath = Astro.url.pathname;
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-20 items-center">
<!-- LOGO SEKCIJA -->
<div class="flex items-center">
<a href="/" class="flex items-center gap-3 group">
<div class="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20 group-hover:rotate-6 transition-transform duration-300">
@@ -21,87 +21,159 @@ const currentPath = Astro.url.pathname;
</a>
</div>
<div id="nav-auth-section" class="hidden flex items-center gap-4 group/auth" data-state="loading">
<div class="hidden group-data-[state=anonymous]/auth:block">
<Button
id="nav-login-btn"
href="/login"
variant="none"
class="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-2xl text-xs font-black uppercase tracking-wider shadow-lg shadow-blue-500/20 active:scale-95 transition-all border border-blue-500 cursor-pointer italic"
>
<i class="fa-solid fa-right-to-bracket"></i> Prijava
</Button>
</div>
<div class="hidden group-data-[state=authenticated]/auth:block">
<Button
id="mobile-menu-button"
variant="none"
class="w-12 h-12 flex items-center justify-center rounded-2xl bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-300 active:scale-90 transition-all border border-gray-100 dark:border-gray-700 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700/50"
>
<i class="fa-solid fa-bars-staggered text-xl"></i>
</Button>
</div>
<!-- MOBILE MENU GUMB -->
<div class="md:hidden flex items-center">
<button
id="mobile-menu-button"
type="button"
class="w-12 h-12 flex items-center justify-center rounded-2xl bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-300 active:scale-90 transition-all border border-gray-100 dark:border-gray-700"
>
<i class="fa-solid fa-bars-staggered text-xl"></i>
</button>
</div>
</div>
</div>
<!-- LEBDEĆI MOBILNI IZBORNIK (Transparentan) -->
<div
id="mobile-menu"
class="hidden absolute top-[calc(100%+0.5rem)] left-4 right-4
class="hidden absolute top-[calc(100%+0.5rem)] md:left-auto left-4 right-4 md:right-8 md:w-80
bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl
border border-white/20 dark:border-gray-800/50
p-4 rounded-[2.5rem] shadow-2xl z-[100]
p-4 rounded-[2.5rem] shadow-2xl shadow-blue-500/5 z-[100]
transition-all animate-in fade-in slide-in-from-top-4"
>
<div class="flex flex-col gap-2">
{site.navigation.map((item) => (
<a
href={item.url}
class="flex items-center justify-between px-6 py-4 rounded-[1.8rem] font-black text-[11px] uppercase tracking-widest text-gray-600 dark:text-gray-300 hover:bg-blue-600 hover:text-white transition-all duration-300 group"
{site.navigation.map((item) => {
// Provjera je li trenutna putanja aktivna (uzima u obzir i potencijalne dinamičke ID pod-rute)
const isCurrent = currentPath === item.url || (item.url === '/operativa/serviseri' && currentPath.includes('/operativa/serviseri'));
return (
<a
href={item.url}
data-nav-link={item.url}
class={`flex items-center justify-between px-6 py-4 rounded-[1.8rem] font-black text-[11px] uppercase tracking-widest transition-all duration-300 group italic ${
isCurrent
? "bg-blue-600 text-white shadow-lg shadow-blue-500/20"
: "text-gray-600 dark:text-gray-300 hover:bg-blue-600 hover:text-white"
}`}
>
{item.name}
<i class="fa-solid fa-chevron-right text-[10px] opacity-30 group-hover:translate-x-1 group-hover:opacity-100 transition-all"></i>
</a>
);
})}
<div class="w-full mt-1 mb-2">
<Button
href="/operativa/radni-nalozi/novi"
variant="none"
class="w-full bg-blue-600 text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center shadow-lg shadow-blue-500/40 active:scale-95 transition-all block italic"
>
{item.name}
<i class="fa-solid fa-chevron-right text-[10px] opacity-30 group-hover:translate-x-1 group-hover:opacity-100 transition-all"></i>
</a>
))}
<i class="fa-solid fa-plus mr-2"></i> Novi nalog
</Button>
</div>
<hr class="my-2 border-gray-100/50 dark:border-gray-800/50" />
<a href="/operativa/radni-nalozi/novi" class="w-full bg-blue-600 text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center shadow-lg shadow-blue-500/40 active:scale-95 transition-all">
<i class="fa-solid fa-plus mr-2"></i> Novi nalog
</a>
<div class="w-full">
<Button
id="logout-mobile"
variant="none"
class="w-full bg-red-600/10 text-red-600 hover:bg-red-600 hover:text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center active:scale-95 transition-all border border-red-100/30 dark:border-red-900/30 cursor-pointer block italic"
>
<i class="fa-solid fa-right-from-bracket mr-2"></i> Odjava iz sustava
</Button>
</div>
</div>
</div>
</nav>
<script>
function setupNavbar() {
import { logout, fetchCurrentUser } from "../lib/api";
async function setupNavbar() {
const btn = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu');
const authSection = document.getElementById('nav-auth-section');
const logoutMobile = document.getElementById('logout-mobile');
if (btn && menu) {
// Re-inicijalizacija gumba za Astro View Transitions
const newBtn = btn.cloneNode(true) as HTMLButtonElement;
btn.parentNode?.replaceChild(newBtn, btn);
if (!authSection) return;
newBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('hidden');
});
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
const stanjeAutentifikacije = token ? 'authenticated' : 'anonymous';
// Zatvori menu na klik izvan njega
document.addEventListener('click', (e) => {
if (!menu.contains(e.target as Node) && !newBtn.contains(e.target as Node)) {
menu.classList.add('hidden');
// 1. Postavljamo stanje u DOM (odmah prebacuje vidljivost gumbova Prijava / Menu)
authSection.setAttribute('data-state', stanjeAutentifikacije);
authSection.classList.remove('hidden');
authSection.classList.add('flex');
// 2. Dinamičko popravljanje URL putanje za servisera ako imamo token i menu elemente
if (token && menu) {
let userProfil = null;
try {
// 🚀 OBRANA OD KRAHA: Omotavamo hvat u zaseban try-catch kako greška s API-ja ne bi srušila cijeli navbar
userProfil = await fetchCurrentUser();
} catch (apiErr) {
console.warn("Navbar nije uspio dohvatiti trenutnog korisnika s API-ja:", apiErr);
}
if (userProfil && userProfil.id) {
const serviserLink = menu.querySelector('[data-nav-link="/operativa/serviseri"]') as HTMLAnchorElement;
if (serviserLink) {
serviserLink.href = `/operativa/serviseri/${userProfil.id}`;
// Popravak aktivne klase ukoliko smo točno na toj dinamičkoj ruti
if (window.location.pathname === `/operativa/serviseri/${userProfil.id}`) {
serviserLink.classList.add('bg-blue-600', 'text-white', 'shadow-lg', 'shadow-blue-500/20');
serviserLink.classList.remove('text-gray-600', 'dark:text-gray-300');
}
}
});
// Zatvori nakon odabira linka
menu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => menu.classList.add('hidden'));
});
}
}
if (!btn || !menu) return;
// Resetiranje event listenera kloniranjem (sprječava duplanje na View Transitions okidanju)
const newBtn = btn.cloneNode(true) as HTMLButtonElement;
btn.parentNode?.replaceChild(newBtn, btn);
newBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('hidden');
});
document.addEventListener('click', (e) => {
if (!menu.contains(e.target as Node) && !newBtn.contains(e.target as Node)) {
menu.classList.add('hidden');
}
});
menu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => menu.classList.add('hidden'));
});
logoutMobile?.addEventListener('click', () => {
logout();
});
}
// Logout funkcija
const logout = () => {
sessionStorage.removeItem('user_session');
window.location.href = '/login';
};
document.getElementById('logout-btn')?.addEventListener('click', logout);
document.getElementById('logout-mobile')?.addEventListener('click', logout);
// Okidanje na inicijalno učitavanje i Astro after-swap
setupNavbar();
document.addEventListener('astro:after-swap', setupNavbar);
</script>

View File

@@ -0,0 +1,161 @@
---
// src/components/PutniNalogLista.astro
import NaslovList from "./NaslovList.astro";
interface Props {
naslov?: string;
prikaziNaslov?: boolean;
voziloId?: string | number;
}
const {
naslov = "Putni nalozi i rute",
prikaziNaslov = true,
voziloId = null,
} = Astro.props;
---
<div class="space-y-6" id="putni-nalozi-sekcija">
{prikaziNaslov && (
<NaslovList
naslov={naslov}
ukupno={0}
label1="Aktivne rute" count1={0} filter1="aktivan"
label2="Završeni" count2={0} filter2="zavrsen"
/>
)}
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
<div id="pn-loader" class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse">
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija putnih naloga...
</div>
<div id="putni-nalozi-list" class="space-y-0"></div>
</div>
</div>
<span id="pn-bridge" data-vozilo-id={voziloId} data-naslov={naslov}></span>
<script>
import { fetchPutniNaloziData } from "../lib/api";
import { getStatusColorClass, formatStatus } from "../utils/ui";
import { initFilters } from "../scripts/filters.js";
async function renderirajPutneNalogeKlijentski() {
const bridge = document.getElementById('pn-bridge');
const container = document.getElementById('putni-nalozi-list');
const loader = document.getElementById('pn-loader');
if (!bridge || !container) return;
const voziloId = bridge.getAttribute('data-vozilo-id');
const originalNaslov = bridge.getAttribute('data-naslov') || 'Putni nalozi i rute';
// Dohvat aktivnog filtera iz URL-a na klijentu (?status=aktivan)
const urlParams = new URLSearchParams(window.location.search);
const statusFilter = urlParams.get('status')?.toLowerCase();
try {
// 1. Dohvat s API-ja (Sada sigurno čita localStorage token u pregledniku)
const data = await fetchPutniNaloziData();
// Prilagodba ako API vraća omotan objekt ili čisti niz
const sviPutniNalozi = Array.isArray(data) ? data : (data?.results || []);
// 2. STROGO FILTRIRANJE PREMA ID-JU VOZILA (Na razini podataka)
let filtriraniNalozi = sviPutniNalozi;
if (voziloId) {
const ciljaniId = parseInt(voziloId);
filtriraniNalozi = sviPutniNalozi.filter(pn => {
const vId = pn.vozilo?.id || pn.vozilo || pn.vozilo_id;
return parseInt(vId) === ciljaniId;
});
}
// 3. Izračun statistike na klijentskoj strani (isključivo za ovo vozilo ako je ID prisutan)
const aktivniCount = filtriraniNalozi.filter(pn => pn.status?.toLowerCase() === 'aktivan').length;
const zavrseniCount = filtriraniNalozi.filter(pn => ['zavrsen', 'završeno'].includes(pn.status?.toLowerCase())).length;
// Opcionalno dodatno filtriranje po statusu s gornjih gumbića
if (statusFilter) {
filtriraniNalozi = filtriraniNalozi.filter(pn => pn.status?.toLowerCase() === statusFilter);
}
// 4. Dinamičko ažuriranje brojača i naslova u DOM-u (NaslovList)
const sekcija = document.getElementById('putni-nalozi-sekcija');
if (sekcija) {
const glavniNaslov = sekcija.querySelector('h2');
if (glavniNaslov) {
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
}
const ukupnoBadge = sekcija.querySelector('h2 + span, .ukupno-badge');
if (ukupnoBadge) {
ukupnoBadge.textContent = filtriraniNalozi.length.toString();
}
const aktivniBadge = sekcija.querySelector('[data-filter="aktivan"] span, [filter-type="aktivan"]');
if (aktivniBadge) aktivniBadge.textContent = aktivniCount.toString();
const zavrseniBadge = sekcija.querySelector('[data-filter="zavrsen"] span, [filter-type="zavrsen"]');
if (zavrseniBadge) zavrseniBadge.textContent = zavrseniCount.toString();
}
// Sakrij loader
if (loader) loader.classList.add('hidden');
// 5. Generiranje HTML-a (Preslikana točna struktura tvog GenericKarticaItem)
if (filtriraniNalozi.length === 0) {
container.innerHTML = `
<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">
Nema evidentiranih putnih naloga
</div>`;
return;
}
container.innerHTML = filtriraniNalozi.map((pn) => {
const statusMalo = pn.status?.toLowerCase();
const statusKlasa = getStatusColorClass(statusMalo);
const relacija = pn.relacija || pn.mjesto_odredista || 'Terenska lokacija';
const vozilo = pn.vozilo_naziv || 'Dodijeljeno vozilo';
const izvrsitelj = pn.korisnik_ime || 'Vozač tehničar';
const pocetniKm = pn.pocetna_km?.toLocaleString('hr-HR') || 0;
return `
<a href="/operativa/putni-nalozi/${pn.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
<i class="fa-solid fa-route"></i>
</div>
<div>
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${pn.broj_naloga}</h4>
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${relacija}</p>
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
Vozilo: ${vozilo} | Izvršitelj: ${izvrsitelj} | Početno: ${pocetniKm} KM
</p>
</div>
</div>
<div class="flex items-center gap-3">
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
${pn.status_prikaz || formatStatus(statusMalo)}
</span>
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
<i class="fa-solid fa-chevron-right text-xs"></i>
</div>
</div>
</div>
`;
}).join('');
// Pokretanje klijentskih filtera nakon što se DOM uspješno izgradi
initFilters();
} catch (err) {
console.error("Kritična greška u klijentskom renderu putnih naloga:", err);
if (container) container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji putnih naloga.</div>';
}
}
// Izvršavanje pri učitavanju i re-inicijalizacija kod Astro View Transitions prebacivanja rute
renderirajPutneNalogeKlijentski();
document.addEventListener('astro:after-swap', renderirajPutneNalogeKlijentski);
</script>

View File

@@ -1,9 +1,6 @@
---
// src/components/RadniNalogLista.astro
import { fetchDashboardData } from "../lib/api";
import { getStatusColorClass, formatStatus } from "../utils/ui";
import NaslovList from "./NaslovList.astro";
import GenericKarticaItem from "./GenericKarticaItem.astro";
interface Props {
limit?: number;
@@ -16,80 +13,190 @@ const {
naslov = "Zadnje aktivnosti",
prikaziNaslov = true,
} = Astro.props;
// 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 - tolerantna na velika/mala slova)
let filtriraniNalozi = nalozi;
if (statusFilter) {
filtriraniNalozi = nalozi.filter(n => n.status?.toLowerCase() === statusFilter);
}
// 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;
// Dinamički naslov ovisno o filteru
const prikazaniNaslov = statusFilter
? `Filtrirano: ${formatStatus(statusFilter)}`
: naslov;
---
<div class="space-y-6">
<div class="space-y-6 group" id="radni-nalozi-sekcija" data-state="loading">
{prikaziNaslov && (
<NaslovList
naslov={prikazaniNaslov}
ukupno={filtriraniNalozi.length}
label1="Planirano"
count1={planiranoCount} // Sada će ispravno prikazati stvarni broj
filter1="planirano"
label2="U radu"
count2={uRaduCount} // Sada će ispravno prikazati stvarni broj
filter2="u_radu"
naslov={naslov}
sinkronizirano={false} label1="Planirano" count1={0} filter1="planirano"
label2="U radu" count2={0} filter2="u_radu"
/>
)}
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
<div id="nalozi-list">
{prikazaniNalozi.length > 0 ? (
prikazaniNalozi.map((n) => (
<GenericKarticaItem
href={`/operativa/radni-nalozi/${n.id}`}
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?.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'}
/>
))
) : (
<div class="p-20 text-center uppercase font-black opacity-20 italic">
Nema zapisa za odabrani status
</div>
)}
<div
id="nalozi-loader"
class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse group-data-[state=loaded]:hidden group-data-[state=error]:hidden"
>
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija radnih naloga...
</div>
<div id="nalozi-list" class="space-y-0">
<div id="nalozi-placeholder" class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest group-data-[state=loaded]:hidden">
Nema zapisa za odabrani status
</div>
</div>
</div>
</div>
<script>
// Ponovno inicijaliziramo klijentsku skriptu za klikove na kartice filtera
import { initFilters } from "../scripts/filters.js";
// Prvo pokretanje
initFilters();
<span id="rn-bridge" data-limit={limit} data-naslov={naslov}></span>
// Podrška za Astro View Transitions
document.addEventListener('astro:after-swap', initFilters);
<script>
import { fetchDashboardData } from "../lib/api";
import { getStatusColorClass, formatStatus } from "../utils/ui";
import { initFilters } from "../scripts/filters.js";
async function renderirajRadneNalogeKlijentski() {
const container = document.getElementById('nalozi-list');
const sekcija = document.getElementById('radni-nalozi-sekcija');
const bridge = document.getElementById('rn-bridge');
if (!container || !sekcija || !bridge) return;
// 1. KLIJENTSKI OSIGURAČ: Provjera autorizacije prije API poziva preko ključa iz tvojeg api.js
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
if (!token) {
// Ako nema tokena, odmah zaključaj listu i baci sigurnosni Error izgled
sekcija.setAttribute('data-state', 'error');
container.innerHTML = `
<div class="p-20 text-center flex flex-col items-center justify-center gap-3">
<div class="w-12 h-12 bg-red-600/10 text-red-600 rounded-xl flex items-center justify-center text-lg mb-2">
<i class="fa-solid fa-shield-halved"></i>
</div>
<h4 class="text-xs font-black uppercase tracking-widest text-red-600">Pristup odbijen</h4>
<p class="text-[10px] text-gray-400 font-bold uppercase tracking-wider italic">Niste autorizirani za pregled operativnih naloga flote.</p>
</div>
`;
return;
}
const limit = parseInt(bridge.getAttribute('data-limit') || '0');
const originalNaslov = bridge.getAttribute('data-naslov') || 'Zadnje aktivnosti';
const urlParams = new URLSearchParams(window.location.search);
const statusFilter = urlParams.get('status')?.toLowerCase();
try {
// Svaki put kad klijent pokrene funkciju, osiguravamo loading stanje
sekcija.setAttribute('data-state', 'loading');
// 2. Dohvat podataka (Šalje fetch zahtjev s ispravnim Authorization zaglavljem)
const data = await fetchDashboardData();
const nalozi = data?.nalozi || [];
// 3. Proračun statistike na klijentskoj strani
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
const zavrsenoCount = nalozi.filter(n => ['zavrsen', 'završeno', 'gotovo', 'zavrseno'].includes(n.status?.toLowerCase())).length;
// 4. 🛠️ POPRAVLJENO: Upisivanje vrijednosti i inteligentno sakrivanje samo ako je brojač točno 0
const card1 = sekcija.querySelector('#card-filter1');
if (card1) {
const val = card1.querySelector('.count-value');
if (val) val.textContent = planiranoCount.toString();
if (planiranoCount > 0) {
card1.classList.remove('hidden');
} else {
card1.classList.add('hidden');
}
}
const card2 = sekcija.querySelector('#card-filter2');
if (card2) {
const val = card2.querySelector('.count-value');
if (val) val.textContent = uRaduCount.toString();
if (uRaduCount > 0) {
card2.classList.remove('hidden');
} else {
card2.classList.add('hidden');
}
}
const card3 = sekcija.querySelector('#card-filter3');
if (card3) {
const val = card3.querySelector('.count-value');
if (val) val.textContent = zavrsenoCount.toString();
if (zavrsenoCount > 0) {
card3.classList.remove('hidden');
} else {
card3.classList.add('hidden');
}
}
// 5. Filtriranje i limitiranje niza prema klijentskom stanju URL-a
let filtriraniNalozi = statusFilter ? nalozi.filter(n => n.status?.toLowerCase() === statusFilter) : nalozi;
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
// Ažuriranje teksta glavnog naslova ovisno o aktivnom filteru
const glavniNaslov = sekcija.querySelector('h2');
if (glavniNaslov) {
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
}
// 6. 🚀 SINKRONIZACIJA PROPSA: Otključavamo kontejner unutar NaslovList.astro prebacivanjem u 'loaded'
const statCardsContainer = sekcija.querySelector('#stat-cards-container');
if (statCardsContainer) {
statCardsContainer.setAttribute('data-state', 'loaded');
}
// Isključivanje općeg loading stanja s krovne sekcije
sekcija.setAttribute('data-state', 'loaded');
// 7. Generiranje HTML kartica radnih naloga unutar tijela
if (prikazaniNalozi.length === 0) {
container.innerHTML = `<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">Nema zapisa za odabrani status</div>`;
return;
}
container.innerHTML = prikazaniNalozi.map((n) => {
const statusMalo = n.status?.toLowerCase();
const statusKlasa = getStatusColorClass(statusMalo);
const ikona = statusMalo === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice';
return `
<a href="/operativa/radni-nalozi/${n.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
<i class="fa-solid ${ikona}"></i>
</div>
<div>
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${n.broj_naloga}</h4>
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${formatStatus(statusMalo)}</p>
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
${n.klijent_naziv || 'Nepoznat klijent'} | ${n.vozilo_naziv || 'Bez vozila'} | ${n.stroj_naziv || 'Bez stroja'}
</p>
</div>
</div>
<div class="flex items-center gap-3">
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
${formatStatus(statusMalo)}
</span>
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
<i class="fa-solid fa-chevron-right text-xs"></i>
</div>
</div>
</div>
`;
}).join('');
// Aktiviraj klijentske klikove na kartice filtera
initFilters();
} catch (err) {
console.error("Kritična greška u klijentskom renderu radnih naloga:", err);
sekcija.setAttribute('data-state', 'error');
container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji operativnih podataka.</div>';
}
}
// Prvo pokretanje pri učitavanju stranice
renderirajRadneNalogeKlijentski();
// Ponovno pokretanje prilikom SPA navigacije kroz aplikaciju
document.addEventListener('astro:after-swap', renderirajRadneNalogeKlijentski);
</script>

View File

@@ -0,0 +1,242 @@
---
// src/components/ServiserTerminal.astro
import Button from "./Button.astro";
import Toast from "./Toast.astro";
---
<div class="space-y-6 group" id="serviser-terminal-sekcija" data-state="loading">
<div class="group-data-[state=loading]:flex hidden items-center gap-3 text-xs font-black text-gray-400 uppercase tracking-widest animate-pulse bg-white dark:bg-gray-800 p-8 rounded-[2rem] border border-gray-100 dark:border-gray-700/50 shadow-md">
<i class="fa-solid fa-circle-notch animate-spin text-blue-600 text-base"></i> Sinkronizacija radnog zadatka s baze...
</div>
<div id="terminal-prazno-stanje" class="hidden p-20 text-center bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-[3rem] uppercase font-black opacity-30 italic text-xs tracking-widest shadow-2xl shadow-blue-500/5">
Trenutno nemate dodijeljenih aktivnih radnih naloga na terenu.
</div>
<div id="terminal-radni-prostor" class="hidden group-data-[state=loaded]:grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 p-8 shadow-xl shadow-blue-500/5">
<div class="flex justify-between items-start gap-4 border-b border-gray-100 dark:border-gray-700/50 pb-4 mb-6">
<div>
<h3 id="rn-broj" class="text-xl font-black uppercase tracking-tight text-gray-900 dark:text-white">#RN-UČITAVANJE</h3>
<p id="rn-klijent" class="text-xs font-bold text-gray-400 mt-0.5 uppercase tracking-wider">Klijent: ...</p>
</div>
<span id="rn-status-znacka" class="text-[10px] font-black px-4 py-2 rounded-full border uppercase tracking-wider bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/20 dark:text-amber-400 dark:border-amber-800/50">
U RADU
</span>
</div>
<div class="space-y-4">
<div>
<h4 class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 mb-1 italic">Prijavljeni kvar / Pogonski stroj flote</h4>
<div id="rn-stroj-detalji" class="p-4 bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 rounded-2xl text-xs font-bold text-gray-700 dark:text-gray-300 shadow-inner">
Učitavanje podataka o stroju...
</div>
</div>
<div class="pt-4">
<Button id="btn-zatvori-rn" variant="none" class="w-full py-4 bg-emerald-600 hover:bg-emerald-700 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg shadow-emerald-500/10 active:scale-95 transition-all">
<i class="fa-solid fa-check-double mr-2"></i> Dovrši servis i aktiviraj stroj
</Button>
</div>
</div>
</div>
</div>
<div class="space-y-6">
<div id="putni-nalog-kartica" class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 p-8 shadow-xl shadow-blue-500/5 flex flex-col justify-between min-h-[300px]">
<div>
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-xl flex items-center justify-center text-md shadow-sm">
<i class="fa-solid fa-truck-ramp-box"></i>
</div>
<div>
<h4 id="pn-broj" class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">PN-UČITAVANJE</h4>
<p id="pn-vozilo" class="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Vozilo: ...</p>
</div>
</div>
<hr class="my-4 border-gray-100 dark:border-gray-700/50" />
<div class="space-y-2">
<label for="zavrsna-km" class="block text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 ml-2 italic">Završna KM Vozila</label>
<input
type="number"
id="zavrsna-km"
class="w-full px-4 py-3.5 bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-blue-600 rounded-xl outline-none text-xs font-bold text-gray-900 dark:text-white shadow-inner"
placeholder="Unesi stanje brojača (km)..."
/>
</div>
</div>
<div class="pt-6">
<Button id="btn-zatvori-pn" variant="danger" class="w-full py-3.5 rounded-2xl text-[10px] tracking-wider italic">
<i class="fa-solid fa-flag-checkered mr-2"></i> Zaključaj putni list
</Button>
</div>
</div>
</div>
</div>
<Toast />
</div>
<script>
import { fetchRadniNalozi, patchNalog } from "../lib/api";
const API_BASE = import.meta.env.PUBLIC_API_URL;
let aktivniNalog: any = null;
async function inicijalizirajTerminalServisera() {
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
const sekcija = document.getElementById('serviser-terminal-sekcija');
const radniProstor = document.getElementById('terminal-radni-prostor');
const praznoStanje = document.getElementById('terminal-prazno-stanje');
if (!token || !sekcija || !radniProstor || !praznoStanje) return;
try {
sekcija.setAttribute('data-state', 'loading');
// 🛠️ OPTIMIZIRANO: Više ne dohvaćamo korisnika klijentski jer WelcomeHeader to rješava na serveru.
// Izravno dohvaćamo radne naloge kroz postojeće REST rute
const naloziData = await fetchRadniNalozi();
// Filtriramo naloge koji su u radu
const mojiNalozi = naloziData.filter((n: any) => n.status?.toLowerCase() === 'u_radu');
if (mojiNalozi.length === 0) {
sekcija.setAttribute('data-state', 'loaded');
radniProstor.classList.add('hidden');
praznoStanje.classList.remove('hidden');
return;
}
aktivniNalog = mojiNalozi[0];
praznoStanje.classList.add('hidden');
radniProstor.classList.remove('hidden');
// Injekcija podataka u DOM
document.getElementById('rn-broj')!.textContent = `#${aktivniNalog.broj_naloga}`;
document.getElementById('rn-klijent')!.textContent = `Klijent: ${aktivniNalog.klijent_naziv || 'Opći klijent'}`;
document.getElementById('rn-stroj-detalji')!.innerHTML = `
<div class="space-y-1">
<p><span class="opacity-50 font-black">STROJ U KVARU:</span> ${aktivniNalog.stroj_naziv || 'Nije specificirano'}</p>
<p><span class="opacity-50 font-black">DODIJELJENO VOZILO:</span> ${aktivniNalog.vozilo_naziv || 'Bez servisnog vozila'}</p>
</div>
`;
// Provjera postojanja povezanog putnog naloga
const putniKartica = document.getElementById('putni-nalog-kartica');
if (putniKartica) {
if (aktivniNalog.putni_nalog) {
putniKartica.classList.remove('hidden');
document.getElementById('pn-broj')!.textContent = `PN-${aktivniNalog.putni_nalog}`;
document.getElementById('pn-vozilo')!.textContent = `Vozilo: ${aktivniNalog.vozilo_naziv || 'Servisni kombi'}`;
} else {
putniKartica.classList.add('hidden');
}
}
// Prebacivanje u loaded stanje -> Tailwind CSS automatski pali radni prostor
sekcija.setAttribute('data-state', 'loaded');
} catch (err) {
console.error("Krah servisnog terminala:", err);
sekcija.setAttribute('data-state', 'error');
}
}
// --- AKCIJA A: ZATVARANJE PUTNOG NALOGA (PATCH na osnovnu stazu) ---
document.getElementById('btn-zatvori-pn')?.addEventListener('click', async (e) => {
const btn = e.currentTarget as HTMLButtonElement;
const kmInput = document.getElementById('zavrsna-km') as HTMLInputElement;
const km = kmInput?.value;
if (!km || parseInt(km, 10) <= 0) {
window.showToast?.("Unesite ispravnu završnu kilometražu vozila.", "error");
return;
}
btn.disabled = true;
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Knjiženje kilometara...';
const url = `${API_BASE}${API_BASE.endsWith('/') ? '' : '/'}operativa/putni-nalozi/${aktivniNalog.putni_nalog}/`;
try {
const res = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({
zavrsna_km: parseInt(km, 10),
status: 'završen'
})
});
if (res.ok) {
window.showToast?.("Putni nalog uspješno zaključen i arhiviran.", "success");
document.getElementById('putni-nalog-kartica')?.classList.add('opacity-40');
btn.classList.add('hidden');
kmInput.disabled = true;
} else {
window.showToast?.("Greška pri zatvaranju putnog lista.", "error");
}
} catch {
window.showToast?.("Mrežna greška u komunikaciji s DRF-om.", "error");
} {
btn.disabled = false;
btn.innerHTML = originalText;
}
});
// --- AKCIJA B: ZATVARANJE RADNOG NALOGA + PROMJENA STATUSI STROJA ---
document.getElementById('btn-zatvori-rn')?.addEventListener('click', async (e) => {
const btn = e.currentTarget as HTMLButtonElement;
btn.disabled = true;
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Zatvaranje naloga i ažuriranje flote...';
// 1. Koristimo postojeću metodu patchNalog za prebacivanje radnog naloga u 'zavrsen'
const resNalog = await patchNalog(aktivniNalog.id, { status: 'zavrsen' });
if (resNalog) {
// 2. REST LANČANA REAKCIJA: Šaljemo PATCH izravno na stazu stroja i prebacujemo ga u 'aktivno'
if (aktivniNalog.stroj) {
const urlStroj = `${API_BASE}${API_BASE.endsWith('/') ? '' : '/'}fleet/strojevi/${aktivniNalog.stroj}/`;
await fetch(urlStroj, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({ status: 'aktivno' }) // Stroj je popravljen i spreman za pogon
});
}
window.showToast?.("Radni nalog dovršen. Stroj je uspješno pušten u rad!", "success");
// Re-render cijelog terminala (prebaciće u prazno stanje jer više nema naloga 'u_radu')
setTimeout(() => inicijalizirajTerminalServisera(), 1200);
} else {
window.showToast?.("Sinkronizacija s DRF poslužiteljem nije uspjela.", "error");
btn.disabled = false;
btn.innerHTML = originalText;
}
});
// Re-inicijalizacija i stabilnost kroz View Transitions SPA navigaciju
inicijalizirajTerminalServisera();
document.addEventListener('astro:after-swap', inicijalizirajTerminalServisera);
</script>