M3: chat widget, owner dashboard, super-admin panel
- Embeddable vanilla-JS chat widget (one script tag + tenant public key):
WebSocket, resumable sessions (24h localStorage token), honeypot + per-IP
and per-message rate limits, Bosnian UI (§7)
- Session auth (bcrypt + signed cookies), owner accounts + super-admins,
admin impersonation ('otvori kao salon', §11)
- Owner dashboard (§10, Bosnian): requests with same actions as email,
call/chat history with transcripts, usage bar; settings for profile,
working hours (manual + LLM paste-to-parse), services CRUD + 'Zalijepi
cjenovnik' LLM import with review step, scheduling (Google OAuth free/busy
read-only + calendar mappings + buffers, partner status), telephony
(forwarding MMI codes per operator, ring group, worker SIP creds + QR),
agent voice/price-mode/notes + widget snippet, notifications/SMS templates
- Super-admin panel (§11): tenant CRUD incl. plan/minutes/paid-until,
partner API config (encrypted secrets), number/SIM registry + assignment,
connectivity test (live availability + dry-run push), versioned prompt
template editor with publish/rollback, playground, usage overview, health
- Fixes: WAL + busy_timeout for sqlite tests, no awaits in WS disconnect
path (deadlock), template publish autoflush bug
- 76 tests green (incl. widget WS e2e booking flow); dashboard, widget and
admin verified live in a browser against Postgres
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
12
gogo/templates/admin/_nav.html
Normal file
12
gogo/templates/admin/_nav.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<header>
|
||||
<div class="logo">Gogo <span>Admin</span></div>
|
||||
<nav>
|
||||
<a href="/admin" class="{{ 'active' if active=='tenants' }}">Saloni</a>
|
||||
<a href="/admin/numbers" class="{{ 'active' if active=='numbers' }}">Brojevi/SIM</a>
|
||||
<a href="/admin/templates" class="{{ 'active' if active=='templates' }}">Prompt šablon</a>
|
||||
<a href="/admin/playground" class="{{ 'active' if active=='playground' }}">Playground</a>
|
||||
<a href="/admin/usage" class="{{ 'active' if active=='usage' }}">Potrošnja</a>
|
||||
<a href="/admin/health" class="{{ 'active' if active=='health' }}">Sistem</a>
|
||||
</nav>
|
||||
<div class="right">{{ admin.email }} · <a href="/admin/logout">Odjava</a></div>
|
||||
</header>
|
||||
37
gogo/templates/admin/health.html
Normal file
37
gogo/templates/admin/health.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Sistem — Gogo Admin{% endblock %}
|
||||
{% block nav %}{% include "admin/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Zdravlje sistema (§11)</h1>
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<h2>Servisi</h2>
|
||||
<table>
|
||||
<tr><td>Baza podataka</td><td><span class="badge {{ 'confirmed' if db_ok else 'rejected' }}">{{ 'OK' if db_ok else 'GREŠKA' }}</span></td></tr>
|
||||
<tr><td>LLM API ključ</td><td><span class="badge {{ 'confirmed' if llm_configured else 'pending' }}">{{ 'postavljen' if llm_configured else 'nije postavljen' }}</span></td></tr>
|
||||
<tr><td>Email (SMTP)</td><td><span class="badge {{ 'confirmed' if email_mode=='smtp' else 'pending' }}">{{ email_mode }}</span></td></tr>
|
||||
<tr><td>SMS provajder</td><td><span class="badge {{ 'confirmed' if sms_mode=='gsm_gateway' else 'pending' }}">{{ sms_mode }}</span></td></tr>
|
||||
<tr><td>Voice pipeline</td><td><span class="badge {{ voice_badge }}">{{ voice_status }}</span></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Greške u slanju (zadnjih 7 dana)</h2>
|
||||
<table>
|
||||
<tr><td>Email greške</td><td>{{ email_errors }}</td></tr>
|
||||
<tr><td>SMS greške</td><td>{{ sms_errors }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% if failed_rows %}
|
||||
<div class="card">
|
||||
<h2>Zadnje greške</h2>
|
||||
<table>
|
||||
<tr><th>Vrijeme</th><th>Tip</th><th>Prima</th><th>Greška</th></tr>
|
||||
{% for f in failed_rows %}
|
||||
<tr><td class="muted">{{ f.at.strftime('%d.%m. %H:%M') }}</td><td>{{ f.kind }}</td>
|
||||
<td>{{ f.to }}</td><td class="muted">{{ f.error[:120] }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
16
gogo/templates/admin/login.html
Normal file
16
gogo/templates/admin/login.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin prijava — Gogo Telefon{% endblock %}
|
||||
{% block content %}
|
||||
<div style="max-width:380px;margin:60px auto">
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0">Gogo <span style="color:var(--brand)">Admin</span></h1>
|
||||
<form method="post" action="/admin/login">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email" required autofocus>
|
||||
<label>Lozinka</label>
|
||||
<input type="password" name="password" required>
|
||||
<div style="margin-top:16px"><button style="width:100%">Prijavi se</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
48
gogo/templates/admin/numbers.html
Normal file
48
gogo/templates/admin/numbers.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Brojevi/SIM — Gogo Admin{% endblock %}
|
||||
{% block nav %}{% include "admin/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Brojevi i SIM kartice (GSM gateway)</h1>
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>Broj</th><th>Gateway port</th><th>Operater</th><th>SIM status</th><th>Salon</th><th></th></tr>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<form method="post" action="/admin/numbers/{{ row.n.id }}">
|
||||
<td><code>{{ row.n.msisdn }}</code></td>
|
||||
<td style="width:110px"><input type="number" name="gateway_port" value="{{ row.n.gateway_port if row.n.gateway_port is not none }}"></td>
|
||||
<td style="width:140px">
|
||||
<select name="operator">
|
||||
<option value="">?</option>
|
||||
{% for op, label in operators.items() %}
|
||||
<option value="{{ op }}" {{ 'selected' if row.n.operator==op }}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td style="width:140px">
|
||||
<select name="sim_status">
|
||||
{% for s in ['unassigned','active','blocked','testing'] %}
|
||||
<option value="{{ s }}" {{ 'selected' if row.n.sim_status==s }}>{{ s }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>{{ row.tenant_name or '—' }}</td>
|
||||
<td style="white-space:nowrap"><button class="small">Sačuvaj</button></form>
|
||||
<form method="post" action="/admin/numbers/{{ row.n.id }}/delete" style="display:inline"
|
||||
onsubmit="return confirm('Obrisati broj?')"><button class="small red">Obriši</button></form></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<form method="post" action="/admin/numbers">
|
||||
<td><input type="text" name="msisdn" placeholder="+38765XXXXXX" required></td>
|
||||
<td><input type="number" name="gateway_port" placeholder="1"></td>
|
||||
<td><select name="operator"><option value="">?</option>
|
||||
{% for op, label in operators.items() %}<option value="{{ op }}">{{ label }}</option>{% endfor %}
|
||||
</select></td>
|
||||
<td></td><td></td>
|
||||
<td><button class="small green">Dodaj</button></td>
|
||||
</form>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
51
gogo/templates/admin/playground.html
Normal file
51
gogo/templates/admin/playground.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Playground — Gogo Admin{% endblock %}
|
||||
{% block nav %}{% include "admin/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Test playground (§11)</h1>
|
||||
<p class="muted">Tekstualni razgovor s asistentom pojedinog salona — koristi njegov sastavljeni
|
||||
prompt, usluge i provajdera termina. Zahtjevi kreirani ovdje se <b>zaista</b> šalju (koristite
|
||||
demo salon za igranje).</p>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="/admin/playground">
|
||||
<label>Salon</label>
|
||||
<select name="tenant_id" {{ 'disabled' if history_json != '[]' }}>
|
||||
{% for tn in tenants %}
|
||||
<option value="{{ tn.id }}" {{ 'selected' if selected_tenant and tn.id==selected_tenant.id }}>{{ tn.name }} ({{ tn.scheduling_provider }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if history_json != '[]' %}<input type="hidden" name="tenant_id" value="{{ selected_tenant.id }}">{% endif %}
|
||||
<label>Kanal</label>
|
||||
<select name="channel">
|
||||
<option value="voice" {{ 'selected' if channel=='voice' }}>voice (telefon)</option>
|
||||
<option value="chat" {{ 'selected' if channel=='chat' }}>chat</option>
|
||||
</select>
|
||||
|
||||
{% if transcript %}
|
||||
<div class="transcript" style="margin:16px 0">
|
||||
{% for t in transcript %}
|
||||
<div class="t {{ t.role }}">{{ t.text }}</div>
|
||||
{% if t.tool_calls %}<div class="tools">⚙ {% for tc in t.tool_calls %}{{ tc.name }}({{ tc.input | tojson }}) {% endfor %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="history" value="{{ history_json | e }}">
|
||||
<label>Poruka klijenta</label>
|
||||
<input type="text" name="message" placeholder="npr. Htjela bih zakazati šišanje u srijedu" autofocus>
|
||||
<div style="margin-top:12px">
|
||||
<button>Pošalji</button>
|
||||
<a class="btn ghost" href="/admin/playground">Novi razgovor</a>
|
||||
</div>
|
||||
</form>
|
||||
{% if error %}<div class="msg error" style="margin-top:12px">{{ error }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
{% if system_prompt %}
|
||||
<details class="card">
|
||||
<summary style="cursor:pointer;font-weight:600">Sastavljeni system prompt</summary>
|
||||
<pre style="white-space:pre-wrap">{{ system_prompt }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
46
gogo/templates/admin/templates.html
Normal file
46
gogo/templates/admin/templates.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Prompt šablon — Gogo Admin{% endblock %}
|
||||
{% block nav %}{% include "admin/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Globalni prompt šablon (§6.3)</h1>
|
||||
<p class="muted">Šablon vrijedi za sve salone odjednom. Dostupne oznake:
|
||||
<code>{salon_profile}</code> <code>{working_hours}</code> <code>{services_table}</code>
|
||||
<code>{notes}</code> <code>{price_mode}</code> <code>{greeting}</code>
|
||||
<code>{current_datetime}</code></p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Nova verzija</h2>
|
||||
<form method="post" action="/admin/templates">
|
||||
<textarea name="body" style="min-height:340px;font-family:monospace">{{ draft_body }}</textarea>
|
||||
<label>Bilješka o izmjeni</label>
|
||||
<input type="text" name="notes" placeholder="npr. dodano pravilo o kućnim posjetama">
|
||||
<label><input type="checkbox" name="publish" checked style="width:auto"> Objavi odmah (postaje aktivna verzija)</label>
|
||||
<button style="margin-top:12px">Sačuvaj verziju</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Verzije</h2>
|
||||
<table>
|
||||
<tr><th>Verzija</th><th>Datum</th><th>Bilješka</th><th>Status</th><th></th></tr>
|
||||
{% for v in versions %}
|
||||
<tr>
|
||||
<td>v{{ v.version }}</td>
|
||||
<td class="muted">{{ v.created_at.strftime('%d.%m.%Y. %H:%M') }}</td>
|
||||
<td>{{ v.notes }}</td>
|
||||
<td>{% if v.published %}<span class="badge confirmed">aktivna</span>{% endif %}</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a class="btn small ghost" href="/admin/templates?load={{ v.version }}">Učitaj u editor</a>
|
||||
{% if not v.published %}
|
||||
<form method="post" action="/admin/templates/{{ v.version }}/publish" style="display:inline">
|
||||
<button class="small green">Objavi (rollback)</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="muted">Nema sačuvanih verzija — koristi se ugrađeni zadani šablon.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
109
gogo/templates/admin/tenant_edit.html
Normal file
109
gogo/templates/admin/tenant_edit.html
Normal file
@@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ t.name if t else 'Novi salon' }} — Gogo Admin{% endblock %}
|
||||
{% block nav %}{% include "admin/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<p><a href="/admin">← Svi saloni</a></p>
|
||||
<h1>{{ t.name if t else 'Novi salon' }}</h1>
|
||||
|
||||
<form method="post" action="{{ '/admin/tenants/' + (t.id|string) if t else '/admin/tenants/new' }}">
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<h2>Osnovno</h2>
|
||||
<label>Naziv</label><input type="text" name="name" value="{{ t.name if t }}" required>
|
||||
<label>Slug <small>(kratki id, bez razmaka)</small></label>
|
||||
<input type="text" name="slug" value="{{ t.slug if t }}" {{ 'readonly' if t }} required>
|
||||
<label>Grad</label><input type="text" name="city" value="{{ t.city if t }}">
|
||||
<label>Status</label>
|
||||
<select name="status">
|
||||
<option value="active" {{ 'selected' if t and t.status=='active' }}>aktivan</option>
|
||||
<option value="disabled" {{ 'selected' if t and t.status=='disabled' }}>isključen</option>
|
||||
</select>
|
||||
{% if not t %}
|
||||
<label>Email vlasnika <small>(kreira se nalog)</small></label>
|
||||
<input type="email" name="owner_email" required>
|
||||
<label>Lozinka vlasnika</label>
|
||||
<input type="text" name="owner_password" required>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Plan i naplata (§12)</h2>
|
||||
<label>Plan</label>
|
||||
<input type="text" name="plan" value="{{ t.plan if t else 'gogo_start' }}">
|
||||
<label>Uključene minute mjesečno</label>
|
||||
<input type="number" name="included_minutes" value="{{ t.included_minutes if t else 300 }}">
|
||||
<label>Plaćeno do <small>(YYYY-MM-DD, prazno = nije plaćeno)</small></label>
|
||||
<input type="text" name="paid_until" value="{{ t.paid_until.strftime('%Y-%m-%d') if t and t.paid_until }}">
|
||||
<label><input type="checkbox" name="hard_cutoff" {{ 'checked' if t and t.hard_cutoff }} style="width:auto">
|
||||
Tvrdi prekid na 100% minuta (zadano: isključeno — asistent uvijek odgovara)</label>
|
||||
<label>LLM model <small>(prazno = globalno zadani)</small></label>
|
||||
<input type="text" name="llm_model" value="{{ t.llm_model if t }}">
|
||||
<label>TTL prijedloga (sati)</label>
|
||||
<input type="number" name="proposal_ttl_hours" value="{{ t.proposal_ttl_hours if t else 24 }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Provajder termina (§8)</h2>
|
||||
<label>Tip</label>
|
||||
<select name="scheduling_provider">
|
||||
<option value="google_calendar" {{ 'selected' if t and t.scheduling_provider=='google_calendar' }}>google_calendar</option>
|
||||
<option value="partner_api" {{ 'selected' if t and t.scheduling_provider=='partner_api' }}>partner_api</option>
|
||||
<option value="mock" {{ 'selected' if t and t.scheduling_provider=='mock' }}>mock (demo/test)</option>
|
||||
</select>
|
||||
<div class="grid2">
|
||||
<div>
|
||||
<label>Partner: base URL</label>
|
||||
<input type="url" name="partner_base_url" value="{{ pconfig.get('base_url','') }}" placeholder="https://booking.salon.ba/gogo/v1">
|
||||
<label>Partner: API ključ <small>(prazno = ne mijenjaj)</small></label>
|
||||
<input type="text" name="partner_api_key" placeholder="{{ '•••• postavljen' if pconfig.get('api_key_encrypted') else '' }}">
|
||||
<label>Partner: webhook secret <small>(prazno = ne mijenjaj)</small></label>
|
||||
<input type="text" name="partner_webhook_secret" placeholder="{{ '•••• postavljen' if pconfig.get('webhook_secret_encrypted') else '' }}">
|
||||
</div>
|
||||
<div>
|
||||
<label><input type="checkbox" name="catalog_sync" {{ 'checked' if pconfig.get('catalog_sync') }} style="width:auto"> Sinhronizuj usluge iz partnera (GET /services)</label>
|
||||
<label><input type="checkbox" name="email_to_owner" {{ 'checked' if pconfig.get('email_to_owner') }} style="width:auto"> Šalji i email vlasniku (uz push u partnera)</label>
|
||||
<label><input type="checkbox" name="polling_fallback" {{ 'checked' if pconfig.get('polling_fallback') }} style="width:auto"> Polling fallback (partner nema webhook)</label>
|
||||
{% if t %}
|
||||
<label>Webhook URL za partnera</label>
|
||||
<code style="display:block;padding:8px">{{ base_url }}/webhooks/partner/{{ t.id }}</code>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Broj / SIM</h2>
|
||||
<label>Dodijeljeni broj</label>
|
||||
<select name="phone_number_id">
|
||||
<option value="">— bez broja —</option>
|
||||
{% for n in numbers %}
|
||||
<option value="{{ n.id }}" {{ 'selected' if n.tenant_id == (t.id if t) }}>
|
||||
{{ n.msisdn }} (port {{ n.gateway_port if n.gateway_port is not none else '?' }}, {{ n.operator or '?' }})
|
||||
{%- if n.tenant_id and (not t or n.tenant_id != t.id) %} — ZAUZET{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="muted">Nove brojeve dodajete pod <a href="/admin/numbers">Brojevi/SIM</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Prompt override <small style="font-weight:400">(samo za neobične salone — inače prazno)</small></h2>
|
||||
<textarea name="prompt_override" style="min-height:120px;font-family:monospace"
|
||||
placeholder="Prazno = koristi globalni šablon">{{ override_body }}</textarea>
|
||||
</div>
|
||||
|
||||
<button>Sačuvaj</button>
|
||||
</form>
|
||||
|
||||
{% if t and t.scheduling_provider == 'partner_api' %}
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h2>Test veze s partnerom (§8.3)</h2>
|
||||
<p class="muted">Pokreće živi <code>GET /availability</code> i dry-run
|
||||
<code>POST /booking-requests</code>, prikazuje sirove odgovore.</p>
|
||||
<form method="post" action="/admin/tenants/{{ t.id }}/test-connection">
|
||||
<button class="ghost">Pokreni test</button>
|
||||
</form>
|
||||
{% if test_result %}<pre>{{ test_result }}</pre>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
27
gogo/templates/admin/tenants.html
Normal file
27
gogo/templates/admin/tenants.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Saloni — Gogo Admin{% endblock %}
|
||||
{% block nav %}{% include "admin/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Saloni <a class="btn small" style="margin-left:12px" href="/admin/tenants/new">+ Novi salon</a></h1>
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>Salon</th><th>Provajder</th><th>Broj</th><th>Plaćeno do</th><th>Minute (mj)</th><th>Status</th><th></th></tr>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td><b>{{ row.tenant.name }}</b><br><span class="muted">{{ row.tenant.city }} · {{ row.tenant.slug }}</span></td>
|
||||
<td>{{ row.tenant.scheduling_provider }}</td>
|
||||
<td>{{ row.msisdn or '—' }}</td>
|
||||
<td>{{ row.tenant.paid_until.strftime('%d.%m.%Y.') if row.tenant.paid_until else '—' }}</td>
|
||||
<td>{{ row.used_minutes }}/{{ row.tenant.included_minutes }}</td>
|
||||
<td><span class="badge {{ 'confirmed' if row.tenant.status=='active' else 'rejected' }}">{{ row.tenant.status }}</span></td>
|
||||
<td style="white-space:nowrap">
|
||||
<a class="btn small ghost" href="/admin/tenants/{{ row.tenant.id }}">Uredi</a>
|
||||
<form method="post" action="/admin/impersonate/{{ row.tenant.id }}" style="display:inline">
|
||||
<button class="small">Otvori kao salon</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
gogo/templates/admin/usage.html
Normal file
26
gogo/templates/admin/usage.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Potrošnja — Gogo Admin{% endblock %}
|
||||
{% block nav %}{% include "admin/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Potrošnja po salonima — {{ month }}</h1>
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>Salon</th><th>Minute asistenta</th><th>Pozivi</th><th>SMS</th><th>Chat</th>
|
||||
<th>Zahtjevi (mj)</th><th>Procj. trošak (LLM+TTS)</th></tr>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td><b>{{ r.tenant.name }}</b></td>
|
||||
<td>{{ r.used_minutes }}/{{ r.tenant.included_minutes }}
|
||||
{% if r.over_100 %}<span class="badge rejected">100%+</span>
|
||||
{% elif r.over_80 %}<span class="badge pending">80%+</span>{% endif %}</td>
|
||||
<td>{{ r.calls }}</td>
|
||||
<td>{{ r.sms }}</td>
|
||||
<td>{{ r.chats }}</td>
|
||||
<td>{{ r.requests }}</td>
|
||||
<td>~{{ r.est_cost_km }} KM</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p class="muted">Procjena troška: {{ cost_note }}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
73
gogo/templates/base.html
Normal file
73
gogo/templates/base.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<!doctype html>
|
||||
<html lang="bs">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Gogo Telefon{% endblock %}</title>
|
||||
<style>
|
||||
:root{--brand:#7c3aed;--brand-dark:#6d28d9;--ok:#16a34a;--warn:#d97706;--err:#dc2626;
|
||||
--bg:#f4f4f5;--card:#fff;--line:#e4e4e7;--muted:#71717a;--text:#18181b}
|
||||
*{box-sizing:border-box}
|
||||
body{font-family:system-ui,sans-serif;margin:0;background:var(--bg);color:var(--text)}
|
||||
a{color:var(--brand)}
|
||||
header{background:#1e1b2e;color:#fff;padding:0 20px;display:flex;align-items:center;gap:24px;flex-wrap:wrap}
|
||||
header .logo{font-weight:800;font-size:17px;padding:14px 0}
|
||||
header .logo span{color:#a78bfa}
|
||||
header nav{display:flex;gap:2px;flex-wrap:wrap}
|
||||
header nav a{color:#c4b5fd;text-decoration:none;padding:16px 12px;font-size:14px}
|
||||
header nav a.active,header nav a:hover{color:#fff;border-bottom:3px solid var(--brand)}
|
||||
header .right{margin-left:auto;font-size:13px;color:#a1a1aa;padding:14px 0}
|
||||
header .right a{color:#a1a1aa}
|
||||
main{max-width:1080px;margin:24px auto;padding:0 16px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px 24px;margin-bottom:20px}
|
||||
.card h2{margin-top:0;font-size:17px}
|
||||
h1{font-size:22px}
|
||||
table{width:100%;border-collapse:collapse;font-size:14px}
|
||||
th{color:var(--muted);text-align:left;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.03em}
|
||||
th,td{padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
tr:last-child td{border-bottom:0}
|
||||
input[type=text],input[type=email],input[type=password],input[type=number],input[type=time],input[type=url],select,textarea{
|
||||
width:100%;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font:inherit;background:#fff}
|
||||
textarea{min-height:90px}
|
||||
label{display:block;font-size:13px;font-weight:600;margin:12px 0 4px}
|
||||
label small{font-weight:400;color:var(--muted)}
|
||||
button,.btn{background:var(--brand);color:#fff;border:0;border-radius:8px;padding:9px 16px;
|
||||
font:inherit;font-weight:600;cursor:pointer;text-decoration:none;display:inline-block;font-size:14px}
|
||||
button:hover,.btn:hover{background:var(--brand-dark)}
|
||||
button.green,.btn.green{background:var(--ok)} button.red,.btn.red{background:var(--err)}
|
||||
button.ghost,.btn.ghost{background:transparent;color:var(--brand);border:1px solid var(--brand)}
|
||||
button.small,.btn.small{padding:5px 10px;font-size:13px;font-weight:500}
|
||||
.badge{display:inline-block;border-radius:20px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
.badge.pending{background:#fef3c7;color:#92400e}
|
||||
.badge.resolved_by_owner,.badge.confirmed,.badge.human_answered,.badge.request_created{background:#dcfce7;color:#166534}
|
||||
.badge.rejected,.badge.expired,.badge.abandoned{background:#fee2e2;color:#991b1b}
|
||||
.badge.info_only,.badge.message_taken{background:#e0e7ff;color:#3730a3}
|
||||
.msg{background:#ecfdf5;border:1px solid #a7f3d0;color:#065f46;border-radius:8px;padding:10px 14px;margin-bottom:16px}
|
||||
.msg.error{background:#fef2f2;border-color:#fecaca;color:#991b1b}
|
||||
.banner{background:#fffbeb;border:1px solid #fde68a;color:#92400e;border-radius:8px;padding:10px 14px;margin-bottom:16px}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
|
||||
@media(max-width:720px){.grid2,.grid3{grid-template-columns:1fr}}
|
||||
.muted{color:var(--muted);font-size:13px}
|
||||
.bar{background:var(--line);border-radius:10px;height:14px;overflow:hidden}
|
||||
.bar>div{height:100%;background:var(--brand)}
|
||||
.bar>div.warn{background:var(--warn)} .bar>div.over{background:var(--err)}
|
||||
code,pre{background:#f4f4f5;border:1px solid var(--line);border-radius:6px;font-size:13px}
|
||||
pre{padding:12px;overflow-x:auto}
|
||||
code{padding:1px 6px}
|
||||
.transcript{display:flex;flex-direction:column;gap:8px}
|
||||
.transcript .t{max-width:75%;padding:8px 12px;border-radius:12px;font-size:14px;white-space:pre-wrap}
|
||||
.transcript .t.assistant{background:#ede9fe;align-self:flex-start}
|
||||
.transcript .t.user{background:#e4e4e7;align-self:flex-end}
|
||||
.transcript .tools{font-size:12px;color:var(--muted);align-self:flex-start}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block nav %}{% endblock %}
|
||||
<main>
|
||||
{% if request.query_params.get('msg') %}<div class="msg">{{ request.query_params.get('msg') }}</div>{% endif %}
|
||||
{% if request.query_params.get('err') %}<div class="msg error">{{ request.query_params.get('err') }}</div>{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
19
gogo/templates/dashboard/_nav.html
Normal file
19
gogo/templates/dashboard/_nav.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<header>
|
||||
<div class="logo">Gogo <span>Telefon</span></div>
|
||||
<nav>
|
||||
<a href="/dash" class="{{ 'active' if active=='requests' }}">Zahtjevi</a>
|
||||
<a href="/dash/calls" class="{{ 'active' if active=='calls' }}">Pozivi</a>
|
||||
<a href="/dash/chats" class="{{ 'active' if active=='chats' }}">Chat</a>
|
||||
<a href="/dash/usage" class="{{ 'active' if active=='usage' }}">Potrošnja</a>
|
||||
<a href="/dash/settings" class="{{ 'active' if active=='settings' }}">Salon</a>
|
||||
<a href="/dash/services" class="{{ 'active' if active=='services' }}">Usluge</a>
|
||||
<a href="/dash/scheduling" class="{{ 'active' if active=='scheduling' }}">Kalendar</a>
|
||||
<a href="/dash/telephony" class="{{ 'active' if active=='telephony' }}">Telefonija</a>
|
||||
<a href="/dash/agent" class="{{ 'active' if active=='agent' }}">Asistent</a>
|
||||
<a href="/dash/notifications" class="{{ 'active' if active=='notifications' }}">Obavještenja</a>
|
||||
</nav>
|
||||
<div class="right">
|
||||
{{ owner.tenant.name }}{% if owner.impersonated_by %} <b>(admin pregled)</b>{% endif %}
|
||||
· <a href="/logout">Odjava</a>
|
||||
</div>
|
||||
</header>
|
||||
50
gogo/templates/dashboard/agent.html
Normal file
50
gogo/templates/dashboard/agent.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Asistent — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Virtuelni asistent</h1>
|
||||
|
||||
<form method="post" action="/dash/agent">
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<h2>Glas i cijene</h2>
|
||||
<label>Glas asistenta</label>
|
||||
<select name="tts_voice">
|
||||
{% for v in voices %}
|
||||
<option value="{{ v.id }}" {{ 'selected' if t.tts_voice==v.id }}>{{ v.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>Kako asistent odgovara na pitanja o cijenama</label>
|
||||
<select name="price_mode">
|
||||
<option value="exact" {{ 'selected' if t.price_mode=='exact' }}>Kaže tačnu cijenu</option>
|
||||
<option value="range" {{ 'selected' if t.price_mode=='range' }}>Kaže raspon (od–do)</option>
|
||||
<option value="on_request" {{ 'selected' if t.price_mode=='on_request' }}>„Cijene na upit"</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Napomene za asistenta</h2>
|
||||
<p class="muted">Činjenice o salonu koje asistent smije reći klijentima
|
||||
(npr. „parking iza zgrade", „ne primamo djecu ispod 7 godina").</p>
|
||||
<textarea name="agent_notes" style="min-height:120px">{{ t.agent_notes }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button>Sačuvaj</button>
|
||||
</form>
|
||||
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h2>Chat widget za vašu web stranicu</h2>
|
||||
<p class="muted">Zalijepite ovaj red koda pred kraj <code><body></code> taga vaše
|
||||
stranice (ili ga pošaljite onome ko vam održava web):</p>
|
||||
<pre><script src="{{ base_url }}/widget.js" data-gogo-key="{{ t.widget_public_key }}" async></script></pre>
|
||||
<p class="muted">Isprobajte razgovor na <a href="/widget-demo/{{ t.widget_public_key }}" target="_blank">demo stranici</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Pozdravna poruka</h2>
|
||||
<p class="muted">Pozdrav i ponašanje asistenta održava Gogo tim centralno — uvijek uključuje
|
||||
obavještenje o snimanju razgovora. Trenutni pozdrav:</p>
|
||||
<blockquote style="border-left:3px solid var(--brand);margin:8px 0;padding:4px 14px;color:var(--muted)">
|
||||
{{ greeting }}
|
||||
</blockquote>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
gogo/templates/dashboard/call_detail.html
Normal file
27
gogo/templates/dashboard/call_detail.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Poziv — Gogo Telefon{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<p><a href="/dash/calls">← Svi pozivi</a></p>
|
||||
<h1>Poziv {{ call.started_at.strftime('%d.%m.%Y. %H:%M') }}
|
||||
<span class="badge {{ call.outcome }}">{{ outcome_labels.get(call.outcome, call.outcome) }}</span></h1>
|
||||
<div class="card">
|
||||
<p><b>Broj:</b> {{ call.caller_msisdn or '—' }} ·
|
||||
<b>Trajanje:</b> {{ call.duration_s }}s ·
|
||||
<b>Naplaćene minute asistenta:</b> {{ (call.agent_seconds_charged / 60) | round(1) }} min</p>
|
||||
{% if call.recording_path %}
|
||||
<audio controls src="/dash/calls/{{ call.id }}/audio" style="width:100%"></audio>
|
||||
{% else %}<p class="muted">Snimka nije dostupna (obrisana nakon isteka perioda čuvanja ili poziv nije snimljen).</p>{% endif %}
|
||||
</div>
|
||||
{% if call.transcript %}
|
||||
<div class="card">
|
||||
<h2>Transkript</h2>
|
||||
<div class="transcript">
|
||||
{% for t in call.transcript %}
|
||||
<div class="t {{ t.role }}">{{ t.text }}</div>
|
||||
{% if t.tool_calls %}<div class="tools">⚙ {% for tc in t.tool_calls %}{{ tc.name }} {% endfor %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
22
gogo/templates/dashboard/calls.html
Normal file
22
gogo/templates/dashboard/calls.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Pozivi — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Historija poziva</h1>
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>Vrijeme</th><th>Broj</th><th>Trajanje</th><th>Ishod</th><th></th></tr>
|
||||
{% for c in calls %}
|
||||
<tr>
|
||||
<td class="muted">{{ c.started_at.strftime('%d.%m. %H:%M') }}</td>
|
||||
<td>{{ c.caller_msisdn or '—' }}</td>
|
||||
<td>{{ c.duration_s // 60 }}:{{ '%02d' % (c.duration_s % 60) }}</td>
|
||||
<td><span class="badge {{ c.outcome }}">{{ outcome_labels.get(c.outcome, c.outcome) }}</span></td>
|
||||
<td><a class="btn small ghost" href="/dash/calls/{{ c.id }}">Detalji</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="muted">Još nema zabilježenih poziva.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
14
gogo/templates/dashboard/chat_detail.html
Normal file
14
gogo/templates/dashboard/chat_detail.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Chat razgovor — Gogo Telefon{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<p><a href="/dash/chats">← Svi razgovori</a></p>
|
||||
<h1>Chat {{ chat.started_at.strftime('%d.%m.%Y. %H:%M') }}</h1>
|
||||
<div class="card">
|
||||
<div class="transcript">
|
||||
{% for m in messages %}
|
||||
<div class="t {{ m.role }}">{{ m.content }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
22
gogo/templates/dashboard/chats.html
Normal file
22
gogo/templates/dashboard/chats.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Chat — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Chat razgovori</h1>
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>Vrijeme</th><th>Poruka</th><th>Ishod</th><th></th></tr>
|
||||
{% for row in sessions %}
|
||||
<tr>
|
||||
<td class="muted">{{ row.chat.started_at.strftime('%d.%m. %H:%M') }}</td>
|
||||
<td class="muted">{{ row.preview }}</td>
|
||||
<td>{% if row.chat.outcome %}<span class="badge {{ row.chat.outcome }}">{{ outcome_labels.get(row.chat.outcome, row.chat.outcome) }}</span>{% endif %}</td>
|
||||
<td><a class="btn small ghost" href="/dash/chats/{{ row.chat.id }}">Detalji</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="muted">Još nema chat razgovora. Postavite widget na vašu web
|
||||
stranicu — kod se nalazi pod <a href="/dash/agent">Asistent</a>.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
gogo/templates/dashboard/login.html
Normal file
19
gogo/templates/dashboard/login.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Prijava — Gogo Telefon{% endblock %}
|
||||
{% block content %}
|
||||
<div style="max-width:380px;margin:60px auto">
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0">Gogo <span style="color:var(--brand)">Telefon</span></h1>
|
||||
<p class="muted">Prijava za vlasnike salona</p>
|
||||
<form method="post" action="/login">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email" required autofocus>
|
||||
<label>Lozinka</label>
|
||||
<input type="password" name="password" required>
|
||||
<div style="margin-top:16px"><button style="width:100%">Prijavi se</button></div>
|
||||
</form>
|
||||
<p class="muted" style="margin-top:16px">Nemate nalog? Gogo Telefon nalozi se otvaraju
|
||||
uz pomoć našeg tima — javite nam se.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
gogo/templates/dashboard/notifications.html
Normal file
28
gogo/templates/dashboard/notifications.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Obavještenja — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Obavještenja</h1>
|
||||
<form method="post" action="/dash/notifications">
|
||||
<div class="card">
|
||||
<h2>Email za zahtjeve</h2>
|
||||
<p class="muted">Na ove adrese stižu novi zahtjevi za termine (jedna po redu):</p>
|
||||
<textarea name="notify_emails" placeholder="vlasnica@salon.ba">{{ emails_text }}</textarea>
|
||||
<label style="margin-top:16px"><input type="checkbox" name="sms_request_received"
|
||||
{{ 'checked' if t.sms_request_received }} style="width:auto">
|
||||
Pošalji klijentu SMS „primili smo vaš zahtjev" odmah po zaprimanju</label>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>SMS poruke klijentima</h2>
|
||||
<p class="muted">Možete prilagoditi tekst poruka. Dostupne oznake:
|
||||
<code>{salon}</code>, <code>{usluga}</code>, <code>{dan}</code>, <code>{datum}</code>,
|
||||
<code>{vrijeme}</code>, <code>{link}</code>. Ostavite prazno za standardni tekst.</p>
|
||||
{% for key, label, default in sms_rows %}
|
||||
<label>{{ label }}</label>
|
||||
<textarea name="sms_{{ key }}" placeholder="{{ default }}">{{ overrides.get(key, '') }}</textarea>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button>Sačuvaj</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
59
gogo/templates/dashboard/request_detail.html
Normal file
59
gogo/templates/dashboard/request_detail.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Zahtjev — {{ req.client_name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<p><a href="/dash">← Svi zahtjevi</a></p>
|
||||
<h1>Zahtjev — {{ req.client_name }}
|
||||
<span class="badge {{ req.status }}">{{ status_labels[req.status] }}</span></h1>
|
||||
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<h2>Podaci</h2>
|
||||
<table>
|
||||
<tr><td class="muted">Klijent</td><td><b>{{ req.client_name }}</b></td></tr>
|
||||
<tr><td class="muted">Telefon</td><td>{{ req.client_phone }}</td></tr>
|
||||
<tr><td class="muted">Usluga</td><td>{{ service_name }}</td></tr>
|
||||
<tr><td class="muted">Izvor</td><td>{{ 'telefonski poziv' if req.source=='voice' else 'web chat' }}</td></tr>
|
||||
{% if req.home_visit %}<tr><td class="muted">Kućna posjeta</td><td>DA — {{ req.address or 'adresa nije navedena' }}</td></tr>{% endif %}
|
||||
{% if req.time_preference_text %}<tr><td class="muted">Željeno vrijeme</td><td>{{ req.time_preference_text }}</td></tr>{% endif %}
|
||||
<tr><td class="muted">Zaprimljen</td><td>{{ req.created_at.strftime('%d.%m.%Y. %H:%M') }}</td></tr>
|
||||
{% if req.resolved_at %}<tr><td class="muted">Zatvoren</td><td>{{ req.resolved_at.strftime('%d.%m.%Y. %H:%M') }}</td></tr>{% endif %}
|
||||
</table>
|
||||
{% if req.summary %}<p><b>Sažetak:</b> {{ req.summary }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Akcije</h2>
|
||||
{% if req.status == 'pending' %}
|
||||
<form method="post" action="/dash/requests/{{ req.id }}/resolve">
|
||||
<button class="green" style="width:100%">✓ Riješeno — kontaktirao/la sam klijenta</button>
|
||||
</form>
|
||||
<p class="muted">Klijentu se ne šalje SMS — dogovorili ste se direktno.</p>
|
||||
{% for s in slots %}
|
||||
<form method="post" action="/dash/requests/{{ req.id }}/confirm/{{ loop.index0 }}" style="margin-top:8px">
|
||||
<button style="width:100%">Potvrdi {{ s }} + pošalji SMS</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
<form method="post" action="/dash/requests/{{ req.id }}/reject" style="margin-top:8px">
|
||||
<textarea name="sms_body" title="SMS koji će klijent dobiti">{{ rejection_sms }}</textarea>
|
||||
<button class="red" style="width:100%;margin-top:8px">Odbij zahtjev i pošalji SMS</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="muted">Zahtjev je zatvoren.
|
||||
{% if req.confirmed_slot %}Potvrđen termin: {{ confirmed_label }}.{% endif %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if transcript %}
|
||||
<div class="card">
|
||||
<h2>Razgovor</h2>
|
||||
<div class="transcript">
|
||||
{% for t in transcript %}
|
||||
<div class="t {{ t.role }}">{{ t.text }}</div>
|
||||
{% if t.tool_calls %}<div class="tools">⚙ {% for tc in t.tool_calls %}{{ tc.name }} {% endfor %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
37
gogo/templates/dashboard/requests.html
Normal file
37
gogo/templates/dashboard/requests.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Zahtjevi — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Zahtjevi za termine</h1>
|
||||
{% if usage.over_80 %}
|
||||
<div class="banner">Iskorišteno je {{ usage.pct }}% uključenih minuta asistenta ovaj mjesec
|
||||
({{ usage.used_minutes }}/{{ usage.included_minutes }} min).</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>Datum</th><th>Klijent</th><th>Usluga</th><th>Termini</th><th>Status</th><th></th></tr>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td class="muted">{{ r.req.created_at.strftime('%d.%m. %H:%M') }}<br>
|
||||
<span class="badge {{ r.req.source }}">{{ 'poziv' if r.req.source=='voice' else 'chat' }}</span></td>
|
||||
<td><b>{{ r.req.client_name }}</b><br><span class="muted">{{ r.req.client_phone }}</span></td>
|
||||
<td>{{ r.service_name }}{% if r.req.home_visit %}<br><span class="muted">🏠 {{ r.req.address or 'kućna posjeta' }}</span>{% endif %}</td>
|
||||
<td>{% for s in r.slots %}{{ s }}<br>{% endfor %}
|
||||
{% if r.req.time_preference_text and not r.slots %}<span class="muted">{{ r.req.time_preference_text }}</span>{% endif %}</td>
|
||||
<td><span class="badge {{ r.req.status }}">{{ status_labels[r.req.status] }}</span></td>
|
||||
<td style="white-space:nowrap">
|
||||
<a class="btn small ghost" href="/dash/requests/{{ r.req.id }}">Detalji</a>
|
||||
{% if r.req.status == 'pending' %}
|
||||
<form method="post" action="/dash/requests/{{ r.req.id }}/resolve" style="display:inline">
|
||||
<button class="small green" title="Kontaktirao/la sam klijenta — zatvori bez SMS-a">Riješeno</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="muted">Još nema zahtjeva. Kad asistent primi zahtjev za termin,
|
||||
pojaviće se ovdje i stići će vam email.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
86
gogo/templates/dashboard/scheduling.html
Normal file
86
gogo/templates/dashboard/scheduling.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Kalendar — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Kalendar i termini</h1>
|
||||
|
||||
{% if t.scheduling_provider == 'partner_api' %}
|
||||
<div class="card">
|
||||
<h2>Vaš booking softver</h2>
|
||||
<p>Gogo je povezan direktno s vašim booking softverom — slobodni termini se čitaju iz njega,
|
||||
a zahtjevi stižu u njega. Tu ih vaše osoblje potvrđuje kao i inače.</p>
|
||||
<table>
|
||||
<tr><td class="muted">Status veze</td>
|
||||
<td>{% if partner_ok is none %}<span class="badge pending">nije testirano</span>
|
||||
{% elif partner_ok %}<span class="badge confirmed">povezano</span>
|
||||
{% else %}<span class="badge rejected">greška veze</span>{% endif %}</td></tr>
|
||||
<tr><td class="muted">Zadnja sinhronizacija usluga</td><td>{{ last_sync or '—' }}</td></tr>
|
||||
</table>
|
||||
<p class="muted">Podešavanje veze vrši Gogo tim — javite nam se ako nešto ne radi.</p>
|
||||
</div>
|
||||
|
||||
{% elif t.scheduling_provider == 'google_calendar' %}
|
||||
<div class="card">
|
||||
<h2>Google kalendar</h2>
|
||||
{% if connection %}
|
||||
<p>Povezan nalog: <b>{{ connection.google_account }}</b>
|
||||
<span class="badge confirmed">aktivno</span></p>
|
||||
<p class="muted">Gogo čita samo zauzetost (slobodno/zauzeto) — nikad sadržaj vaših
|
||||
događaja i nikad ne upisuje u kalendar.</p>
|
||||
<form method="post" action="/dash/scheduling/google/disconnect"
|
||||
onsubmit="return confirm('Prekinuti vezu s Google kalendarom?')">
|
||||
<button class="red small">Prekini vezu</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="muted">Povežite Google nalog da bi asistent nudio termine samo kad ste zaista
|
||||
slobodni. Gogo traži <b>samo</b> pristup zauzetosti (free/busy) — ne vidi sadržaj vaših
|
||||
događaja i ne može ništa upisivati.</p>
|
||||
{% if google_configured %}
|
||||
<a class="btn" href="/dash/scheduling/google/start">Poveži Google kalendar</a>
|
||||
{% else %}
|
||||
<p class="banner">Google prijava još nije podešena na ovom serveru — javite se Gogo timu.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Kalendari po uslugama</h2>
|
||||
<p class="muted">Ako vodite više kalendara (npr. po radnicama ili vrstama usluga), ovdje
|
||||
odredite koji kalendar vrijedi za koju uslugu. ID kalendara nađete u Google Calendar
|
||||
postavkama („Integrate calendar" → Calendar ID); glavni kalendar je <code>primary</code>.</p>
|
||||
<form method="post" action="/dash/scheduling/mappings">
|
||||
<table>
|
||||
<tr><th>Usluga</th><th>Google Calendar ID</th></tr>
|
||||
<tr><td><i>Zadani kalendar (sve usluge)</i></td>
|
||||
<td><input type="text" name="default" value="{{ mappings.get('default','primary') }}"></td></tr>
|
||||
{% for s in services %}
|
||||
<tr><td>{{ s.name }}</td>
|
||||
<td><input type="text" name="svc_{{ s.id }}" value="{{ mappings.get(s.id|string, '') }}"
|
||||
placeholder="(zadani)"></td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<button style="margin-top:12px">Sačuvaj</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Pravila nuđenja termina</h2>
|
||||
<form method="post" action="/dash/scheduling/buffers">
|
||||
<div class="grid2">
|
||||
<div>
|
||||
<label>Najranije nuđenje <small>(sati od trenutka poziva)</small></label>
|
||||
<input type="number" name="min_notice_hours" value="{{ t.min_notice_hours }}" min="0" max="72">
|
||||
</div>
|
||||
<div>
|
||||
<label>Najdalje nuđenje <small>(dana unaprijed)</small></label>
|
||||
<input type="number" name="max_days_ahead" value="{{ t.max_days_ahead }}" min="1" max="60">
|
||||
</div>
|
||||
</div>
|
||||
<button style="margin-top:12px">Sačuvaj</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card"><p class="muted">Demo režim (mock kalendar) — termini se računaju samo iz
|
||||
radnog vremena.</p></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
85
gogo/templates/dashboard/services.html
Normal file
85
gogo/templates/dashboard/services.html
Normal file
@@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Usluge — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Usluge i cjenovnik</h1>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<tr><th>Usluga</th><th>Trajanje</th><th>Cijena (KM)</th><th>Kućna posjeta</th><th>Napomena za asistenta</th><th></th></tr>
|
||||
{% for s in services %}
|
||||
<tr>
|
||||
<form method="post" action="/dash/services/{{ s.id }}">
|
||||
<td><input type="text" name="name" value="{{ s.name }}"></td>
|
||||
<td style="width:90px"><input type="number" name="duration_min" value="{{ s.duration_min }}" min="5" step="5"></td>
|
||||
<td style="width:150px;white-space:nowrap">
|
||||
<input type="number" name="price_min" value="{{ s.price_min if s.price_min is not none }}" step="0.5" style="width:65px;display:inline-block"> –
|
||||
<input type="number" name="price_max" value="{{ s.price_max if s.price_max is not none }}" step="0.5" style="width:65px;display:inline-block">
|
||||
</td>
|
||||
<td style="width:60px;text-align:center"><input type="checkbox" name="home_visit" {{ 'checked' if s.home_visit }}></td>
|
||||
<td><input type="text" name="agent_note" value="{{ s.agent_note }}"></td>
|
||||
<td style="white-space:nowrap">
|
||||
<button class="small">Sačuvaj</button>
|
||||
</form>
|
||||
<form method="post" action="/dash/services/{{ s.id }}/delete" style="display:inline"
|
||||
onsubmit="return confirm('Obrisati uslugu {{ s.name }}?')">
|
||||
<button class="small red">Obriši</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<form method="post" action="/dash/services">
|
||||
<td><input type="text" name="name" placeholder="Nova usluga…" required></td>
|
||||
<td><input type="number" name="duration_min" value="30" min="5" step="5"></td>
|
||||
<td style="white-space:nowrap">
|
||||
<input type="number" name="price_min" step="0.5" style="width:65px;display:inline-block"> –
|
||||
<input type="number" name="price_max" step="0.5" style="width:65px;display:inline-block">
|
||||
</td>
|
||||
<td style="text-align:center"><input type="checkbox" name="home_visit"></td>
|
||||
<td><input type="text" name="agent_note"></td>
|
||||
<td><button class="small green">Dodaj</button></td>
|
||||
</form>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Zalijepi cjenovnik</h2>
|
||||
<p class="muted">Zalijepite cijeli cjenovnik kako god ga imate (sa web stranice, iz Worda,
|
||||
sa Facebooka…). Gogo će ga pretvoriti u tabelu — vi pregledate, ispravite trajanja i potvrdite.</p>
|
||||
<form method="post" action="/dash/services/parse">
|
||||
<textarea name="text" style="min-height:140px" placeholder="npr.
|
||||
Šišanje i feniranje ......... 25-35 KM
|
||||
Farbanje ..................... od 60 KM
|
||||
Manikir ...................... 20 KM"></textarea>
|
||||
<button class="ghost" style="margin-top:8px">Pročitaj cjenovnik</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if parsed %}
|
||||
<div class="card">
|
||||
<h2>Provjeri i potvrdi</h2>
|
||||
<p class="muted">Ovako je Gogo pročitao vaš cjenovnik. Ispravite šta treba pa potvrdite —
|
||||
usluge će biti <b>dodane</b> postojećim.</p>
|
||||
<form method="post" action="/dash/services/import">
|
||||
<table>
|
||||
<tr><th></th><th>Usluga</th><th>Trajanje (min)</th><th>Cijena od</th><th>Cijena do</th><th>Kućna</th><th>Napomena</th></tr>
|
||||
{% for p in parsed %}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="row_{{ loop.index0 }}" checked></td>
|
||||
<td><input type="text" name="name_{{ loop.index0 }}" value="{{ p.name }}"></td>
|
||||
<td><input type="number" name="duration_{{ loop.index0 }}" value="{{ p.duration_min }}" min="5" step="5"></td>
|
||||
<td><input type="number" name="pmin_{{ loop.index0 }}" value="{{ p.price_min if p.price_min is not none }}" step="0.5"></td>
|
||||
<td><input type="number" name="pmax_{{ loop.index0 }}" value="{{ p.price_max if p.price_max is not none }}" step="0.5"></td>
|
||||
<td style="text-align:center"><input type="checkbox" name="home_{{ loop.index0 }}" {{ 'checked' if p.home_visit }}></td>
|
||||
<td><input type="text" name="note_{{ loop.index0 }}" value="{{ p.note or '' }}"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<input type="hidden" name="count" value="{{ parsed | length }}">
|
||||
<button class="green" style="margin-top:12px">Dodaj označene usluge</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
43
gogo/templates/dashboard/settings.html
Normal file
43
gogo/templates/dashboard/settings.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Salon — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Profil salona</h1>
|
||||
<form method="post" action="/dash/settings">
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<h2>Osnovni podaci</h2>
|
||||
<label>Naziv salona</label>
|
||||
<input type="text" name="name" value="{{ t.name }}" required>
|
||||
<label>Adresa</label>
|
||||
<input type="text" name="address" value="{{ t.address }}">
|
||||
<label>Grad</label>
|
||||
<input type="text" name="city" value="{{ t.city }}">
|
||||
<label>Telefon salona <small>(vaš postojeći broj)</small></label>
|
||||
<input type="text" name="phone" value="{{ t.phone }}">
|
||||
<label>Web stranica</label>
|
||||
<input type="text" name="website" value="{{ t.website }}">
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Radno vrijeme</h2>
|
||||
<p class="muted">Format: <code>09:00-13:00, 14:00-18:00</code> (pauza = dva intervala).
|
||||
Prazno = neradni dan.</p>
|
||||
{% for key, label in day_labels %}
|
||||
<label>{{ label }}</label>
|
||||
<input type="text" name="wh_{{ key }}" value="{{ hours_text[key] }}" placeholder="npr. 09:00-18:00">
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<button>Sačuvaj</button>
|
||||
</form>
|
||||
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h2>Zalijepi radno vrijeme</h2>
|
||||
<p class="muted">Zalijepite radno vrijeme kako god ga imate zapisano (sa web stranice,
|
||||
Facebooka…) — Gogo će ga pročitati, a vi samo provjerite i sačuvate.</p>
|
||||
<form method="post" action="/dash/settings/parse-hours">
|
||||
<textarea name="text" placeholder="npr. pon-pet 9-18 (pauza 13-14), subota 9-14, nedjelja ne radimo"></textarea>
|
||||
<button class="ghost" style="margin-top:8px">Pročitaj radno vrijeme</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
80
gogo/templates/dashboard/telephony.html
Normal file
80
gogo/templates/dashboard/telephony.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Telefonija — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Telefonija</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>Vaš Gogo broj</h2>
|
||||
{% if number %}
|
||||
<p style="font-size:22px;margin:4px 0"><b>{{ number.msisdn }}</b></p>
|
||||
<p class="muted">Na ovaj broj se prosljeđuju pozivi s vašeg postojećeg broja kad se niko
|
||||
ne javi. Vaši klijenti i dalje zovu vaš stari broj — ništa se ne mijenja.</p>
|
||||
<h2 style="margin-top:20px">Uključivanje prosljeđivanja</h2>
|
||||
<p class="muted">Ukucajte ove kodove na telefonu na kojem je SIM vašeg salonskog broja
|
||||
(poziv se pokreće tipkom za zvanje). Odaberite svog operatera:</p>
|
||||
{% for f in forwarding %}
|
||||
<details {{ 'open' if loop.first }}>
|
||||
<summary style="cursor:pointer;font-weight:600;padding:6px 0">{{ f.operator_label }}</summary>
|
||||
<table>
|
||||
<tr><td class="muted">Kad se ne javite ({{ t.ring_timeout_s }}s)</td><td><code>{{ f.activate_no_answer }}</code></td></tr>
|
||||
<tr><td class="muted">Kad je zauzeto</td><td><code>{{ f.activate_busy }}</code></td></tr>
|
||||
<tr><td class="muted">Kad ste nedostupni</td><td><code>{{ f.activate_unreachable }}</code></td></tr>
|
||||
<tr><td class="muted">Isključi sva prosljeđivanja</td><td><code>{{ f.deactivate_all }}</code></td></tr>
|
||||
<tr><td class="muted">Provjera statusa</td><td><code>{{ f.check_status }}</code></td></tr>
|
||||
</table>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="banner">Broj vam još nije dodijeljen — Gogo tim će to obaviti pri uključenju.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Zvonjenje osoblju prije asistenta</h2>
|
||||
<form method="post" action="/dash/telephony">
|
||||
<label><input type="checkbox" name="ring_first_enabled" {{ 'checked' if t.ring_first_enabled }}
|
||||
style="width:auto"> Prvo zvoni osoblju, asistent se javlja tek ako se niko ne javi</label>
|
||||
<div class="grid2">
|
||||
<div>
|
||||
<label>Koliko dugo zvoni osoblju <small>(sekundi)</small></label>
|
||||
<input type="number" name="ring_timeout_s" value="{{ t.ring_timeout_s }}" min="5" max="60">
|
||||
</div>
|
||||
<div>
|
||||
<label>Način zvonjenja</label>
|
||||
<select name="ring_strategy">
|
||||
<option value="ring_all" {{ 'selected' if t.ring_strategy=='ring_all' }}>Svi odjednom</option>
|
||||
<option value="sequential" {{ 'selected' if t.ring_strategy=='sequential' }}>Jedan po jedan</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button style="margin-top:12px">Sačuvaj</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Osoblje (aplikacija za javljanje)</h2>
|
||||
<p class="muted">Svaka radnica instalira besplatnu aplikaciju (preporučujemo
|
||||
<b>Linphone</b>, može i Zoiper/Groundwire) i skenira svoj QR kod ili prepiše podatke.
|
||||
Kad zazvoni salon, zazvoni i aplikacija.</p>
|
||||
<table>
|
||||
<tr><th>Ime</th><th>SIP korisničko ime</th><th>Podešavanje</th><th></th></tr>
|
||||
{% for w in workers %}
|
||||
<tr>
|
||||
<td>{{ w.name }}</td>
|
||||
<td><code>{{ w.sip_username }}</code></td>
|
||||
<td><a class="btn small ghost" href="/dash/workers/{{ w.id }}/setup">QR + podaci</a></td>
|
||||
<td><form method="post" action="/dash/workers/{{ w.id }}/delete"
|
||||
onsubmit="return confirm('Ukloniti {{ w.name }} iz zvonjenja?')">
|
||||
<button class="small red">Ukloni</button></form></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="muted">Još nema dodanog osoblja.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<form method="post" action="/dash/workers" style="margin-top:12px;display:flex;gap:8px">
|
||||
<input type="text" name="name" placeholder="Ime radnice/radnika" required style="max-width:260px">
|
||||
<button class="green">Dodaj</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
29
gogo/templates/dashboard/usage.html
Normal file
29
gogo/templates/dashboard/usage.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Potrošnja — {{ owner.tenant.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Potrošnja — {{ usage.month }}</h1>
|
||||
<div class="card">
|
||||
<h2>Minute virtualnog asistenta</h2>
|
||||
<div class="bar"><div class="{{ 'over' if usage.over_100 else ('warn' if usage.over_80 else '') }}"
|
||||
style="width:{{ usage.pct }}%"></div></div>
|
||||
<p><b>{{ usage.used_minutes }}</b> od <b>{{ usage.included_minutes }}</b> uključenih minuta
|
||||
({{ usage.pct }}%)</p>
|
||||
{% if usage.over_100 %}
|
||||
<div class="banner">Prekoračili ste uključene minute. Asistent i dalje odgovara na pozive —
|
||||
javiće vam se Gogo tim oko proširenja paketa.</div>
|
||||
{% endif %}
|
||||
<p class="muted">Vrijeme kad se javi vaše osoblje ne troši minute; chat se ne naplaćuje po
|
||||
minutama.</p>
|
||||
</div>
|
||||
<div class="grid3">
|
||||
<div class="card"><h2>Pozivi asistenta</h2><p style="font-size:28px;margin:0"><b>{{ usage.calls }}</b></p></div>
|
||||
<div class="card"><h2>Poslani SMS</h2><p style="font-size:28px;margin:0"><b>{{ usage.sms_sent }}</b></p></div>
|
||||
<div class="card"><h2>Chat razgovori</h2><p style="font-size:28px;margin:0"><b>{{ usage.chat_sessions }}</b></p></div>
|
||||
</div>
|
||||
{% if owner.tenant.paid_until %}
|
||||
<div class="card"><h2>Pretplata</h2>
|
||||
<p>Plaćeno do: <b>{{ owner.tenant.paid_until.strftime('%d.%m.%Y.') }}</b> —
|
||||
paket „Gogo Start" ({{ owner.tenant.included_minutes }} min/mj).</p></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
25
gogo/templates/dashboard/worker_setup.html
Normal file
25
gogo/templates/dashboard/worker_setup.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Podešavanje aplikacije — {{ worker.name }}{% endblock %}
|
||||
{% block nav %}{% include "dashboard/_nav.html" %}{% endblock %}
|
||||
{% block content %}
|
||||
<p><a href="/dash/telephony">← Telefonija</a></p>
|
||||
<h1>Aplikacija za: {{ worker.name }}</h1>
|
||||
<div class="grid2">
|
||||
<div class="card" style="text-align:center">
|
||||
<h2>Skeniraj QR kod</h2>
|
||||
<img src="/dash/workers/{{ worker.id }}/qr.png" alt="QR za podešavanje" style="max-width:260px">
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Ili prepiši ručno</h2>
|
||||
<table>
|
||||
<tr><td class="muted">Server / domena</td><td><code>{{ domain }}</code></td></tr>
|
||||
<tr><td class="muted">Korisničko ime</td><td><code>{{ worker.sip_username }}</code></td></tr>
|
||||
<tr><td class="muted">Lozinka</td><td><code>{{ worker.sip_password }}</code></td></tr>
|
||||
<tr><td class="muted">Transport</td><td><code>UDP, port 5060</code></td></tr>
|
||||
</table>
|
||||
<p class="muted" style="margin-top:16px">Koraci u Linphone aplikaciji: Assistant →
|
||||
„Use SIP account" → unesite podatke → Save. Provjerite da aplikacija ima dozvolu za
|
||||
obavještenja da bi zvonila.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
15
gogo/templates/widget_demo.html
Normal file
15
gogo/templates/widget_demo.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="bs">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Gogo widget demo</title>
|
||||
<style>body{font-family:system-ui,sans-serif;max-width:640px;margin:60px auto;padding:0 20px;color:#333}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Demo stranica salona</h1>
|
||||
<p>Ovako izgleda Gogo chat widget na web stranici salona — kliknite na ljubičasti balončić
|
||||
u donjem desnom uglu.</p>
|
||||
<script src="/widget.js" data-gogo-key="{{ key }}" async></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user