fc / contacts-bulk-controller.js
Althnayi's picture
Upload 56 files
cca0cf3 verified
Raw
History Blame Contribute Delete
32.5 kB
/**
* @fileoverview Contacts Bulk Operations, Range Selector & File Import/Export Controller
* @module controllers/contacts-bulk-controller
* @description وحدة التحكم في العمليات الجماعية للعملاء، التحديد بالنطاق الرقمي، واستيراد وتصدير الملفات.
*/
(global => {
'use strict';
/**
* تنظيف وتنسيق رقم الهاتف
* @param {string} phone
* @returns {string}
*/
function normalizePhone(phone) {
if (global.FritreeContactsResolver && typeof global.FritreeContactsResolver.normalize === 'function') {
return global.FritreeContactsResolver.normalize(phone);
}
return phone ? String(phone).replace(/[^0-9+]/g, '') : '';
}
/**
* تنزيل محتوى كملف في المتصفح
* @param {string} content
* @param {string} type
* @param {string} filename
*/
function downloadBlob(content, type, filename) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
/**
* وحدة العمليات الجماعية واستيراد/تصدير جهات الاتصال
*/
const BulkActionsEngine = {
/**
* تحليل وقراءة ملفات vCard (.vcf)
* @param {string} vcardText
* @returns {Array<Object>}
*/
parseVCard: function(vcardText) {
const parsedContacts = [];
if (!vcardText) return parsedContacts;
const vcardBlocks = vcardText.split(/END:VCARD/i);
vcardBlocks.forEach(block => {
if (!block.includes('BEGIN:VCARD')) return;
let fullName = '';
let phones = [];
let email = '';
let company = '';
let jobTitle = '';
let note = '';
const lines = block.split(/\r?\n/);
lines.forEach(line => {
const trimmed = line.trim();
if (!trimmed) return;
if (trimmed.toUpperCase().startsWith('FN:') || trimmed.toUpperCase().startsWith('FN;')) {
fullName = trimmed.substring(trimmed.indexOf(':') + 1).trim();
} else if (!fullName && (trimmed.toUpperCase().startsWith('N:') || trimmed.toUpperCase().startsWith('N;'))) {
const rawN = trimmed.substring(trimmed.indexOf(':') + 1).trim();
const nParts = rawN.split(';');
fullName = ((nParts[1] ? nParts[1] + ' ' : '') + (nParts[0] || '')).trim();
} else if (trimmed.toUpperCase().startsWith('TEL') || trimmed.toUpperCase().includes('TEL;')) {
const phoneVal = trimmed.substring(trimmed.indexOf(':') + 1).trim();
const norm = normalizePhone(phoneVal);
if (norm && norm.length >= 7) {
phones.push({ phone: norm, type: 'mobile' });
}
} else if (trimmed.toUpperCase().startsWith('EMAIL')) {
email = trimmed.substring(trimmed.indexOf(':') + 1).trim();
} else if (trimmed.toUpperCase().startsWith('ORG:')) {
company = trimmed.substring(trimmed.indexOf(':') + 1).replace(/;/g, ' ').trim();
} else if (trimmed.toUpperCase().startsWith('TITLE:')) {
jobTitle = trimmed.substring(trimmed.indexOf(':') + 1).trim();
} else if (trimmed.toUpperCase().startsWith('NOTE:')) {
note = trimmed.substring(trimmed.indexOf(':') + 1).trim();
}
});
if (phones.length > 0) {
parsedContacts.push({
phone: phones[0].phone,
altPhones: phones.slice(1),
name: fullName || 'جهة اتصال vCard',
email: email,
company: company,
jobTitle: jobTitle,
lifecycle: 'lead',
priority: 'medium',
source: 'import',
notes: note,
tags: ['vCard_Import']
});
}
});
return parsedContacts;
},
/**
* تحليل وقراءة ملفات Google Contacts CSV القياسية
* @param {string} csvText
* @returns {Array<Object>}
*/
parseGoogleCSV: function(csvText) {
const parsedContacts = [];
if (!csvText) return parsedContacts;
const lines = csvText.split(/\r?\n/);
if (lines.length < 2) return parsedContacts;
const parseCSVLine = (textLine) => {
const arr = [];
let quote = false;
let col = '';
for (let c = 0; c < textLine.length; c++) {
const cc = textLine[c];
if (cc === '"') {
quote = !quote;
} else if (cc === ',' && !quote) {
arr.push(col.trim().replace(/^"|"$/g, ''));
col = '';
} else {
col += cc;
}
}
arr.push(col.trim().replace(/^"|"$/g, ''));
return arr;
};
const headers = parseCSVLine(lines[0]).map(h => h.toLowerCase());
const nameIdx = headers.findIndex(h => h.includes('name') && !h.includes('given') && !h.includes('family'));
const givenNameIdx = headers.findIndex(h => h.includes('given name'));
const familyNameIdx = headers.findIndex(h => h.includes('family name'));
const phoneIndices = [];
headers.forEach((h, i) => {
if (h.includes('phone') || h.includes('mobile') || h.includes('cellular') || h === 'value') {
phoneIndices.push(i);
}
});
const emailIdx = headers.findIndex(h => h.includes('email') || h.includes('e-mail'));
const orgIdx = headers.findIndex(h => h.includes('organization') || h.includes('company'));
const titleIdx = headers.findIndex(h => h.includes('title'));
const groupIdx = headers.findIndex(h => h.includes('group') || h.includes('membership'));
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const row = parseCSVLine(lines[i]);
let contactName = '';
if (nameIdx !== -1 && row[nameIdx]) {
contactName = row[nameIdx];
} else {
const gName = givenNameIdx !== -1 ? row[givenNameIdx] || '' : '';
const fName = familyNameIdx !== -1 ? row[familyNameIdx] || '' : '';
contactName = (gName + ' ' + fName).trim();
}
const emailVal = emailIdx !== -1 ? row[emailIdx] || '' : '';
const companyVal = orgIdx !== -1 ? row[orgIdx] || '' : '';
const jobTitleVal = titleIdx !== -1 ? row[titleIdx] || '' : '';
const rawGroups = groupIdx !== -1 ? row[groupIdx] || '' : '';
const tagsList = ['Google_CSV'];
if (rawGroups) {
rawGroups.split(':::').forEach(g => {
const cleanG = g.replace('* myContacts', '').replace('* My Contacts', '').trim();
if (cleanG) tagsList.push(cleanG);
});
}
const extractedPhones = [];
phoneIndices.forEach(pIdx => {
const rawPhone = row[pIdx];
if (rawPhone) {
const normPhone = normalizePhone(rawPhone);
if (normPhone && normPhone.length >= 7 && !extractedPhones.includes(normPhone)) {
extractedPhones.push(normPhone);
}
}
});
if (extractedPhones.length > 0) {
parsedContacts.push({
phone: extractedPhones[0],
altPhones: extractedPhones.slice(1).map(p => ({ phone: p, type: 'work' })),
name: contactName || 'عميل جوجل',
email: emailVal,
company: companyVal,
jobTitle: jobTitleVal,
lifecycle: 'lead',
priority: 'medium',
source: 'import',
notes: 'تم الاستيراد من Google Contacts',
tags: Array.from(new Set(tagsList))
});
}
}
return parsedContacts;
},
/**
* معالجة استيراد الملفات (vCard / CSV / TXT)
* @param {File} file
*/
processImportFile: async function(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const content = e.target.result;
let imported = [];
if (file.name.endsWith('.vcf') || content.includes('BEGIN:VCARD')) {
imported = this.parseVCard(content);
} else if (file.name.endsWith('.csv') && (content.includes('Given Name') || content.includes('Group Membership') || content.includes('Phone 1'))) {
imported = this.parseGoogleCSV(content);
} else {
const lines = content.split(/\r?\n/);
lines.forEach(line => {
if (!line.trim()) return;
const parts = line.split(',');
const rawPhone = parts[0] ? parts[0].trim().replace(/[^0-9+]/g, '') : '';
const rawName = parts[1] ? parts[1].trim() : 'المستلم';
if (rawPhone.length >= 7) {
imported.push({
phone: normalizePhone(rawPhone),
name: rawName,
tags: ['CSV_Import']
});
}
});
}
if (imported.length > 0) {
const core = global.FritreeContacts;
if (!core || !core.state) return;
const state = core.state;
let addCount = 0;
imported.forEach(imp => {
const normP = normalizePhone(imp.phone);
const exists = state.contacts.some(c => normalizePhone(c.phone) === normP);
if (!exists && normP) {
state.contacts.unshift({
id: 'cnt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4),
name: imp.name || 'المستلم',
phone: normP,
altPhones: imp.altPhones || [],
email: imp.email || '',
company: imp.company || '',
jobTitle: imp.jobTitle || '',
lifecycle: imp.lifecycle || 'lead',
priority: 'medium',
source: 'import',
tags: imp.tags || ['Imported'],
notes: imp.notes || '',
blacklisted: false,
createdAt: new Date().toISOString()
});
addCount++;
}
});
await core.save();
core.refreshUI();
const modalImport = document.getElementById('gpifk7jnfx');
if (modalImport) modalImport.style.display = 'none';
alert(`تم استيراد [${addCount.toLocaleString('en-US')}] جهة اتصال جديدة بنجاح!`);
} else {
alert('ملقتش أرقام موبايل صالحة جوه الملف ده.');
}
};
reader.readAsText(file);
},
/**
* تصدير جهات الاتصال بصيغ متعددة
* @param {string} format ('vcf' | 'google_csv' | 'csv' | 'json')
* @param {Array<Object>} contactsList
*/
exportToFile: function(format = 'vcf', contactsList = []) {
if (!Array.isArray(contactsList) || contactsList.length === 0) {
alert('لا توجد جهات اتصال متاحة للتصدير.');
return;
}
const filename = `جهات_اتصال_فريتري_${new Date().toISOString().slice(0, 10)}`;
if (format === 'vcf') {
let vcfText = '';
contactsList.forEach(c => {
vcfText += 'BEGIN:VCARD\r\nVERSION:3.0\r\n';
vcfText += `FN:${c.name || 'عميل'}\r\n`;
vcfText += `TEL;TYPE=CELL:${c.phone}\r\n`;
if (c.altPhones && Array.isArray(c.altPhones)) {
c.altPhones.forEach(ap => {
const pVal = typeof ap === 'object' ? ap.phone : ap;
vcfText += `TEL;TYPE=WORK:${pVal}\r\n`;
});
}
if (c.email) vcfText += `EMAIL:${c.email}\r\n`;
if (c.company) vcfText += `ORG:${c.company}\r\n`;
if (c.jobTitle) vcfText += `TITLE:${c.jobTitle}\r\n`;
if (c.notes) vcfText += `NOTE:${c.notes}\r\n`;
vcfText += 'END:VCARD\r\n';
});
downloadBlob(vcfText, 'text/vcard;charset=utf-8;', `${filename}.vcf`);
} else if (format === 'google_csv') {
let csv = '\ufeffName,Given Name,Family Name,Group Membership,Phone 1 - Type,Phone 1 - Value,E-mail 1 - Value,Organization 1 - Name,Organization 1 - Title,Notes\n';
contactsList.forEach(c => {
const groupStr = (c.tags || []).join(' ::: ');
csv += `"${c.name || ''}","${c.name || ''}","","${groupStr}","Mobile","${c.phone || ''}","${c.email || ''}","${c.company || ''}","${c.jobTitle || ''}","${(c.notes || '').replace(/"/g, '""')}"\n`;
});
downloadBlob(csv, 'text/csv;charset=utf-8;', `${filename}_Google.csv`);
} else if (format === 'csv') {
let csv = '\ufeffالاسم,رقم الهاتف,الشركة,المسمى الوظيفي,مرحلة المبيعات,البريد الإلكتروني,الوسوم,الملاحظات\n';
contactsList.forEach(c => {
csv += `"${c.name || ''}","${c.phone || ''}","${c.company || ''}","${c.jobTitle || ''}","${c.lifecycle || 'lead'}","${c.email || ''}","${(c.tags || []).join(', ')}","${(c.notes || '').replace(/"/g, '""')}"\n`;
});
downloadBlob(csv, 'text/csv;charset=utf-8;', `${filename}.csv`);
} else if (format === 'json') {
downloadBlob(JSON.stringify(contactsList, null, 2), 'application/json;charset=utf-8;', `${filename}.json`);
}
},
/**
* دمج وإزالة جهات الاتصال المكررة بناءً على رقم الهاتف
*/
deduplicate: async function() {
const core = global.FritreeContacts;
if (!core || !core.state) return;
const state = core.state;
const map = new Map();
let dedupCount = 0;
state.contacts.forEach(c => {
const norm = normalizePhone(c.phone);
if (map.has(norm)) {
const existing = map.get(norm);
if (c.name && c.name !== 'المستلم' && existing.name === 'المستلم') {
existing.name = c.name;
}
existing.tags = Array.from(new Set([...(existing.tags || []), ...(c.tags || [])]));
if (c.notes && !existing.notes.includes(c.notes)) {
existing.notes = (existing.notes ? existing.notes + ' | ' : '') + c.notes;
}
dedupCount++;
} else {
map.set(norm, { ...c, phone: norm });
}
});
state.contacts = Array.from(map.values());
await core.save();
core.refreshUI();
alert(`تم دمج وإزالة [${dedupCount.toLocaleString('en-US')}] جهة اتصال مكررة بنجاح!`);
},
/**
* مسح كامل جهات الاتصال
*/
clearAll: async function() {
if (confirm('تنبيه: هل أنت متأكد إنك عايز تمسح كل جهات الاتصال؟ الإجراء ده ما ينفعش تتراجع عنه!')) {
const core = global.FritreeContacts;
if (!core || !core.state) return;
core.state.contacts = [];
core.state.selectedContactIds.clear();
await core.save();
core.refreshUI();
}
},
/**
* تطبيق التحديد بالنطاق الرقمي (Range Selector Engine)
*/
applyRangeSelection: function() {
const core = global.FritreeContacts;
if (!core || !core.state) return;
const state = core.state;
const fromInput = document.getElementById('rv2zopf0tb');
const toInput = document.getElementById('wll7mokn3p');
const infoText = document.getElementById('wsbcxjitau');
const clearBtn = document.getElementById('b7ab0plbsx');
const displayedList = state.currentlyDisplayedContacts || state.contacts || [];
const totalDisplayed = displayedList.length;
if (totalDisplayed === 0) {
alert('مفيش جهات اتصال معروضة عشان تحدد نطاق منها.');
return;
}
let fromNum = parseInt(fromInput?.value, 10);
let toNum = parseInt(toInput?.value, 10);
if (isNaN(fromNum) || isNaN(toNum)) {
alert('اكتب رقم البداية ورقم النهاية عشان تحدد النطاق (مثال: من 51 إلى 78).');
return;
}
if (fromNum < 1) fromNum = 1;
if (toNum > totalDisplayed) toNum = totalDisplayed;
if (fromNum > toNum) {
alert(`رقم البداية (${fromNum}) لازم يكون أصغر من أو بيساوي رقم النهاية (${toNum}).`);
return;
}
state.selectedContactIds.clear();
let selectedCount = 0;
for (let idx = fromNum - 1; idx < toNum; idx++) {
if (displayedList[idx]) {
state.selectedContactIds.add(displayedList[idx].id);
selectedCount++;
}
}
core.refreshUI();
if (clearBtn) clearBtn.style.display = 'inline-flex';
if (infoText) {
infoText.textContent = `تم تحديد ${selectedCount.toLocaleString('en-US')} جهة اتصال (من رقم #${fromNum} إلى #${toNum})`;
}
if (typeof window.addLog === 'function') {
window.addLog(`تحديد النطاق: تم تحديد [${selectedCount.toLocaleString('en-US')}] جهة اتصال من رقم #${fromNum} إلى #${toNum}.`, 'success');
}
},
/**
* إلغاء التحديد بالنطاق الرقمي
*/
clearRangeSelection: function() {
const core = global.FritreeContacts;
if (!core || !core.state) return;
const state = core.state;
const fromInput = document.getElementById('rv2zopf0tb');
const toInput = document.getElementById('wll7mokn3p');
const infoText = document.getElementById('wsbcxjitau');
const clearBtn = document.getElementById('b7ab0plbsx');
if (fromInput) fromInput.value = '';
if (toInput) toInput.value = '';
if (clearBtn) clearBtn.style.display = 'none';
state.selectedContactIds.clear();
core.refreshUI();
const totalDisplayed = (state.currentlyDisplayedContacts || state.contacts || []).length;
if (infoText) {
infoText.textContent = `إجمالي المعروض دلوقتي: ${totalDisplayed.toLocaleString('en-US')} جهة اتصال (من 1 إلى ${totalDisplayed.toLocaleString('en-US')})`;
}
},
/**
* ربط أحداث العمليات الجماعية
*/
bindEvents: function() {
const core = global.FritreeContacts;
if (!core || !core.state) return;
const state = core.state;
const btnImportModal = document.getElementById('e2rtgyindd');
const btnCloseImport = document.getElementById('nx0cpoqxzw');
const btnExportModal = document.getElementById('nxybir7u8r');
const btnCloseExport = document.getElementById('has3vfwumz');
const btnExecExport = document.getElementById('nah2wji7pd');
const btnDedup = document.getElementById('cdykxxbjv2');
const btnClearAll = document.getElementById('k7qyxs7zru');
const btnBulkDelete = document.getElementById('xt7egwiac3');
const btnBulkTag = document.getElementById('bfywn3ymkh');
const btnBulkUntag = document.getElementById('vfn8iv8lzf');
const btnBulkBlacklist = document.getElementById('jik3jfr08m');
const btnBulkUnblacklist = document.getElementById('amfm0v8cs5');
const btnBulkExport = document.getElementById('ygdgkhes7r');
const btnBulkSendWA = document.getElementById('ybvs0y4u4q');
const btnApplyRange = document.getElementById('zc5zjdewes');
const btnClearRange = document.getElementById('b7ab0plbsx');
const rangeFromInput = document.getElementById('rv2zopf0tb');
const rangeToInput = document.getElementById('wll7mokn3p');
if (btnApplyRange) btnApplyRange.onclick = () => this.applyRangeSelection();
if (btnClearRange) btnClearRange.onclick = () => this.clearRangeSelection();
if (rangeFromInput && rangeToInput) {
const handleEnter = (e) => { if (e.key === 'Enter') this.applyRangeSelection(); };
rangeFromInput.onkeydown = handleEnter;
rangeToInput.onkeydown = handleEnter;
}
if (btnImportModal) btnImportModal.onclick = () => document.getElementById('gpifk7jnfx').style.display = 'flex';
if (btnCloseImport) btnCloseImport.onclick = () => document.getElementById('gpifk7jnfx').style.display = 'none';
if (btnExportModal) btnExportModal.onclick = () => document.getElementById('rbqob9x718').style.display = 'flex';
if (btnCloseExport) btnCloseExport.onclick = () => document.getElementById('rbqob9x718').style.display = 'none';
if (btnExecExport) {
btnExecExport.onclick = () => {
const fmt = document.getElementById('nealj97cx2').value;
this.exportToFile(fmt, state.contacts);
document.getElementById('rbqob9x718').style.display = 'none';
};
}
const dropzone = document.getElementById('h525b2cyd0');
const fileinput = document.getElementById('fi0jafn7lc');
if (dropzone && fileinput) {
dropzone.onclick = () => fileinput.click();
fileinput.onchange = (e) => {
if (e.target.files.length > 0) this.processImportFile(e.target.files[0]);
};
dropzone.ondragover = (e) => { e.preventDefault(); dropzone.style.borderColor = '#1877f2'; };
dropzone.ondragleave = () => { dropzone.style.borderColor = '#00a884'; };
dropzone.ondrop = (e) => {
e.preventDefault();
dropzone.style.borderColor = '#00a884';
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
this.processImportFile(e.dataTransfer.files[0]);
}
};
}
if (btnDedup) btnDedup.onclick = () => this.deduplicate();
if (btnClearAll) btnClearAll.onclick = () => this.clearAll();
if (btnBulkDelete) {
btnBulkDelete.onclick = async () => {
if (state.selectedContactIds.size === 0) {
alert('حدد جهة اتصال واحدة على الأقل الأول!');
return;
}
if (confirm(`هل أنت متأكد إنك عايز تحذف [${state.selectedContactIds.size}] جهة اتصال نهائياً؟`)) {
state.contacts = state.contacts.filter(c => !state.selectedContactIds.has(c.id));
state.selectedContactIds.clear();
await core.save();
core.refreshUI();
}
};
}
if (btnBulkTag) {
btnBulkTag.onclick = async () => {
if (state.selectedContactIds.size === 0) {
alert('حدد جهة اتصال واحدة على الأقل الأول!');
return;
}
const newTag = prompt("اكتب اسم الوسم الجديد اللي عايز تطبقه على المحدد كلو:");
if (newTag && newTag.trim()) {
const tagVal = newTag.trim();
state.contacts.forEach(c => {
if (state.selectedContactIds.has(c.id)) {
c.tags = Array.from(new Set([...(c.tags || []), tagVal]));
}
});
await core.save();
core.refreshUI();
alert(`تم إضافة الوسم "${tagVal}" لجهات الاتصال المحددة بنجاح.`);
}
};
}
if (btnBulkUntag) {
btnBulkUntag.onclick = async () => {
if (state.selectedContactIds.size === 0) {
alert('حدد جهة اتصال واحدة على الأقل الأول!');
return;
}
const selectedContacts = state.contacts.filter(c => state.selectedContactIds.has(c.id));
const tagsOnSelected = new Set();
selectedContacts.forEach(c => (c.tags || []).forEach(t => tagsOnSelected.add(t)));
if (tagsOnSelected.size === 0) {
alert('جهات الاتصال المحددة ما عليهاش أي وسوم دلوقتي عشان تتشال.');
return;
}
const tagListStr = Array.from(tagsOnSelected).join(', ');
const tagToRemove = prompt(`اكتب اسم الوسم اللي عايز تشيله من العناصر المحددة:\nالوسوم المتاحة: [ ${tagListStr} ]`);
if (tagToRemove && tagToRemove.trim()) {
const cleanTag = tagToRemove.trim();
let countRemoved = 0;
state.contacts.forEach(c => {
if (state.selectedContactIds.has(c.id) && c.tags && c.tags.includes(cleanTag)) {
c.tags = c.tags.filter(t => t !== cleanTag);
countRemoved++;
}
});
await core.save();
core.refreshUI();
alert(`تم إزالة الوسم "${cleanTag}" من [${countRemoved}] جهة اتصال بنجاح.`);
}
};
}
if (btnBulkBlacklist) {
btnBulkBlacklist.onclick = async () => {
if (state.selectedContactIds.size === 0) return;
state.contacts.forEach(c => {
if (state.selectedContactIds.has(c.id)) c.blacklisted = true;
});
await core.save();
core.refreshUI();
};
}
if (btnBulkUnblacklist) {
btnBulkUnblacklist.onclick = async () => {
if (state.selectedContactIds.size === 0) return;
state.contacts.forEach(c => {
if (state.selectedContactIds.has(c.id)) c.blacklisted = false;
});
await core.save();
core.refreshUI();
};
}
if (btnBulkExport) {
btnBulkExport.onclick = () => {
if (state.selectedContactIds.size === 0) {
alert('اختار جهات اتصال الأول عشان تصدرها!');
return;
}
const selectedList = state.contacts.filter(c => state.selectedContactIds.has(c.id));
this.exportToFile('vcf', selectedList);
};
}
if (btnBulkSendWA) {
btnBulkSendWA.onclick = () => {
if (state.selectedContactIds.size === 0) {
alert('حدد جهات اتصال الأول عشان تبدأ الحملة!');
return;
}
const selectedList = state.contacts.filter(c => state.selectedContactIds.has(c.id) && !c.blacklisted);
if (global.FritreeWhatsApp && typeof global.FritreeWhatsApp.setRecipients === 'function') {
global.FritreeWhatsApp.setRecipients(selectedList.map(c => ({ phone: c.phone, name: c.name })));
window.location.hash = 'sf8pufcpyl';
}
};
}
}
};
/**
* تصدير وحدة العمليات الجماعية
*/
global.FritreeContactsBulkActions = BulkActionsEngine;
global.FritreeContactsParsers = BulkActionsEngine;
})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);