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