// src/lib/api.js // 1. Osiguravamo da API_BASE uvijek završava s točno jednom kosom crtom const RAW_BASE = import.meta.env.PUBLIC_API_URL; const API_BASE = RAW_BASE.endsWith('/') ? RAW_BASE : `${RAW_BASE}/`; /** * 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 (Maknute početne kose crte!) login: () => 'token/', trenutniKorisnik: () => 'users/me/', // Radni nalozi radniNalozi: (params = {}) => { const baseUrl = 'operativa/radni-nalozi/'; 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/', 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 []; } } /** * Dohvaća sljedeći slobodni broj radnog naloga s backenda * Osigurano protiv duplih kosih crta i preflight redirecta */ export async function fetchSljedeciBrojNaloga() { try { // Pametno spajanje baze i rute (isto kao u ostalim očišćenim metodama) const base = API_BASE.endsWith('/') ? API_BASE : `${API_BASE}/`; const ruta = routes.sljedeciBrojNaloga(); const url = `${base}${ruta}`; const res = await fetch(url, { method: 'GET', headers: getAuthHeaders() // Koristi centralizirane headere umjesto sirovog objekta }); 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; } }