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:
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 %}
|
||||
Reference in New Issue
Block a user