Added Record screen, transcribe, mobile screen share bug fix.
This commit is contained in:
+74
-22
@@ -45,6 +45,15 @@
|
||||
.filters{display:flex;gap:.6rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1rem;}
|
||||
.filters .f{flex:1;min-width:140px;}
|
||||
.filters .lbl{margin:.1rem 0 .15rem;}
|
||||
.srch{max-width:320px;margin:.2rem 0 .9rem;}
|
||||
.pager{display:flex;gap:.5rem;align-items:center;justify-content:flex-end;margin-top:.8rem;font-size:.82rem;color:var(--muted);}
|
||||
.pager button{padding:.32rem .7rem;font-size:.8rem;font-weight:600;background:#eef1f6;color:var(--blue);border:1px solid var(--line);}
|
||||
.pager button:hover:not(:disabled){background:var(--blue-soft);}
|
||||
.pager button:disabled{opacity:.4;cursor:default;}
|
||||
.pwwrap{position:relative;}
|
||||
.pwwrap input{padding-right:2.6rem;}
|
||||
.eye{position:absolute;right:.35rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;}
|
||||
.eye:hover{background:none;color:var(--blue);}
|
||||
.profile{position:relative}
|
||||
.profile .pbtn{display:flex;align-items:center;gap:.4rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.45rem .85rem;font-weight:600;font-size:.88rem;cursor:pointer}
|
||||
.profile .pbtn:hover{background:rgba(255,255,255,.24)}
|
||||
@@ -63,6 +72,10 @@
|
||||
<main id="app"></main>
|
||||
|
||||
<script>
|
||||
const EYE_OFF='<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
|
||||
const EYE_ON='<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||||
function pwField(id,ph){return '<div class="pwwrap"><input id="'+id+'" type="password" placeholder="'+ph+'"><button type="button" class="eye" data-for="'+id+'" aria-label="Show password"></button></div>';}
|
||||
function wireEyes(){document.querySelectorAll('.eye').forEach(b=>{if(b._w)return;b._w=1;b.innerHTML=EYE_OFF;b.onclick=()=>{const inp=document.getElementById(b.getAttribute('data-for'));if(!inp)return;const show=inp.type==='password';inp.type=show?'text':'password';b.innerHTML=show?EYE_ON:EYE_OFF;};});}
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/console">Console / Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
|
||||
@@ -100,7 +113,7 @@ async function authView() {
|
||||
<span class="lbl">Email</span>
|
||||
<input id="li_email" placeholder="you@bizgaze.com" type="email">
|
||||
<span class="lbl">Password</span>
|
||||
<input id="li_pw" placeholder="password" type="password">
|
||||
${pwField("li_pw","password")}
|
||||
<label style="display:flex;align-items:center;gap:.5rem;margin:.7rem 0;color:var(--ink);font-size:.9rem;cursor:pointer"><input type="checkbox" id="li_remember" style="width:18px;height:18px;accent-color:var(--blue);margin:0"> Remember me on this device</label>
|
||||
<button id="li_btn" style="width:100%;margin-top:.5rem">Sign in</button>
|
||||
<p id="li_err" class="muted"></p>
|
||||
@@ -111,12 +124,13 @@ async function authView() {
|
||||
<span class="lbl">Email</span>
|
||||
<input id="rg_email" placeholder="you@bizgaze.com" type="email">
|
||||
<span class="lbl">Password</span>
|
||||
<input id="rg_pw" placeholder="min 8 characters" type="password">
|
||||
${pwField("rg_pw","min 8 characters")}
|
||||
<button id="rg_btn" style="width:100%;margin-top:1rem">Create team</button>
|
||||
<p id="rg_err" class="muted"></p>
|
||||
</div>` : ''}
|
||||
</div>`);
|
||||
document.getElementById('li_btn').onclick = doLogin;
|
||||
wireEyes();
|
||||
onEnter(['li_email','li_pw'], doLogin);
|
||||
if (regOpen) {
|
||||
document.getElementById('tabLogin').onclick = () => toggle(true);
|
||||
@@ -160,7 +174,9 @@ async function dashboard(me) {
|
||||
</div>
|
||||
<div class="card" id="agentsCard">
|
||||
<h2>Agents</h2>
|
||||
<input id="agSearch" class="srch" placeholder="Search agents by name or email">
|
||||
<table id="agents"><thead><tr><th>Email</th><th>Display name</th><th>Role</th><th>Status</th><th style="width:280px"></th></tr></thead><tbody></tbody></table>
|
||||
<div id="agPager" class="pager"></div>
|
||||
<div class="row" style="margin-top:1rem;flex-wrap:wrap">
|
||||
<input id="agEmail" placeholder="agent email" style="max-width:200px">
|
||||
<input id="agName" placeholder="display name (from BizGaze)" style="max-width:210px">
|
||||
@@ -182,7 +198,8 @@ async function dashboard(me) {
|
||||
<button id="fExcel" class="mini" style="padding:.6rem .9rem">⬇ Excel</button>
|
||||
<button id="fPdf" class="mini" style="padding:.6rem .9rem">⬇ PDF</button>
|
||||
</div>
|
||||
<table id="report"><thead><tr><th>Date</th><th>Start time</th><th>Agent</th><th>Ticket</th><th>Time spent</th></tr></thead><tbody></tbody></table>
|
||||
<table id="report"><thead><tr><th>Date</th><th>Start time</th><th>Agent</th><th>Ticket</th><th>Time spent</th><th>Recording / Transcript</th></tr></thead><tbody></tbody></table>
|
||||
<div id="repPager" class="pager"></div>
|
||||
<p id="repSummary" class="muted" style="margin-top:.6rem"></p>
|
||||
</div>`);
|
||||
|
||||
@@ -208,9 +225,15 @@ async function addAgent() {
|
||||
} catch (e) { agOut.textContent = e.message; }
|
||||
}
|
||||
|
||||
async function loadAgents() {
|
||||
const rows = await api('/api/users', null, 'GET');
|
||||
document.querySelector('#agents tbody').innerHTML = rows.map((u) => `
|
||||
const PER_PAGE = 5;
|
||||
function pagerHTML(page, pages, total, fn){
|
||||
if (total <= PER_PAGE) return total ? `<span>${total} total</span>` : '';
|
||||
return `<button ${page<=1?'disabled':''} onclick="${fn}(${page-1})">‹ Prev</button>`
|
||||
+ `<span>Page ${page} of ${pages} · ${total} total</span>`
|
||||
+ `<button ${page>=pages?'disabled':''} onclick="${fn}(${page+1})">Next ›</button>`;
|
||||
}
|
||||
let AGENTS_ALL = [], agentPage = 1, agentSearch = '';
|
||||
function agentRowHTML(u){ return `
|
||||
<tr>
|
||||
<td>${esc(u.email)}</td><td>${esc(u.name || '—')}</td><td>${esc(u.role)}</td>
|
||||
<td><span class="pill ${u.active === 0 ? 'off' : 'on'}">${u.active === 0 ? 'deactivated' : 'active'}</span></td>
|
||||
@@ -223,7 +246,22 @@ async function loadAgents() {
|
||||
}
|
||||
${u.id === ME.id ? '' : `<button class="mini danger" onclick="delAgent('${u.id}','${esc(u.email)}')">Delete</button>`}
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
</tr>`; }
|
||||
async function loadAgents() {
|
||||
AGENTS_ALL = await api('/api/users', null, 'GET');
|
||||
agentPage = 1;
|
||||
const s = document.getElementById('agSearch');
|
||||
if (s && !s._w){ s._w = 1; s.addEventListener('input', () => { agentSearch = s.value.trim().toLowerCase(); agentPage = 1; renderAgents(); }); }
|
||||
renderAgents();
|
||||
}
|
||||
window.agentGo = (p) => { agentPage = p; renderAgents(); };
|
||||
function renderAgents(){
|
||||
const all = agentSearch ? AGENTS_ALL.filter(u => ((u.name||'')+' '+(u.email||'')).toLowerCase().includes(agentSearch)) : AGENTS_ALL;
|
||||
const pages = Math.max(1, Math.ceil(all.length / PER_PAGE));
|
||||
if (agentPage > pages) agentPage = pages;
|
||||
const slice = all.slice((agentPage-1)*PER_PAGE, (agentPage-1)*PER_PAGE + PER_PAGE);
|
||||
document.querySelector('#agents tbody').innerHTML = slice.map(agentRowHTML).join('') || '<tr><td colspan=5 class="muted">No matching agents.</td></tr>';
|
||||
document.getElementById('agPager').innerHTML = pagerHTML(agentPage, pages, all.length, 'agentGo');
|
||||
}
|
||||
|
||||
window.resetPw = async (id, email) => {
|
||||
@@ -268,27 +306,41 @@ function fmtDuration(ms) {
|
||||
return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
|
||||
}
|
||||
|
||||
let REPORT_ROWS = [];
|
||||
async function loadReport() {
|
||||
const q = new URLSearchParams();
|
||||
if (fAgent.value) q.set('agent', fAgent.value);
|
||||
if (fFrom.value) q.set('from', fFrom.value);
|
||||
if (fTo.value) q.set('to', fTo.value);
|
||||
const rows = await api('/api/report?' + q.toString(), null, 'GET');
|
||||
REPORT_ROWS = rows;
|
||||
document.querySelector('#report tbody').innerHTML = rows.map((r) => {
|
||||
const d = new Date(r.started_at);
|
||||
const dur = r.ended_at ? (r.ended_at - r.started_at) : null;
|
||||
return `<tr>
|
||||
let REPORT_ROWS = [], reportPage = 1, reportSearch = '';
|
||||
function reportRowHTML(r){
|
||||
const d = new Date(r.started_at);
|
||||
const dur = r.ended_at ? (r.ended_at - r.started_at) : null;
|
||||
return `<tr>
|
||||
<td>${d.toLocaleDateString()}</td>
|
||||
<td class="muted">${d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</td>
|
||||
<td>${esc(r.agent_name || r.agent_email || '—')}</td>
|
||||
<td>${esc(r.ticket || 'Direct session')}</td>
|
||||
<td>${r.ended_at ? fmtDuration(dur) : '<span class="pill on">in progress</span>'}</td>
|
||||
<td>${[
|
||||
r.recording ? `<a class="mini" style="text-decoration:none;display:inline-block;padding:.32rem .6rem;margin:1px" href="/recordings/${esc(r.recording)}" download>⬇ Video</a>` : '',
|
||||
r.transcript ? `<a class="mini" style="text-decoration:none;display:inline-block;padding:.32rem .6rem;margin:1px" href="/transcripts/${esc(r.transcript)}" download>⬇ Text</a>` : ''
|
||||
].join('') || '<span class="muted">—</span>'}</td>
|
||||
</tr>`;
|
||||
}).join('') || '<tr><td colspan=5 class="muted">No sessions in this period.</td></tr>';
|
||||
const total = rows.reduce((a, r) => a + (r.ended_at ? r.ended_at - r.started_at : 0), 0);
|
||||
repSummary.textContent = rows.length ? `${rows.length} session(s) · total time ${fmtDuration(total)}` : '';
|
||||
}
|
||||
async function loadReport() {
|
||||
const q = new URLSearchParams();
|
||||
if (fAgent.value) q.set('agent', fAgent.value);
|
||||
if (fFrom.value) q.set('from', fFrom.value);
|
||||
if (fTo.value) q.set('to', fTo.value);
|
||||
REPORT_ROWS = await api('/api/report?' + q.toString(), null, 'GET');
|
||||
reportPage = 1;
|
||||
renderReport();
|
||||
}
|
||||
window.reportGo = (p) => { reportPage = p; renderReport(); };
|
||||
function renderReport(){
|
||||
const all = reportSearch ? REPORT_ROWS.filter(r => ((r.agent_name||'')+' '+(r.agent_email||'')+' '+(r.ticket||'')).toLowerCase().includes(reportSearch)) : REPORT_ROWS;
|
||||
const pages = Math.max(1, Math.ceil(all.length / PER_PAGE));
|
||||
if (reportPage > pages) reportPage = pages;
|
||||
const slice = all.slice((reportPage-1)*PER_PAGE, (reportPage-1)*PER_PAGE + PER_PAGE);
|
||||
document.querySelector('#report tbody').innerHTML = slice.map(reportRowHTML).join('') || '<tr><td colspan=6 class="muted">No sessions match.</td></tr>';
|
||||
document.getElementById('repPager').innerHTML = pagerHTML(reportPage, pages, all.length, 'reportGo');
|
||||
const total = all.reduce((a, r) => a + (r.ended_at ? r.ended_at - r.started_at : 0), 0);
|
||||
repSummary.textContent = all.length ? `${all.length} session(s) · total time ${fmtDuration(total)}` : '';
|
||||
}
|
||||
|
||||
function reportData() {
|
||||
|
||||
Reference in New Issue
Block a user