b72d037141
Bug: crm_v2.html, admin_users.html, ostali pozivali /api/v2/auth/me
koji ne postoji u backendu (postoji /api/auth/me bez v2).
401 redirect na /login?reason=unauthorized iako Damir prijavljen.
Fix:
- Frontend: replace /api/v2/auth/me → /api/auth/me u svim file-ovima
- Backend: dodan defensive alias @app.get('/api/v2/auth/me')
1543 lines
61 KiB
Python
1543 lines
61 KiB
Python
#!/usr/bin/env python3
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# Fajl: routers/crm_router.py | v1.0.0 | 05.05.2026
|
|
# Autor: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
|
|
# Lokacija: /opt/pgz-sport/routers/crm_router.py
|
|
# Svrha: Salesforce-Lite CRM (Accounts/Contacts/Leads/Opportunities/Activities/Cases)
|
|
# + pipeline kanban view, /convert za leads, /complete za activities.
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
"""CRM v2 router (Salesforce-style).
|
|
|
|
Prefix: /api/v2/crm
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor
|
|
from fastapi import APIRouter, HTTPException, Query, Body, Depends, Header
|
|
from pydantic import BaseModel, Field
|
|
|
|
router = APIRouter(prefix="/api/v2/crm", tags=["crm-v2"])
|
|
|
|
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
|
|
|
STAGES = ['prospecting', 'qualification', 'proposal', 'negotiation', 'closed_won', 'closed_lost']
|
|
LEAD_STATUSES = ['new', 'contacted', 'qualified', 'lost', 'converted']
|
|
CASE_STATUSES = ['open', 'in_progress', 'waiting', 'resolved', 'closed']
|
|
CASE_PRIORITIES = ['low', 'normal', 'high', 'urgent']
|
|
ACT_TYPES = ['call', 'meeting', 'email', 'task', 'note']
|
|
|
|
|
|
# ─────────── DB helpers ───────────
|
|
|
|
def _conn():
|
|
return psycopg2.connect(DSN, cursor_factory=RealDictCursor)
|
|
|
|
|
|
def _conv(v):
|
|
if isinstance(v, (date, datetime)):
|
|
return v.isoformat()
|
|
if isinstance(v, Decimal):
|
|
return float(v)
|
|
return v
|
|
|
|
|
|
def _row(d):
|
|
if d is None:
|
|
return None
|
|
return {k: _conv(v) for k, v in dict(d).items()}
|
|
|
|
|
|
def _rows(seq):
|
|
return [_row(r) for r in (seq or [])]
|
|
|
|
|
|
# ─────────── Auth helper (lightweight, mirrors v2 router) ───────────
|
|
|
|
def _current_user(authorization: Optional[str] = Header(None)) -> Optional[Dict[str, Any]]:
|
|
"""Resolve session user from Bearer token (token_hash = sha256(token))."""
|
|
if not authorization:
|
|
return None
|
|
tok = authorization.replace("Bearer ", "").strip()
|
|
if not tok:
|
|
return None
|
|
th = hashlib.sha256(tok.encode()).hexdigest()
|
|
try:
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT s.user_id, u.email, u.user_type, u.klub_id, u.savez_id, u.aktivan,
|
|
u.ime, u.prezime
|
|
FROM pgz_sport.user_sessions s
|
|
JOIN pgz_sport.users u ON u.id = s.user_id
|
|
WHERE s.token_hash = %s
|
|
AND s.revoked = false
|
|
AND s.expires_at > now()
|
|
LIMIT 1
|
|
""", (th,))
|
|
r = cur.fetchone()
|
|
if not r:
|
|
return None
|
|
d = _row(r)
|
|
if d.get("aktivan") is False:
|
|
return None
|
|
return d
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _require_user(user=Depends(_current_user)):
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
return user
|
|
|
|
|
|
def _is_admin(user: Dict) -> bool:
|
|
return bool(user) and user.get("user_type") in ("super_admin", "pgz_admin")
|
|
|
|
|
|
def _check_owner_or_admin(user: Dict, owner_user_id: Optional[int]):
|
|
if _is_admin(user):
|
|
return
|
|
if owner_user_id is not None and user.get("user_id") == owner_user_id:
|
|
return
|
|
raise HTTPException(status_code=403, detail="Insufficient privileges")
|
|
|
|
|
|
# ─────────── PIPELINE / DASHBOARD ───────────
|
|
|
|
@router.get("/pipeline")
|
|
def pipeline(user=Depends(_require_user)):
|
|
"""Aggregate opportunities by stage (count, sum, weighted, items)."""
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT o.id, o.naziv, o.stage, o.amount_eur, o.probability,
|
|
o.close_date, o.account_id, o.type,
|
|
a.naziv AS account_naziv
|
|
FROM pgz_sport.crm_opportunities o
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = o.account_id
|
|
ORDER BY o.close_date NULLS LAST, o.id
|
|
""")
|
|
opps = _rows(cur.fetchall())
|
|
|
|
buckets = {s: {"stage": s, "count": 0, "amount_total": 0.0,
|
|
"weighted_total": 0.0, "items": []} for s in STAGES}
|
|
for o in opps:
|
|
s = o.get("stage")
|
|
if s not in buckets:
|
|
continue
|
|
amt = float(o.get("amount_eur") or 0)
|
|
prob = float(o.get("probability") or 0)
|
|
buckets[s]["count"] += 1
|
|
buckets[s]["amount_total"] += amt
|
|
buckets[s]["weighted_total"] += amt * prob / 100.0
|
|
buckets[s]["items"].append(o)
|
|
|
|
return {"stages": [buckets[s] for s in STAGES]}
|
|
|
|
|
|
@router.get("/dashboard")
|
|
def dashboard(user=Depends(_require_user)):
|
|
out = {}
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE stage NOT IN ('closed_won','closed_lost')) AS open_opps,
|
|
COALESCE(SUM(amount_eur) FILTER (WHERE stage NOT IN ('closed_won','closed_lost')), 0) AS open_amount,
|
|
COALESCE(SUM(amount_eur * probability / 100.0)
|
|
FILTER (WHERE stage NOT IN ('closed_won','closed_lost')), 0) AS weighted_amount,
|
|
COUNT(*) FILTER (WHERE stage = 'closed_won' AND close_date >= date_trunc('quarter', current_date)) AS won_q,
|
|
COALESCE(SUM(amount_eur) FILTER (WHERE stage = 'closed_won'
|
|
AND close_date >= date_trunc('quarter', current_date)), 0) AS won_q_amount
|
|
FROM pgz_sport.crm_opportunities
|
|
""")
|
|
out["opportunities"] = _row(cur.fetchone())
|
|
|
|
cur.execute("""
|
|
SELECT status, COUNT(*) AS n
|
|
FROM pgz_sport.crm_leads
|
|
GROUP BY status
|
|
""")
|
|
out["leads_by_status"] = _rows(cur.fetchall())
|
|
|
|
cur.execute("""
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE completed_at IS NULL AND due_at < now()) AS overdue,
|
|
COUNT(*) FILTER (WHERE completed_at IS NULL AND due_at >= now()) AS upcoming,
|
|
COUNT(*) FILTER (WHERE completed_at IS NOT NULL) AS done
|
|
FROM pgz_sport.crm_activities
|
|
""")
|
|
out["activities"] = _row(cur.fetchone())
|
|
|
|
cur.execute("""
|
|
SELECT status, COUNT(*) AS n
|
|
FROM pgz_sport.crm_cases
|
|
GROUP BY status
|
|
""")
|
|
out["cases_by_status"] = _rows(cur.fetchall())
|
|
|
|
cur.execute("SELECT COUNT(*) AS n FROM pgz_sport.crm_accounts")
|
|
out["accounts_total"] = _row(cur.fetchone())["n"]
|
|
|
|
cur.execute("SELECT COUNT(*) AS n FROM pgz_sport.crm_contacts")
|
|
out["contacts_total"] = _row(cur.fetchone())["n"]
|
|
|
|
return out
|
|
|
|
|
|
# ─────────── ACCOUNTS ───────────
|
|
|
|
class AccountIn(BaseModel):
|
|
naziv: str
|
|
type: str = 'klub'
|
|
klub_id: Optional[int] = None
|
|
savez_id: Optional[int] = None
|
|
oib: Optional[str] = None
|
|
email: Optional[str] = None
|
|
telefon: Optional[str] = None
|
|
web: Optional[str] = None
|
|
adresa: Optional[str] = None
|
|
grad: Optional[str] = None
|
|
industry: Optional[str] = None
|
|
napomene: Optional[str] = None
|
|
|
|
|
|
@router.get("/accounts")
|
|
def list_accounts(q: Optional[str] = None, type: Optional[str] = None,
|
|
owner_id: Optional[int] = None, limit: int = 200, offset: int = 0,
|
|
user=Depends(_require_user)):
|
|
where, params = ["1=1"], []
|
|
if q:
|
|
where.append("(a.naziv ILIKE %s OR a.oib ILIKE %s OR a.email ILIKE %s OR a.grad ILIKE %s)")
|
|
like = f"%{q}%"
|
|
params += [like, like, like, like]
|
|
if type:
|
|
where.append("a.type = %s")
|
|
params.append(type)
|
|
if owner_id:
|
|
where.append("a.owner_user_id = %s")
|
|
params.append(owner_id)
|
|
sql = f"""
|
|
SELECT a.*,
|
|
(SELECT COUNT(*) FROM pgz_sport.crm_contacts c WHERE c.account_id = a.id) AS contacts_n,
|
|
(SELECT COUNT(*) FROM pgz_sport.crm_opportunities o WHERE o.account_id = a.id) AS opps_n,
|
|
u.email AS owner_email
|
|
FROM pgz_sport.crm_accounts a
|
|
LEFT JOIN pgz_sport.users u ON u.id = a.owner_user_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY a.updated_at DESC
|
|
LIMIT %s OFFSET %s
|
|
"""
|
|
params += [limit, offset]
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
return {"items": _rows(cur.fetchall())}
|
|
|
|
|
|
@router.post("/accounts")
|
|
def create_account(req: AccountIn, user=Depends(_require_user)):
|
|
if req.type not in ('klub', 'savez', 'sponzor', 'drzava', 'drugo'):
|
|
raise HTTPException(400, "invalid type")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_accounts
|
|
(naziv, type, klub_id, savez_id, oib, email, telefon, web, adresa, grad,
|
|
industry, napomene, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.naziv, req.type, req.klub_id, req.savez_id, req.oib, req.email,
|
|
req.telefon, req.web, req.adresa, req.grad, req.industry,
|
|
req.napomene, user["user_id"]))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.get("/accounts/{aid}")
|
|
def get_account(aid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT * FROM pgz_sport.crm_accounts WHERE id=%s", (aid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
out = _row(row)
|
|
cur.execute("SELECT * FROM pgz_sport.crm_contacts WHERE account_id=%s ORDER BY updated_at DESC", (aid,))
|
|
out["contacts"] = _rows(cur.fetchall())
|
|
cur.execute("SELECT * FROM pgz_sport.crm_opportunities WHERE account_id=%s ORDER BY updated_at DESC", (aid,))
|
|
out["opportunities"] = _rows(cur.fetchall())
|
|
cur.execute("""SELECT * FROM pgz_sport.crm_activities
|
|
WHERE account_id=%s ORDER BY COALESCE(due_at, created_at) DESC LIMIT 50""", (aid,))
|
|
out["activities"] = _rows(cur.fetchall())
|
|
cur.execute("SELECT * FROM pgz_sport.crm_cases WHERE account_id=%s ORDER BY updated_at DESC", (aid,))
|
|
out["cases"] = _rows(cur.fetchall())
|
|
return out
|
|
|
|
|
|
@router.put("/accounts/{aid}")
|
|
def update_account(aid: int, req: AccountIn, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_accounts WHERE id=%s", (aid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_accounts
|
|
SET naziv=%s, type=%s, klub_id=%s, savez_id=%s, oib=%s, email=%s,
|
|
telefon=%s, web=%s, adresa=%s, grad=%s, industry=%s, napomene=%s,
|
|
updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.naziv, req.type, req.klub_id, req.savez_id, req.oib, req.email,
|
|
req.telefon, req.web, req.adresa, req.grad, req.industry, req.napomene, aid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/accounts/{aid}")
|
|
def delete_account(aid: int, user=Depends(_require_user)):
|
|
if not _is_admin(user):
|
|
raise HTTPException(403, "admin only")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("DELETE FROM pgz_sport.crm_accounts WHERE id=%s RETURNING id", (aid,))
|
|
if not cur.fetchone():
|
|
raise HTTPException(404, "not found")
|
|
cn.commit()
|
|
return {"ok": True, "id": aid}
|
|
|
|
|
|
# ─────────── CONTACTS ───────────
|
|
|
|
class ContactIn(BaseModel):
|
|
account_id: Optional[int] = None
|
|
clan_id: Optional[int] = None
|
|
ime: str
|
|
prezime: str
|
|
funkcija: Optional[str] = None
|
|
email: Optional[str] = None
|
|
telefon: Optional[str] = None
|
|
mobitel: Optional[str] = None
|
|
napomene: Optional[str] = None
|
|
|
|
|
|
@router.get("/contacts")
|
|
def list_contacts(account_id: Optional[int] = None, q: Optional[str] = None,
|
|
limit: int = 200, offset: int = 0, user=Depends(_require_user)):
|
|
where, params = ["1=1"], []
|
|
if account_id:
|
|
where.append("c.account_id = %s")
|
|
params.append(account_id)
|
|
if q:
|
|
where.append("(c.ime ILIKE %s OR c.prezime ILIKE %s OR c.email ILIKE %s OR c.funkcija ILIKE %s)")
|
|
like = f"%{q}%"
|
|
params += [like, like, like, like]
|
|
sql = f"""
|
|
SELECT c.*, a.naziv AS account_naziv
|
|
FROM pgz_sport.crm_contacts c
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = c.account_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY c.updated_at DESC
|
|
LIMIT %s OFFSET %s
|
|
"""
|
|
params += [limit, offset]
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
return {"items": _rows(cur.fetchall())}
|
|
|
|
|
|
@router.post("/contacts")
|
|
def create_contact(req: ContactIn, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_contacts
|
|
(account_id, clan_id, ime, prezime, funkcija, email, telefon, mobitel, napomene, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.account_id, req.clan_id, req.ime, req.prezime, req.funkcija,
|
|
req.email, req.telefon, req.mobitel, req.napomene, user["user_id"]))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.get("/contacts/{cid}")
|
|
def get_contact(cid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT c.*, a.naziv AS account_naziv
|
|
FROM pgz_sport.crm_contacts c
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = c.account_id
|
|
WHERE c.id=%s
|
|
""", (cid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.put("/contacts/{cid}")
|
|
def update_contact(cid: int, req: ContactIn, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_contacts WHERE id=%s", (cid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_contacts
|
|
SET account_id=%s, clan_id=%s, ime=%s, prezime=%s, funkcija=%s,
|
|
email=%s, telefon=%s, mobitel=%s, napomene=%s, updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.account_id, req.clan_id, req.ime, req.prezime, req.funkcija,
|
|
req.email, req.telefon, req.mobitel, req.napomene, cid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/contacts/{cid}")
|
|
def delete_contact(cid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_contacts WHERE id=%s", (cid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("DELETE FROM pgz_sport.crm_contacts WHERE id=%s", (cid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": cid}
|
|
|
|
|
|
# ─────────── LEADS ───────────
|
|
|
|
class LeadIn(BaseModel):
|
|
ime: Optional[str] = None
|
|
prezime: Optional[str] = None
|
|
organizacija: Optional[str] = None
|
|
email: Optional[str] = None
|
|
telefon: Optional[str] = None
|
|
izvor: Optional[str] = None
|
|
status: str = 'new'
|
|
napomene: Optional[str] = None
|
|
|
|
|
|
class ConvertReq(BaseModel):
|
|
account: Optional[Dict[str, Any]] = None
|
|
contact: Optional[Dict[str, Any]] = None
|
|
opportunity: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
@router.get("/leads")
|
|
def list_leads(status: Optional[str] = None, q: Optional[str] = None,
|
|
limit: int = 200, offset: int = 0, user=Depends(_require_user)):
|
|
where, params = ["1=1"], []
|
|
if status:
|
|
where.append("status = %s")
|
|
params.append(status)
|
|
if q:
|
|
where.append("(ime ILIKE %s OR prezime ILIKE %s OR organizacija ILIKE %s OR email ILIKE %s)")
|
|
like = f"%{q}%"
|
|
params += [like, like, like, like]
|
|
sql = f"""
|
|
SELECT * FROM pgz_sport.crm_leads
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY updated_at DESC LIMIT %s OFFSET %s
|
|
"""
|
|
params += [limit, offset]
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
return {"items": _rows(cur.fetchall())}
|
|
|
|
|
|
@router.post("/leads")
|
|
def create_lead(req: LeadIn, user=Depends(_require_user)):
|
|
if req.status not in LEAD_STATUSES:
|
|
raise HTTPException(400, "invalid status")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_leads
|
|
(ime, prezime, organizacija, email, telefon, izvor, status, napomene, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.ime, req.prezime, req.organizacija, req.email, req.telefon,
|
|
req.izvor, req.status, req.napomene, user["user_id"]))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.get("/leads/{lid}")
|
|
def get_lead(lid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT * FROM pgz_sport.crm_leads WHERE id=%s", (lid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.put("/leads/{lid}")
|
|
def update_lead(lid: int, req: LeadIn, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_leads WHERE id=%s", (lid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_leads
|
|
SET ime=%s, prezime=%s, organizacija=%s, email=%s, telefon=%s,
|
|
izvor=%s, status=%s, napomene=%s, updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.ime, req.prezime, req.organizacija, req.email, req.telefon,
|
|
req.izvor, req.status, req.napomene, lid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/leads/{lid}")
|
|
def delete_lead(lid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_leads WHERE id=%s", (lid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("DELETE FROM pgz_sport.crm_leads WHERE id=%s", (lid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": lid}
|
|
|
|
|
|
@router.post("/leads/{lid}/convert")
|
|
def convert_lead(lid: int, req: ConvertReq, user=Depends(_require_user)):
|
|
"""Lead → Account + Contact (+ optional Opportunity). Sets lead status=converted."""
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT * FROM pgz_sport.crm_leads WHERE id=%s", (lid,))
|
|
lead = cur.fetchone()
|
|
if not lead:
|
|
raise HTTPException(404, "lead not found")
|
|
if lead["status"] == "converted":
|
|
raise HTTPException(409, "already converted")
|
|
|
|
a_payload = req.account or {}
|
|
a_naziv = a_payload.get("naziv") or lead["organizacija"] or f'{lead.get("ime","")} {lead.get("prezime","")}'.strip() or "Lead"
|
|
a_type = a_payload.get("type") or "drugo"
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_accounts
|
|
(naziv, type, email, telefon, napomene, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s) RETURNING *
|
|
""", (a_naziv, a_type, a_payload.get("email") or lead["email"],
|
|
a_payload.get("telefon") or lead["telefon"],
|
|
a_payload.get("napomene"), user["user_id"]))
|
|
new_account = cur.fetchone()
|
|
|
|
new_contact = None
|
|
if (lead["ime"] or lead["prezime"]) or req.contact:
|
|
c_payload = req.contact or {}
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_contacts
|
|
(account_id, ime, prezime, email, telefon, funkcija, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s) RETURNING *
|
|
""", (new_account["id"],
|
|
c_payload.get("ime") or lead["ime"] or "",
|
|
c_payload.get("prezime") or lead["prezime"] or "",
|
|
c_payload.get("email") or lead["email"],
|
|
c_payload.get("telefon") or lead["telefon"],
|
|
c_payload.get("funkcija"),
|
|
user["user_id"]))
|
|
new_contact = cur.fetchone()
|
|
|
|
new_opp = None
|
|
if req.opportunity:
|
|
o = req.opportunity
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_opportunities
|
|
(account_id, contact_id, naziv, type, stage, amount_eur, probability,
|
|
close_date, napomene, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING *
|
|
""", (new_account["id"], new_contact["id"] if new_contact else None,
|
|
o.get("naziv") or f"Opportunity from lead {lid}",
|
|
o.get("type") or "financiranje",
|
|
o.get("stage") or "prospecting",
|
|
o.get("amount_eur"),
|
|
o.get("probability") or 20,
|
|
o.get("close_date"),
|
|
o.get("napomene"),
|
|
user["user_id"]))
|
|
new_opp = cur.fetchone()
|
|
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_leads
|
|
SET status='converted', converted_account_id=%s, converted_contact_id=%s,
|
|
converted_at=now(), updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (new_account["id"], new_contact["id"] if new_contact else None, lid))
|
|
updated_lead = cur.fetchone()
|
|
cn.commit()
|
|
|
|
return {
|
|
"lead": _row(updated_lead),
|
|
"account": _row(new_account),
|
|
"contact": _row(new_contact),
|
|
"opportunity": _row(new_opp),
|
|
}
|
|
|
|
|
|
# ─────────── OPPORTUNITIES ───────────
|
|
|
|
class OppIn(BaseModel):
|
|
account_id: int
|
|
contact_id: Optional[int] = None
|
|
naziv: str
|
|
type: str = 'financiranje'
|
|
stage: str = 'prospecting'
|
|
amount_eur: Optional[float] = None
|
|
probability: int = 20
|
|
close_date: Optional[str] = None
|
|
napomene: Optional[str] = None
|
|
|
|
|
|
class StageReq(BaseModel):
|
|
stage: str
|
|
|
|
|
|
@router.get("/opportunities")
|
|
def list_opps(stage: Optional[str] = None, account_id: Optional[int] = None,
|
|
q: Optional[str] = None, limit: int = 500, offset: int = 0,
|
|
user=Depends(_require_user)):
|
|
where, params = ["1=1"], []
|
|
if stage:
|
|
where.append("o.stage = %s")
|
|
params.append(stage)
|
|
if account_id:
|
|
where.append("o.account_id = %s")
|
|
params.append(account_id)
|
|
if q:
|
|
where.append("(o.naziv ILIKE %s OR a.naziv ILIKE %s)")
|
|
like = f"%{q}%"
|
|
params += [like, like]
|
|
sql = f"""
|
|
SELECT o.*, a.naziv AS account_naziv
|
|
FROM pgz_sport.crm_opportunities o
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = o.account_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY o.close_date NULLS LAST, o.id DESC
|
|
LIMIT %s OFFSET %s
|
|
"""
|
|
params += [limit, offset]
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
return {"items": _rows(cur.fetchall())}
|
|
|
|
|
|
@router.post("/opportunities")
|
|
def create_opp(req: OppIn, user=Depends(_require_user)):
|
|
if req.stage not in STAGES:
|
|
raise HTTPException(400, "invalid stage")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_opportunities
|
|
(account_id, contact_id, naziv, type, stage, amount_eur, probability,
|
|
close_date, napomene, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.account_id, req.contact_id, req.naziv, req.type, req.stage,
|
|
req.amount_eur, req.probability, req.close_date, req.napomene, user["user_id"]))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.get("/opportunities/{oid}")
|
|
def get_opp(oid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT o.*, a.naziv AS account_naziv
|
|
FROM pgz_sport.crm_opportunities o
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = o.account_id
|
|
WHERE o.id=%s
|
|
""", (oid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.put("/opportunities/{oid}")
|
|
def update_opp(oid: int, req: OppIn, user=Depends(_require_user)):
|
|
if req.stage not in STAGES:
|
|
raise HTTPException(400, "invalid stage")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_opportunities WHERE id=%s", (oid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_opportunities
|
|
SET account_id=%s, contact_id=%s, naziv=%s, type=%s, stage=%s,
|
|
amount_eur=%s, probability=%s, close_date=%s, napomene=%s, updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.account_id, req.contact_id, req.naziv, req.type, req.stage,
|
|
req.amount_eur, req.probability, req.close_date, req.napomene, oid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.patch("/opportunities/{oid}/stage")
|
|
def patch_stage(oid: int, req: StageReq, user=Depends(_require_user)):
|
|
if req.stage not in STAGES:
|
|
raise HTTPException(400, "invalid stage")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_opportunities WHERE id=%s", (oid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_opportunities
|
|
SET stage=%s, updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.stage, oid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/opportunities/{oid}")
|
|
def delete_opp(oid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_opportunities WHERE id=%s", (oid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("DELETE FROM pgz_sport.crm_opportunities WHERE id=%s", (oid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": oid}
|
|
|
|
|
|
# ─────────── ACTIVITIES ───────────
|
|
|
|
class ActivityIn(BaseModel):
|
|
type: str
|
|
subject: str
|
|
body: Optional[str] = None
|
|
account_id: Optional[int] = None
|
|
contact_id: Optional[int] = None
|
|
opportunity_id: Optional[int] = None
|
|
lead_id: Optional[int] = None
|
|
due_at: Optional[str] = None
|
|
|
|
|
|
@router.get("/activities")
|
|
def list_activities(account_id: Optional[int] = None, contact_id: Optional[int] = None,
|
|
opp_id: Optional[int] = None, lead_id: Optional[int] = None,
|
|
type: Optional[str] = None, open_only: Optional[bool] = None,
|
|
limit: int = 200, offset: int = 0,
|
|
user=Depends(_require_user)):
|
|
where, params = ["1=1"], []
|
|
if account_id: where.append("ac.account_id = %s"); params.append(account_id)
|
|
if contact_id: where.append("ac.contact_id = %s"); params.append(contact_id)
|
|
if opp_id: where.append("ac.opportunity_id = %s"); params.append(opp_id)
|
|
if lead_id: where.append("ac.lead_id = %s"); params.append(lead_id)
|
|
if type: where.append("ac.type = %s"); params.append(type)
|
|
if open_only is True:
|
|
where.append("ac.completed_at IS NULL")
|
|
elif open_only is False:
|
|
where.append("ac.completed_at IS NOT NULL")
|
|
sql = f"""
|
|
SELECT ac.*, a.naziv AS account_naziv,
|
|
c.ime || ' ' || c.prezime AS contact_naziv,
|
|
o.naziv AS opp_naziv
|
|
FROM pgz_sport.crm_activities ac
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = ac.account_id
|
|
LEFT JOIN pgz_sport.crm_contacts c ON c.id = ac.contact_id
|
|
LEFT JOIN pgz_sport.crm_opportunities o ON o.id = ac.opportunity_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY COALESCE(ac.due_at, ac.created_at) DESC
|
|
LIMIT %s OFFSET %s
|
|
"""
|
|
params += [limit, offset]
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
return {"items": _rows(cur.fetchall())}
|
|
|
|
|
|
@router.post("/activities")
|
|
def create_activity(req: ActivityIn, user=Depends(_require_user)):
|
|
if req.type not in ACT_TYPES:
|
|
raise HTTPException(400, "invalid type")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_activities
|
|
(type, subject, body, account_id, contact_id, opportunity_id, lead_id,
|
|
due_at, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.type, req.subject, req.body, req.account_id, req.contact_id,
|
|
req.opportunity_id, req.lead_id, req.due_at, user["user_id"]))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.get("/activities/{aid}")
|
|
def get_activity(aid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT * FROM pgz_sport.crm_activities WHERE id=%s", (aid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.put("/activities/{aid}")
|
|
def update_activity(aid: int, req: ActivityIn, user=Depends(_require_user)):
|
|
if req.type not in ACT_TYPES:
|
|
raise HTTPException(400, "invalid type")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_activities WHERE id=%s", (aid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_activities
|
|
SET type=%s, subject=%s, body=%s, account_id=%s, contact_id=%s,
|
|
opportunity_id=%s, lead_id=%s, due_at=%s
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.type, req.subject, req.body, req.account_id, req.contact_id,
|
|
req.opportunity_id, req.lead_id, req.due_at, aid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.patch("/activities/{aid}/complete")
|
|
def complete_activity(aid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
UPDATE pgz_sport.crm_activities
|
|
SET completed_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (aid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
cn.commit()
|
|
return _row(row)
|
|
|
|
|
|
@router.delete("/activities/{aid}")
|
|
def delete_activity(aid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_activities WHERE id=%s", (aid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("DELETE FROM pgz_sport.crm_activities WHERE id=%s", (aid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": aid}
|
|
|
|
|
|
# ─────────── CASES ───────────
|
|
|
|
class CaseIn(BaseModel):
|
|
account_id: Optional[int] = None
|
|
contact_id: Optional[int] = None
|
|
subject: str
|
|
description: Optional[str] = None
|
|
status: str = 'open'
|
|
priority: str = 'normal'
|
|
|
|
|
|
@router.get("/cases")
|
|
def list_cases(status: Optional[str] = None, priority: Optional[str] = None,
|
|
account_id: Optional[int] = None, q: Optional[str] = None,
|
|
limit: int = 200, offset: int = 0, user=Depends(_require_user)):
|
|
where, params = ["1=1"], []
|
|
if status: where.append("k.status = %s"); params.append(status)
|
|
if priority: where.append("k.priority = %s"); params.append(priority)
|
|
if account_id: where.append("k.account_id = %s"); params.append(account_id)
|
|
if q:
|
|
where.append("(k.subject ILIKE %s OR k.description ILIKE %s)")
|
|
like = f"%{q}%"
|
|
params += [like, like]
|
|
sql = f"""
|
|
SELECT k.*, a.naziv AS account_naziv
|
|
FROM pgz_sport.crm_cases k
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = k.account_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY (CASE k.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1
|
|
WHEN 'normal' THEN 2 ELSE 3 END),
|
|
k.updated_at DESC
|
|
LIMIT %s OFFSET %s
|
|
"""
|
|
params += [limit, offset]
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
return {"items": _rows(cur.fetchall())}
|
|
|
|
|
|
@router.post("/cases")
|
|
def create_case(req: CaseIn, user=Depends(_require_user)):
|
|
if req.status not in CASE_STATUSES:
|
|
raise HTTPException(400, "invalid status")
|
|
if req.priority not in CASE_PRIORITIES:
|
|
raise HTTPException(400, "invalid priority")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.crm_cases
|
|
(account_id, contact_id, subject, description, status, priority, owner_user_id)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.account_id, req.contact_id, req.subject, req.description,
|
|
req.status, req.priority, user["user_id"]))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.get("/cases/{cid}")
|
|
def get_case(cid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT k.*, a.naziv AS account_naziv
|
|
FROM pgz_sport.crm_cases k
|
|
LEFT JOIN pgz_sport.crm_accounts a ON a.id = k.account_id
|
|
WHERE k.id=%s
|
|
""", (cid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.put("/cases/{cid}")
|
|
def update_case(cid: int, req: CaseIn, user=Depends(_require_user)):
|
|
if req.status not in CASE_STATUSES:
|
|
raise HTTPException(400, "invalid status")
|
|
if req.priority not in CASE_PRIORITIES:
|
|
raise HTTPException(400, "invalid priority")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_cases WHERE id=%s", (cid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
resolved_at_clause = ", resolved_at=now()" if req.status in ('resolved', 'closed') else ""
|
|
cur.execute(f"""
|
|
UPDATE pgz_sport.crm_cases
|
|
SET account_id=%s, contact_id=%s, subject=%s, description=%s,
|
|
status=%s, priority=%s, updated_at=now() {resolved_at_clause}
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.account_id, req.contact_id, req.subject, req.description,
|
|
req.status, req.priority, cid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/cases/{cid}")
|
|
def delete_case(cid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT owner_user_id FROM pgz_sport.crm_cases WHERE id=%s", (cid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
_check_owner_or_admin(user, row["owner_user_id"])
|
|
cur.execute("DELETE FROM pgz_sport.crm_cases WHERE id=%s", (cid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": cid}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# AGENT F — Salesforce-Lite extra tabs (Članarine / Liječnički / Obrasci)
|
|
# Added: 2026-05-05 by dradulic@outlook.com / damir@rinet.one
|
|
# Tables: pgz_sport.clanarine, pgz_sport.lijecnicki_pregledi,
|
|
# pgz_sport.form_templates, pgz_sport.form_submissions
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
import json as _json
|
|
|
|
CLANARINA_STATUSES = ['nepodmireno', 'djelomicno', 'podmireno', 'storno']
|
|
SUBMISSION_STATUSES = ['draft', 'submitted', 'approved', 'rejected']
|
|
|
|
|
|
# ─────────── ČLANARINE ───────────
|
|
|
|
class ClanarinaIn(BaseModel):
|
|
clan_id: Optional[int] = None
|
|
klub_id: Optional[int] = None
|
|
godina: int
|
|
razdoblje: Optional[str] = None
|
|
iznos_propisan: float
|
|
iznos_placen: Optional[float] = 0
|
|
datum_uplate: Optional[str] = None
|
|
nacin_uplate: Optional[str] = None
|
|
referenca: Optional[str] = None
|
|
racun_broj: Optional[str] = None
|
|
status: str = 'nepodmireno'
|
|
napomena: Optional[str] = None
|
|
|
|
|
|
@router.get("/clanarine")
|
|
def list_clanarine(klub_id: Optional[int] = None, clan_id: Optional[int] = None,
|
|
godina: Optional[int] = None, status: Optional[str] = None,
|
|
limit: int = 500, user=Depends(_require_user)):
|
|
where, args = ["1=1"], []
|
|
if klub_id: where.append("c.klub_id=%s"); args.append(klub_id)
|
|
if clan_id: where.append("c.clan_id=%s"); args.append(clan_id)
|
|
if godina: where.append("c.godina=%s"); args.append(godina)
|
|
if status: where.append("c.status=%s"); args.append(status)
|
|
args.append(limit)
|
|
sql = f"""
|
|
SELECT c.*,
|
|
(cl.ime||' '||cl.prezime) AS clan_naziv,
|
|
k.naziv AS klub_naziv
|
|
FROM pgz_sport.clanarine c
|
|
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY c.godina DESC, c.id DESC
|
|
LIMIT %s
|
|
"""
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, args)
|
|
items = _rows(cur.fetchall())
|
|
return {"items": items, "count": len(items)}
|
|
|
|
|
|
@router.get("/clanarine/{cid}")
|
|
def get_clanarina(cid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT c.*,
|
|
(cl.ime||' '||cl.prezime) AS clan_naziv,
|
|
k.naziv AS klub_naziv
|
|
FROM pgz_sport.clanarine c
|
|
LEFT JOIN pgz_sport.clanovi cl ON cl.id = c.clan_id
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
|
WHERE c.id=%s
|
|
""", (cid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.post("/clanarine")
|
|
def create_clanarina(req: ClanarinaIn, user=Depends(_require_user)):
|
|
if req.status not in CLANARINA_STATUSES:
|
|
raise HTTPException(400, "invalid status")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.clanarine
|
|
(clan_id, klub_id, godina, razdoblje, iznos_propisan, iznos_placen,
|
|
datum_uplate, nacin_uplate, referenca, racun_broj, status, napomena)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.clan_id, req.klub_id, req.godina, req.razdoblje,
|
|
req.iznos_propisan, req.iznos_placen or 0,
|
|
req.datum_uplate, req.nacin_uplate, req.referenca,
|
|
req.racun_broj, req.status, req.napomena))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.put("/clanarine/{cid}")
|
|
def update_clanarina(cid: int, req: ClanarinaIn, user=Depends(_require_user)):
|
|
if req.status not in CLANARINA_STATUSES:
|
|
raise HTTPException(400, "invalid status")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT id FROM pgz_sport.clanarine WHERE id=%s", (cid,))
|
|
if not cur.fetchone():
|
|
raise HTTPException(404, "not found")
|
|
cur.execute("""
|
|
UPDATE pgz_sport.clanarine
|
|
SET clan_id=%s, klub_id=%s, godina=%s, razdoblje=%s,
|
|
iznos_propisan=%s, iznos_placen=%s, datum_uplate=%s,
|
|
nacin_uplate=%s, referenca=%s, racun_broj=%s,
|
|
status=%s, napomena=%s, updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.clan_id, req.klub_id, req.godina, req.razdoblje,
|
|
req.iznos_propisan, req.iznos_placen or 0,
|
|
req.datum_uplate, req.nacin_uplate, req.referenca,
|
|
req.racun_broj, req.status, req.napomena, cid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/clanarine/{cid}")
|
|
def delete_clanarina(cid: int, user=Depends(_require_user)):
|
|
if not _is_admin(user):
|
|
raise HTTPException(403, "admin only")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("DELETE FROM pgz_sport.clanarine WHERE id=%s", (cid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": cid}
|
|
|
|
|
|
# ─────────── LIJEČNIČKI PREGLEDI ───────────
|
|
|
|
class LijecnickiIn(BaseModel):
|
|
clan_id: int
|
|
klub_id: Optional[int] = None
|
|
datum_pregleda: str
|
|
vrijedi_do: Optional[str] = None
|
|
vrsta_pregleda: Optional[str] = None
|
|
ustanova: Optional[str] = None
|
|
lijecnik: Optional[str] = None
|
|
spreman_za_natjecanje: Optional[bool] = True
|
|
ekg: Optional[bool] = False
|
|
krv: Optional[bool] = False
|
|
spirometrija: Optional[bool] = False
|
|
nalaz: Optional[str] = None
|
|
komentar_lijecnika: Optional[str] = None
|
|
preporuke: Optional[str] = None
|
|
iznos: Optional[float] = None
|
|
iznos_zzjz: Optional[float] = 0
|
|
iznos_klub: Optional[float] = 0
|
|
iznos_clan: Optional[float] = 0
|
|
datum_placanja: Optional[str] = None
|
|
placeno: Optional[bool] = False
|
|
racun_broj: Optional[str] = None
|
|
nacin_placanja: Optional[str] = None
|
|
napomena: Optional[str] = None
|
|
|
|
|
|
@router.get("/lijecnicki")
|
|
def list_lijecnicki(klub_id: Optional[int] = None, clan_id: Optional[int] = None,
|
|
expiring: Optional[bool] = None, limit: int = 500,
|
|
user=Depends(_require_user)):
|
|
"""expiring=true → vrijedi_do u idućih 30d ili manje (ili prošlo)."""
|
|
where, args = ["1=1"], []
|
|
if klub_id: where.append("l.klub_id=%s"); args.append(klub_id)
|
|
if clan_id: where.append("l.clan_id=%s"); args.append(clan_id)
|
|
if expiring:
|
|
where.append("(l.vrijedi_do IS NULL OR l.vrijedi_do <= current_date + interval '30 days')")
|
|
args.append(limit)
|
|
sql = f"""
|
|
SELECT l.*,
|
|
(cl.ime||' '||cl.prezime) AS clan_naziv,
|
|
k.naziv AS klub_naziv
|
|
FROM pgz_sport.lijecnicki_pregledi l
|
|
LEFT JOIN pgz_sport.clanovi cl ON cl.id = l.clan_id
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = l.klub_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY l.datum_pregleda DESC, l.id DESC
|
|
LIMIT %s
|
|
"""
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, args)
|
|
items = _rows(cur.fetchall())
|
|
return {"items": items, "count": len(items)}
|
|
|
|
|
|
@router.get("/lijecnicki/{lid}")
|
|
def get_lijecnicki(lid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT l.*,
|
|
(cl.ime||' '||cl.prezime) AS clan_naziv,
|
|
k.naziv AS klub_naziv
|
|
FROM pgz_sport.lijecnicki_pregledi l
|
|
LEFT JOIN pgz_sport.clanovi cl ON cl.id = l.clan_id
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = l.klub_id
|
|
WHERE l.id=%s
|
|
""", (lid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.post("/lijecnicki")
|
|
def create_lijecnicki(req: LijecnickiIn, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.lijecnicki_pregledi
|
|
(clan_id, klub_id, datum_pregleda, vrijedi_do, vrsta_pregleda,
|
|
ustanova, lijecnik, spreman_za_natjecanje, ekg, krv, spirometrija,
|
|
nalaz, komentar_lijecnika, preporuke,
|
|
iznos, iznos_zzjz, iznos_klub, iznos_clan,
|
|
datum_placanja, placeno, racun_broj, nacin_placanja, napomena)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,
|
|
%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
RETURNING *
|
|
""", (req.clan_id, req.klub_id, req.datum_pregleda, req.vrijedi_do,
|
|
req.vrsta_pregleda, req.ustanova, req.lijecnik,
|
|
req.spreman_za_natjecanje, req.ekg, req.krv, req.spirometrija,
|
|
req.nalaz, req.komentar_lijecnika, req.preporuke,
|
|
req.iznos, req.iznos_zzjz or 0, req.iznos_klub or 0, req.iznos_clan or 0,
|
|
req.datum_placanja, req.placeno, req.racun_broj,
|
|
req.nacin_placanja, req.napomena))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.put("/lijecnicki/{lid}")
|
|
def update_lijecnicki(lid: int, req: LijecnickiIn, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT id FROM pgz_sport.lijecnicki_pregledi WHERE id=%s", (lid,))
|
|
if not cur.fetchone():
|
|
raise HTTPException(404, "not found")
|
|
cur.execute("""
|
|
UPDATE pgz_sport.lijecnicki_pregledi
|
|
SET clan_id=%s, klub_id=%s, datum_pregleda=%s, vrijedi_do=%s,
|
|
vrsta_pregleda=%s, ustanova=%s, lijecnik=%s,
|
|
spreman_za_natjecanje=%s, ekg=%s, krv=%s, spirometrija=%s,
|
|
nalaz=%s, komentar_lijecnika=%s, preporuke=%s,
|
|
iznos=%s, iznos_zzjz=%s, iznos_klub=%s, iznos_clan=%s,
|
|
datum_placanja=%s, placeno=%s, racun_broj=%s,
|
|
nacin_placanja=%s, napomena=%s, updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.clan_id, req.klub_id, req.datum_pregleda, req.vrijedi_do,
|
|
req.vrsta_pregleda, req.ustanova, req.lijecnik,
|
|
req.spreman_za_natjecanje, req.ekg, req.krv, req.spirometrija,
|
|
req.nalaz, req.komentar_lijecnika, req.preporuke,
|
|
req.iznos, req.iznos_zzjz or 0, req.iznos_klub or 0, req.iznos_clan or 0,
|
|
req.datum_placanja, req.placeno, req.racun_broj,
|
|
req.nacin_placanja, req.napomena, lid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/lijecnicki/{lid}")
|
|
def delete_lijecnicki(lid: int, user=Depends(_require_user)):
|
|
if not _is_admin(user):
|
|
raise HTTPException(403, "admin only")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("DELETE FROM pgz_sport.lijecnicki_pregledi WHERE id=%s", (lid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": lid}
|
|
|
|
|
|
# ─────────── OBRASCI (templates + submissions) ───────────
|
|
|
|
class SubmissionIn(BaseModel):
|
|
template_id: Optional[int] = None
|
|
template_code: Optional[str] = None
|
|
klub_id: Optional[int] = None
|
|
clan_id: Optional[int] = None
|
|
data: Dict[str, Any] = Field(default_factory=dict)
|
|
status: str = 'draft' # draft / submitted
|
|
reference_no: Optional[str] = None
|
|
|
|
|
|
class SubmissionStatusIn(BaseModel):
|
|
status: str # submitted / approved / rejected
|
|
rejected_reason: Optional[str] = None
|
|
|
|
|
|
@router.get("/obrasci")
|
|
def list_obrasci_templates(kategorija: Optional[str] = None,
|
|
active: bool = True, user=Depends(_require_user)):
|
|
where, args = [], []
|
|
if active:
|
|
where.append("active=true")
|
|
if kategorija:
|
|
where.append("kategorija=%s"); args.append(kategorija)
|
|
sql = "SELECT id, code, naziv, kategorija, opis, schema_json, required_role, active FROM pgz_sport.form_templates"
|
|
if where:
|
|
sql += " WHERE " + " AND ".join(where)
|
|
sql += " ORDER BY kategorija NULLS LAST, naziv"
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, args)
|
|
items = _rows(cur.fetchall())
|
|
return {"items": items, "count": len(items)}
|
|
|
|
|
|
@router.get("/obrasci/submission")
|
|
def list_obrasci_submissions(status: Optional[str] = None,
|
|
klub_id: Optional[int] = None,
|
|
template_code: Optional[str] = None,
|
|
clan_id: Optional[int] = None,
|
|
limit: int = 500,
|
|
user=Depends(_require_user)):
|
|
where, args = ["1=1"], []
|
|
if status: where.append("s.status=%s"); args.append(status)
|
|
if klub_id: where.append("s.klub_id=%s"); args.append(klub_id)
|
|
if template_code: where.append("s.template_code=%s"); args.append(template_code)
|
|
if clan_id: where.append("s.clan_id=%s"); args.append(clan_id)
|
|
args.append(limit)
|
|
sql = f"""
|
|
SELECT s.id, s.template_id, s.template_code, s.klub_id, s.clan_id,
|
|
s.user_id, s.data, s.status, s.reference_no,
|
|
s.submitted_at, s.reviewed_at, s.approved_at, s.rejected_reason,
|
|
s.created_at, s.updated_at,
|
|
t.naziv AS template_naziv, t.kategorija,
|
|
k.naziv AS klub_naziv,
|
|
(cl.ime||' '||cl.prezime) AS clan_naziv,
|
|
u.email AS submitter_email
|
|
FROM pgz_sport.form_submissions s
|
|
LEFT JOIN pgz_sport.form_templates t ON t.id = s.template_id
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = s.klub_id
|
|
LEFT JOIN pgz_sport.clanovi cl ON cl.id = s.clan_id
|
|
LEFT JOIN pgz_sport.users u ON u.id = s.user_id
|
|
WHERE {' AND '.join(where)}
|
|
ORDER BY s.created_at DESC
|
|
LIMIT %s
|
|
"""
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, args)
|
|
items = _rows(cur.fetchall())
|
|
return {"items": items, "count": len(items)}
|
|
|
|
|
|
@router.get("/obrasci/submission/{sid}")
|
|
def get_obrazac_submission(sid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
SELECT s.*,
|
|
t.naziv AS template_naziv, t.kategorija, t.schema_json,
|
|
k.naziv AS klub_naziv,
|
|
(cl.ime||' '||cl.prezime) AS clan_naziv,
|
|
u.email AS submitter_email
|
|
FROM pgz_sport.form_submissions s
|
|
LEFT JOIN pgz_sport.form_templates t ON t.id = s.template_id
|
|
LEFT JOIN pgz_sport.klubovi k ON k.id = s.klub_id
|
|
LEFT JOIN pgz_sport.clanovi cl ON cl.id = s.clan_id
|
|
LEFT JOIN pgz_sport.users u ON u.id = s.user_id
|
|
WHERE s.id=%s
|
|
""", (sid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.post("/obrasci/submission")
|
|
def create_obrazac_submission(req: SubmissionIn, user=Depends(_require_user)):
|
|
if req.status not in ('draft', 'submitted'):
|
|
raise HTTPException(400, "invalid status (draft|submitted)")
|
|
tid = req.template_id
|
|
tcode = req.template_code
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
if tid and not tcode:
|
|
cur.execute("SELECT code FROM pgz_sport.form_templates WHERE id=%s", (tid,))
|
|
r = cur.fetchone()
|
|
if not r:
|
|
raise HTTPException(400, "unknown template_id")
|
|
tcode = r["code"]
|
|
elif tcode and not tid:
|
|
cur.execute("SELECT id FROM pgz_sport.form_templates WHERE code=%s", (tcode,))
|
|
r = cur.fetchone()
|
|
if not r:
|
|
raise HTTPException(400, "unknown template_code")
|
|
tid = r["id"]
|
|
elif not tid and not tcode:
|
|
raise HTTPException(400, "template_id or template_code required")
|
|
|
|
submitted_at = "now()" if req.status == 'submitted' else "NULL"
|
|
cur.execute(f"""
|
|
INSERT INTO pgz_sport.form_submissions
|
|
(template_id, template_code, klub_id, user_id, clan_id,
|
|
data, status, reference_no, submitted_at)
|
|
VALUES (%s,%s,%s,%s,%s,%s::jsonb,%s,%s,{submitted_at})
|
|
RETURNING *
|
|
""", (tid, tcode, req.klub_id, user["user_id"], req.clan_id,
|
|
_json.dumps(req.data or {}), req.status, req.reference_no))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.put("/obrasci/submission/{sid}")
|
|
def update_obrazac_submission(sid: int, req: SubmissionIn, user=Depends(_require_user)):
|
|
"""Submitter can update their own draft."""
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT user_id, status FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
if not _is_admin(user) and row["user_id"] != user["user_id"]:
|
|
raise HTTPException(403, "not your submission")
|
|
if row["status"] not in ('draft', 'submitted') and not _is_admin(user):
|
|
raise HTTPException(400, "submission already finalized")
|
|
cur.execute("""
|
|
UPDATE pgz_sport.form_submissions
|
|
SET klub_id=%s, clan_id=%s, data=%s::jsonb,
|
|
status=%s, reference_no=%s,
|
|
submitted_at = CASE WHEN %s='submitted' AND submitted_at IS NULL THEN now() ELSE submitted_at END,
|
|
updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.klub_id, req.clan_id, _json.dumps(req.data or {}),
|
|
req.status, req.reference_no, req.status, sid))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.put("/obrasci/submission/{sid}/status")
|
|
def set_submission_status(sid: int, req: SubmissionStatusIn, user=Depends(_require_user)):
|
|
"""Approve/reject (admin) or self-submit a draft (owner)."""
|
|
if req.status not in SUBMISSION_STATUSES:
|
|
raise HTTPException(400, "invalid status")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT user_id, status FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
|
|
if req.status in ('approved', 'rejected'):
|
|
if not _is_admin(user):
|
|
raise HTTPException(403, "admin only")
|
|
elif req.status == 'submitted':
|
|
if row["user_id"] != user["user_id"] and not _is_admin(user):
|
|
raise HTTPException(403, "not your submission")
|
|
|
|
if req.status == 'approved':
|
|
cur.execute("""
|
|
UPDATE pgz_sport.form_submissions
|
|
SET status='approved', approved_by=%s, approved_at=now(),
|
|
reviewed_by=%s, reviewed_at=now(), updated_at=now()
|
|
WHERE id=%s RETURNING *
|
|
""", (user["user_id"], user["user_id"], sid))
|
|
elif req.status == 'rejected':
|
|
cur.execute("""
|
|
UPDATE pgz_sport.form_submissions
|
|
SET status='rejected', rejected_reason=%s,
|
|
reviewed_by=%s, reviewed_at=now(), updated_at=now()
|
|
WHERE id=%s RETURNING *
|
|
""", (req.rejected_reason, user["user_id"], sid))
|
|
elif req.status == 'submitted':
|
|
cur.execute("""
|
|
UPDATE pgz_sport.form_submissions
|
|
SET status='submitted',
|
|
submitted_at = COALESCE(submitted_at, now()),
|
|
updated_at=now()
|
|
WHERE id=%s RETURNING *
|
|
""", (sid,))
|
|
else: # draft
|
|
cur.execute("""
|
|
UPDATE pgz_sport.form_submissions
|
|
SET status='draft', updated_at=now()
|
|
WHERE id=%s RETURNING *
|
|
""", (sid,))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/obrasci/submission/{sid}")
|
|
def delete_obrazac_submission(sid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT user_id, status FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
if not _is_admin(user) and row["user_id"] != user["user_id"]:
|
|
raise HTTPException(403, "not your submission")
|
|
cur.execute("DELETE FROM pgz_sport.form_submissions WHERE id=%s", (sid,))
|
|
cn.commit()
|
|
return {"ok": True, "id": sid}
|
|
|
|
|
|
@router.get("/obrasci/{tid}")
|
|
def get_obrazac_template(tid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT * FROM pgz_sport.form_templates WHERE id=%s", (tid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
# ─────────── E-MAIL TEMPLATES (CRM v2 GUI redesign — RUSH-4) ───────────
|
|
# Author: dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
|
# Table: pgz_sport.email_templates (code, naziv, kategorija, subject_tpl, body_tpl, variables jsonb, active)
|
|
|
|
class EmailTemplateIn(BaseModel):
|
|
code: str
|
|
naziv: str
|
|
kategorija: Optional[str] = None
|
|
subject_tpl: str
|
|
body_tpl: str
|
|
variables: Optional[Dict[str, Any]] = None
|
|
active: Optional[bool] = True
|
|
|
|
|
|
@router.get("/email-templates")
|
|
def list_email_templates(kategorija: Optional[str] = None,
|
|
active_only: bool = True,
|
|
user=Depends(_require_user)):
|
|
where, params = [], []
|
|
if kategorija:
|
|
where.append("kategorija = %s"); params.append(kategorija)
|
|
if active_only:
|
|
where.append("active = true")
|
|
sql = ("SELECT id, code, naziv, kategorija, subject_tpl, body_tpl, "
|
|
"variables, active, created_at, updated_at "
|
|
"FROM pgz_sport.email_templates")
|
|
if where:
|
|
sql += " WHERE " + " AND ".join(where)
|
|
sql += " ORDER BY kategorija NULLS LAST, naziv"
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute(sql, params)
|
|
items = _rows(cur.fetchall())
|
|
return {"items": items, "count": len(items)}
|
|
|
|
|
|
@router.get("/email-templates/{tid}")
|
|
def get_email_template(tid: int, user=Depends(_require_user)):
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("SELECT * FROM pgz_sport.email_templates WHERE id=%s", (tid,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "not found")
|
|
return _row(row)
|
|
|
|
|
|
@router.post("/email-templates")
|
|
def create_email_template(req: EmailTemplateIn, user=Depends(_require_user)):
|
|
if not _is_admin(user):
|
|
raise HTTPException(403, "admin only")
|
|
import json as _json
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
INSERT INTO pgz_sport.email_templates
|
|
(code, naziv, kategorija, subject_tpl, body_tpl, variables, active)
|
|
VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s)
|
|
RETURNING *
|
|
""", (req.code, req.naziv, req.kategorija, req.subject_tpl, req.body_tpl,
|
|
_json.dumps(req.variables or {}), bool(req.active)))
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.put("/email-templates/{tid}")
|
|
def update_email_template(tid: int, req: EmailTemplateIn, user=Depends(_require_user)):
|
|
if not _is_admin(user):
|
|
raise HTTPException(403, "admin only")
|
|
import json as _json
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("""
|
|
UPDATE pgz_sport.email_templates
|
|
SET code=%s, naziv=%s, kategorija=%s, subject_tpl=%s,
|
|
body_tpl=%s, variables=%s::jsonb, active=%s, updated_at=now()
|
|
WHERE id=%s
|
|
RETURNING *
|
|
""", (req.code, req.naziv, req.kategorija, req.subject_tpl, req.body_tpl,
|
|
_json.dumps(req.variables or {}), bool(req.active), tid))
|
|
if cur.rowcount == 0:
|
|
raise HTTPException(404, "not found")
|
|
cn.commit()
|
|
return _row(cur.fetchone())
|
|
|
|
|
|
@router.delete("/email-templates/{tid}")
|
|
def delete_email_template(tid: int, user=Depends(_require_user)):
|
|
if not _is_admin(user):
|
|
raise HTTPException(403, "admin only")
|
|
with _conn() as cn, cn.cursor() as cur:
|
|
cur.execute("DELETE FROM pgz_sport.email_templates WHERE id=%s", (tid,))
|
|
if cur.rowcount == 0:
|
|
raise HTTPException(404, "not found")
|
|
cn.commit()
|
|
return {"ok": True, "id": tid}
|