• דף הבית
    • אינדקס קישורים
    • פוסטים אחרונים
    • משתמשים
    • חיפוש בהגדרות המתקדמות
    • חיפוש גוגל בפורום
    • ניהול המערכת
    • ניהול המערכת - שרת private
    • הרשמה
    • התחברות

    תגובה | דוח ניתוח האזנה

    מתוזמן נעוץ נעול הועבר פורום מפתחים API
    1 פוסטים 1 כותבים 21 צפיות 1 עוקבים
    טוען פוסטים נוספים
    • מהישן לחדש
    • מהחדש לישן
    • הכי הרבה הצבעות
    תגובה
    • תגובה כנושא
    התחברו כדי לפרסם תגובה
    נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.
    • י מנותק
      יב
      נערך לאחרונה על ידי

      בנוגע למה שעשה @ע.ג. בדוח נתוני האזנה.
      עשיתי שינויים קלים לצרכיי, ואני משתף את זה.
      הוספה;

      1. שמות השלוחות.
      2. חיפוש חופשי (לא יודע אם זה נוגע לכולם, אבל לי אישית זה הועיל).
      3. קבצים/מאזינים מובילים.
      4. במידה ומישהו שמע כמה פעמים קובץ, יש אפשרות לראות את מספר הפעמים.
      5. חלוקה של הדוח לדפים (ברירת מחדל 20 שורות בדף, ניתן לשינוי)

      <!DOCTYPE html>
      <html lang="he" dir="rtl">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>מערכת ניתוח נתוני האזנה - Pro Dashboard</title>
          <!-- Tailwind CSS -->
          <script src="https://cdn.tailwindcss.com"></script>
          <!-- XLSX Library -->
          <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
          <!-- Lucide Icons -->
          <script src="https://unpkg.com/lucide@latest"></script>
          <style>
              @import url('https://fonts.googleapis.com/css2?family=Assistant:wght@300;400;600;700&display=swap');
              
              body {
                  font-family: 'Assistant', sans-serif;
                  background-color: #f8fafc;
                  color: #1e293b;
              }
      
              .glass-card {
                  background: rgba(255, 255, 255, 0.95);
                  backdrop-filter: blur(10px);
                  border: 1px solid rgba(255, 255, 255, 0.2);
                  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
              }
      
              .custom-scrollbar::-webkit-scrollbar { width: 6px; height: 6px; }
              .custom-scrollbar::-webkit-scrollbar-track { background: #f1f5f9; }
              .custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
              .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
      
              .animate-fade-in { animation: fadeIn 0.5s ease-out forwards; }
              @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
      
              .progress-fill { transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); }
              .bg-green-gradient { background: linear-gradient(90deg, #10b981, #34d399); }
              .bg-orange-gradient { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
              .bg-red-gradient { background: linear-gradient(90deg, #ef4444, #f87171); }
      
              .filter-row input, .filter-row select {
                  background: #ffffff;
                  border: 1px solid #e2e8f0;
                  font-size: 11px;
                  padding: 4px 6px;
                  border-radius: 4px;
                  width: 100%;
                  outline: none;
                  transition: all 0.2s;
              }
              .filter-row input:focus { border-color: #6366f1; box-shadow: 0 0 0 1px #6366f1; }
      
              .page-btn {
                  padding-left: 0.75rem;
                  padding-right: 0.75rem;
                  padding-top: 0.25rem;
                  padding-bottom: 0.25rem;
                  border-radius: 0.5rem;
                  border-width: 1px;
                  border-color: #e2e8f0;
                  font-size: 0.875rem;
                  line-height: 1.25rem;
                  font-weight: 500;
                  transition-property: all;
                  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
                  transition-duration: 150ms;
              }
              .page-btn:hover { background-color: #f8fafc; }
              .page-btn:disabled { opacity: 0.5; }
              .page-btn.active {
                  background-color: #4f46e5;
                  color: #ffffff;
                  border-color: #4f46e5;
              }
      
              .detail-row { background-color: #f8fafc; }
          </style>
      </head>
      <body class="min-h-screen pb-12">
      
          <!-- Header -->
          <nav class="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
              <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                  <div class="flex justify-between h-16 items-center">
                      <div class="flex items-center gap-3">
                          <div class="bg-indigo-600 p-2 rounded-lg shadow-lg shadow-indigo-200">
                              <i data-lucide="bar-chart-3" class="text-white w-6 h-6"></i>
                          </div>
                          <h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 to-violet-600">
                              מנתח נתוני האזנה Pro
                          </h1>
                      </div>
                      <div id="statusIndicator" class="text-sm font-medium text-slate-500 flex items-center gap-2">
                          <span class="w-2 h-2 rounded-full bg-slate-300"></span>
                          ממתין לפעולה
                      </div>
                  </div>
              </div>
          </nav>
      
          <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8 space-y-8">
              
              <!-- Info & Setup Section -->
              <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
                  
                  <!-- Setup Panel -->
                  <div class="lg:col-span-2 glass-card rounded-2xl p-6 animate-fade-in">
                      <div class="flex items-center gap-2 mb-6">
                          <i data-lucide="settings" class="text-indigo-600 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold">הגדרות שליפה</h2>
                      </div>
                      
                      <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                          <div class="space-y-2">
                              <label class="text-sm font-medium text-slate-700">מתאריך</label>
                              <input type="date" id="from-date" class="w-full px-4 py-2 rounded-xl border border-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none">
                          </div>
                          <div class="space-y-2">
                              <label class="text-sm font-medium text-slate-700">עד תאריך</label>
                              <input type="date" id="to-date" class="w-full px-4 py-2 rounded-xl border border-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none">
                          </div>
                          <div class="md:col-span-2 space-y-2">
                              <label class="text-sm font-medium text-slate-700">טוקן API</label>
                              <div class="flex gap-2">
                                  <div class="relative flex-1">
                                      <input type="password" id="api-token" placeholder="הזן טוקן..." class="w-full px-4 py-2 pr-10 rounded-xl border border-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none">
                                      <i data-lucide="key" class="absolute left-3 top-2.5 text-slate-400 w-4 h-4"></i>
                                  </div>
                                  <button onclick="clearToken()" class="px-3 py-2 text-rose-500 hover:bg-rose-50 rounded-xl transition-colors" title="נקה טוקן">
                                      <i data-lucide="trash-2" class="w-5 h-5"></i>
                                  </button>
                              </div>
                          </div>
                      </div>
      
                      <div class="mt-8 flex flex-wrap gap-4">
                          <button onclick="fetchData()" id="fetch-btn" class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-8 py-3 rounded-xl font-semibold transition-all shadow-lg shadow-indigo-200 active:scale-95">
                              <i data-lucide="download" class="w-5 h-5"></i>
                              משוך נתונים
                          </button>
                          <button onclick="exportToExcel()" id="export-btn" class="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-3 rounded-xl font-semibold transition-all shadow-lg shadow-emerald-200 active:scale-95 hidden">
                              <i data-lucide="file-spreadsheet" class="w-5 h-5"></i>
                              ייצא לאקסל
                          </button>
                      </div>
                  </div>
      
                  <!-- Important Info Box -->
                  <div class="glass-card rounded-2xl p-6 bg-indigo-50/50 border-indigo-100 animate-fade-in" style="animation-delay: 0.1s">
                      <div class="flex items-center gap-2 mb-4">
                          <i data-lucide="info" class="text-indigo-600 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold text-indigo-900">מידע חשוב</h2>
                      </div>
                      <div class="text-xs text-indigo-800 space-y-4 leading-relaxed">
                          <div class="bg-white/80 p-3 rounded-lg border border-indigo-100 shadow-sm">
                              <p class="font-bold mb-1 text-indigo-900">דרישות מערכת:</p>
                              <p>המערכת מיועדת למערכות בהן מוגדר שמירת דוח יומי. יש לוודא שבקובץ <code class="bg-indigo-100 px-1 rounded">ivr.ini</code> מופיעה ההגדרה:</p>
                              <code class="block mt-1 font-mono text-indigo-600 bg-indigo-100/50 p-1 rounded text-center font-bold">log_playback_play_stop=yes</code>
                          </div>
                          <div class="space-y-2 opacity-90">
                              <p class="flex items-start gap-2">
                                  <span class="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0"></span>
                                  <span>המערכת משקללת רצף האזנה אמיתי ומסננת כפילויות האזנה או דילוגים.</span>
                              </p>
                              <p class="flex items-start gap-2">
                                  <span class="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0"></span>
                                  <span>התוצאה משקפת זמן האזנה בפועל לתוכן הקובץ.</span>
                              </p>
                              <p class="flex items-start gap-2">
                                  <span class="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0"></span>
                                  <span>לא מומלץ להפיק דוחות בין 4 ל-5 לפנות בוקר (זמן עדכון שרתים).</span>
                              </p>
                          </div>
                      </div>
                  </div>
              </div>
      
              <!-- Stats Grid -->
              <div id="statsContainer" class="grid grid-cols-1 md:grid-cols-4 gap-6 hidden animate-fade-in">
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-indigo-500">
                      <p class="text-sm text-slate-500 font-medium">סה"כ רשומות</p>
                      <h3 id="statTotal" class="text-2xl font-bold mt-1">0</h3>
                  </div>
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-emerald-500">
                      <p class="text-sm text-slate-500 font-medium">ממוצע האזנה</p>
                      <h3 id="statAvg" class="text-2xl font-bold mt-1">0%</h3>
                  </div>
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-amber-500">
                      <p class="text-sm text-slate-500 font-medium">האזנה נטו (שעות)</p>
                      <h3 id="statNetHours" class="text-2xl font-bold mt-1">0</h3>
                  </div>
                  <div class="glass-card rounded-2xl p-5 border-r-4 border-rose-500">
                      <p class="text-sm text-slate-500 font-medium">האזנה ברוטו (שעות)</p>
                      <h3 id="statGrossHours" class="text-2xl font-bold mt-1">0</h3>
                  </div>
              </div>
      
              <!-- Top Files & Top Listeners -->
              <div id="insightsContainer" class="grid grid-cols-1 lg:grid-cols-2 gap-8 hidden animate-fade-in">
                  <!-- Top Files -->
                  <div class="glass-card rounded-2xl p-6">
                      <div class="flex items-center gap-2 mb-6">
                          <i data-lucide="trending-up" class="text-indigo-600 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold">קבצים מובילים בהאזנה</h2>
                      </div>
                      <div id="topFilesList" class="space-y-4">
                          <!-- Top files will be injected here -->
                      </div>
                  </div>
                  <!-- Top Listeners -->
                  <div class="glass-card rounded-2xl p-6">
                      <div class="flex items-center gap-2 mb-6">
                          <i data-lucide="award" class="text-amber-500 w-5 h-5"></i>
                          <h2 class="text-lg font-semibold text-slate-800">מאזינים מובילים (נטו)</h2>
                      </div>
                      <div id="topListenersList" class="space-y-4">
                          <!-- Top listeners will be injected here -->
                      </div>
                  </div>
              </div>
      
              <!-- Main Table Area -->
              <div class="glass-card rounded-2xl overflow-hidden animate-fade-in" style="animation-delay: 0.2s">
                  <div class="p-6 border-b border-slate-100 bg-slate-50/50 flex flex-wrap gap-4 items-end justify-between">
                      <div class="flex flex-wrap gap-4 items-end">
                          <div class="w-64 space-y-2">
                              <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">חיפוש חופשי</label>
                              <div class="relative">
                                  <input type="text" id="searchInput" oninput="applyMultiFilter()" placeholder="שם, טלפון, קובץ..." class="w-full px-4 py-2 pr-10 rounded-xl border border-slate-200 outline-none focus:ring-2 focus:ring-indigo-500">
                                  <i data-lucide="search" class="absolute left-3 top-2.5 text-slate-400 w-4 h-4"></i>
                              </div>
                          </div>
                          <div class="w-32 space-y-2">
                              <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">אחוז מינימלי</label>
                              <input type="number" id="filter-percent" oninput="applyMultiFilter()" class="w-full px-4 py-2 rounded-xl border border-slate-200 outline-none focus:ring-2 focus:ring-indigo-500" placeholder="0">
                          </div>
                      </div>
                      <div id="statFiltered" class="text-xs font-bold text-indigo-600 bg-indigo-50 px-3 py-1 rounded-full">0 רשומות מוצגות</div>
                  </div>
      
                  <div class="custom-scrollbar overflow-x-auto">
                      <table id="results-table" class="w-full text-right border-collapse hidden">
                          <thead>
                              <tr id="header-row" class="bg-slate-50 text-slate-500 text-[11px] font-bold uppercase tracking-wider">
                                  <!-- Dynamic Headers -->
                              </tr>
                              <tr id="filter-row" class="filter-row bg-white border-b border-slate-100">
                                  <!-- Dynamic Filters -->
                              </tr>
                          </thead>
                          <tbody id="table-body" class="divide-y divide-slate-100 text-sm">
                              <!-- Results -->
                          </tbody>
                      </table>
                      <div id="emptyState" class="p-20 text-center text-slate-400">
                          <div class="flex flex-col items-center gap-4">
                              <i data-lucide="database" class="w-12 h-12 opacity-20"></i>
                              <p class="text-lg">טרם נטענו נתונים</p>
                          </div>
                      </div>
                  </div>
      
                  <!-- Pagination -->
                  <div id="pagination-container" class="px-6 py-4 bg-slate-50 border-t border-slate-200 flex flex-wrap items-center justify-between gap-4 hidden">
                      <div class="flex items-center gap-4">
                          <span class="text-sm text-slate-500">הצג</span>
                          <select id="rows-per-page" onchange="updateRowsPerPage()" class="px-2 py-1 rounded border border-slate-200 text-sm outline-none">
                              <option value="10">10</option>
                              <option value="20" selected>20</option>
                              <option value="50">50</option>
                              <option value="100">100</option>
                          </select>
                      </div>
                      <div class="flex items-center gap-2" id="page-numbers"></div>
                      <div class="text-sm text-slate-500" id="pagination-info"></div>
                  </div>
              </div>
          </main>
      
          <!-- Loading Overlay -->
          <div id="loadingOverlay" class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[100] flex items-center justify-center hidden">
              <div class="bg-white rounded-2xl p-8 flex flex-col items-center gap-4 shadow-2xl max-w-sm w-full mx-4">
                  <div class="w-16 h-16 border-4 border-indigo-100 border-t-indigo-600 rounded-full animate-spin"></div>
                  <div class="text-center">
                      <h3 class="text-lg font-bold text-slate-800">מעבד נתונים...</h3>
                      <p id="loadingStatus" class="text-sm text-slate-500 mt-1">מתחבר לשרת</p>
                  </div>
                  <div class="w-full bg-slate-100 rounded-full h-2 mt-2 overflow-hidden">
                      <div id="loadingProgress" class="bg-indigo-600 h-full transition-all duration-300" style="width: 0%"></div>
                  </div>
              </div>
          </div>
      
          <script>
              // --- Initialization ---
              lucide.createIcons();
              
              // Helper: Remove Hebrew Nikud (diacritics)
              function removeNikud(str) {
                  if (!str) return "";
                  return str.replace(/[\u0591-\u05C7]/g, "");
              }
      
              // Default dates: From 7 days ago to today
              const todayDate = new Date();
              const lastWeekDate = new Date();
              lastWeekDate.setDate(todayDate.getDate() - 7);
              
              document.getElementById('from-date').value = lastWeekDate.toISOString().split('T')[0];
              document.getElementById('to-date').value = todayDate.toISOString().split('T')[0];
      
              const savedToken = localStorage.getItem('api_token_cached');
              if (savedToken) document.getElementById('api-token').value = savedToken;
      
              let currentData = [];
              let filteredData = [];
              let sortDirections = Array(12).fill(true);
              let hasEnterId = true;
              let currentPage = 1;
              let rowsPerPage = 20;
              let expandedRows = new Set();
              const extensionTitlesCache = {};
      
              // --- Core Logic ---
      
              function clearToken() {
                  if (confirm("האם למחוק את הטוקן השמור?")) {
                      localStorage.removeItem('api_token_cached');
                      document.getElementById('api-token').value = '';
                  }
              }
      
              async function fetchData() {
                  const fromDate = document.getElementById('from-date').value;
                  const toDate = document.getElementById('to-date').value;
                  const token = document.getElementById('api-token').value;
                  
                  if (!fromDate || !toDate || !token) { alert("נא להזין את כל הפרטים"); return; }
                  localStorage.setItem('api_token_cached', token);
      
                  const overlay = document.getElementById('loadingOverlay');
                  const statusText = document.getElementById('loadingStatus');
                  const progressBar = document.getElementById('loadingProgress');
                  overlay.classList.remove('hidden');
      
                  const dates = getDatesRange(new Date(fromDate), new Date(toDate));
                  const dataMap = {};
                  hasEnterId = true;
      
                  try {
                      for (let i = 0; i < dates.length; i++) {
                          const date = dates[i];
                          statusText.innerText = `מושך נתונים: ${date}`;
                          progressBar.style.width = `${Math.round(((i + 1) / dates.length) * 100)}%`;
      
                          const url = `https://www.call2all.co.il/ym/api/RenderYMGRFile?token=${token}&wath=ivr2:/Log/LogPlaybackPlayStop/LogPlaybackPlayStop.${date}.ymgr&convertType=csv&notLoadLang=1`;
                          const response = await fetch(url);
                          if (response.ok) {
                              const csv = await response.text();
                              processCsvSegment(csv, dataMap, date);
                          }
                      }
      
                      // Fetch Extension Titles
                      const uniquePaths = [...new Set(Object.values(dataMap).map(item => item.info.Folder))];
                      await fetchExtensionTitles(uniquePaths, token);
      
                      finalizeData(dataMap);
                  } catch (err) {
                      alert(`שגיאה: ${err.message}`);
                  } finally {
                      overlay.classList.add('hidden');
                  }
              }
      
              async function fetchExtensionTitles(paths, token) {
                  const statusText = document.getElementById('loadingStatus');
                  const parents = new Set();
                  
                  paths.forEach(p => {
                      if (!p) return;
                      const parts = p.split('/').filter(x => x);
                      if (parts.length === 0) return;
                      
                      if (parts.length === 1) {
                          parents.add("/");
                      } else {
                          parts.pop();
                          parents.add(parts.join('/'));
                      }
                  });
      
                  const parentPaths = Array.from(parents);
                  for (let i = 0; i < parentPaths.length; i++) {
                      const pPath = parentPaths[i];
                      statusText.innerText = `מעדכן שמות שלוחות: ${pPath}`;
                      try {
                          const url = `https://www.call2all.co.il/ym/api/GetIVR2Dir?token=${token}&path=${pPath}`;
                          const response = await fetch(url);
                          if (response.ok) {
                              const res = await response.json();
                              if (res.dirs && Array.isArray(res.dirs)) {
                                  res.dirs.forEach(d => {
                                      const fullPath = (pPath === "/" || pPath === "") ? d.name : `${pPath}/${d.name}`;
                                      if (d.extTitle) {
                                          extensionTitlesCache[fullPath] = d.extTitle;
                                      }
                                  });
                              }
                          }
                      } catch (e) {
                          console.error(`Error fetching titles for ${pPath}:`, e);
                      }
                  }
              }
      
              function processCsvSegment(text, dataMap, date) {
                  const rows = text.replace(/^\uFEFF/, "").trim().split(/\r?\n/).map(r => r.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/));
                  if (rows.length < 2) return;
                  const headers = rows[0].map(h => h.replace(/"/g, '').trim());
      
                  if (!headers.includes("EnterId")) hasEnterId = false;
      
                  for (let i = 1; i < rows.length; i++) {
                      let row = {};
                      headers.forEach((h, idx) => row[h] = (rows[i][idx] || "").replace(/"/g, '').trim());
                      if (!row["FileLength"] || row["FileLength"] === "0:0:0") continue;
      
                      // Normalize folder path
                      row["Folder"] = (row["Folder"] || "").replace(/^\/+|\/+$/g, '');
      
                      const flenSec = timeToSeconds(row["FileLength"]);
                      const identityKey = hasEnterId ? row["EnterId"] : row["Phone"];
                      const key = `${identityKey}_${row["Folder"]}_${row["Current"]}_${row["FileLength"]}`;
                      
                      if (!dataMap[key]) {
                          dataMap[key] = { info: row, intervals: [], flenSec: flenSec, rawSessions: [] };
                      }
      
                      let start = (parseInt(row["PositionPlay"]) || 0) / 1000;
                      let stop = (isNaN(parseInt(row["PositionStop"])) || parseInt(row["PositionStop"]) === -1) ? flenSec : parseInt(row["PositionStop"]) / 1000;
                      
                      dataMap[key].intervals.push([start, stop]);
                      dataMap[key].rawSessions.push({
                          date: date,
                          start: start,
                          stop: stop,
                          duration: stop - start,
                          time: row["Time"] || ""
                      });
                  }
              }
      
              function finalizeData(dataMap) {
                  currentData = Object.values(dataMap).map(item => {
                      const netSec = mergeIntervals(item.intervals);
                      const grossSec = item.rawSessions.reduce((acc, s) => acc + s.duration, 0);
                      
                      // Find last session
                      const last = item.rawSessions.reduce((prev, current) => {
                          const prevDt = `${prev.date} ${prev.time}`;
                          const currDt = `${current.date} ${current.time}`;
                          return currDt > prevDt ? current : prev;
                      });
      
                      return {
                          ...item,
                          netSec: netSec,
                          grossSec: grossSec,
                          lastDate: last.date,
                          lastTime: last.time,
                          percent: item.flenSec > 0 ? Math.min(100, Math.round((netSec / item.flenSec) * 100)) : 0
                      };
                  });
                  
                  if (currentData.length > 0) {
                      setupTableStructure();
                      updateStats();
                      updateInsights();
                      applyMultiFilter();
                      
                      document.getElementById('results-table').classList.remove('hidden');
                      document.getElementById('emptyState').classList.add('hidden');
                      document.getElementById('statsContainer').classList.remove('hidden');
                      document.getElementById('insightsContainer').classList.remove('hidden');
                      document.getElementById('export-btn').classList.remove('hidden');
                      document.getElementById('pagination-container').classList.remove('hidden');
                      document.getElementById('statusIndicator').innerHTML = `<span class="w-2 h-2 rounded-full bg-emerald-500"></span> נתונים נטענו`;
                  } else {
                      alert("לא נמצאו נתונים לטווח זה");
                  }
              }
      
              function updateInsights() {
                  // Top Files
                  const fileStats = {};
                  currentData.forEach(d => {
                      const key = `${d.info.Folder} > ${d.info.Current}`;
                      if (!fileStats[key]) fileStats[key] = { name: d.info.Current, folder: d.info.Folder, totalNet: 0, count: 0 };
                      fileStats[key].totalNet += d.netSec;
                      fileStats[key].count++;
                  });
                  const topFiles = Object.values(fileStats).sort((a, b) => b.totalNet - a.totalNet).slice(0, 5);
                  const maxFileNet = topFiles[0]?.totalNet || 1;
                  document.getElementById('topFilesList').innerHTML = topFiles.map(f => {
                      const folderTitle = extensionTitlesCache[f.folder] ? ` (${extensionTitlesCache[f.folder]})` : '';
                      return `
                      <div class="space-y-1">
                          <div class="flex justify-between text-sm">
                              <span class="font-bold text-slate-700 truncate ml-4">${f.name}</span>
                              <span class="text-slate-400 font-mono text-xs">${formatSeconds(f.totalNet)}</span>
                          </div>
                          <div class="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
                              <div class="bg-indigo-500 h-full" style="width: ${(f.totalNet / maxFileNet) * 100}%"></div>
                          </div>
                          <div class="text-[10px] text-slate-400">${f.folder}${folderTitle} | ${f.count} מאזינים</div>
                      </div>
                  `}).join('');
      
                  // Top Listeners
                  const listenerStats = {};
                  currentData.forEach(d => {
                      const id = hasEnterId ? d.info.EnterId : d.info.Phone;
                      const name = hasEnterId ? d.info.ValName : d.info.Phone;
                      if (!listenerStats[id]) listenerStats[id] = { name: name, id: id, totalNet: 0 };
                      listenerStats[id].totalNet += d.netSec;
                  });
                  const topListeners = Object.values(listenerStats).sort((a, b) => b.totalNet - a.totalNet).slice(0, 5);
                  const maxListenerNet = topListeners[0]?.totalNet || 1;
                  document.getElementById('topListenersList').innerHTML = topListeners.map(l => `
                      <div class="space-y-1">
                          <div class="flex justify-between text-sm">
                              <div class="flex flex-col">
                                  <span class="font-bold text-slate-700">${l.name}</span>
                                  <span class="text-[10px] text-slate-400 font-mono">${l.id}</span>
                              </div>
                              <span class="text-amber-600 font-mono text-xs font-bold">${formatSeconds(l.totalNet)}</span>
                          </div>
                          <div class="w-full bg-slate-50 h-2 rounded-full overflow-hidden">
                              <div class="bg-amber-400 h-full" style="width: ${(l.totalNet / maxListenerNet) * 100}%"></div>
                          </div>
                      </div>
                  `).join('');
              }
      
              function setupTableStructure() {
                  const headerRow = document.getElementById('header-row');
                  const filterRow = document.getElementById('filter-row');
                  
                  const commonHeaders = `
                      <th class="px-4 py-3 cursor-pointer" onclick="sortTable('folder')">שלוחה ↕</th>
                      <th class="px-4 py-3 cursor-pointer" onclick="sortTable('file')">קובץ ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('last')">האזנה אחרונה ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('length')">אורך ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('net')">נטו ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('gross')">ברוטו ↕</th>
                      <th class="px-4 py-3 text-center cursor-pointer" onclick="sortTable('percent')">אחוז ↕</th>
                      <th class="px-4 py-3 text-center">פעולות</th>
                  `;
                  
                  const commonFilters = `
                      <th class="px-2 py-2"><input type="text" id="f-folder" oninput="applyMultiFilter()" placeholder="סנן שלוחה..."></th>
                      <th class="px-2 py-2"><input type="text" id="f-file" oninput="applyMultiFilter()" placeholder="סנן קובץ..."></th>
                      <th></th><th></th><th></th><th></th><th></th><th></th>
                  `;
      
                  if (hasEnterId) {
                      headerRow.innerHTML = `<th class="px-4 py-3 cursor-pointer" onclick="sortTable('school')">כיתה ↕</th><th class="px-4 py-3 cursor-pointer" onclick="sortTable('name')">שם וזיהוי ↕</th>` + commonHeaders;
                      filterRow.innerHTML = `<th class="px-2 py-2"><input type="text" id="f-school" oninput="applyMultiFilter()" placeholder="סנן כיתה..."></th><th class="px-2 py-2"><input type="text" id="f-name" oninput="applyMultiFilter()" placeholder="סנן שם או זיהוי..."></th>` + commonFilters;
                  } else {
                      headerRow.innerHTML = `<th class="px-4 py-3 cursor-pointer" onclick="sortTable('name')">טלפון ↕</th>` + commonHeaders;
                      filterRow.innerHTML = `<th class="px-2 py-2"><input type="text" id="f-name" oninput="applyMultiFilter()" placeholder="סנן טלפון..."></th>` + commonFilters;
                  }
              }
      
              function renderTable() {
                  const tbody = document.getElementById('table-body');
                  const totalItems = filteredData.length;
                  const startIdx = (currentPage - 1) * rowsPerPage;
                  const pageData = filteredData.slice(startIdx, startIdx + rowsPerPage);
      
                  let html = '';
                  pageData.forEach((item, idx) => {
                      const colorClass = item.percent > 80 ? 'bg-green-gradient' : (item.percent > 40 ? 'bg-orange-gradient' : 'bg-red-gradient');
                      const textClass = item.percent > 80 ? 'text-emerald-600' : (item.percent > 40 ? 'text-amber-600' : 'text-rose-600');
                      const rowId = `row-${startIdx + idx}`;
                      const isExpanded = expandedRows.has(rowId);
                      const hasDuplicates = item.rawSessions.length > 1;
      
                      let identityCell = '';
                      if (hasEnterId) {
                          identityCell = `<td class="px-4 py-3 text-slate-500">${item.info.School || '-'}</td>
                                         <td class="px-4 py-3">
                                             <div class="font-bold text-slate-800">${item.info.ValName}</div>
                                             <div class="text-[10px] text-slate-400">${item.info.EnterId}</div>
                                         </td>`;
                      } else {
                          identityCell = `<td class="px-4 py-3 font-bold text-slate-800">${item.info.Phone}</td>`;
                      }
      
                      html += `
                          <tr class="hover:bg-slate-50 transition-colors">
                              ${identityCell}
                              <td class="px-4 py-3">
                                  <div class="flex flex-col">
                                      <span class="bg-slate-100 px-2 py-1 rounded text-[10px] font-bold text-slate-500 w-fit">${item.info.Folder}</span>
                                      ${extensionTitlesCache[item.info.Folder] ? `<span class="text-[10px] text-indigo-600 font-medium mt-1 leading-tight">${extensionTitlesCache[item.info.Folder]}</span>` : ''}
                                  </div>
                              </td>
                              <td class="px-4 py-3 text-slate-600 truncate max-w-[150px]">${item.info.Current}</td>
                              <td class="px-4 py-3 text-center">
                                  <div class="text-[11px] font-bold text-slate-700">${item.lastDate}</div>
                                  <div class="text-[10px] text-slate-400 font-mono">${item.lastTime}</div>
                              </td>
                              <td class="px-4 py-3 text-center font-mono text-slate-400">${formatSeconds(item.flenSec)}</td>
                              <td class="px-4 py-3 text-center font-mono text-indigo-600 font-bold">${formatSeconds(item.netSec)}</td>
                              <td class="px-4 py-3 text-center font-mono text-slate-400 text-xs">${formatSeconds(item.grossSec)}</td>
                              <td class="px-4 py-3">
                                  <div class="flex items-center gap-3 min-w-[100px]">
                                      <div class="flex-1 bg-slate-100 rounded-full h-1.5 overflow-hidden">
                                          <div class="progress-fill h-full ${colorClass}" style="width: ${item.percent}%"></div>
                                      </div>
                                      <span class="text-xs font-bold ${textClass}">${item.percent}%</span>
                                  </div>
                              </td>
                              <td class="px-4 py-3 text-center">
                                  <button onclick="toggleRow('${rowId}')" class="flex items-center gap-1 mx-auto px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all shadow-sm ${hasDuplicates ? 'bg-indigo-600 text-white hover:bg-indigo-700' : 'bg-slate-100 text-slate-300 cursor-default'}">
                                      <i data-lucide="${isExpanded ? 'chevron-up' : 'list-plus'}" class="w-3.5 h-3.5"></i>
                                      ${isExpanded ? 'סגור' : 'פירוט האזנות'}
                                  </button>
                              </td>
                          </tr>
                      `;
      
                      if (isExpanded) {
                          html += `
                              <tr class="detail-row animate-fade-in">
                                  <td colspan="12" class="px-8 py-4">
                                      <div class="bg-white rounded-xl border border-indigo-100 shadow-sm overflow-hidden">
                                          <div class="bg-indigo-50 px-4 py-2 text-[10px] font-bold text-indigo-600 border-b border-indigo-100">פירוט האזנות כפולות / חוזרות</div>
                                          <table class="w-full text-[11px] text-right">
                                              <thead class="bg-slate-50 text-slate-400 font-bold uppercase">
                                                  <tr>
                                                      <th class="px-4 py-2">תאריך</th>
                                                      <th class="px-4 py-2">שעה</th>
                                                      <th class="px-4 py-2">התחלה בקובץ</th>
                                                      <th class="px-4 py-2">סיום בקובץ</th>
                                                      <th class="px-4 py-2">משך האזנה</th>
                                                  </tr>
                                              </thead>
                                              <tbody class="divide-y divide-slate-50">
                                                  ${item.rawSessions.map(s => `
                                                      <tr>
                                                          <td class="px-4 py-2">${s.date}</td>
                                                          <td class="px-4 py-2">${s.time}</td>
                                                          <td class="px-4 py-2 font-mono">${formatSeconds(s.start)}</td>
                                                          <td class="px-4 py-2 font-mono">${formatSeconds(s.stop)}</td>
                                                          <td class="px-4 py-2 font-bold text-indigo-600">${formatSeconds(s.duration)}</td>
                                                      </tr>
                                                  `).join('')}
                                              </tbody>
                                          </table>
                                      </div>
                                  </td>
                              </tr>
                          `;
                      }
                  });
                  
                  tbody.innerHTML = html || `<tr><td colspan="12" class="p-10 text-center text-slate-400">אין תוצאות</td></tr>`;
                  renderPaginationControls(totalItems, Math.ceil(totalItems / rowsPerPage));
                  lucide.createIcons();
              }
      
              function toggleRow(id) {
                  if (expandedRows.has(id)) expandedRows.delete(id);
                  else expandedRows.add(id);
                  renderTable();
              }
      
              function applyMultiFilter() {
                  const search = removeNikud(document.getElementById('searchInput').value.toLowerCase());
                  const minP = parseInt(document.getElementById('filter-percent').value) || 0;
                  
                  const fSchool = removeNikud(document.getElementById('f-school')?.value.toLowerCase() || "");
                  const fName = removeNikud(document.getElementById('f-name')?.value.toLowerCase() || "");
                  const fFolder = removeNikud(document.getElementById('f-folder')?.value.toLowerCase() || "");
                  const fFile = removeNikud(document.getElementById('f-file')?.value.toLowerCase() || "");
      
                  filteredData = currentData.filter(item => {
                      const tSchool = removeNikud((item.info.School || "").toLowerCase());
                      const tName = removeNikud((hasEnterId ? (item.info.ValName || "") : item.info.Phone).toLowerCase());
                      const tFolder = removeNikud(item.info.Folder.toLowerCase());
                      const tFolderTitle = removeNikud((extensionTitlesCache[item.info.Folder] || "").toLowerCase());
                      const tFile = removeNikud(item.info.Current.toLowerCase());
                      const tId = removeNikud((item.info.EnterId || "").toLowerCase());
                      
                      const matchSearch = tName.includes(search) || tFolder.includes(search) || tFile.includes(search) || tId.includes(search) || tFolderTitle.includes(search);
                      const matchPercent = item.percent >= minP;
                      
                      const matchSpecific = tSchool.includes(fSchool) && 
                                          (tName.includes(fName) || tId.includes(fName)) && 
                                          (tFolder.includes(fFolder) || tFolderTitle.includes(fFolder)) && 
                                          tFile.includes(fFile);
      
                      return matchSearch && matchPercent && matchSpecific;
                  });
      
                  document.getElementById('statFiltered').innerText = `${filteredData.length.toLocaleString()} רשומות מוצגות`;
                  currentPage = 1;
                  renderTable();
              }
      
              function updateStats() {
                  const total = currentData.length;
                  const avg = total > 0 ? Math.round(currentData.reduce((s, d) => s + d.percent, 0) / total) : 0;
                  const netSec = currentData.reduce((s, d) => s + d.netSec, 0);
                  const grossSec = currentData.reduce((s, d) => s + d.grossSec, 0);
      
                  document.getElementById('statTotal').innerText = total.toLocaleString();
                  document.getElementById('statAvg').innerText = `${avg}%`;
                  document.getElementById('statNetHours').innerText = Math.round(netSec / 3600).toLocaleString();
                  document.getElementById('statGrossHours').innerText = Math.round(grossSec / 3600).toLocaleString();
              }
      
              function renderPaginationControls(totalItems, totalPages) {
                  const container = document.getElementById('page-numbers');
                  const info = document.getElementById('pagination-info');
                  if (totalItems === 0) { container.innerHTML = ''; info.innerText = ''; return; }
      
                  info.innerText = `מציג ${((currentPage-1)*rowsPerPage)+1}-${Math.min(currentPage*rowsPerPage, totalItems)} מתוך ${totalItems}`;
                  let btns = `<button onclick="changePage(${currentPage-1})" class="page-btn" ${currentPage===1?'disabled':''}><i data-lucide="chevron-right" class="w-4 h-4"></i></button>`;
                  for (let i = 1; i <= totalPages; i++) {
                      if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
                          btns += `<button onclick="changePage(${i})" class="page-btn ${i===currentPage?'active':''}">${i}</button>`;
                      } else if (i === currentPage - 3 || i === currentPage + 3) {
                          btns += `<span class="px-1 text-slate-300">...</span>`;
                      }
                  }
                  btns += `<button onclick="changePage(${currentPage+1})" class="page-btn" ${currentPage===totalPages?'disabled':''}><i data-lucide="chevron-left" class="w-4 h-4"></i></button>`;
                  container.innerHTML = btns;
              }
      
              function changePage(p) { currentPage = p; renderTable(); }
              function updateRowsPerPage() { rowsPerPage = parseInt(document.getElementById('rows-per-page').value); currentPage = 1; renderTable(); }
      
              function sortTable(key) {
                  const idx = ['school','name','folder','file','last','length','net','gross','percent'].indexOf(key);
                  sortDirections[idx] = !sortDirections[idx];
                  const dir = sortDirections[idx] ? 1 : -1;
                  
                  currentData.sort((a, b) => {
                      let v1, v2;
                      if (key === 'school') { v1 = a.info.School || ""; v2 = b.info.School || ""; }
                      else if (key === 'name') { v1 = (hasEnterId ? a.info.ValName : a.info.Phone) || ""; v2 = (hasEnterId ? b.info.ValName : b.info.Phone) || ""; }
                      else if (key === 'folder') { v1 = a.info.Folder; v2 = b.info.Folder; }
                      else if (key === 'file') { v1 = a.info.Current; v2 = b.info.Current; }
                      else if (key === 'last') { v1 = `${a.lastDate} ${a.lastTime}`; v2 = `${b.lastDate} ${b.lastTime}`; }
                      else if (key === 'length') { v1 = a.flenSec; v2 = b.flenSec; }
                      else if (key === 'net') { v1 = a.netSec; v2 = b.netSec; }
                      else if (key === 'gross') { v1 = a.grossSec; v2 = b.grossSec; }
                      else if (key === 'percent') { v1 = a.percent; v2 = b.percent; }
                      return v1 < v2 ? -1 * dir : (v1 > v2 ? 1 * dir : 0);
                  });
                  applyMultiFilter();
              }
      
              function timeToSeconds(t){
                  const p = t.trim().split(':').map(Number);
                  return p.length === 3 ? p[0]*3600 + p[1]*60 + p[2] : (p.length === 2 ? p[0]*60 + p[1] : 0);
              }
              function formatSeconds(s){
                  let h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sec=Math.floor(s%60);
                  return `${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:${sec.toString().padStart(2,'0')}`;
              }
              function mergeIntervals(arr){
                  if(!arr.length) return 0;
                  arr.sort((a,b) => a[0]-b[0]);
                  let total = 0, [start, end] = arr[0];
                  for(let i=1; i<arr.length; i++){
                      if(arr[i][0] <= end) end = Math.max(end, arr[i][1]);
                      else { total += (end - start); [start, end] = arr[i]; }
                  }
                  return total + (end - start);
              }
              function getDatesRange(s,e){
                  const d=[]; let c=new Date(s);
                  while(c<=e){ d.push(c.toISOString().split('T')[0]); c.setDate(c.getDate()+1); }
                  return d;
              }
      
              function exportToExcel() {
                  const exportedData = filteredData.map(item => {
                      let obj = {};
                      if (hasEnterId) {
                          obj["כיתה"] = item.info.School || '-';
                          obj["שם תלמיד"] = item.info.ValName || '';
                          obj["זיהוי"] = item.info.EnterId || '';
                          obj["שלוחה"] = item.info.Folder + (extensionTitlesCache[item.info.Folder] ? ` (${extensionTitlesCache[item.info.Folder]})` : '');
                          obj["שם הקובץ"] = item.info.Current;
                          obj["תאריך האזנה אחרונה"] = item.lastDate;
                          obj["שעת האזנה אחרונה"] = item.lastTime;
                          obj["אורך קובץ"] = formatSeconds(item.flenSec);
                          obj["האזנה נטו"] = formatSeconds(item.netSec);
                          obj["האזנה ברוטו"] = formatSeconds(item.grossSec);
                          obj["אחוז האזנה"] = item.percent + '%';
                      } else {
                          obj["טלפון"] = item.info.Phone;
                          obj["שלוחה"] = item.info.Folder + (extensionTitlesCache[item.info.Folder] ? ` (${extensionTitlesCache[item.info.Folder]})` : '');
                          obj["שם הקובץ"] = item.info.Current;
                          obj["תאריך האזנה אחרונה"] = item.lastDate;
                          obj["שעת האזנה אחרונה"] = item.lastTime;
                          obj["אורך קובץ"] = formatSeconds(item.flenSec);
                          obj["האזנה נטו"] = formatSeconds(item.netSec);
                          obj["האזנה ברוטו"] = formatSeconds(item.grossSec);
                          obj["אחוז האזנה"] = item.percent + '%';
                      }
                      return obj;
                  });
                  const ws = XLSX.utils.json_to_sheet(exportedData);
                  const wb = XLSX.utils.book_new();
                  XLSX.utils.book_append_sheet(wb, ws, "דוח האזנה");
                  XLSX.writeFile(wb, `דוח_האזנה_${new Date().toISOString().slice(0,10)}.xlsx`);
              }
          </script>
      </body>
      </html>
      
      

      תגובה 1 תגובה אחרונה תגובה ציטוט 1
      • פוסט ראשון
        פוסט אחרון