add from v002

This commit is contained in:
2026-05-20 21:24:00 +02:00
parent 1a6ba86d0b
commit 83fb8c69bb
98 changed files with 10878 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
---
// src/components/AkcijePanel.astro
import Button from './Button.astro';
interface Props {
tip: 'dashboard' | 'radni-nalog' | 'stroj' | 'vlasnik';
podaci: any;
}
const { tip, podaci } = Astro.props;
// Pomoćna funkcija za formatiranje datuma
const formatDate = (date: string) =>
new Date(date).toLocaleDateString('hr-HR', { day: '2-digit', month: '2-digit', year: 'numeric' });
---
<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>
<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' && (
<>
<!-- 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>
<div class="space-y-4">
{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>
</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>
</div>
</>
)}
{tip === 'stroj' && (
<>
<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">Zadnji Atest</span>
<span class="text-xs font-black text-green-500 italic">{formatDate(podaci.datum_zadnjeg_atesta)}</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">Godina Proizv.</span>
<span class="text-xs font-black dark:text-gray-200">{podaci.godina_proizvodnje}</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">Sustav ID</span>
<span class="text-xs font-black dark:text-gray-200">#{podaci.id}</span>
</div>
</>
)}
</div>
</div>
</aside>

View File

@@ -0,0 +1,30 @@
---
// src/components/Button.astro
interface Props {
type?: "button" | "submit";
variant?: "primary" | "danger" | "outline";
id?: string;
class?: string;
}
const { type = "button", variant = "primary", id, class: className } = Astro.props;
const variants = {
primary: "bg-blue-600 hover:bg-blue-700 text-white shadow-blue-500/20",
danger: "bg-red-600 hover:bg-red-700 text-white shadow-red-500/20",
outline: "bg-transparent border-2 border-gray-200 text-gray-500 hover:border-blue-600 hover:text-blue-600 shadow-none"
};
---
<button
type={type}
id={id}
class:list={[
"inline-flex items-center justify-center gap-4 px-10 py-6 rounded-[2rem] font-black uppercase text-xs tracking-[0.4em] transition-all active:scale-[0.98] shadow-2xl",
variants[variant],
className
]}
>
<slot name="icon" />
<slot />
</button>

View File

@@ -0,0 +1,77 @@
---
// src/components/Gallery.astro
import { Image } from 'astro:assets';
import 'photoswipe/style.css';
interface Props {
// Prilagođeno tvom serializeru: 'url' je zapravo 'slika' u JSON-u
images: {
slika: string;
opis?: string;
width?: number;
height?: number;
}[];
galleryId: string;
}
const { images = [], galleryId } = Astro.props;
// Filtriramo važeće slike (pazimo na naziv polja 'slika' iz serializera)
const validImages = images.filter(img => img && img.slika);
---
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4" id={galleryId}>
{validImages.map((img) => (
<a
href={img.slika}
data-pswp-width={img.width || 1600}
data-pswp-height={img.height || 1200}
target="_blank"
class="group relative block aspect-square overflow-hidden rounded-[2rem] bg-gray-100 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 shadow-sm"
>
<Image
src={img.slika}
alt={img.opis || "Detalj kvara"}
width={400}
height={400}
class="object-cover h-full w-full transition-transform duration-500 group-hover:scale-110"
/>
<!-- Overlay s opisom ako postoji -->
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center text-white p-4 text-center">
<i class="fa-solid fa-expand text-xl mb-2"></i>
{img.opis && <span class="text-[10px] font-black uppercase tracking-widest">{img.opis}</span>}
</div>
</a>
))}
</div>
{validImages.length === 0 && (
<div class="p-12 border-2 border-dashed border-gray-100 dark:border-gray-800 rounded-[2rem] text-center">
<i class="fa-solid fa-images text-gray-200 dark:text-gray-700 text-3xl mb-3"></i>
<p class="text-gray-400 text-xs font-medium italic">Nema priložene foto dokumentacije.</p>
</div>
)}
<script>
import PhotoSwipeLightbox from 'photoswipe/lightbox';
const initPhotoSwipe = () => {
// Hvata sve galerije, neovisno o prefiksu, važno je da imaju ID
const lightboxes = document.querySelectorAll('.grid[id]');
lightboxes.forEach((el) => {
const lightbox = new PhotoSwipeLightbox({
gallery: `#${el.id}`,
children: 'a',
pswpModule: () => import('photoswipe'),
// Dodajemo opciju za kretanje kotačićem miša
wheelToZoom: true
});
lightbox.init();
});
};
// Inicijalizacija pri učitavanju i pri Astro navigaciji (View Transitions)
initPhotoSwipe();
document.addEventListener('astro:after-swap', initPhotoSwipe);
</script>

View File

@@ -0,0 +1,114 @@
---
// src/components/GenericKarticaItem.astro
import { getStatusColorClass } from "../utils/ui";
interface Props {
href: string;
status: string;
statusBojaClass?: string;
ikona: string;
naslov: string;
subNaslov?: string;
metaTekst: string;
statusPrikaz: string;
bojaTeme?: "blue" | "indigo" | "emerald" | "red"; // Podrška za teme
}
const {
href,
status,
statusBojaClass,
ikona,
naslov,
subNaslov,
metaTekst,
statusPrikaz,
bojaTeme = "blue"
} = Astro.props;
// Mapiranje tema na Tailwind klase
const themes = {
blue: {
hover: "hover:bg-blue-50/30 dark:hover:bg-blue-900/10",
text: "group-hover:text-blue-600 dark:group-hover:text-blue-400",
iconBg: "group-hover:bg-blue-600",
arrow: "group-hover:text-blue-600"
},
indigo: {
hover: "hover:bg-indigo-50/30 dark:hover:bg-indigo-900/10",
text: "group-hover:text-indigo-600 dark:group-hover:text-indigo-400",
iconBg: "group-hover:bg-indigo-600",
arrow: "group-hover:text-indigo-600"
},
emerald: {
hover: "hover:bg-emerald-50/30 dark:hover:bg-emerald-900/10",
text: "group-hover:text-emerald-600 dark:group-hover:text-emerald-400",
iconBg: "group-hover:bg-emerald-600",
arrow: "group-hover:text-emerald-400"
}
};
const activeTheme = themes[bojaTeme] || themes.blue;
const resolvedStatusColor = statusBojaClass || getStatusColorClass(status);
---
<a
href={href}
class:list={[
"nalog-item flex items-center justify-between p-8 border-b border-gray-50 dark:border-gray-700 last:border-0 group transition-all duration-300",
activeTheme.hover
]}
data-status={status}
>
<div class="flex items-center gap-6 text-left">
<!-- Ikona s dinamičkom bojom na hoveru -->
<div class:list={[
"w-14 h-14 rounded-2xl flex items-center justify-center text-xl shadow-inner transition-all duration-300 bg-gray-50 dark:bg-gray-900 text-gray-400 group-hover:text-white",
activeTheme.iconBg
]}>
<i class:list={["fa-solid", ikona, status === 'u_radu' ? 'animate-pulse' : '']}></i>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-3">
<span class:list={[
"font-black text-gray-900 dark:text-white text-xl uppercase tracking-tighter italic transition-colors leading-none",
activeTheme.text
]}>
{naslov}
</span>
{subNaslov && (
<span class="text-[10px] font-black bg-gray-900 text-white px-2 py-0.5 rounded border border-gray-700 uppercase tracking-[0.2em]">
{subNaslov}
</span>
)}
</div>
<span class="text-gray-400 text-[10px] font-black uppercase tracking-widest mt-2 italic leading-none">
{metaTekst}
</span>
</div>
</div>
<div class="flex items-center gap-8">
<!-- Status indikator desno -->
<div class="hidden lg:flex flex-col items-end text-right">
<div class="flex items-center gap-2 mb-1">
<div class:list={["w-2 h-2 rounded-full", resolvedStatusColor]}></div>
<span class="text-[8px] font-black uppercase text-gray-400 tracking-[0.2em]">Status</span>
</div>
<span class:list={[
"text-xs font-black uppercase italic transition-colors",
status === 'zavrseno' || status === 'naplaceno' || status === 'aktivan' ? 'text-emerald-500' : activeTheme.text
]}>
{statusPrikaz}
</span>
</div>
<!-- Strelica -->
<i class:list={[
"fa-solid fa-arrow-right-long text-gray-200 group-hover:translate-x-2 transition-all duration-300",
activeTheme.arrow
]}></i>
</div>
</a>

View File

@@ -0,0 +1,152 @@
---
// src/components/Kalendar.astro
import { fetchCalendarEvents } from "../../lib/api";
import { getStatusColorClass } from "../../utils/ui";
// 1. Dohvat podataka s Django API-ja kroz tvoj centralizirani lib
const apiEvents = await fetchCalendarEvents();
// 2. Grupiranje događaja po datumu (YYYY-MM-DD)
const eventsByDate = apiEvents.reduce((acc, event) => {
const dateKey = new Date(event.start).toISOString().split('T')[0];
if (!acc[dateKey]) acc[dateKey] = [];
acc[dateKey].push(event);
return acc;
}, {});
// Postavke za trenutni prikaz (Svibanj 2026)
const daysInMonth = 31;
const firstDayOffset = 5; // Petak
const currentYearMonth = "2026-05";
---
<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 font-sans transition-all">
<div class="flex justify-between items-center bg-gray-50/50 dark:bg-gray-900/50 px-8 py-6 border-b border-gray-100 dark:border-gray-700">
<button class="text-gray-400 hover:text-blue-600 transition-colors text-2xl font-black">&lsaquo;</button>
<div class="text-center">
<h2 class="text-xl font-black uppercase tracking-tighter italic leading-none">Svibanj 2026</h2>
<span class="text-[10px] text-blue-600 font-bold uppercase tracking-[0.2em] mt-1 italic">Raspored servisa</span>
</div>
<button class="text-gray-400 hover:text-blue-600 transition-colors text-2xl font-black">&rsaquo;</button>
</div>
<div id="calendar-body">
<div class="grid grid-cols-7 text-center text-[10px] text-gray-400 dark:text-gray-500 uppercase font-black tracking-[0.2em] py-4 border-b border-gray-50 dark:border-gray-700/50">
<div>Ned</div><div>Pon</div><div>Uto</div><div>Sri</div><div>Čet</div><div>Pet</div><div>Sub</div>
</div>
<div class="grid grid-cols-7 text-center" id="days-grid">
{Array.from({ length: firstDayOffset }).map(() => (
<div class="py-6 border-b border-r border-gray-50 dark:border-gray-700/10 opacity-0"></div>
))}
{Array.from({ length: daysInMonth }).map((_, i) => {
const day = i + 1;
const dateKey = `${currentYearMonth}-${day.toString().padStart(2, '0')}`;
const dayEvents = eventsByDate[dateKey] || [];
return (
<div
class="day-cell py-6 relative cursor-pointer hover:bg-blue-50/50 dark:hover:bg-gray-700/30 transition-all border-b border-r border-gray-50 dark:border-gray-700/30 group"
data-day={day}
data-events={JSON.stringify(dayEvents)}
>
<span class={`text-3xl font-black italic tracking-tighter transition-colors ${dayEvents.length > 0 ? 'text-gray-900 dark:text-white' : 'text-gray-300 dark:text-gray-600'} group-hover:text-blue-600`}>
{day.toString().padStart(2, '0')}
</span>
<div class="flex justify-center gap-1 mt-1 h-1.5">
{dayEvents.map(event => (
<div class={`w-1.5 h-1.5 rounded-full ${getStatusColorClass(event.tip)}`}></div>
))}
</div>
</div>
);
})}
<div id="event-panel" class="hidden col-span-7 bg-blue-600 text-white p-8 relative overflow-visible shadow-2xl">
<div class="absolute -top-2 left-0 w-full h-2">
<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>
<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');
const arrow = document.getElementById('panel-arrow');
const eventList = document.getElementById('event-list');
const dayCells = document.querySelectorAll('.day-cell');
if (!grid || !panel) return;
dayCells.forEach(cell => {
cell.addEventListener('click', () => {
const eventsData = JSON.parse(cell.getAttribute('data-events') || '[]');
if (eventsData.length > 0) {
const allItems = Array.from(grid.children);
const index = allItems.indexOf(cell);
// 1. Pozicioniranje strelice (Matematički precizno)
const colIndex = index % 7;
const percentage = (colIndex * (100 / 7)) + (100 / 7 / 2);
arrow.style.left = `calc(${percentage}% - 12px)`;
// 2. Injekcija panela na kraj tjedna
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">
${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>
</div>
</div>
`).join('');
targetCell.after(panel);
panel.classList.remove('hidden');
} else {
panel.classList.add('hidden');
}
});
});
}
initKalendar();
document.addEventListener('astro:after-swap', initKalendar);
</script>

View File

@@ -0,0 +1,59 @@
---
// src/components/ListaStrojeva.astro
import NaslovList from "./NaslovList.astro";
import GenericKarticaItem from "./GenericKarticaItem.astro";
import { getStatusColorClass, formatStatus } from "../utils/ui";
interface Props {
strojevi: any[];
naslov?: string;
prikaziNaslov?: boolean;
}
const {
strojevi = [],
naslov = "Tehničke jedinice",
prikaziNaslov = true
} = Astro.props;
// Statistika za NaslovList (npr. koliko ih je aktivno ili na servisu ako imaš te podatke)
const ukupnoStrojeva = strojevi.length;
// Pretpostavljamo da strojevi imaju status, ako nemaju, možemo staviti fiksne labele
const uRadu = strojevi.filter(s => s.status === 'u_radu').length;
---
<div class="space-y-6">
{prikaziNaslov && (
<NaslovList
naslov={naslov}
ukupno={ukupnoStrojeva}
label1="Ukupno"
count1={ukupnoStrojeva}
label2="Aktivno"
count2={uRadu}
/>
)}
<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="strojevi-list">
{strojevi.length > 0 ? strojevi.map((s) => (
<GenericKarticaItem
href={`/fleet/strojevi/${s.id}`}
status={s.status || 'aktivan'}
statusBojaClass={getStatusColorClass(s.status || 'aktivan')}
ikona="fa-screwdriver-wrench"
naslov={s.naziv}
subNaslov={s.serijski_broj}
metaTekst={`${s.vlasnik_naziv} | Radni sati: ${parseFloat(s.radni_sati || 0).toLocaleString('hr-HR')} h`}
statusPrikaz={formatStatus(s.status || 'aktivan')}
/>
)) : (
<div class="p-20 text-center italic text-gray-400 text-[10px] uppercase tracking-widest leading-none">
<i class="fa-solid fa-box-open text-3xl text-gray-100 dark:text-gray-700 mb-4 block"></i>
Nema dostupnih tehničkih jedinica
</div>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,103 @@
---
// src/components/LoginForm.astro
import Button from "./Button.astro";
---
<div class="max-w-md mx-auto w-full p-10 bg-white dark:bg-gray-900 rounded-[3rem] border border-gray-100 dark:border-gray-800 shadow-2xl shadow-blue-500/10">
<div class="mb-10 text-center sm:text-left">
<div class="inline-flex w-14 h-14 bg-blue-600 rounded-2xl items-center justify-center shadow-lg shadow-blue-500/20 mb-6 rotate-3 sm:-ml-2">
<i class="fa-solid fa-lock text-white text-xl"></i>
</div>
<h2 class="text-4xl font-black text-gray-900 dark:text-white uppercase tracking-tighter italic leading-none">Prijava</h2>
<p class="text-gray-500 dark:text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] mt-3 opacity-70 italic">
ServisLog <span class="text-blue-600">/</span> Terminal
</p>
</div>
<form id="login-form" class="space-y-6">
<div>
<label for="email" class="block text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 mb-2 ml-4 italic">Korisnički E-mail</label>
<input
type="email"
id="email"
name="email"
required
class="w-full px-6 py-4 bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-blue-600 rounded-2xl outline-none transition-all text-gray-900 dark:text-white font-bold italic shadow-inner"
placeholder="serviser@tvrtka.hr"
/>
</div>
<div>
<label for="password" class="block text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 mb-2 ml-4 italic">Lozinka</label>
<input
type="password"
id="password"
name="password"
required
class="w-full px-6 py-4 bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-blue-600 rounded-2xl outline-none transition-all text-gray-900 dark:text-white font-bold tracking-[0.3em] shadow-inner"
placeholder="••••••••"
/>
</div>
<div class="pt-2">
<Button
type="submit"
id="login-btn"
variant="primary"
class="w-full !py-5 italic"
>
<i class="fa-solid fa-bolt" slot="icon"></i>
Pokreni Sustav
</Button>
</div>
</form>
</div>
<script>
import { login } from "../lib/api";
const loginForm = document.querySelector('#login-form') as HTMLFormElement;
const btn = document.querySelector('#login-btn') as HTMLButtonElement;
loginForm?.addEventListener('submit', async (e) => {
e.preventDefault();
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...';
const formData = new FormData(loginForm);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
try {
const result = await login(email, password);
if (result.success) {
sessionStorage.setItem('pending_toast', JSON.stringify({
type: 'success',
message: 'Pristup odobren. Dobrodošli u sustav.'
}));
window.location.href = '/';
} 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
window.showToast?.("Sustav nedostupan. Provjerite vezu.", "error");
btn.disabled = false;
btn.innerHTML = originalContent;
}
});
</script>

View File

@@ -0,0 +1,59 @@
---
// src/components/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;
}
const { naslov, ukupno, label1, count1, filter1, label2, count2, filter2, label3, count3, filter3 } = 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="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}
>
<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>
</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>
)}
</div>
</div>

View File

@@ -0,0 +1,107 @@
---
// src/components/Navbar.astro
import site from "../data/site.json";
const currentPath = Astro.url.pathname;
---
<nav class="sticky top-0 z-[100] w-full bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-100 dark:border-gray-800">
<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">
<i class="fa-solid fa-screwdriver-wrench text-white text-lg"></i>
</div>
<span class="text-xl font-black tracking-tighter text-gray-900 dark:text-white uppercase">
{site.title.split(' ')[0]}<span class="text-blue-600">{site.title.split(' ')[1] || ''}</span>
</span>
</a>
</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
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]
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"
>
{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>
))}
<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>
</div>
</nav>
<script>
function setupNavbar() {
const btn = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu');
if (btn && menu) {
// Re-inicijalizacija gumba za Astro View Transitions
const newBtn = btn.cloneNode(true) as HTMLButtonElement;
btn.parentNode?.replaceChild(newBtn, btn);
newBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('hidden');
});
// 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');
}
});
// Zatvori nakon odabira linka
menu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => menu.classList.add('hidden'));
});
}
}
// 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);
setupNavbar();
document.addEventListener('astro:after-swap', setupNavbar);
</script>

View File

@@ -0,0 +1,94 @@
---
// 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;
naslov?: string;
prikaziNaslov?: boolean;
}
const {
limit = 0,
naslov = "Zadnje aktivnosti",
prikaziNaslov = true,
} = Astro.props;
// 1. Dohvat filtera iz URL-a (npr. ?status=u_radu)
const statusFilter = Astro.url.searchParams.get('status');
// 2. Dohvat podataka s API-ja
const { nalozi = [] } = await fetchDashboardData();
// 3. Logika filtriranja (Serverska strana)
let filtriraniNalozi = nalozi;
if (statusFilter) {
filtriraniNalozi = nalozi.filter(n => n.status === 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;
// 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">
{prikaziNaslov && (
<NaslovList
naslov={prikazaniNaslov}
ukupno={filtriraniNalozi.length}
label1="Planirano"
count1={planiranoCount}
filter1="planirano"
label2="U radu"
count2={uRaduCount}
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}
statusBojaClass={getStatusColorClass(n.status)}
ikona={n.status === '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)}
bojaTeme={n.boja_teme || 'blue'}
/>
))
) : (
<div class="p-20 text-center uppercase font-black opacity-20 italic">
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();
// Podrška za Astro View Transitions (ako ih koristiš)
document.addEventListener('astro:after-swap', initFilters);
</script>

View File

@@ -0,0 +1,48 @@
---
// src/components/StatsGrid.astro
interface Props {
label: string;
value: string | number;
icon: string;
variant?: 'blue' | 'yellow' | 'emerald' | 'gray';
animate?: boolean;
filterValue?: string; // NOVO: npr. 'u_radu', 'planirano'...
class?: string;
}
const {
label,
value,
icon,
variant = 'blue',
animate = false,
filterValue = "",
class: className = ""
} = Astro.props;
const themes = {
blue: { container: "border-gray-100 dark:border-gray-700", iconBg: "bg-blue-50 dark:bg-blue-900/30", iconColor: "text-blue-600 dark:text-blue-400", labelColor: "text-gray-400" },
yellow: { container: "border-yellow-500/20 border-2", iconBg: "bg-yellow-500", iconColor: "text-white", labelColor: "text-yellow-600" },
emerald: { container: "border-gray-100 dark:border-gray-700", iconBg: "bg-emerald-50 dark:bg-emerald-900/30", iconColor: "text-emerald-600 dark:text-emerald-400", labelColor: "text-gray-400" },
gray: { container: "border-gray-100 dark:border-gray-700", iconBg: "bg-gray-100 dark:bg-gray-800", iconColor: "text-gray-500", labelColor: "text-gray-400" }
};
const theme = themes[variant];
---
<div
class={`stat-card bg-white dark:bg-gray-800 p-3 sm:p-6 rounded-2xl sm:rounded-[2.5rem] shadow-sm border flex flex-row items-center justify-center gap-2 sm:gap-5 cursor-pointer hover:scale-[1.02] active:scale-95 transition-all ${theme.container} ${className}`}
>
<div class={`p-1.5 sm:p-4 rounded-lg sm:rounded-2xl w-7 h-7 sm:w-16 sm:h-16 flex items-center justify-center flex-none ${theme.iconBg} ${theme.iconColor} ${animate ? 'animate-pulse' : ''}`}>
<i class={`fa-solid ${icon} text-xs sm:text-2xl`}></i>
</div>
<div class="flex flex-col justify-center">
<h3 class="text-sm sm:text-4xl font-black text-gray-900 dark:text-white leading-none tracking-tighter">
{value}
</h3>
<p class={`text-[6px] sm:text-xs font-black uppercase tracking-widest mt-0.5 sm:mt-1 ${theme.labelColor}`}>
{label}
</p>
</div>
</div>

View File

@@ -0,0 +1,73 @@
---
// src/components/Toast.astro
---
<div id="global-toast" class="fixed bottom-0 left-0 w-full z-[999] hidden transition-all duration-500 transform translate-y-full">
<div id="toast-bg" class="px-6 py-6 flex items-center shadow-[0_-10px_40px_rgba(0,0,0,0.2)] border-t">
<div class="max-w-7xl mx-auto w-full flex items-center gap-6">
<!-- Icon Container -->
<div id="toast-icon-wrapper" class="w-14 h-14 bg-white/20 rounded-[1.2rem] flex items-center justify-center shadow-inner">
<i id="toast-icon" class="fa-solid text-2xl"></i>
</div>
<!-- Text Content -->
<div class="flex flex-col">
<span id="toast-label" class="text-[10px] font-black uppercase tracking-[0.3em] opacity-70 leading-none italic"></span>
<p id="toast-message" class="text-lg font-black uppercase tracking-tight mt-1 italic leading-none text-white"></p>
</div>
<!-- Close Button -->
<button id="close-toast" class="ml-auto w-10 h-10 hover:bg-white/10 rounded-xl transition-colors text-white">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
</div>
<script>
// Logika koja se može pozvati s bilo koje stranice
window.showToast = (message, type = 'success') => {
const toast = document.getElementById('global-toast');
const bg = document.getElementById('toast-bg');
const icon = document.getElementById('toast-icon');
const label = document.getElementById('toast-label');
const msg = document.getElementById('toast-message');
if (!toast || !bg || !icon || !label || !msg) return;
// Konfiguracija tema
const themes = {
success: { bg: 'bg-emerald-600', border: 'border-emerald-500', icon: 'fa-check-double', label: 'Sustav Potvrđuje' },
error: { bg: 'bg-red-600', border: 'border-red-500', icon: 'fa-circle-exclamation', label: 'Sustav Odbija' },
warning: { bg: 'bg-orange-500', border: 'border-orange-400', icon: 'fa-triangle-exclamation', label: 'Sustav Upozorava' }
};
const theme = themes[type] || themes.success;
// Primjena stilova
bg.className = `${theme.bg} ${theme.border} text-white px-6 py-6 flex items-center shadow-2xl border-t`;
icon.className = `fa-solid ${theme.icon} text-2xl text-white`;
label.innerText = theme.label;
msg.innerText = message;
// Animacija ulaska
toast.classList.remove('hidden', 'translate-y-full');
// Auto-hide nakon 5s
setTimeout(() => {
toast.classList.add('translate-y-full');
}, 5000);
};
// Zatvaranje na gumb
document.getElementById('close-toast')?.addEventListener('click', () => {
document.getElementById('global-toast')?.classList.add('translate-y-full');
});
// Provjera SessionStorage-a (za poruke nakon redirecta)
const pending = sessionStorage.getItem('pending_toast');
if (pending) {
const { type, message } = JSON.parse(pending);
window.showToast(message, type);
sessionStorage.removeItem('pending_toast');
}
</script>

View File

@@ -0,0 +1,36 @@
---
// src/components/WelcomeHeader.astro
import site from "../data/site.json";
const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
const currentPageData = site.navigation.find(item => item.url === currentPath);
console.log("Current Path:", currentPath);
const {
welcomeHeaderTextH1 = "Dobrodošli",
welcomeHeaderTextH1dodatno = "",
welcomeHeaderPodnaslov = "Pregled sustava",
welcomeHeaderPovratniURL = "",
welcomeHeaderDisplay = true
} = currentPageData || {};
---
{welcomeHeaderDisplay && (
<header class="mb-12 px-2">
{welcomeHeaderPovratniURL && (
<a href={welcomeHeaderPovratniURL} class="text-blue-600 text-[10px] font-black uppercase tracking-widest no-underline flex items-center gap-2 mb-6 hover:opacity-70 transition-opacity italic">
<i class="fa-solid fa-arrow-left text-[8px]"></i> Povratak
</a>
)}
<div class="text-left">
<h1 class="text-4xl sm:text-6xl font-black text-gray-900 dark:text-white uppercase tracking-tighter leading-none italic">
{welcomeHeaderTextH1} <span class="text-blue-600"> {welcomeHeaderTextH1dodatno}</span>
</h1>
<p class="text-gray-500 dark:text-gray-400 mt-4 font-medium italic text-lg leading-tight">
{welcomeHeaderPodnaslov}
</p>
</div>
</header>
)}