Nof2030 / app.js
stat2025's picture
Upload 4 files
f3bf432 verified
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 };
/* ====== Worker ====== */
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(); // تنفيذ تلقائي
}
};
}
/* ====== رفع الملف (إصلاح النقر + دعم dropZone/Area) ====== */
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];
}
// اختيار 5 لكل باحث
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();
});
}); // DOMContentLoaded