Download contacts-bulk-controller.js from Althnayi/fc: direct link, hf CLI and curl.
- Browser
- Download file 32.5 kB
-
https://huggingface.co/spaces/Althnayi/fc/resolve/main/contacts-bulk-controller.js
- Command line
-
hf download hf://spaces/Althnayi/fc/contacts-bulk-controller.js
-
curl -L -o contacts-bulk-controller.js https://huggingface.co/spaces/Althnayi/fc/resolve/main/contacts-bulk-controller.js
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); |