UniveralList

This commit is contained in:
2026-05-26 06:29:06 +00:00
parent e0a3dea68a
commit 3c4cf657c3
13 changed files with 350 additions and 258 deletions

View File

@@ -1,29 +0,0 @@
// src/components/RadniNalogLista.jsx
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
import { useStore } from '@nanostores/preact';
import { radniNalozi, setRadniNalozi } from '../stores/operativaStore';
export default function RadniNalogLista({ nalozi = [], limit = 5 }) {
useEffect(() => {
if (nalozi.length > 0) setRadniNalozi(nalozi);
}, [nalozi]);
const podaciIzStora = useStore(radniNalozi);
if (!podaciIzStora.length) return <div>Učitavanje...</div>;
return (
<ul>
{podaciIzStora.slice(0, limit).map(n => (
// KLJUČNA PROMJENA: li je ovdje glavni element, a ne div
<li key={n.id} style={{ marginBottom: '10px' }}>
{n.broj_naloga} -
<a href={`/operativa/radni-nalozi/${n.id}`} style={{ marginLeft: '10px' }}>
Pregled
</a>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,11 @@
// src/components/common/LoadingSpinner.jsx
import { h } from 'preact';
export default function LoadingSpinner({ text = "Učitavanje podataka..." }) {
return (
<div class="flex items-center justify-center p-12 text-gray-500">
<div class="animate-spin mr-3 h-5 w-5 border-2 border-gray-400 border-t-transparent rounded-full"></div>
{text}
</div>
);
}

View File

@@ -0,0 +1,46 @@
// src/components/common/UniversalList.jsx
import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { useStore } from '@nanostores/preact';
import { getBaseUrl } from '../lib/nav';
import LoadingSpinner from './LoadingSpinner.jsx';
export default function UniversalList({ store, setter, fetchMethod, limit, baseName, children }) {
const [loading, setLoading] = useState(true);
const list = useStore(store);
// Ako je baseName poslan, dohvati URL, inače ostavi undefined
const baseUrl = baseName ? getBaseUrl(baseName) : undefined;
useEffect(() => {
async function fetchData() {
if (list.length > 0) {
setLoading(false);
return;
}
try {
setLoading(true);
const data = await fetchMethod();
setter(data || []);
} catch (error) {
console.error("Greška pri dohvatu:", error);
setter([]);
} finally {
setTimeout(() => setLoading(false), 1000);
}
}
fetchData();
}, []);
if (loading) return <LoadingSpinner text="Učitavanje..." />;
const displayList = limit ? list.slice(0, limit) : list;
return (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* children funkcija sada prima item i opcionalni baseUrl */}
{displayList.map(item => children(item, baseUrl))}
</div>
);
}

View File

@@ -1,31 +0,0 @@
// src/components/edit/RadniNalogEditorStatus.jsx
// Ovo postaje višak
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { patchNalog } from '../../lib/api';
import Button from '../Button'; // Import nove komponente
export default function RadniNalogEditorStatus({ nalogId, pocetniStatus }) {
const [status, setStatus] = useState(pocetniStatus);
const [loading, setLoading] = useState(false);
const azurirajStatus = async () => {
setLoading(true);
const rezultat = await patchNalog(nalogId, { status: 'ZAVRSENO' });
setLoading(false);
if (rezultat) {
setStatus('ZAVRSENO');
window.showToast?.("Status ažuriran", "success");
}
};
return (
<Button
label={status === 'ZAVRSENO' ? 'Završen' : 'Završi radni nalog'}
onClick={azurirajStatus}
loading={loading}
disabled={status === 'ZAVRSENO'}
/>
);
}

View File

@@ -1,21 +1,19 @@
// src/components/fleet/FleetStrojeviLista.jsx
// src/components/lista/FleetStrojeviLista.jsx
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
import { useStore } from '@nanostores/preact';
import { strojevi, setStrojevi } from '../stores/fleetStore';
export default function FleetStrojeviLista({ initialData = [] }) {
useEffect(() => {
if (initialData.length > 0) setStrojevi(initialData);
}, [initialData]);
const list = useStore(strojevi);
if (!list.length) return <div>Učitavanje strojeva...</div>;
import UniversalList from '../common/UniversalList.jsx';
import { strojevi, setStrojevi } from '../../stores/fleetStore.js';
import { getStrojevi } from '../../lib/api.js';
export default function FleetStrojeviLista({ limit = 10 }) {
return (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{list.map(s => (
<UniversalList
store={strojevi}
setter={setStrojevi}
fetchMethod={getStrojevi}
baseName="Registrirani strojevi"
limit={limit}
>
{(s, baseUrl) => (
<div key={s.id} class="p-6 border border-gray-200 rounded-2xl bg-white shadow-sm hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-4">
<h3 class="text-xl font-black text-gray-900">{s.naziv}</h3>
@@ -33,13 +31,13 @@ export default function FleetStrojeviLista({ initialData = [] }) {
</ul>
<a
href={`/fleet/strojevi/${s.id}`}
href={`${baseUrl}/${s.id}`}
class="block text-center w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-700 transition"
>
Pregled detalja
</a>
</div>
))}
</div>
)}
</UniversalList>
);
}

View File

@@ -1,21 +1,20 @@
// src/components/fleet/FleetVozilaLista.jsx
// src/components/lista/FleetVozilaLista.jsx
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
import { useStore } from '@nanostores/preact';
import { vozila, setVozila } from '../stores/fleetStore';
export default function FleetVozilaLista({ initialData = [], baseUrl }) {
useEffect(() => {
if (initialData.length > 0) setVozila(initialData);
}, [initialData]);
const list = useStore(vozila);
if (!list.length) return <div>Učitavanje voznog parka...</div>;
import UniversalList from '../common/UniversalList.jsx';
import { vozila, setVozila } from '../../stores/fleetStore.js';
import { getVozila } from '../../lib/api.js';
export default function FleetVozilaLista({ limit = 10 }) {
return (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{list.map(v => (
<UniversalList
store={vozila}
setter={setVozila}
fetchMethod={getVozila}
baseName="Vozni Park"
limit={limit}
>
{/* Ovdje primaš SAMO JEDAN objekt (v) i bazni URL */}
{(v, baseUrl) => (
<div key={v.id} class="p-6 border border-gray-200 rounded-2xl bg-white shadow-sm hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-4">
<h3 class="text-xl font-black text-gray-900">{v.naziv}</h3>
@@ -23,12 +22,12 @@ export default function FleetVozilaLista({ initialData = [], baseUrl }) {
{v.status_prikaz}
</span>
</div>
<ul class="text-sm text-gray-600 space-y-2 mb-4">
<li><strong>Registracija:</strong> {v.registracija}</li>
<li><strong>Trenutna km:</strong> {v.trenutni_kilometri.toLocaleString()} km</li>
</ul>
<a
href={`${baseUrl}/${v.id}`}
class="block text-center w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-700 transition"
@@ -36,7 +35,7 @@ export default function FleetVozilaLista({ initialData = [], baseUrl }) {
Pregled detalja
</a>
</div>
))}
</div>
)}
</UniversalList>
);
}

View File

@@ -0,0 +1,28 @@
// src/components/lista/RadniNalogLista.jsx
import { h } from 'preact';
import UniversalList from '../common/UniversalList.jsx';
// Pretpostavljam da koristiš store za nalozi, ne za strojeve
import { radniNalozi, setRadniNalozi } from '../../stores/operativaStore.js';
import { getRadniNalozi } from '../../lib/api.js';
export default function RadniNalogLista({ limit = 10 }) {
return (
<UniversalList
store={radniNalozi}
setter={setRadniNalozi}
fetchMethod={getRadniNalozi}
baseName="Radni nalozi"
limit={limit}
>
{(n, baseUrl) => (
// Ovdje iscrtavaš SAMO JEDAN element (li ili div)
<li key={n.id} class="mb-2 p-2 border-b border-gray-700">
{n.broj_naloga} -
<a href={`${baseUrl}/${n.id}`} class="ml-4 text-blue-400 hover:text-blue-300">
Pregled
</a>
</li>
)}
</UniversalList>
);
}

View File

@@ -2,157 +2,108 @@
import { h } from "preact";
import { useState, useEffect } from "preact/hooks";
import { navigate } from "astro:transitions/client";
import { createNalog, getPutniNalozi, getStrojeviData, fetchKupciData } from "../../lib/api";
import { createNalog, getPutniNalozi, getStrojevi, getKupci } from "../../lib/api";
import { showToast } from "../../stores/toastStore";
import Button from "../Button.jsx";
// DRY komponenta za selekciju polja
const FormSelect = ({ label, items, onChange, placeholder, disabled, renderItem }) => (
<div class="mb-4">
<label class="block text-sm font-bold text-gray-300 mb-1">{label}</label>
<select
disabled={disabled}
class="w-full p-3 bg-gray-700 rounded text-white border border-gray-600 disabled:opacity-50"
onChange={onChange}
>
<option value="">-- {placeholder} --</option>
{items.map(item => renderItem(item))}
</select>
</div>
);
export default function NoviRadniNalogForm() {
const [putniNalogMode, setPutniNalogMode] = useState('none');
const [putniNalozi, setPutniNalozi] = useState([]);
const [selectedPutniId, setSelectedPutniId] = useState('');
const [formData, setFormData] = useState({ klijent: '', stroj: '', opis_kvara: '', putni_mode: 'none', putni_id: '' });
const [data, setData] = useState({ klijenti: [], strojevi: [], putniNalozi: [] });
const [loading, setLoading] = useState(false);
// 1. Inicijalni dohvat klijenata
useEffect(() => {
getKupci().then(c => setData(prev => ({ ...prev, klijenti: c })));
}, []);
// 2. Dohvat strojeva ovisno o odabranom klijentu
useEffect(() => {
if (formData.klijent) {
getStrojevi(formData.klijent).then(s => setData(prev => ({ ...prev, strojevi: s })));
} else {
setData(prev => ({ ...prev, strojevi: [] }));
}
}, [formData.klijent]);
// 3. Dohvat putnih naloga samo ako je odabran mod "existing"
useEffect(() => {
if (formData.putni_mode === 'existing') {
getPutniNalozi().then(pn => setData(prev => ({ ...prev, putniNalozi: pn })));
}
}, [formData.putni_mode]);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
const [klijenti, setKlijenti] = useState([]);
const [klijentId, setKlijentId] = useState('');
const [strojevi, setStrojevi] = useState([]);
const [selectedStrojId, setSelectedStrojId] = useState('');
const payload = new FormData(e.target);
if (formData.putni_mode === 'new') payload.append('kreiraj_putni', 'true');
// Ako je existing, FormData već ima 'putni_nalog' polje iz select-a
const [isFetchingNaloge, setIsFetchingNaloge] = useState(false);
const [isFetchingStrojevi, setIsFetchingStrojevi] = useState(false);
const [loading, setLoading] = useState(false);
const success = await createNalog(payload);
if (success) {
showToast("Radni nalog uspješno kreiran!", "success");
navigate('/operativa/radni-nalozi');
} else {
setLoading(false);
}
};
// Dohvati listu klijenata pri učitavanju forme
useEffect(() => {
// Pretpostavljam da imaš funkciju getKlijenti() u api.js
fetchKupciData().then(data => setKlijenti(data));
}, []);
return (
<form onSubmit={handleSubmit} class="space-y-6">
<FormSelect
label="Klijent" items={data.klijenti} placeholder="Odaberite klijenta"
onChange={(e) => setFormData({...formData, klijent: e.target.value})}
renderItem={k => <option value={k.id}>{k.naziv}</option>}
/>
// Dohvat Putnih naloga
useEffect(() => {
if (putniNalogMode === 'existing') {
setIsFetchingNaloge(true);
getPutniNalozi()
.then(data => setPutniNalozi(data))
.finally(() => setIsFetchingNaloge(false));
}
}, [putniNalogMode]);
<FormSelect
label="Stroj" items={data.strojevi} placeholder="Odaberite stroj" disabled={!formData.klijent}
onChange={(e) => setFormData({...formData, stroj: e.target.value})}
renderItem={s => <option value={s.id}>{s.naziv} (S/N: {s.serijski_broj})</option>}
/>
// 2. Dohvat Strojeva ovisno o klijentu
useEffect(() => {
// Popravljeno: slušamo klijentId, a ne klijenti (koji je niz)
if (klijentId) {
setIsFetchingStrojevi(true);
getStrojeviData(klijentId)
.then(data => setStrojevi(data))
.finally(() => setIsFetchingStrojevi(false));
} else {
setStrojevi([]);
setSelectedStrojId('');
}
}, [klijentId]);
<div class="mb-4">
<label class="block text-sm font-bold text-gray-300 mb-1">Opis kvara</label>
<textarea name="opis_kvara" required class="w-full p-3 bg-gray-700 rounded text-white" />
</div>
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
<div class="border-t border-gray-700 pt-6">
<FormSelect
label="Putni nalog"
items={[{id: 'none', naziv: 'Bez putnog naloga'}, {id: 'new', naziv: 'Kreiraj novi'}, {id: 'existing', naziv: 'Poveži postojeći'}]}
placeholder="Odaberite status putnog naloga"
onChange={(e) => setFormData({...formData, putni_mode: e.target.value})}
renderItem={m => <option value={m.id}>{m.naziv}</option>}
/>
const formData = new FormData(e.target);
// Logika za povezivanje putnog naloga koju backend view sada očekuje
if (putniNalogMode === 'existing' && selectedPutniId) {
formData.append('putni_nalog', selectedPutniId);
} else if (putniNalogMode === 'new') {
formData.append('kreiraj_putni', 'true');
}
const result = await createNalog(formData);
if (result) {
showToast("Radni nalog uspješno kreiran!", "success");
navigate('/operativa/radni-nalozi');
} else {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} class="space-y-6">
{/* Ovdje idu tvoja polja za nalog (klijent, stroj, opis...) */}
<div>
<label class="block text-gray-300">Klijent:</label>
<select
name="klijent"
required
class="w-full p-3 bg-gray-700 rounded text-white"
onChange={(e) => setKlijentId(e.target.value)}
>
<option value="">-- Odaberite klijenta --</option>
{/* OVDJE PROVJERI: */}
{klijenti.map(k => (
<option key={k.id} value={k.id}>
{k.naziv}
</option>
))}
</select>
</div>
<div>
<label class="block text-gray-300">Stroj (ID):</label>
<select
name="stroj"
required
class="w-full p-3 bg-gray-700 rounded text-white"
onChange={(e) => setSelectedStrojId(e.target.value)}
>
<option value="">-- Odaberite stroj --</option>
{strojevi.map(stroj => (
<option key={stroj.id} value={stroj.id}>
{stroj.naziv}
</option>
))}
</select>
</div>
<div>
<label class="block text-gray-300">Opis kvara:</label>
<textarea name="opis_kvara" required class="w-full p-3 bg-gray-700 rounded"></textarea>
</div>
<div class="mt-6 border-t pt-4">
<label class="block mb-2 font-bold text-gray-300">Putni nalog:</label>
<select
onChange={(e) => setPutniNalogMode(e.target.value)}
class="w-full p-3 rounded bg-gray-700 text-white border border-gray-600"
>
<option value="none">Bez putnog naloga</option>
<option value="new">Kreiraj novi prazni putni nalog</option>
<option value="existing">Poveži na postojeći</option>
</select>
{putniNalogMode === 'existing' && (
<select
value={selectedPutniId}
onChange={(e) => setSelectedPutniId(e.target.value)}
disabled={isFetchingNaloge}
class={`w-full p-3 mt-3 rounded bg-gray-700 text-white border border-gray-500 ${isFetchingNaloge ? 'opacity-50 cursor-wait' : ''}`}
>
{isFetchingNaloge ? (
<option>Učitavanje putnih naloga...</option>
) : (
<option value="">-- Odaberite postojeći putni nalog --</option>
)}
{!isFetchingNaloge && putniNalozi.map(pn => (
<option key={pn.id} value={pn.id}>
{pn.broj_naloga} | {pn.status} | {pn.mjesto_odredista}
</option>
))}
</select>
)}
</div>
<Button type="submit" loading={loading} variant="primary" className="w-full">
Kreiraj Radni Nalog
</Button>
</form>
);
{formData.putni_mode === 'existing' && (
<FormSelect
label="Odaberite nalog" items={data.putniNalozi} placeholder="Popis aktivnih naloga"
onChange={(e) => setFormData({...formData, putni_id: e.target.value})}
renderItem={pn => <option value={pn.id}>{pn.broj_naloga} - {pn.mjesto_odredista}</option>}
/>
)}
</div>
<Button type="submit" loading={loading} className="w-full">
Kreiraj Radni Nalog
</Button>
</form>
);
}

View File

@@ -72,13 +72,13 @@ const { title } = Astro.props;
))}
</nav>
<ToastContainer client:only="preact" />
<ToastContainer client:only="preact" transition:persist />
<header>
<AuthStatus client:only="preact" />
<AuthStatus client:only="preact" transition:persist />
</header>
<main>
<main transition:animate="fade" >
<slot />
</main>

View File

@@ -0,0 +1,7 @@
// src/lib/nav.js
import siteConfig from '../data/site.json';
export function getBaseUrl(name) {
const item = siteConfig.navigation.find(n => n.name === name);
return item ? item.url : '/';
}

View File

@@ -1,6 +1,7 @@
---
// srs/pages/fleet/strojevi.astro
import Layout from '../../layouts/Layout.astro';
import FleetStrojeviLista from '../../components/FleetStrojeviLista.jsx';
import FleetStrojeviLista from '../../components/lista/FleetStrojeviLista.jsx';
import { getStrojevi } from '../../lib/api'; // Ovdje koristi getStrojevi
const strojeviData = await getStrojevi(); // I ovdje koristi getStrojevi
@@ -10,6 +11,6 @@ const strojeviData = await getStrojevi(); // I ovdje koristi getStrojevi
<main class="p-8">
<h1 class="text-3xl font-black mb-6 uppercase">Pregled strojeva</h1>
<FleetStrojeviLista client:load initialData={strojeviData} />
<FleetStrojeviLista client:load>
</main>
</Layout>

View File

@@ -2,11 +2,9 @@
// src/pages/index.astro
import Layout from "../layouts/Layout.astro";
import siteConfig from "../data/site.json";
import { getRadniNalozi, getVozila } from "../lib/api";
import RadniNalogLista from '../components/RadniNalogLista.jsx';
const nalozi = await getRadniNalozi();
const vozila = await getVozila();
import RadniNalogLista from '../components/lista/RadniNalogLista.jsx';
import FleetStrojeviLista from '../components/lista/FleetStrojeviLista.jsx';
import FleetVozilaLista from "../components/lista/FleetVozilaLista";
---
<Layout title="Dashboard">
@@ -18,19 +16,18 @@ const vozila = await getVozila();
<main class="dashboard-grid">
<section>
<h2>Radni nalozi</h2>
<RadniNalogLista client:load nalozi={nalozi} limit={5} />
{/* client:only osigurava da se lista sama dohvati u pregledniku */}
<RadniNalogLista client:only="preact" limit={5} />
</section>
<section>
<h2>Stanje flote</h2>
<div>
<p>Ukupno vozila: {vozila.length}</p>
<ul>
{vozila.map((vozilo) => (
<li>{vozilo.marka} {vozilo.model} ({vozilo.registracija})</li>
))}
</ul>
</div>
<FleetStrojeviLista client:only="preact" limit={5} />
</section>
<section>
<h2>Stanje flote</h2>
<FleetVozilaLista client:only="preact" limit={5} />
</section>
</main>
</Layout>

View File

@@ -0,0 +1,114 @@
## Skeleton za List View
```jsx
// src/components/common/UniversalList.jsx
import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { useStore } from '@nanostores/preact';
import { getBaseUrl } from '../lib/nav';
import LoadingSpinner from './LoadingSpinner.jsx';
export default function UniversalList({ store, setter, fetchMethod, limit, baseName, children }) {
const [loading, setLoading] = useState(true);
const list = useStore(store);
// Ako je baseName poslan, dohvati URL, inače ostavi undefined
const baseUrl = baseName ? getBaseUrl(baseName) : undefined;
useEffect(() => {
async function fetchData() {
if (list.length > 0) {
setLoading(false);
return;
}
try {
setLoading(true);
const data = await fetchMethod();
setter(data || []);
} catch (error) {
console.error("Greška pri dohvatu:", error);
setter([]);
} finally {
setTimeout(() => setLoading(false), 1000);
}
}
fetchData();
}, []);
if (loading) return <LoadingSpinner text="Učitavanje..." />;
const displayList = limit ? list.slice(0, limit) : list;
return (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* children funkcija sada prima item i opcionalni baseUrl */}
{displayList.map(item => children(item, baseUrl))}
</div>
);
}
```
## Loading komponenta
```jsx
// src/components/common/LoadingSpinner.jsx
import { h } from 'preact';
export default function LoadingSpinner({ text = "Učitavanje podataka..." }) {
return (
<div class="flex items-center justify-center p-12 text-gray-500">
<div class="animate-spin mr-3 h-5 w-5 border-2 border-gray-400 border-t-transparent rounded-full"></div>
{text}
</div>
);
}
```
## Komponenta FleetStrojeviLista.jsx koristi Skeleton
```jsx
/// src/components/fleet/FleetStrojeviLista.jsx
import { h } from 'preact';
import UniversalList from '../common/UniversalList.jsx';
import { strojevi, setStrojevi } from '../stores/fleetStore';
import { getStrojevi } from '../lib/api';
export default function FleetStrojeviLista({ limit = 10 }) {
return (
<UniversalList
store={strojevi}
setter={setStrojevi}
fetchMethod={getStrojevi}
baseName="Registrirani strojevi"
limit={limit}
>
{(s, baseUrl) => (
<div key={s.id} class="p-6 border border-gray-200 rounded-2xl bg-white shadow-sm hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-4">
<h3 class="text-xl font-black text-gray-900">{s.naziv}</h3>
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-bold rounded-full uppercase">
{s.tip_human_readable}
</span>
</div>
<ul class="text-sm text-gray-600 space-y-1 mb-4">
<li><strong>Marka/Model:</strong> {s.marka} {s.model_stroja}</li>
<li><strong>Serijski br:</strong> {s.serijski_broj}</li>
<li><strong>Vlasnik:</strong> {s.vlasnik_naziv}</li>
<li><strong>Radni sati:</strong> {s.radni_sati} h</li>
{s.registracija && <li><strong>Reg:</strong> {s.registracija}</li>}
</ul>
<a
href={`${baseUrl}/${s.id}`}
class="block text-center w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-700 transition"
>
Pregled detalja
</a>
</div>
)}
</UniversalList>
);
}
```