Merge agent2-putni: Putni nalozi CRUD + status workflow
This commit is contained in:
+248
-7
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Fajl: routers/erp_full_router.py | v1.1.0 | 05.05.2026
|
||||
# Fajl: routers/erp_full_router.py | v1.2.0 | 05.05.2026
|
||||
# Autor: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
|
||||
# Lokacija: /opt/pgz-sport/routers/erp_full_router.py
|
||||
# Svrha: FULL ERP (SAP-Lite) — kontni plan, dnevnik, glavna knjiga,
|
||||
@@ -8,6 +8,10 @@
|
||||
# PDV, plaće, izvještaji (Bilanca/PnL/Cashflow), PDF/XLSX export,
|
||||
# invoice_uploads (OCR), expense_reports (Putni nalozi), payments.
|
||||
# v1.1.0 (2026-05-05): + POST /invoice-uploads multipart upload (Agent E).
|
||||
# v1.2.0 (2026-05-05): + Full CRUD za Putni nalozi (/putni-nalozi + alias
|
||||
# /expense-reports): GET/POST/PATCH/DELETE + status workflow
|
||||
# (draft → poslano → odobreno/odbijeno → isplaceno) + auto cost_total
|
||||
# + approved_at/paid_at na prijelazima (Agent 2).
|
||||
# Mount: /api/v2/erp/*
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
from __future__ import annotations
|
||||
@@ -1223,12 +1227,7 @@ async def invoice_uploads_create(
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# 12) PUTNI NALOZI / EXPENSE REPORTS
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
@router.get("/expense-reports")
|
||||
def expense_reports_list(klub_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
report_type: Optional[str] = None,
|
||||
godina: Optional[int] = None,
|
||||
limit: int = 200):
|
||||
def _expense_reports_list_impl(klub_id, status, report_type, godina, limit):
|
||||
where = ["1=1"]
|
||||
params: list = []
|
||||
if klub_id:
|
||||
@@ -1252,6 +1251,248 @@ def expense_reports_list(klub_id: Optional[int] = None,
|
||||
return {"count": len(rows), "rows": rows}
|
||||
|
||||
|
||||
@router.get("/expense-reports")
|
||||
def expense_reports_list(klub_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
report_type: Optional[str] = None,
|
||||
godina: Optional[int] = None,
|
||||
limit: int = 200):
|
||||
return _expense_reports_list_impl(klub_id, status, report_type, godina, limit)
|
||||
|
||||
|
||||
@router.get("/putni-nalozi")
|
||||
def putni_nalozi_list(klub_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
report_type: Optional[str] = None,
|
||||
godina: Optional[int] = None,
|
||||
limit: int = 200):
|
||||
return _expense_reports_list_impl(klub_id, status, report_type, godina, limit)
|
||||
|
||||
|
||||
# ── Putni nalog single + CRUD ──────────────────────────────────────
|
||||
class PutniNalogIn(BaseModel):
|
||||
klub_id: int
|
||||
user_id: Optional[int] = None
|
||||
clan_id: Optional[int] = None
|
||||
report_type: str = "sluzbeno_putovanje"
|
||||
report_no: Optional[str] = None
|
||||
destination: str
|
||||
purpose: str
|
||||
date_from: date
|
||||
date_to: date
|
||||
vehicle_type: Optional[str] = None
|
||||
vehicle_plate: Optional[str] = None
|
||||
km_driven: float = 0
|
||||
km_rate: float = 0.42
|
||||
cost_transport: float = 0
|
||||
cost_lodging: float = 0
|
||||
cost_meals: float = 0
|
||||
cost_other: float = 0
|
||||
dnevnice_count: float = 0
|
||||
dnevnice_amount: float = 30.00
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class PutniNalogPatch(BaseModel):
|
||||
klub_id: Optional[int] = None
|
||||
user_id: Optional[int] = None
|
||||
clan_id: Optional[int] = None
|
||||
report_type: Optional[str] = None
|
||||
report_no: Optional[str] = None
|
||||
destination: Optional[str] = None
|
||||
purpose: Optional[str] = None
|
||||
date_from: Optional[date] = None
|
||||
date_to: Optional[date] = None
|
||||
vehicle_type: Optional[str] = None
|
||||
vehicle_plate: Optional[str] = None
|
||||
km_driven: Optional[float] = None
|
||||
km_rate: Optional[float] = None
|
||||
cost_transport: Optional[float] = None
|
||||
cost_lodging: Optional[float] = None
|
||||
cost_meals: Optional[float] = None
|
||||
cost_other: Optional[float] = None
|
||||
dnevnice_count: Optional[float] = None
|
||||
dnevnice_amount: Optional[float] = None
|
||||
notes: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# Allowed status transitions
|
||||
_PN_TRANSITIONS = {
|
||||
"draft": {"poslano", "odbijeno"},
|
||||
"poslano": {"odobreno", "odbijeno"},
|
||||
"odobreno": {"isplaceno"},
|
||||
"isplaceno": set(),
|
||||
"odbijeno": set(),
|
||||
}
|
||||
|
||||
|
||||
def _pn_calc_total(km_driven, km_rate, c_tr, c_lo, c_me, c_ot, dn_c, dn_a):
|
||||
return (
|
||||
_f(km_driven) * _f(km_rate)
|
||||
+ _f(c_tr) + _f(c_lo) + _f(c_me) + _f(c_ot)
|
||||
+ _f(dn_c) * _f(dn_a)
|
||||
)
|
||||
|
||||
|
||||
def _pn_get_one(pid: int):
|
||||
head = db_one(
|
||||
"SELECT er.*, k.naziv AS klub_naziv "
|
||||
"FROM pgz_sport.expense_reports er "
|
||||
"LEFT JOIN pgz_sport.klubovi k ON k.id=er.klub_id "
|
||||
"WHERE er.id=%s", (pid,))
|
||||
if not head:
|
||||
raise HTTPException(404, f"Putni nalog #{pid} ne postoji")
|
||||
racuni = db_query(
|
||||
"SELECT pnr.id, pnr.invoice_id, pnr.kategorija, pnr.napomena, pnr.attached_at, "
|
||||
"i.invoice_no, i.vendor_name, i.amount_gross, i.currency "
|
||||
"FROM pgz_sport.putni_nalog_racuni pnr "
|
||||
"LEFT JOIN pgz_sport.invoices i ON i.id=pnr.invoice_id "
|
||||
"WHERE pnr.putni_nalog_id=%s ORDER BY pnr.id DESC", (pid,))
|
||||
return {"head": head, "racuni": racuni}
|
||||
|
||||
|
||||
def _pn_create(body: PutniNalogIn):
|
||||
cost_total = _pn_calc_total(
|
||||
body.km_driven, body.km_rate,
|
||||
body.cost_transport, body.cost_lodging, body.cost_meals, body.cost_other,
|
||||
body.dnevnice_count, body.dnevnice_amount)
|
||||
rid = db_exec(
|
||||
"INSERT INTO pgz_sport.expense_reports "
|
||||
"(klub_id, user_id, clan_id, report_type, report_no, destination, purpose, "
|
||||
" date_from, date_to, vehicle_type, vehicle_plate, km_driven, km_rate, "
|
||||
" cost_transport, cost_lodging, cost_meals, cost_other, cost_total, "
|
||||
" dnevnice_count, dnevnice_amount, status, notes, created_at, updated_at) "
|
||||
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'draft',%s, now(), now()) "
|
||||
"RETURNING id",
|
||||
(body.klub_id, body.user_id, body.clan_id, body.report_type, body.report_no,
|
||||
body.destination, body.purpose, body.date_from, body.date_to,
|
||||
body.vehicle_type, body.vehicle_plate,
|
||||
_f(body.km_driven), _f(body.km_rate),
|
||||
_f(body.cost_transport), _f(body.cost_lodging),
|
||||
_f(body.cost_meals), _f(body.cost_other), cost_total,
|
||||
_f(body.dnevnice_count), _f(body.dnevnice_amount),
|
||||
body.notes),
|
||||
returning=True)
|
||||
row = db_one("SELECT * FROM pgz_sport.expense_reports WHERE id=%s", (rid,))
|
||||
return {"ok": True, "id": rid, "row": row}
|
||||
|
||||
|
||||
def _pn_patch(pid: int, body: PutniNalogPatch):
|
||||
current = db_one("SELECT * FROM pgz_sport.expense_reports WHERE id=%s", (pid,))
|
||||
if not current:
|
||||
raise HTTPException(404, f"Putni nalog #{pid} ne postoji")
|
||||
|
||||
data = body.dict(exclude_unset=True)
|
||||
|
||||
# Status workflow validation
|
||||
new_status = data.get("status")
|
||||
if new_status is not None and new_status != current["status"]:
|
||||
allowed = _PN_TRANSITIONS.get(current["status"], set())
|
||||
if new_status not in allowed:
|
||||
raise HTTPException(
|
||||
400,
|
||||
f"Nedozvoljen prijelaz statusa: {current['status']} → {new_status}. "
|
||||
f"Dozvoljeni: {sorted(allowed) or '(nijedan)'}")
|
||||
|
||||
sets = []
|
||||
params: list = []
|
||||
cost_fields = {"km_driven", "km_rate", "cost_transport", "cost_lodging",
|
||||
"cost_meals", "cost_other", "dnevnice_count", "dnevnice_amount"}
|
||||
cost_changed = bool(cost_fields.intersection(data.keys()))
|
||||
|
||||
for col in ("klub_id", "user_id", "clan_id", "report_type", "report_no",
|
||||
"destination", "purpose", "date_from", "date_to",
|
||||
"vehicle_type", "vehicle_plate", "km_driven", "km_rate",
|
||||
"cost_transport", "cost_lodging", "cost_meals", "cost_other",
|
||||
"dnevnice_count", "dnevnice_amount", "notes", "status"):
|
||||
if col in data:
|
||||
sets.append(f"{col}=%s")
|
||||
params.append(data[col])
|
||||
|
||||
if cost_changed:
|
||||
# Recompute using merged values
|
||||
merged = dict(current)
|
||||
merged.update(data)
|
||||
new_total = _pn_calc_total(
|
||||
merged["km_driven"], merged["km_rate"],
|
||||
merged["cost_transport"], merged["cost_lodging"],
|
||||
merged["cost_meals"], merged["cost_other"],
|
||||
merged["dnevnice_count"], merged["dnevnice_amount"])
|
||||
sets.append("cost_total=%s")
|
||||
params.append(new_total)
|
||||
|
||||
if new_status == "odobreno":
|
||||
sets.append("approved_at=now()")
|
||||
if new_status == "isplaceno":
|
||||
sets.append("paid_at=now()")
|
||||
|
||||
sets.append("updated_at=now()")
|
||||
|
||||
if not sets:
|
||||
return {"ok": True, "no_changes": True, "row": current}
|
||||
|
||||
params.append(pid)
|
||||
db_exec(f"UPDATE pgz_sport.expense_reports SET {', '.join(sets)} WHERE id=%s",
|
||||
tuple(params))
|
||||
row = db_one("SELECT * FROM pgz_sport.expense_reports WHERE id=%s", (pid,))
|
||||
return {"ok": True, "id": pid, "row": row}
|
||||
|
||||
|
||||
def _pn_delete(pid: int):
|
||||
cur = db_one("SELECT status FROM pgz_sport.expense_reports WHERE id=%s", (pid,))
|
||||
if not cur:
|
||||
raise HTTPException(404, f"Putni nalog #{pid} ne postoji")
|
||||
if cur["status"] != "draft":
|
||||
raise HTTPException(
|
||||
400,
|
||||
f"Brisanje dopušteno samo za status='draft' (trenutni: {cur['status']})")
|
||||
db_exec("DELETE FROM pgz_sport.putni_nalog_racuni WHERE putni_nalog_id=%s", (pid,))
|
||||
db_exec("DELETE FROM pgz_sport.expense_reports WHERE id=%s", (pid,))
|
||||
return {"ok": True, "deleted": pid}
|
||||
|
||||
|
||||
# ── Routes (both /putni-nalozi and /expense-reports prefixes) ─────
|
||||
@router.get("/putni-nalozi/{pid}")
|
||||
def putni_nalog_get(pid: int):
|
||||
return _pn_get_one(pid)
|
||||
|
||||
|
||||
@router.get("/expense-reports/{pid}")
|
||||
def expense_report_get(pid: int):
|
||||
return _pn_get_one(pid)
|
||||
|
||||
|
||||
@router.post("/putni-nalozi")
|
||||
def putni_nalog_create(body: PutniNalogIn):
|
||||
return _pn_create(body)
|
||||
|
||||
|
||||
@router.post("/expense-reports")
|
||||
def expense_report_create(body: PutniNalogIn):
|
||||
return _pn_create(body)
|
||||
|
||||
|
||||
@router.patch("/putni-nalozi/{pid}")
|
||||
def putni_nalog_patch(pid: int, body: PutniNalogPatch):
|
||||
return _pn_patch(pid, body)
|
||||
|
||||
|
||||
@router.patch("/expense-reports/{pid}")
|
||||
def expense_report_patch(pid: int, body: PutniNalogPatch):
|
||||
return _pn_patch(pid, body)
|
||||
|
||||
|
||||
@router.delete("/putni-nalozi/{pid}")
|
||||
def putni_nalog_delete(pid: int):
|
||||
return _pn_delete(pid)
|
||||
|
||||
|
||||
@router.delete("/expense-reports/{pid}")
|
||||
def expense_report_delete(pid: int):
|
||||
return _pn_delete(pid)
|
||||
|
||||
|
||||
@router.get("/putni-nalog-racuni")
|
||||
def putni_nalog_racuni_list(putni_nalog_id: Optional[int] = None,
|
||||
invoice_id: Optional[int] = None,
|
||||
|
||||
+195
-15
@@ -90,6 +90,11 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
.badge.knjizen{background:var(--green);color:var(--bg0)}
|
||||
.badge.placen{background:var(--pgz-gold);color:var(--bg0)}
|
||||
.badge.otkazan{background:var(--red);color:#fff}
|
||||
.badge.draft{background:var(--bg4);color:var(--t1)}
|
||||
.badge.poslano{background:var(--cyan);color:var(--bg0)}
|
||||
.badge.odobreno{background:var(--green);color:var(--bg0)}
|
||||
.badge.odbijeno{background:var(--red);color:#fff}
|
||||
.badge.isplaceno{background:var(--pgz-gold);color:var(--bg0)}
|
||||
|
||||
.dnev-line-row{display:grid;grid-template-columns:140px 1fr 100px 100px 1fr 30px;gap:6px;margin-bottom:6px;align-items:center}
|
||||
.dnev-line-row input,.dnev-line-row select{background:var(--bg2);border:1px solid var(--rim);border-radius:4px;padding:5px 8px;color:var(--t1);font-size:12px;width:100%}
|
||||
@@ -297,16 +302,20 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
<!-- ============ PUTNI NALOZI / EXPENSE REPORTS ============ -->
|
||||
<section class="panel" id="panel-putni">
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">Putni nalozi i ostali troškovi (expense_reports)</div></div>
|
||||
<div class="card-h">
|
||||
<div class="card-t">Putni nalozi (expense_reports)</div>
|
||||
<button class="btn gold" onclick="openPutniModal()">+ Novi putni nalog</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<label>Tip <select id="pn-type"><option value="">— svi —</option><option value="putni_nalog">Putni nalog</option><option value="expense">Trošak</option></select></label>
|
||||
<label>Status <select id="pn-status"><option value="">— svi —</option><option value="draft">draft</option><option value="podnesen">podnesen</option><option value="odobren">odobren</option><option value="isplacen">isplacen</option><option value="rejected">rejected</option></select></label>
|
||||
<label>Godina <input type="number" id="pn-godina" placeholder="2026" style="width:90px"></label>
|
||||
<label>Status <select id="pn-status"><option value="">— svi —</option><option value="draft">draft</option><option value="poslano">poslano</option><option value="odobreno">odobreno</option><option value="odbijeno">odbijeno</option><option value="isplaceno">isplaceno</option></select></label>
|
||||
<label>Klub ID <input type="number" id="pn-klub" placeholder="ID kluba" style="width:90px"></label>
|
||||
<label>Tip <select id="pn-type"><option value="">— svi —</option><option value="sluzbeno_putovanje">Službeno putovanje</option><option value="putni_nalog">Putni nalog</option><option value="expense">Trošak</option></select></label>
|
||||
<button class="btn" onclick="loadExpenseReports()">Osvježi</button>
|
||||
<button id="pn-export-btn" class="export-btn" type="button">Export ▾</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="pn-tbl"><thead><tr><th>#</th><th>Tip</th><th>Klub</th><th>Odredište</th><th>Svrha</th><th>Od</th><th>Do</th><th class="num">Km</th><th class="num">Trošak</th><th class="num">Dnevnice</th><th>Status</th></tr></thead><tbody><tr><td colspan="11" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
|
||||
<table id="pn-tbl"><thead><tr><th>#</th><th>Br</th><th>Klub</th><th>Destinacija</th><th>Od</th><th>Do</th><th class="num">Km</th><th class="num">Cost total</th><th>Status</th><th>Akcije</th></tr></thead><tbody><tr><td colspan="10" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
|
||||
</div>
|
||||
<div id="pn-detail" style="display:none;margin-top:14px;border-top:1px solid var(--bd);padding-top:12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
@@ -526,6 +535,39 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="m-pn" onclick="if(event.target===this)closeModal('m-pn')">
|
||||
<div class="modal" style="width:min(820px,96vw)">
|
||||
<h3 id="m-pn-title">Novi putni nalog</h3>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<div class="form-row"><label>Klub ID *</label><input type="number" id="pn-f-klub"></div>
|
||||
<div class="form-row"><label>User ID</label><input type="number" id="pn-f-user"></div>
|
||||
<div class="form-row"><label>Član ID</label><input type="number" id="pn-f-clan"></div>
|
||||
<div class="form-row"><label>Tip</label><select id="pn-f-type"><option value="sluzbeno_putovanje">Službeno putovanje</option><option value="putni_nalog">Putni nalog</option><option value="expense">Trošak</option></select></div>
|
||||
<div class="form-row"><label>Br. naloga</label><input id="pn-f-no" placeholder="auto / opc"></div>
|
||||
<div class="form-row"><label>Destinacija *</label><input id="pn-f-dest"></div>
|
||||
<div class="form-row" style="grid-column:1/3"><label>Svrha *</label><input id="pn-f-purpose"></div>
|
||||
<div class="form-row"><label>Datum od *</label><input type="date" id="pn-f-from"></div>
|
||||
<div class="form-row"><label>Datum do *</label><input type="date" id="pn-f-to"></div>
|
||||
<div class="form-row"><label>Vozilo (tip)</label><input id="pn-f-vtype" placeholder="osobno / službeno"></div>
|
||||
<div class="form-row"><label>Reg. oznaka</label><input id="pn-f-vplate"></div>
|
||||
<div class="form-row"><label>Km voženo</label><input type="number" step="0.1" id="pn-f-km" value="0" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row"><label>€ / km</label><input type="number" step="0.01" id="pn-f-kmrate" value="0.42" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row"><label>Trošak prijevoz</label><input type="number" step="0.01" id="pn-f-tr" value="0" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row"><label>Trošak smještaj</label><input type="number" step="0.01" id="pn-f-lo" value="0" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row"><label>Trošak hrana</label><input type="number" step="0.01" id="pn-f-me" value="0" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row"><label>Ostali troškovi</label><input type="number" step="0.01" id="pn-f-ot" value="0" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row"><label>Br. dnevnica</label><input type="number" step="0.5" id="pn-f-dnc" value="0" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row"><label>Iznos dnevnice</label><input type="number" step="0.01" id="pn-f-dna" value="30.00" oninput="recalcPutniTotal()"></div>
|
||||
<div class="form-row" style="grid-column:1/3"><label>Napomena</label><textarea id="pn-f-notes" rows="2"></textarea></div>
|
||||
</div>
|
||||
<div class="dnev-balans ok" id="pn-f-total" style="margin-top:10px">Ukupno: 0,00 €</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn sec" onclick="closeModal('m-pn')">Odustani</button>
|
||||
<button class="btn gold" onclick="savePutni()">Spremi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="m-pl" onclick="if(event.target===this)closeModal('m-pl')">
|
||||
<div class="modal">
|
||||
<h3>Obračun plaće (HR 2026)</h3>
|
||||
@@ -1106,35 +1148,56 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// ===== PUTNI NALOZI / EXPENSE REPORTS =====
|
||||
function _pnStatusActions(r){
|
||||
const id = r.id;
|
||||
const s = r.status || 'draft';
|
||||
const acts = [];
|
||||
if (s === 'draft') {
|
||||
acts.push(`<button class="btn green" title="Pošalji na odobrenje" onclick="event.stopPropagation();putniSetStatus(${id},'poslano')">Pošalji</button>`);
|
||||
acts.push(`<button class="btn red" title="Odbij" onclick="event.stopPropagation();putniSetStatus(${id},'odbijeno')">Odbij</button>`);
|
||||
} else if (s === 'poslano') {
|
||||
acts.push(`<button class="btn green" title="Odobri" onclick="event.stopPropagation();putniSetStatus(${id},'odobreno')">Odobri</button>`);
|
||||
acts.push(`<button class="btn red" title="Odbij" onclick="event.stopPropagation();putniSetStatus(${id},'odbijeno')">Odbij</button>`);
|
||||
} else if (s === 'odobreno') {
|
||||
acts.push(`<button class="btn gold" title="Označi kao isplaćeno" onclick="event.stopPropagation();putniSetStatus(${id},'isplaceno')">Isplati</button>`);
|
||||
}
|
||||
acts.push(`<button class="btn sec" title="Uredi" onclick="event.stopPropagation();editPutni(${id})">✎</button>`);
|
||||
if (s === 'draft') {
|
||||
acts.push(`<button class="btn red" title="Obriši" onclick="event.stopPropagation();deletePutni(${id})">×</button>`);
|
||||
}
|
||||
return acts.join(' ');
|
||||
}
|
||||
|
||||
async function loadExpenseReports(){
|
||||
const tbody = document.querySelector('#pn-tbl tbody');
|
||||
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="10" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
|
||||
try {
|
||||
const t = document.getElementById('pn-type').value;
|
||||
const s = document.getElementById('pn-status').value;
|
||||
const g = document.getElementById('pn-godina').value;
|
||||
const k = document.getElementById('pn-klub').value;
|
||||
const p = new URLSearchParams();
|
||||
if(t) p.set('report_type', t);
|
||||
if(s) p.set('status', s);
|
||||
if(g) p.set('godina', g);
|
||||
const d = await api('/expense-reports?'+p.toString());
|
||||
if(k) p.set('klub_id', k);
|
||||
const d = await api('/putni-nalozi?'+p.toString());
|
||||
tbody.innerHTML = (d.rows||[]).length
|
||||
? d.rows.map(r=>`<tr onclick="expenseDetail(${r.id})" style="cursor:pointer">
|
||||
<td>${r.id}</td>
|
||||
<td>${r.report_type||''}</td>
|
||||
<td>${r.report_no||'—'}</td>
|
||||
<td>${r.klub_naziv||r.klub_id||''}</td>
|
||||
<td>${r.destination||''}</td>
|
||||
<td>${r.purpose||''}</td>
|
||||
<td>${r.date_from||''}</td>
|
||||
<td>${r.date_to||''}</td>
|
||||
<td class="num">${fmt(r.km_driven)}</td>
|
||||
<td class="num">${fmt(r.cost_total)}</td>
|
||||
<td class="num">${fmt(r.dnevnice_amount)}</td>
|
||||
<td class="num"><b>${fmt(r.cost_total)}</b></td>
|
||||
<td><span class="badge ${r.status||''}">${r.status||''}</span></td>
|
||||
<td style="white-space:nowrap">${_pnStatusActions(r)}</td>
|
||||
</tr>`).join('')
|
||||
: `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Nema putnih naloga.</td></tr>`;
|
||||
: `<tr><td colspan="10" style="color:var(--t2);text-align:center;padding:14px">Nema putnih naloga.</td></tr>`;
|
||||
} catch(e) {
|
||||
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="10" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1142,14 +1205,131 @@ async function expenseDetail(id){
|
||||
try {
|
||||
document.getElementById('pn-detail').style.display='block';
|
||||
document.getElementById('pn-detail-title').textContent = `Vezani računi za putni nalog #${id}`;
|
||||
const d = await api('/putni-nalog-racuni?putni_nalog_id='+id);
|
||||
const d = await api('/putni-nalozi/'+id);
|
||||
const tb = document.querySelector('#pn-rac-tbl tbody');
|
||||
tb.innerHTML = (d.rows||[]).length
|
||||
? d.rows.map(r=>`<tr><td>${r.id}</td><td>${r.invoice_no||('#'+r.invoice_id)}</td><td>${r.vendor_name||''}</td><td class="num">${fmt(r.amount_gross)}</td><td>${r.currency||''}</td><td>${r.kategorija||''}</td><td>${(r.attached_at||'').slice(0,10)}</td></tr>`).join('')
|
||||
const racuni = d.racuni || [];
|
||||
tb.innerHTML = racuni.length
|
||||
? racuni.map(r=>`<tr><td>${r.id}</td><td>${r.invoice_no||('#'+r.invoice_id)}</td><td>${r.vendor_name||''}</td><td class="num">${fmt(r.amount_gross)}</td><td>${r.currency||''}</td><td>${r.kategorija||''}</td><td>${(r.attached_at||'').slice(0,10)}</td></tr>`).join('')
|
||||
: `<tr><td colspan="7" style="color:var(--t2);text-align:center;padding:10px">Nema vezanih računa.</td></tr>`;
|
||||
} catch(e) { alert('Greška: '+e.message); }
|
||||
}
|
||||
|
||||
function _pnSetForm(r){
|
||||
const set = (id,v) => { const el = document.getElementById(id); if(el) el.value = (v==null?'':v); };
|
||||
set('pn-f-klub', r.klub_id);
|
||||
set('pn-f-user', r.user_id);
|
||||
set('pn-f-clan', r.clan_id);
|
||||
set('pn-f-type', r.report_type || 'sluzbeno_putovanje');
|
||||
set('pn-f-no', r.report_no);
|
||||
set('pn-f-dest', r.destination);
|
||||
set('pn-f-purpose', r.purpose);
|
||||
set('pn-f-from', (r.date_from||'').toString().slice(0,10));
|
||||
set('pn-f-to', (r.date_to||'').toString().slice(0,10));
|
||||
set('pn-f-vtype', r.vehicle_type);
|
||||
set('pn-f-vplate', r.vehicle_plate);
|
||||
set('pn-f-km', r.km_driven||0);
|
||||
set('pn-f-kmrate', r.km_rate||0.42);
|
||||
set('pn-f-tr', r.cost_transport||0);
|
||||
set('pn-f-lo', r.cost_lodging||0);
|
||||
set('pn-f-me', r.cost_meals||0);
|
||||
set('pn-f-ot', r.cost_other||0);
|
||||
set('pn-f-dnc', r.dnevnice_count||0);
|
||||
set('pn-f-dna', r.dnevnice_amount||30.00);
|
||||
set('pn-f-notes', r.notes);
|
||||
recalcPutniTotal();
|
||||
}
|
||||
|
||||
function recalcPutniTotal(){
|
||||
const v = id => parseFloat(document.getElementById(id).value)||0;
|
||||
const total = v('pn-f-km')*v('pn-f-kmrate') + v('pn-f-tr') + v('pn-f-lo')
|
||||
+ v('pn-f-me') + v('pn-f-ot') + v('pn-f-dnc')*v('pn-f-dna');
|
||||
document.getElementById('pn-f-total').textContent = `Ukupno: ${fmt(total)} €`;
|
||||
}
|
||||
|
||||
function openPutniModal(){
|
||||
document.getElementById('m-pn-title').textContent = 'Novi putni nalog';
|
||||
document.getElementById('m-pn').dataset.id = '';
|
||||
_pnSetForm({
|
||||
report_type: 'sluzbeno_putovanje',
|
||||
date_from: new Date().toISOString().slice(0,10),
|
||||
date_to: new Date().toISOString().slice(0,10),
|
||||
km_driven: 0, km_rate: 0.42,
|
||||
cost_transport: 0, cost_lodging: 0, cost_meals: 0, cost_other: 0,
|
||||
dnevnice_count: 0, dnevnice_amount: 30.00,
|
||||
});
|
||||
openModal('m-pn');
|
||||
}
|
||||
|
||||
async function editPutni(id){
|
||||
try {
|
||||
const d = await api('/putni-nalozi/'+id);
|
||||
document.getElementById('m-pn-title').textContent = `Putni nalog #${id} (${d.head.status})`;
|
||||
document.getElementById('m-pn').dataset.id = id;
|
||||
_pnSetForm(d.head);
|
||||
openModal('m-pn');
|
||||
} catch(e){ alert('Greška: '+e.message); }
|
||||
}
|
||||
|
||||
async function savePutni(){
|
||||
const v = id => document.getElementById(id).value;
|
||||
const num = id => { const x = parseFloat(v(id)); return isNaN(x)?0:x; };
|
||||
const intOrNull = id => { const x = parseInt(v(id)); return isNaN(x)?null:x; };
|
||||
const strOrNull = id => { const s = v(id).trim(); return s?s:null; };
|
||||
const body = {
|
||||
klub_id: intOrNull('pn-f-klub'),
|
||||
user_id: intOrNull('pn-f-user'),
|
||||
clan_id: intOrNull('pn-f-clan'),
|
||||
report_type: v('pn-f-type') || 'sluzbeno_putovanje',
|
||||
report_no: strOrNull('pn-f-no'),
|
||||
destination: v('pn-f-dest').trim(),
|
||||
purpose: v('pn-f-purpose').trim(),
|
||||
date_from: v('pn-f-from'),
|
||||
date_to: v('pn-f-to'),
|
||||
vehicle_type: strOrNull('pn-f-vtype'),
|
||||
vehicle_plate: strOrNull('pn-f-vplate'),
|
||||
km_driven: num('pn-f-km'),
|
||||
km_rate: num('pn-f-kmrate'),
|
||||
cost_transport: num('pn-f-tr'),
|
||||
cost_lodging: num('pn-f-lo'),
|
||||
cost_meals: num('pn-f-me'),
|
||||
cost_other: num('pn-f-ot'),
|
||||
dnevnice_count: num('pn-f-dnc'),
|
||||
dnevnice_amount: num('pn-f-dna'),
|
||||
notes: strOrNull('pn-f-notes'),
|
||||
};
|
||||
if (!body.klub_id) { alert('Klub ID je obavezan.'); return; }
|
||||
if (!body.destination) { alert('Destinacija je obavezna.'); return; }
|
||||
if (!body.purpose) { alert('Svrha je obavezna.'); return; }
|
||||
if (!body.date_from || !body.date_to) { alert('Datumi su obavezni.'); return; }
|
||||
const id = document.getElementById('m-pn').dataset.id;
|
||||
try {
|
||||
if (id) {
|
||||
await api('/putni-nalozi/'+id, { method:'PATCH', body: JSON.stringify(body) });
|
||||
} else {
|
||||
await api('/putni-nalozi', { method:'POST', body: JSON.stringify(body) });
|
||||
}
|
||||
closeModal('m-pn');
|
||||
loadExpenseReports();
|
||||
} catch(e){ alert('Greška: '+e.message); }
|
||||
}
|
||||
|
||||
async function putniSetStatus(id, newStatus){
|
||||
const labels = {poslano:'pošaljete na odobrenje', odobreno:'odobrite', odbijeno:'odbijete', isplaceno:'označite kao isplaćeno'};
|
||||
if(!confirm(`Sigurno ${labels[newStatus]||'promijenite status'} za putni nalog #${id}?`)) return;
|
||||
try {
|
||||
await api('/putni-nalozi/'+id, { method:'PATCH', body: JSON.stringify({status:newStatus}) });
|
||||
loadExpenseReports();
|
||||
} catch(e){ alert('Greška: '+e.message); }
|
||||
}
|
||||
|
||||
async function deletePutni(id){
|
||||
if(!confirm(`Obriši putni nalog #${id}? (samo draft)`)) return;
|
||||
try {
|
||||
await api('/putni-nalozi/'+id, { method:'DELETE' });
|
||||
loadExpenseReports();
|
||||
} catch(e){ alert('Greška: '+e.message); }
|
||||
}
|
||||
|
||||
// ===== PAYMENTS =====
|
||||
async function loadPayments(){
|
||||
const tbody = document.querySelector('#py-tbl tbody');
|
||||
|
||||
Reference in New Issue
Block a user