ne radi vite server
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
161
001.FRONTEND/src/components/PutniNalogLista.astro
Normal file
161
001.FRONTEND/src/components/PutniNalogLista.astro
Normal 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>
|
||||
@@ -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>
|
||||
242
001.FRONTEND/src/components/ServiserTerminal.astro
Normal file
242
001.FRONTEND/src/components/ServiserTerminal.astro
Normal 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>
|
||||
Reference in New Issue
Block a user