/**
* @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}
${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);