/** * @fileoverview Contacts Table & Cards View Rendering Engine & Core Controller * @module ui/contacts-renderer * @description محرك رسم وتحديث جدول وبطاقات جهات الاتصال، فلاتر البحث المتقدمة، وترتيب الأعمدة التفاعلي. */ (global => { 'use strict'; /** * الحالة المركزية لجهات الاتصال */ const ContactsState = { contacts: [], currentlyDisplayedContacts: [], selectedContactIds: new Set(), searchQuery: '', activeFilterTag: 'all', filterLifecycle: 'all', filterBlacklist: 'all', filterHasEmail: 'all', filterDateFrom: '', filterDateTo: '', sortColumn: 'createdAt', sortDirection: 'desc', viewMode: 'table', isLoaded: false }; /** * احتساب مظهر وشارة تاريخ المتابعة القادمة * @param {string} followupDateStr * @returns {{ text: string, color: string, bg: string, badge: string }} */ function getFollowupDateStyle(followupDateStr) { if (!followupDateStr) return { text: '-', color: 'var(--text-muted)', bg: 'var(--bg-subtle)', badge: 'غير مجدولة' }; const followupDate = new Date(followupDateStr); if (isNaN(followupDate.getTime())) return { text: '-', color: 'var(--text-muted)', bg: 'var(--bg-subtle)', badge: 'غير مجدولة' }; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const targetDay = new Date(followupDate.getFullYear(), followupDate.getMonth(), followupDate.getDate()); const diffTime = targetDay.getTime() - today.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const dateFormatted = targetDay.toLocaleDateString('en-US'); if (diffDays <= 0) { return { text: dateFormatted, color: 'var(--danger)', bg: 'var(--danger-light)', badge: diffDays === 0 ? 'المتابعة اليوم!' : 'متأخرة' }; } else if (diffDays <= 7) { return { text: dateFormatted, color: 'var(--warning-hover)', bg: 'var(--warning-light)', badge: `خلال ${diffDays} أيام` }; } else { return { text: dateFormatted, color: 'var(--success-hover)', bg: 'var(--success-light)', badge: `بعد ${diffDays} يوم` }; } } /** * توليد وسم مرحلة المبيعات (Lifecycle Stage Badge) * @param {string} stage * @returns {string} HTML */ function getLifecycleBadgeHTML(stage) { const stages = { 'lead': { label: 'عميل محتمل', color: '#0284c7', bg: 'rgba(2, 132, 199, 0.12)', border: 'rgba(2, 132, 199, 0.3)' }, 'qualified': { label: 'عميل مؤهل', color: '#d97706', bg: 'rgba(217, 119, 6, 0.12)', border: 'rgba(217, 119, 6, 0.3)' }, 'active': { label: 'عميل حالي', color: '#16a34a', bg: 'rgba(22, 163, 74, 0.12)', border: 'rgba(22, 163, 74, 0.3)' }, 'vip': { label: 'عميل متميز VIP', color: '#9333ea', bg: 'rgba(147, 51, 234, 0.12)', border: 'rgba(147, 51, 234, 0.3)' }, 'won': { label: 'صفقة مكتملة', color: '#059669', bg: 'rgba(5, 150, 105, 0.12)', border: 'rgba(5, 150, 105, 0.3)' }, 'lost': { label: 'غير مهتم', color: '#dc2626', bg: 'rgba(220, 38, 38, 0.12)', border: 'rgba(220, 38, 38, 0.3)' } }; const conf = stages[stage] || stages['lead']; return `${conf.label}`; } /** * محرك رسم جهات الاتصال */ const ContactsRendererEngine = { /** * تحديث شارة عدد العناصر المحددة */ updateSelectedCountBadge: function() { const badge = document.getElementById('qh64ktmllu'); if (badge) { badge.textContent = `المحدد حالياً: ${ContactsState.selectedContactIds.size.toLocaleString('en-US')}`; } }, /** * تحديث حالة مربع تحديد الكل */ updateSelectAllCheckboxState: function() { const selectAllChk = document.getElementById('ubt2zni00m'); const rowCheckboxes = document.querySelectorAll('.i07h98mtfb'); if (!selectAllChk || rowCheckboxes.length === 0) return; const allChecked = Array.from(rowCheckboxes).every(cb => cb.checked); selectAllChk.checked = allChecked; }, /** * رسم جدول وبطاقات جهات الاتصال بعد الفلترة والترتيب */ renderTable: function() { const state = ContactsState; const displayContainer = document.getElementById('tu54lhiegr'); if (!displayContainer) return; const q = (state.searchQuery || '').toLowerCase(); const tagF = state.activeFilterTag; const lcF = state.filterLifecycle; const blF = state.filterBlacklist; const emailF = state.filterHasEmail; const dateFrom = state.filterDateFrom; const dateTo = state.filterDateTo; // تطبيق فلاتر البحث المتقدمة let filtered = state.contacts.filter(c => { const nameStr = c.name || ''; const phoneStr = c.phone || ''; const emailStr = c.email || ''; const compStr = c.company || ''; const jobStr = c.jobTitle || ''; const notesStr = c.notes || ''; const matchQuery = !q || nameStr.toLowerCase().includes(q) || phoneStr.includes(q) || (c.altPhones && c.altPhones.some(ap => (typeof ap === 'object' ? ap.phone : ap).includes(q))) || emailStr.toLowerCase().includes(q) || compStr.toLowerCase().includes(q) || jobStr.toLowerCase().includes(q) || notesStr.toLowerCase().includes(q); let matchTag = true; if (tagF !== 'all') matchTag = c.tags && c.tags.includes(tagF); let matchLC = true; if (lcF !== 'all') matchLC = (c.lifecycle || 'lead') === lcF; let matchBL = true; if (blF === 'clean') matchBL = !c.blacklisted; else if (blF === 'blacklisted') matchBL = !!c.blacklisted; let matchEmail = true; if (emailF === 'yes') matchEmail = !!c.email; else if (emailF === 'no') matchEmail = !c.email; let matchDate = true; if (dateFrom && c.createdAt) matchDate = matchDate && new Date(c.createdAt) >= new Date(dateFrom); if (dateTo && c.createdAt) matchDate = matchDate && new Date(c.createdAt) <= new Date(dateTo + 'T23:59:59'); return matchQuery && matchTag && matchLC && matchBL && matchEmail && matchDate; }); // تطبيق الترتيب const col = state.sortColumn; const dir = state.sortDirection === 'asc' ? 1 : -1; filtered.sort((a, b) => { if (col === 'name') { return dir * (a.name || '').localeCompare(b.name || '', 'ar'); } else if (col === 'phone') { return dir * (a.phone || '').localeCompare(b.phone || ''); } else if (col === 'lifecycle') { return dir * (a.lifecycle || 'lead').localeCompare(b.lifecycle || 'lead'); } else if (col === 'tags') { const tagsA = (a.tags || []).join(', '); const tagsB = (b.tags || []).join(', '); return dir * tagsA.localeCompare(tagsB, 'ar'); } else if (col === 'followup') { const dateA = a.nextFollowupDate ? new Date(a.nextFollowupDate).getTime() : 0; const dateB = b.nextFollowupDate ? new Date(b.nextFollowupDate).getTime() : 0; return dir * (dateA - dateB); } else { const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0; const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0; return dir * (dateA - dateB); } }); state.currentlyDisplayedContacts = filtered; // تحديث بطاقات الإحصائيات const totalEl = document.getElementById('x7y3ihnuz6'); const vipEl = document.getElementById('hl58n1vsdz'); const tagsEl = document.getElementById('z9imk5h96y'); const blacklistedEl = document.getElementById('xvhp7umte1'); if (totalEl) totalEl.textContent = state.contacts.length.toLocaleString('en-US'); if (vipEl) vipEl.textContent = state.contacts.filter(c => c.lifecycle === 'vip' || c.lifecycle === 'won').length.toLocaleString('en-US'); const allTags = new Set(); state.contacts.forEach(c => (c.tags || []).forEach(t => allTags.add(t))); if (tagsEl) tagsEl.textContent = allTags.size.toLocaleString('en-US'); if (blacklistedEl) blacklistedEl.textContent = state.contacts.filter(c => c.blacklisted).length.toLocaleString('en-US'); const rangeInfoText = document.getElementById('wsbcxjitau'); if (rangeInfoText) { rangeInfoText.textContent = `المعروض حالياً: ${filtered.length.toLocaleString('en-US')} جهة اتصال (من 1 إلى ${filtered.length.toLocaleString('en-US')})`; } const selectTag = document.getElementById('rk1gpbb0ma'); if (selectTag && selectTag.options.length <= 1) { selectTag.innerHTML = ''; allTags.forEach(t => { const opt = document.createElement('option'); opt.value = t; opt.textContent = `وسم: ${t}`; selectTag.appendChild(opt); }); selectTag.value = tagF; } this.updateSelectedCountBadge(); if (filtered.length === 0) { displayContainer.innerHTML = `
لا توجد أي جهات اتصال مطابقة لشروط البحث والتصفية حالياً.
`; return; } // 1. عرض البطاقات (Cards View) if (state.viewMode === 'cards') { displayContainer.innerHTML = ''; const cardsGrid = document.createElement('div'); cardsGrid.style.cssText = 'display:grid; grid-template-columns:repeat(auto-fill, minmax(clamp(250px, 24vw, 320px), 1fr)); gap:12px; max-height:550px; overflow-y:auto; padding:2px; box-sizing:border-box;'; filtered.forEach((c, indexIdx) => { const itemNumber = indexIdx + 1; c._displayIndex = itemNumber; const isSelected = state.selectedContactIds.has(c.id); const fuStyle = getFollowupDateStyle(c.nextFollowupDate); const card = document.createElement('div'); card.style.cssText = `background:var(--bg-card); border:1px solid ${isSelected ? 'var(--primary)' : (c.blacklisted ? 'var(--danger-border)' : 'var(--border)')}; border-radius:var(--radius-lg); padding:14px; display:flex; flex-direction:column; gap:8px; box-shadow:var(--shadow-xs); position:relative; ${isSelected ? 'background:var(--primary-light);' : (c.blacklisted ? 'background:var(--danger-light);' : '')}`; let phonesHTML = `
${c.phone}
`; if (c.altPhones && Array.isArray(c.altPhones) && c.altPhones.length > 0) { c.altPhones.forEach(ap => { const pVal = typeof ap === 'object' ? ap.phone : ap; const pType = typeof ap === 'object' ? ap.type : 'عمل'; phonesHTML += `
${pVal} (${pType})
`; }); } card.innerHTML = ` #${itemNumber}
${c.name} ${c.jobTitle || ''} ${c.company ? `@ ${c.company}` : ''}
${getLifecycleBadgeHTML(c.lifecycle || 'lead')}
${phonesHTML} ${c.email ? `
${c.email}
` : ''}
متابعة: ${fuStyle.text} (${fuStyle.badge})
`; const chk = card.querySelector('.i07h98mtfb'); chk.addEventListener('change', (e) => { e.stopPropagation(); if (chk.checked) state.selectedContactIds.add(c.id); else state.selectedContactIds.delete(c.id); this.updateSelectAllCheckboxState(); this.updateSelectedCountBadge(); this.renderTable(); }); card.querySelector('.ysx77m6bsw').onclick = () => { if (global.FritreeContactsEditor && typeof global.FritreeContactsEditor.openModal === 'function') { global.FritreeContactsEditor.openModal(c.id); } }; card.querySelector('.fp89nb580e').onclick = async () => { if (confirm(`هل أنت متأكد من حذف العميل "${c.name}"؟`)) { state.contacts = state.contacts.filter(x => x.id !== c.id); state.selectedContactIds.delete(c.id); await global.FritreeContacts.save(); this.renderTable(); } }; card.querySelector('.uqi0ybfjg4').onclick = () => { if (global.FritreeWhatsApp && typeof global.FritreeWhatsApp.setRecipients === 'function') { global.FritreeWhatsApp.setRecipients([{ phone: c.phone, name: c.name }]); window.location.hash = 'sf8pufcpyl'; } }; cardsGrid.appendChild(card); }); displayContainer.appendChild(cardsGrid); } else { // 2. عرض الجدول (Table View) displayContainer.innerHTML = `
الرقم العميل والشركة أرقام الموبايل مرحلة المبيعات الوسوم المتابعة القادمة الإجراءات
`; const tbody = displayContainer.querySelector('#ojipewrl1o'); const selectAllChk = displayContainer.querySelector('#ubt2zni00m'); const sortHeaders = displayContainer.querySelectorAll('.fp6v22zaby'); sortHeaders.forEach(th => { const key = th.dataset.sortKey; const icon = th.querySelector('i'); if (icon) { if (key === state.sortColumn) { icon.className = state.sortDirection === 'asc' ? 'fa-solid fa-sort-up' : 'fa-solid fa-sort-down'; icon.style.opacity = '1'; icon.style.color = 'var(--primary)'; } else { icon.className = 'fa-solid fa-sort'; icon.style.opacity = '0.4'; icon.style.color = 'inherit'; } } th.onclick = (e) => { e.stopPropagation(); if (state.sortColumn === key) { state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'; } else { state.sortColumn = key; state.sortDirection = 'asc'; } this.renderTable(); }; }); if (selectAllChk) { const allRenderedSelected = filtered.length > 0 && filtered.every(c => state.selectedContactIds.has(c.id)); selectAllChk.checked = allRenderedSelected; selectAllChk.onclick = (e) => { const isChecked = e.target.checked; filtered.forEach(c => { if (isChecked) state.selectedContactIds.add(c.id); else state.selectedContactIds.delete(c.id); }); this.renderTable(); }; } filtered.forEach((c, indexIdx) => { const itemNumber = indexIdx + 1; c._displayIndex = itemNumber; const tr = document.createElement('tr'); tr.dataset.id = c.id; tr.dataset.blacklisted = c.blacklisted ? 'true' : 'false'; const isSelected = state.selectedContactIds.has(c.id); if (isSelected) { tr.style.backgroundColor = 'var(--primary-light)'; } else if (c.blacklisted) { tr.style.backgroundColor = 'var(--danger-light)'; } const tdChk = document.createElement('td'); tdChk.style.textAlign = 'center'; const chk = document.createElement('input'); chk.type = 'checkbox'; chk.className = 'i07h98mtfb'; chk.value = c.id; chk.checked = isSelected; chk.style.cssText = 'width:16px; height:16px; cursor:pointer; accent-color:var(--primary);'; chk.addEventListener('change', (e) => { e.stopPropagation(); if (chk.checked) { state.selectedContactIds.add(c.id); tr.style.backgroundColor = 'var(--primary-light)'; } else { state.selectedContactIds.delete(c.id); tr.style.backgroundColor = c.blacklisted ? 'var(--danger-light)' : ''; } this.updateSelectAllCheckboxState(); this.updateSelectedCountBadge(); }); tdChk.appendChild(chk); const tdIndex = document.createElement('td'); tdIndex.style.textAlign = 'center'; tdIndex.innerHTML = `#${itemNumber}`; const tdName = document.createElement('td'); tdName.style.fontWeight = '700'; tdName.innerHTML = ` ${c.name}
${c.jobTitle ? `${c.jobTitle}` : ''} ${c.company ? `@ ${c.company}` : ''}
`; const tdPhones = document.createElement('td'); tdPhones.style.direction = 'ltr'; tdPhones.style.textAlign = 'right'; const phonesBox = document.createElement('div'); phonesBox.style.cssText = 'display:flex; flex-direction:column; gap:2px; text-align:right; font-family:var(--font-code);'; const mainPhoneDiv = document.createElement('div'); mainPhoneDiv.style.cssText = 'font-weight:700; font-size:11.5px;'; mainPhoneDiv.innerHTML = `${c.phone}`; phonesBox.appendChild(mainPhoneDiv); if (c.altPhones && Array.isArray(c.altPhones) && c.altPhones.length > 0) { c.altPhones.forEach(ap => { const pVal = typeof ap === 'object' ? ap.phone : ap; const pType = typeof ap === 'object' ? ap.type : 'عمل'; if (pVal) { const altDiv = document.createElement('div'); altDiv.style.cssText = 'font-size:10px; color:var(--text-muted); font-weight:normal;'; altDiv.textContent = `${pVal} (${pType})`; phonesBox.appendChild(altDiv); } }); } tdPhones.appendChild(phonesBox); const tdLifecycle = document.createElement('td'); tdLifecycle.innerHTML = getLifecycleBadgeHTML(c.lifecycle || 'lead'); const tdTags = document.createElement('td'); if (c.tags && c.tags.length > 0) { c.tags.forEach(t => { const tagChip = document.createElement('span'); tagChip.style.cssText = 'background:var(--bg-subtle); color:var(--primary); border:1px solid var(--border); font-size:9px; padding:2px 6px; border-radius:var(--radius-pill); font-weight:700; margin-left:4px; display:inline-block;'; tagChip.textContent = t; tdTags.appendChild(tagChip); }); } else { tdTags.textContent = '-'; } const tdFollowup = document.createElement('td'); const fuStyle = getFollowupDateStyle(c.nextFollowupDate); tdFollowup.style.cssText = 'font-size:11px; font-weight:700;'; tdFollowup.innerHTML = ` ${fuStyle.text}
${fuStyle.badge}
`; const tdActions = document.createElement('td'); tdActions.style.textAlign = 'center'; const editBtn = document.createElement('button'); editBtn.className = 'f3ko84hxzm'; editBtn.title = 'تعديل ملف العميل'; editBtn.innerHTML = ' '; editBtn.onclick = () => { if (global.FritreeContactsEditor && typeof global.FritreeContactsEditor.openModal === 'function') { global.FritreeContactsEditor.openModal(c.id); } }; const delBtn = document.createElement('button'); delBtn.className = 'f3ko84hxzm'; delBtn.style.color = 'var(--danger)'; delBtn.title = 'حذف'; delBtn.innerHTML = ' '; delBtn.onclick = async () => { if (confirm(`هل أنت متأكد من حذف العميل "${c.name}"؟`)) { state.contacts = state.contacts.filter(x => x.id !== c.id); state.selectedContactIds.delete(c.id); await global.FritreeContacts.save(); this.renderTable(); } }; tdActions.appendChild(editBtn); tdActions.appendChild(delBtn); tr.appendChild(tdChk); tr.appendChild(tdIndex); tr.appendChild(tdName); tr.appendChild(tdPhones); tr.appendChild(tdLifecycle); tr.appendChild(tdTags); tr.appendChild(tdFollowup); tr.appendChild(tdActions); tbody.appendChild(tr); }); } }, /** * ربط مستمعات الفلاتر وأزرار تغيير العرض */ bindFilterEvents: function() { const state = ContactsState; const searchInput = document.getElementById('n1ysqfcdai'); const tagSelect = document.getElementById('rk1gpbb0ma'); const lifecycleSelect = document.getElementById('hqh1kdfr3z'); const blacklistSelect = document.getElementById('u8qlaic1mh'); const sortSelect = document.getElementById('ranpfg2wg4'); const dateFromInput = document.getElementById('z3wip3m4zk'); const dateToInput = document.getElementById('m8dxissd3m'); const btnViewTable = document.getElementById('gewqxqhw1a'); const btnViewCards = document.getElementById('eqric71940'); if (btnViewTable && btnViewCards) { btnViewTable.onclick = () => { state.viewMode = 'table'; btnViewTable.classList.add('vn5qn7dmpk'); btnViewCards.classList.remove('vn5qn7dmpk'); this.renderTable(); }; btnViewCards.onclick = () => { state.viewMode = 'cards'; btnViewCards.classList.add('vn5qn7dmpk'); btnViewTable.classList.remove('vn5qn7dmpk'); this.renderTable(); }; } if (searchInput) searchInput.oninput = (e) => { state.searchQuery = e.target.value; this.renderTable(); }; if (tagSelect) tagSelect.onchange = (e) => { state.activeFilterTag = e.target.value; this.renderTable(); }; if (lifecycleSelect) lifecycleSelect.onchange = (e) => { state.filterLifecycle = e.target.value; this.renderTable(); }; if (blacklistSelect) blacklistSelect.onchange = (e) => { state.filterBlacklist = e.target.value; this.renderTable(); }; if (sortSelect) { sortSelect.onchange = (e) => { const val = e.target.value; if (val === 'date_desc') { state.sortColumn = 'createdAt'; state.sortDirection = 'desc'; } else if (val === 'date_asc') { state.sortColumn = 'createdAt'; state.sortDirection = 'asc'; } else if (val === 'name_asc') { state.sortColumn = 'name'; state.sortDirection = 'asc'; } else if (val === 'name_desc') { state.sortColumn = 'name'; state.sortDirection = 'desc'; } this.renderTable(); }; } if (dateFromInput) dateFromInput.onchange = (e) => { state.filterDateFrom = e.target.value; this.renderTable(); }; if (dateToInput) dateToInput.onchange = (e) => { state.filterDateTo = e.target.value; this.renderTable(); }; } }; /** * وحدة جهات الاتصال الرئيسية */ global.FritreeContacts = { state: ContactsState, getContacts: () => ContactsState.contacts, lookup: (phone) => { if (global.FritreeContactsResolver && typeof global.FritreeContactsResolver.lookupByPhone === 'function') { return global.FritreeContactsResolver.lookupByPhone(phone, ContactsState.contacts); } return null; }, load: async function() { if (global.FritreeContactsVault && typeof global.FritreeContactsVault.load === 'function') { ContactsState.contacts = await global.FritreeContactsVault.load(); } else if (global.FritreeStorage) { ContactsState.contacts = await global.FritreeStorage.get('local_contacts_database', []); } ContactsState.isLoaded = true; ContactsRendererEngine.renderTable(); return ContactsState.contacts; }, save: async function() { if (global.FritreeContactsVault && typeof global.FritreeContactsVault.save === 'function') { await global.FritreeContactsVault.save(ContactsState.contacts); } else if (global.FritreeStorage) { await global.FritreeStorage.set('local_contacts_database', ContactsState.contacts); } }, refreshUI: function() { ContactsRendererEngine.renderTable(); }, init: async function() { if (global.FritreeContactsLayout) { global.FritreeContactsLayout.buildLayout(); } await this.load(); ContactsRendererEngine.bindFilterEvents(); if (global.FritreeContactsEditor && typeof global.FritreeContactsEditor.bindEvents === 'function') { global.FritreeContactsEditor.bindEvents(); } if (global.FritreeContactsBulkActions && typeof global.FritreeContactsBulkActions.bindEvents === 'function') { global.FritreeContactsBulkActions.bindEvents(); } if (global.FritreeContactsResolver && typeof global.FritreeContactsResolver.bindGlobalAutoResolution === 'function') { global.FritreeContactsResolver.bindGlobalAutoResolution(); } return true; } }; global.FritreeContactsRenderer = ContactsRendererEngine; if (typeof document !== 'undefined') { if (document.readyState === 'complete' || document.readyState === 'interactive') { global.FritreeContacts.init(); } else { document.addEventListener('DOMContentLoaded', () => global.FritreeContacts.init()); } } })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);