Files
fleet-template.008/biljeske/LOCALSTORAGE_UI_OBAVIJESTI.MD
2026-05-31 19:21:25 +02:00

400 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Upit
```md
Obrazloži kao da radiš prezentaciju kako je implementirano rješenje preko localStorage
Obrazloži kako se mijenja stanje u UI iz loading u loaded
Obrazloži što je potrebno na django rest frameworku da ovo funkcionira
```
# Odgovor (Gemini)
Evo izravne inženjerske prezentacije i analize arhitekture sustava, fokusirane **isključivo** na priložene datoteke (`api.js`, `index.astro` i `RadniNalogLista.astro`), s jasnim obrazloženjem svih traženih mehanizama i DRF preduvjeta.
---
## 1. Implementacija autentifikacije i autorizacije preko `localStorage`-a
U priloženom kodu, upravljanje sesijom i dokazivanje identiteta prema Django backendu u potpunosti se oslanja na klijentski `localStorage`.
```
[Klijent: login()] ───> POST /token/ ───> [Django Simple JWT]
(access & refresh) <───┘
localStorage.setItem('access_token', data.access)
[Klijent: getAuthHeaders()] ──┴──> Headers: { Authorization: Bearer <token> }
```
### Detaljna mehanika unutar priloženih datoteka:
*
**Pohrana pri prijavi:** Unutar `api.js`, funkcija `login(email, password)` šalje POST zahtjev na endpoint definiran u `routes.login()`. Kada Django odobri pristup, asinkroni kôd presreće odgovor i upisuje tokene u memoriju preglednika:
```javascript
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
```
*
**Presretanje i slaganje zaglavlja (Interceptors):** Funkcija `getAuthHeaders(bodyData)` služi kao centralni generator sigurnosnih metapodataka. Ona prvo provjerava nalazi li se kôd u kontekstu preglednika kako bi sigurno pristupila `localStorage`-u bez rušenja Node.js okruženja:
```javascript
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
```
Ako token postoji, on se ubacuje u standardni format Simple JWT-a: `headers['Authorization'] = 'Bearer ' + token`.
*
**Automatsko čišćenje:** Funkcija `logout()` u `api.js` rješava suprotan proces briše ključeve `access_token` i `refresh_token` iz `localStorage`-a te preusmjerava korisnika na stranicu za prijavu.
---
## 2. Tranzicija stanja u UI-ju iz *Loading* u *Loaded*
Budući da su priložene datoteke `index.astro` i `RadniNalogLista.astro` u ovom trenutku konfigurirane kao **serverske komponente (SSR)** , tranzicija stanja iz *Loading* u *Loaded* odvija se na razini samog poslužitelja (Node.js/Docker) prije nego što HTML uopće stigne do preglednika.
### Korak po korak: Kako se mijenja stanje unutar priloženog koda
1. **Asinkroni paralelni dohvat (Podaci na čekanju / "Loading"):**
Na samom vrhu `index.astro`, unutar frontmattera (`---`), pokreće se paralelni dohvat podataka s baze pomoću `Promise.all`. U tom milisekundnom prozoru, dok Node.js čeka odgovor s Django API-ja, aplikacija je u "Loading" stanju na razini poslužitelja:
```javascript
const [data, user] = await Promise.all([ fetchDashboardData(), fetchCurrentUser() ]);
```
2. **Obrada i punjenje strukture (Data Hydration / "Loaded"):**
Čim se `Promise.all` razriješi, stanje prelazi u "Loaded" na serveru. Podaci se destrukturiraju unutar `index.astro` i `RadniNalogLista.astro`. Istog trenutka, asinkrone varijable popunjavaju statička mjesta u kodu:
*
`user?.first_name` definira pozdrav u `WelcomeHeader` komponenti.
*
`nalozi.filter(...)` proračunava točne brojke za `StatsGrid` kartice (Planirano, U radu, Gotovo).
3. **Isporuka čistog HTML-a klijentu:**
Komponenta `RadniNalogLista.astro` prima podatke iz `fetchDashboardData()`. Ako nalozi postoje, `.map()` funkcija generira njezinu strukturu i iscrtava gotove `GenericKarticaItem` komponente s točnim brojevima i stanjima. Korisnik na ekranu ne vidi nikakvo učitavanje niti kosture, već mu se stranica otvara trenutno napunjena podacima.
---
## 3. Što je potrebno na Django REST Frameworku da ovo funkcionira
Pregledom poslanih klijentskih metoda unutar `api.js`, tvoj Django backend mora striktno podržavati sljedeće DRF značajke:
### A. Konfiguracija Simple JWT Autentifikacije
Unutar `api.js`, funkcija `getAuthHeaders()` injektira `Authorization: Bearer <token>`. Da bi DRF prepoznao ovaj potpis, u tvojem Django `settings.py` mora biti uključen Simple JWT:
```python
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
```
Također, endpoint za prijavu unutar Django `urls.py` mora odgovarati stazi `token/` koju poziva `routes.login()`.
### B. Podrška za pametno prepoznavanje Content-Type-a (JSON vs Multipart)
Funkcija `getAuthHeaders()` dinamički prilagođava zaglavlja ovisno o tome šalju li se sirovi tekstualni podaci ili binarne datoteke s terena:
```javascript
const isFormData = bodyData instanceof FormData;
if (!isFormData) { headers['Content-Type'] = 'application/json'; }
```
*
**Za `createVozilo` i `patchNalog`:** Šalje se čisti `application/json`. Django mora imati uključen `JSONParser` (što je DRF default).
*
**Za `createNalog`:** Šalje se `FormData` objekt jer učitavaš slike s terena (`RadniNalogSlika`). Tvoj Django `RadniNalogViewSet` (ili pripadajuća funkcija) mora eksplicitno imati definiran `parser_classes = [MultiPartParser, FormParser]` kako bi ispravno razdvojio tekstualna polja od slikovnih datoteka unutar `request.data`.
### C. Dinamičko filtriranje i Query Parameters na ViewSetovima
Unutar `api.js`, objekt `routes` dinamički gradi query stringove za filtriranje:
*
`radniNalozi(params)` gradi string poput `operativa/radni-nalozi/?status=u_radu` ili filtrira prema vozilu.
*
`strojevi(vlasnikId)` gradi stazu `fleet/strojevi/?vlasnik=id`.
Da bi ovo radilo, tvoji DRF ViewSetovi na backendu moraju implementirati `django-filter` backend ili ručno prepisati `get_queryset()` metodu:
```python
# Primjer na backendu za fleet/strojevi/
def get_queryset(self):
queryset = Stroj.objects.all()
vlasnik_id = self.request.query_params.get('vlasnik')
if vlasnik_id:
queryset = queryset.filter(vlasnik_id=vlasnik_id)
return queryset
```
### D. CORS (Cross-Origin Resource Sharing) Propusnost
Budući da Astro aplikacija šalje HTTP zahtjeve s jedne adrese/porta na `PUBLIC_API_URL` (koji je definiran u `API_BASE`) , tvoj Django mora imati instaliran `django-cors-headers` paket te dopuštenu domenu frontenda kroz `CORS_ALLOWED_ORIGINS` konfiguraciju kako preglednik ne bi blokirao asinkrone `fetch` pozive.
# Client-Side Rendering - CSR
Evo kompletnog, tehnički strukturiranog i rafiniranog nacrta prezentacije za tvoj *ServisLog Terminal*. Odgovor je fokusiran **isključivo na priložene datoteke (`api.js`, `index.astro` i tvoj zadnji klijentski `RadniNalogLista.astro`)** te detaljno obuhvaća novu logiku sinkronizacije stanja između učitavanja podataka i upravljanja vizualnim elementima u `NaslovList.astro`.
---
# PREZENTACIJA: Migracija na CSR i Sinkronizacija Stanja Sučelja
## SLIJD 1: Arhitektura Autentifikacije i Tok Tokena (localStorage)
Uklanjanjem ovisnosti o kolačićima na poslužitelju, aplikacija je prebačena na stopostotni klijentski model autorizacije. To eliminira serverske krahove (`Given token not valid...`) jer se provjera identiteta odvija unutar izoliranog okruženja preglednika.
```
[Klijent: login()] ───> POST /api/token/ ───> [Django REST Framework]
(access & refresh) <───┘
localStorage.setItem('access_token', data.access)
[Klijent: getAuthHeaders()] ──┴──> Headers: { Authorization: Bearer <token> }
```
### Tehnička mehanika unutar priloženih datoteka:
* **Pohrana pri prijavi:** Asinkrona funkcija `login(email, password)` u `api.js` šalje korisničke podatke na backend. Nakon validacije, klijentski kôd presreće odgovor i trajno upisuje tokene u memoriju:
```javascript
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
```
* **Centralizirani presretač (getAuthHeaders):** Prije slanja bilo kojeg operativnog zahtjeva (poput `fetchDashboardData()`), funkcija `getAuthHeaders(bodyData)` provjerava postojanje `window` objekta kako bi sigurno pročitala memoriju klijenta:
```javascript
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
```
Ako token postoji, on se injektira u standardno HTTP zaglavlje: `headers['Authorization'] = 'Bearer ' + token`.
---
## SLIJD 2: Životni vijek Tranzicije Sučelja (Loading -> Loaded)
Klijentsko renderiranje donosi asinkroni životni vijek u kojem se elementi sučelja ne prikazuju odjednom, već se postupno aktiviraju (hidriraju) onog trenutka kada podaci stignu s mreže.
### Tri faze tranzicije u sučelju:
1. **Faza Učitavanja (Loading):** Poslužitelj isporučuje kostur stranice. Korisnik odmah vidi animirani krug (`#nalozi-loader`) s pulsirajućim tekstom *"Sinkronizacija radnih naloga..."*. Istovremeno, gornje kartice filtera s brojačima su **potpuno sakrivene** kako korisnik ne bi vidio nule i kako ne bi mogao okinuti preuranjeni klik.
2. **Faza Hidracije (Data Processing):** Klijentski JavaScript u pozadini izvršava `fetchDashboardData()`, prima sirovi niz naloga, preračunava statistiku (`planiranoCount`, `uRaduCount`), filtrira elemente prema stanjima iz URL-a i gradi HTML stabla.
3. **Faza Prikaza (Loaded):** Izvršava se atomska zamjena CSS klasa u DOM-u. Loader se skriva, generirane kartice radnih naloga se ubacuju u kontejner, a kartice s brojačima u zaglavlju glatko postaju vidljive s točnim, svježim vrijednostima.
---
## SLIJD 3: Sinkronizacija Stanja preko Klase `#nalozi-loader` i Varijabli Sučelja
Ovaj mehanizam rješava kritičan problem sinkronizacije: kako spriječiti prikaz praznih stat-kartica u `NaslovList.astro` dok `RadniNalogLista.astro` još uvijek čeka podatke s API-ja.
### Implementacija u `NaslovList.astro`:
Desni kontejner koji drži stat-kartice (`#stat-cards-container`) inicijalno se isporučuje s nultom vidljivošću i blokiranim interakcijama pomoću Tailwind pomoćnih klasa:
```html
<div id="stat-cards-container" class="opacity-0 pointer-events-none transition-opacity duration-300">
</div>
```
### Upravljanje stanjem unutar `RadniNalogLista.astro`:
Unutar asinkrone funkcije `renderirajRadneNalogeKlijentski()`, stanje se mijenja izravnom manipulacijom DOM elemenata tek **nakon uspješnog `try` bloka**:
```javascript
// 1. Upisivanje svježe proračunatih vrijednosti u DOM podkomponente
const planiranoValue = sekcija.querySelector('[data-filter="planirano"] .count-value');
if (planiranoValue) planiranoValue.textContent = planiranoCount.toString();
const uRaduValue = sekcija.querySelector('[data-filter="u_radu"] .count-value');
if (uRaduValue) uRaduValue.textContent = uRaduCount.toString();
// 2. TRANZICIJA STANJA: Uklanjanje loadera i aktivacija kartica
if (loader) {
loader.classList.add('hidden'); // Sakrivamo pulsirajući loader operacije
}
if (statCardsContainer) {
// Gasimo nevidljivost i ponovno dopuštamo klikove na klijentske filtre
statCardsContainer.classList.remove('opacity-0', 'pointer-events-none');
statCardsContainer.classList.add('opacity-100');
}
```
---
## SLIJD 4: Preduvjeti na Django REST Frameworku (Backend)
Da bi klijentski kod iz `api.js` i `RadniNalogLista.astro` radio bez pogrešaka, DRF mora striktno podržavati četiri arhitektonska standarda:
* **CORS (Cross-Origin Resource Sharing) Propusnost:** Budući da Astro šalje asinkrone fetch zahtjeve s klijenta (`localhost:4321`) na domenu backenda, u Django `settings.py` mora biti uključen `django-cors-headers` middleware, a adresa frontenda mora biti upisana u `CORS_ALLOWED_ORIGINS` listu.
* **Simple JWT Validacija:** Backend mora prepoznati i dekodirati `Authorization: Bearer <token>` zaglavlje koje generira funkcija `getAuthHeaders()`. Polje odgovora na `/api/token/` endpointu mora vraćati objekt s ključem `access`.
* **Multi-Parser Podrška (JSON vs Multipart):** Funkcija `getAuthHeaders()` u `api.js` provjerava tip podataka prije slanja:
```javascript
const isFormData = bodyData instanceof FormData;
if (!isFormData) { headers['Content-Type'] = 'application/json'; }
```
To znači da Django ViewSetovi moraju imati omogućene odgovarajuće parsere. Za bazične preglede (`fetchDashboardData`) koristi se `JSONParser`, dok za funkciju kreiranja naloga sa slikama s terena (`createNalog`), Django klasa mora imati `parser_classes = [MultiPartParser, FormParser]`.
* **Query Parametri za Filtriranje:** Kako bi klijentski URL parametri poput `?status=u_radu` vratili ispravne podatke, Django ViewSet mora presretati zahtjeve i filtrirati SQL upite na razini baze kroz `get_queryset()` metodu ili preko `DjangoFilterBackend` paketa prije slanja JSON-a natrag u Astro.
# Server-Side Rendering - SSR
U priloženom kodu datoteke `RadniNalogLista.astro` koji je postavljen kao **SSR (Server-Side Rendering)** komponenta, klasa **`nalozi-loader` uopće ne postoji niti se koristi**.
Međutim, ako tu komponentu želimo prebaciti na **klijentsko renderiranje (Client-side rendering)** kako bi vukla podatke izravno iz `localStorage`-a u pregledniku, uvođenje klase/ID-ja `nalozi-loader` postaje ključni mehanizam za upravljanje stanjem sučelja.
Evo detaljnog obrazloženja kako taj mehanizam točno funkcionira kroz životni vijek klijentske komponente, podijeljenog u tri cjeline:
---
## 1. Konceptualni prikaz: Životni vijek tranzicije sučelja
Kada se učitavanje prebaci na klijenta, sučelje prolazi kroz asinkroni proces zamjene elemenata u DOM-u:
```
[Klijent otvara stranicu]
├──> Renderira se samo statični HTML skeleton s loaderom
│ (Vidljiv element: #nalozi-loader, Sakriven element: #nalozi-list)
├──> Okida se asinkroni JavaScript: fetchDashboardData()
│ (Preglednik čita JWT token iz localStorage-a i šalje zahtjev)
[Podaci stigli s Django API-ja]
├──> JS generira HTML kartice unutar #nalozi-list
└──> ATOMSKA ZAMJENA STANJA (Pomoću CSS klasa):
#nalozi-loader ──> .classList.add('hidden')
#nalozi-list ──> .classList.remove('hidden')
```
---
## 2. Implementacija unutar strukture `RadniNalogLista.astro`
Unutar samog Astro HTML koda (ispod frontmattera), struktura se postavlja tako da loader zauzima cijeli prostor predviđen za listu, sprječavajući "skakanje" sučelja (*layout shift*) dok se podaci čekaju:
```html
<div class="space-y-6" id="radni-nalozi-sekcija">
<NaslovList ... />
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl overflow-hidden">
<div id="nalozi-loader" class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse">
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i>
Sinkronizacija radnih naloga...
</div>
<div id="nalozi-list" class="space-y-0"></div>
</div>
</div>
```
---
## 3. Kako JavaScript upravlja loaderom i stanjem (Loaded)
Unutar `<script>` bloka u komponenti, logika pronalazi element `nalozi-loader` preko njegovog ID-ja ili klase i manipulira njegovom vidljivošću pomoću Tailwindovih pomoćnih klasa:
```javascript
<script>
import { fetchDashboardData } from "../lib/api";
import { getStatusColorClass, formatStatus } from "../utils/ui";
async function renderirajRadneNaloge() {
const container = document.getElementById('nalozi-list');
const loader = document.getElementById('nalozi-loader'); // <--- Hvatanje loader elementa
if (!container) return;
try {
// 1. Pokretanje mrežnog zahtjeva (Korisnik u ovom trenu i dalje vidi vrteći loader)
const data = await fetchDashboardData();
const nalozi = data?.nalozi || [];
// 2. Generiranje HTML strukture za kartice (Data Hydration)
if (nalozi.length === 0) {
container.innerHTML = `<div class="p-20 text-center uppercase">Nema zapisa</div>`;
} else {
container.innerHTML = nalozi.map(n => {
return ``;
}).join('');
}
// 3. 🛠️ ATOMSKA ZAMJENA STANJA (Iz Loading u Loaded)
// Kada je HTML uspješno generiran i umetnut u DOM, skrivamo loader dodavanjem klase 'hidden'
if (loader) {
loader.classList.add('hidden');
}
} catch (err) {
console.error("Greška pri renderu:", err);
if (loader) {
// U slučaju greške, loader gasi animaciju i prikazuje poruku o grešci
loader.classList.remove('animate-pulse');
loader.innerHTML = '<span class="text-red-500">Greška pri sinkronizaciji podataka.</span>';
}
}
}
// Pokretanje funkcije
renderirajRadneNaloge();
</script>
```
### Zašto je ovaj pristup s `nalozi-loader` tehnički superiorniji za klijentski render?
1. **Nema praznog prostora:** Korisnik odmah dobiva povratnu informaciju (animirani krug i pulsirajući tekst) da aplikacija komunicira s Proxmox/Django backendom.
2. **JWT Sigurnost:** Budući da se `fetchDashboardData()` izvršava tek unutar ovog klijentskog skripta, funkcija uspješno čita token iz `localStorage.getItem('access_token')`, što u potpunosti rješava grešku `Given token not valid for any token type` s kojom si se susretao na serverskoj strani aplikacije.