|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
const els = { |
|
lockOverlay: document.getElementById('lockOverlay'), |
|
lockInput: document.getElementById('lockInput'), |
|
lockBtn: document.getElementById('lockBtn'), |
|
lockErr: document.getElementById('lockErr'), |
|
|
|
drop: document.querySelector('#dropArea, #dropZone'), |
|
choose: document.querySelector('#chooseBtn, [data-role="choose"]'), |
|
file: document.getElementById('fileInput'), |
|
fileName: document.getElementById('fileName'), |
|
|
|
month: document.getElementById('monthSelect'), |
|
week: document.getElementById('weekSelect'), |
|
|
|
colComplete: document.getElementById('colComplete'), |
|
colFullfilled: document.getElementById('colFullfilled'), |
|
colInspectorNo: document.getElementById('colInspectorNo'), |
|
colInspectorName:document.getElementById('colInspectorName'), |
|
colResearcher: document.getElementById('colResearcher'), |
|
colPublicId: document.getElementById('colPublicId'), |
|
colWeek: document.getElementById('colWeek'), |
|
colMonth: document.getElementById('colMonth'), |
|
colRegion: document.getElementById('colRegion'), |
|
|
|
saveCols: document.getElementById('saveCols'), |
|
resetCols: document.getElementById('resetCols'), |
|
|
|
btnProcess: document.getElementById('btnProcess'), |
|
btnPer: document.getElementById('btnDownloadPerInspector'), |
|
btnMaster: document.getElementById('btnDownloadMaster'), |
|
|
|
stats: document.getElementById('stats'), |
|
noData: document.getElementById('noData'), |
|
cards: document.getElementById('cards'), |
|
toast: document.getElementById('toast'), |
|
}; |
|
|
|
const show = el => el && el.classList.add('show'); |
|
const hide = el => el && el.classList.remove('show'); |
|
const toast = (msg, type='info')=>{ |
|
const t = els.toast; if (!t) return; |
|
t.textContent = msg; |
|
t.style.display = 'block'; |
|
clearTimeout(t._h); t._h = setTimeout(()=> t.style.display='none', 3500); |
|
t.className = 'toast ' + type; |
|
}; |
|
|
|
|
|
function toWesternDigits(s){ if (s == null) return ''; const map = {'٠':'0','١':'1','٢':'2','٣':'3','٤':'4','٥':'5','٦':'6','٧':'7','٨':'8','٩':'9'}; return String(s).replace(/[٠-٩]/g, d => map[d]); } |
|
function normalizeNum(v){ const s = toWesternDigits(v).trim(); const m = s.match(/\d+/); return m ? String(parseInt(m[0],10)) : s; } |
|
function truthy(v){ |
|
const s = toWesternDigits(v).toString().trim().toLowerCase(); |
|
const neg = ['غير','لا','لم','not','no','غير مكتمل','غير مستوف','غير مستوفي']; |
|
if (neg.some(x=> s.includes(x))) return false; |
|
const pos = ['true','1','yes','y','نعم','مكتمل','منجز','استوفيت','مستوفي','مستوفى','تم','ok','✓','✔','كلي']; |
|
return pos.some(x=> s.includes(x)); |
|
} |
|
function uniqueNonEmpty(arr){ return [...new Set(arr.map(v => v==null? '' : String(v).trim()).filter(Boolean))]; } |
|
function safeName(s){ return String(s||'').replace(/[\\/:*?"<>|]/g,'-').slice(0,50); } |
|
function hash32(str){ let h=2166136261>>>0; str=String(str||''); for(let i=0;i<str.length;i++){ h^=str.charCodeAt(i); h=Math.imul(h,16777619)>>>0; } return h>>>0; } |
|
function rng(seed){ let x=hash32(seed)||1; return ()=>{ x^=x<<13; x>>>=0; x^=x>>>17; x^=x<<5; x>>>=0; return (x>>>0)/0x100000000; }; } |
|
function pickRandomStable(arr, max, seedKey){ if (arr.length <= max) return arr.slice(); const r=rng(seedKey), a=arr.slice(); for(let i=a.length-1;i>0;i--){ const j=Math.floor(r()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a.slice(0, max); } |
|
|
|
|
|
const DEFAULT_COLS = { |
|
nameA:'G', idB:'B', inspectorF:'F', weekH:'H', monthO:'O', regionAR:'AR', |
|
fullyMetAB:'AB', completeBL:'BL', inspectorNo:'' |
|
}; |
|
function loadCols(){ try{ return Object.assign({}, DEFAULT_COLS, JSON.parse(localStorage.getItem('colmap')||'{}')); } catch{ return {...DEFAULT_COLS}; } } |
|
let COLS = loadCols(); |
|
|
|
|
|
(function initLock(){ |
|
const passed = localStorage.getItem('passedAuth') === '1'; |
|
if (!passed) { show(els.lockOverlay); } else { els.lockOverlay.style.display = 'none'; els.lockOverlay.style.pointerEvents='none'; } |
|
els.lockBtn?.addEventListener('click', tryEnter); |
|
els.lockInput?.addEventListener('keydown', e=>{ if (e.key==='Enter') tryEnter(); }); |
|
function tryEnter(){ |
|
const ok = (els.lockInput.value||'').trim() === '2030'; |
|
if (ok){ |
|
localStorage.setItem('passedAuth','1'); |
|
els.lockOverlay.style.display = 'none'; |
|
els.lockOverlay.style.pointerEvents = 'none'; |
|
} else { document.getElementById('lockErr').textContent = 'الرقم السري غير صحيح'; } |
|
} |
|
})(); |
|
|
|
|
|
const state = { rows: [], cols: [], byInspector: new Map(), filtered: [], seed: null }; |
|
|
|
|
|
let worker = null; |
|
try { worker = new Worker('./worker.js'); } catch(e){ console.warn('Worker fail', e); } |
|
if (worker){ |
|
worker.onmessage = (e)=>{ |
|
const { type, payload, error } = e.data || {}; |
|
if (type === 'error') { toast(error || 'خطأ في قراءة الملف','error'); return; } |
|
if (type === 'parsed'){ |
|
state.rows = payload.rows || []; |
|
state.cols = payload.cols || []; |
|
fillColSelectors(state.cols); |
|
populateMonthWeek(state.rows); |
|
state.seed = (()=>{ try{ const a=new Uint32Array(2); crypto.getRandomValues(a); return `${a[0]}-${a[1]}`; }catch{return String(Date.now())} })(); |
|
toast(`تمت قراءة ${state.rows.length} صفًا`, 'success'); |
|
processData(); |
|
} |
|
}; |
|
} |
|
|
|
|
|
function wireUploadArea(){ |
|
const chooseBtn = els.choose; |
|
const drop = els.drop || document.querySelector('[data-role="drop"]'); |
|
const file = els.file; |
|
|
|
if (chooseBtn) chooseBtn.addEventListener('click', ()=> file?.click()); |
|
if (drop){ |
|
drop.addEventListener('click', ()=> file?.click()); |
|
drop.addEventListener('keydown', e=>{ if (e.key==='Enter'||e.key===' ') file?.click(); }); |
|
['dragenter','dragover'].forEach(t => drop.addEventListener(t, e=>{ e.preventDefault(); })); |
|
['dragleave','drop'].forEach(t => drop.addEventListener(t, e=>{ e.preventDefault(); })); |
|
drop.addEventListener('drop', e=>{ const f=e.dataTransfer.files?.[0]; if (f) handleFile(f); }); |
|
} |
|
if (file){ |
|
file.addEventListener('change', ()=> handleFile(file.files?.[0])); |
|
} |
|
} |
|
wireUploadArea(); |
|
|
|
function handleFile(file){ |
|
if (!file) return; |
|
els.fileName.textContent = `الملف: ${file.name}`; |
|
const fr = new FileReader(); |
|
fr.onload = e => { |
|
const buf = e.target.result; |
|
worker?.postMessage({ type:'parse', buffer: buf }, [buf]); |
|
toast('جاري قراءة الملف…'); |
|
}; |
|
fr.onerror = ()=> toast('تعذر قراءة الملف','error'); |
|
fr.readAsArrayBuffer(file); |
|
} |
|
|
|
|
|
function populateMonthWeek(rows){ |
|
const months = uniqueNonEmpty(rows.map(r => normalizeNum(r[COLS.monthO]))); |
|
const weeks = uniqueNonEmpty(rows.map(r => normalizeNum(r[COLS.weekH]))); |
|
els.month.innerHTML = '<option value="">الكل</option>' + months.map(v=>`<option>${v}</option>`).join(''); |
|
els.week.innerHTML = '<option value="">الكل</option>' + weeks.map(v=>`<option>${v}</option>`).join(''); |
|
els.month.onchange = processData; |
|
els.week.onchange = processData; |
|
} |
|
|
|
|
|
function fillColSelectors(cols){ |
|
const sorted = cols.slice().sort((a,b)=> a.localeCompare(b,'en')); |
|
const fill = (sel, def) => { if (!sel) return; sel.innerHTML = '<option value="">— اختر —</option>'+sorted.map(c=>`<option value="${c}">${c}</option>`).join(''); if (def && sorted.includes(def)) sel.value = def; }; |
|
fill(els.colComplete, COLS.completeBL || 'BL'); |
|
fill(els.colFullfilled, COLS.fullyMetAB || 'AB'); |
|
fill(els.colInspectorNo, COLS.inspectorNo || ''); |
|
fill(els.colInspectorName,COLS.inspectorF || 'F'); |
|
fill(els.colResearcher, COLS.nameA || 'G'); |
|
fill(els.colPublicId, COLS.idB || 'B'); |
|
fill(els.colWeek, COLS.weekH || 'H'); |
|
fill(els.colMonth, COLS.monthO || 'O'); |
|
fill(els.colRegion, COLS.regionAR || 'AR'); |
|
|
|
els.saveCols?.addEventListener('click', ()=>{ |
|
const map = { |
|
completeBL: els.colComplete.value || 'BL', |
|
fullyMetAB: els.colFullfilled.value || 'AB', |
|
inspectorNo: els.colInspectorNo.value || '', |
|
inspectorF: els.colInspectorName.value || 'F', |
|
nameA: els.colResearcher.value || 'G', |
|
idB: els.colPublicId.value || 'B', |
|
weekH: els.colWeek.value || 'H', |
|
monthO: els.colMonth.value || 'O', |
|
regionAR: els.colRegion.value || 'AR', |
|
}; |
|
localStorage.setItem('colmap', JSON.stringify(map)); |
|
Object.assign(COLS, map); |
|
toast('تم الحفظ','success'); |
|
}); |
|
|
|
els.resetCols?.addEventListener('click', ()=>{ |
|
localStorage.removeItem('colmap'); Object.assign(COLS, DEFAULT_COLS); |
|
fillColSelectors(sorted); toast('تمت إعادة الافتراضيات','info'); |
|
}); |
|
} |
|
|
|
|
|
const MAX_PER = 5; |
|
function processData(){ |
|
if (!state.rows.length){ showNoData(true,'لم يتم رفع ملف'); return; } |
|
const wantMonth = normalizeNum(els.month.value); |
|
const wantWeek = normalizeNum(els.week.value); |
|
|
|
const ok = state.rows.filter(r=>{ |
|
const ab = truthy(r[COLS.fullyMetAB]); |
|
const bl = truthy(r[COLS.completeBL]); |
|
if (!(ab && bl)) return false; |
|
const rm = normalizeNum(r[COLS.monthO] ?? ''); |
|
const rw = normalizeNum(r[COLS.weekH] ?? ''); |
|
if (wantMonth && rm !== wantMonth) return false; |
|
if (wantWeek && rw !== wantWeek) return false; |
|
return true; |
|
}); |
|
|
|
|
|
const byInspector = new Map(); |
|
for (const row of ok){ |
|
const inspName = (row[COLS.inspectorF] ?? 'غير محدد') || 'غير محدد'; |
|
if (!byInspector.has(inspName)) byInspector.set(inspName, { rowsByRes:new Map(), inspNo: null }); |
|
const pack = byInspector.get(inspName); |
|
const res = (row[COLS.nameA] ?? 'غير محدد') || 'غير محدد'; |
|
if (!pack.rowsByRes.has(res)) pack.rowsByRes.set(res, []); |
|
pack.rowsByRes.get(res).push(row); |
|
const noCol = COLS.inspectorNo; |
|
if (noCol && row[noCol] != null) pack.inspNo = row[noCol]; |
|
} |
|
|
|
|
|
const out = new Map(); |
|
for (const [insp, pack] of byInspector){ |
|
const picked = []; |
|
for (const [res, arr] of pack.rowsByRes){ |
|
picked.push(...pickRandomStable(arr, MAX_PER, `${state.seed}|${insp}|${res}|${wantMonth}|${wantWeek}`)); |
|
} |
|
out.set(insp, { rows:picked, inspNo: pack.inspNo, researchers: pack.rowsByRes.size }); |
|
} |
|
|
|
state.filtered = ok; |
|
state.byInspector = out; |
|
|
|
render(); |
|
} |
|
|
|
els.btnProcess?.addEventListener('click', processData); |
|
|
|
|
|
function showNoData(show,msg='لا توجد بيانات'){ |
|
els.noData.style.display = show? '' : 'none'; |
|
els.noData.textContent = msg; |
|
if (show) els.cards.innerHTML = ''; |
|
} |
|
function render(){ |
|
const inspectorCount = [...state.byInspector.keys()].length; |
|
let totalSelected = 0; for (const {rows} of state.byInspector.values()) totalSelected += rows.length; |
|
els.stats.textContent = `عدد المفتشين: ${inspectorCount} — السجلات المختارة: ${totalSelected} — إجمالي المطابقة قبل الاختيار: ${state.filtered.length}`; |
|
|
|
const root = els.cards; root.innerHTML = ''; |
|
const entries = [...state.byInspector.entries()]; |
|
if (!entries.length){ showNoData(true,'لا توجد بيانات مطابقة للإعدادات الحالية'); return; } |
|
showNoData(false); |
|
|
|
for (const [inspName, meta] of entries){ |
|
const rows = meta.rows; |
|
const researchersCount = meta.researchers || 0; |
|
const inspNo = meta.inspNo ? String(meta.inspNo) : '—'; |
|
|
|
const card = document.createElement('div'); |
|
card.className = 'inspector-card'; |
|
|
|
const h3 = document.createElement('h3'); |
|
h3.textContent = `مفتش ${inspName}`; |
|
card.appendChild(h3); |
|
|
|
const kpis = document.createElement('div'); |
|
kpis.className = 'kpis'; |
|
kpis.innerHTML = ` |
|
<div class="kpi"><span class="label">رقم المفتش</span><span class="value">${inspNo}</span></div> |
|
<div class="kpi"><span class="label">عدد الباحثين</span><span class="value">${researchersCount}</span></div> |
|
<div class="kpi"><span class="label">إجمالي العينة</span><span class="value ok">${rows.length}</span></div> |
|
`; |
|
card.appendChild(kpis); |
|
|
|
const footer = document.createElement('footer'); |
|
const hideBtn = document.createElement('button'); hideBtn.textContent='إخفاء'; |
|
hideBtn.addEventListener('click', ()=> card.remove()); |
|
const dlBtn = document.createElement('button'); dlBtn.textContent='تحميل'; dlBtn.className='primary'; |
|
const title = meta.inspNo ? `مفتش ${meta.inspNo}` : `مفتش ${inspName}`; |
|
dlBtn.addEventListener('click', ()=> downloadSingleInspector(title, rows)); |
|
footer.appendChild(hideBtn); footer.appendChild(dlBtn); |
|
card.appendChild(footer); |
|
|
|
root.appendChild(card); |
|
} |
|
} |
|
|
|
|
|
async function ensureExcelJS(){ |
|
if (!window.ExcelJS){ |
|
await new Promise((res,rej)=>{ |
|
const s=document.createElement('script'); |
|
s.src='https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js'; |
|
s.onload=res; s.onerror=rej; document.head.appendChild(s); |
|
}); |
|
} |
|
} |
|
async function buildWorkbookSheets(sheets){ |
|
await ensureExcelJS(); |
|
const wb=new ExcelJS.Workbook(); |
|
sheets.forEach(([name, headers, rows])=>{ |
|
const ws=wb.addWorksheet(String(name||'Sheet').slice(0,31)); |
|
ws.addRow(headers); rows.forEach(r=>ws.addRow(r)); |
|
ws.getRow(1).font={bold:true}; ws.views=[{state:'frozen',ySplit:1}]; |
|
ws.columns = headers.map(()=>({width:22})); |
|
}); |
|
const buf=await wb.xlsx.writeBuffer(); |
|
return URL.createObjectURL(new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})); |
|
} |
|
async function downloadSingleInspector(title, rows){ |
|
if (!rows.length){ toast('لا توجد بيانات','error'); return; } |
|
const headers = ['اسم الباحث','معرف عام','منطقة العد','الأسبوع','الشهر','المفتش']; |
|
const data = rows.map(r => [ |
|
r[DEFAULT_COLS.nameA] ? r[DEFAULT_COLS.nameA] : r[COLS.nameA] || '', |
|
r[DEFAULT_COLS.idB] ? r[DEFAULT_COLS.idB] : r[COLS.idB] || '', |
|
r[DEFAULT_COLS.regionAR] ? r[DEFAULT_COLS.regionAR] : r[COLS.regionAR] || '', |
|
r[DEFAULT_COLS.weekH] ? r[DEFAULT_COLS.weekH] : r[COLS.weekH] || '', |
|
r[DEFAULT_COLS.monthO] ? r[DEFAULT_COLS.monthO] : r[COLS.monthO] || '', |
|
r[DEFAULT_COLS.inspectorF] ? r[DEFAULT_COLS.inspectorF] : r[COLS.inspectorF] || '', |
|
]); |
|
const href = await buildWorkbookSheets([[title, headers, data]]); |
|
const a=document.createElement('a'); a.href=href; a.download=`تقرير_${safeName(title)}.xlsx`; document.body.appendChild(a); a.click(); a.remove(); |
|
} |
|
els.btnPer?.addEventListener('click', async ()=>{ |
|
for (const [insp, meta] of state.byInspector){ |
|
const title = meta.inspNo ? `مفتش ${meta.inspNo}` : `مفتش ${insp}`; |
|
await downloadSingleInspector(title, meta.rows); |
|
} |
|
}); |
|
els.btnMaster?.addEventListener('click', async ()=>{ |
|
const headers = ['اسم الباحث','معرف عام','منطقة العد','الأسبوع','الشهر','المفتش']; |
|
const sheets = []; |
|
for (const [insp, meta] of state.byInspector){ |
|
if (!meta.rows.length) continue; |
|
const title = meta.inspNo ? `مفتش ${meta.inspNo}` : `مفتش ${insp}`; |
|
const data = meta.rows.map(r => [ |
|
r[COLS.nameA]||'', r[COLS.idB]||'', r[COLS.regionAR]||'', |
|
r[COLS.weekH]||'', r[COLS.monthO]||'', r[COLS.inspectorF]||'', |
|
]); |
|
sheets.push([title, headers, data]); |
|
} |
|
if (!sheets.length){ toast('لا توجد بيانات','error'); return; } |
|
const href = await buildWorkbookSheets(sheets); |
|
const a=document.createElement('a'); a.href=href; a.download='تقارير_جميع_المفتشين.xlsx'; document.body.appendChild(a); a.click(); a.remove(); |
|
}); |
|
}); |
|
|