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