📊 מציג לוגים מתקדם ומשוכלל
-
מצורף בספוילר קובץ להצגת הלוגים של המערכת (לוגי 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>מתקדם ומשוכלל, עם הרבה אופציות מועילות
נסו ותהנו..עריכה: שיניתי לגירסה מתוקנת יותר, בעת שגיאת טוקן מוחזרת הודעה מתאימה
עריכה: תיקון נוסף, ניתן להזין נתיב וחיפוש קבצי הלוג יתבצע על כל תתי הנתיבים שתחתיו