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 { h } from 'preact';
|
||||||
import { useEffect } from 'preact/hooks';
|
import UniversalList from '../common/UniversalList.jsx';
|
||||||
import { useStore } from '@nanostores/preact';
|
import { strojevi, setStrojevi } from '../../stores/fleetStore.js';
|
||||||
import { strojevi, setStrojevi } from '../stores/fleetStore';
|
import { getStrojevi } from '../../lib/api.js';
|
||||||
|
|
||||||
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>;
|
|
||||||
|
|
||||||
|
export default function FleetStrojeviLista({ limit = 10 }) {
|
||||||
return (
|
return (
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<UniversalList
|
||||||
{list.map(s => (
|
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 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">
|
<div class="flex justify-between items-start mb-4">
|
||||||
<h3 class="text-xl font-black text-gray-900">{s.naziv}</h3>
|
<h3 class="text-xl font-black text-gray-900">{s.naziv}</h3>
|
||||||
@@ -33,13 +31,13 @@ export default function FleetStrojeviLista({ initialData = [] }) {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<a
|
<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"
|
class="block text-center w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-700 transition"
|
||||||
>
|
>
|
||||||
Pregled detalja
|
Pregled detalja
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</UniversalList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
// src/components/fleet/FleetVozilaLista.jsx
|
// src/components/lista/FleetVozilaLista.jsx
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useEffect } from 'preact/hooks';
|
import UniversalList from '../common/UniversalList.jsx';
|
||||||
import { useStore } from '@nanostores/preact';
|
import { vozila, setVozila } from '../../stores/fleetStore.js';
|
||||||
import { vozila, setVozila } from '../stores/fleetStore';
|
import { getVozila } from '../../lib/api.js';
|
||||||
|
|
||||||
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>;
|
|
||||||
|
|
||||||
|
export default function FleetVozilaLista({ limit = 10 }) {
|
||||||
return (
|
return (
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<UniversalList
|
||||||
{list.map(v => (
|
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 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">
|
<div class="flex justify-between items-start mb-4">
|
||||||
<h3 class="text-xl font-black text-gray-900">{v.naziv}</h3>
|
<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}
|
{v.status_prikaz}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="text-sm text-gray-600 space-y-2 mb-4">
|
<ul class="text-sm text-gray-600 space-y-2 mb-4">
|
||||||
<li><strong>Registracija:</strong> {v.registracija}</li>
|
<li><strong>Registracija:</strong> {v.registracija}</li>
|
||||||
<li><strong>Trenutna km:</strong> {v.trenutni_kilometri.toLocaleString()} km</li>
|
<li><strong>Trenutna km:</strong> {v.trenutni_kilometri.toLocaleString()} km</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={`${baseUrl}/${v.id}`}
|
href={`${baseUrl}/${v.id}`}
|
||||||
class="block text-center w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-700 transition"
|
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
|
Pregled detalja
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 { h } from "preact";
|
||||||
import { useState, useEffect } from "preact/hooks";
|
import { useState, useEffect } from "preact/hooks";
|
||||||
import { navigate } from "astro:transitions/client";
|
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 { showToast } from "../../stores/toastStore";
|
||||||
import Button from "../Button.jsx";
|
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() {
|
export default function NoviRadniNalogForm() {
|
||||||
const [putniNalogMode, setPutniNalogMode] = useState('none');
|
const [formData, setFormData] = useState({ klijent: '', stroj: '', opis_kvara: '', putni_mode: 'none', putni_id: '' });
|
||||||
const [putniNalozi, setPutniNalozi] = useState([]);
|
const [data, setData] = useState({ klijenti: [], strojevi: [], putniNalozi: [] });
|
||||||
const [selectedPutniId, setSelectedPutniId] = useState('');
|
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 payload = new FormData(e.target);
|
||||||
const [klijentId, setKlijentId] = useState('');
|
if (formData.putni_mode === 'new') payload.append('kreiraj_putni', 'true');
|
||||||
const [strojevi, setStrojevi] = useState([]);
|
// Ako je existing, FormData već ima 'putni_nalog' polje iz select-a
|
||||||
const [selectedStrojId, setSelectedStrojId] = useState('');
|
|
||||||
|
|
||||||
const [isFetchingNaloge, setIsFetchingNaloge] = useState(false);
|
const success = await createNalog(payload);
|
||||||
const [isFetchingStrojevi, setIsFetchingStrojevi] = useState(false);
|
if (success) {
|
||||||
const [loading, setLoading] = useState(false);
|
showToast("Radni nalog uspješno kreiran!", "success");
|
||||||
|
navigate('/operativa/radni-nalozi');
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Dohvati listu klijenata pri učitavanju forme
|
return (
|
||||||
useEffect(() => {
|
<form onSubmit={handleSubmit} class="space-y-6">
|
||||||
// Pretpostavljam da imaš funkciju getKlijenti() u api.js
|
<FormSelect
|
||||||
fetchKupciData().then(data => setKlijenti(data));
|
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
|
<FormSelect
|
||||||
useEffect(() => {
|
label="Stroj" items={data.strojevi} placeholder="Odaberite stroj" disabled={!formData.klijent}
|
||||||
if (putniNalogMode === 'existing') {
|
onChange={(e) => setFormData({...formData, stroj: e.target.value})}
|
||||||
setIsFetchingNaloge(true);
|
renderItem={s => <option value={s.id}>{s.naziv} (S/N: {s.serijski_broj})</option>}
|
||||||
getPutniNalozi()
|
/>
|
||||||
.then(data => setPutniNalozi(data))
|
|
||||||
.finally(() => setIsFetchingNaloge(false));
|
|
||||||
}
|
|
||||||
}, [putniNalogMode]);
|
|
||||||
|
|
||||||
// 2. Dohvat Strojeva ovisno o klijentu
|
<div class="mb-4">
|
||||||
useEffect(() => {
|
<label class="block text-sm font-bold text-gray-300 mb-1">Opis kvara</label>
|
||||||
// Popravljeno: slušamo klijentId, a ne klijenti (koji je niz)
|
<textarea name="opis_kvara" required class="w-full p-3 bg-gray-700 rounded text-white" />
|
||||||
if (klijentId) {
|
</div>
|
||||||
setIsFetchingStrojevi(true);
|
|
||||||
getStrojeviData(klijentId)
|
|
||||||
.then(data => setStrojevi(data))
|
|
||||||
.finally(() => setIsFetchingStrojevi(false));
|
|
||||||
} else {
|
|
||||||
setStrojevi([]);
|
|
||||||
setSelectedStrojId('');
|
|
||||||
}
|
|
||||||
}, [klijentId]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
<div class="border-t border-gray-700 pt-6">
|
||||||
e.preventDefault();
|
<FormSelect
|
||||||
setLoading(true);
|
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);
|
{formData.putni_mode === 'existing' && (
|
||||||
|
<FormSelect
|
||||||
// Logika za povezivanje putnog naloga koju backend view sada očekuje
|
label="Odaberite nalog" items={data.putniNalozi} placeholder="Popis aktivnih naloga"
|
||||||
if (putniNalogMode === 'existing' && selectedPutniId) {
|
onChange={(e) => setFormData({...formData, putni_id: e.target.value})}
|
||||||
formData.append('putni_nalog', selectedPutniId);
|
renderItem={pn => <option value={pn.id}>{pn.broj_naloga} - {pn.mjesto_odredista}</option>}
|
||||||
} else if (putniNalogMode === 'new') {
|
/>
|
||||||
formData.append('kreiraj_putni', 'true');
|
)}
|
||||||
}
|
</div>
|
||||||
|
|
||||||
const result = await createNalog(formData);
|
<Button type="submit" loading={loading} className="w-full">
|
||||||
|
Kreiraj Radni Nalog
|
||||||
if (result) {
|
</Button>
|
||||||
showToast("Radni nalog uspješno kreiran!", "success");
|
</form>
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -72,13 +72,13 @@ const { title } = Astro.props;
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<ToastContainer client:only="preact" />
|
<ToastContainer client:only="preact" transition:persist />
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<AuthStatus client:only="preact" />
|
<AuthStatus client:only="preact" transition:persist />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main transition:animate="fade" >
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</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 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
|
import { getStrojevi } from '../../lib/api'; // Ovdje koristi getStrojevi
|
||||||
|
|
||||||
const strojeviData = await getStrojevi(); // I 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">
|
<main class="p-8">
|
||||||
<h1 class="text-3xl font-black mb-6 uppercase">Pregled strojeva</h1>
|
<h1 class="text-3xl font-black mb-6 uppercase">Pregled strojeva</h1>
|
||||||
|
|
||||||
<FleetStrojeviLista client:load initialData={strojeviData} />
|
<FleetStrojeviLista client:load>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
@@ -2,11 +2,9 @@
|
|||||||
// src/pages/index.astro
|
// src/pages/index.astro
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
import siteConfig from "../data/site.json";
|
import siteConfig from "../data/site.json";
|
||||||
import { getRadniNalozi, getVozila } from "../lib/api";
|
import RadniNalogLista from '../components/lista/RadniNalogLista.jsx';
|
||||||
import RadniNalogLista from '../components/RadniNalogLista.jsx';
|
import FleetStrojeviLista from '../components/lista/FleetStrojeviLista.jsx';
|
||||||
|
import FleetVozilaLista from "../components/lista/FleetVozilaLista";
|
||||||
const nalozi = await getRadniNalozi();
|
|
||||||
const vozila = await getVozila();
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Dashboard">
|
<Layout title="Dashboard">
|
||||||
@@ -18,19 +16,18 @@ const vozila = await getVozila();
|
|||||||
<main class="dashboard-grid">
|
<main class="dashboard-grid">
|
||||||
<section>
|
<section>
|
||||||
<h2>Radni nalozi</h2>
|
<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>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Stanje flote</h2>
|
<h2>Stanje flote</h2>
|
||||||
<div>
|
<FleetStrojeviLista client:only="preact" limit={5} />
|
||||||
<p>Ukupno vozila: {vozila.length}</p>
|
</section>
|
||||||
<ul>
|
|
||||||
{vozila.map((vozilo) => (
|
<section>
|
||||||
<li>{vozilo.marka} {vozilo.model} ({vozilo.registracija})</li>
|
<h2>Stanje flote</h2>
|
||||||
))}
|
<FleetVozilaLista client:only="preact" limit={5} />
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</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