v004
This commit is contained in:
468
001.FRONTEND/src/lib/api.js
Normal file
468
001.FRONTEND/src/lib/api.js
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user