feat(maria): profil de client (firma), asociat cu mai multe numere

Un client poate avea mai multi angajati care scriu pe WhatsApp de pe
numere diferite; toti gasesc acum acelasi profil (rag/client.py,
~/.maria-bridge/clients.json). Profilul intra in prompt sub o sectiune
DESPRE CLIENT, la fiecare mesaj, stateless ca restul Mariei — dar nu
atinge cautarea/scorul din rag/rank.py, ca sa nu strice pragurile deja
calibrate. Administrare din dashboard (sectiune noua "Clienti cunoscuti"),
nu din chat, ca sa ramana pe canalul autentificat.

117/117 teste maria-whatsapp-bridge + teste noi pentru rutele de dashboard.

Co-Authored-By: Claude Agent <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-09-08 10:35:30 +00:00
parent f25a8157fa
commit 610638ad30
7 changed files with 398 additions and 12 deletions

View File

@@ -120,6 +120,26 @@
<div id="mEsc" class="empty">se încarcă…</div>
</section>
<section class="card">
<h2>Clienți cunoscuți <span class="count" id="mClientCount"></span></h2>
<div class="table-scroll">
<table class="stack-mobil">
<thead><tr><th>Nume</th><th>Numere</th><th></th><th></th></tr></thead>
<tbody id="mClients"></tbody>
</table>
</div>
<h2 style="margin-top:var(--space-5)" id="mClientFormTitle">Adaugă client</h2>
<input type="text" id="mClientId" placeholder="id (ex: acme)" style="width:100%;margin-bottom:var(--space-3)">
<input type="text" id="mClientNume" placeholder="nume firmă" style="width:100%;margin-bottom:var(--space-3)">
<textarea id="mClientProfil" placeholder="profil (context liber despre client)..." style="width:100%;min-height:80px;margin-bottom:var(--space-3)"></textarea>
<textarea id="mClientNumere" placeholder="numere de telefon, unul pe linie" style="width:100%;min-height:80px;font-family:monospace"></textarea>
<div class="actions" style="margin-top:var(--space-3)">
<button class="primary" onclick="mSaveClient()">Salvează</button>
<button class="small" onclick="mResetClientForm()">Renunță</button>
</div>
<div class="detail" id="mClientMsg"></div>
</section>
<section class="card">
<h2>Documente indexate <span class="count" id="mDocCount"></span></h2>
<div class="detail" id="mIndexDetail"></div>
@@ -521,6 +541,61 @@ async function mDelDoc(name) {
await mReindex();
}
var mClientsCache = [];
async function mLoadClients() {
var res = await api('api/maria/clients');
var clienti = res.ok ? (res.data.clients || []) : [];
mClientsCache = clienti;
document.getElementById('mClientCount').textContent = clienti.length ? clienti.length : '';
document.getElementById('mClients').innerHTML = clienti.length ? clienti.map(function (c) {
return '<tr><td data-label="Nume">' + esc(c.nume || c.id) + '</td>'
+ '<td data-label="Numere" class="mono">' + esc((c.numere || []).join(', ')) + '</td>'
+ '<td><button class="small" onclick="mEditClient(\'' + esc(c.id) + '\')">Editează</button></td>'
+ '<td><button class="small danger" onclick="mDelClient(\'' + esc(c.id) + '\')">Șterge</button></td></tr>';
}).join('') : '<tr><td colspan="4" class="empty" data-label="">niciun client</td></tr>';
}
function mEditClient(id) {
var c = mClientsCache.find(function (x) { return x.id === id; });
if (!c) return;
document.getElementById('mClientId').value = c.id;
document.getElementById('mClientId').disabled = true;
document.getElementById('mClientNume').value = c.nume || '';
document.getElementById('mClientProfil').value = c.profil || '';
document.getElementById('mClientNumere').value = (c.numere || []).join('\n');
document.getElementById('mClientFormTitle').textContent = 'Editează client: ' + (c.nume || c.id);
document.getElementById('mClientMsg').textContent = '';
}
function mResetClientForm() {
document.getElementById('mClientId').value = '';
document.getElementById('mClientId').disabled = false;
document.getElementById('mClientNume').value = '';
document.getElementById('mClientProfil').value = '';
document.getElementById('mClientNumere').value = '';
document.getElementById('mClientFormTitle').textContent = 'Adaugă client';
document.getElementById('mClientMsg').textContent = '';
}
async function mSaveClient() {
var id = document.getElementById('mClientId').value.trim();
var nume = document.getElementById('mClientNume').value.trim();
var profil = document.getElementById('mClientProfil').value;
var numere = document.getElementById('mClientNumere').value;
var msg = document.getElementById('mClientMsg');
if (!id || !nume) { msg.textContent = 'completează id și nume'; return; }
var res = await post('api/maria/clients', { id: id, nume: nume, profil: profil, numere: numere });
if (!res.ok) { msg.textContent = 'eșuat: ' + (res.data.error || res.status); return; }
mResetClientForm();
await mLoadClients();
}
async function mDelClient(id) {
if (!confirm('Ștergi clientul ' + id + '?')) return;
await post('api/maria/clients/delete', { id: id });
await mLoadClients();
}
var mLogFile = 'bridge';
function mSetLog(which) { mLogFile = which; mRefreshLogs(); }
async function mRefreshLogs() {
@@ -535,7 +610,7 @@ async function mRefreshLogs() {
// ── pornire ───────────────────────────────────────────────────
refresh(); refreshDoctor(); refreshLogs();
mRefresh(); mRefreshLogs();
mRefresh(); mRefreshLogs(); mLoadClients();
setInterval(refresh, 5000);
setInterval(refreshDoctor, 30000);
setInterval(mRefresh, 4000);