MinhDo123 commited on
Commit
d9b1aa7
·
verified ·
1 Parent(s): 5005316

Update frontend/dashboard.html

Browse files
Files changed (1) hide show
  1. frontend/dashboard.html +102 -44
frontend/dashboard.html CHANGED
@@ -156,63 +156,124 @@
156
  </main>
157
 
158
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  let rawData = [];
160
  let currentPeriod = 'today';
161
  let busChart, intChart, sentChart;
162
 
163
- // --- HÀM MỚI: GỌI API LẤY DỮ LIỆU THẬT ---
164
  async function fetchRealData() {
 
 
 
165
  try {
166
- // Hiệu ứng loading nhẹ
167
- document.getElementById('status-dot').className = "w-2 h-2 bg-yellow-400 rounded-full animate-ping";
 
 
 
 
 
168
 
169
- const response = await fetch('/api/dashboard-stats');
 
 
 
 
170
  const result = await response.json();
171
 
172
  if (result.status === 'success') {
173
- // Map dữ liệu từ Backend sang format của Dashboard
174
  rawData = result.data.map(item => ({
175
  id: item.id || 0,
176
- time: item.timestamp, // Format từ backend
177
- dur: item.duration || 0,
178
- // Xử lý cost: Nếu là dict thì lấy value, không thì lấy chính nó
179
  cost: (typeof item.cost === 'object') ? item.cost.value : item.cost,
180
  csat: (typeof item.cost === 'object') ? item.cost.csat : (item.csat || 4),
181
- ai: item.ai_resolved,
182
  intent: item.intent || 'general',
183
- sent: item.sentiment || 'neutral',
184
- upsell: item.upsell || false
185
  }));
186
 
187
- // Sắp xếp mới nhất lên đầu
188
  rawData.sort((a, b) => new Date(b.time) - new Date(a.time));
189
 
190
  loadDashboard(currentPeriod);
191
 
192
- // Cập nhật trạng thái Online
193
- document.getElementById('status-dot').className = "w-2 h-2 bg-green-400 rounded-full animate-pulse";
194
- document.getElementById('last-update').innerText = new Date().toLocaleTimeString('vi-VN');
 
 
 
 
195
  }
196
  } catch (error) {
197
- console.error("Lỗi lấy dữ liệu:", error);
198
- document.getElementById('status-dot').className = "w-2 h-2 bg-red-500 rounded-full";
 
 
 
 
 
 
199
  }
200
  }
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  function formatAHT(seconds) {
203
  if (!seconds || isNaN(seconds)) return "00:00";
204
  const m = Math.floor(seconds / 60).toString().padStart(2, '0');
205
  const s = (seconds % 60).toString().padStart(2, '0');
206
  return `${m}:${s}`;
207
  }
208
-
209
  function filterData(period) {
210
  if (!rawData || rawData.length === 0) return [];
211
  const now = new Date();
212
  const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
213
- const startOfWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
 
214
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
215
-
216
  return rawData.filter(item => {
217
  const itemTime = new Date(item.time);
218
  if (period === 'today') return itemTime >= startOfDay;
@@ -225,7 +286,7 @@
225
  function calculateMetrics(data) {
226
  const total = data.length;
227
  if (total === 0) return { totalCalls: 0, avgCost: 0, aht: 0, fcr: 0, avgCSAT: 0, intents: { network:0, cancel:0, competitor:0, low_data:0 }, sentiments: { pos:0, neu:0, neg:0 } };
228
-
229
  const totalCost = data.reduce((sum, c) => sum + (Number(c.cost) || 0), 0);
230
  const totalDur = data.reduce((sum, c) => sum + (Number(c.dur) || 0), 0);
231
  const totalCSAT = data.reduce((sum, c) => sum + (Number(c.csat) || 0), 0);
@@ -233,16 +294,17 @@
233
 
234
  const intents = { network: 0, cancel: 0, competitor: 0, low_data: 0 };
235
  const sents = { pos: 0, neu: 0, neg: 0 };
236
-
237
  data.forEach(c => {
238
  const i = (c.intent || "").toLowerCase();
239
  if(i.includes('network')) intents.network++;
240
  else if(i.includes('low_data') || i.includes('data')) intents.low_data++;
241
  else if(i.includes('cancel')) intents.cancel++;
242
- else intents.competitor++; // Gom các loại khác vào competitor hoặc general
243
-
244
- if(c.sent === 'positive') sents.pos++;
245
- else if(c.sent === 'negative') sents.neg++;
 
246
  else sents.neu++;
247
  });
248
 
@@ -260,9 +322,7 @@
260
  function renderCharts(metrics, period) {
261
  Chart.defaults.font.family = 'Plus Jakarta Sans';
262
  Chart.defaults.color = '#64748b';
263
-
264
- // Chart Logic (Vẫn dùng dummy cho đường line chart vì chưa đủ data history)
265
- // Nhưng Pie/Doughnut dùng data thật
266
  const labels = period === 'today' ? ['08:00','10:00','12:00','14:00','16:00'] : ['Th 2','Th 3','Th 4','Th 5','Th 6','Th 7','CN'];
267
  const retentionData = [65,68,62,70,75,72,78];
268
  const upsellData = [10,12,8,15,14,13,16];
@@ -304,11 +364,10 @@
304
  function updateFeed() {
305
  const feedContainer = document.getElementById('activity-feed');
306
  feedContainer.innerHTML = '';
307
- // Lấy 10 cuộc gọi gần nhất
308
  const recentItems = rawData.slice(0,10);
309
-
310
  if(recentItems.length === 0) {
311
- feedContainer.innerHTML = '<div class="text-center text-slate-400 py-10">Chưa có dữ liệu cuộc gọi</div>';
312
  return;
313
  }
314
 
@@ -318,16 +377,18 @@
318
  if(item.upsell){
319
  iconClass='bg-emerald-100 text-emerald-600'; icon='fa-arrow-trend-up'; title='Upsell Thành công!'; msg=`KH ${item.id} đã chốt đơn.`;
320
  }
321
- else if(item.intent==='low_data'){
322
  iconClass='bg-purple-100 text-purple-600'; icon='fa-database'; title='Phàn nàn Data'; msg=`KH ${item.id} báo dung lượng thấp.`;
323
  }
324
- else if(item.sent==='negative'){
325
  iconClass='bg-rose-100 text-rose-600'; icon='fa-triangle-exclamation'; title='Cảnh báo Rủi ro'; msg=`KH ${item.id} có thái độ tiêu cực.`;
326
  }
327
 
328
- // Format thời gian
329
  let dateObj = new Date(item.time);
330
- const timeString = isNaN(dateObj) ? "N/A" : dateObj.toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'});
 
 
 
331
 
332
  feedContainer.innerHTML+=`
333
  <div class="flex gap-4 p-4 bg-slate-50 rounded-xl border border-slate-100 hover:bg-white hover:shadow-md transition-all">
@@ -343,7 +404,7 @@
343
  <div class="mt-2 flex gap-3 text-[10px] font-semibold text-slate-400 uppercase tracking-wide">
344
  <span><i class="fas fa-star text-amber-400 mr-1"></i> ${item.csat}/5</span>
345
  <span><i class="fas fa-clock text-blue-400 mr-1"></i> ${item.dur}s</span>
346
- <span><i class="fas fa-coins text-green-500 mr-1"></i> ${Number(item.cost).toLocaleString()}đ</span>
347
  </div>
348
  </div>
349
  </div>
@@ -361,25 +422,22 @@
361
  function loadDashboard(period){
362
  const filteredData = filterData(period);
363
  const metrics = calculateMetrics(filteredData);
364
-
365
- // Animate Numbers
366
  document.getElementById('kpi-total-calls').innerText = metrics.totalCalls.toLocaleString();
367
  document.getElementById('kpi-cost').innerText = metrics.avgCost.toLocaleString();
368
  document.getElementById('kpi-aht').innerText = formatAHT(metrics.aht);
369
  document.getElementById('kpi-fcr').innerText = metrics.fcr;
370
  document.getElementById('fcr-bar').style.width = `${metrics.fcr}%`;
371
  document.getElementById('kpi-csat').innerText = metrics.avgCSAT;
372
-
373
  renderCharts(metrics, period);
374
  updateFeed();
375
  }
376
 
377
  document.addEventListener('DOMContentLoaded', ()=>{
378
- // Gọi lần đầu
379
  fetchRealData();
380
-
381
- // Tự động refresh mỗi 10 giây
382
- setInterval(fetchRealData, 10000);
383
  });
384
  </script>
385
 
 
156
  </main>
157
 
158
  <script>
159
+ // --- CẤU HÌNH API THÔNG MINH ---
160
+ // Giúp Dashboard chạy được cả trên Localhost, File HTML rời, và Hugging Face
161
+ function getBaseUrl() {
162
+ const host = window.location.hostname;
163
+
164
+ // 1. Nếu chạy Localhost (IP 127.0.0.1 hoặc localhost)
165
+ if (host === 'localhost' || host === '127.0.0.1') {
166
+ // Mặc định HF Spaces chạy port 7860, Local thường là 8000
167
+ // Bạn có thể sửa thành 8000 nếu chạy local
168
+ return "http://localhost:7860";
169
+ }
170
+
171
+ // 2. Nếu chạy file HTML trực tiếp (file://)
172
+ if (window.location.protocol === 'file:') {
173
+ return "http://localhost:7860";
174
+ }
175
+
176
+ // 3. Nếu chạy trên Hugging Face (Production)
177
+ // Trả về chuỗi rỗng để trình duyệt tự nối vào domain hiện tại
178
+ return "";
179
+ }
180
+
181
+ const API_BASE = getBaseUrl();
182
+ console.log("👉 API BASE URL:", API_BASE || "(Relative Path)");
183
+
184
  let rawData = [];
185
  let currentPeriod = 'today';
186
  let busChart, intChart, sentChart;
187
 
188
+ // --- HÀM GỌI API ---
189
  async function fetchRealData() {
190
+ const statusDot = document.getElementById('status-dot');
191
+ const lastUpdate = document.getElementById('last-update');
192
+
193
  try {
194
+ // Hiệu ứng đang tải
195
+ statusDot.className = "w-2 h-2 bg-yellow-400 rounded-full animate-ping";
196
+ lastUpdate.innerText = "Syncing...";
197
+
198
+ // Gọi API với đường dẫn đã xử lý
199
+ // Thêm timestamp để tránh cache trình duyệt
200
+ const response = await fetch(`${API_BASE}/api/dashboard-stats?t=${new Date().getTime()}`);
201
 
202
+ // Kiểm tra nếu Server báo lỗi (VD: 500 Internal Server Error)
203
+ if (!response.ok) {
204
+ throw new Error(`Server Error: ${response.status}`);
205
+ }
206
+
207
  const result = await response.json();
208
 
209
  if (result.status === 'success') {
210
+ // Map dữ liệu
211
  rawData = result.data.map(item => ({
212
  id: item.id || 0,
213
+ time: item.timestamp || item.time, // Handle cả 2 trường hợp tên biến
214
+ dur: item.duration || item.dur || 0,
 
215
  cost: (typeof item.cost === 'object') ? item.cost.value : item.cost,
216
  csat: (typeof item.cost === 'object') ? item.cost.csat : (item.csat || 4),
217
+ ai: item.ai_resolved || item.ai,
218
  intent: item.intent || 'general',
219
+ sent: item.sentiment || item.sent || 'neutral',
220
+ upsell: item.upsell_success || item.upsell || false
221
  }));
222
 
223
+ // Sắp xếp
224
  rawData.sort((a, b) => new Date(b.time) - new Date(a.time));
225
 
226
  loadDashboard(currentPeriod);
227
 
228
+ // Cập nhật trạng thái XANH (Thành công)
229
+ statusDot.className = "w-2 h-2 bg-green-400 rounded-full animate-pulse";
230
+ lastUpdate.innerText = new Date().toLocaleTimeString('vi-VN');
231
+ // Xóa thông báo lỗi nếu có
232
+ document.getElementById('connection-error-msg')?.remove();
233
+ } else {
234
+ throw new Error("API trả về dữ liệu lỗi");
235
  }
236
  } catch (error) {
237
+ console.error(" LỖI DASHBOARD:", error);
238
+
239
+ // Cập nhật trạng thái ĐỎ (Lỗi)
240
+ statusDot.className = "w-2 h-2 bg-red-600 rounded-full";
241
+ lastUpdate.innerText = "Mất kết nối";
242
+
243
+ // Hiển thị lỗi lên màn hình để dễ debug trên HF
244
+ showErrorOnScreen(error.message);
245
  }
246
  }
247
 
248
+ // Hàm hiển thị lỗi nhỏ trên giao diện (giúp debug nhanh)
249
+ function showErrorOnScreen(msg) {
250
+ if(!document.getElementById('connection-error-msg')) {
251
+ const div = document.createElement('div');
252
+ div.id = 'connection-error-msg';
253
+ div.className = "fixed bottom-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-2 rounded shadow-lg text-xs z-50";
254
+ document.body.appendChild(div);
255
+ div.innerText = "⚠️ Lỗi: " + msg;
256
+ } else {
257
+ document.getElementById('connection-error-msg').innerText = "⚠️ Lỗi: " + msg;
258
+ }
259
+ }
260
+
261
+ // --- CÁC HÀM HELPER & CHART (GIỮ NGUYÊN LOGIC CŨ) ---
262
  function formatAHT(seconds) {
263
  if (!seconds || isNaN(seconds)) return "00:00";
264
  const m = Math.floor(seconds / 60).toString().padStart(2, '0');
265
  const s = (seconds % 60).toString().padStart(2, '0');
266
  return `${m}:${s}`;
267
  }
268
+
269
  function filterData(period) {
270
  if (!rawData || rawData.length === 0) return [];
271
  const now = new Date();
272
  const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
273
+ // Fix logic tuần: Lấy 7 ngày gần nhất
274
+ const startOfWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
275
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
276
+
277
  return rawData.filter(item => {
278
  const itemTime = new Date(item.time);
279
  if (period === 'today') return itemTime >= startOfDay;
 
286
  function calculateMetrics(data) {
287
  const total = data.length;
288
  if (total === 0) return { totalCalls: 0, avgCost: 0, aht: 0, fcr: 0, avgCSAT: 0, intents: { network:0, cancel:0, competitor:0, low_data:0 }, sentiments: { pos:0, neu:0, neg:0 } };
289
+
290
  const totalCost = data.reduce((sum, c) => sum + (Number(c.cost) || 0), 0);
291
  const totalDur = data.reduce((sum, c) => sum + (Number(c.dur) || 0), 0);
292
  const totalCSAT = data.reduce((sum, c) => sum + (Number(c.csat) || 0), 0);
 
294
 
295
  const intents = { network: 0, cancel: 0, competitor: 0, low_data: 0 };
296
  const sents = { pos: 0, neu: 0, neg: 0 };
297
+
298
  data.forEach(c => {
299
  const i = (c.intent || "").toLowerCase();
300
  if(i.includes('network')) intents.network++;
301
  else if(i.includes('low_data') || i.includes('data')) intents.low_data++;
302
  else if(i.includes('cancel')) intents.cancel++;
303
+ else intents.competitor++;
304
+
305
+ const s = (c.sent || "").toLowerCase();
306
+ if(s.includes('pos')) sents.pos++;
307
+ else if(s.includes('neg')) sents.neg++;
308
  else sents.neu++;
309
  });
310
 
 
322
  function renderCharts(metrics, period) {
323
  Chart.defaults.font.family = 'Plus Jakarta Sans';
324
  Chart.defaults.color = '#64748b';
325
+
 
 
326
  const labels = period === 'today' ? ['08:00','10:00','12:00','14:00','16:00'] : ['Th 2','Th 3','Th 4','Th 5','Th 6','Th 7','CN'];
327
  const retentionData = [65,68,62,70,75,72,78];
328
  const upsellData = [10,12,8,15,14,13,16];
 
364
  function updateFeed() {
365
  const feedContainer = document.getElementById('activity-feed');
366
  feedContainer.innerHTML = '';
 
367
  const recentItems = rawData.slice(0,10);
368
+
369
  if(recentItems.length === 0) {
370
+ feedContainer.innerHTML = '<div class="text-center text-slate-400 py-10 text-sm">Chưa có dữ liệu cuộc gọi nào.</div>';
371
  return;
372
  }
373
 
 
377
  if(item.upsell){
378
  iconClass='bg-emerald-100 text-emerald-600'; icon='fa-arrow-trend-up'; title='Upsell Thành công!'; msg=`KH ${item.id} đã chốt đơn.`;
379
  }
380
+ else if(item.intent.includes('low_data')){
381
  iconClass='bg-purple-100 text-purple-600'; icon='fa-database'; title='Phàn nàn Data'; msg=`KH ${item.id} báo dung lượng thấp.`;
382
  }
383
+ else if(item.sent.includes('neg')){
384
  iconClass='bg-rose-100 text-rose-600'; icon='fa-triangle-exclamation'; title='Cảnh báo Rủi ro'; msg=`KH ${item.id} có thái độ tiêu cực.`;
385
  }
386
 
 
387
  let dateObj = new Date(item.time);
388
+ const timeString = isNaN(dateObj) ? "--:--" : dateObj.toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'});
389
+
390
+ // Format cost
391
+ const costDisplay = item.cost ? Number(item.cost).toLocaleString() : "0";
392
 
393
  feedContainer.innerHTML+=`
394
  <div class="flex gap-4 p-4 bg-slate-50 rounded-xl border border-slate-100 hover:bg-white hover:shadow-md transition-all">
 
404
  <div class="mt-2 flex gap-3 text-[10px] font-semibold text-slate-400 uppercase tracking-wide">
405
  <span><i class="fas fa-star text-amber-400 mr-1"></i> ${item.csat}/5</span>
406
  <span><i class="fas fa-clock text-blue-400 mr-1"></i> ${item.dur}s</span>
407
+ <span><i class="fas fa-coins text-green-500 mr-1"></i> ${costDisplay}đ</span>
408
  </div>
409
  </div>
410
  </div>
 
422
  function loadDashboard(period){
423
  const filteredData = filterData(period);
424
  const metrics = calculateMetrics(filteredData);
425
+
 
426
  document.getElementById('kpi-total-calls').innerText = metrics.totalCalls.toLocaleString();
427
  document.getElementById('kpi-cost').innerText = metrics.avgCost.toLocaleString();
428
  document.getElementById('kpi-aht').innerText = formatAHT(metrics.aht);
429
  document.getElementById('kpi-fcr').innerText = metrics.fcr;
430
  document.getElementById('fcr-bar').style.width = `${metrics.fcr}%`;
431
  document.getElementById('kpi-csat').innerText = metrics.avgCSAT;
432
+
433
  renderCharts(metrics, period);
434
  updateFeed();
435
  }
436
 
437
  document.addEventListener('DOMContentLoaded', ()=>{
 
438
  fetchRealData();
439
+ // Refresh mỗi 5s
440
+ setInterval(fetchRealData, 5000);
 
441
  });
442
  </script>
443