v004
This commit is contained in:
1288
001.FRONTEND/package-lock.json
generated
1288
001.FRONTEND/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,11 @@
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev": "astro dev --host",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
"astro": "astro",
|
||||
"start": "node ./dist/server/entry.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^10.0.6",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Button from './Button.astro';
|
||||
|
||||
interface Props {
|
||||
tip: 'dashboard' | 'radni-nalog' | 'stroj' | 'vlasnik';
|
||||
tip: 'dashboard' | 'radni-nalog' | 'stroj' | 'vlasnik' | 'vozilo'; // DODANO: vozilo
|
||||
podaci: any;
|
||||
}
|
||||
|
||||
@@ -16,117 +16,138 @@ const formatDate = (date: string) =>
|
||||
---
|
||||
|
||||
<aside class="space-y-6">
|
||||
<!-- SEKCIJA 1: GLAVNE AKCIJE (Uvijek vidljivo) -->
|
||||
<div class="grid gap-3">
|
||||
{tip === 'dashboard' && (
|
||||
<>
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight italic px-4">
|
||||
Upravljanje
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<a href="/operativa/radni-nalozi/novi" class="group bg-blue-600 text-white p-6 rounded-[2rem] flex items-center gap-4 hover:bg-blue-700 transition-all shadow-xl shadow-blue-500/20 active:scale-95">
|
||||
<div class="bg-white text-blue-600 w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg group-hover:rotate-12 transition-transform shrink-0">
|
||||
<i class="fa-solid fa-plus text-xl"></i>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span class="font-black uppercase text-xs tracking-widest leading-none">Novi nalog</span>
|
||||
<span class="text-[10px] opacity-70 mt-1 italic font-bold uppercase">Inicijaliziraj nalog</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="grid gap-3">
|
||||
{tip === 'dashboard' && (
|
||||
<>
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight italic px-4">
|
||||
Upravljanje
|
||||
</h2>
|
||||
<a href="/fleet/vozila" class="group bg-white dark:bg-gray-800 text-gray-900 dark:text-white p-6 rounded-[2rem] flex items-center gap-4 hover:border-blue-600 border border-gray-100 dark:border-gray-700 transition-all shadow-sm active:scale-95">
|
||||
<div class="bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 w-12 h-12 rounded-2xl flex items-center justify-center group-hover:rotate-12 transition-transform shrink-0">
|
||||
<i class="fa-solid fa-truck-pickup text-xl"></i>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span class="font-black uppercase text-xs tracking-widest dark:text-white leading-none">Vozni park</span>
|
||||
<span class="text-[10px] text-gray-400 mt-1 italic font-bold uppercase tracking-tighter">Status flote</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/kupci/svi" class="group bg-white dark:bg-gray-800 text-gray-900 dark:text-white p-6 rounded-[2rem] flex items-center gap-4 hover:border-indigo-600 border border-gray-100 dark:border-gray-700 transition-all shadow-sm active:scale-95">
|
||||
<div class="bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 w-12 h-12 rounded-2xl flex items-center justify-center group-hover:rotate-12 transition-transform shrink-0">
|
||||
<i class="fa-solid fa-address-book text-xl"></i>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span class="font-black uppercase text-xs tracking-widest dark:text-white leading-none">Klijenti</span>
|
||||
<span class="text-[10px] text-gray-400 mt-1 italic font-bold uppercase tracking-tighter">Baza partnera</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button variant="primary" class="w-full justify-center gap-2 !rounded-2xl py-4 uppercase font-black italic tracking-widest">
|
||||
<i class="fa-solid fa-print"></i> Ispis naloga
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tip === 'radni-nalog' && (
|
||||
<>
|
||||
<button class="w-full py-4 bg-blue-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-blue-700 transition-all shadow-lg shadow-blue-500/20">
|
||||
<i class="fa-solid fa-check-double mr-2"></i> Završi nalog
|
||||
</button>
|
||||
<button class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-gray-200 transition-all">
|
||||
<i class="fa-solid fa-print mr-2"></i> Ispis (PDF)
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tip === 'stroj' && (
|
||||
<>
|
||||
<button class="w-full py-4 bg-green-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-green-700 transition-all shadow-lg shadow-green-500/20">
|
||||
<i class="fa-solid fa-plus mr-2"></i> Novi Radni Nalog
|
||||
</button>
|
||||
<button class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest">
|
||||
<i class="fa-solid fa-file-contract mr-2"></i> Atesti i Dokumenti
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tip === 'vlasnik' && (
|
||||
<>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[2.5rem] p-6 shadow-xl border border-gray-100 dark:border-gray-700 mb-6">
|
||||
<label class="text-[9px] font-black uppercase text-indigo-600 block mb-4 tracking-[0.2em] italic">Upravljanje klijentom</label>
|
||||
<div class="grid gap-3">
|
||||
<Button class="w-full py-4 bg-indigo-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-indigo-700 transition-all shadow-lg shadow-indigo-500/20">
|
||||
<i class="fa-solid fa-pen-to-square mr-2"></i> Uredi podatke
|
||||
</Button>
|
||||
<a href={`tel:${podaci.telefon}`} class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-gray-200 transition-all text-center no-underline">
|
||||
<i class="fa-solid fa-phone mr-2"></i> Nazovi klijenta
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-indigo-50/50 dark:bg-indigo-900/10 rounded-[2.5rem] p-6 border border-dashed border-indigo-200 dark:border-indigo-800">
|
||||
<label class="text-[9px] font-black uppercase text-indigo-400 block mb-4 tracking-[0.2em] italic">Podaci o tvrtki</label>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col border-b border-indigo-100/50 dark:border-indigo-900/30 pb-2">
|
||||
<span class="text-[9px] font-bold text-gray-400 uppercase">OIB</span>
|
||||
<span class="text-sm font-black dark:text-gray-200 tracking-tighter">{podaci.oib}</span>
|
||||
</div>
|
||||
<div class="flex flex-col border-b border-indigo-100/50 dark:border-indigo-900/30 pb-2">
|
||||
<span class="text-[9px] font-bold text-gray-400 uppercase">Adresa</span>
|
||||
<span class="text-sm font-black dark:text-gray-200 leading-tight">{podaci.adresa}, {podaci.grad}</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[9px] font-bold text-gray-400 uppercase">Email</span>
|
||||
<span class="text-sm font-black text-indigo-600 truncate">{podaci.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tip === 'vozilo' && (
|
||||
<>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[2.5rem] p-6 shadow-xl border border-gray-100 dark:border-gray-700 mb-2">
|
||||
<label class="text-[9px] font-black uppercase text-blue-600 block mb-4 tracking-[0.2em] italic">Upravljanje vozilom</label>
|
||||
<div class="grid gap-3">
|
||||
<Button
|
||||
id="btn-brzi-servis"
|
||||
variant="primary"
|
||||
class="w-full justify-center py-4 rounded-2xl font-black uppercase text-[11px] tracking-widest shadow-lg shadow-blue-500/20"
|
||||
data-id={podaci.id}
|
||||
>
|
||||
<i class="fa-solid fa-screwdriver-wrench mr-2"></i> Otvori Servis
|
||||
</Button>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<a href="/operativa/radni-nalozi/novi" class="group bg-blue-600 text-white p-6 rounded-[2rem] flex items-center gap-4 hover:bg-blue-700 transition-all shadow-xl shadow-blue-500/20 active:scale-95">
|
||||
<div class="bg-white text-blue-600 w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg group-hover:rotate-12 transition-transform shrink-0">
|
||||
<i class="fa-solid fa-plus text-xl"></i>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span class="font-black uppercase text-xs tracking-widest leading-none">Novi nalog</span>
|
||||
<span class="text-[10px] opacity-70 mt-1 italic font-bold uppercase">Inicijaliziraj nalog</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/fleet/vozila" class="group bg-white dark:bg-gray-800 text-gray-900 dark:text-white p-6 rounded-[2rem] flex items-center gap-4 hover:border-blue-600 border border-gray-100 dark:border-gray-700 transition-all shadow-sm active:scale-95">
|
||||
<div class="bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 w-12 h-12 rounded-2xl flex items-center justify-center group-hover:rotate-12 transition-transform shrink-0">
|
||||
<i class="fa-solid fa-truck-pickup text-xl"></i>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span class="font-black uppercase text-xs tracking-widest dark:text-white leading-none">Vozni park</span>
|
||||
<span class="text-[10px] text-gray-400 mt-1 italic font-bold uppercase tracking-tighter">Status flote</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/kupci/svi" class="group bg-white dark:bg-gray-800 text-gray-900 dark:text-white p-6 rounded-[2rem] flex items-center gap-4 hover:border-indigo-600 border border-gray-100 dark:border-gray-700 transition-all shadow-sm active:scale-95">
|
||||
<div class="bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 w-12 h-12 rounded-2xl flex items-center justify-center group-hover:rotate-12 transition-transform shrink-0">
|
||||
<i class="fa-solid fa-address-book text-xl"></i>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span class="font-black uppercase text-xs tracking-widest dark:text-white leading-none">Klijenti</span>
|
||||
<span class="text-[10px] text-gray-400 mt-1 italic font-bold uppercase tracking-tighter">Baza partnera</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button variant="primary" class="w-full justify-center gap-2 !rounded-2xl py-4 uppercase font-black italic tracking-widest">
|
||||
<i class="fa-solid fa-print"></i> Ispis naloga
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
href={`/operativa/radni-nalozi?vozilo=${podaci.id}`}
|
||||
variant="secondary"
|
||||
class="w-full justify-center py-4 rounded-2xl font-black uppercase text-[11px] tracking-widest"
|
||||
>
|
||||
<i class="fa-solid fa-file-invoice mr-2 opacity-70"></i> Radni Nalozi
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tip === 'radni-nalog' && (
|
||||
<>
|
||||
<button class="w-full py-4 bg-blue-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-blue-700 transition-all shadow-lg shadow-blue-500/20">
|
||||
<i class="fa-solid fa-check-double mr-2"></i> Završi nalog
|
||||
</button>
|
||||
<button class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-gray-200 transition-all">
|
||||
<i class="fa-solid fa-print mr-2"></i> Ispis (PDF)
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tip === 'stroj' && (
|
||||
<>
|
||||
<button class="w-full py-4 bg-green-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-green-700 transition-all shadow-lg shadow-green-500/20">
|
||||
<i class="fa-solid fa-plus mr-2"></i> Novi Radni Nalog
|
||||
</button>
|
||||
<button class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest">
|
||||
<i class="fa-solid fa-file-contract mr-2"></i> Atesti i Dokumenti
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tip === 'vlasnik' && (
|
||||
<>
|
||||
<!-- AKCIJE ZA KUPCA -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[2.5rem] p-6 shadow-xl border border-gray-100 dark:border-gray-700 mb-6">
|
||||
<label class="text-[9px] font-black uppercase text-indigo-600 block mb-4 tracking-[0.2em] italic">Upravljanje klijentom</label>
|
||||
<div class="grid gap-3">
|
||||
<Button class="w-full py-4 bg-indigo-600 text-white rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-indigo-700 transition-all shadow-lg shadow-indigo-500/20">
|
||||
<i class="fa-solid fa-pen-to-square mr-2"></i> Uredi podatke
|
||||
</Button>
|
||||
<a href={`tel:${podaci.telefon}`} class="w-full py-4 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-2xl font-black uppercase text-[11px] tracking-widest hover:bg-gray-200 transition-all text-center no-underline">
|
||||
<i class="fa-solid fa-phone mr-2"></i> Nazovi klijenta
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DETALJI TVRTKE (Metadata preseljena s glavnog ekrana) -->
|
||||
<div class="bg-indigo-50/50 dark:bg-indigo-900/10 rounded-[2.5rem] p-6 border border-dashed border-indigo-200 dark:border-indigo-800">
|
||||
<label class="text-[9px] font-black uppercase text-indigo-400 block mb-4 tracking-[0.2em] italic">Podaci o tvrtki</label>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col border-b border-indigo-100/50 dark:border-indigo-900/30 pb-2">
|
||||
<span class="text-[9px] font-bold text-gray-400 uppercase">OIB</span>
|
||||
<span class="text-sm font-black dark:text-gray-200 tracking-tighter">{podaci.oib}</span>
|
||||
</div>
|
||||
<div class="flex flex-col border-b border-indigo-100/50 dark:border-indigo-900/30 pb-2">
|
||||
<span class="text-[9px] font-bold text-gray-400 uppercase">Adresa</span>
|
||||
<span class="text-sm font-black dark:text-gray-200 leading-tight">{podaci.adresa}, {podaci.grad}</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[9px] font-bold text-gray-400 uppercase">Email</span>
|
||||
<span class="text-sm font-black text-indigo-600 truncate">{podaci.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
<!-- SEKCIJA 2: KONTEKSTUALNI DETALJI (Ovdje selimo manje bitne stvari) -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900/40 rounded-[2.5rem] p-6 border border-dashed border-gray-200 dark:border-gray-700">
|
||||
<label class="text-[9px] font-black uppercase text-gray-400 block mb-4 tracking-[0.2em] italic">Informacije sustava</label>
|
||||
|
||||
@@ -134,16 +155,12 @@ const formatDate = (date: string) =>
|
||||
{tip === 'radni-nalog' && (
|
||||
<>
|
||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Prioritet</span>
|
||||
<span class="text-xs font-black text-red-500 uppercase italic">Visok</span>
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Status</span>
|
||||
<span class="text-xs font-black text-red-500 uppercase italic">{podaci.status}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Izradio</span>
|
||||
<span class="text-xs font-black dark:text-gray-200">Admin Sustava</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Ažurirano</span>
|
||||
<span class="text-xs font-black dark:text-gray-200">{formatDate(podaci.datum_azuriranja)}</span>
|
||||
<span class="text-xs font-black dark:text-gray-200">{podaci.izvrsitelj_ime}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -164,6 +181,19 @@ const formatDate = (date: string) =>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tip === 'vozilo' && (
|
||||
<>
|
||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">Sustav ID</span>
|
||||
<span class="text-xs font-black dark:text-gray-200">#{podaci.id}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-end border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase">DB Status</span>
|
||||
<span class="text-xs font-black text-blue-500 uppercase italic">{podaci.status}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -8,9 +8,26 @@ const apiEvents = await fetchCalendarEvents();
|
||||
|
||||
// 2. Grupiranje događaja po datumu (YYYY-MM-DD)
|
||||
const eventsByDate = apiEvents.reduce((acc, event) => {
|
||||
if (!event.start) return acc;
|
||||
|
||||
const dateKey = new Date(event.start).toISOString().split('T')[0];
|
||||
if (!acc[dateKey]) acc[dateKey] = [];
|
||||
acc[dateKey].push(event);
|
||||
|
||||
// Koristimo čista, razdvojena polja s novog Django serializera
|
||||
acc[dateKey].push({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
opis: event.opis_cisti, // Čisti opis kvara s backenda
|
||||
start: event.start,
|
||||
tip: (event.tip || event.status || 'planirano').toLowerCase(),
|
||||
radni_nalog: event.radni_nalog || event.id,
|
||||
je_radni_nalog: event.je_radni_nalog !== undefined ? event.je_radni_nalog : true,
|
||||
izvrsitelj_ime: event.izvrsitelj_ime,
|
||||
klijent: event.klijent || null,
|
||||
stroj: event.stroj || null,
|
||||
vozilo: event.vozilo_naziv // Čisti naziv vozila s backenda
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@@ -70,18 +87,13 @@ const currentYearMonth = "2026-05";
|
||||
<div id="panel-arrow" class="absolute w-0 h-0 border-l-[12px] border-l-transparent border-r-[12px] border-r-transparent border-b-[12px] border-b-blue-600 transition-all duration-300"></div>
|
||||
</div>
|
||||
<div id="event-list" class="flex flex-col gap-6 w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatStatusLocal(status) {
|
||||
const map = { 'u_radu': 'U radu', 'planirano': 'Planirano', 'zavrseno': 'Završeno', 'servis': 'Na servisu' };
|
||||
return map[status] || status;
|
||||
}
|
||||
|
||||
function initKalendar() {
|
||||
const grid = document.getElementById('days-grid');
|
||||
const panel = document.getElementById('event-panel');
|
||||
@@ -99,44 +111,72 @@ const currentYearMonth = "2026-05";
|
||||
const allItems = Array.from(grid.children);
|
||||
const index = allItems.indexOf(cell);
|
||||
|
||||
// 1. Pozicioniranje strelice (Matematički precizno)
|
||||
// 1. Pozicioniranje strelice
|
||||
const colIndex = index % 7;
|
||||
const percentage = (colIndex * (100 / 7)) + (100 / 7 / 2);
|
||||
arrow.style.left = `calc(${percentage}% - 12px)`;
|
||||
|
||||
// 2. Injekcija panela na kraj tjedna
|
||||
// 2. Injekcija panela na kraj tjedna (redka)
|
||||
const rowEndIndex = Math.floor(index / 7) * 7 + 6;
|
||||
const targetCell = allItems[Math.min(rowEndIndex, allItems.length - 1)];
|
||||
|
||||
// 3. Generiranje liste (text-left poravnanje)
|
||||
eventList.innerHTML = eventsData.map(ev => `
|
||||
<div class="flex items-start gap-5 border-b border-white/20 pb-6 last:border-0 last:pb-0 group/item">
|
||||
<a href="/operativa/radni-nalozi/${ev.radni_nalog}" class="bg-white/10 hover:bg-white hover:text-blue-600 text-white p-4 rounded-2xl transition-all shadow-lg shrink-0 group-hover/item:scale-105 active:scale-95">
|
||||
<i class="fa-solid ${ev.je_radni_nalog ? 'fa-file-signature' : 'fa-wrench'} text-2xl"></i>
|
||||
</a>
|
||||
<div class="flex flex-col flex-1 text-left">
|
||||
<div class="flex flex-wrap justify-between items-start gap-2">
|
||||
<a href="/operativa/radni-nalozi/${ev.radni_nalog}" class="hover:underline">
|
||||
<span class="font-black uppercase text-2xl tracking-tighter italic leading-none">${ev.title}</span>
|
||||
</a>
|
||||
<span class="text-[10px] font-black bg-white/20 px-3 py-1 rounded-full uppercase tracking-widest border border-white/10">
|
||||
${formatStatusLocal(ev.tip)}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm opacity-90 font-medium mt-2 leading-relaxed max-w-2xl text-left w-full">
|
||||
// 3. Generiranje liste (Struktura točno prema tvojoj ispravno.png skici)
|
||||
eventList.innerHTML = eventsData.map(ev => {
|
||||
const klijentStroj = [ev.klijent, ev.stroj].filter(Boolean).join(' | ');
|
||||
const infoLinija = klijentStroj
|
||||
? `<div class="text-[13px] opacity-80 font-medium tracking-tight mt-1"><i class="fa-solid fa-industry text-[11px] mr-1 opacity-60"></i> ${klijentStroj}</div>`
|
||||
: '';
|
||||
|
||||
// Čisti naziv vozila u zasebnom redu (bez labela "Vozilo:")
|
||||
const voziloRed = ev.vozilo
|
||||
? `<p class="text-sm font-medium mt-1 text-white opacity-90">
|
||||
${ev.vozilo}
|
||||
</p>`
|
||||
: '';
|
||||
|
||||
// ISTAKNUTO: Opis kvara u posebnom uokvirenom kontejneru radi boljeg kontrasta
|
||||
const opisRed = `
|
||||
<div class="mt-3 p-3 bg-black/15 rounded-xl border border-white/10 text-left">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-amber-300 opacity-90 mb-1">
|
||||
Opis kvara:
|
||||
</p>
|
||||
<p class="text-sm font-medium text-white leading-relaxed">
|
||||
${ev.opis}
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-3">
|
||||
<span class="text-[10px] font-black uppercase tracking-widest bg-black/30 px-3 py-1.5 rounded-lg flex items-center gap-2">
|
||||
<i class="fa-solid fa-user-gear opacity-50"></i> ${ev.izvrsitelj_ime}
|
||||
</span>
|
||||
<span class="text-[10px] font-black uppercase tracking-widest bg-black/30 px-3 py-1.5 rounded-lg flex items-center gap-2">
|
||||
<i class="fa-solid fa-clock opacity-50"></i> ${new Date(ev.start).toLocaleTimeString('hr-HR', {hour: '2-digit', minute:'2-digit'})} h
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="flex items-start gap-5 border-b border-white/20 pb-6 last:border-0 last:pb-0 group/item">
|
||||
<a href="/operativa/radni-nalozi/${ev.radni_nalog}" class="bg-white/10 hover:bg-white hover:text-blue-600 text-white p-4 rounded-2xl transition-all shadow-lg shrink-0 group-hover/item:scale-105 active:scale-95">
|
||||
<i class="fa-solid ${ev.je_radni_nalog ? 'fa-file-signature' : 'fa-wrench'} text-2xl"></i>
|
||||
</a>
|
||||
<div class="flex flex-col flex-1 text-left">
|
||||
|
||||
<div class="flex justify-between items-start">
|
||||
<a href="/operativa/radni-nalozi/${ev.radni_nalog}" class="hover:underline">
|
||||
<span class="font-black uppercase text-2xl tracking-tighter italic leading-none">${ev.title}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
${infoLinija}
|
||||
|
||||
${voziloRed}
|
||||
|
||||
${opisRed}
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-3">
|
||||
<span class="text-[10px] font-black uppercase tracking-widest bg-black/30 px-3 py-1.5 rounded-lg flex items-center gap-2">
|
||||
<i class="fa-solid fa-user-gear opacity-50"></i> ${ev.izvrsitelj_ime}
|
||||
</span>
|
||||
<span class="text-[10px] font-black uppercase tracking-widest bg-black/30 px-3 py-1.5 rounded-lg flex items-center gap-2">
|
||||
<i class="fa-solid fa-clock opacity-50"></i> ${new Date(ev.start).toLocaleTimeString('hr-HR', {hour: '2-digit', minute:'2-digit'})} h
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
targetCell.after(panel);
|
||||
panel.classList.remove('hidden');
|
||||
|
||||
@@ -17,22 +17,23 @@ const {
|
||||
prikaziNaslov = true,
|
||||
} = Astro.props;
|
||||
|
||||
// 1. Dohvat filtera iz URL-a (npr. ?status=u_radu)
|
||||
const statusFilter = Astro.url.searchParams.get('status');
|
||||
// 1. Dohvat filtera iz URL-a (pretvaramo u mala slova radi sigurnosti)
|
||||
const statusFilter = Astro.url.searchParams.get('status')?.toLowerCase();
|
||||
|
||||
// 2. Dohvat podataka s API-ja
|
||||
const { nalozi = [] } = await fetchDashboardData();
|
||||
|
||||
// 3. Logika filtriranja (Serverska strana)
|
||||
// 3. Logika filtriranja (Serverska strana - tolerantna na velika/mala slova)
|
||||
let filtriraniNalozi = nalozi;
|
||||
|
||||
if (statusFilter) {
|
||||
filtriraniNalozi = nalozi.filter(n => n.status === statusFilter);
|
||||
filtriraniNalozi = nalozi.filter(n => n.status?.toLowerCase() === statusFilter);
|
||||
}
|
||||
|
||||
// 4. Izračun brojeva za NaslovList (uvijek se računa iz originalne liste nalozi)
|
||||
const planiranoCount = nalozi.filter(n => n.status === 'planirano').length;
|
||||
const uRaduCount = nalozi.filter(n => n.status === 'u_radu').length;
|
||||
// 4. POPRAVLJENO: Izračun brojeva uz obvezno pretvaranje u mala slova (.toLowerCase())
|
||||
// Na ovaj način 'PLANIRANO' s backenda sigurno postaje 'planirano' u JS-u
|
||||
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
||||
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
||||
|
||||
// 5. Primjena limita na prikaz (npr. top 5 nalozi)
|
||||
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
|
||||
@@ -49,10 +50,10 @@ const prikazaniNaslov = statusFilter
|
||||
naslov={prikazaniNaslov}
|
||||
ukupno={filtriraniNalozi.length}
|
||||
label1="Planirano"
|
||||
count1={planiranoCount}
|
||||
count1={planiranoCount} // Sada će ispravno prikazati stvarni broj
|
||||
filter1="planirano"
|
||||
label2="U radu"
|
||||
count2={uRaduCount}
|
||||
count2={uRaduCount} // Sada će ispravno prikazati stvarni broj
|
||||
filter2="u_radu"
|
||||
/>
|
||||
)}
|
||||
@@ -63,13 +64,13 @@ const prikazaniNaslov = statusFilter
|
||||
prikazaniNalozi.map((n) => (
|
||||
<GenericKarticaItem
|
||||
href={`/operativa/radni-nalozi/${n.id}`}
|
||||
status={n.status}
|
||||
statusBojaClass={getStatusColorClass(n.status)}
|
||||
ikona={n.status === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice'}
|
||||
status={n.status?.toLowerCase()} // Šaljemo ujednačeni lowercase u podkomponente
|
||||
statusBojaClass={getStatusColorClass(n.status?.toLowerCase())}
|
||||
ikona={n.status?.toLowerCase() === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice'}
|
||||
naslov={`#${n.broj_naloga}`}
|
||||
subNaslov={formatStatus(n.status)}
|
||||
metaTekst={`${n.klijent_naziv} | ${n.vozilo_naziv} | ${n.stroj_naziv}`}
|
||||
statusPrikaz={formatStatus(n.status)}
|
||||
subNaslov={formatStatus(n.status?.toLowerCase())}
|
||||
metaTekst={`${n.klijent_naziv || 'Nepoznat klijent'} | ${n.vozilo_naziv || 'Bez vozila'} | ${n.stroj_naziv || 'Bez stroja'}`}
|
||||
statusPrikaz={formatStatus(n.status?.toLowerCase())}
|
||||
bojaTeme={n.boja_teme || 'blue'}
|
||||
/>
|
||||
))
|
||||
@@ -89,6 +90,6 @@ const prikazaniNaslov = statusFilter
|
||||
// Prvo pokretanje
|
||||
initFilters();
|
||||
|
||||
// Podrška za Astro View Transitions (ako ih koristiš)
|
||||
// Podrška za Astro View Transitions
|
||||
document.addEventListener('astro:after-swap', initFilters);
|
||||
</script>
|
||||
468
001.FRONTEND/src/lib/api.js
Normal file
468
001.FRONTEND/src/lib/api.js
Normal file
@@ -0,0 +1,468 @@
|
||||
// src/lib/api.js
|
||||
const API_BASE = import.meta.env.PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* POMOĆNA FUNKCIJA:端 Dohvaća token i postavlja Headere
|
||||
*/
|
||||
function getAuthHeaders(bodyData = {}) {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
||||
const headers = {};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`; // Usklađeno sa Simple JWT standardom
|
||||
}
|
||||
|
||||
// Provjera je li bodyData FormData (za slike/naloge)
|
||||
const isFormData = bodyData instanceof FormData;
|
||||
|
||||
// Ako NIJE FormData, šaljemo JSON
|
||||
if (!isFormData) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
/**
|
||||
* Centralizirana obrada odgovora s Toast podrškom
|
||||
*/
|
||||
async function handleResponse(res) {
|
||||
if (res.status === 401) {
|
||||
console.warn("Token istekao ili je nevažeći.");
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
console.error("API Error Response:", errorData);
|
||||
|
||||
let msg = "Greška pri sinkronizaciji podataka.";
|
||||
|
||||
if (errorData.detail) {
|
||||
msg = errorData.detail;
|
||||
}
|
||||
else if (typeof errorData === 'object' && errorData !== null) {
|
||||
const kljuceviGresaka = Object.keys(errorData);
|
||||
|
||||
if (kljuceviGresaka.length > 0) {
|
||||
const prvoPolje = kljuceviGresaka[0];
|
||||
const greskaVrijednost = errorData[prvoPolje];
|
||||
|
||||
if (Array.isArray(greskaVrijednost) && greskaVrijednost.length > 0) {
|
||||
msg = `${prvoPolje}: ${greskaVrijednost[0]}`;
|
||||
} else if (typeof greskaVrijednost === 'string') {
|
||||
msg = `${prvoPolje}: ${greskaVrijednost}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.showToast?.(msg, "error");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// VRAĆA PARSIRAN OBJEKT - Stream je zatvoren nakon ove linije!
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
// --- RUTE ---
|
||||
export const routes = {
|
||||
// Autentifikacija
|
||||
login: () => '/token/',
|
||||
trenutniKorisnik: () => '/users/me/',
|
||||
|
||||
// Radni nalozi
|
||||
radniNalozi: (params = {}) => {
|
||||
const baseUrl = '/operativa/radni-nalozi/';
|
||||
// Čistimo null i undefined vrijednosti iz filtera
|
||||
const cleanParams = Object.fromEntries(
|
||||
Object.entries(params).filter(([_, v]) => v != null)
|
||||
);
|
||||
const queryString = new URLSearchParams(cleanParams).toString();
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
},
|
||||
radniNalogDetalji: (id) => `/operativa/radni-nalozi/${id}/`,
|
||||
sljedeciBrojNaloga: () => '/operativa/radni-nalozi/sljedeci-broj/',
|
||||
|
||||
// Putni nalozi i logistika
|
||||
putniNalozi: () => '/operativa/putni-nalozi/',
|
||||
|
||||
// Vozila i strojevi (Fleet modul)
|
||||
vozila: () => '/fleet/vozila/', // USKLAĐENO: Prebačeno pod zajednički /fleet/ namespace
|
||||
strojevi: (vlasnikId = null) => {
|
||||
const baseUrl = '/fleet/strojevi/';
|
||||
if (vlasnikId) return `${baseUrl}?vlasnik=${vlasnikId}`;
|
||||
return baseUrl;
|
||||
},
|
||||
strojDetalji: (id) => `/fleet/strojevi/${id}/`,
|
||||
|
||||
// Kupci / Klijenti
|
||||
kupciSvi: () => '/kupci/svi/',
|
||||
kupacDetalji: (id) => `/kupci/svi/${id}/`,
|
||||
|
||||
// Kalendar i raspored
|
||||
mojRaspored: () => '/kalendar/moj-raspored/',
|
||||
kalendarDogadaji: () => '/kalendar/dogadaji/'
|
||||
};
|
||||
|
||||
// --- API METODE ---
|
||||
|
||||
/**
|
||||
* Dohvat profila trenutno prijavljenog korisnika
|
||||
*/
|
||||
export async function fetchCurrentUser() {
|
||||
try {
|
||||
const url = `${API_BASE}${routes.trenutniKorisnik()}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
return await handleResponse(res);
|
||||
} catch (e) {
|
||||
console.error("fetchCurrentUser Failure:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat i filtriranje radnih naloga (za tablice i liste u operativi)
|
||||
*/
|
||||
export async function fetchRadniNalozi(params = {}) {
|
||||
try {
|
||||
// Logika čišćenja i slaganja query parametara je sada delegirana routes objektu!
|
||||
const url = `${API_BASE}${routes.radniNalozi(params)}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
return await handleResponse(res);
|
||||
} catch (e) {
|
||||
console.error("fetchRadniNalozi Failure:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paralelni dohvat podataka za glavno dispečersko sučelje (Dashboard flote i naloga)
|
||||
*/
|
||||
export async function fetchDashboardData() {
|
||||
try {
|
||||
// Dinamički povlačimo staze iz routes objekta za paralelno okidanje
|
||||
const urlVozila = `${API_BASE}${routes.vozila()}`;
|
||||
const urlNalozi = `${API_BASE}${routes.radniNalozi()}`;
|
||||
|
||||
const [resV, resN] = await Promise.all([
|
||||
fetch(urlVozila, { method: 'GET', headers: getAuthHeaders() }),
|
||||
fetch(urlNalozi, { method: 'GET', headers: getAuthHeaders() })
|
||||
]);
|
||||
|
||||
// Budući da Dashboard podatke često renderiraš u paralelnim karticama,
|
||||
// koristimo brzi i sigurni fallback u slučaju prazne baze ili neočekivanog formata
|
||||
const vozilaData = resV.ok ? await resV.json().catch(() => []) : [];
|
||||
const naloziData = resN.ok ? await resN.json().catch(() => []) : [];
|
||||
|
||||
return {
|
||||
vozila: Array.isArray(vozilaData) ? vozilaData : (vozilaData.results || []),
|
||||
nalozi: Array.isArray(naloziData) ? naloziData : (naloziData.results || [])
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("FetchDashboardData Failure:", e);
|
||||
return { vozila: [], nalozi: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prijava korisnika i pohrana JWT tokena
|
||||
*/
|
||||
export async function login(email, password) {
|
||||
try {
|
||||
// Povlačenje centralizirane rute za token
|
||||
const url = `${API_BASE}${routes.login()}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || "Neuspješna prijava");
|
||||
|
||||
localStorage.setItem('access_token', data.access);
|
||||
localStorage.setItem('refresh_token', data.refresh);
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
console.error("Login Error:", e);
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Odjava korisnika i čišćenje lokalne pohrane
|
||||
*/
|
||||
export function logout() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
if (typeof window !== 'undefined') window.location.href = '/login';
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat detalja pojedinačnog kupca/klijenta
|
||||
*/
|
||||
export async function fetchKupacDetalji(id) {
|
||||
try {
|
||||
const url = `${API_BASE}${routes.kupacDetalji(id)}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
return await handleResponse(res);
|
||||
} catch (e) {
|
||||
console.error(`Greška u fetchKupacDetalji za ID ${id}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unos novog servisnog vozila u bazu flote
|
||||
*/
|
||||
export async function createVozilo(payload) {
|
||||
try {
|
||||
const url = `${API_BASE}${routes.vozila()}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await handleResponse(res);
|
||||
if (data && typeof window !== 'undefined') {
|
||||
window.showToast?.("Vozilo uspješno uneseno!", "success");
|
||||
}
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error("Greška u createVozilo:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kreiranje novog radnog naloga (šalje se FormData zbog učitavanja slika s terena)
|
||||
*/
|
||||
export async function createNalog(formData) {
|
||||
try {
|
||||
// Koristimo bazičnu rutu bez parametara za POST zahtjev
|
||||
const url = `${API_BASE}${routes.radniNalozi()}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(formData), // Automatski izbacuje Content-Type za FormData
|
||||
body: formData
|
||||
});
|
||||
return await handleResponse(res);
|
||||
} catch (e) {
|
||||
console.error("Greška u createNalog:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcijalno ažuriranje radnog naloga (npr. promjena statusa, dodavanje opisa) preko centralizirane rute
|
||||
*/
|
||||
export async function patchNalog(id, data) {
|
||||
try {
|
||||
// Koristimo novu stazu za detalje naloga iz routes objekta
|
||||
const url = `${API_BASE}${routes.radniNalogDetalji(id)}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
return await handleResponse(res);
|
||||
} catch (e) {
|
||||
console.error(`Greška u patchNalog za ID ${id}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat svih kalendarskih događaja (planirani servisi, atesti, tereni)
|
||||
*/
|
||||
export async function fetchCalendarEvents() {
|
||||
try {
|
||||
// Koristimo novu stazu iz routes objekta
|
||||
const url = `${API_BASE}${routes.kalendarDogadaji()}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
// Centralizirana obrada odgovora s Toast error handlingom
|
||||
const data = await handleResponse(res);
|
||||
|
||||
if (!data) return []; // Siguran fallback ako zahtjev baci grešku
|
||||
|
||||
return Array.isArray(data) ? data : (data.results || []);
|
||||
} catch (e) {
|
||||
console.error("fetchCalendarEvents Failure:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat osobnog rasporeda servisera iz kalendara
|
||||
*/
|
||||
export async function fetchMojRaspored() {
|
||||
try {
|
||||
// Koristimo stazu iz routes objekta
|
||||
const url = `${API_BASE}${routes.mojRaspored()}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
// Centralizirana obrada odgovora s Toast error handlingom
|
||||
const data = await handleResponse(res);
|
||||
|
||||
if (!data) return []; // Siguran fallback na prazan niz ako zahtjev ne prođe
|
||||
|
||||
return Array.isArray(data) ? data : (data.results || []);
|
||||
} catch (e) {
|
||||
console.error("fetchMojRaspored Failure:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat popisa svih kupaca/klijenata za dropdown u novom nalogu
|
||||
*/
|
||||
export async function fetchKupciData() {
|
||||
try {
|
||||
// Koristimo stazu iz routes objekta
|
||||
const url = `${API_BASE}${routes.kupciSvi()}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
// Centralizirana obrada odgovora
|
||||
const data = await handleResponse(res);
|
||||
|
||||
if (!data) return { kupci: [] }; // Fallback ako je token nevažeći
|
||||
|
||||
// Vraćamo objekt u formatu koji novi.astro destrukturira na serveru
|
||||
return {
|
||||
kupci: Array.isArray(data) ? data : (data.results || [])
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("fetchKupciData Failure:", e);
|
||||
return { kupci: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat popisa strojeva (opcionalno filtrirano po vlasniku) pomoću centralizirane rute
|
||||
*/
|
||||
export async function fetchStrojeviData(vlasnikId = null) {
|
||||
try {
|
||||
// Generiramo ispravan URL preko routes objekta
|
||||
const url = `${API_BASE}${routes.strojevi(vlasnikId)}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
// Koristimo centraliziranu obradu odgovora (s Toast podrškom)
|
||||
const data = await handleResponse(res);
|
||||
|
||||
if (!data) return { strojevi: [] }; // Siguran fallback ako je zahtjev prekinut (401, 500...)
|
||||
|
||||
// Vraćamo objekt sa strojevima prateći strukturu koju novi.astro očekuje (.map destructuring)
|
||||
return {
|
||||
strojevi: Array.isArray(data) ? data : (data.results || [])
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("fetchStrojeviData Failure:", e);
|
||||
return { strojevi: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat popisa svih putnih naloga pomoću centralizirane rute i handleResponse-a
|
||||
*/
|
||||
export async function fetchPutniNaloziData() {
|
||||
try {
|
||||
// Koristimo zajedničku stazu iz routes objekta
|
||||
const url = `${API_BASE}${routes.putniNalozi()}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
// Koristimo centraliziranu obradu odgovora (s Toast error handlingom)
|
||||
const data = await handleResponse(res);
|
||||
|
||||
if (!data) return []; // Fallback ako je handleResponse presreo grešku i vratio null
|
||||
|
||||
// Provjera vraća li Django čistu listu ili paginirani rezultatski objekt (results)
|
||||
return Array.isArray(data) ? data : (data.results || []);
|
||||
} catch (e) {
|
||||
console.error("Greška pri dohvatu putnih naloga:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSljedeciBrojNaloga() {
|
||||
try {
|
||||
// Koristimo novu rutu iz routes objekta
|
||||
const url = `${API_BASE}${routes.sljedeciBrojNaloga()}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.error("Greška na backendu pri dohvaćanju brojača:", res.status);
|
||||
return { broj_naloga: "RN-2026-XXXX" };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Mrežna greška u fetchSljedeciBrojNaloga:", err);
|
||||
return { broj_naloga: "RN-2026-XXXX" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kreiranje novog putnog naloga - S koso crtom (trailing slash) za Django usklađenost
|
||||
*/
|
||||
export async function createPutniNalog(radniNalogId, voziloId) {
|
||||
try {
|
||||
// Koristimo novu stazu iz routes objekta
|
||||
const url = `${API_BASE}${routes.putniNalozi()}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
radni_nalog_id: parseInt(radniNalogId, 10),
|
||||
vozilo: parseInt(voziloId, 10)
|
||||
})
|
||||
});
|
||||
|
||||
// Vraća gotov JSON objekt (ili null ako je toast okinuo grešku)
|
||||
return await handleResponse(res);
|
||||
} catch (e) {
|
||||
console.error("createPutniNalog krah:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
156
001.FRONTEND/src/pages/fleet/vozila/[id].astro
Normal file
156
001.FRONTEND/src/pages/fleet/vozila/[id].astro
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
// src/pages/fleet/vozila/[id].astro
|
||||
import Layout from "../../../layouts/Layout.astro"; // Prilagodi putanju svom glavnom layoutu
|
||||
import { fetchCalendarEvents } from "../../../lib/api"; // Pretpostavka lokacije tvog centraliziranog API-ja
|
||||
import { getStatusColorClass } from "../../../utils/ui";
|
||||
|
||||
// 1. Dohvat ID-ja iz URL parametara
|
||||
const { id } = Astro.params;
|
||||
|
||||
// 2. Dohvat podataka s Django API-ja
|
||||
// Ovdje koristimo tvoj centralizirani API klijent. Ako imaš fetchVoziloDetails(id), iskoristi ga.
|
||||
// Kao siguran fallback, dohvaćamo sva vozila i filtriramo ono koje nam treba.
|
||||
const response = await fetch(`${Astro.url.origin}/api/fleet/vozila`); // ili izravno tvoj DRF endpoint ako lib nema pojedinačni fetch
|
||||
let vozilo = null;
|
||||
|
||||
try {
|
||||
// Simulacija dohvata preko tvog centraliziranog klijenta ili izravnog fetch-a
|
||||
// Ako tvoj api.js ima metodu npr. fetchVozilo(id), zamijeni ovo s: vozilo = await fetchVozilo(id);
|
||||
const svaVozilaResponse = await fetch("http://127.0.0.1:8000/api/fleet/vozila/"); // Primjer internog DRF endpointa
|
||||
const vozila = await svaVozilaResponse.json();
|
||||
vozilo = vozila.find((v) => v.id === parseInt(id));
|
||||
} catch (error) {
|
||||
console.error("Greška pri dohvaćanju podataka o vozilu:", error);
|
||||
}
|
||||
|
||||
// Ako vozilo ne postoji, preusmjeravamo na 404 ili na listu vozila
|
||||
if (!vozilo) {
|
||||
return Astro.redirect("/fleet/vozila?error=not-found");
|
||||
}
|
||||
|
||||
// Pomoćna funkcija za formatiranje statusa lokalno ako zakaže status_prikaz
|
||||
function formatStatusLocal(status) {
|
||||
const map = {
|
||||
'aktivan': 'Aktivan',
|
||||
'servis': 'Na servisu',
|
||||
'neaktivan': 'Izvan pogona'
|
||||
};
|
||||
return map[status?.toLowerCase()] || status;
|
||||
}
|
||||
|
||||
const statusKlasa = getStatusColorClass(vozilo.status?.toLowerCase());
|
||||
---
|
||||
|
||||
<Layout title={`Vozilo | ${vozilo.naziv}`}>
|
||||
<div class="max-w-4xl mx-auto px-4 py-8 font-sans">
|
||||
|
||||
<a
|
||||
href="/fleet/vozila"
|
||||
class="inline-flex items-center gap-2 text-sm font-black uppercase tracking-widest text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors mb-6"
|
||||
>
|
||||
<i class="fa-solid fa-arrow-left-long"></i> Povratak na popis
|
||||
</a>
|
||||
|
||||
<div class="w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-2xl rounded-[3rem] border border-gray-100 dark:border-gray-700 overflow-hidden transition-all">
|
||||
|
||||
<div class="flex flex-wrap justify-between items-center bg-gray-50/50 dark:bg-gray-900/50 px-8 py-8 border-b border-gray-100 dark:border-gray-700 gap-4">
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="bg-blue-600/10 text-blue-600 dark:text-blue-400 p-5 rounded-2xl shadow-md">
|
||||
<i class="fa-solid fa-truck text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl md:text-4xl font-black uppercase tracking-tighter italic leading-none">
|
||||
{vozilo.naziv}
|
||||
</h1>
|
||||
<span class="text-sm font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest block mt-1">
|
||||
Interni ID: #{vozilo.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class={`text-xs font-black px-4 py-2 rounded-full uppercase tracking-widest border shadow-sm ${statusKlasa}`}>
|
||||
{vozilo.status_prikaz || formatStatusLocal(vozilo.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-900/40 p-6 rounded-2xl border border-gray-100 dark:border-gray-700/50">
|
||||
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 dark:text-gray-500 block mb-1">
|
||||
Registracijska oznaka
|
||||
</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-id-card text-xl text-blue-600 dark:text-blue-400 opacity-70"></i>
|
||||
<span class="text-2xl font-black tracking-tight uppercase italic text-gray-800 dark:text-gray-100">
|
||||
{vozilo.registracija}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-900/40 p-6 rounded-2xl border border-gray-100 dark:border-gray-700/50">
|
||||
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 dark:text-gray-500 block mb-1">
|
||||
Trenutna kilometraža
|
||||
</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-gauge text-xl text-blue-600 dark:text-blue-400 opacity-70"></i>
|
||||
<span class="text-2xl font-black tracking-tight text-gray-800 dark:text-gray-100">
|
||||
{vozilo.trenutni_kilometri?.toLocaleString('hr-HR') || 0} <span class="text-sm font-bold text-gray-400 uppercase">KM</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 bg-blue-600 text-white p-6 rounded-2xl shadow-xl relative overflow-hidden group">
|
||||
<div class="absolute right-6 bottom-2 opacity-10 text-9xl font-black pointer-events-none transition-transform group-hover:scale-110 duration-500">
|
||||
<i class="fa-solid fa-route"></i>
|
||||
</div>
|
||||
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-white/60 block mb-1">
|
||||
Trenutni raspored / Baza
|
||||
</span>
|
||||
<h3 class="text-xl font-black uppercase tracking-tight italic">
|
||||
Terenska Baza
|
||||
</h3>
|
||||
<p class="text-xs text-white/80 font-medium mt-1 max-w-xl">
|
||||
Vozilo je mapirano na centralni logistički sustav fleet managementa. Sve izmjene kilometara i servisnih naloga sinkroniziraju se u realnom vremenu s radnim nalozima operativnog tima.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="px-8 py-5 bg-gray-50 dark:bg-gray-900/30 border-t border-gray-100 dark:border-gray-700 flex justify-end gap-3">
|
||||
<a
|
||||
href={`/operativa/radni-nalozi?vozilo=${vozilo.id}`}
|
||||
class="px-5 py-2.5 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-black uppercase tracking-widest rounded-xl border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 transition-all active:scale-95 shadow-sm"
|
||||
>
|
||||
<i class="fa-solid fa-file-invoice mr-1.5 opacity-70"></i> Radni Nalozi
|
||||
</a>
|
||||
<button
|
||||
id="btn-brzi-servis"
|
||||
data-id={vozilo.id}
|
||||
class="px-5 py-2.5 bg-blue-600 text-white text-xs font-black uppercase tracking-widest rounded-xl hover:bg-blue-700 transition-all active:scale-95 shadow-lg shadow-blue-600/20"
|
||||
>
|
||||
<i class="fa-solid fa-screwdriver-wrench mr-1.5"></i> Otvori Servis
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
// Klijentska skripta za brze akcije na stranici vozila
|
||||
function initDetaljiVozila() {
|
||||
const servisBtn = document.getElementById('btn-brzi-servis');
|
||||
if (servisBtn) {
|
||||
servisBtn.addEventListener('click', () => {
|
||||
const voziloId = servisBtn.getAttribute('data-id');
|
||||
// Primjer brze operativne akcije (npr. otvaranje modala ili preusmjeravanje)
|
||||
console.log(`Otvaram brzu servisnu prijavu za vozilo ID: ${voziloId}`);
|
||||
window.location.href = `/operativa/radni-nalozi/novo?vozilo_id=${voziloId}&tip=servis`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Podrška za Astro View Transitions i standardno učitavanje
|
||||
initDetaljiVozila();
|
||||
document.addEventListener('astro:after-swap', initDetaljiVozila);
|
||||
</script>
|
||||
@@ -3,16 +3,18 @@
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import Button from "../../../components/Button.astro";
|
||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro"; // Importamo novi header
|
||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
||||
import NaslovList from "../../../components/NaslovList.astro";
|
||||
import GenericKarticaItem from "../../../components/GenericKarticaItem.astro";
|
||||
|
||||
// Importiranje utilitija
|
||||
// Importiranje utilitija i centraliziranog API-ja
|
||||
import { fetchDashboardData, fetchCurrentUser } from "../../../lib/api";
|
||||
import { getStatusColorClass } from "../../../utils/ui";
|
||||
import { getStatusColorClass, formatStatus } from "../../../utils/ui";
|
||||
|
||||
// 1. DOHVAT PODATAKA
|
||||
// Paralelno dohvaćamo podatke o vozilima i trenutnom korisniku za Header
|
||||
// 1. DOHVAT FILTERA IZ URL-a (npr. ?status=servis)
|
||||
const statusFilter = Astro.url.searchParams.get('status')?.toLowerCase();
|
||||
|
||||
// 2. DOHVAT PODATAKA
|
||||
const [dashboardData, user] = await Promise.all([
|
||||
fetchDashboardData(),
|
||||
fetchCurrentUser()
|
||||
@@ -20,66 +22,75 @@ const [dashboardData, user] = await Promise.all([
|
||||
|
||||
const vozila = dashboardData?.vozila || [];
|
||||
|
||||
// 2. LOGIKA STATISTIKE
|
||||
const aktivnaVozila = vozila.filter(v => v.status === 'aktivan').length;
|
||||
const naServisu = vozila.filter(v => v.status === 'servis').length;
|
||||
const neaktivnaVozila = vozila.filter(v => v.status === 'neaktivan').length;
|
||||
// 3. LOGIKA STATISTIKE (Uvijek se računa iz originalnog niza, neovisno o filteru)
|
||||
const aktivnaVozila = vozila.filter(v => v.status?.toLowerCase() === 'aktivan').length;
|
||||
const naServisu = vozila.filter(v => v.status?.toLowerCase() === 'servis').length;
|
||||
const neaktivnaVozila = vozila.filter(v => v.status?.toLowerCase() === 'neaktivan').length;
|
||||
|
||||
// Podaci za WelcomeHeader
|
||||
const imeKorisnika = user?.first_name || "Serviser";
|
||||
// const ulogaKorisnika = user?.is_serviser ? "Terenski Tehničar" : "Logistika";
|
||||
// 4. LOGIKA FILTRIRANJA (Serverska strana)
|
||||
let filtriraniVozila = vozila;
|
||||
if (statusFilter) {
|
||||
filtriraniVozila = vozila.filter(v => v.status?.toLowerCase() === statusFilter);
|
||||
}
|
||||
|
||||
// Možeš dodati limit kroz props ako ovu stranicu ikada budeš koristio kao parcijalnu komponentu
|
||||
const limit = 0;
|
||||
const prikazanaVozila = limit > 0 ? filtriraniVozila.slice(0, limit) : filtriraniVozila;
|
||||
|
||||
// Dinamički naslov liste ovisno o odabranom filteru flote
|
||||
const prikazaniNaslov = statusFilter
|
||||
? `Flota: ${formatStatus(statusFilter)}`
|
||||
: "Aktivna Flota";
|
||||
---
|
||||
|
||||
<Layout title="Vozni park | Flota">
|
||||
<div class="w-full space-y-10">
|
||||
|
||||
<!-- WELCOME HEADER -->
|
||||
<WelcomeHeader />
|
||||
|
||||
<!-- LISTA VOZILA + -->
|
||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
||||
|
||||
<!-- LIJEVI STUPAC: POPIS -->
|
||||
<div class="md:col-span-7 space-y-6">
|
||||
<NaslovList
|
||||
naslov="Aktivna Flota"
|
||||
ukupno={vozila.length}
|
||||
naslov={prikazaniNaslov}
|
||||
ukupno={filtriraniVozila.length}
|
||||
|
||||
label1="Spremni"
|
||||
filter1="planirano"
|
||||
count1={aktivnaVozila}
|
||||
filter1="aktivan" count1={aktivnaVozila}
|
||||
|
||||
label2="Servis"
|
||||
count2={naServisu}
|
||||
filter2="u_radu"
|
||||
filter2="servis" count2={naServisu}
|
||||
|
||||
label3="Neaktivni"
|
||||
count3={neaktivnaVozila}
|
||||
filter3="neaktivan"
|
||||
filter3="neaktivan" count3={neaktivnaVozila}
|
||||
/>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
||||
<div id="vozila-list">
|
||||
{vozila.length > 0 ? vozila.map((v) => (
|
||||
{prikazanaVozila.length > 0 ? prikazanaVozila.map((v) => (
|
||||
<GenericKarticaItem
|
||||
href={`/fleet/vozila/${v.id}`}
|
||||
status={v.status}
|
||||
statusBojaClass={getStatusColorClass(v.status)}
|
||||
ikona={v.status === 'aktivan' ? 'fa-truck' : 'fa-screwdriver-wrench'}
|
||||
status={v.status?.toLowerCase()}
|
||||
statusBojaClass={getStatusColorClass(v.status?.toLowerCase())}
|
||||
ikona={v.status?.toLowerCase() === 'aktivan' ? 'fa-truck' : 'fa-screwdriver-wrench'}
|
||||
naslov={v.naziv}
|
||||
subNaslov={v.registracija}
|
||||
metaTekst={`Prijavljeno: ${v.trenutni_kilometri.toLocaleString()} KM | Lokacija: Baza`}
|
||||
statusPrikaz={v.status_prikaz}
|
||||
metaTekst={`Prijavljeno: ${v.trenutni_kilometri?.toLocaleString() || 0} KM | Lokacija: ${v.lokacija || v.trenutna_lokacija || 'Baza (Zagreb)'}`}
|
||||
statusPrikaz={v.status_prikaz || formatStatus(v.status?.toLowerCase())}
|
||||
/>
|
||||
)) : (
|
||||
<div class="p-24 text-center">
|
||||
<i class="fa-solid fa-truck-dash text-4xl text-gray-100 dark:text-gray-700 mb-4 block"></i>
|
||||
<p class="italic text-gray-400 text-[10px] uppercase tracking-widest leading-none">Baza podataka je prazna</p>
|
||||
<Button variant="outline" class="mt-6" id="btn-dodaj-prvo">Dodaj prvo vozilo</Button>
|
||||
<p class="italic text-gray-400 text-[10px] uppercase tracking-widest leading-none">Nema vozila za odabrani status</p>
|
||||
{!statusFilter && (
|
||||
<Button variant="outline" class="mt-6" id="btn-dodaj-prvo">Dodaj prvo vozilo</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DESNI PANEL - AKCIJE -->
|
||||
<div class="md:col-span-3">
|
||||
<AkcijePanel />
|
||||
</div>
|
||||
@@ -89,6 +100,7 @@ const imeKorisnika = user?.first_name || "Serviser";
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
// Inicijalizacija klikova na kartice i filtere unutar flote
|
||||
import { initFilters } from "../../../scripts/filters";
|
||||
initFilters();
|
||||
document.addEventListener('astro:after-swap', initFilters);
|
||||
|
||||
@@ -7,10 +7,10 @@ import RadniNalogLista from "../components/RadniNalogLista.astro";
|
||||
import StatsGrid from "../components/StatsGrid.astro";
|
||||
import Kalendar from "../components/Kalendar/Kalendar.astro";
|
||||
|
||||
// API i Utils
|
||||
// API i Centralizirani Utils iz api.js
|
||||
import { fetchDashboardData, fetchCurrentUser } from "../lib/api";
|
||||
|
||||
// 1. Dohvat podataka (paralelno radi brzine)
|
||||
// 1. Dohvat podataka (paralelno radi brzine i performansi u homelabu)
|
||||
const [data, user] = await Promise.all([
|
||||
fetchDashboardData(),
|
||||
fetchCurrentUser()
|
||||
@@ -19,19 +19,22 @@ const [data, user] = await Promise.all([
|
||||
const nalozi = data?.nalozi || [];
|
||||
const imeKorisnika = user?.first_name || "Kolega";
|
||||
|
||||
// 2. Kalkulacija statistike samo za StatsGrid (ostalo ide u RadniNalogLista)
|
||||
const naloziURadu = nalozi.filter(n => n.status === 'u_radu').length;
|
||||
const planiraniNalozi = nalozi.filter(n => n.status === 'planirano').length;
|
||||
const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status === 'naplaceno').length;
|
||||
// 2. POPRAVLJENO: Kalkulacija statistike tolerantna na velika/mala slova (.toLowerCase())
|
||||
// Osiguravamo da se podaci iz baze (PLANIRANO, U_RADU) savršeno upare sa StatsGrid filterima
|
||||
const naloziURadu = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
||||
const planiraniNalozi = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
||||
|
||||
const zavrseniNalozi = nalozi.filter(n => {
|
||||
const statusMalo = n.status?.toLowerCase();
|
||||
return statusMalo === 'zavrseno' || statusMalo === 'naplaceno';
|
||||
}).length;
|
||||
---
|
||||
|
||||
<Layout title="Dashboard | ServisLog">
|
||||
<div class="w-full space-y-5">
|
||||
|
||||
<!-- WELCOME HEADER -->
|
||||
<WelcomeHeader />
|
||||
<WelcomeHeader ime={imeKorisnika} />
|
||||
|
||||
<!-- STATS GRID -->
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-6 px-2">
|
||||
<StatsGrid
|
||||
label="Planirano"
|
||||
@@ -57,7 +60,6 @@ const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status ==
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- RADNI NALOZI LISTA + AKCIJE PANEL -->
|
||||
<div class="grid md:grid-cols-10 gap-8 items-start px-2 pb-10">
|
||||
|
||||
<div class="md:col-span-7 space-y-8">
|
||||
@@ -74,7 +76,8 @@ const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status ==
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div class="mt-5">
|
||||
<Kalendar />
|
||||
</div>
|
||||
</Layout>
|
||||
@@ -82,7 +85,7 @@ const zavrseniNalozi = nalozi.filter(n => n.status === 'zavrseno' || n.status ==
|
||||
<script>
|
||||
import { initFilters } from "../scripts/filters.js";
|
||||
|
||||
// Inicijalizacija filtera za StatsGrid klikove
|
||||
// Inicijalizacija filtera za StatsGrid klikove na klijentu
|
||||
initFilters();
|
||||
|
||||
// Re-inicijalizacija kod navigacije (Astro View Transitions)
|
||||
|
||||
@@ -6,7 +6,7 @@ import WelcomeHeader from "../components/WelcomeHeader.astro";
|
||||
---
|
||||
|
||||
<Layout title="Prijava u sustav">
|
||||
<div class="flex items-center justify-center bg-gray-50 dark:bg-gray-950 px-4">
|
||||
<div class="flex items-center justify-center px-4">
|
||||
|
||||
<!-- WELCOME HEADER -->
|
||||
<WelcomeHeader />
|
||||
|
||||
@@ -11,13 +11,20 @@ const { id } = Astro.params;
|
||||
const API_BASE = import.meta.env.PUBLIC_API_URL;
|
||||
|
||||
let nalog = null;
|
||||
let dostupnaVozila = [];
|
||||
|
||||
try {
|
||||
// Dohvaćamo detaljne podatke (RadniNalogDetaljiSerializer)
|
||||
const res = await fetch(`${API_BASE}/operativa/radni-nalozi/${id}/?t=${Date.now()}`);
|
||||
if (res.ok) nalog = await res.json();
|
||||
// 1. Dohvaćamo detaljne podatke o radnom nalogu (RadniNalogDetaljiSerializer)
|
||||
const resNalog = await fetch(`${API_BASE}/operativa/radni-nalozi/${id}/?t=${Date.now()}`);
|
||||
if (resNalog.ok) nalog = await resNalog.json();
|
||||
|
||||
// 2. Ako radni nalog nema vezan putni nalog, dohvaćamo popis vozila za dropdown u klijentskom otoku
|
||||
if (nalog && !nalog.putni_nalog) {
|
||||
const resVozila = await fetch(`${API_BASE}/fleet/vozila/`);
|
||||
if (resVozila.ok) dostupnaVozila = await resVozila.json();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Greška pri dohvatu detalja naloga:", e);
|
||||
console.error("Greška pri dohvatu podataka s API-ja:", e);
|
||||
}
|
||||
|
||||
if (!nalog) return Astro.redirect("/404");
|
||||
@@ -31,7 +38,6 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
|
||||
<Layout title={`Nalog #${nalog.broj_naloga}`}>
|
||||
<div class="w-full space-y-10 pb-20">
|
||||
|
||||
<!-- 1. HEADER SEKCIJA -->
|
||||
<header class="px-2 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
|
||||
<div>
|
||||
<a href="/" class="text-blue-600 text-[10px] font-black uppercase tracking-[0.3em] hover:underline flex items-center gap-2 mb-6 no-underline">
|
||||
@@ -51,88 +57,79 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="flex items-center gap-3 px-6 py-3 bg-white dark:bg-gray-800 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-xl">
|
||||
<div class:list={[
|
||||
"w-3 h-3 rounded-full",
|
||||
getStatusColorClass(nalog.status),
|
||||
{"animate-pulse": nalog.status === 'u_radu'}
|
||||
{"animate-pulse": nalog.status === 'U_RADU'}
|
||||
]}></div>
|
||||
<span class="font-black uppercase text-xs tracking-widest text-gray-700 dark:text-gray-200">
|
||||
{nalog.status_display}
|
||||
{nalog.status_display || nalog.status}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 2. GLAVNI SADRŽAJ (Grid 10 stupaca) -->
|
||||
<div class="grid md:grid-cols-10 gap-8 items-start px-2">
|
||||
|
||||
<div class="md:col-span-7 space-y-8">
|
||||
|
||||
<!-- Kartica: Opis kvara -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] p-10 md:p-16 border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 relative overflow-hidden">
|
||||
<label class="text-[10px] font-black uppercase tracking-[0.3em] text-blue-600 block mb-6 italic">Opis kvara / Zadatak</label>
|
||||
<p class="text-2xl md:text-3xl font-medium text-gray-700 dark:text-gray-200 italic leading-tight tracking-tight">
|
||||
"{nalog.opis_kvara}"
|
||||
{nalog.opis_kvara}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Grid: Stroj i Klijent -->
|
||||
<div class="grid sm:grid-cols-2 gap-6">
|
||||
<!-- STROJ (Tehnički podaci) -->
|
||||
<div class="bg-gray-900 rounded-[2.5rem] p-8 text-white shadow-xl relative overflow-hidden group">
|
||||
<i class="fa-solid fa-screwdriver-wrench absolute -right-4 -bottom-4 text-8xl opacity-10 -rotate-12 group-hover:rotate-0 transition-transform duration-700"></i>
|
||||
<label class="text-[9px] font-black uppercase text-gray-500 block mb-4 italic tracking-widest">Stroj na popravku</label>
|
||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic leading-none mb-2">{nalog.stroj.naziv}</h3>
|
||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic leading-none mb-2">{nalog.stroj?.naziv}</h3>
|
||||
<span class="inline-block bg-blue-600 text-white px-3 py-1 rounded font-black text-[10px] tracking-widest uppercase mb-4">
|
||||
SN: {nalog.stroj?serijski_broj}
|
||||
SN: {nalog.stroj?.serijski_broj || 'Nema SN'}
|
||||
</span>
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs text-gray-400 font-medium uppercase">Radni sati: <span class="text-white font-black">{nalog.stroj.radni_sati} h</span></p>
|
||||
<p class="text-xs text-gray-400 font-medium uppercase">Lokacija: <span class="text-white font-black">{nalog.stroj.lokacija || 'Teren'}</span></p>
|
||||
<p class="text-xs text-gray-400 font-medium uppercase">Radni sati: <span class="text-white font-black">{nalog.stroj?.radni_sati || 0} h</span></p>
|
||||
<p class="text-xs text-gray-400 font-medium uppercase">Lokacija: <span class="text-white font-black">{nalog.stroj?.lokacija || 'Teren'}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KLIJENT (Kontakt podaci) -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[2.5rem] p-8 border border-gray-100 dark:border-gray-700 shadow-sm">
|
||||
<label class="text-[9px] font-black uppercase text-gray-400 block mb-4 italic tracking-widest">Klijent / Vlasnik</label>
|
||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic dark:text-white leading-none mb-2">{nalog.klijent.naziv}</h3>
|
||||
<p class="text-sm text-gray-500 font-medium">{nalog.klijent.grad} <span class="mx-1 text-blue-500">•</span> OIB: {nalog.klijent.oib}</p>
|
||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic dark:text-white leading-none mb-2">{nalog.klijent?.naziv}</h3>
|
||||
<p class="text-sm text-gray-500 font-medium">{nalog.klijent?.grad} <span class="mx-1 text-blue-500">•</span> OIB: {nalog.klijent?.oib}</p>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-4">
|
||||
<a href={`tel:${nalog.klijent.telefon}`} class="flex items-center gap-2 text-blue-600 font-black text-[10px] uppercase no-underline hover:opacity-70 transition-opacity">
|
||||
<a href={`tel:${nalog.klijent?.telefon}`} class="flex items-center gap-2 text-blue-600 font-black text-[10px] uppercase no-underline hover:opacity-70 transition-opacity">
|
||||
<div class="w-7 h-7 rounded-full bg-blue-50 flex items-center justify-center"><i class="fa-solid fa-phone"></i></div> Nazovi
|
||||
</a>
|
||||
<a href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(nalog.klijent.adresa + ' ' + nalog.klijent.grad)}`} target="_blank" class="flex items-center gap-2 text-gray-500 font-black text-[10px] uppercase no-underline hover:opacity-70 transition-opacity">
|
||||
<a href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent((nalog.klijent?.adresa || '') + ' ' + (nalog.klijent?.grad || ''))}`} target="_blank" class="flex items-center gap-2 text-gray-500 font-black text-[10px] uppercase no-underline hover:opacity-70 transition-opacity">
|
||||
<div class="w-7 h-7 rounded-full bg-gray-50 flex items-center justify-center"><i class="fa-solid fa-location-dot"></i></div> Navigacija
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kartica: Logistika (Vozilo) -->
|
||||
{nalog.vozilo ? (
|
||||
<div class="bg-blue-50 dark:bg-blue-900/10 rounded-[2.5rem] p-8 border border-blue-100 dark:border-blue-900/30 flex flex-col sm:flex-row justify-between items-center gap-6">
|
||||
<div class="text-center sm:text-left">
|
||||
<label class="text-[9px] font-black uppercase text-blue-400 block mb-2 italic tracking-widest">Servisno Vozilo</label>
|
||||
<h3 class="text-xl font-black uppercase dark:text-white leading-tight">{nalog.vozilo.naziv}</h3>
|
||||
<span class="text-sm font-bold text-blue-600">{nalog.vozilo.registracija}</span>
|
||||
</div>
|
||||
<div class="px-6 py-2 bg-white dark:bg-gray-800 rounded-2xl border border-blue-100 dark:border-blue-900/20 shadow-sm text-center">
|
||||
<p class="text-[10px] text-gray-400 uppercase font-black mb-1">Početni KM</p>
|
||||
<p class="text-lg font-black text-blue-600 leading-none">
|
||||
{nalog.vozilo.trenutni_kilometri?.toLocaleString() || '0'} km
|
||||
</p>
|
||||
<div class="bg-gray-900 rounded-[2.5rem] p-8 text-white shadow-xl relative overflow-hidden group">
|
||||
<i class="fa-solid fa-van-shuttle absolute -right-4 -bottom-4 text-8xl opacity-10 -rotate-12 group-hover:rotate-0 transition-transform duration-700"></i>
|
||||
<label class="text-[9px] font-black uppercase text-gray-500 block mb-4 italic tracking-widest">Servisno vozilo</label>
|
||||
<h3 class="text-2xl font-black uppercase tracking-tighter italic leading-none mb-2">{nalog.vozilo?.naziv}</h3>
|
||||
<span class="inline-block bg-blue-600 text-white px-3 py-1 rounded font-black text-[10px] tracking-widest uppercase mb-4">
|
||||
Registracija: {nalog.vozilo?.registracija || ''}
|
||||
</span>
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs text-gray-400 font-medium uppercase">Trenutni km: <span class="text-white font-black">{nalog.vozilo.trenutni_kilometri?.toLocaleString() || '0'} km</span></p>
|
||||
<p class="text-xs text-gray-400 font-medium uppercase">Status vozila: <span class="text-white font-black">{nalog.vozilo?.status_prikaz || 'Aktivno na terenu'}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="bg-gray-50 dark:bg-gray-900/30 rounded-[2.5rem] p-8 border border-dashed border-gray-200 dark:border-gray-800 flex items-center gap-4 opacity-60">
|
||||
<i class="fa-solid fa-walking text-gray-400"></i>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Radni nalog bez zaduženog vozila</p>
|
||||
<i class="fa-solid fa-truck-ramp-box text-gray-400"></i>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 italic">Radni nalog bez aktivnog putnog naloga / vozila</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- SEKCIJA: GALERIJA (PhotoSwipe) -->
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between px-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 italic">Foto dokumentacija terena</label>
|
||||
@@ -149,12 +146,53 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
|
||||
|
||||
</div>
|
||||
|
||||
<!-- DESNI PANEL (3 Stupca) -->
|
||||
<div class="md:col-span-3 space-y-6 sticky top-10">
|
||||
<!-- Komponenta za gumbe (Završi, Odgodi, Print...) -->
|
||||
<AkcijePanel tip="radni-nalog" podaci={nalog} />
|
||||
|
||||
<!-- Dodatni info o ažuriranju -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[2rem] p-6 border border-gray-100 dark:border-gray-700 shadow-sm">
|
||||
<h4 class="text-[9px] font-black uppercase text-gray-400 mb-4 tracking-widest italic">Logistika i Terenski Put</h4>
|
||||
|
||||
{nalog.putni_nalog ? (
|
||||
<div class="bg-green-50 dark:bg-green-900/10 p-5 rounded-2xl border border-green-100 dark:border-green-900/20 flex flex-col gap-2">
|
||||
<p class="text-xs font-black text-green-700 dark:text-green-400 leading-tight flex items-center gap-2">
|
||||
<i class="fa-solid fa-file-circle-check text-sm"></i> Povezan putni nalog #{nalog.putni_nalog.broj_naloga}
|
||||
</p>
|
||||
<div class="text-[11px] text-gray-500 dark:text-gray-400 space-y-1 border-t border-b border-gray-100 dark:border-gray-700/50 py-2 my-1">
|
||||
<p>Polazna kilometraža: <span class="font-black text-gray-700 dark:text-gray-200">{nalog.putni_nalog.pocetna_km?.toLocaleString()} km</span></p>
|
||||
<p class="flex items-center gap-1.5">Status puta: <span class="inline-block bg-green-200 dark:bg-green-900/40 text-green-800 dark:text-green-300 text-[9px] font-black px-2 py-0.5 rounded uppercase tracking-wider">{nalog.putni_nalog.status}</span></p>
|
||||
</div>
|
||||
<a href={`/logistika/putni-nalozi/${nalog.putni_nalog.id}`} class="text-[10px] uppercase font-black text-blue-600 dark:text-blue-400 no-underline hover:underline pt-1 flex items-center gap-1">
|
||||
Otvori putni nalog <i class="fa-solid fa-arrow-right text-[8px]"></i>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div class="space-y-3 mt-2">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-relaxed mb-2">
|
||||
Ovaj radni nalog trenutno nema vezan putni nalog. Za potrebe obrade dnevnica odaberite vozilo:
|
||||
</p>
|
||||
|
||||
<select
|
||||
id="pn-vozilo-select"
|
||||
class="w-full p-3 text-xs rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 dark:text-white font-black uppercase tracking-wider focus:outline-none focus:border-blue-500 cursor-pointer"
|
||||
>
|
||||
<option value="">-- Odaberi Servisno Vozilo --</option>
|
||||
{dostupnaVozila.map(v => (
|
||||
<option value={v.id}>{v.registracija} - {v.naziv}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
id="pn-generiraj-btn"
|
||||
data-nalog-id={nalog.id}
|
||||
class="w-full py-4 px-6 bg-blue-600 hover:bg-blue-700 text-white font-black text-xs uppercase tracking-widest rounded-2xl transition-all shadow-lg shadow-blue-500/20 text-center flex items-center justify-center gap-2 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<i id="pn-btn-icon" class="fa-solid fa-file-shield"></i>
|
||||
<span id="pn-btn-text">Generiraj Putni Nalog</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[2rem] p-6 border border-gray-100 dark:border-gray-700 shadow-sm">
|
||||
<h4 class="text-[9px] font-black uppercase text-gray-400 mb-4 tracking-widest">Zadnja aktivnost</h4>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -170,12 +208,86 @@ const datumKreiranja = new Date(nalog.datum_kreiranja).toLocaleDateString('hr-HR
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Kod ili ID -->
|
||||
<div class="text-center p-6 border-2 border-dashed border-gray-100 dark:border-gray-800 rounded-[2rem]">
|
||||
<p class="text-[8px] font-black text-gray-300 uppercase tracking-[0.4em]">Sistemski ID: {nalog.id}</p>
|
||||
<p class="text-[8px] font-black text-gray-300 dark:text-gray-600 uppercase tracking-[0.4em]">Sistemski ID: {nalog.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
|
||||
<script>
|
||||
import { createPutniNalog } from '../../../lib/api.js';
|
||||
|
||||
function initLogistika() {
|
||||
const gumb = document.getElementById('pn-generiraj-btn');
|
||||
const select = document.getElementById('pn-vozilo-select');
|
||||
const btnIcon = document.getElementById('pn-btn-icon');
|
||||
const btnText = document.getElementById('pn-btn-text');
|
||||
|
||||
if (!gumb || !select) return;
|
||||
|
||||
gumb.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const radniNalogId = gumb.getAttribute('data-nalog-id');
|
||||
const odabranoVoziloId = select.value;
|
||||
|
||||
// 1. Brza klijentska validacija
|
||||
if (!odabranoVoziloId) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.showToast?.("Molimo odaberite službeno vozilo.", "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Pokretanje vizualnog loading stanja
|
||||
gumb.disabled = true;
|
||||
select.disabled = true;
|
||||
if (btnText) btnText.textContent = 'Generiranje...';
|
||||
if (btnIcon) btnIcon.className = 'fa-solid fa-spinner animate-spin';
|
||||
|
||||
try {
|
||||
// 3. Slanje zahtjeva na Django backend (Kroz pročišćeni api.js)
|
||||
const data = await createPutniNalog(radniNalogId, odabranoVoziloId);
|
||||
|
||||
if (data) {
|
||||
// KONZISTENTAN UKLOP (Opcija B):
|
||||
// Spremamo poruku u formatu koji Toast.astro već automatski prepoznaje i čita nakon reloada
|
||||
sessionStorage.setItem(
|
||||
'pending_toast',
|
||||
JSON.stringify({
|
||||
type: 'success',
|
||||
message: 'Putni nalog uspješno generiran!'
|
||||
})
|
||||
);
|
||||
|
||||
// Instantno osvježavanje stranice - SSR iscrtava novu povezanu zelenu karticu
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Ako je krah, handleResponse unutar api.js je već okinuo privremeni crveni showToast
|
||||
ponistiLoading();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Greška unutar klijentske skripte logistike:", err);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.showToast?.("Komunikacija s poslužiteljem nije uspjela.", "error");
|
||||
}
|
||||
ponistiLoading();
|
||||
}
|
||||
});
|
||||
|
||||
function ponistiLoading() {
|
||||
gumb.disabled = false;
|
||||
select.disabled = false;
|
||||
if (btnText) btnText.textContent = 'Generiraj Putni Nalog';
|
||||
if (btnIcon) btnIcon.className = 'fa-solid fa-file-shield';
|
||||
}
|
||||
}
|
||||
|
||||
// Pokretanje skripte ovisno o Astro životnom ciklusu stranice
|
||||
initLogistika();
|
||||
document.addEventListener('astro:page-load', initLogistika);
|
||||
</script>
|
||||
@@ -127,7 +127,7 @@ const { strojevi } = await fetchStrojeviData();
|
||||
const realInput = document.getElementById('real-stroj-input') as HTMLInputElement;
|
||||
const strojOptions = document.querySelectorAll('.stroj-opt');
|
||||
|
||||
// Funkcija za inicijalizaciju broja naloga na klijentu
|
||||
// Inicijalizacija broja naloga na klijentu
|
||||
async function initBrojNaloga() {
|
||||
const displayElement = document.getElementById('broj-text');
|
||||
if (displayElement) {
|
||||
@@ -136,7 +136,6 @@ const { strojevi } = await fetchStrojeviData();
|
||||
}
|
||||
}
|
||||
|
||||
// Pozivamo funkciju čim se skripta učita
|
||||
initBrojNaloga();
|
||||
|
||||
// 1. DROPDOWN LOGIKA
|
||||
@@ -175,35 +174,55 @@ const { strojevi } = await fetchStrojeviData();
|
||||
// 3. SLANJE NA API
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('submit-btn');
|
||||
if (!btn) return;
|
||||
const gumb = document.getElementById('submit-btn') as HTMLButtonElement;
|
||||
if (!gumb) return;
|
||||
|
||||
btn.setAttribute('disabled', 'true');
|
||||
const originalContent = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Sinkronizacija...';
|
||||
gumb.setAttribute('disabled', 'true');
|
||||
const originalContent = gumb.innerHTML;
|
||||
gumb.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Sinkronizacija...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Brišemo broj_naloga kako bi backend aktivirao save() logiku
|
||||
// Brišemo broj_naloga kako bi backend sam aktivirao save() sekvencu
|
||||
formData.delete('broj_naloga');
|
||||
|
||||
// Dodavanje izvrsitelja iz localStorage
|
||||
const userString = localStorage.getItem('user_info');
|
||||
if (userString) {
|
||||
const user = JSON.parse(userString);
|
||||
formData.append('izvrsitelj', user.id);
|
||||
}
|
||||
|
||||
const nalog = await createNalog(formData);
|
||||
try {
|
||||
const nalog = await createNalog(formData);
|
||||
|
||||
if (nalog && nalog.id) {
|
||||
window.location.href = `/operativa/radni-nalozi/${nalog.id}`;
|
||||
} else {
|
||||
btn.removeAttribute('disabled');
|
||||
btn.innerHTML = originalContent;
|
||||
if (nalog && nalog.id) {
|
||||
// Budući da views.py sada radi refresh_from_db(), nalog.broj_naloga je ovdje 100% čisti 'RN-2026-XXXX'
|
||||
sessionStorage.setItem(
|
||||
'pending_toast',
|
||||
JSON.stringify({
|
||||
type: 'success',
|
||||
message: `Radni nalog ${nalog.broj_naloga} uspješno kreiran!`
|
||||
})
|
||||
);
|
||||
|
||||
window.location.href = `/operativa/radni-nalozi/${nalog.id}`;
|
||||
} else {
|
||||
ponistiLoading(gumb, originalContent);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Kritičan krah pri izradi naloga:", err);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.showToast?.("Komunikacija s poslužiteljem nije uspjela.", "error");
|
||||
}
|
||||
ponistiLoading(gumb, originalContent);
|
||||
}
|
||||
});
|
||||
|
||||
function ponistiLoading(gumb: HTMLElement, originalContent: string) {
|
||||
gumb.removeAttribute('disabled');
|
||||
gumb.innerHTML = originalContent;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (trigger && optionsList && !trigger.contains(e.target as Node) && !optionsList.contains(e.target as Node)) {
|
||||
optionsList.classList.add('hidden');
|
||||
|
||||
Reference in New Issue
Block a user