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

    דוח נתוני האזנה

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

      מצורף קוד משוכלל למעקב אחר נתוני האזנה של התלמידים במרחב הקולי
      המערכת משקללת את רצף ההאזנה האמיתי ומסננת כפילויות האזנה או דילוגים, כך שהתוצאה משקפת את זמן האזנה לקובץ בפועל, ולא את משך הזמן שבו הקובץ היה בהשמעה
      מומלץ מאד עבור מוסדות המשתמשים במרחב הקולי
      הקוד בספוילר
      יש להעתיק את הקוד, ולהדביק ב"פנקס רשימות", ולשמור בשם ניתוח_נתוני_האזנה.html

      <!DOCTYPE html>
      <html lang="he" dir="rtl">
      <head>
          <meta charset="UTF-8">
          <title>ניתוח נתוני האזנה מתקדם</title>
          <style>
              :root {
                  --primary-color: #1a73e8;
                  --primary-hover: #1557b0;
                  --success-color: #28a745;
                  --bg-color: #f0f2f5;
                  --card-bg: #ffffff;
                  --border-color: #e4e6eb;
                  --text-main: #1c1e21;
                  --text-secondary: #65676b;
              }
      
              body { 
                  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; 
                  background-color: var(--bg-color); 
                  margin: 20px; 
                  color: var(--text-main);
                  line-height: 1.5;
              }
      
              .container { 
                  max-width: 1400px; 
                  margin: auto; 
                  background: var(--card-bg); 
                  padding: 30px; 
                  border-radius: 12px; 
                  box-shadow: 0 8px 24px rgba(0,0,0,0.08); 
              }
      
              h2 { 
                  color: var(--primary-color); 
                  text-align: center; 
                  margin-bottom: 10px; 
                  font-weight: 600;
                  font-size: 1.8rem;
              }
      
              .instructions {
                  text-align: center;
                  color: var(--text-secondary);
                  font-size: 14px;
                  margin-bottom: 30px;
                  line-height: 1.6;
              }
              
              .setup-panel { 
                  background: #f8f9fa; 
                  padding: 20px; 
                  border-radius: 10px; 
                  border: 1px solid var(--border-color); 
                  margin-bottom: 25px; 
                  display: flex; 
                  gap: 20px; 
                  align-items: flex-end; 
                  flex-wrap: wrap; 
              }
      
              .form-group { 
                  display: flex; 
                  flex-direction: column; 
                  gap: 8px; 
                  flex: 1; 
                  min-width: 160px; 
              }
      
              /* עיצוב חדש לשדה הטוקן עם כפתור המחיקה */
              .token-input-wrapper {
                  display: flex;
                  border: 1px solid #dddfe2;
                  border-radius: 6px;
                  overflow: hidden;
                  background: white;
                  transition: focus-within 0.2s;
              }
              
              .token-input-wrapper:focus-within {
                  border-color: var(--primary-color); 
                  box-shadow: 0 0 0 2px rgba(26,115,232,0.15); 
              }
      
              .token-input-wrapper input {
                  border: none !important;
                  box-shadow: none !important;
                  flex-grow: 1;
                  padding: 10px 14px;
              }
      
              #clear-token-btn {
                  background-color: #f8f9fa;
                  color: #65676b;
                  border: none;
                  border-right: 1px solid #dddfe2;
                  min-width: 40px;
                  cursor: pointer;
                  font-size: 16px;
                  display: flex;
                  align-items: center;
                  justify-content: center;
              }
      
              #clear-token-btn:hover {
                  background-color: #fee2e2;
                  color: #dc3545;
              }
      
              label { font-weight: 600; font-size: 0.9rem; color: var(--text-secondary); }
      
              input, select, button { 
                  padding: 10px 14px; 
                  border: 1px solid #dddfe2; 
                  border-radius: 6px; 
                  font-size: 14px; 
                  outline: none; 
                  transition: all 0.2s;
              }
              
              button { 
                  background-color: var(--primary-color); 
                  color: white; 
                  border: none; 
                  cursor: pointer; 
                  font-weight: 600; 
                  min-width: 130px; 
              }
      
              button:hover { 
                  background-color: var(--primary-hover); 
                  transform: translateY(-1px); 
              }
      
              #export-btn { background-color: var(--success-color); }
              #export-btn:hover { background-color: #218838; }
      
              .table-wrapper { 
                  overflow-x: auto; 
                  margin-top: 20px; 
                  border-radius: 8px; 
                  border: 1px solid var(--border-color); 
              }
      
              table { 
                  width: 100%; 
                  border-collapse: collapse; 
                  background: white; 
                  min-width: 1100px; 
              }
      
              th { 
                  background-color: #f8f9fa; 
                  color: var(--text-secondary); 
                  padding: 15px 12px; 
                  text-align: right; 
                  position: sticky; 
                  top: 0; 
                  z-index: 10; 
                  border-bottom: 2px solid #dee2e6;
                  cursor: pointer;
                  user-select: none;
              }
      
              td { 
                  padding: 12px; 
                  border-bottom: 1px solid #eff2f5; 
                  font-size: 14px; 
                  vertical-align: middle; 
              }
      
              tr:hover { background-color: #f8faff; }
      
              .filter-row { background-color: #ffffff; }
              .filter-row input, .filter-row select { 
                  width: 100%; 
                  padding: 6px; 
                  font-size: 13px; 
                  border: 1px solid #e1e4e8; 
                  background: #fafafa; 
                  box-sizing: border-box;
              }
      
              .progress-container { 
                  display: flex; 
                  align-items: center; 
                  gap: 10px; 
                  min-width: 140px; 
              }
      
              .progress-bar { 
                  background: #e9ecef; 
                  border-radius: 20px; 
                  flex-grow: 1; 
                  height: 10px; 
                  overflow: hidden; 
              }
      
              .progress-fill { 
                  height: 100%; 
                  border-radius: 20px; 
                  transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); 
              }
      
              .bg-green { background: linear-gradient(90deg, #28a745, #34ce57); }
              .bg-orange { background: linear-gradient(90deg, #ffc107, #ffca2c); }
              .bg-red { background: linear-gradient(90deg, #dc3545, #ea4335); }
      
              #status-msg { 
                  margin: 15px 0; 
                  font-weight: 500; 
                  text-align: center; 
                  padding: 12px; 
                  border-radius: 6px; 
                  min-height: 20px;
              }
      
              .loading { color: var(--primary-color); background: #e8f0fe; display: block; }
              .error { color: #d93025; background: #fce8e6; display: block; }
              
              small { color: var(--text-secondary); display: block; margin-top: 2px; }
          </style>
      </head>
      <body>
      
      <div class="container">
          <h2>מערכת ניתוח האזנה מתקדמת</h2>
          
          <div class="instructions">
              נא להזין את טווח התאריכים המבוקש ואת ה-Token של המערכת. (הטוקן נשמר במטמון של הדפדפן הנוכחי (ניתן למחוק), ואינו מועבר לשום מקום אחר).<br>
              ניתן לסנן את התוצאות, ולייצא את הנתונים המצומצמים לקובץ Excel.<br>
              המערכת משקללת את רצף ההאזנה האמיתי ומסננת כפילויות האזנה או דילוגים, כך שהתוצאה משקפת את זמן האזנה לקובץ בפועל, ולא את משך הזמן שבו הקובץ היה בהשמעה.<br>
              המערכת מציגה את נתוני האזנה לקבצים מוקלטים, ולא את נתוני השתתפות בועידות ושידורים חיים.<br>
              עד 40% האזנה, יופיע צבע אדום. בין 40% ל-80%, יופיע צבע צהוב. מעל 80% האזנה, יופיע צבע ירוק.<br>
              המערכת מיועדת להצגת נתוני האזנה במערכות בהם מוגדר שמירת קובץ דוח יומי (יש להגדיר בקובץ ivr.ini את ההגדרה: log_playback_play_stop=yes ).<br>
              לתשומת ליבכם: לא מומלץ להפיק דוחות בין 4 ל-5 לפנות בוקר, בשעות אלו יתכן שהדוח לא יהיה מדוייק.<br>
          </div>
          
          <div class="setup-panel">
              <div class="form-group">
                  <label>מתאריך:</label>
                  <input type="date" id="from-date">
              </div>
              <div class="form-group">
                  <label>עד תאריך:</label>
                  <input type="date" id="to-date">
              </div>
              <div class="form-group">
                  <label>Token:</label>
                  <div class="token-input-wrapper">
                      <button id="clear-token-btn" title="נקה טוקן שמור">✕</button>
                      <input type="text" id="api-token" placeholder="הכנס טוקן API...">
                  </div>
              </div>
              <button id="fetch-btn">משוך נתונים</button>
              <button id="export-btn" style="display:none;">ייצא לאקסל את הנתונים המסוננים</button>
          </div>
      
          <div id="status-msg"></div>
      
          <div class="table-wrapper">
              <table id="results-table" style="display:none;">
                  <thead>
                      <tr>
                          <th onclick="sortTable(0)">כיתה ↕</th>
                          <th onclick="sortTable(1)">שם ות"ז ↕</th>
                          <th onclick="sortTable(2)">שלוחה ↕</th>
                          <th onclick="sortTable(3)">שם הקובץ ↕</th>
                          <th onclick="sortTable(4)">אורך קובץ ↕</th>
                          <th onclick="sortTable(5)">האזנה נטו ↕</th>
                          <th onclick="sortTable(6)">אחוז האזנה ↕</th>
                      </tr>
                      <tr class="filter-row">
                          <th><select id="filter-school" class="col-filter-action"><option value="">הכל</option></select></th>
                          <th><input type="text" id="filter-name" class="col-filter-action" placeholder="חיפוש..."></th>
                          <th><input type="text" id="filter-folder" class="col-filter-action" placeholder="סנן שלוחה..."></th>
                          <th><input type="text" id="filter-file" class="col-filter-action" placeholder="סנן קובץ..."></th>
                          <th></th> 
                          <th></th> 
                          <th>
                              <div class="filter-group" style="display:flex; gap:5px; align-items:center;">
                                  <span style="font-size:11px">מעל</span>
                                  <input type="number" id="filter-percent" class="col-filter-action" min="0" max="100" placeholder="0" style="width:50px">
                                  <span style="font-size:11px">%</span>
                              </div>
                          </th>
                      </tr>
                  </thead>
                  <tbody id="table-body"></tbody>
              </table>
          </div>
      </div>
      
      <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
      
      <script>
          // הגדרת תאריכי ברירת מחדל
          const today = new Date().toISOString().split('T')[0];
          document.getElementById('from-date').value = today;
          document.getElementById('to-date').value = today;
      
          // טעינת טוקן שמור מהדפדפן
          const savedToken = localStorage.getItem('api_token_cached');
          if (savedToken) {
              document.getElementById('api-token').value = savedToken;
          }
      
          let currentData = []; 
          let sortDirections = Array(7).fill(true);
      
          const fetchBtn = document.getElementById('fetch-btn');
          const exportBtn = document.getElementById('export-btn');
          const statusMsg = document.getElementById('status-msg');
          const clearTokenBtn = document.getElementById('clear-token-btn');
      
          // פונקציית מחיקת טוקן
          clearTokenBtn.onclick = function(e) {
              if (confirm("האם למחוק את הטוקן השמור?")) {
                  localStorage.removeItem('api_token_cached');
                  document.getElementById('api-token').value = '';
                  statusMsg.innerHTML = 'הטוקן נמחק מהזיכרון';
                  setTimeout(() => statusMsg.innerHTML = '', 2000);
              }
          };
      
          fetchBtn.onclick = async function() {
              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);
      
              statusMsg.innerHTML = '<span class="loading">טוען נתונים מהשרת, אנא המתן...</span>';
              const dates = getDatesRange(new Date(fromDate), new Date(toDate));
              const dataMap = {};
      
              try {
                  const results = await Promise.all(dates.map(date => 
                      fetch(`https://www.call2all.co.il/ym/api/RenderYMGRFile?token=${token}&wath=ivr2:/Log/LogPlaybackPlayStop/LogPlaybackPlayStop.${date}.ymgr&convertType=csv&notLoadLang=1`)
                      .then(res => res.ok ? res.text() : null)
                      .catch(err => { console.error(`Error fetching date ${date}:`, err); return null; })
                  ));
                  
                  results.forEach(csv => { if(csv) processCsvSegment(csv, dataMap); });
                  finalizeData(dataMap);
              } catch (err) {
                  statusMsg.innerHTML = `<span class="error">שגיאה כללית בשליפת הנתונים: ${err.message}</span>`;
              }
          };
      
          function processCsvSegment(text, dataMap) {
              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());
      
              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;
      
                  const flenSec = timeToSeconds(row["FileLength"]);
                  const key = `${row["EnterId"]}_${row["Folder"]}_${row["Current"]}_${row["FileLength"]}`;
                  if (!dataMap[key]) dataMap[key] = { info: row, intervals: [], flenSec: flenSec };
      
                  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]);
              }
          }
      
          function finalizeData(dataMap) {
              currentData = Object.values(dataMap).map(item => {
                  const netSec = mergeIntervals(item.intervals);
                  return {
                      ...item,
                      netSec: netSec,
                      percent: item.flenSec > 0 ? Math.min(100, Math.round((netSec / item.flenSec) * 100)) : 0
                  };
              });
              populateSchoolDropdown();
              renderTable(currentData);
              if (currentData.length > 0) {
                  exportBtn.style.display = 'inline-block';
                  document.getElementById('results-table').style.display = 'table';
              } else {
                  statusMsg.innerHTML = '<span class="error">לא נמצאו נתונים לטווח התאריכים הנבחר.</span>';
              }
          }
      
          function populateSchoolDropdown() {
              const schools = [...new Set(currentData.map(item => item.info.School || "-"))].sort();
              const select = document.getElementById('filter-school');
              select.innerHTML = '<option value="">הכל</option>';
              schools.forEach(s => {
                  const opt = document.createElement('option');
                  opt.value = s; opt.textContent = s;
                  select.appendChild(opt);
              });
          }
      
          function renderTable(data) {
              const tbody = document.getElementById('table-body');
              let html = '';
              data.forEach(item => {
                  const color = item.percent > 80 ? 'bg-green' : (item.percent > 40 ? 'bg-orange' : 'bg-red');
                  html += `<tr>
                      <td>${item.info.School || '-'}</td>
                      <td><strong>${item.info.ValName}</strong><small>${item.info.EnterId}</small></td>
                      <td>${item.info.Folder.replace(/\//g, '>')}</td>
                      <td>${item.info.Current}</td>
                      <td>${formatSeconds(item.flenSec)}</td>
                      <td>${formatSeconds(item.netSec)}</td>
                      <td>
                          <div class="progress-container">
                              <div class="progress-bar"><div class="progress-fill ${color}" style="width: ${item.percent}%"></div></div>
                              <span style="font-weight:bold; min-width:35px">${item.percent}%</span>
                          </div>
                      </td>
                  </tr>`;
              });
              tbody.innerHTML = html;
              applyMultiFilter();
          }
      
          function applyMultiFilter() {
              const fSchool = document.getElementById('filter-school').value.toLowerCase();
              const fName = document.getElementById('filter-name').value.toLowerCase();
              const fFolder = document.getElementById('filter-folder').value.toLowerCase();
              const fFile = document.getElementById('filter-file').value.toLowerCase();
              const fMinPercent = parseInt(document.getElementById('filter-percent').value) || 0;
              const rows = document.querySelectorAll('#table-body tr');
              let count = 0;
              rows.forEach(row => {
                  const tSchool = row.cells[0].innerText.toLowerCase();
                  const tName = row.cells[1].innerText.toLowerCase();
                  const tFolder = row.cells[2].innerText.toLowerCase();
                  const tFile = row.cells[3].innerText.toLowerCase();
                  const tPercent = parseInt(row.cells[6].innerText);
                  const match = (fSchool === "" || tSchool === fSchool) && (tName.includes(fName)) && (tFolder.includes(fFolder)) && (tFile.includes(fFile)) && (tPercent >= fMinPercent);
                  row.style.display = match ? '' : 'none';
                  if (match) count++;
              });
              statusMsg.innerHTML = `נמצאו ${count} רשומות לאחר סינון.`;
          }
      
          document.querySelectorAll('.col-filter-action').forEach(el => { el.oninput = applyMultiFilter; });
      
          function sortTable(idx) {
              sortDirections[idx] = !sortDirections[idx];
              const dir = sortDirections[idx] ? 1 : -1;
              currentData.sort((a, b) => {
                  let v1, v2;
                  switch(idx) {
                      case 0: v1 = (a.info.School || "").toLowerCase(); v2 = (b.info.School || "").toLowerCase(); break;
                      case 1: v1 = (a.info.ValName || "").toLowerCase(); v2 = (b.info.ValName || "").toLowerCase(); break;
                      case 2: v1 = a.info.Folder.toLowerCase(); v2 = b.info.Folder.toLowerCase(); break;
                      case 3: v1 = a.info.Current.toLowerCase(); v2 = b.info.Current.toLowerCase(); break;
                      case 4: v1 = a.flenSec; v2 = b.flenSec; break;
                      case 5: v1 = a.netSec; v2 = b.netSec; break;
                      case 6: v1 = a.percent; v2 = b.percent; break;
                  }
                  return v1 < v2 ? -1 * dir : (v1 > v2 ? 1 * dir : 0);
              });
              renderTable(currentData);
          }
      
          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;
          }
      
          exportBtn.onclick = function() {
              const rows = document.querySelectorAll('#table-body tr');
              const exportedData = [];
              rows.forEach(row => {
                  if (row.style.display !== 'none') {
                      exportedData.push({
                          "כיתה": row.cells[0].innerText,
                          "שם תלמיד": row.cells[1].querySelector('strong').innerText,
                          "תעודת זהות": row.cells[1].querySelector('small').innerText,
                          "שלוחה": row.cells[2].innerText,
                          "שם הקובץ": row.cells[3].innerText,
                          "אורך קובץ": row.cells[4].innerText,
                          "האזנה נטו": row.cells[5].innerText,
                          "אחוז האזנה": row.cells[6].innerText.replace('%', '')
                      });
                  }
              });
              const ws = XLSX.utils.json_to_sheet(exportedData);
              const wb = XLSX.utils.book_new();
              XLSX.utils.book_append_sheet(wb, ws, "דוח האזנה");
              
              const now = new Date().toISOString(); 
              const ts = now.replace('T', '_').replace(/[:.]/g, '-').slice(0, 19);
              
              XLSX.writeFile(wb, `דוח_האזנה_${ts}.xlsx`);
          };
      </script>
      
      </body>
      </html>
      

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