Servis: potrazivani delovi, garancija, zakljucavanje statusa

Nova tabela servisni_potrazivani_delovi — visak preko stanja ide u potrazivane, lager NE ide u minus. Merge istog artikla na nalogu. Vizuelni indikator prekoracenja u tabeli (crveno). Status zakljucan na Ceka delove dok ima potrazivanih. Garancija: kalendar sa default vrednoscu. data-refresh podrzava vise selektora. Brisanje dela brise i potrazivane. Inline animation:none za AJAX refresh.
This commit is contained in:
2026-06-22 03:17:29 +02:00
parent c1ca619444
commit d4dcecb3ea
11 changed files with 475 additions and 155 deletions
+53 -15
View File
@@ -47,12 +47,16 @@
{{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;">
<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}}
<option value="{{.}}"{{if eq . $.Nalog.Status}} selected{{end}}>{{.}}</option>
{{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>
@@ -86,12 +90,30 @@
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
</div>
{{end}}
{{if .Nalog.GarancijaDo}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Garancija do</div>
<div style="font-size:14px;color:#16a34a;font-weight:500;">{{.Nalog.GarancijaDo.Format "02.01.2006."}}</div>
{{if index .Dozvole "servis.izmeni"}}
<form method="post" action="/servis/{{.Nalog.ID}}/garancija" data-refresh="#nalog-zaglavlje" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<span style="font-size:14px;{{if .Nalog.GarancijaDo}}color:#16a34a;font-weight:500;{{else}}color:var(--tekst-sporedni);{{end}}">
{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "02.01.2006."}}{{else}}—{{end}}
</span>
<input type="date" name="garancija_do" id="garancija-datum" value="{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "2006-01-02"}}{{end}}"
onchange="var cb=this.form.querySelector('[name=bez_garancije]');if(cb)cb.checked=false;this.form.requestSubmit()"
style="position:absolute;opacity:0;width:0;height:0;pointer-events:none;" tabindex="-1">
<button type="button" onclick="document.getElementById('garancija-datum').showPicker()"
style="background:none;border:0.5px solid var(--ivica);border-radius:6px;cursor:pointer;padding:4px 6px;font-size:14px;line-height:1;" title="Izmeni datum garancije">📅</button>
<label style="font-size:12px;color:var(--tekst-sporedni);cursor:pointer;display:flex;align-items:center;gap:3px;">
<input type="checkbox" name="bez_garancije" value="1" onchange="this.form.requestSubmit()" style="cursor:pointer;"
{{if not .Nalog.GarancijaDo}}checked{{end}}> bez garancije
</label>
</form>
{{else}}
<div style="font-size:14px;{{if .Nalog.GarancijaDo}}color:#16a34a;font-weight:500;{{else}}color:var(--tekst-sporedni);{{end}}">
{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "02.01.2006."}}{{else}}—{{end}}
</div>
{{end}}
</div>
{{end}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Klijent</div>
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">
@@ -245,7 +267,7 @@
<!-- forma za dodavanje dela -->
{{if index .Dozvole "servis.izmeni"}}
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-spisak">
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-spisak, #nalog-zaglavlje">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="margin-bottom:10px;">
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kategorija</label>
@@ -292,14 +314,20 @@
</thead>
<tbody>
{{range .ServisniDelovi}}
<tr style="border-bottom:0.5px solid var(--ivica);">
<tr data-artikal-id="{{.ArtikalID}}" data-kolicina="{{.Kolicina}}" data-potrazivano="{{.Potrazivano}}" style="border-bottom:0.5px solid var(--ivica);{{if .Potrazivano}}background:rgba(220,38,38,0.06);{{end}}">
<td style="padding:9px 10px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td>
<td style="padding:9px 10px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td>
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .CenaKomada}} din</td>
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
<td style="padding:9px 10px;text-align:center;font-size:14px;{{if .Potrazivano}}color:#dc2626;font-weight:600;{{else}}color:var(--tekst-glavni);{{end}}">
{{.Kolicina}}{{if .Potrazivano}}<span style="font-weight:400;"> + {{.Potrazivano}} na čekanju</span>{{end}}
</td>
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">
{{if .Kolicina}}{{dinari .CenaKomada}} din{{else}}—{{end}}
</td>
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
{{if .Kolicina}}{{dinari .Ukupno}} din{{else}}—{{end}}
</td>
<td style="padding:9px 10px;text-align:center;">
{{if index $.Dozvole "servis.izmeni"}}
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak">
{{if and .ID (index $.Dozvole "servis.izmeni")}}
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak, #nalog-zaglavlje">
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
<button type="submit"
data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?"
@@ -367,11 +395,21 @@
var stanjeIzabrano = 0;
if (!artikalSel) return;
// boji „U magacinu" i polje količine crveno kada tražimo više nego što ima na
// stanju (dozvoljeno — backorder; nalog će preći u „Čeka delove" pri čuvanju);
// koristi CSS klasu .prekoraceno umesto inline stilova (main.css)
// boji „U magacinu" i polje količine crveno kada ukupno (postojeće u tabeli
// + nova količina) prelazi stanje; čita direktno iz DOM-a tabele jer se ona
// osvežava AJAX-om, pa JSON varijabla zastareva
function osveziPrekoracenje() {
var preko = (parseInt(kol && kol.value, 10) || 0) > stanjeIzabrano && artikalSel.value;
var novaKol = parseInt(kol && kol.value, 10) || 0;
var postojeciKol = 0;
if (artikalSel.value) {
var aid = artikalSel.value;
var redovi = document.querySelectorAll('#delovi-spisak tr[data-artikal-id="' + aid + '"]');
redovi.forEach(function (tr) {
postojeciKol += (parseInt(tr.getAttribute('data-kolicina'), 10) || 0) +
(parseInt(tr.getAttribute('data-potrazivano'), 10) || 0);
});
}
var preko = (postojeciKol + novaKol) > stanjeIzabrano && artikalSel.value;
if (umagacinu) umagacinu.classList.toggle('prekoraceno', preko);
if (kol) kol.classList.toggle('prekoraceno', preko);
}
+23 -30
View File
@@ -391,36 +391,29 @@
.then(function (rr) { return rr.text(); })
.then(function (html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var novi = doc.querySelector(ciljSel);
var stari = document.querySelector(ciljSel);
if (novi && stari) {
// bez-anim spречava treptaj re-animacije kartica pri zameni
novi.classList.add('bez-anim');
var sc = document.scrollingElement || document.documentElement;
var syDoc = sc ? sc.scrollTop : 0;
var gsStari = document.getElementById('sadrzaj');
var syGs = gsStari ? gsStari.scrollTop : 0;
stari.replaceWith(novi);
void novi.offsetHeight;
novi.classList.remove('bez-anim');
// replaceWith ne pokreće <script> iz fragmenta — re-izvrši ih
// ručno (npr. da se combobox/pretrage ponovo povežu)
novi.querySelectorAll('script').forEach(function (old) {
var s = document.createElement('script');
if (old.src) s.src = old.src; else s.textContent = old.textContent;
old.parentNode.replaceChild(s, old);
});
if (sc) sc.scrollTop = syDoc;
var gsNovi = document.getElementById('sadrzaj');
if (gsNovi) gsNovi.scrollTop = syGs;
// re-veži event handlere (AJAX submit, potvrda brisanja...)
// na sveže umetnute elemente — inače bi nove forme išle
// punim POST-om (skok na vrh), a × bez potvrde
ntechInicijalizuj();
}
// forma koja nije deo osveženog dela ostaje u DOM-u —
// isprazni je za sledeći unos (za one koje osvežavaju sebe
// ovo je bezuspešno jer su zamenjene, što je u redu)
// podržava više selektora razdvojenih zarezom (npr. "#a, #b")
var sc = document.scrollingElement || document.documentElement;
var syDoc = sc ? sc.scrollTop : 0;
var gsStari = document.getElementById('sadrzaj');
var syGs = gsStari ? gsStari.scrollTop : 0;
ciljSel.split(',').forEach(function (sel) {
sel = sel.trim();
var novi = doc.querySelector(sel);
var stari = document.querySelector(sel);
if (novi && stari) {
novi.style.animation = 'none';
stari.replaceWith(novi);
novi.querySelectorAll('script').forEach(function (old) {
var s = document.createElement('script');
if (old.src) s.src = old.src; else s.textContent = old.textContent;
old.parentNode.replaceChild(s, old);
});
}
});
if (sc) sc.scrollTop = syDoc;
var gsNovi = document.getElementById('sadrzaj');
if (gsNovi) gsNovi.scrollTop = syGs;
ntechInicijalizuj();
if (document.body.contains(f)) f.reset();
if (btn) btn.disabled = false;
})