Servis: auto-snimanje forme izmene + hx-boost navigacija (bez bljeska)

- Forma Izmeni nalog snima se automatski (change na celoj formi, X-Autosave→204), bez dugmeta Sačuvaj
- Ispravka: trazene_nadogradnje je NOT NULL — slati prazan string umesto NULL
- Iz kartice detalja uklonjeni dropdown statusa i datum završetka
- Forma izmene više ne ubacuje težak JSON svih klijenata (nepotreban u izmeni)
- Navigacioni linkovi (Kategorije, Izmeni, Nazad) dobili hx-boost — swap #glavni-sadrzaj bez punog reloda, pozadina ne trepće
This commit is contained in:
2026-06-23 01:08:14 +02:00
parent 3e5d27f0a2
commit bdce399592
6 changed files with 96 additions and 37 deletions
+1 -1
View File
@@ -27,7 +27,7 @@
{{end}}
<!-- nazad dugme -->
<a href="/magacin" class="nazad-link">
<a href="/magacin" class="nazad-link" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
Nazad na magacin
</a>
+1 -1
View File
@@ -30,7 +30,7 @@
<a href="/magacin/novi?tip={{.Tip}}" class="btn-primarno">+ {{if eq .Tip "usluga"}}Nova usluga{{else if eq .Tip "trosak"}}Novi trošak{{else}}Novi artikal{{end}}</a>
{{end}}
{{if and (eq .Tip "proizvod") (index .Dozvole "kategorija.pregled")}}
<a href="/magacin/kategorije" class="btn-primarno">Kategorije</a>
<a href="/magacin/kategorije" class="btn-primarno" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">Kategorije</a>
{{end}}
</div>
+2 -24
View File
@@ -30,7 +30,7 @@
{{end}}
<!-- nazad dugme -->
<a href="/servis" class="nazad-link">
<a href="/servis" class="nazad-link" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
Nazad na servis
</a>
@@ -44,22 +44,6 @@
</span>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
{{template "status-badge-detalji" .Nalog.Status}}
<form method="post" action="/servis/{{.Nalog.ID}}/status" style="display:flex;align-items:center;gap:6px;" data-refresh="#nalog-zaglavlje">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<select name="status" style="font-size:13px;padding:4px 8px;border-radius:6px;border:0.5px solid var(--ivica);background:var(--kartica-pozadina);color:var(--tekst-glavni);cursor:pointer;"{{if .ZakljucanStatus}} disabled title="Status zaključan dok ima delova koji nedostaju"{{end}}>
{{range .SviStatusi}}
{{/* „Čeka delove" je automatski status — ne nudi se ručno, osim ako je nalog već u njemu */}}
{{if or (ne . "Čeka delove") (eq . $.Nalog.Status)}}
<option value="{{.}}"{{if eq . $.Nalog.Status}} selected{{end}}>{{.}}</option>
{{end}}
{{end}}
</select>
{{if .ZakljucanStatus}}
<span style="font-size:12px;color:#dc2626;">🔒 Status zaključan — obrišite delove koji nedostaju</span>
{{else}}
<button type="submit" class="btn-sekundarno" style="font-size:13px;padding:4px 12px;">Promeni</button>
{{end}}
</form>
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
@@ -76,7 +60,7 @@
Otpremnica
</a>
{{end}}
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno">
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
Izmeni nalog
</a>
</div>
@@ -92,12 +76,6 @@
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.PredvidjenDatum.Format "02.01.2006."}}</div>
</div>
{{end}}
{{if .Nalog.DatumZavrsetka}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Datum završetka</div>
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
</div>
{{end}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Garancija do</div>
<div style="font-size:14px;{{if .Nalog.GarancijaDo}}color:#16a34a;font-weight:500;{{else}}color:var(--tekst-sporedni);{{end}}">
+70 -9
View File
@@ -5,14 +5,18 @@
{{define "sadrzaj"}}
<div style="width:100%;">
<!-- podaci za Alpine: lista klijenata i eventualno već vezani klijent (kod izmene) -->
{{if not .Izmena}}
<!-- podaci za Alpine pretragu klijenta — potrebni samo pri novom nalogu;
u izmeni se blok klijenta ne prikazuje, pa ne ubacujemo težak inline JSON svih klijenata
(blokirao bi render forme i pravio bljesak pozadine pri otvaranju) -->
<script>
window.__klijentiNaloga = {{json .Klijenti}};
window.__izabraniKlijentId = {{.Nalog.KlijentIDVrednost}};
</script>
{{end}}
<!-- nazad dugme -->
<a href="{{if .Izmena}}/servis/{{.Nalog.ID}}{{else}}/servis{{end}}" class="nazad-link">
<a href="{{if .Izmena}}/servis/{{.Nalog.ID}}{{else}}/servis{{end}}" class="nazad-link" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
{{if .Izmena}}Nazad na nalog{{else}}Nazad na servis{{end}}
</a>
@@ -21,7 +25,7 @@
<div class="poruka-greska greska-animacija" style="margin-bottom:16px;">{{.Greska}}</div>
{{end}}
<form method="POST" action="{{if .Izmena}}/servis/izmeni/{{.Nalog.ID}}{{else}}/servis/novi{{end}}">
<form id="forma-nalog" method="POST" action="{{if .Izmena}}/servis/izmeni/{{.Nalog.ID}}{{else}}/servis/novi{{end}}"{{if .Izmena}} data-autosave="1"{{end}}>
<!-- broj naloga ide kao hidden — server ga je generisao, vraćamo ga pri POST-u -->
<input type="hidden" name="broj_naloga" value="{{.Nalog.BrojNaloga}}">
@@ -304,18 +308,75 @@
style="width:100%;resize:vertical;">{{.Nalog.Napomena}}</textarea>
</div>
<!-- dugmad -->
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:18px;">
<a href="{{if .Izmena}}/servis/{{.Nalog.ID}}{{else}}/servis{{end}}" class="btn-sekundarno">Odustani</a>
<button type="submit" class="btn-primarno">
{{if .Izmena}}Sačuvaj izmene{{else}}Sačuvaj nalog{{end}}
</button>
{{if .Izmena}}
<!-- izmena se snima automatski u realnom vremenu; indikator stanja umesto dugmeta -->
<div style="display:flex;justify-content:flex-end;align-items:center;min-height:24px;margin-top:18px;">
<span id="autosave-status" style="font-size:13px;color:var(--tekst-sporedni);transition:opacity 0.3s;opacity:0;"></span>
</div>
{{else}}
<!-- dugmad (samo za novi nalog) -->
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:18px;">
<a href="/servis" class="btn-sekundarno">Odustani</a>
<button type="submit" class="btn-primarno">Sačuvaj nalog</button>
</div>
{{end}}
</div>
</form>
</div>
<script>
// automatsko snimanje forme „Izmeni nalog" — okida se na `change` bilo kog polja.
// `change` se za tekst/textarea/number okine na blur (samo ako je vrednost promenjena),
// a za select/date/checkbox na izbor — jedan slušalac pokriva sve tipove.
(function () {
var forma = document.getElementById('forma-nalog');
if (!forma || forma.dataset.autosave !== '1') return;
var status = document.getElementById('autosave-status');
var slanjeUToku = false;
var ponoviPoZavrsetku = false;
function prikaziStatus(tekst, boja) {
if (!status) return;
status.textContent = tekst;
status.style.color = boja;
status.style.opacity = '1';
}
function sakrijStatus() {
if (status) status.style.opacity = '0';
}
function snimi() {
// ako je slanje već u toku, zapamti da treba ponovo snimiti kad se završi
if (slanjeUToku) { ponoviPoZavrsetku = true; return; }
slanjeUToku = true;
prikaziStatus('Čuvam…', 'var(--tekst-sporedni)');
var podaci = new URLSearchParams(new FormData(forma));
fetch(forma.action, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Autosave': '1' },
body: podaci,
}).then(function (odgovor) {
if (odgovor.ok) {
prikaziStatus('Sačuvano ✓', '#16a34a');
setTimeout(sakrijStatus, 2000);
} else {
return odgovor.text().then(function (poruka) {
prikaziStatus(poruka.trim() || 'Greška pri čuvanju', '#dc2626');
});
}
}).catch(function () {
prikaziStatus('Greška pri čuvanju', '#dc2626');
}).finally(function () {
slanjeUToku = false;
if (ponoviPoZavrsetku) { ponoviPoZavrsetku = false; snimi(); }
});
}
forma.addEventListener('change', snimi);
})();
</script>
<style>
.tip-opcija {
display: flex; align-items: center; gap: 8px;