${waiting>0?’يمكنك طلب رابط جديد بعد’:’يمكنك طلب الرابط الآن’}${waiting>0?formatPhoneCountdown(waiting):’متاح’}
${adminEscape(reviewLabel)}`;
}
const shell=document.querySelector(‘.report-shell’);if(shell)shell.scrollTo({top:0,behavior:force?’auto’:’smooth’});
if(stage===1&&state.maps.mini)setTimeout(()=>state.maps.mini.invalidateSize(),80);
return true;
}
async function handleReportSubmit(e){
e.preventDefault();
showConfirmModal(“هل أنت متأكد من إرسال هذا التقرير؟”, async () => {
const data=Object.fromEntries(new FormData(e.target).entries());
data.lat=document.getElementById(‘lat’).value;data.lon=document.getElementById(‘lon’).value;
if (data.type === ‘assault_mode’) {
if (!data.assault_subtype) { showAppModal(‘خطأ’, “يرجى تحديد نوع الاعتداء”, ‘error’); return; }
data.type = data.assault_subtype;
delete data.assault_subtype;
}
data.is_circular_flight = 0;
data.aircraft_direction = null;
if(!data.lat){showAppModal(‘خطأ’, “حدد الموقع”, ‘error’);return;}
if(!data.city || data.city.trim() === “”) { showAppModal(‘خطأ’, “يرجى تحديد المنطقة (اسم القرية أو المدينة)!”, ‘error’); return; }
try{const res=await fetch(`${API_URL}?action=add_report`,{method:’POST’,body:JSON.stringify(data)});const result=await res.json();if(res.ok){showAppModal(‘وصل بلاغك’, “ظهر البلاغ كبلاغ منفرد، وسيتحوّل إلى متقاطع جماعياً عند ورود بلاغ مستقل قريب.”, ‘success’);e.target.reset();document.getElementById(‘lat’).value=””;document.getElementById(‘lon’).value=””;document.getElementById(‘selected-location-name’).textContent=”لم تحدد موقعاً بعد”;delete document.getElementById(‘selected-location-name’).dataset.state;toggleReportStage(1,true);router.navigate(‘home’);}else showAppModal(‘تعذر الإرسال’, result.error||”فشل الإرسال”, ‘error’);}catch(x){showAppModal(‘خطأ’, “خطأ في الاتصال”, ‘error’);}
});
}
// — STATS OVERHAUL LOGIC —
state.statsData = null;
state.statsPeriod = ‘daily’;
state.statsMapMode=”markers”; // ‘markers’ | ‘heat’
state.statsHeatPoints = [];
state.statsCurrentFilter=”all”;
function stepStatsDate(days) {
const input = document.getElementById(‘stats-date’);
const curVal = input.value || getBeirutDateString();
const d = new Date(curVal);
d.setDate(d.getDate() + days);
input.value = d.toISOString().split(‘T’)[0];
loadStats();
}
function setStatsToday() {
const input = document.getElementById(‘stats-date’);
input.value = getBeirutDateString();
loadStats();
}
function switchStatsPeriod(period) {
state.statsPeriod = period;
document.querySelectorAll(‘.period-btn’).forEach(b => {
b.classList.toggle(‘active’, b.dataset.period === period);
});
updateStatsKPIs();
}
function updateStatsKPIs() {
if(!state.statsData) return;
const p = state.statsPeriod;
let tot = state.statsData.totals;
if(p === ‘weekly’) tot = state.statsData.weekly_totals;
if(p === ‘monthly’) tot = state.statsData.monthly_totals;
if(!tot) tot = {};
const totalCount = parseInt(tot.total || 0, 10);
const uav = parseInt(tot.uav || 0, 10);
const fighter = parseInt(tot.fighter || 0, 10);
const shelling = parseInt(tot.shelling || 0, 10);
const drone = parseInt(tot.drone || 0, 10);
const heli = parseInt(tot.helicopter || 0, 10);
const calcPct = (v) => totalCount > 0 ? `${Math.round((v / totalCount) * 100)}%` : ‘0%’;
document.getElementById(‘kpi-uav’).innerText = uav;
document.getElementById(‘kpi-uav-pct’).innerText = calcPct(uav);
document.getElementById(‘kpi-uav-bar’).style.width = calcPct(uav);
document.getElementById(‘kpi-fighter’).innerText = fighter;
document.getElementById(‘kpi-fighter-pct’).innerText = calcPct(fighter);
document.getElementById(‘kpi-fighter-bar’).style.width = calcPct(fighter);
document.getElementById(‘kpi-shelling’).innerText = shelling;
document.getElementById(‘kpi-shelling-pct’).innerText = calcPct(shelling);
document.getElementById(‘kpi-shelling-bar’).style.width = calcPct(shelling);
document.getElementById(‘kpi-drone’).innerText = drone;
document.getElementById(‘kpi-drone-pct’).innerText = calcPct(drone);
document.getElementById(‘kpi-drone-bar’).style.width = calcPct(drone);
document.getElementById(‘kpi-helicopter’).innerText = heli;
document.getElementById(‘kpi-helicopter-pct’).innerText = calcPct(heli);
document.getElementById(‘kpi-helicopter-bar’).style.width = calcPct(heli);
const periodLabel = p === ‘daily’ ? ‘اليوم’ : (p === ‘weekly’ ? ‘آخر 7 أيام’ : ‘هذا الشهر’);
document.getElementById(‘stats-total-badge’).innerText = `${totalCount} بلاغ (${periodLabel})`;
}
function setStatsMapMode(mode) {
state.statsMapMode = mode;
const btnMarkers = document.getElementById(‘btn-mode-markers’);
const btnHeat = document.getElementById(‘btn-mode-heat’);
if(btnMarkers) btnMarkers.classList.toggle(‘active’, mode === ‘markers’);
if(btnHeat) btnHeat.classList.toggle(‘active’, mode === ‘heat’);
const labelEl = document.getElementById(‘stats-map-mode-label’);
if(!state.maps.stats) return;
if(mode === ‘heat’) {
// Remove icon markers
state.statsMarkers.forEach(m => state.maps.stats.removeLayer(m));
// Add heat layer
if(!state.heatLayer && state.statsHeatPoints.length > 0) {
state.heatLayer = L.heatLayer(state.statsHeatPoints, {
radius: 32, blur: 24, maxZoom: 16, minOpacity: 0.45,
gradient: { 0.2: ‘#38bdf8’, 0.4: ‘#10b981’, 0.6: ‘#eab308’, 0.8: ‘#f97316’, 1.0: ‘#ef4444’ }
});
}
if(state.heatLayer) state.heatLayer.addTo(state.maps.stats);
if(labelEl) labelEl.innerHTML = ‘ خريطة الكثافة الحرارية (Heatmap)’;
} else {
// Remove heat layer
if(state.heatLayer) state.maps.stats.removeLayer(state.heatLayer);
// Re-add icon markers
state.statsMarkers.forEach(m => m.addTo(state.maps.stats));
if(labelEl) labelEl.innerHTML = ‘ عرض الرموز المباشرة’;
}
}
function recenterStatsMap() {
if(state.maps.stats) {
state.maps.stats.setView([33.8547, 35.8623], 9);
}
}
function filterStatsList(type) {
state.statsCurrentFilter = type;
document.querySelectorAll(‘#stats-type-filter .filter-chip’).forEach(c => {
c.classList.toggle(‘active’, c.dataset.type === type);
});
renderStatsList();
}
function renderStatsList() {
if(!state.statsData || !state.statsData.details) return;
const filter = state.statsCurrentFilter;
const rows = state.statsData.details.filter(r => {
if(filter === ‘all’) return true;
if(filter === ‘shelling’) return assaultTypes.includes(r.type);
if(filter === ‘drone’) return [‘drone’, ‘bomb_quad’].includes(r.type);
if(filter === ‘uav’) return [‘uav’, ‘raid_drone’, ‘bomb_drone’].includes(r.type);
if(filter === ‘fighter’) return [‘fighter’, ‘raid_fighter’].includes(r.type);
return r.type === filter;
});
const tb = document.getElementById(‘stats-table-body’);
const mobList = document.getElementById(‘stats-mobile-list’);
if(tb) tb.innerHTML = ”;
if(mobList) mobList.innerHTML = ”;
if(rows.length === 0) {
if(tb) tb.innerHTML = ‘
‘;
if(mobList) mobList.innerHTML = ‘لا توجد بلاغات مسجلة لهذا النوع في هذا اليوم
‘;
return;
}
rows.forEach(r => {
const c = assaultTypes.includes(r.type) ? ‘text-shelling font-bold’ : (r.type===’uav’?’text-danger font-bold’:(r.type===’fighter’?’text-accent font-bold’:’text-warning font-bold’));
const s = translate.source[r.report_source] || ‘سماع’;
const sIcon = r.report_source === ‘visual’ ? ‘‘ : ‘‘;
const timeStr = formatBeirutTime(r.created_at);
const cityName = adminEscape(r.city_name || ‘منطقة غير محددة’);
const typeLabel = translate.type[r.type] || r.type;
// Desktop Table Row
if(tb) {
tb.innerHTML += `
`;
}
// Mobile Card Item
if(mobList) {
mobList.innerHTML += `
${eventIconMarkup(r.type, ‘w-7 h-7 object-contain drop-shadow-sm’)}
${typeLabel}
• ${sIcon} ${s}
${cityName}
${timeStr}
${!isNaN(parseFloat(r.lat)) ? ` الخريطة` : ”}
`;
}
});
}
function focusStatsReport(lat, lon) {
if(state.maps.stats) {
state.maps.stats.setView([lat, lon], 12, { animate: true });
const card = document.querySelector(‘.stats-map-card’);
if(card) window.scrollTo({ top: card.offsetTop – 60, behavior: ‘smooth’ });
}
}
const arabicDays = [‘الأحد’, ‘الاثنين’, ‘الثلاثاء’, ‘الأربعاء’, ‘الخميس’, ‘الجمعة’, ‘السبت’];
async function loadStats(){
const d = document.getElementById(‘stats-date’).value || getBeirutDateString();
const dateBadge = document.getElementById(‘stats-map-date’);
if(dateBadge) dateBadge.innerText = d;
// Single Unified Map
if(!state.maps.stats) {
state.maps.stats = L.map(‘stats-map’, {
zoomControl: false,
zoomSnap: 0.5,
scrollWheelZoom: false // Prevent touch/wheel trapping
}).setView([33.8547, 35.8623], 9);
L.tileLayer(‘https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png’, {
className: ‘ajwa-soft-tiles’,
maxZoom: 19,
attribution: ‘© OpenStreetMap’
}).addTo(state.maps.stats);
L.control.zoom({ position: ‘topright’ }).addTo(state.maps.stats);
}
try {
const r = await fetch(`${API_URL}?action=get_stats&date=${d}&t=${Date.now()}`);
const j = await r.json();
state.statsData = j;
// Update date subtitle
const dateObj = new Date(d);
const dayName = !isNaN(dateObj.getTime()) ? arabicDays[dateObj.getDay()] : ”;
const subEl = document.getElementById(‘period-sub-daily’);
if(subEl) subEl.innerText = dayName;
// Update KPIs
updateStatsKPIs();
// Clear markers
state.statsMarkers.forEach(mk => state.maps.stats.removeLayer(mk));
state.statsMarkers = [];
state.statsHeatPoints = [];
let geoReportsCount = 0;
const validDetails = j.details.filter(r => !isNaN(parseFloat(r.lat)) && !isNaN(parseFloat(r.lon)));
const dispersedCoords = (window.AjwaData && window.AjwaData.disperseCoordinates)
? window.AjwaData.disperseCoordinates(validDetails, r => parseFloat(r.lat), r => parseFloat(r.lon))
: (function(items){
const clusters = [];
items.forEach(item => {
const lat = parseFloat(item.lat), lon = parseFloat(item.lon);
let cluster = clusters.find(c => Math.hypot(lat – c.baseLat, (lon – c.baseLon) * Math.cos(c.baseLat * Math.PI / 180)) {
const n = c.items.length;
const cosLat = Math.cos(c.baseLat * Math.PI / 180) || 0.835;
if (n === 1) m.set(c.items[0], [c.baseLat, c.baseLon]);
else {
const r = n === 2 ? 0.0020 : (n {
const angle = (2 * Math.PI * i) / n – (Math.PI / 2);
m.set(it, [c.baseLat + r * Math.sin(angle), c.baseLon + (r / cosLat) * Math.cos(angle)]);
});
}
});
return m;
})(validDetails);
validDetails.forEach(r => {
geoReportsCount++;
const timeStr = formatBeirutTime(r.created_at);
const ic = L.divIcon({
html: `${eventIconMarkup(r.type,’map-event-icon’)}
`,
className: ‘custom-div-icon’,
iconSize: [40, 40],
iconAnchor: [20, 20],
popupAnchor: [0, -24]
});
const coords = dispersedCoords.get(r) || [parseFloat(r.lat), parseFloat(r.lon)];
const marker = L.marker(coords, {icon: ic}).bindPopup(`${translate.type[r.type] || r.type}
${r.city_name || ”}
${timeStr}`);
state.statsMarkers.push(marker);
let intensity = 0.55;
if(assaultTypes.includes(r.type)) intensity = 1.0;
state.statsHeatPoints.push([parseFloat(r.lat), parseFloat(r.lon), intensity]);
});
const countEl = document.getElementById(‘stats-map-count-badge’);
if(countEl) countEl.innerText = `${geoReportsCount} حدث محدد جغرافياً`;
// Set map layer based on current mode (default: markers, heatmap only on-demand)
setStatsMapMode(state.statsMapMode);
if(state.statsMarkers && state.statsMarkers.length > 0) {
try {
const group = L.featureGroup(state.statsMarkers);
state.maps.stats.fitBounds(group.getBounds().pad(0.25));
} catch(e) {}
} else if(state.maps.stats) {
state.maps.stats.setView([33.8547, 35.8623], 9);
}
// Render activity list
renderStatsList();
// Format Chart.js Activity curve
if(j.chart && j.chart.length > 0) {
const chartLabels = j.chart.map(i => {
try {
const dt = new Date(i.date);
return `${arabicDays[dt.getDay()]} ${dt.getDate()}`;
} catch(e) { return i.date; }
});
const chartValues = j.chart.map(i => i.count);
const peak = Math.max(…chartValues, 0);
const peakBadge = document.getElementById(‘chart-peak-badge’);
if(peakBadge) peakBadge.innerText = `أعلى نشاط: ${peak} بلاغ`;
if(state.chart) state.chart.destroy();
const canvas = document.getElementById(‘statsChart’);
if(canvas) {
const ctx = canvas.getContext(‘2d’);
const gradient = ctx.createLinearGradient(0, 0, 0, 240);
gradient.addColorStop(0, ‘rgba(56, 189, 248, 0.45)’);
gradient.addColorStop(1, ‘rgba(56, 189, 248, 0.0)’);
state.chart = new Chart(ctx, {
type: ‘line’,
data: {
labels: chartLabels,
datasets: [{
label: ‘عدد البلاغات’,
data: chartValues,
borderColor: ‘#38bdf8’,
borderWidth: 3,
backgroundColor: gradient,
fill: true,
tension: 0.35,
pointBackgroundColor: ‘#38bdf8’,
pointBorderColor: ‘#0f172a’,
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: ‘#0f172a’,
titleColor: ‘#38bdf8’,
bodyColor: ‘#ffffff’,
borderColor: ‘#334155’,
borderWidth: 1,
padding: 10,
displayColors: false,
callbacks: {
label: (context) => ` ${context.parsed.y} بلاغ مسجل`
}
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: ‘rgba(255, 255, 255, 0.06)’ },
ticks: { color: ‘#94a3b8’, font: { size: 10 } }
},
x: {
grid: { display: false },
ticks: { color: ‘#94a3b8’, font: { size: 11, weight: ‘bold’ } }
}
}
}
});
}
}
} catch(x){
console.error(“loadStats error:”, x);
}
}
async function generateAISummary(){
if (!state.user || state.user.is_active != 1) return;
document.getElementById(‘modal-ai-analysis’).classList.remove(‘hidden’);
document.getElementById(‘ai-analysis-content’).innerHTML=’
جاري التحليل…
‘;
try {
const d = document.getElementById(‘stats-date’).value || getBeirutDateString();
const r = await fetch(`${API_URL}?action=get_stats&date=${d}&t=${Date.now()}`);
const j = await r.json();
const c = `التاريخ: ${d}\nإجمالي: ${j.totals.total}\nالمناطق: ${j.details.slice(0,10).map(x=>x.city_name).join(‘, ‘)}`;
const s = await callGemini(“لخص الوضع”, c, d);
document.getElementById(‘ai-analysis-content’).innerHTML = marked.parse(s);
} catch(x) {
document.getElementById(‘ai-analysis-content’).innerHTML = ‘فشل في التحليل’;
}
}
async function callGemini(p, c, d = null) {
if(!aiEnabled) return “غير مفعل”;
const payload = { prompt: p, context: c }; if(d) payload.date = d;
const r = await fetch(`${API_URL}?action=ask_ai`, { method: ‘POST’, body: JSON.stringify(payload) });
const j = await r.json(); return j.response || “خطأ”;
}
const adminEscape=value=>String(value??”).replace(/[&”‘]/g,c=>({‘&’:’&’,'<':'’:’>’,'”‘:'”‘,”‘”:”’}[c]));
const adminSearchText=value=>String(value??”).toLowerCase().trim();
async function refreshAdmin(){
const refreshButtons=document.querySelectorAll(‘#view-admin button[onclick=”refreshAdmin()”]’);refreshButtons.forEach(button=>button.disabled=true);
try{
const r=await fetch(`${API_URL}?action=admin_dashboard&admin_id=${state.user.id}&t=${Date.now()}`,{cache:’no-store’});const j=await r.json();
if(!r.ok||!Array.isArray(j.users)||!Array.isArray(j.reports))throw new Error(j.error||’تعذر تحميل بيانات الإدارة’);
state.adminData=j;
const userIds=new Set(j.users.map(item=>item.id)),reportIds=new Set(j.reports.map(item=>item.id));
state.adminSelection.users.forEach(id=>{if(!userIds.has(id))state.adminSelection.users.delete(id);});state.adminSelection.reports.forEach(id=>{if(!reportIds.has(id))state.adminSelection.reports.delete(id);});
document.getElementById(‘admin-total-users’).textContent=j.users.length;document.getElementById(‘admin-total-reports’).textContent=j.reports.length;document.getElementById(‘admin-total-reports-detail’).textContent=`${j.reports.filter(report=>Number(report.is_active)!==0).length} ظاهر · ${j.reports.filter(report=>Number(report.is_active)===0).length} مخفي`;document.getElementById(‘admin-banned-users’).textContent=j.users.filter(u=>Number(u.is_banned)===1).length;document.getElementById(‘admin-verified-users’).textContent=j.users.filter(u=>u.phone_verified_at).length;
document.getElementById(‘admin-users-tab-count’).textContent=j.users.length;document.getElementById(‘admin-reports-tab-count’).textContent=j.reports.length;document.getElementById(‘admin-push-count’).textContent=j.push_count||0;
renderAdminTables();
}catch(error){showAppModal(‘تعذر التحديث’,error.message||’فشل تحميل لوحة الإدارة’,’error’);}
finally{refreshButtons.forEach(button=>button.disabled=false);}
}
function switchAdminTab(tab){
state.adminTab=tab;
document.getElementById(‘admin-users’).classList.toggle(‘hidden’,tab!==’users’);document.getElementById(‘admin-reports’).classList.toggle(‘hidden’,tab!==’reports’);document.getElementById(‘admin-settings’).classList.toggle(‘hidden’,tab!==’settings’);
document.getElementById(‘admin-toolbar’).classList.toggle(‘hidden’,tab===’settings’);document.getElementById(‘admin-user-filters’).classList.toggle(‘hidden’,tab!==’users’);document.getElementById(‘admin-report-filters’).classList.toggle(‘hidden’,tab!==’reports’);
[‘users’,’reports’,’settings’].forEach(name=>{const button=document.getElementById(`btn-admin-${name}`);button.classList.toggle(‘is-active’,name===tab);button.setAttribute(‘aria-selected’,String(name===tab));});
const search=document.getElementById(‘admin-search’);if(search)search.placeholder=tab===’reports’?’ابحث بالمنطقة أو المبلّغ’:’ابحث بالاسم أو البريد أو الهاتف’;
if(tab===’settings’){document.getElementById(‘admin-bulk-bar’).classList.add(‘hidden’);loadAdminSettings();}else renderAdminTables();
}
function adminFiltersChanged(){state.pagination.adminUsers=1;state.pagination.adminReports=1;renderAdminTables();}
function clearAdminSearch(){const input=document.getElementById(‘admin-search’);input.value=””;input.focus();adminFiltersChanged();}
function resetAdminFilters(){
document.getElementById(‘admin-search’).value=””;[‘admin-filter-users’,’admin-filter-verification’,’admin-filter-reports’,’admin-filter-source’,’admin-filter-report-status’,’admin-filter-date’].forEach(id=>{const input=document.getElementById(id);if(input)input.value=”all”;});adminFiltersChanged();
}
async function loadAdminSettings(){try{const r=await fetch(`${API_URL}?action=get_admin_settings`);const j=await r.json();const input=document.getElementById(‘admin-gemini-key’);input.value=””;input.placeholder=j.gemini_configured?’المفتاح محفوظ — أدخل مفتاحاً جديداً لاستبداله’:’أدخل مفتاح Gemini’;}catch(x){}}
async function saveAdminSettings(){const k=document.getElementById(‘admin-gemini-key’).value;try{const r=await fetch(`${API_URL}?action=update_settings`,{method:’POST’,body:JSON.stringify({admin_id:state.user.id,gemini_key:k})});if(r.ok){showAppModal(‘تم’, “تم حفظ الإعدادات”, ‘success’);fetchConfig();}else showAppModal(‘خطأ’, “فشل الحفظ”, ‘error’);}catch(x){}}
function toggleKeyVisibility(){const i=document.getElementById(‘admin-gemini-key’);i.type=i.type===’password’?’text’:’password’;}
function renderPaginationControls(totalItems, itemsPerPage, currentPage, context) {
const totalPages = Math.ceil(totalItems / itemsPerPage); if(totalPages <= 1) return '';
let html = `السابق`;
let startPage = Math.max(1, currentPage – 2); let endPage = Math.min(totalPages, currentPage + 2);
for (let i = startPage; i {
if(context === ‘users’) state.pagination.adminUsers = page;
else if(context === ‘reports’) state.pagination.adminReports = page;
else if(context === ‘profile’) state.pagination.profileReports = page;
if(context === ‘profile’) renderMyReports();
else renderAdminTables();
}
function sortData(data, sortConfig) {
return […data].sort((a, b) => {
let va = a[sortConfig.col], vb = b[sortConfig.col];
if (typeof va === ‘string’) va = va.toLowerCase();
if (typeof vb === ‘string’) vb = vb.toLowerCase();
if (va vb) return sortConfig.dir === ‘asc’ ? 1 : -1;
return 0;
});
}
function adminSort(context, col) {
const current = state.adminSort[context];
if(current.col === col) current.dir = current.dir === ‘asc’ ? ‘desc’ : ‘asc’;
else { current.col = col; current.dir=”asc”; }
renderAdminTables();
}
function filteredAdminUsers(){
const query=adminSearchText(document.getElementById(‘admin-search’).value),status=document.getElementById(‘admin-filter-users’).value,verification=document.getElementById(‘admin-filter-verification’).value;
let users=state.adminData.users.filter(user=>[user.username,user.email,user.phone_number,user.registration_ip].some(value=>adminSearchText(value).includes(query)));
if(status===’active’)users=users.filter(user=>Number(user.is_active)===1&&Number(user.is_banned)===0);if(status===’banned’)users=users.filter(user=>Number(user.is_banned)===1);if(status===’inactive’)users=users.filter(user=>Number(user.is_active)===0);
if(verification===’phone’)users=users.filter(user=>user.phone_verified_at);if(verification===’email’)users=users.filter(user=>user.email_verified_at);if(verification===’unverified’)users=users.filter(user=>!user.phone_verified_at&&!user.email_verified_at);
return sortData(users,state.adminSort.users);
}
function filteredAdminReports(){
const query=adminSearchText(document.getElementById(‘admin-search’).value),type=document.getElementById(‘admin-filter-reports’).value,source=document.getElementById(‘admin-filter-source’).value,status=document.getElementById(‘admin-filter-report-status’).value,period=document.getElementById(‘admin-filter-date’).value;
let reports=state.adminData.reports.filter(report=>[report.city_name,report.username,translate.type[report.type],report.type].some(value=>adminSearchText(value).includes(query)));
if(type!==’all’)reports=reports.filter(report=>type===’assault’?assaultTypes.includes(report.type):report.type===type);if(source!==’all’)reports=reports.filter(report=>(report.report_source===’audio’?’auditory’:report.report_source)===source);
if(status===’active’)reports=reports.filter(report=>Number(report.is_active)!==0);if(status===’inactive’)reports=reports.filter(report=>Number(report.is_active)===0);
if(period!==’all’){const days=period===’today’?0:period===’7d’?6:29;const cutoff=new Date();cutoff.setHours(0,0,0,0);cutoff.setDate(cutoff.getDate()-days);reports=reports.filter(report=>{const parsed=new Date(String(report.created_at||”).replace(‘ ‘,’T’));return !Number.isNaN(parsed.getTime())&&parsed>=cutoff;});}
return sortData(reports,state.adminSort.reports);
}
function renderAdminTables(){
if(!state.adminData||!Array.isArray(state.adminData.users)||!Array.isArray(state.adminData.reports))return;
const itemsPerPage = parseInt(document.getElementById(‘admin-rows’).value);
const users=filteredAdminUsers();
const userPages=Math.max(1,Math.ceil(users.length/itemsPerPage));state.pagination.adminUsers=Math.min(state.pagination.adminUsers,userPages);const usersPage=state.pagination.adminUsers;
const usersSlice = users.slice((usersPage – 1) * itemsPerPage, usersPage * itemsPerPage);
state.adminVisibleIds.users=usersSlice.filter(user=>user.role!==’admin’).map(user=>user.id);
const ub = document.getElementById(‘admin-users-body’); ub.innerHTML=”;
if(!usersSlice.length)ub.innerHTML=’
‘;
usersSlice.forEach(u=>{
const isAdmin=u.role===’admin’,checked=state.adminSelection.users.has(u.id);let status=””;if(Number(u.is_banned)===1)status=”محظور”;else if(Number(u.is_active)===1)status=”نشط”;else status=”غير مفعل”;
const phoneVerification=`${u.phone_verified_at?’واتساب موثق’:’واتساب غير موثق’}${(!u.phone_verified_at&&u.phone_number&&!isAdmin)?`إرسال الرابط`:”}`;
const verification=`${phoneVerification}${u.email_verified_at?’بريد موثق’:’بريد غير موثق’}`;
const actions=isAdmin?’مدير النظام’:`${Number(u.is_active)===1?’تعطيل’:’تفعيل’}${Number(u.is_banned)===1?’فك الحظر’:’حظر’}كلمة المرورحذف`;
ub.innerHTML+=`
${verification}
${actions}
`;
});
document.getElementById(‘admin-users-pagination’).innerHTML = renderPaginationControls(users.length, itemsPerPage, usersPage, ‘users’);
document.getElementById(‘admin-users-result-count’).textContent=`عرض ${usersSlice.length} من ${users.length} عضواً`;
const reports=filteredAdminReports();const reportPages=Math.max(1,Math.ceil(reports.length/itemsPerPage));state.pagination.adminReports=Math.min(state.pagination.adminReports,reportPages);const reportsPage=state.pagination.adminReports;
const reportsSlice = reports.slice((reportsPage – 1) * itemsPerPage, reportsPage * itemsPerPage);
state.adminVisibleIds.reports=reportsSlice.map(report=>report.id);
const rb = document.getElementById(‘admin-reports-body’); rb.innerHTML=”;
if(!reportsSlice.length)rb.innerHTML=’
‘;
reportsSlice.forEach(r=>{
const c = assaultTypes.includes(r.type) ? ‘text-shelling’ : (r.type===’uav’?’text-danger’:(r.type===’fighter’?’text-accent’:’text-warning’));
const time = formatBeirutTime(r.created_at);
const date = formatBeirutDateTime(r.created_at).split(‘,’)[0];
const checked=state.adminSelection.reports.has(r.id),sourceLabel=(r.report_source===’visual’?’مشاهدة’:’سماع’),reportActive=Number(r.is_active)!==0;
rb.innerHTML+=`
${reportActive?’إخفاء’:’إظهار’}تعديلحذف
`;
});
document.getElementById(‘admin-reports-pagination’).innerHTML = renderPaginationControls(reports.length, itemsPerPage, reportsPage, ‘reports’);
document.getElementById(‘admin-reports-result-count’).textContent=`عرض ${reportsSlice.length} من ${reports.length} بلاغاً`;
document.querySelectorAll(‘[data-sort-context]’).forEach(icon=>{const config=state.adminSort[icon.dataset.sortContext],active=config.col===icon.dataset.sortCol;icon.className=`sort-icon fa-solid ${active?(config.dir===’asc’?’fa-sort-up’:’fa-sort-down’):’fa-sort’}${active?’ sort-active’:”}`;});
[‘users’,’reports’].forEach(context=>{const all=document.getElementById(`admin-select-all-${context}`),ids=state.adminVisibleIds[context];if(all){all.checked=ids.length>0&&ids.every(id=>state.adminSelection[context].has(id));all.indeterminate=ids.some(id=>state.adminSelection[context].has(id))&&!all.checked;}});
updateAdminBulkBar();
}
function toggleAdminSelection(context,id,checked){if(checked)state.adminSelection[context].add(id);else state.adminSelection[context].delete(id);const all=document.getElementById(`admin-select-all-${context}`),ids=state.adminVisibleIds[context];if(all){all.checked=ids.length>0&&ids.every(item=>state.adminSelection[context].has(item));all.indeterminate=ids.some(item=>state.adminSelection[context].has(item))&&!all.checked;}updateAdminBulkBar();}
function toggleAdminPageSelection(context,checked){state.adminVisibleIds[context].forEach(id=>checked?state.adminSelection[context].add(id):state.adminSelection[context].delete(id));renderAdminTables();}
function clearAdminSelection(){if(state.adminTab===’users’||state.adminTab===’reports’)state.adminSelection[state.adminTab].clear();renderAdminTables();}
function updateAdminBulkBar(){const context=state.adminTab,count=(context===’users’||context===’reports’)?state.adminSelection[context].size:0,bar=document.getElementById(‘admin-bulk-bar’);bar.classList.toggle(‘hidden’,count===0||context===’settings’);document.getElementById(‘admin-selection-count’).textContent=count;document.getElementById(‘admin-user-bulk-actions’).classList.toggle(‘hidden’,context!==’users’);document.getElementById(‘admin-report-bulk-actions’).classList.toggle(‘hidden’,context!==’reports’);}
function bulkAdminAction(operation){const context=operation.endsWith(‘_reports’)?’reports’:’users’,ids=[…state.adminSelection[context]];if(!ids.length)return;const labels={activate_users:’تفعيل الحسابات المحددة’,deactivate_users:’تعطيل الحسابات المحددة’,ban_users:’حظر الحسابات المحددة’,unban_users:’فك حظر الحسابات المحددة’,delete_users:’حذف الحسابات المحددة نهائياً’,activate_reports:’إظهار البلاغات المحددة للعامة’,deactivate_reports:’إخفاء البلاغات المحددة عن العامة’,delete_reports:’حذف البلاغات المحددة نهائياً’};showConfirmModal(`هل تريد ${labels[operation]}؟`,async()=>{try{const response=await fetch(`${API_URL}?action=admin_bulk_action`,{method:’POST’,body:JSON.stringify({type:operation,target_ids:ids})});const result=await response.json();if(!response.ok)throw new Error(result.error||’فشل تنفيذ الإجراء’);state.adminSelection[context].clear();await refreshAdmin();showAppModal(‘تم التنفيذ’,`تم تحديث ${result.affected||ids.length} عنصراً`,’success’);}catch(error){showAppModal(‘تعذر التنفيذ’,error.message,’error’);}});}
async function openUserModal(uid){
const u=state.adminData.users.find(x=>x.id===uid);if(!u)return;
toggleModal(‘modal-user’, true);
document.getElementById(‘modal-user-content’).innerHTML = ‘
جاري التحميل…
‘;
try {
const r=await fetch(`${API_URL}?action=get_user_reports&target_id=${uid}&t=${Date.now()}`);
const j=await r.json();
const reps=j.data||[];
document.getElementById(‘modal-user-content’).innerHTML=`
IP
${u.registration_ip||’-‘}
تاريخ الانضمام
${u.created_at.split(‘ ‘)[0]}
آخر التقارير
| النوع | المنطقة | الوقت |
|---|---|---|
| ${translate.type[x.type]} | ${x.city_name} | ${formatBeirutDateTime(x.created_at)} |
`;
} catch(e) {
document.getElementById(‘modal-user-content’).innerHTML = ‘
فشل تحميل البيانات
‘;
}
}
async function adminAction(t,tid){
showConfirmModal(“هل أنت متأكد من هذا الإجراء؟”, async () => {
try{const response=await fetch(`${API_URL}?action=admin_action`,{method:’POST’,body:JSON.stringify({admin_id:state.user.id,target_id:tid,type:t})});const result=await response.json();if(!response.ok)throw new Error(result.error||’فشل تنفيذ الإجراء’);await refreshAdmin();showAppModal(‘تم’,’تم تنفيذ الإجراء بنجاح’,’success’);}catch(error){showAppModal(‘تعذر التنفيذ’,error.message,’error’);}
});
}
async function adminResendPhoneActivation(userId,button){
const original=button.innerHTML;
button.disabled=true;
button.innerHTML=’جارٍ الإرسال’;
try{
const response=await fetch(`${API_URL}?action=admin_action`,{method:’POST’,body:JSON.stringify({admin_id:state.user.id,target_id:userId,type:’resend_phone_activation’})});
const result=await response.json();
if(!response.ok)throw new Error(result.error||’تعذر إرسال رابط التوثيق’);
showAppModal(‘تم الإرسال’,result.message||’تم إرسال رابط التوثيق عبر واتساب’,’success’);
}catch(error){showAppModal(‘تعذر الإرسال’,error.message,’error’);}
finally{button.disabled=false;button.innerHTML=original;}
}
let editCityTimeout = null;
function handleEditCityInput(q) {
clearTimeout(editCityTimeout);
const resultsDiv = document.getElementById(‘edit-city-results’);
const spinner = document.getElementById(‘edit-city-spinner’);
if (!resultsDiv) return;
const query = (q || ”).trim();
if (query.length {
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&countrycodes=lb&addressdetails=1&limit=25&accept-language=ar`);
const data = await res.json();
if (spinner) spinner.classList.add(‘hidden’);
resultsDiv.innerHTML = ”;
const allowedTypes = [‘city’, ‘town’, ‘village’, ‘hamlet’, ‘administrative’, ‘suburb’, ‘neighbourhood’, ‘locality’];
const filtered = Array.isArray(data) ? data.filter(item => {
if (item.address && item.address.country_code !== ‘lb’) return false;
const isAllowed = (item.class === ‘place’ && allowedTypes.includes(item.type)) ||
(item.class === ‘boundary’ && item.type === ‘administrative’) ||
allowedTypes.includes(item.type);
return isAllowed;
}) : [];
const itemsToShow = filtered.length ? filtered.slice(0, 7) : (Array.isArray(data) ? data.slice(0, 5) : []);
if (itemsToShow.length > 0) {
resultsDiv.classList.remove(‘hidden’);
itemsToShow.forEach(item => {
let cleanName = (item.address && (item.address.village || item.address.town || item.address.city || item.address.suburb || item.address.hamlet)) || item.name || query;
const opt = document.createElement(‘div’);
opt.className=”p-2.5 hover:bg-blue-600 hover:text-white cursor-pointer border-b border-gray-700 text-right text-xs transition-colors flex justify-between items-center”;
opt.innerHTML = `${cleanName}${parseFloat(item.lat).toFixed(4)}, ${parseFloat(item.lon).toFixed(4)}`;
opt.onclick = () => {
document.getElementById(‘edit-report-city’).value = cleanName;
const latEl = document.getElementById(‘edit-report-lat’);
const lonEl = document.getElementById(‘edit-report-lon’);
const badgeEl = document.getElementById(‘edit-coords-badge’);
if (latEl) latEl.value = item.lat;
if (lonEl) lonEl.value = item.lon;
if (badgeEl) badgeEl.innerText = `${parseFloat(item.lat).toFixed(4)}, ${parseFloat(item.lon).toFixed(4)}`;
resultsDiv.classList.add(‘hidden’);
};
resultsDiv.appendChild(opt);
});
} else {
resultsDiv.classList.add(‘hidden’);
}
} catch(e) {
if (spinner) spinner.classList.add(‘hidden’);
console.error(“Edit city search error:”, e);
}
}, 350);
}
function openEditReportModal(reportId) {
const report = state.adminData.reports.find(r => r.id === reportId); if(!report) return;
document.getElementById(‘edit-report-id’).value = report.id;
document.getElementById(‘edit-report-type’).value = report.type;
document.getElementById(‘edit-report-city’).value = report.city_name || ”;
document.getElementById(‘edit-report-source’).value = report.report_source || ‘visual’;
const latEl = document.getElementById(‘edit-report-lat’);
const lonEl = document.getElementById(‘edit-report-lon’);
const badgeEl = document.getElementById(‘edit-coords-badge’);
if (latEl) latEl.value = report.lat || ”;
if (lonEl) lonEl.value = report.lon || ”;
if (badgeEl) {
if (report.lat && report.lon) {
badgeEl.innerText = `${parseFloat(report.lat).toFixed(4)}, ${parseFloat(report.lon).toFixed(4)}`;
} else {
badgeEl.innerText=”غير محدد”;
}
}
const resEl = document.getElementById(‘edit-city-results’);
if (resEl) { resEl.innerHTML = ”; resEl.classList.add(‘hidden’); }
toggleModal(‘modal-edit-report’, true);
}
async function submitEditReport(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
data.admin_id = state.user.id;
try {
const res = await fetch(`${API_URL}?action=edit_report`, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(data)
});
const j = await res.json();
if (res.ok) {
showAppModal(‘نجاح’, “تم حفظ التعديلات بنجاح”, ‘success’);
toggleModal(‘modal-edit-report’, false);
refreshAdmin();
} else {
showAppModal(‘خطأ’, j.error || “تعذر حفظ التعديلات”, ‘error’);
}
} catch (err) {
showAppModal(‘خطأ’, “فشل الاتصال بالخادم”, ‘error’);
}
}
window.addEventListener(‘load’, async () => {
// 1. تهيئة الخريطة إذا كانت موجودة
try {
if(!state.maps.main && document.getElementById(‘map’)) {
state.maps.main = L.map(‘map’, {zoomControl:false,zoomSnap:.25}).setView([33.86, 35.86], window.innerWidth {
const hv = window.location.hash.slice(1);
if(hv && document.getElementById(`view-${hv}`)) {
router.navigate(hv);
}
});
// 5. التحقق من وجود إحداثيات في الرابط (للمشاركة)
const latParam = u.get(‘lat’);
const lonParam = u.get(‘lon’);
if (latParam && lonParam && Number.isFinite(Number(latParam)) && Number.isFinite(Number(lonParam)) && Math.abs(Number(latParam)) <= 90 && Math.abs(Number(lonParam)) {
if(state.maps.main) {
state.maps.main.setView([latParam, lonParam], 14);
L.popup()
.setLatLng([latParam, lonParam])
.setContent(‘
موقع البلاغ
‘)
.openOn(state.maps.main);
document.getElementById(‘map’).scrollIntoView({behavior: ‘smooth’});
}
}, 1500);
}
// جلب الإعدادات والتقارير
const savedRegion=localStorage.getItem(‘ajwa_push_region’);if(savedRegion&&document.getElementById(‘push-region’))document.getElementById(‘push-region’).value=savedRegion;
await fetchConfig();
fetchMapReports();
});
function urlBase64ToUint8Array(base64String) { const padding = ‘=’.repeat((4 – base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, ‘+’).replace(/_/g, ‘/’); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i {event.preventDefault();deferredInstallPrompt=event;if(localStorage.getItem(‘ajwa_install_dismissed’)!==’1′)document.getElementById(‘install-prompt’).hidden=false;});
async function installAjwaApp(){if(!deferredInstallPrompt)return;await deferredInstallPrompt.prompt();deferredInstallPrompt=null;document.getElementById(‘install-prompt’).hidden=true;}
function dismissInstall(){localStorage.setItem(‘ajwa_install_dismissed’,’1′);document.getElementById(‘install-prompt’).hidden=true;}
async function registerServiceWorker() { if (‘serviceWorker’ in navigator) { try { const registration = await navigator.serviceWorker.register(‘sw.js?v=20260917.7′,{updateViaCache:’none’}); if(‘PushManager’ in window) checkSubscription(registration); } catch (error) { console.error(‘SW Error:’, error); } } }
async function checkSubscription(registration) { const subscription = await registration.pushManager.getSubscription(); if (subscription) { const btnIcon = document.getElementById(‘notif-btn-icon’); if(btnIcon) btnIcon.className = “fa-solid fa-bell text-yellow-400 text-lg”; } else { const prompt = document.getElementById(‘push-prompt’); if(prompt) setTimeout(() => prompt.classList.add(‘show’), 5000); } }
function closePushPrompt() { const p = document.getElementById(‘push-prompt’); if(p) p.classList.remove(‘show’); }
async function subscribeToPush() {
if (!vapidPublicKey) return showAppModal(‘خطأ’, “خطأ في إعدادات الخادم”, ‘error’);
if (‘serviceWorker’ in navigator) {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) });
const region=document.getElementById(‘push-region’)?.value||localStorage.getItem(‘ajwa_push_region’)||’all’;
const payload=subscription.toJSON();payload.region=region;localStorage.setItem(‘ajwa_push_region’,region);
await fetch(`${API_URL}?action=subscribe_push`, { method: ‘POST’, body: JSON.stringify(payload) });
showAppModal(‘تم’, region===’all’?”تم تفعيل تنبيهات كل لبنان “:`تم تفعيل تنبيهات ${region} `, ‘success’);
const icon = document.getElementById(‘notif-btn-icon’); if(icon) icon.className = “fa-solid fa-bell text-yellow-400 text-lg”;
closePushPrompt();
} catch (err) { console.error(err); showAppModal(‘خطأ’, “فشل التفعيل. تأكد من إعطاء الصلاحية.”, ‘error’); }
}
}
async function togglePushSubscription() {
if (‘serviceWorker’ in navigator) {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
if (sub) {
showConfirmModal(“هل تريد إيقاف التنبيهات؟”, async () => {
await sub.unsubscribe();
await fetch(`${API_URL}?action=unsubscribe_push`, {method:’POST’, body:JSON.stringify({endpoint: sub.endpoint})});
document.getElementById(‘notif-btn-icon’).className = “fa-regular fa-bell text-gray-400 text-lg”;
showAppModal(‘تم’, “تم إيقاف التنبيهات.”, ‘success’);
});
} else { subscribeToPush(); }
}
}
