מצורף בספוילר קובץ להצגת הלוגים של המערכת (לוגי ymgr)
יש להעתיק את הקוד, לשמור בפקס רשימות וכדו', ולקרוא לקובץ מציג_לוגים_מתקדם.html
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>מציג לוגים מתקדם</title>
<style>
/* ------------------- עיצוב כללי ------------------- */
body {
font-family: 'Arial Hebrew', Arial, sans-serif;
margin: 0;
padding: 30px;
text-align: right;
direction: rtl;
background-color: #e9eef2;
color: #333;
}
.container {
max-width: 1700px;
margin: 0 auto;
}
h1 {
color: #1a5c92;
text-align: center;
margin-bottom: 30px;
font-size: 2em;
font-weight: bold;
}
/* ------------------- אזור הבקרה (פאנלים) ------------------- */
.control-panel {
background-color: #ffffff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 25px;
/* מרווח בין הפאנלים */
}
.control-row {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.control-panel label {
font-weight: bold;
color: #1a5c92;
white-space: nowrap;
}
.control-panel input[type="text"],
.control-panel select {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1em;
text-align: right;
direction: rtl;
}
/* סטייל לאינפוט של נתיב ידני */
.manual-path-input::placeholder {
color: #999;
font-style: italic;
}
.control-panel input#filter-input {
direction: rtl;
text-align: right;
margin-top: 5px;
}
.control-panel button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s;
}
.control-panel button:hover:not(:disabled) {
background-color: #45a049;
}
.control-panel button:disabled {
background-color: #a0a0a0;
cursor: not-allowed;
}
.control-panel .full-width {
flex-grow: 1;
}
.filter-group {
display: flex;
gap: 5px;
flex-grow: 1;
align-items: center;
}
.clear-filter-btn {
background-color: #c62828 !important;
padding: 10px 10px !important;
height: 40px;
font-size: 0.9em !important;
margin: 0;
white-space: nowrap;
}
.clear-filter-btn:hover:not(:disabled) {
background-color: #a02020 !important;
}
/* ------------------- בקרת נראות עמודות ------------------- */
#column-visibility-panel {
background-color: #f7f7f7;
padding: 15px;
border-radius: 8px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
margin-top: 15px;
}
#column-visibility-panel h4 {
margin-top: 0;
color: #1a5c92;
border-bottom: 1px dashed #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
/* 🆕 הוספנו פלקס כדי שהכפתור ישב ליד הכותרת */
display: flex;
align-items: center;
}
/* 🆕 סטייל לכפתור האיפוס */
.reset-cols-btn {
background-color: #28a745 !important;
color: white;
padding: 4px 8px !important;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 0.9em !important;
z-index: 100;
transition: background-color 0.3s;
direction: rtl;
margin-right: 15px;
/* מרווח מהכותרת */
height: auto !important;
line-height: normal !important;
white-space: nowrap;
}
.reset-cols-btn:hover:not(:disabled) {
background-color: #1e7e34 !important;
}
.col-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.col-checkbox-group label {
font-weight: normal;
color: #333;
user-select: none;
white-space: nowrap;
}
.col-checkbox-group label input[type="checkbox"] {
display: inline-block;
}
/* ------------------- פקדי פגינציה ------------------- */
.pagination-controls {
display: flex;
flex-direction: column;
justify-content: center;
/* ממורכז */
align-items: center;
padding: 10px 20px;
background-color: #f4f7f9;
border: 1px solid #ddd;
border-radius: 8px;
margin: 15px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
user-select: none;
flex-wrap: wrap;
gap: 10px;
/* מרווח בין קבוצות/טקסט */
}
/* קבוצת כפתורי הניווט ( Prev + Numbers + Next ) */
.pagination-nav-group {
display: flex;
align-items: center;
flex-wrap: wrap;
margin: 5px 0;
/* מרווח עליון/תחתון קטן */
}
/* סטיילינג כפתורים כללי */
.pagination-controls button {
padding: 8px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
white-space: nowrap;
}
/* סטיילינג כפתורים "קודם" ו"הבא" */
.pagination-controls .nav-button {
background-color: #1a5c92;
color: white;
padding: 8px 15px;
}
.pagination-controls .nav-button:hover:not(:disabled) {
background-color: #15476d;
}
.pagination-controls button:disabled {
background-color: #a0a0a0;
cursor: not-allowed;
}
/* רווחים בין כפתורי הניווט הראשיים לקבוצת המספרים */
.pagination-controls .nav-button.prev {
margin-left: 10px;
}
.pagination-controls .nav-button.next {
margin-right: 10px;
}
.pagination-controls .status-text {
font-weight: bold;
color: #1a5c92;
white-space: nowrap;
margin: 5px 0;
text-align: center;
}
/* סטיילינג למספרי העמודים (השינוי העיקרי) */
.pagination-controls .page-numbers {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.pagination-controls .page-numbers button {
background-color: #f7f7f7;
color: #1a5c92;
padding: 8px 5px;
/* צמצום padding כדי לאפשר רוחב קבוע קטן יותר */
border: 1px solid #ccc;
margin: 0 3px;
font-weight: normal;
/* 🆕 רוחב קבוע למספרי העמודים */
min-width: 50px;
text-align: center;
/* ------------------------------------- */
}
.pagination-controls .page-numbers button.active {
background-color: #1a5c92;
color: white;
font-weight: bold;
border: 1px solid #1a5c92;
cursor: default;
}
.pagination-controls .page-numbers button:hover:not(.active):not(:disabled) {
background-color: #e0f2f1;
}
/* ------------------- אזור התוצאות ------------------- */
#status-message {
margin-bottom: 15px;
padding: 10px;
border-radius: 5px;
font-weight: bold;
}
/* 🆕 סטייל מיוחד להבהרת הטוקן */
/* הסרנו את המרווחים הפנימיים והחיצוניים מכיוון שזה עכשיו בתוך control-row חדש */
#token-clarification {
font-size: 0.8em;
/* גופן לא גדול */
color: #555;
/* צבע אפור כהה */
text-align: right;
direction: rtl;
}
.data-table-container {
background-color: #ffffff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: auto;
margin-bottom: 25px;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
/* Sticky Header */
.data-table th {
border: 1px solid #ddd;
padding: 8px 12px;
text-align: center;
background-color: #f2f2f2;
color: #1a5c92;
font-weight: bold;
cursor: pointer;
user-select: none;
position: sticky;
top: 0;
z-index: 20;
max-width: 150px;
white-space: normal;
word-break: break-word;
}
.data-table th:after {
content: '';
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
}
.data-table th.asc:after {
content: ' ▲';
color: #4CAF50;
}
.data-table th.desc:after {
content: ' ▼';
color: #c62828;
}
.data-table td {
border: 1px solid #ddd;
padding: 8px 12px;
text-align: center;
max-width: 150px;
white-space: normal;
overflow-x: auto;
word-break: break-word;
}
.data-table tr {
cursor: pointer;
transition: background-color 0.15s;
}
.data-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.data-table tr:hover {
background-color: #e0f2f1;
}
/* הסתרת עמודות על פי ה-data-column-key */
.col-hidden {
display: none;
}
/* הגדרת רוחב מקסימלי לתאי API ספציפיים */
.data-table td[data-column-key="ApiSend"],
.data-table td[data-column-key="ApiAnswer"] {
max-width: 350px;
font-size: 0.9em;
direction: ltr;
}
/* ------------------- פאנל פרטים ------------------- */
#details-panel {
background-color: #fcfcfc;
border: 1px solid #ddd;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-top: 20px;
margin-bottom: 20px;
display: none;
text-align: right;
/* 🆕 הוספת מיקום יחסי עבור כפתור הסגירה המוחלט */
position: relative;
}
/* 🆕 סטייל לכפתור הסגירה - מעודכן למיקום שמאלי וקטן */
#details-panel .close-btn {
position: absolute;
top: 15px;
left: 20px;
/* 👈 מעביר את הכפתור לשמאל! */
background-color: #c62828;
color: white;
padding: 4px 8px;
/* מקטין את הכפתור */
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 0.9em;
/* מקטין את הפונט */
z-index: 100;
transition: background-color 0.3s;
direction: ltr;
}
#details-panel .close-btn:hover {
background-color: #a02020;
}
#details-panel h3 {
/* הסרנו את ה-padding-right שהיה מיותר */
/* שים לב! אם אתה משתמש ב-padding-left, זה יהיה המרווח משמאל (כפתור הסגירה) */
padding-left: 50px;
/* מרווח משמאל כדי למנוע התנגשות עם כפתור ה-X */
color: #1a5c92;
border-bottom: 2px solid #1a5c92;
padding-bottom: 5px;
margin-top: 0;
margin-bottom: 15px;
/* ודא שאין padding-right גבוה מדי שדוחף את הטקסט יותר מדי שמאלה */
}
#details-panel dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 10px 20px;
margin: 0;
padding: 0;
}
#details-panel dt {
font-weight: bold;
color: #333;
grid-column: 1;
white-space: nowrap;
}
#details-panel dd {
margin: 0;
grid-column: 2;
text-align: left;
direction: ltr;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
background-color: #eee;
padding: 5px;
border-radius: 3px;
overflow-x: auto;
max-width: 100%;
white-space: pre-wrap;
display: flex;
align-items: center;
gap: 10px;
}
#details-panel dd button {
background-color: #1a5c92;
color: white;
padding: 3px 8px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 0.8em;
flex-shrink: 0;
transition: background-color 0.2s, color 0.2s;
}
#details-panel dd button:hover:not(:disabled) {
background-color: #15476d;
}
/* סטייל ייעודי לכפתור העתקה לאחר לחיצה */
#details-panel dd button.copied-success {
background-color: #4CAF50;
/* ירוק */
}
#details-panel dd .value-text {
flex-grow: 1;
white-space: pre-wrap;
word-break: break-word;
}
/* עיצוב JSON מעוצב */
#details-panel dd .json-formatted {
white-space: pre;
}
/* איפוס העיצוב המיוחד מה-option */
#log-file-select option[disabled][selected] {
color: initial !important;
background-color: initial !important;
font-weight: initial !important;
font-size: initial !important;
padding: initial;
}
/* ------------------- מערכת סינון מתקדמת ------------------- */
#advanced-filter-controls {
border: 1px dashed #1a5c92;
padding: 10px;
margin-bottom: 15px;
border-radius: 5px;
background-color: #fcfdff;
}
.filter-rule-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px dotted #ddd;
}
.filter-rule-row:last-child {
border-bottom: none;
}
.filter-rule-row label {
font-weight: normal;
color: #333;
white-space: nowrap;
}
.filter-rule-row select,
.filter-rule-row input[type="text"] {
padding: 8px;
border-radius: 4px;
font-size: 0.9em;
flex-grow: 1;
}
.filter-rule-row .col-select,
.filter-rule-row .match-select {
flex-grow: 0;
min-width: 120px;
}
.filter-rule-row .value-input {
flex-grow: 3;
}
.filter-rule-row .remove-btn {
background-color: #dc3545 !important;
padding: 8px 10px !important;
height: auto;
font-size: 0.8em !important;
margin: 0;
flex-shrink: 0;
}
.filter-rule-row .remove-btn:hover:not(:disabled) {
background-color: #c82333 !important;
}
</style>
</head>
<body>
<div class="container">
<h1>📊 מציג לוגים מתקדם ומשוכלל</h1>
<div class="control-panel" id="panel-auth-select">
<div class="control-row">
<label for="user-token">הזן טוקן (token):</label>
<input type="text" id="user-token" placeholder="הכנס כאן את הטוקן שלך - `api_key` (לעת עתה עד השינוי המתוכנן יש להכניס: `מערכת:סיסמה` )" class="example-token-value">
<button onclick="fetchFilesAndSetup()" id="fetch-files-btn">טען רשימת קבצי לוג 🔄</button>
</div>
<div class="control-row" style="margin-top: -10px; margin-bottom: 5px; align-items: flex-start;">
<label for="user-token" style="visibility: hidden; user-select: none; opacity: 0; pointer-events: none;">הזן טוקן (token):</label>
<div id="token-clarification" class="full-width">
הבהרה: הטוקן אינו נשמר בשום צורה, ואינו מועבר לשום מקום. הטוקן משמש את התוכנה בלבד, באופן מקומי בלבד, ובעת הפעולה הנוכחית בלבד.
</div>
<button style="visibility: hidden; pointer-events: none; background-color: transparent; border: none; padding: 10px 20px; font-size: 1em;"></button>
</div>
<div class="control-row" id="file-selection-row" style="display: none; margin-bottom: 0;">
<label for="log-file-select" id="log-file-label">בחר קובץ לוג להצגה:</label>
<select id="log-file-select" class="full-width" disabled
onchange="handleFileSelectionChange()"></select>
<button onclick="fetchData()" id="fetch-data-btn" disabled>הצג לוג נבחר</button>
</div>
<div class="control-row" id="manual-path-row" style="display: none; margin-top: 15px;">
<label for="manual-path-input">נתיב תיקייה/קובץ:</label>
<input type="text" id="manual-path-input" class="full-width manual-path-input"
placeholder="הזן נתיב (ללא הקידומת `ivr2:` ). או נתיב תיקייה (לדוגמה: `4/2/6` ), או נתיב קובץ מלא (לדוגמה: `Log/LogPlaybackPlayStop/LogPlaybackPlayStop.2025-10-17.ymgr` )"
onkeypress="if(event.key === 'Enter') { processManualPathEntry(); }">
<button onclick="processManualPathEntry()" id="confirm-manual-path-btn">טען</button>
</div>
</div>
<div id="status-message"></div>
<div class="control-panel" id="panel-export"
style="display: none; padding: 15px 20px; justify-content: flex-start;">
<div class="control-row" style="margin-bottom: 0;">
<label style="font-weight: bold; color: #1a5c92; margin-left: 20px;">ייצוא טבלה:</label>
<button onclick="exportData('excel', this)" id="export-excel-btn" disabled
style="background-color: #1e7e34;">ייצא ל-CSV (תואם Excel) 📊</button>
</div>
</div>
<div class="control-panel" id="panel-filter-cols" style="display: none;">
<div id="advanced-filter-controls">
</div>
<div class="control-row" style="margin-top: 15px; justify-content: flex-end; gap: 15px;">
<button onclick="addFilterRule()" style="background-color: #28a745; margin-left: auto;">+ הוסף תנאי
סינון</button>
<label for="filter-logic-select" style="margin: 0; font-weight: bold;">לוגיקת סינון כללית:</label>
<select id="filter-logic-select" onchange="filterTable()" style="width: auto; flex-grow: 0;">
<option value="AND">AND (חייב לעמוד בכל התנאים)</option>
<option value="OR">OR (חייב לעמוד לפחות באחד התנאים)</option>
</select>
<button onclick="clearAllFilters()" class="clear-filter-btn" disabled>נקה הכל 🧹</button>
<button onclick="filterTable()" id="apply-filter-btn">הפעל סינון 🔎</button>
</div>
<div id="column-visibility-panel">
<h4>
👁️ שליטה בנראות עמודות:
<button class="reset-cols-btn" onclick="resetColumnVisibilityToDefault()">איפוס לברירת מחדל ♻️</button>
</h4>
<div id="col-checkbox-container" class="col-checkbox-group">
</div>
</div>
</div>
<div id="details-panel" style="display: none;">
<button class="close-btn" onclick="closeDetailsPanel()">X סגור</button>
<h3>פרטי שורה נבחרת</h3>
<dl id="details-content"></dl>
</div>
<div class="data-table-container">
<div id="results">
<p style="text-align: center; color: #666;">שלב 1: אנא הכנס טוקן ולחץ על "טען רשימת קבצי לוג" כדי
להתחיל.</p>
</div>
</div>
</div>
<script>
// ה-URL הקבוע
const API_BASE = "https://www.call2all.co.il/ym/api/";
const API_DIR_PATH = "GetIVR2Dir";
const API_RENDER_FILE = "RenderYMGRFile"; // משתנה ללא convertType
// ------------------- משתנים גלובליים לפגינציה ודאטה -------------------
let currentColumns = [];
let sortState = { columnIndex: -1, direction: 'asc' };
let tableData = [];
let selectedFileName = '';
// 🆕 משתנים חדשים לפגינציה, דאטה מסונן/ממוין, ומצב נראות העמודות
let filteredAndSortedData = []; // הדאטה אחרי סינון ומיון (ממנו נציג את ה-slice)
const ROWS_PER_PAGE = 100; // קובע כמה שורות להציג בכל עמוד
let currentPage = 1; // העמוד הנוכחי
let filterRules = []; // מערך שיכיל את כללי הסינון המורכבים
// 🆕 משתנה גלובלי זמני לשמירת מצב נראות העמודות *עבור הלוג הנוכחי בלבד*
let currentColumnVisibilityState = {};
document.addEventListener('DOMContentLoaded', () => {
const tokenInput = document.getElementById('user-token');
tokenInput.addEventListener('focus', function () {
this.classList.remove('example-token-value');
});
tokenInput.addEventListener('blur', function () {
if (this.value === '') {
this.classList.remove('example-token-value');
}
});
tokenInput.addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
fetchFilesAndSetup();
}
});
// הסתרת הפאנלים בתחילה
document.getElementById('panel-export').style.display = 'none';
document.getElementById('panel-filter-cols').style.display = 'none';
});
/**
* מטפל בשינוי בחירת הקובץ. מציג/מסתיר את שדה הקלט הידני.
*/
function handleFileSelectionChange() {
const selectElement = document.getElementById('log-file-select');
const manualPathRow = document.getElementById('manual-path-row');
if (selectElement.value === '__other__') {
manualPathRow.style.display = 'flex';
} else {
manualPathRow.style.display = 'none';
}
}
// ------------------- הוספת פונקציית העזר populateLogDropdown -------------------
/**
* פונקציית עזר למילוי רשימת הגלילה של הלוגים (משותף לטעינה ראשונית וטעינה ידנית).
* @param {Array<Object>} allYmgrFiles - רשימת הקבצים שמכילה את שדה what ו-mtime.
* @param {string} initialText - הטקסט שיופיע בכותרת ברירת המחדל של הגלילה.
* @param {boolean} isRecursiveSearch - האם התוצאות הגיעו מחיפוש רקורסיבי (כדי להחליט מה לבחור אוטומטית).
*/
function populateLogDropdown(allYmgrFiles, initialText, isRecursiveSearch = true) {
const select = document.getElementById('log-file-select');
const selectRow = document.getElementById('file-selection-row');
const fetchDataBtn = document.getElementById('fetch-data-btn');
const statusDiv = document.getElementById('status-message');
const resultsDiv = document.getElementById('results');
const manualPathRow = document.getElementById('manual-path-row');
// מיון וניקוי
allYmgrFiles.sort((a, b) => a.what.localeCompare(b.what));
// 1. הגדרת שורת ההוראה כ-disabled ו-selected
select.innerHTML = `<option value="" disabled selected>${initialText}</option>`;
// הוספת אפשרות "אחר"
select.innerHTML += '<option value="__other__">אחר (הזנה ידנית)</option>';
// הוספת הקבצים שנמצאו
allYmgrFiles.forEach(f => {
const option = document.createElement('option');
option.value = f.what;
// החלפת 'ivr2:' ב-''
const friendlyName = f.what.replace('ivr2:', '').replace(/\//g, '/');
option.textContent = `${friendlyName} (${f.mtime || 'אין תאריך'})`;
select.appendChild(option);
});
select.disabled = false;
fetchDataBtn.disabled = false;
selectRow.style.display = 'flex';
manualPathRow.style.display = 'none';
const fileCount = allYmgrFiles.length;
if (fileCount > 0) {
// 2. שמירת שורת ההוראה כברירת מחדל, אלא אם זה קובץ יחיד שהוזן ידנית
if (fileCount === 1 && !isRecursiveSearch) {
// מקרה מיוחד: נמצא קובץ לוג יחיד מחיפוש לא רקורסיבי (כנראה קובץ ספציפי הוזן)
select.value = allYmgrFiles[0].what;
statusDiv.innerHTML = `<span style="color:#2E7D32;">✅ נמצא קובץ לוג בודד: ${allYmgrFiles[0].what.replace('ivr2:', '')}. אנא לחץ "הצג לוג נבחר".</span>`;
} else {
// מקרה כללי: נמצאו מספר קבצים או קובץ אחד בחיפוש רקורסיבי.
// משאירים את ה-select על הערך הריק שנבחר בשלב 1 (שורת ההוראה).
statusDiv.innerHTML = `<span style="color:#2E7D32;">✅ רשימת הקבצים נטענה בהצלחה. נמצאו ${fileCount} קבצי לוג. אנא בחר קובץ ולחץ "הצג לוג נבחר".</span>`;
}
resultsDiv.innerHTML = '<p style="text-align: center; color: #1a5c92;">שלב 2/3: בחר קובץ לוג מהרשימה ולחץ על "הצג לוג נבחר".</p>';
} else {
// אם לא נמצאו קבצים
select.value = '__other__';
handleFileSelectionChange(); // מציג שוב את שדה הקלט
statusDiv.innerHTML = `<span style="color:#c62828;">❌ לא נמצאו קבצי לוג בנתיב המבוקש. אנא נסה נתיב אחר.</span>`;
resultsDiv.innerHTML = '<p style="text-align: center; color: #c62828;">אנא הזן נתיב חדש ובצע חיפוש.</p>';
}
}
// ------------------- סוף הוספת פונקציית העזר -------------------
/**
* פונקציה רקורסיבית לטעינת כל קבצי ה-YMGR מנתיב נתון ומתיקיות המשנה שלו.
*/
async function fetchFilesFromPath(path, token, allFiles = []) {
const encodedPath = encodeURIComponent(path);
const fullUrl = `${API_BASE}${API_DIR_PATH}?path=${encodedPath}&token=${token}`;
try {
const response = await fetch(fullUrl);
const jsonResponse = await response.json();
// 🆕 שינוי 2: בדיקה אם הטוקן אינו תקין (ResponseStatus אינו OK).
if (jsonResponse.responseStatus !== 'OK') {
// זורק שגיאה עם המידע המלא, שתטופל בפונקציה הקוראת
throw new Error(`API_ERROR:${jsonResponse.responseStatus}:${jsonResponse.message}`);
}
const ymgrFiles = jsonResponse.files.filter(f => f.fileType === 'YMGR');
allFiles.push(...ymgrFiles);
for (const dir of jsonResponse.dirs) {
const newPath = `${path}/${dir.name}`;
// יש להבטיח ששגיאות בנתיבי משנה לא יעצרו את הכל
await fetchFilesFromPath(newPath, token, allFiles);
}
return allFiles;
} catch (error) {
// אם השגיאה היא שגיאת API שזרקנו (טוקן לא תקין) - נזרוק אותה שוב.
if (error.message.startsWith('API_ERROR')) {
throw error;
}
console.error(`❌ שגיאת רשת/קוד בטעינת נתיב: ${path}`, error);
// במקרה של שגיאת רשת אחרת - נחזיר את מה שיש ונמשיך
return allFiles;
}
}
/**
* פונקציה לטעינת רשימת קבצי ה-YMGR (כולל רקורסיה) והכנת הממשק לבחירה.
*/
async function fetchFilesAndSetup() {
const tokenInput = document.getElementById('user-token');
const token = tokenInput.value.trim();
const statusDiv = document.getElementById('status-message');
const select = document.getElementById('log-file-select');
const selectRow = document.getElementById('file-selection-row');
const fetchDataBtn = document.getElementById('fetch-data-btn');
const resultsDiv = document.getElementById('results');
const manualPathRow = document.getElementById('manual-path-row');
const exportPanel = document.getElementById('panel-export');
const filterPanel = document.getElementById('panel-filter-cols');
const exportExcelBtn = document.getElementById('export-excel-btn');
statusDiv.innerHTML = '<span style="color:#1a5c92;">טוען רשימת קבצים באופן רקורסיבי... 🔄</span>';
select.innerHTML = '<option value="">טוען...</option>';
select.disabled = true;
fetchDataBtn.disabled = true;
resultsDiv.innerHTML = '<p style="text-align: center; color: #666;">מחפש קבצי לוג בכל תיקיות המשנה...</p>';
document.getElementById('details-panel').style.display = 'none';
document.getElementById('apply-filter-btn').disabled = true;
document.querySelector('.clear-filter-btn').disabled = true;
manualPathRow.style.display = 'none';
exportPanel.style.display = 'none';
filterPanel.style.display = 'none';
exportExcelBtn.disabled = true;
if (!token) {
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ אנא הכנס טוקן תקין.</span>`;
return;
}
tokenInput.classList.remove('example-token-value');
const initialPath = 'ivr2:/Log';
let allYmgrFiles = [];
let isTokenInvalid = false;
try {
// שימוש ב-await כיוון ש-fetchFilesFromPath עשויה לזרוק שגיאה
allYmgrFiles = await fetchFilesFromPath(initialPath, token);
} catch (error) {
if (error.message.startsWith('API_ERROR')) {
const parts = error.message.split(':');
const status = parts[1];
const msg = parts.slice(2).join(':');
// 🆕 שינוי 2: טיפול בהודעות שגיאה ספציפיות לטוקן לא תקין
if (status === 'FORBIDDEN' || status === 'EXCEPTION' || msg.includes('session token is invalid') || msg.includes('user name or password do not match')) {
statusDiv.innerHTML = `<span style="color:#c62828; font-size:1.1em; font-weight: bold;">❌ שגיאת טוקן: הטוקן אינו תקין או פג תוקף.</span>
<span style="font-size: 0.9em; color:#888;"> פרטי שגיאה: ${msg}</span>`;
isTokenInvalid = true;
} else {
// שגיאת API אחרת
statusDiv.innerHTML = `<span style="color:#c62828;">❌ שגיאת API כללית בטעינת רשימת הקבצים: ${msg}</span>`;
isTokenInvalid = true; // נתייחס לזה כשגיאה חוסמת
}
} else {
// שגיאת רשת או אחרת (שגיאת קוד)
statusDiv.innerHTML = `<span style="color:#c62828;">❌ שגיאה כללית או שגיאת רשת. אנא בדוק את הקונסול.</span>`;
isTokenInvalid = true;
}
}
if (isTokenInvalid) {
resultsDiv.innerHTML = '<p style="text-align: center; color: #c62828;">אנא תקן את הטוקן ונסה שוב.</p>';
return; // יציאה במקרה של שגיאת טוקן
}
if (allYmgrFiles.length === 0) {
statusDiv.innerHTML = `<span style="color:#1a5c92;">✅ הנתונים נטענו בהצלחה, אך לא נמצאו קבצי YMGR בנתיב: ${initialPath} או בתיקיות המשנה.</span>`;
// במקרה שלא נמצאו קבצים, עדיין מאפשרים הזנה ידנית:
select.innerHTML = '<option value="" disabled selected>בחר קובץ לוג להצגה...</option><option value="__other__" selected>אחר (הזנה ידנית)</option>';
select.disabled = false;
fetchDataBtn.disabled = false;
selectRow.style.display = 'flex';
manualPathRow.style.display = 'flex';
resultsDiv.innerHTML = '<p style="text-align: center; color: #1a5c92;">שלב 2: לא נמצאו קבצים אוטומטית. ניתן להזין נתיב ידני למטה.</p>';
return;
}
// 🆕 שימוש בפונקציית העזר החדשה במקום הלוגיקה המקורית
const initialText = 'שים לב! כברירת מחדל מוצגים ברשימה רק לוגים מתיקיית `Log`, לבחירת לוג מנתיב אחר בחר `אחר`.';
populateLogDropdown(allYmgrFiles, initialText, true);
}
// ------------------- הוספת פונקציית processManualPathEntry -------------------
/**
* פונקציה שמטפלת בהזנה ידנית של נתיב (תיקייה או קובץ) וממלאת את רשימת הגלילה.
*/
async function processManualPathEntry() {
const token = document.getElementById('user-token').value.trim();
const manualPathInput = document.getElementById('manual-path-input');
const manualPath = manualPathInput.value.trim();
const statusDiv = document.getElementById('status-message');
const select = document.getElementById('log-file-select');
const selectRow = document.getElementById('file-selection-row');
const fetchDataBtn = document.getElementById('fetch-data-btn');
const resultsDiv = document.getElementById('results');
const manualPathRow = document.getElementById('manual-path-row');
if (!token) {
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ אנא הכנס טוקן תקין.</span>`;
return;
}
if (!manualPath) {
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ אנא הזן נתיב לוג או תיקייה.</span>`;
return;
}
// איפוס UI
statusDiv.innerHTML = `<span style="color:#1a5c92;">מבצע חיפוש בנתיב: ${manualPath} 🔄</span>`;
select.disabled = true;
fetchDataBtn.disabled = true;
selectRow.style.display = 'flex';
resultsDiv.innerHTML = '<p style="text-align: center; color: #666;">מחפש קבצי לוג...</p>';
const cleanPath = manualPath.replace(/^\/|\/$/g, '');
const basePath = `ivr2:${cleanPath}`;
let allYmgrFiles = [];
let isApiError = false;
// 1. בדיקה אם הנתיב הוא קובץ ספציפי (מסתיים ב-.ymgr)
if (cleanPath.toLowerCase().endsWith('.ymgr')) {
// אם הוזן קובץ ספציפי - מוסיפים אותו בלבד לרשימה
const fakeFile = {
what: basePath,
name: cleanPath.split('/').pop(),
fileType: 'YMGR',
mtime: 'נתיב ידני',
size: 0
};
allYmgrFiles = [fakeFile];
const initialText = 'נמצא קובץ יחיד, ניתן להציג אותו או לבחור בחיפוש ידני נוסף.';
populateLogDropdown(allYmgrFiles, initialText, false); // false = לא רקורסיבי
return;
}
// 2. נניח שמדובר בנתיב תיקייה ומבצעים חיפוש רקורסיבי
try {
allYmgrFiles = await fetchFilesFromPath(basePath, token);
} catch (error) {
if (error.message.startsWith('API_ERROR')) {
const parts = error.message.split(':');
const status = parts[1];
const msg = parts.slice(2).join(':');
if (status === 'FORBIDDEN' || status === 'EXCEPTION' || msg.includes('session token is invalid') || msg.includes('user name or password do not match')) {
statusDiv.innerHTML = `<span style="color:#c62828; font-size:1.1em; font-weight: bold;">❌ שגיאת טוקן: הטוקן אינו תקין או פג תוקף.</span>
<span style="font-size: 0.9em; color:#888;"> פרטי שגיאה: ${msg}</span>`;
isApiError = true;
} else if (msg.includes('dir not found')) {
// זו כנראה לא שגיאה חוסמת, אלא שהנתיב שהוזן לא נמצא, נמשיך הלאה.
console.warn(`הנתיב שהוזן לא נמצא כתיקייה: ${manualPath}. מנסה להניח שמדובר בטקסט חלקי...`);
}
}
}
if (isApiError) return;
// 3. מילוי רשימת הגלילה עם הקבצים שנמצאו (אם נמצאו)
const initialText = `נמצאו לוגים בנתיב: ${manualPath}.`;
populateLogDropdown(allYmgrFiles, initialText, true);
}
// ------------------- סוף הוספת הפונקציה -------------------
/**
* מנתח מחרוזת CSV באמצעות הספרייה PapaParse.
*/
function parseCSV(csv) {
const results = Papa.parse(csv, {
header: true,
skipEmptyLines: true,
trimHeaders: true,
delimiter: ',',
});
return results.data;
}
/**
* פונקציה לטעינת הנתונים מהקובץ הנבחר והצגתם.
*/
async function fetchData() {
const token = document.getElementById('user-token').value.trim();
const selectElement = document.getElementById('log-file-select');
let selectedFileWhat = selectElement.value;
let finalWhatPath;
// אם לא נבחר קובץ ספציפי
if (!selectedFileWhat) {
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ אנא בחר קובץ לוג מהרשימה או בחר 'אחר'.</span>`;
return;
}
// 🆕 טיפול בנתיב ידני (אם נבחר 'אחר') - מפנה ללחיצה על הכפתור החדש
if (selectedFileWhat === '__other__') {
const statusDiv = document.getElementById('status-message');
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ אנא בחר קובץ לוג מהרשימה. לבחירת לוגים מנתיב אחר, הזן נתיב בתיבה ולחץ 'חפש קבצים/אישור'.</span>`;
document.getElementById('manual-path-row').style.display = 'flex'; // ודא שהקלט הידני גלוי
return;
}
// ------------------------------------------------------------------
finalWhatPath = selectedFileWhat; // השתמש בנתיב המלא מהרשימה
// מציאת שם הקובץ הנבחר (כולל הנתיב המוצג)
selectedFileName = selectElement.options[selectElement.selectedIndex].textContent; // 🆕 שמירה בגלובלי
const resultsDiv = document.getElementById('results');
const statusDiv = document.getElementById('status-message');
const exportPanel = document.getElementById('panel-export');
const filterPanel = document.getElementById('panel-filter-cols');
const exportExcelBtn = document.getElementById('export-excel-btn');
if (!token) {
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ אנא הכנס טוקן תקין.</span>`;
return;
}
resultsDiv.innerHTML = '<div style="text-align:center; color:#1a5c92;">טוען נתונים מהלוג המבוקש... 🔄</div>';
statusDiv.innerHTML = '<span style="color:#1a5c92;">טוען נתונים מהלוג המבוקש... 🔄</span>';
document.getElementById('apply-filter-btn').disabled = true;
document.querySelector('.clear-filter-btn').disabled = true;
exportExcelBtn.disabled = true;
document.getElementById('details-panel').style.display = 'none';
exportPanel.style.display = 'none';
filterPanel.style.display = 'none';
const encodedWhat = encodeURIComponent(finalWhatPath);
// שינוי ה-convertType ל-csv
const fullUrl = `${API_BASE}${API_RENDER_FILE}?convertType=csv&token=${token}&wath=${encodedWhat}`;
try {
const response = await fetch(fullUrl);
const rawText = await response.text();
if (rawText.includes('Not Found') || rawText.includes('File not found') || rawText.includes('session token is invalid') || rawText.includes('user name or password do not match')) {
statusDiv.innerHTML = `<span style="color:#c62828;">❌ שגיאה: לא ניתן לטעון את הלוג. ייתכן שהקובץ לא קיים, הטוקן אינו תקין או שגיאת API.</span>`;
resultsDiv.innerHTML = '';
return;
}
// טיפול בקובץ ריק או ללא תוכן (כותרות בלבד)
const lines = rawText.trim().split('\n');
if (lines.length <= 1) {
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ הקובץ נטען, אך הוא ריק או מכיל כותרות בלבד.</span>`;
resultsDiv.innerHTML = '';
return;
}
const csv = rawText;
tableData = parseCSV(csv);
if (tableData.length === 0) {
statusDiv.innerHTML = `<span style="color:#c62828;">⚠️ שגיאת ניתוח CSV או שאין נתונים בלוג.</span>`;
resultsDiv.innerHTML = '';
return;
}
// קביעת העמודות מתוך כותרות ה-CSV
currentColumns = Object.keys(tableData[0]);
// 🆕 איפוס מצב הנראות הגלובלי (הזמני)
currentColumnVisibilityState = {};
// הגדרת הנתונים המסוננים והממוינים ככל הנתונים בהתחלה
filteredAndSortedData = [...tableData];
currentPage = 1; // איפוס לעמוד הראשון
// קריאה לפונקציה החדשה להגדרת נראות העמודות
setupColumnVisibilityControls(currentColumns, selectedFileName);
renderPaginatedTable(filteredAndSortedData, currentColumns);
attachSortListeners(currentColumns);
// הפעלת מערכת הסינון המתקדמת
setupAdvancedFilterControls(currentColumns);
// עדכון פקדים
document.getElementById('apply-filter-btn').disabled = false;
document.querySelector('.clear-filter-btn').disabled = false;
exportExcelBtn.disabled = false;
// הצגת פאנלי הייצוא והסינון
exportPanel.style.display = 'flex';
filterPanel.style.display = 'block';
const recordCount = tableData.length;
// עדכון טקסט הסטטוס
statusDiv.innerHTML = `<span style="color:#2E7D32;">✅ הנתונים נטענו בהצלחה. סה"כ שורות בלוג: ${recordCount}. פרטי הלוג: ${selectedFileName}.</span>`;
} catch (error) {
console.error("Fetch/Parsing Error:", error);
statusDiv.innerHTML = `<span style="color:#c62828;">❌ שגיאה כללית בטעינת הנתונים או בניתוח ה-CSV. אנא בדוק את הקונסול.</span>`;
resultsDiv.innerHTML = '';
}
}
/**
* פונקציה לייצוא נתוני הטבלה לקובץ CSV/Excel עם פידבק UI.
*/
function exportData(type, buttonElement) {
if (!tableData || tableData.length === 0) {
alert("אין נתונים לייצוא.");
return;
}
const originalText = buttonElement.textContent;
const originalBg = buttonElement.style.backgroundColor;
// 1. UI Feedback - Start
buttonElement.textContent = 'מייצא...⏳';
buttonElement.style.backgroundColor = '#ffc107';
buttonElement.disabled = true;
const columns = currentColumns;
const mimeType = 'text/csv;charset=utf-8;'; // CSV with BOM
// לוגיקה ליצירת שם קובץ
let baseName = selectedFileName;
const parts = baseName.split('(');
let mainPart = parts[0].trim().replace(/\s*>\s*/g, '/');
if (mainPart.endsWith('.ymgr')) {
mainPart = mainPart.substring(0, mainPart.lastIndexOf('.'));
}
const filename = `Log_Export_${mainPart.replace(/[^a-zA-Z0-9]/g, '_')}_${new Date().toISOString().slice(0, 10)}.csv`;
// 3. יצירת CSV
const csvData = [columns.join(',')];
filteredAndSortedData.forEach(item => {
const row = columns.map(col => {
let value = item[col] !== undefined ? String(item[col]) : '';
// 4. ניקוי והגנה - החלף גרשיים כפולים (") בגרשיים כפולים כפולים ("") ועטוף הכל בגרשיים כפולים אם מכיל פסיקים או גרשיים.
value = value.replace(/"/g, '""');
if (value.includes(',') || value.includes('\n') || value.includes('"')) {
value = `"${value}"`;
}
return value;
}).join(',');
csvData.push(row);
});
const csv = csvData.join('\n');
// 4. הורדה לקובץ (כולל BOM לתמיכה טובה בעברית ב-Excel)
const blob = new Blob(["\uFEFF", csv], { type: mimeType });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 5. UI Feedback - Success
buttonElement.textContent = '✅ הושלם';
buttonElement.style.backgroundColor = '#28a745';
// Revert after a short delay
setTimeout(() => {
buttonElement.textContent = originalText;
buttonElement.style.backgroundColor = originalBg;
buttonElement.disabled = false;
}, 2000);
} else {
alert('הדפדפן אינו תומך בהורדה אוטומטית. אנא העתק את הנתונים ידנית.');
// 5. UI Feedback - Revert on failure
buttonElement.textContent = originalText;
buttonElement.style.backgroundColor = originalBg;
buttonElement.disabled = false;
}
}
/**
* פונקציה לייצור HTML של פקדי הפגינציה (כולל מספרי עמודים).
*/
function createPaginationControls(dataLength, current, total) {
if (total <= 1) return '';
const isPrevDisabled = current === 1;
const isNextDisabled = current === total;
const startIndex = (current - 1) * ROWS_PER_PAGE + 1;
let endIndex = Math.min(startIndex + ROWS_PER_PAGE - 1, dataLength);
let pageNumbersHtml = '';
const MAX_PAGES_SHOWN = 5; // מספר הכפתורים המקסימלי שיוצג
// חישוב טווח הכפתורים להצגה
let startPage = Math.max(1, current - Math.floor(MAX_PAGES_SHOWN / 2));
let endPage = Math.min(total, startPage + MAX_PAGES_SHOWN - 1);
// התאמה אם הגענו לסוף
if (endPage - startPage + 1 < MAX_PAGES_SHOWN) {
startPage = Math.max(1, endPage - MAX_PAGES_SHOWN + 1);
}
// בניית כפתורי מספרי העמודים
pageNumbersHtml += '<div class="page-numbers">';
// כפתור '1...' אם אנחנו לא קרובים להתחלה
if (startPage > 1) {
pageNumbersHtml += `<button onclick="goToPage(1)">1...</button>`;
}
// כפתורי העמודים המרכזיים
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === current ? 'active' : '';
pageNumbersHtml += `<button class="${activeClass}" onclick="goToPage(${i})">${i}</button>`;
}
// כפתור '...Total' אם אנחנו לא קרובים לסוף
if (endPage < total) {
pageNumbersHtml += `<button onclick="goToPage(${total})">...${total}</button>`;
}
pageNumbersHtml += '</div>';
// יצירת פקדי הפגינציה
return `
<div class="pagination-controls">
<div class="pagination-nav-group">
<button class="nav-button prev" onclick="goToPage(${current - 1})" ${isPrevDisabled ? 'disabled' : ''}>« הקודם</button>
${pageNumbersHtml}
<button class="nav-button next" onclick="goToPage(${current + 1})" ${isNextDisabled ? 'disabled' : ''}>הבא »</button>
</div>
<div class="status-text">
מציג שורות ${startIndex} עד ${endIndex} מתוך ${dataLength} | עמוד ${current} מתוך ${total}
</div>
</div>
`;
}
/**
* מנווט לעמוד פגינציה ספציפי ומרנדר את הטבלה מחדש.
*/
function goToPage(pageNumber) {
if (pageNumber === currentPage || pageNumber < 1) return;
const totalPages = Math.ceil(filteredAndSortedData.length / ROWS_PER_PAGE);
if (pageNumber > totalPages && totalPages > 0) return;
currentPage = pageNumber;
renderPaginatedTable(filteredAndSortedData, currentColumns);
// גלילה למעלה לראש הטבלה
document.getElementById('results').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/**
* מרנדר את הטבלה עם נתונים מפוגננים (slice).
* @param {Array<Object>} data - הנתונים המסונן/ממוין
* @param {Array<string>} columns - רשימת העמודות
*/
function renderPaginatedTable(data, columns) {
const resultsDiv = document.getElementById('results');
const dataLength = data.length;
// 1. חישוב פגינציה
const totalPages = Math.ceil(dataLength / ROWS_PER_PAGE);
// ודא שהעמוד הנוכחי נמצא בטווח
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
} else if (currentPage < 1) {
currentPage = 1;
} else if (totalPages === 0) {
currentPage = 0;
}
const startIndex = (currentPage - 1) * ROWS_PER_PAGE;
const endIndex = startIndex + ROWS_PER_PAGE;
const currentSlice = data.slice(startIndex, endIndex);
// 2. יצירת פקדי הפגינציה
const paginationControlsHtml = createPaginationControls(dataLength, currentPage, totalPages);
let html = paginationControlsHtml; // הוספת הפקדים מעל הטבלה
// אם אין נתונים בסינון, הצג הודעה
if (dataLength === 0) {
html += '<p style="text-align: center; color: #c62828; font-weight: bold; margin-top: 20px;">❌ לא נמצאו שורות תואמות לכללי הסינון הנוכחיים.</p>';
resultsDiv.innerHTML = html;
return;
}
// 3. בניית הטבלה
html += '<table class="data-table" id="call-log-table"><thead><tr>';
// עמודת אינדקס
html += '<th data-column-key="__index_col__">#</th>';
// יצירת הכותרות (Headers)
columns.forEach(col => {
const headerClass = sortState.columnIndex !== -1 && columns[sortState.columnIndex - 1] === col ? sortState.direction : '';
html += `<th data-column-key="${col}" class="${headerClass}">${col}</th>`;
});
html += '</tr></thead><tbody>';
// בניית שורות הטבלה (מה-slice הנוכחי)
currentSlice.forEach((item, indexInSlice) => {
// האינדקס הגלובלי (במערך הממוין/מסונן)
const globalIndex = startIndex + indexInSlice;
html += `<tr onclick="showRowDetails(this, ${globalIndex})">`;
// הצגת האינדקס הגלובלי
html += `<td>${globalIndex + 1}</td>`;
columns.forEach(col => {
const cellValue = item[col] !== undefined ? item[col] : '';
html += `<td data-column-key="${col}">${cellValue}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
html += paginationControlsHtml; // הוספת הפקדים מתחת לטבלה
resultsDiv.innerHTML = html;
// החלת נראות העמודות לאחר יצירת הטבלה
applyColumnVisibility();
}
/**
* מוסיף מאזיני אירועים לכותרות הטבלה לצורך מיון.
*/
function attachSortListeners(columns) {
const table = document.getElementById('call-log-table');
if (!table) return;
const headers = table.querySelectorAll('th');
headers.forEach((header, index) => {
// מחיקת מאזינים ישנים
if (header.sortHandler) {
header.removeEventListener('click', header.sortHandler);
}
// העמודה הראשונה היא האינדקס, לה נותנים מפתח מיוחד
const columnKey = index === 0 ? '__index_col__' : columns[index - 1];
// יצירת המאזין החדש
const newHandler = () => {
sortTable(index, columns, columnKey);
};
// שמירת המאזין על הכותרת כדי למחוק אותו בפעם הבאה
header.sortHandler = newHandler;
header.addEventListener('click', newHandler);
});
}
/**
* מבצע מיון של הנתונים הגלובליים המסוננים/ממוינים ומציג את התוצאות מחדש.
*/
function sortTable(columnIndexInTable, columns, columnKey) {
const table = document.getElementById('call-log-table');
const header = table.querySelectorAll('th')[columnIndexInTable];
let direction = sortState.direction;
// אם זו אותה עמודה, החלף כיוון
if (sortState.columnIndex === columnIndexInTable) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
direction = 'asc';
}
sortState.columnIndex = columnIndexInTable;
sortState.direction = direction;
// הסרת קלאסים מכל הכותרות
table.querySelectorAll('th').forEach(th => {
th.classList.remove('asc', 'desc');
});
header.classList.add(direction);
// קביעת סוג המיון
const columnName = columnKey;
const isNumeric = columnName === '__index_col__' || columnName.includes('שניות') || columnName.includes('מספר') || columnName.includes('Time') || columnName.includes('טלפון') || columnName.includes('כמות');
// מיון הנתונים המסוננים/ממוינים בזיכרון
filteredAndSortedData.sort((item1, item2) => {
let v1 = item1[columnKey] !== undefined ? String(item1[columnKey]) : '';
let v2 = item2[columnKey] !== undefined ? String(item2[columnKey]) : '';
let comparison = 0;
if (columnKey === '__index_col__') {
// מיון לפי אינדקס גלובלי (כאשר אין מידע אחר)
// משתמשים במיקום המקורי שלהם ב-tableData כדי לשמור על יציבות
const index1 = tableData.indexOf(item1);
const index2 = tableData.indexOf(item2);
comparison = index1 - index2;
} else if (isNumeric) {
// למיון מספרי
const cleanV1 = v1.replace(/[^\d.]/g, '');
const cleanV2 = v2.replace(/[^\d.]/g, '');
v1 = parseFloat(cleanV1) || 0;
v2 = parseFloat(cleanV2) || 0;
comparison = v1 - v2;
} else {
// למיון טקסט
comparison = v1.localeCompare(v2);
}
return comparison * (direction === 'asc' ? 1 : -1);
});
currentPage = 1; // איפוס לעמוד הראשון לאחר מיון
renderPaginatedTable(filteredAndSortedData, currentColumns);
}
// ------------------- פונקציות סינון מורכבות 🆕 -------------------
/**
* מפעיל את פקדי הסינון המתקדמים ויוצר את כלל הסינון הראשון.
*/
function setupAdvancedFilterControls(columns) {
const filterContainer = document.getElementById('advanced-filter-controls');
filterContainer.innerHTML = '';
filterRules = [];
// וודא ש'נקה הכל' וכפתור ההפעלה זמינים
document.querySelector('.clear-filter-btn').disabled = false;
document.getElementById('apply-filter-btn').disabled = false;
// יוצר את כלל הסינון הראשון
addFilterRule(columns);
}
/**
* מוסיף כלל סינון חדש לטופס.
*/
function addFilterRule() {
const columns = currentColumns;
const ruleId = Date.now();
const ruleIndex = filterRules.length;
const newRule = {
id: ruleId,
logic: ruleIndex > 0 ? 'AND' : null, // הכלל הראשון לא צריך לוגיקה מקדימה
column: '__all_cols__',
matchType: 'contains',
value: ''
};
filterRules.push(newRule);
const container = document.getElementById('advanced-filter-controls');
const columnOptions = columns.map(col => `<option value="${col}">${col}</option>`).join('');
const ruleHtml = `
<div class="filter-rule-row" data-rule-id="${ruleId}">
${ruleIndex > 0 ? `<label for="logic-select-${ruleId}">שילוב:</label>
<select id="logic-select-${ruleId}" class="col-select" onchange="updateFilterRule(${ruleId}, 'logic', this.value)">
<option value="AND">וגם (AND)</option>
<option value="OR">או (OR)</option>
</select>` : ''}
<label for="column-select-${ruleId}">סנן לפי עמודה:</label>
<select id="column-select-${ruleId}" class="col-select" onchange="updateFilterRule(${ruleId}, 'column', this.value)">
<option value="__all_cols__">כל העמודות</option>
${columnOptions}
</select>
<label for="match-select-${ruleId}">סוג סינון:</label>
<select id="match-select-${ruleId}" class="match-select" onchange="updateFilterRule(${ruleId}, 'matchType', this.value)">
<option value="contains">מכיל (לבן - 'מופיע')</option>
<option value="not_contains">לא מכיל (שחור - 'לא מופיע')</option>
<option value="exact">שווה בדיוק</option>
<option value="not_exact">לא שווה בדיוק</option>
<option value="starts_with">מתחיל ב</option>
<option value="ends_with">נגמר ב</option>
<option value="is_empty">ריק</option>
<option value="is_not_empty">לא ריק</option>
</select>
<label for="value-input-${ruleId}">ערך:</label>
<input type="text" id="value-input-${ruleId}" class="value-input" oninput="updateFilterRule(${ruleId}, 'value', this.value)" placeholder="ערך לסינון..." onkeypress="if(event.key === 'Enter') { filterTable(); }">
<button class="remove-btn" onclick="removeFilterRule(${ruleId})">X הסר</button>
</div>
`;
container.insertAdjacentHTML('beforeend', ruleHtml);
// וודא שברירת המחדל של העמודה מוגדרת
document.getElementById(`column-select-${ruleId}`).value = '__all_cols__';
}
/**
* מעדכן כלל סינון במערך הגלובלי.
*/
function updateFilterRule(ruleId, key, value) {
const rule = filterRules.find(r => r.id === ruleId);
if (rule) {
// שמירת הערך כפי שהוזן, ללא הסרת רווחים
rule[key] = value;
}
}
/**
* מסיר כלל סינון מה-HTML ומהמערך הגלובלי.
*/
function removeFilterRule(ruleId) {
filterRules = filterRules.filter(r => r.id !== ruleId);
const elementToRemove = document.querySelector(`.filter-rule-row[data-rule-id="${ruleId}"]`);
if (elementToRemove) {
elementToRemove.remove();
// אם הסרנו את הכלל הראשון (אינדקס 0), ודא שהכלל הבא אחריו לא מציג שילוב
const firstRemainingRule = document.querySelector('.filter-rule-row:first-of-type');
if (firstRemainingRule) {
const logicSelect = firstRemainingRule.querySelector('[id^="logic-select-"]');
const logicLabel = firstRemainingRule.querySelector('label[for^="logic-select-"]');
if (logicSelect) {
logicSelect.remove();
}
if (logicLabel) {
logicLabel.remove();
}
}
}
// הפעל סינון אוטומטית אם היו כללים פעילים
if (tableData.length > 0) {
filterTable();
}
}
/**
* מנקה את כל כללי הסינון ומאפס את הטבלה לנתוני המקור.
*/
function clearAllFilters() {
const filterContainer = document.getElementById('advanced-filter-controls');
const statusDiv = document.getElementById('status-message');
filterContainer.innerHTML = '';
filterRules = [];
document.getElementById('filter-logic-select').value = 'AND';
// איפוס נתונים
filteredAndSortedData = [...tableData];
sortState = { columnIndex: -1, direction: 'asc' }; // איפוס מיון
currentPage = 1;
// רינדור מחדש
renderPaginatedTable(filteredAndSortedData, currentColumns);
// איפוס פקדים
document.querySelector('.clear-filter-btn').disabled = true;
document.getElementById('apply-filter-btn').disabled = true;
// יוצר כלל סינון ראשון מחדש כדי לאפשר סינון קל
setupAdvancedFilterControls(currentColumns);
document.querySelector('.clear-filter-btn').disabled = true; // שוב, כדי שישאר כבוי
statusDiv.innerHTML = `<span style="color:#28a745; font-weight: bold;">✅ כללי הסינון נוקו. מוצגות כל ${tableData.length} השורות.</span>`;
// מחיקת הודעת הסטטוס לאחר מספר שניות
setTimeout(() => {
if (statusDiv.innerHTML.includes('כללי הסינון נוקו')) {
statusDiv.innerHTML = `<span style="color:#2E7D32;">✅ הנתונים נטענו בהצלחה. סה"כ שורות בלוג: ${tableData.length}. פרטי הלוג: ${selectedFileName}.</span>`;
}
}, 3000);
}
/**
* מבצע את הסינון על הנתונים הגלובליים ומציג את התוצאות מחדש.
*/
function filterTable() {
const statusDiv = document.getElementById('status-message');
const generalLogic = document.getElementById('filter-logic-select').value;
// כללים פעילים (שיש להם ערך)
const activeRules = filterRules.filter(r => r.value.trim() !== '' || r.matchType === 'is_empty' || r.matchType === 'is_not_empty');
if (activeRules.length === 0) {
// אין כללי סינון פעילים
filteredAndSortedData = [...tableData];
} else {
filteredAndSortedData = tableData.filter(item => {
// 1. יצירת מערך של תוצאות (true/false) לכל כלל סינון
const ruleResults = activeRules.map(rule => {
return applyRuleToRow(item, rule);
});
// 2. שילוב התוצאות לפי לוגיקת AND/OR הכללית
if (generalLogic === 'AND') {
// אם AND, כל התוצאות חייבות להיות TRUE
return ruleResults.every(result => result === true);
} else if (generalLogic === 'OR') {
// אם OR, מספיק שאחת התוצאות תהיה TRUE
return ruleResults.some(result => result === true);
}
return false; // ברירת מחדל
});
}
// איפוס המיון ועדכון הטבלה
sortState = { columnIndex: -1, direction: 'asc' }; // איפוס מיון
currentPage = 1;
renderPaginatedTable(filteredAndSortedData, currentColumns);
// עדכון הודעת הסטטוס על הסינון
const totalCount = tableData.length;
const recordCount = filteredAndSortedData.length;
if (activeRules.length > 0 && recordCount === 0) {
statusDiv.innerHTML = `<span style="color:#c62828;">❌ לא נמצאו שורות תואמות. ייתכן שיש סתירה בין חוקי הסינון. מוצגות 0 מתוך ${totalCount} שורות.</span>`;
} else if (activeRules.length > 0) {
statusDiv.innerHTML = `<span style="color:#1a5c92;">🔎 סינון הופעל בהצלחה. מוצגות ${recordCount} שורות מתוך ${totalCount} סה"כ.</span>`;
} else {
// אם אין כללים פעילים (למרות שהיה קליק על הכפתור)
statusDiv.innerHTML = `<span style="color:#2E7D32;">✅ הנתונים נטענו בהצלחה. סה"כ שורות בלוג: ${totalCount}. פרטי הלוג: ${selectedFileName}.</span>`;
}
}
/**
* מחיל כלל סינון אחד על שורת נתונים בודדת.
*/
function applyRuleToRow(item, rule) {
const { column, matchType, value } = rule;
// הפיכת ערך הסינון לרישיות קטנות וקיצוץ רווחים
const filterValue = value.toLowerCase().trim();
if (matchType === 'is_empty') {
if (column === '__all_cols__') {
// מחזיר אמת אם לפחות עמודה אחת ריקה
return currentColumns.some(col => !item[col] || String(item[col]).trim() === '');
}
// מחזיר אמת אם העמודה הספציפית ריקה
return !item[column] || String(item[column]).trim() === '';
}
if (matchType === 'is_not_empty') {
if (column === '__all_cols__') {
// מחזיר אמת אם לפחות עמודה אחת לא ריקה
return currentColumns.some(col => item[col] && String(item[col]).trim() !== '');
}
// מחזיר אמת אם העמודה הספציפית אינה ריקה
return item[column] && String(item[column]).trim() !== '';
}
// אם נבחרה אפשרות "כל העמודות"
if (column === '__all_cols__') {
// במקרה זה, מחזירים אמת אם הכלל מתקיים בלפחות עמודה אחת
return currentColumns.some(col => {
const cellValue = String(item[col] || '').toLowerCase();
return performComparison(cellValue, filterValue, matchType);
});
}
// עבור עמודה ספציפית
const cellValue = String(item[column] || '').toLowerCase();
return performComparison(cellValue, filterValue, matchType);
}
/**
* פונקציית עזר לביצוע ההשוואה בפועל (עבור סינון).
*/
function performComparison(cellValue, filterValue, matchType) {
// טיפול מיוחד במקרה של שני ערכים ריקים
if (cellValue === '' && filterValue === '') {
// אם שניהם ריקים, רק סוגי סינון "שחורים" (לא מכיל / לא שווה) יכשלו.
if (matchType.startsWith('not_')) {
return false;
}
return true; // אם מחפשים 'מכיל' ערך ריק או 'שווה' לערך ריק, זה יתאים
}
// טיפול במקרה של תא ריק מול ערך סינון לא ריק
if (cellValue === '' && filterValue !== '') {
// זה יתאים רק לסינון שחור ("לא מכיל" / "לא שווה")
if (matchType === 'not_contains' || matchType === 'not_exact') {
return true;
}
// סוגי סינון "לבנים" (כמו 'מכיל', 'שווה', 'מתחיל ב', 'נגמר ב') נכשלים
return false;
}
switch (matchType) {
case 'contains':
return cellValue.includes(filterValue);
case 'not_contains':
return !cellValue.includes(filterValue);
case 'exact':
return cellValue === filterValue;
case 'not_exact':
return cellValue !== filterValue;
case 'starts_with':
return cellValue.startsWith(filterValue);
case 'ends_with':
return cellValue.endsWith(filterValue);
default:
return true;
}
}
/**
* פונקציה לניקוי הסינון והצגת כל השורות.
*/
function clearFilter() {
clearAllFilters();
}
// ------------------- פונקציות נראות עמודות 🆕 -------------------
/**
* שומר או מאחזר את מצב הנראות של עמודה ספציפית במצב הגלובלי הזמני.
*/
function toggleColumnVisibility(columnKey) {
const checkbox = document.querySelector(`input[data-column-key="${CSS.escape(columnKey)}"]`);
if (!checkbox) return;
const isVisible = checkbox.checked;
currentColumnVisibilityState[columnKey] = isVisible;
applyColumnVisibility(); // החלת השינוי על הטבלה
}
/**
* מגדיר את פקדי הצ'קבוקסים לנראות העמודות על פי העמודות בלוג הנבחר.
* @param {Array<string>} columns - רשימת העמודות של הלוג הנוכחי.
* @param {string} logFileName - שם קובץ הלוג
*/
function setupColumnVisibilityControls(columns, logFileName) {
const container = document.getElementById('col-checkbox-container');
container.innerHTML = '';
// 1. קביעת עמודות ברירת מחדל ללוג API
const API_DEFAULT_COLUMNS = [
'שלוחה', 'טלפון', 'תאריך לועזי', 'שעה', 'תאריך עברי', 'ApiSend', 'ApiAnswer',
'25Phone', '25Date', '25Time', '25HebrewDate', '25ApiSend', '25ApiAnswer'
];
let defaultVisibleHeaders = [...columns];
const logNameLower = (logFileName || '').toLowerCase();
const useApiDefault = logNameLower.includes('logapi.ymgr') || logNameLower.includes('logapi');
if (useApiDefault) {
// לוגיקת ברירת מחדל מיוחדת ללוגי API
defaultVisibleHeaders = API_DEFAULT_COLUMNS;
}
// 2. 🆕 איפוס ואתחול המצב הגלובלי הזמני לברירת המחדל
currentColumnVisibilityState = {};
columns.forEach(col => {
if (col === '__index_col__') return;
const defaultIsVisible = defaultVisibleHeaders.includes(col);
// שמירת ברירת המחדל במצב הגלובלי הזמני
currentColumnVisibilityState[col] = defaultIsVisible;
const label = document.createElement('label');
// שימוש במצב המאותחל עבור ה-checked state
label.innerHTML = `
<input type="checkbox" data-column-key="${col}" onchange="toggleColumnVisibility('${col}')" ${defaultIsVisible ? 'checked' : ''}>
${col}
`;
container.appendChild(label);
});
// 3. החלת הנראות מיד לאחר יצירת הטבלה
applyColumnVisibility();
}
/**
* פונקציה לאיפוס נראות העמודות לברירת המחדל של הלוג הנוכחי.
*/
function resetColumnVisibilityToDefault() {
if (currentColumns.length === 0) return;
// 1. קריאה חוזרת ל-setupColumnVisibilityControls כדי לחשב ולאתחל את המצב הגלובלי מחדש
// (פעולה זו מאתחלת את currentColumnVisibilityState על בסיס ברירות המחדל).
setupColumnVisibilityControls(currentColumns, selectedFileName);
// 2. רינדור מחדש של הטבלה כדי להחיל את הנראות החדשה
renderPaginatedTable(filteredAndSortedData, currentColumns);
// 3. הצגת הודעת סטטוס
const statusDiv = document.getElementById('status-message');
const originalContent = statusDiv.innerHTML;
statusDiv.innerHTML = '<span style="color:#28a745; font-weight: bold;">✅ נראות העמודות אופסה לברירת המחדל של הלוג הנוכחי.</span>';
// מחיקת הודעת הסטטוס לאחר מספר שניות
setTimeout(() => {
if (statusDiv.innerHTML.includes('נראות העמודות אופסה')) {
statusDiv.innerHTML = originalContent;
}
}, 3000);
}
/**
* מחילה את מצב הנראות הנוכחי (currentColumnVisibilityState) על הטבלה ב-DOM.
*/
function applyColumnVisibility() {
const table = document.getElementById('call-log-table');
if (!table) return;
// מעבר על כל מצבי הנראות
Object.keys(currentColumnVisibilityState).forEach(col => {
const isVisible = currentColumnVisibilityState[col];
const escapedCol = CSS.escape(col);
// חפש הן את כותרות העמודות (TH) והן את תאי הנתונים (TD)
const colElements = document.querySelectorAll(`[data-column-key="${escapedCol}"]`);
colElements.forEach(el => {
if (el.tagName === 'TH' || el.tagName === 'TD') {
el.classList.toggle('col-hidden', !isVisible);
}
});
const checkbox = document.querySelector(`input[data-column-key="${escapedCol}"]`);
if (checkbox) {
// וודא שמצב הצ'קבוקס משקף את המצב בפועל
checkbox.checked = isVisible;
}
});
}
/**
* פונקציה להצגת פרטי שורה מלאים בפורמט קריא.
*/
function showRowDetails(rowElement, rowIndex) {
const detailsPanel = document.getElementById('details-panel');
const detailsContent = document.getElementById('details-content');
document.querySelectorAll('#call-log-table tr').forEach(tr => {
tr.style.outline = 'none';
});
rowElement.style.outline = '3px solid #1a5c92';
const fullData = filteredAndSortedData[rowIndex];
let html = '';
html += `<dt># מספר שורה</dt><dd class="value-text">${rowIndex + 1}</dd>`;
currentColumns.forEach(key => {
const rawValue = fullData[key] !== undefined ? fullData[key] : '';
if (rawValue || rawValue === 0) {
let valueToDisplay = rawValue;
let copyButtonHtml = '';
// עיצוב JSON
if (key === 'ApiSend' || key === 'ApiAnswer') {
try {
const jsonObject = JSON.parse(rawValue);
valueToDisplay = JSON.stringify(jsonObject, null, 2);
} catch (e) {
// לא JSON
}
}
// הוספת כפתור העתקה
if (key === 'ApiSend' || key === 'ApiAnswer' || key === 'ApiTime') {
copyButtonHtml = `<button onclick="copyToClipboard(this.previousElementSibling.textContent, this)">העתק</button>`;
}
valueToDisplay = String(valueToDisplay);
html += `<dt>${key}</dt>`;
html += `<dd>
<span class="value-text ${valueToDisplay.includes('\n') ? 'json-formatted' : ''}">${valueToDisplay}</span>
${copyButtonHtml}
</dd>`;
}
});
detailsContent.innerHTML = html;
detailsPanel.style.display = 'block';
// detailsPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/**
* 🆕 פונקציה לסגירת פאנל פרטי השורה הנבחרת.
*/
function closeDetailsPanel() {
const detailsPanel = document.getElementById('details-panel');
detailsPanel.style.display = 'none';
// הסרת ההדגשה מהשורה הנבחרת
document.querySelectorAll('#call-log-table tr').forEach(tr => {
tr.style.outline = 'none';
});
}
/**
* 🆕 פונקציה להעתקת טקסט ללוח המערכת עם פידבק UI.
*/
function copyToClipboard(text, buttonElement) {
navigator.clipboard.writeText(text).then(() => {
const originalText = buttonElement.textContent;
const originalBg = buttonElement.style.backgroundColor;
buttonElement.textContent = 'הועתק! ✅';
buttonElement.classList.add('copied-success');
setTimeout(() => {
buttonElement.textContent = originalText;
buttonElement.classList.remove('copied-success');
buttonElement.style.backgroundColor = originalBg; // החזרת הצבע המקורי
}, 1500);
}).catch(err => {
console.error('Failed to copy text: ', err);
alert('שגיאה בהעתקה: ' + err);
});
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
</body>
</html>
מתקדם ומשוכלל, עם הרבה אופציות מועילות
נסו ותהנו..
עריכה: שיניתי לגירסה מתוקנת יותר, בעת שגיאת טוקן מוחזרת הודעה מתאימה
עריכה: תיקון נוסף, ניתן להזין נתיב וחיפוש קבצי הלוג יתבצע על כל תתי הנתיבים שתחתיו
