(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);
});
});
}
})();