מודל הורדת נתונים מקובץ ymgr לגוגל שיטס
-
@מיכאלוש האם קרה משהו לסקריפט למה זה לא עובד?
-
@יעקב95139 דבר ראשון למה להקפיץ
דבר שני שינית את המס' מערכת וסיסמה לטוקן? -
@יעקב95139 כתב במודל הורדת נתונים מקובץ ymgr לגוגל שיטס:
@מיכאלוש האם קרה משהו לסקריפט למה זה לא עובד?
@מיכאלוש
אכן נתקלתי בימים האחרונים באיטיות נוראה של טעינת הקובץ.באם יש לזה פיתרון נשמח לשמוע...
-
לא יודע האם זה המקום או לכתוב פוסט חדש...
נתקלתי בלקוח שרצה לחזור לכל אדם שפנה למערכת, ולא משנה לאיזה שלוחה הוא נכנס.
ביקשתי מגיימני שיכין לי מערכת CRM קטנה..מוזמנים גם אתם להנות!
אגב, המערכת מורידה אוטומטי את הקובץ ymgr של השיחות נכנסות ומעדכנת לפי הנתונים הקיימים.
כמובן, תוכלו לעלות את אותו לAI ולעשות שינוי שיתאים גם לכם.תהנו!
===group -
@יוניבני לא העלת את הקובץ רק קישור
-
@יוניבני
א"א להעלות קבצי HTML.
אז או שתעלה את הקוד של התוכנה או מהדרייב.
זו ניראת תוכנה ממש מושקעת!!!
ממש יפה שאתה משתף!!! -
@אA כתב במודל הורדת נתונים מקובץ ymgr לגוגל שיטס:
@יוניבני
א"א להעלות קבצי HTML.
אז או שתעלה את הקוד של התוכנה או מהדרייב.
זו ניראת תוכנה ממש מושקעת!!!
ממש יפה שאתה משתף!!!אני מצרף את הקוד,
להעתיק אותו לפנקס רשומות, ולשמור את הקובץ עם סיומות 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> <!-- Tailwind CSS לעיצוב נקי ומהיר --> <script src="https://cdn.tailwindcss.com"></script> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f3f4f6; } .returned-row { background-color: #fef2f2 !important; border-right: 4px solid #ef4444; } .table-input { width: 100%; padding: 0.25rem; border: 1px solid #e5e7eb; border-radius: 0.25rem; outline: none; transition: border-color 0.2s; } .table-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); } select.table-input { padding-right: 1.5rem; background-color: white; } /* סגנון לכרטיסיות לחיצות */ .dashboard-card { transition: all 0.2s; cursor: pointer; } .dashboard-card:hover { transform: translateY(-2px); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); } </style> </head> <body class="text-gray-800 h-screen flex flex-col"> <!-- רשימת השלמה אוטומטית לקורסים (נסתרת) --> <datalist id="courseList"></datalist> <!-- Navbar --> <nav class="bg-blue-600 text-white shadow-md p-4 flex justify-between items-center shrink-0"> <div class="flex items-center gap-3"> <i class="fas fa-book-reader text-2xl"></i> <h1 class="text-xl font-bold">מעקב פניות - מכון תכתבו מה שאתם רוצים</h1> </div> <div class="flex gap-3"> <button onclick="openSettingsModal()" class="bg-indigo-600 hover:bg-indigo-500 border border-indigo-400 px-4 py-2 rounded shadow transition flex items-center gap-2"> <i class="fas fa-cog"></i> הגדרות וגיבוי </button> <button onclick="openPasteModal()" class="bg-blue-600 hover:bg-blue-500 border border-blue-400 px-4 py-2 rounded shadow transition flex items-center gap-2"> <i class="fas fa-paste"></i> סנכרון מהדבקה </button> <button onclick="autoFetchData()" class="bg-white text-blue-800 font-semibold hover:bg-gray-100 px-4 py-2 rounded shadow transition flex items-center gap-2"> <i class="fas fa-sync-alt" id="syncIcon"></i> סנכרון אוטומטי </button> <button onclick="exportToExcel()" class="bg-green-600 hover:bg-green-500 border border-green-400 px-4 py-2 rounded shadow transition flex items-center gap-2"> <i class="fas fa-file-excel"></i> ייצוא לאקסל </button> </div> </nav> <!-- Dashboard --> <div class="p-6 shrink-0"> <div class="grid grid-cols-1 md:grid-cols-4 gap-4"> <div id="card-all" class="bg-white p-4 rounded-lg shadow border-b-4 border-blue-500 flex justify-between items-center cursor-pointer hover:bg-blue-50 ring-4 ring-blue-500 ring-opacity-50" onclick="setFilter('all')"> <div> <p class="text-sm text-gray-500">סה"כ מספרי טלפון <span class="text-xs font-normal">(לחץ לכל הנתונים)</span></p> <p class="text-2xl font-bold" id="dashTotal">0</p> </div> <i class="fas fa-users text-3xl text-blue-200"></i> </div> <div id="card-waiting" class="bg-white p-4 rounded-lg shadow border-b-4 border-yellow-500 flex justify-between items-center cursor-pointer hover:bg-yellow-50" onclick="setFilter('waiting')"> <div> <p class="text-sm text-gray-500">לא נוצר קשר <span class="text-xs font-normal">(לחץ לסינון)</span></p> <p class="text-2xl font-bold text-yellow-600" id="dashWaiting">0</p> </div> <i class="fas fa-clock text-3xl text-yellow-200"></i> </div> <div id="card-returned" class="bg-white p-4 rounded-lg shadow border-b-4 border-red-500 flex justify-between items-center cursor-pointer hover:bg-red-50" onclick="setFilter('returned')"> <div> <p class="text-sm text-gray-500">חזרו שוב! <span class="text-xs font-normal">(לחץ לסינון)</span></p> <p class="text-2xl font-bold text-red-600" id="dashReturned">0</p> </div> <i class="fas fa-phone-volume text-3xl text-red-200"></i> </div> <div id="card-done" class="bg-white p-4 rounded-lg shadow border-b-4 border-green-500 flex justify-between items-center cursor-pointer hover:bg-green-50" onclick="setFilter('done')"> <div> <p class="text-sm text-gray-500">טופלו (ירד/ממשיך) <span class="text-xs font-normal">(לחץ לסינון)</span></p> <p class="text-2xl font-bold text-green-600" id="dashDone">0</p> </div> <i class="fas fa-check-circle text-3xl text-green-200"></i> </div> </div> </div> <!-- Controls --> <div class="px-6 pb-2 flex justify-between items-center shrink-0"> <div class="relative w-1/3"> <i class="fas fa-search absolute right-3 top-3 text-gray-400"></i> <input type="text" id="searchInput" oninput="renderTable()" placeholder="חיפוש לפי טלפון או שם..." class="w-full pl-4 pr-10 py-2 rounded-lg border shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> </div> <div id="activeFilterBadge" class="hidden bg-blue-100 text-blue-800 text-sm font-semibold px-3 py-1 rounded-full flex items-center gap-2 shadow-sm border border-blue-200"> <span>מסנן פעיל</span> <button onclick="setFilter('all')" class="text-blue-500 hover:text-blue-700 bg-white rounded-full w-5 h-5 flex items-center justify-center"><i class="fas fa-times text-xs"></i></button> </div> </div> <!-- Table Container --> <div class="flex-1 px-6 pb-6 overflow-hidden"> <div class="bg-white rounded-lg shadow h-full overflow-auto border border-gray-200"> <table class="w-full text-sm text-right whitespace-nowrap"> <thead class="bg-gray-100 text-gray-600 sticky top-0 z-10 shadow-sm"> <tr> <th class="py-3 px-4 w-10 text-center"></th> <th class="py-3 px-4 font-semibold cursor-pointer hover:bg-gray-200" onclick="sortBy('phone')">טלפון <span id="sort-phone"></span></th> <th class="py-3 px-4 font-semibold w-32 cursor-pointer hover:bg-gray-200" onclick="sortBy('name')">שם <span id="sort-name"></span></th> <th class="py-3 px-4 font-semibold w-32 cursor-pointer hover:bg-gray-200" onclick="sortBy('contactStatus')">סטטוס קשר <span id="sort-contactStatus"></span></th> <th class="py-3 px-4 font-semibold w-40 cursor-pointer hover:bg-gray-200" onclick="sortBy('callNotes')">הערות שיחה <span id="sort-callNotes"></span></th> <th class="py-3 px-4 font-semibold w-32 cursor-pointer hover:bg-gray-200" onclick="sortBy('followupStatus')">סטטוס המשך <span id="sort-followupStatus"></span></th> <th class="py-3 px-4 font-semibold w-40 cursor-pointer hover:bg-gray-200" onclick="sortBy('statusNotes')">הערות סטטוס <span id="sort-statusNotes"></span></th> <th class="py-3 px-4 font-semibold w-32 cursor-pointer hover:bg-gray-200" onclick="sortBy('course')">קורס <span id="sort-course"></span></th> <th class="py-3 px-4 font-semibold text-center cursor-pointer hover:bg-gray-200" onclick="sortBy('lastCallTimestamp')">שיחה אחרונה <span id="sort-lastCallTimestamp"></span></th> </tr> </thead> <tbody id="tableBody" class="divide-y divide-gray-200"> <!-- Rows will be rendered here via JS --> </tbody> </table> <div id="emptyState" class="hidden text-center py-12 text-gray-500"> <i class="fas fa-box-open text-4xl mb-3 text-gray-300"></i> <p>אין נתונים להצגה או שאין תוצאות לסינון הנוכחי.</p> </div> </div> </div> <!-- Paste Modal --> <div id="pasteModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50"> <div class="bg-white rounded-lg p-6 w-1/2 shadow-2xl"> <h2 class="text-xl font-bold mb-4">הדבקת נתונים ידנית (גיבוי)</h2> <p class="text-sm text-gray-600 mb-2">פתח את הקובץ של ימות המשיח בפנקס רשומות, העתק את כל הטקסט והדבק אותו כאן:</p> <textarea id="pasteArea" rows="10" class="w-full border rounded p-2 mb-4 text-left" dir="ltr" placeholder="Folder#main%Phone#0533141200%..."></textarea> <div class="flex justify-end gap-3"> <button onclick="closePasteModal()" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">ביטול</button> <button onclick="processPastedData()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">קלוט נתונים</button> </div> </div> </div> <!-- Settings & Backup Modal --> <div id="settingsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50"> <div class="bg-white rounded-lg p-6 w-1/3 shadow-2xl max-w-lg"> <h2 class="text-xl font-bold mb-4 border-b pb-2"><i class="fas fa-cog"></i> הגדרות, גיבוי ושחזור</h2> <div class="mb-6"> <h3 class="font-bold mb-2">חיבור לימות המשיח (טוקן)</h3> <div class="flex items-center gap-2 mb-2"> <input type="password" id="tokenInput" placeholder="הכנס טוקן (או מספר טלפון:סיסמה או טוקן שנוצר עבור זה)" class="w-full border rounded p-2 text-left" dir="ltr"> <button onclick="saveToken()" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">שמור</button> </div> <p id="tokenStatus" class="text-sm font-semibold text-gray-500">סטטוס: לא נבדק</p> </div> <div class="mb-6 border-t pt-4"> <h3 class="font-bold mb-2">גיבוי ושחזור נתונים</h3> <p class="text-sm text-gray-600 mb-3">ייצוא קובץ הגיבוי שומר את כל הנתונים, הסטטוסים והטוקן, ומאפשר העברה מאובטחת למחשב אחר.</p> <div class="flex gap-3"> <button onclick="exportBackup()" class="flex-1 bg-gray-800 text-white px-4 py-2 rounded hover:bg-gray-700 flex justify-center items-center gap-2 text-sm font-semibold"> <i class="fas fa-download"></i> הורד גיבוי (JSON) </button> <label class="flex-1 bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 flex justify-center items-center gap-2 cursor-pointer text-sm font-semibold"> <i class="fas fa-upload"></i> טען גיבוי למערכת <input type="file" id="importFileInput" accept=".json" class="hidden" onchange="importBackup(event)"> </label> </div> </div> <div class="flex justify-end gap-3 border-t pt-4"> <button onclick="closeSettingsModal()" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 font-semibold">סגור</button> </div> </div> </div> <!-- History Modal --> <div id="historyModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50"> <div class="bg-white rounded-lg p-6 w-2/3 shadow-2xl max-w-2xl max-h-[80vh] flex flex-col"> <h2 class="text-xl font-bold mb-4 border-b pb-2 flex justify-between items-center"> <span><i class="fas fa-history text-blue-500"></i> היסטוריית עדכונים: <span id="historyPhone" class="font-normal" dir="ltr"></span> <span id="historyName" class="text-gray-500 text-base"></span></span> </h2> <div id="historyContent" class="overflow-y-auto flex-1 bg-gray-50 p-4 rounded border"> <!-- History items will be injected here --> </div> <div class="flex justify-end mt-4 pt-4 border-t"> <button onclick="closeHistoryModal()" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 font-semibold">סגור</button> </div> </div> </div> <!-- Setup Script --> <script> // Database Setup in LocalStorage let db = JSON.parse(localStorage.getItem('yemot_crm_db')) || {}; let savedToken = localStorage.getItem('yemot_crm_token') || ""; // Constants & State let currentFilter = 'all'; // יכול להיות: 'all', 'waiting', 'returned', 'done' let sortCol = 'lastCallTimestamp'; let sortDesc = true; // Form Options const contactStatuses = ["לא נוצר קשר", "שיחה לא נענתה", "נוצרה שיחה"]; const followupStatuses = ["", "ליצור שיחה נוספת", "ירד", "ממשיך"]; // Init document.addEventListener('DOMContentLoaded', () => { updateCourseList(); // עדכון רשימת הקורסים להשלמה אוטומטית updateTokenStatusUI(); renderTable(); }); // ------------------ Data Processing Logic ------------------ // function parseYmgrText(text) { const lines = text.split('\n').map(l => l.trim()).filter(l => l); const parsed = {}; lines.forEach(line => { const parts = line.split('%'); const data = {}; parts.forEach(part => { const [key, val] = part.split('#'); if (key) data[key] = val || ''; }); if (data.Phone && data.EnterDate && data.EnterTime) { const [day, month, year] = data.EnterDate.split('/'); const isoStr = `${year}-${month}-${day}T${data.EnterTime}`; const timestamp = new Date(isoStr).getTime(); if (!parsed[data.Phone]) { parsed[data.Phone] = { phone: data.Phone, latestTimestamp: timestamp, latestFormatted: `${data.EnterDate} ${data.EnterTime}` }; } else { if (timestamp > parsed[data.Phone].latestTimestamp) { parsed[data.Phone].latestTimestamp = timestamp; parsed[data.Phone].latestFormatted = `${data.EnterDate} ${data.EnterTime}`; } } } }); return parsed; } function mergeData(newParsedData) { let added = 0; let updated = 0; for (const [phone, info] of Object.entries(newParsedData)) { if (!db[phone]) { // New record db[phone] = { phone: phone, name: "", contactStatus: "לא נוצר קשר", callNotes: "", followupStatus: "", statusNotes: "", course: "", lastCallTimestamp: info.latestTimestamp, lastCallFormatted: info.latestFormatted, isReturned: false, addedAt: Date.now(), history: [] // Array to store historical states }; added++; } else { if (info.latestTimestamp > db[phone].lastCallTimestamp) { db[phone].lastCallTimestamp = info.latestTimestamp; db[phone].lastCallFormatted = info.latestFormatted; db[phone].isReturned = true; db[phone].contactStatus = "לא נוצר קשר"; updated++; } } } saveDb(); updateCourseDatalist(); renderTable(); alert(`סנכרון הושלם בהצלחה!\nמספרים חדשים שנוספו: ${added}\nמתקשרים שחזרו למערכת שוב: ${updated}`); } function saveDb() { localStorage.setItem('yemot_crm_db', JSON.stringify(db)); updateCourseDatalist(); updateDashboard(); } function updateCourseDatalist() { const datalist = document.getElementById('courseList'); if(!datalist) return; datalist.innerHTML = ''; const courses = [...new Set(Object.values(db).map(r => r.course).filter(c => c && c.trim() !== ''))]; courses.forEach(c => { const option = document.createElement('option'); option.value = c; datalist.appendChild(option); }); } // ------------------ Data Fetching ------------------ // async function autoFetchData() { if (!savedToken) { alert("לא מוגדר טוקן חיבור. אנא הכנס טוקן בחלון ההגדרות."); openSettingsModal(); return; } const icon = document.getElementById('syncIcon'); icon.classList.add('fa-spin'); const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const url = `https://www.call2all.co.il/ym/api/DownloadFile?token=${savedToken}&path=ivr2:Log/LogFolderEnterExit-${year}-${month}.ymgr`; try { const response = await fetch(url); if (!response.ok) throw new Error('Network response was not ok'); const text = await response.text(); if (text.includes('"responseStatus":"EXCEPTION"') || text.trim().startsWith('{')) { throw new Error('Token error'); } document.getElementById('tokenStatus').innerHTML = '<span class="text-green-600 font-bold"><i class="fas fa-check-circle"></i> חיבור הצליח (הטוקן תקין)</span>'; const parsed = parseYmgrText(text); mergeData(parsed); } catch (error) { console.error('Fetch error:', error); document.getElementById('tokenStatus').innerHTML = '<span class="text-red-600 font-bold"><i class="fas fa-times-circle"></i> שגיאת חיבור. טוקן שגוי או חסימה.</span>'; alert('הסנכרון האוטומטי נכשל.\nככל הנראה הטוקן שגוי או שהדפדפן חוסם את החיבור (CORS).\nאנא בדוק את הטוקן בחלון ההגדרות, או השתמש בסנכרון מהדבקה.'); openSettingsModal(); } finally { icon.classList.remove('fa-spin'); } } // ------------------ Formatting Helpers ------------------ // function formatPhoneForWhatsApp(phone) { // Remove any non-digit characters let cleanPhone = phone.replace(/\D/g, ''); // If it starts with 0, replace with 972 if (cleanPhone.startsWith('0')) { cleanPhone = '972' + cleanPhone.substring(1); } return cleanPhone; } // ------------------ UI & Filtering & Sorting ------------------ // function sortBy(col) { if (sortCol === col) { sortDesc = !sortDesc; } else { sortCol = col; sortDesc = col === 'lastCallTimestamp'; // תאריך כברירת מחדל יורד, השאר עולה } renderTable(); } function setFilter(filterType) { currentFilter = filterType; // איפוס עיצוב מכל הכרטיסיות document.getElementById('card-all').classList.remove('ring-4', 'ring-blue-500', 'ring-opacity-50'); document.getElementById('card-waiting').classList.remove('ring-4', 'ring-yellow-500', 'ring-opacity-50'); document.getElementById('card-returned').classList.remove('ring-4', 'ring-red-500', 'ring-opacity-50'); document.getElementById('card-done').classList.remove('ring-4', 'ring-green-500', 'ring-opacity-50'); // הוספת עיצוב לכרטיסייה הנבחרת if (filterType === 'all') document.getElementById('card-all').classList.add('ring-4', 'ring-blue-500', 'ring-opacity-50'); if (filterType === 'waiting') document.getElementById('card-waiting').classList.add('ring-4', 'ring-yellow-500', 'ring-opacity-50'); if (filterType === 'returned') document.getElementById('card-returned').classList.add('ring-4', 'ring-red-500', 'ring-opacity-50'); if (filterType === 'done') document.getElementById('card-done').classList.add('ring-4', 'ring-green-500', 'ring-opacity-50'); // עדכון תגית הסינון const badge = document.getElementById('activeFilterBadge'); if (filterType === 'all') badge.classList.add('hidden'); else { badge.classList.remove('hidden'); const badgeText = badge.querySelector('span'); if(filterType === 'waiting') badgeText.innerText = 'מסנן פעיל: ממתינים'; if(filterType === 'returned') badgeText.innerText = 'מסנן פעיל: חזרו שוב'; if(filterType === 'done') badgeText.innerText = 'מסנן פעיל: טופלו'; } renderTable(); } function updateCourseList() { const courses = new Set(); Object.values(db).forEach(rec => { if (rec.course && rec.course.trim() !== '') { courses.add(rec.course.trim()); } }); } function renderTable() { const tbody = document.getElementById('tableBody'); const search = document.getElementById('searchInput').value.toLowerCase(); // איפוס חיצי מיון document.querySelectorAll('th span[id^="sort-"]').forEach(el => el.innerHTML = ''); const sortIconEl = document.getElementById('sort-' + sortCol); if (sortIconEl) { sortIconEl.innerHTML = sortDesc ? '<i class="fas fa-sort-down ml-1"></i>' : '<i class="fas fa-sort-up ml-1"></i>'; } tbody.innerHTML = ''; let records = Object.values(db); // סינון הנתונים records = records.filter(rec => { const matchSearch = rec.phone.includes(search) || (rec.name || '').toLowerCase().includes(search) || (rec.course || '').toLowerCase().includes(search); let matchFilter = true; if (currentFilter === 'waiting') matchFilter = rec.contactStatus === "לא נוצר קשר"; if (currentFilter === 'returned') matchFilter = rec.isReturned === true; if (currentFilter === 'done') matchFilter = (rec.followupStatus === "ירד" || rec.followupStatus === "ממשיך"); return matchSearch && matchFilter; }); // מיון הנתונים records.sort((a, b) => { let valA = a[sortCol] !== undefined ? a[sortCol] : ''; let valB = b[sortCol] !== undefined ? b[sortCol] : ''; if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); if (valA < valB) return sortDesc ? 1 : -1; if (valA > valB) return sortDesc ? -1 : 1; return 0; }); if (records.length === 0) { document.getElementById('emptyState').classList.remove('hidden'); } else { document.getElementById('emptyState').classList.add('hidden'); } records.forEach(rec => { const tr = document.createElement('tr'); tr.className = `hover:bg-gray-50 transition ${rec.isReturned ? 'returned-row' : ''}`; let indicatorHTML = ''; if (rec.isReturned) { indicatorHTML = `<button onclick="clearReturned('${rec.phone}')" title="לחץ לסימון כטופל" class="text-red-500 hover:text-red-700 font-bold bg-red-100 rounded-full w-6 h-6 flex items-center justify-center mx-auto mb-1"><i class="fas fa-exclamation text-xs"></i></button>`; } // כפתור היסטוריה indicatorHTML += `<button onclick="openHistoryModal('${rec.phone}')" title="היסטוריית שינויים" class="text-gray-400 hover:text-blue-500 font-bold rounded-full w-6 h-6 flex items-center justify-center mx-auto"><i class="fas fa-history text-sm"></i></button>`; // קישור לוואטסאפ const waPhone = formatPhoneForWhatsApp(rec.phone); const waLink = `https://wa.me/${waPhone}`; // השלמת תאימות במידה ויש שורות ישנות שאין להן שדה 'course' const safeCourse = rec.course || ''; tr.innerHTML = ` <td class="py-2 px-4 flex flex-col items-center justify-center h-full">${indicatorHTML}</td> <td class="py-2 px-4 font-bold text-gray-700" dir="ltr"> <a href="${waLink}" target="_blank" title="פתח שיחת WhatsApp" class="text-blue-600 hover:text-blue-800 hover:underline flex items-center gap-1 justify-end"> ${rec.phone} <i class="fab fa-whatsapp text-green-500"></i> </a> </td> <td class="py-2 px-4"><input type="text" value="${rec.name}" onchange="updateRecord('${rec.phone}', 'name', this.value)" placeholder="שם הלקוח" class="table-input bg-transparent"></td> <td class="py-2 px-4"> <select onchange="updateRecord('${rec.phone}', 'contactStatus', this.value)" class="table-input ${rec.contactStatus === 'לא נוצר קשר' ? 'text-red-600 font-bold bg-red-50' : ''}"> ${contactStatuses.map(s => `<option value="${s}" ${rec.contactStatus === s ? 'selected' : ''}>${s}</option>`).join('')} </select> </td> <td class="py-2 px-4"><input type="text" value="${rec.callNotes}" onchange="updateRecord('${rec.phone}', 'callNotes', this.value)" placeholder="הערות..." class="table-input bg-transparent"></td> <td class="py-2 px-4"> <select onchange="updateRecord('${rec.phone}', 'followupStatus', this.value)" class="table-input"> ${followupStatuses.map(s => `<option value="${s}" ${rec.followupStatus === s ? 'selected' : ''}>${s}</option>`).join('')} </select> </td> <td class="py-2 px-4"><input type="text" value="${rec.statusNotes}" onchange="updateRecord('${rec.phone}', 'statusNotes', this.value)" placeholder="הערות סטטוס..." class="table-input bg-transparent"></td> <td class="py-2 px-4 bg-blue-50 bg-opacity-30"><input type="text" list="courseList" value="${safeCourse}" onchange="updateRecord('${rec.phone}', 'course', this.value)" placeholder="קורס..." class="table-input bg-transparent font-semibold border-gray-200 focus:border-blue-500"></td> <td class="py-2 px-4 text-center text-gray-500 text-xs" dir="ltr">${rec.lastCallFormatted}</td> `; tbody.appendChild(tr); }); updateDashboard(); } function updateRecord(phone, field, value) { if (db[phone]) { const oldVal = db[phone][field]; // שמירת היסטוריה אם השדה שונה והוא אחד מהשדות המרכזיים if (oldVal !== value && ['contactStatus', 'callNotes', 'followupStatus', 'statusNotes', 'course'].includes(field)) { if (!db[phone].history) db[phone].history = []; // מתרגם שם שדה לעברית עבור ההיסטוריה const fieldNamesHe = { 'contactStatus': 'סטטוס קשר', 'callNotes': 'הערות שיחה', 'followupStatus': 'סטטוס המשך', 'statusNotes': 'הערות סטטוס', 'course': 'קורס' }; db[phone].history.push({ date: new Date().toLocaleString('he-IL'), fieldChanged: fieldNamesHe[field] || field, oldValue: oldVal, newValue: value }); } // עדכון הערך החדש db[phone][field] = value; // הסרה אוטומטית של צבע "חזר למערכת" אם השורה נערכה if (db[phone].isReturned) { db[phone].isReturned = false; } saveDb(); // אם עדכנו קורס, נעדכן את רשימת ההשלמה האוטומטית if(field === 'course') updateCourseList(); // רנדור כדי להעלים את הצבע האדום או לעדכן מיון/סינון renderTable(); } } function clearReturned(phone) { if (db[phone]) { db[phone].isReturned = false; saveDb(); renderTable(); } } function updateDashboard() { const records = Object.values(db); const total = records.length; const waiting = records.filter(r => r.contactStatus === "לא נוצר קשר").length; const returned = records.filter(r => r.isReturned).length; const done = records.filter(r => r.followupStatus === "ירד" || r.followupStatus === "ממשיך").length; document.getElementById('dashTotal').innerText = total; document.getElementById('dashWaiting').innerText = waiting; document.getElementById('dashReturned').innerText = returned; document.getElementById('dashDone').innerText = done; } // ------------------ Modals, Settings & Import/Export ------------------ // function openSettingsModal() { document.getElementById('tokenInput').value = savedToken ? "********" : ""; document.getElementById('settingsModal').classList.remove('hidden'); } function closeSettingsModal() { document.getElementById('settingsModal').classList.add('hidden'); } function saveToken() { const inputVal = document.getElementById('tokenInput').value.trim(); if (inputVal && inputVal !== "********") { savedToken = inputVal; localStorage.setItem('yemot_crm_token', savedToken); document.getElementById('tokenStatus').innerHTML = '<span class="text-blue-600 font-bold"><i class="fas fa-info-circle"></i> הטוקן נשמר. ייבדק בסנכרון הבא.</span>'; } } function updateTokenStatusUI() { if (savedToken) { document.getElementById('tokenStatus').innerHTML = '<span class="text-green-600 font-bold"><i class="fas fa-key"></i> טוקן קיים במערכת (ממתין לבדיקה בסנכרון)</span>'; } else { document.getElementById('tokenStatus').innerHTML = '<span class="text-red-600 font-bold"><i class="fas fa-exclamation-triangle"></i> טוקן לא מוגדר</span>'; } } function exportBackup() { const backupData = { version: "1.3", // Updated version timestamp: Date.now(), token: savedToken, db: db }; const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(backupData)); const dlAnchorElem = document.createElement('a'); dlAnchorElem.setAttribute("href", dataStr); dlAnchorElem.setAttribute("download", `Machon_Backup_${new Date().toLocaleDateString('he-IL').replace(/\./g, '-')}.json`); document.body.appendChild(dlAnchorElem); dlAnchorElem.click(); dlAnchorElem.remove(); } function importBackup(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const importedData = JSON.parse(e.target.result); if (importedData.db) { db = importedData.db; // Migrate old DB format to new (ensure history array exists) Object.values(db).forEach(rec => { if(!rec.history) rec.history = []; }); saveDb(); if (importedData.token) { savedToken = importedData.token; localStorage.setItem('yemot_crm_token', savedToken); updateTokenStatusUI(); } updateCourseList(); renderTable(); alert("הנתונים שוחזרו בהצלחה!"); closeSettingsModal(); } else { alert("שגיאה: קובץ הגיבוי לא תקין או ריק."); } } catch (err) { alert("שגיאה בקריאת הקובץ. ודא שזהו קובץ ה-JSON שהורדת מהמערכת."); } }; reader.readAsText(file); event.target.value = ''; // Reset input } function openPasteModal() { document.getElementById('pasteModal').classList.remove('hidden'); } function closePasteModal() { document.getElementById('pasteModal').classList.add('hidden'); document.getElementById('pasteArea').value = ''; } function processPastedData() { const text = document.getElementById('pasteArea').value; if (!text.trim()) return; const parsed = parseYmgrText(text); mergeData(parsed); closePasteModal(); } // History Modal Logic function openHistoryModal(phone) { const rec = db[phone]; if(!rec) return; document.getElementById('historyPhone').innerText = rec.phone; document.getElementById('historyName').innerText = rec.name ? `(${rec.name})` : ''; const container = document.getElementById('historyContent'); container.innerHTML = ''; const historyArr = rec.history || []; if (historyArr.length === 0) { container.innerHTML = `<div class="text-center text-gray-500 my-8"><i class="fas fa-inbox text-3xl mb-2"></i><br>אין עדיין היסטוריית שינויים מתועדת ללקוח זה.</div>`; } else { // Reverse array to show newest first [...historyArr].reverse().forEach(h => { const item = document.createElement('div'); item.className = "mb-3 p-3 bg-white border rounded shadow-sm flex flex-col gap-1"; item.innerHTML = ` <div class="text-xs text-gray-500 flex justify-between"> <span><i class="fas fa-edit"></i> שונה שדה: <strong>${h.fieldChanged}</strong></span> <span dir="ltr">${h.date}</span> </div> <div class="text-sm mt-1"> <span class="text-gray-400 line-through">${h.oldValue || '<em>ריק</em>'}</span> <i class="fas fa-arrow-left text-blue-400 mx-2"></i> <span class="font-semibold text-gray-800">${h.newValue || '<em>ריק</em>'}</span> </div> `; container.appendChild(item); }); } document.getElementById('historyModal').classList.remove('hidden'); } function closeHistoryModal() { document.getElementById('historyModal').classList.add('hidden'); } function exportToExcel() { const records = Object.values(db).sort((a, b) => b.lastCallTimestamp - a.lastCallTimestamp); if(records.length === 0) return alert('אין נתונים לייצוא'); // Header row let csvContent = "טלפון,שם,סטטוס התקשרות,הערות שיחה,סטטוס המשך,הערות סטטוס,קורס,תאריך שיחה אחרונה,חזר שוב לאחר שיחה\n"; records.forEach(r => { const cleanPhone = `="${r.phone}"`; // Fix excel dropping leading zeros const row = [ cleanPhone, `"${r.name || ''}"`, `"${r.contactStatus || ''}"`, `"${r.callNotes || ''}"`, `"${r.followupStatus || ''}"`, `"${r.statusNotes || ''}"`, `"${r.course || ''}"`, `"${r.lastCallFormatted || ''}"`, r.isReturned ? 'כן' : 'לא' ].join(","); csvContent += row + "\n"; }); // Add BOM for Hebrew encoding in Excel const bom = "\uFEFF"; const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); const url = URL.createObjectURL(blob); const date = new Date(); const fileName = `Machon_Torat_Hayoledet_CRM_${date.toLocaleDateString('he-IL').replace(/\./g, '-')}.csv`; link.setAttribute("href", url); link.setAttribute("download", fileName); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } </script> </body> </html> ```|| -
@יוניבני
ממש יפה הממשק! -
@יוניבני יפה מאד יישר כח על השיתוף ממש מושקע
-
זה יכול להתאים למודול תור, בעיקר כשיביאו את מפתח הAPI לניהול התור החינמי.