474 lines
15 KiB
JavaScript
474 lines
15 KiB
JavaScript
// 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;
|
|
}
|
|
} |