feat(settings): add GoMag API config, Oracle dropdowns, compact 2x2 layout

- Remove ID_GESTIUNE from config (unused)
- Add GoMag API settings (key, shop, days_back, limit) to SQLite — editable without restart
- sync_service reads GoMag settings from SQLite before download
- gomag_client.download_orders accepts api_key/api_shop/limit overrides
- New GET /api/settings/sectii and /api/settings/politici endpoints for Oracle dropdowns
  (nom_sectii.sectie, crm_politici_preturi.nume_lista_preturi)
- id_pol, id_sectie, transport_id_pol, discount_id_pol now use select dropdowns
- order_reader extracts discount_vat from GoMag JSON discounts[].vat
- import_service uses GoMag discount_vat as primary, settings as fallback
- settings.html redesigned to compact 2x2 grid (GoMag API | Import ROA / Transport | Discount)
- settings.js v2: loadDropdowns() sequential before loadSettings()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-16 16:39:59 +00:00
parent a0649279cf
commit 227dabd6d4
8 changed files with 268 additions and 97 deletions

View File

@@ -38,7 +38,6 @@ class Settings(BaseSettings):
# ROA Import Settings # ROA Import Settings
ID_POL: int = 0 ID_POL: int = 0
ID_GESTIUNE: int = 0
ID_SECTIE: int = 0 ID_SECTIE: int = 0
# GoMag API # GoMag API

View File

@@ -1,7 +1,10 @@
import asyncio import asyncio
import json import json
import logging
from datetime import datetime from datetime import datetime
logger = logging.getLogger(__name__)
from fastapi import APIRouter, Request, BackgroundTasks from fastapi import APIRouter, Request, BackgroundTasks
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
@@ -10,6 +13,7 @@ from pathlib import Path
from typing import Optional from typing import Optional
from ..services import sync_service, scheduler_service, sqlite_service, invoice_service from ..services import sync_service, scheduler_service, sqlite_service, invoice_service
from .. import database
router = APIRouter(tags=["sync"]) router = APIRouter(tags=["sync"])
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
@@ -29,6 +33,10 @@ class AppSettingsUpdate(BaseModel):
discount_id_pol: str = "" discount_id_pol: str = ""
id_pol: str = "" id_pol: str = ""
id_sectie: str = "" id_sectie: str = ""
gomag_api_key: str = ""
gomag_api_shop: str = ""
gomag_order_days_back: str = "7"
gomag_limit: str = "100"
# API endpoints # API endpoints
@@ -457,16 +465,21 @@ async def get_schedule():
@router.get("/api/settings") @router.get("/api/settings")
async def get_app_settings(): async def get_app_settings():
"""Get application settings.""" """Get application settings."""
settings = await sqlite_service.get_app_settings() from ..config import settings as config_settings
s = await sqlite_service.get_app_settings()
return { return {
"transport_codmat": settings.get("transport_codmat", ""), "transport_codmat": s.get("transport_codmat", ""),
"transport_vat": settings.get("transport_vat", "21"), "transport_vat": s.get("transport_vat", "21"),
"discount_codmat": settings.get("discount_codmat", ""), "discount_codmat": s.get("discount_codmat", ""),
"transport_id_pol": settings.get("transport_id_pol", ""), "transport_id_pol": s.get("transport_id_pol", ""),
"discount_vat": settings.get("discount_vat", "19"), "discount_vat": s.get("discount_vat", "19"),
"discount_id_pol": settings.get("discount_id_pol", ""), "discount_id_pol": s.get("discount_id_pol", ""),
"id_pol": settings.get("id_pol", ""), "id_pol": s.get("id_pol", ""),
"id_sectie": settings.get("id_sectie", ""), "id_sectie": s.get("id_sectie", ""),
"gomag_api_key": s.get("gomag_api_key", "") or config_settings.GOMAG_API_KEY,
"gomag_api_shop": s.get("gomag_api_shop", "") or config_settings.GOMAG_API_SHOP,
"gomag_order_days_back": s.get("gomag_order_days_back", "") or str(config_settings.GOMAG_ORDER_DAYS_BACK),
"gomag_limit": s.get("gomag_limit", "") or str(config_settings.GOMAG_LIMIT),
} }
@@ -481,4 +494,48 @@ async def update_app_settings(config: AppSettingsUpdate):
await sqlite_service.set_app_setting("discount_id_pol", config.discount_id_pol) await sqlite_service.set_app_setting("discount_id_pol", config.discount_id_pol)
await sqlite_service.set_app_setting("id_pol", config.id_pol) await sqlite_service.set_app_setting("id_pol", config.id_pol)
await sqlite_service.set_app_setting("id_sectie", config.id_sectie) await sqlite_service.set_app_setting("id_sectie", config.id_sectie)
await sqlite_service.set_app_setting("gomag_api_key", config.gomag_api_key)
await sqlite_service.set_app_setting("gomag_api_shop", config.gomag_api_shop)
await sqlite_service.set_app_setting("gomag_order_days_back", config.gomag_order_days_back)
await sqlite_service.set_app_setting("gomag_limit", config.gomag_limit)
return {"success": True} return {"success": True}
@router.get("/api/settings/sectii")
async def get_sectii():
"""Get list of sections from Oracle for dropdown."""
def _query():
conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT id_sectie, sectie FROM nom_sectii WHERE sters=0 AND inactiv=0 ORDER BY id_sectie"
)
return [{"id": str(row[0]), "label": f"{row[0]} - {row[1]}"} for row in cur]
finally:
database.pool.release(conn)
try:
return await asyncio.to_thread(_query)
except Exception as e:
logger.error(f"get_sectii error: {e}")
return []
@router.get("/api/settings/politici")
async def get_politici():
"""Get list of price policies from Oracle for dropdown."""
def _query():
conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT id_pol, nume_lista_preturi FROM crm_politici_preturi WHERE sters=0 ORDER BY id_pol"
)
return [{"id": str(row[0]), "label": f"{row[0]} - {row[1]}"} for row in cur]
finally:
database.pool.release(conn)
try:
return await asyncio.to_thread(_query)
except Exception as e:
logger.error(f"get_politici error: {e}")
return []

View File

@@ -16,19 +16,27 @@ logger = logging.getLogger(__name__)
async def download_orders( async def download_orders(
json_dir: str, json_dir: str,
days_back: int = None, days_back: int = None,
api_key: str = None,
api_shop: str = None,
limit: int = None,
log_fn: Callable[[str], None] = None, log_fn: Callable[[str], None] = None,
) -> dict: ) -> dict:
"""Download orders from GoMag API and save as JSON files. """Download orders from GoMag API and save as JSON files.
Returns dict with keys: pages, total, files (list of saved file paths). Returns dict with keys: pages, total, files (list of saved file paths).
If API keys are not configured, returns immediately with empty result. If API keys are not configured, returns immediately with empty result.
Optional api_key, api_shop, limit override config.settings values.
""" """
def _log(msg: str): def _log(msg: str):
logger.info(msg) logger.info(msg)
if log_fn: if log_fn:
log_fn(msg) log_fn(msg)
if not settings.GOMAG_API_KEY or not settings.GOMAG_API_SHOP: effective_key = api_key or settings.GOMAG_API_KEY
effective_shop = api_shop or settings.GOMAG_API_SHOP
effective_limit = limit or settings.GOMAG_LIMIT
if not effective_key or not effective_shop:
_log("GoMag API keys neconfigurați, skip download") _log("GoMag API keys neconfigurați, skip download")
return {"pages": 0, "total": 0, "files": []} return {"pages": 0, "total": 0, "files": []}
@@ -40,8 +48,8 @@ async def download_orders(
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
headers = { headers = {
"Apikey": settings.GOMAG_API_KEY, "Apikey": effective_key,
"ApiShop": settings.GOMAG_API_SHOP, "ApiShop": effective_shop,
"User-Agent": "Mozilla/5.0", "User-Agent": "Mozilla/5.0",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
@@ -57,7 +65,7 @@ async def download_orders(
params = { params = {
"startDate": start_date, "startDate": start_date,
"page": page, "page": page,
"limit": settings.GOMAG_LIMIT, "limit": effective_limit,
} }
try: try:
response = await client.get(settings.GOMAG_API_URL, headers=headers, params=params) response = await client.get(settings.GOMAG_API_URL, headers=headers, params=params)

View File

@@ -92,7 +92,8 @@ def build_articles_json(items, order=None, settings=None) -> str:
articles.append(article_dict) articles.append(article_dict)
# Discount total with quantity -1 (positive price) # Discount total with quantity -1 (positive price)
if order.discount_total > 0 and discount_codmat: if order.discount_total > 0 and discount_codmat:
discount_vat = settings.get("discount_vat", "19") # Use GoMag JSON discount VAT if available, fallback to settings
discount_vat = getattr(order, 'discount_vat', None) or settings.get("discount_vat", "19")
article_dict = { article_dict = {
"sku": discount_codmat, "sku": discount_codmat,
"quantity": "-1", "quantity": "-1",

View File

@@ -57,6 +57,7 @@ class OrderData:
total: float = 0.0 total: float = 0.0
delivery_cost: float = 0.0 delivery_cost: float = 0.0
discount_total: float = 0.0 discount_total: float = 0.0
discount_vat: Optional[str] = None
payment_name: str = "" payment_name: str = ""
delivery_name: str = "" delivery_name: str = ""
source_file: str = "" source_file: str = ""
@@ -160,11 +161,14 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
# Parse delivery cost # Parse delivery cost
delivery_cost = float(delivery.get("total", 0) or 0) if isinstance(delivery, dict) else 0.0 delivery_cost = float(delivery.get("total", 0) or 0) if isinstance(delivery, dict) else 0.0
# Parse discount total (sum of all discount values) # Parse discount total (sum of all discount values) and VAT from first discount item
discount_total = 0.0 discount_total = 0.0
discount_vat = None
for d in data.get("discounts", []): for d in data.get("discounts", []):
if isinstance(d, dict): if isinstance(d, dict):
discount_total += float(d.get("value", 0) or 0) discount_total += float(d.get("value", 0) or 0)
if discount_vat is None and d.get("vat") is not None:
discount_vat = str(d["vat"])
return OrderData( return OrderData(
id=str(data.get("id", order_id)), id=str(data.get("id", order_id)),
@@ -178,6 +182,7 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
total=float(data.get("total", 0) or 0), total=float(data.get("total", 0) or 0),
delivery_cost=delivery_cost, delivery_cost=delivery_cost,
discount_total=discount_total, discount_total=discount_total,
discount_vat=discount_vat,
payment_name=str(payment.get("name", "")) if isinstance(payment, dict) else "", payment_name=str(payment.get("name", "")) if isinstance(payment, dict) else "",
delivery_name=str(delivery.get("name", "")) if isinstance(delivery, dict) else "", delivery_name=str(delivery.get("name", "")) if isinstance(delivery, dict) else "",
source_file=source_file source_file=source_file

View File

@@ -129,8 +129,18 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
# Phase 0: Download orders from GoMag API # Phase 0: Download orders from GoMag API
_update_progress("downloading", "Descărcare comenzi din GoMag API...") _update_progress("downloading", "Descărcare comenzi din GoMag API...")
_log_line(run_id, "Descărcare comenzi din GoMag API...") _log_line(run_id, "Descărcare comenzi din GoMag API...")
# Read GoMag settings from SQLite (override config defaults)
dl_settings = await sqlite_service.get_app_settings()
gomag_key = dl_settings.get("gomag_api_key") or None
gomag_shop = dl_settings.get("gomag_api_shop") or None
gomag_days_str = dl_settings.get("gomag_order_days_back")
gomag_days = int(gomag_days_str) if gomag_days_str else None
gomag_limit_str = dl_settings.get("gomag_limit")
gomag_limit = int(gomag_limit_str) if gomag_limit_str else None
dl_result = await gomag_client.download_orders( dl_result = await gomag_client.download_orders(
json_dir, log_fn=lambda msg: _log_line(run_id, msg) json_dir, log_fn=lambda msg: _log_line(run_id, msg),
api_key=gomag_key, api_shop=gomag_shop,
days_back=gomag_days, limit=gomag_limit,
) )
if dl_result["files"]: if dl_result["files"]:
_log_line(run_id, f"GoMag: {dl_result['total']} comenzi în {dl_result['pages']} pagini → {len(dl_result['files'])} fișiere") _log_line(run_id, f"GoMag: {dl_result['total']} comenzi în {dl_result['pages']} pagini → {len(dl_result['files'])} fișiere")

View File

@@ -1,11 +1,57 @@
let settAcTimeout = null; let settAcTimeout = null;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
loadSettings(); await loadDropdowns();
await loadSettings();
wireAutocomplete('settTransportCodmat', 'settTransportAc'); wireAutocomplete('settTransportCodmat', 'settTransportAc');
wireAutocomplete('settDiscountCodmat', 'settDiscountAc'); wireAutocomplete('settDiscountCodmat', 'settDiscountAc');
}); });
async function loadDropdowns() {
try {
const [sectiiRes, politiciRes] = await Promise.all([
fetch('/api/settings/sectii'),
fetch('/api/settings/politici')
]);
const sectii = await sectiiRes.json();
const politici = await politiciRes.json();
const sectieEl = document.getElementById('settIdSectie');
if (sectieEl) {
sectieEl.innerHTML = '<option value="">— selectează secție —</option>';
sectii.forEach(s => {
sectieEl.innerHTML += `<option value="${escHtml(s.id)}">${escHtml(s.label)}</option>`;
});
}
const polEl = document.getElementById('settIdPol');
if (polEl) {
polEl.innerHTML = '<option value="">— selectează politică —</option>';
politici.forEach(p => {
polEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
});
}
const tPolEl = document.getElementById('settTransportIdPol');
if (tPolEl) {
tPolEl.innerHTML = '<option value="">— implicită —</option>';
politici.forEach(p => {
tPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
});
}
const dPolEl = document.getElementById('settDiscountIdPol');
if (dPolEl) {
dPolEl.innerHTML = '<option value="">— implicită —</option>';
politici.forEach(p => {
dPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
});
}
} catch (err) {
console.error('loadDropdowns error:', err);
}
}
async function loadSettings() { async function loadSettings() {
try { try {
const res = await fetch('/api/settings'); const res = await fetch('/api/settings');
@@ -19,6 +65,10 @@ async function loadSettings() {
if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || ''; if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || '';
if (el('settIdPol')) el('settIdPol').value = data.id_pol || ''; if (el('settIdPol')) el('settIdPol').value = data.id_pol || '';
if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || ''; if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || '';
if (el('settGomagApiKey')) el('settGomagApiKey').value = data.gomag_api_key || '';
if (el('settGomagApiShop')) el('settGomagApiShop').value = data.gomag_api_shop || '';
if (el('settGomagDaysBack')) el('settGomagDaysBack').value = data.gomag_order_days_back || '7';
if (el('settGomagLimit')) el('settGomagLimit').value = data.gomag_limit || '100';
} catch (err) { } catch (err) {
console.error('loadSettings error:', err); console.error('loadSettings error:', err);
} }
@@ -35,6 +85,10 @@ async function saveSettings() {
discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '', discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '',
id_pol: el('settIdPol')?.value?.trim() || '', id_pol: el('settIdPol')?.value?.trim() || '',
id_sectie: el('settIdSectie')?.value?.trim() || '', id_sectie: el('settIdSectie')?.value?.trim() || '',
gomag_api_key: el('settGomagApiKey')?.value?.trim() || '',
gomag_api_shop: el('settGomagApiShop')?.value?.trim() || '',
gomag_order_days_back: el('settGomagDaysBack')?.value?.trim() || '7',
gomag_limit: el('settGomagLimit')?.value?.trim() || '100',
}; };
try { try {
const res = await fetch('/api/settings', { const res = await fetch('/api/settings', {

View File

@@ -3,42 +3,74 @@
{% block nav_settings %}active{% endblock %} {% block nav_settings %}active{% endblock %}
{% block content %} {% block content %}
<h4 class="mb-4">Setari</h4> <h4 class="mb-3">Setari</h4>
<!-- Import ROA Card --> <div class="row g-3 mb-3">
<div class="card mb-4"> <!-- GoMag API card -->
<div class="card-header">Import ROA</div> <div class="col-md-6">
<div class="card-body"> <div class="card h-100">
<div class="row g-3"> <div class="card-header py-2 px-3 fw-semibold">GoMag API</div>
<div class="col-md-3"> <div class="card-body py-2 px-3">
<label class="form-label form-label-sm mb-1">ID Politica de Pret (ID_POL)</label> <div class="mb-2">
<input type="number" class="form-control form-control-sm" id="settIdPol" placeholder="ex: 1"> <label class="form-label mb-0 small">API Key</label>
<small class="text-muted">Lasa gol pentru valoarea implicita din config</small> <input type="text" class="form-control form-control-sm" id="settGomagApiKey" placeholder="4c5e46...">
</div>
<div class="mb-2">
<label class="form-label mb-0 small">Shop URL</label>
<input type="text" class="form-control form-control-sm" id="settGomagApiShop" placeholder="https://coffeepoint.ro">
</div>
<div class="row g-2">
<div class="col-6">
<label class="form-label mb-0 small">Zile înapoi</label>
<input type="number" class="form-control form-control-sm" id="settGomagDaysBack" value="7" min="1">
</div>
<div class="col-6">
<label class="form-label mb-0 small">Limită/pagină</label>
<input type="number" class="form-control form-control-sm" id="settGomagLimit" value="100" min="1">
</div> </div>
<div class="col-md-3">
<label class="form-label form-label-sm mb-1">ID Sectie (ID_SECTIE)</label>
<input type="number" class="form-control form-control-sm" id="settIdSectie" placeholder="ex: 1">
<small class="text-muted">Lasa gol pentru valoarea implicita din config</small>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Transport Card --> <!-- Import ROA card -->
<div class="card mb-4"> <div class="col-md-6">
<div class="card-header">Transport</div> <div class="card h-100">
<div class="card-body"> <div class="card-header py-2 px-3 fw-semibold">Import ROA</div>
<div class="row g-3"> <div class="card-body py-2 px-3">
<div class="col-md-4"> <div class="mb-2">
<label class="form-label form-label-sm mb-1">CODMAT Transport</label> <label class="form-label mb-0 small">Secție (ID_SECTIE)</label>
<select class="form-select form-select-sm" id="settIdSectie">
<option value="">— selectează secție —</option>
</select>
</div>
<div class="mb-2">
<label class="form-label mb-0 small">Politică de Preț (ID_POL)</label>
<select class="form-select form-select-sm" id="settIdPol">
<option value="">— selectează politică —</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-3">
<!-- Transport card -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header py-2 px-3 fw-semibold">Transport</div>
<div class="card-body py-2 px-3">
<div class="mb-2">
<label class="form-label mb-0 small">CODMAT Transport</label>
<div class="position-relative"> <div class="position-relative">
<input type="text" class="form-control form-control-sm" id="settTransportCodmat" placeholder="ex: TRANSPORT" autocomplete="off"> <input type="text" class="form-control form-control-sm" id="settTransportCodmat" placeholder="ex: TRANSPORT" autocomplete="off">
<div class="autocomplete-dropdown d-none" id="settTransportAc"></div> <div class="autocomplete-dropdown d-none" id="settTransportAc"></div>
</div> </div>
<small class="text-muted">Lasa gol pentru a nu adauga transport la import</small>
</div> </div>
<div class="col-md-2"> <div class="row g-2">
<label class="form-label form-label-sm mb-1">TVA Transport (%)</label> <div class="col-6">
<label class="form-label mb-0 small">TVA Transport (%)</label>
<select class="form-select form-select-sm" id="settTransportVat"> <select class="form-select form-select-sm" id="settTransportVat">
<option value="5">5%</option> <option value="5">5%</option>
<option value="9">9%</option> <option value="9">9%</option>
@@ -46,30 +78,32 @@
<option value="21" selected>21%</option> <option value="21" selected>21%</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-6">
<label class="form-label form-label-sm mb-1">ID Pol Transport (optional)</label> <label class="form-label mb-0 small">Politică Transport</label>
<input type="number" class="form-control form-control-sm" id="settTransportIdPol" placeholder="ex: 2"> <select class="form-select form-select-sm" id="settTransportIdPol">
<small class="text-muted">Politica de pret specifica pentru transport</small> <option value="">— implicită —</option>
</select>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Discount Card --> <!-- Discount card -->
<div class="card mb-4"> <div class="col-md-6">
<div class="card-header">Discount</div> <div class="card h-100">
<div class="card-body"> <div class="card-header py-2 px-3 fw-semibold">Discount</div>
<div class="row g-3"> <div class="card-body py-2 px-3">
<div class="col-md-4"> <div class="mb-2">
<label class="form-label form-label-sm mb-1">CODMAT Discount</label> <label class="form-label mb-0 small">CODMAT Discount</label>
<div class="position-relative"> <div class="position-relative">
<input type="text" class="form-control form-control-sm" id="settDiscountCodmat" placeholder="ex: DISCOUNT" autocomplete="off"> <input type="text" class="form-control form-control-sm" id="settDiscountCodmat" placeholder="ex: DISCOUNT" autocomplete="off">
<div class="autocomplete-dropdown d-none" id="settDiscountAc"></div> <div class="autocomplete-dropdown d-none" id="settDiscountAc"></div>
</div> </div>
<small class="text-muted">Lasa gol pentru a nu adauga discount la import</small>
</div> </div>
<div class="col-md-2"> <div class="row g-2">
<label class="form-label form-label-sm mb-1">TVA Discount (%)</label> <div class="col-6">
<label class="form-label mb-0 small">TVA Discount (fallback %)</label>
<select class="form-select form-select-sm" id="settDiscountVat"> <select class="form-select form-select-sm" id="settDiscountVat">
<option value="5">5%</option> <option value="5">5%</option>
<option value="9">9%</option> <option value="9">9%</option>
@@ -77,22 +111,25 @@
<option value="21">21%</option> <option value="21">21%</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-6">
<label class="form-label form-label-sm mb-1">ID Pol Discount (optional)</label> <label class="form-label mb-0 small">Politică Discount</label>
<input type="number" class="form-control form-control-sm" id="settDiscountIdPol" placeholder="ex: 2"> <select class="form-select form-select-sm" id="settDiscountIdPol">
<small class="text-muted">Politica de pret specifica pentru discount</small> <option value="">— implicită —</option>
</select>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-3">
<button class="btn btn-primary" onclick="saveSettings()">Salveaza Setarile</button> <button class="btn btn-primary btn-sm" onclick="saveSettings()">Salvează Setările</button>
<span id="settSaveResult" class="ms-2"></span> <span id="settSaveResult" class="ms-2 small"></span>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=1"></script> <script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=2"></script>
{% endblock %} {% endblock %}