|
|
@@ -3,333 +3,75 @@
|
|
3
|
3
|
<head>
|
|
4
|
4
|
<meta charset="UTF-8">
|
|
5
|
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
|
-<title>BizGaze Support — Console</title>
|
|
|
6
|
+<title>BizGaze Support</title>
|
|
7
|
7
|
<style>
|
|
8
|
|
- :root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#fff; --line:#e6e9ef; --green:#16a34a; --red:#b91c1c; }
|
|
|
8
|
+ :root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --line:#e6e9ef; }
|
|
9
|
9
|
*{box-sizing:border-box;}
|
|
10
|
|
- body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;}
|
|
11
|
|
- header{background:var(--blue);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center;}
|
|
12
|
|
- .brandrow{display:flex;align-items:center;gap:.6rem;}
|
|
13
|
|
- .logo{width:30px;height:30px;border-radius:8px;background:var(--brand);display:grid;place-items:center;font-weight:800;color:var(--blue);}
|
|
14
|
|
- .brand{font-weight:700;color:#fff;font-size:1.05rem;} .brand span{color:var(--brand);font-weight:600;}
|
|
15
|
|
- .who{color:#dbe4f5;font-size:.85rem;margin-right:.7rem;}
|
|
16
|
|
- main{max-width:1020px;margin:1.8rem auto;padding:0 1rem;}
|
|
17
|
|
- .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1.5rem;margin-bottom:1.25rem;box-shadow:0 6px 18px rgba(20,30,60,.05);}
|
|
18
|
|
- h2{font-size:1rem;margin:0 0 1rem;color:var(--blue);}
|
|
19
|
|
- input,select{width:100%;padding:.6rem .7rem;border-radius:10px;border:2px solid var(--line);background:#fbfcfe;color:var(--ink);margin:.25rem 0;font-size:.92rem;}
|
|
20
|
|
- input:focus,select:focus{outline:none;border-color:var(--brand);}
|
|
21
|
|
- button{padding:.6rem 1.1rem;background:var(--brand);color:var(--ink);border:none;border-radius:10px;font-weight:700;cursor:pointer;font-size:.92rem;}
|
|
22
|
|
- button:hover{background:var(--brand-d);}
|
|
23
|
|
- button.ghost{background:transparent;color:#dbe4f5;border:1px solid #46598c;font-weight:600;}
|
|
24
|
|
- button.ghost:hover{background:var(--blue-d);}
|
|
25
|
|
- button.mini{padding:.32rem .6rem;font-size:.76rem;font-weight:600;background:#eef1f6;color:var(--blue);border:1px solid var(--line);}
|
|
26
|
|
- button.mini:hover{background:var(--blue-soft);}
|
|
27
|
|
- button.mini.danger{color:var(--red);}
|
|
28
|
|
- .row{display:flex;gap:.5rem;align-items:center;}
|
|
29
|
|
- .muted{color:var(--muted);font-size:.85rem;}
|
|
30
|
|
- table{width:100%;border-collapse:collapse;font-size:.88rem;}
|
|
31
|
|
- th{color:var(--muted);font-weight:600;font-size:.76rem;text-transform:uppercase;letter-spacing:.04em;}
|
|
32
|
|
- th,td{text-align:left;padding:.55rem .5rem;border-bottom:1px solid var(--line);}
|
|
33
|
|
- .pill{font-size:.74rem;font-weight:600;padding:.15rem .55rem;border-radius:99px;}
|
|
34
|
|
- .pill.on{background:#ecfdf3;color:#15803d;} .pill.off{background:#fee2e2;color:var(--red);}
|
|
35
|
|
- .hidden{display:none;}
|
|
36
|
|
- .tabs{display:flex;gap:.5rem;margin-bottom:1.2rem;}
|
|
37
|
|
- .tabs button{background:#eef1f6;color:var(--muted);font-weight:600;}
|
|
38
|
|
- .tabs button.active{background:var(--blue);color:#fff;}
|
|
39
|
|
- .quick{display:flex;align-items:center;justify-content:space-between;gap:1rem;background:linear-gradient(120deg,var(--blue),var(--blue-d));color:#fff;border:none;}
|
|
40
|
|
- .quick h2{color:#fff;margin:0 0 .25rem;}
|
|
41
|
|
- .quick p{margin:0;color:#cdd7ee;font-size:.88rem;}
|
|
42
|
|
- .quick a{background:var(--brand);color:var(--ink);text-decoration:none;font-weight:700;padding:.7rem 1.3rem;border-radius:10px;white-space:nowrap;}
|
|
43
|
|
- .quick a:hover{background:var(--brand-d);}
|
|
44
|
|
- .lbl{display:block;font-size:.74rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:.7rem 0 .15rem;}
|
|
45
|
|
- .filters{display:flex;gap:.6rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1rem;}
|
|
46
|
|
- .filters .f{flex:1;min-width:140px;}
|
|
47
|
|
- .filters .lbl{margin:.1rem 0 .15rem;}
|
|
|
10
|
+ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;min-height:100vh;display:flex;flex-direction:column;}
|
|
|
11
|
+ header{background:var(--blue);padding:.85rem 1.5rem;display:flex;justify-content:space-between;align-items:center;}
|
|
|
12
|
+ .brandrow{display:flex;align-items:center;gap:.7rem;}
|
|
|
13
|
+ .brand{font-weight:700;color:#fff;font-size:1.1rem;} .brand span{color:var(--brand);font-weight:600;}
|
|
|
14
|
+ .signin{color:#dbe4f5;text-decoration:none;font-size:.9rem;border:1px solid #46598c;border-radius:8px;padding:.45rem 1rem;}
|
|
|
15
|
+ .signin:hover{background:var(--blue-d);}
|
|
|
16
|
+ .wrap{flex:1;display:grid;place-items:center;padding:2.5rem 1rem;}
|
|
|
17
|
+ .inner{max-width:780px;width:100%;text-align:center;}
|
|
|
18
|
+ h1{color:var(--blue);font-size:1.8rem;margin:0 0 .4rem;}
|
|
|
19
|
+ .sub{color:var(--muted);margin-bottom:2.2rem;}
|
|
|
20
|
+ .choices{display:flex;gap:1.4rem;flex-wrap:wrap;justify-content:center;}
|
|
|
21
|
+ .choice{flex:1;min-width:260px;max-width:340px;background:#fff;border:1px solid var(--line);border-radius:18px;padding:2.2rem 1.6rem;text-decoration:none;color:var(--ink);box-shadow:0 10px 30px rgba(20,30,60,.06);transition:transform .12s,box-shadow .12s;}
|
|
|
22
|
+ .choice:hover{transform:translateY(-3px);box-shadow:0 16px 38px rgba(20,30,60,.12);border-color:var(--brand);}
|
|
|
23
|
+ .icon{width:66px;height:66px;border-radius:18px;display:grid;place-items:center;margin:0 auto 1.1rem;}
|
|
|
24
|
+ .icon.share{background:#FFF7DA;} .icon.connect{background:var(--blue-soft);}
|
|
|
25
|
+ .icon svg{width:34px;height:34px;}
|
|
|
26
|
+ .choice h3{margin:.2rem 0 .4rem;color:var(--blue);font-size:1.15rem;}
|
|
|
27
|
+ .choice p{margin:0;color:var(--muted);font-size:.9rem;line-height:1.5;}
|
|
|
28
|
+ .foot{color:var(--muted);font-size:.82rem;margin-top:2.4rem;}
|
|
|
29
|
+ footer{text-align:center;color:#9aa3b2;font-size:.8rem;padding:1rem;}
|
|
|
30
|
+ .profile{position:relative}
|
|
|
31
|
+ .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}
|
|
|
32
|
+ .profile .pbtn:hover{background:rgba(255,255,255,.24)}
|
|
|
33
|
+ .profile .pmenu{position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #e6e9ef;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.18);min-width:190px;overflow:hidden;z-index:5000;display:none}
|
|
|
34
|
+ .profile .pmenu.open{display:block}
|
|
|
35
|
+ .profile .pmenu a{display:block;padding:.6rem .9rem;color:#1f2430;text-decoration:none;font-size:.9rem;cursor:pointer}
|
|
|
36
|
+ .profile .pmenu a:hover{background:#f1f5f9}
|
|
|
37
|
+ .profile .pmenu a.danger{color:#b91c1c;border-top:1px solid #eef1f6}
|
|
48
|
38
|
</style>
|
|
49
|
39
|
</head>
|
|
50
|
40
|
<body>
|
|
51
|
41
|
<header>
|
|
52
|
|
- <div class="brandrow"><img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">BizGaze <span>Support</span> <span style="color:#8ea3cf;font-weight:500;font-size:.85rem">· Console</span></div></div>
|
|
53
|
|
- <div class="row"><span id="who" class="who"></span><button id="logoutBtn" class="ghost hidden">Log out</button></div>
|
|
|
42
|
+ <div class="brandrow">
|
|
|
43
|
+ <img src="/logo.png" alt="" style="height:40px;width:auto;max-width:170px;border-radius:8px;object-fit:contain;background:#fff;padding:4px 10px" onerror="this.style.display='none'">
|
|
|
44
|
+ <div class="brand">BizGaze <span>Support</span></div>
|
|
|
45
|
+ </div>
|
|
|
46
|
+ <div id="authArea"><a class="signin" href="/console">Staff sign in</a></div>
|
|
54
|
47
|
</header>
|
|
55
|
|
-<main id="app"></main>
|
|
56
|
|
-
|
|
57
|
|
-<script>
|
|
58
|
|
-const app = document.getElementById('app');
|
|
59
|
|
-const who = document.getElementById('who');
|
|
60
|
|
-const logoutBtn = document.getElementById('logoutBtn');
|
|
61
|
|
-
|
|
62
|
|
-async function api(path, body, method = 'POST') {
|
|
63
|
|
- const opt = { method, headers: { 'Content-Type': 'application/json' } };
|
|
64
|
|
- if (body) opt.body = JSON.stringify(body);
|
|
65
|
|
- const r = await fetch(path, opt);
|
|
66
|
|
- const data = await r.json().catch(() => ({}));
|
|
67
|
|
- if (!r.ok) throw new Error(data.error || 'request failed');
|
|
68
|
|
- return data;
|
|
69
|
|
-}
|
|
70
|
|
-
|
|
71
|
|
-logoutBtn.onclick = async () => { await api('/api/logout'); location.reload(); };
|
|
72
|
|
-
|
|
73
|
|
-function onEnter(ids, fn){ ids.forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); fn(); } }); }); }
|
|
74
|
|
-
|
|
75
|
|
-function view(html) { app.innerHTML = html; }
|
|
76
|
|
-
|
|
77
|
|
-// ---------- Auth ----------
|
|
78
|
|
-function authView() {
|
|
79
|
|
- who.textContent = '';
|
|
80
|
|
- logoutBtn.classList.add('hidden');
|
|
81
|
|
- view(`
|
|
82
|
|
- <div class="card" style="max-width:420px;margin:3rem auto">
|
|
83
|
|
- <div class="tabs">
|
|
84
|
|
- <button id="tabLogin" class="active">Sign in</button>
|
|
85
|
|
- <button id="tabReg">Register team</button>
|
|
86
|
|
- </div>
|
|
87
|
|
- <div id="loginForm">
|
|
88
|
|
- <span class="lbl">Email</span>
|
|
89
|
|
- <input id="li_email" placeholder="you@bizgaze.com" type="email">
|
|
90
|
|
- <span class="lbl">Password</span>
|
|
91
|
|
- <input id="li_pw" placeholder="password" type="password">
|
|
92
|
|
- <button id="li_btn" style="width:100%;margin-top:1rem">Sign in</button>
|
|
93
|
|
- <p id="li_err" class="muted"></p>
|
|
94
|
|
- </div>
|
|
95
|
|
- <div id="regForm" class="hidden">
|
|
96
|
|
- <span class="lbl">Team name</span>
|
|
97
|
|
- <input id="rg_team" placeholder="e.g. BizGaze Support">
|
|
98
|
|
- <span class="lbl">Email</span>
|
|
99
|
|
- <input id="rg_email" placeholder="you@bizgaze.com" type="email">
|
|
100
|
|
- <span class="lbl">Password</span>
|
|
101
|
|
- <input id="rg_pw" placeholder="min 8 characters" type="password">
|
|
102
|
|
- <button id="rg_btn" style="width:100%;margin-top:1rem">Create team</button>
|
|
103
|
|
- <p id="rg_err" class="muted"></p>
|
|
104
|
|
- </div>
|
|
105
|
|
- </div>`);
|
|
106
|
|
- document.getElementById('tabLogin').onclick = () => { toggle(true); };
|
|
107
|
|
- document.getElementById('tabReg').onclick = () => { toggle(false); };
|
|
108
|
|
- function toggle(login) {
|
|
109
|
|
- document.getElementById('loginForm').classList.toggle('hidden', !login);
|
|
110
|
|
- document.getElementById('regForm').classList.toggle('hidden', login);
|
|
111
|
|
- document.getElementById('tabLogin').classList.toggle('active', login);
|
|
112
|
|
- document.getElementById('tabReg').classList.toggle('active', !login);
|
|
113
|
|
- }
|
|
114
|
|
- document.getElementById('li_btn').onclick = doLogin;
|
|
115
|
|
- document.getElementById('rg_btn').onclick = doRegister;
|
|
116
|
|
- onEnter(['li_email','li_pw'], doLogin);
|
|
117
|
|
- onEnter(['rg_team','rg_email','rg_pw'], doRegister);
|
|
118
|
|
-}
|
|
119
|
|
-
|
|
120
|
|
-async function doLogin() {
|
|
121
|
|
- try {
|
|
122
|
|
- await api('/api/login', { email: li_email.value, password: li_pw.value });
|
|
123
|
|
- location.reload();
|
|
124
|
|
- } catch (e) { li_err.textContent = e.message; }
|
|
125
|
|
-}
|
|
126
|
|
-
|
|
127
|
|
-async function doRegister() {
|
|
128
|
|
- try {
|
|
129
|
|
- await api('/api/register', { email: rg_email.value, password: rg_pw.value, teamName: rg_team.value });
|
|
130
|
|
- await api('/api/login', { email: rg_email.value, password: rg_pw.value });
|
|
131
|
|
- location.reload();
|
|
132
|
|
- } catch (e) { rg_err.textContent = e.message; }
|
|
133
|
|
-}
|
|
134
|
|
-
|
|
135
|
|
-// ---------- Dashboard ----------
|
|
136
|
|
-let ME = null;
|
|
137
|
|
-async function dashboard(me) {
|
|
138
|
|
- ME = me;
|
|
139
|
|
- who.textContent = `${me.name || me.email} · ${me.role}`;
|
|
140
|
|
- logoutBtn.classList.remove('hidden');
|
|
141
|
|
- view(`
|
|
142
|
|
- <div class="card quick">
|
|
143
|
|
- <div><h2>Start a support session</h2><p>Customer gives you their 6-digit code from the share page.</p></div>
|
|
144
|
|
- <a href="/connect">Open connect page →</a>
|
|
145
|
|
- </div>
|
|
146
|
|
- <div class="card" id="agentsCard">
|
|
147
|
|
- <h2>Agents</h2>
|
|
148
|
|
- <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>
|
|
149
|
|
- <div class="row" style="margin-top:1rem;flex-wrap:wrap">
|
|
150
|
|
- <input id="agEmail" placeholder="agent email" style="max-width:200px">
|
|
151
|
|
- <input id="agName" placeholder="display name (from BizGaze)" style="max-width:210px">
|
|
152
|
|
- <input id="agPw" placeholder="temporary password" style="max-width:170px">
|
|
153
|
|
- <select id="agRole" style="max-width:140px">
|
|
154
|
|
- <option value="technician">technician</option><option value="admin">admin</option><option value="viewer">view-only</option>
|
|
155
|
|
- </select>
|
|
156
|
|
- <button id="agAdd">Add agent</button>
|
|
157
|
|
- </div>
|
|
158
|
|
- <p id="agOut" class="muted"></p>
|
|
|
48
|
+<div class="wrap">
|
|
|
49
|
+ <div class="inner">
|
|
|
50
|
+ <h1>How can we help you today?</h1>
|
|
|
51
|
+ <div class="sub">Secure remote support — no downloads, you stay in control.</div>
|
|
|
52
|
+ <div class="choices">
|
|
|
53
|
+ <a class="choice" href="/share">
|
|
|
54
|
+ <div class="icon share"><svg viewBox="0 0 24 24" fill="none" stroke="#BA7515" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div>
|
|
|
55
|
+ <h3>Share my screen</h3>
|
|
|
56
|
+ <p>You need help. Get a one-time code and show your screen to a BizGaze support agent.</p>
|
|
|
57
|
+ </a>
|
|
|
58
|
+ <a class="choice" href="/connect">
|
|
|
59
|
+ <div class="icon connect"><svg viewBox="0 0 24 24" fill="none" stroke="#1F3B73" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17V7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10"/><path d="M2 21h20"/><path d="m9 9 3 3-3 3"/></svg></div>
|
|
|
60
|
+ <h3>Connect to a screen</h3>
|
|
|
61
|
+ <p>You're a support agent. Sign in, then enter the customer's code to view their screen.</p>
|
|
|
62
|
+ </a>
|
|
159
|
63
|
</div>
|
|
160
|
|
- <div class="card">
|
|
161
|
|
- <h2>Session report</h2>
|
|
162
|
|
- <div class="filters">
|
|
163
|
|
- <div class="f"><span class="lbl">Agent</span><select id="fAgent"><option value="">All agents</option></select></div>
|
|
164
|
|
- <div class="f"><span class="lbl">From</span><input id="fFrom" type="date"></div>
|
|
165
|
|
- <div class="f"><span class="lbl">To</span><input id="fTo" type="date"></div>
|
|
166
|
|
- <button id="fApply">Apply</button>
|
|
167
|
|
- <button id="fExcel" class="mini" style="padding:.6rem .9rem">⬇ Excel</button>
|
|
168
|
|
- <button id="fPdf" class="mini" style="padding:.6rem .9rem">⬇ PDF</button>
|
|
169
|
|
- </div>
|
|
170
|
|
- <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>
|
|
171
|
|
- <p id="repSummary" class="muted" style="margin-top:.6rem"></p>
|
|
172
|
|
- </div>`);
|
|
173
|
|
-
|
|
174
|
|
- if (me.role !== 'admin') document.getElementById('agentsCard').style.display = 'none';
|
|
175
|
|
- else {
|
|
176
|
|
- document.getElementById('agAdd').onclick = addAgent;
|
|
177
|
|
- onEnter(['agEmail','agName','agPw'], addAgent);
|
|
178
|
|
- await loadAgents();
|
|
179
|
|
- }
|
|
180
|
|
- document.getElementById('fApply').onclick = loadReport;
|
|
181
|
|
- document.getElementById('fExcel').onclick = exportExcel;
|
|
182
|
|
- document.getElementById('fPdf').onclick = exportPdf;
|
|
183
|
|
- await populateAgentFilter();
|
|
184
|
|
- await loadReport();
|
|
185
|
|
-}
|
|
186
|
|
-
|
|
187
|
|
-async function addAgent() {
|
|
188
|
|
- try {
|
|
189
|
|
- const r = await api('/api/users', { email: agEmail.value, name: agName.value, password: agPw.value, role: agRole.value });
|
|
190
|
|
- agOut.textContent = `Agent ${r.email} added. Share the email + temporary password — they sign in at /connect.`;
|
|
191
|
|
- agEmail.value = ''; agName.value = ''; agPw.value = '';
|
|
192
|
|
- loadAgents(); populateAgentFilter();
|
|
193
|
|
- } catch (e) { agOut.textContent = e.message; }
|
|
194
|
|
-}
|
|
195
|
|
-
|
|
196
|
|
-async function loadAgents() {
|
|
197
|
|
- const rows = await api('/api/users', null, 'GET');
|
|
198
|
|
- document.querySelector('#agents tbody').innerHTML = rows.map((u) => `
|
|
199
|
|
- <tr>
|
|
200
|
|
- <td>${esc(u.email)}</td><td>${esc(u.name || '—')}</td><td>${esc(u.role)}</td>
|
|
201
|
|
- <td><span class="pill ${u.active === 0 ? 'off' : 'on'}">${u.active === 0 ? 'deactivated' : 'active'}</span></td>
|
|
202
|
|
- <td>
|
|
203
|
|
- <button class="mini" onclick="resetPw('${u.id}','${esc(u.email)}')">Reset password</button>
|
|
204
|
|
- <button class="mini" onclick="renameAgent('${u.id}','${esc(u.email)}')">Edit name</button>
|
|
205
|
|
- ${u.id === ME.id ? '' : (u.active === 0
|
|
206
|
|
- ? `<button class="mini" onclick="manage('${u.id}','activate')">Activate</button>`
|
|
207
|
|
- : `<button class="mini danger" onclick="manage('${u.id}','deactivate')">Deactivate</button>`)
|
|
208
|
|
- }
|
|
209
|
|
- ${u.id === ME.id ? '' : `<button class="mini danger" onclick="delAgent('${u.id}','${esc(u.email)}')">Delete</button>`}
|
|
210
|
|
- </td>
|
|
211
|
|
- </tr>`).join('');
|
|
212
|
|
-}
|
|
213
|
|
-
|
|
214
|
|
-window.resetPw = async (id, email) => {
|
|
215
|
|
- const pw = prompt(`New password for ${email} (min 8 characters):`);
|
|
216
|
|
- if (!pw) return;
|
|
217
|
|
- try { await api('/api/users/manage', { id, action: 'reset-password', password: pw }); agOut.textContent = `Password reset for ${email}. They were signed out everywhere.`; }
|
|
218
|
|
- catch (e) { agOut.textContent = e.message; }
|
|
219
|
|
-};
|
|
220
|
|
-window.renameAgent = async (id, email) => {
|
|
221
|
|
- const name = prompt(`Display name for ${email} (as in the BizGaze app):`);
|
|
222
|
|
- if (!name) return;
|
|
223
|
|
- try { await api('/api/users/manage', { id, action: 'rename', name }); loadAgents(); }
|
|
224
|
|
- catch (e) { agOut.textContent = e.message; }
|
|
225
|
|
-};
|
|
226
|
|
-window.manage = async (id, action) => {
|
|
227
|
|
- try { await api('/api/users/manage', { id, action }); loadAgents(); }
|
|
228
|
|
- catch (e) { agOut.textContent = e.message; }
|
|
229
|
|
-};
|
|
230
|
|
-window.delAgent = async (id, email) => {
|
|
231
|
|
- if (!confirm(`Delete ${email}? This cannot be undone. (Tip: Deactivate keeps their history.)`)) return;
|
|
232
|
|
- try { await api('/api/users/manage', { id, action: 'delete' }); loadAgents(); populateAgentFilter(); }
|
|
233
|
|
- catch (e) { agOut.textContent = e.message; }
|
|
234
|
|
-};
|
|
235
|
|
-
|
|
236
|
|
-// ---------- Session report ----------
|
|
237
|
|
-async function populateAgentFilter() {
|
|
238
|
|
- try {
|
|
239
|
|
- const rows = await api('/api/users', null, 'GET');
|
|
240
|
|
- const sel = document.getElementById('fAgent');
|
|
241
|
|
- const cur = sel.value;
|
|
242
|
|
- sel.innerHTML = '<option value="">All agents</option>' + rows.map(u => `<option value="${esc(u.email)}">${esc(u.name || u.email)}</option>`).join('');
|
|
243
|
|
- sel.value = cur;
|
|
244
|
|
- } catch { /* non-admins cannot list agents; filter stays "All" */ }
|
|
245
|
|
-}
|
|
246
|
|
-
|
|
247
|
|
-function fmtDuration(ms) {
|
|
248
|
|
- if (ms == null) return '—';
|
|
249
|
|
- const s = Math.round(ms / 1000);
|
|
250
|
|
- if (s < 60) return s + 's';
|
|
251
|
|
- const m = Math.floor(s / 60), r = s % 60;
|
|
252
|
|
- if (m < 60) return m + 'm ' + r + 's';
|
|
253
|
|
- return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
|
|
254
|
|
-}
|
|
255
|
|
-
|
|
256
|
|
-let REPORT_ROWS = [];
|
|
257
|
|
-async function loadReport() {
|
|
258
|
|
- const q = new URLSearchParams();
|
|
259
|
|
- if (fAgent.value) q.set('agent', fAgent.value);
|
|
260
|
|
- if (fFrom.value) q.set('from', fFrom.value);
|
|
261
|
|
- if (fTo.value) q.set('to', fTo.value);
|
|
262
|
|
- const rows = await api('/api/report?' + q.toString(), null, 'GET');
|
|
263
|
|
- REPORT_ROWS = rows;
|
|
264
|
|
- document.querySelector('#report tbody').innerHTML = rows.map((r) => {
|
|
265
|
|
- const d = new Date(r.started_at);
|
|
266
|
|
- const dur = r.ended_at ? (r.ended_at - r.started_at) : null;
|
|
267
|
|
- return `<tr>
|
|
268
|
|
- <td>${d.toLocaleDateString()}</td>
|
|
269
|
|
- <td class="muted">${d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</td>
|
|
270
|
|
- <td>${esc(r.agent_name || r.agent_email || '—')}</td>
|
|
271
|
|
- <td>${esc(r.ticket || 'Direct session')}</td>
|
|
272
|
|
- <td>${r.ended_at ? fmtDuration(dur) : '<span class="pill on">in progress</span>'}</td>
|
|
273
|
|
- </tr>`;
|
|
274
|
|
- }).join('') || '<tr><td colspan=5 class="muted">No sessions in this period.</td></tr>';
|
|
275
|
|
- const total = rows.reduce((a, r) => a + (r.ended_at ? r.ended_at - r.started_at : 0), 0);
|
|
276
|
|
- repSummary.textContent = rows.length ? `${rows.length} session(s) · total time ${fmtDuration(total)}` : '';
|
|
277
|
|
-}
|
|
278
|
|
-
|
|
279
|
|
-function reportData() {
|
|
280
|
|
- return REPORT_ROWS.map((r) => {
|
|
281
|
|
- const d = new Date(r.started_at);
|
|
282
|
|
- return {
|
|
283
|
|
- date: d.toLocaleDateString(), start: d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}),
|
|
284
|
|
- agent: r.agent_name || r.agent_email || '', ticket: r.ticket || 'Direct session',
|
|
285
|
|
- spent: r.ended_at ? fmtDuration(r.ended_at - r.started_at) : 'in progress',
|
|
286
|
|
- };
|
|
287
|
|
- });
|
|
288
|
|
-}
|
|
289
|
|
-
|
|
290
|
|
-function exportExcel() {
|
|
291
|
|
- const rows = reportData();
|
|
292
|
|
- if (!rows.length) { repSummary.textContent = 'Nothing to export for this period.'; return; }
|
|
293
|
|
- const head = ['Date','Start time','Agent','Ticket','Time spent'];
|
|
294
|
|
- const csvCell = (v) => '"' + String(v).replace(/"/g, '""') + '"';
|
|
295
|
|
- const csv = '\ufeff' + [head, ...rows.map(r => [r.date, r.start, r.agent, r.ticket, r.spent])]
|
|
296
|
|
- .map(line => line.map(csvCell).join(',')).join('\r\n');
|
|
297
|
|
- const a = document.createElement('a');
|
|
298
|
|
- a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
|
299
|
|
- a.download = 'session-report.csv';
|
|
300
|
|
- a.click(); URL.revokeObjectURL(a.href);
|
|
301
|
|
-}
|
|
302
|
|
-
|
|
303
|
|
-function exportPdf() {
|
|
304
|
|
- const rows = reportData();
|
|
305
|
|
- if (!rows.length) { repSummary.textContent = 'Nothing to export for this period.'; return; }
|
|
306
|
|
- const period = (fFrom.value || 'start') + ' to ' + (fTo.value || 'today');
|
|
307
|
|
- const agentSel = fAgent.value || 'All agents';
|
|
308
|
|
- const w = window.open('', '_blank');
|
|
309
|
|
- w.document.write('<html><head><title>Session report</title><style>' +
|
|
310
|
|
- 'body{font-family:Segoe UI,Arial,sans-serif;color:#1f2430;margin:32px}' +
|
|
311
|
|
- 'h1{font-size:18px;color:#1F3B73;border-bottom:3px solid #FFC708;padding-bottom:6px}' +
|
|
312
|
|
- '.meta{color:#6b7280;font-size:12px;margin-bottom:14px}' +
|
|
313
|
|
- 'table{width:100%;border-collapse:collapse;font-size:12px}' +
|
|
314
|
|
- 'th{background:#1F3B73;color:#fff;text-align:left;padding:6px 8px}' +
|
|
315
|
|
- 'td{padding:6px 8px;border-bottom:1px solid #e6e9ef}' +
|
|
316
|
|
- '</style></head><body>' +
|
|
317
|
|
- '<h1>BizGaze Support — Session report</h1>' +
|
|
318
|
|
- '<div class="meta">Agent: ' + esc(agentSel) + ' · Period: ' + esc(period) + ' · Generated ' + new Date().toLocaleString() + '</div>' +
|
|
319
|
|
- '<table><tr><th>Date</th><th>Start time</th><th>Agent</th><th>Ticket</th><th>Time spent</th></tr>' +
|
|
320
|
|
- rows.map(r => '<tr><td>' + [r.date, r.start, esc(r.agent), esc(r.ticket), r.spent].join('</td><td>') + '</td></tr>').join('') +
|
|
321
|
|
- '</table><div class="meta" style="margin-top:12px">' + esc(repSummary.textContent) + '</div></body></html>');
|
|
322
|
|
- w.document.close();
|
|
323
|
|
- w.onload = () => { w.print(); };
|
|
324
|
|
-}
|
|
325
|
|
-
|
|
326
|
|
-function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
327
|
|
-
|
|
328
|
|
-// ---------- Boot ----------
|
|
329
|
|
-(async function () {
|
|
330
|
|
- try { const me = await api('/api/me', null, 'GET'); dashboard(me); }
|
|
331
|
|
- catch { authView(); }
|
|
332
|
|
-})();
|
|
|
64
|
+ <div class="foot">🔒 Screen sharing only starts after the customer approves it, and can be stopped anytime.</div>
|
|
|
65
|
+ </div>
|
|
|
66
|
+</div>
|
|
|
67
|
+<footer>© BizGaze · Remote Support</footer>
|
|
|
68
|
+<script>
|
|
|
69
|
+function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
|
|
70
|
+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>';}
|
|
|
71
|
+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='/';};}
|
|
|
72
|
+function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
|
|
|
73
|
+makeBrandClickable();
|
|
|
74
|
+(async function(){try{const r=await fetch('/api/me');if(r.ok){const me=await r.json();document.getElementById('authArea').innerHTML=profileHTML(me.name||me.email);wireProfile();}}catch(_){}})();
|
|
333
|
75
|
</script>
|
|
334
|
76
|
</body>
|
|
335
|
77
|
</html>
|