(function() { 'use strict'; // ============================================================ // المتغيرات الأساسية // ============================================================ let currentLang = 'ar'; const tableBody = document.getElementById('tableBody'); const totalAmount = document.getElementById('totalAmount'); const lettersValue = document.getElementById('lettersValue'); const emptyRow = document.getElementById('emptyRow'); const addBtn = document.getElementById('addBtn'); const resetBtn = document.getElementById('resetBtn'); const pdfBtn = document.getElementById('pdfBtn'); const imgBtn = document.getElementById('imgBtn'); const langBtn = document.getElementById('langBtn'); const langText = document.getElementById('langText'); const toast = document.getElementById('toast'); const toastText = document.getElementById('toastText'); // ============================================================ // الفئات // ============================================================ const categories = [ { value: 'البناء', icon: 'fa-hard-hat', ar: 'البناء', fr: 'Construction' }, { value: 'الكهرباء', icon: 'fa-bolt', ar: 'الكهرباء', fr: 'Électricité' }, { value: 'السباكة', icon: 'fa-wrench', ar: 'السباكة', fr: 'Plomberie' }, { value: 'الجبس', icon: 'fa-paint-roller', ar: 'الجبس', fr: 'Plâtre' }, { value: 'الصباغة', icon: 'fa-palette', ar: 'الصباغة', fr: 'Peinture' }, { value: 'النجارة', icon: 'fa-hammer', ar: 'النجارة', fr: 'Menuiserie' }, { value: 'أخرى', icon: 'fa-tools', ar: 'أخرى', fr: 'Autre' } ]; // ============================================================ // دوال مساعدة // ============================================================ function formatAmount(v) { return Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function parseAmount(v) { if (!v || v.trim() === '') return NaN; return parseFloat(v.replace(/\s/g, '').replace(/,/g, '.').replace(/[^0-9.]/g, '')); } function getIcon(cat) { const found = categories.find(c => c.value === cat); return found ? found.icon : 'fa-tools'; } function getCatLabel(cat) { const found = categories.find(c => c.value === cat); return found ? (currentLang === 'ar' ? found.ar : found.fr) : cat; } // ============================================================ // تحويل الأرقام إلى حروف (عربية) // ============================================================ function numberToWordsAr(n) { if (n === 0) return 'صفر'; const units = ['', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية', 'تسعة']; const tens = ['', 'عشرة', 'عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون']; const hundreds = ['', 'مائة', 'مائتان', 'ثلاثمائة', 'أربعمائة', 'خمسمائة', 'ستمائة', 'سبعمائة', 'ثمانمائة', 'تسعمائة']; function u(n) { return units[n] || ''; } function t(n) { if (n < 10) return u(n); if (n < 20) { const map = ['', '', '', '', '', '', '', '', '', '', 'عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر', 'ستة عشر', 'سبعة عشر', 'ثمانية عشر', 'تسعة عشر' ]; return map[n]; } const unit = n % 10, ten = Math.floor(n / 10); return unit === 0 ? tens[ten] : u(unit) + ' و' + tens[ten]; } function h(n) { if (n === 0) return ''; if (n < 100) return t(n); const hund = Math.floor(n / 100), rem = n % 100; let result = hundreds[hund]; if (rem > 0) result += ' و' + t(rem); return result; } function th(n) { if (n === 0) return ''; if (n === 1) return 'ألف'; if (n === 2) return 'ألفان'; if (n >= 3 && n <= 10) return u(n) + ' آلاف'; return h(n) + ' ألف'; } const millions = Math.floor(n / 1000000); const thousands = Math.floor((n % 1000000) / 1000); const remainder = n % 1000; let result = ''; if (millions > 0) { if (millions === 1) result += 'مليون'; else if (millions === 2) result += 'مليونان'; else if (millions >= 3 && millions <= 10) result += u(millions) + ' ملايين'; else result += h(millions) + ' مليون'; if (thousands > 0 || remainder > 0) result += ' و'; } if (thousands > 0) { result += th(thousands); if (remainder > 0) result += ' و'; } if (remainder > 0) result += h(remainder); return result.trim(); } function formatCurrencyAr(amount) { const d = Math.floor(amount); const c = Math.round((amount - d) * 100); let result = numberToWordsAr(d) + ' درهم'; if (c > 0) result += ' و ' + numberToWordsAr(c) + ' سنتيم'; return result; } // ============================================================ // تحويل الأرقام إلى حروف (فرنسية - مبسطة) // ============================================================ function numberToWordsFr(n) { if (n === 0) return 'zéro'; const u = ['', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf']; const t = ['', 'dix', 'vingt', 'trente', 'quarante', 'cinquante', 'soixante', 'soixante-dix', 'quatre-vingt', 'quatre-vingt-dix' ]; if (n < 10) return u[n]; if (n < 20) { const map = ['', '', '', '', '', '', '', '', '', '', 'dix', 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize', 'dix-sept', 'dix-huit', 'dix-neuf' ]; return map[n]; } if (n < 100) { const ten = Math.floor(n / 10), unit = n % 10; if (unit === 0) return t[ten]; return t[ten] + '-' + u[unit]; } if (n < 1000) { const h = Math.floor(n / 100), r = n % 100; let result = h === 1 ? 'cent' : u[h] + ' cent'; if (r > 0) result += ' ' + numberToWordsFr(r); return result; } if (n < 1000000) { const th = Math.floor(n / 1000), r = n % 1000; let result = th === 1 ? 'mille' : numberToWordsFr(th) + ' mille'; if (r > 0) result += ' ' + numberToWordsFr(r); return result; } return n.toString(); } function formatCurrencyFr(amount) { const d = Math.floor(amount); const c = Math.round((amount - d) * 100); let result = numberToWordsFr(d) + ' dirhams'; if (c > 0) result += ' et ' + numberToWordsFr(c) + ' centimes'; return result; } // ============================================================ // حساب المجموع // ============================================================ function calculateTotal() { let total = 0; document.querySelectorAll('.amount-input').forEach(inp => { const val = parseAmount(inp.value); if (!isNaN(val) && val > 0) total += val; }); totalAmount.textContent = formatAmount(total); if (total > 0) { lettersValue.textContent = currentLang === 'ar' ? formatCurrencyAr(total) : formatCurrencyFr(total); } else { lettersValue.textContent = currentLang === 'ar' ? 'صفر درهم' : 'zéro dirhams'; } return total; } // ============================================================ // إعادة ترقيم الصفوف // ============================================================ function renumberRows() { const rows = document.querySelectorAll('#tableBody tr:not(#emptyRow)'); rows.forEach((row, i) => { const circle = row.querySelector('.num-circle'); if (circle) circle.textContent = i + 1; }); emptyRow.style.display = rows.length === 0 ? '' : 'none'; } // ============================================================ // إنشاء خيارات الفئات // ============================================================ function createCatOptions(selected) { let html = ''; categories.forEach(cat => { const sel = cat.value === selected ? 'selected' : ''; const label = currentLang === 'ar' ? cat.ar : cat.fr; html += ``; }); return html; } // ============================================================ // إنشاء عنصر بيان // ============================================================ function createDescItem(value) { const div = document.createElement('div'); div.className = 'desc-item'; const dot = document.createElement('span'); dot.className = 'dot'; dot.textContent = '•'; div.appendChild(dot); const input = document.createElement('input'); input.type = 'text'; input.placeholder = currentLang === 'ar' ? 'أدخل البيان...' : 'Description...'; input.value = value || ''; div.appendChild(input); const remove = document.createElement('button'); remove.className = 'remove'; remove.innerHTML = ''; remove.addEventListener('click', function() { const container = div.parentElement; if (container.children.length > 1) { div.remove(); } else { alert(currentLang === 'ar' ? 'لا يمكن حذف البيان الأخير' : 'Impossible de supprimer'); } }); div.appendChild(remove); return div; } // ============================================================ // إنشاء صف جديد // ============================================================ function createRow(catVal, descValues, amountVal) { const tr = document.createElement('tr'); // رقم const tdNum = document.createElement('td'); tdNum.className = 'col-num'; const circle = document.createElement('span'); circle.className = 'num-circle'; circle.textContent = '1'; tdNum.appendChild(circle); tr.appendChild(tdNum); // صنف const tdCat = document.createElement('td'); tdCat.className = 'col-cat'; const badge = document.createElement('div'); badge.className = 'cat-badge'; const icon = document.createElement('i'); const initIcon = getIcon(catVal || 'أخرى'); icon.className = 'fas ' + initIcon; const select = document.createElement('select'); select.innerHTML = createCatOptions(catVal || 'أخرى'); select.className = 'cat-select'; select.addEventListener('change', function() { icon.className = 'fas ' + getIcon(this.value); }); badge.appendChild(icon); badge.appendChild(select); tdCat.appendChild(badge); tr.appendChild(tdCat); // بيان const tdDesc = document.createElement('td'); tdDesc.className = 'col-desc'; const container = document.createElement('div'); container.className = 'desc-list'; if (descValues && descValues.length > 0 && descValues[0] !== '') { descValues.forEach(v => { if (v.trim() !== '') container.appendChild(createDescItem(v)); }); } if (container.children.length === 0) container.appendChild(createDescItem('')); const addDesc = document.createElement('button'); addDesc.className = 'add-desc'; addDesc.innerHTML = ' ' + (currentLang === 'ar' ? 'إضافة' : 'Ajouter'); addDesc.addEventListener('click', function() { const newItem = createDescItem(''); container.appendChild(newItem); const inp = newItem.querySelector('input'); if (inp) setTimeout(() => inp.focus(), 100); }); tdDesc.appendChild(container); tdDesc.appendChild(addDesc); tr.appendChild(tdDesc); // مبلغ const tdAmt = document.createElement('td'); tdAmt.className = 'col-amount'; const input = document.createElement('input'); input.type = 'text'; input.className = 'amount-input'; input.placeholder = '0,00'; input.inputMode = 'decimal'; if (amountVal && amountVal.trim() !== '') { const parsed = parseAmount(amountVal); if (!isNaN(parsed) && parsed > 0) input.value = formatAmount(parsed); else input.value = amountVal; } input.addEventListener('focus', function() { const val = parseAmount(this.value); if (!isNaN(val) && val > 0) this.value = val.toString(); else this.value = ''; }); input.addEventListener('input', function() { let raw = this.value.replace(/[^0-9.]/g, ''); const parts = raw.split('.'); if (parts.length > 2) raw = parts[0] + '.' + parts.slice(1).join(''); if (parts.length === 2 && parts[1].length > 2) raw = parts[0] + '.' + parts[1].slice(0, 2); this.value = raw; calculateTotal(); }); input.addEventListener('blur', function() { const val = parseAmount(this.value); if (!isNaN(val) && val > 0) this.value = formatAmount(val); else this.value = ''; calculateTotal(); }); tdAmt.appendChild(input); tr.appendChild(tdAmt); // عمليات const tdAct = document.createElement('td'); tdAct.className = 'col-act'; const del = document.createElement('button'); del.innerHTML = ''; del.addEventListener('click', function() { const rows = document.querySelectorAll('#tableBody tr:not(#emptyRow)'); if (rows.length > 1) { tr.remove(); renumberRows(); calculateTotal(); } else { alert(currentLang === 'ar' ? 'لا يمكن حذف الصف الأخير' : 'Impossible de supprimer'); } }); tdAct.appendChild(del); tr.appendChild(tdAct); return tr; } // ============================================================ // إضافة صف // ============================================================ function addRow() { if (emptyRow.style.display !== 'none') emptyRow.style.display = 'none'; const tr = createRow('', [''], ''); tableBody.appendChild(tr); renumberRows(); calculateTotal(); const firstInput = tr.querySelector('.desc-item input'); if (firstInput) setTimeout(() => firstInput.focus(), 100); } // ============================================================ // مسح الكل // ============================================================ function resetAll() { if (confirm(currentLang === 'ar' ? 'حذف جميع الأصناف؟' : 'Supprimer tous les articles ?')) { document.querySelectorAll('#tableBody tr:not(#emptyRow)').forEach(row => row.remove()); emptyRow.style.display = ''; calculateTotal(); renumberRows(); } } // ============================================================ // تبديل اللغة // ============================================================ function switchLanguage(lang) { currentLang = lang; document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr'; document.documentElement.lang = lang === 'ar' ? 'ar' : 'fr'; document.body.dir = lang === 'ar' ? 'rtl' : 'ltr'; langText.textContent = lang === 'ar' ? '🇫🇷 FR' : '🇸🇦 AR'; document.querySelectorAll('[data-ar][data-fr]').forEach(el => { el.textContent = lang === 'ar' ? el.getAttribute('data-ar') : el.getAttribute('data-fr'); }); document.querySelectorAll('.cat-select').forEach(sel => { const val = sel.value; sel.innerHTML = createCatOptions(val); }); document.querySelectorAll('.add-desc').forEach(btn => { const icon = btn.querySelector('i'); btn.innerHTML = ''; if (icon) btn.appendChild(icon); btn.appendChild(document.createTextNode(' ' + (lang === 'ar' ? 'إضافة' : 'Ajouter'))); }); document.querySelectorAll('.desc-item input').forEach(inp => { inp.placeholder = lang === 'ar' ? 'أدخل البيان...' : 'Description...'; }); const emptyMsg = emptyRow.querySelector('.empty-msg'); if (emptyMsg) { const d1 = emptyMsg.querySelector('div:first-of-type'); const d2 = emptyMsg.querySelector('.sub'); if (d1) d1.textContent = lang === 'ar' ? 'لا توجد أصناف بعد' : 'Aucun article'; if (d2) d2.textContent = lang === 'ar' ? 'انقر على "إضافة صنف" للبدء' : 'Cliquez sur "Ajouter"'; } toastText.textContent = lang === 'ar' ? 'جاري التحميل...' : 'Chargement...'; calculateTotal(); } // ============================================================ // تنبيه // ============================================================ function showToast(show) { if (show) toast.classList.add('show'); else toast.classList.remove('show'); } // ============================================================ // التقاط الصفحة // ============================================================ async function capturePage() { const el = document.getElementById('app'); const controls = document.querySelector('.btn-row'); const addBtnEl = document.getElementById('addBtn'); const actionBtns = document.querySelectorAll('.col-act button'); const actionCells = document.querySelectorAll('.col-act'); const addDescBtns = document.querySelectorAll('.add-desc'); const removeBtns = document.querySelectorAll('.remove'); const thead = document.querySelector('#mainTable thead tr'); const lastTh = thead ? thead.querySelector('th:last-child') : null; const wrap = document.querySelector('.table-wrap'); if (controls) controls.style.display = 'none'; if (addBtnEl) addBtnEl.style.display = 'none'; actionBtns.forEach(b => b.style.display = 'none'); actionCells.forEach(c => c.style.display = 'none'); addDescBtns.forEach(b => b.style.display = 'none'); removeBtns.forEach(b => b.style.display = 'none'); if (lastTh) lastTh.style.display = 'none'; let prevOverflow = ''; if (wrap) { prevOverflow = wrap.style.overflowX; wrap.style.overflowX = 'visible'; wrap.style.maxWidth = 'none'; wrap.scrollLeft = 0; } window.scrollTo(0, 0); try { const canvas = await html2canvas(el, { scale: 2.5, useCORS: true, allowTaint: true, logging: false, backgroundColor: '#ffffff', width: el.scrollWidth, height: el.scrollHeight, windowWidth: el.scrollWidth, windowHeight: el.scrollHeight, scrollX: 0, scrollY: 0, onclone: function(cloned) { cloned.querySelectorAll('input, select').forEach(el2 => { el2.style.border = 'none'; el2.style.background = 'transparent'; el2.style.boxShadow = 'none'; }); } }); return canvas; } finally { if (wrap) { wrap.style.overflowX = prevOverflow; wrap.style.maxWidth = ''; } if (controls) controls.style.display = ''; if (addBtnEl) addBtnEl.style.display = ''; actionBtns.forEach(b => b.style.display = ''); actionCells.forEach(c => c.style.display = ''); addDescBtns.forEach(b => b.style.display = ''); removeBtns.forEach(b => b.style.display = ''); if (lastTh) lastTh.style.display = ''; } } // ============================================================ // تحميل PDF // ============================================================ async function downloadPDF() { const { jsPDF } = window.jspdf; pdfBtn.disabled = true; showToast(true); try { const canvas = await capturePage(); const pdf = new jsPDF('p', 'mm', 'a4'); const pageW = 210, pageH = 297; const imgW = pageW; const imgH = (canvas.height * imgW) / canvas.width; const data = canvas.toDataURL('image/jpeg', 1.0); let hLeft = imgH, pos = 0; pdf.addImage(data, 'JPEG', 0, pos, imgW, imgH); hLeft -= pageH; while (hLeft > 0) { pos = hLeft - imgH; pdf.addPage(); pdf.addImage(data, 'JPEG', 0, pos, imgW, imgH); hLeft -= pageH; } const fname = currentLang === 'ar' ? 'دوفي_اصلاح_ترميم.pdf' : 'devis_reparation.pdf'; pdf.save(fname); } catch (err) { alert('خطأ: ' + err.message); } pdfBtn.disabled = false; showToast(false); } // ============================================================ // تحميل صورة // ============================================================ async function downloadImage() { imgBtn.disabled = true; showToast(true); try { const canvas = await capturePage(); const link = document.createElement('a'); const fname = currentLang === 'ar' ? 'دوفي_اصلاح_ترميم.png' : 'devis_reparation.png'; link.download = fname; link.href = canvas.toDataURL('image/png'); link.click(); } catch (err) { alert('خطأ: ' + err.message); } imgBtn.disabled = false; showToast(false); } // ============================================================ // الأحداث // ============================================================ addBtn.addEventListener('click', addRow); resetBtn.addEventListener('click', resetAll); pdfBtn.addEventListener('click', downloadPDF); imgBtn.addEventListener('click', downloadImage); langBtn.addEventListener('click', function() { switchLanguage(currentLang === 'ar' ? 'fr' : 'ar'); }); document.addEventListener('keydown', function(e) { if (e.key === 'Enter' && e.target.classList.contains('amount-input')) { e.preventDefault(); addRow(); const rows = document.querySelectorAll('#tableBody tr:not(#emptyRow)'); const last = rows[rows.length - 1]; if (last) { const inp = last.querySelector('.desc-item input'); if (inp) setTimeout(() => inp.focus(), 100); } } }); // ============================================================ // بدء التشغيل // ============================================================ emptyRow.style.display = ''; totalAmount.textContent = '0,00'; calculateTotal(); // Service Worker if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('sw.js').catch(function(err) { console.warn('Service worker failed:', err); }); }); } })();