@יענקי-פולק תעשה שישמיע את הודעת הקמפיין למי שלא רשום ברשית"פ עיין פה להגדרות
-
RE: פילטר לפי רשימת תפוצה
-
RE: עזרה דחוףףףףףףףףף!!!!
@אליהו-קטורזה אתה צריך להגדיר את השלוחה כ API למחוק את ההגדרות של שלוחת הקלטה
-
RE: הבטיחו לנו איזה שדרוג! מישהו קיבל אותו?
@איל-משולש מה ייתן לך להפעיל ב API אם הפילטר חיוג לפי מס לא עובד
זה יעזור רק למי שיש רק מספר אחד -
RE: תגובה ל"הכירו את מודול ההתראות: חדש וייחודי מבית ימות המשיח 🤩"
@CUBASE במייל כתוב מפורש שזה לא ידרוש ממנו שום הגדרה
-
RE: תגובות: מדריך | כך תגדירו וירטואל+ במערכת רגילה!
@CUBASE אם אני מגדיר מספר לשלוחה פנימית שהשלוחה הפנימית תהיה מודל תור אפשרי שזה לא ישדר מענה?
אם אני שם את זה במערכת שיש בה עוד דברים מה יקרה אם ההגדרה של לא לשדר מענה בשאר השלוחות? -
RE: שינוי מס' פל' דרך האלפון | מודול תור | עזרה
@פלפל-שחור תעיין בשרשור הזה
יש שם כמה אופציות תלוי מה אתה צריך
בכל אופן אם המטרה להוציא שיחה אם זיהוי מערכת בחינם שם זה ההסבר -
RE: הקלטת השיחה בלבד במודול תור
@CUBASE תירגמתי אותו לסקירפט גוגל עובד מעולה
משתמש באחסון של גוגל דרייב
יש בעיה שגוגל יכול לרוץ עד 6 דקות אני הגדרתי לו שכל פעם הוא סורק את כל הלוג לוקח את נתיב ההקלטה וחותך את זמן ההמתנה למענה ושיחות שהוא טיפל בהם הוא שומר בקובץ בדרייב רק שמות וככה הוא יודע לא לערוך אותם שוב אבל עדיין זה יכול לקחת זמן
אני לא יודע מה מגיע במייל אם ההקלטה אבל צריך להגדיר לו שיקח משם את נתיב ההקלטה אם הוא מופיע ואז יחפש בלוג רק את זמן ההמתנה למענה וככה כל מייל נכנס הוא יעבוד על שיחה אחת
זה הקוד// --- הגדרות מערכת --- const TOKEN = "כאן להזין טוקן"; const LOG_PATH = "ivr2:Log/LogQueueOK.2026-03-30.ymgr"; const API_URL = "https://www.call2all.co.il/ym/api/"; // --- הגדרות דרייב --- const DRIVE_FOLDER_NAME = "Yemot_Audio_Processing"; // שם התיקייה שתיווצר בדרייב const JSON_FILENAME = "processed_calls.json"; function processYmCalls() { Logger.log("--- תחילת ריצת מערכת ---"); // השגת או יצירת תיקיית העבודה בדרייב let workFolder = getOrCreateDriveFolder(DRIVE_FOLDER_NAME); // טעינת רשימת השיחות שכבר טופלו let processedPaths = getProcessedCalls(workFolder); Logger.log("שולף את קובץ הלוג: " + LOG_PATH); // --- שלב 1: קריאת הלוג מימות המשיח --- let logRes = getYmApi("RenderYMGRFile", { token: TOKEN, wath: LOG_PATH, convertType: 'json', notLoadLang: 1 }); let data = JSON.parse(logRes.getContentText()); if (!data || !data.data) { Logger.log("שגיאה: לא נמצאו נתונים בלוג או שהטוקן/נתיב שגויים."); return; } Logger.log("קובץ הלוג נקרא בהצלחה, מתחיל סריקת רשומות..."); // --- שלב 2: עיבוד הרשומות --- for (let i = 0; i < data.data.length; i++) { let row = data.data[i]; // בודק אם זו שיחה שנענתה ויש נתיב להקלטה if (row.QueueStatus === 'ANSWER' && row.QueueRecordPath) { let remotePath = row.QueueRecordPath; let waitSeconds = parseInt(row.QueueWaitingSeconds || 0); // בדיקה האם הקובץ כבר טופל if (processedPaths.indexOf(remotePath) !== -1) { continue; // מדלג } if (waitSeconds > 0) { Logger.log("זוהתה שיחה חדשה. נתיב: " + remotePath + " | זמן חיתוך: " + waitSeconds + " שניות."); // הורדת הקובץ Logger.log("מוריד את הקובץ..."); let fileResponse = getYmApi("DownloadFile", { token: TOKEN, path: remotePath }); let originalBlob = fileResponse.getBlob(); if (originalBlob.getBytes().length > 44) { originalBlob.setName("orig_" + waitSeconds + ".wav"); // שמירת הקובץ המקורי בדרייב לצורך ניתוח let origDriveFile = workFolder.createFile(originalBlob); Logger.log("הקובץ המקורי נשמר בדרייב לניתוח."); // ביצוע החיתוך Logger.log("מתחיל חיתוך..."); let trimmedBlob = smartTrimWavGas(origDriveFile.getBlob(), waitSeconds); if (trimmedBlob) { // שמירת הקובץ החתוך בדרייב let trimDriveFile = workFolder.createFile(trimmedBlob); // יצירת הנתיב החדש let pathParts = remotePath.split('/'); let fileName = pathParts.pop(); let newRemotePath = pathParts.join('/') + '/חתוכים/' + fileName; Logger.log("מעלה קובץ חדש: " + newRemotePath); // העלאה חזרה לימות המשיח let uploadRes = UrlFetchApp.fetch(API_URL + "UploadFile", { method: 'post', payload: { token: TOKEN, path: newRemotePath, file: trimDriveFile.getBlob() }, muteHttpExceptions: true }); let uploadData = JSON.parse(uploadRes.getContentText()); if (uploadData.responseStatus === 'OK') { Logger.log("הקובץ הועלה בהצלחה."); // עדכון ה-JSON processedPaths.push(remotePath); saveProcessedCalls(workFolder, processedPaths); } else { Logger.log("שגיאה בהעלאה: " + uploadRes.getContentText()); } // ניקוי הקובץ החתוך מהדרייב (העברה לאשפה) trimDriveFile.setTrashed(true); } else { Logger.log("שגיאה בחיתוך הקובץ."); } // ניקוי הקובץ המקורי מהדרייב origDriveFile.setTrashed(true); } else { Logger.log("שגיאה: הקובץ ריק או קצר מדי."); } } else { // זמן המתנה 0 - נסמן כטופל ונדלג Logger.log("שיחה בנתיב " + remotePath + " ללא זמן המתנה. מסמן כטופל ומדלג."); processedPaths.push(remotePath); saveProcessedCalls(workFolder, processedPaths); } Logger.log("--------------------------------------------------"); } } Logger.log("--- הריצה הסתיימה בהצלחה ---"); } // ================= פונקציות עזר ================= /** * פונקציה לתקשורת API בסיסית */ function getYmApi(endpoint, params) { let url = API_URL + endpoint; let options = { method: 'post', payload: params, muteHttpExceptions: true }; return UrlFetchApp.fetch(url, options); } /** * פונקציה להשגת תיקיית העבודה בדרייב או יצירתה במידה ואינה קיימת */ function getOrCreateDriveFolder(folderName) { let folders = DriveApp.getFoldersByName(folderName); if (folders.hasNext()) { return folders.next(); } else { return DriveApp.createFolder(folderName); } } /** * קריאת רשימת השיחות שטופלו מתוך ה-JSON בדרייב */ function getProcessedCalls(folder) { let files = folder.getFilesByName(JSON_FILENAME); if (files.hasNext()) { let file = files.next(); let content = file.getBlob().getDataAsString(); try { return JSON.parse(content || "[]"); } catch (e) { return []; } } return []; } /** * שמירת רשימת השיחות המעודכנת לקובץ ה-JSON בדרייב */ function saveProcessedCalls(folder, data) { let files = folder.getFilesByName(JSON_FILENAME); let content = JSON.stringify(data); if (files.hasNext()) { files.next().setContent(content); } else { folder.createFile(JSON_FILENAME, content, MimeType.PLAIN_TEXT); } } /** * פונקציה לחיתוך קובץ WAV בגוגל סקריפט ברמת ה-Bytes (ללא תוספים) */ function smartTrimWavGas(sourceBlob, secondsToSkip) { let gasBytes = sourceBlob.getBytes(); // המרת הבתים ממערך חתום (של GAS) למערך לא-חתום (של JS) לצורך קריאת משתנים מדויקת let buffer = new ArrayBuffer(gasBytes.length); let uint8View = new Uint8Array(buffer); for (let i = 0; i < gasBytes.length; i++) { uint8View[i] = gasBytes[i] < 0 ? gasBytes[i] + 256 : gasBytes[i]; } let dataView = new DataView(buffer); // שליפת ByteRate ו-BlockAlign מה-Header let byteRate = dataView.getUint32(28, true); // little-endian let blockAlign = dataView.getUint16(32, true); // little-endian let bytesToSkip = Math.floor((secondsToSkip * byteRate) / blockAlign) * blockAlign; let dataStart = 44 + bytesToSkip; if (dataStart >= gasBytes.length) { return null; // זמן החיתוך ארוך מהקובץ } let newDataLength = gasBytes.length - dataStart; // בניית קובץ חדש בזיכרון let newBuffer = new ArrayBuffer(44 + newDataLength); let newUint8View = new Uint8Array(newBuffer); let newDataView = new DataView(newBuffer); // העתקת ה-Header המקורי for (let i = 0; i < 44; i++) { newUint8View[i] = uint8View[i]; } // העתקת נתוני הקול שנשארו (החל מהנקודה שחתכנו) for (let i = 0; i < newDataLength; i++) { newUint8View[44 + i] = uint8View[dataStart + i]; } // עדכון גדלים ב-Header let newSubChunk2Size = newDataLength; let newChunkSize = newSubChunk2Size + 36; newDataView.setUint32(4, newChunkSize, true); newDataView.setUint32(40, newSubChunk2Size, true); // המרה חזרה למערך בתים חתום ש-GAS יודע לעבוד איתו let finalGasBytes = []; for (let i = 0; i < newUint8View.length; i++) { let b = newUint8View[i]; finalGasBytes.push(b > 127 ? b - 256 : b); } return Utilities.newBlob(finalGasBytes, 'audio/wav', 'trimmed_' + Date.now() + '.wav'); } -
RE: למה אין נתונים לקווים? 🤔
@קו-המוסיקה עיין פה
בקצרה בכל שלוחה השמעת קבציםlog_playback_play_stop=yes log_playback_play_stop_month_save_root_log=yes -
RE: למה אין נתונים לקווים? 🤔
@קו-המוסיקה זה כותב לך שלא נמצא לוג
מוגדר לך במערכת שמירת לוג האזנה חודשי? -
RE: הקלטת השיחה בלבד במודול תור
@CUBASE זה קוד בPHP שחותך הוא לא משתמש בשרתים חיצוניים לחיתוך הוא מתבסס על זה שזה קובץ WAV וחותך ביטים מתוך הזיכרון בדקתי אצלי והוא עובד מעולה ניתן גם להמיר לגוגל סקריפט אני מאמין
הגדרתי לו להעלות לתיקייה חדשה
הוא שומר לעצמו את הקבצים שכבר טופלו כדי שלא יחתוך פעמיים את אותו קובץ
אתה יכול גם לאחסן אותו בשרת PHP ובגוגל לתזמן שבמייל נכנס יעיר אותו<?php // --- הגדרות מערכת --- $token = ""; // הטוקן שלך $logPath = "ivr2:Log/LogQueueOK.2026-03-31.ymgr"; // נתיב הלוג של התור $apiUrl = "https://www.call2all.co.il/ym/api/"; // --- הגדרות סביבה מקומית --- $tempDir = __DIR__ . '/temp_audio/'; if (!is_dir($tempDir)) { mkdir($tempDir, 0777, true); } $processedFile = __DIR__ . '/processed_calls.json'; // קובץ לשמירת הקלטות שכבר טופלו $logFile = __DIR__ . '/script_log.txt'; // קובץ לוג מקומי מפורט // --- פונקציית כתיבה ללוג --- function addLog($msg) { global $logFile; $timestamp = date('d/m/Y H:i:s'); $logMessage = "[$timestamp] $msg" . PHP_EOL; file_put_contents($logFile, $logMessage, FILE_APPEND); echo $logMessage . "<br>"; // הדפסה למסך במקביל } // טעינת רשימת השיחות שכבר טופלו בעבר $processedPaths = []; if (file_exists($processedFile)) { $processedPaths = json_decode(file_get_contents($processedFile), true) ?: []; } addLog("--- תחילת ריצת מערכת ---"); addLog("שולף את קובץ הלוג: $logPath"); // --- שלב 1: קריאת הלוג מימות המשיח --- $logData = getYmApi($apiUrl . "RenderYMGRFile", [ 'token' => $token, 'wath' => $logPath, 'convertType' => 'json', 'notLoadLang' => 1 ]); $data = json_decode($logData, true); if (!$data || !isset($data['data'])) { addLog("שגיאה: לא נמצאו נתונים בלוג או שהטוקן שגוי/הנתיב לא קיים."); die("שגיאה קריטית, קרא את קובץ הלוג לפרטים."); } addLog("קובץ הלוג נקרא בהצלחה, מתחיל סריקת רשומות..."); // --- שלב 2: עיבוד הרשומות --- foreach ($data['data'] as $row) { // בודק אם זו שיחה שנענתה ויש נתיב להקלטה if (isset($row['QueueStatus']) && $row['QueueStatus'] === 'ANSWER' && !empty($row['QueueRecordPath'])) { $remotePath = $row['QueueRecordPath']; $waitSeconds = (int)$row['QueueWaitingSeconds']; // בדיקה האם הקובץ כבר טופל בריצות קודמות if (in_array($remotePath, $processedPaths)) { continue; // מדלג בשקט לפריט הבא } // אם היה זמן המתנה, צריך לחתוך if ($waitSeconds > 0) { addLog("זוהתה שיחה חדשה. נתיב: $remotePath | זמן חיתוך נדרש: $waitSeconds שניות."); $localOrig = $tempDir . 'orig_' . md5($remotePath) . '.wav'; $localTrim = $tempDir . 'trim_' . md5($remotePath) . '.wav'; // הורדת הקובץ מימות המשיח addLog("מוריד את הקובץ..."); $fileContent = getYmApi($apiUrl . "DownloadFile", [ 'token' => $token, 'path' => $remotePath ]); if ($fileContent && strlen($fileContent) > 44) { file_put_contents($localOrig, $fileContent); addLog("הקובץ הורד ונשמר זמנית בהצלחה. גודל הקובץ: " . strlen($fileContent) . " בתים."); // ביצוע החיתוך הדינמי addLog("מתחיל תהליך חיתוך של $waitSeconds שניות מההתחלה..."); if (smartTrimWav($localOrig, $localTrim, $waitSeconds)) { addLog("הקובץ נחתך בהצלחה."); // --- יצירת נתיב חדש לתיקיית "חתוכים" --- // מחלצים את הנתיב עד לתיקייה ואת שם הקובץ ומכניסים את "חתוכים" באמצע $pathParts = pathinfo($remotePath); $newRemotePath = $pathParts['dirname'] . '/חתוכים/' . $pathParts['basename']; addLog("מעלה קובץ חדש לנתיב הבדיקות: $newRemotePath"); $uploadStatus = uploadToYm($apiUrl . "UploadFile", $token, $newRemotePath, $localTrim); $uploadRes = json_decode($uploadStatus, true); if (isset($uploadRes['responseStatus']) && $uploadRes['responseStatus'] === 'OK') { addLog("הקובץ הועלה בהצלחה."); // סימון הקובץ כ'טופל' כדי שלא ייבדק שוב $processedPaths[] = $remotePath; file_put_contents($processedFile, json_encode($processedPaths)); } else { addLog("שגיאה בהעלאת הקובץ: $uploadStatus"); } } else { addLog("שגיאה בחיתוך הקובץ (יתכן שזמן החיתוך ארוך מאורך הקובץ הכולל)."); } // ניקוי קבצים זמניים מהשרת המקומי @unlink($localOrig); @unlink($localTrim); } else { addLog("שגיאה: הקובץ ריק או קצר מדי (פחות מ-44 בתים). לא ניתן לחתוך."); } } else { // זמן המתנה הוא 0, אין צורך לחתוך, נסמן כטופל כדי לדלג עליו בהמשך addLog("שיחה בנתיב $remotePath ללא זמן המתנה (0 שניות). מסמן כטופל ומדלג."); $processedPaths[] = $remotePath; file_put_contents($processedFile, json_encode($processedPaths)); } addLog("--------------------------------------------------"); } } addLog("--- הריצה הסתיימה בהצלחה ---"); // ================= פונקציות עזר ================= /** * פונקציה לחיתוך קובץ WAV מבוסס Header ו-ByteRate ללא תוספים */ function smartTrimWav($sourcePath, $destPath, $secondsToSkip) { $fp = fopen($sourcePath, 'rb'); if (!$fp) return false; // קורא את כותרת ה-WAV (ה-44 בתים הראשונים) $header = fread($fp, 44); // שליפת ה-Byte Rate מתוך ה-Header של הקובץ (כדי לתמוך בכל קצב דגימה שהם יחזירו) $info = unpack('VbyteRate', substr($header, 28, 4)); $bytesPerSecond = $info['byteRate']; // הוספת Block Align שמיישר את הבתים לפי ערוצים (16bit = 2 bytes) $blockAlignInfo = unpack('vblockAlign', substr($header, 32, 2)); $blockAlign = $blockAlignInfo['blockAlign']; // חישוב כמות הבתים שיש לדלג, מחייב חלוקה מדויקת ב- BlockAlign כדי לא להשחית את גל הקול $bytesToSkip = floor(($secondsToSkip * $bytesPerSecond) / $blockAlign) * $blockAlign; $fileSize = filesize($sourcePath); $dataStart = 44 + $bytesToSkip; // מוודא שאנחנו לא מנסים לחתוך יותר אורך ממה שקיים בקובץ if ($dataStart >= $fileSize) { fclose($fp); return false; } fseek($fp, $dataStart); $data = stream_get_contents($fp); fclose($fp); // עדכון גדלים בכותרת הקובץ (תיקון ה-Header) $newSubChunk2Size = strlen($data); $newChunkSize = $newSubChunk2Size + 36; // הזרקת הגדלים החדשים (בפורמט 32-bit little-endian) $header = substr_replace($header, pack('V', $newChunkSize), 4, 4); $header = substr_replace($header, pack('V', $newSubChunk2Size), 40, 4); return file_put_contents($destPath, $header . $data); } /** * פונקציה לתקשורת API (מתודת POST) */ function getYmApi($url, $params) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // מונע שגיאות תעודת אבטחה בשרתים מסוימים $res = curl_exec($ch); curl_close($ch); return $res; } /** * פונקציה להעלאת קובץ בפורמט multipart/form-data */ function uploadToYm($url, $token, $path, $localFile) { $ch = curl_init(); $cFile = new CURLFile($localFile, 'audio/wav', 'file.wav'); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'token' => $token, 'path' => $path, 'file' => $cFile ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $res = curl_exec($ch); curl_close($ch); return $res; } ?> -
RE: הקלטת השיחה בלבד במודול תור
@CUBASE סביר להניח שזה באג
אתה צריך לכתוב קוד שיקרא מהלוג כמה שניות המתין למענה ואז לחתוך מתחילת הקובץ את מספר השניות פחות שניה אחת -
RE: למה אין נתונים לקווים? 🤔
@קו-המוסיקה @אa @565906
מצורף קוד 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, sans-serif; background: #f0f2f5; margin: 0; padding: 20px; color: #333; } .container { max-width: 1200px; margin: auto; background: #fff; padding: 25px; border-radius: 15px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); } h1 { margin-top: 0; color: #1a4388; text-align: center; border-bottom: 2px solid #eee; padding-bottom: 15px; } .setup-panel { background: #f8fafc; padding: 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 20px; display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end; } .input-group { display: flex; flex-direction: column; gap: 5px; } label { font-weight: bold; font-size: 14px; } input, select { padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 14px; } button { border: none; background: #1a4388; color: #fff; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; transition: background 0.2s; } button:hover { background: #133266; } button.secondary { background: #10b981; } button.secondary:hover { background: #059669; } .summary-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 20px; } .card { background: #fff; padding: 15px; border-radius: 10px; border: 1px solid #e2e8f0; box-shadow: 0 2px 4px rgba(0,0,0,0.02); text-align: center; } .card .val { font-size: 22px; font-weight: bold; color: #1a4388; display: block; margin-bottom: 5px; } .card .lbl { font-size: 13px; color: #64748b; font-weight: bold; } .tabs { display: flex; gap: 10px; margin-bottom: 15px; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; display: none; } .tab-btn { background: #e2e8f0; color: #333; padding: 10px 15px; border-radius: 8px; cursor: pointer; font-weight: bold; border: none; } .tab-btn.active { background: #1a4388; color: #fff; } .tab-content { display: none; } .tab-content.active { display: block; } .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } @media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } } .table-container { overflow-x: auto; max-height: 400px; border: 1px solid #e2e8f0; border-radius: 10px; margin-bottom: 20px; background: #fff;} table { width: 100%; border-collapse: collapse; text-align: right; } th { position: sticky; top: 0; background: #1a4388; color: white; padding: 10px; font-size: 14px; } td { padding: 10px; border-bottom: 1px solid #f1f5f9; font-size: 14px; } tr:hover { background: #f8fafc; } .error { color: #991b1b; background: #fef2f2; padding: 12px; border-radius: 8px; border: 1px solid #fee2e2; margin: 10px 0; } .success { color: #065f46; background: #ecfdf5; padding: 12px; border-radius: 8px; border: 1px solid #d1fae5; margin: 10px 0; } .warning { color: #854d0e; background: #fefce8; padding: 12px; border-radius: 8px; border: 1px solid #fef08a; margin: 10px 0; } h3 { color: #1a4388; margin-bottom: 10px; border-bottom: 1px solid #e2e8f0; padding-bottom: 5px; } </style> </head> <body> <div class="container"> <h1>ניהול נתוני שיחות מתקדם - ימות המשיח</h1> <div class="setup-panel"> <div class="input-group"> <label>טוקן API:</label> <input type="password" id="token" placeholder="הכנס טוקן..." /> </div> <div class="input-group"> <label>שנה:</label> <input type="number" id="year" value="2026" style="width: 80px;" /> </div> <div class="input-group"> <label>חודש:</label> <select id="month"> <option value="01">01</option><option value="02">02</option> <option value="03">03</option><option value="04" selected>04</option> <option value="05">05</option><option value="06">06</option> <option value="07">07</option><option value="08">08</option> <option value="09">09</option><option value="10">10</option> <option value="11">11</option><option value="12">12</option> </select> </div> <div class="input-group"> <label>רף דקות להגדרת שיחה (מעל/מתחת):</label> <input type="number" id="minLimit" value="15" style="width: 100px;" /> </div> <button onclick="fetchData()">משוך נתונים</button> </div> <div id="statusMessage"></div> <div id="summaryCards" class="summary-cards" style="display:none;"> <div class="card"><span class="val" id="totalCalls">0</span><span class="lbl">סה"כ כניסות למערכת</span></div> <div class="card"><span class="val" id="totalListeners">0</span><span class="lbl">מאזינים יוניקיים</span></div> <div class="card"><span class="val" id="avgListenerTime">0:00</span><span class="lbl">ממוצע למאזין</span></div> <div class="card"><span class="val" id="callsOverLimit">0</span><span class="lbl">שיחות מעל הרף</span></div> <div class="card"><span class="val" id="callsUnderLimit">0</span><span class="lbl">שיחות מתחת לרף</span></div> </div> <div class="tabs" id="tabsMenu"> <button class="tab-btn active" onclick="switchTab('tab-extensions')">נתוני שלוחות</button> <button class="tab-btn" onclick="switchTab('tab-files')">נתוני קבצים והאזנות</button> <button class="tab-btn" onclick="switchTab('tab-daily')">סטטיסטיקה יומית ושעות</button> </div> <div id="tab-extensions" class="tab-content active"> <div class="grid-2"> <div> <h3>השלוחות המובילות בזמן האזנה (טופ 10)</h3> <div class="table-container"> <table> <thead><tr><th>מקום</th><th>שלוחה</th><th>שם</th><th>זמן כולל</th></tr></thead> <tbody id="topTimeExtensions"></tbody> </table> </div> </div> <div> <h3>השלוחות עם הכי הרבה כניסות</h3> <div class="table-container"> <table> <thead><tr><th>מקום</th><th>שלוחה</th><th>שם</th><th>כמות כניסות</th></tr></thead> <tbody id="topCallsExtensions"></tbody> </table> </div> </div> </div> </div> <div id="tab-files" class="tab-content"> <div class="grid-2"> <div> <h3>הקבצים המושמעים ביותר</h3> <div class="table-container"> <table> <thead><tr><th>שלוחה</th><th>קובץ</th><th>כמות השמעות</th><th>זמן השמעה מצטבר</th></tr></thead> <tbody id="topFiles"></tbody> </table> </div> </div> <div> <h3>קבצים עם הכי הרבה עזיבות (האזנה חלקית)</h3> <div class="table-container"> <table> <thead><tr><th>שלוחה</th><th>קובץ</th><th>מספר עזיבות באמצע</th></tr></thead> <tbody id="dropoffFiles"></tbody> </table> </div> </div> </div> </div> <div id="tab-daily" class="tab-content"> <div class="grid-2"> <div> <h3>מאזינים לפי ימים</h3> <div class="table-container"> <table> <thead><tr><th>תאריך</th><th>סה"כ כניסות</th><th>מאזינים יוניקיים</th></tr></thead> <tbody id="dailyStats"></tbody> </table> </div> </div> <div> <h3>השעות הפעילות ביותר (לפי כניסות)</h3> <div class="table-container"> <table> <thead><tr><th>שעה</th><th>כמות כניסות</th></tr></thead> <tbody id="hourlyStats"></tbody> </table> </div> </div> </div> </div> </div> <script> function showMsg(text, type) { const el = document.getElementById("statusMessage"); el.innerHTML = `<div class="${type}">${text}</div>`; } function switchTab(tabId) { document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active')); document.getElementById(tabId).classList.add('active'); event.currentTarget.classList.add('active'); } function formatHMS(s) { if (isNaN(s) || s < 0) return "0:00"; const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sec = Math.floor(s % 60); return h > 0 ? `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}` : `${m}:${String(sec).padStart(2,'0')}`; } async function fetchData() { const token = document.getElementById("token").value; const year = document.getElementById("year").value; const month = document.getElementById("month").value; if (!token) { showMsg("אנא הזן טוקן תקני", "error"); return; } const pathEnterExit = `ivr2:Log/LogFolderEnterExit-${year}-${month}.ymgr`; const pathPlayback = `ivr2:Log/LogPlaybackPlayStop/LogPlaybackPlayStop.${year}-${month}.ymgr`; const urlEnterExit = `https://private.call2all.co.il/ym/api/RenderYMGRFile?token=${token}&wath=${pathEnterExit}&convertType=json¬LoadLang=1`; const urlPlayback = `https://private.call2all.co.il/ym/api/RenderYMGRFile?token=${token}&wath=${pathPlayback}&convertType=json¬LoadLang=1`; showMsg("מתחבר לשרת ימות המשיח, מושך נתונים...", "success"); try { const [resEnterExit, resPlayback] = await Promise.all([ fetch(urlEnterExit).then(r => r.json()).catch(() => ({ responseStatus: "ERROR" })), fetch(urlPlayback).then(r => r.json()).catch(() => ({ responseStatus: "ERROR" })) ]); if (resEnterExit.responseStatus !== "OK" && resPlayback.responseStatus !== "OK") { showMsg("שגיאה מהשרת: לא נמצאו קבצי לוג לחודש זה.", "error"); return; } let warning = ""; if (resEnterExit.responseStatus !== "OK") warning += " לא נמצא לוג כניסות (EnterExit)."; if (resPlayback.responseStatus !== "OK") warning += " לא נמצא לוג השמעות קבצים (PlaybackPlayStop)."; if (warning) showMsg("הערה: " + warning, "warning"); else showMsg("הנתונים נמשכו ועובדו בהצלחה!", "success"); processData(resEnterExit.data || [], resPlayback.data || []); } catch (err) { showMsg("שגיאת תקשורת עם השרת.", "error"); } } function processData(folderData, playbackData) { const limitMinutes = parseFloat(document.getElementById("minLimit").value) || 15; const limitSeconds = limitMinutes * 60; // ----- עיבוד נתוני כניסה/יציאה (FolderEnterExit) ----- const extensions = {}; const daily = {}; const hourly = {}; const phones = {}; let gCalls = 0; let gSeconds = 0; let callsOverLimit = 0; let callsUnderLimit = 0; folderData.forEach(row => { const ext = row["Folder"]; const seconds = parseFloat(row["TimeTotal"]) || 0; const title = row["PathTitle"] || row["ValName"] || ""; const phone = row["Phone"]; const date = row["EnterDate"]; const time = row["EnterTime"]; if (!ext) return; // שלוחות if (!extensions[ext]) extensions[ext] = { ext, title, calls: 0, seconds: 0 }; extensions[ext].calls++; extensions[ext].seconds += seconds; // מאזינים (Phone) if (phone) { if (!phones[phone]) phones[phone] = { count: 0, totalSeconds: 0 }; phones[phone].count++; phones[phone].totalSeconds += seconds; } // חתך זמן (רף) if (seconds >= limitSeconds) callsOverLimit++; else callsUnderLimit++; // נתונים יומיים if (date) { if (!daily[date]) daily[date] = { calls: 0, uniquePhones: new Set() }; daily[date].calls++; if (phone) daily[date].uniquePhones.add(phone); } // שעות פעילות if (time) { const hour = time.split(":")[0]; if (!hourly[hour]) hourly[hour] = 0; hourly[hour]++; } gCalls++; gSeconds += seconds; }); // ----- עיבוד נתוני קבצים (PlaybackPlayStop) ----- const files = {}; playbackData.forEach(row => { const ext = row["Folder"] || ""; const fileName = row["FileName"] || row["ValName"] || ""; const playSeconds = parseFloat(row["TimeTotal"] || row["PlayTotalTime"]) || 0; const fileKey = ext + "/" + fileName; if (!fileName) return; if (!files[fileKey]) files[fileKey] = { ext, fileName, plays: 0, totalSeconds: 0, dropoffs: 0 }; files[fileKey].plays++; files[fileKey].totalSeconds += playSeconds; // חישוב עזיבות אמצע: נניח שעזיבה היא אם הושמע פחות מ-80% מהקובץ או אם סטאטוס/סיבה היא לא רגילה // מכיוון שהמפתחות משתנים, הדרך הפשוטה לזהות "נטישה" מהירה היא שיחה שנותקה פחות מ-15 שניות לסיום (אם יש אורך) או חיתוך. // נשתמש בהגיון בסיסי: אם המאזין שמע פחות מ-10 שניות או פחות מהאורך (במידה וקיים). const fileLength = parseFloat(row["FileLength"]); if (fileLength && playSeconds < fileLength - 5) { files[fileKey].dropoffs++; } else if (!fileLength && playSeconds < 30) { // גיבוי למקרה שאין אורך קובץ מדויק files[fileKey].dropoffs++; } }); renderUI(extensions, daily, hourly, phones, files, gCalls, callsOverLimit, callsUnderLimit); } function renderUI(extensions, daily, hourly, phones, files, gCalls, callsOverLimit, callsUnderLimit) { document.getElementById("summaryCards").style.display = "grid"; document.getElementById("tabsMenu").style.display = "flex"; const uniqueListenersCount = Object.keys(phones).length; let avgTime = 0; if (uniqueListenersCount > 0) { const totalSystemTime = Object.values(phones).reduce((sum, p) => sum + p.totalSeconds, 0); avgTime = totalSystemTime / uniqueListenersCount; } // כרטיסיות למעלה document.getElementById("totalCalls").innerText = gCalls; document.getElementById("totalListeners").innerText = uniqueListenersCount; document.getElementById("avgListenerTime").innerText = formatHMS(avgTime); document.getElementById("callsOverLimit").innerText = callsOverLimit; document.getElementById("callsUnderLimit").innerText = callsUnderLimit; const extArr = Object.values(extensions); const filesArr = Object.values(files); // 1. שלוחות טופ זמן const topTimeExt = [...extArr].sort((a, b) => b.seconds - a.seconds).slice(0, 10); document.getElementById("topTimeExtensions").innerHTML = topTimeExt.map((e, i) => `<tr><td>${i+1}</td><td><strong>${e.ext}</strong></td><td>${e.title}</td><td>${formatHMS(e.seconds)}</td></tr>` ).join(""); // 2. שלוחות טופ כניסות const topCallsExt = [...extArr].sort((a, b) => b.calls - a.calls); document.getElementById("topCallsExtensions").innerHTML = topCallsExt.map((e, i) => `<tr><td>${i+1}</td><td><strong>${e.ext}</strong></td><td>${e.title}</td><td>${e.calls}</td></tr>` ).join(""); // 3. קבצים מושמעים ביותר const topFiles = [...filesArr].sort((a, b) => b.plays - a.plays).slice(0, 30); // מציג עד 30 document.getElementById("topFiles").innerHTML = topFiles.map(f => `<tr><td>${f.ext}</td><td>${f.fileName}</td><td>${f.plays}</td><td>${formatHMS(f.totalSeconds)}</td></tr>` ).join(""); // 4. עזיבות באמצע const dropoffFiles = [...filesArr].sort((a, b) => b.dropoffs - a.dropoffs).filter(f => f.dropoffs > 0).slice(0, 20); document.getElementById("dropoffFiles").innerHTML = dropoffFiles.map(f => `<tr><td>${f.ext}</td><td>${f.fileName}</td><td style="color:red; font-weight:bold;">${f.dropoffs}</td></tr>` ).join(""); // 5. יומי const dailyHtml = Object.keys(daily).sort().map(date => `<tr><td>${date}</td><td>${daily[date].calls}</td><td>${daily[date].uniquePhones.size}</td></tr>` ).join(""); document.getElementById("dailyStats").innerHTML = dailyHtml; // 6. שעות const hourlyHtml = Object.keys(hourly).sort((a, b) => hourly[b] - hourly[a]).map(hour => `<tr><td>${hour}:00</td><td>${hourly[hour]}</td></tr>` ).join(""); document.getElementById("hourlyStats").innerHTML = hourlyHtml; } </script> </body> </html> כמובן שכל אחד יכול לשדרג אם בינה -
RE: למה אין נתונים לקווים? 🤔
@קו-המוסיקה
זה הנמסקנות שהגעתי האם יש ההארות ההערות?
דוח סטטיסטיקה למערכות
בכל שלוחה איזה נתונים מקבלים זמני האזנה הכי גבוהים מ1 - 10
אילו שלוחות הכי מואזנות בכל המערכת
לאלו שלוחות נכנסים הכי הרבה בכל המערכת
כמה מאזינים נכנסים כל יום למערכת /לכל שלוחה
זמן האזנה ממוצע לכל מאזין
הקבצים אם הכי הרבה עזיבות באמצע
כמה פעמים כל קובץ הושמע
כמה שיחות נכנסו ושהו מעל 15 דקות וכמה נותקו בפחות מכך(שיהיה ניתן לבחור את הדקות)
מה השעות הכי פעילות להאזנה -
RE: למה אין נתונים לקווים? 🤔
@אa יש פה קוד שהכנתי למישהו הוא עובד על שלוחות לא על קבצים זה הרעיון
@קו-המוסיקה אם תפרט יותר מה צריך אני יכול גם לשדרג את הקוד הזה