• דף הבית
    • אינדקס קישורים
    • פוסטים אחרונים
    • משתמשים
    • חיפוש בהגדרות המתקדמות
    • חיפוש גוגל בפורום
    • ניהול המערכת
    • ניהול המערכת - שרת private
    • הרשמה
    • התחברות
    1. דף הבית
    2. עובד את השם
    3. פוסטים
    ע
    מנותק
    • פרופיל
    • עוקב אחרי 0
    • עוקבים 0
    • נושאים 2
    • פוסטים 47
    • קבוצות 0

    פוסטים

    פוסטים אחרונים הגבוה ביותר שנוי במחלוקת
    • RE: מערכת קבוצה חברתית

      @hgbeh אם הבנתי מה אתה מתכוון, הוא שאל אותך אם להוריד פרסומות , צריך להקיש 0, זה עוקף את השאלה.

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם
    • RE: אני צריך דוח על שלוחה מסוימת איך אני מוציא את זה

      @הפצת-התורה תפתח ת'דף

      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: אני צריך דוח על שלוחה מסוימת איך אני מוציא את זה

      @זרח הוא רצה דוחות על שלוחות מסויימות ויש שם.

      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: שידור חי אינטרנטי תקלה

      מצטער שאני לא יכול להצביע לכם, פשוט אני משתמש די חדש.

      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: אני צריך דוח על שלוחה מסוימת איך אני מוציא את זה

      @הפצת-התורה יש לך את זה וגם את זה:
      (תפתח בפנקס רשימות ותשמור בשם עם סיומת HTML)

      <!DOCTYPE html>
      <html lang="he" dir="rtl">
      <head>
          <meta charset="UTF-8">
          <title>ניתוח נתוני מערכות | גרסה יציבה ומדויקת</title>
          <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
          <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
          <style>
              :root { --bg: #0f172a; --card: #1e293b; --primary: #38bdf8; --accent: #22c55e; --text: #f1f5f9; --border: #334155; --danger: #ef4444; --warning: #f59e0b; }
              body { font-family: 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); margin: 0; display: flex; flex-direction: column; height: 100vh; }
              header { background: #1e293b; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); }
              .logo-box { display: flex; align-items: center; gap: 12px; }
              .logo-icon { background: linear-gradient(135deg, var(--primary), #0ea5e9); color: #0f172a; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; }
              .logo-text { font-size: 1.4rem; font-weight: 800; color: var(--text); }
              .config-box { display: flex; gap: 12px; align-items: center; }
              input, select { background: #0f172a; border: 1px solid var(--border); color: white; padding: 10px; border-radius: 8px; outline: none; }
              input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); cursor: pointer; }
              .btn-run { background: var(--primary); color: #0f172a; border: none; padding: 10px 25px; border-radius: 8px; font-weight: bold; cursor: pointer; transition: 0.3s; }
              .btn-run:hover { opacity: 0.8; }
              .nav-tabs { background: #1e293b; display: flex; padding: 0 20px; border-bottom: 1px solid var(--border); }
              .tab { padding: 15px 25px; cursor: pointer; color: #94a3b8; border-bottom: 3px solid transparent; transition: 0.3s; }
              .tab.active { color: var(--primary); border-bottom-color: var(--primary); background: rgba(56, 189, 248, 0.07); }
              .main-content { flex: 1; padding: 25px; overflow-y: auto; background: #0f172a; }
              .card { background: var(--card); padding: 25px; border-radius: 16px; border: 1px solid var(--border); margin-bottom: 25px; }
              .stat-num { font-size: 2.5rem; font-weight: 800; color: var(--primary); }
              .control-bar { display: flex; gap: 20px; align-items: center; background: #334155; padding: 15px 25px; border-radius: 12px 12px 0 0; }
              .search-input { background: #0f172a; border: 1px solid var(--border); color: white; padding: 8px 15px; border-radius: 8px; width: 250px; }
              table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 0 0 12px 12px; overflow: hidden; }
              th { text-align: right; padding: 15px; background: #475569; color: white; font-size: 0.85rem; }
              td { padding: 15px; border-bottom: 1px solid var(--border); font-size: 0.95rem; }
              tr.clickable { cursor: pointer; }
              tr.clickable:hover { background: rgba(56, 189, 248, 0.1); }
              .bar-container { width: 100px; background: #0f172a; height: 10px; border-radius: 10px; display: inline-block; overflow: hidden; vertical-align: middle; margin-left: 8px; }
              .bar-fill { height: 100%; background: var(--accent); }
              .modal-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.85); display: none; justify-content: center; align-items: center; z-index: 1000; }
              .modal-content { background: var(--card); width: 85%; max-height: 85%; border-radius: 20px; border: 1px solid var(--primary); overflow-y: auto; padding: 30px; position: relative; }
              .close-modal { position: absolute; top: 20px; left: 20px; color: white; font-size: 2rem; cursor: pointer; }
              .progress-container { width: 100%; background: #1e293b; height: 20px; display: none; position: relative; text-align: center; }
              #progress-fill { height: 100%; background: var(--primary); width: 0%; transition: 0.4s; position: absolute; top:0; right:0; }
              #progress-text { position: relative; z-index: 2; font-size: 0.8rem; font-weight: bold; color: white; line-height: 20px; }
              .hidden { display: none; }
              .live-status { display: flex; align-items: center; gap: 6px; font-size: 0.75rem; padding: 4px 10px; border-radius: 20px; background: #334155; }
              .dot { width: 8px; height: 8px; border-radius: 50%; background: #666; }
              .dot.active { background: var(--accent); animation: pulse 2s infinite; }
              @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
          </style>
      </head>
      <body>
       
      <header>
          <div class="logo-box">
              <div class="logo-icon"><i class="fas fa-chart-pie"></i></div>
              <div class="logo-text">ניתוח נתוני מערכות</div>
              <div class="live-status"><div id="live-dot" class="dot"></div> <span id="live-text">OFFLINE</span></div>
          </div>
          <div class="config-box">
              <input type="password" id="apiToken" placeholder="טוקן API">
              <input type="date" id="startDate">
              <input type="date" id="endDate">
              <button class="btn-run" onclick="manualStart()">הפעל ניתוח</button>
          </div>
      </header>
       
      <div class="progress-container" id="progBar">
          <div id="progress-fill"></div>
          <span id="progress-text">0%</span>
      </div>
       
      <div class="nav-tabs">
          <div class="tab active" onclick="switchTab(event, 'dash-tab')">דאשבורד</div>
          <div class="tab" onclick="switchTab(event, 'ext-tab')">שלוחות</div>
          <div class="tab" onclick="switchTab(event, 'play-tab')">השמעות</div>
          <div class="tab" onclick="switchTab(event, 'users-tab')">מאזינים</div>
          <div class="tab" style="color: var(--warning)" onclick="switchTab(event, 'debug-tab')">לוג המערכת</div>
      </div>
       
      <div class="main-content">
          <div id="dash-tab" class="tab-content">
              <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px; margin-bottom: 25px;">
                  <div class="card"><span style="color: #94a3b8">מאזינים ייחודיים בטבלה</span><br><span id="stat-users" class="stat-num">0</span></div>
                  <div class="card"><span style="color: #94a3b8">סה"כ דקות שיחה (מערכת)</span><br><span id="stat-min" class="stat-num">0</span></div>
                  <div class="card"><span style="color: #94a3b8">סה"כ השמעות שהושלמו עד הסוף</span><br><span id="stat-comp" class="stat-num">0</span></div>
              </div>
              <div class="card"><h3>גרף פעילות יומי</h3><canvas id="dailyChart" height="100"></canvas></div>
          </div>
       
          <div id="ext-tab" class="tab-content hidden">
              <div class="control-bar">
                  <select id="extSort" onchange="renderExts()"><option value="count">כמות כניסות</option><option value="sec">זמן שהייה</option></select>
                  <input type="text" id="extSearch" class="search-input" placeholder="חפש שלוחה..." onkeyup="renderExts()">
              </div>
              <table id="extTable"><thead><tr><th>שם השלוחה</th><th>תיאור שלוחה</th><th>כניסות</th><th>דקות שהייה</th><th>ממוצע</th></tr></thead><tbody></tbody></table>
          </div>
       
          <div id="play-tab" class="tab-content hidden">
              <div class="control-bar">
                  <select id="playSort" onchange="renderPlays()"><option value="count">פופולריות</option><option value="avg">אחוז השלמה</option></select>
                  <input type="text" id="playSearch" class="search-input" placeholder="חפש קובץ..." onkeyup="renderPlays()">
              </div>
              <table id="playTable"><thead><tr><th>שם קובץ</th><th>שלוחה</th><th>מספר השמעות</th><th>סה"כ דקות</th><th>אחוז השלמה</th><th>נטישה</th></tr></thead><tbody></tbody></table>
          </div>
       
          <div id="users-tab" class="tab-content hidden">
              <div class="control-bar">
                  <select id="userSort" onchange="renderUsers()"><option value="sec">זמן האזנה כולל</option><option value="calls">מספר שיחות</option></select>
                  <input type="text" id="userSearch" class="search-input" placeholder="חפש טלפון או שם..." onkeyup="renderUsers()">
              </div>
              <table id="usersTable"><thead><tr><th>מספר טלפון</th><th>שם מאזין</th><th>דקות האזנה</th><th>מספר שיחות</th><th>סטטוס</th></tr></thead><tbody></tbody></table>
          </div>
       
          <div id="debug-tab" class="tab-content hidden">
              <div class="card"><div id="debug-console" style="background:#000; color:#22c55e; padding:20px; height:400px; overflow-y:auto; font-family:monospace; border-radius: 8px;"></div></div>
          </div>
      </div>
       
      <!-- מודאל פירוט מאזין -->
      <div id="userModal" class="modal-overlay" onclick="closeModal()">
          <div class="modal-content" onclick="event.stopPropagation()">
              <span class="close-modal" onclick="closeModal()">&times;</span>
              <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
                  <h2 id="modalTitle" style="color: var(--primary); margin:0;"></h2>
                  <select id="modalFilter" onchange="filterModalContent()" style="background: var(--bg); color:white; border: 1px solid var(--primary); padding: 5px 15px; border-radius: 5px;">
                      <option value="all">הכל</option>
                      <option value="שלוחה">שלוחות בלבד</option>
                      <option value="השמעה">השמעות בלבד</option>
                  </select>
              </div>
              <table id="modalTable">
                  <thead>
                      <tr><th>סוג פעילות</th><th>שלוחה/קובץ</th><th>זמן שהייה</th><th>תאריך (עברי ולועזי)</th></tr>
                  </thead>
                  <tbody></tbody>
              </table>
          </div>
      </div>
       
      <!-- מודאל פירוט שלוחה -->
      <div id="extModal" class="modal-overlay" onclick="closeExtModal()">
          <div class="modal-content" onclick="event.stopPropagation()">
              <span class="close-modal" onclick="closeExtModal()">&times;</span>
              <h2 id="extModalTitle" style="color: var(--primary); margin-bottom: 20px;"></h2>
              <table id="extModalTable">
                  <thead>
                      <tr>
                          <th>שם מאזין</th>
                          <th>מספר טלפון</th>
                          <th>כמות כניסות (לשלוחה זו)</th>
                          <th>זמן שהייה (בשלוחה זו)</th>
                      </tr>
                  </thead>
                  <tbody></tbody>
              </table>
          </div>
      </div>
       
      <script>
          const API = "https://www.call2all.co.il/ym/api/";
          let dataStore = { exts: {}, plays: {}, users: {}, daily: {}, rawActivity: [], totalSec: 0, completed: 0, names: {} };
          let currentModalPhone = "";
          let dailyChart = null;
          let autoRefreshTimer = null;
       
          function addLog(msg, color = '#22c55e') {
              const console = document.getElementById('debug-console');
              if(!console) return;
              const div = document.createElement('div');
              div.style.color = color;
              div.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
              console.appendChild(div);
              console.scrollTop = console.scrollHeight; 
          }
       
          function normalizePhone(p) {
              if (!p) return "חסוי";
              let s = p.toString().replace(/\D/g, '');
              if (s.length > 0 && !s.startsWith('0') && s.length >= 8) s = '0' + s;
              return s;
          }
       
          function formatExtName(ext) {
              if (!ext) return "ראשית";
              let str = ext.toString().trim();
              if (str === "" || str === "/" || str.toLowerCase() === "main" || str.toLowerCase() === "root") {
                  return "ראשית";
              }
              return str;
          }
       
          function parseYemotDateToIso(dStr) {
              if (!dStr || !dStr.includes('/')) return null;
              const parts = dStr.split('/');
              if (parts.length === 3) {
                  const d = parts[0].padStart(2, '0');
                  const m = parts[1].padStart(2, '0');
                  const y = parts[2];
                  return `${y}-${m}-${d}`;
              }
              return null;
          }
       
          function timeStrToSeconds(tStr) {
              if (!tStr) return 0;
              if (!tStr.includes(':')) return parseInt(tStr) || 0;
              const p = tStr.split(':').map(Number);
              if (p.length === 3) return (p[0] * 3600) + (p[1] * 60) + p[2];
              if (p.length === 2) return (p[0] * 60) + p[1];
              return parseInt(tStr) || 0;
          }
       
          function ensureUser(phone) {
              if (!dataStore.users[phone]) {
                  dataStore.users[phone] = { sec: 0, calls: 0 };
              }
          }
       
          async function fetchNames(token) {
              try {
                  const res = await fetch(`${API}DownloadFile?token=${token}&path=ivr2:/EnterIDValName.ini`);
                  const text = await res.text();
                  if(text && !text.includes("NOT_FOUND")) {
                      text.split(/\r?\n/).forEach(line => {
                          if (line.includes('=')) {
                              const [phone, name] = line.split('=');
                              const cleanPhone = normalizePhone(phone);
                              if (cleanPhone && name) dataStore.names[cleanPhone] = name.trim();
                          }
                      });
                      addLog(`נטענו שמות מקובץ ההגדרות הראשי`);
                  }
              } catch(e) { }
          }
       
          async function manualStart() {
              if(autoRefreshTimer) clearInterval(autoRefreshTimer);
              await startFullAnalysis();
              document.getElementById('live-dot').classList.add('active');
              document.getElementById('live-text').innerText = "LIVE (פעיל)";
          }
       
          async function startFullAnalysis() {
              const token = document.getElementById('apiToken').value;
              const startStr = document.getElementById('startDate').value; 
              const endStr = document.getElementById('endDate').value;     
              
              if (!token) return alert("נא להזין טוקן API");
       
              addLog(`מתחיל עיבוד נתונים מתאריך ${startStr} עד ${endStr}...`);
              document.getElementById('progBar').style.display = 'block';
              updateProgress(5, 100);
              
              dataStore = { exts: {}, plays: {}, users: {}, daily: {}, rawActivity: [], totalSec: 0, completed: 0, names: {} };
              
              await fetchNames(token);
              updateProgress(10, 100);
       
              // חישוב חודשים בטוח (מניעת תקלות של אזור זמן)
              let currM = new Date(startStr);
              currM.setDate(1); // עקיפת בעיית 31 לחודש
              let endM = new Date(endStr);
              let monthsToFetch = [];
              while (currM <= endM || (currM.getFullYear() === endM.getFullYear() && currM.getMonth() === endM.getMonth())) {
                  let y = currM.getFullYear();
                  let m = String(currM.getMonth() + 1).padStart(2, '0');
                  monthsToFetch.push(`${y}-${m}`);
                  currM.setMonth(currM.getMonth() + 1);
              }
       
              // אובייקט לאיתור שיחות כפולות (למניעת קפיצת דקות)
              let callMap = {};
       
              for (let month of monthsToFetch) {
                  try {
                      addLog(`מייבא לוג כניסות עבור חודש ${month}...`);
                      const res = await fetch(`${API}RenderYMGRFile?token=${token}&wath=ivr2:/Log/LogFolderEnterExit-${month}.ymgr&convertType=json`);
                      const json = await res.json();
                      
                      if (json.data && Array.isArray(json.data)) {
                          json.data.forEach(row => {
                              const gregDate = row["תאריך"]; 
                              const hebDate = row["תאריך עברי"] || row["עברי"] || ""; 
                              const isoDate = parseYemotDateToIso(gregDate);
                              
                              if (!isoDate) return;
                              
                              // סינון מוחלט של ימים לפי טקסט (למשל "2026-03-31") - מונע כפילויות!
                              if (isoDate >= startStr && isoDate <= endStr) {
                                  
                                  const extName = formatExtName(row["שלוחה"]);
                                  const phone = normalizePhone(row["טלפון"]);
                                  const sec = parseInt(row["סה\"כ שניות"]) || 0;
                                  const callId = row["מזהה שיחה"];
                                  const nameFromLog = row["שם מזהה"];
                                  
                                  if (nameFromLog && !dataStore.names[phone]) dataStore.names[phone] = nameFromLog;
       
                                  // -- חישוב נתונים לשלוחות --
                                  if (!dataStore.exts[extName]) {
                                      dataStore.exts[extName] = { count: 0, sec: 0, title: row["כותרת שלוחה"] || "", extUsers: {} };
                                  }
                                  dataStore.exts[extName].count++;
                                  dataStore.exts[extName].sec += sec;
       
                                  if (!dataStore.exts[extName].extUsers[phone]) {
                                      dataStore.exts[extName].extUsers[phone] = { count: 0, sec: 0 };
                                  }
                                  dataStore.exts[extName].extUsers[phone].count++;
                                  dataStore.exts[extName].extUsers[phone].sec += sec;
       
                                  // -- איסוף זמן למאזין וכללי במערכת (עם מניעת כפילויות של מזהה שיחה) --
                                  if (callId) {
                                      if (!callMap[callId]) callMap[callId] = { phone: phone, maxSec: 0 };
                                      if (sec > callMap[callId].maxSec) callMap[callId].maxSec = sec;
                                  } else {
                                      // במקרה נדיר שאין מזהה שיחה
                                      ensureUser(phone);
                                      dataStore.users[phone].sec += sec;
                                      dataStore.users[phone].calls++;
                                      dataStore.totalSec += sec;
                                  }
       
                                  if (!dataStore.daily[isoDate]) dataStore.daily[isoDate] = 0;
                                  dataStore.daily[isoDate]++;
       
                                  const displayDate = hebDate ? `${hebDate} (${gregDate})` : gregDate;
                                  dataStore.rawActivity.push({ phone, type: 'שלוחה', name: extName, sec: sec + " ש'", date: `${displayDate} ${row["התחלה שעה"]||''}` });
                              }
                          });
                      }
                  } catch(e) { addLog(`לא נמצא לוג כניסות בחודש ${month}`, "orange"); }
              }
       
              // עדכון סך הדקות והשיחות במערכת (מסונן ללא כפילויות!)
              Object.values(callMap).forEach(call => {
                  dataStore.totalSec += call.maxSec;
                  ensureUser(call.phone);
                  dataStore.users[call.phone].sec += call.maxSec;
                  dataStore.users[call.phone].calls += 1;
              });
       
              updateProgress(50, 100);
       
              // חישוב ימים להשמעות בטוח
              let daysToFetch = [];
              let currDay = new Date(startStr);
              let endDay = new Date(endStr);
              while(currDay <= endDay) {
                  let y = currDay.getFullYear();
                  let m = String(currDay.getMonth() + 1).padStart(2, '0');
                  let d = String(currDay.getDate()).padStart(2, '0');
                  daysToFetch.push(`${y}-${m}-${d}`);
                  currDay.setDate(currDay.getDate() + 1);
              }
       
              for (let i=0; i<daysToFetch.length; i++) {
                  const day = daysToFetch[i];
                  try {
                      const res = await fetch(`${API}RenderYMGRFile?token=${token}&wath=ivr2:/Log/LogPlaybackPlayStop/LogPlaybackPlayStop.${day}.ymgr&convertType=json`);
                      const json = await res.json();
                      
                      if (json.data && Array.isArray(json.data)) {
                          json.data.forEach(row => {
                              const gregDate = row["תאריך"] || day;
                              const isoDate = parseYemotDateToIso(gregDate);
                              
                              // וידוא שההשמעה בטווח התאריכים המדויק
                              if (isoDate && (isoDate < startStr || isoDate > endStr)) return;
       
                              const file = row["השמעה"];
                              if (!file) return;
       
                              const folder = formatExtName(row["שלוחה"]);
                              const phone = normalizePhone(row["טלפון"]);
                              const sec = parseInt(row["סה\"כ שניות"]) || 0;
                              const fileLenSec = timeStrToSeconds(row["אורך הקובץ"]);
                              const exitPoint = row["נקודת יציאה"];
                              const nameFromLog = row["שם"];
                              
                              const hebDate = row["עברי"] || row["תאריך עברי"] || "";
                              const displayDate = hebDate ? `${hebDate} (${gregDate})` : gregDate;
                              
                              if (nameFromLog && !dataStore.names[phone]) dataStore.names[phone] = nameFromLog;
                              ensureUser(phone); // מוודא שמי שהאזין לקובץ יופיע בטבלת מאזינים גם אם לא נכנס לשלוחה רשמית
       
                              const isEnd = (exitPoint === "סוף") || (fileLenSec > 0 && sec >= fileLenSec - 2);
                              if (isEnd) dataStore.completed++;
       
                              if (!dataStore.plays[file]) dataStore.plays[file] = { count: 0, sec: 0, drops: 0, pcts: [], folder: folder };
                              dataStore.plays[file].count++;
                              dataStore.plays[file].sec += sec;
                              
                              const pct = fileLenSec > 0 ? Math.min(100, Math.round((sec / fileLenSec) * 100)) : (isEnd ? 100 : 0);
                              dataStore.plays[file].pcts.push(pct);
                              
                              if (!isEnd) dataStore.plays[file].drops++;
       
                              dataStore.rawActivity.push({ phone, type: 'השמעה', name: file, sec: sec + " ש'", date: `${displayDate} ${row["התחלה שעה"]||''}` });
                          });
                      }
                  } catch(e) {}
                  
                  updateProgress(50 + Math.round(((i + 1) / daysToFetch.length) * 50), 100);
              }
       
              addLog("עיבוד הנתונים הסתיים בהצלחה.");
              renderUI();
          }
       
          function updateProgress(c, t) { 
              const pct = Math.round((c/t)*100);
              document.getElementById('progress-fill').style.width = pct + '%'; 
              document.getElementById('progress-text').innerText = pct + '%';
          }
       
          function renderUI() {
              document.getElementById('progBar').style.display = 'none';
              
              // כאן הסנכרון המושלם - הדאשבורד מציג בדיוק את מספר המאזינים שיש בטבלה
              document.getElementById('stat-users').innerText = Object.keys(dataStore.users).length.toLocaleString();
              
              document.getElementById('stat-min').innerText = Math.floor(dataStore.totalSec / 60).toLocaleString();
              document.getElementById('stat-comp').innerText = dataStore.completed.toLocaleString();
              
              renderExts(); 
              renderPlays(); 
              renderUsers(); 
              updateChart();
          }
       
          function renderExts() {
              const sort = document.getElementById('extSort').value;
              const q = document.getElementById('extSearch').value.toLowerCase();
              let items = Object.entries(dataStore.exts).map(([name, d]) => ({name, ...d})).filter(i => i.name.toLowerCase().includes(q));
              items.sort((a,b) => b[sort] - a[sort]);
              document.querySelector('#extTable tbody').innerHTML = items.map(i => `
                  <tr class="clickable" onclick="openExtDetail('${i.name}')">
                      <td style="color:var(--primary); font-weight:600">${i.name}</td>
                      <td>${i.title || '-'}</td>
                      <td>${i.count}</td>
                      <td>${(i.sec/60).toFixed(1)}</td>
                      <td>${i.count ? Math.round(i.sec/i.count) : 0} ש'</td>
                  </tr>
              `).join('');
          }
       
          function renderPlays() {
              const sort = document.getElementById('playSort').value;
              const q = document.getElementById('playSearch').value.toLowerCase();
              let items = Object.entries(dataStore.plays).map(([name, d]) => {
                  const avg = d.pcts && d.pcts.length ? Math.round(d.pcts.reduce((a,b)=>a+b,0)/d.pcts.length) : 0;
                  const drop = d.count ? Math.round((d.drops/d.count)*100) : 0;
                  return {name, avg, drop, ...d};
              }).filter(i => i.name.toLowerCase().includes(q));
              items.sort((a,b) => sort === 'count' ? b.count - a.count : b.avg - a.avg);
              document.querySelector('#playTable tbody').innerHTML = items.map(i => `
                  <tr><td>${i.name}</td><td>${i.folder || '-'}</td><td>${i.count}</td><td>${(i.sec/60).toFixed(1)}</td>
                  <td><div class="bar-container"><div class="bar-fill" style="width:${i.avg}%"></div></div> ${i.avg}%</td>
                  <td style="color:${i.drop > 50 ? 'var(--danger)' : 'var(--accent)'}">${i.drop}%</td></tr>
              `).join('');
          }
       
          function renderUsers() {
              const sortKey = document.getElementById('userSort').value;
              const q = document.getElementById('userSearch').value;
              let items = Object.entries(dataStore.users).map(([phone, d]) => ({
                  phone, name: dataStore.names[phone] || "לא ידוע", sec: d.sec || 0, calls: d.calls || 0
              })).filter(i => i.phone.includes(q) || i.name.includes(q));
              items.sort((a,b) => b[sortKey] - a[sortKey]);
              document.querySelector('#usersTable tbody').innerHTML = items.map((i, idx) => `
                  <tr class="clickable" onclick="openUserDetail('${i.phone}')">
                      <td style="font-weight:bold">${i.phone}</td><td style="color:var(--warning)">${i.name}</td>
                      <td style="color:var(--primary); font-weight:bold">${(i.sec/60).toFixed(1)}</td><td>${i.calls}</td>
                      <td>${idx < 3 ? '🏆 מוביל' : 'מאזין'}</td>
                  </tr>
              `).join('');
          }
       
          function openUserDetail(phone) {
              currentModalPhone = phone;
              document.getElementById('modalTitle').innerText = `פירוט מאזין: ${phone} (${dataStore.names[phone] || "לא ידוע"})`;
              filterModalContent();
              document.getElementById('userModal').style.display = 'flex';
          }
       
          function filterModalContent() {
              const filter = document.getElementById('modalFilter').value;
              const activity = dataStore.rawActivity.filter(a => a.phone === currentModalPhone);
              const filtered = filter === 'all' ? activity : activity.filter(a => a.type === filter);
              document.querySelector('#modalTable tbody').innerHTML = filtered.map(a => `
                  <tr><td style="color:${a.type==='השמעה'?'var(--primary)':'var(--accent)'}; font-weight:bold">${a.type}</td>
                  <td>${a.name}</td><td>${a.sec}</td><td>${a.date}</td></tr>
              `).join('');
          }
       
          function closeModal() { document.getElementById('userModal').style.display = 'none'; }
          
          function openExtDetail(extName) {
              document.getElementById('extModalTitle').innerText = `פירוט מאזינים בשלוחה: ${extName}`;
              const extData = dataStore.exts[extName];
              if(!extData) return;
       
              let usersArr = Object.entries(extData.extUsers).map(([phone, d]) => {
                  return { phone, name: dataStore.names[phone] || "לא ידוע", count: d.count, sec: d.sec };
              });
       
              usersArr.sort((a,b) => b.sec - a.sec);
       
              document.querySelector('#extModalTable tbody').innerHTML = usersArr.map(u => `
                  <tr>
                      <td style="color:var(--warning)">${u.name}</td>
                      <td style="font-weight:bold">${u.phone}</td>
                      <td>${u.count}</td>
                      <td style="color:var(--primary); font-weight:bold">${(u.sec/60).toFixed(1)} דק'</td>
                  </tr>
              `).join('');
       
              document.getElementById('extModal').style.display = 'flex';
          }
       
          function closeExtModal() { document.getElementById('extModal').style.display = 'none'; }
       
          function switchTab(e, id) {
              document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
              document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
              document.getElementById(id).classList.remove('hidden');
              e.currentTarget.classList.add('active');
          }
          
          function updateChart() {
              if (dailyChart) dailyChart.destroy();
              const ctx = document.getElementById('dailyChart').getContext('2d');
              const sortedDates = Object.keys(dataStore.daily).sort();
              const sortedData = sortedDates.map(d => dataStore.daily[d]);
       
              dailyChart = new Chart(ctx, {
                  type: 'line',
                  data: { labels: sortedDates, datasets: [{ label: 'פעולות במערכת', data: sortedData, borderColor: '#38bdf8', tension: 0.4, fill: true, backgroundColor: 'rgba(56, 189, 248, 0.1)' }] },
                  options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } }
              });
          }
       
          let dEnd = new Date();
          let dStart = new Date();
          dStart.setDate(dEnd.getDate() - 7);
          document.getElementById('endDate').value = dEnd.toISOString().split('T')[0];
          document.getElementById('startDate').value = dStart.toISOString().split('T')[0];
      </script>
      </body>
      </html>
      
      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: תגובה לקבצי HTML לשימוש במערכות ימות המשיח

      @אופיר תודה רבה

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם
    • RE: מוזיקת רקע לתפריט

      @הקו-המוביל וכן בזה

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם
    • RE: מוזיקת רקע לתפריט

      @הקו-המוביל סליחה שאני מקפיץ, אבל תסתכל באילו:

        %D7%A4%D7%AA%D7%99%D7%97-%D7%94%D7%97%D7%93%D7%A9%D7%95%D7%AA-%D7%9E%D7%91%D7%96%D7%A7-%D7%9E%D7%A1%D7%9A-%D7%99%D7%A8%D7%95%D7%A7.mp3

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם
    • RE: תקלה!!! באתר הניהול מופיע לי שכל המאזינים כעת מאותו מספר

      תודה רבה

      פורסם בבאגים במערכת
      ע
      עובד את השם
    • RE: קו עם כל הקווים

      @יענקי-פולק תודה רבה

      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: תקלה!!! באתר הניהול מופיע לי שכל המאזינים כעת מאותו מספר

      עכשיו זה כבר לא מראה את התקלה הזאת

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

      @שואל-שאלה תגדיר כך:

      type=nitoviya
      nitoviya_dial_to=1700111010
      title=ר' מיילך
      
      
      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: תקלה!!! באתר הניהול מופיע לי שכל המאזינים כעת מאותו מספר

      וכן הזמן שיחה לא מתאים עם הזמן שיחות נכנסות
      92b4b261-7662-4dc7-b01d-45ad3e55710a-image.png

      פורסם בבאגים במערכת
      ע
      עובד את השם
    • תקלה!!! באתר הניהול מופיע לי שכל המאזינים כעת מאותו מספר

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

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

      לפני חצי שעה העלית שידור חי וזזה טלפון של הצינתוק של מידע כשר ולא של המערכת עצמה

      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: שידור חי אינטרנטי תקלה

      מקפיץ.
      זה עדיין לא עובד.

      פורסם בשאלות ועזרה הדדית
      ע
      עובד את השם
    • RE: תגובות | פיתוח פרטי | לייק ודיסלייק בהשמעת קבצים 👍/👎

      מישהו יודע למה לא עובד לי?

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם
    • RE: תגובה לקבצי HTML לשימוש במערכות ימות המשיח

      @אופיר אתה יכול להעלות את זה כקובץ HTML?

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם
    • RE: תגובה לקבצי HTML לשימוש במערכות ימות המשיח

      @אופיר הוא העלה את זה ואני מכניס לפנקס רשימות ושומר כHTML וזה פותח אבל לא יודע למה זה לא עובד והקישורים שהוא הביא עבדו לי אתה יכול לנסות?

      <!DOCTYPE html>
      <html lang="he" dir="rtl">
      <head>
         <meta charset="UTF-8">
         <title>הורדת שלוחות למחשב</title>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
         <style>
             body { font-family: 'Segoe UI', Tahoma, sans-serif; margin: 20px; background-color: #f4f7f6; text-align: right; direction: rtl; }
             .container { max-width: 1100px; margin: auto; background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
             .section { border: 1px solid #e0e0e0; padding: 15px; margin-bottom: 15px; border-radius: 8px; background: #fafafa; }
             #logArea { 
                 background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; 
                 height: 250px; overflow-y: auto; font-family: 'Consolas', monospace; font-size: 13px; margin-top: 10px;
             }
             .log-info { color: #4fc3f7; }
             .log-success { color: #00ff00; font-weight: bold; }
             .log-error { color: #ff5252; }
             button { padding: 10px 20px; background: #3498db; color: white; border: none; cursor: pointer; border-radius: 5px; font-weight: bold; transition: background 0.3s; }
             button:hover { background: #2980b9; }
             button:disabled { background: #bdc3c7; }
             input { padding: 10px; margin: 5px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
             .token-input { width: 350px; }
             .config-input { width: 60px; text-align: center; }
             table { width: 100%; border-collapse: collapse; margin-top: 15px; }
             th, td { border: 1px solid #ddd; padding: 12px; text-align: right; }
             .progress-container { background: #e0e0e0; border-radius: 20px; height: 25px; margin: 15px 0; overflow: hidden; display: none; }
             .progress-bar { width: 0%; height: 100%; background: linear-gradient(90deg, #2ecc71, #27ae60); transition: width 0.3s; color: white; text-align: center; line-height: 25px; font-weight: bold; }
         </style>
      </head>
      <body>
      
      <div class="container">
         <h2>הורדת מבנה שלוחות למחשב (ZIP)</h2>
         
         <div class="section">
             <strong>⚙️ הגדרות:</strong>
             סרוק שלוחות בעלות <input type="number" id="digitCount" class="config-input" value="2" min="1" max="4"> ספרות.
             שם קובץ לשמירה: <input type="text" id="fileNameInput" placeholder="backup_ivr" value="backup_ivr">
         </div>
      
         <div class="section">
             <strong>1. פרטי המערכת</strong><br>
             <input type="text" id="srcToken" class="token-input" placeholder="הכנס טוקן מקור...">
             <input type="text" id="srcPath" class="path-input" placeholder="נתיב (למשל /)" value="/">
             <button onclick="loadFolder(document.getElementById('srcPath').value)">טען רשימת קבצים</button>
         </div>
      
         <div class="section">
             <strong>2. ביצוע הורדה</strong><br>
             <button id="downloadBtn" onclick="startDownload()" disabled style="background:#27ae60;">בחר מיקום והורד ZIP</button>
             
             <div class="progress-container" id="progContainer">
                 <div id="progBar" class="progress-bar">0%</div>
             </div>
             <div id="logArea">מוכן...</div>
         </div>
      
         <div id="fileArea" style="display:none;">
             <table id="fileTable">
                 <thead>
                     <tr>
                         <th style="width: 40px;"><input type="checkbox" id="masterCheck" checked onclick="toggleAll(this)"></th>
                         <th>סוג</th>
                         <th>שם</th>
                     </tr>
                 </thead>
                 <tbody id="fileTableBody"></tbody>
             </table>
         </div>
      </div>
      
      <script>
      let currentViewPath = "/";
      let zip = new JSZip();
      
      function addLog(msg, type = '') {
         const logArea = document.getElementById('logArea');
         const div = document.createElement('div');
         div.className = `log-${type}`;
         div.innerHTML = `[${new Date().toLocaleTimeString()}] ${msg}`;
         logArea.appendChild(div);
         logArea.scrollTop = logArea.scrollHeight;
      }
      
      async function loadFolder(path) {
         const token = document.getElementById('srcToken').value;
         const digits = parseInt(document.getElementById('digitCount').value) || 2;
         const maxRange = Math.pow(10, digits) - 1;
         if (!token) { alert("נא להזין טוקן"); return; }
      
         currentViewPath = path;
         const tbody = document.getElementById('fileTableBody');
         tbody.innerHTML = '<tr><td colspan="3">סורק...</td></tr>';
      
         try {
             const res = await fetch(`https://www.call2all.co.il/ym/api/GetIVR2Dir?token=${token}&path=${path}`);
             const data = await res.json();
             let filesMap = new Map();
      
             if (data.files) data.files.forEach(f => filesMap.set(f.name, f));
      
             const scanPromises = [];
             for (let i = 0; i <= maxRange; i++) {
                 const n = i.toString();
                 if (filesMap.has(n)) continue;
                 scanPromises.push(
                     fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${token}&what=ivr2:${path}/${n}/ext.ini`)
                     .then(r => r.json())
                     .then(d => { if (d.contents !== undefined) filesMap.set(n, { name: n, fileType: "DIR" }); })
                     .catch(() => {})
                 );
             }
             await Promise.all(scanPromises);
      
             tbody.innerHTML = '';
             filesMap.forEach(f => {
                 tbody.insertAdjacentHTML('beforeend', `
                     <tr>
                         <td><input type="checkbox" class="file-check" data-name="${f.name}" data-type="${f.fileType}" checked></td>
                         <td>${f.fileType === "DIR" || !isNaN(f.name) ? "שלוחה" : "קובץ"}</td>
                         <td>${f.name}</td>
                     </tr>`);
             });
      
             document.getElementById('fileArea').style.display = 'block';
             document.getElementById('downloadBtn').disabled = false;
             addLog("סריקה הושלמה.", "info");
         } catch (e) { addLog("שגיאה בסריקה", "error"); }
      }
      
      async function downloadRecursive(token, path, name, type, zipFolder) {
         const sPath = `${path}/${name}`.replace(/\/+/g, '/');
         const digits = parseInt(document.getElementById('digitCount').value) || 2;
      
         if (type === "DIR" || !isNaN(name)) {
             addLog(`מוריד שלוחה: ${sPath}`, "info");
             const newFolder = zipFolder.folder(name);
             
             try {
                 const iniRes = await fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${token}&what=ivr2:${sPath}/ext.ini`);
                 const iniData = await iniRes.json();
                 if (iniData.contents !== undefined) newFolder.file("ext.ini.txt", iniData.contents);
             } catch(e) {}
      
             const res = await fetch(`https://www.call2all.co.il/ym/api/GetIVR2Dir?token=${token}&path=${sPath}`);
             const data = await res.json();
             let children = data.files || [];
      
             const subScan = [];
             for(let i=0; i <= Math.pow(10, digits)-1; i++) {
                 const n = i.toString();
                 if(!children.find(c => c.name === n)) {
                     subScan.push(
                         fetch(`https://www.call2all.co.il/ym/api/GetTextFile?token=${token}&what=ivr2:${sPath}/${n}/ext.ini`)
                         .then(r => r.json())
                         .then(d => { if(d.contents !== undefined) children.push({name: n, fileType: "DIR"}); })
                     );
                 }
             }
             await Promise.all(subScan);
      
             for (const f of children) {
                 if (f.name === "ext.ini") continue;
                 await downloadRecursive(token, sPath, f.name, f.fileType, newFolder);
             }
         } else {
             try {
                 addLog(`מוריד קובץ: ${sPath}`, "info");
                 let finalName = name;
                 const lowerName = name.toLowerCase();
                 const isAudio = lowerName.endsWith('.mp3') || lowerName.endsWith('.wav');
                 
                 if (!isAudio && !lowerName.endsWith('.txt')) {
                     finalName = name + ".txt";
                 }
      
                 const dl = await fetch(`https://www.call2all.co.il/ym/api/DownloadFile?token=${token}&path=ivr2:${sPath}`);
                 const blob = await dl.blob();
                 zipFolder.file(finalName, blob);
             } catch(e) { addLog(`שגיאה בהורדת קובץ ${sPath}`, "error"); }
         }
      }
      
      async function startDownload() {
         const token = document.getElementById('srcToken').value;
         const userFileName = document.getElementById('fileNameInput').value || 'backup_ivr';
         const selected = Array.from(document.querySelectorAll('.file-check:checked'));
         
         if (selected.length === 0) { alert("לא נבחרו קבצים להורדה"); return; }
      
         // בקשת מיקום שמירה מהמשתמש לפני תחילת העבודה (בדפדפנים תומכים)
         let fileHandle = null;
         try {
             if ('showSaveFilePicker' in window) {
                 fileHandle = await window.showSaveFilePicker({
                     suggestedName: `${userFileName}.zip`,
                     types: [{
                         description: 'ZIP Archive',
                         accept: {'application/zip': ['.zip']},
                     }],
                 });
             }
         } catch (err) {
             if (err.name === 'AbortError') return; // המשתמש ביטל את חלונית השמירה
             addLog("דפדפן לא תומך בבחירת מיקום מראש, ההורדה תתבצע כרגיל בסיום.", "info");
         }
      
         zip = new JSZip(); 
         document.getElementById('downloadBtn').disabled = true;
         document.getElementById('progContainer').style.display = 'block';
      
         for (let i = 0; i < selected.length; i++) {
             const name = selected[i].getAttribute('data-name');
             const type = selected[i].getAttribute('data-type');
             await downloadRecursive(token, currentViewPath, name, type, zip);
             let p = Math.round(((i + 1) / selected.length) * 100);
             document.getElementById('progBar').style.width = p + '%';
             document.getElementById('progBar').innerText = p + '%';
         }
      
         addLog("מכין קובץ ZIP סופי...", "info");
         const content = await zip.generateAsync({type:"blob"});
      
         if (fileHandle) {
             // שמירה למיקום שהמשתמש בחר מראש
             const writable = await fileHandle.createWritable();
             await writable.write(content);
             await writable.close();
         } else {
             // הורדה רגילה לתיקיית ההורדות
             const link = document.createElement('a');
             link.href = URL.createObjectURL(content);
             link.download = `${userFileName}.zip`;
             link.click();
         }
         
         addLog("✅ ההורדה והשמירה הסתיימו בהצלחה!", "success");
         document.getElementById('downloadBtn').disabled = false;
      }
      
      function toggleAll(source) {
         document.querySelectorAll('.file-check').forEach(cb => cb.checked = source.checked);
      }
      </script>
      </body>
      </html>
      
      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם
    • RE: תגובה לקבצי HTML לשימוש במערכות ימות המשיח

      @אופיר תראה בזה

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ע
      עובד את השם