This commit is contained in:
2026-05-22 06:08:51 +00:00
parent c078a257e9
commit bafec754d1
131 changed files with 2346 additions and 893 deletions

468
001.FRONTEND/src/lib/api.js Normal file
View File

@@ -0,0 +1,468 @@
// src/lib/api.js
const API_BASE = import.meta.env.PUBLIC_API_URL;
/**
* POMOĆNA FUNKCIJA:端 Dohvaća token i postavlja Headere
*/
function getAuthHeaders(bodyData = {}) {
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`; // Usklađeno sa Simple JWT standardom
}
// Provjera je li bodyData FormData (za slike/naloge)
const isFormData = bodyData instanceof FormData;
// Ako NIJE FormData, šaljemo JSON
if (!isFormData) {
headers['Content-Type'] = 'application/json';
}
return headers;
}
/**
* Centralizirana obrada odgovora s Toast podrškom
*/
async function handleResponse(res) {
if (res.status === 401) {
console.warn("Token istekao ili je nevažeći.");
}
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
console.error("API Error Response:", errorData);
let msg = "Greška pri sinkronizaciji podataka.";
if (errorData.detail) {
msg = errorData.detail;
}
else if (typeof errorData === 'object' && errorData !== null) {
const kljuceviGresaka = Object.keys(errorData);
if (kljuceviGresaka.length > 0) {
const prvoPolje = kljuceviGresaka[0];
const greskaVrijednost = errorData[prvoPolje];
if (Array.isArray(greskaVrijednost) && greskaVrijednost.length > 0) {
msg = `${prvoPolje}: ${greskaVrijednost[0]}`;
} else if (typeof greskaVrijednost === 'string') {
msg = `${prvoPolje}: ${greskaVrijednost}`;
}
}
}
if (typeof window !== 'undefined') {
window.showToast?.(msg, "error");
}
return null;
}
// VRAĆA PARSIRAN OBJEKT - Stream je zatvoren nakon ove linije!
return await res.json();
}
// --- RUTE ---
export const routes = {
// Autentifikacija
login: () => '/token/',
trenutniKorisnik: () => '/users/me/',
// Radni nalozi
radniNalozi: (params = {}) => {
const baseUrl = '/operativa/radni-nalozi/';
// Čistimo null i undefined vrijednosti iz filtera
const cleanParams = Object.fromEntries(
Object.entries(params).filter(([_, v]) => v != null)
);
const queryString = new URLSearchParams(cleanParams).toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
},
radniNalogDetalji: (id) => `/operativa/radni-nalozi/${id}/`,
sljedeciBrojNaloga: () => '/operativa/radni-nalozi/sljedeci-broj/',
// Putni nalozi i logistika
putniNalozi: () => '/operativa/putni-nalozi/',
// Vozila i strojevi (Fleet modul)
vozila: () => '/fleet/vozila/', // USKLAĐENO: Prebačeno pod zajednički /fleet/ namespace
strojevi: (vlasnikId = null) => {
const baseUrl = '/fleet/strojevi/';
if (vlasnikId) return `${baseUrl}?vlasnik=${vlasnikId}`;
return baseUrl;
},
strojDetalji: (id) => `/fleet/strojevi/${id}/`,
// Kupci / Klijenti
kupciSvi: () => '/kupci/svi/',
kupacDetalji: (id) => `/kupci/svi/${id}/`,
// Kalendar i raspored
mojRaspored: () => '/kalendar/moj-raspored/',
kalendarDogadaji: () => '/kalendar/dogadaji/'
};
// --- API METODE ---
/**
* Dohvat profila trenutno prijavljenog korisnika
*/
export async function fetchCurrentUser() {
try {
const url = `${API_BASE}${routes.trenutniKorisnik()}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
return await handleResponse(res);
} catch (e) {
console.error("fetchCurrentUser Failure:", e);
return null;
}
}
/**
* Dohvat i filtriranje radnih naloga (za tablice i liste u operativi)
*/
export async function fetchRadniNalozi(params = {}) {
try {
// Logika čišćenja i slaganja query parametara je sada delegirana routes objektu!
const url = `${API_BASE}${routes.radniNalozi(params)}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
return await handleResponse(res);
} catch (e) {
console.error("fetchRadniNalozi Failure:", e);
return [];
}
}
/**
* Paralelni dohvat podataka za glavno dispečersko sučelje (Dashboard flote i naloga)
*/
export async function fetchDashboardData() {
try {
// Dinamički povlačimo staze iz routes objekta za paralelno okidanje
const urlVozila = `${API_BASE}${routes.vozila()}`;
const urlNalozi = `${API_BASE}${routes.radniNalozi()}`;
const [resV, resN] = await Promise.all([
fetch(urlVozila, { method: 'GET', headers: getAuthHeaders() }),
fetch(urlNalozi, { method: 'GET', headers: getAuthHeaders() })
]);
// Budući da Dashboard podatke često renderiraš u paralelnim karticama,
// koristimo brzi i sigurni fallback u slučaju prazne baze ili neočekivanog formata
const vozilaData = resV.ok ? await resV.json().catch(() => []) : [];
const naloziData = resN.ok ? await resN.json().catch(() => []) : [];
return {
vozila: Array.isArray(vozilaData) ? vozilaData : (vozilaData.results || []),
nalozi: Array.isArray(naloziData) ? naloziData : (naloziData.results || [])
};
} catch (e) {
console.error("FetchDashboardData Failure:", e);
return { vozila: [], nalozi: [] };
}
}
/**
* Prijava korisnika i pohrana JWT tokena
*/
export async function login(email, password) {
try {
// Povlačenje centralizirane rute za token
const url = `${API_BASE}${routes.login()}`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Neuspješna prijava");
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
return { success: true };
} catch (e) {
console.error("Login Error:", e);
return { success: false, error: e.message };
}
}
/**
* Odjava korisnika i čišćenje lokalne pohrane
*/
export function logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
if (typeof window !== 'undefined') window.location.href = '/login';
}
/**
* Dohvat detalja pojedinačnog kupca/klijenta
*/
export async function fetchKupacDetalji(id) {
try {
const url = `${API_BASE}${routes.kupacDetalji(id)}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
return await handleResponse(res);
} catch (e) {
console.error(`Greška u fetchKupacDetalji za ID ${id}:`, e);
return null;
}
}
/**
* Unos novog servisnog vozila u bazu flote
*/
export async function createVozilo(payload) {
try {
const url = `${API_BASE}${routes.vozila()}`;
const res = await fetch(url, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(payload)
});
const data = await handleResponse(res);
if (data && typeof window !== 'undefined') {
window.showToast?.("Vozilo uspješno uneseno!", "success");
}
return data;
} catch (e) {
console.error("Greška u createVozilo:", e);
return null;
}
}
/**
* Kreiranje novog radnog naloga (šalje se FormData zbog učitavanja slika s terena)
*/
export async function createNalog(formData) {
try {
// Koristimo bazičnu rutu bez parametara za POST zahtjev
const url = `${API_BASE}${routes.radniNalozi()}`;
const res = await fetch(url, {
method: 'POST',
headers: getAuthHeaders(formData), // Automatski izbacuje Content-Type za FormData
body: formData
});
return await handleResponse(res);
} catch (e) {
console.error("Greška u createNalog:", e);
return null;
}
}
/**
* Parcijalno ažuriranje radnog naloga (npr. promjena statusa, dodavanje opisa) preko centralizirane rute
*/
export async function patchNalog(id, data) {
try {
// Koristimo novu stazu za detalje naloga iz routes objekta
const url = `${API_BASE}${routes.radniNalogDetalji(id)}`;
const res = await fetch(url, {
method: 'PATCH',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
return await handleResponse(res);
} catch (e) {
console.error(`Greška u patchNalog za ID ${id}:`, e);
return null;
}
}
/**
* Dohvat svih kalendarskih događaja (planirani servisi, atesti, tereni)
*/
export async function fetchCalendarEvents() {
try {
// Koristimo novu stazu iz routes objekta
const url = `${API_BASE}${routes.kalendarDogadaji()}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
// Centralizirana obrada odgovora s Toast error handlingom
const data = await handleResponse(res);
if (!data) return []; // Siguran fallback ako zahtjev baci grešku
return Array.isArray(data) ? data : (data.results || []);
} catch (e) {
console.error("fetchCalendarEvents Failure:", e);
return [];
}
}
/**
* Dohvat osobnog rasporeda servisera iz kalendara
*/
export async function fetchMojRaspored() {
try {
// Koristimo stazu iz routes objekta
const url = `${API_BASE}${routes.mojRaspored()}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
// Centralizirana obrada odgovora s Toast error handlingom
const data = await handleResponse(res);
if (!data) return []; // Siguran fallback na prazan niz ako zahtjev ne prođe
return Array.isArray(data) ? data : (data.results || []);
} catch (e) {
console.error("fetchMojRaspored Failure:", e);
return [];
}
}
/**
* Dohvat popisa svih kupaca/klijenata za dropdown u novom nalogu
*/
export async function fetchKupciData() {
try {
// Koristimo stazu iz routes objekta
const url = `${API_BASE}${routes.kupciSvi()}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
// Centralizirana obrada odgovora
const data = await handleResponse(res);
if (!data) return { kupci: [] }; // Fallback ako je token nevažeći
// Vraćamo objekt u formatu koji novi.astro destrukturira na serveru
return {
kupci: Array.isArray(data) ? data : (data.results || [])
};
} catch (e) {
console.error("fetchKupciData Failure:", e);
return { kupci: [] };
}
}
/**
* Dohvat popisa strojeva (opcionalno filtrirano po vlasniku) pomoću centralizirane rute
*/
export async function fetchStrojeviData(vlasnikId = null) {
try {
// Generiramo ispravan URL preko routes objekta
const url = `${API_BASE}${routes.strojevi(vlasnikId)}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
// Koristimo centraliziranu obradu odgovora (s Toast podrškom)
const data = await handleResponse(res);
if (!data) return { strojevi: [] }; // Siguran fallback ako je zahtjev prekinut (401, 500...)
// Vraćamo objekt sa strojevima prateći strukturu koju novi.astro očekuje (.map destructuring)
return {
strojevi: Array.isArray(data) ? data : (data.results || [])
};
} catch (e) {
console.error("fetchStrojeviData Failure:", e);
return { strojevi: [] };
}
}
/**
* Dohvat popisa svih putnih naloga pomoću centralizirane rute i handleResponse-a
*/
export async function fetchPutniNaloziData() {
try {
// Koristimo zajedničku stazu iz routes objekta
const url = `${API_BASE}${routes.putniNalozi()}`;
const res = await fetch(url, {
method: 'GET',
headers: getAuthHeaders()
});
// Koristimo centraliziranu obradu odgovora (s Toast error handlingom)
const data = await handleResponse(res);
if (!data) return []; // Fallback ako je handleResponse presreo grešku i vratio null
// Provjera vraća li Django čistu listu ili paginirani rezultatski objekt (results)
return Array.isArray(data) ? data : (data.results || []);
} catch (e) {
console.error("Greška pri dohvatu putnih naloga:", e);
return [];
}
}
export async function fetchSljedeciBrojNaloga() {
try {
// Koristimo novu rutu iz routes objekta
const url = `${API_BASE}${routes.sljedeciBrojNaloga()}`;
const res = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
return await res.json();
} else {
console.error("Greška na backendu pri dohvaćanju brojača:", res.status);
return { broj_naloga: "RN-2026-XXXX" };
}
} catch (err) {
console.error("Mrežna greška u fetchSljedeciBrojNaloga:", err);
return { broj_naloga: "RN-2026-XXXX" };
}
}
/**
* Kreiranje novog putnog naloga - S koso crtom (trailing slash) za Django usklađenost
*/
export async function createPutniNalog(radniNalogId, voziloId) {
try {
// Koristimo novu stazu iz routes objekta
const url = `${API_BASE}${routes.putniNalozi()}`;
const res = await fetch(url, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
radni_nalog_id: parseInt(radniNalogId, 10),
vozilo: parseInt(voziloId, 10)
})
});
// Vraća gotov JSON objekt (ili null ako je toast okinuo grešku)
return await handleResponse(res);
} catch (e) {
console.error("createPutniNalog krah:", e);
return null;
}
}