UniveralList
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
11
002.FRONTEND/src/components/common/LoadingSpinner.jsx
Normal file
11
002.FRONTEND/src/components/common/LoadingSpinner.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
002.FRONTEND/src/components/common/UniversalList.jsx
Normal file
46
002.FRONTEND/src/components/common/UniversalList.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -36,7 +35,7 @@ export default function FleetVozilaLista({ initialData = [], baseUrl }) {
|
||||
Pregled detalja
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</UniversalList>
|
||||
);
|
||||
}
|
||||
28
002.FRONTEND/src/components/lista/RadniNalogLista.jsx
Normal file
28
002.FRONTEND/src/components/lista/RadniNalogLista.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
const [klijenti, setKlijenti] = useState([]);
|
||||
const [klijentId, setKlijentId] = useState('');
|
||||
const [strojevi, setStrojevi] = useState([]);
|
||||
const [selectedStrojId, setSelectedStrojId] = useState('');
|
||||
// 1. Inicijalni dohvat klijenata
|
||||
useEffect(() => {
|
||||
getKupci().then(c => setData(prev => ({ ...prev, klijenti: c })));
|
||||
}, []);
|
||||
|
||||
const [isFetchingNaloge, setIsFetchingNaloge] = useState(false);
|
||||
const [isFetchingStrojevi, setIsFetchingStrojevi] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// 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]);
|
||||
|
||||
// Dohvati listu klijenata pri učitavanju forme
|
||||
useEffect(() => {
|
||||
// Pretpostavljam da imaš funkciju getKlijenti() u api.js
|
||||
fetchKupciData().then(data => setKlijenti(data));
|
||||
}, []);
|
||||
// 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]);
|
||||
|
||||
// Dohvat Putnih naloga
|
||||
useEffect(() => {
|
||||
if (putniNalogMode === 'existing') {
|
||||
setIsFetchingNaloge(true);
|
||||
getPutniNalozi()
|
||||
.then(data => setPutniNalozi(data))
|
||||
.finally(() => setIsFetchingNaloge(false));
|
||||
}
|
||||
}, [putniNalogMode]);
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
// 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]);
|
||||
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 handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const success = await createNalog(payload);
|
||||
if (success) {
|
||||
showToast("Radni nalog uspješno kreiran!", "success");
|
||||
navigate('/operativa/radni-nalozi');
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
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>}
|
||||
/>
|
||||
|
||||
// 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');
|
||||
}
|
||||
<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>}
|
||||
/>
|
||||
|
||||
const result = await createNalog(formData);
|
||||
<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>
|
||||
|
||||
if (result) {
|
||||
showToast("Radni nalog uspješno kreiran!", "success");
|
||||
navigate('/operativa/radni-nalozi');
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
<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>}
|
||||
/>
|
||||
|
||||
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>
|
||||
{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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
<Button type="submit" loading={loading} className="w-full">
|
||||
Kreiraj Radni Nalog
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
7
002.FRONTEND/src/lib/nav.js
Normal file
7
002.FRONTEND/src/lib/nav.js
Normal 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 : '/';
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
114
biljeske/SKELETON_LIST_VIEW.md
Normal file
114
biljeske/SKELETON_LIST_VIEW.md
Normal 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user