אני צריך דוח על שלוחה מסוימת איך אני מוציא את זה
-
אני צריך דוח על שלוחה מי התקשר וכמה זמן ומי השאיר הקלטה
-
@הפצת-התורה
יש כמה דרכים וכמה סוגי דוחות.
מי התקשר, שונה מאד ממי הקליט.
לדעת מי הקליט, אפשר לראות משלוח ההקלטות עצמה.
אתה רוצה הורדה? או צפיה בגוגל שיטס בזמן אמת? -
@הפצת-התורה יש לך את זה וגם את זה:
(תפתח בפנקס רשימות ותשמור בשם עם סיומת 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()">×</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()">×</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>