f488623920
- routers/ocr_router.py: POST /api/ocr/upload (Tesseract+pdf2image, regex field extraction) - pgz_sport_api.py: mount ocr_router with try/except guard - static/erp_full.html: nova tab "📷 OCR" + panel - static/crm_v2.html: OCR upload modal/tab Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1358 lines
72 KiB
HTML
1358 lines
72 KiB
HTML
<!DOCTYPE html>
|
||
<!--
|
||
erp_full.html — FULL ERP (SAP-Lite) za PGŽ Sport
|
||
Author: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
|
||
Version: 1.0.0
|
||
Date: 2026-05-05
|
||
Description: Sub-tabs Dnevnik | Glavna knjiga | Partneri | Računi | PDV | Plaće | Proračun | Izvještaji
|
||
-->
|
||
<html lang="hr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>PGŽ SPORT — ERP (SAP-Lite)</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--pgz-blue:#003087; --pgz-blue2:#004CC4; --pgz-gold:#F4C430;
|
||
--bg0:#08090e; --bg1:#0d1021; --bg2:#111628; --bg3:#161d35; --bg4:#1c2542;
|
||
--rim:#1e2a50; --rim2:#283560;
|
||
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
|
||
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
|
||
--font:'Inter',sans-serif; --mono:'JetBrains Mono',monospace;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
html,body{height:100%}
|
||
body{font-family:var(--font);background:var(--bg0);color:var(--t1);font-size:13px;overflow-x:hidden}
|
||
button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
|
||
::-webkit-scrollbar{width:8px;height:8px}
|
||
::-webkit-scrollbar-track{background:var(--bg1)}
|
||
::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:4px}
|
||
|
||
.main{padding:0 0 0 0;flex:1;min-width:0}
|
||
.tb{background:var(--bg1);border-bottom:1px solid var(--rim);padding:12px 22px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:5}
|
||
.tb-t{font-size:15px;font-weight:700;color:var(--t0)}
|
||
.tb-s{font-size:11px;color:var(--t2)}
|
||
.content{padding:18px 22px}
|
||
|
||
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--rim);margin-bottom:14px;flex-wrap:wrap}
|
||
.tab{padding:9px 16px;cursor:pointer;color:var(--t2);font-weight:600;font-size:12px;border-bottom:2px solid transparent;transition:all .15s;background:none;border-left:0;border-right:0;border-top:0}
|
||
.tab:hover{color:var(--t1)}
|
||
.tab.active{color:var(--pgz-gold);border-bottom-color:var(--pgz-gold)}
|
||
|
||
.panel{display:none}
|
||
.panel.active{display:block}
|
||
|
||
.card{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:14px;margin-bottom:14px}
|
||
.card-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--rim)}
|
||
.card-t{font-weight:700;color:var(--t0);font-size:13px}
|
||
|
||
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
|
||
.toolbar input,.toolbar select{background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:7px 10px;color:var(--t1);font-size:12px}
|
||
.toolbar input:focus,.toolbar select:focus{border-color:var(--pgz-blue2)}
|
||
.toolbar label{font-size:11px;color:var(--t2);display:flex;align-items:center;gap:6px}
|
||
.btn{background:var(--pgz-blue2);border:0;color:#fff;padding:7px 14px;border-radius:5px;cursor:pointer;font-size:12px;font-weight:600;transition:background .15s}
|
||
.btn:hover{background:var(--pgz-blue)}
|
||
.btn.gold{background:var(--pgz-gold);color:var(--bg0)}
|
||
.btn.gold:hover{background:#e0b220}
|
||
.btn.green{background:var(--green);color:var(--bg0)}
|
||
.btn.red{background:var(--red);color:#fff}
|
||
.btn.sec{background:var(--bg3);color:var(--t1);border:1px solid var(--rim)}
|
||
|
||
table{width:100%;border-collapse:collapse;font-size:12px}
|
||
table th{background:var(--bg3);color:var(--t2);text-transform:uppercase;font-size:10px;letter-spacing:.5px;padding:8px 10px;text-align:left;border-bottom:1px solid var(--rim);font-weight:700}
|
||
table td{padding:7px 10px;border-bottom:1px solid var(--rim);color:var(--t1)}
|
||
table tbody tr{cursor:pointer;transition:background .15s}
|
||
table tbody tr:hover{background:var(--bg3)}
|
||
.num{font-family:var(--mono);text-align:right}
|
||
.tbl-wrap{overflow-x:auto;max-height:600px;overflow-y:auto;border:1px solid var(--rim);border-radius:6px}
|
||
|
||
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:100;align-items:flex-start;justify-content:center;padding-top:40px}
|
||
.modal-bg.show{display:flex}
|
||
.modal{background:var(--bg1);border:1px solid var(--rim);border-radius:8px;padding:18px;width:min(720px,94vw);max-height:90vh;overflow-y:auto}
|
||
.modal h3{font-size:14px;font-weight:700;color:var(--pgz-gold);margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid var(--rim)}
|
||
.form-row{display:grid;grid-template-columns:140px 1fr;gap:8px;margin-bottom:8px;align-items:center}
|
||
.form-row label{font-size:11px;color:var(--t2)}
|
||
.form-row input,.form-row select,.form-row textarea{width:100%;background:var(--bg2);border:1px solid var(--rim);border-radius:4px;padding:6px 9px;color:var(--t1);font-size:12px}
|
||
.form-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px;padding-top:10px;border-top:1px solid var(--rim)}
|
||
|
||
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:14px}
|
||
.kpi{background:linear-gradient(135deg,var(--bg2) 0%,var(--bg1) 100%);border:1px solid var(--rim);border-radius:8px;padding:12px 14px;position:relative}
|
||
.kpi::before{content:"";position:absolute;top:0;left:0;width:3px;height:100%;background:var(--pgz-gold)}
|
||
.kpi.g::before{background:var(--green)}
|
||
.kpi.r::before{background:var(--red)}
|
||
.kpi-l{font-size:10.5px;color:var(--t2);text-transform:uppercase;letter-spacing:1px;font-weight:600}
|
||
.kpi-v{font-size:22px;font-weight:800;color:var(--t0);margin-top:4px;font-family:var(--mono)}
|
||
|
||
.badge{display:inline-block;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase}
|
||
.badge.nacrt{background:var(--bg4);color:var(--t1)}
|
||
.badge.knjizen{background:var(--green);color:var(--bg0)}
|
||
.badge.placen{background:var(--pgz-gold);color:var(--bg0)}
|
||
.badge.otkazan{background:var(--red);color:#fff}
|
||
|
||
.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%}
|
||
.dnev-balans{padding:8px;background:var(--bg3);border-radius:4px;margin-top:8px;font-family:var(--mono);font-size:11px}
|
||
.dnev-balans.ok{color:var(--green)}
|
||
.dnev-balans.bad{color:var(--red)}
|
||
</style>
|
||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||
<script src="/static/shared/sidebar.js" defer data-active="erp_full"></script>
|
||
</head>
|
||
<body>
|
||
|
||
<main class="main">
|
||
<div class="tb">
|
||
<div>
|
||
<div class="tb-t">ERP — SAP-Lite</div>
|
||
<div class="tb-s">Dvostavno knjigovodstvo · HR-RRIF kontni plan · FINA e-Račun</div>
|
||
</div>
|
||
<div class="tb-s"><span style="color:var(--green)">●</span> /api/v2/erp/*</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="tabs">
|
||
<button class="tab active" data-panel="dnevnik">📓 Dnevnik</button>
|
||
<button class="tab" data-panel="glavna">📊 Glavna knjiga</button>
|
||
<button class="tab" data-panel="partneri">🤝 Partneri</button>
|
||
<button class="tab" data-panel="racuni">🧾 Računi</button>
|
||
<button class="tab" data-panel="uploads">📎 Uploads (OCR)</button>
|
||
<button class="tab" data-panel="ocr">📷 OCR</button>
|
||
<button class="tab" data-panel="putni">✈ Putni nalozi</button>
|
||
<button class="tab" data-panel="payments">💰 Plaćanja</button>
|
||
<button class="tab" data-panel="pdv">% PDV</button>
|
||
<button class="tab" data-panel="place">💼 Plaće</button>
|
||
<button class="tab" data-panel="proracun">€ Proračun</button>
|
||
<button class="tab" data-panel="izvjestaji">📈 Izvještaji</button>
|
||
<button class="tab" data-panel="kontni">📚 Kontni plan</button>
|
||
</div>
|
||
|
||
<!-- ============ DNEVNIK ============ -->
|
||
<section class="panel active" id="panel-dnevnik">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Dnevnik knjiženja</div>
|
||
<button class="btn gold" onclick="openDnevnikModal()">+ Novi zapis</button>
|
||
</div>
|
||
<div class="toolbar">
|
||
<label>Godina <input type="number" id="dnev-godina" value="2026" style="width:90px"></label>
|
||
<label>Tip <select id="dnev-tip"><option value="">— svi —</option><option value="rucno">Ručno</option><option value="racun_u">Račun ulazni</option><option value="racun_i">Račun izlazni</option><option value="placa">Plaća</option><option value="storno">Storno</option></select></label>
|
||
<button class="btn" onclick="loadDnevnik()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="dnev-tbl"><thead><tr><th>#</th><th>Datum</th><th>Opis</th><th>Tip</th><th class="num">Stavki</th><th class="num">Ukupno</th><th>Akcije</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ GLAVNA KNJIGA ============ -->
|
||
<section class="panel" id="panel-glavna">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Glavna knjiga (saldo po kontu)</div>
|
||
<button class="btn sec" onclick="exportXlsx('glavna-knjiga', 2026)">⬇ XLSX</button>
|
||
</div>
|
||
<div class="toolbar">
|
||
<label>Klasa <select id="gk-klasa"><option value="">— sve —</option><option>0</option><option>1</option><option>2</option><option>3</option><option>4</option><option>7</option><option>9</option></select></label>
|
||
<button class="btn" onclick="loadGlavnaKnjiga()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="gk-tbl"><thead><tr><th>Šifra</th><th>Naziv</th><th>Klasa</th><th>Vrsta</th><th class="num">Duguje</th><th class="num">Potražuje</th><th class="num">Saldo</th><th class="num">Stavki</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ PARTNERI ============ -->
|
||
<section class="panel" id="panel-partneri">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Partneri (kupci/dobavljači)</div>
|
||
<button class="btn gold" onclick="openPartnerModal()">+ Novi partner</button>
|
||
</div>
|
||
<div class="toolbar">
|
||
<input id="part-q" placeholder="Pretraži (naziv/OIB)…">
|
||
<label>Vrsta <select id="part-vrsta"><option value="">— sve —</option><option value="kupac">Kupac</option><option value="dobavljac">Dobavljač</option><option value="oba">Oba</option></select></label>
|
||
<button class="btn" onclick="loadPartneri()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="part-tbl"><thead><tr><th>#</th><th>OIB</th><th>Naziv</th><th>Vrsta</th><th>Grad</th><th>IBAN</th><th class="num">Saldo</th><th>Akcije</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ RAČUNI ============ -->
|
||
<section class="panel" id="panel-racuni">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Računi</div>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="btn gold" onclick="openRacunModal('ulazni')">+ Ulazni</button>
|
||
<button class="btn green" onclick="openRacunModal('izlazni')">+ Izlazni</button>
|
||
<label class="btn sec" style="cursor:pointer;display:inline-flex;align-items:center;gap:6px">
|
||
📥 Import e-Račun XML
|
||
<input type="file" id="eracun-file" accept=".xml,application/xml" style="display:none" onchange="importERacun()">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar">
|
||
<label>Tip <select id="rac-tip"><option value="ulazni">Ulazni</option><option value="izlazni">Izlazni</option></select></label>
|
||
<label>Status <select id="rac-status"><option value="">— svi —</option><option value="nacrt">Nacrt</option><option value="knjizen">Knjižen</option><option value="placen">Plaćen</option><option value="otkazan">Otkazan</option></select></label>
|
||
<label>Godina <input type="number" id="rac-godina" value="2026" style="width:90px"></label>
|
||
<button class="btn" onclick="loadRacuni()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="rac-tbl"><thead><tr><th>#</th><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th><th class="num">Brutto</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" za učitavanje…</td></tr></tbody></table>
|
||
</div>
|
||
<div id="rac-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">
|
||
<h4 id="rac-detail-title" style="font-size:12px;color:var(--t2);margin:0">Stavke</h4>
|
||
<button class="btn sec" onclick="document.getElementById('rac-detail').style.display='none'">× Zatvori</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="rac-stavke-tbl"><thead><tr><th>#</th><th>Naziv</th><th class="num">Količina</th><th>JM</th><th class="num">Cijena</th><th class="num">Popust %</th><th class="num">PDV %</th><th class="num">Neto</th><th class="num">Brutto</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
<h4 id="rac-uploads-title" style="font-size:12px;color:var(--t2);margin:12px 0 6px;display:none">Privitci (uploads)</h4>
|
||
<div id="rac-uploads-wrap" style="display:none" class="tbl-wrap">
|
||
<table id="rac-uploads-tbl"><thead><tr><th>#</th><th>Datoteka</th><th class="num">Veličina</th><th>Mime</th><th>OCR</th><th>Datum</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ INVOICE UPLOADS (OCR) ============ -->
|
||
<section class="panel" id="panel-uploads">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Računi (OCR) — Invoice Uploads / AI extraction</div>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="btn primary" onclick="document.getElementById('up-file').click()">📎 Upload novi račun</button>
|
||
<input type="file" id="up-file" accept="application/pdf,image/*" style="display:none" onchange="uploadInvoiceFile(this.files[0])">
|
||
</div>
|
||
</div>
|
||
<div id="up-drop" style="border:2px dashed var(--rim2);border-radius:8px;padding:18px;text-align:center;color:var(--t2);margin-bottom:10px;background:var(--bg3)">
|
||
Dovuci PDF / JPG / PNG ovdje (max 25 MB) ili koristi gumb gore.
|
||
<div id="up-progress" style="margin-top:6px;font-size:11px;color:var(--t1)"></div>
|
||
</div>
|
||
<div class="toolbar">
|
||
<input id="up-q" placeholder="Datoteka / vendor / broj…">
|
||
<label>Status <select id="up-status"><option value="">— svi —</option><option value="pending">pending</option><option value="ocr_done">ocr_done</option><option value="approved">approved</option><option value="rejected">rejected</option></select></label>
|
||
<button class="btn" onclick="loadUploads()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="up-tbl"><thead><tr><th>#</th><th>Datoteka</th><th class="num">Veličina</th><th>Vendor</th><th>OIB</th><th>Br. računa</th><th>Datum</th><th class="num">Brutto</th><th>OCR status</th><th class="num">Conf</th><th>Račun</th><th>Akcije</th></tr></thead><tbody><tr><td colspan="12" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ OCR (Računi) — lightweight /api/ocr/upload ============ -->
|
||
<section class="panel" id="panel-ocr">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">📷 OCR — Računi (Tesseract + regex extrakcija)</div>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="btn" onclick="ocrHealth()">🩺 Health</button>
|
||
</div>
|
||
</div>
|
||
<div style="padding:10px;border:2px dashed var(--rim2);border-radius:8px;background:var(--bg3);margin-bottom:10px">
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<input type="file" id="ocr-file" accept="application/pdf,image/jpeg,image/jpg,image/png">
|
||
<button class="btn primary" onclick="ocrUpload()">⬆ Upload</button>
|
||
<span id="ocr-status" style="color:var(--t2);font-size:11px"></span>
|
||
</div>
|
||
<div id="ocr-health" style="margin-top:6px;font-size:11px;color:var(--t1)"></div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Ekstrahirana polja</div></div>
|
||
<div class="tbl-wrap" style="padding:6px">
|
||
<table id="ocr-fields"><tbody>
|
||
<tr><th style="text-align:left;width:140px">vendor</th><td id="ocr-vendor">—</td></tr>
|
||
<tr><th style="text-align:left">OIB</th><td id="ocr-oib">—</td></tr>
|
||
<tr><th style="text-align:left">invoice_no</th><td id="ocr-invno">—</td></tr>
|
||
<tr><th style="text-align:left">date</th><td id="ocr-date">—</td></tr>
|
||
<tr><th style="text-align:left">amount</th><td id="ocr-amount">—</td></tr>
|
||
<tr><th style="text-align:left">ocr_status</th><td id="ocr-ostatus">—</td></tr>
|
||
<tr><th style="text-align:left">confidence</th><td id="ocr-conf">—</td></tr>
|
||
<tr><th style="text-align:left">file</th><td id="ocr-file-info">—</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<div style="padding:10px;display:flex;gap:8px">
|
||
<!-- TODO: stvarna integracija sa pgz_sport.racuni_ulazni (real save) -->
|
||
<button class="btn primary" onclick="ocrSaveRacun()">💾 Spremi u racuni_ulazni</button>
|
||
<button class="btn" onclick="ocrReset()">↺ Reset</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Prepoznati tekst (OCR)</div></div>
|
||
<pre id="ocr-text" style="white-space:pre-wrap;max-height:420px;overflow:auto;padding:10px;font-size:11px;background:var(--bg2);color:var(--t1);margin:0">— prazno —</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ 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="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>
|
||
<button class="btn" onclick="loadExpenseReports()">Osvježi</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>
|
||
</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">
|
||
<h4 id="pn-detail-title" style="font-size:12px;color:var(--t2);margin:0">Vezani računi</h4>
|
||
<button class="btn sec" onclick="document.getElementById('pn-detail').style.display='none'">× Zatvori</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="pn-rac-tbl"><thead><tr><th>#</th><th>Broj računa</th><th>Vendor</th><th class="num">Brutto</th><th>Valuta</th><th>Kategorija</th><th>Datum</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ PAYMENTS ============ -->
|
||
<section class="panel" id="panel-payments">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Plaćanja / Bank Reconciliation (payments)</div></div>
|
||
<div class="toolbar">
|
||
<label>Status <select id="py-status"><option value="">— svi —</option><option value="unmatched">unmatched</option><option value="matched">matched</option><option value="manual">manual</option></select></label>
|
||
<label>Način <select id="py-method"><option value="">— svi —</option><option value="transfer">transfer</option><option value="cash">cash</option><option value="card">card</option></select></label>
|
||
<label>Godina <input type="number" id="py-godina" placeholder="2026" style="width:90px"></label>
|
||
<button class="btn" onclick="loadPayments()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="py-tbl"><thead><tr><th>#</th><th>Datum</th><th>Klub</th><th class="num">Iznos</th><th>Valuta</th><th>Način</th><th>IBAN OD</th><th>IBAN ZA</th><th>Referenca</th><th>Račun</th><th>Putni nalog</th><th>Match</th></tr></thead><tbody><tr><td colspan="12" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ PDV ============ -->
|
||
<section class="panel" id="panel-pdv">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">PDV — Knjige + Obrazac</div>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="btn sec" onclick="exportXlsx('pdv-u', document.getElementById('pdv-godina').value, document.getElementById('pdv-mjesec').value)">⬇ U XLSX</button>
|
||
<button class="btn sec" onclick="exportXlsx('pdv-i', document.getElementById('pdv-godina').value, document.getElementById('pdv-mjesec').value)">⬇ I XLSX</button>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar">
|
||
<label>Godina <input type="number" id="pdv-godina" value="2026" style="width:90px"></label>
|
||
<label>Mjesec <input type="number" id="pdv-mjesec" min="1" max="12" placeholder="1-12" style="width:80px"></label>
|
||
<button class="btn" onclick="loadPdv()">Osvježi</button>
|
||
</div>
|
||
<div id="pdv-summary" class="kpi-grid"></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||
<div>
|
||
<h4 style="font-size:12px;color:var(--t2);margin-bottom:8px">KNJIGA U-RA (ulazni)</h4>
|
||
<div class="tbl-wrap"><table id="pdv-u-tbl"><thead><tr><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th></tr></thead><tbody></tbody></table></div>
|
||
</div>
|
||
<div>
|
||
<h4 style="font-size:12px;color:var(--t2);margin-bottom:8px">KNJIGA I-RA (izlazni)</h4>
|
||
<div class="tbl-wrap"><table id="pdv-i-tbl"><thead><tr><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th></tr></thead><tbody></tbody></table></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ PLAĆE ============ -->
|
||
<section class="panel" id="panel-place">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Zaposlenici i obračun plaća</div>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="btn gold" onclick="openZapModal()">+ Zaposlenik</button>
|
||
<button class="btn green" onclick="openPlacaModal()">€ Obračun plaće</button>
|
||
</div>
|
||
</div>
|
||
<h4 style="font-size:12px;color:var(--t2);margin:8px 0">Zaposlenici</h4>
|
||
<div class="tbl-wrap" style="margin-bottom:14px">
|
||
<table id="zap-tbl"><thead><tr><th>#</th><th>OIB</th><th>Ime</th><th>Prezime</th><th>Klub</th><th>Mjesto</th><th class="num">Bruto</th><th>Aktivan</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
<h4 style="font-size:12px;color:var(--t2);margin:8px 0">Obračuni plaća</h4>
|
||
<div class="toolbar">
|
||
<label>Godina <input type="number" id="pl-godina" value="2026" style="width:90px"></label>
|
||
<label>Mjesec <input type="number" id="pl-mjesec" placeholder="1-12" style="width:80px"></label>
|
||
<button class="btn" onclick="loadPlace()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="pl-tbl"><thead><tr><th>#</th><th>Zaposlenik</th><th>God/Mj</th><th class="num">Bruto</th><th class="num">Doprinosi iz</th><th class="num">Dohodnina</th><th class="num">Neto</th><th class="num">Doprinosi na</th><th class="num">Trošak</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ PRORAČUN ============ -->
|
||
<section class="panel" id="panel-proracun">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Proračun PGŽ Sport (po godinama)</div></div>
|
||
<div class="tbl-wrap">
|
||
<table id="pr-tbl"><thead><tr><th>Godina</th><th class="num">Proračun PGŽ</th><th class="num">Rebalans 1</th><th class="num">Rebalans 2</th><th class="num">Ukupno PGŽ</th><th class="num">Ministarstvo</th><th class="num">Ukupno</th><th>Napomena</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ IZVJEŠTAJI ============ -->
|
||
<section class="panel" id="panel-izvjestaji">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Izvještaji (Bilanca · PnL · Cashflow)</div>
|
||
</div>
|
||
<div class="toolbar">
|
||
<label>Tip <select id="iz-tip"><option value="bilanca">Bilanca</option><option value="pnl">PnL (Račun dobiti/gubitka)</option><option value="cashflow">Cashflow</option></select></label>
|
||
<label>Godina <input type="number" id="iz-godina" value="2026" style="width:90px"></label>
|
||
<button class="btn" onclick="loadIzvjestaj()">Generiraj</button>
|
||
<button class="btn sec" onclick="exportXlsx(document.getElementById('iz-tip').value, document.getElementById('iz-godina').value)">⬇ XLSX</button>
|
||
<button class="btn sec" onclick="exportPdf(document.getElementById('iz-tip').value, document.getElementById('iz-godina').value)">⬇ PDF</button>
|
||
</div>
|
||
<div id="iz-out"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ KONTNI PLAN ============ -->
|
||
<section class="panel" id="panel-kontni">
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">Kontni plan (HR-RRIF)</div>
|
||
<button class="btn gold" onclick="openKontoModal()">+ Novi konto</button>
|
||
</div>
|
||
<div class="toolbar">
|
||
<input id="kp-q" placeholder="Šifra ili naziv…">
|
||
<label>Klasa <select id="kp-klasa"><option value="">— sve —</option><option>0</option><option>1</option><option>2</option><option>3</option><option>4</option><option>7</option><option>9</option></select></label>
|
||
<label>Vrsta <select id="kp-vrsta"><option value="">— sve —</option><option value="aktiva">Aktiva</option><option value="pasiva">Pasiva</option><option value="prihod">Prihod</option><option value="rashod">Rashod</option><option value="kapital">Kapital</option></select></label>
|
||
<button class="btn" onclick="loadKontniPlan()">Osvježi</button>
|
||
</div>
|
||
<div class="tbl-wrap">
|
||
<table id="kp-tbl"><thead><tr><th>Šifra</th><th>Naziv</th><th>Klasa</th><th>Vrsta</th><th>Aktivan</th><th>Akcije</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- ===== MODALS ===== -->
|
||
<div class="modal-bg" id="m-konto" onclick="if(event.target===this)closeModal('m-konto')">
|
||
<div class="modal">
|
||
<h3>Novi / izmjena konta</h3>
|
||
<div class="form-row"><label>Šifra</label><input id="k-sifra"></div>
|
||
<div class="form-row"><label>Naziv</label><input id="k-naziv"></div>
|
||
<div class="form-row"><label>Klasa</label><input type="number" id="k-klasa" min="0" max="9"></div>
|
||
<div class="form-row"><label>Vrsta</label><select id="k-vrsta"><option value="aktiva">Aktiva</option><option value="pasiva">Pasiva</option><option value="prihod">Prihod</option><option value="rashod">Rashod</option><option value="kapital">Kapital</option><option value="izvanbilanca">Izvanbilanca</option></select></div>
|
||
<div class="form-actions">
|
||
<button class="btn sec" onclick="closeModal('m-konto')">Odustani</button>
|
||
<button class="btn gold" onclick="saveKonto()">Spremi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-bg" id="m-partner" onclick="if(event.target===this)closeModal('m-partner')">
|
||
<div class="modal">
|
||
<h3>Novi / izmjena partnera</h3>
|
||
<div class="form-row"><label>Naziv</label><input id="p-naziv"></div>
|
||
<div class="form-row"><label>OIB</label><input id="p-oib" maxlength="11"></div>
|
||
<div class="form-row"><label>Vrsta</label><select id="p-vrsta"><option value="oba">Oba</option><option value="kupac">Kupac</option><option value="dobavljac">Dobavljač</option></select></div>
|
||
<div class="form-row"><label>IBAN</label><input id="p-iban"></div>
|
||
<div class="form-row"><label>Adresa</label><input id="p-adresa"></div>
|
||
<div class="form-row"><label>Grad</label><input id="p-grad"></div>
|
||
<div class="form-row"><label>Email</label><input id="p-email"></div>
|
||
<div class="form-row"><label>Telefon</label><input id="p-telefon"></div>
|
||
<div class="form-actions">
|
||
<button class="btn sec" onclick="closeModal('m-partner')">Odustani</button>
|
||
<button class="btn gold" onclick="savePartner()">Spremi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-bg" id="m-dnev" onclick="if(event.target===this)closeModal('m-dnev')">
|
||
<div class="modal" style="width:min(960px,96vw)">
|
||
<h3>Novi zapis u dnevniku</h3>
|
||
<div class="form-row"><label>Datum</label><input type="date" id="d-datum"></div>
|
||
<div class="form-row"><label>Opis</label><input id="d-opis"></div>
|
||
<div class="form-row"><label>Tip dokumenta</label><select id="d-tip"><option value="rucno">Ručno</option><option value="racun_u">Račun ulazni</option><option value="racun_i">Račun izlazni</option><option value="placa">Plaća</option></select></div>
|
||
<h4 style="font-size:11px;color:var(--t2);margin:14px 0 6px">STAVKE (D=duguje, P=potražuje, samo jedno > 0)</h4>
|
||
<div id="d-lines"></div>
|
||
<button class="btn sec" onclick="addDnevLine()" style="margin-top:8px">+ Dodaj stavku</button>
|
||
<div id="d-balans" class="dnev-balans">Duguje: 0,00 · Potražuje: 0,00</div>
|
||
<div class="form-actions">
|
||
<button class="btn sec" onclick="closeModal('m-dnev')">Odustani</button>
|
||
<button class="btn gold" onclick="saveDnev()">Spremi zapis</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-bg" id="m-rac" onclick="if(event.target===this)closeModal('m-rac')">
|
||
<div class="modal" style="width:min(900px,96vw)">
|
||
<h3 id="m-rac-title">Novi račun</h3>
|
||
<div class="form-row"><label>Broj</label><input id="r-broj"></div>
|
||
<div class="form-row"><label>Partner</label><select id="r-partner"></select></div>
|
||
<div class="form-row"><label>Datum izdavanja</label><input type="date" id="r-datum"></div>
|
||
<div class="form-row"><label>Dospijeće</label><input type="date" id="r-dospjece"></div>
|
||
<div class="form-row"><label>Status</label><select id="r-status"><option value="nacrt">Nacrt</option><option value="knjizen">Knjižen (auto-knjiženje)</option></select></div>
|
||
<h4 style="font-size:11px;color:var(--t2);margin:14px 0 6px">STAVKE</h4>
|
||
<div id="r-lines"></div>
|
||
<button class="btn sec" onclick="addRacLine()" style="margin-top:8px">+ Dodaj stavku</button>
|
||
<div id="r-summary" class="dnev-balans">Neto: 0,00 · PDV: 0,00 · Brutto: 0,00</div>
|
||
<div class="form-actions">
|
||
<button class="btn sec" onclick="closeModal('m-rac')">Odustani</button>
|
||
<button class="btn gold" onclick="saveRac()">Spremi račun</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-bg" id="m-zap" onclick="if(event.target===this)closeModal('m-zap')">
|
||
<div class="modal">
|
||
<h3>Novi zaposlenik</h3>
|
||
<div class="form-row"><label>Ime</label><input id="z-ime"></div>
|
||
<div class="form-row"><label>Prezime</label><input id="z-prezime"></div>
|
||
<div class="form-row"><label>OIB</label><input id="z-oib" maxlength="11"></div>
|
||
<div class="form-row"><label>Klub ID</label><input type="number" id="z-klub"></div>
|
||
<div class="form-row"><label>Radno mjesto</label><input id="z-mjesto"></div>
|
||
<div class="form-row"><label>Plata bruto</label><input type="number" step="0.01" id="z-bruto"></div>
|
||
<div class="form-row"><label>IBAN</label><input id="z-iban"></div>
|
||
<div class="form-actions">
|
||
<button class="btn sec" onclick="closeModal('m-zap')">Odustani</button>
|
||
<button class="btn gold" onclick="saveZap()">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>
|
||
<div class="form-row"><label>Zaposlenik</label><select id="pl-zap"></select></div>
|
||
<div class="form-row"><label>Godina</label><input type="number" id="pl-god" value="2026"></div>
|
||
<div class="form-row"><label>Mjesec</label><input type="number" id="pl-mj" min="1" max="12"></div>
|
||
<div class="form-row"><label>Bruto (€)</label><input type="number" step="0.01" id="pl-bruto" placeholder="prazno = iz zaposlenika"></div>
|
||
<div class="form-row"><label>Osobni odbitak</label><input type="number" step="0.01" id="pl-odb" value="600"></div>
|
||
<div class="form-row"><label>Prirez %</label><input type="number" step="0.1" id="pl-prirez" value="0"></div>
|
||
<div class="form-row"><label>Datum isplate</label><input type="date" id="pl-isplata"></div>
|
||
<div class="form-row"><label>Knjiži u dnevnik</label><input type="checkbox" id="pl-knjizi"></div>
|
||
<div class="form-actions">
|
||
<button class="btn sec" onclick="closeModal('m-pl')">Odustani</button>
|
||
<button class="btn gold" onclick="savePlaca()">Obračunaj</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = '/api/v2/erp';
|
||
const AUTH = () => ({ 'Authorization': 'Bearer ' + (localStorage.getItem('jwt') || localStorage.getItem('access_token') || 'admin-pgz-2026') });
|
||
const fmt = n => (Number(n||0)).toLocaleString('hr-HR',{minimumFractionDigits:2,maximumFractionDigits:2});
|
||
|
||
async function api(path, opts={}) {
|
||
const r = await fetch(API + path, { headers: { 'Content-Type':'application/json', ...AUTH() }, ...opts });
|
||
if (!r.ok) {
|
||
let detail = await r.text();
|
||
throw new Error(`${r.status}: ${detail}`);
|
||
}
|
||
return r.json();
|
||
}
|
||
|
||
// Tab switching
|
||
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
|
||
document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));
|
||
t.classList.add('active');
|
||
const panel = t.dataset.panel;
|
||
document.getElementById('panel-' + panel).classList.add('active');
|
||
loaders[panel] && loaders[panel]();
|
||
}));
|
||
|
||
function closeModal(id){ document.getElementById(id).classList.remove('show'); }
|
||
function openModal(id){ document.getElementById(id).classList.add('show'); }
|
||
|
||
// ===== KONTNI PLAN =====
|
||
async function loadKontniPlan(){
|
||
const q = document.getElementById('kp-q').value;
|
||
const klasa = document.getElementById('kp-klasa').value;
|
||
const vrsta = document.getElementById('kp-vrsta').value;
|
||
const params = new URLSearchParams();
|
||
if (q) params.set('q', q);
|
||
if (klasa) params.set('klasa', klasa);
|
||
if (vrsta) params.set('vrsta', vrsta);
|
||
const d = await api('/kontni-plan?' + params.toString());
|
||
const tbody = document.querySelector('#kp-tbl tbody');
|
||
tbody.innerHTML = d.rows.map(r=>`<tr>
|
||
<td><b>${r.sifra}</b></td><td>${r.naziv}</td><td>${r.klasa}</td><td>${r.vrsta}</td>
|
||
<td>${r.aktivan?'✓':'✗'}</td>
|
||
<td><button class="btn sec" onclick="event.stopPropagation();editKonto(${r.id})">✎</button></td>
|
||
</tr>`).join('');
|
||
}
|
||
function openKontoModal(){
|
||
['k-sifra','k-naziv','k-klasa'].forEach(i=>document.getElementById(i).value='');
|
||
document.getElementById('m-konto').dataset.id='';
|
||
openModal('m-konto');
|
||
}
|
||
async function editKonto(id){
|
||
const d = await api('/kontni-plan?'); // we don't have a single GET, so fetch & filter
|
||
const r = d.rows.find(x=>x.id===id);
|
||
if(!r) return;
|
||
document.getElementById('k-sifra').value=r.sifra;
|
||
document.getElementById('k-naziv').value=r.naziv;
|
||
document.getElementById('k-klasa').value=r.klasa;
|
||
document.getElementById('k-vrsta').value=r.vrsta;
|
||
document.getElementById('m-konto').dataset.id=id;
|
||
openModal('m-konto');
|
||
}
|
||
async function saveKonto(){
|
||
const body = {
|
||
sifra: document.getElementById('k-sifra').value.trim(),
|
||
naziv: document.getElementById('k-naziv').value.trim(),
|
||
klasa: parseInt(document.getElementById('k-klasa').value),
|
||
vrsta: document.getElementById('k-vrsta').value,
|
||
aktivan: true
|
||
};
|
||
const id = document.getElementById('m-konto').dataset.id;
|
||
try {
|
||
if(id) await api('/kontni-plan/'+id, { method:'PUT', body: JSON.stringify(body) });
|
||
else await api('/kontni-plan', { method:'POST', body: JSON.stringify(body) });
|
||
closeModal('m-konto'); loadKontniPlan();
|
||
} catch(e){ alert(e.message); }
|
||
}
|
||
|
||
// ===== PARTNERI =====
|
||
async function loadPartneri(){
|
||
const q = document.getElementById('part-q').value;
|
||
const v = document.getElementById('part-vrsta').value;
|
||
const p = new URLSearchParams();
|
||
if(q)p.set('q',q); if(v)p.set('vrsta',v);
|
||
const d = await api('/partneri?'+p.toString());
|
||
const tbody = document.querySelector('#part-tbl tbody');
|
||
tbody.innerHTML = d.rows.map(r=>`<tr onclick="loadPartnerSaldo(${r.id})">
|
||
<td>${r.id}</td><td>${r.oib||'—'}</td><td><b>${r.naziv}</b></td>
|
||
<td>${r.vrsta}</td><td>${r.grad||''}</td><td>${r.iban||''}</td>
|
||
<td class="num" id="ps-${r.id}">…</td>
|
||
<td><button class="btn sec" onclick="event.stopPropagation();editPartner(${r.id})">✎</button></td>
|
||
</tr>`).join('');
|
||
d.rows.forEach(r => {
|
||
api('/partneri/'+r.id+'/saldo').then(x => {
|
||
const el = document.getElementById('ps-'+r.id);
|
||
if(el) el.textContent = fmt(x.info?.saldo || 0);
|
||
}).catch(()=>{});
|
||
});
|
||
}
|
||
function openPartnerModal(){
|
||
['p-naziv','p-oib','p-iban','p-adresa','p-grad','p-email','p-telefon'].forEach(i=>document.getElementById(i).value='');
|
||
document.getElementById('m-partner').dataset.id='';
|
||
openModal('m-partner');
|
||
}
|
||
async function editPartner(id){
|
||
const d = await api('/partneri?');
|
||
const r = d.rows.find(x=>x.id===id);
|
||
if(!r) return;
|
||
document.getElementById('p-naziv').value=r.naziv||'';
|
||
document.getElementById('p-oib').value=r.oib||'';
|
||
document.getElementById('p-vrsta').value=r.vrsta;
|
||
document.getElementById('p-iban').value=r.iban||'';
|
||
document.getElementById('p-adresa').value=r.adresa||'';
|
||
document.getElementById('p-grad').value=r.grad||'';
|
||
document.getElementById('p-email').value=r.email||'';
|
||
document.getElementById('p-telefon').value=r.telefon||'';
|
||
document.getElementById('m-partner').dataset.id=id;
|
||
openModal('m-partner');
|
||
}
|
||
async function savePartner(){
|
||
const body = {
|
||
naziv: document.getElementById('p-naziv').value.trim(),
|
||
oib: document.getElementById('p-oib').value.trim() || null,
|
||
vrsta: document.getElementById('p-vrsta').value,
|
||
iban: document.getElementById('p-iban').value.trim() || null,
|
||
adresa: document.getElementById('p-adresa').value || null,
|
||
grad: document.getElementById('p-grad').value || null,
|
||
email: document.getElementById('p-email').value || null,
|
||
telefon: document.getElementById('p-telefon').value || null,
|
||
};
|
||
const id = document.getElementById('m-partner').dataset.id;
|
||
try {
|
||
if(id) await api('/partneri/'+id, { method:'PUT', body: JSON.stringify(body) });
|
||
else await api('/partneri', { method:'POST', body: JSON.stringify(body) });
|
||
closeModal('m-partner'); loadPartneri();
|
||
} catch(e){ alert(e.message); }
|
||
}
|
||
async function loadPartnerSaldo(id){
|
||
const d = await api('/partneri/'+id+'/saldo');
|
||
alert(`Partner saldo: ${fmt(d.info?.saldo||0)} EUR\nDuguje: ${fmt(d.info?.uk_duguje||0)}\nPotražuje: ${fmt(d.info?.uk_potrazuje||0)}\nBroj stavki: ${d.stavke.length}`);
|
||
}
|
||
|
||
// ===== DNEVNIK =====
|
||
async function loadDnevnik(){
|
||
const g = document.getElementById('dnev-godina').value;
|
||
const t = document.getElementById('dnev-tip').value;
|
||
const p = new URLSearchParams();
|
||
if(g) p.set('godina', g);
|
||
if(t) p.set('dokument_tip', t);
|
||
const d = await api('/dnevnik?'+p.toString());
|
||
const tbody = document.querySelector('#dnev-tbl tbody');
|
||
tbody.innerHTML = d.rows.map(r=>`<tr onclick="dnevDetail(${r.id})">
|
||
<td>${r.redni_broj||r.id}</td><td>${r.datum}</td><td>${r.opis||''}</td>
|
||
<td>${r.dokument_tip||''}</td><td class="num">${r.broj_stavki}</td>
|
||
<td class="num">${fmt(r.uk_duguje)}</td>
|
||
<td><button class="btn sec" onclick="event.stopPropagation();dnevStorno(${r.id})">↺ Storno</button></td>
|
||
</tr>`).join('');
|
||
}
|
||
async function dnevDetail(id){
|
||
const d = await api('/dnevnik/'+id);
|
||
let html = `<b>${d.head.opis||''}</b> · ${d.head.datum} · #${d.head.id}\n\nSTAVKE:\n`;
|
||
d.stavke.forEach(s => html += `${s.konto_sifra} ${s.konto_naziv}: D=${fmt(s.duguje)} P=${fmt(s.potrazuje)}\n`);
|
||
alert(html);
|
||
}
|
||
async function dnevStorno(id){
|
||
if(!confirm('Sigurno storno za zapis #'+id+'?')) return;
|
||
try {
|
||
await api('/dnevnik/'+id+'/storno', { method:'POST' });
|
||
loadDnevnik();
|
||
} catch(e){ alert(e.message); }
|
||
}
|
||
let kontoCache = [];
|
||
async function loadKontoCache(){
|
||
if(kontoCache.length) return kontoCache;
|
||
const d = await api('/kontni-plan?aktivan=true');
|
||
kontoCache = d.rows;
|
||
return kontoCache;
|
||
}
|
||
async function openDnevnikModal(){
|
||
await loadKontoCache();
|
||
document.getElementById('d-datum').value = new Date().toISOString().slice(0,10);
|
||
document.getElementById('d-opis').value = '';
|
||
document.getElementById('d-tip').value = 'rucno';
|
||
document.getElementById('d-lines').innerHTML = '';
|
||
addDnevLine(); addDnevLine();
|
||
updateDnevBalans();
|
||
openModal('m-dnev');
|
||
}
|
||
function addDnevLine(){
|
||
const opts = kontoCache.map(k=>`<option value="${k.id}">${k.sifra} — ${k.naziv}</option>`).join('');
|
||
const div = document.createElement('div');
|
||
div.className = 'dnev-line-row';
|
||
div.innerHTML = `
|
||
<select class="d-konto">${opts}</select>
|
||
<input class="d-opis" placeholder="opis stavke">
|
||
<input type="number" step="0.01" class="d-duguje" placeholder="Duguje" oninput="updateDnevBalans()">
|
||
<input type="number" step="0.01" class="d-potrazuje" placeholder="Potražuje" oninput="updateDnevBalans()">
|
||
<input type="number" class="d-partner" placeholder="Partner ID (opc)">
|
||
<button class="btn red" onclick="this.parentElement.remove();updateDnevBalans()">×</button>`;
|
||
document.getElementById('d-lines').appendChild(div);
|
||
}
|
||
function updateDnevBalans(){
|
||
const rows = document.querySelectorAll('#d-lines .dnev-line-row');
|
||
let d=0,p=0;
|
||
rows.forEach(r => {
|
||
d += parseFloat(r.querySelector('.d-duguje').value)||0;
|
||
p += parseFloat(r.querySelector('.d-potrazuje').value)||0;
|
||
});
|
||
const el = document.getElementById('d-balans');
|
||
el.textContent = `Duguje: ${fmt(d)} · Potražuje: ${fmt(p)} · Razlika: ${fmt(d-p)}`;
|
||
el.className = 'dnev-balans ' + (d===p && d>0 ? 'ok' : 'bad');
|
||
}
|
||
async function saveDnev(){
|
||
const stavke = Array.from(document.querySelectorAll('#d-lines .dnev-line-row')).map(r => ({
|
||
konto_id: parseInt(r.querySelector('.d-konto').value),
|
||
opis: r.querySelector('.d-opis').value,
|
||
duguje: parseFloat(r.querySelector('.d-duguje').value)||0,
|
||
potrazuje: parseFloat(r.querySelector('.d-potrazuje').value)||0,
|
||
partner_id: parseInt(r.querySelector('.d-partner').value)||null
|
||
}));
|
||
const body = {
|
||
datum: document.getElementById('d-datum').value,
|
||
opis: document.getElementById('d-opis').value,
|
||
dokument_tip: document.getElementById('d-tip').value,
|
||
stavke
|
||
};
|
||
try {
|
||
await api('/dnevnik', { method:'POST', body: JSON.stringify(body) });
|
||
closeModal('m-dnev'); loadDnevnik();
|
||
} catch(e){ alert(e.message); }
|
||
}
|
||
|
||
// ===== GLAVNA KNJIGA =====
|
||
async function loadGlavnaKnjiga(){
|
||
const k = document.getElementById('gk-klasa').value;
|
||
const p = new URLSearchParams();
|
||
if(k) p.set('klasa', k);
|
||
const d = await api('/glavna-knjiga?'+p.toString());
|
||
const tbody = document.querySelector('#gk-tbl tbody');
|
||
tbody.innerHTML = d.rows.map(r=>`<tr>
|
||
<td><b>${r.sifra}</b></td><td>${r.naziv}</td><td>${r.klasa}</td><td>${r.vrsta}</td>
|
||
<td class="num">${fmt(r.sum_duguje)}</td>
|
||
<td class="num">${fmt(r.sum_potrazuje)}</td>
|
||
<td class="num"><b>${fmt(r.saldo)}</b></td>
|
||
<td class="num">${r.broj_stavki}</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
// ===== RAČUNI =====
|
||
let partnerCache = [];
|
||
async function loadPartnerCache(){
|
||
if(partnerCache.length) return partnerCache;
|
||
const d = await api('/partneri?');
|
||
partnerCache = d.rows;
|
||
return partnerCache;
|
||
}
|
||
async function loadRacuni(){
|
||
const tip = document.getElementById('rac-tip').value;
|
||
const status = document.getElementById('rac-status').value;
|
||
const godina = document.getElementById('rac-godina').value;
|
||
const p = new URLSearchParams();
|
||
if(status) p.set('status', status);
|
||
if(godina) p.set('godina', godina);
|
||
const d = await api('/racuni/'+tip+'?'+p.toString());
|
||
const tbody = document.querySelector('#rac-tbl tbody');
|
||
tbody.innerHTML = d.rows.map(r=>`<tr onclick="racDetail('${tip}',${r.id})">
|
||
<td>${r.id}</td><td>${r.broj||''}</td><td>${r.datum_izdavanja}</td>
|
||
<td>${r.partner_naziv||''}</td><td>${r.partner_oib||''}</td>
|
||
<td class="num">${fmt(r.iznos_neto)}</td>
|
||
<td class="num">${fmt(r.iznos_pdv)}</td>
|
||
<td class="num"><b>${fmt(r.iznos_brutto)}</b></td>
|
||
<td><span class="badge ${r.status}">${r.status}</span></td>
|
||
<td>${r.status==='nacrt'?`<button class="btn green" onclick="event.stopPropagation();knjizi('${tip}',${r.id})">Knjiži</button>`:''}</td>
|
||
</tr>`).join('');
|
||
}
|
||
async function knjizi(tip, id){
|
||
if(!confirm('Knjižiti račun #'+id+' u dnevnik?')) return;
|
||
try{ await api('/racuni/'+tip+'/'+id+'/knjizi', {method:'POST'}); loadRacuni(); }
|
||
catch(e){ alert(e.message); }
|
||
}
|
||
async function racDetail(tip, id){
|
||
try {
|
||
const d = await api('/racuni/'+tip+'/'+id);
|
||
document.getElementById('rac-detail').style.display = 'block';
|
||
document.getElementById('rac-detail-title').textContent =
|
||
`Stavke računa ${tip} · #${id} · ${d.head.broj||'(bez broja)'} · ${d.head.partner_naziv||''} · Neto ${fmt(d.head.iznos_neto)} · Brutto ${fmt(d.head.iznos_brutto)}`;
|
||
const sb = document.querySelector('#rac-stavke-tbl tbody');
|
||
sb.innerHTML = (d.stavke||[]).length
|
||
? d.stavke.map((s,ix)=>`<tr><td>${ix+1}</td><td>${s.naziv||''}</td><td class="num">${fmt(s.kolicina)}</td><td>${s.jed_mjera||''}</td><td class="num">${fmt(s.cijena_jed)}</td><td class="num">${fmt(s.popust_pct)}</td><td class="num">${fmt(s.pdv_pct)}</td><td class="num">${fmt(s.iznos_neto)}</td><td class="num"><b>${fmt(s.iznos_brutto)}</b></td></tr>`).join('')
|
||
: `<tr><td colspan="9" style="color:var(--t2);text-align:center;padding:14px">Nema stavki.</td></tr>`;
|
||
// For ulazni: show linked invoice_uploads (file_name)
|
||
const upT = document.getElementById('rac-uploads-title');
|
||
const upW = document.getElementById('rac-uploads-wrap');
|
||
if (tip === 'ulazni') {
|
||
upT.style.display='block'; upW.style.display='block';
|
||
try {
|
||
const u = await api('/racuni/ulazni/'+id+'/uploads');
|
||
const ub = document.querySelector('#rac-uploads-tbl tbody');
|
||
ub.innerHTML = (u.rows||[]).length
|
||
? u.rows.map(r=>`<tr><td>${r.id}</td><td><a href="/uploads/${r.file_path||''}" target="_blank">${r.file_name||''}</a></td><td class="num">${fmt((r.file_size||0)/1024)} KB</td><td>${r.mime||''}</td><td>${r.ocr_status||''}</td><td>${(r.uploaded_at||'').slice(0,10)}</td></tr>`).join('')
|
||
: `<tr><td colspan="6" style="color:var(--t2);text-align:center;padding:10px">Nema privitaka.</td></tr>`;
|
||
} catch(e){
|
||
document.querySelector('#rac-uploads-tbl tbody').innerHTML = `<tr><td colspan="6" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
|
||
}
|
||
} else {
|
||
upT.style.display='none'; upW.style.display='none';
|
||
}
|
||
} catch(e) { alert('Greška: '+e.message); }
|
||
}
|
||
async function openRacunModal(tip){
|
||
await loadPartnerCache();
|
||
document.getElementById('m-rac').dataset.tip = tip;
|
||
document.getElementById('m-rac-title').textContent = 'Novi ' + tip + ' račun';
|
||
document.getElementById('r-broj').value='';
|
||
document.getElementById('r-datum').value = new Date().toISOString().slice(0,10);
|
||
document.getElementById('r-dospjece').value = '';
|
||
document.getElementById('r-status').value = 'nacrt';
|
||
document.getElementById('r-partner').innerHTML = partnerCache.map(p=>`<option value="${p.id}">${p.naziv}${p.oib?' · '+p.oib:''}</option>`).join('');
|
||
document.getElementById('r-lines').innerHTML='';
|
||
addRacLine();
|
||
updateRacSummary();
|
||
openModal('m-rac');
|
||
}
|
||
function addRacLine(){
|
||
const div = document.createElement('div');
|
||
div.className = 'dnev-line-row';
|
||
div.style.gridTemplateColumns = '1fr 60px 80px 80px 60px 30px';
|
||
div.innerHTML = `
|
||
<input class="r-naziv" placeholder="Naziv stavke">
|
||
<input type="number" step="0.01" class="r-kol" value="1" oninput="updateRacSummary()">
|
||
<input type="number" step="0.01" class="r-cij" placeholder="Cijena" oninput="updateRacSummary()">
|
||
<input type="number" step="0.01" class="r-pop" value="0" placeholder="Popust %" oninput="updateRacSummary()">
|
||
<input type="number" step="0.01" class="r-pdv" value="25" placeholder="PDV %" oninput="updateRacSummary()">
|
||
<button class="btn red" onclick="this.parentElement.remove();updateRacSummary()">×</button>`;
|
||
document.getElementById('r-lines').appendChild(div);
|
||
}
|
||
function updateRacSummary(){
|
||
const rows = document.querySelectorAll('#r-lines .dnev-line-row');
|
||
let neto=0, pdv=0;
|
||
rows.forEach(r => {
|
||
const k = parseFloat(r.querySelector('.r-kol').value)||0;
|
||
const c = parseFloat(r.querySelector('.r-cij').value)||0;
|
||
const pop = parseFloat(r.querySelector('.r-pop').value)||0;
|
||
const ppdv = parseFloat(r.querySelector('.r-pdv').value)||0;
|
||
const n = k*c*(1-pop/100);
|
||
neto += n;
|
||
pdv += n*ppdv/100;
|
||
});
|
||
document.getElementById('r-summary').textContent = `Neto: ${fmt(neto)} · PDV: ${fmt(pdv)} · Brutto: ${fmt(neto+pdv)}`;
|
||
}
|
||
async function saveRac(){
|
||
const tip = document.getElementById('m-rac').dataset.tip;
|
||
const stavke = Array.from(document.querySelectorAll('#r-lines .dnev-line-row')).map(r => ({
|
||
naziv: r.querySelector('.r-naziv').value,
|
||
kolicina: parseFloat(r.querySelector('.r-kol').value)||1,
|
||
cijena_jed: parseFloat(r.querySelector('.r-cij').value)||0,
|
||
popust_pct: parseFloat(r.querySelector('.r-pop').value)||0,
|
||
pdv_pct: parseFloat(r.querySelector('.r-pdv').value)||25,
|
||
}));
|
||
const body = {
|
||
broj: document.getElementById('r-broj').value || null,
|
||
partner_id: parseInt(document.getElementById('r-partner').value),
|
||
datum_izdavanja: document.getElementById('r-datum').value,
|
||
datum_dospjeca: document.getElementById('r-dospjece').value || null,
|
||
status: document.getElementById('r-status').value,
|
||
stavke
|
||
};
|
||
try {
|
||
await api('/racuni/'+tip, { method:'POST', body: JSON.stringify(body) });
|
||
closeModal('m-rac');
|
||
loadRacuni();
|
||
} catch(e){ alert(e.message); }
|
||
}
|
||
async function importERacun(){
|
||
const f = document.getElementById('eracun-file').files[0];
|
||
if(!f) return;
|
||
const fd = new FormData();
|
||
fd.append('file', f);
|
||
const r = await fetch(API+'/racuni/eracun-import', { method:'POST', headers: AUTH(), body: fd });
|
||
const d = await r.json();
|
||
alert(r.ok ? `e-Račun importiran:\nBroj: ${d.broj}\nNeto: ${fmt(d.neto)} PDV: ${fmt(d.pdv)} Brutto: ${fmt(d.brutto)}` : 'Greška: '+JSON.stringify(d));
|
||
document.getElementById('eracun-file').value='';
|
||
loadRacuni();
|
||
}
|
||
|
||
// ===== PDV =====
|
||
async function loadPdv(){
|
||
const g = document.getElementById('pdv-godina').value;
|
||
const m = document.getElementById('pdv-mjesec').value;
|
||
const params = new URLSearchParams({ godina:g });
|
||
if(m) params.set('mjesec', m);
|
||
const [u,i,o] = await Promise.all([
|
||
api('/pdv/knjiga-u?'+params.toString()),
|
||
api('/pdv/knjiga-i?'+params.toString()),
|
||
api('/pdv/obrazac?'+params.toString())
|
||
]);
|
||
document.querySelector('#pdv-u-tbl tbody').innerHTML = u.rows.map(r=>`<tr><td>${r.broj||''}</td><td>${r.datum_izdavanja}</td><td>${r.partner_naziv||''}</td><td>${r.partner_oib||''}</td><td class="num">${fmt(r.iznos_neto)}</td><td class="num">${fmt(r.iznos_pdv)}</td></tr>`).join('');
|
||
document.querySelector('#pdv-i-tbl tbody').innerHTML = i.rows.map(r=>`<tr><td>${r.broj||''}</td><td>${r.datum_izdavanja}</td><td>${r.partner_naziv||''}</td><td>${r.partner_oib||''}</td><td class="num">${fmt(r.iznos_neto)}</td><td class="num">${fmt(r.iznos_pdv)}</td></tr>`).join('');
|
||
document.getElementById('pdv-summary').innerHTML = `
|
||
<div class="kpi"><div class="kpi-l">Pretporez (U)</div><div class="kpi-v">${fmt(o.ulazni.pdv)} €</div></div>
|
||
<div class="kpi"><div class="kpi-l">PDV obveza (I)</div><div class="kpi-v">${fmt(o.izlazni.pdv)} €</div></div>
|
||
<div class="kpi ${o.obveza_za_uplatu>0?'r':'g'}"><div class="kpi-l">Obveza za uplatu</div><div class="kpi-v">${fmt(o.obveza_za_uplatu)} €</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Pretporez za povrat</div><div class="kpi-v">${fmt(o.pretporez_za_povrat)} €</div></div>`;
|
||
}
|
||
|
||
// ===== ZAPOSLENICI + PLAĆE =====
|
||
async function loadZap(){
|
||
const d = await api('/zaposlenici');
|
||
document.querySelector('#zap-tbl tbody').innerHTML = d.rows.map(r=>`<tr>
|
||
<td>${r.id}</td><td>${r.oib||'—'}</td><td>${r.ime}</td><td>${r.prezime}</td>
|
||
<td>${r.klub_naziv||'—'}</td><td>${r.radno_mjesto||''}</td>
|
||
<td class="num">${fmt(r.plata_bruto)}</td><td>${r.aktivan?'✓':'✗'}</td>
|
||
</tr>`).join('');
|
||
}
|
||
async function loadPlace(){
|
||
const g = document.getElementById('pl-godina').value;
|
||
const m = document.getElementById('pl-mjesec').value;
|
||
const p = new URLSearchParams();
|
||
if(g)p.set('godina',g); if(m)p.set('mjesec',m);
|
||
const d = await api('/place/obracun?'+p.toString());
|
||
document.querySelector('#pl-tbl tbody').innerHTML = d.rows.map(r=>`<tr>
|
||
<td>${r.id}</td><td>${r.ime} ${r.prezime}</td><td>${r.godina}/${String(r.mjesec).padStart(2,'0')}</td>
|
||
<td class="num">${fmt(r.bruto)}</td>
|
||
<td class="num">${fmt(r.doprinosi_iz_plate)}</td>
|
||
<td class="num">${fmt(r.dohodnina)}</td>
|
||
<td class="num"><b>${fmt(r.neto)}</b></td>
|
||
<td class="num">${fmt(r.doprinosi_na_plate)}</td>
|
||
<td class="num">${fmt(r.ukupni_trosak)}</td>
|
||
</tr>`).join('');
|
||
}
|
||
function openZapModal(){
|
||
['z-ime','z-prezime','z-oib','z-klub','z-mjesto','z-bruto','z-iban'].forEach(i=>document.getElementById(i).value='');
|
||
openModal('m-zap');
|
||
}
|
||
async function saveZap(){
|
||
const body = {
|
||
ime: document.getElementById('z-ime').value,
|
||
prezime: document.getElementById('z-prezime').value,
|
||
oib: document.getElementById('z-oib').value || null,
|
||
klub_id: parseInt(document.getElementById('z-klub').value)||null,
|
||
radno_mjesto: document.getElementById('z-mjesto').value || null,
|
||
plata_bruto: parseFloat(document.getElementById('z-bruto').value)||0,
|
||
iban: document.getElementById('z-iban').value || null,
|
||
aktivan: true
|
||
};
|
||
try { await api('/zaposlenici', { method:'POST', body: JSON.stringify(body) }); closeModal('m-zap'); loadZap(); }
|
||
catch(e){ alert(e.message); }
|
||
}
|
||
async function openPlacaModal(){
|
||
const d = await api('/zaposlenici');
|
||
document.getElementById('pl-zap').innerHTML = d.rows.map(r=>`<option value="${r.id}">${r.ime} ${r.prezime}</option>`).join('');
|
||
openModal('m-pl');
|
||
}
|
||
async function savePlaca(){
|
||
const body = {
|
||
zaposlenik_id: parseInt(document.getElementById('pl-zap').value),
|
||
godina: parseInt(document.getElementById('pl-god').value),
|
||
mjesec: parseInt(document.getElementById('pl-mj').value),
|
||
bruto: parseFloat(document.getElementById('pl-bruto').value) || null,
|
||
osobni_odbitak: parseFloat(document.getElementById('pl-odb').value)||600,
|
||
prirez_pct: parseFloat(document.getElementById('pl-prirez').value)||0,
|
||
datum_isplate: document.getElementById('pl-isplata').value || null,
|
||
knjizi: document.getElementById('pl-knjizi').checked
|
||
};
|
||
try {
|
||
const r = await api('/place/obracun', { method:'POST', body: JSON.stringify(body) });
|
||
alert(`Bruto: ${fmt(r.calc.bruto)}\nDoprinosi iz: ${fmt(r.calc.doprinosi_iz_plate)}\nDohodnina: ${fmt(r.calc.dohodnina)}\nNeto: ${fmt(r.calc.neto)}\nUkupni trošak: ${fmt(r.calc.ukupni_trosak)}`);
|
||
closeModal('m-pl'); loadPlace();
|
||
} catch(e){ alert(e.message); }
|
||
}
|
||
|
||
// ===== PRORAČUN =====
|
||
async function loadProracun(){
|
||
const tbody = document.querySelector('#pr-tbl tbody');
|
||
tbody.innerHTML = `<tr><td colspan="8" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
|
||
try {
|
||
const d = await api('/proracun');
|
||
tbody.innerHTML = (d.rows||[]).length
|
||
? d.rows.map(r=>`<tr>
|
||
<td><b>${r.godina}</b></td><td class="num">${fmt(r.proracun_pgz)}</td>
|
||
<td class="num">${fmt(r.rebalans1)}</td><td class="num">${fmt(r.rebalans2)}</td>
|
||
<td class="num"><b>${fmt(r.ukupno_pgz)}</b></td><td class="num">${fmt(r.ministarstvo)}</td>
|
||
<td class="num"><b>${fmt(r.ukupno)}</b></td><td>${r.napomena||''}</td>
|
||
</tr>`).join('')
|
||
: `<tr><td colspan="8" style="color:var(--t2);text-align:center;padding:14px">Nema podataka.</td></tr>`;
|
||
} catch(e) {
|
||
tbody.innerHTML = `<tr><td colspan="8" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
// ===== INVOICE UPLOADS =====
|
||
async function loadUploads(){
|
||
const tbody = document.querySelector('#up-tbl tbody');
|
||
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
|
||
try {
|
||
const q = (document.getElementById('up-q').value||'').trim();
|
||
const st = document.getElementById('up-status').value;
|
||
const p = new URLSearchParams();
|
||
if(q) p.set('q', q);
|
||
if(st) p.set('ocr_status', st);
|
||
const d = await api('/invoice-uploads?'+p.toString());
|
||
tbody.innerHTML = (d.rows||[]).length
|
||
? d.rows.map(r=>`<tr>
|
||
<td>${r.id}</td>
|
||
<td><a href="/uploads/${r.file_path||''}" target="_blank">${r.file_name||''}</a></td>
|
||
<td class="num">${fmt((r.file_size||0)/1024)} KB</td>
|
||
<td>${r.ai_vendor_name||''}</td>
|
||
<td>${r.ai_vendor_oib||''}</td>
|
||
<td>${r.ai_invoice_no||''}</td>
|
||
<td>${r.ai_invoice_date||''}</td>
|
||
<td class="num">${fmt(r.ai_amount_gross)}</td>
|
||
<td><span class="badge ${r.ocr_status||''}">${r.ocr_status||'—'}</span></td>
|
||
<td class="num">${r.ocr_confidence!=null?fmt(r.ocr_confidence)+' %':''}</td>
|
||
<td>${r.invoice_id?('#'+r.invoice_id):'—'}</td>
|
||
<td><a class="btn sec" href="/uploads/${r.file_path||''}" target="_blank">Otvori</a></td>
|
||
</tr>`).join('')
|
||
: `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Nema uploadova.</td></tr>`;
|
||
} catch(e) {
|
||
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
// Upload new invoice file via multipart
|
||
async function uploadInvoiceFile(file){
|
||
if(!file) return;
|
||
const prog = document.getElementById('up-progress');
|
||
prog.textContent = 'Šaljem ' + file.name + ' (' + Math.round(file.size/1024) + ' KB)…';
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
// Note: no Content-Type header — browser sets multipart boundary
|
||
const r = await fetch(API + '/invoice-uploads', {
|
||
method:'POST', body: fd, headers: AUTH()
|
||
});
|
||
if(!r.ok) throw new Error('HTTP ' + r.status + ' ' + (await r.text()));
|
||
const j = await r.json();
|
||
prog.textContent = '✓ Uploaded #' + j.id + ' (' + Math.round((j.file_size||0)/1024) + ' KB) — OCR pending.';
|
||
document.getElementById('up-file').value = '';
|
||
loadUploads();
|
||
} catch(e) {
|
||
prog.textContent = '✗ Greška: ' + e.message;
|
||
}
|
||
}
|
||
|
||
// Drag & drop on uploads card
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const drop = document.getElementById('up-drop');
|
||
if(!drop) return;
|
||
['dragenter','dragover'].forEach(ev => drop.addEventListener(ev, e => {
|
||
e.preventDefault(); e.stopPropagation();
|
||
drop.style.background='var(--bg2)';
|
||
}));
|
||
['dragleave','drop'].forEach(ev => drop.addEventListener(ev, e => {
|
||
e.preventDefault(); e.stopPropagation();
|
||
drop.style.background='var(--bg3)';
|
||
}));
|
||
drop.addEventListener('drop', e => {
|
||
if(e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]){
|
||
uploadInvoiceFile(e.dataTransfer.files[0]);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ===== PUTNI NALOZI / EXPENSE REPORTS =====
|
||
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>`;
|
||
try {
|
||
const t = document.getElementById('pn-type').value;
|
||
const s = document.getElementById('pn-status').value;
|
||
const g = document.getElementById('pn-godina').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());
|
||
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.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><span class="badge ${r.status||''}">${r.status||''}</span></td>
|
||
</tr>`).join('')
|
||
: `<tr><td colspan="11" 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>`;
|
||
}
|
||
}
|
||
|
||
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 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('')
|
||
: `<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); }
|
||
}
|
||
|
||
// ===== PAYMENTS =====
|
||
async function loadPayments(){
|
||
const tbody = document.querySelector('#py-tbl tbody');
|
||
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
|
||
try {
|
||
const s = document.getElementById('py-status').value;
|
||
const m = document.getElementById('py-method').value;
|
||
const g = document.getElementById('py-godina').value;
|
||
const p = new URLSearchParams();
|
||
if(s) p.set('matched_status', s);
|
||
if(m) p.set('payment_method', m);
|
||
if(g) p.set('godina', g);
|
||
const d = await api('/payments?'+p.toString());
|
||
tbody.innerHTML = (d.rows||[]).length
|
||
? d.rows.map(r=>`<tr>
|
||
<td>${r.id}</td>
|
||
<td>${r.payment_date||''}</td>
|
||
<td>${r.klub_naziv||r.klub_id||''}</td>
|
||
<td class="num"><b>${fmt(r.amount)}</b></td>
|
||
<td>${r.currency||''}</td>
|
||
<td>${r.payment_method||''}</td>
|
||
<td>${r.iban_from||''}</td>
|
||
<td>${r.iban_to||''}</td>
|
||
<td>${r.reference||''}</td>
|
||
<td>${r.invoice_id?('#'+r.invoice_id):'—'}</td>
|
||
<td>${r.expense_report_id?('#'+r.expense_report_id):'—'}</td>
|
||
<td><span class="badge ${r.matched_status||''}">${r.matched_status||''}</span></td>
|
||
</tr>`).join('')
|
||
: `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Nema plaćanja.</td></tr>`;
|
||
} catch(e) {
|
||
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
// ===== IZVJEŠTAJI =====
|
||
async function loadIzvjestaj(){
|
||
const tip = document.getElementById('iz-tip').value;
|
||
const g = document.getElementById('iz-godina').value;
|
||
const d = await api('/izvjestaji/'+tip+'?godina='+g);
|
||
const out = document.getElementById('iz-out');
|
||
if(tip==='bilanca'){
|
||
out.innerHTML = `
|
||
<div class="kpi-grid">
|
||
<div class="kpi g"><div class="kpi-l">Ukupno aktiva</div><div class="kpi-v">${fmt(d.ukupno_aktiva)} €</div></div>
|
||
<div class="kpi"><div class="kpi-l">Ukupno pasiva</div><div class="kpi-v">${fmt(d.ukupno_pasiva)} €</div></div>
|
||
<div class="kpi ${d.balans_ok?'g':'r'}"><div class="kpi-l">Balans</div><div class="kpi-v">${d.balans_ok?'✓ OK':'✗ neusklađen'}</div></div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">AKTIVA</h4>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Saldo</th></tr></thead>
|
||
<tbody>${d.aktiva.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.saldo)}</td></tr>`).join('')}</tbody></table></div></div>
|
||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">PASIVA</h4>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Saldo</th></tr></thead>
|
||
<tbody>${d.pasiva.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.saldo)}</td></tr>`).join('')}</tbody></table></div></div>
|
||
</div>`;
|
||
} else if(tip==='pnl'){
|
||
out.innerHTML = `
|
||
<div class="kpi-grid">
|
||
<div class="kpi g"><div class="kpi-l">Prihodi</div><div class="kpi-v">${fmt(d.ukupno_prihodi)} €</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Rashodi</div><div class="kpi-v">${fmt(d.ukupno_rashodi)} €</div></div>
|
||
<div class="kpi ${d.rezultat>=0?'g':'r'}"><div class="kpi-l">Rezultat (${d.tip_rezultata})</div><div class="kpi-v">${fmt(d.rezultat)} €</div></div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">PRIHODI</h4>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Iznos</th></tr></thead>
|
||
<tbody>${d.prihodi.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.iznos)}</td></tr>`).join('')}</tbody></table></div></div>
|
||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">RASHODI</h4>
|
||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Iznos</th></tr></thead>
|
||
<tbody>${d.rashodi.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.iznos)}</td></tr>`).join('')}</tbody></table></div></div>
|
||
</div>`;
|
||
} else {
|
||
out.innerHTML = `
|
||
<div class="tbl-wrap"><table><thead><tr><th>Mjesec</th><th class="num">Uplate</th><th class="num">Isplate</th><th class="num">Net</th></tr></thead>
|
||
<tbody>${d.po_mjesecu.map(r=>`<tr><td>${r.mjesec}</td><td class="num">${fmt(r.uplate)}</td><td class="num">${fmt(r.isplate)}</td><td class="num"><b>${fmt(Number(r.uplate)-Number(r.isplate))}</b></td></tr>`).join('')}</tbody></table></div>`;
|
||
}
|
||
}
|
||
|
||
function exportXlsx(report, godina, mjesec){
|
||
let url = API+'/export/xlsx/'+report+'?godina='+godina;
|
||
if(mjesec) url += '&mjesec='+mjesec;
|
||
window.open(url, '_blank');
|
||
}
|
||
function exportPdf(report, godina){
|
||
window.open(API+'/export/pdf/'+report+'?godina='+godina, '_blank');
|
||
}
|
||
|
||
// ===== OCR (lightweight /api/ocr) =====
|
||
const OCR_API = '/sport/api/ocr';
|
||
let _ocrLast = null;
|
||
|
||
function _ocrSet(id, val){
|
||
const el = document.getElementById(id);
|
||
if(el) el.textContent = (val === null || val === undefined || val === '') ? '—' : String(val);
|
||
}
|
||
|
||
async function ocrHealth(){
|
||
const out = document.getElementById('ocr-health');
|
||
if(out) out.textContent = '...checking';
|
||
try {
|
||
const r = await fetch(OCR_API + '/health');
|
||
const j = await r.json();
|
||
if(out){
|
||
out.textContent = 'tesseract: ' + (j.tesseract_available ? 'OK' : 'NO') +
|
||
' · pdf2image: ' + (j.pdf2image_available ? 'OK' : 'NO') +
|
||
' · upload_dir: ' + (j.upload_dir || '?');
|
||
}
|
||
} catch(e){
|
||
if(out) out.textContent = 'health err: ' + (e && e.message || e);
|
||
}
|
||
}
|
||
|
||
async function ocrUpload(){
|
||
const f = document.getElementById('ocr-file').files[0];
|
||
const stat = document.getElementById('ocr-status');
|
||
if(!f){ if(stat) stat.textContent = 'odaberi datoteku'; return; }
|
||
if(stat) stat.textContent = 'uploading…';
|
||
const fd = new FormData();
|
||
fd.append('file', f);
|
||
try {
|
||
const r = await fetch(OCR_API + '/upload', { method: 'POST', body: fd });
|
||
const j = await r.json();
|
||
if(!r.ok){
|
||
if(stat) stat.textContent = 'err ' + r.status + ': ' + (j && j.detail || '');
|
||
return;
|
||
}
|
||
_ocrLast = j;
|
||
const ex = j.extracted || {};
|
||
_ocrSet('ocr-vendor', ex.vendor);
|
||
_ocrSet('ocr-oib', ex.oib);
|
||
_ocrSet('ocr-invno', ex.invoice_no);
|
||
_ocrSet('ocr-date', ex.date);
|
||
_ocrSet('ocr-amount', ex.amount);
|
||
_ocrSet('ocr-ostatus', j.ocr_status);
|
||
_ocrSet('ocr-conf', j.ocr_confidence);
|
||
_ocrSet('ocr-file-info', (j.file_name || '?') + ' · ' + (j.file_size||0) + ' B · ' + (j.mime||'?'));
|
||
const txt = document.getElementById('ocr-text');
|
||
if(txt) txt.textContent = j.ocr_text || '— (prazno / OCR nije izvršen) —';
|
||
if(stat) stat.textContent = 'done · id=' + (j.id == null ? 'n/a' : j.id);
|
||
} catch(e){
|
||
if(stat) stat.textContent = 'err: ' + (e && e.message || e);
|
||
}
|
||
}
|
||
|
||
function ocrReset(){
|
||
['ocr-vendor','ocr-oib','ocr-invno','ocr-date','ocr-amount','ocr-ostatus','ocr-conf','ocr-file-info'].forEach(id => _ocrSet(id, null));
|
||
const txt = document.getElementById('ocr-text');
|
||
if(txt) txt.textContent = '— prazno —';
|
||
const stat = document.getElementById('ocr-status');
|
||
if(stat) stat.textContent = '';
|
||
_ocrLast = null;
|
||
}
|
||
|
||
function ocrSaveRacun(){
|
||
// TODO: stvarna integracija sa pgz_sport.racuni_ulazni (real save) — wire later
|
||
if(!_ocrLast){ alert('Nema OCR podatka. Prvo uploadaj račun.'); return; }
|
||
alert('TODO: spremi u racuni_ulazni\nfile_path: ' + (_ocrLast.file_path || '?') +
|
||
'\nvendor: ' + ((_ocrLast.extracted||{}).vendor || '?') +
|
||
'\namount: ' + ((_ocrLast.extracted||{}).amount || '?'));
|
||
}
|
||
|
||
// Lazy loaders per panel
|
||
const loaders = {
|
||
dnevnik: loadDnevnik,
|
||
glavna: loadGlavnaKnjiga,
|
||
partneri: loadPartneri,
|
||
racuni: loadRacuni,
|
||
uploads: loadUploads,
|
||
putni: loadExpenseReports,
|
||
payments: loadPayments,
|
||
pdv: loadPdv,
|
||
place: () => { loadZap(); loadPlace(); },
|
||
proracun: loadProracun,
|
||
izvjestaji: loadIzvjestaj,
|
||
kontni: loadKontniPlan,
|
||
ocr: ocrHealth
|
||
};
|
||
|
||
// Switch programmatically (used by deep links: ?tab=uploads / #tab=putni)
|
||
function activateTab(panelId){
|
||
const t = document.querySelector('.tab[data-panel="' + panelId + '"]');
|
||
if(!t) return false;
|
||
t.click();
|
||
return true;
|
||
}
|
||
|
||
// Initial
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadKontoCache();
|
||
loadPartnerCache();
|
||
// Deep-link support: ?tab=<panel> or #tab=<panel>
|
||
let target = null;
|
||
try {
|
||
const u = new URL(window.location.href);
|
||
target = u.searchParams.get('tab');
|
||
if(!target && u.hash){
|
||
const m = u.hash.match(/tab=([a-z]+)/i);
|
||
if(m) target = m[1];
|
||
}
|
||
} catch(e) {}
|
||
if(target && activateTab(target)){
|
||
// tab.click() already triggers loader
|
||
return;
|
||
}
|
||
loadDnevnik();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|