بحث السيارات

زائر
وضع عدم الاتصال — تُعرض بيانات محفوظة على جهازك
// ============================================================ // إعدادات الاتصال بـ Supabase // ============================================================ const SUPABASE_URL = "https://lvikmzphfunvkmpezmin.supabase.co"; const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx2aWttenBoZnVudmttcGV6bWluIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODgxNDk0NzksImV4cCI6MjEwMzcyNTQ3OX0.O5yUZVOP2SzYBAjQ1URUd89AQXpAzGYuIhu9QtzfhPo"; const TABLE_NAME = "cars_key_data"; const { createClient } = supabase; // ============================================================ // تعطيل القفل المشترك بين النوافذ // ============================================================ // مكتبة Supabase تستخدم navigator.locks لتنسيق تجديد التوكن بين // التبويبات. إذا احتُجز القفل في نافذة أخرى (أو تعطّلت نافذة وهي // تحمله) فإن getSession تنتظر بلا نهاية — لا تنجح ولا تفشل، فتبقى // الشاشة معلّقة. نمرّر قفلاً بديلاً ينفّذ مباشرة بلا انتظار. // المقابل: قد يتجدد التوكن مرتين عند فتح نوافذ متعددة، وهو أمر // غير ضار هنا إطلاقاً. const noWaitLock = async (_name, _timeout, fn) => await fn(); // حارس زمني لأي عملية قد تتعلّق function withTimeout(promise, ms, label){ return Promise.race([ promise, new Promise((_, rej) => setTimeout(() => rej(new Error('انتهت المهلة: ' + label)), ms)) ]); } const db = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { auth: { lock: noWaitLock, persistSession: true, autoRefreshToken: true } }); // ============================================================ // إعدادات حاسبة Pin Code للموديلات الحديثة (2007+, SMARTRA-3) // حالياً بدون خدمة فعلية — عبّئها لاحقاً إذا اشتركت بمزود حقيقي // (لا يوجد API عام موثوق مجاني لهذا الجيل، عادة تحتاج دونجل/سوفتوير // مثل UHS Hardware DiagCode أو خدمة ويب مدفوعة موثوقة) // ============================================================ const PIN_API_ENDPOINT = "https://example-pin-service.com/api/v1/hyundai-kia"; const PIN_API_KEY = "YOUR_API_KEY_HERE"; const PROG_LABELS = { manual:"يدوية", obd:"جهاز OBD مطلوب" }; // ============================================================ // بيانات الدعم — عدّلها من هنا فقط // ============================================================ const SUPPORT_WHATSAPP = '966555917073'; // بصيغة دولية بلا + const SUPPORT_EMAIL = 'nasemm56@hotmail.com'; // حالة وصول المستخدم: guest | pending | suspended | verified let ACCESS = 'guest'; const LOCKED = '🔒 للفنيين الموثّقين'; // ============================================================ // تهريب المخرجات — الدفاع الأساسي ضد XSS // كل قيمة قادمة من قاعدة البيانات (أو من ملف CSV مستورد) تمر من هنا // قبل إدراجها في الصفحة. بدونها، خانة ملاحظات تحتوي وسماً مثل // تنفّذ كوداً في متصفح كل فني يفتح البطاقة. // ============================================================ function esc(v){ if(v === null || v === undefined) return ''; return String(v) .replace(/&/g,'&') .replace(//g,'>') .replace(/"/g,'"') .replace(/'/g,'''); } // روابط الصور: نرفض أي مخطط غير http/https (يمنع javascript: و data:) function safeUrl(v){ const u = String(v || '').trim(); return /^https?:\/\//i.test(u) ? u : ''; } let CAR_DATA = []; const selMake = document.getElementById('sel-make'); const selModel = document.getElementById('sel-model'); const selYear = document.getElementById('sel-year'); const selKeyType = document.getElementById('sel-keytype'); const selMarket = document.getElementById('sel-market'); const KEYTYPE_LABELS = { smart:"بصمة", remote:"مفتاح بريموت", blade:"مفتاح عادي" }; const btnSearch = document.getElementById('btn-search'); const statusEl = document.getElementById('status'); const resultsEl = document.getElementById('results'); function uniq(arr){ return [...new Set(arr)]; } // ---------- تعبئة القوائم عبر دوال آمنة (بدون سحب الجدول) ---------- // لم نعد نستخدم select * — الجدول مغلق الآن. كل قائمة تُجلب عند الحاجة. // تسميات حقول البطاقة تتبع اللغة المختارة function fieldLabels(){ return { transponder: t('f_chip'), blade: t('f_blade'), freq: t('f_freq'), prog: t('f_prog'), eeprom: t('f_eeprom'), fcc: t('f_fcc'), oem: t('f_oem'), special: t('f_special'), steps: t('f_steps'), warn: t('f_warn') }; } async function loadData(){ // بلا شبكة: نبني القوائم من النسخة المحفوظة على الجهاز if(isOffline()){ if(fillFromOffline()){ statusEl.textContent = ""; return; } statusEl.textContent = "لا يوجد اتصال ولا بيانات محفوظة على هذا الجهاز."; return; } statusEl.textContent = "جارٍ التحميل..."; const { data, error } = await db.rpc('list_makes'); if(error){ // فشل الشبكة رغم أن المتصفح يظنّ نفسه متصلاً if(fillFromOffline()){ statusEl.textContent = "تعذّر الاتصال — تُعرض بيانات محفوظة على جهازك."; return; } statusEl.textContent = "تعذّر الاتصال بقاعدة البيانات: " + error.message; console.error(error); return; } selMake.innerHTML = ''; (data || []).forEach(r => { const opt = document.createElement('option'); opt.value = r.value; opt.textContent = r.label; // قيمة موحّدة، عنوان كامل selMake.appendChild(opt); }); statusEl.textContent = ""; selMake.disabled = false; } async function populateModels(){ selModel.innerHTML = ''; selYear.innerHTML = ''; selYear.disabled = true; if(!selMake.value){ selModel.disabled = true; updateSearchState(); return; } selModel.disabled = true; if(isOffline() && OFFLINE_CARS){ const gm = new Map(); OFFLINE_CARS.filter(r => arBase(r.make) === selMake.value).forEach(r => { const b = arBase(r.model); if(!b) return; if(!gm.has(b) || r.model.length > gm.get(b).length) gm.set(b, r.model); }); [...gm.entries()].sort((a,b) => a[1].localeCompare(b[1],'ar')).forEach(([val,lab]) => { const o = document.createElement('option'); o.value = val; o.textContent = lab; selModel.appendChild(o); }); selModel.disabled = false; updateSearchState(); return; } const { data, error } = await db.rpc('list_models', { p_make: selMake.value }); if(error){ statusEl.textContent = "تعذّر جلب الموديلات: " + error.message; return; } (data || []).forEach(r => { const opt = document.createElement('option'); opt.value = r.value; opt.textContent = r.label; selModel.appendChild(opt); }); selModel.disabled = false; updateSearchState(); } async function populateYears(){ selYear.innerHTML = ''; if(!selModel.value){ selYear.disabled = true; updateSearchState(); return; } selYear.disabled = true; if(isOffline() && OFFLINE_CARS){ const ys = new Set(); OFFLINE_CARS.filter(r => arBase(r.make) === selMake.value && arBase(r.model) === selModel.value) .forEach(r => { for(let y = r.year_from; y <= r.year_to; y++) ys.add(y); }); [...ys].sort((a,b) => a-b).forEach(y => { const o = document.createElement('option'); o.value = y; o.textContent = y; selYear.appendChild(o); }); selYear.disabled = false; updateSearchState(); return; } const { data, error } = await db.rpc('list_years', { p_make: selMake.value, p_model: selModel.value }); if(error){ statusEl.textContent = "تعذّر جلب السنوات: " + error.message; return; } (data || []).forEach(r => { const opt = document.createElement('option'); opt.value = r.year; opt.textContent = r.year; selYear.appendChild(opt); }); selYear.disabled = false; updateSearchState(); } function updateSearchState(){ btnSearch.disabled = !(selMake.value && selModel.value && selYear.value); } selMake.addEventListener('change', populateModels); selModel.addEventListener('change', populateYears); selYear.addEventListener('change', updateSearchState); selMarket.addEventListener('change', updateSearchState); selKeyType.addEventListener('change', updateSearchState); btnSearch.addEventListener('click', async () => { const make = selMake.value; const model = selModel.value; const year = parseInt(selYear.value, 10); const market = selMarket.value; const keyType = selKeyType.value; // بلا شبكة: نبحث في النسخة المحفوظة if(isOffline()){ const local = searchOffline(make, model, year, market, keyType); if(local === null){ statusEl.textContent = 'لا يوجد اتصال ولا بيانات محفوظة على هذا الجهاز.'; return; } renderResults(local.map(mapRow), {make, model, year}); return; } btnSearch.disabled = true; statusEl.textContent = 'جارٍ البحث...'; const { data, error } = await db.rpc('search_cars', { p_make: make, p_model: model, p_year: year, p_key_type: keyType || null, p_market: market || null }); btnSearch.disabled = false; if(error){ resultsEl.innerHTML = ''; // رسالة تجاوز الحد تأتي من قاعدة البيانات نفسها statusEl.textContent = /الحد المسموح/.test(error.message) ? '⏳ ' + error.message : 'تعذّر البحث: ' + error.message; return; } const matches = (data || []).map(mapRow); renderResults(matches, {make, model, year}); }); function mapRow(row){ return { id: row.id, make: row.make, model: row.model, yearFrom: row.year_from, yearTo: row.year_to, market: row.market, keyType: row.key_type || '', transponderType: row.transponder_type, keyBlade: row.key_blade, remoteFrequency: row.remote_frequency, programming: { type: row.programming_type, details: row.programming_details }, eepromLocation: row.eeprom_location, fccId: row.fcc_id || '', boardImageUrl: row.board_image_url || '', oemPartNumber: row.oem_part_number || '', specialRequirements: row.special_requirements || '', executionSteps: row.execution_steps || '', warningsNotes: row.warnings_notes || '', // صفوف المزامنة لا تحمل هذا الحقل لأنها لا تُمنح إلا للفني الموثّق isFullAccess: row.is_full_access === undefined ? true : row.is_full_access === true }; } // نص الدعوة يتغيّر حسب حالة المستخدم — «سجّل الدخول» بلا معنى لمن هو داخل function lockedCta(){ if(ACCESS === 'pending') return 'حسابك قيد المراجعة. تواصل مع الدعم'; if(ACCESS === 'suspended') return 'حسابك موقوف. راسل الدعم'; return 'سجّل الدخول'; } function renderResults(matches, ctx){ resultsEl.innerHTML = ''; if(matches.length === 0){ statusEl.textContent = ''; resultsEl.innerHTML = `
🧰
لا توجد بيانات لهذا الموديل/السنة في قاعدة البيانات حالياً.
`; return; } statusEl.textContent = `تم العثور على ${matches.length} نتيجة لـ ${ctx.make} ${ctx.model} ${ctx.year}`.replace(/\s+/g,' ').trim(); matches.forEach(c => { const marketLabel = c.market === 'gcc' ? 'خليجي' : 'أمريكي'; const keyTypeLabel = KEYTYPE_LABELS[c.keyType] || ''; const progClass = c.programming.type === 'obd' ? 'obd' : 'manual'; const card = document.createElement('div'); card.className = 'card'; card.innerHTML = `

${esc(c.make)} ${esc(c.model)}

${esc(c.yearFrom)} – ${esc(c.yearTo)}
${marketLabel} ${keyTypeLabel ? `${keyTypeLabel}` : ''}
نوع الشريحة${esc(c.transponderType) || '—'}
سلاح المفتاح / لِيشي${c.isFullAccess ? (esc(c.keyBlade) || '—') : LOCKED}
تردد الريموت${esc(c.remoteFrequency) || '—'}
طريقة البرمجة${PROG_LABELS[c.programming.type] || c.programming.type || '—'}${c.isFullAccess ? (c.programming.details ? ' — ' + esc(c.programming.details) : '') : ' — ' + LOCKED}
موقع EEPROM / BCM${c.isFullAccess ? (esc(c.eepromLocation) || '—') : LOCKED}
${c.isFullAccess && c.oemPartNumber ? `
رقم القطعة الأصلي (OEM)${esc(c.oemPartNumber)}
` : ''} ${c.fccId ? `
FCC ID${esc(c.fccId)} ↗
` : ''}
${c.isFullAccess && c.id ? `
✍️ أبلغ عن خطأ في هذه البيانات
` : ''} ${!c.isFullAccess ? `
🔒 أكواد Lishi وخطوات البرمجة ومواقع EEPROM متاحة للفنيين الموثّقين فقط. ${lockedCta()}
` : ''} ${c.fccId ? `
دليل الترددات ورموز FCC ID: افتح الرابط أعلاه لتفاصيل الجهاز الرسمية من سجل FCC الأمريكي (تردد دقيق، الشركة المصنّعة، تقرير الاختبار). الريموتات البديلة (Xhorse VVDI / Autel Universal / KEYDIY) المتوافقة عادة تُطابق حسب نفس التردد ونفس نوع الشريحة الظاهرين أعلاه — تأكد من SKU المحدد المطابق مع موردك قبل الشراء لأن FCC ID وحده لا يضمن التوافق الكامل مع كل الطُرز الفرعية.
` : ''} ${c.specialRequirements ? `
⚠ متطلبات خاصة: ${esc(c.specialRequirements)}
` : ''} ${c.executionSteps ? `
خطوات التنفيذ كاملة
${esc(c.executionSteps)}
${c.warningsNotes ? `
${esc(c.warningsNotes)}
` : ''}
` : (c.warningsNotes ? `
${esc(c.warningsNotes)}
` : '')} ${c.isFullAccess && c.id ? `
` : ''} `; resultsEl.appendChild(card); }); // نجلب صور كل النتائج بطلب واحد بعد رسم البطاقات attachGalleries(matches.filter(m => m.id).map(m => String(m.id))); } // ============================================================ // حاسبة VIN → PIN // ============================================================ // حاسبة PIN محلية للموديلات القديمة (SMARTRA-2، عادة حتى 2006) // خوارزمية موثّقة كمصدر مفتوح (Dante383/smartra-vin-to-pin على GitHub) // تعمل بالكامل داخل المتصفح — بدون إنترنت ولا اشتراك. // تنويه: هذا نظام قديم بسيط (غير مبني على تشفير قوي)، ولذلك يمكن حسابه // رياضياً بخلاف الأنظمة الأحدث (SMARTRA-3 فما فوق) التي تحتاج مصدر بيانات // حقيقي من المصنّع أو مزوّد مرخّص. // ============================================================ function smartraGetMsb(value){ return (value >>> 16) & 0xFFFF; } function smartraGetLsb(value){ return value & 0xFFFF; } function calculateSmartra2Pin(last6DigitsOfVin){ let output = last6DigitsOfVin >>> 0; for(let index = 0; index <= 0x27; index++){ if((smartraGetMsb(output) & 0x8000) === 0){ output = (output << 1) >>> 0; }else{ output = (output << 1) >>> 0; const msb = (smartraGetMsb(output) ^ 0x7798) & 0xFFFF; const lsb = (smartraGetLsb(output) ^ 0x2990) & 0xFFFF; output = ((msb << 16) | lsb) >>> 0; } } return output % 1000000; } // تخمين سنة الموديل تقريبياً من الخانة العاشرة في VIN (معيار عالمي عام، // وليس معلومة خاصة بالمناعة/التشفير) — يُستخدم فقط لتحديد أي نظام SMARTRA // نفترضه، وليس مصدراً دقيقاً 100% بسبب دورة الـ30 سنة المتكررة في المعيار. const VIN_YEAR_CODES = { 'X':1999,'Y':2000,'1':2001,'2':2002,'3':2003,'4':2004,'5':2005,'6':2006, '7':2007,'8':2008,'9':2009,'A':2010,'B':2011,'C':2012,'D':2013,'E':2014, 'F':2015,'G':2016,'H':2017,'J':2018,'K':2019,'L':2020,'M':2021,'N':2022, 'P':2023,'R':2024,'S':2025,'T':2026 }; function guessModelYearFromVIN(vin){ const code = vin.charAt(9); return VIN_YEAR_CODES[code] || null; } // ============================================================ // استدعاء API خارجي للموديلات الحديثة (2007+) — فعّله عند الاشتراك // ============================================================ async function calculatePinFromVIN(vin){ const response = await fetch(PIN_API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + PIN_API_KEY }, body: JSON.stringify({ vin: vin }) }); if(!response.ok){ throw new Error("فشل استدعاء خدمة حساب PIN (HTTP " + response.status + ")"); } return await response.json(); // المتوقع: { pin: "1234", ... } } document.getElementById('btn-pin').addEventListener('click', async () => { const vinInput = document.getElementById('vin-input'); const resultBox = document.getElementById('pin-result'); const vin = vinInput.value.trim().toUpperCase(); if(vin.length !== 17){ resultBox.className = 'pin-result show'; resultBox.textContent = 'رقم الهيكل يجب أن يكون 17 خانة بالضبط.'; return; } const guessedYear = guessModelYearFromVIN(vin); const last6Str = vin.slice(-6); const last6Num = Number(last6Str); const last6IsNumeric = /^[0-9]{6}$/.test(last6Str); resultBox.className = 'pin-result show'; // الحالة 1: موديل قديم على الأغلب (قبل 2007) — حساب محلي فوري if(guessedYear && guessedYear < 2007){ if(!last6IsNumeric){ resultBox.textContent = 'آخر 6 خانات من VIN تحتوي حروفاً — هذا غير معتاد لهذا النظام والحساب المحلي قد يكون غير دقيق. تحقق من رقم الهيكل.'; return; } const pin = calculateSmartra2Pin(last6Num); const pinStr = String(pin).padStart(6, '0'); resultBox.innerHTML = `PIN Code (SMARTRA-2, محلي): ${pinStr}
موديل متوقع: ${guessedYear} — تحقق دائماً بمصدر رسمي قبل الاعتماد عليه في العمل الفعلي.`; return; } // الحالة 2: موديل حديث على الأغلب (2007+) — يحتاج API حقيقي if(PIN_API_ENDPOINT.includes('example-pin-service.com') || PIN_API_KEY === 'YOUR_API_KEY_HERE'){ resultBox.innerHTML = `موديل متوقع: ${guessedYear || 'غير معروف'} — هذا الجيل (SMARTRA-3 فما فوق) لا يوجد له حساب رياضي مضمون محلياً. تحتاج تشترك بمصدر حقيقي (دونجل/سوفتوير أو خدمة ويب مرخّصة) وتضع رابط ومفتاح الخدمة في PIN_API_ENDPOINT و PIN_API_KEY داخل الكود.`; return; } resultBox.textContent = 'جارٍ الحساب عبر الخدمة الخارجية...'; try{ const result = await calculatePinFromVIN(vin); resultBox.textContent = "PIN Code: " + (result.pin || JSON.stringify(result)); }catch(err){ resultBox.textContent = "خطأ: " + err.message; } }); // التهيئة تُنفَّذ في نهاية الملف بعد تعريف كل شيء // ============================================================ // دليل أدوات Lishi (مرجع سريع) — إضافة // ============================================================ const LISHI_TABLE = "lishi_tools"; let LISHI_DATA = []; const lishiSearch = document.getElementById('lishi-search'); const lishiList = document.getElementById('lishi-list'); const lishiStatus = document.getElementById('lishi-status'); async function loadLishiTools(){ lishiStatus.textContent = "جارٍ تحميل دليل أدوات Lishi..."; const { data, error } = await db.rpc('list_lishi_tools'); if(error){ lishiStatus.textContent = "تعذّر تحميل دليل Lishi: " + error.message; console.error(error); return; } LISHI_DATA = data || []; lishiStatus.textContent = ""; renderLishi(""); } function renderLishi(query){ const q = query.trim().toLowerCase(); const filtered = q ? LISHI_DATA.filter(t => (t.tool_name||'').toLowerCase().includes(q) || (t.blade_code||'').toLowerCase().includes(q) || (t.supported_cars||'').toLowerCase().includes(q)) : LISHI_DATA; if(filtered.length === 0){ lishiList.innerHTML = `
🔧
لا توجد نتائج مطابقة.
`; return; } lishiList.innerHTML = filtered.map(t => { const full = t.is_full_access === true; return `
${esc(t.tool_name)} (${esc(t.blade_code)})
السيارات المدعومة: ${esc(t.supported_cars)}
القص: ${full ? (esc(t.cut_positions) || '—') : LOCKED}
اتجاه الالتقاط: ${full ? (esc(t.pick_direction) || '—') : LOCKED}
${full && t.notes ? `
ملاحظة: ${esc(t.notes)}
` : ''}
`; }).join(''); } lishiSearch.addEventListener('input', () => renderLishi(lishiSearch.value)); // ============================================================ // تسجيل دخول الفنيين (للوصول للتفاصيل الفنية) // ============================================================ const loginModal = document.getElementById('login-modal'); const accessChip = document.getElementById('access-chip'); const btnOpenLogin = document.getElementById('btn-open-login'); const btnSignout = document.getElementById('btn-signout'); function openLogin(){ loginModal.classList.add('show'); document.getElementById('lg-email').focus(); } function closeLogin(){ loginModal.classList.remove('show'); } btnOpenLogin.addEventListener('click', () => { closeDrawer(); openLogin(); }); document.getElementById('btn-close-login').addEventListener('click', closeLogin); loginModal.addEventListener('click', e => { if(e.target === loginModal) closeLogin(); }); // روابط "سجّل الدخول" داخل بطاقات النتائج document.addEventListener('click', e => { const sup = e.target.closest && e.target.closest('.sup-link'); if(sup){ e.preventDefault(); closeDrawer(); switchPane('pane-support'); return; } const fix = e.target.closest && e.target.closest('.fix-link'); if(fix){ e.preventDefault(); startCorrection(fix.dataset.id, fix.dataset.label); return; } if(e.target.classList && e.target.classList.contains('login-link')){ e.preventDefault(); closeDrawer(); openLogin(); } }); function setLgStatus(msg, cls){ const el = document.getElementById('lg-status'); el.className = 'lg-status' + (cls ? ' ' + cls : ''); el.textContent = msg || ''; } // ============================================================ // بصمة رقم الرخصة // ============================================================ // الشاشة تَعِد الفني بأن الرقم «يُخزَّن كبصمة مشفّرة فقط». الوفاء // بهذا الوعد يعني ألا يمرّ الرقم في options.data (تُخزَّن نصاً في // auth.users). نحفظه مؤقتاً في sessionStorage — ذاكرة التبويب // وحده، تُمحى بإغلاقه — ونرسله إلى الدالة فور توفّر جلسة. const PENDING_LICENSE_KEY = 'autokey_pending_license'; async function flushLicenseFingerprint(){ let lic = ''; try { lic = sessionStorage.getItem(PENDING_LICENSE_KEY) || ''; } catch(_){} if(!lic) return; try { const { error } = await db.rpc('set_license_fingerprint', { p_license: lic }); // نمسح المخزَّن في الحالتين: النجاح، أو رفض الدالة للقيمة. // إبقاؤه بعد رفض صريح يعني محاولة أبدية بلا فائدة. if(!error || /غير صالح/.test(error.message || '')){ try { sessionStorage.removeItem(PENDING_LICENSE_KEY); } catch(_){} } } catch(_){ /* الشبكة — نعيد المحاولة عند الدخول القادم */ } } document.getElementById('btn-do-login').addEventListener('click', async () => { const email = document.getElementById('lg-email').value.trim(); const password = document.getElementById('lg-password').value; const btn = document.getElementById('btn-do-login'); if(!email || !password){ setLgStatus('أدخل البريد وكلمة المرور.','err'); return; } btn.disabled = true; setLgStatus('جارٍ التحقق...','busy'); const { error } = await db.auth.signInWithPassword({ email, password }); btn.disabled = false; if(error){ setLgStatus('بيانات الدخول غير صحيحة.','err'); return; } document.getElementById('lg-password').value = ''; setLgStatus('',''); closeLogin(); await flushLicenseFingerprint(); // 🔒 بصمة الرخصة إن كانت معلّقة await refreshAccess(); }); ['lg-email','lg-password'].forEach(id => { document.getElementById(id).addEventListener('keydown', e => { if(e.key === 'Enter') document.getElementById('btn-do-login').click(); }); }); // تبديل بين تسجيل الدخول والحساب الجديد function switchTab(which){ const isLogin = which === 'login'; document.getElementById('pane-login').style.display = isLogin ? '' : 'none'; document.getElementById('pane-signup').style.display = isLogin ? 'none' : ''; document.getElementById('tab-login').classList.toggle('active', isLogin); document.getElementById('tab-signup').classList.toggle('active', !isLogin); } document.getElementById('tab-login').addEventListener('click', () => switchTab('login')); document.getElementById('tab-signup').addEventListener('click', () => switchTab('signup')); function setSuStatus(msg, cls){ const el = document.getElementById('su-status'); el.className = 'lg-status' + (cls ? ' ' + cls : ''); el.textContent = msg || ''; } document.getElementById('btn-do-signup').addEventListener('click', async () => { const name = document.getElementById('su-name').value.trim(); const workshop = document.getElementById('su-workshop').value.trim(); const city = document.getElementById('su-city').value.trim(); const license = document.getElementById('su-license').value.trim(); const email = document.getElementById('su-email').value.trim(); const password = document.getElementById('su-password').value; const btn = document.getElementById('btn-do-signup'); if(!name || !email || !password){ setSuStatus('الاسم والبريد وكلمة المرور مطلوبة.','err'); return; } if(password.length < 6){ setSuStatus('كلمة المرور 6 أحرف على الأقل.','err'); return; } btn.disabled = true; setSuStatus('جارٍ إرسال الطلب...','busy'); // 🔒 الرقم في ذاكرة التبويب فقط حتى تتوفّر جلسة نرسله فيها try { if(license) sessionStorage.setItem(PENDING_LICENSE_KEY, license); } catch(_){} // البيانات الوصفية فقط — الدور والحالة تُحدَّدان في قاعدة البيانات const { data, error } = await db.auth.signUp({ email, password, // 🔒 رقم الرخصة لا يمرّ هنا 2026-09-11: كل ما في options.data // يُخزَّن نصاً صريحاً في auth.users.raw_user_meta_data ويمرّ في // سجلات المصادقة — وهذا يناقض ما تَعِد به هذه الشاشة حرفياً. // يُرسل بعد الدخول إلى set_license_fingerprint التي تحسب // البصمة ولا تحفظ الأصل. options: { data: { display_name: name, workshop_name: workshop, city: city }} }); btn.disabled = false; if(error){ setSuStatus(/already|registered/i.test(error.message) ? 'هذا البريد مسجّل مسبقاً. استخدم تسجيل الدخول.' : 'تعذّر التسجيل: ' + error.message, 'err'); return; } if(data.session) await flushLicenseFingerprint(); ['su-name','su-workshop','su-city','su-license','su-email','su-password'] .forEach(id => document.getElementById(id).value = ''); if(data.session){ setSuStatus('✔ تم إنشاء الحساب. طلبك قيد المراجعة.','ok'); await refreshAccess(); }else{ setSuStatus('✔ تم إرسال الطلب. افحص بريدك لتأكيد الحساب، ثم انتظر اعتماد المدير.','ok'); } }); btnSignout.addEventListener('click', async () => { closeDrawer(); await db.auth.signOut(); await refreshAccess(); }); // تحديث حالة الوصول وإعادة تحميل ما يعتمد عليها async function refreshAccess(){ const { data: { session } } = await db.auth.getSession(); if(session){ // نتحقق من الصلاحية الفعلية من قاعدة البيانات، لا من مجرد وجود جلسة const { data: profile } = await db .from('profiles') .select('role, status') .eq('id', session.user.id) .maybeSingle(); const verified = profile && ['locksmith','moderator','super_admin'].includes(profile.role) && profile.status === 'verified'; // نفرّق بين الحالات الأربع: الموقوف ليس كالمعلّق if(verified) ACCESS = 'verified'; else if(profile && profile.status === 'suspended') ACCESS = 'suspended'; else if(profile) ACCESS = 'pending'; else ACCESS = 'guest'; accessChip.textContent = t('chip_' + ACCESS); accessChip.classList.toggle('full', ACCESS === 'verified'); accessChip.classList.toggle('bad', ACCESS === 'suspended'); btnSync.style.display = verified ? '' : 'none'; document.getElementById('nav-contrib').style.display = verified ? '' : 'none'; if(verified){ loadMyContribs(); loadWorkshop(); } else document.getElementById('nav-workshop').style.display = 'none'; // شريط توضيحي لمن ليس موثّقاً — بدل تركه يخمّن سبب الأقفال showAccessBanner(); btnOpenLogin.style.display = 'none'; btnSignout.style.display = ''; }else{ ACCESS = 'guest'; accessChip.textContent = t('chip_guest'); accessChip.classList.remove('full'); accessChip.classList.remove('bad'); btnSync.style.display = 'none'; showAccessBanner(); document.getElementById('nav-contrib').style.display = 'none'; document.getElementById('nav-workshop').style.display = 'none'; btnOpenLogin.style.display = ''; btnSignout.style.display = 'none'; } // إعادة تحميل دليل Lishi ومسح النتائج القديمة (تغيّر مستوى الوصول) await loadLishiTools(); resultsEl.innerHTML = ''; statusEl.textContent = ''; } // ============================================================ // العمل بدون إنترنت (PWA) // ============================================================ // نستخدم IndexedDB وليس localStorage لأن الأخير محدود بحوالي 5 ميجابايت // وسيمتلئ مع نمو قاعدة البيانات لآلاف السجلات. const DB_NAME = 'autokey-offline'; const STORE = 'kv'; function idb(){ return new Promise((res, rej) => { const r = indexedDB.open(DB_NAME, 1); r.onupgradeneeded = () => { r.result.createObjectStore(STORE); }; r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); } async function idbSet(k, v){ const db = await idb(); return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite'); t.objectStore(STORE).put(v, k); t.oncomplete = () => res(true); t.onerror = () => rej(t.error); }); } async function idbGet(k){ const db = await idb(); return new Promise((res, rej) => { const t = db.transaction(STORE, 'readonly'); const q = t.objectStore(STORE).get(k); q.onsuccess = () => res(q.result); q.onerror = () => rej(q.error); }); } let OFFLINE_CARS = null; // نسخة محلية من البيانات let LAST_SYNC = null; const offlineBar = document.getElementById('offline-bar'); const offlineText = document.getElementById('offline-text'); const btnSync = document.getElementById('btn-sync'); function isOffline(){ return !navigator.onLine; } function fmtDate(iso){ if(!iso) return ''; try{ return new Date(iso).toLocaleDateString('ar', { year:'numeric', month:'long', day:'numeric' }); }catch(e){ return iso.slice(0,10); } } function updateOfflineBar(){ if(isOffline()){ offlineBar.classList.add('show'); if(OFFLINE_CARS && OFFLINE_CARS.length){ offlineBar.classList.add('synced'); offlineText.textContent = `📴 بلا اتصال — ${OFFLINE_CARS.length} سجل محفوظ على جهازك (مزامنة ${fmtDate(LAST_SYNC)})`; }else{ offlineBar.classList.remove('synced'); offlineText.textContent = '📴 بلا اتصال — لا توجد بيانات محفوظة. سجّل الدخول واضغط «تنزيل للعمل بلا نت» عند توفر الشبكة.'; } }else{ offlineBar.classList.remove('show'); } } window.addEventListener('online', () => { updateOfflineBar(); }); window.addEventListener('offline', () => { updateOfflineBar(); }); async function loadOfflineCache(){ try{ const pack = await idbGet('cars'); if(pack && Array.isArray(pack.rows)){ OFFLINE_CARS = pack.rows; LAST_SYNC = pack.syncedAt; } }catch(e){ /* لا يمنع عمل التطبيق */ } updateOfflineBar(); } // تنزيل البيانات للاستخدام دون شبكة btnSync.addEventListener('click', async () => { closeDrawer(); switchPane('pane-search'); btnSync.disabled = true; const lbl = btnSync.querySelector('.dl'); const original = lbl.textContent; lbl.textContent = 'جارٍ التنزيل...'; const { data, error } = await db.rpc('sync_all_cars'); if(error){ lbl.textContent = original; btnSync.disabled = false; statusEl.textContent = /الحد المسموح/.test(error.message) ? '⏳ ' + error.message : 'تعذّر التنزيل: ' + error.message; return; } const rows = data || []; const syncedAt = new Date().toISOString(); try{ await idbSet('cars', { rows, syncedAt }); OFFLINE_CARS = rows; LAST_SYNC = syncedAt; statusEl.textContent = `✔ تم حفظ ${rows.length} سجل على جهازك — يعمل الآن بلا إنترنت`; }catch(e){ statusEl.textContent = 'تعذّر الحفظ على الجهاز: ' + e.message; } lbl.textContent = original; btnSync.disabled = false; updateOfflineBar(); }); // البحث محلياً عند انقطاع الشبكة function searchOffline(make, model, year, market, keyType){ if(!OFFLINE_CARS) return null; return OFFLINE_CARS.filter(r => arBase(r.make) === arBase(make) && arBase(r.model) === arBase(model) && year >= r.year_from && year <= r.year_to && (market === '' || r.market === market) && (keyType === '' || r.key_type === keyType) ); } // تعبئة القوائم من النسخة المحلية عند انقطاع الشبكة function fillFromOffline(){ if(!OFFLINE_CARS) return false; // نجمع على الاسم الأساسي بنفس منطق قاعدة البيانات const g = new Map(); OFFLINE_CARS.forEach(r => { const b = arBase(r.make); if(!b) return; if(!g.has(b) || r.make.length > g.get(b).length) g.set(b, r.make); }); selMake.innerHTML = ''; [...g.entries()].sort((a,b) => a[1].localeCompare(b[1],'ar')).forEach(([val,lab]) => { const o = document.createElement('option'); o.value = val; o.textContent = lab; selMake.appendChild(o); }); selMake.disabled = false; return true; } // ============================================================ // اللغة: عربي / English // ============================================================ // نطاقها الموقع العام فقط. لوحة التحكم تبقى عربية لأن مستخدمها // شخص واحد يقرأ العربية، فترجمتها جهد بلا مستفيد. const LANG = { ar: { dir:'rtl', name:'العربية', other:'English', nav_search:'بحث السيارات', nav_lishi:'دليل Lishi', nav_pin:'حاسبة PIN', nav_contrib:'أضف سيارة', nav_workshop:'ورشتي', nav_support:'الدعم والتواصل', nav_sync:'تنزيل للعمل بلا نت', nav_login:'دخول الفنيين', nav_admin:'لوحة التحكم', nav_logout:'تسجيل الخروج', h_search:'ابحث عن السيارة', l_make:'الماركة', l_model:'الموديل', l_year:'سنة الصنع', l_keytype:'نوع المفتاح', l_market:'السوق', b_search:'بحث', ph_quick:'بحث سريع: اسم السيارة، رمز الشريحة، أو FCC ID', t_search:'بحث السيارات', t_lishi:'دليل Lishi', t_pin:'حاسبة PIN', t_contrib:'أضف سيارة', t_support:'الدعم والتواصل', t_workshop:'ورشتي', chip_guest:'زائر', chip_pending:'بانتظار التوثيق', chip_suspended:'الحساب موقوف', chip_verified:'فني موثّق', f_chip:'نوع الشريحة', f_blade:'سلاح المفتاح / لِيشي', f_freq:'تردد الريموت', f_prog:'طريقة البرمجة', f_eeprom:'موقع EEPROM / BCM', f_fcc:'FCC ID', f_oem:'رقم القطعة الأصلي (OEM)', f_special:'متطلبات خاصة', f_steps:'خطوات التنفيذ', f_warn:'تحذيرات وملاحظات', locked:'للفنيين الموثّقين', login_link:'سجّل الدخول', market_gcc:'خليجي', market_us:'أمريكي', key_smart:'بصمة', key_remote:'بريموت', key_blade:'عادي', prog_manual:'يدوية', prog_obd:'جهاز OBD مطلوب', found:(n,q)=>`تم العثور على ${n} نتيجة لـ ${q}`, none:'لا توجد بيانات لهذا الموديل/السنة في قاعدة البيانات حالياً.', sel_make:'اختر الماركة', sel_model:'اختر الموديل', sel_year:'اختر السنة', all_types:'كل الأنواع', all_markets:'كل الأسواق', loading:'جارٍ التحميل...' }, en: { dir:'ltr', name:'English', other:'العربية', nav_search:'Search cars', nav_lishi:'Lishi guide', nav_pin:'PIN calculator', nav_contrib:'Add a car', nav_workshop:'My workshop', nav_support:'Support', nav_sync:'Download for offline use', nav_login:'Technician login', nav_admin:'Admin panel', nav_logout:'Sign out', h_search:'Find a vehicle', l_make:'Make', l_model:'Model', l_year:'Year', l_keytype:'Key type', l_market:'Market', b_search:'Search', ph_quick:'Quick search: car name, transponder, or FCC ID', t_search:'Search cars', t_lishi:'Lishi guide', t_pin:'PIN calculator', t_contrib:'Add a car', t_support:'Support', t_workshop:'My workshop', chip_guest:'Guest', chip_pending:'Pending approval', chip_suspended:'Account suspended', chip_verified:'Verified technician', f_chip:'Transponder', f_blade:'Key blade / Lishi', f_freq:'Remote frequency', f_prog:'Programming method', f_eeprom:'EEPROM / BCM location', f_fcc:'FCC ID', f_oem:'OEM part number', f_special:'Special requirements', f_steps:'Procedure', f_warn:'Warnings & notes', locked:'Verified technicians only', login_link:'Sign in', market_gcc:'GCC', market_us:'USA', key_smart:'Smart key', key_remote:'Remote', key_blade:'Blade', prog_manual:'Manual', prog_obd:'OBD tool required', found:(n,q)=>`${n} result(s) for ${q}`, none:'No data for this model/year in the database yet.', sel_make:'Select make', sel_model:'Select model', sel_year:'Select year', all_types:'All types', all_markets:'All markets', loading:'Loading...' } }; let CUR_LANG = 'ar'; try{ CUR_LANG = localStorage.getItem('autokeyLang') || 'ar'; }catch(e){} function t(key){ const d = LANG[CUR_LANG] || LANG.ar; return (key in d) ? d[key] : (LANG.ar[key] !== undefined ? LANG.ar[key] : key); } function applyLang(lang){ CUR_LANG = (lang === 'en') ? 'en' : 'ar'; try{ localStorage.setItem('autokeyLang', CUR_LANG); }catch(e){} const d = LANG[CUR_LANG]; document.documentElement.lang = CUR_LANG; document.documentElement.dir = d.dir; document.querySelectorAll('[data-i18n]').forEach(el => { const v = t(el.dataset.i18n); if(typeof v === 'string') el.textContent = v; }); document.querySelectorAll('[data-i18n-ph]').forEach(el => { const v = t(el.dataset.i18nPh); if(typeof v === 'string') el.placeholder = v; }); const lbl = document.getElementById('lang-label'); if(lbl) lbl.textContent = d.other; // العناصر التي تُبنى بالجافاسكربت تحتاج إعادة رسم const titles = { 'pane-search':'t_search', 'pane-lishi':'t_lishi', 'pane-pin':'t_pin', 'pane-contrib':'t_contrib', 'pane-support':'t_support', 'pane-workshop':'t_workshop' }; const active = document.querySelector('.tabpane[style*="block"], .tabpane:not([style])'); if(active && titles[active.id]) document.getElementById('page-title').textContent = t(titles[active.id]); refreshAccessChip(); } function refreshAccessChip(){ const chip = document.getElementById('access-chip'); if(!chip) return; chip.textContent = t('chip_' + (ACCESS || 'guest')); } document.getElementById('btn-lang').addEventListener('click', () => { applyLang(CUR_LANG === 'ar' ? 'en' : 'ar'); closeDrawer(); }); // ============================================================ // القائمة الجانبية // ============================================================ const PANE_TITLES = { 'pane-search':'بحث السيارات', 'pane-lishi':'دليل Lishi', 'pane-pin':'حاسبة PIN', 'pane-contrib':'أضف سيارة', 'pane-support':'الدعم والتواصل', 'pane-workshop':'ورشتي' }; const drawerEl = document.getElementById('drawer'); const drawerBack = document.getElementById('drawer-back'); function openDrawer(){ drawerEl.classList.add('open'); drawerBack.classList.add('show'); } function closeDrawer(){ drawerEl.classList.remove('open'); drawerBack.classList.remove('show'); } document.getElementById('btn-menu').addEventListener('click', openDrawer); drawerBack.addEventListener('click', closeDrawer); document.addEventListener('keydown', e => { if(e.key === 'Escape') closeDrawer(); }); function switchPane(id){ document.querySelectorAll('.tabpane').forEach(el => { el.style.display = (el.id === id) ? 'block' : 'none'; }); document.querySelectorAll('.dnav[data-pane]').forEach(b => { b.classList.toggle('active', b.dataset.pane === id); }); const K = { 'pane-search':'t_search','pane-lishi':'t_lishi','pane-pin':'t_pin', 'pane-contrib':'t_contrib','pane-support':'t_support','pane-workshop':'t_workshop' }; document.getElementById('page-title').textContent = K[id] ? t(K[id]) : (PANE_TITLES[id] || ''); window.scrollTo({ top:0 }); } drawerEl.addEventListener('click', e => { const b = e.target.closest('.dnav[data-pane]'); if(b){ switchPane(b.dataset.pane); closeDrawer(); } }); // ---------- البحث السريع ---------- const quickQ = document.getElementById('quick-q'); const btnQuick = document.getElementById('btn-quick'); // تطبيع مطابق لدالة قاعدة البيانات، ليعمل البحث بلا إنترنت بنفس السلوك const AR_FROM = 'أإآٱىةؤئ٠١٢٣٤٥٦٧٨٩کیچپگژًٌٍَُِّْٰـ'; const AR_TO = 'اااايهوي0123456789كيجبكز'; function arNormalize(v){ let out = ''; for(const ch of String(v || '')){ const i = AR_FROM.indexOf(ch); if(i === -1) out += ch; else if(i < AR_TO.length) out += AR_TO[i]; // ما بعد ذلك تشكيل وتطويل — يُحذف } return out.replace(/\s+/g, ' ').trim().toLowerCase(); } // مطابق لدالة ar_base في قاعدة البيانات function arBase(v){ return arNormalize(v) .replace(/\([^)]*\)/g, ' ') .replace(/\s+/g, ' ') .replace(/[\s/\-]+$/, '') .trim(); } async function runQuickSearch(){ const q = quickQ.value.trim(); if(q.length < 2){ statusEl.textContent = 'اكتب حرفين على الأقل.'; return; } // بلا شبكة: نبحث في النسخة المحفوظة بنفس منطق التطبيع if(isOffline()){ if(!OFFLINE_CARS){ statusEl.textContent = 'لا يوجد اتصال ولا بيانات محفوظة.'; return; } const nq = arNormalize(q); const hits = OFFLINE_CARS.filter(r => arNormalize(r.make).includes(nq) || arNormalize(r.model).includes(nq) || arNormalize(r.transponder_type).includes(nq) || arNormalize(r.key_blade).includes(nq) || arNormalize(r.fcc_id).includes(nq) ); resultsEl.innerHTML = ''; statusEl.textContent = `تم العثور على ${hits.length} نتيجة لـ "${q}" (بلا اتصال)`; renderResults(hits.map(mapRow), {make:q, model:'', year:''}); return; } btnQuick.disabled = true; statusEl.textContent = 'جارٍ البحث...'; const { data, error } = await db.rpc('search_text', { p_query: q }); btnQuick.disabled = false; if(error){ statusEl.textContent = /الحد المسموح/.test(error.message) ? '⏳ ' + error.message : 'تعذّر البحث: ' + error.message; return; } statusEl.textContent = `تم العثور على ${(data||[]).length} نتيجة لـ "${q}"`; renderResults((data || []).map(mapRow), {make:q, model:'', year:''}); } btnQuick.addEventListener('click', runQuickSearch); quickQ.addEventListener('keydown', e => { if(e.key === 'Enter') runQuickSearch(); }); // ============================================================ // ورشتي // ============================================================ let MY_WS = null; async function loadWorkshop(){ const nav = document.getElementById('nav-workshop'); const { data, error } = await db.rpc('my_workshop'); const row = (!error && data && data.length) ? data[0] : null; MY_WS = row; if(!row){ nav.style.display = 'none'; return; } nav.style.display = ''; document.getElementById('ws-name').textContent = row.name || 'ورشتي'; const isOwner = row.my_role === 'owner'; const free = Math.max(0, row.seat_limit - row.seats_used); document.getElementById('ws-info').innerHTML = (row.city ? 'المدينة: ' + esc(row.city) + '
' : '') + 'دورك: ' + (isOwner ? 'صاحب الورشة' : 'فني') + '
' + 'المقاعد: ' + row.seats_used + ' من ' + row.seat_limit + '' + (isOwner ? ' — متبقٍ ' + free : '') + '
' + (row.status === 'suspended' ? '⛔ الورشة موقوفة — الوصول معطّل لكل فنييها.' : ''); document.getElementById('ws-owner-box').style.display = (isOwner && row.status === 'active') ? 'block' : 'none'; loadWsMembers(); } async function loadWsMembers(){ const el = document.getElementById('ws-members'); const { data, error } = await db.rpc('workshop_members'); if(error){ el.textContent = 'تعذّر التحميل.'; return; } const rows = data || []; const isOwner = MY_WS && MY_WS.my_role === 'owner'; el.innerHTML = rows.map(r => `
${esc(r.display_name) || esc(r.email)} ${r.workshop_role === 'owner' ? 'صاحب الورشة' : ''}
${esc(r.email)} · ${r.status === 'verified' ? 'مفعّل' : esc(r.status)}
${(isOwner && r.workshop_role === 'member') ? `` : ''}
`).join('') || '
لا يوجد فنيون بعد.
'; } document.getElementById('btn-ws-add').addEventListener('click', async () => { const email = document.getElementById('ws-email').value.trim(); const el = document.getElementById('ws-status'); if(!email){ el.className='lg-status err'; el.textContent='أدخل بريد الفني.'; return; } el.className='lg-status busy'; el.textContent='جارٍ الإضافة...'; const { data, error } = await db.rpc('workshop_add_member', { p_email: email }); if(error){ el.className='lg-status err'; el.textContent='✕ ' + error.message; return; } el.className='lg-status ok'; el.textContent='✔ ' + (data || 'تمت الإضافة'); document.getElementById('ws-email').value = ''; loadWorkshop(); }); document.getElementById('ws-members').addEventListener('click', async (e) => { const b = e.target.closest('.ws-del'); if(!b) return; if(!confirm('إزالة هذا الفني؟ سيفقد الوصول ويعود «بانتظار التوثيق».')) return; const { error } = await db.rpc('workshop_remove_member', { p_user_id: b.dataset.uid }); const el = document.getElementById('ws-status'); if(error){ el.className='lg-status err'; el.textContent='✕ ' + error.message; return; } loadWorkshop(); }); // ============================================================ // معرض صور البطاقات // ============================================================ const IMG_KIND_AR = { remote:'الريموت', board:'البوردة', lishi:'موضع القص', key:'المفتاح', tool:'الجهاز', other:'صورة' }; // نجلب صور كل النتائج بطلب واحد بدل طلب لكل بطاقة async function attachGalleries(ids){ if(!ids.length || ACCESS !== 'verified') return; const { data, error } = await db.rpc('car_images_for', { p_ids: ids }); if(error || !data || !data.length) return; const byCar = new Map(); data.forEach(r => { if(!byCar.has(r.car_id)) byCar.set(r.car_id, []); byCar.get(r.car_id).push(r); }); byCar.forEach((imgs, carId) => { const holder = document.querySelector(`[data-gal="${CSS.escape(carId)}"]`); if(!holder) return; // 🔒 safeUrl ترفض javascript: و data: — esc وحدها لا تمنعهما، // لأنها تمنع الخروج من الخانة لا المخطط داخلها. holder.innerHTML = imgs.map(im => { const u = safeUrl(im.url); if(!u) return ''; return `
${esc(IMG_KIND_AR[im.kind] || 'صورة')}
`; }).join(''); holder.querySelectorAll('img').forEach(g => g.addEventListener('error', () => { g.style.opacity = 0.25; })); }); } // ============================================================ // حالة الوصول والدعم // ============================================================ const WA_URL = 'https://wa.me/' + SUPPORT_WHATSAPP; const MAIL_URL = 'mailto:' + SUPPORT_EMAIL; document.getElementById('sup-wa').href = WA_URL; document.getElementById('sup-mail').href = MAIL_URL; const ACCESS_TEXT = { guest: { cls:'', msg:'' }, pending: { cls:'info', msg:'حسابك قيد المراجعة. ستُفتح التفاصيل الفنية فور اعتماده. تواصل مع الدعم' }, suspended: { cls:'warn', msg:'⛔ تم إيقاف حسابك، والتفاصيل الفنية غير متاحة. راسل الدعم لمعرفة السبب' }, verified: { cls:'', msg:'' } }; function showAccessBanner(){ const bar = document.getElementById('access-banner'); const st = ACCESS_TEXT[ACCESS] || ACCESS_TEXT.guest; if(!st.msg){ bar.className = 'access-banner'; bar.innerHTML = ''; return; } bar.className = 'access-banner show ' + st.cls; bar.innerHTML = st.msg; const SUP = { pending:['بانتظار التوثيق','أرسل لنا اسمك واسم ورشتك لتسريع الاعتماد.'], suspended:['موقوف','وضّح لنا ما حدث وسنراجع الحساب.'], verified:['موثّق','حسابك مفعّل بالكامل.'], guest:['زائر','سجّل حساباً للوصول للتفاصيل الفنية.'] }; const s2 = SUP[ACCESS] || SUP.guest; document.getElementById('sup-state').textContent = s2[0]; document.getElementById('sup-hint').textContent = s2[1]; } // ============================================================ // مساهمات الفنيين // ============================================================ const CONTRIB_FIELDS = { 'c-make':'make', 'c-model':'model', 'c-yfrom':'year_from', 'c-yto':'year_to', 'c-market':'market', 'c-keytype':'key_type', 'c-chip':'transponder_type', 'c-blade':'key_blade', 'c-freq':'remote_frequency', 'c-eeprom':'eeprom_location', 'c-oem':'oem_part_number', 'c-steps':'execution_steps', 'c-warn':'warnings_notes', 'c-prog-type':'programming_type', 'c-prog-details':'programming_details', 'c-fcc':'fcc_id', 'c-special':'special_requirements', 'c-confidence':'confidence', 'c-source':'data_source' }; // خانة الاختيار تُقرأ منفصلة لأن قيمتها checked وليست value function contribEepromFlag(){ const el = document.getElementById('c-req-eeprom'); return el && el.checked ? true : null; } function setContribStatus(msg, cls){ const el = document.getElementById('contrib-status'); el.className = 'lg-status' + (cls ? ' ' + cls : ''); el.textContent = msg || ''; } function resetContribForm(){ Object.keys(CONTRIB_FIELDS).forEach(id => { const el = document.getElementById(id); if(el) el.value = (id === 'c-market') ? 'gcc' : ''; }); document.getElementById('c-note').value = ''; const cre = document.getElementById('c-req-eeprom'); if(cre) cre.checked = false; document.getElementById('c-target').value = ''; document.getElementById('c-target-note').style.display = 'none'; CONTRIB_IMAGES = []; renderContribImages(); document.getElementById('c-img-status').textContent = ''; document.getElementById('contrib-title').textContent = 'أضف سيارة إلى المرجع'; setContribStatus('',''); } document.getElementById('btn-contrib-reset').addEventListener('click', resetContribForm); // تصحيح بطاقة قائمة: يملأ النموذج ويحدد السجل المقصود function startCorrection(id, label){ document.getElementById('c-target').value = id; const note = document.getElementById('c-target-note'); note.style.display = 'block'; note.innerHTML = '✍️ تصحيح على: ' + esc(label) + ' — املأ الحقول التي تريد تصحيحها فقط، واترك الباقي فارغاً.'; document.getElementById('contrib-title').textContent = 'تصحيح بيانات سيارة'; switchPane('pane-contrib'); } // ---------- إرفاق صور بالمساهمة ---------- let CONTRIB_IMAGES = []; function renderContribImages(){ const el = document.getElementById('c-img-list'); el.innerHTML = CONTRIB_IMAGES.length ? '
' + CONTRIB_IMAGES.map((im, idx) => `
${esc(IMG_KIND_AR[im.kind] || 'صورة')} ✕
`).join('') + '
' : ''; } document.getElementById('c-img-list').addEventListener('click', e => { const a = e.target.closest('a[data-rm]'); if(!a) return; e.preventDefault(); CONTRIB_IMAGES.splice(Number(a.dataset.rm), 1); renderContribImages(); }); document.getElementById('btn-c-img').addEventListener('click', async () => { const files = document.getElementById('c-img-file').files; const st = document.getElementById('c-img-status'); const btn = document.getElementById('btn-c-img'); const kind = document.getElementById('c-img-kind').value; if(!files.length){ st.className='lg-status err'; st.textContent='اختر صورة أولاً.'; return; } btn.disabled = true; let ok = 0, fail = null; for(const file of files){ st.className='lg-status busy'; st.textContent = `جارٍ الرفع ${ok + 1} من ${files.length}...`; // مجلد contrib منفصل: الصور لا تُنشر قبل اعتماد المساهمة const path = `contrib/${Date.now()}_${file.name.replace(/[^a-zA-Z0-9.\-_]/g,'')}`; try{ const { error: upErr } = await db.storage.from('car-boards').upload(path, file); if(upErr) throw upErr; const { data: pub } = db.storage.from('car-boards').getPublicUrl(path); CONTRIB_IMAGES.push({ url: pub.publicUrl, kind, caption: null }); ok++; }catch(err){ fail = (err && err.message) ? err.message : String(err); break; } } btn.disabled = false; document.getElementById('c-img-file').value = ''; if(fail){ st.className='lg-status err'; st.textContent = `رُفعت ${ok} ثم فشل: ${fail}`; } else { st.className='lg-status ok'; st.textContent = `✔ أُرفقت ${ok} صورة`; } renderContribImages(); }); document.getElementById('btn-contrib-send').addEventListener('click', async () => { const btn = document.getElementById('btn-contrib-send'); const target = document.getElementById('c-target').value; const kind = target ? 'correction' : 'new'; const payload = {}; Object.entries(CONTRIB_FIELDS).forEach(([id, key]) => { const v = (document.getElementById(id).value || '').trim(); if(v !== '') payload[key] = v; // الحقول الفارغة لا تُرسل إطلاقاً }); if(kind === 'new' && (!payload.make || !payload.model)){ setContribStatus('الماركة والموديل مطلوبان لإضافة سيارة جديدة.','err'); return; } const eepromFlag = contribEepromFlag(); if(eepromFlag) payload.requires_eeprom = true; if(payload.programming_type === 'eeprom') payload.requires_eeprom = true; if(CONTRIB_IMAGES.length) payload.images = CONTRIB_IMAGES; if(kind === 'correction' && Object.keys(payload).length === 0 && !CONTRIB_IMAGES.length){ setContribStatus('أدخل الحقل الذي تريد تصحيحه على الأقل.','err'); return; } btn.disabled = true; setContribStatus('جارٍ الإرسال...','busy'); const { error } = await db.rpc('submit_contribution', { p_kind: kind, p_payload: payload, p_note: document.getElementById('c-note').value.trim() || null, p_target_id: target || null }); btn.disabled = false; if(error){ setContribStatus(/الحد المسموح/.test(error.message) ? '⏳ ' + error.message : '✕ ' + error.message, 'err'); return; } setContribStatus('✔ وصل اقتراحك للمراجعة. شكراً لك.','ok'); resetContribForm(); loadMyContribs(); }); const CSTAT = { pending:'قيد المراجعة', approved:'✔ اعتُمد', rejected:'✕ رُفض' }; async function loadMyContribs(){ const el = document.getElementById('my-contribs'); const { data, error } = await db.rpc('my_contributions'); if(error){ el.textContent = 'تعذّر التحميل.'; return; } const rows = data || []; if(!rows.length){ el.textContent = 'لم ترسل أي مساهمة بعد.'; return; } el.innerHTML = rows.map(r => `
${esc(r.label) || (r.kind === 'correction' ? 'تصحيح' : 'إضافة')} ${r.review_note ? '
'+esc(r.review_note)+'' : ''}
${CSTAT[r.status] || esc(r.status)}
`).join(''); } // ============================================================ // إشعار تثبيت التطبيق // ============================================================ // المتصفح يطلق beforeinstallprompt عندما يرى الموقع مؤهلاً للتثبيت. // نحتجز الحدث ونعرض شريطاً بلغة المستخدم بدل ترك الأمر لقائمة // المتصفح التي لا يعرفها كثيرون. let deferredInstall = null; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredInstall = e; if(sessionStorage.getItem('installDismissed') === '1') return; showInstallBar(); }); function showInstallBar(){ if(document.getElementById('install-bar')) return; const bar = document.createElement('div'); bar.id = 'install-bar'; bar.className = 'install-bar'; bar.innerHTML = '
🔑
' + '
ثبّت AutoKey Pro' + 'وصول أسرع، ويعمل داخل الكراج بلا إنترنت
' + '' + ''; document.body.appendChild(bar); document.getElementById('ib-yes').addEventListener('click', async () => { if(!deferredInstall) return; deferredInstall.prompt(); try{ await deferredInstall.userChoice; }catch(e){} deferredInstall = null; bar.remove(); }); document.getElementById('ib-no').addEventListener('click', () => { sessionStorage.setItem('installDismissed','1'); bar.remove(); }); } // بعد التثبيت لا معنى لبقاء الشريط window.addEventListener('appinstalled', () => { const b = document.getElementById('install-bar'); if(b) b.remove(); deferredInstall = null; }); // تسجيل الـ Service Worker — يجعل التطبيق يفتح بلا شبكة if('serviceWorker' in navigator){ window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').then(reg => { // نتحقق من وجود تحديث عند كل فتح للتطبيق reg.update().catch(() => {}); reg.addEventListener('updatefound', () => { const nw = reg.installing; if(!nw) return; nw.addEventListener('statechange', () => { // نسخة جديدة جاهزة وهناك نسخة قديمة تعمل حالياً if(nw.state === 'installed' && navigator.serviceWorker.controller){ showUpdateBanner(); } }); }); }).catch(err => { console.warn('تعذّر تسجيل Service Worker:', err); }); }); } // إشعار تحديث بدل إعادة تحميل تلقائية — الإعادة التلقائية قد تدخل // في حلقة لا تنتهي، والفني قد يكون في منتصف عملية برمجة. function showUpdateBanner(){ if(document.getElementById('update-bar')) return; const bar = document.createElement('div'); bar.id = 'update-bar'; bar.style.cssText = 'position:fixed; bottom:0; left:0; right:0; z-index:60;' + 'background:var(--amber); color:#1a1206; padding:12px 16px;' + 'display:flex; align-items:center; justify-content:space-between; gap:10px;' + 'font-size:0.82rem; font-weight:600;'; bar.innerHTML = 'يتوفر تحديث للتطبيق' + ''; document.body.appendChild(bar); document.getElementById('btn-do-update').addEventListener('click', async () => { try{ const regs = await navigator.serviceWorker.getRegistrations(); await Promise.all(regs.map(r => r.unregister())); }catch(e){ /* نكمل على أي حال */ } location.reload(); }); } // ============================================================ // التهيئة — الترتيب مقصود // ============================================================ // يجب أن تُنفَّذ بعد تعريف كل المتغيّرات أعلاه. استدعاء loadData // قبل تعريف OFFLINE_CARS كان يُسقط التحميل بالكامل عند انقطاع الشبكة. (async () => { try{ applyLang(CUR_LANG); }catch(e){ console.warn('تعذّر تطبيق اللغة:', e); } try{ await loadOfflineCache(); // نقرأ النسخة المحفوظة أولاً }catch(e){ console.warn('تعذّر قراءة النسخة المحلية:', e); } try{ await refreshAccess(); // يضبط الصلاحية وزر المزامنة ودليل Lishi }catch(e){ console.warn('تعذّر تحديث حالة الوصول:', e); } try{ await loadData(); // تعبئة قائمة الماركات }catch(e){ console.error(e); statusEl.textContent = 'تعذّر تحميل القوائم: ' + (e && e.message ? e.message : e); } })(); // سنة التذييل — تتحدث تلقائياً كل عام (function(){ const y = document.getElementById('foot-year'); if(y) y.textContent = new Date().getFullYear(); })();